multi-workspace 3.2.9__tar.gz → 3.2.11__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.9 → multi_workspace-3.2.11}/PKG-INFO +1 -1
  2. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/_version.py +2 -2
  3. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/app_api.py +84 -0
  4. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/branch.py +15 -6
  5. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/git_helpers.py +6 -6
  6. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/.gitignore +0 -0
  7. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/LICENSE +0 -0
  8. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/README.md +0 -0
  9. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/__init__.py +0 -0
  10. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/__main__.py +0 -0
  11. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/add.py +0 -0
  12. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/api.py +0 -0
  13. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/bootstrap.py +0 -0
  14. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/cli.py +0 -0
  15. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/cli_helpers.py +0 -0
  16. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/collaborator.py +0 -0
  17. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/convert_monorepo.py +0 -0
  18. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/doctor.py +0 -0
  19. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/errors.py +0 -0
  20. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/git_run.py +0 -0
  21. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/git_set_branch.py +0 -0
  22. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/ignore_files.py +0 -0
  23. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/init.py +0 -0
  24. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/logging.py +0 -0
  25. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/managed_blocks.py +0 -0
  26. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/open.py +0 -0
  27. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/paths.py +0 -0
  28. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/registry.py +0 -0
  29. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/remove.py +0 -0
  30. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/repo_urls.py +0 -0
  31. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/repos.py +0 -0
  32. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/resources/init_readme.md +0 -0
  33. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/service.py +0 -0
  34. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/settings.py +0 -0
  35. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/sync.py +0 -0
  36. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/sync_agents.py +0 -0
  37. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/sync_github.py +0 -0
  38. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/sync_vscode.py +0 -0
  39. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/sync_vscode_devcontainer.py +0 -0
  40. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/sync_vscode_extensions.py +0 -0
  41. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/sync_vscode_helpers.py +0 -0
  42. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/sync_vscode_launch.py +0 -0
  43. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/sync_vscode_settings.py +0 -0
  44. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/sync_vscode_tasks.py +0 -0
  45. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/utils.py +0 -0
  46. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/multi/worktree.py +0 -0
  47. {multi_workspace-3.2.9 → multi_workspace-3.2.11}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-workspace
3
- Version: 3.2.9
3
+ Version: 3.2.11
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.9'
22
- __version_tuple__ = version_tuple = (3, 2, 9)
21
+ __version__ = version = '3.2.11'
22
+ __version_tuple__ = version_tuple = (3, 2, 11)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -50,6 +50,12 @@ class SubRepoPayload(TypedDict):
50
50
  path: str
51
51
 
52
52
 
53
+ class RepoBranchPayload(TypedDict):
54
+ name: str
55
+ path: str
56
+ branch: str
57
+
58
+
53
59
  def _run_git(
54
60
  args: list[str],
55
61
  cwd: Path,
@@ -120,6 +126,81 @@ def _subrepo_payloads(repo_path: Path) -> list[SubRepoPayload] | None:
120
126
  ]
121
127
 
122
128
 
129
+ def _describe_git_head(repo_path: Path) -> str | None:
130
+ if not repo_path.exists():
131
+ return None
132
+
133
+ if not (repo_path / ".git").exists():
134
+ return None
135
+
136
+ if not is_git_repo_root(repo_path):
137
+ return None
138
+
139
+ try:
140
+ branch = _run_git(
141
+ ["symbolic-ref", "--quiet", "--short", "HEAD"],
142
+ cwd=repo_path,
143
+ timeout=10,
144
+ ).stdout.strip()
145
+ if branch:
146
+ return branch
147
+ except (OSError, subprocess.SubprocessError):
148
+ pass
149
+
150
+ try:
151
+ short_sha = _run_git(
152
+ ["rev-parse", "--short", "HEAD"],
153
+ cwd=repo_path,
154
+ timeout=10,
155
+ ).stdout.strip()
156
+ except (OSError, subprocess.SubprocessError):
157
+ return None
158
+
159
+ return f"(detached at {short_sha})" if short_sha else None
160
+
161
+
162
+ def get_project_branches(repo_path: str | Path) -> list[RepoBranchPayload]:
163
+ project_path = Path(repo_path)
164
+ root_name = project_path.name or str(project_path)
165
+ subrepos = _load_subrepos(project_path) or []
166
+ repo_entries = [
167
+ {
168
+ "name": root_name,
169
+ "path": project_path,
170
+ },
171
+ *[
172
+ {
173
+ "name": repo.name,
174
+ "path": Path(repo.path),
175
+ }
176
+ for repo in subrepos
177
+ ],
178
+ ]
179
+
180
+ branch_entries: list[RepoBranchPayload] = []
181
+ seen_paths: set[Path] = set()
182
+ for entry in repo_entries:
183
+ entry_path = entry["path"]
184
+ resolved_path = entry_path.resolve()
185
+ if resolved_path in seen_paths:
186
+ continue
187
+ seen_paths.add(resolved_path)
188
+
189
+ branch = _describe_git_head(entry_path)
190
+ if branch is None:
191
+ continue
192
+
193
+ branch_entries.append(
194
+ {
195
+ "name": str(entry["name"]),
196
+ "path": str(entry_path),
197
+ "branch": branch,
198
+ }
199
+ )
200
+
201
+ return branch_entries
202
+
203
+
123
204
  def _get_git_status_internal(repo_path: Path, ignore_untracked: bool) -> RepoStatus:
