From 54fa31cd844b3de887a1915755b110287a4d2bcb Mon Sep 17 00:00:00 2001 From: Brian Gan Date: Thu, 30 Jul 2026 23:21:22 +0000 Subject: [PATCH] project: derive HEAD fallback from git's own default branch When the default branch cannot be determined dynamically, repo fell back to a hardcoded "refs/heads/master". This can point at a branch that does not exist on the server, since projects increasingly default to "main". Rather than swap one hardcoded name for another, ask git itself what it would use. Bug: 483758905 Change-Id: Ic66712acea98f8e548a2d6d8211865ee5c416b4d Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/612845 Tested-by: Brian Gan Reviewed-by: Gavin Mak Commit-Queue: Brian Gan --- project.py | 29 +++++++++++++++++++--- tests/test_project.py | 57 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/project.py b/project.py index 36039321f..45860d228 100644 --- a/project.py +++ b/project.py @@ -15,6 +15,7 @@ import datetime import errno import filecmp +import functools import glob import os import platform @@ -4663,6 +4664,26 @@ class SyncBuffer: self._pending_failures = [] +@functools.lru_cache(maxsize=None) +def _DefaultBranchFallback() -> str: + """Return the ref to use when remote default branch can't be resolved.""" + + def _git(args: List[str]) -> str: + p = GitCommand( + None, + args, + capture_stdout=True, + capture_stderr=True, + log_as_error=False, + ) + return p.stdout.strip() if p.Wait() == 0 else "" + + branch = _git(["var", "GIT_DEFAULT_BRANCH"]) or _git( + ["config", "--get", "init.defaultBranch"] + ) + return f"refs/heads/{branch or 'master'}" + + class MetaProject(Project): """A special project housed under .repo.""" @@ -4676,7 +4697,7 @@ class MetaProject(Project): worktree=worktree, remote=RemoteSpec("origin"), relpath=".repo/%s" % name, - revisionExpr="refs/heads/master", + revisionExpr=_DefaultBranchFallback(), revisionId=None, groups=None, ) @@ -5134,9 +5155,9 @@ class ManifestProject(MetaProject): if is_new: default_branch = self.ResolveRemoteHead() if default_branch is None: - # If the remote doesn't have HEAD configured, default to - # master. - default_branch = "refs/heads/master" + # If the remote doesn't have HEAD configured, fall back + # to whatever git uses as its default branch. + default_branch = _DefaultBranchFallback() self.revisionExpr = default_branch else: self.PreSync() diff --git a/tests/test_project.py b/tests/test_project.py index 1ebeb85d7..342ef6b8a 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -20,10 +20,11 @@ from pathlib import Path import shutil import subprocess import tempfile -from typing import Optional +from typing import Dict, List, Optional, Tuple import unittest from unittest import mock +import pytest import utils_for_test import error @@ -814,6 +815,60 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase): self.assertFalse(os.path.exists(proj.gitdir)) +_VAR_CMD: List[str] = ["var", "GIT_DEFAULT_BRANCH"] +_CONFIG_CMD: List[str] = ["config", "--get", "init.defaultBranch"] + + +@pytest.mark.parametrize( + "responses, expected_ref, expected_calls", + ( + # git >= 2.35 answers `git var GIT_DEFAULT_BRANCH`. + ({"var": (0, "jellybean\n")}, "refs/heads/jellybean", [_VAR_CMD]), + # Older git: `git var` fails, so read init.defaultBranch instead. + ( + {"var": (1, ""), "config": (0, "custom\n")}, + "refs/heads/custom", + [_VAR_CMD, _CONFIG_CMD], + ), + # Nothing configured anywhere: git's historical built-in default. + ( + {"var": (1, ""), "config": (1, "")}, + "refs/heads/master", + [_VAR_CMD, _CONFIG_CMD], + ), + ), + ids=("git_var", "old_git_reads_config", "unconfigured_defaults_to_master"), +) +def test_default_branch_fallback( + responses: Dict[str, Tuple[int, str]], + expected_ref: str, + expected_calls: List[List[str]], +) -> None: + """_DefaultBranchFallback resolves the default branch via git.""" + seen: List[List[str]] = [] + + class FakeGitCommand: + # Emulate git by returning the canned response for the subcommand. + def __init__( + self, project_: Optional[project.Project], cmdv: List[str], **kwargs + ) -> None: + self.returncode, self.stdout = responses[cmdv[0]] + seen.append(cmdv) + + def Wait(self) -> int: + return self.returncode + + # The result is memoized, so clear it before (to bypass any cached real + # value) and after (so the mocked value doesn't leak to other tests). + project._DefaultBranchFallback.cache_clear() + try: + with mock.patch.object(project, "GitCommand", FakeGitCommand): + assert project._DefaultBranchFallback() == expected_ref + assert seen == expected_calls + finally: + project._DefaultBranchFallback.cache_clear() + + def _create_mock_project( tempdir, use_local_gitdirs=False,