Skip to content

Commit 9e3d8d6

Browse files
fix(ci_visibility): git metadata fallback commands [backport 1.20] (#7193)
Backport 345b85a from #7178 to 1.20. 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 2e4a380 commit 9e3d8d6

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
@@ -69,7 +71,7 @@ def start(self, cwd=None):
6971
self._worker = Process(
7072
target=CIVisibilityGitClient._run_protocol,
7173
args=(self._serializer, self._requests_mode, self._base_url, self._tags, self._response),
72-
kwargs={"cwd": cwd},
74+
kwargs={"cwd": cwd, "log_level": log.level},
7375
)
7476
self._worker.start()
7577

@@ -88,8 +90,10 @@ def _run_protocol(
8890
_tags=None, # Optional[Dict[str, str]]
8991
_response=None, # Optional[Response]
9092
cwd=None, # Optional[str]
93+
log_level=0, # int
9194
):
9295
# type: (...) -> None
96+
log.setLevel(log_level)
9397
if _tags is None:
9498
_tags = {}
9599
repo_url = cls._get_repository_url(tags=_tags, cwd=cwd)
@@ -98,7 +102,6 @@ def _run_protocol(
98102
log.debug("Shallow repository detected on git > 2.27 detected, unshallowing")
99103
try:
100104
cls._unshallow_repository(cwd=cwd)
101-
log.debug("Unshallowing done")
102105
except ValueError:
103106
log.warning("Failed to unshallow repository, continuing to send pack data", exc_info=True)
104107

@@ -163,7 +166,7 @@ def _do_request(cls, requests_mode, base_url, endpoint, payload, serializer, hea
163166
_headers.update(headers)
164167
try:
165168
conn = get_connection(url)
166-
log.debug("Sending request: %s %s %s %s", ("POST", url, payload, _headers))
169+
log.debug("Sending request: %s %s %s %s", "POST", url, payload, _headers)
167170
conn.request("POST", url, payload, _headers)
168171
resp = compat.get_connection_response(conn)
169172
log.debug("Response status: %s", resp.status)
@@ -209,7 +212,47 @@ def _is_shallow_repository(cls, cwd=None):
209212
@classmethod
210213
def _unshallow_repository(cls, cwd=None):
211214
# type () -> None
212-
_unshallow_repository(cwd=cwd)
215+
try:
216+
remote = _extract_clone_defaultremotename()
217+
except ValueError as e:
218+
log.debug("Failed to get default remote: %s", e)
219+
return
220+
221+
try:
222+
CIVisibilityGitClient._unshallow_repository_to_local_head(remote, cwd=cwd)
223+
return
224+
except ValueError as e:
225+
log.debug("Could not unshallow repository to local head: %s", e)
226+
227+
try:
228+
CIVisibilityGitClient._unshallow_repository_to_upstream(remote, cwd=cwd)
229+
return
230+
except ValueError as e:
231+
log.debug("Could not unshallow to upstream: %s", e)
232+
233+
log.debug("Unshallowing to default")
234+
try:
235+
_unshallow_repository(cwd=cwd, repo=remote)
236+
log.debug("Unshallowing to default successful")
237+
return
238+
except ValueError as e:
239+
log.debug("Unshallowing failed: %s", e)
240+
241+
@classmethod
242+
def _unshallow_repository_to_local_head(cls, remote, cwd=None):
243+
# type (str, Optional[str) -> None
244+
head = extract_commit_sha(cwd=cwd)
245+
log.debug("Unshallowing to local head %s", head)
246+
_unshallow_repository(cwd=cwd, repo=remote, refspec=head)
247+
log.debug("Unshallowing to local head successful")
248+
249+
@classmethod
250+
def _unshallow_repository_to_upstream(cls, remote, cwd=None):
251+
# type (str, Optional[str) -> None
252+
upstream = _extract_upstream_sha(cwd=cwd)
253+
log.debug("Unshallowing to upstream %s", upstream)
254+
_unshallow_repository(cwd=cwd, repo=remote, refspec=upstream)
255+
log.debug("Unshallowing to upstream")
213256

214257

215258
class CIVisibilityGitClientSerializerV1(object):

tests/ci_visibility/test_ci_visibility.py

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

956956

957-
def test_unshallow_repository():
958-
with mock.patch("ddtrace.internal.ci_visibility.git_client._unshallow_repository") as mock_unshallow_repository:
959-
CIVisibilityGitClient._unshallow_repository(cwd="/path/to/repo")
960-
mock_unshallow_repository.assert_called_once_with(cwd="/path/to/repo")
957+
def test_unshallow_repository_local_head():
958+
with mock.patch(
959+
"ddtrace.internal.ci_visibility.git_client._extract_clone_defaultremotename", return_value="origin"
960+
):
961+
with mock.patch("ddtrace.internal.ci_visibility.git_client.extract_commit_sha", return_value="myfakesha"):
962+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess_command:
963+
CIVisibilityGitClient._unshallow_repository(cwd="/path/to/repo")
964+
mock_git_subprocess_command.assert_called_once_with(
965+
[
966+
"fetch",
967+
'--shallow-since="1 month ago"',
968+
"--update-shallow",
969+
"--filter=blob:none",
970+
"--recurse-submodules=no",
971+
"origin",
972+
"myfakesha",
973+
],
974+
cwd="/path/to/repo",
975+
)
976+
977+
978+
def test_unshallow_repository_upstream():
979+
with mock.patch(
980+
"ddtrace.internal.ci_visibility.git_client._extract_clone_defaultremotename", return_value="origin"
981+
):
982+
with mock.patch(
983+
"ddtrace.internal.ci_visibility.git_client.CIVisibilityGitClient._unshallow_repository_to_local_head",
984+
side_effect=ValueError,
985+
):
986+
with mock.patch(
987+
"ddtrace.internal.ci_visibility.git_client._extract_upstream_sha", return_value="myupstreamsha"
988+
):
989+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess_command:
990+
CIVisibilityGitClient._unshallow_repository(cwd="/path/to/repo")
991+
mock_git_subprocess_command.assert_called_once_with(
992+
[
993+
"fetch",
994+
'--shallow-since="1 month ago"',
995+
"--update-shallow",
996+
"--filter=blob:none",
997+
"--recurse-submodules=no",
998+
"origin",
999+
"myupstreamsha",
1000+
],
1001+
cwd="/path/to/repo",
1002+
)
1003+
1004+
1005+
def test_unshallow_repository_full():
1006+
with mock.patch(
1007+
"ddtrace.internal.ci_visibility.git_client._extract_clone_defaultremotename", return_value="origin"
1008+
):
1009+
with mock.patch(
1010+
"ddtrace.internal.ci_visibility.git_client.CIVisibilityGitClient._unshallow_repository_to_local_head",
1011+
side_effect=ValueError,
1012+
):
1013+
with mock.patch(
1014+
"ddtrace.internal.ci_visibility.git_client.CIVisibilityGitClient._unshallow_repository_to_upstream",
1015+
side_effect=ValueError,
1016+
):
1017+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess_command:
1018+
CIVisibilityGitClient._unshallow_repository(cwd="/path/to/repo")
1019+
mock_git_subprocess_command.assert_called_once_with(
1020+
[
1021+
"fetch",
1022+
'--shallow-since="1 month ago"',
1023+
"--update-shallow",
1024+
"--filter=blob:none",
1025+
"--recurse-submodules=no",
1026+
"origin",
1027+
],
1028+
cwd="/path/to/repo",
1029+
)
1030+
1031+
1032+
def test_unshallow_respository_cant_get_remote():
1033+
with mock.patch(
1034+
"ddtrace.internal.ci_visibility.git_client._extract_clone_defaultremotename", side_effect=ValueError
1035+
):
1036+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess_command:
1037+
CIVisibilityGitClient._unshallow_repository()
1038+
mock_git_subprocess_command.assert_not_called()
9611039

9621040

9631041
def test_encoder_pack_payload():

tests/tracer/test_ci.py

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

277277

278-
def test_unshallow_repository(git_repo):
279-
with mock.patch(
280-
"ddtrace.ext.git._extract_clone_defaultremotename", return_value="myremote"
281-
) as mock_defaultremotename:
282-
with mock.patch(
283-
"ddtrace.ext.git.extract_commit_sha", return_value="mycommitshaaaaaaaaaaaa123"
284-
) as mock_extract_sha:
285-
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess:
286-
git._unshallow_repository(cwd=git_repo)
287-
288-
mock_defaultremotename.assert_called_once_with(git_repo)
289-
mock_extract_sha.assert_called_once_with(git_repo)
290-
mock_git_subprocess.assert_called_once_with(
291-
[
292-
"fetch",
293-
"--update-shallow",
294-
"--filter=blob:none",
295-
"--recurse-submodules=no",
296-
'--shallow-since="1 month ago"',
297-
"myremote",
298-
"mycommitshaaaaaaaaaaaa123",
299-
],
300-
cwd=git_repo,
301-
)
278+
def test_unshallow_repository_bare(git_repo):
279+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess:
280+
git._unshallow_repository(cwd=git_repo)
281+
mock_git_subprocess.assert_called_once_with(
282+
[
283+
"fetch",
284+
'--shallow-since="1 month ago"',
285+
"--update-shallow",
286+
"--filter=blob:none",
287+
"--recurse-submodules=no",
288+
],
289+
cwd=git_repo,
290+
)
291+
292+
293+
def test_unshallow_repository_bare_repo(git_repo):
294+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess:
295+
git._unshallow_repository(cwd=git_repo, repo="myremote")
296+
mock_git_subprocess.assert_called_once_with(
297+
[
298+
"fetch",
299+
'--shallow-since="1 month ago"',
300+
"--update-shallow",
301+
"--filter=blob:none",
302+
"--recurse-submodules=no",
303+
"myremote",
304+
],
305+
cwd=git_repo,
306+
)
307+
308+
309+
def test_unshallow_repository_bare_repo_refspec(git_repo):
310+
with mock.patch("ddtrace.ext.git._git_subprocess_cmd") as mock_git_subprocess:
311+
git._unshallow_repository(cwd=git_repo, repo="myremote", refspec="mycommitshaaaaaaaaaaaa123")
312+
mock_git_subprocess.assert_called_once_with(
313+
[
314+
"fetch",
315+
'--shallow-since="1 month ago"',
316+
"--update-shallow",
317+
"--filter=blob:none",
318+
"--recurse-submodules=no",
319+
"myremote",
320+
"mycommitshaaaaaaaaaaaa123",
321+
],
322+
cwd=git_repo,
323+
)
302324

303325

304326
def test_extract_clone_defaultremotename():
305327
with mock.patch("ddtrace.ext.git._git_subprocess_cmd", return_value="default_remote_name") as mock_git_subprocess:
306328
assert git._extract_clone_defaultremotename(cwd=git_repo) == "default_remote_name"
307-
mock_git_subprocess.assert_called_once_with("config --default origin --get clone.defaultRemoteName")
329+
mock_git_subprocess.assert_called_once_with(
330+
"config --default origin --get clone.defaultRemoteName", cwd=git_repo
331+
)

0 commit comments

Comments
 (0)