Skip to content

Commit 4b9456a

Browse files
authored
Add log to ddev git (DataDog#21512)
* add log to git.py * changelog * Requested changes * Refactor test * Add context manager to replace if * Fix method call
1 parent 667aa26 commit 4b9456a

3 files changed

Lines changed: 142 additions & 0 deletions

File tree

ddev/changelog.d/21512.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Adds a new method `log` in the `GitRepository` class.

ddev/src/ddev/utils/git.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,48 @@ def latest_commit(self) -> GitCommit:
6868
sha, subject = self.capture('log', '-1', '--format=%H%n%s').splitlines()
6969
return GitCommit(sha, subject=subject)
7070

71+
def log(self, args: list[str], n: int | None = None, source: str = "HEAD") -> list[dict[str, str]]:
72+
"""
73+
The log is returned as a list of dictionaries where the keys and values of each element are
74+
specified from *args. These need to be provided in the format `"<key>:<git_format_placeholder>"`
75+
76+
Examples:
77+
Get the last n commits from `myBranch` getting the hash, author and subject
78+
79+
git.log("hash:%H", "author:%an", "subject:%s", n=20, source="myBranch")
80+
81+
"""
82+
if not args:
83+
return []
84+
85+
keys: list[str] = []
86+
format_parts: list[str] = []
87+
for arg in args:
88+
try:
89+
key, format = arg.split(":", 1)
90+
keys.append(key)
91+
format_parts.append(format)
92+
except ValueError as e:
93+
raise ValueError(f"Invalid argument: {arg}. Expected format: key:format") from e
94+
95+
pretty_format = "%x00".join(format_parts)
96+
cmd = ['--no-pager', 'log', f"--pretty=format:{pretty_format}"]
97+
if n is not None:
98+
cmd.append(f"-n {n}")
99+
100+
cmd.append(source)
101+
102+
command_output = self.capture(*cmd).strip().splitlines()
103+
104+
commits: list[dict[str, str]] = []
105+
106+
for line in command_output:
107+
line_parts = line.split("\x00")
108+
commit_dict = dict(zip(keys, line_parts, strict=True))
109+
commits.append(commit_dict)
110+
111+
return commits
112+
71113
def pull(self, ref):
72114
return self.capture('pull', 'origin', ref)
73115

ddev/tests/utils/test_git.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
44
import subprocess
5+
from contextlib import nullcontext
56

67
import pytest
78

@@ -10,6 +11,32 @@
1011
from tests.helpers.git import ClonedRepo
1112

1213

14+
@pytest.fixture(scope="module")
15+
def set_up_repository(local_clone: ClonedRepo):
16+
repo = Repository(local_clone.path.name, str(local_clone.path))
17+
18+
repo.git.capture("config", "user.name", "test_user")
19+
20+
repo.git.capture("checkout", "master")
21+
(repo.path / "test1.txt").touch()
22+
repo.git.capture("add", ".")
23+
repo.git.capture("commit", "-m", "test1")
24+
(repo.path / "test2.txt").touch()
25+
repo.git.capture("add", ".")
26+
repo.git.capture("commit", "-m", "test2")
27+
28+
repo.git.capture("checkout", "-b", "my-branch")
29+
30+
(repo.path / "test3.txt").touch()
31+
repo.git.capture("add", ".")
32+
repo.git.capture("commit", "-m", "test3")
33+
34+
repo.git.capture("checkout", "master")
35+
36+
yield repo
37+
local_clone.reset_branch()
38+
39+
1340
def test_current_branch(repository):
1441
repo = Repository(repository.path.name, str(repository.path))
1542

@@ -44,6 +71,78 @@ def test_get_latest_commit(repository):
4471
assert short_sha2 not in commit_status1
4572

4673

74+
@pytest.mark.parametrize(
75+
"args, n, source, expected, context",
76+
[
77+
(
78+
["author:%an", "message:%f"],
79+
None,
80+
None,
81+
[
82+
{"author": "test_user", "message": "test2"},
83+
{"author": "test_user", "message": "test1"},
84+
],
85+
nullcontext(),
86+
),
87+
(
88+
["author:%an", "message:%f"],
89+
2,
90+
None,
91+
[{"author": "test_user", "message": "test2"}, {"author": "test_user", "message": "test1"}],
92+
nullcontext(),
93+
),
94+
(
95+
["author:%an", "message:%f"],
96+
0,
97+
None,
98+
[],
99+
nullcontext(),
100+
),
101+
(
102+
["author:%an", "message:%f"],
103+
3,
104+
"my-branch",
105+
[
106+
{"author": "test_user", "message": "test3"},
107+
{"author": "test_user", "message": "test2"},
108+
{"author": "test_user", "message": "test1"},
109+
],
110+
nullcontext(),
111+
),
112+
(
113+
["%H", "%f"],
114+
1,
115+
None,
116+
None,
117+
pytest.raises(ValueError),
118+
),
119+
],
120+
ids=[
121+
"test_log_no_n",
122+
"test_log_two_commits",
123+
"test_log_zero_commits",
124+
"test_log_branch_three_commits",
125+
"test_log_invalid_format_raises",
126+
],
127+
)
128+
def test_get_log(set_up_repository, local_clone, config_file, args, n, source, expected, context):
129+
config_file.model.repos['core'] = str(local_clone.path)
130+
config_file.save()
131+
132+
repo = set_up_repository
133+
kwargs = {}
134+
if n is not None:
135+
kwargs['n'] = n
136+
if source:
137+
kwargs['source'] = source
138+
139+
with context:
140+
if n is None:
141+
assert len(expected) < len(repo.git.log(args, **kwargs))
142+
else:
143+
assert repo.git.log(args, **kwargs) == expected
144+
145+
47146
def test_tags(repository):
48147
repo = Repository(repository.path.name, str(repository.path))
49148

0 commit comments

Comments
 (0)