Skip to content

Commit c00f5dc

Browse files
committed
feat(config): add the commit_encoding setting for commit message files
`commit_encoding` sets the encoding of the files that hold a commit message: the `cz commit --edit` file, the `--write-message-to-file` file, the `--retry` backup file and the `cz check --commit-msg-file` file. The default is `None`, which means `encoding`. The last three files already use `encoding`, so they do not change by default. Assisted By: AI
1 parent 3457ec3 commit c00f5dc

8 files changed

Lines changed: 136 additions & 11 deletions

File tree

‎commitizen/commands/check.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ def _get_commit_message(self) -> str | None:
124124

125125
# Get commit message from file (--commit-msg-file)
126126
return Path(self.commit_msg_file).read_text(
127-
encoding=self.config.settings["encoding"]
127+
encoding=self.config.settings["commit_encoding"]
128+
or self.config.settings["encoding"]
128129
)
129130

130131
def _get_commits(self) -> list[git.GitCommit]:

‎commitizen/commands/commit.py‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,23 @@ def __init__(self, config: BaseConfig, arguments: CommitArgs) -> None:
5757
self.arguments = arguments
5858
self.backup_file_path = get_backup_file_path()
5959

60+
def _get_commit_encoding(self) -> str:
61+
"""Return the encoding of the files that hold the commit message.
62+
63+
`commit_encoding` falls back to `encoding` when it is not set.
64+
"""
65+
return (
66+
self.config.settings["commit_encoding"] or self.config.settings["encoding"]
67+
)
68+
6069
def _read_backup_message(self) -> str | None:
6170
# Check the commit backup file exists
6271
if not self.backup_file_path.is_file():
6372
return None
6473

6574
# Read commit message from backup
6675
return self.backup_file_path.read_text(
67-
encoding=self.config.settings["encoding"]
76+
encoding=self._get_commit_encoding()
6877
).strip()
6978

7079
def _get_message_by_prompt_commit_questions(self) -> str:
@@ -130,13 +139,22 @@ def _wrap_body(self, message: str) -> str:
130139
return "\n".join(chain(lines[:2], wrapped_body_lines))
131140

132141
def manual_edit(self, message: str) -> str:
142+
"""Open the message in the git editor and return the edited message.
143+
144+
The temporary file uses `commit_encoding` (or `encoding`) instead of the
145+
locale encoding. On Windows, the locale encoding is a legacy code page
146+
that cannot encode all characters.
147+
148+
Raises:
149+
RuntimeError: If no editor is set or the editor is not found.
150+
"""
133151
editor = git.get_core_editor()
134152
if editor is None:
135153
raise RuntimeError("No 'editor' value given and no default available.")
136154
exec_path = shutil.which(editor)
137155
if exec_path is None:
138156
raise RuntimeError(f"Editor '{editor}' not found.")
139-
encoding = self.config.settings["encoding"]
157+
encoding = self._get_commit_encoding()
140158
with tempfile.NamedTemporaryFile(
141159
mode="w", delete=False, encoding=encoding
142160
) as file:
@@ -191,7 +209,7 @@ def __call__(self) -> None:
191209

192210
if write_message_to_file:
193211
with smart_open(
194-
write_message_to_file, "w", encoding=self.config.settings["encoding"]
212+
write_message_to_file, "w", encoding=self._get_commit_encoding()
195213
) as file:
196214
file.write(commit_message)
197215

@@ -207,7 +225,7 @@ def __call__(self) -> None:
207225

208226
# Create commit backup
209227
with smart_open(
210-
self.backup_file_path, "w", encoding=self.config.settings["encoding"]
228+
self.backup_file_path, "w", encoding=self._get_commit_encoding()
211229
) as f:
212230
f.write(commit_message)
213231

‎commitizen/defaults.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Settings(TypedDict, total=False):
4141
changelog_incremental: bool
4242
changelog_merge_prerelease: bool
4343
changelog_start_rev: str | None
44+
commit_encoding: str | None
4445
customize: CzSettings
4546
encoding: str
4647
extras: dict[str, Any]
@@ -111,6 +112,7 @@ class Settings(TypedDict, total=False):
111112
"post_bump_hooks": [],
112113
"prerelease_offset": 0,
113114
"encoding": ENCODING,
115+
"commit_encoding": None, # default to encoding
114116
"always_signoff": False,
115117
"template": None, # default provided by plugin
116118
"extras": {},

