Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 0354a9d

Browse files
authored
Merge pull request #731 from crai0/write-message-to-file
feat(commit): add --write-message-to-file option
2 parents 6656cb4 + f04a719 commit 0354a9d

File tree

8 files changed

+198
-1
lines changed

8 files changed

+198
-1
lines changed

‎commitizen/cli.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import argparse
22
import logging
33
import sys
4+
from pathlib import Path
45
from functools import partial
56
from types import TracebackType
67
from typing import List
@@ -62,10 +63,16 @@
6263
"action": "store_true",
6364
"help": "show output to stdout, no commit, no modified files",
6465
},
66+
{
67+
"name": "--write-message-to-file",
68+
"type": Path,
69+
"metavar": "FILE_PATH",
70+
"help": "write message to file before commiting (can be combined with --dry-run)",
71+
},
6572
{
6673
"name": ["-s", "--signoff"],
6774
"action": "store_true",
68-
"help": "Sign off the commit",
75+
"help": "sign off the commit",
6976
},
7077
],
7178
},

‎commitizen/commands/commit.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
NoAnswersError,
1515
NoCommitBackupError,
1616
NotAGitProjectError,
17+
NotAllowed,
1718
NothingToCommitError,
1819
)
1920
from commitizen.git import smart_open
@@ -63,10 +64,14 @@ def prompt_commit_questions(self) -> str:
6364

6465
def __call__(self):
6566
dry_run: bool = self.arguments.get("dry_run")
67+
write_message_to_file = self.arguments.get("write_message_to_file")
6668

6769
if git.is_staging_clean() and not dry_run:
6870
raise NothingToCommitError("No files added to staging!")
6971

72+
if write_message_to_file is not None and write_message_to_file.is_dir():
73+
raise NotAllowed(f"{write_message_to_file} is a directory")
74+
7075
retry: bool = self.arguments.get("retry")
7176

7277
if retry:
@@ -76,6 +81,10 @@ def __call__(self):
7681

7782
out.info(f"\n{m}\n")
7883

84+
if write_message_to_file:
85+
with smart_open(write_message_to_file, "w") as file:
86+
file.write(m)
87+
7988
if dry_run:
8089
raise DryRunExit()
8190

‎docs/commit.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ In your terminal run `cz commit` or the shortcut `cz c` to generate a guided git
66

77
A commit can be signed off using `cz commit --signoff` or the shortcut `cz commit -s`.
88

9+
You can run `cz commit --write-message-to-file COMMIT_MSG_FILE` to additionally save the
10+
generated message to a file. This can be combined with the `--dry-run` flag to only
11+
write the message to a file and not modify files and create a commit. A possible use
12+
case for this is to [automatically prepare a commit message](./tutorials/auto_prepare_commit_message.md).
13+
914
!!! note
1015
To maintain platform compatibility, the `commit` command disable ANSI escaping in its output.
1116
In particular pre-commit hooks coloring will be deactivated as discussed in [commitizen-tools/commitizen#417](https://github.com/commitizen-tools/commitizen/issues/417).
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Automatically prepare message before commit
2+
3+
## About
4+
5+
It can be desirable to use commitizen for all types of commits (i.e. regular, merge,
6+
squash) so that the complete git history adheres to the commit message convention
7+
without ever having to call `cz commit`.
8+
9+
To automatically prepare a commit message prior to committing, you can
10+
use a [prepare-commit-msg Git hook](prepare-commit-msg-docs):
11+
12+
> This hook is invoked by git-commit right after preparing the
13+
> default log message, and before the editor is started.
14+
15+
To automatically perform arbitrary cleanup steps after a succesful commit you can use a
16+
[post-commit Git hook][post-commit-docs]:
17+
18+
> This hook is invoked by git-commit. It takes no parameters, and is invoked after a
19+
> commit is made.
20+
21+
A combination of these two hooks allows for enforcing the usage of commitizen so that
22+
whenever a commit is about to be created, commitizen is used for creating the commit
23+
message. Running `git commit` or `git commit -m "..."` for example, would trigger
24+
commitizen and use the generated commit message for the commit.
25+
26+
## Installation
27+
28+
Copy the hooks from [here](https://github.com/commitizen-tools/hooks) into the `.git/hooks` folder and make them
29+
executable by running the following commands from the root of your Git repository:
30+
31+
```bash
32+
wget -o .git/hooks/prepare-commit-msg https://github.com/commitizen-tools/hooks/prepare-commit-msg.py
33+
chmod +x .git/hooks/prepare-commit-msg
34+
wget -o .git/hooks/post-commit https://github.com/commitizen-tools/hooks/post-commit.py
35+
chmod +x .git/hooks/post-commit
36+
```
37+
38+
## Features
39+
40+
- Commits can be created using both `cz commit` and the regular `git commit`
41+
- The hooks automatically create a backup of the commit message that can be reused if
42+
the commit failed
43+
- The commit message backup can also be used via `cz commit --retry`
44+
45+
[post-commit-docs]: https://git-scm.com/docs/githooks#_post_commit
46+
[prepare-commit-msg-docs]: https://git-scm.com/docs/githooks#_prepare_commit_msg

‎hooks/post-commit.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python
2+
import os
3+
import tempfile
4+
from pathlib import Path
5+
6+
7+
def post_commit():
8+
backup_file = Path(
9+
tempfile.gettempdir(), f"cz.commit{os.environ.get('USER', '')}.backup"
10+
)
11+
12+
# remove backup file if it exists
13+
if backup_file.is_file():
14+
backup_file.unlink()
15+
16+
17+
if __name__ == "__main__":
18+
exit(post_commit())

‎hooks/prepare-commit-msg.py‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python
2+
import os
3+
import shutil
4+
import subprocess
5+
import sys
6+
import tempfile
7+
from pathlib import Path
8+
from subprocess import CalledProcessError
9+
10+
11+
def prepare_commit_msg(commit_msg_file: Path) -> int:
12+
# check that commitizen is installed
13+
if shutil.which("cz") is None:
14+
print("commitizen is not installed!")
15+
return 0
16+
17+
# check if the commit message needs to be generated using commitizen
18+
if (
19+
subprocess.run(
20+
[
21+
"cz",
22+
"check",
23+
"--commit-msg-file",
24+
commit_msg_file,
25+
],
26+
capture_output=True,
27+
).returncode
28+
!= 0
29+
):
30+
backup_file = Path(
31+
tempfile.gettempdir(), f"cz.commit{os.environ.get('USER', '')}.backup"
32+
)
33+
34+
if backup_file.is_file():
35+
# confirm if commit message from backup file should be reused
36+
answer = input("retry with previous message? [y/N]: ")
37+
if answer.lower() == "y":
38+
shutil.copyfile(backup_file, commit_msg_file)
39+
return 0
40+
41+
# use commitizen to generate the commit message
42+
try:
43+
subprocess.run(
44+
[
45+
"cz",
46+
"commit",
47+
"--dry-run",
48+
"--write-message-to-file",
49+
commit_msg_file,
50+
],
51+
stdin=sys.stdin,
52+
stdout=sys.stdout,
53+
).check_returncode()
54+
except CalledProcessError as error:
55+
return error.returncode
56+
57+
# write message to backup file
58+
shutil.copyfile(commit_msg_file, backup_file)
59+
60+
61+
if __name__ == "__main__":
62+
# make hook interactive by attaching /dev/tty to stdin
63+
with open("/dev/tty") as tty:
64+
sys.stdin = tty
65+
exit(prepare_commit_msg(sys.argv[1]))

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ nav:
4343
- Tutorials:
4444
- Writing commits: "tutorials/writing_commits.md"
4545
- Auto check commits: "tutorials/auto_check.md"
46+
- Auto prepare commit message: "tutorials/auto_prepare_commit_message.md"
4647
- GitLab CI: "tutorials/gitlab_ci.md"
4748
- Github Actions: "tutorials/github_actions.md"
4849
- Jenkins pipeline: "tutorials/jenkins_pipeline.md"

‎tests/commands/test_commit_command.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
NoAnswersError,
1313
NoCommitBackupError,
1414
NotAGitProjectError,
15+
NotAllowed,
1516
NothingToCommitError,
1617
)
1718

@@ -109,6 +110,51 @@ def test_commit_command_with_dry_run_option(config, mocker: MockFixture):
109110
commit_cmd()
110111

111112

113+
@pytest.mark.usefixtures("staging_is_clean")
114+
def test_commit_command_with_write_message_to_file_option(
115+
config, tmp_path, mocker: MockFixture
116+
):
117+
tmp_file = tmp_path / "message"
118+
119+
prompt_mock = mocker.patch("questionary.prompt")
120+
prompt_mock.return_value = {
121+
"prefix": "feat",
122+
"subject": "user created",
123+
"scope": "",
124+
"is_breaking_change": False,
125+
"body": "",
126+
"footer": "",
127+
}
128+
129+
commit_mock = mocker.patch("commitizen.git.commit")
130+
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
131+
success_mock = mocker.patch("commitizen.out.success")
132+
133+
commands.Commit(config, {"write_message_to_file": tmp_file})()
134+
success_mock.assert_called_once()
135+
assert tmp_file.exists()
136+
assert tmp_file.read_text() == "feat: user created"
137+
138+
139+
@pytest.mark.usefixtures("staging_is_clean")
140+
def test_commit_command_with_invalid_write_message_to_file_option(
141+
config, tmp_path, mocker: MockFixture
142+
):
143+
prompt_mock = mocker.patch("questionary.prompt")
144+
prompt_mock.return_value = {
145+
"prefix": "feat",
146+
"subject": "user created",
147+
"scope": "",
148+
"is_breaking_change": False,
149+
"body": "",
150+
"footer": "",
151+
}
152+
153+
with pytest.raises(NotAllowed):
154+
commit_cmd = commands.Commit(config, {"write_message_to_file": tmp_path})
155+
commit_cmd()
156+
157+
112158
@pytest.mark.usefixtures("staging_is_clean")
113159
def test_commit_command_with_signoff_option(config, mocker: MockFixture):
114160
prompt_mock = mocker.patch("questionary.prompt")

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /