Skip to content

Commit efc37b4

Browse files
add gitinfo class (#106)
* add gitinfo class * add check for detached head * add detached head unit test * run black * Add comment to walrus operator
1 parent 1f3e980 commit efc37b4

File tree

4 files changed

+130
-43
lines changed

4 files changed

+130
-43
lines changed

bdiff/__init__.py

Whitespace-only changes.

bdiff/git_bdiff.py

Lines changed: 85 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -37,34 +37,93 @@ def __init__(self, cmd):
3737
)
3838

3939

40-
class GitBDiff:
41-
"""Class which generates a branch diff."""
42-
43-
# Name of primary branch - default is main
44-
primary_branch = "main"
45-
46-
# Match hex commit IDs
47-
_hash_pattern = re.compile(r"^\s*([0-9a-f]{40})\s*$")
40+
class GitBase:
41+
"""
42+
Base class for gitbdiff functionality
43+
"""
4844

4945
# Match branch names. This should catch all valid names but may
5046
# also some invalid names through. This should matter given that
5147
# it is being used to match git command output. For a complete
5248
# overview of the naming scheme, see man git check-ref-format
5349
_branch_pattern = re.compile(r"^\s*([^\s~\^\:\?\*\[]+[^.])\s*$")
5450

55-
def __init__(self, parent=None, repo=None):
56-
self.parent = parent or self.primary_branch
51+
# Text returned if in a detached head
52+
detached_head_reference = "detched_head_state"
5753

54+
def __init__(self, parent=None, repo=None):
5855
if repo is None:
5956
self._repo = None
6057
else:
6158
self._repo = Path(repo)
6259
if not self._repo.is_dir():
6360
raise GitBDiffError(f"{repo} is not a directory")
6461

62+
def get_branch_name(self):
63+
"""Get the name of the current branch."""
64+
result = None
65+
for line in self.run_git(["branch", "--show-current"]):
66+
# Set m to self._branch_pattern result
67+
# Then check m evaluates to True
68+
if m := self._branch_pattern.match(line):
69+
result = m.group(1)
70+
break
71+
else:
72+
# Check for being in a Detached Head state
73+
for line in self.run_git(["branch"]):
74+
if "HEAD detached" in line:
75+
result = self.detached_head_reference
76+
break
77+
else:
78+
raise GitBDiffError("unable to get branch name")
79+
return result
80+
81+
def run_git(self, args):
82+
"""Run a git command and yield the output."""
83+
84+
if not isinstance(args, list):
85+
raise TypeError("args must be a list")
86+
cmd = ["git"] + args
87+
88+
# Run the the command in the repo directory, capture the
89+
# output, and check for errors. The build in error check is
90+
# not used to allow specific git errors to be treated more
91+
# precisely
92+
proc = subprocess.run(
93+
cmd, capture_output=True, check=False, shell=False, cwd=self._repo
94+
)
95+
96+
for line in proc.stderr.decode("utf-8").split("\n"):
97+
if line.startswith("fatal: not a git repository"):
98+
raise GitBDiffNotGit(cmd)
99+
if line.startswith("fatal: "):
100+
raise GitBDiffError(line[7:])
101+
102+
if proc.returncode != 0:
103+
raise GitBDiffError(f"command returned {proc.returncode}")
104+
105+
yield from proc.stdout.decode("utf-8").split("\n")
106+
107+
108+
class GitBDiff(GitBase):
109+
"""Class which generates a branch diff."""
110+
111+
# Name of primary branch - default is main
112+
primary_branch = "main"
113+
114+
# Match hex commit IDs
115+
_hash_pattern = re.compile(r"^\s*([0-9a-f]{40})\s*$")
116+
117+
def __init__(self, parent=None, repo=None):
118+
self.parent = parent or self.primary_branch
119+
120+
super().__init__(parent, repo)
121+
65122
self.ancestor = self.get_branch_point()
66123
self.current = self.get_latest_commit()
67124
self.branch = self.get_branch_name()
125+
if self.branch == self.detached_head_reference:
126+
raise GitBDiffError("Can't get a diff for a repo in detached head state")
68127

69128
def get_branch_point(self):
70129
"""Get the branch point from the parent repo.
@@ -96,17 +155,6 @@ def get_latest_commit(self):
96155
raise GitBDiffError("current revision not found")
97156
return result
98157

99-
def get_branch_name(self):
100-
"""Get the name of the current branch."""
101-
result = None
102-
for line in self.run_git(["branch", "--show-current"]):
103-
if m := self._branch_pattern.match(line):
104-
result = m.group(1)
105-
break
106-
else:
107-
raise GitBDiffError("unable to get branch name")
108-
return result
109-
110158
@property
111159
def is_branch(self):
112160
"""Whether this is a branch or main."""
@@ -126,28 +174,24 @@ def files(self):
126174
if line != "":
127175
yield line
128176

129-
def run_git(self, args):
130-
"""Run a git command and yield the output."""
131177

132-
if not isinstance(args, list):
133-
raise TypeError("args must be a list")
134-
cmd = ["git"] + args
178+
class GitInfo(GitBase):
179+
"""
180+
Class to contain info of a git repo
181+
"""
135182

136-
# Run the the command in the repo directory, capture the
137-
# output, and check for errors. The build in error check is
138-
# not used to allow specific git errors to be treated more
139-
# precisely
140-
proc = subprocess.run(
141-
cmd, capture_output=True, check=False, shell=False, cwd=self._repo
142-
)
183+
def __init__(self, repo=None):
184+
super().__init__(repo=repo)
143185

144-
for line in proc.stderr.decode("utf-8").split("\n"):
145-
if line.startswith("fatal: not a git repository"):
146-
raise GitBDiffNotGit(cmd)
147-
if line.startswith("fatal: "):
148-
raise GitBDiffError(line[7:])
186+
self.branch = self.get_branch_name()
149187

150-
if proc.returncode != 0:
151-
raise GitBDiffError(f"command returned {proc.returncode}")
188+
def is_main(self):
189+
"""
190+
Returns true if branch matches a main-like branch name as defined below
191+
Count detached head as main-like as we cannot get a diff for this
192+
"""
152193

153-
yield from proc.stdout.decode("utf-8").split("\n")
194+
main_like = ("main", "stable", "trunk", "master", self.detached_head_reference)
195+
if self.branch in main_like:
196+
return True
197+
return False

bdiff/tests/__init__.py

Whitespace-only changes.

bdiff/tests/test_git_bdiff.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import subprocess
1313
import pytest
1414

15-
from git_bdiff import GitBDiff, GitBDiffError, GitBDiffNotGit
15+
from ..git_bdiff import GitBDiff, GitBDiffError, GitBDiffNotGit, GitInfo, GitBase
1616

1717

1818
# Disable warnings caused by the use of pytest fixtures
@@ -58,9 +58,14 @@ def git_repo(tmpdir_factory):
5858
subprocess.run(["git", "checkout", "-b", "overwrite"], check=True)
5959
add_to_repo(0, 10, "Overwriting", "at")
6060

61-
# Switch back to the main branch ready for testing
61+
# Switch back to the main branch
6262
subprocess.run(["git", "checkout", "main"], check=True)
6363

64+
# Add other trunk-like branches, finishing back in main
65+
for branch in ("stable", "master", "trunk"):
66+
subprocess.run(["git", "checkout", "-b", branch], check=True)
67+
subprocess.run(["git", "checkout", "main"], check=True)
68+
6469
return location
6570

6671

@@ -214,3 +219,41 @@ def test_git_run(git_repo):
214219
# Run a command that should return non-zero
215220
list(i for i in bdiff.run_git(["commit", "-m", "''"]))
216221
assert "command returned 1" in str(exc.value)
222+
223+
224+
def test_is_main(git_repo):
225+
"""Test is_trunk function"""
226+
227+
os.chdir(git_repo)
228+
229+
for branch in ("stable", "master", "trunk", "main", "mybranch"):
230+
info = GitInfo()
231+
subprocess.run(["git", "checkout", branch], check=True)
232+
if branch == "my_branch":
233+
assert not info.is_main()
234+
else:
235+
assert info.is_main()
236+
237+
238+
def find_previous_hash():
239+
"""
240+
Loop over a git log output and extract a hash that isn't the current head
241+
"""
242+
243+
result = subprocess.run(["git", "log"], check=True, capture_output=True, text=True)
244+
for line in result.stdout.split("\n"):
245+
if line.startswith("commit") and "HEAD" not in line:
246+
return line.split()[1]
247+
248+
249+
def test_detached_head(git_repo):
250+
"""Test Detached Head State"""
251+
252+
os.chdir(git_repo)
253+
subprocess.run(["git", "checkout", "main"], check=True)
254+
255+
commit_hash = find_previous_hash()
256+
subprocess.run(["git", "checkout", commit_hash], check=True)
257+
258+
git_base = GitBase()
259+
assert git_base.get_branch_name() == git_base.detached_head_reference

0 commit comments

Comments
 (0)