Skip to content

Commit 5fbe399

Browse files
committed
fix(vcs): support finding merge-base in git shallow clones
This requires us to keep deepening the repository. There's likely a more efficient way to do this, but for this is at least better than an error.
1 parent effba6f commit 5fbe399

File tree

2 files changed

+87
-4
lines changed

2 files changed

+87
-4
lines changed

src/taskgraph/util/vcs.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,10 @@ def base_rev(self):
388388
def branch(self):
389389
return self.run("branch", "--show-current").strip() or None
390390

391+
@property
392+
def is_shallow(self):
393+
return self.run("rev-parse", "--is-shallow-repository").strip() == "true"
394+
391395
@property
392396
def all_remote_names(self):
393397
remotes = self.run("remote").splitlines()
@@ -546,10 +550,36 @@ def update(self, ref):
546550
self.run("checkout", ref)
547551

548552
def find_latest_common_revision(self, base_ref_or_rev, head_rev):
549-
try:
550-
return self.run("merge-base", base_ref_or_rev, head_rev).strip()
551-
except subprocess.CalledProcessError:
552-
return self.NULL_REVISION
553+
def run_merge_base():
554+
try:
555+
return self.run("merge-base", base_ref_or_rev, head_rev).strip()
556+
except subprocess.CalledProcessError:
557+
return None
558+
559+
if not self.is_shallow:
560+
return run_merge_base() or self.NULL_REVISION
561+
562+
rev = run_merge_base()
563+
deepen = 10
564+
while not rev:
565+
self.run("fetch", "--deepen", str(deepen), self.remote_name)
566+
rev = run_merge_base()
567+
deepen = deepen * 10
568+
569+
try:
570+
self.run("rev-list", "--max-parents=0", head_rev)
571+
# We've reached a root commit, stop trying to deepen.
572+
break
573+
except subprocess.CalledProcessError:
574+
pass
575+
576+
if not rev:
577+
# If we still haven't found a merge base, unshallow the repo and
578+
# try one last time.
579+
self.run("fetch", "--unshallow", self.remote_name)
580+
rev = run_merge_base()
581+
582+
return rev or self.NULL_REVISION
553583

554584
def does_revision_exist_locally(self, revision):
555585
try:

test/test_util_vcs.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

55
import os
6+
import shutil
67
import subprocess
78
from pathlib import Path
89
from textwrap import dedent
@@ -495,6 +496,58 @@ def test_find_latest_common_revision(repo_with_remote):
495496
)
496497

497498

499+
def test_find_latest_common_revision_shallow_clone(
500+
tmpdir, git_repo, default_git_branch
501+
):
502+
"""Test finding common revision in a shallow clone that requires deepening."""
503+
remote_path = str(tmpdir / "remote_repo")
504+
shutil.copytree(git_repo, remote_path)
505+
506+
# Add several commits to the remote repository to create depth
507+
remote_repo = get_repository(remote_path)
508+
509+
# Create multiple commits to establish depth
510+
for i in range(5):
511+
test_file = os.path.join(remote_path, f"file_{i}.txt")
512+
with open(test_file, "w") as f:
513+
f.write(f"content {i}")
514+
remote_repo.run("add", test_file)
515+
remote_repo.run("commit", "-m", f"Commit {i}")
516+
517+
# Store the head revision of remote for comparison
518+
remote_head = remote_repo.head_rev
519+
520+
# Create a shallow clone with depth 1
521+
# Need to use file:// protocol for --depth to work with local repos
522+
shallow_clone_path = str(tmpdir / "shallow_clone")
523+
subprocess.check_call(
524+
["git", "clone", "--depth", "1", f"file://{remote_path}", shallow_clone_path]
525+
)
526+
527+
shallow_repo = get_repository(shallow_clone_path)
528+
assert shallow_repo.is_shallow
529+
530+
remote_name = "origin"
531+
532+
# Create a new commit in the shallow clone to diverge from remote
533+
new_file = os.path.join(shallow_clone_path, "local_file.txt")
534+
with open(new_file, "w") as f:
535+
f.write("local content")
536+
shallow_repo.run("add", new_file)
537+
shallow_repo.run("commit", "-m", "Local commit")
538+
539+
# Now try to find the common revision - this should trigger deepening
540+
# because the shallow clone doesn't have enough history
541+
base_ref = f"{remote_name}/{default_git_branch}"
542+
result = shallow_repo.find_latest_common_revision(base_ref, shallow_repo.head_rev)
543+
544+
# The result should be the remote's head (the common ancestor)
545+
assert result == remote_head
546+
547+
# Verify the repository has been deepened
548+
assert shallow_repo.does_revision_exist_locally(result)
549+
550+
498551
def test_does_revision_exist_locally(repo):
499552
first_revision = repo.head_rev
500553

0 commit comments

Comments
 (0)