summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--subcmds/info.py84
-rw-r--r--tests/test_subcmds_info.py118
2 files changed, 174 insertions, 28 deletions
diff --git a/subcmds/info.py b/subcmds/info.py
index 594e633a3..e120cae33 100644
--- a/subcmds/info.py
+++ b/subcmds/info.py
@@ -18,7 +18,7 @@ import io
18import json 18import json
19import optparse 19import optparse
20import sys 20import sys
21from typing import Any, Dict, List, NamedTuple 21from typing import Any, Dict, List, NamedTuple, Optional, Tuple
22 22
23from color import Coloring 23from color import Coloring
24from command import DEFAULT_LOCAL_JOBS 24from command import DEFAULT_LOCAL_JOBS
@@ -189,18 +189,26 @@ class Info(PagedCommand):
189 "superproject_revision": srev, 189 "superproject_revision": srev,
190 } 190 }
191 191
192 @staticmethod
193 def _GetCurrentBranch(branches: Dict[str, Any]) -> Optional[str]:
194 """Return the name of the current branch from a GetBranches mapping."""
195 return next(
196 (name for name, branch in branches.items() if branch.current), None
197 )
198
192 @classmethod 199 @classmethod
193 def _getProjectData(cls, project) -> Dict[str, Any]: 200 def _getProjectData(cls, project) -> Dict[str, Any]:
194 """Gather project data as a dict.""" 201 """Gather project data as a dict."""
202 branches = project.GetBranches()
203 currentBranch = cls._GetCurrentBranch(branches)
195 data = { 204 data = {
196 "name": project.name, 205 "name": project.name,
197 "mount_path": project.worktree, 206 "mount_path": project.worktree,
198 "current_revision": project.GetHeadRevisionId() 207 "current_revision": project.GetHeadRevisionId()
199 or project.GetRevisionId(), 208 or project.GetRevisionId(),
200 "manifest_revision": project.revisionExpr, 209 "manifest_revision": project.revisionExpr,
201 "local_branches": list(project.GetBranches()), 210 "local_branches": list(branches),
202 } 211 }
203 currentBranch = project.CurrentBranch
204 if currentBranch: 212 if currentBranch:
205 data["current_branch"] = currentBranch 213 data["current_branch"] = currentBranch
206 return data 214 return data
@@ -285,6 +293,9 @@ class Info(PagedCommand):
285 text = out.nofmt_printer("text") 293 text = out.nofmt_printer("text")
286 dimtext = out.printer("dimtext", attr="dim") 294 dimtext = out.printer("dimtext", attr="dim")
287 295
296 branches = project.GetBranches()
297 currentBranch = cls._GetCurrentBranch(branches)
298
288 heading("Project: ") 299 heading("Project: ")
289 headtext(project.name) 300 headtext(project.name)
290 out.nl() 301 out.nl()
@@ -297,7 +308,6 @@ class Info(PagedCommand):
297 headtext(project.GetHeadRevisionId() or project.GetRevisionId()) 308 headtext(project.GetHeadRevisionId() or project.GetRevisionId())
298 out.nl() 309 out.nl()
299 310
300 currentBranch = project.CurrentBranch
301 if currentBranch: 311 if currentBranch:
302 heading("Current branch: ") 312 heading("Current branch: ")
303 headtext(currentBranch) 313 headtext(currentBranch)
@@ -307,7 +317,7 @@ class Info(PagedCommand):
307 headtext(project.revisionExpr) 317 headtext(project.revisionExpr)
308 out.nl() 318 out.nl()
309 319
310 localBranches = list(project.GetBranches().keys()) 320 localBranches = list(branches)
311 heading("Local Branches: ") 321 heading("Local Branches: ")
312 redtext(str(len(localBranches))) 322 redtext(str(len(localBranches)))
313 if localBranches: 323 if localBranches:
@@ -327,25 +337,10 @@ class Info(PagedCommand):
327 branch = branch[len(R_HEADS) :] 337 branch = branch[len(R_HEADS) :]
328 logTarget = R_M + branch 338 logTarget = R_M + branch
329 339
330 bareTmp = project.bare_git._bare 340 localCommits, originCommits = cls._GetDiffCommits(
331 project.bare_git._bare = False 341 project, logTarget
332 localCommits = project.bare_git.rev_list(
333 "--abbrev=8",
334 "--abbrev-commit",
335 "--pretty=oneline",
336 logTarget + "..",
337 "--",
338 ) 342 )
339 343
340 originCommits = project.bare_git.rev_list(
341 "--abbrev=8",
342 "--abbrev-commit",
343 "--pretty=oneline",
344 ".." + logTarget,
345 "--",
346 )
347 project.bare_git._bare = bareTmp
348
349 heading("Local Commits: ") 344 heading("Local Commits: ")
350 redtext(str(len(localCommits))) 345 redtext(str(len(localCommits)))
351 dimtext(" (on current branch)") 346 dimtext(" (on current branch)")
@@ -375,6 +370,35 @@ class Info(PagedCommand):
375 370
376 return buf.getvalue() 371 return buf.getvalue()
377 372
373 @classmethod
374 def _GetDiffCommits(
375 cls, project: Any, log_target: str
376 ) -> Tuple[List[str], List[str]]:
377 """Return local-only and remote-only commits from one history walk."""
378 git = getattr(project, "work_git", None) or getattr(
379 project, "bare_git", None
380 )
381 if git is None:
382 return [], []
383 commits = git.rev_list(
384 "--left-right",
385 "--abbrev=8",
386 "--abbrev-commit",
387 "--pretty=oneline",
388 f"HEAD...{log_target}",
389 "--",
390 )
391 if isinstance(commits, str):
392 commits = commits.splitlines()
393 local = []
394 remote = []
395 for commit in commits:
396 if commit.startswith("<"):
397 local.append(commit[1:])
398 elif commit.startswith(">"):
399 remote.append(commit[1:])
400 return local, remote
401
378 def _printDiffInfo(self, opt, args): 402 def _printDiffInfo(self, opt, args):
379 projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only) 403 projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
380 404
@@ -404,10 +428,18 @@ class Info(PagedCommand):
404 project = cls.get_parallel_context()["projects"][project_idx] 428 project = cls.get_parallel_context()["projects"][project_idx]
405 429
406 branches = [] 430 branches = []
407 br = [project.GetUploadableBranch(x) for x in project.GetBranches()] 431 local_branches = project.GetBranches()
408 br = [x for x in br if x] 432 current_branch = cls._GetCurrentBranch(local_branches)
409 if opt.current_branch: 433 if opt.current_branch:
410 br = [x for x in br if x.name == project.CurrentBranch] 434 candidate_branches = (
435 [current_branch]
436 if current_branch and current_branch in local_branches
437 else []
438 )
439 else:
440 candidate_branches = local_branches
441 br = [project.GetUploadableBranch(x) for x in candidate_branches]
442 br = [x for x in br if x]
411 443
412 for b in br: 444 for b in br:
413 branches.append( 445 branches.append(
@@ -416,7 +448,7 @@ class Info(PagedCommand):
416 name=b.name, 448 name=b.name,
417 commits=b.commits, 449 commits=b.commits,
418 date=b.date, 450 date=b.date,
419 is_current=b.name == project.CurrentBranch, 451 is_current=b.name == current_branch,
420 ) 452 )
421 ) 453 )
422 return branches 454 return branches
diff --git a/tests/test_subcmds_info.py b/tests/test_subcmds_info.py
index 922b1088c..aa69b7b48 100644
--- a/tests/test_subcmds_info.py
+++ b/tests/test_subcmds_info.py
@@ -208,7 +208,7 @@ def test_get_project_data_uses_head_revision() -> None:
208 project.name = "foo" 208 project.name = "foo"
209 project.worktree = "/path/to/foo" 209 project.worktree = "/path/to/foo"
210 project.revisionExpr = "refs/heads/main" 210 project.revisionExpr = "refs/heads/main"
211 project.GetBranches.return_value = [] 211 project.GetBranches.return_value = {}
212 212
213 # GetHeadRevisionId() returns a SHA, it should be used. 213 # GetHeadRevisionId() returns a SHA, it should be used.
214 project.GetHeadRevisionId.return_value = "head_sha_12345" 214 project.GetHeadRevisionId.return_value = "head_sha_12345"
@@ -235,7 +235,9 @@ def test_json_with_projects(capsys) -> None:
235 project.name = "foo" 235 project.name = "foo"
236 project.worktree = "/path/to/foo" 236 project.worktree = "/path/to/foo"
237 project.revisionExpr = "refs/heads/main" 237 project.revisionExpr = "refs/heads/main"
238 project.GetBranches.return_value = {"branch1": mock.MagicMock()} 238 branch = mock.MagicMock()
239 branch.current = True
240 project.GetBranches.return_value = {"branch1": branch}
239 project.GetHeadRevisionId.return_value = "head_sha_12345" 241 project.GetHeadRevisionId.return_value = "head_sha_12345"
240 project.CurrentBranch = "branch1" 242 project.CurrentBranch = "branch1"
241 243
@@ -253,3 +255,115 @@ def test_json_with_projects(capsys) -> None:
253 assert project_data["manifest_revision"] == "refs/heads/main" 255 assert project_data["manifest_revision"] == "refs/heads/main"
254 assert project_data["local_branches"] == ["branch1"] 256 assert project_data["local_branches"] == ["branch1"]
255 assert project_data["current_branch"] == "branch1" 257 assert project_data["current_branch"] == "branch1"
258
259
260def test_diff_commits_uses_one_left_right_walk() -> None:
261 """Local and remote commits are partitioned from one rev-list."""
262 project = mock.MagicMock()
263 project.work_git.rev_list.return_value = [
264 "<11111111 local commit",
265 ">22222222 remote commit",
266 ]
267
268 local, remote = info.Info._GetDiffCommits(project, "refs/remotes/m/main")
269
270 assert local == ["11111111 local commit"]
271 assert remote == ["22222222 remote commit"]
272 project.work_git.rev_list.assert_called_once_with(
273 "--left-right",
274 "--abbrev=8",
275 "--abbrev-commit",
276 "--pretty=oneline",
277 "HEAD...refs/remotes/m/main",
278 "--",
279 )
280
281
282def test_diff_commits_falls_back_to_bare_git_when_no_worktree() -> None:
283 """Bare or worktree-less projects fall back to bare_git for history walk."""
284 project = mock.MagicMock()
285 project.work_git = None
286 project.bare_git.rev_list.return_value = [
287 "<11111111 local commit",
288 ">22222222 remote commit",
289 ]
290
291 local, remote = info.Info._GetDiffCommits(project, "refs/remotes/m/main")
292
293 assert local == ["11111111 local commit"]
294 assert remote == ["22222222 remote commit"]
295 project.bare_git.rev_list.assert_called_once_with(
296 "--left-right",
297 "--abbrev=8",
298 "--abbrev-commit",
299 "--pretty=oneline",
300 "HEAD...refs/remotes/m/main",
301 "--",
302 )
303
304
305def test_diff_commits_empty_output() -> None:
306 """Empty rev-list output produces empty local and remote commit lists."""
307 project = mock.MagicMock()
308 project.work_git.rev_list.return_value = []
309
310 local, remote = info.Info._GetDiffCommits(project, "refs/remotes/m/main")
311
312 assert local == []
313 assert remote == []
314
315
316def test_get_current_branch() -> None:
317 """_GetCurrentBranch identifies the branch with current=True."""
318 b1 = mock.MagicMock(current=False)
319 b2 = mock.MagicMock(current=True)
320 assert info.Info._GetCurrentBranch({"b1": b1, "b2": b2}) == "b2"
321 assert info.Info._GetCurrentBranch({"b1": b1}) is None
322 assert info.Info._GetCurrentBranch({}) is None
323
324
325def test_overview_helper_current_branch_filters_before_uploadable(
326 monkeypatch: pytest.MonkeyPatch,
327) -> None:
328 """_OverviewHelper only checks uploadable state for the current branch."""
329 project = mock.MagicMock()
330 project.RelPath.return_value = "proj"
331 b1 = mock.MagicMock(current=False)
332 b2 = mock.MagicMock(current=True)
333 project.GetBranches.return_value = {"b1": b1, "b2": b2}
334 uploadable = mock.MagicMock(commits=["c1"], date="2026-09-21")
335 uploadable.name = "b2"
336 project.GetUploadableBranch.return_value = uploadable
337 monkeypatch.setattr(
338 info.Info,
339 "get_parallel_context",
340 lambda: {"projects": [project]},
341 )
342 opt = mock.MagicMock(current_branch=True, this_manifest_only=False)
343
344 result = info.Info._OverviewHelper(0, opt)
345
346 project.GetUploadableBranch.assert_called_once_with("b2")
347 assert len(result) == 1
348 assert result[0].name == "b2"
349 assert result[0].is_current is True
350
351
352def test_overview_helper_current_branch_detached_head_skips_uploadable(
353 monkeypatch: pytest.MonkeyPatch,
354) -> None:
355 """_OverviewHelper skips GetUploadableBranch when detached with -b."""
356 project = mock.MagicMock()
357 b1 = mock.MagicMock(current=False)
358 project.GetBranches.return_value = {"b1": b1}
359 monkeypatch.setattr(
360 info.Info,
361 "get_parallel_context",
362 lambda: {"projects": [project]},
363 )
364 opt = mock.MagicMock(current_branch=True, this_manifest_only=False)
365
366 result = info.Info._OverviewHelper(0, opt)
367
368 project.GetUploadableBranch.assert_not_called()
369 assert result == []