multi-workspace 3.2.8__tar.gz → 3.2.10__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.
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/PKG-INFO +1 -1
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/_version.py +2 -2
- multi_workspace-3.2.10/multi/branch.py +66 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/cli.py +2 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/cli_helpers.py +3 -1
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/git_helpers.py +37 -17
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/git_run.py +5 -1
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/.gitignore +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/LICENSE +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/README.md +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/__init__.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/__main__.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/add.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/api.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/app_api.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/bootstrap.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/collaborator.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/convert_monorepo.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/doctor.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/errors.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/git_set_branch.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/ignore_files.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/init.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/logging.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/managed_blocks.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/open.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/paths.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/registry.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/remove.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/repo_urls.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/repos.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/resources/init_readme.md +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/service.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/settings.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/sync.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/sync_agents.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/sync_github.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/sync_vscode.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/sync_vscode_devcontainer.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/sync_vscode_extensions.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/sync_vscode_helpers.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/sync_vscode_launch.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/sync_vscode_settings.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/sync_vscode_tasks.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/utils.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/multi/worktree.py +0 -0
- {multi_workspace-3.2.8 → multi_workspace-3.2.10}/pyproject.toml +0 -0
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (3, 2,
|
|
21
|
+
__version__ = version = '3.2.10'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 2, 10)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,66 @@
|
|
|
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, 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.
|
|
21
|
+
"""
|
|
22
|
+
root_branch = describe_head(paths.root_dir)
|
|
23
|
+
logger.info(f"{paths.root_dir.name} (root): {root_branch}")
|
|
24
|
+
|
|
25
|
+
if paths.settings.is_monorepo():
|
|
26
|
+
# Sub-repos are part of the root repo in monorepo mode.
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
all_match = True
|
|
30
|
+
for repo in load_repos(paths):
|
|
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
|
|
39
|
+
expected_branch = repo.fixed_branch or root_branch
|
|
40
|
+
if branch == expected_branch:
|
|
41
|
+
logger.info(f"{repo.name}: {branch}")
|
|
42
|
+
else:
|
|
43
|
+
all_match = False
|
|
44
|
+
if repo.fixed_branch:
|
|
45
|
+
expectation = f"fixed branch {expected_branch}"
|
|
46
|
+
else:
|
|
47
|
+
expectation = f"root branch {root_branch}"
|
|
48
|
+
logger.warning(f"{repo.name}: {branch} (expected {expectation})")
|
|
49
|
+
return all_match
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@click.command(name="branch")
|
|
53
|
+
def branch_cmd() -> None:
|
|
54
|
+
"""Show the current branch of the root repo and every sub-repo.
|
|
55
|
+
|
|
56
|
+
Read-only: unlike `multi set-branch` and `multi git`, this works with
|
|
57
|
+
dirty working trees, mismatched branches, and detached HEADs.
|
|
58
|
+
Exits with an error status when repos are not on their expected branches.
|
|
59
|
+
"""
|
|
60
|
+
paths = Paths(Path.cwd())
|
|
61
|
+
if not report_branches(paths):
|
|
62
|
+
raise GitError(
|
|
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."
|
|
66
|
+
)
|
|
@@ -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
|
-
|
|
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,31 +24,51 @@ 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
|
|
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
|
-
|
|
31
|
-
return repo.active_branch.name
|
|
29
|
+
return git.Repo(repo_path)
|
|
32
30
|
except (InvalidGitRepositoryError, NoSuchPathError) as e:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
|
|
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 =
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|