multi-workspace 3.0.0__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.0.0/.gitignore +185 -0
- multi_workspace-3.0.0/LICENSE +9 -0
- multi_workspace-3.0.0/PKG-INFO +57 -0
- multi_workspace-3.0.0/README.md +32 -0
- multi_workspace-3.0.0/multi/__init__.py +1 -0
- multi_workspace-3.0.0/multi/__main__.py +4 -0
- multi_workspace-3.0.0/multi/_version.py +34 -0
- multi_workspace-3.0.0/multi/cli.py +45 -0
- multi_workspace-3.0.0/multi/cli_helpers.py +79 -0
- multi_workspace-3.0.0/multi/errors.py +22 -0
- multi_workspace-3.0.0/multi/git_helpers.py +109 -0
- multi_workspace-3.0.0/multi/git_run.py +59 -0
- multi_workspace-3.0.0/multi/git_set_branch.py +66 -0
- multi_workspace-3.0.0/multi/ignore_files.py +104 -0
- multi_workspace-3.0.0/multi/init.py +193 -0
- multi_workspace-3.0.0/multi/logging.py +32 -0
- multi_workspace-3.0.0/multi/paths.py +85 -0
- multi_workspace-3.0.0/multi/repos.py +92 -0
- multi_workspace-3.0.0/multi/resources/init_readme.md +11 -0
- multi_workspace-3.0.0/multi/rules.py +100 -0
- multi_workspace-3.0.0/multi/settings.py +35 -0
- multi_workspace-3.0.0/multi/sync.py +89 -0
- multi_workspace-3.0.0/multi/sync_claude.py +88 -0
- multi_workspace-3.0.0/multi/sync_ruff.py +85 -0
- multi_workspace-3.0.0/multi/sync_vscode.py +51 -0
- multi_workspace-3.0.0/multi/sync_vscode_extensions.py +52 -0
- multi_workspace-3.0.0/multi/sync_vscode_helpers.py +169 -0
- multi_workspace-3.0.0/multi/sync_vscode_launch.py +105 -0
- multi_workspace-3.0.0/multi/sync_vscode_settings.py +100 -0
- multi_workspace-3.0.0/multi/sync_vscode_tasks.py +93 -0
- multi_workspace-3.0.0/multi/utils.py +163 -0
- multi_workspace-3.0.0/pyproject.toml +65 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
CLAUDE.md
|
|
2
|
+
.data/
|
|
3
|
+
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
# C extensions
|
|
10
|
+
*.so
|
|
11
|
+
|
|
12
|
+
# Distribution / packaging
|
|
13
|
+
.Python
|
|
14
|
+
build/
|
|
15
|
+
develop-eggs/
|
|
16
|
+
dist/
|
|
17
|
+
downloads/
|
|
18
|
+
eggs/
|
|
19
|
+
.eggs/
|
|
20
|
+
lib/
|
|
21
|
+
lib64/
|
|
22
|
+
parts/
|
|
23
|
+
sdist/
|
|
24
|
+
var/
|
|
25
|
+
wheels/
|
|
26
|
+
share/python-wheels/
|
|
27
|
+
*.egg-info/
|
|
28
|
+
.installed.cfg
|
|
29
|
+
*.egg
|
|
30
|
+
MANIFEST
|
|
31
|
+
|
|
32
|
+
# PyInstaller
|
|
33
|
+
# Usually these files are written by a python script from a template
|
|
34
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
35
|
+
*.manifest
|
|
36
|
+
*.spec
|
|
37
|
+
|
|
38
|
+
# Installer logs
|
|
39
|
+
pip-log.txt
|
|
40
|
+
pip-delete-this-directory.txt
|
|
41
|
+
|
|
42
|
+
# Unit test / coverage reports
|
|
43
|
+
htmlcov/
|
|
44
|
+
.tox/
|
|
45
|
+
.nox/
|
|
46
|
+
.coverage
|
|
47
|
+
.coverage.*
|
|
48
|
+
.cache
|
|
49
|
+
nosetests.xml
|
|
50
|
+
coverage.xml
|
|
51
|
+
*.cover
|
|
52
|
+
*.py,cover
|
|
53
|
+
.hypothesis/
|
|
54
|
+
.pytest_cache/
|
|
55
|
+
cover/
|
|
56
|
+
|
|
57
|
+
# Translations
|
|
58
|
+
*.mo
|
|
59
|
+
*.pot
|
|
60
|
+
|
|
61
|
+
# Django stuff:
|
|
62
|
+
*.log
|
|
63
|
+
local_settings.py
|
|
64
|
+
db.sqlite3
|
|
65
|
+
db.sqlite3-journal
|
|
66
|
+
|
|
67
|
+
# Flask stuff:
|
|
68
|
+
instance/
|
|
69
|
+
.webassets-cache
|
|
70
|
+
|
|
71
|
+
# Scrapy stuff:
|
|
72
|
+
.scrapy
|
|
73
|
+
|
|
74
|
+
# Sphinx documentation
|
|
75
|
+
docs/_build/
|
|
76
|
+
|
|
77
|
+
# PyBuilder
|
|
78
|
+
.pybuilder/
|
|
79
|
+
target/
|
|
80
|
+
|
|
81
|
+
# Jupyter Notebook
|
|
82
|
+
.ipynb_checkpoints
|
|
83
|
+
|
|
84
|
+
# IPython
|
|
85
|
+
profile_default/
|
|
86
|
+
ipython_config.py
|
|
87
|
+
|
|
88
|
+
# pyenv
|
|
89
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
90
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
91
|
+
# .python-version
|
|
92
|
+
|
|
93
|
+
# pipenv
|
|
94
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
95
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
96
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
97
|
+
# install all needed dependencies.
|
|
98
|
+
#Pipfile.lock
|
|
99
|
+
|
|
100
|
+
# poetry
|
|
101
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
102
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
103
|
+
# commonly ignored for libraries.
|
|
104
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
105
|
+
#poetry.lock
|
|
106
|
+
|
|
107
|
+
# pdm
|
|
108
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
109
|
+
#pdm.lock
|
|
110
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
111
|
+
# in version control.
|
|
112
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
113
|
+
.pdm.toml
|
|
114
|
+
|
|
115
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
116
|
+
__pypackages__/
|
|
117
|
+
|
|
118
|
+
# Celery stuff
|
|
119
|
+
celerybeat-schedule
|
|
120
|
+
celerybeat.pid
|
|
121
|
+
|
|
122
|
+
# SageMath parsed files
|
|
123
|
+
*.sage.py
|
|
124
|
+
|
|
125
|
+
# Environments
|
|
126
|
+
.env
|
|
127
|
+
.env.gha
|
|
128
|
+
.venv
|
|
129
|
+
env/
|
|
130
|
+
venv/
|
|
131
|
+
ENV/
|
|
132
|
+
env.bak/
|
|
133
|
+
venv.bak/
|
|
134
|
+
|
|
135
|
+
# Spyder project settings
|
|
136
|
+
.spyderproject
|
|
137
|
+
.spyproject
|
|
138
|
+
|
|
139
|
+
# Rope project settings
|
|
140
|
+
.ropeproject
|
|
141
|
+
|
|
142
|
+
# mkdocs documentation
|
|
143
|
+
/site
|
|
144
|
+
|
|
145
|
+
# mypy
|
|
146
|
+
.mypy_cache/
|
|
147
|
+
.dmypy.json
|
|
148
|
+
dmypy.json
|
|
149
|
+
|
|
150
|
+
# Pyre type checker
|
|
151
|
+
.pyre/
|
|
152
|
+
|
|
153
|
+
# pytype static type analyzer
|
|
154
|
+
.pytype/
|
|
155
|
+
|
|
156
|
+
# Cython debug symbols
|
|
157
|
+
cython_debug/
|
|
158
|
+
|
|
159
|
+
# Ruff stuff:
|
|
160
|
+
.ruff_cache/
|
|
161
|
+
|
|
162
|
+
# PyPI configuration file
|
|
163
|
+
.pypirc
|
|
164
|
+
|
|
165
|
+
# Virtual Environment
|
|
166
|
+
.env
|
|
167
|
+
.venv
|
|
168
|
+
env/
|
|
169
|
+
venv/
|
|
170
|
+
ENV/
|
|
171
|
+
|
|
172
|
+
# IDE
|
|
173
|
+
.idea/
|
|
174
|
+
# .vscode/
|
|
175
|
+
*.swp
|
|
176
|
+
*.swo
|
|
177
|
+
|
|
178
|
+
# OS
|
|
179
|
+
.DS_Store
|
|
180
|
+
Thumbs.db
|
|
181
|
+
|
|
182
|
+
staticfiles
|
|
183
|
+
|
|
184
|
+
.env
|
|
185
|
+
_version.py
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Gabriel Montague
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: multi-workspace
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Multi
|
|
5
|
+
Project-URL: Repository, https://github.com/gabemontague/multi
|
|
6
|
+
Project-URL: Issues, https://github.com/gabemontague/multi/issues
|
|
7
|
+
Author-email: Gabe Montague <gabemontague@outlook.com>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Requires-Dist: click>=8.3.1
|
|
17
|
+
Requires-Dist: gitpython>=3.1.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pyinstaller>=6.9.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-cov>=6.1.1; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8.3.5; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.11.10; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# multi
|
|
27
|
+
|
|
28
|
+
`multi` is the best way to work with VS Code/Cursor on multiple Git repos at once. It is an alternative to [multi-root workspaces](https://code.visualstudio.com/docs/editing/workspaces/multi-root-workspaces) that offers more flexibility and control. With `multi`, you can gain control over how tasks, debug runnables, and various IDE and linter settings are combined from multiple project repos ("sub-repos") located in the same folder.
|
|
29
|
+
|
|
30
|
+
Features:
|
|
31
|
+
|
|
32
|
+
- Generates files in your root `.vscode` folder from sub-repo `launch.json`, `tasks.json`, and `settings.json` files.
|
|
33
|
+
- Generates `CLAUDE.md` files from Cursor rules.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
### Using `pipx`:
|
|
38
|
+
|
|
39
|
+
- Install [pipx](https://github.com/pypa/pipx)
|
|
40
|
+
- Run `pipx install multi-sync`
|
|
41
|
+
|
|
42
|
+
### Using `uv`
|
|
43
|
+
|
|
44
|
+
- Install [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
|
45
|
+
- Run `uv tool install multi-sync`
|
|
46
|
+
|
|
47
|
+
## Getting started
|
|
48
|
+
|
|
49
|
+
To get started, create a new workspace directory that will house all your related repos and run:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
multi init
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
When prompted, paste in the URLs of all the repositories you want to have in your workspace. You can optionally specify descriptions of what they do, which will be used to create a new repo-directories.mdc Cursor/Claude rule.
|
|
56
|
+
|
|
57
|
+
It is recommended you also install the [VS Code Extension](https://marketplace.visualstudio.com/items?itemName=montaguegabe.multi-sync) that automatically keeps your project synced when edits are made to synced files. To manually sync, you can run `multi sync`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# multi
|
|
2
|
+
|
|
3
|
+
`multi` is the best way to work with VS Code/Cursor on multiple Git repos at once. It is an alternative to [multi-root workspaces](https://code.visualstudio.com/docs/editing/workspaces/multi-root-workspaces) that offers more flexibility and control. With `multi`, you can gain control over how tasks, debug runnables, and various IDE and linter settings are combined from multiple project repos ("sub-repos") located in the same folder.
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
|
|
7
|
+
- Generates files in your root `.vscode` folder from sub-repo `launch.json`, `tasks.json`, and `settings.json` files.
|
|
8
|
+
- Generates `CLAUDE.md` files from Cursor rules.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
### Using `pipx`:
|
|
13
|
+
|
|
14
|
+
- Install [pipx](https://github.com/pypa/pipx)
|
|
15
|
+
- Run `pipx install multi-sync`
|
|
16
|
+
|
|
17
|
+
### Using `uv`
|
|
18
|
+
|
|
19
|
+
- Install [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
|
20
|
+
- Run `uv tool install multi-sync`
|
|
21
|
+
|
|
22
|
+
## Getting started
|
|
23
|
+
|
|
24
|
+
To get started, create a new workspace directory that will house all your related repos and run:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
multi init
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
When prompted, paste in the URLs of all the repositories you want to have in your workspace. You can optionally specify descriptions of what they do, which will be used to create a new repo-directories.mdc Cursor/Claude rule.
|
|
31
|
+
|
|
32
|
+
It is recommended you also install the [VS Code Extension](https://marketplace.visualstudio.com/items?itemName=montaguegabe.multi-sync) that automatically keeps your project synced when edits are made to synced files. To manually sync, you can run `multi sync`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '3.0.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (3, 0, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from multi._version import __version__
|
|
4
|
+
from multi.cli_helpers import common_command_wrapper
|
|
5
|
+
from multi.git_run import git_cmd
|
|
6
|
+
from multi.git_set_branch import set_branch_cmd
|
|
7
|
+
from multi.init import init_cmd
|
|
8
|
+
from multi.sync import sync_cmd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def print_version(ctx, param, value):
|
|
12
|
+
if not value or ctx.resilient_parsing:
|
|
13
|
+
return
|
|
14
|
+
click.echo(f"multi (multi-sync) {__version__}")
|
|
15
|
+
ctx.exit()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
@click.option(
|
|
20
|
+
"--version",
|
|
21
|
+
is_flag=True,
|
|
22
|
+
callback=print_version,
|
|
23
|
+
expose_value=False,
|
|
24
|
+
is_eager=True,
|
|
25
|
+
help="Show the version and exit.",
|
|
26
|
+
)
|
|
27
|
+
def main():
|
|
28
|
+
"""VS Code Multi - Manage multiple Git repositories in VS Code.
|
|
29
|
+
|
|
30
|
+
This CLI tool enables seamless work across multiple Git repositories within VS Code.
|
|
31
|
+
Key features:
|
|
32
|
+
- Synchronize Git operations across root and sub-repositories
|
|
33
|
+
- Merge .vscode configurations (launch.json, tasks.json, settings.json)
|
|
34
|
+
- Manage consistent branch states across all repositories
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
main.add_command(common_command_wrapper(set_branch_cmd))
|
|
40
|
+
main.add_command(common_command_wrapper(sync_cmd))
|
|
41
|
+
main.add_command(common_command_wrapper(git_cmd))
|
|
42
|
+
main.add_command(common_command_wrapper(init_cmd))
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from multi.errors import GitError
|
|
10
|
+
from multi.git_helpers import check_all_on_same_branch
|
|
11
|
+
from multi.logging import configure_logging
|
|
12
|
+
from multi.paths import Paths
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def common_command_wrapper(command_to_wrap: click.Command) -> click.Command:
|
|
16
|
+
"""
|
|
17
|
+
Wraps an existing Click command to add common functionality:
|
|
18
|
+
- A --verbose option for detailed logging.
|
|
19
|
+
- Standardized error handling and logging.
|
|
20
|
+
This function modifies the command_to_wrap in-place.
|
|
21
|
+
"""
|
|
22
|
+
original_callback = command_to_wrap.callback
|
|
23
|
+
if not original_callback:
|
|
24
|
+
# This should generally not happen if command_to_wrap is created via @click.command
|
|
25
|
+
raise ValueError(
|
|
26
|
+
f"Command '{command_to_wrap.name or 'Unnamed'}' has no callback to wrap."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@functools.wraps(original_callback)
|
|
30
|
+
def new_wrapped_callback(**kwargs):
|
|
31
|
+
# Pop the verbose flag. It's added by this wrapper to the command's params.
|
|
32
|
+
# Click will pass it in kwargs to this new_callback.
|
|
33
|
+
verbose_value = kwargs.pop("verbose", False)
|
|
34
|
+
|
|
35
|
+
# Configure logging based on verbosity
|
|
36
|
+
log_level = logging.DEBUG if verbose_value else logging.INFO
|
|
37
|
+
configure_logging(level=log_level)
|
|
38
|
+
|
|
39
|
+
exit_code = None
|
|
40
|
+
try:
|
|
41
|
+
# Call the original command's callback with its intended kwargs
|
|
42
|
+
return original_callback(**kwargs)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger = logging.getLogger(__name__) # Get logger after configuration
|
|
45
|
+
logger.error(str(e)) # This will use the emoji formatter
|
|
46
|
+
if verbose_value:
|
|
47
|
+
# For verbose mode, also print traceback directly to stderr
|
|
48
|
+
click.secho("\nDebug traceback:", fg="yellow", err=True)
|
|
49
|
+
click.secho(traceback.format_exc(), fg="yellow", err=True)
|
|
50
|
+
exit_code = 1
|
|
51
|
+
|
|
52
|
+
# After every command, check that all sub-repos are on the same branch as the root repo
|
|
53
|
+
try:
|
|
54
|
+
paths = Paths(Path.cwd())
|
|
55
|
+
check_all_on_same_branch(paths=paths, raise_error=True)
|
|
56
|
+
except GitError as e:
|
|
57
|
+
click.secho(e.args[0], fg="red", err=True)
|
|
58
|
+
|
|
59
|
+
if exit_code is not None:
|
|
60
|
+
sys.exit(exit_code)
|
|
61
|
+
|
|
62
|
+
# Replace the command's callback with our new wrapped version
|
|
63
|
+
command_to_wrap.callback = new_wrapped_callback
|
|
64
|
+
|
|
65
|
+
# Add the --verbose option to the command's parameters, if not already present
|
|
66
|
+
# This ensures the `verbose` kwarg is available in new_wrapped_callback
|
|
67
|
+
if not any(
|
|
68
|
+
isinstance(p, click.Option) and p.name == "verbose"
|
|
69
|
+
for p in command_to_wrap.params
|
|
70
|
+
):
|
|
71
|
+
verbose_option = click.Option(
|
|
72
|
+
["--verbose"],
|
|
73
|
+
is_flag=True,
|
|
74
|
+
help="Enable verbose output.",
|
|
75
|
+
# expose_value=True is default, making 'verbose' a kwarg to the callback
|
|
76
|
+
)
|
|
77
|
+
command_to_wrap.params.append(verbose_option)
|
|
78
|
+
|
|
79
|
+
return command_to_wrap # Return the modified command
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class NoRepositoriesError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GitError(Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RepoNotCleanError(GitError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RulesError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RuleParseError(RulesError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RulesNotCombinableError(RulesError):
|
|
22
|
+
pass
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
import git
|
|
6
|
+
from git.exc import InvalidGitRepositoryError
|
|
7
|
+
|
|
8
|
+
from multi.errors import GitError, RepoNotCleanError
|
|
9
|
+
from multi.paths import Paths
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_git_repo_root(repo_path: Path) -> bool:
|
|
15
|
+
# Will fail for submodules and worktrees, but these aren't used by us
|
|
16
|
+
return (repo_path / ".git").is_dir()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_current_branch(repo_path: Path) -> str:
|
|
20
|
+
"""Get the current branch name of a git repository."""
|
|
21
|
+
try:
|
|
22
|
+
repo = git.Repo(repo_path)
|
|
23
|
+
return repo.active_branch.name
|
|
24
|
+
except InvalidGitRepositoryError as e:
|
|
25
|
+
logger.error("Failed to determine current branch")
|
|
26
|
+
raise GitError("Failed to determine current branch") from e
|
|
27
|
+
except TypeError:
|
|
28
|
+
# Detached HEAD state - active_branch raises TypeError
|
|
29
|
+
return "HEAD"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_all_on_same_branch(paths: Paths, raise_error: bool = True) -> bool:
|
|
33
|
+
"""Validate that all repositories are on the same branch."""
|
|
34
|
+
from multi.repos import load_repos
|
|
35
|
+
|
|
36
|
+
root_branch = get_current_branch(paths.root_dir)
|
|
37
|
+
repo_branches = [
|
|
38
|
+
(repo, get_current_branch(repo.path)) for repo in load_repos(paths)
|
|
39
|
+
]
|
|
40
|
+
for repo, branch in repo_branches:
|
|
41
|
+
if branch != root_branch:
|
|
42
|
+
if raise_error:
|
|
43
|
+
raise GitError(
|
|
44
|
+
f"Repository {repo.name} is not on the same branch as the root repository. Please fix. {repo.name}: {branch}, Root: {root_branch}"
|
|
45
|
+
)
|
|
46
|
+
return False
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_repo_is_clean(repo_path: Path, raise_error: bool = True) -> bool:
|
|
51
|
+
# Check if this is a git repository
|
|
52
|
+
if not is_git_repo_root(repo_path):
|
|
53
|
+
raise GitError(
|
|
54
|
+
f"{repo_path} is not a git repository or has not been initialized properly (no .git folder)"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Make sure we have a clean working directory
|
|
58
|
+
try:
|
|
59
|
+
repo = git.Repo(repo_path)
|
|
60
|
+
# is_dirty checks for modified/staged files, untracked_files checks for new files
|
|
61
|
+
is_clean = not repo.is_dirty(untracked_files=True)
|
|
62
|
+
except InvalidGitRepositoryError as e:
|
|
63
|
+
logger.error("Failed to check working directory status")
|
|
64
|
+
raise GitError("Failed to check working directory status") from e
|
|
65
|
+
|
|
66
|
+
if not is_clean:
|
|
67
|
+
if raise_error:
|
|
68
|
+
raise RepoNotCleanError(
|
|
69
|
+
f"Working directory is not clean in {repo_path}. Please commit or stash changes first."
|
|
70
|
+
)
|
|
71
|
+
return False
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def check_all_repos_are_clean(paths: Paths, raise_error: bool = True) -> bool:
|
|
76
|
+
"""Check if all repositories are clean."""
|
|
77
|
+
from multi.repos import load_repos
|
|
78
|
+
|
|
79
|
+
# Check root repo
|
|
80
|
+
if not check_repo_is_clean(paths.root_dir, raise_error):
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
# Check sub-repos
|
|
84
|
+
return all(
|
|
85
|
+
check_repo_is_clean(repo.path, raise_error) for repo in load_repos(paths)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check_branch_existence(repo_path: Path, branch_name: str) -> Tuple[bool, bool]:
|
|
90
|
+
try:
|
|
91
|
+
repo = git.Repo(repo_path)
|
|
92
|
+
except InvalidGitRepositoryError as e:
|
|
93
|
+
logger.error("Failed to check if branch exists")
|
|
94
|
+
raise GitError("Failed to check if branch exists") from e
|
|
95
|
+
|
|
96
|
+
# Check if branch exists locally
|
|
97
|
+
exists_locally = branch_name in [head.name for head in repo.heads]
|
|
98
|
+
|
|
99
|
+
# Check if branch exists remotely
|
|
100
|
+
try:
|
|
101
|
+
remote_refs = [ref.name for ref in repo.remotes.origin.refs]
|
|
102
|
+
exists_remotely = f"origin/{branch_name}" in remote_refs
|
|
103
|
+
except Exception:
|
|
104
|
+
logger.debug(
|
|
105
|
+
f"Could not check remote branches in {repo_path}, assuming branch doesn't exist remotely"
|
|
106
|
+
)
|
|
107
|
+
exists_remotely = False
|
|
108
|
+
|
|
109
|
+
return exists_locally, exists_remotely
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from multi.errors import GitError
|
|
9
|
+
from multi.git_helpers import check_all_on_same_branch
|
|
10
|
+
from multi.paths import Paths
|
|
11
|
+
from multi.repos import load_repos
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_git_command(repo_path: Path, git_args: List[str]) -> None:
|
|
17
|
+
"""Run a git command in the specified repository."""
|
|
18
|
+
command_str = " ".join(git_args)
|
|
19
|
+
logger.info(f"Running 'git {command_str}' in {repo_path}")
|
|
20
|
+
|
|
21
|
+
cmd = ["git"] + git_args
|
|
22
|
+
try:
|
|
23
|
+
outputs = subprocess.run(
|
|
24
|
+
cmd,
|
|
25
|
+
cwd=repo_path,
|
|
26
|
+
check=check,
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
).stdout.strip()
|
|
30
|
+
logger.info(f"Output from {repo_path}:\n{outputs}")
|
|
31
|
+
except subprocess.CalledProcessError as e:
|
|
32
|
+
logger.error(f"Failed to run git command in {repo_path}")
|
|
33
|
+
raise GitError(f"Failed to run git command in {repo_path}") from e
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_git_in_all_repos(paths: Paths, git_args: List[str]) -> None:
|
|
37
|
+
"""Run git command across all repositories."""
|
|
38
|
+
# First check if all repos are on the same branch
|
|
39
|
+
check_all_on_same_branch(raise_error=True)
|
|
40
|
+
|
|
41
|
+
# Run in root repo first
|
|
42
|
+
run_git_command(paths.root_dir, git_args)
|
|
43
|
+
|
|
44
|
+
# Then run in all sub-repos
|
|
45
|
+
for repo in load_repos(paths.settings):
|
|
46
|
+
run_git_command(repo.path, git_args)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@click.command(name="git")
|
|
50
|
+
@click.argument("git_args", nargs=-1, required=True)
|
|
51
|
+
def git_cmd(git_args: tuple[str, ...]) -> None:
|
|
52
|
+
"""Run a git command across all repositories.
|
|
53
|
+
|
|
54
|
+
GIT_ARGS: The git command and arguments to run (e.g. 'pull' or 'checkout main')
|
|
55
|
+
|
|
56
|
+
Example: multi git pull
|
|
57
|
+
multi git checkout -b feature/new-branch
|
|
58
|
+
"""
|
|
59
|
+
run_git_in_all_repos(list(git_args))
|