monarbor 0.1.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.
@@ -0,0 +1,27 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.11"
19
+
20
+ - name: Install build tools
21
+ run: pip install build
22
+
23
+ - name: Build package
24
+ run: python -m build
25
+
26
+ - name: Publish to PyPI
27
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ *.egg
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.so
9
+ .env
10
+ .venv
11
+ venv/
12
+ *.swp
13
+ .DS_Store
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: monarbor
3
+ Version: 0.1.0
4
+ Summary: Monarbor - AI 友好的逻辑大仓命令行工具
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: click>=8.0
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: rich>=13.0
10
+
11
+ # Monarbor
12
+
13
+ AI 友好的逻辑大仓命令行工具。一个 `mona.yaml` 配置文件描述所有仓库,一套命令统一管理。
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ pip install monarbor
19
+ ```
20
+
21
+ 开发模式安装:
22
+
23
+ ```bash
24
+ pip install -e .
25
+ ```
26
+
27
+ ## 快速开始
28
+
29
+ ```bash
30
+ # 初始化一个逻辑大仓
31
+ monarbor init
32
+
33
+ # 添加仓库
34
+ monarbor add --path business-a/frontend --name "前端项目" --url "https://git.example.com/org/frontend.git"
35
+
36
+ # 拉取所有代码
37
+ monarbor clone
38
+
39
+ # 查看状态
40
+ monarbor status
41
+ ```
42
+
43
+ ## 命令一览
44
+
45
+ | 命令 | 说明 |
46
+ |------|------|
47
+ | `monarbor clone` | 拉取大仓下所有项目代码 |
48
+ | `monarbor pull` | 更新所有已 clone 仓库的代码 |
49
+ | `monarbor status` | 显示所有仓库的分支、改动、同步状态 |
50
+ | `monarbor list` | 以树形结构列出所有仓库 |
51
+ | `monarbor exec <cmd>` | 在所有仓库中执行命令 |
52
+ | `monarbor checkout <dev\|test\|prod>` | 批量切换到指定分支类型 |
53
+ | `monarbor init` | 初始化新的逻辑大仓 |
54
+ | `monarbor add` | 向当前大仓添加仓库 |
55
+
56
+ ## 常用场景
57
+
58
+ ### 拉取所有代码
59
+
60
+ ```bash
61
+ # 默认 clone dev 分支
62
+ monarbor clone
63
+
64
+ # clone 测试分支
65
+ monarbor clone -b test
66
+
67
+ # 递归 clone(包括嵌套的子逻辑大仓)
68
+ monarbor clone -r
69
+
70
+ # 只 clone 某个业务线
71
+ monarbor clone --filter business-a
72
+ ```
73
+
74
+ ### 批量切换分支
75
+
76
+ ```bash
77
+ # 全部切到测试分支
78
+ monarbor checkout test
79
+
80
+ # 全部切到生产分支
81
+ monarbor checkout prod
82
+ ```
83
+
84
+ ### 批量执行命令
85
+
86
+ ```bash
87
+ # 查看每个仓库最近 5 条提交
88
+ monarbor exec "git log --oneline -5"
89
+
90
+ # 全部安装依赖
91
+ monarbor exec "npm install"
92
+
93
+ # 只在某个业务线执行
94
+ monarbor exec "pnpm build" --filter business-a
95
+ ```
96
+
97
+ ### 嵌套逻辑大仓
98
+
99
+ 当子目录下存在自己的 `mona.yaml` 时,带 `-r` 参数即可递归处理:
100
+
101
+ ```bash
102
+ monarbor clone -r # 递归 clone
103
+ monarbor status -r # 递归查看状态
104
+ monarbor list -r # 递归列出树形结构
105
+ ```
106
+
107
+ ## mona.yaml 格式
108
+
109
+ ```yaml
110
+ name: "我的大仓"
111
+ description: "大仓描述"
112
+ owner: your-name
113
+
114
+ repos:
115
+ - path: business-a/frontend
116
+ name: "前端项目"
117
+ repo_url: "https://git.example.com/org/frontend.git"
118
+ tech_stack: [typescript, react]
119
+ branches:
120
+ dev: develop
121
+ test: release/test
122
+ prod: main
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,117 @@
1
+ # Monarbor
2
+
3
+ AI 友好的逻辑大仓命令行工具。一个 `mona.yaml` 配置文件描述所有仓库,一套命令统一管理。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install monarbor
9
+ ```
10
+
11
+ 开发模式安装:
12
+
13
+ ```bash
14
+ pip install -e .
15
+ ```
16
+
17
+ ## 快速开始
18
+
19
+ ```bash
20
+ # 初始化一个逻辑大仓
21
+ monarbor init
22
+
23
+ # 添加仓库
24
+ monarbor add --path business-a/frontend --name "前端项目" --url "https://git.example.com/org/frontend.git"
25
+
26
+ # 拉取所有代码
27
+ monarbor clone
28
+
29
+ # 查看状态
30
+ monarbor status
31
+ ```
32
+
33
+ ## 命令一览
34
+
35
+ | 命令 | 说明 |
36
+ |------|------|
37
+ | `monarbor clone` | 拉取大仓下所有项目代码 |
38
+ | `monarbor pull` | 更新所有已 clone 仓库的代码 |
39
+ | `monarbor status` | 显示所有仓库的分支、改动、同步状态 |
40
+ | `monarbor list` | 以树形结构列出所有仓库 |
41
+ | `monarbor exec <cmd>` | 在所有仓库中执行命令 |
42
+ | `monarbor checkout <dev\|test\|prod>` | 批量切换到指定分支类型 |
43
+ | `monarbor init` | 初始化新的逻辑大仓 |
44
+ | `monarbor add` | 向当前大仓添加仓库 |
45
+
46
+ ## 常用场景
47
+
48
+ ### 拉取所有代码
49
+
50
+ ```bash
51
+ # 默认 clone dev 分支
52
+ monarbor clone
53
+
54
+ # clone 测试分支
55
+ monarbor clone -b test
56
+
57
+ # 递归 clone(包括嵌套的子逻辑大仓)
58
+ monarbor clone -r
59
+
60
+ # 只 clone 某个业务线
61
+ monarbor clone --filter business-a
62
+ ```
63
+
64
+ ### 批量切换分支
65
+
66
+ ```bash
67
+ # 全部切到测试分支
68
+ monarbor checkout test
69
+
70
+ # 全部切到生产分支
71
+ monarbor checkout prod
72
+ ```
73
+
74
+ ### 批量执行命令
75
+
76
+ ```bash
77
+ # 查看每个仓库最近 5 条提交
78
+ monarbor exec "git log --oneline -5"
79
+
80
+ # 全部安装依赖
81
+ monarbor exec "npm install"
82
+
83
+ # 只在某个业务线执行
84
+ monarbor exec "pnpm build" --filter business-a
85
+ ```
86
+
87
+ ### 嵌套逻辑大仓
88
+
89
+ 当子目录下存在自己的 `mona.yaml` 时,带 `-r` 参数即可递归处理:
90
+
91
+ ```bash
92
+ monarbor clone -r # 递归 clone
93
+ monarbor status -r # 递归查看状态
94
+ monarbor list -r # 递归列出树形结构
95
+ ```
96
+
97
+ ## mona.yaml 格式
98
+
99
+ ```yaml
100
+ name: "我的大仓"
101
+ description: "大仓描述"
102
+ owner: your-name
103
+
104
+ repos:
105
+ - path: business-a/frontend
106
+ name: "前端项目"
107
+ repo_url: "https://git.example.com/org/frontend.git"
108
+ tech_stack: [typescript, react]
109
+ branches:
110
+ dev: develop
111
+ test: release/test
112
+ prod: main
113
+ ```
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,3 @@
1
+ """Monarbor - AI 友好的逻辑大仓命令行工具"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,323 @@
1
+ """Monarbor 命令行入口。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ from rich.tree import Tree as RichTree
11
+
12
+ from . import __version__
13
+ from .config import CONFIG_FILENAME, MonorepoConfig, RepoDef, find_nested_monorepos, walk_monorepos
14
+ from .git_ops import (
15
+ ahead_behind,
16
+ checkout,
17
+ clone,
18
+ current_branch,
19
+ fetch,
20
+ is_dirty,
21
+ pull,
22
+ run_in_repo,
23
+ )
24
+
25
+ console = Console()
26
+
27
+
28
+ def find_root(start: Path | None = None) -> Path:
29
+ """向上查找最近的 mona.yaml 所在目录。"""
30
+ current = (start or Path.cwd()).resolve()
31
+ while True:
32
+ if (current / CONFIG_FILENAME).exists():
33
+ return current
34
+ parent = current.parent
35
+ if parent == current:
36
+ break
37
+ current = parent
38
+ raise click.ClickException(f"未找到 {CONFIG_FILENAME},请在逻辑大仓目录下运行,或使用 monarbor init 初始化")
39
+
40
+
41
+ @click.group()
42
+ @click.version_option(__version__, prog_name="monarbor")
43
+ def main():
44
+ """Monarbor - AI 友好的逻辑大仓命令行工具"""
45
+ pass
46
+
47
+
48
+ # ── monarbor clone ───────────────────────────────────────────
49
+
50
+
51
+ @main.command(name="clone")
52
+ @click.option("-r", "--recursive", is_flag=True, help="递归 clone 嵌套的子逻辑大仓")
53
+ @click.option("-b", "--branch-type", type=click.Choice(["dev", "test", "prod"]), default="dev", help="clone 哪个分支类型 (默认 dev)")
54
+ @click.option("--filter", "path_filter", default=None, help="只 clone 路径前缀匹配的仓库 (如 business-a)")
55
+ def clone_repos(recursive: bool, branch_type: str, path_filter: str | None):
56
+ """拉取大仓下所有项目代码。"""
57
+ root = find_root()
58
+ configs = list(walk_monorepos(root, recursive=recursive))
59
+ total, success, skipped = 0, 0, 0
60
+
61
+ for config in configs:
62
+ if config.root != root:
63
+ console.rule(f"[bold]嵌套大仓: {config.name}[/bold] ({config.root.relative_to(root)})")
64
+
65
+ for repo in config.repos:
66
+ if path_filter and not repo.path.startswith(path_filter):
67
+ continue
68
+ total += 1
69
+ target = config.root / repo.path
70
+ if target.exists() and (target / ".git").exists():
71
+ console.print(f" [dim]跳过[/dim] {repo.path} (已存在)")
72
+ skipped += 1
73
+ continue
74
+
75
+ branch = repo.branches.get(branch_type)
76
+ console.print(f" [cyan]克隆[/cyan] {repo.name} → {repo.path} [dim](branch: {branch})[/dim]")
77
+ result = clone(repo.repo_url, target, branch=branch)
78
+ if result.ok:
79
+ success += 1
80
+ console.print(f" [green]✓[/green]")
81
+ else:
82
+ console.print(f" [red]✗ {result.error}[/red]")
83
+
84
+ console.print(f"\n[bold]完成:[/bold] {success} 克隆, {skipped} 跳过, {total - success - skipped} 失败 (共 {total})")
85
+
86
+
87
+ # ── monarbor pull ────────────────────────────────────────────
88
+
89
+
90
+ @main.command(name="pull")
91
+ @click.option("-r", "--recursive", is_flag=True, help="递归 pull 嵌套大仓")
92
+ def pull_repos(recursive: bool):
93
+ """拉取所有已 clone 仓库的最新代码。"""
94
+ root = find_root()
95
+ configs = list(walk_monorepos(root, recursive=recursive))
96
+ total, success = 0, 0
97
+
98
+ for config in configs:
99
+ for repo in config.repos:
100
+ target = config.root / repo.path
101
+ if not (target / ".git").exists():
102
+ continue
103
+ total += 1
104
+ console.print(f" [cyan]拉取[/cyan] {repo.name} ({repo.path})")
105
+ result = pull(target)
106
+ if result.ok:
107
+ success += 1
108
+ console.print(f" [green]✓[/green] {result.output or 'Already up to date.'}")
109
+ else:
110
+ console.print(f" [red]✗ {result.error}[/red]")
111
+
112
+ console.print(f"\n[bold]完成:[/bold] {success}/{total} 成功")
113
+
114
+
115
+ # ── monarbor status ──────────────────────────────────────────
116
+
117
+
118
+ @main.command()
119
+ @click.option("-r", "--recursive", is_flag=True, help="递归显示嵌套大仓")
120
+ @click.option("--fetch/--no-fetch", default=False, help="先 fetch 远端再显示状态")
121
+ def status(recursive: bool, fetch: bool):
122
+ """显示所有仓库的当前状态。"""
123
+ root = find_root()
124
+ configs = list(walk_monorepos(root, recursive=recursive))
125
+
126
+ table = Table(title="仓库状态")
127
+ table.add_column("项目", style="bold")
128
+ table.add_column("路径", style="dim")
129
+ table.add_column("分支", style="cyan")
130
+ table.add_column("状态")
131
+ table.add_column("同步")
132
+
133
+ for config in configs:
134
+ for repo in config.repos:
135
+ target = config.root / repo.path
136
+ rel_path = str(target.relative_to(root))
137
+ if not (target / ".git").exists():
138
+ table.add_row(repo.name, rel_path, "-", "[dim]未 clone[/dim]", "-")
139
+ continue
140
+
141
+ if fetch:
142
+ from .git_ops import fetch as git_fetch
143
+ git_fetch(target)
144
+
145
+ branch = current_branch(target)
146
+ dirty = is_dirty(target)
147
+ dirty_label = "[red]有改动[/red]" if dirty else "[green]干净[/green]"
148
+ a, b = ahead_behind(target)
149
+ sync_parts = []
150
+ if a:
151
+ sync_parts.append(f"[yellow]↑{a}[/yellow]")
152
+ if b:
153
+ sync_parts.append(f"[yellow]↓{b}[/yellow]")
154
+ sync_label = " ".join(sync_parts) if sync_parts else "[green]同步[/green]"
155
+
156
+ table.add_row(repo.name, rel_path, branch, dirty_label, sync_label)
157
+
158
+ console.print(table)
159
+
160
+
161
+ # ── monarbor list ────────────────────────────────────────────
162
+
163
+
164
+ @main.command(name="list")
165
+ @click.option("-r", "--recursive", is_flag=True, help="递归显示嵌套大仓")
166
+ def list_repos(recursive: bool):
167
+ """以树形结构列出所有仓库。"""
168
+ root = find_root()
169
+ configs = list(walk_monorepos(root, recursive=recursive))
170
+
171
+ for config in configs:
172
+ tree = RichTree(f"[bold]{config.name}[/bold] [dim]({config.root.relative_to(root) if config.root != root else '.'})[/dim]")
173
+ groups: dict[str, list[RepoDef]] = {}
174
+ for repo in config.repos:
175
+ parts = repo.path.split("/")
176
+ group = parts[0] if len(parts) > 1 else "."
177
+ groups.setdefault(group, []).append(repo)
178
+
179
+ for group_name, repos in sorted(groups.items()):
180
+ if group_name == ".":
181
+ for repo in repos:
182
+ tree.add(f"{repo.name} [dim]{repo.path}[/dim] [cyan]{'|'.join(repo.tech_stack)}[/cyan]")
183
+ else:
184
+ branch = tree.add(f"[bold]{group_name}/[/bold]")
185
+ for repo in repos:
186
+ sub_path = "/".join(repo.path.split("/")[1:])
187
+ branch.add(f"{repo.name} [dim]{sub_path}[/dim] [cyan]{'|'.join(repo.tech_stack)}[/cyan]")
188
+
189
+ nested = find_nested_monorepos(config.root)
190
+ for n in nested:
191
+ tree.add(f"[bold magenta]📦 {n.name}/[/bold magenta] [dim](嵌套大仓)[/dim]")
192
+
193
+ console.print(tree)
194
+
195
+
196
+ # ── monarbor exec ────────────────────────────────────────────
197
+
198
+
199
+ @main.command()
200
+ @click.argument("command")
201
+ @click.option("-r", "--recursive", is_flag=True, help="递归执行到嵌套大仓")
202
+ @click.option("--filter", "path_filter", default=None, help="只在路径前缀匹配的仓库中执行")
203
+ def exec_cmd(command: str, recursive: bool, path_filter: str | None):
204
+ """在所有已 clone 的仓库中执行命令。
205
+
206
+ 示例: monarbor exec "git log --oneline -5"
207
+ """
208
+ root = find_root()
209
+ configs = list(walk_monorepos(root, recursive=recursive))
210
+
211
+ for config in configs:
212
+ for repo in config.repos:
213
+ if path_filter and not repo.path.startswith(path_filter):
214
+ continue
215
+ target = config.root / repo.path
216
+ if not (target / ".git").exists():
217
+ continue
218
+ console.rule(f"[bold]{repo.name}[/bold] ({repo.path})")
219
+ result = run_in_repo(target, command)
220
+ if result.output:
221
+ console.print(result.output)
222
+ if result.error:
223
+ console.print(f"[red]{result.error}[/red]")
224
+
225
+
226
+ # ── monarbor checkout ────────────────────────────────────────
227
+
228
+
229
+ @main.command(name="checkout")
230
+ @click.argument("branch_type", type=click.Choice(["dev", "test", "prod"]))
231
+ @click.option("-r", "--recursive", is_flag=True, help="递归切换嵌套大仓")
232
+ @click.option("--filter", "path_filter", default=None, help="只切换路径前缀匹配的仓库")
233
+ def checkout_repos(branch_type: str, recursive: bool, path_filter: str | None):
234
+ """将所有仓库切换到指定分支类型 (dev/test/prod)。"""
235
+ root = find_root()
236
+ configs = list(walk_monorepos(root, recursive=recursive))
237
+
238
+ for config in configs:
239
+ for repo in config.repos:
240
+ if path_filter and not repo.path.startswith(path_filter):
241
+ continue
242
+ target = config.root / repo.path
243
+ if not (target / ".git").exists():
244
+ continue
245
+ branch = repo.branches.get(branch_type)
246
+ if not branch:
247
+ console.print(f" [yellow]跳过[/yellow] {repo.name}: 未配置 {branch_type} 分支")
248
+ continue
249
+ console.print(f" [cyan]切换[/cyan] {repo.name} → {branch}")
250
+ result = checkout(target, branch)
251
+ if result.ok:
252
+ console.print(f" [green]✓[/green]")
253
+ else:
254
+ console.print(f" [red]✗ {result.error}[/red]")
255
+
256
+
257
+ # ── monarbor init ────────────────────────────────────────────
258
+
259
+
260
+ @main.command()
261
+ @click.option("--name", prompt="大仓名称", help="逻辑大仓名称")
262
+ @click.option("--owner", prompt="负责人", help="负责人")
263
+ def init(name: str, owner: str):
264
+ """在当前目录初始化一个新的逻辑大仓。"""
265
+ config_path = Path.cwd() / CONFIG_FILENAME
266
+ if config_path.exists():
267
+ raise click.ClickException(f"{CONFIG_FILENAME} 已存在")
268
+
269
+ content = f"""name: "{name}"
270
+ description: ""
271
+ owner: {owner}
272
+
273
+ repos: []
274
+ """
275
+ config_path.write_text(content, encoding="utf-8")
276
+ console.print(f"[green]✓[/green] 已创建 {CONFIG_FILENAME}")
277
+
278
+
279
+ # ── monarbor add ─────────────────────────────────────────────
280
+
281
+
282
+ @main.command()
283
+ @click.option("--path", "repo_path", prompt="仓库路径 (如 business-a/frontend)", help="仓库在大仓中的相对路径")
284
+ @click.option("--name", "repo_name", prompt="项目名称", help="项目显示名称")
285
+ @click.option("--url", "repo_url", prompt="Git 仓库地址", help="Git 仓库 URL")
286
+ @click.option("--dev-branch", default="develop", help="开发分支 (默认 develop)")
287
+ @click.option("--test-branch", default="release/test", help="测试分支 (默认 release/test)")
288
+ @click.option("--prod-branch", default="main", help="生产分支 (默认 main)")
289
+ def add(repo_path: str, repo_name: str, repo_url: str, dev_branch: str, test_branch: str, prod_branch: str):
290
+ """向当前大仓添加一个仓库。"""
291
+ import yaml
292
+
293
+ root = find_root()
294
+ config_path = root / CONFIG_FILENAME
295
+ with open(config_path, "r", encoding="utf-8") as f:
296
+ data = yaml.safe_load(f)
297
+
298
+ repos = data.get("repos", [])
299
+ for r in repos:
300
+ if r.get("path") == repo_path:
301
+ raise click.ClickException(f"路径 {repo_path} 已存在于配置中")
302
+
303
+ new_repo = {
304
+ "path": repo_path,
305
+ "name": repo_name,
306
+ "repo_url": repo_url,
307
+ "branches": {
308
+ "dev": dev_branch,
309
+ "test": test_branch,
310
+ "prod": prod_branch,
311
+ },
312
+ }
313
+ repos.append(new_repo)
314
+ data["repos"] = repos
315
+
316
+ with open(config_path, "w", encoding="utf-8") as f:
317
+ yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
318
+
319
+ console.print(f"[green]✓[/green] 已添加 {repo_name} ({repo_path})")
320
+
321
+
322
+ if __name__ == "__main__":
323
+ main()
@@ -0,0 +1,91 @@
1
+ """mona.yaml 配置的加载与解析。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Iterator
9
+
10
+ import yaml
11
+
12
+ CONFIG_FILENAME = "mona.yaml"
13
+
14
+
15
+ @dataclass
16
+ class RepoDef:
17
+ """一个仓库的定义。"""
18
+
19
+ path: str
20
+ name: str
21
+ repo_url: str
22
+ description: str = ""
23
+ tech_stack: list[str] = field(default_factory=list)
24
+ branches: dict[str, str] = field(default_factory=dict)
25
+
26
+ @property
27
+ def dev_branch(self) -> str:
28
+ return self.branches.get("dev", "develop")
29
+
30
+ @property
31
+ def test_branch(self) -> str:
32
+ return self.branches.get("test", "release/test")
33
+
34
+ @property
35
+ def prod_branch(self) -> str:
36
+ return self.branches.get("prod", "main")
37
+
38
+
39
+ @dataclass
40
+ class MonorepoConfig:
41
+ """一个逻辑大仓的配置。"""
42
+
43
+ name: str
44
+ owner: str
45
+ root: Path
46
+ description: str = ""
47
+ repos: list[RepoDef] = field(default_factory=list)
48
+
49
+ @classmethod
50
+ def load(cls, root: Path) -> MonorepoConfig:
51
+ config_path = root / CONFIG_FILENAME
52
+ if not config_path.exists():
53
+ raise FileNotFoundError(f"未找到配置文件: {config_path}")
54
+ with open(config_path, "r", encoding="utf-8") as f:
55
+ data = yaml.safe_load(f)
56
+ repos = [RepoDef(**r) for r in data.get("repos", [])]
57
+ return cls(
58
+ name=data.get("name", ""),
59
+ owner=data.get("owner", ""),
60
+ description=data.get("description", ""),
61
+ root=root.resolve(),
62
+ repos=repos,
63
+ )
64
+
65
+
66
+ def find_nested_monorepos(root: Path, exclude_paths: set[str] | None = None) -> list[Path]:
67
+ """扫描子目录,找到所有嵌套的逻辑大仓。"""
68
+ nested = []
69
+ exclude = exclude_paths or set()
70
+ for entry in sorted(root.iterdir()):
71
+ if not entry.is_dir() or entry.name.startswith("."):
72
+ continue
73
+ if entry.name in exclude:
74
+ continue
75
+ config = entry / CONFIG_FILENAME
76
+ if config.exists():
77
+ nested.append(entry)
78
+ else:
79
+ nested.extend(find_nested_monorepos(entry, exclude))
80
+ return nested
81
+
82
+
83
+ def walk_monorepos(root: Path, recursive: bool = False) -> Iterator[MonorepoConfig]:
84
+ """遍历当前大仓,可选递归加载嵌套大仓。"""
85
+ config = MonorepoConfig.load(root)
86
+ yield config
87
+
88
+ if recursive:
89
+ repo_paths = {r.path.split("/")[0] for r in config.repos}
90
+ for nested_root in find_nested_monorepos(root, exclude_paths=repo_paths):
91
+ yield from walk_monorepos(nested_root, recursive=True)
@@ -0,0 +1,98 @@
1
+ """Git 操作封装。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass
11
+ class GitResult:
12
+ ok: bool
13
+ output: str
14
+ error: str = ""
15
+
16
+
17
+ def run_git(args: list[str], cwd: Path | None = None) -> GitResult:
18
+ try:
19
+ result = subprocess.run(
20
+ ["git", *args],
21
+ cwd=cwd,
22
+ capture_output=True,
23
+ text=True,
24
+ timeout=300,
25
+ )
26
+ return GitResult(
27
+ ok=result.returncode == 0,
28
+ output=result.stdout.strip(),
29
+ error=result.stderr.strip(),
30
+ )
31
+ except subprocess.TimeoutExpired:
32
+ return GitResult(ok=False, output="", error="操作超时 (300s)")
33
+ except FileNotFoundError:
34
+ return GitResult(ok=False, output="", error="未找到 git 命令,请确认已安装 git")
35
+
36
+
37
+ def clone(repo_url: str, target: Path, branch: str | None = None) -> GitResult:
38
+ args = ["clone"]
39
+ if branch:
40
+ args.extend(["-b", branch])
41
+ args.extend([repo_url, str(target)])
42
+ return run_git(args)
43
+
44
+
45
+ def pull(repo_path: Path) -> GitResult:
46
+ return run_git(["pull"], cwd=repo_path)
47
+
48
+
49
+ def current_branch(repo_path: Path) -> str:
50
+ result = run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_path)
51
+ return result.output if result.ok else "(unknown)"
52
+
53
+
54
+ def is_dirty(repo_path: Path) -> bool:
55
+ result = run_git(["status", "--porcelain"], cwd=repo_path)
56
+ return bool(result.output)
57
+
58
+
59
+ def checkout(repo_path: Path, branch: str) -> GitResult:
60
+ return run_git(["checkout", branch], cwd=repo_path)
61
+
62
+
63
+ def fetch(repo_path: Path) -> GitResult:
64
+ return run_git(["fetch", "--all", "--prune"], cwd=repo_path)
65
+
66
+
67
+ def ahead_behind(repo_path: Path) -> tuple[int, int]:
68
+ """返回 (ahead, behind) 相对于上游的提交数。"""
69
+ result = run_git(
70
+ ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"],
71
+ cwd=repo_path,
72
+ )
73
+ if not result.ok:
74
+ return (0, 0)
75
+ parts = result.output.split()
76
+ if len(parts) == 2:
77
+ return (int(parts[0]), int(parts[1]))
78
+ return (0, 0)
79
+
80
+
81
+ def run_in_repo(repo_path: Path, command: str) -> GitResult:
82
+ """在仓库目录下执行任意 shell 命令。"""
83
+ try:
84
+ result = subprocess.run(
85
+ command,
86
+ cwd=repo_path,
87
+ capture_output=True,
88
+ text=True,
89
+ timeout=120,
90
+ shell=True,
91
+ )
92
+ return GitResult(
93
+ ok=result.returncode == 0,
94
+ output=result.stdout.strip(),
95
+ error=result.stderr.strip(),
96
+ )
97
+ except subprocess.TimeoutExpired:
98
+ return GitResult(ok=False, output="", error="命令超时 (120s)")
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: monarbor
3
+ Version: 0.1.0
4
+ Summary: Monarbor - AI 友好的逻辑大仓命令行工具
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: click>=8.0
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: rich>=13.0
10
+
11
+ # Monarbor
12
+
13
+ AI 友好的逻辑大仓命令行工具。一个 `mona.yaml` 配置文件描述所有仓库,一套命令统一管理。
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ pip install monarbor
19
+ ```
20
+
21
+ 开发模式安装:
22
+
23
+ ```bash
24
+ pip install -e .
25
+ ```
26
+
27
+ ## 快速开始
28
+
29
+ ```bash
30
+ # 初始化一个逻辑大仓
31
+ monarbor init
32
+
33
+ # 添加仓库
34
+ monarbor add --path business-a/frontend --name "前端项目" --url "https://git.example.com/org/frontend.git"
35
+
36
+ # 拉取所有代码
37
+ monarbor clone
38
+
39
+ # 查看状态
40
+ monarbor status
41
+ ```
42
+
43
+ ## 命令一览
44
+
45
+ | 命令 | 说明 |
46
+ |------|------|
47
+ | `monarbor clone` | 拉取大仓下所有项目代码 |
48
+ | `monarbor pull` | 更新所有已 clone 仓库的代码 |
49
+ | `monarbor status` | 显示所有仓库的分支、改动、同步状态 |
50
+ | `monarbor list` | 以树形结构列出所有仓库 |
51
+ | `monarbor exec <cmd>` | 在所有仓库中执行命令 |
52
+ | `monarbor checkout <dev\|test\|prod>` | 批量切换到指定分支类型 |
53
+ | `monarbor init` | 初始化新的逻辑大仓 |
54
+ | `monarbor add` | 向当前大仓添加仓库 |
55
+
56
+ ## 常用场景
57
+
58
+ ### 拉取所有代码
59
+
60
+ ```bash
61
+ # 默认 clone dev 分支
62
+ monarbor clone
63
+
64
+ # clone 测试分支
65
+ monarbor clone -b test
66
+
67
+ # 递归 clone(包括嵌套的子逻辑大仓)
68
+ monarbor clone -r
69
+
70
+ # 只 clone 某个业务线
71
+ monarbor clone --filter business-a
72
+ ```
73
+
74
+ ### 批量切换分支
75
+
76
+ ```bash
77
+ # 全部切到测试分支
78
+ monarbor checkout test
79
+
80
+ # 全部切到生产分支
81
+ monarbor checkout prod
82
+ ```
83
+
84
+ ### 批量执行命令
85
+
86
+ ```bash
87
+ # 查看每个仓库最近 5 条提交
88
+ monarbor exec "git log --oneline -5"
89
+
90
+ # 全部安装依赖
91
+ monarbor exec "npm install"
92
+
93
+ # 只在某个业务线执行
94
+ monarbor exec "pnpm build" --filter business-a
95
+ ```
96
+
97
+ ### 嵌套逻辑大仓
98
+
99
+ 当子目录下存在自己的 `mona.yaml` 时,带 `-r` 参数即可递归处理:
100
+
101
+ ```bash
102
+ monarbor clone -r # 递归 clone
103
+ monarbor status -r # 递归查看状态
104
+ monarbor list -r # 递归列出树形结构
105
+ ```
106
+
107
+ ## mona.yaml 格式
108
+
109
+ ```yaml
110
+ name: "我的大仓"
111
+ description: "大仓描述"
112
+ owner: your-name
113
+
114
+ repos:
115
+ - path: business-a/frontend
116
+ name: "前端项目"
117
+ repo_url: "https://git.example.com/org/frontend.git"
118
+ tech_stack: [typescript, react]
119
+ branches:
120
+ dev: develop
121
+ test: release/test
122
+ prod: main
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,14 @@
1
+ .gitignore
2
+ README.md
3
+ pyproject.toml
4
+ .github/workflows/publish.yml
5
+ monarbor/__init__.py
6
+ monarbor/cli.py
7
+ monarbor/config.py
8
+ monarbor/git_ops.py
9
+ monarbor.egg-info/PKG-INFO
10
+ monarbor.egg-info/SOURCES.txt
11
+ monarbor.egg-info/dependency_links.txt
12
+ monarbor.egg-info/entry_points.txt
13
+ monarbor.egg-info/requires.txt
14
+ monarbor.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ monarbor = monarbor.cli:main
@@ -0,0 +1,3 @@
1
+ click>=8.0
2
+ pyyaml>=6.0
3
+ rich>=13.0
@@ -0,0 +1 @@
1
+ monarbor
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "setuptools-scm"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "monarbor"
7
+ version = "0.1.0"
8
+ description = "Monarbor - AI 友好的逻辑大仓命令行工具"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "click>=8.0",
13
+ "pyyaml>=6.0",
14
+ "rich>=13.0",
15
+ ]
16
+
17
+ [project.scripts]
18
+ monarbor = "monarbor.cli:main"
19
+
20
+ [tool.setuptools.packages.find]
21
+ include = ["monarbor*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+