multi-workspace 3.2.8__tar.gz → 3.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/PKG-INFO +1 -1
  2. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/_version.py +2 -2
  3. multi_workspace-3.2.9/multi/branch.py +57 -0
  4. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/cli.py +2 -0
  5. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/cli_helpers.py +3 -1
  6. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/git_helpers.py +31 -11
  7. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/git_run.py +5 -1
  8. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/.gitignore +0 -0
  9. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/LICENSE +0 -0
  10. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/README.md +0 -0
  11. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/__init__.py +0 -0
  12. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/__main__.py +0 -0
  13. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/add.py +0 -0
  14. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/api.py +0 -0
  15. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/app_api.py +0 -0
  16. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/bootstrap.py +0 -0
  17. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/collaborator.py +0 -0
  18. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/convert_monorepo.py +0 -0
  19. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/doctor.py +0 -0
  20. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/errors.py +0 -0
  21. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/git_set_branch.py +0 -0
  22. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/ignore_files.py +0 -0
  23. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/init.py +0 -0
  24. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/logging.py +0 -0
  25. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/managed_blocks.py +0 -0
  26. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/open.py +0 -0
  27. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/paths.py +0 -0
  28. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/registry.py +0 -0
  29. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/remove.py +0 -0
  30. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/repo_urls.py +0 -0
  31. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/repos.py +0 -0
  32. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/resources/init_readme.md +0 -0
  33. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/service.py +0 -0
  34. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/settings.py +0 -0
  35. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/sync.py +0 -0
  36. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/sync_agents.py +0 -0
  37. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/sync_github.py +0 -0
  38. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/sync_vscode.py +0 -0
  39. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/sync_vscode_devcontainer.py +0 -0
  40. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/sync_vscode_extensions.py +0 -0
  41. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/sync_vscode_helpers.py +0 -0
  42. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/sync_vscode_launch.py +0 -0
  43. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/sync_vscode_settings.py +0 -0
  44. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/sync_vscode_tasks.py +0 -0
  45. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/utils.py +0 -0
  46. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/multi/worktree.py +0 -0
  47. {multi_workspace-3.2.8 → multi_workspace-3.2.9}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-workspace
3
- Version: 3.2.8
3
+ Version: 3.2.9
4
4
  Summary: Multi
5
5
  Project-URL: Repository, https://github.com/gabemontague/multi
6
6
  Project-URL: Issues, https://github.com/gabemontague/multi/issues
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '3.2.8'
22
- __version_tuple__ = version_tuple = (3, 2, 8)
21
+ __version__ = version = '3.2.9'
22
+ __version_tuple__ = version_tuple = (3, 2, 9)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,57 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import click
5
+
6
+ from multi.errors import GitError
7
+ from multi.git_helpers import describe_head
8
+ from multi.paths import Paths
9
+ from multi.repos import load_repos
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def report_branches(paths: Paths) -> bool:
15
+ """Log the current branch of the root repo and each sub-repo.
16
+
17
+ Read-only: works with dirty working trees, mismatched branches, and
18
+ detached HEADs (for example in worktrees created by `multi worktree add`).
19
+ Returns True when every repo is on its expected branch.
20
+ """
21
+ root_branch = describe_head(paths.root_dir)
22
+ logger.info(f"{paths.root_dir.name} (root): {root_branch}")
23
+
24
+ if paths.settings.is_monorepo():
25
+ # Sub-repos are part of the root repo in monorepo mode.
26
+ return True
27
+
28
+ all_match = True
29
+ for repo in load_repos(paths):
30
+ branch = describe_head(repo.path)
31
+ expected_branch = repo.fixed_branch or root_branch
32
+ if branch == expected_branch:
33
+ logger.info(f"{repo.name}: {branch}")
34
+ else:
35
+ all_match = False
36
+ if repo.fixed_branch:
37
+ expectation = f"fixed branch {expected_branch}"
38
+ else:
39
+ expectation = f"root branch {root_branch}"
40
+ logger.warning(f"{repo.name}: {branch} (expected {expectation})")
41
+ return all_match
42
+
43
+
44
+ @click.command(name="branch")
45
+ def branch_cmd() -> None:
46
+ """Show the current branch of the root repo and every sub-repo.
47
+
48
+ Read-only: unlike `multi set-branch` and `multi git`, this works with
49
+ dirty working trees, mismatched branches, and detached HEADs.
50
+ Exits with an error status when repos are not on their expected branches.
51
+ """
52
+ paths = Paths(Path.cwd())
53
+ if not report_branches(paths):
54
+ raise GitError(
55
+ "Repositories are not all on their expected branches. "
56
+ "Use `multi set-branch` (with clean working trees) to fix."
57
+ )
@@ -2,6 +2,7 @@ import click
2
2
 
3
3
  from multi._version import __version__
4
4
  from multi.add import add_cmd
5
+ from multi.branch import branch_cmd
5
6
  from multi.cli_helpers import common_command_wrapper
6
7
  from multi.collaborator import collaborator_cmd
7
8
  from multi.convert_monorepo import convert_monorepo_cmd
@@ -46,6 +47,7 @@ def main():
46
47
 
