Compare commits
2 Commits
1.1.0
...
88c77f8ac6
| Author | SHA1 | Date | |
|---|---|---|---|
|
88c77f8ac6
|
|||
|
5aa382a4c9
|
@@ -1,11 +1,40 @@
|
||||
# jj-cz: Conventional Commits for Jujutsu
|
||||
|
||||
An interactive CLI tool that guides Jujutsu users through creating
|
||||
[conventional commit](https://www.conventionalcommits.org/) messages.
|
||||
<h1 align="center">Bakit</h1>
|
||||
<div align="center">
|
||||
<strong>
|
||||
An interactive CLI tool that guides Jujutsu users through creating <a href="https://www.conventionalcommits.org/" rel="noopener">conventional commit</a> messages.
|
||||
</strong>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
<!-- CI -->
|
||||
<a href="https://labs.phundrak.com/phundrak/jj-cz/actions?workflow=action.yml&branch=develop">
|
||||
<img src="https://labs.phundrak.com/phundrak/jj-cz/actions/workflows/action.yml/badge.svg?branch=develop" alt="actions status" />
|
||||
</a>
|
||||
<!-- Crates.io -->
|
||||
<a href="https://crates.io/crates/sqlx">
|
||||
<img src="https://img.shields.io/crates/v/jj-cz.svg" alt="Crates.io version"/>
|
||||
</a>
|
||||
<!-- License -->
|
||||
<a href="#license">
|
||||
<img src="https://img.shields.io/badge/License-MIT-blue" alt="MIT License" />
|
||||
</a>
|
||||
<a href="#license">
|
||||
<img src="https://img.shields.io/badge/License-GPL--3.0--or--later-blue" alt="GPL License" />
|
||||
</a>
|
||||
<!-- Tools -->
|
||||
<a href="https://www.gnu.org/software/emacs/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Made%20with-GNU%2FEmacs-blueviolet.svg?logo=GNU%20Emacs&logoColor=white" alt="Made with GNU/Emacs" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
[](assets/demo.cast)
|
||||
|
||||
## Features
|
||||
|
||||
- Interactive prompts for type, scope, and description
|
||||
- Interactive prompts for type, scope, breaking changes, and description
|
||||
- All 11 commit types with descriptions (feat, fix, docs, style,
|
||||
refactor, perf, test, build, ci, chore, revert)
|
||||
- Optional scope with validation
|
||||
@@ -28,12 +57,23 @@ You can also set the revision message of a few revisions at once, or
|
||||
target a single revision other than the current one.
|
||||
|
||||
```sh
|
||||
jj-cz @- xs develop
|
||||
jj-cz @- xs develop # assuming the revision xs and the bookmark develop exist
|
||||
```
|
||||
|
||||
No explicit revision is simply the equivalent of `jj-cz @`, like
|
||||
`jj desc`.
|
||||
|
||||
If you want to create a new revision after calling `jj-cz` on a single
|
||||
revision, you can use the `-n` or `--new` flag.
|
||||
|
||||
```sh
|
||||
jj-cz -n # equivalent of `jj-cz && jj new`
|
||||
jj-cz xs -n # equivalent of `jj-cz xs && jj new`
|
||||
jj-cz -n xs # equivalent of `jj-cz xs && jj new`
|
||||
```
|
||||
|
||||
You cannot, however, call `jj-cz` on multiple revisions with the `--new` flag active.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A Jujutsu repository
|
||||
@@ -93,3 +133,30 @@ Just make sure Rust is available on your machine (duh!).
|
||||
```sh
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
## Tips and questions
|
||||
### Running `jj cz` instead of `jj-cz`
|
||||
|
||||
I do not actually use `jj-cz`, but `jj cz`. I just find it more
|
||||
natural to treat it as its own jj subcommand. To achieve that, you can
|
||||
simply add an alias to your jujutsu configuration.
|
||||
```toml
|
||||
[aliases]
|
||||
cz = ["utils", "exec", "--", "jj-cz"]
|
||||
```
|
||||
|
||||
### `$EDITOR` and editing the revision’s body message
|
||||
|
||||
`jj-cz` relies on your `$EDITOR` variable to open a temporary file in
|
||||
which you’ll write the body of your commit. This body does not include
|
||||
some footers `jj-cz` may include by itself, such as the breaking
|
||||
change footer.
|
||||
|
||||
In some cases, you may not notice a new editor open. In this case,
|
||||
check whether you already have an editor open, the file might be
|
||||
there. In my case, if I already have an open Emacsclient, it will open
|
||||
there.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under either the [MIT](LICENSE.MIT.md) or [GPL-3.0](LICENSE.GPL.md) licenses, as you prefer.
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
{"version":3,"term":{"cols":120,"rows":18,"type":"screen-256color","version":"tmux 3.6a","theme":{"fg":"#d8dee9","bg":"#2e3440","palette":"#3b4252:#bf616a:#a3be8c:#ebcb8b:#81a1c1:#b48ead:#88c0d0:#e5e9f0:#4c566a:#bf616a:#a3be8c:#d08770:#5e81ac:#b48ead:#8fbcbb:#eceff4"}},"timestamp":1781441421,"env":{"SHELL":"bash --norc"}}
|
||||
[0.003, "o", "bash-5.3$ "]
|
||||
[2.269, "o", "j"]
|
||||
[0.149, "o", "j"]
|
||||
[0.256, "o", " "]
|
||||
[0.075, "o", "s"]
|
||||
[0.120, "o", "h"]
|
||||
[0.149, "o", "o"]
|
||||
[0.135, "o", "w"]
|
||||
[0.225, "o", "\r\n"]
|
||||
[0.079, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2004h\u001b[>4;2m"]
|
||||
[0.001, "o", "\u001b[?25l\u001b[HCommit ID: \u001b[34mb6890ed4d3c203026c4b6c349c61c918486732e4\u001b[39m\u001b[K\u001b[2;1HChange ID: \u001b[35mymmvxvqykryrsxmnxzttymztwtkpltqz\u001b[39m\u001b[K\u001b[3;1HAuthor : \u001b[33mLucien Cartier-Tilet\u001b[39m <\u001b[33mlucien@phundrak.com\u001b[39m> (\u001b[36m2026-06-14 14:31:57\u001b[39m)\u001b[K\u001b[4;1HCommitter: \u001b[33mLucien Cartier-Tilet\u001b[39m <\u001b[33mlucien@phundrak.com\u001b[39m> (\u001b[36m2026-06-14 14:50:24\u001b[39m)\u001b[K\u001b[5;1HSignature: \u001b[32mgood\u001b[39m signature by \u001b[33mlucien@phundrak.com\u001b[39m \u001b[36mSHA256:CE0HPsbW3L2YiJETx1zYZ2muMptaAqTN2g3498KrMkc\u001b[39m\u001b[K\u001b[6;1H\u001b[K\u001b[7;1H\u001b[33m (no description set)\u001b[39m\u001b[K\u001b[8;1H\u001b[K\u001b[9;1H\u001b[33mModified regular file README.md:\u001b[39m\u001b[K\u001b[10;1H ...\u001b[K\u001b[11;1H\u001b[0;2m\u000f\u001b[31m 3\u001b[0m\u000f \u001b[0;2m\u000f\u001b[32m 3\u001b[0m\u000f: An interactive CLI tool that guides Jujutsu users through creating\u001b[K\u001b[12;1H\u001b[0;2m\u000f\u001b[31m 4\u001b[0m\u000f \u001b[0;2m\u000f\u001b[32m 4\u001b[0m\u000f: [conventional commit](https://www.conventionalcommits.org/) messages.\u001b[K\u001b[13;1H\u001b[0;2m\u000f\u001b[31m 5\u001b[0m\u000f \u001b[0;2m\u000f\u001b[32m 5\u001b[0m\u000f: \u001b[K\u001b[14;1H \u001b[32m 6\u001b[39m: \u001b[0;4m\u000f\u001b[32m[](assets/demo.cast)\u001b[0m\u000f\u001b[K\u001b[15;1H \u001b[32m 7\u001b[39m: \u001b[K\u001b[16;1H\u001b[0;2m\u000f\u001b[31m 6\u001b[0m\u000f \u001b[0;2m\u000f\u001b[32m 8\u001b[0m\u000f: ## Features\u001b[K\u001b[17;1H\u001b[0;2m\u000f\u001b[31m 7\u001b[0m\u000f \u001b[0;2m\u000f\u001b[32m 9\u001b[0m\u000f: \u001b[K\u001b[18;1H\u001b[30m\u001b[47m wrap lines 1- 17/ 84 \u001b[39m\u001b[K\r\u001b[49m"]
|
||||
[1.270, "o", "\u001b[34h\u001b[?25h\u001b[1;18r\u001b[18;1H\u001b[J\u001b[34h\u001b[?25h\u001b[?2004l\u001b[>4;0m"]
|
||||
[0.002, "o", "bash-5.3$ "]
|
||||
[0.403, "o", "j"]
|
||||
[0.137, "o", "j"]
|
||||
[0.284, "o", " "]
|
||||
[0.584, "o", "\b \b"]
|
||||
[0.046, "o", "-"]
|
||||
[0.360, "o", "c"]
|
||||
[0.089, "o", "z"]
|
||||
[0.316, "o", "\r\n"]
|
||||
[0.027, "o", "\u001b[?2004h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[38;5;10m?\u001b[39m Select commit type: \r\n\u001b[38;5;14m>\u001b["]
|
||||
[0.000, "o", "39m \u001b[38;5;14"]
|
||||
[0.000, "o", "m"]
|
||||
[0.000, "o", "feat\u001b[39m\r\n\u001b[38;5;14m \u001b[39m fix\r\n\u001b[38;5;14m \u001b[39m"]
|
||||
[0.000, "o", " docs\r\n\u001b["]
|
||||
[0.000, "o", "38;5;14m \u001b[39m style\r\n\u001b[38;5;14m "]
|
||||
[0.000, "o", "\u001b[39m refactor\r\n\u001b[38;5;14m \u001b[39m perf"]
|
||||
[0.000, "o", "\r\n\u001b["]
|
||||
[0.000, "o", "38;5;14m "]
|
||||
[0.000, "o", "\u001b[39m test"]
|
||||
[0.000, "o", "\r\n\u001b[38;"]
|
||||
[0.000, "o", "5;14m"]
|
||||
[0.000, "o", " \u001b[39m build\r"]
|
||||
[0.000, "o", "\n\u001b[38;5;14"]
|
||||
[0.000, "o", "m \u001b["]
|
||||
[0.000, "o", "39m"]
|
||||
[0.000, "o", " ci\r"]
|
||||
[0.000, "o", "\n"]
|
||||
[0.000, "o", "\u001b[38;5;14m \u001b[39"]
|
||||
[0.000, "o", "m "]
|
||||
[0.000, "o", "chore"]
|
||||
[0.000, "o", "\r\n\u001b["]
|
||||
[0.000, "o", "38;5;14m"]
|
||||
[0.000, "o", " \u001b["]
|
||||
[0.000, "o", "39m revert\r"]
|
||||
[0.000, "o", "\n\u001b[38;5;14m"]
|
||||
[0.000, "o", "["]
|
||||
[0.000, "o", "\u001b[39"]
|
||||
[0.000, "o", "m\u001b["]
|
||||
[0.000, "o", "38;5;14mUse arrow keys to navigate, Enter to select. See https://www.conventionalcommits.org/ for details.\u001b[39m\u001b[38;"]
|
||||
[0.000, "o", "5;14m]\u001b[39"]
|
||||
[0.000, "o", "m\r\u001b[12"]
|
||||
[0.000, "o", "A\u001b[22"]
|
||||
[0.000, "o", "C\u001b[?25h"]
|
||||
[0.377, "o", "\u001b[?25l\u001b[22D\u001b[38;5;10m?\u001b[39m Select commit type: d \u001b[K\r\n\u001b[38;5;14m>\u001b[39m \u001b[38;5;14mdocs\u001b[39m\u001b[K"]
|
||||
[0.000, "o", "\r\n\u001b[38;5;14m \u001b[39m build\u001b[K\r\n\u001b[38;5;14m[\u001b[39m\u001b[38;5;14mUse arrow keys to navigate, Enter to select. See https://www.conventionalcommits.org/ for details."]
|
||||
[0.000, "o", "\u001b[39m\u001b[38;5;14m]\u001b[39m\u001b[K\r"]
|
||||
[0.000, "o", "\n\u001b[2K\r\n\u001b[2K\r\n\u001b[2K\r\n\u001b[2K\r\n"]
|
||||
[0.000, "o", "\u001b[2K\r\n\u001b[2K"]
|
||||
[0.000, "o", "\r\n\u001b[2K\r\n\u001b[2K"]
|
||||
[0.000, "o", "\r\n"]
|
||||
[0.000, "o", "\u001b[2K\r\u001b["]
|
||||
[0.000, "o", "12A\u001b[23"]
|
||||
[0.000, "o", "C\u001b[?25h"]
|
||||
[0.091, "o", "\u001b[?25l\u001b[23D\u001b[38;5;10m?\u001b[39m Select commit type: do \u001b[K\r\n\r\n\u001b[38;5;14m[\u001b[39m\u001b[38;5;14mUse arrow keys to navigate, Enter to select. See https://www.conventionalcommits.org/ for details.\u001b[39m\u001b[38;5;14m]\u001b[39m\u001b[K\r\n"]
|
||||
[0.000, "o", "\u001b[2K\r\u001b[3A\u001b[24C\u001b[?25h"]
|
||||
[0.119, "o", "\u001b[?25l\u001b[24D\u001b[38;5;10m?\u001b[39m Select commit type: doc \u001b[K\r\n\r\n\r\u001b[2A\u001b[25C\u001b[?25h"]
|
||||
[0.391, "o", "\u001b[?25l\u001b[25D\u001b[38;5;10m>\u001b[39m Select commit type: \u001b[38;5;14mdocs: Documentation only changes\u001b[39m\u001b[K\r\n\u001b[2K\r\n\u001b[2K\r\n\u001b[?25h"]
|
||||
[0.000, "o", "\u001b[2A\u001b[?25h\u001b[?2004l\u001b[?2004h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[38;5;10m?\u001b[39m Enter scope (optional): \u001b[38;"]
|
||||
[0.000, "o", "5;8mLeave empty if no scope\u001b[39"]
|
||||
[0.000, "o", "m \r\n\u001b[38;5;14m[\u001b[39m\u001b[38;5;14mScope is optional. If provided, it should be a noun representing the section of code affected (e.g., 'api', 'ui', 'conf\u001b[39m"]
|
||||
[0.000, "o", "\r\n"]
|
||||
[0.000, "o", "\u001b[38;5;14mig'). Max 30 characters.\u001b["]
|
||||
[0.000, "o", "39m"]
|
||||
[0.000, "o", "\u001b[38;5;14m]\u001b[39m\r\u001b["]
|
||||
[0.000, "o", "2A\u001b["]
|
||||
[0.000, "o", "26C\u001b[?25h"]
|
||||
[1.140, "o", "\u001b[?25l\u001b[26D\u001b[38;5;10m?\u001b[39"]
|
||||
[0.000, "o", "m Enter scope (optional): R \u001b[K\r\n\r\n\r\u001b[2A\u001b[27C\u001b[?25h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[27D\u001b[38;5;10m?\u001b[39m Enter scope (optional): RE \u001b[K\r\n\r\n\r\u001b["]
|
||||
[0.000, "o", "2A\u001b[28C\u001b[?25h"]
|
||||
[0.076, "o", "\u001b[?25l\u001b[28D\u001b[38;5;10m?\u001b[39m Enter scope (optional): REA \u001b[K\r\n\r\n\r\u001b[2A\u001b[29C\u001b[?25h"]
|
||||
[0.089, "o", "\u001b[?25l\u001b[29D\u001b[38;5;10m?\u001b[39m Enter scope (optional): READ \u001b[K\r\n\r\n\r\u001b[2A\u001b[30C\u001b[?25h"]
|
||||
[0.090, "o", "\u001b[?25l\u001b[30D\u001b[38;5;10m?\u001b[39m Enter scope (optional): READM \u001b[K\r\n\r\n\r\u001b[2A\u001b[31C\u001b[?25h"]
|
||||
[0.272, "o", "\u001b[?25l\u001b[31D\u001b[38;5;10m?\u001b[39m Enter scope (optional): README \u001b[K\r\n\r\n\r\u001b[2A\u001b[32C\u001b[?25h"]
|
||||
[0.748, "o", "\u001b[?25l\u001b[32D\u001b[38;5;10m>\u001b[39m Enter scope (optional): \u001b[38;5;14mREADME\u001b[39m\u001b[K\r\n\u001b[2K\r\n\u001b[2K\r\n\u001b[?25h\u001b[2A\u001b[?25h\u001b[?2004l"]
|
||||
[0.000, "o", "\u001b[?2004h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[38;5;10m?\u001b[39m Enter description (required): \r\n\u001b[38;5;14"]
|
||||
[0.001, "o", "m[\u001b[39m\u001b[38;5;14mDescription is required. Short summary in imperative mood (e.g., 'add feature', 'fix bug'). Soft limit: 50 characters.\u001b[39m\u001b[38;5;14m]\u001b[39m\r\u001b[1A\u001b[32C\u001b[?25h"]
|
||||
[1.229, "o", "\u001b[?25l\u001b[32D\u001b[38;5;10m?\u001b[39m Enter description (required): u \u001b[K\r\n\r\u001b[1A\u001b[33C\u001b[?25h"]
|
||||
[0.090, "o", "\u001b[?25l\u001b[33D\u001b[38;5;10m?\u001b[39m Enter description (required): up \u001b[K\r\n\r\u001b[1A\u001b[34C\u001b[?25h"]
|
||||
[0.076, "o", "\u001b[?25l\u001b[34D\u001b[38;5;10m"]
|
||||
[0.000, "o", "?\u001b[39m Enter description (required): upd \u001b[K\r\n\r\u001b[1A\u001b[35C\u001b[?25h"]
|
||||
[0.134, "o", "\u001b[?25l\u001b[35D\u001b[38;5;10m?\u001b[39m Enter description (required): upda \u001b[K\r\n\r\u001b[1A\u001b[36C\u001b[?25h"]
|
||||
[0.075, "o", "\u001b[?25l\u001b[36D\u001b[38;5;10m?\u001b[39m Enter description (required): updat \u001b[K\r\n\r\u001b[1A\u001b[37C\u001b[?25h"]
|
||||
[0.225, "o", "\u001b[?25l\u001b[37D\u001b[38;5;10m?\u001b[39m Enter description (required): update \u001b[K\r\n\r\u001b[1A\u001b[38C\u001b[?25h"]
|
||||
[0.090, "o", "\u001b[?25l\u001b[38D\u001b[38;5;10m?\u001b[39m Enter description (required): update \u001b[K\r\n\r\u001b[1A\u001b[39C\u001b[?25h"]
|
||||
[0.255, "o", "\u001b[?25l\u001b[39D\u001b[38;5;10m?\u001b[39m Enter description (required): update t \u001b[K\r\n\r\u001b[1A\u001b[40C\u001b[?25h\u001b[?25l\u001b[40D\u001b[38;5;10m?\u001b[39m Enter description (required): update th \u001b[K\r\n\r\u001b[1"]
|
||||
[0.000, "o", "A\u001b[41C\u001b[?25h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[41D\u001b[38;5;10m?\u001b[39m Enter description (required): update the \u001b[K\r\n\r\u001b[1A\u001b[42C\u001b[?25h"]
|
||||
[0.166, "o", "\u001b[?25l\u001b[42D\u001b[38;5;10m?\u001b[39"]
|
||||
[0.000, "o", "m Enter description (required): update the \u001b[K\r\n\r\u001b[1A\u001b[43C\u001b[?25h"]
|
||||
[0.329, "o", "\u001b[?25l\u001b[43D\u001b[38;5;10m?\u001b[39m Enter description (required): update the R \u001b[K\r\n\r\u001b[1A\u001b[44C\u001b[?25h\u001b[?25l\u001b[44D\u001b[38;5;10m?\u001b[39m Enter description (required):"]
|
||||
[0.000, "o", " update the RE \u001b[K\r\n\r\u001b[1A\u001b[45C\u001b[?25h"]
|
||||
[0.075, "o", "\u001b[?25l\u001b[45D\u001b[38;5;10m?\u001b[39m Enter description (required): update the REA \u001b[K\r\n\r\u001b[1A\u001b[46C\u001b[?25h"]
|
||||
[0.150, "o", "\u001b[?25l\u001b[46D\u001b[38;5;10m?\u001b[39m Enter description (required): "]
|
||||
[0.000, "o", "update the READ \u001b[K\r\n\r\u001b[1A\u001b[47C\u001b[?25h"]
|
||||
[0.060, "o", "\u001b[?25l\u001b[47D\u001b[38;5;10m?\u001b[39m Enter description (required): update the READM \u001b[K\r\n\r\u001b[1A\u001b[48C\u001b[?25h"]
|
||||
[0.300, "o", "\u001b[?25l\u001b[48D\u001b["]
|
||||
[0.000, "o", "38;5;10m?\u001b[39m Enter description (required): update the README \u001b[K\r"]
|
||||
[0.000, "o", "\n\r\u001b[1A"]
|
||||
[0.000, "o", "\u001b[49C\u001b[?25h"]
|
||||
[0.600, "o", "\u001b[?25l\u001b[49D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README \u001b[K\r\n\r\u001b[1A\u001b[50C\u001b[?25h"]
|
||||
[0.165, "o", "\u001b[?25l\u001b[50D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README t \u001b[K\r\n\r\u001b[1A"]
|
||||
[0.000, "o", "\u001b[51C\u001b[?25h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[51D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to \u001b[K\r"]
|
||||
[0.000, "o", "\n\r"]
|
||||
[0.000, "o", "\u001b[1A\u001b[52C\u001b[?25h"]
|
||||
[0.165, "o", "\u001b[?25l\u001b[52D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to \u001b[K\r\n\r\u001b[1A\u001b[53C\u001b[?25h"]
|
||||
[0.075, "o", "\u001b[?25l\u001b[53D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to r \u001b[K\r\n\r\u001b[1A\u001b[54C\u001b[?25h"]
|
||||
[0.090, "o", "\u001b[?25l\u001b[54D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to re \u001b[K\r\n\r\u001b[1A\u001b[55C\u001b[?25h"]
|
||||
[0.180, "o", "\u001b[?25l\u001b[55D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to ref \u001b[K\r\n\r\u001b[1A\u001b[56C\u001b[?25h"]
|
||||
[0.151, "o", "\u001b[?25l\u001b[56D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to refl \u001b[K\r\n\r\u001b[1A\u001b[57C\u001b[?25h"]
|
||||
[0.254, "o", "\u001b[?25l\u001b[57D\u001b[38;5;10m?\u001b[39m Enter description (required): "]
|
||||
[0.000, "o", "update the README to refle \u001b[K\r\n\r\u001b[1A\u001b[58C\u001b[?25h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[58D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflec \u001b[K"]
|
||||
[0.000, "o", "\r\n\r\u001b[1A\u001b["]
|
||||
[0.000, "o", "59C\u001b[?25h"]
|
||||
[0.210, "o", "\u001b[?25l\u001b[59D\u001b[38;5;10m"]
|
||||
[0.000, "o", "?\u001b[39m Enter description (required): update the README to reflect \u001b[K\r\n\r\u001b[1A"]
|
||||
[0.000, "o", "\u001b[60C\u001b[?25h"]
|
||||
[0.151, "o", "\u001b[?25l\u001b[60D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect \u001b[K\r\n\r\u001b[1A\u001b[61C\u001b[?25h"]
|
||||
[0.074, "o", "\u001b[?25l\u001b[61D\u001b[38;5;10m?\u001b[39"]
|
||||
[0.000, "o", "m Enter description (required): update the README to reflect n \u001b[K\r\n\r\u001b[1A\u001b[62C\u001b[?25h"]
|
||||
[0.075, "o", "\u001b[?25l\u001b[62D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect ne \u001b[K\r\n\r\u001b[1A\u001b[63C\u001b[?25h"]
|
||||
[0.135, "o", "\u001b[?25l\u001b[63D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new \u001b[K\r\n\r\u001b[1A\u001b[64C\u001b[?25h"]
|
||||
[0.256, "o", "\u001b[?25l\u001b[64D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new \u001b[K\r\n\r\u001b[1A\u001b[65C\u001b[?25h"]
|
||||
[0.179, "o", "\u001b[?25l\u001b[65D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new f \u001b[K\r\n\r\u001b[1A\u001b[66C\u001b[?25h"]
|
||||
[0.104, "o", "\u001b[?25l\u001b[66D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new fe \u001b[K\r\n\r\u001b[1A\u001b[67C\u001b[?25h"]
|
||||
[0.016, "o", "\u001b[?25l\u001b[67D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new fea \u001b[K\r\n\r\u001b["]
|
||||
[0.000, "o", "1A\u001b[68C\u001b[?25h"]
|
||||
[0.240, "o", "\u001b[?25l\u001b[68D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new feat \u001b[K\r\n\r\u001b[1A\u001b[69C\u001b[?25h"]
|
||||
[0.075, "o", "\u001b[?25l\u001b[69D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new featr \u001b[K\r\n\r\u001b[1A\u001b[70C\u001b[?25h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[70D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new featru \u001b[K\r\n\r\u001b[1A\u001b[71C\u001b[?25h"]
|
||||
[0.735, "o", "\u001b[?25l\u001b[71D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new featr \u001b[K\r\n\r\u001b[1A\u001b[70C\u001b[?25h"]
|
||||
[0.179, "o", "\u001b[?25l\u001b[70D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new feat \u001b[K"]
|
||||
[0.000, "o", "\r\n\r\u001b[1A\u001b[69C\u001b[?25h"]
|
||||
[0.091, "o", "\u001b[?25l\u001b[69D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new featu \u001b[K\r\n\r\u001b[1A\u001b[70C\u001b[?25h"]
|
||||
[0.166, "o", "\u001b[?25l\u001b[70D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new featur \u001b[K\r\n\r\u001b[1A\u001b[71C\u001b[?25h"]
|
||||
[0.253, "o", "\u001b[?25l\u001b[71D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new feature \u001b[K\r\n\r\u001b[1A\u001b[72C\u001b[?25h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[72D\u001b[38;5;10m?\u001b[39m Enter description (required): update the README to reflect new features \u001b[K\r\n\r\u001b[1A\u001b[73C\u001b[?25h"]
|
||||
[0.796, "o", "\u001b[?25l\u001b[73D\u001b[38;5;10m>\u001b[39m Enter description (required): \u001b[38;5;14m"]
|
||||
[0.000, "o", "update the README to reflect new features\u001b[39m\u001b[K\r\n\u001b[2K\r\n\u001b[?25h\u001b[1A\u001b[?25h\u001b[?2004l"]
|
||||
[0.000, "o", "\u001b[?2004h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[38;5;10m?\u001b[39m "]
|
||||
[0.000, "o", "Does this revision include a breaking change? (y/N) \r\u001b["]
|
||||
[0.000, "o", "54C\u001b[?25h"]
|
||||
[1.079, "o", "\u001b[?25l\u001b[54D\u001b[38;5;10m>\u001b[39m Does this revision include a breaking change? \u001b[38;5;14mNo\u001b[39m\u001b[K\r\n\u001b[?25h"]
|
||||
[0.000, "o", "\u001b[?25h\u001b[?2004l"]
|
||||
[0.000, "o", "\u001b[?2004h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[38;5;10m?\u001b[39m Add a body? (y/N) \r\u001b[20C\u001b[?25h"]
|
||||
[1.035, "o", "\u001b[?25l\u001b[20D\u001b[38;5;10m>\u001b[39m Add a body? \u001b[38;5;14mNo\u001b[39m\u001b[K\r\n\u001b[?25h"]
|
||||
[0.000, "o", "\u001b[?25h\u001b[?2004l"]
|
||||
[0.000, "o", "\r\n📝 Commit Message Preview:\r\n┌──────────────────────────────────────────────────────────────────────────┐\r\n│ docs(README): update the README to reflect new features │\r\n└──────────────────────────────────────────────────────────────────────────┘\r\n\r\n\u001b[?2004h"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[38;5;10m?\u001b[39m Apply this commit message? (Y/n) "]
|
||||
[0.000, "o", "\r\n\u001b[38;5;14m[\u001b[39m\u001b[38;5;14m"]
|
||||
[0.000, "o", "Select 'No' to cancel and start over\u001b[39m"]
|
||||
[0.000, "o", "\u001b[38;"]
|
||||
[0.000, "o", "5;14m]\u001b[39m\r\u001b[1A\u001b[35C\u001b[?25h"]
|
||||
[1.185, "o", "\u001b[?25l\u001b[35D\u001b[38;5;10m>\u001b[39m Apply this commit message? \u001b[38;5;14mYes\u001b[39m\u001b[K\r\n\u001b[2K\r\n\u001b[?25h\u001b[1A\u001b[?25h\u001b[?2004l"]
|
||||
[0.015, "o", "✅ Commit message applied successfully!\r\n"]
|
||||
[0.001, "o", "bash-5.3$ "]
|
||||
[1.394, "o", "j"]
|
||||
[0.182, "o", "j"]
|
||||
[0.254, "o", " "]
|
||||
[0.225, "o", "s"]
|
||||
[0.060, "o", "h"]
|
||||
[0.256, "o", "o"]
|
||||
[0.118, "o", "w"]
|
||||
[0.225, "o", "\r\n"]
|
||||
[0.087, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2004h\u001b[>4;2m"]
|
||||
[0.000, "o", "\u001b[?25l\u001b[HCommit ID: \u001b[34m9a200a9c47baa5a8a43c46e5fd9cc6a5ebd5bddb\u001b[39m\u001b[K\u001b[2;1HChange ID: \u001b[35mymmvxvqykryrsxmnxzttymztwtkpltqz\u001b[39m\u001b[K\u001b[3;1HAuthor : \u001b[33mLucien Cartier-Tilet\u001b[39m <\u001b[33mlucien@phundrak.com\u001b[39m> (\u001b[36m2026-06-14 14:31:57\u001b[39m)\u001b[K\u001b[4;1HCommitter: \u001b[33mLucien Cartier-Tilet\u001b[39m <\u001b[33mlucien@phundrak.com\u001b[39m> (\u001b[36m2026-06-14 14:50:46\u001b[39m)\u001b[K\u001b[5;1HSignature: \u001b[32mgood\u001b[39m signature by \u001b[33mlucien@phundrak.com\u001b[39m \u001b[36mSHA256:CE0HPsbW3L2YiJETx1zYZ2muMptaAqTN2g3498KrMkc\u001b[39m\u001b[K\u001b[6;1H\u001b[K\u001b[7;1H docs(README): update the README to reflect new features\u001b[K\u001b[8;1H\u001b[K\u001b[9;1H\u001b[33mModified regular file README.md:\u001b[39m\u001b[K\u001b[10;1H ...\u001b[K\u001b[11;1H\u001b[0;2m\u000f\u001b[31m 3\u001b[0m\u000f \u001b[0;2m\u000f\u001b[32m 3\u001b[0m\u000f: An interactive CLI tool that guides Jujutsu users through creating\u001b[K\u001b[12;1H\u001b[0;2m\u000f\u001b[31m 4\u001b[0m\u000f \u001b[0;2m\u000f\u001b[32m 4\u001b[0m\u000f: [conventional commit](https://www.conventionalcommits.org/) messages.\u001b[K\u001b[13;1H\u001b[0;2m\u000f\u001b[31m 5\u001b[0m\u000f \u001b[0;2m\u000f\u001b[32m 5\u001b[0m\u000f: \u001b[K\u001b[14;1H \u001b[32m 6\u001b[39m: \u001b[0;4m\u000f\u001b[32m[](assets/demo.cast)\u001b[0m\u000f\u001b[K\u001b[15;1H \u001b[32m 7\u001b[39m: \u001b[K\u001b[16;1H\u001b[0;2m\u000f\u001b[31m 6\u001b[0m\u000f \u001b[0;2m\u000f\u001b[32m 8\u001b[0m\u000f: ## Features\u001b[K\u001b[17;1H\u001b[0;2m\u000f\u001b[31m 7\u001b[0m\u000f \u001b[0;2m\u000f\u001b[32m 9\u001b[0m\u000f: \u001b[K\u001b[18;1H\u001b[30m\u001b[47m wrap lines 1- 17/258 \u001b[39m\u001b[K\r\u001b[49m"]
|
||||
[1.968, "o", "\u001b[34h\u001b[?25h\u001b[1;18r\u001b[18;1H\u001b[J\u001b[34h\u001b[?25h\u001b[?2004l\u001b[>4;0m"]
|
||||
[0.002, "o", "bash-5.3$ "]
|
||||
[0.764, "o", "exit\r\n"]
|
||||
[0.001, "x", "0"]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
@@ -18,6 +18,10 @@ pub struct Cli {
|
||||
/// The revision(s) whose description to edit (default: @)
|
||||
#[arg(value_name = "REVSETS")]
|
||||
revsets: Vec<String>,
|
||||
|
||||
/// Create a new child revision after editing the description
|
||||
#[arg(short, long)]
|
||||
new: bool,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
@@ -29,4 +33,95 @@ impl Cli {
|
||||
self.revsets.iter().map(|s| s.as_str()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_new(&self) -> bool {
|
||||
self.new
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), jj_cz::Error> {
|
||||
if self.new && self.revsets().len() > 1 {
|
||||
Err(jj_cz::Error::NewFlagWithMultipleRevisions)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn revsets_defaults_to_at() {
|
||||
let cli = Cli::parse_from(["jj-cz"]);
|
||||
assert_eq!(cli.revsets(), vec!["@"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revsets_returns_provided_values() {
|
||||
let cli = Cli::parse_from(["jj-cz", "abc", "def"]);
|
||||
let revsets = cli.revsets();
|
||||
assert_eq!(revsets, vec!["abc", "def"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revsets_single_revset() {
|
||||
let cli = Cli::parse_from(["jj-cz", "xyz"]);
|
||||
assert_eq!(cli.revsets(), vec!["xyz"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_new_returns_false_by_default() {
|
||||
let cli = Cli::parse_from(["jj-cz"]);
|
||||
assert!(!cli.create_new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_new_returns_true_with_flag() {
|
||||
let cli = Cli::parse_from(["jj-cz", "--new"]);
|
||||
assert!(cli.create_new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_new_returns_true_with_short_flag() {
|
||||
let cli = Cli::parse_from(["jj-cz", "-n"]);
|
||||
assert!(cli.create_new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_ok_with_no_args() {
|
||||
let cli = Cli::parse_from(["jj-cz"]);
|
||||
assert!(cli.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_ok_with_new_and_single_revset() {
|
||||
let cli = Cli::parse_from(["jj-cz", "--new", "@"]);
|
||||
assert!(cli.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_ok_with_multiple_revsets_no_new() {
|
||||
let cli = Cli::parse_from(["jj-cz", "abc", "def"]);
|
||||
assert!(cli.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_err_with_new_and_multiple_revsets() {
|
||||
let cli = Cli::parse_from(["jj-cz", "--new", "abc", "def"]);
|
||||
let result = cli.validate();
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
jj_cz::Error::NewFlagWithMultipleRevisions
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_derives_debug() {
|
||||
let cli = Cli::parse_from(["jj-cz", "--new", "@"]);
|
||||
let debug = format!("{:?}", cli);
|
||||
assert!(debug.contains("Cli"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ pub enum Error {
|
||||
RevsetResolutionError { revset: String, context: String },
|
||||
#[error("Revision set '{revset}' resolves to multiple commits; specify a single revision")]
|
||||
MultipleRevisions { revset: String },
|
||||
#[error("--new cannot be used with multiple revisions")]
|
||||
NewFlagWithMultipleRevisions,
|
||||
}
|
||||
|
||||
impl From<ScopeError> for Error {
|
||||
|
||||
@@ -263,6 +263,33 @@ impl JjExecutor for JjLib {
|
||||
})?;
|
||||
Ok(commit.description().trim_end().to_string())
|
||||
}
|
||||
|
||||
async fn new_revision(&self, revset: &str) -> Result<(), Error> {
|
||||
let commit_id = self.get_commit_id(revset).await?;
|
||||
let repo = self.repo.lock()?.clone();
|
||||
let mut tx = repo.start_transaction();
|
||||
let parent_commit =
|
||||
tx.repo()
|
||||
.store()
|
||||
.get_commit(&commit_id)
|
||||
.map_err(|e| Error::JjOperation {
|
||||
context: e.to_string(),
|
||||
})?;
|
||||
tx.repo_mut()
|
||||
.check_out(self.workspace_name.clone(), &parent_commit)
|
||||
.await
|
||||
.map_err(|e| Error::JjOperation {
|
||||
context: e.to_string(),
|
||||
})?;
|
||||
let new_repo =
|
||||
tx.commit("jj-cz: create new revision")
|
||||
.await
|
||||
.map_err(|e| Error::JjOperation {
|
||||
context: e.to_string(),
|
||||
})?;
|
||||
*self.repo.lock()? = new_repo;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+88
-5
@@ -11,18 +11,22 @@ use std::sync::{Mutex, atomic::AtomicBool};
|
||||
/// Mock implementation of JjExecutor for testing
|
||||
#[derive(Debug)]
|
||||
pub struct MockJjExecutor {
|
||||
/// Response to return from is_repository()
|
||||
/// Response to return from `is_repository()`
|
||||
is_repo_response: Result<bool, Error>,
|
||||
/// Response to return from describe()
|
||||
/// Response to return from `describe()`
|
||||
describe_response: Result<(), Error>,
|
||||
/// Track described revsets
|
||||
described_revsets: Mutex<Vec<String>>,
|
||||
/// Track response to return from get_description()
|
||||
/// Track response to return from `get_description()`
|
||||
get_description_response: Result<String, Error>,
|
||||
/// Track calls to is_repository()
|
||||
/// Track calls to `is_repository()`
|
||||
is_repo_called: AtomicBool,
|
||||
/// Track calls to describe() with the message passed
|
||||
/// Track calls to `describe()` with the message passed
|
||||
describe_calls: Mutex<Vec<String>>,
|
||||
/// Track response to return from `new_revision()`
|
||||
new_revision_response: Result<(), Error>,
|
||||
/// Track calls to `new_revision()`
|
||||
new_revision_calls: Mutex<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for MockJjExecutor {
|
||||
@@ -34,6 +38,8 @@ impl Default for MockJjExecutor {
|
||||
get_description_response: Ok(String::new()),
|
||||
is_repo_called: AtomicBool::new(false),
|
||||
describe_calls: Mutex::new(Vec::new()),
|
||||
new_revision_response: Ok(()),
|
||||
new_revision_calls: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +72,15 @@ impl MockJjExecutor {
|
||||
pub fn describe_messages(&self) -> Vec<String> {
|
||||
self.describe_calls.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn with_new_revision_response(mut self, response: Result<(), Error>) -> Self {
|
||||
self.new_revision_response = response;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn new_revision_calls(&self) -> Vec<String> {
|
||||
self.new_revision_calls.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
@@ -97,6 +112,17 @@ impl JjExecutor for MockJjExecutor {
|
||||
async fn get_description(&self, _revset: &str) -> Result<String, Error> {
|
||||
self.get_description_response.clone()
|
||||
}
|
||||
|
||||
async fn new_revision(&self, revset: &str) -> Result<(), Error> {
|
||||
self.new_revision_calls
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(revset.to_string());
|
||||
match &self.new_revision_response {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(e.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -245,4 +271,61 @@ mod tests {
|
||||
mock.is_repository().await.unwrap();
|
||||
assert!(mock.was_is_repo_called());
|
||||
}
|
||||
|
||||
/// Test mock new_revision() records the revset
|
||||
#[tokio::test]
|
||||
async fn mock_new_revision_records_revset() {
|
||||
let mock = MockJjExecutor::new();
|
||||
let result = mock.new_revision("@").await;
|
||||
assert!(result.is_ok());
|
||||
let calls = mock.new_revision_calls();
|
||||
assert_eq!(calls.len(), 1);
|
||||
assert_eq!(calls[0], "@");
|
||||
}
|
||||
|
||||
/// Test mock new_revision() records multiple calls
|
||||
#[tokio::test]
|
||||
async fn mock_new_revision_records_multiple_calls() {
|
||||
let mock = MockJjExecutor::new();
|
||||
mock.new_revision("@").await.unwrap();
|
||||
mock.new_revision("abc").await.unwrap();
|
||||
mock.new_revision("xyz").await.unwrap();
|
||||
let calls = mock.new_revision_calls();
|
||||
assert_eq!(calls.len(), 3);
|
||||
assert_eq!(calls[0], "@");
|
||||
assert_eq!(calls[1], "abc");
|
||||
assert_eq!(calls[2], "xyz");
|
||||
}
|
||||
|
||||
/// Test mock new_revision() returns configured error
|
||||
#[tokio::test]
|
||||
async fn mock_new_revision_returns_error() {
|
||||
let mock = MockJjExecutor::new().with_new_revision_response(Err(Error::RepositoryLocked));
|
||||
let result = mock.new_revision("@").await;
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), Error::RepositoryLocked));
|
||||
}
|
||||
|
||||
/// Test mock new_revision() records revset even on error
|
||||
#[tokio::test]
|
||||
async fn mock_new_revision_records_revset_on_error() {
|
||||
let mock = MockJjExecutor::new().with_new_revision_response(Err(Error::JjOperation {
|
||||
context: "failed".to_string(),
|
||||
}));
|
||||
let result = mock.new_revision("abc").await;
|
||||
assert!(result.is_err());
|
||||
let calls = mock.new_revision_calls();
|
||||
assert_eq!(calls.len(), 1);
|
||||
assert_eq!(calls[0], "abc");
|
||||
}
|
||||
|
||||
/// Test mock new_revision() can be inspected after success
|
||||
#[tokio::test]
|
||||
async fn mock_new_revision_returns_ok_and_tracks_revset() {
|
||||
let mock = MockJjExecutor::new();
|
||||
let result = mock.new_revision("my-feature").await;
|
||||
assert!(result.is_ok());
|
||||
let calls = mock.new_revision_calls();
|
||||
assert_eq!(calls, vec!["my-feature"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ pub trait JjExecutor: Send + Sync {
|
||||
|
||||
/// Get the current description of a specific revision
|
||||
async fn get_description(&self, revset: &str) -> Result<String, Error>;
|
||||
|
||||
/// Create a new empty child revision parented on `revset`.
|
||||
///
|
||||
/// Equivalent to `jj new <revset>`
|
||||
async fn new_revision(&self, revset: &str) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
mod commit;
|
||||
mod error;
|
||||
pub mod error;
|
||||
mod jj;
|
||||
mod prompts;
|
||||
|
||||
|
||||
+17
-13
@@ -10,7 +10,7 @@ const EXIT_CANCELLED: i32 = 130; // Same as SIGINT (Ctrl+C)
|
||||
const EXIT_ERROR: i32 = 1;
|
||||
|
||||
/// Map application errors to appropriate exit codes
|
||||
fn error_to_exit_code(error: &Error) -> i32 {
|
||||
fn error_to_exit_code(error: Error) -> i32 {
|
||||
match error {
|
||||
Error::Cancelled => EXIT_CANCELLED,
|
||||
Error::NotARepository => EXIT_ERROR,
|
||||
@@ -24,6 +24,7 @@ fn error_to_exit_code(error: &Error) -> i32 {
|
||||
Error::FailedReadingConfig { .. } => EXIT_ERROR,
|
||||
Error::RevsetResolutionError { .. } => EXIT_ERROR,
|
||||
Error::MultipleRevisions { .. } => EXIT_ERROR,
|
||||
Error::NewFlagWithMultipleRevisions => EXIT_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,25 +35,26 @@ fn is_interactive_terminal() -> bool {
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> Result<(), ()> {
|
||||
let cli = cli::Cli::parse();
|
||||
|
||||
cli.validate().map_err(exit_on_error)?;
|
||||
|
||||
if !is_interactive_terminal() {
|
||||
eprintln!("❌ Error: jj-cz requires an interactive terminal (TTY)");
|
||||
eprintln!(" This tool cannot be used in non-interactive mode or when piping input.");
|
||||
eprintln!(" Use --help for usage information.");
|
||||
process::exit(EXIT_ERROR);
|
||||
}
|
||||
let executor = match JjLib::new().await {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error: {}", e);
|
||||
process::exit(EXIT_ERROR);
|
||||
}
|
||||
};
|
||||
let executor = JjLib::new().await.map_err(exit_on_error)?;
|
||||
let workflow = CommitWorkflow::new(executor);
|
||||
for revset in cli.revsets() {
|
||||
let result = workflow.run_for_revset(revset).await;
|
||||
handle_result(result);
|
||||
if cli.create_new() {
|
||||
println!("Creating a new revision after {revset}");
|
||||
workflow.new_revision(revset).await.map_err(exit_on_error)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_result(result: Result<(), Error>) {
|
||||
@@ -62,11 +64,13 @@ async fn main() {
|
||||
println!("🟡 Operation cancelled by user.");
|
||||
process::exit(EXIT_CANCELLED);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error: {}", e);
|
||||
process::exit(error_to_exit_code(&e));
|
||||
}
|
||||
Err(e) => exit_on_error(e),
|
||||
}
|
||||
}
|
||||
process::exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
fn exit_on_error(e: Error) {
|
||||
eprintln!("❌ Error: {}", e);
|
||||
process::exit(error_to_exit_code(e));
|
||||
}
|
||||
|
||||
@@ -184,6 +184,10 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
Err(Error::Cancelled)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_revision(&self, revset: &str) -> Result<(), Error> {
|
||||
self.executor.new_revision(revset).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -762,4 +766,59 @@ mod tests {
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0], "fix: fix crash");
|
||||
}
|
||||
|
||||
/// Test workflow new_revision() records the revset
|
||||
#[tokio::test]
|
||||
async fn workflow_new_revision_records_revset() {
|
||||
let mock_executor = MockJjExecutor::new();
|
||||
let workflow = CommitWorkflow::new(mock_executor);
|
||||
|
||||
let result = workflow.new_revision("@").await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let calls = workflow.executor.new_revision_calls();
|
||||
assert_eq!(calls, vec!["@"]);
|
||||
}
|
||||
|
||||
/// Test workflow new_revision() propagates executor errors
|
||||
#[tokio::test]
|
||||
async fn workflow_new_revision_propagates_error() {
|
||||
let mock_executor =
|
||||
MockJjExecutor::new().with_new_revision_response(Err(Error::RepositoryLocked));
|
||||
let workflow = CommitWorkflow::new(mock_executor);
|
||||
|
||||
let result = workflow.new_revision("@").await;
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), Error::RepositoryLocked));
|
||||
}
|
||||
|
||||
/// Test workflow run_for_revset() followed by new_revision() records both
|
||||
///
|
||||
/// This mirrors the actual usage pattern in main.rs.
|
||||
#[tokio::test]
|
||||
async fn workflow_describe_then_new_revision() {
|
||||
let mock_executor = MockJjExecutor::new().with_is_repo_response(Ok(true));
|
||||
let mock_prompts = MockPrompts::new()
|
||||
.with_commit_type(CommitType::Feat)
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("add feature").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
let workflow = CommitWorkflow::with_prompts(mock_executor, mock_prompts);
|
||||
|
||||
workflow.run_for_revset("@").await.expect("describe failed");
|
||||
workflow
|
||||
.new_revision("@")
|
||||
.await
|
||||
.expect("new_revision failed");
|
||||
|
||||
let messages = workflow.executor.describe_messages();
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert!(messages[0].contains("feat:"));
|
||||
|
||||
let calls = workflow.executor.new_revision_calls();
|
||||
assert_eq!(calls, vec!["@"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,3 +135,155 @@ fn test_jj_operation_context() {
|
||||
panic!("Expected JjOperation variant");
|
||||
}
|
||||
}
|
||||
|
||||
/// Test conversion from std::io::Error
|
||||
#[test]
|
||||
fn test_from_io_error() {
|
||||
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
|
||||
let error: Error = io_err.into();
|
||||
assert!(matches!(error, Error::FailedGettingCurrentDir));
|
||||
}
|
||||
|
||||
/// Test conversion from std::sync::PoisonError
|
||||
#[test]
|
||||
fn test_from_poison_error() {
|
||||
let mutex = std::sync::Mutex::new(());
|
||||
// Poison the mutex by panicking while holding the lock
|
||||
let poison_err = std::panic::catch_unwind(|| {
|
||||
let _guard = mutex.lock().unwrap();
|
||||
panic!("deliberate panic");
|
||||
});
|
||||
assert!(poison_err.is_err());
|
||||
|
||||
// Now lock should fail with PoisonError
|
||||
let result = mutex.lock();
|
||||
assert!(result.is_err());
|
||||
|
||||
let error: Error = result.unwrap_err().into();
|
||||
assert!(matches!(error, Error::JjOperation { .. }));
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
"Repository operation failed: internal lock poisoned"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test from_revset_evaluation_error constructs RevsetResolutionError
|
||||
#[test]
|
||||
fn test_from_revset_evaluation_error() {
|
||||
let underlying = std::io::Error::new(std::io::ErrorKind::Other, "store failure");
|
||||
let eval_err = jj_lib::revset::RevsetEvaluationError::Other(Box::new(underlying));
|
||||
let error = Error::from_revset_evaluation_error("@", eval_err);
|
||||
assert!(matches!(error, Error::RevsetResolutionError { .. }));
|
||||
let description = format!("{}", error);
|
||||
assert!(description.contains("@"));
|
||||
assert!(description.contains("store failure"));
|
||||
}
|
||||
|
||||
/// Test from_revset_resolution_error constructs RevsetResolutionError
|
||||
#[test]
|
||||
fn test_from_revset_resolution_error() {
|
||||
let resolution_err = jj_lib::revset::RevsetResolutionError::NoSuchRevision {
|
||||
name: "nonexistent".to_string(),
|
||||
candidates: Vec::new(),
|
||||
};
|
||||
let error = Error::from_revset_resolution_error("@", resolution_err);
|
||||
assert!(matches!(error, Error::RevsetResolutionError { .. }));
|
||||
let description = format!("{}", error);
|
||||
assert!(description.contains("@"));
|
||||
assert!(description.contains("nonexistent"));
|
||||
}
|
||||
|
||||
/// Test NewFlagWithMultipleRevisions error display
|
||||
#[test]
|
||||
fn test_new_flag_with_multiple_revisions() {
|
||||
let error = Error::NewFlagWithMultipleRevisions;
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
"--new cannot be used with multiple revisions"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test NonInteractive error display
|
||||
#[test]
|
||||
fn test_non_interactive() {
|
||||
let error = Error::NonInteractive;
|
||||
assert_eq!(format!("{}", error), "Non-interactive terminal detected");
|
||||
}
|
||||
|
||||
/// Test FailedReadingConfig error display
|
||||
#[test]
|
||||
fn test_failed_reading_config() {
|
||||
let error = Error::FailedReadingConfig {
|
||||
context: "config parse error".to_string(),
|
||||
};
|
||||
let description = format!("{}", error);
|
||||
assert!(description.contains("config parse error"));
|
||||
}
|
||||
|
||||
/// Test MultipleRevisions error display
|
||||
#[test]
|
||||
fn test_multiple_revisions() {
|
||||
let error = Error::MultipleRevisions {
|
||||
revset: "abc | def".to_string(),
|
||||
};
|
||||
let description = format!("{}", error);
|
||||
assert!(description.contains("abc | def"));
|
||||
assert!(description.contains("multiple commits"));
|
||||
}
|
||||
|
||||
/// Test RepositoryLocked error display
|
||||
#[test]
|
||||
fn test_repository_locked() {
|
||||
let error = Error::RepositoryLocked;
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
"Repository is locked by another process"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test FailedGettingCurrentDir error display
|
||||
#[test]
|
||||
fn test_failed_getting_current_dir() {
|
||||
let error = Error::FailedGettingCurrentDir;
|
||||
assert_eq!(format!("{}", error), "Could not get current directory");
|
||||
}
|
||||
|
||||
/// Test error matching on all variants
|
||||
#[test]
|
||||
fn test_error_matching_all_variants() {
|
||||
let variants: Vec<Error> = vec![
|
||||
Error::InvalidScope("s".into()),
|
||||
Error::InvalidDescription("d".into()),
|
||||
Error::InvalidCommitMessage("m".into()),
|
||||
Error::NotARepository,
|
||||
Error::JjOperation {
|
||||
context: "c".into(),
|
||||
},
|
||||
Error::RepositoryLocked,
|
||||
Error::FailedGettingCurrentDir,
|
||||
Error::FailedReadingConfig {
|
||||
context: "c".into(),
|
||||
},
|
||||
Error::Cancelled,
|
||||
Error::NonInteractive,
|
||||
Error::RevsetResolutionError {
|
||||
revset: "@".into(),
|
||||
context: "c".into(),
|
||||
},
|
||||
Error::MultipleRevisions { revset: "@".into() },
|
||||
Error::NewFlagWithMultipleRevisions,
|
||||
];
|
||||
|
||||
// All variants should be displayable without panicking
|
||||
for variant in &variants {
|
||||
let _ = format!("{}", variant);
|
||||
let _ = format!("{:?}", variant);
|
||||
}
|
||||
|
||||
// Verify all variants can be cloned
|
||||
let cloned: Vec<Error> = variants.iter().map(|e| e.clone()).collect();
|
||||
assert_eq!(variants.len(), cloned.len());
|
||||
for (original, clone) in variants.iter().zip(cloned.iter()) {
|
||||
assert_eq!(original, clone);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user