Skip to content

Commit 1cc5858

Browse files
committed
refactor(git): code cleanup and better test coverage
1 parent a0cc490 commit 1cc5858

File tree

2 files changed

+61
-104
lines changed

2 files changed

+61
-104
lines changed

commitizen/git.py

+43-90
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,14 @@
11
from __future__ import annotations
22

33
import os
4-
from enum import Enum
5-
from os import linesep
64
from pathlib import Path
75
from tempfile import NamedTemporaryFile
86

97
from commitizen import cmd, out
108
from commitizen.exceptions import GitCommandError
119

12-
UNIX_EOL = "\n"
13-
WINDOWS_EOL = "\r\n"
14-
15-
16-
class EOLTypes(Enum):
17-
"""The EOL type from `git config core.eol`."""
18-
19-
LF = "lf"
20-
CRLF = "crlf"
21-
NATIVE = "native"
22-
23-
def get_eol_for_open(self) -> str:
24-
"""Get the EOL character for `open()`."""
25-
map = {
26-
EOLTypes.CRLF: WINDOWS_EOL,
27-
EOLTypes.LF: UNIX_EOL,
28-
EOLTypes.NATIVE: linesep,
29-
}
30-
31-
return map[self]
10+
_UNIX_EOL = "\n"
11+
_WINDOWS_EOL = "\r\n"
3212

3313

3414
class GitObject:
@@ -37,9 +17,7 @@ class GitObject:
3717
date: str
3818

3919
def __eq__(self, other) -> bool:
40-
if not hasattr(other, "rev"):
41-
return False
42-
return self.rev == other.rev # type: ignore
20+
return hasattr(other, "rev") and self.rev == other.rev
4321

4422

