Skip to content

Commit dff1b18

Browse files
fix(ci_visibility): git metadata fallback commands [backport 2.1] (#7195)
Backport 345b85a from #7178 to 2.1. This adds some fallback commands to the git metadata gathering done by the CI Visibility git client to handle: * cases where there is a commit present in the branch that is not in the remote * cases where a branch is not tracking an upstream (eg: detached HEAD states) Minor extra changes to propagate log level from the parent process to the subprocess created for the client startup operation. No release note due to ITR still being in beta for Python/pytest. ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) are followed. If no release note is required, add label `changelog/no-changelog`. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. - [x] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. - [x] Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) - [x] If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from `@DataDog/security-design-and-guidance`. - [x] This PR doesn't touch any of that. Co-authored-by: Romain Komorn <[email protected]>
1 parent c90d915 commit dff1b18

File tree

4 files changed

+194
-42
lines changed

4 files changed

+194
-42
lines changed

ddtrace/ext/git.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,14 @@ def _set_safe_directory():
101101

102102

103103
def _extract_clone_defaultremotename(cwd=None):
104-
output = _git_subprocess_cmd("config --default origin --get clone.defaultRemoteName")
104+
# type: (Optional[str]) -> str
105+
output = _git_subprocess_cmd("config --default origin --get clone.defaultRemoteName", cwd=cwd)
106+
return output
107+
108+
109+
def _extract_upstream_sha(cwd=None):
110+
# type: (Optional[str]) -> str
111+
output = _git_subprocess_cmd("rev-parse @{upstream}", cwd=cwd)
105112
return output
106113

107114

@@ -111,22 +118,22 @@ def _is_shallow_repository(cwd=None):
111118
return output.strip() == "true"
112119

113120

114-
def _unshallow_repository(cwd=None):
115-
# type (Optional[str]) -> None
116-
remote_name = _extract_clone_defaultremotename(cwd)
117-
head_sha = extract_commit_sha(cwd)
121+
def _unshallow_repository(cwd=None, repo=None, refspec=None):
122+
# type (Optional[str], Optional[str], Optional[str]) -> None
118123

119124
cmd = [
120125
"fetch",
126+
'--shallow-since="1 month ago"',
121127
"--update-shallow",
122128
"--filter=blob:none",
123129
"--recurse-submodules=no",
124-
'--shallow-since="1 month ago"',
125-
remote_name,
126-
head_sha,
127130
]
131+
if repo is not None:
132+
cmd.append(repo)
133+
if refspec is not None:
134+
cmd.append(refspec)
128135

129-
_git_subprocess_cmd(cmd, cwd=cwd)
136+
return _git_subprocess_cmd(cmd, cwd=cwd)
130137

131138

132139
def extract_user_info(cwd=None):

ddtrace/internal/ci_visibility/git_client.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from typing import Tuple # noqa
88

99
from ddtrace.ext import ci
10+
from ddtrace.ext.git import _extract_clone_defaultremotename
11+
from ddtrace.ext.git import _extract_upstream_sha
1012
from ddtrace.ext.git import _get_rev_list
1113
from ddtrace.ext.git import _is_shallow_repository
1214
from ddtrace.ext.git import _unshallow_repository
@@ -65,7 +67,7 @@ def start(self, cwd=None):
6567
self._worker = Process(
6668
target=CIVisibilityGitClient._run_protocol,
6769
args=(self._serializer, self._requests_mode, self._base_url, self._tags, self._response),
68-
kwargs={"cwd": cwd},
70+
kwargs={"cwd": cwd, "log_level": log.level},
6971
)
7072
self._worker.start()
7173

@@ -84,8 +86,10 @@ def _run_protocol(
8486
_tags=None, # Optional[Dict[str, str]]
8587
_response=None, # Optional[Response]
8688
cwd=None, # Optional[str]
89+
log_level=0, # int
8790
):
8891
# type: (...) -> None
92+
log.setLevel(log_level)
8993
if _tags is None:
9094
_tags = {}
9195
repo_url = cls._get_repository_url(tags=_tags, cwd=cwd)
@@ -94,7 +98,6 @@ def _run_protocol(
9498
log.debug("Shallow repository detected on git > 2.27 detected, unshallowing")
9599
try:
96100
cls._unshallow_repository(cwd=cwd)
97-
log.debug("Unshallowing done")
98101
except ValueError:
99102
log.warning("Failed to unshallow repository, continuing to send pack data", exc_info=True)
100103

@@ -157,7 +160,7 @@ def _do_request(cls, requests_mode, base_url, endpoint, payload, serializer, hea
157160
_headers.update(headers)
158161
try:
159162
conn = get_connection(url)
160-
log.debug("Sending request: %s %s %s %s", ("POST", url, payload, _headers))
163+
log.debug("Sending request: %s %s %s %s", "POST", url, payload, _headers)
161164
conn.request("POST", url, payload, _headers)
162165
resp = compat.get_connection_response(conn)
163166
log.debug("Response status: %s", resp.status)
@@ -203,7 +206,47 @@ def _is_shallow_repository(cls, cwd=None):
203206
@classmethod
204207
def _unshallow_repository(cls, cwd=None):
205208
# type () -> None
206-
_unshallow_repository(cwd=cwd)
209+
try:
210+
remote = _extract_clone_defaultremotename()
211+
except ValueError as e:
212+
log.debug("Failed to get default remote: %s", e)
213+
return
214+
215+
try:
216+
CIVisibilityGitClient._unshallow_repository_to_local_head(remote, cwd=cwd)
217+
return
218+
except ValueError as e:
219+
log.debug("Could not unshallow repository to local head: %s", e)
220+
221+
try:
222+
CIVisibilityGitClient._unshallow_repository_to_upstream(remote, cwd=cwd)
223+
return
224+
except ValueError as e:
225+
log.debug("Could not unshallow to upstream: %s", e)
226+
227+
log.debug("Unshallowing to default")
228+
try:
229+
_unshallow_repository(cwd=cwd, repo=remote)
230+
log.debug("Unshallowing to default successful")
231+
return
232+
except ValueError as e:
233+
log.debug("Unshallowing failed: %s", e)
234+
235+
@classmethod
236+
def _unshallow_repository_to_local_head(cls, remote, cwd=None):
237+
# type (str, Optional[str) -> None
238+
head = extract_commit_sha(cwd=cwd)
239+
log.debug("Unshallowing to local head %s", head)
240+
_unshallow_repository(cwd=cwd, repo=remote, refspec=head)
241+
log.debug("Unshallowing to local head successful")
242+
243+
@classmethod
244+
def _unshallow_repository_to_upstream(cls, remote, cwd=None):
245+
# type (str, Optional[str) -> None
246+
upstream = _extract_upstream_sha(cwd=cwd)
247+
log.debug("Unshallowing to upstream %s", upstream)
248+
_unshallow_repository(cwd=cwd, repo=remote, refspec=upstream)
249+
log.debug("Unshallowing to upstream")
207250

208251

209252
class CIVisibilityGitClientSerializerV1(object):

tests/ci_visibility/test_ci_visibility.py

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -923,10 +923,88 @@ def test_is_shallow_repository_false():
923923
mock_is_shallow_repository.assert_called_once_with(cwd="/path/to/repo")
924924

925925

926-
def test_unshallow_repository():
927-
with mock.patch("ddtrace.internal.ci_visibility.git_client._unshallow_repository") as mock_unshallow_repository:
928-
CIVisibilityGitClient._unshallow_repository(cwd="/path/to/repo")
929-
mock_unshallow_repository.assert_called_once_with(cwd="/path/to/repo")
926+
def test_unshallow_repository_local_head():
927+
with mock.patch(
928+
"ddtrace.internal.ci_visibility.git_client._extract_clone_defaultremotename", return_value="origin"
929+
):
930+
with mock.patch("ddtrace.internal.ci_visibility.git_client.extract_commit_sha", return_value="myfakesha"):
931+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess_command:
932+
CIVisibilityGitClient._unshallow_repository(cwd="/path/to/repo")
933+
mock_git_subprocess_command.assert_called_once_with(
934+
[
935+
"fetch",
936+
'--shallow-since="1 month ago"',
937+
"--update-shallow",
938+
"--filter=blob:none",
939+
"--recurse-submodules=no",
940+
"origin",
941+
"myfakesha",
942+
],
943+
cwd="/path/to/repo",
944+
)
945+
946+
947+
def test_unshallow_repository_upstream():
948+
with mock.patch(
949+
"ddtrace.internal.ci_visibility.git_client._extract_clone_defaultremotename", return_value="origin"
950+
):
951+
with mock.patch(
952+
"ddtrace.internal.ci_visibility.git_client.CIVisibilityGitClient._unshallow_repository_to_local_head",
953+
side_effect=ValueError,
954+
):
955+
with mock.patch(
956+
"ddtrace.internal.ci_visibility.git_client._extract_upstream_sha", return_value="myupstreamsha"
957+
):
958+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess_command:
959+
CIVisibilityGitClient._unshallow_repository(cwd="/path/to/repo")
960+
mock_git_subprocess_command.assert_called_once_with(
961+
[
962+
"fetch",
963+
'--shallow-since="1 month ago"',
964+
"--update-shallow",
965+
"--filter=blob:none",
966+
"--recurse-submodules=no",
967+
"origin",
968+
"myupstreamsha",
969+
],
970+
cwd="/path/to/repo",
971+
)
972+
973+
974+
def test_unshallow_repository_full():
975+
with mock.patch(
976+
"ddtrace.internal.ci_visibility.git_client._extract_clone_defaultremotename", return_value="origin"
977+
):
978+
with mock.patch(
979+
"ddtrace.internal.ci_visibility.git_client.CIVisibilityGitClient._unshallow_repository_to_local_head",
980+
side_effect=ValueError,
981+
):
982+
with mock.patch(
983+
"ddtrace.internal.ci_visibility.git_client.CIVisibilityGitClient._unshallow_repository_to_upstream",
984+
side_effect=ValueError,
985+
):
986+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess_command:
987+
CIVisibilityGitClient._unshallow_repository(cwd="/path/to/repo")
988+
mock_git_subprocess_command.assert_called_once_with(
989+
[
990+
"fetch",
991+
'--shallow-since="1 month ago"',
992+
"--update-shallow",
993+
"--filter=blob:none",
994+
"--recurse-submodules=no",
995+
"origin",
996+
],
997+
cwd="/path/to/repo",
998+
)
999+
1000+
1001+
def test_unshallow_respository_cant_get_remote():
1002+
with mock.patch(
1003+
"ddtrace.internal.ci_visibility.git_client._extract_clone_defaultremotename", side_effect=ValueError
1004+
):
1005+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess_command:
1006+
CIVisibilityGitClient._unshallow_repository()
1007+
mock_git_subprocess_command.assert_not_called()
9301008

9311009

9321010
def test_encoder_pack_payload():

tests/tracer/test_ci.py

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -289,33 +289,57 @@ def test_is_shallow_repository_false(git_repo):
289289
mock_git_subprocess.assert_called_once_with("rev-parse --is-shallow-repository", cwd=git_repo)
290290

291291

292-
def test_unshallow_repository(git_repo):
293-
with mock.patch(
294-
"ddtrace.ext.git._extract_clone_defaultremotename", return_value="myremote"
295-
) as mock_defaultremotename:
296-
with mock.patch(
297-
"ddtrace.ext.git.extract_commit_sha", return_value="mycommitshaaaaaaaaaaaa123"
298-
) as mock_extract_sha:
299-
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess:
300-
git._unshallow_repository(cwd=git_repo)
301-
302-
mock_defaultremotename.assert_called_once_with(git_repo)
303-
mock_extract_sha.assert_called_once_with(git_repo)
304-
mock_git_subprocess.assert_called_once_with(
305-
[
306-
"fetch",
307-
"--update-shallow",
308-
"--filter=blob:none",
309-
"--recurse-submodules=no",
310-
'--shallow-since="1 month ago"',
311-
"myremote",
312-
"mycommitshaaaaaaaaaaaa123",
313-
],
314-
cwd=git_repo,
315-
)
292+
def test_unshallow_repository_bare(git_repo):
293+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess:
294+
git._unshallow_repository(cwd=git_repo)
295+
mock_git_subprocess.assert_called_once_with(
296+
[
297+
"fetch",
298+
'--shallow-since="1 month ago"',
299+
"--update-shallow",
300+
"--filter=blob:none",
301+
"--recurse-submodules=no",
302+
],
303+
cwd=git_repo,
304+
)
305+
306+
307+
def test_unshallow_repository_bare_repo(git_repo):
308+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess:
309+
git._unshallow_repository(cwd=git_repo, repo="myremote")
310+
mock_git_subprocess.assert_called_once_with(
311+
[
312+
"fetch",
313+
'--shallow-since="1 month ago"',
314+
"--update-shallow",
315+
"--filter=blob:none",
316+
"--recurse-submodules=no",
317+
"myremote",
318+
],
319+
cwd=git_repo,
320+
)
321+
322+
323+
def test_unshallow_repository_bare_repo_refspec(git_repo):
324+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess:
325+
git._unshallow_repository(cwd=git_repo, repo="myremote", refspec="mycommitshaaaaaaaaaaaa123")
326+
mock_git_subprocess.assert_called_once_with(
327+
[
328+
"fetch",
329+
'--shallow-since="1 month ago"',
330+
"--update-shallow",
331+
"--filter=blob:none",
332+
"--recurse-submodules=no",
333+
"myremote",
334+
"mycommitshaaaaaaaaaaaa123",
335+
],
336+
cwd=git_repo,
337+
)
316338

317339

318340
def test_extract_clone_defaultremotename():
319341
with mock.patch("ddtrace.ext.git._git_subprocess_cmd", return_value="default_remote_name") as mock_git_subprocess:
320342
assert git._extract_clone_defaultremotename(cwd=git_repo) == "default_remote_name"
321-
mock_git_subprocess.assert_called_once_with("config --default origin --get clone.defaultRemoteName")
343+
mock_git_subprocess.assert_called_once_with(
344+
"config --default origin --get clone.defaultRemoteName", cwd=git_repo
345+
)

0 commit comments

Comments
 (0)