‎docs/config/commit.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ When true, breaking changes will be also indicated by an exclamation mark in the
1111

1212
When false, breaking changes will be only indicated by `BREAKING CHANGE:` in the footer. See [writing commits](../tutorials/writing_commits.md) for more details.
1313

14+
## `commit_encoding`
15+
16+
- Type: `str`
17+
- Default: `None`
18+
19+
Sets the character encoding of the files that hold a commit message:
20+
21+
- the file that `cz commit --edit` opens in the editor
22+
- the file that `cz commit --write-message-to-file` writes
23+
- the backup file that `cz commit --retry` reads
24+
- the file that `cz check --commit-msg-file` reads
25+
26+
If it is not set, Commitizen uses [`encoding`](#encoding). Set it when your editor saves commit messages in a different encoding from your other files.
27+
1428
## `encoding`
1529

1630
- Type: `str`

‎docs/tutorials/writing_commits.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ Commitizen supports Unicode characters (including emojis) in commit messages. Th
147147
148148
By default, Commitizen uses `utf-8` encoding. You can configure a different encoding through the `encoding` [configuration option][configuration].
149149
150+
Files that hold a commit message, such as the file that `cz commit --edit` opens in your editor, use the [`commit_encoding`][commit-encoding] option. If it is not set, they use `encoding`.
151+
150152
## Related Documentation
151153
152154
- [Conventional Commits Specification][conventional_commits]
@@ -157,4 +159,5 @@ By default, Commitizen uses `utf-8` encoding. You can configure a different enco
157159
[conventional_commits]: https://www.conventionalcommits.org
158160
[cz_emoji]: ../third-party-plugins/cz-emoji.md
159161
[configuration]: ../config/commit.md#encoding
162+
[commit-encoding]: ../config/commit.md#commit_encoding
160163
[breaking-change-config]: ../config/commit.md#breaking_change_exclamation_in_title

‎tests/commands/test_check_command.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,22 @@ def test_check_conventional_commit(
144144
success_mock.assert_called_once()
145145

146146

147+
def test_check_commit_msg_file_uses_commit_encoding(
148+
config, success_mock: MockType, tmp_path
149+
):
150+
"""`--commit-msg-file` reads the file with `commit_encoding`."""
151+
# Arrange
152+
config.settings["commit_encoding"] = "utf-16"
153+
commit_msg_file = tmp_path / "temp_commit_file"
154+
commit_msg_file.write_text("feat: add 안녕하세요 ✨", encoding="utf-16")
155+
156+
# Act
157+
commands.Check(config=config, arguments={"commit_msg_file": commit_msg_file})()
158+
159+
# Assert
160+
success_mock.assert_called_once()
161+
162+
147163
def test_check_command_when_commit_file_not_found(config):
148164
with pytest.raises(FileNotFoundError):
149165
commands.Check(config=config, arguments={"commit_msg_file": "no_such_file"})()

‎tests/commands/test_commit_command.py‎

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,43 @@ def test_commit_backup_on_failure(
8383
assert Path(temp_file).exists()
8484

8585

86+
@pytest.mark.usefixtures("staging_is_clean", "prompt_mock_feat")
87+
def test_commit_backup_uses_commit_encoding(config, mocker: MockFixture):
88+
"""A failed commit writes the backup file with `commit_encoding`."""
89+
# Arrange
90+
config.settings["commit_encoding"] = "utf-16"
91+
mocker.patch(
92+
"commitizen.git.commit", return_value=cmd.Command("", "error", b"", b"", 9)
93+
)
94+
mocker.patch("commitizen.out.error")
95+
commit_cmd = commands.Commit(config, {})
96+
97+
# Act
98+
with pytest.raises(CommitError):
99+
commit_cmd()
100+
101+
# Assert
102+
assert (
103+
commit_cmd.backup_file_path.read_text(encoding="utf-16")
104+
== "feat: user created\n\ncloses #21"
105+
)
106+
107+
108+
@pytest.mark.usefixtures("staging_is_clean")
109+
def test_commit_retry_reads_backup_with_commit_encoding(config, commit_mock: MockType):
110+
"""`cz commit --retry` reads the backup file with `commit_encoding`."""
111+
# Arrange
112+
config.settings["commit_encoding"] = "utf-16"
113+
commit_cmd = commands.Commit(config, {"retry": True})
114+
commit_cmd.backup_file_path.write_text("feat: add 안녕하세요 ✨", encoding="utf-16")
115+
116+
# Act
117+
commit_cmd()
118+
119+
# Assert
120+
commit_mock.assert_called_with("feat: add 안녕하세요 ✨", args=[])
121+
122+
86123
@pytest.mark.usefixtures("staging_is_clean", "commit_mock")
87124
def test_commit_retry_fails_no_backup(config):
88125
with pytest.raises(
@@ -168,6 +205,22 @@ def test_commit_command_with_write_message_to_file_option(
168205
assert "feat: user created" in tmp_file.read_text()
169206

170207

208+
@pytest.mark.usefixtures("staging_is_clean", "commit_mock", "prompt_mock_feat")
209+
def test_commit_command_with_write_message_to_file_uses_commit_encoding(
210+
config, tmp_path
211+
):
212+
"""`--write-message-to-file` writes the message with `commit_encoding`."""
213+
# Arrange
214+
config.settings["commit_encoding"] = "utf-16"
215+
tmp_file = tmp_path / "message"
216+
217+
# Act
218+
commands.Commit(config, {"write_message_to_file": tmp_file})()
219+
220+
# Assert
221+
assert tmp_file.read_text(encoding="utf-16") == "feat: user created\n\ncloses #21"
222+
223+
171224
@pytest.mark.usefixtures("staging_is_clean", "prompt_mock_feat")
172225
def test_commit_command_with_invalid_write_message_to_file_option(config, tmp_path):
173226
with pytest.raises(NotAllowed):
@@ -321,22 +374,38 @@ def test_manual_edit(editor, config, mocker: MockFixture, tmp_path):
321374

322375

323376
@pytest.mark.usefixtures("staging_is_clean")
324-
def test_manual_edit_keeps_non_ascii_message(config, mocker: MockFixture):
325-
"""The editor file uses the configured encoding, not the locale encoding.
377+
@pytest.mark.parametrize(
378+
("encoding", "commit_encoding", "file_encoding"),
379+
[
380+
("utf-8", None, "utf-8"),
381+
("utf-16", None, "utf-16"),
382+
("utf-8", "utf-16", "utf-16"),
383+
],
384+
)
385+
def test_manual_edit_keeps_non_ascii_message(
386+
encoding: str,
387+
commit_encoding: str | None,
388+
file_encoding: str,
389+
config,
390+
mocker: MockFixture,
391+
):
392+
"""The editor file uses `commit_encoding`, or `encoding` if it is not set.
326393
327-
On Windows the locale encoding is a legacy code page (e.g. cp1252), which
328-
cannot encode this message.
394+
The locale encoding is not used. On Windows, it is a legacy code page
395+
(e.g. cp1252), which cannot encode this message.
329396
"""
330397
# Arrange
398+
config.settings["encoding"] = encoding
399+
config.settings["commit_encoding"] = commit_encoding
331400
message = "feat: add 안녕하세요 greeting ✨"
332401
edited_message = "feat: add 안녕하세요 greeting ✨ (edited)"
333402
mocker.patch("commitizen.git.get_core_editor", return_value="vim")
334403
mocker.patch("shutil.which", return_value="vim")
335404

336405
def edit_in_editor(argv: list[str]) -> int:
337406
message_file = Path(argv[1])
338-
assert message_file.read_text(encoding="utf-8") == message
339-
message_file.write_text(edited_message, encoding="utf-8")
407+
assert message_file.read_text(encoding=file_encoding) == message
408+
message_file.write_text(edited_message, encoding=file_encoding)
340409
return 0
341410

342411
mocker.patch("subprocess.call", side_effect=edit_in_editor)

‎tests/test_conf.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"post_bump_hooks": ["scripts/slack_notification.sh"],
109109
"prerelease_offset": 0,
110110
"encoding": "utf-8",
111+
"commit_encoding": None,
111112
"always_signoff": False,
112113
"template": None,
113114
"extras": {},
@@ -149,6 +150,7 @@
149150
"post_bump_hooks": ["scripts/slack_notification.sh"],
150151
"prerelease_offset": 0,
151152
"encoding": "utf-8",
153+
"commit_encoding": None,
152154
"always_signoff": False,
153155
"template": None,
154156
"extras": {},

0 commit comments

Comments
 (0)