-
-
Notifications
You must be signed in to change notification settings - Fork 286
refactor(git): code cleanup and test coverage #1417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1cc5858
61c254f
40abbbd
20f4892
3962b80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,44 +2,52 @@ | |
|
||
import os | ||
from enum import Enum | ||
from os import linesep | ||
from functools import lru_cache | ||
from pathlib import Path | ||
from tempfile import NamedTemporaryFile | ||
|
||
from commitizen import cmd, out | ||
from commitizen.exceptions import GitCommandError | ||
|
||
UNIX_EOL = "\n" | ||
WINDOWS_EOL = "\r\n" | ||
|
||
|
||
class EOLTypes(Enum): | ||
class EOLType(Enum): | ||
"""The EOL type from `git config core.eol`.""" | ||
|
||
LF = "lf" | ||
CRLF = "crlf" | ||
NATIVE = "native" | ||
|
||
def get_eol_for_open(self) -> str: | ||
@classmethod | ||
def for_open(cls) -> str: | ||
c = cmd.run("git config core.eol") | ||
eol = c.out.strip().upper() | ||
return cls._char_for_open()[cls._safe_cast(eol)] | ||
|
||
@classmethod | ||
def _safe_cast(cls, eol: str) -> EOLType: | ||
try: | ||
return cls[eol] | ||
except KeyError: | ||
return cls.NATIVE | ||
|
||
@classmethod | ||
@lru_cache | ||
def _char_for_open(cls) -> dict[EOLType, str]: | ||
"""Get the EOL character for `open()`.""" | ||
map = { | ||
EOLTypes.CRLF: WINDOWS_EOL, | ||
EOLTypes.LF: UNIX_EOL, | ||
EOLTypes.NATIVE: linesep, | ||
return { | ||
cls.LF: "\n", | ||
cls.CRLF: "\r\n", | ||
cls.NATIVE: os.linesep, | ||
} | ||
|
||
return map[self] | ||
|
||
|
||
class GitObject: | ||
rev: str | ||
name: str | ||
date: str | ||
|
||
def __eq__(self, other) -> bool: | ||
if not hasattr(other, "rev"): | ||
return False | ||
return self.rev == other.rev # type: ignore | ||
return hasattr(other, "rev") and self.rev == other.rev | ||
|
||
|
||
class GitCommit(GitObject): | ||
|
@@ -63,6 +71,62 @@ def __init__( | |
def message(self): | ||
return f"{self.title}\n\n{self.body}".strip() | ||
|
||
@classmethod | ||
def from_rev_and_commit(cls, rev_and_commit: str) -> GitCommit: | ||
"""Create a GitCommit instance from a formatted commit string. | ||
|
||
This method parses a multi-line string containing commit information in the following format: | ||
``` | ||
<rev> | ||
<parents> | ||
<title> | ||
<author> | ||
<author_email> | ||
<body_line_1> | ||
<body_line_2> | ||
... | ||
``` | ||
|
||
Args: | ||
rev_and_commit (str): A string containing commit information with fields separated by newlines. | ||
- rev: The commit hash/revision | ||
- parents: Space-separated list of parent commit hashes | ||
- title: The commit title/message | ||
- author: The commit author's name | ||
- author_email: The commit author's email | ||
- body: Optional multi-line commit body | ||
|
||
Returns: | ||
GitCommit: A new GitCommit instance with the parsed information. | ||
|
||
Example: | ||
>>> commit_str = '''abc123 | ||
... def456 ghi789 | ||
... feat: add new feature | ||
... John Doe | ||
... john@example.com | ||
... This is a detailed description | ||
... of the new feature''' | ||
>>> commit = GitCommit.from_rev_and_commit(commit_str) | ||
>>> commit.rev | ||
'abc123' | ||
>>> commit.title | ||
'feat: add new feature' | ||
>>> commit.parents | ||
['def456', 'ghi789'] | ||
""" | ||
rev, parents, title, author, author_email, *body_list = rev_and_commit.split( | ||
"\n" | ||
) | ||
return cls( | ||
rev=rev.strip(), | ||
title=title.strip(), | ||
body="\n".join(body_list).strip(), | ||
author=author, | ||
author_email=author_email, | ||
parents=[p for p in parents.strip().split(" ") if p], | ||
) | ||
|
||
def __repr__(self): | ||
return f"{self.title} ({self.rev})" | ||
|
||
|
@@ -101,13 +165,11 @@ def tag( | |
# according to https://git-scm.com/book/en/v2/Git-Basics-Tagging, | ||
# we're not able to create lightweight tag with message. | ||
# by adding message, we make it a annotated tags | ||
c = cmd.run(f'git tag {_opt} "{tag if _opt == "" or msg is None else msg}"') | ||
return c | ||
return cmd.run(f'git tag {_opt} "{tag if _opt == "" or msg is None else msg}"') | ||
|
||
|
||
def add(*args: str) -> cmd.Command: | ||
c = cmd.run(f"git add {' '.join(args)}") | ||
return c | ||
return cmd.run(f"git add {' '.join(args)}") | ||
|
||
|
||
def commit( | ||
|
@@ -140,24 +202,10 @@ def get_commits( | |
) -> list[GitCommit]: | ||
"""Get the commits between start and end.""" | ||
git_log_entries = _get_log_as_str_list(start, end, args) | ||
git_commits = [] | ||
for rev_and_commit in git_log_entries: | ||
if not rev_and_commit: | ||
continue | ||
rev, parents, title, author, author_email, *body_list = rev_and_commit.split( | ||
"\n" | ||
) | ||
if rev_and_commit: | ||
git_commit = GitCommit( | ||
rev=rev.strip(), | ||
title=title.strip(), | ||
body="\n".join(body_list).strip(), | ||
author=author, | ||
author_email=author_email, | ||
parents=[p for p in parents.strip().split(" ") if p], | ||
) | ||
git_commits.append(git_commit) | ||
return git_commits | ||
return [ | ||
GitCommit.from_rev_and_commit(rev_and_commit) | ||
for rev_and_commit in filter(None, git_log_entries) | ||
] | ||
|
||
|
||
def get_filenames_in_commit(git_reference: str = ""): | ||
|
@@ -170,8 +218,7 @@ def get_filenames_in_commit(git_reference: str = ""): | |
c = cmd.run(f"git show --name-only --pretty=format: {git_reference}") | ||
if c.return_code == 0: | ||
return c.out.strip().split("\n") | ||
else: | ||
raise GitCommandError(c.err) | ||
raise GitCommandError(c.err) | ||
|
||
|
||
def get_tags( | ||
|
@@ -197,16 +244,11 @@ def get_tags( | |
if c.err: | ||
out.warn(f"Attempting to proceed after: {c.err}") | ||
|
||
if not c.out: | ||
return [] | ||
|
||
git_tags = [ | ||
return [ | ||
GitTag.from_line(line=line, inner_delimiter=inner_delimiter) | ||
for line in c.out.split("\n")[:-1] | ||
] | ||
|
||
return git_tags | ||
|
||
|
||
def tag_exist(tag: str) -> bool: | ||
c = cmd.run(f"git tag --list {tag}") | ||
|
@@ -231,18 +273,18 @@ def get_tag_message(tag: str) -> str | None: | |
return c.out.strip() | ||
|
||
|
||
def get_tag_names() -> list[str | None]: | ||
def get_tag_names() -> list[str]: | ||
c = cmd.run("git tag --list") | ||
if c.err: | ||
return [] | ||
return [tag.strip() for tag in c.out.split("\n") if tag.strip()] | ||
return [tag for raw in c.out.split("\n") if (tag := raw.strip())] | ||
|
||
|
||
def find_git_project_root() -> Path | None: | ||
c = cmd.run("git rev-parse --show-toplevel") | ||
if not c.err: | ||
return Path(c.out.strip()) | ||
return None | ||
if c.err: | ||
return None | ||
return Path(c.out.strip()) | ||
|
||
|
||
def is_staging_clean() -> bool: | ||
|
@@ -253,32 +295,7 @@ def is_staging_clean() -> bool: | |
|
||
def is_git_project() -> bool: | ||
c = cmd.run("git rev-parse --is-inside-work-tree") | ||
if c.out.strip() == "true": | ||
return True | ||
return False | ||
|
||
|
||
def get_eol_style() -> EOLTypes: | ||
c = cmd.run("git config core.eol") | ||
eol = c.out.strip().lower() | ||
|
||
# We enumerate the EOL types of the response of | ||
# `git config core.eol`, and map it to our enumration EOLTypes. | ||
# | ||
# It is just like the variant of the "match" syntax. | ||
map = { | ||
"lf": EOLTypes.LF, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why remove the enum? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just found it hard to trace because it first converts a string to a enum and then converts it back to a string. I thought skipping the enum part may make more sense. I added the enum back but reordered and rename the functions. Please review |
||
"crlf": EOLTypes.CRLF, | ||
"native": EOLTypes.NATIVE, | ||
} | ||
|
||
# If the response of `git config core.eol` is in the map: | ||
if eol in map: | ||
return map[eol] | ||
else: | ||
# The default value is "native". | ||
# https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreeol | ||
return map["native"] | ||
return c.out.strip() == "true" | ||
bearomorphism marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
def get_core_editor() -> str | None: | ||
|
@@ -288,22 +305,18 @@ def get_core_editor() -> str | None: | |
return None | ||
|
||
|
||
def smart_open(*args, **kargs): | ||
def smart_open(*args, **kwargs): | ||
"""Open a file with the EOL style determined from Git.""" | ||
return open(*args, newline=get_eol_style().get_eol_for_open(), **kargs) | ||
return open(*args, newline=EOLType.for_open(), **kwargs) | ||
|
||
|
||
def _get_log_as_str_list(start: str | None, end: str, args: str) -> list[str]: | ||
"""Get string representation of each log entry""" | ||
delimiter = "----------commit-delimiter----------" | ||
log_format: str = "%H%n%P%n%s%n%an%n%ae%n%b" | ||
git_log_cmd = ( | ||
f"git -c log.showSignature=False log --pretty={log_format}{delimiter} {args}" | ||
) | ||
if start: | ||
command = f"{git_log_cmd} {start}..{end}" | ||
else: | ||
command = f"{git_log_cmd} {end}" | ||
command_range = f"{start}..{end}" if start else end | ||
command = f"git -c log.showSignature=False log --pretty={log_format}{delimiter} {args} {command_range}" | ||
|
||
c = cmd.run(command) | ||
if c.return_code != 0: | ||
raise GitCommandError(c.err) | ||
|
Uh oh!
There was an error while loading. Please reload this page.