info: reuse branch snapshots and combine history walks

Derive the current branch from GetBranches so attached HEAD and refs are
resolved once, including overview reporting. Replace the two
complementary rev-list calls in --all output with one --left-right
symmetric-difference walk and partition its records in memory.

Bug: 553599402
Change-Id: I01d5db804cbf591ecc0e08d6605971f0f5a38538
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/634005
Tested-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Brian Gan <brgan@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
This commit is contained in:
Gavin Mak authored and gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com committed 2026-09-21 13:40:06 -07:00
1 parent f6f5946422
commit d5c17e9186
2 files changed
+174 -28

No files matched your search

+58 -26
View File
@@ -18,7 +18,7 @@ import io
import json
import optparse
import sys
from typing import Any, Dict, List, NamedTuple
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
from color import Coloring
from command import DEFAULT_LOCAL_JOBS
@@ -189,18 +189,26 @@ class Info(PagedCommand):
"superproject_revision": srev,
}
@staticmethod
def _GetCurrentBranch(branches: Dict[str, Any]) -> Optional[str]:
"""Return the name of the current branch from a GetBranches mapping."""
return next(
(name for name, branch in branches.items() if branch.current), None
)
@classmethod
def _getProjectData(cls, project) -> Dict[str, Any]:
"""Gather project data as a dict."""
branches = project.GetBranches()
currentBranch = cls._GetCurrentBranch(branches)
data = {
"name": project.name,
"mount_path": project.worktree,
"current_revision": project.GetHeadRevisionId()
or project.GetRevisionId(),
"manifest_revision": project.revisionExpr,
"local_branches": list(project.GetBranches()),
"local_branches": list(branches),
}
currentBranch = project.CurrentBranch
if currentBranch:
data["current_branch"] = currentBranch
return data
@@ -285,6 +293,9 @@ class Info(PagedCommand):
text = out.nofmt_printer("text")
dimtext = out.printer("dimtext", attr="dim")
branches = project.GetBranches()
currentBranch = cls._GetCurrentBranch(branches)
heading("Project: ")
headtext(project.name)
out.nl()
@@ -297,7 +308,6 @@ class Info(PagedCommand):
headtext(project.GetHeadRevisionId() or project.GetRevisionId())
out.nl()
currentBranch = project.CurrentBranch
if currentBranch:
heading("Current branch: ")
headtext(currentBranch)
@@ -307,7 +317,7 @@ class Info(PagedCommand):
headtext(project.revisionExpr)
out.nl()
localBranches = list(project.GetBranches().keys())
localBranches = list(branches)
heading("Local Branches: ")
redtext(str(len(localBranches)))
if localBranches:
@@ -327,25 +337,10 @@ class Info(PagedCommand):
branch = branch[len(R_HEADS) :]
logTarget = R_M + branch
bareTmp = project.bare_git._bare
project.bare_git._bare = False
localCommits = project.bare_git.rev_list(
"--abbrev=8",
"--abbrev-commit",
"--pretty=oneline",
logTarget + "..",
"--",
localCommits, originCommits = cls._GetDiffCommits(
project, logTarget
)
originCommits = project.bare_git.rev_list(
"--abbrev=8",
"--abbrev-commit",
"--pretty=oneline",
".." + logTarget,
"--",
)
project.bare_git._bare = bareTmp
heading("Local Commits: ")
redtext(str(len(localCommits)))
dimtext(" (on current branch)")
@@ -375,6 +370,35 @@ class Info(PagedCommand):
return buf.getvalue()
@classmethod
def _GetDiffCommits(
cls, project: Any, log_target: str
) -> Tuple[List[str], List[str]]:
"""Return local-only and remote-only commits from one history walk."""
git = getattr(project, "work_git", None) or getattr(
project, "bare_git", None
)
if git is None:
return [], []
commits = git.rev_list(
"--left-right",
"--abbrev=8",
"--abbrev-commit",
"--pretty=oneline",
f"HEAD...{log_target}",
"--",
)
if isinstance(commits, str):
commits = commits.splitlines()
local = []
remote = []
for commit in commits:
if commit.startswith("<"):
local.append(commit[1:])
elif commit.startswith(">"):
remote.append(commit[1:])
return local, remote
def _printDiffInfo(self, opt, args):
projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
@@ -404,10 +428,18 @@ class Info(PagedCommand):
project = cls.get_parallel_context()["projects"][project_idx]
branches = []
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
br = [x for x in br if x]
local_branches = project.GetBranches()
current_branch = cls._GetCurrentBranch(local_branches)
if opt.current_branch:
br = [x for x in br if x.name == project.CurrentBranch]
candidate_branches = (
[current_branch]
if current_branch and current_branch in local_branches
else []
)
else:
candidate_branches = local_branches
br = [project.GetUploadableBranch(x) for x in candidate_branches]
br = [x for x in br if x]
for b in br:
branches.append(
@@ -416,7 +448,7 @@ class Info(PagedCommand):
name=b.name,
commits=b.commits,
date=b.date,
is_current=b.name == project.CurrentBranch,
is_current=b.name == current_branch,
)
)
return branches
+116 -2
View File
@@ -208,7 +208,7 @@ def test_get_project_data_uses_head_revision() -> None:
project.name = "foo"
project.worktree = "/path/to/foo"
project.revisionExpr = "refs/heads/main"
project.GetBranches.return_value = []
project.GetBranches.return_value = {}
# GetHeadRevisionId() returns a SHA, it should be used.
project.GetHeadRevisionId.return_value = "head_sha_12345"
@@ -235,7 +235,9 @@ def test_json_with_projects(capsys) -> None:
project.name = "foo"
project.worktree = "/path/to/foo"
project.revisionExpr = "refs/heads/main"
project.GetBranches.return_value = {"branch1": mock.MagicMock()}
branch = mock.MagicMock()
branch.current = True
project.GetBranches.return_value = {"branch1": branch}
project.GetHeadRevisionId.return_value = "head_sha_12345"
project.CurrentBranch = "branch1"
@@ -253,3 +255,115 @@ def test_json_with_projects(capsys) -> None:
assert project_data["manifest_revision"] == "refs/heads/main"
assert project_data["local_branches"] == ["branch1"]
assert project_data["current_branch"] == "branch1"
def test_diff_commits_uses_one_left_right_walk() -> None:
"""Local and remote commits are partitioned from one rev-list."""
project = mock.MagicMock()
project.work_git.rev_list.return_value = [
"<11111111 local commit",
">22222222 remote commit",
]
local, remote = info.Info._GetDiffCommits(project, "refs/remotes/m/main")
assert local == ["11111111 local commit"]
assert remote == ["22222222 remote commit"]
project.work_git.rev_list.assert_called_once_with(
"--left-right",
"--abbrev=8",
"--abbrev-commit",
"--pretty=oneline",
"HEAD...refs/remotes/m/main",
"--",
)
def test_diff_commits_falls_back_to_bare_git_when_no_worktree() -> None:
"""Bare or worktree-less projects fall back to bare_git for history walk."""
project = mock.MagicMock()
project.work_git = None
project.bare_git.rev_list.return_value = [
"<11111111 local commit",
">22222222 remote commit",
]
local, remote = info.Info._GetDiffCommits(project, "refs/remotes/m/main")
assert local == ["11111111 local commit"]
assert remote == ["22222222 remote commit"]
project.bare_git.rev_list.assert_called_once_with(
"--left-right",
"--abbrev=8",
"--abbrev-commit",
"--pretty=oneline",
"HEAD...refs/remotes/m/main",
"--",
)
def test_diff_commits_empty_output() -> None:
"""Empty rev-list output produces empty local and remote commit lists."""
project = mock.MagicMock()
project.work_git.rev_list.return_value = []
local, remote = info.Info._GetDiffCommits(project, "refs/remotes/m/main")
assert local == []
assert remote == []
def test_get_current_branch() -> None:
"""_GetCurrentBranch identifies the branch with current=True."""
b1 = mock.MagicMock(current=False)
b2 = mock.MagicMock(current=True)
assert info.Info._GetCurrentBranch({"b1": b1, "b2": b2}) == "b2"
assert info.Info._GetCurrentBranch({"b1": b1}) is None
assert info.Info._GetCurrentBranch({}) is None
def test_overview_helper_current_branch_filters_before_uploadable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""_OverviewHelper only checks uploadable state for the current branch."""
project = mock.MagicMock()
project.RelPath.return_value = "proj"
b1 = mock.MagicMock(current=False)
b2 = mock.MagicMock(current=True)
project.GetBranches.return_value = {"b1": b1, "b2": b2}
uploadable = mock.MagicMock(commits=["c1"], date="2026-09-21")
uploadable.name = "b2"
project.GetUploadableBranch.return_value = uploadable
monkeypatch.setattr(
info.Info,
"get_parallel_context",
lambda: {"projects": [project]},
)
opt = mock.MagicMock(current_branch=True, this_manifest_only=False)
result = info.Info._OverviewHelper(0, opt)
project.GetUploadableBranch.assert_called_once_with("b2")
assert len(result) == 1
assert result[0].name == "b2"
assert result[0].is_current is True
def test_overview_helper_current_branch_detached_head_skips_uploadable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""_OverviewHelper skips GetUploadableBranch when detached with -b."""
project = mock.MagicMock()
b1 = mock.MagicMock(current=False)
project.GetBranches.return_value = {"b1": b1}
monkeypatch.setattr(
info.Info,
"get_parallel_context",
lambda: {"projects": [project]},
)
opt = mock.MagicMock(current_branch=True, this_manifest_only=False)
result = info.Info._OverviewHelper(0, opt)
project.GetUploadableBranch.assert_not_called()
assert result == []