4523
class GitCommit(GitObject):
@@ -63,6 +41,20 @@ def __init__(
6341
def message(self):
6442
return f"{self.title}\n\n{self.body}".strip()
6543

44+
@classmethod
45+
def from_rev_and_commit(cls, rev_and_commit: str) -> GitCommit:
46+
rev, parents, title, author, author_email, *body_list = rev_and_commit.split(
47+
"\n"
48+
)
49+
return cls(
50+
rev=rev.strip(),
51+
title=title.strip(),
52+
body="\n".join(body_list).strip(),
53+
author=author,
54+
author_email=author_email,
55+
parents=[p for p in parents.strip().split(" ") if p],
56+
)
57+
6658
def __repr__(self):
6759
return f"{self.title} ({self.rev})"
6860

@@ -101,13 +93,11 @@ def tag(
10193
# according to https://git-scm.com/book/en/v2/Git-Basics-Tagging,
10294
# we're not able to create lightweight tag with message.
10395
# by adding message, we make it a annotated tags
104-
c = cmd.run(f'git tag {_opt} "{tag if _opt == "" or msg is None else msg}"')
105-
return c
96+
return cmd.run(f'git tag {_opt} "{tag if _opt == "" or msg is None else msg}"')
10697

10798

10899
def add(*args: str) -> cmd.Command:
109-
c = cmd.run(f"git add {' '.join(args)}")
110-
return c
100+
return cmd.run(f"git add {' '.join(args)}")
111101

112102

113103
def commit(
@@ -140,24 +130,10 @@ def get_commits(
140130
) -> list[GitCommit]:
141131
"""Get the commits between start and end."""
142132
git_log_entries = _get_log_as_str_list(start, end, args)
143-
git_commits = []
144-
for rev_and_commit in git_log_entries:
145-
if not rev_and_commit:
146-
continue
147-
rev, parents, title, author, author_email, *body_list = rev_and_commit.split(
148-
"\n"
149-
)
150-
if rev_and_commit:
151-
git_commit = GitCommit(
152-
rev=rev.strip(),
153-
title=title.strip(),
154-
body="\n".join(body_list).strip(),
155-
author=author,
156-
author_email=author_email,
157-
parents=[p for p in parents.strip().split(" ") if p],
158-
)
159-
git_commits.append(git_commit)
160-
return git_commits
133+
return [
134+
GitCommit.from_rev_and_commit(rev_and_commit)
135+
for rev_and_commit in filter(None, git_log_entries)
136+
]
161137

162138

163139
def get_filenames_in_commit(git_reference: str = ""):
@@ -170,8 +146,7 @@ def get_filenames_in_commit(git_reference: str = ""):
170146
c = cmd.run(f"git show --name-only --pretty=format: {git_reference}")
171147
if c.return_code == 0:
172148
return c.out.strip().split("\n")
173-
else:
174-
raise GitCommandError(c.err)
149+
raise GitCommandError(c.err)
175150

176151

177152
def get_tags(
@@ -197,16 +172,11 @@ def get_tags(
197172
if c.err:
198173
out.warn(f"Attempting to proceed after: {c.err}")
199174

200-
if not c.out:
201-
return []
202-
203-
git_tags = [
175+
return [
204176
GitTag.from_line(line=line, inner_delimiter=inner_delimiter)
205177
for line in c.out.split("\n")[:-1]
206178
]
207179

208-
return git_tags
209-
210180

211181
def tag_exist(tag: str) -> bool:
212182
c = cmd.run(f"git tag --list {tag}")
@@ -231,18 +201,18 @@ def get_tag_message(tag: str) -> str | None:
231201
return c.out.strip()
232202

233203

234-
def get_tag_names() -> list[str | None]:
204+
def get_tag_names() -> list[str]:
235205
c = cmd.run("git tag --list")
236206
if c.err:
237207
return []
238-
return [tag.strip() for tag in c.out.split("\n") if tag.strip()]
208+
return list(filter(None, (tag.strip() for tag in c.out.split("\n"))))
239209

240210

241211
def find_git_project_root() -> Path | None:
242212
c = cmd.run("git rev-parse --show-toplevel")
243-
if not c.err:
244-
return Path(c.out.strip())
245-
return None
213+
if c.err:
214+
return None
215+
return Path(c.out.strip())
246216

247217

248218
def is_staging_clean() -> bool:
@@ -253,32 +223,19 @@ def is_staging_clean() -> bool:
253223

254224
def is_git_project() -> bool:
255225
c = cmd.run("git rev-parse --is-inside-work-tree")
256-
if c.out.strip() == "true":
257-
return True
258-
return False
226+
return c.out.strip() == "true"
259227

260228

261-
def get_eol_style() -> EOLTypes:
229+
def get_eol_for_open() -> str:
230+
# See: https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreeol
262231
c = cmd.run("git config core.eol")
263232
eol = c.out.strip().lower()
264233

265-
# We enumerate the EOL types of the response of
266-
# `git config core.eol`, and map it to our enumration EOLTypes.
267-
#
268-
# It is just like the variant of the "match" syntax.
269-
map = {
270-
"lf": EOLTypes.LF,
271-
"crlf": EOLTypes.CRLF,
272-
"native": EOLTypes.NATIVE,
273-
}
274-
275-
# If the response of `git config core.eol` is in the map:
276-
if eol in map:
277-
return map[eol]
278-
else:
279-
# The default value is "native".
280-
# https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreeol
281-
return map["native"]
234+
if eol == "lf":
235+
return _UNIX_EOL
236+
if eol == "crlf":
237+
return _WINDOWS_EOL
238+
return os.linesep
282239

283240

284241
def get_core_editor() -> str | None:
@@ -288,22 +245,18 @@ def get_core_editor() -> str | None:
288245
return None
289246

290247

291-
def smart_open(*args, **kargs):
248+
def smart_open(*args, **kwargs):
292249
"""Open a file with the EOL style determined from Git."""
293-
return open(*args, newline=get_eol_style().get_eol_for_open(), **kargs)
250+
return open(*args, newline=get_eol_for_open(), **kwargs)
294251

295252

296253
def _get_log_as_str_list(start: str | None, end: str, args: str) -> list[str]:
297254
"""Get string representation of each log entry"""
298255
delimiter = "----------commit-delimiter----------"
299256
log_format: str = "%H%n%P%n%s%n%an%n%ae%n%b"
300-
git_log_cmd = (
301-
f"git -c log.showSignature=False log --pretty={log_format}{delimiter} {args}"
302-
)
303-
if start:
304-
command = f"{git_log_cmd} {start}..{end}"
305-
else:
306-
command = f"{git_log_cmd} {end}"
257+
command_range = f"{start}..{end}" if start else end
258+
command = f"git -c log.showSignature=False log --pretty={log_format}{delimiter} {args} {command_range}"
259+
307260
c = cmd.run(command)
308261
if c.return_code != 0:
309262
raise GitCommandError(c.err)

tests/test_git.py

+18-14
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ def test_get_reachable_tags_with_commits(
7979
monkeypatch.setenv("LANGUAGE", f"{locale}.UTF-8")
8080
monkeypatch.setenv("LC_ALL", f"{locale}.UTF-8")
8181
with tmp_commitizen_project.as_cwd():
82-
tags = git.get_tags(reachable_only=True)
83-
assert tags == []
82+
assert git.get_tags(reachable_only=True) == []
8483

8584

8685
def test_get_tag_names(mocker: MockFixture):
@@ -271,7 +270,7 @@ def test_get_commits_with_signature():
271270
def test_get_tag_names_has_correct_arrow_annotation():
272271
arrow_annotation = inspect.getfullargspec(git.get_tag_names).annotations["return"]
273272

274-
assert arrow_annotation == "list[str | None]"
273+
assert arrow_annotation == "list[str]"
275274

276275

277276
def test_get_latest_tag_name(tmp_commitizen_project):
@@ -317,24 +316,18 @@ def test_is_staging_clean_when_updating_file(tmp_commitizen_project):
317316
assert git.is_staging_clean() is False
318317

319318

320-
def test_git_eol_style(tmp_commitizen_project):
319+
def test_get_eol_for_open(tmp_commitizen_project):
321320
with tmp_commitizen_project.as_cwd():
322-
assert git.get_eol_style() == git.EOLTypes.NATIVE
321+
assert git.get_eol_for_open() == os.linesep
323322

324323
cmd.run("git config core.eol lf")
325-
assert git.get_eol_style() == git.EOLTypes.LF
324+
assert git.get_eol_for_open() == "\n"
326325

327326
cmd.run("git config core.eol crlf")
328-
assert git.get_eol_style() == git.EOLTypes.CRLF
327+
assert git.get_eol_for_open() == "\r\n"
329328

330329
cmd.run("git config core.eol native")
331-
assert git.get_eol_style() == git.EOLTypes.NATIVE
332-
333-
334-
def test_eoltypes_get_eol_for_open():
335-
assert git.EOLTypes.get_eol_for_open(git.EOLTypes.NATIVE) == os.linesep
336-
assert git.EOLTypes.get_eol_for_open(git.EOLTypes.LF) == "\n"
337-
assert git.EOLTypes.get_eol_for_open(git.EOLTypes.CRLF) == "\r\n"
330+
assert git.get_eol_for_open() == os.linesep
338331

339332

340333
def test_get_core_editor(mocker):
@@ -401,3 +394,14 @@ def test_commit_with_spaces_in_path(mocker, file_path, expected_cmd):
401394

402395
mock_run.assert_called_once_with(expected_cmd)
403396
mock_unlink.assert_called_once_with(file_path)
397+
398+
399+
def test_get_filenames_in_commit_error(mocker: MockFixture):
400+
"""Test that GitCommandError is raised when git command fails."""
401+
mocker.patch(
402+
"commitizen.cmd.run",
403+
return_value=FakeCommand(out="", err="fatal: bad object HEAD", return_code=1),
404+
)
405+
with pytest.raises(exceptions.GitCommandError) as excinfo:
406+
git.get_filenames_in_commit()
407+
assert str(excinfo.value) == "fatal: bad object HEAD"

0 commit comments

Comments
 (0)