124
205
  if not repo_path.exists():
125
206
  return "missing"
@@ -536,6 +617,7 @@ def _project_summary_metadata(repo_path: str | Path) -> dict[str, Any]:
536
617
  "status": get_project_status(project_path),
537
618
  "doctorResult": _doctor_result(project_path),
538
619
  "subRepos": _subrepo_payloads(project_path),
620
+ "branches": get_project_branches(project_path),
539
621
  }
540
622
 
541
623
 
@@ -545,6 +627,7 @@ def _project_summary_fallback_metadata() -> dict[str, Any]:
545
627
  "status": "unknown",
546
628
  "doctorResult": None,
547
629
  "subRepos": None,
630
+ "branches": [],
548
631
  }
549
632
 
550
633
 
@@ -697,6 +780,7 @@ def get_project_detail(repo_path: str | Path) -> dict[str, Any]:
697
780
  "historyGroups": get_combined_history(project_path),
698
781
  "syncStatesByRepoPath": sync_states_by_repo_path,
699
782
  "subRepos": subrepos,
783
+ "branches": get_project_branches(project_path),
700
784
  }
701
785
 
702
786
 
@@ -14,9 +14,10 @@ logger = logging.getLogger(__name__)
14
14
  def report_branches(paths: Paths) -> bool:
15
15
  """Log the current branch of the root repo and each sub-repo.
16
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.
17
+ Read-only: works with dirty working trees, mismatched branches, detached
18
+ HEADs (for example in worktrees created by `multi worktree add`), and
19
+ sub-repos that have not been synced yet (reported as missing).
20
+ Returns True when every repo is present and on its expected branch.
20
21
  """
21
22
  root_branch = describe_head(paths.root_dir)
22
23
  logger.info(f"{paths.root_dir.name} (root): {root_branch}")
@@ -27,7 +28,14 @@ def report_branches(paths: Paths) -> bool:
27
28
 
28
29
  all_match = True
29
30
  for repo in load_repos(paths):
30
- branch = describe_head(repo.path)
31
+ try:
32
+ branch = describe_head(repo.path)
33
+ except GitError:
34
+ # Missing or uninitialized sub-repo: report it and keep going so
35
+ # the rest of the workspace is still listed.
36
+ all_match = False
37
+ logger.warning(f"{repo.name}: (missing — run `multi sync`)")
38
+ continue
31
39
  expected_branch = repo.fixed_branch or root_branch
32
40
  if branch == expected_branch:
33
41
  logger.info(f"{repo.name}: {branch}")
@@ -52,6 +60,7 @@ def branch_cmd() -> None:
52
60
  paths = Paths(Path.cwd())
53
61
  if not report_branches(paths):
54
62
  raise GitError(
55
- "Repositories are not all on their expected branches. "
56
- "Use `multi set-branch` (with clean working trees) to fix."
63
+ "Repositories are missing or not on their expected branches. "
64
+ "Run `multi sync` to clone missing repos, or `multi set-branch` "
65
+ "(with clean working trees) to fix branch mismatches."
57
66
  )
@@ -28,12 +28,12 @@ def _open_repo_for_branch_lookup(repo_path: Path) -> git.Repo:
28
28
  try:
29
29
  return git.Repo(repo_path)
30
30
  except (InvalidGitRepositoryError, NoSuchPathError) as e:
31
- msg = (
32
- f"Could not determine current branch for {repo_path}: not a git repository. "
33
- "Initialize it with `git init -b main` or run `multi sync` from the workspace root."
34
- )
35
- logger.error(msg)
36
- raise GitError(msg) from e
31
+ # No logging here: callers (or the CLI error wrapper) report the
32
+ # error exactly once.
33
+ raise GitError(
34
+ f"Could not determine current branch for {repo_path.name}: not a git "
35
+ "repository. Run `multi sync` from the workspace root."
36
+ ) from e
37
37
 
38
38
 
39
39
  def get_current_branch(repo_path: Path) -> str: