mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-07-15 21:27:00 +00:00
status: Show ahead/behind info for local branches
When viewing `repo status`, it is difficult to distinguish between branches that have active unpushed changes and stale branches that are fully synced. Previously, developers had to run commands like `repo forall -c "git status"` to see their ahead/behind counts. This change updates Project.PrintWorkTreeStatus to automatically calculate and display the number of commits a branch is ahead and/or behind its upstream tracking branch. We use `git rev-list --left-right --count` to fetch this information natively and efficiently. If the branch is completely synced with upstream, no extra text is shown. Added tests for ahead-only, behind-only, diverged, no-tracking, and fully-synced branch states. Bug: 319412954 Change-Id: I23879b2d472c7a7e11d01b565428a84b1b4f09c1 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/602423 Reviewed-by: Gavin Mak <gavinmak@google.com> Tested-by: Brian Gan <brgan@google.com> Commit-Queue: Brian Gan <brgan@google.com>
This commit is contained in:
committed by
gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
91986011b0
commit
e7cac4bca6
@@ -68,6 +68,15 @@ branch devwork
|
||||
\fB\-m\fR
|
||||
subcmds/status.py
|
||||
.PP
|
||||
If the branch is tracking an upstream branch, the number of commits ahead and/or
|
||||
behind is also shown:
|
||||
.TP
|
||||
project repo/
|
||||
branch devwork [ahead 1, behind 2]
|
||||
.TP
|
||||
\fB\-m\fR
|
||||
subcmds/status.py
|
||||
.PP
|
||||
The first column explains how the staging area (index) differs from the last
|
||||
commit (HEAD). Its values are always displayed in upper case and have the
|
||||
following meanings:
|
||||
|
||||
+24
-3
@@ -996,11 +996,32 @@ class Project:
|
||||
out.nl()
|
||||
return "DIRTY"
|
||||
|
||||
branch = self.CurrentBranch
|
||||
if branch is None:
|
||||
branch_name = self.CurrentBranch
|
||||
if branch_name is None:
|
||||
out.nobranch("(*** NO BRANCH ***)")
|
||||
else:
|
||||
out.branch("branch %s", branch)
|
||||
branch_obj = self.GetBranch(branch_name)
|
||||
ahead_behind = ""
|
||||
try:
|
||||
local_merge = branch_obj.LocalMerge
|
||||
if local_merge:
|
||||
left_right = self.work_git.rev_list(
|
||||
"--left-right",
|
||||
"--count",
|
||||
f"{local_merge}...{R_HEADS}{branch_name}",
|
||||
)
|
||||
left, right = left_right[0].split()
|
||||
behind = int(left)
|
||||
ahead = int(right)
|
||||
if ahead and behind:
|
||||
ahead_behind = f" [ahead {ahead}, behind {behind}]"
|
||||
elif ahead:
|
||||
ahead_behind = f" [ahead {ahead}]"
|
||||
elif behind:
|
||||
ahead_behind = f" [behind {behind}]"
|
||||
except GitError:
|
||||
pass
|
||||
out.branch("branch %s%s", branch_name, ahead_behind)
|
||||
out.nl()
|
||||
|
||||
if rb:
|
||||
|
||||
@@ -54,6 +54,12 @@ project 'repo' on branch 'devwork':
|
||||
project repo/ branch devwork
|
||||
-m subcmds/status.py
|
||||
|
||||
If the branch is tracking an upstream branch, the number of commits
|
||||
ahead and/or behind is also shown:
|
||||
|
||||
project repo/ branch devwork [ahead 1, behind 2]
|
||||
-m subcmds/status.py
|
||||
|
||||
The first column explains how the staging area (index) differs from
|
||||
the last commit (HEAD). Its values are always displayed in upper
|
||||
case and have the following meanings:
|
||||
|
||||
@@ -107,6 +107,18 @@ def _assert_project_header(line: str, project_path: str, branch: str) -> None:
|
||||
assert line == expected
|
||||
|
||||
|
||||
def _assert_project_header_with_ahead_behind(
|
||||
line: str,
|
||||
project_path: str,
|
||||
branch: str,
|
||||
ahead_behind: str,
|
||||
) -> None:
|
||||
"""Assert a status project header line includes ahead/behind info."""
|
||||
suffix = f"branch {branch}{ahead_behind}"
|
||||
expected = f"project {(project_path + '/ '):<40}{suffix}"
|
||||
assert line == expected
|
||||
|
||||
|
||||
def _assert_orphan_block(lines: List[str], expected: List[str]) -> None:
|
||||
"""Assert orphan block header and entries, independent of entry ordering."""
|
||||
assert lines
|
||||
@@ -218,3 +230,224 @@ def test_empty_status_after_start_shows_started_branch(
|
||||
lines = _status_lines(stdout.getvalue())
|
||||
assert len(lines) == 1
|
||||
_assert_project_header(lines[0], project_path, started_branch)
|
||||
|
||||
|
||||
def _setup_remote_tracking_branch(
|
||||
manifest: manifest_xml.XmlManifest,
|
||||
branch_name: str,
|
||||
) -> None:
|
||||
"""Create a branch tracking a remote ref, like ``repo start``.
|
||||
|
||||
In a real repo checkout, ``repo start`` creates a branch that
|
||||
tracks a remote tracking ref (e.g. refs/remotes/origin/main).
|
||||
This sets up the same config in both the worktree and the
|
||||
project gitdir (where repo reads its config from).
|
||||
"""
|
||||
proj = list(manifest.paths.values())[0]
|
||||
worktree = Path(proj.worktree)
|
||||
proj_gitdir = Path(proj.gitdir)
|
||||
|
||||
# Create the remote tracking ref in the worktree.
|
||||
subprocess.check_call(
|
||||
["git", "update-ref", "refs/remotes/origin/main", "main"],
|
||||
cwd=worktree,
|
||||
)
|
||||
# Create the new branch from main in the worktree.
|
||||
subprocess.check_call(
|
||||
["git", "checkout", "-q", "-b", branch_name, "main"],
|
||||
cwd=worktree,
|
||||
)
|
||||
# Write remote and branch config into the *project gitdir* config,
|
||||
# which is where repo's Project.config reads from.
|
||||
cfg = str(proj_gitdir / "config")
|
||||
subprocess.check_call(
|
||||
[
|
||||
"git",
|
||||
"config",
|
||||
"-f",
|
||||
cfg,
|
||||
"remote.origin.url",
|
||||
"http://localhost/fake",
|
||||
],
|
||||
)
|
||||
subprocess.check_call(
|
||||
[
|
||||
"git",
|
||||
"config",
|
||||
"-f",
|
||||
cfg,
|
||||
"remote.origin.fetch",
|
||||
"+refs/heads/*:refs/remotes/origin/*",
|
||||
],
|
||||
)
|
||||
subprocess.check_call(
|
||||
["git", "config", "-f", cfg, f"branch.{branch_name}.remote", "origin"],
|
||||
)
|
||||
subprocess.check_call(
|
||||
[
|
||||
"git",
|
||||
"config",
|
||||
"-f",
|
||||
cfg,
|
||||
f"branch.{branch_name}.merge",
|
||||
"refs/heads/main",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_status_branch_ahead_of_upstream(
|
||||
repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
|
||||
) -> None:
|
||||
"""Verify status shows [ahead N] for local commits."""
|
||||
topdir, manifest = repo_client_checkout
|
||||
project_path = next(iter(manifest.paths.keys()))
|
||||
project_worktree = topdir / project_path
|
||||
|
||||
_setup_remote_tracking_branch(manifest, "feature")
|
||||
subprocess.check_call(
|
||||
["git", "commit", "-q", "--allow-empty", "-m", "c1"],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
subprocess.check_call(
|
||||
["git", "commit", "-q", "--allow-empty", "-m", "c2"],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
|
||||
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||
_run_status(manifest, [])
|
||||
|
||||
lines = _status_lines(stdout.getvalue())
|
||||
assert len(lines) == 1
|
||||
_assert_project_header_with_ahead_behind(
|
||||
lines[0], project_path, "feature", " [ahead 2]"
|
||||
)
|
||||
|
||||
|
||||
def test_status_branch_behind_upstream(
|
||||
repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
|
||||
) -> None:
|
||||
"""Verify status shows [behind N] when upstream is ahead."""
|
||||
topdir, manifest = repo_client_checkout
|
||||
project_path = next(iter(manifest.paths.keys()))
|
||||
project_worktree = topdir / project_path
|
||||
|
||||
_setup_remote_tracking_branch(manifest, "feature")
|
||||
# Advance the remote tracking ref past the feature branch.
|
||||
subprocess.check_call(
|
||||
["git", "checkout", "-q", "main"], cwd=project_worktree
|
||||
)
|
||||
subprocess.check_call(
|
||||
["git", "commit", "-q", "--allow-empty", "-m", "upstream"],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
subprocess.check_call(
|
||||
["git", "update-ref", "refs/remotes/origin/main", "main"],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
subprocess.check_call(
|
||||
["git", "checkout", "-q", "feature"],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
|
||||
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||
_run_status(manifest, [])
|
||||
|
||||
lines = _status_lines(stdout.getvalue())
|
||||
assert len(lines) == 1
|
||||
_assert_project_header_with_ahead_behind(
|
||||
lines[0], project_path, "feature", " [behind 1]"
|
||||
)
|
||||
|
||||
|
||||
def test_status_branch_ahead_and_behind(
|
||||
repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
|
||||
) -> None:
|
||||
"""Verify [ahead N, behind M] when branch has diverged."""
|
||||
topdir, manifest = repo_client_checkout
|
||||
project_path = next(iter(manifest.paths.keys()))
|
||||
project_worktree = topdir / project_path
|
||||
|
||||
_setup_remote_tracking_branch(manifest, "feature")
|
||||
# Add a local commit on feature.
|
||||
subprocess.check_call(
|
||||
["git", "commit", "-q", "--allow-empty", "-m", "local"],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
# Advance the remote tracking ref independently.
|
||||
subprocess.check_call(
|
||||
["git", "checkout", "-q", "main"], cwd=project_worktree
|
||||
)
|
||||
subprocess.check_call(
|
||||
["git", "commit", "-q", "--allow-empty", "-m", "upstream"],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
subprocess.check_call(
|
||||
["git", "update-ref", "refs/remotes/origin/main", "main"],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
subprocess.check_call(
|
||||
["git", "checkout", "-q", "feature"],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
|
||||
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||
_run_status(manifest, [])
|
||||
|
||||
lines = _status_lines(stdout.getvalue())
|
||||
assert len(lines) == 1
|
||||
_assert_project_header_with_ahead_behind(
|
||||
lines[0],
|
||||
project_path,
|
||||
"feature",
|
||||
" [ahead 1, behind 1]",
|
||||
)
|
||||
|
||||
|
||||
def test_status_branch_no_tracking_no_ahead_behind(
|
||||
repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
|
||||
) -> None:
|
||||
"""Verify no ahead/behind when branch has no upstream."""
|
||||
topdir, manifest = repo_client_checkout
|
||||
project_path = next(iter(manifest.paths.keys()))
|
||||
project_worktree = topdir / project_path
|
||||
|
||||
subprocess.check_call(
|
||||
[
|
||||
"git",
|
||||
"checkout",
|
||||
"-q",
|
||||
"-b",
|
||||
"orphan-branch",
|
||||
"--no-track",
|
||||
"main",
|
||||
],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
subprocess.check_call(
|
||||
["git", "commit", "-q", "--allow-empty", "-m", "c1"],
|
||||
cwd=project_worktree,
|
||||
)
|
||||
|
||||
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||
_run_status(manifest, [])
|
||||
|
||||
lines = _status_lines(stdout.getvalue())
|
||||
assert len(lines) == 1
|
||||
_assert_project_header(lines[0], project_path, "orphan-branch")
|
||||
|
||||
|
||||
def test_status_branch_synced_no_ahead_behind(
|
||||
repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
|
||||
) -> None:
|
||||
"""Verify no ahead/behind when branch is fully synced."""
|
||||
topdir, manifest = repo_client_checkout
|
||||
project_path = next(iter(manifest.paths.keys()))
|
||||
|
||||
_setup_remote_tracking_branch(manifest, "synced")
|
||||
|
||||
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||
_run_status(manifest, [])
|
||||
|
||||
lines = _status_lines(stdout.getvalue())
|
||||
assert len(lines) == 1
|
||||
_assert_project_header(lines[0], project_path, "synced")
|
||||
|
||||
Reference in New Issue
Block a user