Compare commits
3 Commits
a5bec93228
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
4ad6e944b2
|
|||
|
bd4aaff3f3
|
|||
|
e6ac6890b2
|
@@ -1,13 +1,45 @@
|
||||
# jj-cz: Conventional Commits for Jujutsu
|
||||
---
|
||||
include_toc: true
|
||||
gitea: none
|
||||
---
|
||||
|
||||
An interactive CLI tool that guides Jujutsu users through creating
|
||||
[conventional commit](https://www.conventionalcommits.org/) messages.
|
||||
|
||||
<h1 align="center">jj-cz: Conventional Commits for Jujutsu</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
|
||||
- All 11 commit types with descriptions (feat, fix, docs, style,
|
||||
refactor, perf, test, build, ci, chore, revert)
|
||||
- Interactive prompts for type, scope, breaking changes, ticket references, and description
|
||||
- All 11 commit types with descriptions (feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert)
|
||||
- Optional scope with validation
|
||||
- 72-character first-line limit enforcement
|
||||
- Preview before applying
|
||||
@@ -28,12 +60,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 xs`
|
||||
jj-cz -n xs # equivalent of `jj-cz xs && jj new xs`
|
||||
```
|
||||
|
||||
You cannot, however, call `jj-cz` on multiple revisions with the `--new` flag active.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A Jujutsu repository
|
||||
@@ -93,3 +136,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 |
@@ -38,6 +38,12 @@ command = [
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.coverage]
|
||||
command = [
|
||||
"cargo", "tarpaulin", "--config", ".tarpaulin.local.toml", "--features", "test-utils"
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = ["cargo", "doc", "--color", "always", "--no-deps"]
|
||||
need_stdout = false
|
||||
@@ -82,3 +88,4 @@ allow_warnings = true
|
||||
[keybindings]
|
||||
# alt-m = "job:my-job"
|
||||
c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target
|
||||
v = "job:coverage"
|
||||
@@ -9,7 +9,10 @@ pkgs.stdenv.mkDerivation rec {
|
||||
nativeBuildInputs = [pkgs.zip];
|
||||
buildPhase = ''
|
||||
mkdir -p $out/dist
|
||||
zip -j $out/dist/${name}.zip ${bin}/bin/jj-cz* ${src}/README.md ${src}/LICENSE.*
|
||||
# zip -j $out/dist/${name}.zip ${bin}/bin/jj-cz* ${src}/README.md ${src}/LICENSE.*
|
||||
cp ${bin}/bin/jj-cz* $out/dist/
|
||||
cp ${src}/README.md $out/dist/
|
||||
cp ${src}/LICENSE.* $out/dist/
|
||||
'';
|
||||
installPhase = "";
|
||||
dontConfigure = true;
|
||||
|
||||
@@ -4,10 +4,12 @@ pub trait Footer {
|
||||
|
||||
fn as_footer(&self) -> String {
|
||||
let default = format!("{}: {}", self.prefix(), self.note());
|
||||
if default.chars().count() > 72 {
|
||||
let mut footer = if default.chars().count() > 72 {
|
||||
textwrap::wrap(&default, 71).join("\n ")
|
||||
} else {
|
||||
default
|
||||
}
|
||||
};
|
||||
footer.push('\n');
|
||||
footer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Body, BreakingChange, CommitType, Description, Scope};
|
||||
use super::{Body, BreakingChange, CommitType, Description, Footer, References, Scope};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that can occur when creating a ConventionalCommit
|
||||
@@ -23,6 +23,7 @@ pub struct ConventionalCommit {
|
||||
description: Description,
|
||||
breaking_change: BreakingChange,
|
||||
body: Body,
|
||||
references: References,
|
||||
}
|
||||
|
||||
impl ConventionalCommit {
|
||||
@@ -44,6 +45,7 @@ impl ConventionalCommit {
|
||||
description: Description,
|
||||
breaking_change: BreakingChange,
|
||||
body: Body,
|
||||
references: References,
|
||||
) -> Result<Self, CommitMessageError> {
|
||||
let commit = Self {
|
||||
commit_type,
|
||||
@@ -51,6 +53,7 @@ impl ConventionalCommit {
|
||||
description,
|
||||
breaking_change,
|
||||
body,
|
||||
references,
|
||||
};
|
||||
let len = commit.first_line_len();
|
||||
if len > Self::FIRST_LINE_MAX_LENGTH {
|
||||
@@ -92,6 +95,7 @@ impl ConventionalCommit {
|
||||
&self.description,
|
||||
&self.breaking_change,
|
||||
&self.body,
|
||||
&self.references,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -106,14 +110,16 @@ impl ConventionalCommit {
|
||||
description: &Description,
|
||||
breaking_change: &BreakingChange,
|
||||
body: &Body,
|
||||
references: &References,
|
||||
) -> String {
|
||||
let scope = scope.header_segment();
|
||||
let breaking_change_header = breaking_change.header_segment();
|
||||
let breaking_change_footer = breaking_change.as_footer();
|
||||
let refs_footer = references.as_footer();
|
||||
format!(
|
||||
r#"{commit_type}{scope}{breaking_change_header}: {description}
|
||||
{}
|
||||
{breaking_change_footer}"#,
|
||||
{breaking_change_footer}{refs_footer}"#,
|
||||
body.format()
|
||||
)
|
||||
.trim()
|
||||
@@ -154,6 +160,7 @@ mod tests {
|
||||
description,
|
||||
breaking_change,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
)
|
||||
.expect("test commit should have valid line length")
|
||||
}
|
||||
@@ -637,6 +644,7 @@ mod tests {
|
||||
Description::parse(&desc_44).unwrap(),
|
||||
BreakingChange::No,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
let commit = result.unwrap();
|
||||
@@ -667,6 +675,7 @@ mod tests {
|
||||
Description::parse(&desc_31).unwrap(),
|
||||
BreakingChange::No,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
@@ -691,6 +700,7 @@ mod tests {
|
||||
Description::parse(&desc_40).unwrap(),
|
||||
BreakingChange::No,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
@@ -711,6 +721,7 @@ mod tests {
|
||||
test_description("quick fix"),
|
||||
BreakingChange::No,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -724,6 +735,7 @@ mod tests {
|
||||
test_description("add feature"),
|
||||
BreakingChange::No,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -750,6 +762,7 @@ mod tests {
|
||||
test_description("test"),
|
||||
BreakingChange::No,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
// Just verify it's a Result by using is_ok()
|
||||
assert!(result.is_ok());
|
||||
@@ -781,6 +794,7 @@ mod tests {
|
||||
desc,
|
||||
BreakingChange::No,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
// new() itself calls git_conventional::Commit::parse internally, so
|
||||
// if this is Ok, SC-002 is satisfied for this case.
|
||||
@@ -918,6 +932,7 @@ mod tests {
|
||||
Description::parse(&desc_44).unwrap(),
|
||||
BreakingChange::Yes,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
@@ -940,6 +955,7 @@ mod tests {
|
||||
test_description("quick fix"),
|
||||
long_note.into(),
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -959,6 +975,7 @@ mod tests {
|
||||
&commit.description,
|
||||
&BreakingChange::No,
|
||||
&Body::default(),
|
||||
&References::default(),
|
||||
);
|
||||
assert_eq!(preview, commit.format());
|
||||
}
|
||||
@@ -972,6 +989,7 @@ mod tests {
|
||||
&test_description("drop legacy API"),
|
||||
&"removes legacy endpoint".into(),
|
||||
&Body::default(),
|
||||
&References::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
preview,
|
||||
@@ -988,6 +1006,7 @@ mod tests {
|
||||
&test_description("drop Node 6"),
|
||||
&"Node 6 is no longer supported".into(),
|
||||
&Body::default(),
|
||||
&References::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
preview,
|
||||
@@ -1093,6 +1112,7 @@ mod tests {
|
||||
test_description("add feature"),
|
||||
BreakingChange::No,
|
||||
Body::from("This explains the change."),
|
||||
References::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1110,6 +1130,7 @@ mod tests {
|
||||
test_description("handle null response"),
|
||||
BreakingChange::No,
|
||||
Body::from("Null responses were previously unhandled."),
|
||||
References::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1127,6 +1148,7 @@ mod tests {
|
||||
test_description("update README"),
|
||||
BreakingChange::No,
|
||||
Body::from("First paragraph.\n\nSecond paragraph."),
|
||||
References::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1146,6 +1168,7 @@ mod tests {
|
||||
test_description("drop legacy API"),
|
||||
"removes legacy endpoint".into(),
|
||||
Body::from("The endpoint was deprecated in v2."),
|
||||
References::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1163,6 +1186,7 @@ mod tests {
|
||||
&test_description("add feature"),
|
||||
&BreakingChange::No,
|
||||
&Body::from("This explains the change."),
|
||||
&References::default(),
|
||||
);
|
||||
assert_eq!(preview, "feat: add feature\n\nThis explains the change.");
|
||||
}
|
||||
@@ -1178,6 +1202,7 @@ mod tests {
|
||||
&test_description("drop old API"),
|
||||
&"old API removed".into(),
|
||||
&Body::from("Migration guide: see CHANGELOG."),
|
||||
&References::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
preview,
|
||||
|
||||
@@ -18,3 +18,6 @@ pub use body::Body;
|
||||
|
||||
mod message;
|
||||
pub use message::{CommitMessageError, ConventionalCommit};
|
||||
|
||||
mod references;
|
||||
pub use references::References;
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
use super::Footer;
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct References(Vec<String>);
|
||||
|
||||
impl<T> From<T> for References
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
let references: Vec<String> = value
|
||||
.to_string()
|
||||
.split(",")
|
||||
.flat_map(|e| match e.trim() {
|
||||
"" => None,
|
||||
e => Some(e.to_string()),
|
||||
})
|
||||
.collect();
|
||||
Self(references)
|
||||
}
|
||||
}
|
||||
|
||||
impl Footer for References {
|
||||
fn prefix(&self) -> &str {
|
||||
"Refs: "
|
||||
}
|
||||
|
||||
fn note(&self) -> &str {
|
||||
""
|
||||
}
|
||||
|
||||
fn as_footer(&self) -> String {
|
||||
if self.0.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let footers: Vec<String> = self
|
||||
.0
|
||||
.iter()
|
||||
.map(|r| format!("{}{r}\n", self.prefix()))
|
||||
.collect();
|
||||
footers.join("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Default is empty
|
||||
#[test]
|
||||
fn default_is_empty() {
|
||||
let refs = References::default();
|
||||
assert!(refs.0.is_empty());
|
||||
}
|
||||
|
||||
/// Empty input produces empty references
|
||||
#[test]
|
||||
fn from_empty_string() {
|
||||
assert_eq!(References::from(""), References::default());
|
||||
}
|
||||
|
||||
/// Whitespace-only input produces empty references
|
||||
#[test]
|
||||
fn from_whitespace_only() {
|
||||
assert_eq!(References::from(" "), References::default());
|
||||
}
|
||||
|
||||
/// Single reference without commas
|
||||
#[test]
|
||||
fn from_single_reference() {
|
||||
let refs = References::from("#123");
|
||||
assert_eq!(refs.0, vec!["#123".to_string()]);
|
||||
}
|
||||
|
||||
/// Comma-separated references are split and trimmed
|
||||
#[test]
|
||||
fn from_comma_separated() {
|
||||
let refs = References::from("#123, #456");
|
||||
assert_eq!(refs.0, vec!["#123".to_string(), "#456".to_string()]);
|
||||
}
|
||||
|
||||
/// Leading whitespace around references is trimmed
|
||||
#[test]
|
||||
fn from_trims_leading_whitespace() {
|
||||
let refs = References::from(" #123, #456");
|
||||
assert_eq!(refs.0, vec!["#123".to_string(), "#456".to_string()]);
|
||||
}
|
||||
|
||||
/// Trailing whitespace around references is trimmed
|
||||
#[test]
|
||||
fn from_trims_trailing_whitespace() {
|
||||
let refs = References::from("#123 , #456 ");
|
||||
assert_eq!(refs.0, vec!["#123".to_string(), "#456".to_string()]);
|
||||
}
|
||||
|
||||
/// Empty segments from consecutive commas are filtered out
|
||||
#[test]
|
||||
fn from_filters_empty_segments() {
|
||||
let refs = References::from("#123,,, #456");
|
||||
assert_eq!(refs.0, vec!["#123".to_string(), "#456".to_string()]);
|
||||
}
|
||||
|
||||
/// From works with owned String
|
||||
#[test]
|
||||
fn from_owned_string() {
|
||||
let input = "#123, #456".to_string();
|
||||
let refs = References::from(input);
|
||||
assert_eq!(refs.0, vec!["#123".to_string(), "#456".to_string()]);
|
||||
}
|
||||
|
||||
/// as_footer returns empty string for empty references
|
||||
#[test]
|
||||
fn as_footer_empty() {
|
||||
let refs = References::default();
|
||||
assert_eq!(refs.as_footer(), "");
|
||||
}
|
||||
|
||||
/// as_footer returns single line for one reference
|
||||
#[test]
|
||||
fn as_footer_single() {
|
||||
let refs = References::from("#123");
|
||||
assert_eq!(refs.as_footer(), "Refs: #123\n");
|
||||
}
|
||||
|
||||
/// as_footer returns multiple lines for multiple references
|
||||
#[test]
|
||||
fn as_footer_multiple() {
|
||||
let refs = References::from("#123, #456");
|
||||
assert_eq!(refs.as_footer(), "Refs: #123\nRefs: #456\n");
|
||||
}
|
||||
|
||||
/// as_footer handles Jira-style references
|
||||
#[test]
|
||||
fn as_footer_jira_style() {
|
||||
let refs = References::from("OPS-456, PROJ-789");
|
||||
assert_eq!(refs.as_footer(), "Refs: OPS-456\nRefs: PROJ-789\n");
|
||||
}
|
||||
|
||||
/// Footer trait prefix returns correct value
|
||||
#[test]
|
||||
fn footer_prefix() {
|
||||
let refs = References::default();
|
||||
assert_eq!(refs.prefix(), "Refs: ");
|
||||
}
|
||||
|
||||
/// Footer trait note returns empty string
|
||||
#[test]
|
||||
fn footer_note() {
|
||||
let refs = References::default();
|
||||
assert_eq!(refs.note(), "");
|
||||
}
|
||||
|
||||
/// Clone produces equal value
|
||||
#[test]
|
||||
fn clone_equality() {
|
||||
let refs = References::from("#123, #456");
|
||||
let cloned = refs.clone();
|
||||
assert_eq!(refs, cloned);
|
||||
}
|
||||
|
||||
/// Debug output is available
|
||||
#[test]
|
||||
fn debug_output() {
|
||||
let refs = References::from("#123");
|
||||
let debug = format!("{:?}", refs);
|
||||
assert!(debug.contains("References"));
|
||||
}
|
||||
|
||||
/// Different references are not equal
|
||||
#[test]
|
||||
fn inequality_different_refs() {
|
||||
let a = References::from("#123");
|
||||
let b = References::from("#456");
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
/// Empty vs non-empty are not equal
|
||||
#[test]
|
||||
fn inequality_empty_vs_non_empty() {
|
||||
let empty = References::default();
|
||||
let non_empty = References::from("#123");
|
||||
assert_ne!(empty, non_empty);
|
||||
}
|
||||
}
|
||||
+59
-1
@@ -8,7 +8,7 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
commit::types::{Body, BreakingChange, CommitType, Description, Scope},
|
||||
commit::types::{Body, BreakingChange, CommitType, Description, References, Scope},
|
||||
error::Error,
|
||||
prompts::prompter::Prompter,
|
||||
};
|
||||
@@ -20,6 +20,7 @@ enum MockResponse {
|
||||
Scope(Scope),
|
||||
Description(Description),
|
||||
BreakingChange(BreakingChange),
|
||||
References(References),
|
||||
Body(Body),
|
||||
Confirm(bool),
|
||||
Error(Error),
|
||||
@@ -81,6 +82,15 @@ impl MockPrompts {
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the mock to return specific references
|
||||
pub fn with_references(self, references: References) -> Self {
|
||||
self.responses
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(MockResponse::References(references));
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the mock to return a specific body response
|
||||
pub fn with_body(self, body: Body) -> Self {
|
||||
self.responses
|
||||
@@ -140,6 +150,14 @@ impl MockPrompts {
|
||||
.contains(&"input_breaking_change".to_string())
|
||||
}
|
||||
|
||||
/// Check if input_references was called
|
||||
pub fn was_references_called(&self) -> bool {
|
||||
self.prompts_called
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&"input_references".to_string())
|
||||
}
|
||||
|
||||
/// Check if confirm_apply was called
|
||||
pub fn was_confirm_called(&self) -> bool {
|
||||
self.prompts_called
|
||||
@@ -207,6 +225,18 @@ impl Prompter for MockPrompts {
|
||||
}
|
||||
}
|
||||
|
||||
fn input_references(&self) -> Result<References, Error> {
|
||||
self.prompts_called
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push("input_references".to_string());
|
||||
match self.responses.lock().unwrap().remove(0) {
|
||||
MockResponse::References(r) => Ok(r),
|
||||
MockResponse::Error(e) => Err(e),
|
||||
_ => panic!("MockPrompts: Expected References response, got different type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_body(&self) -> Result<Body, Error> {
|
||||
self.prompts_called
|
||||
.lock()
|
||||
@@ -336,6 +366,34 @@ mod tests {
|
||||
assert!(mock.emitted_messages().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_input_references() {
|
||||
let refs = References::from("#123, #456");
|
||||
let mock = MockPrompts::new().with_references(refs.clone());
|
||||
let result = mock.input_references();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), refs);
|
||||
assert!(mock.was_references_called());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_input_references_default() {
|
||||
let mock = MockPrompts::new().with_references(References::default());
|
||||
let result = mock.input_references();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), References::default());
|
||||
assert!(mock.was_references_called());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_input_references_error() {
|
||||
let mock = MockPrompts::new().with_error(Error::Cancelled);
|
||||
let result = mock.input_references();
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), Error::Cancelled));
|
||||
assert!(mock.was_references_called());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_input_breaking_change_no() {
|
||||
let mock = MockPrompts::new().with_breaking_change(BreakingChange::No);
|
||||
|
||||
+17
-1
@@ -9,7 +9,7 @@ use inquire::{Confirm, Text};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
commit::types::{Body, BreakingChange, CommitType, Description, Scope},
|
||||
commit::types::{Body, BreakingChange, CommitType, Description, References, Scope},
|
||||
error::Error,
|
||||
};
|
||||
|
||||
@@ -33,6 +33,9 @@ pub trait Prompter {
|
||||
/// Prompt the user to optionally add a free-form body via an external editor
|
||||
fn input_body(&self) -> Result<Body, Error>;
|
||||
|
||||
/// Prompt the user to optionally add comma-separated ticket references
|
||||
fn input_references(&self) -> Result<References, Error>;
|
||||
|
||||
/// Prompt the user to confirm applying the commit message
|
||||
fn confirm_apply(&self, message: &str) -> Result<bool, Error>;
|
||||
|
||||
@@ -91,6 +94,19 @@ impl Prompter for RealPrompts {
|
||||
}
|
||||
}
|
||||
|
||||
fn input_references(&self) -> Result<References, Error> {
|
||||
let answer = inquire::Text::new("Enter comma-separated references (optional):")
|
||||
.with_help_message("References are optional. If provided, will become footer(s) in the commit message. References must be comma-separated.")
|
||||
.with_placeholder("Leave empty if no references")
|
||||
.prompt_skippable()
|
||||
.map_err(|_| Error::Cancelled)?;
|
||||
match answer {
|
||||
None => Ok(References::default()),
|
||||
Some(s) if s.trim().is_empty() => Ok(References::default()),
|
||||
Some(s) => Ok(References::from(s)),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_description(&self) -> Result<Description, Error> {
|
||||
loop {
|
||||
let answer = Text::new("Enter description (required):")
|
||||
|
||||
+42
-4
@@ -6,7 +6,7 @@
|
||||
use crate::{
|
||||
commit::types::{
|
||||
Body, BreakingChange, CommitMessageError, CommitType, ConventionalCommit, Description,
|
||||
Scope,
|
||||
References, Scope,
|
||||
},
|
||||
error::Error,
|
||||
jj::JjExecutor,
|
||||
@@ -65,8 +65,16 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
let scope = self.scope_input()?;
|
||||
let description = self.description_input()?;
|
||||
let breaking_change = self.breaking_change_input()?;
|
||||
let references = self.references_input()?;
|
||||
let body = self.body_input()?;
|
||||
match self.preview_and_confirm(commit_type, scope, description, breaking_change, body) {
|
||||
match self.preview_and_confirm(
|
||||
commit_type,
|
||||
scope,
|
||||
description,
|
||||
breaking_change,
|
||||
body,
|
||||
references,
|
||||
) {
|
||||
Ok(conventional_commit) => {
|
||||
self.executor
|
||||
.describe(revset, &conventional_commit.to_string())
|
||||
@@ -113,6 +121,11 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
self.prompts.input_breaking_change()
|
||||
}
|
||||
|
||||
/// Prompt user for references
|
||||
fn references_input(&self) -> Result<References, Error> {
|
||||
self.prompts.input_references()
|
||||
}
|
||||
|
||||
/// Prompt user to optionally add a free-form body via an external editor
|
||||
fn body_input(&self) -> Result<Body, Error> {
|
||||
self.prompts.input_body()
|
||||
@@ -129,6 +142,7 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
description: Description,
|
||||
breaking_change: BreakingChange,
|
||||
body: Body,
|
||||
references: References,
|
||||
) -> Result<ConventionalCommit, Error> {
|
||||
// Format the message for preview
|
||||
let message = ConventionalCommit::format_preview(
|
||||
@@ -137,6 +151,7 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
&description,
|
||||
&breaking_change,
|
||||
&body,
|
||||
&references,
|
||||
);
|
||||
|
||||
// Try to build the conventional commit (this validates the 72-char limit)
|
||||
@@ -146,6 +161,7 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
description.clone(),
|
||||
breaking_change,
|
||||
body,
|
||||
references,
|
||||
) {
|
||||
Ok(cc) => cc,
|
||||
Err(CommitMessageError::FirstLineTooLong { actual, max }) => {
|
||||
@@ -276,8 +292,15 @@ mod tests {
|
||||
let description = Description::parse("test description").unwrap();
|
||||
let breaking_change = BreakingChange::No;
|
||||
let body = Body::default();
|
||||
let result =
|
||||
workflow.preview_and_confirm(commit_type, scope, description, breaking_change, body);
|
||||
let references = References::default();
|
||||
let result = workflow.preview_and_confirm(
|
||||
commit_type,
|
||||
scope,
|
||||
description,
|
||||
breaking_change,
|
||||
body,
|
||||
references,
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
@@ -322,6 +345,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("add new feature").unwrap())
|
||||
.with_breaking_change(BreakingChange::Yes)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -355,6 +379,7 @@ mod tests {
|
||||
.with_scope(Scope::parse("api").unwrap())
|
||||
.with_description(Description::parse("fix bug").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(false); // User cancels at confirmation
|
||||
|
||||
@@ -379,11 +404,13 @@ mod tests {
|
||||
.with_scope(Scope::parse("very-long-scope-name").unwrap())
|
||||
.with_description(Description::parse("a".repeat(45)).unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
// Second iteration: short enough to succeed
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("short description").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -478,6 +505,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("test").unwrap())
|
||||
.with_breaking_change(BreakingChange::Yes)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -501,6 +529,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("test").unwrap())
|
||||
.with_breaking_change(BreakingChange::Yes)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -519,6 +548,7 @@ mod tests {
|
||||
.with_scope(Scope::parse("api").unwrap())
|
||||
.with_description(Description::parse("test").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -566,6 +596,7 @@ mod tests {
|
||||
Description::parse("remove old API").unwrap(),
|
||||
BreakingChange::Yes,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "expected Ok, got: {:?}", result);
|
||||
@@ -593,6 +624,7 @@ mod tests {
|
||||
Description::parse("drop legacy API").unwrap(),
|
||||
breaking_change,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "expected Ok, got: {:?}", result);
|
||||
@@ -623,6 +655,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("remove old API").unwrap())
|
||||
.with_breaking_change(BreakingChange::Yes)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -665,6 +698,7 @@ mod tests {
|
||||
Description::parse("add feature").unwrap(),
|
||||
BreakingChange::No,
|
||||
Body::from("This explains the change."),
|
||||
References::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "expected Ok, got: {:?}", result);
|
||||
@@ -692,6 +726,7 @@ mod tests {
|
||||
Description::parse("drop legacy API").unwrap(),
|
||||
"removes legacy endpoint".into(),
|
||||
Body::from("The endpoint was deprecated in v2."),
|
||||
References::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "expected Ok, got: {:?}", result);
|
||||
@@ -718,6 +753,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("add feature").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::from("This explains the change."))
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -750,6 +786,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("fix crash").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -803,6 +840,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("add feature").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ fn test_from_poison_error() {
|
||||
/// 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 underlying = std::io::Error::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 { .. }));
|
||||
@@ -281,7 +281,7 @@ fn test_error_matching_all_variants() {
|
||||
}
|
||||
|
||||
// Verify all variants can be cloned
|
||||
let cloned: Vec<Error> = variants.iter().map(|e| e.clone()).collect();
|
||||
let cloned: Vec<Error> = variants.to_vec();
|
||||
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