47
48
  main.add_command(common_command_wrapper(add_cmd))
48
49
  main.add_command(collaborator_cmd)
50
+ main.add_command(common_command_wrapper(branch_cmd))
49
51
  main.add_command(common_command_wrapper(set_branch_cmd))
50
52
  main.add_command(common_command_wrapper(sync_cmd))
51
53
  main.add_command(common_command_wrapper(git_cmd))
@@ -11,7 +11,9 @@ from multi.git_helpers import check_all_on_same_branch
11
11
  from multi.logging import configure_logging
12
12
  from multi.paths import Paths
13
13
 
14
- COMMANDS_WITHOUT_WORKSPACE_BRANCH_CHECK = {"doctor", "recent-users"}
14
+ # `branch` is excluded because it reports branch state itself (including
15
+ # mismatches); re-running the post-command check would duplicate the output.
16
+ COMMANDS_WITHOUT_WORKSPACE_BRANCH_CHECK = {"branch", "doctor", "recent-users"}
15
17
 
16
18
 
17
19
  def get_install_set_from_context() -> str | None:
@@ -24,11 +24,9 @@ def is_git_repo_root(repo_path: Path) -> bool:
24
24
  return Path(repo.working_tree_dir or "").resolve() == repo_path.resolve()
25
25
 
26
26
 
27
- def get_current_branch(repo_path: Path) -> str:
28
- """Get the current branch name of a git repository."""
27
+ def _open_repo_for_branch_lookup(repo_path: Path) -> git.Repo:
29
28
  try:
30
- repo = git.Repo(repo_path)
31
- return repo.active_branch.name
29
+ return git.Repo(repo_path)
32
30
  except (InvalidGitRepositoryError, NoSuchPathError) as e:
33
31
  msg = (
34
32
  f"Could not determine current branch for {repo_path}: not a git repository. "
@@ -36,19 +34,41 @@ def get_current_branch(repo_path: Path) -> str:
36
34
  )
37
35
  logger.error(msg)
38
36
  raise GitError(msg) from e
39
- except TypeError:
40
- # Detached HEAD state - active_branch raises TypeError
37
+
38
+
39
+ def get_current_branch(repo_path: Path) -> str:
40
+ """Get the current branch name of a git repository."""
41
+ repo = _open_repo_for_branch_lookup(repo_path)
42
+ if repo.head.is_detached:
43
+ # Detached HEAD state has no branch name.
41
44
  return "HEAD"
45
+ return repo.active_branch.name
46
+
47
+
48
+ def describe_head(repo_path: Path) -> str:
49
+ """Return the current branch name, or a detached-HEAD description.
50
+
51
+ Read-only: works with dirty working trees and in linked worktrees
52
+ (where ``.git`` is a file). A detached HEAD is reported as
53
+ ``(detached at <short-sha>)`` instead of failing.
54
+ """
55
+ repo = _open_repo_for_branch_lookup(repo_path)
56
+ if repo.head.is_detached:
57
+ return f"(detached at {repo.git.rev_parse('--short', 'HEAD')})"
58
+ return repo.active_branch.name
42
59
 
43
60
 
44
61
  def check_all_on_same_branch(paths: Paths, raise_error: bool = True) -> bool:
45
- """Validate that all repositories are on the expected branch."""
62
+ """Validate that all repositories are on the expected branch.
63
+
64
+ Read-only: does not require clean working trees. Detached HEADs are
65
+ reported as ``(detached at <short-sha>)`` and never match an expected
66
+ branch name.
67
+ """
46
68
  from multi.repos import load_repos
47
69
 
48
- root_branch = get_current_branch(paths.root_dir)
49
- repo_branches = [
50
- (repo, get_current_branch(repo.path)) for repo in load_repos(paths)
51
- ]
70
+ root_branch = describe_head(paths.root_dir)
71
+ repo_branches = [(repo, describe_head(repo.path)) for repo in load_repos(paths)]
52
72
  for repo, branch in repo_branches:
53
73
  expected_branch = repo.fixed_branch or root_branch
54
74
  if branch != expected_branch:
@@ -46,7 +46,7 @@ def run_git_in_all_repos(paths: Paths, git_args: List[str]) -> None:
46
46
  run_git_command(repo.path, git_args)
47
47
 
48
48
 
49
- @click.command(name="git")
49
+ @click.command(name="git", context_settings={"ignore_unknown_options": True})
50
50
  @click.argument("git_args", nargs=-1, required=True)
51
51
  def git_cmd(git_args: tuple[str, ...]) -> None:
52
52
  """Run a git command across all repositories.
@@ -55,6 +55,10 @@ def git_cmd(git_args: tuple[str, ...]) -> None:
55
55
 
56
56
  Example: multi git pull
57
57
  multi git checkout -b feature/new-branch
58
+ multi git branch --show-current
59
+
60
+ Requires all repositories to be on the same branch (working trees may be
61
+ dirty). To inspect branches when they mismatch, use `multi branch`.
58
62
  """
59
63
  paths = Paths(Path.cwd())
60
64
  if paths.settings.is_monorepo():
File without changes