Skill: zyjnicemoe/git-manager-skill (version 2.4.0)

--- name: git-manager description: > This skill should be used when the user needs to perform Git repository operations, including cloning, pulling, merging, rebasing, committing, staging, Git LFS management, reflog recovery, worktree management, grep search, cherry-pick, revert, bisect, and batch operations from GitHub, GitLab, Gitea, Bitbucket, or Azure DevOps platforms. It supports bulk cloning by group ID, project ID, user ID, workspace, or organization, and full-featured Git workflow including commit operations, LFS, bisect debugging, and worktree multi-branch support. Use this skill for any multi-repo management, synchronization, or Git workflow automation tasks. author: zhuyijun url: https://zyjblogs.cn --- # Git Manager Git 仓库管理技能，支持 **GitHub、GitLab、Gitea、Bitbucket、Azure DevOps** 五大平台的**克隆、拉取、合并、衍合、提交、暂存区操作、Git LFS**，以及批量克隆仓库。 ## 核心脚本 | 脚本 | 功能 | |------|------| | `scripts/git_ops.py` | 单仓库完整操作：clone / pull / fetch / branch / checkout / merge / rebase / **reflog / describe / worktree / grep / cherry-pick / revert / bisect / stash** / diff / log / status / show / blame / tag / remote / clean / gc / **lfs** | | `scripts/batch_clone.py` | 批量克隆：覆盖 GitHub/GitLab/Gitea/Bitbucket/Azure DevOps 五大平台，支持 **--limit / --lfs / --ssh / --workers / --format json** | | `scripts/batch_pull.py` | 批量更新：扫描本地目录下所有 git 仓库并批量 pull/fetch，支持并发 | | `scripts/git_lfs.py` | Git LFS 专用工具：批量跟踪模式、迁移、检查仓库 LFS 状态 | 执行脚本时使用 Python 3.8+，无需额外依赖（只用标准库）： ```bash python scripts/batch_clone.py [args] python scripts/batch_pull.py [args] python scripts/git_ops.py [args] python scripts/git_lfs.py [args] ``` --- ## 操作流程 ### 1. 单仓库操作（git_ops.py） **克隆**： ```bash python scripts/git_ops.py clone <URL> [dest] [-b branch] [--depth N] [--single-branch] [--lfs] ``` **拉取**： ```bash python scripts/git_ops.py pull <repo_path> [--rebase] [--ff-only] [--remote origin] ``` **合并**： ```bash python scripts/git_ops.py merge <repo_path> <branch> [--no-ff] [--squash] [-m message] ``` **衍合**： ```bash python scripts/git_ops.py rebase <repo_path> [--onto <base>] [--branch <branch>] [-i] ``` **暂存文件**： ```bash python scripts/git_ops.py add <repo_path> <files...> # 暂存指定文件 python scripts/git_ops.py add <repo_path> -A # 暂存全部（包括未跟踪） python scripts/git_ops.py add <repo_path> -u # 暂存已跟踪文件 python scripts/git_ops.py add <repo_path> -p # 交互式暂存补丁 ``` **提交**： ```bash python scripts/git_ops.py commit <repo_path> -m "提交信息" # 基本提交 python scripts/git_ops.py commit <repo_path> -a -m "更新" # 自动暂存已跟踪文件 python scripts/git_ops.py commit <repo_path> --amend -m "修改信息" # 修改上次提交 ``` **暂存区重置**： ```bash python scripts/git_ops.py reset <repo_path> # 取消暂存全部（保留修改） python scripts/git_ops.py reset <repo_path> --hard HEAD~1 # 硬重置到上一次提交 python scripts/git_ops.py reset <repo_path> --soft HEAD~1 # 软重置（保留修改在暂存区） ``` **Stash（暂存工作区）**： ```bash python scripts/git_ops.py stash <repo_path> --save "work in progress" python scripts/git_ops.py stash <repo_path> --pop # 弹出最近 stash python scripts/git_ops.py stash <repo_path> --list # 列出所有 stash python scripts/git_ops.py stash <repo_path> --drop stash@{0} # 删除指定 stash ``` **查看差异**： ```bash python scripts/git_ops.py diff <repo_path> # 工作区 vs 暂存区 python scripts/git_ops.py diff <repo_path> --staged # 暂存区 vs HEAD python scripts/git_ops.py diff <repo_path> --stat # 显示统计信息 python scripts/git_ops.py diff <repo_path> main..feature # 比较两个分支 ``` **查看提交历史**： ```bash python scripts/git_ops.py log <repo_path> -n 20 # 最近 20 条 python scripts/git_ops.py log <repo_path> --oneline --graph --all # 图形化全部分支 python scripts/git_ops.py log <repo_path> --author "name" # 按作者过滤 python scripts/git_ops.py log <repo_path> --since "2024-01-01" --until "2024-12-31" # 日期范围 python scripts/git_ops.py log <repo_path> --grep "fix" # 按提交信息过滤 ``` **查看文件逐行历史**： ```bash python scripts/git_ops.py blame <repo_path> src/main.py -L 10,20 ``` **标签管理**： ```bash python scripts/git_ops.py tag <repo_path> --list # 列出标签 python scripts/git_ops.py tag <repo_path> --create -m "v1.0.0" v1.0.0 # 创建标签 python scripts/git_ops.py tag <repo_path> --delete v0.9.0 # 删除标签 python scripts/git_ops.py tag <repo_path> --push v1.0.0 # 推送标签 ``` **远端管理**： ```bash python scripts/git_ops.py remote <repo_path> --list # 列出远端 python scripts/git_ops.py remote <repo_path> --add upstream <URL> # 添加远端 python scripts/git_ops.py remote <repo_path> --set-url origin <URL> # 修改 URL python scripts/git_ops.py remote <repo_path> --remove upstream # 删除远端 ``` **清理未跟踪文件**： ```bash python scripts/git_ops.py clean <repo_path> --dry-run # 预览 python scripts/git_ops.py clean <repo_path> -f -d # 强制删除未跟踪文件和目录 ``` **垃圾回收**： ```bash python scripts/git_ops.py gc <repo_path> --aggressive # 激进压缩 python scripts/git_ops.py gc <repo_path> --prune # 清理悬空对象 ``` **引用日志（找回丢失的提交）**： ```bash python scripts/git_ops.py reflog <repo_path> -n 20 # 最近 20 条 reflog python scripts/git_ops.py reflog <repo_path> HEAD # 特定引用的 reflog ``` **语义化版本描述**： ```bash python scripts/git_ops.py describe <repo_path> # 基于最近标签 python scripts/git_ops.py describe <repo_path> --tags # 只考虑标签 python scripts/git_ops.py describe <repo_path> --all # 考虑所有分支的标签 python scripts/git_ops.py describe <repo_path> --match "v*" # 只匹配 v 开头的标签 ``` **工作树管理**： ```bash python scripts/git_ops.py worktree <repo_path> --list # 列出所有工作树 python scripts/git_ops.py worktree <repo_path> --add ../feature-dir feature # 添加工作树 python scripts/git_ops.py worktree <repo_path> --remove ../feature-dir # 删除工作树 python scripts/git_ops.py worktree <repo_path> --prune # 清理失效工作树 python scripts/git_ops.py worktree <repo_path> --lock ../feature-dir --reason "临时" # 锁定 ``` **仓库文本搜索**： ```bash python scripts/git_ops.py grep <repo_path> "TODO" # 搜索 TODO python scripts/git_ops.py grep <repo_path> "FIXME" -i # 忽略大小写 python scripts/git_ops.py grep <repo_path> "func.*init" -E # 扩展正则 python scripts/git_ops.py grep <repo_path> "error" -n -c # 显示行号和匹配数 python scripts/git_ops.py grep <repo_path> "TODO" -l # 只显示文件名 python scripts/git_ops.py grep <repo_path> "TODO" -B 2 -A 2 # 前后各 2 行上下文 ``` **选取性应用提交**： ```bash python scripts/git_ops.py cherry-pick <repo_path> abc1234 # cherry-pick 单个提交 python scripts/git_ops.py cherry-pick <repo_path> abc..def # cherry-pick 范围 python scripts/git_ops.py cherry-pick <repo_path> --no-commit # 执行但不自动提交 python scripts/git_ops.py cherry-pick <repo_path> --continue # 解决冲突后继续 python scripts/git_ops.py cherry-pick <repo_path> --abort # 放弃并恢复 ``` **安全撤销（生成反向提交）**： ```bash python scripts/git_ops.py revert <repo_path> abc1234 # revert 单个提交 python scripts/git_ops.py revert <repo_path> --no-commit # 执行但不自动提交 python scripts/git_ops.py revert <repo_path> --continue # 解决冲突后继续 python scripts/git_ops.py revert <repo_path> --abort # 放弃并恢复 ``` **二分查找定位 bug**： ```bash python scripts/git_ops.py bisect <repo_path> --start HEAD v1.0.0 # 启动 bisect python scripts/git_ops.py bisect <repo_path> --good # 标记当前为 good python scripts/git_ops.py bisect <repo_path> --bad # 标记当前为 bad python scripts/git_ops.py bisect <repo_path> --skip # 跳过当前提交 python scripts/git_ops.py bisect <repo_path> --reset # 重置并退出 python scripts/git_ops.py bisect <repo_path> --run "make test" # 自动运行测试 ``` **log 增强**： ```bash python scripts/git_ops.py log <repo_path> --reverse # 逆序显示（从最早到最新） python scripts/git_ops.py log <repo_path> --follow src/main.py # 追踪文件重命名 ``` **diff 增强**： ```bash python scripts/git_ops.py diff <repo_path> --color-words # 词级别高亮 python scripts/git_ops.py diff <repo_path> --ws-error-highlight # 高亮空白符错误 ``` **分支设置上游**： ```bash python scripts/git_ops.py branch <repo_path> -u origin/main # 当前分支设上游 python scripts/git_ops.py branch <repo_path> feature -u origin/main # 指定分支设上游 ``` **Stash 增强**： ```bash python scripts/git_ops.py stash <repo_path> --save "wip" -u # 同时暂存未跟踪文件 python scripts/git_ops.py stash <repo_path> --pop stash@{2} # 弹出指定 stash ``` --- ### 2. 批量克隆（batch_clone.py） **必填参数**： - `--platform`：`github` / `gitlab` / `gitea` / `bitbucket` / `azure` - `--type`：`org` / `group` / `user` / `project` - `--id`：组织名 / group ID / 用户 ID / project ID - `--output`：本地保存目录 **可选参数**： - `--host`：GitLab/Gitea 实例地址（GitHub 不需要） - `--token`：API 访问令牌，也可通过 `GITHUB_TOKEN` / `GITLAB_TOKEN` / `GITEA_TOKEN` 环境变量传入 - `--ssh`：使用 SSH 克隆（默认 HTTPS） - `--branch`：指定克隆分支 - `--depth N`：浅克隆 - `--update`：仓库已存在时执行 pull 更新（默认跳过） - `--filter <keyword>`：按仓库名过滤 - `--limit N`：只处理前 N 个仓库（如 `--limit 5`） - `--archived`：包含已归档仓库 - `--lfs`：克隆后初始化 Git LFS，追加常见二进制文件跟踪规则 - `--dry-run`：仅列出不克隆 - `--workers N`：并发克隆线程数（默认 1，4-8 可显著加速） - `--format json`：JSON 格式输出（便于程序化处理） - `--recursive / --no-recursive`：包含/排除 GitLab 子组（默认开启） **平台 ID 类型对应关系**： | 平台 | `--type` 选项 | `--id` 填写内容 | |------|--------------|----------------| | GitHub | `org` | 组织名（如 `my-org`） | | GitHub | `user` | 用户名（如 `johndoe`） | | GitHub | `project` | `owner/repo`（如 `my-org/my-repo`） | | GitLab | `group` | Group 数字 ID 或路径 | | GitLab | `user` | 用户数字 ID 或用户名 | | GitLab | `project` | Project 数字 ID | | Gitea | `org` | 组织名 | | Gitea | `user` | 用户名 | | Bitbucket | `workspace` | Workspace ID | | Azure DevOps | `project` | 项目名（需配合 `--org`） | **完整示例**： ```bash # GitHub 用户前 5 个仓库，启用 LFS，并发 4 线程 python scripts/batch_clone.py --platform github --type user --id zyjnicemoe \ --output ./repos --limit 5 --lfs --workers 4 # GitLab group 批量克隆（包含子组，JSON 输出） python scripts/batch_clone.py --platform gitlab --host https://gitlab.com \ --type group --id 123456 --token glpat-xxx --output ./repos --format json # Gitea 组织批量克隆 python scripts/batch_clone.py --platform gitea --host https://git.example.com \ --type org --id myorg --output ./repos --lfs --workers 8 # 通过环境变量设置 token（更安全，不暴露在命令行） export GITLAB_TOKEN=glpat-xxx python scripts/batch_clone.py --platform gitlab --host https://gitlab.com \ --type group --id 123456 --output ./repos # Bitbucket workspace 下所有仓库 python scripts/batch_clone.py --platform bitbucket \ --type workspace --id my-workspace --token BB_TOKEN --output ./repos # Azure DevOps 项目下所有仓库 python scripts/batch_clone.py --platform azure \ --org my-org --project MyProject --token AZURE_PAT --output ./repos ``` --- ### 3. 批量更新（batch_pull.py） ```bash python scripts/batch_pull.py <root_dir> [options] ``` **关键参数**： - `--rebase`：以 rebase 方式拉取 - `--fetch`：仅 fetch，不合并 - `--stash`：有未提交修改时自动 stash - `--workers N`：并发线程数（加速大量仓库） - `--filter <keyword>`：只处理名称含关键字的仓库 - `--max-depth N`：扫描目录深度（默认 3） --- ### 4. Git LFS 工具（git_lfs.py） ```bash python scripts/git_lfs.py <repo_dir> [options] ``` **参数**： - `--track <pattern>`：添加 LFS 跟踪模式（如 `*.zip`, `*.psd`） - `--untrack <pattern>`：取消跟踪模式 - `--install`：初始化 LFS - `--fetch`：LFS Fetch - `--pull`：LFS Pull - `--push`：LFS Push - `--ls-files`：列出 LFS 跟踪的文件 - `--ls-tracks`：列出当前跟踪模式 - `--scan`：扫描仓库中的 LFS 对象 - `--status`：显示 LFS 状态 - `--migrate [--pattern <p>] [--to <backend>]`：迁移文件到 LFS 或从 LFS 迁出 **常用示例**： ```bash # 在仓库中启用 LFS 并跟踪常见二进制格式 python scripts/git_lfs.py ./my-repo --install python scripts/git_lfs.py ./my-repo --track "*.zip" "*.tar.gz" "*.psd" "*.pdf" # 批量为目录下所有仓库启用 LFS（结合 find） Get-ChildItem -Recurse -Directory | ForEach-Object { python scripts/git_lfs.py $_.FullName --install 2>$null } ``` --- ## 参考资料 - `references/api_reference.md`：各平台 API 端点、认证方式、仓库字段说明、Token 创建指南 - `references/examples.md`：完整使用示例，覆盖 GitHub/GitLab/Gitea/Bitbucket/Azure DevOps 典型场景 需要更详细的 API 信息或示例时，读取对应参考文件。 --- ## 重要注意事项 1. **Token 安全**：避免在命令行历史中暴露 token，建议使用环境变量或 Git credential helper 2. **Rate Limit**：GitHub 未认证只有 60次/小时，批量操作必须使用 token 3. **SSH vs HTTPS**：使用 `--ssh` 需提前配置 SSH Key 到对应平台 4. **rebase 风险**：对已推送到远端的公共分支不要使用 rebase 5. **冲突处理**：merge/rebase 遇到冲突会停止，需手动解决后 `git add` + `git merge/rebase --continue` 6. **GitLab group_id**：进入 Group → Settings → General 查看数字 ID；也可用 URL 编码的路径（如 `my-group%2Fsub-group`） 7. **Gitea 分页**：使用 `limit` 参数（非 `per_page`），脚本已自动处理差异 8. **Git LFS**：确保目标机器已安装 `git-lfs`，可用 `git lfs install` 初始化 --- ## 常见用户请求及处理方式 | 用户说 | 使用哪个脚本 | 关键参数 | |--------|------------|---------| | "克隆 GitHub org 下所有仓库" | `batch_clone.py` | `--platform github --type org --id <org>` | | "克隆 GitLab group 123 下所有项目" | `batch_clone.py` | `--platform gitlab --type group --id 123` | | "用我的 Gitea 账号克隆所有仓库" | `batch_clone.py` | `--platform gitea --type user --id <username>` | | "克隆 Bitbucket workspace 下所有仓库" | `batch_clone.py` | `--platform bitbucket --type workspace --id <workspace>` | | "克隆 Azure DevOps 项目下所有仓库" | `batch_clone.py` | `--platform azure --org <org> --project <proj>` | | "克隆后启用 LFS" | `batch_clone.py` | `--platform github --type user --id <user> --lfs` | | "只克隆前 5 个仓库" | `batch_clone.py` | `--platform github --type org --id <org> --limit 5` | | "快速并发克隆 50 个仓库" | `batch_clone.py` | `--platform github --type org --id <org> --workers 4` | | "JSON 输出供程序解析" | `batch_clone.py` | `--platform github --type user --id <user> --format json` | | "更新本地 ./repos 下所有仓库" | `batch_pull.py` | `./repos` | | "用 rebase 方式同步所有仓库" | `batch_pull.py` | `./repos --rebase` | | "暂存并提交修改" | `git_ops.py` | `add <path> -A && commit <path> -m "msg"` | | "合并 feature 分支到当前分支" | `git_ops.py` | `merge <path> <branch> --no-ff` | | "把我的分支 rebase 到 main" | `git_ops.py` | `rebase <path> --branch main` | | "查看当前有哪些修改" | `git_ops.py` | `diff <path>` | | "暂存工作区修改，稍后恢复" | `git_ops.py` | `stash <path> --save "wip"` | | "查看某个文件的逐行历史" | `git_ops.py` | `blame <path> <file>` | | "不小心 reset --hard 了，找回提交" | `git_ops.py` | `reflog <path>` | | "查看当前版本号（基于 tag）" | `git_ops.py` | `describe <path>` | | "同时在两个分支上工作" | `git_ops.py` | `worktree <path> --add ../dir feature` | | "在仓库里搜索某个函数/变量" | `git_ops.py` | `grep <path> "functionName"` | | "把某个提交应用到我当前分支" | `git_ops.py` | `cherry-pick <path> <commit>` | | "安全撤销某个已经 push 的提交" | `git_ops.py` | `revert <path> <commit>` | | "自动定位哪个 commit 引入了 bug" | `git_ops.py` | `bisect <path> --start HEAD v1.0.0` | | "为仓库启用 LFS 并跟踪大文件" | `git_lfs.py` | `--install --track "*.zip" "*.psd"` |

path: CHANGELOG.md
content: # Changelog

All notable changes to `git-manager` skill are documented here.

## [2.2.0] - 2026-04-15

### Added
- **Bitbucket 支持**：新增 `--platform bitbucket`，支持按 workspace 批量克隆仓库
  - API: `GET https://api.bitbucket.org/2.0/repositories/{workspace}`
  - 认证: Bearer token 或 Basic Auth
- **Azure DevOps 支持**：新增 `--platform azure`，支持按 Project 批量克隆仓库
  - API: `GET https://dev.azure.com/{org}/{project}/_apis/git/repositories`
  - 认证: Basic Auth (`base64(":PAT")`)
  - 新增参数: `--org`（组织名）、`--project`（项目名）
- **http_get 增强**：支持 `auth_type` 参数，区分 bearer 和 basic 认证

### Updated
- `batch_clone.py` platform choices: github / gitlab / gitea / **bitbucket** / **azure**
- `SKILL.md`: 更新平台支持列表，新增 Bitbucket/Azure DevOps 示例
- `api_reference.md`: 新增 Bitbucket 和 Azure DevOps API 文档
- `examples.md`: 新增场景四（Bitbucket）和场景五（Azure DevOps）

---

## [2.1.0] - 2026-04-14

### Added
- **`git_ops.py`** - 🆕 新增 `reflog` 命令：查看引用日志，用于找回丢失的提交（救命刚需）
- **`git_ops.py`** - 🆕 新增 `describe` 命令：显示语义化版本，基于最近标签（如 v1.2.0-5-gabc1234）
- **`git_ops.py`** - 🆕 新增 `worktree` 命令：管理工作树，支持 list/add/remove/prune/lock/unlock
- **`git_ops.py`** - 🆕 新增 `grep` 命令：在仓库中搜索文本，支持正则、大小写、上下文等丰富选项
- **`git_ops.py`** - 🆕 新增 `cherry-pick` 命令：选取性应用提交，支持 --continue/--abort/--skip
- **`git_ops.py`** - 🆕 新增 `revert` 命令：生成反向提交（安全撤销），支持合并冲突解决流程
- **`git_ops.py`** - 🆕 新增 `bisect` 命令：二分查找定位 bug，支持自动化 bisect run
- **`git_ops.py`** - log 子命令新增 `--reverse`（逆序显示）和 `--follow`（追踪文件重命名历史）
- **`git_ops.py`** - diff 子命令新增 `--color-words`（词级别差异高亮）和 `--ws-error-highlight`
- **`git_ops.py`** - branch 子命令新增 `-u/--set-upstream-to`（设置上游分支）
- **`git_ops.py`** - stash 子命令新增 `-u/--include-untracked`（同时暂存未跟踪文件）

### Fixed
- **`git_ops.py`** - 修复 `stash pop` 不支持指定 stash ID 的 bug（`--pop stash@{2}` 现在可用）
- **`git_ops.py`** - 修复 `log --oneline` 与 `--format` 参数冲突（重命名 dest 为 `format_`）
- **`git_ops.py`** - 补全 log 子命令 argparse 中缺失的 `--format` 参数定义

## [2.0.0] - 2026-04-14

### Added
- **`batch_clone.py`** - 新增 `--workers N` 并发克隆支持，显著加速大批量仓库克隆
- **`batch_clone.py`** - 新增 `--format json` JSON 格式输出，便于程序化调用和集成
- **`batch_clone.py`** - 新增 `--recursive / --no-recursive` GitLab 子组包含控制（默认开启）
- **`batch_clone.py`** - Token 支持环境变量 `GITHUB_TOKEN` / `GITLAB_TOKEN` / `GITEA_TOKEN`，避免命令行暴露敏感信息
- **`batch_clone.py`** - 新增启动信息显示：Token 状态、并发线程数
- **`batch_pull.py`** - 新增 `--dry-run` 预览模式，安全查看即将更新的仓库列表
- **`git_lfs.py`** - 新增 10 个子命令：`install`, `track`, `untrack`, `fetch`, `pull`, `push`, `ls-files`, `ls-tracks`, `scan`, `status`, `migrate`
- **`git_ops.py`** - 扩展至 18 个子命令，新增：`add`, `commit`, `reset`, `stash`, `diff`, `log`, `show`, `blame`, `tag`, `remote`, `clean`, `gc`, `lfs`
- **`README.md`** / **`README_en.md`** - 新增中英双语使用文档
- **`marketplace.json`** - 新增 skills.sh 发现元数据

### Fixed
- **`git_ops.py`** - 修复 pull `--rebase` 参数构造死代码（删除无效的 `cmd[1]` 赋值）
- **`git_lfs.py`** - 修复 track 命令传参错误（`git lfs track add` → `git lfs track`）
- **`batch_clone.py`** - Token 获取逻辑统一使用显式参数或环境变量，避免 None 比较问题

### Changed
- **`batch_clone.py`** - 重构 `batch_clone()` 函数，支持并发和 JSON 输出双模式
- **`SKILL.md`** - 同步新增参数说明和使用示例，补充用户场景映射表

---

## [1.0.0] - 2026-04-14

### Added
- **`batch_clone.py`** - 批量克隆 GitHub / GitLab / Gitea 仓库
- **`batch_pull.py`** - 批量拉取/更新本地 Git 仓库
- **`git_ops.py`** - 单仓库 Git 操作基础命令
- **`references/api_reference.md`** - 各平台 API 端点速查
- **`references/examples.md`** - 完整使用示例

### Improved
- `log --reverse`: 逆序显示历史
- `log --follow`: 追踪文件重命名历史
- `diff --color-words`: 词级别 diff，更清晰
- `diff --ws-error-highlight`: 高亮空白字符变更
- `branch -u/--set-upstream-to`: 设置上游分支
- `stash -u/--include-untracked`: stash 时包含未跟踪文件

### Fixed
- `stash pop`: 修复不支持 stash ID（如 `stash@{2}`）的 bug
- `log --format`: 修复与 `--oneline` dest 冲突的问题


path: marketplace.json
content: {
  "name": "git-manager",
  "description": "Git repository management skill - batch clone, pull, merge, rebase, LFS for GitHub, GitLab, Gitea",
  "author": "zhuyijun",
  "homepage": "https://zyjblogs.cn",
  "tags": [
    "git",
    "github",
    "gitlab",
    "gitea",
    "clone",
    "batch-clone",
    "git-lfs",
    "repository-management",
    "vercel-skills",
    "agent-skill"
  ]
}


path: README.md
content: # Git Manager

> 作者：zhuyijun · https://zyjblogs.cn

### 概述

`git-manager` 是专业的 Git 仓库管理工具，支持 GitHub、GitLab、Gitea 全平台的**批量克隆、批量拉取、合并、衍合、提交、暂存区操作、Git LFS 管理**等功能。所有脚本基于 Python 标准库实现，无需额外安装依赖，开箱即用。

### 功能一览

| 功能 | 说明 |
|------|------|
| **批量克隆** | 按 org / group / user / project ID 批量克隆 GitHub / GitLab / Gitea 仓库，支持 `--limit N`、`--lfs`、`--filter`、`--depth`、`--workers N` 并发、`--format json`、`--recursive` 子组 |
| **批量拉取** | 扫描本地目录树，并发批量 `git pull`/`fetch`，支持 rebase 模式和自动 stash，支持 `--dry-run` 预览 |
| **单仓库操作** | clone, pull, fetch, merge, rebase, branch, checkout, status, diff, log, show, blame, tag, remote, clean, gc |
| **暂存区操作** | `add`（部分/全部/交互式）、`commit`（含 `--amend` 修改提交）、`reset`（soft/mixed/hard） |
| **Stash 暂存** | `stash`（save/pop/list/drop/clear），安全保存工作区修改 |
| **Git LFS** | `--install`、`--track`、`--untrack`、`--fetch`、`--pull`、`--push`、`--ls-files`、`--ls-tracks`、`--migrate` |

### 文件结构

```
git-manager/
├── SKILL.md                 # 技能主文件
├── README.md                # 中文使用说明
├── README_en.md             # English documentation
├── CHANGELOG.md             # 版本变更记录
├── scripts/
│   ├── batch_clone.py       # 批量克隆脚本（含并发 + JSON 输出）
│   ├── batch_pull.py        # 批量拉取/更新脚本
│   ├── git_ops.py           # 单仓库操作脚本
│   └── git_lfs.py           # Git LFS 专用工具
└── references/
    ├── api_reference.md     # 平台 API 速查
    └── examples.md          # 完整使用示例
```

### 快速上手

```powershell
# Python 解释器路径
$PY = "C:\Users\zhuyi\.workbuddy\binaries\python\versions\3.13.12\python.exe"

# 批量克隆 GitHub 用户前 5 个仓库，启用 LFS，4 线程并发
$PY scripts/batch_clone.py --platform github --type user --id zyjnicemoe `
  --output ./repos --limit 5 --lfs --workers 4

# 批量克隆 GitLab group（包含子组，JSON 输出）
$PY scripts/batch_clone.py --platform gitlab `
  --host https://gitlab.com --type group --id 123456 `
  --token glpat-xxx --output ./repos --format json --workers 8

# Token 通过环境变量传入（更安全）
$env:GITLAB_TOKEN = "glpat-xxx"
$PY scripts/batch_clone.py --platform gitlab `
  --host https://gitlab.com --type group --id 123456 --output ./repos

# 批量克隆 Gitea 组织
$PY scripts/batch_clone.py --platform gitea `
  --host https://git.example.com --type org --id myorg `
  --output ./repos

# 批量更新 ./repos 下所有仓库（rebase 模式，4 个并发）
$PY scripts/batch_pull.py ./repos --rebase --workers 4

# 单仓库：克隆（启用 LFS），然后暂存并提交
$PY scripts/git_ops.py clone https://github.com/user/repo.git ./my-repo --lfs
$PY scripts/git_ops.py add ./my-repo -A
$PY scripts/git_ops.py commit ./my-repo -m "Initial commit"

# 启用 LFS 并跟踪常见二进制格式
$PY scripts/git_lfs.py ./my-repo --install
$PY scripts/git_lfs.py ./my-repo --track "*.zip" "*.tar.gz" "*.psd" "*.pdf"
```

### 核心参数说明

**`batch_clone.py`**：

| 参数 | 说明 |
|------|------|
| `--platform` | 平台：`github` / `gitlab` / `gitea` |
| `--type` | ID 类型：`org` / `group` / `user` / `project` |
| `--id` | 组织名 / group ID / 用户名 / project ID |
| `--host` | 平台地址（GitLab/Gitea 必填，GitHub 自动识别） |
| `--token` | API 访问令牌（私有仓库必须；GitHub 未认证每小时 60 次） |
| `--ssh` | 使用 SSH URL 克隆（需提前配置 SSH Key） |
| `--branch` | 指定克隆分支 |
| `--depth N` | 浅克隆，只保留最近 N 个提交 |
| `--update` | 仓库存在时执行 pull 更新（默认跳过） |
| `--filter <关键字>` | 按仓库名过滤 |
| `--limit N` | 只克隆前 N 个仓库 |
| `--archived` | 包含已归档仓库 |
| `--lfs` | 克隆后初始化 Git LFS，追加常见二进制文件跟踪规则 |
| `--dry-run` | 预览模式，仅列出不实际克隆 |

**`batch_pull.py`**：

| 参数 | 说明 |
|------|------|
| `--rebase` | 使用 `git rebase` 代替 `git merge` |
| `--fetch` | 仅 fetch，不合并 |
| `--stash` | 拉取前自动 stash 未提交的修改 |
| `--workers N` | 并发线程数（默认 4） |
| `--filter <关键字>` | 只处理名称含关键字的仓库 |
| `--max-depth N` | 扫描目录深度（默认 3） |

**`git_ops.py` 子命令**：

```
clone  pull  fetch  branch  checkout  merge  rebase
add    commit reset stash  diff    status   log    show
blame  tag   remote clean  gc      lfs
```

### 注意事项

1. **Token 安全**：避免在命令行历史中暴露 token，建议使用 Git credential helper 或环境变量管理
2. **Rate Limit**：GitHub 未认证只有 60 次/小时，批量操作务必使用 token
3. **SSH vs HTTPS**：使用 `--ssh` 需提前将 SSH Key 配置到对应平台
4. **rebase 风险**：已推送到远端的公共分支不要使用 rebase，避免产生冲突历史
5. **冲突处理**：merge/rebase 遇到冲突会停止，需手动解决后执行 `git add` + `git merge/rebase --continue`
6. **GitLab group_id**：进入 Group → Settings → General 查看数字 ID；也可用 URL 编码路径（如 `my-group%2Fsub-group`）
7. **Gitea 分页**：使用 `limit` 参数（非 `per_page`），脚本已自动处理差异
8. **Git LFS**：确保目标机器已安装 `git-lfs`，可用 `git lfs install` 初始化


path: README_en.md
content: # Git Manager

> Author: zhuyijun · https://zyjblogs.cn

### Overview

`git-manager` is a powerful Git repository management tool for full-featured operations across GitHub, GitLab, and Gitea platforms. It supports batch cloning, pulling, merging, rebasing, committing, staging area operations, Git LFS management, and more — all via simple command-line scripts with zero extra dependencies (Python 3.8+, standard library only).

### Features

| Feature | Description |
|---------|-------------|
| **Batch Clone** | Clone all repos from a GitHub org/user, GitLab group/user/project, or Gitea org/user by ID. Supports `--limit N`, `--lfs`, `--filter`, `--depth`, `--workers N` concurrency, `--format json`, `--recursive` subgroups |
| **Batch Pull** | Scan a local directory tree and batch `git pull`/`fetch` across all discovered repositories with concurrency support, `--dry-run` preview mode |
| **Single-repo Ops** | clone, pull, fetch, merge, rebase, branch, checkout, status, diff, log, show, blame, tag, remote, clean, gc |
| **Staging Area** | `add` (partial/full/interactive), `commit` (with `--amend`), `reset` (soft/mixed/hard) |
| **Stash** | `stash` (save/pop/list/drop/clear), preserves working directory changes safely |
| **Git LFS** | `--install`, `--track`, `--untrack`, `--fetch`, `--pull`, `--push`, `--ls-files`, `--ls-tracks`, `--migrate` |

### File Structure

```
git-manager/
├── SKILL.md                 # Skill definition file
├── README.md                # Chinese documentation
├── README_en.md             # This file (English documentation)
├── CHANGELOG.md             # Version changelog
├── scripts/
│   ├── batch_clone.py       # Batch cloning script (concurrency + JSON output)
│   ├── batch_pull.py        # Batch pull/fetch script
│   ├── git_ops.py           # Single-repo operations
│   └── git_lfs.py           # Git LFS utilities
└── references/
    ├── api_reference.md     # Platform API quick reference
    └── examples.md          # Complete usage examples
```

### Quick Start

```bash
# Python interpreter path (managed runtime)
PY=C:\Users\zhuyi\.workbuddy\binaries\python\versions\3.13.12\python.exe

# Batch clone GitHub user's first 5 repos, enable LFS, 4-thread concurrency
$PY scripts/batch_clone.py --platform github --type user --id zyjnicemoe \
  --output ./repos --limit 5 --lfs --workers 4

# Batch clone GitLab group (with subgroups, JSON output, 8 threads)
$PY scripts/batch_clone.py --platform gitlab \
  --host https://gitlab.com --type group --id 123456 \
  --token glpat-xxx --output ./repos --format json --workers 8

# Token via environment variable (more secure)
export GITLAB_TOKEN=glpat-xxx
$PY scripts/batch_clone.py --platform gitlab \
  --host https://gitlab.com --type group --id 123456 --output ./repos

# Batch clone Gitea organization
$PY scripts/batch_clone.py --platform gitea \
  --host https://git.example.com --type org --id myorg \
  --output ./repos

# Batch update all repos under ./repos (rebase mode, 4 workers)
$PY scripts/batch_pull.py ./repos --rebase --workers 4

# Single repo: clone with LFS, then commit
$PY scripts/git_ops.py clone https://github.com/user/repo.git ./my-repo --lfs
$PY scripts/git_ops.py add ./my-repo -A
$PY scripts/git_ops.py commit ./my-repo -m "Initial commit"

# Enable LFS and track common binary formats
$PY scripts/git_lfs.py ./my-repo --install
$PY scripts/git_lfs.py ./my-repo --track "*.zip" "*.tar.gz" "*.psd" "*.pdf"
```

### Key Arguments

**`batch_clone.py`**:

| Argument | Description |
|----------|-------------|
| `--platform` | Platform: `github` \| `gitlab` \| `gitea` |
| `--type` | ID type: `org` \| `group` \| `user` \| `project` |
| `--id` | Org name, group ID, username, or project ID |
| `--host` | Platform URL (GitLab/Gitea only; GitHub auto-detected) |
| `--token` | API access token (required for private repos; GitHub anonymous: 60 req/hr) |
| `--ssh` | Use SSH URLs instead of HTTPS |
| `--branch` | Clone specific branch |
| `--depth N` | Shallow clone with N commits |
| `--update` | Pull and update existing repos instead of skipping |
| `--filter <keyword>` | Filter repos by name |
| `--limit N` | Clone only the first N repos |
| `--archived` | Include archived repos |
| `--lfs` | Initialize Git LFS after clone with common binary file tracking rules |
| `--dry-run` | Preview only, no actual cloning |

**`batch_pull.py`**:

| Argument | Description |
|----------|-------------|
| `--rebase` | Use `git rebase` instead of `git merge` |
| `--fetch` | Only fetch, do not merge |
| `--stash` | Auto-stash uncommitted changes before pulling |
| `--workers N` | Number of concurrent workers (default: 4) |
| `--filter <keyword>` | Only process repos whose names contain keyword |
| `--max-depth N` | Directory scan depth (default: 3) |

**`git_ops.py`** sub-commands:

```
clone, pull, fetch, branch, checkout, merge, rebase
add, commit, reset, stash, diff, status, log, show
blame, tag, remote, clean, gc, lfs
```

### Notes

- **Token security**: Avoid exposing tokens in shell history. Use Git credential helpers or environment variables.
- **Rate limits**: GitHub unauthenticated = 60 req/hr; use a token for batch operations.
- **SSH vs HTTPS**: `--ssh` requires SSH keys configured on the target platform.
- **Rebase warning**: Never rebase already-pushed public branches.
- **Conflict handling**: Merge/rebase conflicts stop the operation; resolve manually, then `git add` + `git merge/rebase --continue`.
- **GitLab group_id**: Go to Group → Settings → General to find the numeric ID; you can also use the URL-encoded path (e.g. `my-group%2Fsub-group`).
- **Gitea pagination**: Uses `limit` parameter (not `per_page`); the script handles the difference automatically.
- **Git LFS**: Ensure `git-lfs` is installed on the machine (`git lfs install`).


path: references/api_reference.md
content: # Git Manager - API 参考文档

## 平台 API 端点速查

### GitHub

| 操作 | 端点 |
|------|------|
| 列出组织仓库 | `GET https://api.github.com/orgs/{org}/repos?per_page=100&page=N` |
| 列出用户仓库 | `GET https://api.github.com/users/{username}/repos?per_page=100&page=N` |
| 获取单个仓库 | `GET https://api.github.com/repos/{owner}/{repo}` |
| 搜索仓库 | `GET https://api.github.com/search/repositories?q={query}` |

**认证方式**：
```
[REDACTED_SECRET]
```

**Rate Limit**：
- 未认证：60次/小时
- 认证后：5000次/小时
- 大量操作**必须**使用 token

**仓库对象关键字段**：
```json
{
  "name": "repo-name",
  "clone_url": "https://github.com/org/repo.git",
  "ssh_url": "[REDACTED_SECRET]:org/repo.git",
  "archived": false,
  "default_branch": "main",
  "private": false
}
```

---

### GitLab

| 操作 | 端点 |
|------|------|
| 列出 Group 仓库 | `GET {host}/api/v4/groups/{id}/projects?per_page=100&page=N&include_subgroups=true` |
| 列出用户仓库 | `GET {host}/api/v4/users/{id}/projects?per_page=100&page=N` |
| 获取单个仓库 | `GET {host}/api/v4/projects/{id}` |
| 搜索仓库 | `GET {host}/api/v4/projects?search={query}` |

**认证方式（二选一）**：
```
[REDACTED_SECRET]
PRIVATE-[REDACTED_SECRET]
```

**ID 说明**：
- `group_id`：群组数字 ID 或 URL 编码的 namespace 路径（如 `my-group%2Fsub-group`）
- `user_id`：用户数字 ID 或用户名
- `project_id`：项目数字 ID 或 URL 编码的完整路径

**仓库对象关键字段**：
```json
{
  "id": 123,
  "name": "repo-name",
  "path": "repo-path",
  "http_url_to_repo": "https://gitlab.com/group/repo.git",
  "ssh_url_to_repo": "[REDACTED_SECRET]:group/repo.git",
  "archived": false,
  "default_branch": "main"
}
```

---

### Gitea

| 操作 | 端点 |
|------|------|
| 列出组织仓库 | `GET {host}/api/v1/orgs/{org}/repos?limit=50&page=N` |
| 列出用户仓库 | `GET {host}/api/v1/users/{username}/repos?limit=50&page=N` |
| 获取单个仓库 | `GET {host}/api/v1/repos/{owner}/{repo}` |
| 搜索仓库 | `GET {host}/api/v1/repos/search?q={query}&limit=50&page=N` |

**认证方式**：
```
[REDACTED_SECRET]
```
或 Basic Auth：
```
Authorization: Basic base64(username:password)
```

**仓库对象关键字段**：
```json
{
  "name": "repo-name",
  "clone_url": "https://gitea.example.com/user/repo.git",
  "ssh_url": "[REDACTED_SECRET]:user/repo.git",
  "archived": false,
  "default_branch": "main",
  "private": false
}
```

---

### Bitbucket

| 操作 | 端点 |
|------|------|
| 列出 Workspace 仓库 | `GET https://api.bitbucket.org/2.0/repositories/{workspace}?pagelen=100&page=N` |

**认证方式**：
```
Authorization: Bearer BB_TOKEN
```
或 Basic Auth：`base64(email:token)`

**仓库对象关键字段**：
```json
{
  "name": "repo-name",
  "slug": "repo-slug",
  "links": {
    "clone": [
      {"href": "https://bitbucket.org/workspace/repo.git", "name": "https"},
      {"href": "[REDACTED_SECRET]:workspace/repo.git", "name": "ssh"}
    ]
  },
  "is_private": false,
  "project": {"key": "PROJ", "name": "Project Name"}
}
```

**Token 创建**：
1. 访问 Bitbucket → Personal Settings → App passwords
2. 创建新密码，勾选 `read:repository` 权限

---

### Azure DevOps

| 操作 | 端点 |
|------|------|
| 列出 Project 仓库 | `GET https://dev.azure.com/{org}/{project}/_apis/git/repositories?api-version=6.0` |

**认证方式**：
```
Authorization: Basic base64(":PAT")
```

**仓库对象关键字段**：
```json
{
  "id": "guid-repo-id",
  "name": "repo-name",
  "project": {"name": "Project Name"},
  "defaultBranch": "refs/heads/main"
}
```
**Clone URL 构造**：
- HTTPS: `https://dev.azure.com/{org}/{project}/_git/{repo}`
- SSH: `[REDACTED_SECRET]:v3/{org}/{project}/{repo}`

**Token 创建**：
1. 访问 Azure DevOps → User Settings → Personal Access Tokens
2. 创建 Token，范围选择 `Code` → `Read`

---

## Token 创建指南

### GitHub Personal Access Token
1. 访问 GitHub → Settings → Developer settings → Personal access tokens
2. 选择 "Tokens (classic)"
3. 所需权限：`repo`（私有仓库）或 `public_repo`（公开仓库）

### GitLab Personal Access Token
1. 访问 GitLab → User Settings → Access Tokens
2. 所需权限：`read_api`（只读）或 `api`（读写）
3. 也支持 Project Token 和 Group Token

### Gitea Access Token
1. 访问 Gitea → User Settings → Applications
2. 填写 Token Name，选择权限后生成

### Bitbucket App Password
1. 访问 Bitbucket → Personal Settings → App passwords
2. 创建密码，勾选 `read:repository` 权限

### Azure DevOps Personal Access Token
1. 访问 Azure DevOps → User Settings → Personal Access Tokens
2. 创建 Token，作用域选择 `Code` → `Read`

---

## Git 命令速查

### 克隆
```bash
# 标准克隆
git clone https://example.com/repo.git [dest]

# 指定分支
git clone -b main https://example.com/repo.git

# 浅克隆（只获取最近 N 次提交）
git clone --depth 1 https://example.com/repo.git

# SSH 克隆
git clone [REDACTED_SECRET]:group/repo.git
```

### 拉取
```bash
# 标准 pull（merge）
git pull origin main

# rebase 方式拉取
git pull --rebase origin main

# 仅快进
git pull --ff-only origin main

# 先 fetch 再决定如何合并
git fetch origin
git merge origin/main
```

### 合并
```bash
# 标准合并
git merge feature-branch

# 禁用快进（保留合并提交）
git merge --no-ff feature-branch

# 压缩所有提交为一个
git merge --squash feature-branch && git commit

# 中止合并
git merge --abort
```

### 衍合（Rebase）
```bash
# 将当前分支变基到 main
git rebase main

# 交互式 rebase（整理最近 N 次提交）
git rebase -i HEAD~N

# 变基到指定基底
git rebase --onto new-base old-base feature

# 中止 rebase
git rebase --abort

# 继续 rebase（解决冲突后）
git rebase --continue
```

### 冲突解决
```bash
# 查看冲突文件
git diff --name-only --diff-filter=U

# 接受所有 ours/theirs
git checkout --ours conflicted_file
git checkout --theirs conflicted_file

# 标记冲突已解决
git add conflicted_file

# 合并时继续
git merge --continue
# 或
git rebase --continue
```

---

## 常见问题

### Q: 私有仓库需要验证怎么办？
- 推荐方式：生成 Access Token，使用 HTTPS URL 格式：  
  `https://[REDACTED_SECRET]/org/repo.git`
- 或配置 SSH key，使用 SSH URL

### Q: GitLab Group ID 在哪里查？
- 进入 Group 页面 → Settings → General → Group ID（数字）

### Q: 为什么 rebase 会丢失提交？
- `rebase` 会重写提交历史，已推送到远端的分支谨慎使用
- 共享分支应优先使用 `merge --no-ff`

### Q: 浅克隆（shallow clone）的限制？
- `--depth N` 克隆后无法直接 `git fetch --unshallow` 到完整历史
- 仅适合 CI/CD 场景，日常开发不建议


path: references/examples.md
content: # Git Manager - 使用示例

## 场景一：批量克隆 GitHub Organization 下所有仓库

```bash
# 克隆公开 org（无需 token）
python scripts/batch_clone.py \
  --platform github \
  --type org \
  --id my-organization \
  --output ./my-org-repos

# 克隆私有 org（需要 token）
python scripts/batch_clone.py \
  --platform github \
  --type org \
  --id my-organization \
  --token [REDACTED_SECRET] \
  --output ./my-org-repos

# 使用 SSH 克隆（需配置 SSH Key）
python scripts/batch_clone.py \
  --platform github \
  --type org \
  --id my-organization \
  --token [REDACTED_SECRET] \
  --ssh \
  --output ./my-org-repos

# 预览将克隆哪些仓库（不实际克隆）
python scripts/batch_clone.py \
  --platform github \
  --type org \
  --id my-organization \
  --dry-run
```

---

## 场景二：批量克隆 GitLab Group 下所有项目

```bash
# 按 group_id（数字）批量克隆
python scripts/batch_clone.py \
  --platform gitlab \
  --host https://gitlab.com \
  --type group \
  --id 1234567 \
  --token glpat-xxxxxxxxxxxxxxxxxxxx \
  --output ./gitlab-group-repos

# 私有 GitLab 实例
python scripts/batch_clone.py \
  --platform gitlab \
  --host https://gitlab.yourcompany.com \
  --type group \
  --id my-team \
  --token glpat-xxxxxxxxxxxxxxxxxxxx \
  --output ./company-repos

# 克隆单个 GitLab 项目（按 project_id）
python scripts/batch_clone.py \
  --platform gitlab \
  --host https://gitlab.com \
  --type project \
  --id 987654 \
  --token glpat-xxxxxxxxxxxxxxxxxxxx \
  --output ./single-project

# 包含已归档项目
python scripts/batch_clone.py \
  --platform gitlab \
  --host https://gitlab.com \
  --type group \
  --id 1234567 \
  --token glpat-xxxxxxxxxxxxxxxxxxxx \
  --archived \
  --output ./all-repos
```

---

## 场景三：批量克隆 Gitea 用户 / 组织仓库

```bash
# Gitea 组织下所有仓库
python scripts/batch_clone.py \
  --platform gitea \
  --host https://gitea.example.com \
  --type org \
  --id my-org \
  --token your_token \
  --output ./gitea-repos

# Gitea 用户仓库
python scripts/batch_clone.py \
  --platform gitea \
  --host https://gitea.example.com \
  --type user \
  --id johndoe \
  --token your_token \
  --output ./johndoe-repos

# 仅克隆名称含 "backend" 的仓库
python scripts/batch_clone.py \
  --platform gitea \
  --host https://gitea.example.com \
  --type org \
  --id my-org \
  --token your_token \
  --filter backend \
  --output ./backend-repos
```

---

## 场景四：批量克隆 Bitbucket Workspace 仓库

```bash
# Bitbucket workspace 下所有仓库
python scripts/batch_clone.py \
  --platform bitbucket \
  --type workspace \
  --id my-workspace \
  --token BB_TOKEN \
  --output ./bitbucket-repos

# 仅克隆公开仓库
python scripts/batch_clone.py \
  --platform bitbucket \
  --type workspace \
  --id my-workspace \
  --token BB_TOKEN \
  --filter frontend \
  --output ./frontend-repos
```

---

## 场景五：批量克隆 Azure DevOps 项目仓库

```bash
# Azure DevOps 项目下所有仓库（需要 PAT）
python scripts/batch_clone.py \
  --platform azure \
  --org my-organization \
  --project MyProject \
  --token AZURE_PAT \
  --output ./azure-repos

# 使用 SSH 克隆
python scripts/batch_clone.py \
  --platform azure \
  --org my-organization \
  --project MyProject \
  --token AZURE_PAT \
  --ssh \
  --output ./azure-repos

# 限制克隆前 10 个仓库
python scripts/batch_clone.py \
  --platform azure \
  --org my-organization \
  --project MyProject \
  --token AZURE_PAT \
  --limit 10 \
  --output ./azure-repos
```

---

## 场景六：批量拉取本地仓库更新

```bash
# 更新目录下所有仓库
python scripts/batch_pull.py ./repos

# 以 rebase 方式拉取（更干净的提交历史）
python scripts/batch_pull.py ./repos --rebase

# 有未提交修改时自动 stash
python scripts/batch_pull.py ./repos --stash

# 仅 fetch（查看有哪些更新，不合并）
python scripts/batch_pull.py ./repos --fetch

# 并发执行（适合仓库数量多时加速）
python scripts/batch_pull.py ./repos --workers 4

# 只更新名称含 "api" 的仓库
python scripts/batch_pull.py ./repos --filter api

# 先预览再执行
python scripts/batch_pull.py ./repos --dry-run
```

---

## 场景七：单仓库基础操作

```bash
# 克隆单个仓库
python scripts/git_ops.py clone https://github.com/org/repo.git
python scripts/git_ops.py clone https://github.com/org/repo.git ./my-repo -b develop

# 拉取更新
python scripts/git_ops.py pull ./my-repo
python scripts/git_ops.py pull ./my-repo --rebase

# 合并分支
python scripts/git_ops.py merge ./my-repo feature/login
python scripts/git_ops.py merge ./my-repo feature/login --no-ff -m "合并登录功能"
python scripts/git_ops.py merge ./my-repo feature/login --squash

# 衍合
python scripts/git_ops.py rebase ./my-repo --branch main
python scripts/git_ops.py rebase ./my-repo --onto main old-feature

# Fetch
python scripts/git_ops.py fetch ./my-repo
python scripts/git_ops.py fetch ./my-repo --all

# 查看状态
python scripts/git_ops.py status ./my-repo
```

---

## 场景八：已有仓库目录批量更新（克隆时使用 --update）

```bash
# 第一次克隆
python scripts/batch_clone.py --platform github --type org --id my-org --output ./repos

# 之后定期更新（--update 表示仓库已存在时执行 pull）
python scripts/batch_clone.py --platform github --type org --id my-org --output ./repos --update

# 或直接使用 batch_pull.py 更新
python scripts/batch_pull.py ./repos --stash --rebase
```

---

## 常用 Token 配置方式

### 方式一：命令行参数
```bash
python scripts/batch_clone.py --token YOUR_TOKEN ...
```

### 方式二：环境变量（推荐，避免泄露）
```bash
# 设置环境变量
export GITHUB_TOKEN=ghp_xxx
export GITLAB_TOKEN=glpat_xxx
export GITEA_TOKEN=your_token
export BITBUCKET_TOKEN=BB_TOKEN
export AZURE_TOKEN=AZURE_PAT

# 在脚本中读取（需修改脚本或由 WorkBuddy 传递）
```

### 方式三：Git credential helper
```bash
# 配置 credential store
git config --global credential.helper store

# 或使用 keychain（macOS）
git config --global credential.helper osxkeychain
```


path: scripts/batch_clone.py
content: #!/usr/bin/env python3
"""
batch_clone.py - 批量克隆 Git 仓库
支持 GitHub / GitLab / Gitea / Bitbucket / Azure DevOps 批量克隆
"""

import argparse
import base64
import json
import os
import subprocess
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError
from urllib.parse import quote


# ─────────────────────────────────────────────
# HTTP 辅助
# ─────────────────────────────────────────────

def http_get(url: str, token: str = None, accept: str = "application/json",
             auth_type: str = "bearer") -> dict | list:
    """发送 GET 请求，返回解析后的 JSON
    auth_type: 'bearer'（GitHub/GitLab/Gitea/Bitbucket）或 'basic'（Azure DevOps）
    """
    headers = {"Accept": accept, "User-Agent": "git-manager-skill/1.0"}
    if token:
        if auth_type == "basic":
            # Azure DevOps: Basic Base64(":PAT")
            encoded = base64.b64encode(f":{token}".encode()).decode()
            headers["Authorization"] = f"Basic {encoded}"
        else:
            headers["Authorization"] = f"Bearer {token}"
            headers["PRIVATE-TOKEN"] = token  # GitLab 兼容
    req = Request(url, headers=headers)
    try:
        with urlopen(req, timeout=30) as resp:
            return json.loads(resp.read().decode())
    except HTTPError as e:
        body = e.read().decode(errors="replace")
        print(f"[HTTP {e.code}] {url}\n  {body[:300]}")
        return None
    except URLError as e:
        print(f"[URL Error] {url}: {e.reason}")
        return None


def paginate(base_url: str, token: str = None, page_param: str = "page",
             per_page_param: str = "per_page", per_page: int = 100) -> list:
    """自动分页获取所有数据"""
    results = []
    page = 1
    sep = "&" if "?" in base_url else "?"
    while True:
        url = f"{base_url}{sep}{page_param}={page}&{per_page_param}={per_page}"
        data = http_get(url, token)
        if not data:
            break
        if isinstance(data, dict):
            # Gitea 风格：{"data": [...]}
            items = data.get("data", [])
        else:
            items = data
        if not items:
            break
        results.extend(items)
        if len(items) < per_page:
            break
        page += 1
        time.sleep(0.2)  # 避免频率限制
    return results


# ─────────────────────────────────────────────
# GitHub
# ─────────────────────────────────────────────

def github_list_org_repos(org: str, token: str = None) -> list:
    """列出 GitHub Organization 下所有仓库"""
    url = f"https://api.github.com/orgs/{quote(org)}/repos"
    return paginate(url, token, per_page_param="per_page")


def github_list_user_repos(username: str, token: str = None) -> list:
    """列出 GitHub 用户下所有仓库"""
    url = f"https://api.github.com/users/{quote(username)}/repos"
    return paginate(url, token, per_page_param="per_page")


def github_get_repo(owner: str, repo: str, token: str = None) -> dict:
    """获取单个 GitHub 仓库信息"""
    url = f"https://api.github.com/repos/{quote(owner)}/{quote(repo)}"
    return http_get(url, token)


# ─────────────────────────────────────────────
# GitLab
# ─────────────────────────────────────────────

def gitlab_list_group_repos(host: str, group_id: str, token: str = None, recursive: bool = True) -> list:
    """列出 GitLab Group 下所有仓库"""
    gid = quote(str(group_id), safe="")
    include_subgroups = "true" if recursive else "false"
    url = f"{host}/api/v4/groups/{gid}/projects?include_subgroups={include_subgroups}"
    return paginate(url, token)


def gitlab_list_user_repos(host: str, user_id: str, token: str = None) -> list:
    """列出 GitLab User 下所有仓库"""
    uid = quote(str(user_id), safe="")
    url = f"{host}/api/v4/users/{uid}/projects"
    return paginate(url, token)


def gitlab_get_repo(host: str, project_id: str, token: str = None) -> dict:
    """通过 project_id 获取单个 GitLab 仓库"""
    pid = quote(str(project_id), safe="")
    url = f"{host}/api/v4/projects/{pid}"
    return http_get(url, token)


# ─────────────────────────────────────────────
# Gitea
# ─────────────────────────────────────────────

def gitea_list_org_repos(host: str, org: str, token: str = None) -> list:
    """列出 Gitea Organization 下所有仓库"""
    url = f"{host}/api/v1/orgs/{quote(org)}/repos"
    return paginate(url, token, page_param="page", per_page_param="limit")


def gitea_list_user_repos(host: str, username: str, token: str = None) -> list:
    """列出 Gitea 用户下所有仓库"""
    url = f"{host}/api/v1/users/{quote(username)}/repos"
    return paginate(url, token, page_param="page", per_page_param="limit")


def gitea_list_org_teams(host: str, org: str, token: str = None) -> list:
    """列出 Gitea 组织下所有 Team"""
    url = f"{host}/api/v1/orgs/{quote(org)}/teams"
    return paginate(url, token, page_param="page", per_page_param="limit")


# ─────────────────────────────────────────────
# Bitbucket
# ─────────────────────────────────────────────

def bitbucket_list_workspace_repos(workspace: str, token: str = None) -> list:
    """列出 Bitbucket Workspace 下所有仓库"""
    url = f"https://api.bitbucket.org/2.0/repositories/{quote(workspace)}"
    repos = paginate(url, token, page_param="page", per_page_param="pagelen")
    # 标准化字段，便于后续统一处理
    for r in repos:
        r["name"] = r.get("name", r.get("slug", "unknown"))
        r["archived"] = r.get("is_private", False)  # Bitbucket 无 archived 字段
        # 提取 clone URL
        clone_links = r.get("links", {}).get("clone", [])
        for c in clone_links:
            if c.get("name") == "https":
                r["clone_url"] = c.get("href", "")
            elif c.get("name") == "ssh":
                r["ssh_url"] = c.get("href", "")
    return repos


# ─────────────────────────────────────────────
# Azure DevOps
# ─────────────────────────────────────────────

def azure_list_project_repos(organization: str, project: str, token: str = None) -> list:
    """列出 Azure DevOps Project 下所有 Git 仓库"""
    url = (f"https://dev.azure.com/{quote(organization)}/"
           f"{quote(project)}/_apis/git/repositories?api-version=6.0")
    data = http_get(url, token, auth_type="basic")
    if not data:
        return []
    repos = data.get("value", [])
    # 标准化字段
    for r in repos:
        r["name"] = r.get("name", "unknown")
        r["archived"] = False
        # 构造 clone URL（Azure DevOps API 不直接返回 clone URL）
        proj = r.get("project", {}).get("name", project)
        repo_name = r.get("name", "unknown")
        r["clone_url"] = f"https://dev.azure.com/{organization}/{proj}/_git/{quote(repo_name)}"
        r["ssh_url"] = f"[REDACTED_SECRET]:v3/{organization}/{proj}/{quote(repo_name)}"
    return repos


# ─────────────────────────────────────────────
# 提取 clone URL
# ─────────────────────────────────────────────

def extract_clone_url(repo_info: dict, platform: str, use_ssh: bool) -> tuple[str, str]:
    """从仓库信息中提取克隆 URL 和仓库名"""
    if platform == "github":
        name = repo_info.get("name", "unknown")
        url = repo_info.get("ssh_url" if use_ssh else "clone_url", "")
    elif platform == "gitlab":
        name = repo_info.get("path", repo_info.get("name", "unknown"))
        url = repo_info.get("ssh_url_to_repo" if use_ssh else "http_url_to_repo", "")
    elif platform == "gitea":
        name = repo_info.get("name", "unknown")
        url = repo_info.get("ssh_url" if use_ssh else "clone_url", "")
    elif platform == "bitbucket":
        name = repo_info.get("name", "unknown")
        url = repo_info.get("ssh_url" if use_ssh else "clone_url", "")
    elif platform == "azure":
        name = repo_info.get("name", "unknown")
        url = repo_info.get("ssh_url" if use_ssh else "clone_url", "")
    else:
        name = repo_info.get("name", "unknown")
        url = repo_info.get("clone_url", repo_info.get("html_url", ""))
    return name, url


# ─────────────────────────────────────────────
# 克隆单个仓库
# ─────────────────────────────────────────────

def clone_repo(url: str, dest_dir: str, branch: str = None, depth: int = None,
               skip_existing: bool = True, lfs: bool = False) -> str:
    """克隆或更新单个仓库，返回 'cloned' / 'updated' / 'skipped' / 'failed'"""
    dest_path = Path(dest_dir)

    # 已存在时拉取更新
    if (dest_path / ".git").exists():
        if skip_existing:
            print(f"  [SKIP] 已存在，跳过: {dest_dir}")
            return "skipped"
        print(f"  [UPDATE] 已存在，执行 git pull: {dest_dir}")
        result = subprocess.run(
            ["git", "pull", "--ff-only"],
            cwd=dest_dir, capture_output=True, text=True
        )
        if result.returncode == 0:
            print(f"  [OK] 更新成功")
            if lfs:
                _run_lfs_install(dest_dir)
            return "updated"
        else:
            print(f"  [WARN] 更新失败: {result.stderr.strip()}")
            return "failed"

    cmd = ["git", "clone", "--progress"]
    if branch:
        cmd += ["-b", branch]
    if depth:
        cmd += ["--depth", str(depth)]
    cmd += [url, dest_dir]

    print(f"  [CLONE] {url}")
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode == 0:
        print(f"  [OK] 克隆成功 -> {dest_dir}")
        if lfs:
            _run_lfs_install(dest_dir)
        return "cloned"
    else:
        err = result.stderr.strip()
        print(f"  [FAIL] 克隆失败: {err[:200]}")
        return "failed"


def _run_lfs_install(repo_dir: str):
    """对已克隆的仓库初始化 Git LFS"""
    result = subprocess.run(
        ["git", "lfs", "install", "--quiet"],
        cwd=repo_dir, capture_output=True, text=True
    )
    if result.returncode == 0:
        # 追加 .gitattributes（如果存在 LFS 跟踪规则）
        gitattributes = Path(repo_dir) / ".gitattributes"
        if gitattributes.exists():
            with open(gitattributes, "r", encoding="utf-8") as f:
                content = f.read()
            if "filter=lfs" not in content:
                # 追加 LFS 相关规则
                lfs_patterns = ["*.zip", "*.tar", "*.gz", "*.bin", "*.exe",
                                "*.dmg", "*.pkg", "*.iso", "*.img",
                                "*.pdf", "*.png", "*.jpg", "*.jpeg",
                                "*.mp4", "*.mp3", "*.wav", "*.mov",
                                "*.psd", "*.ai", "*.sketch", "*.fig"]
                new_rules = "\n".join(
                    f"{p} filter=lfs diff=lfs merge=lfs -text"
                    for p in lfs_patterns if p not in content
                )
                with open(gitattributes, "a", encoding="utf-8") as f:
                    f.write(f"\n# Git LFS\n{new_rules}\n")
                print(f"  [LFS] 已更新 .gitattributes")
        else:
            print(f"  [LFS] 已初始化（无 .gitattributes）")
    else:
        print(f"  [LFS] 初始化失败（git-lfs 可能未安装）")


# ─────────────────────────────────────────────
# 批量克隆入口
# ─────────────────────────────────────────────

def batch_clone(repos: list, platform: str, output_dir: str,
                use_ssh: bool = False, branch: str = None,
                depth: int = None, skip_existing: bool = True,
                dry_run: bool = False, lfs: bool = False,
                workers: int = 1, output_format: str = "text") -> dict:
    """批量克隆仓库列表"""
    stats = {"cloned": 0, "updated": 0, "skipped": 0, "failed": 0, "total": len(repos)}
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    results = []  # 用于 JSON 输出

    def clone_one(repo):
        name, url = extract_clone_url(repo, platform, use_ssh)
        dest = str(output_path / name)
        if dry_run:
            lfs_tag = " [LFS]" if lfs else ""
            msg = f"  [DRY-RUN] 将克隆: {url} -> {dest}{lfs_tag}"
            print(f"\n[{repos.index(repo)+1}/{len(repos)}] {name}\n  {msg}")
            return {"name": name, "url": url, "dest": dest, "status": "dry-run"}
        status = clone_repo(url, dest, branch, depth, skip_existing, lfs=lfs)
        return {"name": name, "url": url, "dest": dest, "status": status}

    if workers > 1:
        # 并发模式
        with ThreadPoolExecutor(max_workers=workers) as executor:
            futures = {executor.submit(clone_one, repo): repo for repo in repos}
            done = 0
            for future in as_completed(futures):
                done += 1
                res = future.result()
                results.append(res)
                status = res["status"]
                stats[status] = stats.get(status, 0) + 1
                if output_format == "text":
                    icon = {"cloned": "✅", "updated": "🔄", "skipped": "⏭️", "failed": "❌", "dry-run": "🔍"}.get(status, "?")
                    print(f"  [{done}/{len(repos)}] {icon} {res['name']}")
    else:
        # 顺序模式
        for i, repo in enumerate(repos, 1):
            res = clone_one(repo)
            results.append(res)
            stats[res["status"]] = stats.get(res["status"], 0) + 1

    # JSON 输出
    if output_format == "json":
        output = {"stats": stats, "repos": results}
        print(json.dumps(output, ensure_ascii=False, indent=2))

    return stats


# ─────────────────────────────────────────────
# CLI
# ─────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(
        description="批量克隆 Git 仓库（GitHub/GitLab/Gitea/Bitbucket/Azure DevOps）",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  # GitHub organization 下所有仓库
  python batch_clone.py --platform github --type org --id my-org --output ./repos

  # GitLab group（需要 token）
  python batch_clone.py --platform gitlab --host https://gitlab.com --type group --id 123 --token glpat-xxx

  # Gitea 用户仓库（使用 SSH）
  python batch_clone.py --platform gitea --host https://gitea.example.com --type user --id johndoe --ssh

  # Bitbucket workspace 下所有仓库
  python batch_clone.py --platform bitbucket --type workspace --id my-workspace --token BB_TOKEN

  # Azure DevOps 项目下所有仓库
  python batch_clone.py --platform azure --org my-org --project MyProject --token AZURE_PAT

  # Dry run 预览
  python batch_clone.py --platform github --type org --id my-org --dry-run
"""
    )
    parser.add_argument("--platform",
                        choices=["github", "gitlab", "gitea", "bitbucket", "azure"],
                        required=True, help="Git 平台类型")
    parser.add_argument("--host", default=None,
                        help="GitLab/Gitea 主机地址（如 https://gitlab.com），GitHub/Bitbucket 不需要")
    parser.add_argument("--org", dest="organization", default=None,
                        help="Azure DevOps 组织名（--platform azure 时必填）")
    parser.add_argument("--project", default=None,
                        help="Azure DevOps 项目名（--platform azure 时必填）")
    parser.add_argument("--type", dest="id_type",
                        choices=["org", "group", "user", "project"],
                        required=True, help="ID 类型（org/group/user/project）")
    parser.add_argument("--id", dest="target_id", required=True,
                        help="组织名/组 ID/用户 ID/项目 ID")
    parser.add_argument("--token", default=None,
                        help="API 访问令牌，也可通过环境变量 GITHUB_TOKEN / GITLAB_TOKEN / GITEA_TOKEN 传入")
    parser.add_argument("--output", default="./repos", help="本地存储目录（默认 ./repos）")
    parser.add_argument("--branch", default=None, help="指定克隆分支")
    parser.add_argument("--depth", type=int, default=None, help="浅克隆深度")
    parser.add_argument("--ssh", action="store_true", help="使用 SSH 克隆（默认 HTTPS）")
    parser.add_argument("--update", action="store_true",
                        help="仓库已存在时执行 pull 更新（默认跳过）")
    parser.add_argument("--dry-run", action="store_true", help="仅列出仓库，不实际克隆")
    parser.add_argument("--filter", default=None,
                        help="按仓库名过滤（支持子字符串匹配，如 --filter api）")
    parser.add_argument("--archived", action="store_true",
                        help="包含已归档仓库（默认排除）")
    parser.add_argument("--limit", type=int, default=None,
                        help="最多处理前 N 个仓库（如 --limit 5）")
    parser.add_argument("--lfs", action="store_true",
                        help="克隆后初始化 Git LFS 并追加常见二进制文件跟踪规则到 .gitattributes")
    parser.add_argument("--workers", type=int, default=1,
                        help="并发克隆线程数（默认 1，设为 4 可显著加速大批量克隆）")
    parser.add_argument("--format", choices=["text", "json"], default="text",
                        help="输出格式：text（默认）或 json（便于程序化处理）")
    parser.add_argument("--recursive", action="store_true", default=True,
                        help="包含 GitLab 子组（默认开启，使用 --no-recursive 禁用）")
    parser.add_argument("--no-recursive", dest="recursive", action="store_false",
                        help="禁用 GitLab 子组递归")

    args = parser.parse_args()

    # 设置默认 host / organization / project
    if args.platform == "github":
        host = "https://api.github.com"
    elif args.platform == "gitlab":
        host = args.host.rstrip("/") if args.host else "https://gitlab.com"
    elif args.platform == "gitea":
        if not args.host:
            print("[ERROR] Gitea 需要指定 --host 参数")
            sys.exit(1)
        host = args.host.rstrip("/")
    elif args.platform == "azure":
        if not args.organization or not args.project:
            print("[ERROR] Azure DevOps 需要指定 --org 和 --project 参数")
            sys.exit(1)

    # 获取仓库列表
    # Token 优先使用显式参数，其次使用环境变量
    env_map = {
        "github": "GITHUB_TOKEN",
        "gitlab": "GITLAB_TOKEN",
        "gitea": "GITEA_TOKEN",
    }
    env_token = os.environ.get(env_map.get(args.platform.upper(), ""), "")
    [REDACTED_SECRET] or env_token
    if not token and args.platform != "github":
        print("[提示] 未指定 token，部分私有仓库可能无法访问")
    elif not [REDACTED_SECRET]"[提示] 未指定 token，GitHub API 限速 60次/小时，建议设置 GITHUB_TOKEN")

    print(f"\n{'='*60}")
    print(f"平台: {args.platform.upper()}  |  类型: {args.id_type}  |  ID: {args.target_id}")
    print(f"输出目录: {args.output}")
    print(f"Token: {'✅ 已设置' if token else '❌ 未设置'}")
    if args.workers > 1:
        print(f"并发: {args.workers} 线程")
    print(f"{'='*60}")
    print("\n[正在获取仓库列表...]")

    repos = []
    platform = args.platform
    tid = args.target_id

    if platform == "github":
        if args.id_type in ("org", "group"):
            repos = github_list_org_repos(tid, token)
        elif args.id_type == "user":
            repos = github_list_user_repos(tid, token)
        elif args.id_type == "project":
            r = github_get_repo(tid.split("/")[0], tid.split("/")[-1], token)
            if r:
                repos = [r]

    elif platform == "gitlab":
        if args.id_type in ("group",):
            repos = gitlab_list_group_repos(host, tid, token, recursive=args.recursive)
        elif args.id_type == "user":
            repos = gitlab_list_user_repos(host, tid, token)
        elif args.id_type in ("project",):
            r = gitlab_get_repo(host, tid, token)
            if r:
                repos = [r]
        elif args.id_type == "org":
            repos = gitlab_list_group_repos(host, tid, token, recursive=args.recursive)

    elif platform == "gitea":
        if args.id_type in ("org", "group"):
            repos = gitea_list_org_repos(host, tid, token)
        elif args.id_type == "user":
            repos = gitea_list_user_repos(host, tid, token)

    elif platform == "bitbucket":
        if args.id_type in ("workspace", "org", "group"):
            repos = bitbucket_list_workspace_repos(tid, args.token)
        elif args.id_type == "user":
            repos = bitbucket_list_workspace_repos(tid, args.token)

    elif platform == "azure":
        if args.id_type in ("project",):
            repos = azure_list_project_repos(args.organization, args.project, args.token)
        elif args.id_type in ("org",):
            # Azure 没有 org 级别的 repo，只能按 project 操作
            print("[ERROR] Azure DevOps 不支持 org 级别，请使用 --type project --project <项目名>")
            sys.exit(1)

    if repos is None:
        repos = []

    print(f"[获取完成] 共 {len(repos)} 个仓库")

    # 过滤归档仓库
    if not args.archived:
        before = len(repos)
        repos = [r for r in repos if not r.get("archived", False)]
        filtered = before - len(repos)
        if filtered:
            print(f"[过滤] 排除 {filtered} 个已归档仓库")

    # 名称过滤
    if args.filter:
        before = len(repos)
        repos = [r for r in repos if args.filter.lower() in r.get("name", "").lower()]
        print(f"[过滤] 名称含 '{args.filter}': {before} -> {len(repos)} 个")

    # 数量限制
    if args.limit and args.limit > 0:
        repos = repos[:args.limit]
        print(f"[限制] 只处理前 {args.limit} 个仓库")

    if not repos:
        print("[WARN] 没有找到任何仓库，请检查 ID、权限或 token")
        sys.exit(0)

    # 打印仓库列表
    print(f"\n{'─'*60}")
    print(f"待处理仓库列表（共 {len(repos)} 个）:")
    for i, r in enumerate(repos, 1):
        name = r.get("name", r.get("path", "?"))
        print(f"  {i:3}. {name}")
    print(f"{'─'*60}")

    # 执行批量克隆
    stats = batch_clone(
        repos=repos,
        platform=platform,
        output_dir=args.output,
        use_ssh=args.ssh,
        branch=args.branch,
        depth=args.depth,
        skip_existing=not args.update,
        dry_run=args.dry_run,
        lfs=args.lfs,
        workers=args.workers,
        output_format=args.format,
    )

    # 统计结果
    if args.format == "text":
        print(f"\n{'='*60}")
        print("批量操作完成！")
        print(f"  总计: {stats['total']}")
        if not args.dry_run:
            print(f"  克隆: {stats.get('cloned', 0)}")
            print(f"  更新: {stats.get('updated', 0)}")
            print(f"  跳过: {stats.get('skipped', 0)}")
            print(f"  失败: {stats.get('failed', 0)}")
        print(f"{'='*60}")


if __name__ == "__main__":
    main()


path: scripts/batch_pull.py
content: #!/usr/bin/env python3
"""
batch_pull.py - 批量拉取/更新本地 Git 仓库
扫描指定目录下所有 git 仓库并执行 pull/fetch/rebase
"""

import argparse
import os
import subprocess
import sys
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed


def is_git_repo(path: Path) -> bool:
    return (path / ".git").exists() or (path / "HEAD").exists()


def find_git_repos(root: str, max_depth: int = 3) -> list:
    """递归查找指定目录下所有 git 仓库"""
    repos = []
    root_path = Path(root)
    if not root_path.exists():
        print(f"[ERROR] 目录不存在: {root}")
        return repos

    def _scan(path: Path, depth: int):
        if depth > max_depth:
            return
        if is_git_repo(path):
            repos.append(str(path))
            return  # 找到 git 仓库就不再深入
        try:
            for child in sorted(path.iterdir()):
                if child.is_dir() and not child.name.startswith("."):
                    _scan(child, depth + 1)
        except PermissionError:
            pass

    _scan(root_path, 0)
    return repos


def git_run(args: list, cwd: str) -> tuple[int, str, str]:
    """执行 git 命令，返回 (returncode, stdout, stderr)"""
    result = subprocess.run(
        ["git"] + args,
        cwd=cwd,
        capture_output=True,
        text=True,
        encoding="utf-8",
        errors="replace",
        timeout=120,
    )
    return result.returncode, result.stdout.strip(), result.stderr.strip()


def get_repo_info(repo_path: str) -> dict:
    """获取仓库基本信息"""
    info = {"path": repo_path, "name": Path(repo_path).name}
    rc, branch, _ = git_run(["rev-parse", "--abbrev-ref", "HEAD"], repo_path)
    info["branch"] = branch if rc == 0 else "unknown"
    rc, remote, _ = git_run(["remote", "get-url", "origin"], repo_path)
    info["remote"] = remote if rc == 0 else "none"
    return info


def update_repo(repo_path: str, mode: str = "pull", rebase: bool = False,
                stash: bool = False, remote: str = "origin", branch: str = None) -> dict:
    """更新单个仓库，返回操作结果"""
    result = {
        "path": repo_path,
        "name": Path(repo_path).name,
        "status": "unknown",
        "message": "",
    }

    try:
        # 获取当前分支
        rc, current_branch, _ = git_run(["rev-parse", "--abbrev-ref", "HEAD"], repo_path)
        result["branch"] = current_branch if rc == 0 else "detached"

        # 检查是否有未提交更改
        rc, diff_stat, _ = git_run(["status", "--porcelain"], repo_path)
        has_changes = bool(diff_stat)

        # 可选：stash 暂存未提交修改
        stashed = False
        if has_changes and stash:
            rc, _, err = git_run(["stash", "push", "-m", "git-manager auto stash"], repo_path)
            if rc == 0:
                stashed = True
                result["message"] += "[stash] "
            else:
                result["status"] = "warn"
                result["message"] += f"stash 失败: {err} "

        if mode == "fetch":
            rc, out, err = git_run(["fetch", remote, "--prune"], repo_path)
            if rc == 0:
                result["status"] = "ok"
                result["message"] += "fetch 成功"
            else:
                result["status"] = "failed"
                result["message"] += err[:100]

        elif mode == "pull":
            pull_args = ["pull"]
            if rebase:
                pull_args.append("--rebase")
            else:
                pull_args.append("--ff-only")
            pull_args.append(remote)
            if branch:
                pull_args.append(branch)

            rc, out, err = git_run(pull_args, repo_path)
            if rc == 0:
                result["status"] = "ok"
                if "Already up to date" in out or "up to date" in out.lower():
                    result["message"] += "已是最新"
                else:
                    result["message"] += "已更新"
            else:
                result["status"] = "failed"
                result["message"] += err[:100]

        # 恢复 stash
        if stashed:
            rc, _, err = git_run(["stash", "pop"], repo_path)
            if rc != 0:
                result["message"] += f" [stash pop 失败: {err[:60]}]"

    except subprocess.TimeoutExpired:
        result["status"] = "timeout"
        result["message"] = "操作超时（>120s）"
    except Exception as e:
        result["status"] = "error"
        result["message"] = str(e)

    return result


def print_result(res: dict, idx: int, total: int):
    status_icons = {
        "ok": "✅",
        "failed": "❌",
        "warn": "⚠️",
        "skipped": "⏭️",
        "timeout": "⏰",
        "error": "💥",
        "unknown": "❓",
    }
    icon = status_icons.get(res["status"], "?")
    branch = res.get("branch", "")
    name = res.get("name", res["path"])
    msg = res.get("message", "")
    print(f"  [{idx:3}/{total}] {icon} {name} [{branch}] - {msg}")


def main():
    parser = argparse.ArgumentParser(
        description="批量拉取/更新本地 Git 仓库",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  # 扫描 ./repos 下所有仓库并 pull
  python batch_pull.py ./repos

  # 使用 rebase 方式拉取
  python batch_pull.py ./repos --rebase

  # 仅 fetch，不合并
  python batch_pull.py ./repos --fetch

  # 有未提交修改时自动 stash
  python batch_pull.py ./repos --stash

  # 并发执行（加速）
  python batch_pull.py ./repos --workers 4

  # 只更新仓库名含 'api' 的
  python batch_pull.py ./repos --filter api
"""
    )
    parser.add_argument("root", help="要扫描的根目录")
    parser.add_argument("--fetch", action="store_true", help="仅 fetch，不合并")
    parser.add_argument("--rebase", action="store_true", help="使用 rebase 拉取")
    parser.add_argument("--stash", action="store_true", help="遇到未提交修改时自动 stash")
    parser.add_argument("--remote", default="origin", help="远端名称（默认 origin）")
    parser.add_argument("--branch", default=None, help="指定拉取分支")
    parser.add_argument("--max-depth", type=int, default=3, help="扫描目录深度（默认 3）")
    parser.add_argument("--filter", default=None, help="只处理仓库名含该字符串的仓库")
    parser.add_argument("--workers", type=int, default=1, help="并发线程数（默认 1）")
    parser.add_argument("--dry-run", action="store_true", help="仅列出，不执行")

    args = parser.parse_args()

    # 查找仓库
    print(f"\n[扫描] {args.root}（最大深度: {args.max_depth}）")
    repos = find_git_repos(args.root, args.max_depth)

    if args.filter:
        repos = [r for r in repos if args.filter.lower() in Path(r).name.lower()]
        print(f"[过滤] 名称含 '{args.filter}'")

    if not repos:
        print("[WARN] 未找到任何 git 仓库")
        sys.exit(0)

    print(f"[找到] {len(repos)} 个仓库\n")

    if args.dry_run:
        for i, r in enumerate(repos, 1):
            info = get_repo_info(r)
            print(f"  {i:3}. {info['name']} [{info['branch']}]  {info['remote']}")
        sys.exit(0)

    mode = "fetch" if args.fetch else "pull"
    print(f"操作模式: {mode.upper()}" + (" --rebase" if args.rebase else "") + "\n")
    print("─" * 60)

    results = []
    total = len(repos)

    if args.workers > 1:
        # 并发模式
        with ThreadPoolExecutor(max_workers=args.workers) as executor:
            futures = {
                executor.submit(
                    update_repo, r, mode, args.rebase, args.stash, args.remote, args.branch
                ): r
                for r in repos
            }
            done_count = 0
            for future in as_completed(futures):
                done_count += 1
                res = future.result()
                results.append(res)
                print_result(res, done_count, total)
    else:
        # 顺序模式
        for i, r in enumerate(repos, 1):
            res = update_repo(r, mode, args.rebase, args.stash, args.remote, args.branch)
            results.append(res)
            print_result(res, i, total)

    # 统计
    ok = sum(1 for r in results if r["status"] == "ok")
    failed = sum(1 for r in results if r["status"] in ("failed", "error", "timeout"))
    print("\n" + "=" * 60)
    print(f"完成  ✅ 成功: {ok}  ❌ 失败: {failed}  总计: {total}")
    if failed:
        print("\n失败列表:")
        for r in results:
            if r["status"] in ("failed", "error", "timeout"):
                print(f"  - {r['name']}: {r['message']}")
    print("=" * 60)

    sys.exit(0 if failed == 0 else 1)


if __name__ == "__main__":
    main()


path: scripts/git_lfs.py
content: #!/usr/bin/env python3
"""
Git LFS 专用工具
支持跟踪模式管理、LFS 文件扫描、迁移等功能
"""

import argparse
import os
import subprocess
import sys
from pathlib import Path


def run_git_lfs(args_list, repo_path):
    """运行 git lfs 命令"""
    cmd = ["git", "lfs"] + args_list
    result = subprocess.run(
        cmd,
        cwd=repo_path,
        capture_output=True,
        text=True,
        encoding="utf-8",
        errors="replace",
    )
    return result


def cmd_install(args):
    """初始化 Git LFS"""
    print(f"[Git LFS] 初始化仓库: {args.repo_dir}")
    result = run_git_lfs(["install"], args.repo_dir)
    if result.returncode == 0:
        print("[成功] Git LFS 已初始化")
        if result.stdout:
            print(result.stdout.strip())
    else:
        print(f"[失败] {result.stderr.strip()}")


def cmd_track(args):
    """添加 LFS 跟踪模式"""
    if not args.patterns:
        print("[错误] 必须指定至少一个文件模式，如 --track '*.zip'")
        return
    print(f"[Git LFS] 添加跟踪模式: {args.patterns}")
    for pattern in args.patterns:
        result = run_git_lfs(["track", pattern], args.repo_dir)
        if result.returncode == 0:
            print(f"  [OK] {pattern}")
        else:
            print(f"  [失败] {pattern}: {result.stderr.strip()}")


def cmd_untrack(args):
    """取消 LFS 跟踪模式"""
    if not args.patterns:
        print("[错误] 必须指定至少一个文件模式")
        return
    print(f"[Git LFS] 取消跟踪模式: {args.patterns}")
    gitattributes = Path(args.repo_dir) / ".gitattributes"
    if not gitattributes.exists():
        print("[提示] .gitattributes 文件不存在")
        return
    content = gitattributes.read_text(encoding="utf-8")
    removed = []
    for pattern in args.patterns:
        new_lines = []
        for line in content.splitlines():
            if not (f'"{pattern}"' in line or pattern in line):
                new_lines.append(line)
        removed_count = len(content.splitlines()) - len(new_lines)
        if removed_count > 0:
            removed.append(pattern)
        content = "\n".join(new_lines)
    gitattributes.write_text(content, encoding="utf-8")
    if removed:
        print(f"  [已移除] {', '.join(removed)}")
    else:
        print("  [未找到匹配项]")


def cmd_fetch(args):
    """LFS Fetch"""
    print(f"[Git LFS] Fetch: {args.repo_dir}")
    extra = ["--all"] if args.all else []
    result = run_git_lfs(["fetch"] + extra, args.repo_dir)
    if result.returncode == 0:
        print("[成功]")
        if result.stdout:
            print(result.stdout.strip())
    else:
        print(f"[失败] {result.stderr.strip()}")


def cmd_pull(args):
    """LFS Pull"""
    print(f"[Git LFS] Pull: {args.repo_dir}")
    result = run_git_lfs(["pull"], args.repo_dir)
    if result.returncode == 0:
        print("[成功]")
        if result.stdout:
            print(result.stdout.strip())
    else:
        print(f"[失败] {result.stderr.strip()}")


def cmd_push(args):
    """LFS Push"""
    print(f"[Git LFS] Push: {args.repo_dir}")
    extra = ["--all"] if args.all else []
    result = run_git_lfs(["push"] + extra + ["origin", args.branch or "main"], args.repo_dir)
    if result.returncode == 0:
        print("[成功]")
        if result.stdout:
            print(result.stdout.strip())
    else:
        print(f"[失败] {result.stderr.strip()}")


def cmd_ls_files(args):
    """列出 LFS 跟踪的文件"""
    print(f"[Git LFS] 列出跟踪文件: {args.repo_dir}")
    result = run_git_lfs(["ls-files", "--long"], args.repo_dir)
    if result.returncode == 0:
        lines = result.stdout.strip().splitlines()
        if lines:
            print(f"[{len(lines)} 个文件]")
            for line in lines:
                print(f"  {line}")
        else:
            print("[无 LFS 文件]")
    else:
        print(f"[失败] {result.stderr.strip()}")


def cmd_ls_tracks(args):
    """列出当前跟踪模式"""
    print(f"[Git LFS] 当前跟踪模式: {args.repo_dir}")
    gitattributes = Path(args.repo_dir) / ".gitattributes"
    if not gitattributes.exists():
        print("[无] .gitattributes 不存在")
        return
    content = gitattributes.read_text(encoding="utf-8")
    lines = [l.strip() for l in content.splitlines() if l.strip() and not l.strip().startswith("#")]
    if lines:
        print(f"[{len(lines)} 个规则]")
        for line in lines:
            print(f"  {line}")
    else:
        print("[无跟踪规则]")


def cmd_scan(args):
    """扫描仓库中的 LFS 对象"""
    print(f"[Git LFS] 扫描仓库: {args.repo_dir}")
    result = run_git_lfs(["ls-files", "--size"], args.repo_dir)
    if result.returncode == 0:
        lines = [l for l in result.stdout.strip().splitlines() if l.strip()]
        print(f"[{len(lines)} 个 LFS 对象]")
        for line in lines[: args.limit or 20]:
            print(f"  {line}")
        if len(lines) > (args.limit or 20):
            print(f"  ... 还有 {len(lines) - (args.limit or 20)} 个")
    else:
        print(f"[失败] {result.stderr.strip()}")


def cmd_status(args):
    """显示 LFS 状态"""
    print(f"[Git LFS] 状态: {args.repo_dir}")
    result = run_git_lfs(["status"], args.repo_dir)
    if result.returncode == 0:
        print(result.stdout.strip())
    else:
        print(f"[失败] {result.stderr.strip()}")


def cmd_migrate(args):
    """迁移文件到 LFS 或从 LFS 迁出"""
    print(f"[Git LFS] 迁移: {args.repo_dir}")
    if not args.pattern:
        print("[错误] 必须指定 --pattern 参数")
        return
    to_backend = args.to or "lfs"
    print(f"[迁移] 模式: {args.pattern} -> {to_backend}")
    result = run_git_lfs(
        ["migrate", "import", "--include=" + args.pattern, "--fixup"],
        args.repo_dir,
    )
    if result.returncode == 0:
        print("[成功] 迁移完成")
        if result.stdout:
            print(result.stdout.strip())
        print("\n[提示] 迁移后请提交更改: git add -A && git commit -m 'Migrate to LFS'")
    else:
        print(f"[失败] {result.stderr.strip()}")


def main():
    parser = argparse.ArgumentParser(
        description="Git LFS 专用工具 - 跟踪模式管理、文件扫描、迁移",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    sub = parser.add_subparsers(dest="cmd", required=True)

    p_install = sub.add_parser("install", help="初始化 Git LFS")
    p_install.add_argument("repo_dir", help="仓库路径")

    p_track = sub.add_parser("track", help="添加 LFS 跟踪模式")
    p_track.add_argument("repo_dir", help="仓库路径")
    p_track.add_argument("patterns", nargs="+", help="文件模式，如 *.zip *.psd")

    p_untrack = sub.add_parser("untrack", help="取消 LFS 跟踪模式")
    p_untrack.add_argument("repo_dir", help="仓库路径")
    p_untrack.add_argument("patterns", nargs="+", help="文件模式")

    p_fetch = sub.add_parser("fetch", help="LFS Fetch")
    p_fetch.add_argument("repo_dir", help="仓库路径")
    p_fetch.add_argument("--all", action="store_true", help="抓取所有引用")

    p_pull = sub.add_parser("pull", help="LFS Pull")
    p_pull.add_argument("repo_dir", help="仓库路径")

    p_push = sub.add_parser("push", help="LFS Push")
    p_push.add_argument("repo_dir", help="仓库路径")
    p_push.add_argument("--all", action="store_true", help="推送所有引用")
    p_push.add_argument("--branch", default=None, help="指定分支")

    p_ls_files = sub.add_parser("ls-files", help="列出 LFS 跟踪的文件")
    p_ls_files.add_argument("repo_dir", help="仓库路径")

    p_ls_tracks = sub.add_parser("ls-tracks", help="列出当前跟踪模式")
    p_ls_tracks.add_argument("repo_dir", help="仓库路径")

    p_scan = sub.add_parser("scan", help="扫描仓库中的 LFS 对象")
    p_scan.add_argument("repo_dir", help="仓库路径")
    p_scan.add_argument("--limit", type=int, default=20, help="最多显示数量")

    p_status = sub.add_parser("status", help="显示 LFS 状态")
    p_status.add_argument("repo_dir", help="仓库路径")

    p_migrate = sub.add_parser("migrate", help="迁移文件到 LFS 或从 LFS 迁出")
    p_migrate.add_argument("repo_dir", help="仓库路径")
    p_migrate.add_argument("--pattern", required=True, help="文件模式，如 '*.zip'")
    p_migrate.add_argument(
        "--to",
        default="lfs",
        choices=["lfs", "git"],
        help="迁移目标: lfs 或 git (默认: lfs)",
    )

    args = parser.parse_args()

    cmd_map = {
        "install": cmd_install,
        "track": cmd_track,
        "untrack": cmd_untrack,
        "fetch": cmd_fetch,
        "pull": cmd_pull,
        "push": cmd_push,
        "ls-files": cmd_ls_files,
        "ls-tracks": cmd_ls_tracks,
        "scan": cmd_scan,
        "status": cmd_status,
        "migrate": cmd_migrate,
    }

    cmd_map[args.cmd](args)


if __name__ == "__main__":
    main()


path: scripts/git_ops.py
content: #!/usr/bin/env python3
"""
git_ops.py - Git 仓库操作工具（增强版）
支持：克隆、拉取、合并、衍合、提交、暂存区操作、Git LFS
"""

import argparse
import subprocess
import sys
import re
import os
from pathlib import Path
from typing import Optional


# ─────────────────────────────────────────────
# 工具函数
# ─────────────────────────────────────────────

def run_git(args: list, cwd: str = None, capture: bool = True,
            check: bool = True, env: dict = None) -> subprocess.CompletedProcess:
    """执行 git 命令并返回结果"""
    cmd = ["git"] + args
    cwd = cwd or os.getcwd()
    # 统一路径为绝对路径
    cwd = str(Path(cwd).resolve())

    print(f"  $ git {' '.join(args)}")

    full_env = os.environ.copy()
    if env:
        full_env.update(env)

    try:
        result = subprocess.run(
            cmd,
            cwd=cwd,
            capture_output=capture,
            text=True,
            encoding="utf-8",
            errors="replace",
            env=full_env,
        )
        if result.stdout:
            sys.stdout.write(result.stdout)
        if result.stderr:
            sys.stderr.write(result.stderr)
        if check and result.returncode != 0:
            return result
        return result
    except FileNotFoundError:
        print("[ERROR] git 未安装或不在 PATH 中")
        sys.exit(1)


def _check_repo(path: str) -> bool:
    """检查目录是否为 git 仓库"""
    p = Path(path)
    if not p.exists():
        print(f"[ERROR] 路径不存在: {path}")
        return False
    if not (p / ".git").exists():
        print(f"[ERROR] 不是 git 仓库（缺少 .git 目录）: {path}")
        return False
    return True


def _fmt_bytes(n: int) -> str:
    for unit in ["B", "KB", "MB", "GB"]:
        if abs(n) < 1024:
            return f"{n:.1f}{unit}"
        n /= 1024
    return f"{n:.1f}TB"


# ─────────────────────────────────────────────
# 基础操作：clone / pull / fetch
# ─────────────────────────────────────────────

def cmd_clone(args):
    """克隆仓库"""
    cmd = ["clone", "--progress"]
    if args.branch:
        cmd += ["-b", args.branch]
    if args.depth:
        cmd += ["--depth", str(args.depth)]
    if args.bare:
        cmd.append("--bare")
    if args.single_branch:
        cmd.append("--single-branch")
    if args.shallow_submodules:
        cmd.append("--shallow-submodules")
    cmd.append(args.url)
    if args.dest:
        cmd.append(args.dest)

    print(f"\n[克隆] {args.url} -> {args.dest or '当前目录'}")
    result = run_git(cmd, check=False)
    if result.returncode == 0:
        print("[OK] 克隆成功")
        if args.lfs:
            target = args.dest or Path.cwd() / Path(args.url).stem
            cmd_lfs_install(target, quiet=False)
    else:
        print("[FAIL] 克隆失败")
        sys.exit(1)


def cmd_pull(args):
    """拉取更新"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["pull", "--progress"]
    if args.rebase:
        cmd = ["pull", "--rebase", "--progress"]
    if args.ff_only:
        cmd.append("--ff-only")
    if args.no_commit:
        cmd.append("--no-commit")
    cmd.append(args.remote or "origin")
    if args.branch:
        cmd.append(args.branch)

    print(f"\n[拉取] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 拉取成功")
    else:
        print("[FAIL] 拉取失败")
        sys.exit(1)


def cmd_fetch(args):
    """获取远端更新"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["fetch", "--progress"]
    if args.all:
        cmd.append("--all")
    elif args.remote:
        cmd.append(args.remote)
    if args.prune:
        cmd.append("--prune")
    if args.tags:
        cmd.append("--tags")
    if args.depth:
        cmd += ["--depth", str(args.depth)]

    print(f"\n[Fetch] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] Fetch 成功")
    else:
        print("[FAIL] Fetch 失败")
        sys.exit(1)


# ─────────────────────────────────────────────
# 分支操作：branch / checkout
# ─────────────────────────────────────────────

def cmd_branch(args):
    """分支管理"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["branch"]
    if args.list:
        cmd.append("-a")
    elif args.create:
        cmd = ["checkout", "-b", args.branch_name]
    elif args.delete and args.branch_name:
        if args.force:
            cmd = ["branch", "-D", args.branch_name]
        else:
            cmd = ["branch", "-d", args.branch_name]
    elif args.rename and args.branch_name:
        cmd = ["branch", "-m", args.branch_name, args.new_name]
    elif args.copy and args.branch_name:
        cmd = ["branch", "-c", args.branch_name, args.new_name]
    elif args.set_upstream:
        cmd = ["branch", "-u", args.set_upstream]
        if args.branch_name:
            cmd.insert(2, args.branch_name)

    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 分支操作成功")
    else:
        sys.exit(1)


def cmd_checkout(args):
    """切换分支"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["checkout"]
    if args.create:
        cmd.append("-b")
    if args.force:
        cmd.append("--force")
    if args.orphan:
        cmd = ["checkout", "--orphan", args.branch]
    elif args.branch:
        cmd.append(args.branch)

    print(f"\n[切换分支] {args.repo_path} -> {args.branch or '(当前)'}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 切换成功")
    else:
        sys.exit(1)


# ─────────────────────────────────────────────
# 合并与衍合
# ─────────────────────────────────────────────

def cmd_merge(args):
    """合并分支"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["merge"]
    if args.no_ff:
        cmd.append("--no-ff")
    if args.squash:
        cmd.append("--squash")
    if args.abort:
        cmd = ["merge", "--abort"]
    if args.continue_:
        cmd = ["merge", "--continue"]
    if args.message:
        cmd += ["-m", args.message]
    if args.branch:
        cmd.append(args.branch)

    print(f"\n[合并] {args.repo_path} <- {args.branch or '继续'}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 合并成功")
    else:
        print("[FAIL] 合并失败（可能存在冲突，请手动解决）")
        sys.exit(1)


def cmd_rebase(args):
    """衍合"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["rebase"]
    if args.interactive:
        cmd.append("-i")
    if args.onto:
        cmd += ["--onto", args.onto]
    if args.abort:
        cmd = ["rebase", "--abort"]
    if args.continue_:
        cmd = ["rebase", "--continue"]
    if args.skip:
        cmd = ["rebase", "--skip"]
    if args.branch:
        cmd.append(args.branch)

    print(f"\n[衍合] {args.repo_path} onto {args.onto or args.branch or 'upstream'}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 衍合成功")
    else:
        print("[FAIL] 衍合失败（可能存在冲突，请手动解决）")
        sys.exit(1)


# ─────────────────────────────────────────────
# 暂存区 / 提交
# ─────────────────────────────────────────────

def cmd_add(args):
    """暂存文件"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["add"]
    if args.all:
        cmd = ["add", "-A"]
    elif args.update:
        cmd = ["add", "-u"]
    elif args.patch:
        cmd = ["add", "-p"]
    elif args.interactive:
        cmd = ["add", "-i"]
    else:
        cmd += args.files

    print(f"\n[暂存] {args.repo_path}: {' '.join(args.files) if args.files else '全部'}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 暂存成功")
    else:
        sys.exit(1)


def cmd_reset(args):
    """重置暂存区"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["reset"]
    if args.soft:
        cmd.append("--soft")
    elif args.mixed:
        cmd.append("--mixed")
    elif args.hard:
        cmd.append("--hard")
    if args.commit:
        cmd.append(args.commit)
    elif args.files:
        cmd += args.files

    scope = "HEAD" if not args.commit and not args.files else (args.commit or "文件")
    print(f"\n[Reset {scope}] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 重置成功")
    else:
        sys.exit(1)


def cmd_commit(args):
    """提交暂存区"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["commit"]
    if args.message:
        cmd += ["-m", args.message]
    if args.amend:
        cmd.append("--amend")
    if args.no_edit:
        cmd.append("--no-edit")
    if args.all:
        cmd.append("-a")
    if args.allow_empty:
        cmd.append("--allow-empty")
    if args.allow_new:
        cmd.append("--allow-empty")

    print(f"\n[提交] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 提交成功")
    else:
        sys.exit(1)


def cmd_stash(args):
    """暂存工作区修改"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["stash"]
    if args.save:
        cmd += ["save", args.save]
        if args.include_untracked:
            cmd.append("--include-untracked")
    elif args.pop:
        cmd = ["stash", "pop"]
        if args.stash_id:
            cmd.append(args.stash_id)
    elif args.apply:
        cmd = ["stash", "apply"]
        if args.stash_id:
            cmd.append(args.stash_id)
    elif args.list:
        cmd = ["stash", "list"]
        run_git(cmd, cwd=args.repo_path)
        return
    elif args.show:
        cmd = ["stash", "show"]
        if args.stash_id:
            cmd.append(args.stash_id)
        run_git(cmd, cwd=args.repo_path)
        return
    elif args.drop:
        cmd = ["stash", "drop"]
        if args.stash_id:
            cmd.append(args.stash_id)
    elif args.clear:
        cmd = ["stash", "clear"]

    print(f"\n[Stash] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 操作成功")
    else:
        sys.exit(1)


# ─────────────────────────────────────────────
# 查看操作：diff / log / status / show / blame
# ─────────────────────────────────────────────

def cmd_diff(args):
    """查看差异"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["diff"]
    if args.staged:
        cmd.append("--cached")
    if args.stat:
        cmd.append("--stat")
    if args.name_only:
        cmd.append("--name-only")
    if args.name_status:
        cmd.append("--name-status")
    if args.color == "never":
        cmd.append("--no-color")
    if args.color == "always":
        cmd.append("--color=always")
    if args.branch:
        cmd.append(args.branch)
    if args.compare:
        cmd.append(args.compare)
    if args.color_words:
        cmd.append("--color-words")
    if args.ws_error_highlight:
        cmd.append("--ws-error-highlight=all")
    if args.files:
        cmd += args.files

    print(f"\n[Diff] {args.repo_path}")
    run_git(cmd, cwd=args.repo_path)


def cmd_status(args):
    """查看状态"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["status"]
    if args.short:
        cmd.append("-s")
    if args.branch:
        cmd.append("-b")
    if args.ignored:
        cmd.append("--ignored")
    if args.untracked_files == "all":
        cmd.append("--untracked-files=all")
    elif args.untracked_files == "no":
        cmd.append("--untracked-files=no")

    print(f"\n[状态] {args.repo_path}")
    run_git(cmd, cwd=args.repo_path)


def cmd_log(args):
    """查看提交历史"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["log"]
    if args.oneline:
        cmd.append("--oneline")
    if args.stat:
        cmd.append("--stat")
    if args.patch:
        cmd.append("-p")
    if args.format_:
        cmd += ["--format", args.format_]
    if args.graph:
        cmd.append("--graph")
    if args.all:
        cmd.append("--all")
    if args.decorate:
        cmd.append("--decorate")
    if args.n:
        cmd.append(f"-{args.n}")
    if args.author:
        cmd += ["--author", args.author]
    if args.since:
        cmd += ["--since", args.since]
    if args.until:
        cmd += ["--until", args.until]
    if args.grep:
        cmd += ["--grep", args.grep]
    if args.file:
        cmd += ["--", args.file]
    if args.branch:
        cmd.append(args.branch)
    if args.reverse:
        cmd.append("--reverse")
    if args.follow:
        cmd.append("--follow")

    print(f"\n[Log] {args.repo_path}")
    run_git(cmd, cwd=args.repo_path)


def cmd_show(args):
    """查看提交或文件详情"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["show"]
    if args.stat:
        cmd.append("--stat")
    if args.name_only:
        cmd.append("--name-only")
    if args.format:
        cmd += ["--format", args.format]
    if args.object:
        cmd.append(args.object)

    print(f"\n[Show] {args.repo_path}")
    run_git(cmd, cwd=args.repo_path)


def cmd_blame(args):
    """文件 blame"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["blame"]
    if args.L:
        cmd += ["-L", f"{args.L},{args.L_end or args.L}"]
    if args.numbers:
        cmd.append("-n")
    cmd.append(args.file)

    print(f"\n[Blame] {args.file}")
    run_git(cmd, cwd=args.repo_path)


def cmd_tag(args):
    """标签管理"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["tag"]
    if args.list:
        run_git(["tag", "-l"], cwd=args.repo_path)
        return
    if args.create:
        cmd.append(args.tag_name)
        if args.message:
            cmd += ["-m", args.message]
        if args.commit:
            cmd.append(args.commit)
    elif args.delete:
        cmd = ["tag", "-d", args.tag_name]
    elif args.push:
        cmd = ["push", args.remote or "origin", args.tag_name]

    print(f"\n[Tag] {args.tag_name}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 操作成功")
    else:
        sys.exit(1)


def cmd_remote(args):
    """远端管理"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["remote"]
    if args.list:
        run_git(["remote", "-v"], cwd=args.repo_path)
        return
    if args.add:
        cmd = ["remote", "add", args.remote_name, args.url]
    elif args.remove:
        cmd = ["remote", "remove", args.remote_name]
    elif args.rename:
        cmd = ["remote", "rename", args.remote_name, args.new_name]
    elif args.set_url:
        cmd = ["remote", "set-url", args.remote_name, args.url]
    elif args.show:
        cmd = ["remote", "show", args.remote_name]

    print(f"\n[Remote] {args.remote_name or ''}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 操作成功")
    else:
        sys.exit(1)


def cmd_clean(args):
    """清理未跟踪文件"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["clean"]
    if args.dry_run:
        cmd.append("--dry-run")
    if args.dirs:
        cmd.append("-d")
    if args.force:
        cmd.append("-f")
    if args.files:
        cmd += args.files

    print(f"\n[Clean] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 清理完成")
    else:
        sys.exit(1)


def cmd_gc(args):
    """垃圾回收"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["gc"]
    if args.aggressive:
        cmd.append("--aggressive")
    if args.prune:
        cmd += ["--prune=now"]
    if args.auto:
        cmd.append("--auto")

    print(f"\n[GC] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] GC 完成")
    else:
        sys.exit(1)


def cmd_reflog(args):
    """查看引用日志（找回丢失的提交）"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["reflog"]
    if args.show:
        cmd = ["reflog", "show"]
    if args.expire:
        cmd = ["reflog", "expire"]
        if args.dry_run:
            cmd.append("--dry-run")
        if args.all:
            cmd.append("--all")
    if args.n:
        cmd.append(f"-{args.n}")
    if args.ref:
        cmd.append(args.ref)

    print(f"\n[Reflog] {args.repo_path}")
    run_git(cmd, cwd=args.repo_path)


def cmd_describe(args):
    """显示基于最近标签的语义化版本"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["describe"]
    if args.all:
        cmd.append("--all")
    if args.tags:
        cmd.append("--tags")
    if args.always:
        cmd.append("--always")
    if args.abbrev:
        cmd += ["--abbrev", str(args.abbrev)]
    if args.exact_match:
        cmd.append("--exact-match")
    if args.long:
        cmd.append("--long")
    if args.candidates:
        cmd += ["--candidates", str(args.candidates)]
    if args.match:
        cmd += ["--match", args.match]
    if args.exclude:
        cmd += ["--exclude", args.exclude]
    if args.contains:
        cmd += ["--contains", args.contains]
    if args.connected:
        cmd.append("--always")
    if args.debug:
        cmd.append("--debug")
    if args.object:
        cmd.append(args.object)

    print(f"\n[Describe] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode != 0:
        print("[提示] 当前没有任何标签，请先使用 tag 命令创建标签")
        sys.exit(1)


def cmd_worktree(args):
    """工作树管理"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["worktree"]
    if args.list:
        run_git(["worktree", "list"], cwd=args.repo_path)
        return
    if args.add:
        cmd = ["worktree", "add"]
        if args.force:
            cmd.append("--force")
        if args.checkout:
            cmd += ["-b", args.checkout]
        cmd.append(args.path)
        if args.branch:
            cmd.append(args.branch)
    elif args.remove:
        cmd = ["worktree", "remove"]
        if args.force:
            cmd.append("--force")
        cmd.append(args.path)
    elif args.prune:
        cmd = ["worktree", "prune"]
        if args.dry_run:
            cmd.append("--dry-run")
        if args.verbose:
            cmd.append("--verbose")
    elif args.lock:
        cmd = ["worktree", "lock"]
        if args.reason:
            cmd += ["--reason", args.reason]
        cmd.append(args.path)
    elif args.unlock:
        cmd = ["worktree", "unlock"]
        cmd.append(args.path)

    print(f"\n[Worktree] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] 操作成功")
    else:
        sys.exit(1)


def cmd_grep(args):
    """在仓库中搜索文本"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["grep"]
    if args.ignore_case:
        cmd.append("-i")
    if args.regexp_extended:
        cmd.append("-E")
    if args.regexp_basic:
        cmd.append("-G")
    if args.fixed_strings:
        cmd.append("-F")
    if args.invert:
        cmd.append("--invert-match")
    if args.word_regexp:
        cmd.append("-w")
    if args.line_no:
        cmd.append("-n")
    if args.count:
        cmd.append("-c")
    if args.files_with_matches:
        cmd.append("-l")
    if args.files_without_match:
        cmd.append("-L")
    if args.column:
        cmd.append("--column")
    if args.extended:
        cmd.append("--extended-regexp")
    if args.only_matching:
        cmd.append("-o")
    if args.max_count:
        cmd += ["--max-count", str(args.max_count)]
    if args.context:
        cmd += ["--context", str(args.context)]
    if args.before_context:
        cmd += ["-B", str(args.before_context)]
    if args.after_context:
        cmd += ["-A", str(args.after_context)]
    if args.branch:
        cmd.append(args.branch)
    if args.pattern:
        cmd.append(args.pattern)
    if args.files:
        cmd += ["--"] + args.files

    print(f"\n[Grep] {args.repo_path}: {args.pattern or ''}")
    run_git(cmd, cwd=args.repo_path)


def cmd_cherry_pick(args):
    """选取性应用提交"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["cherry-pick"]
    if args.continue_:
        cmd = ["cherry-pick", "--continue"]
    elif args.abort:
        cmd = ["cherry-pick", "--abort"]
    elif args.skip:
        cmd = ["cherry-pick", "--skip"]
    else:
        if args.no_commit:
            cmd.append("--no-commit")
        if args.mainline:
            cmd += ["--mainline", str(args.mainline)]
        if args.edit:
            cmd.append("--edit")
        if args.cleanup:
            cmd += ["--cleanup", args.cleanup]
        if args.no_verify:
            cmd.append("-n")
        if args.include:
            for c in args.include:
                cmd += ["-x", c]
        if args.exclude:
            for c in args.exclude:
                cmd += ["--no-commit"]  # 跳过
        if args.ff:
            cmd.append("--ff")
        if args.force:
            cmd.append("--force")
        if args.quiet:
            cmd.append("-q")
        if args.commit:
            cmd.append(args.commit)

    print(f"\n[Cherry-Pick] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] Cherry-pick 成功")
    else:
        print("[FAIL] Cherry-pick 失败（可能存在冲突，请手动解决）")
        sys.exit(1)


def cmd_revert(args):
    """生成反向提交（安全撤销）"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["revert"]
    if args.continue_:
        cmd = ["revert", "--continue"]
    elif args.abort:
        cmd = ["revert", "--abort"]
    elif args.skip:
        cmd = ["revert", "--skip"]
    else:
        if args.no_commit:
            cmd.append("--no-commit")
        if args.edit:
            cmd.append("--edit")
        if args.no_verify:
            cmd.append("-n")
        if args.cleanup:
            cmd += ["--cleanup", args.cleanup]
        if args.quiet:
            cmd.append("-q")
        if args.include:
            for c in args.include:
                cmd.append(c)
        if args.exclude:
            for c in args.exclude:
                cmd.append(c)
        if args.ff:
            cmd.append("--ff")
        if args.commit:
            cmd.append(args.commit)

    print(f"\n[Revert] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode == 0:
        print("[OK] Revert 成功")
    else:
        print("[FAIL] Revert 失败（可能存在冲突，请手动解决）")
        sys.exit(1)


def cmd_bisect(args):
    """二分查找定位 bug"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["bisect"]
    if args.start:
        cmd = ["bisect", "start"]
        if args.bad:
            cmd.append(args.bad)
        if args.good:
            cmd.append(args.good)
    elif args.good:
        cmd = ["bisect", "good", args.good]
    elif args.bad:
        cmd = ["bisect", "bad", args.bad]
    elif args.skip:
        cmd = ["bisect", "skip"]
        if args.revision:
            cmd.append(args.revision)
    elif args.reset:
        cmd = ["bisect", "reset"]
        if args.revision:
            cmd.append(args.revision)
    elif args.terms:
        cmd = ["bisect", "terms", args.terms]
    elif args.visual:
        cmd = ["bisect", "visualize"]
    elif args.log:
        cmd = ["bisect", "log"]
    elif args.run:
        cmd = ["bisect", "run"]
        if args.command:
            cmd.append(args.command)
    else:
        # 默认：启动 bisect
        cmd = ["bisect", "start"]

    print(f"\n[Bisect] {args.repo_path}")
    result = run_git(cmd, cwd=args.repo_path, check=False)
    if result.returncode != 0:
        print("[提示] bisect 需要提供已知 good/bad 提交，如: git_ops.py bisect ./repo --start HEAD v1.0.0")
        sys.exit(1)
    else:
        print("[OK] Bisect 操作成功")


def cmd_lfs(args):
    """Git LFS 操作"""
    if not _check_repo(args.repo_path):
        sys.exit(1)
    cmd = ["lfs"]
    if args.install:
        run_git(["lfs", "install"], cwd=args.repo_path)
        print("[OK] Git LFS 初始化完成")
        return
    if args.track:
        patterns = args.patterns or ["*"]
        for p in patterns:
            run_git(["lfs", "track", p], cwd=args.repo_path)
        print(f"[OK] 跟踪模式: {', '.join(patterns)}")
        return
    if args.untrack:
        patterns = args.untrack
        for p in patterns:
            run_git(["lfs", "track", "--exclude", p], cwd=args.repo_path)
        print(f"[OK] 取消跟踪: {', '.join(patterns)}")
        return
    if args.fetch:
        run_git(["lfs", "fetch"], cwd=args.repo_path)
        print("[OK] LFS Fetch 完成")
        return
    if args.pull:
        run_git(["lfs", "pull"], cwd=args.repo_path)
        print("[OK] LFS Pull 完成")
        return
    if args.push:
        cmd = ["lfs", "push"]
        if args.remote:
            cmd += [args.remote, args.branch or "main"]
        run_git(cmd, cwd=args.repo_path)
        print("[OK] LFS Push 完成")
        return
    if args.ls_files:
        run_git(["lfs", "ls-files"], cwd=args.repo_path)
        return
    if args.track_list:
        run_git(["lfs", "track"], cwd=args.repo_path)
        return
    if args.scan:
        run_git(["lfs", "scan"], cwd=args.repo_path)
        return

    # 默认显示状态
    run_git(["lfs", "status"], cwd=args.repo_path)


# ─────────────────────────────────────────────
# LFS 初始化（克隆时自动调用）
# ─────────────────────────────────────────────

def cmd_lfs_install(repo_path: str, quiet: bool = True):
    """对已有仓库初始化 LFS"""
    cmd = ["lfs", "install"]
    if quiet:
        cmd.append("--quiet")
    result = run_git(cmd, cwd=repo_path, check=False)
    if result.returncode == 0:
        print(f"  [LFS] 已初始化 Git LFS: {repo_path}")
    else:
        print(f"  [LFS] LFS 初始化失败（git-lfs 可能未安装）")


# ─────────────────────────────────────────────
# 主入口
# ─────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(
        description="Git 仓库操作工具（增强版）",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    subparsers = parser.add_subparsers(dest="command", help="操作命令")

    # ── clone ─────────────────────────────────
    p = subparsers.add_parser("clone", help="克隆仓库")
    p.add_argument("url", help="仓库 URL")
    p.add_argument("dest", nargs="?", help="目标目录")
    p.add_argument("-b", "--branch", help="指定分支")
    p.add_argument("--depth", type=int, help="浅克隆深度")
    p.add_argument("--bare", action="store_true", help="裸仓库")
    p.add_argument("--single-branch", action="store_true", help="只克隆一个分支")
    p.add_argument("--shallow-submodules", action="store_true", help="子模块浅克隆")
    p.add_argument("--lfs", action="store_true", help="克隆后初始化 Git LFS")

    # ── pull ──────────────────────────────────
    p = subparsers.add_parser("pull", help="拉取更新")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("--remote", help="远端名称（默认 origin）")
    p.add_argument("--branch", help="目标分支")
    p.add_argument("--rebase", action="store_true", help="使用 rebase 方式拉取")
    p.add_argument("--ff-only", action="store_true", help="仅快进合并")
    p.add_argument("--no-commit", action="store_true", help="拉取后不自动提交")

    # ── fetch ─────────────────────────────────
    p = subparsers.add_parser("fetch", help="获取远端更新")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("--remote", help="远端名称")
    p.add_argument("--all", action="store_true", help="获取所有远端")
    p.add_argument("--prune", action="store_true", default=True, help="清理失效引用")
    p.add_argument("--tags", action="store_true", help="获取标签")
    p.add_argument("--depth", type=int, help="获取深度")

    # ── branch ─────────────────────────────────
    p = subparsers.add_parser("branch", help="分支管理")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("--list", action="store_true", help="列出分支")
    p.add_argument("--create", action="store_true", help="创建分支")
    p.add_argument("--delete", action="store_true", help="删除分支")
    p.add_argument("--rename", action="store_true", help="重命名分支")
    p.add_argument("--copy", action="store_true", help="复制分支")
    p.add_argument("--force", action="store_true", help="强制操作")
    p.add_argument("-u", "--set-upstream-to", dest="set_upstream", help="设置上游分支（如 -u origin/main）")
    p.add_argument("branch_name", nargs="?", help="分支名")
    p.add_argument("new_name", nargs="?", help="新分支名")

    # ── checkout ───────────────────────────────
    p = subparsers.add_parser("checkout", help="切换分支")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("branch", nargs="?", help="目标分支")
    p.add_argument("--create", "-b", action="store_true", help="创建并切换")
    p.add_argument("--force", "-f", action="store_true", help="强制切换")
    p.add_argument("--orphan", action="store_true", help="切换到新孤儿分支")

    # ── merge ──────────────────────────────────
    p = subparsers.add_parser("merge", help="合并分支")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("branch", nargs="?", help="要合并的分支")
    p.add_argument("--no-ff", action="store_true", help="禁止快进合并")
    p.add_argument("--squash", action="store_true", help="压缩合并")
    p.add_argument("--message", "-m", help="合并提交信息")
    p.add_argument("--abort", action="store_true", help="放弃合并")
    p.add_argument("--continue", dest="continue_", action="store_true", help="继续合并")

    # ── rebase ─────────────────────────────────
    p = subparsers.add_parser("rebase", help="衍合")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("branch", nargs="?", help="要衍合的分支")
    p.add_argument("--onto", help="目标基底")
    p.add_argument("-i", "--interactive", action="store_true", help="交互式衍合")
    p.add_argument("--abort", action="store_true", help="放弃衍合")
    p.add_argument("--continue", dest="continue_", action="store_true", help="继续衍合")
    p.add_argument("--skip", action="store_true", help="跳过当前提交")

    # ── add ─────────────────────────────────────
    p = subparsers.add_parser("add", help="暂存文件")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("files", nargs="*", help="要暂存的文件")
    p.add_argument("-A", "--all", action="store_true", help="暂存全部（包括未跟踪）")
    p.add_argument("-u", "--update", action="store_true", help="暂存已跟踪文件")
    p.add_argument("-p", "--patch", action="store_true", help="交互式暂存补丁")
    p.add_argument("-i", "--interactive", action="store_true", help="交互式暂存")

    # ── commit ─────────────────────────────────
    p = subparsers.add_parser("commit", help="提交")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("-m", "--message", required=True, help="提交信息")
    p.add_argument("-a", "--all", action="store_true", help="自动暂存已跟踪文件")
    p.add_argument("--amend", action="store_true", help="修改上一次提交")
    p.add_argument("--no-edit", action="store_true", help="仅修改提交信息不改变内容")
    p.add_argument("--allow-empty", action="store_true", help="允许空提交")

    # ── reset ───────────────────────────────────
    p = subparsers.add_parser("reset", help="重置暂存区")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("commit", nargs="?", help="重置到指定提交")
    p.add_argument("files", nargs="*", help="要取消暂存的文件")
    p.add_argument("--soft", action="store_true", help="软重置（保留修改）")
    p.add_argument("--mixed", action="store_true", help="混合重置（默认）")
    p.add_argument("--hard", action="store_true", help="硬重置（丢弃修改）")

    # ── stash ───────────────────────────────────
    p = subparsers.add_parser("stash", help="暂存工作区修改")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("--save", metavar="MSG", help="保存并附加信息")
    p.add_argument("-u", "--include-untracked", action="store_true", help="同时暂存未跟踪文件")
    p.add_argument("--pop", action="store_true", help="弹出最近 stash")
    p.add_argument("--apply", action="store_true", help="应用最近 stash（不删除）")
    p.add_argument("--list", action="store_true", help="列出所有 stash")
    p.add_argument("--show", action="store_true", help="显示 stash 内容")
    p.add_argument("--drop", action="store_true", help="删除 stash")
    p.add_argument("--clear", action="store_true", help="清空所有 stash")
    p.add_argument("stash_id", nargs="?", help="Stash 编号（如 stash@{0}）")

    # ── diff ────────────────────────────────────
    p = subparsers.add_parser("diff", help="查看差异")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("files", nargs="*", help="比较的文件")
    p.add_argument("--staged", action="store_true", help="比较暂存区与 HEAD")
    p.add_argument("--stat", action="store_true", help="显示统计信息")
    p.add_argument("--name-only", action="store_true", help="只显示文件名")
    p.add_argument("--name-status", action="store_true", help="显示文件状态")
    p.add_argument("--branch", help="比较分支")
    p.add_argument("--compare", help="比较两个提交（如 abc..def）")
    p.add_argument("--color", choices=["auto", "always", "never"], default="auto")
    p.add_argument("--color-words", action="store_true", help="词级别高亮差异")
    p.add_argument("--ws-error-highlight", action="store_true", help="高亮空白符错误")

    # ── status ──────────────────────────────────
    p = subparsers.add_parser("status", help="查看状态")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("-s", "--short", action="store_true", help="简洁格式")
    p.add_argument("-b", "--branch", action="store_true", help="显示分支信息")
    p.add_argument("--ignored", action="store_true", help="显示忽略文件")
    p.add_argument("--untracked-files", choices=["all", "no"], default="normal",
                    help="未跟踪文件处理")

    # ── log ─────────────────────────────────────
    p = subparsers.add_parser("log", help="查看提交历史")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("branch", nargs="?", help="分支名")
    p.add_argument("-n", type=int, help="显示条数")
    p.add_argument("--oneline", action="store_true", help="单行格式")
    p.add_argument("--graph", action="store_true", help="图形化显示")
    p.add_argument("--all", action="store_true", help="显示所有分支")
    p.add_argument("--decorate", action="store_true", help="显示分支标签")
    p.add_argument("--stat", action="store_true", help="显示统计")
    p.add_argument("-p", "--patch", action="store_true", help="显示补丁")
    p.add_argument("--author", help="按作者过滤")
    p.add_argument("--since", help="起始日期（如 2024-01-01）")
    p.add_argument("--until", help="截止日期")
    p.add_argument("--grep", help="按提交信息过滤")
    p.add_argument("--file", help="只看某文件的提交")
    p.add_argument("--format", dest="format_", help="输出格式（如 oneline, short）")
    p.add_argument("--reverse", action="store_true", help="逆序显示")
    p.add_argument("--follow", action="store_true", help="追踪文件重命名历史")

    # ── show ─────────────────────────────────────
    p = subparsers.add_parser("show", help="查看提交详情")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("object", nargs="?", help="对象（提交/标签等）")
    p.add_argument("--stat", action="store_true", help="显示统计")
    p.add_argument("--name-only", action="store_true", help="只显示文件名")
    p.add_argument("--format", help="输出格式（如 oneline, short）")

    # ── blame ────────────────────────────────────
    p = subparsers.add_parser("blame", help="文件逐行历史")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("file", help="文件路径")
    p.add_argument("-L", type=int, help="起始行")
    p.add_argument("--L-end", type=int, dest="L_end", help="结束行")
    p.add_argument("-n", "--numbers", action="store_true", help="显示行号")

    # ── tag ─────────────────────────────────────
    p = subparsers.add_parser("tag", help="标签管理")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("--list", action="store_true", help="列出标签")
    p.add_argument("--create", action="store_true", help="创建标签")
    p.add_argument("--delete", action="store_true", help="删除标签")
    p.add_argument("--push", action="store_true", help="推送标签")
    p.add_argument("tag_name", nargs="?", help="标签名")
    p.add_argument("-m", "--message", help="标签信息")
    p.add_argument("--commit", help="指定提交（默认 HEAD）")
    p.add_argument("--remote", help="远端名称")

    # ── remote ──────────────────────────────────
    p = subparsers.add_parser("remote", help="远端管理")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("--list", action="store_true", help="列出远端")
    p.add_argument("--add", action="store_true", help="添加远端")
    p.add_argument("--remove", action="store_true", help="删除远端")
    p.add_argument("--rename", action="store_true", help="重命名远端")
    p.add_argument("--set-url", action="store_true", help="修改 URL")
    p.add_argument("--show", action="store_true", help="显示远端详情")
    p.add_argument("remote_name", nargs="?", help="远端名称")
    p.add_argument("new_name", nargs="?", help="新名称")
    p.add_argument("url", nargs="?", help="URL")

    # ── clean ────────────────────────────────────
    p = subparsers.add_parser("clean", help="清理未跟踪文件")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("--dry-run", action="store_true", help="预览模式")
    p.add_argument("-d", "--dirs", action="store_true", help="清理目录")
    p.add_argument("-f", "--force", action="store_true", help="强制执行")
    p.add_argument("files", nargs="*", help="指定文件/目录")

    # ── gc ──────────────────────────────────────
    p = subparsers.add_parser("gc", help="垃圾回收")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("--aggressive", action="store_true", help="激进压缩")
    p.add_argument("--prune", action="store_true", help="清理悬空对象")
    p.add_argument("--auto", action="store_true", help="自动模式")

    # ── reflog ───────────────────────────────────
    p = subparsers.add_parser("reflog", help="查看引用日志（找回丢失的提交）")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("ref", nargs="?", help="指定引用（如 HEAD、origin/main）")
    p.add_argument("-n", "--max-count", type=int, dest="n", help="显示条数")
    p.add_argument("--show", action="store_true", help="相当于 git show")
    p.add_argument("--expire", action="store_true", help="清理过期条目")
    p.add_argument("--dry-run", action="store_true", help="预览模式")
    p.add_argument("--all", action="store_true", help="处理所有引用")

    # ── describe ─────────────────────────────────
    p = subparsers.add_parser("describe", help="显示语义化版本（基于最近标签）")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("object", nargs="?", help="描述对象（默认 HEAD）")
    p.add_argument("--all", action="store_true", help="考虑所有分支的标签")
    p.add_argument("--tags", action="store_true", help="只考虑标签")
    p.add_argument("--always", action="store_true", help="当无标签时使用 commit hash")
    p.add_argument("--abbrev", type=int, default=7, help="commit hash 缩写长度（默认 7）")
    p.add_argument("--exact-match", action="store_true", help="只匹配精确的标签")
    p.add_argument("--long", action="store_true", help="始终输出长格式")
    p.add_argument("--candidates", type=int, default=10, help="最多候选标签数（默认 10）")
    p.add_argument("--match", help="只匹配符合条件的标签（glob 模式）")
    p.add_argument("--exclude", help="排除符合条件的标签（glob 模式）")
    p.add_argument("--contains", help="在包含指定提交的标签上停止")
    p.add_argument("--connected", action="store_true", help="验证描述对象连通性")
    p.add_argument("--debug", action="store_true", help="调试信息")

    # ── worktree ─────────────────────────────────
    p = subparsers.add_parser("worktree", help="管理工作树（多分支同时工作）")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("path", nargs="?", help="工作树路径")
    p.add_argument("branch", nargs="?", help="分支名")
    p.add_argument("--list", action="store_true", help="列出所有工作树")
    p.add_argument("--add", action="store_true", help="添加工作树")
    p.add_argument("--remove", action="store_true", help="删除工作树")
    p.add_argument("--prune", action="store_true", help="清理失效工作树")
    p.add_argument("--lock", action="store_true", help="锁定工作树")
    p.add_argument("--unlock", action="store_true", help="解锁工作树")
    p.add_argument("--force", action="store_true", help="强制操作")
    p.add_argument("-b", "--checkout", help="创建并切换到新分支")
    p.add_argument("--reason", help="锁定原因")
    p.add_argument("--dry-run", dest="dry_run", action="store_true", help="预览模式")
    p.add_argument("--verbose", action="store_true", help="详细输出")

    # ── grep ─────────────────────────────────────
    p = subparsers.add_parser("grep", help="在仓库中搜索文本")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("pattern", nargs="?", help="搜索模式（字符串或正则）")
    p.add_argument("files", nargs="*", help="搜索的文件")
    p.add_argument("-i", "--ignore-case", action="store_true", help="忽略大小写")
    p.add_argument("-E", "--regexp-extended", action="store_true", help="扩展正则")
    p.add_argument("-G", "--regexp-basic", action="store_true", help="基本正则（默认）")
    p.add_argument("-F", "--fixed-strings", action="store_true", help="固定字符串")
    p.add_argument("--invert-match", action="store_true", help="反向匹配")
    p.add_argument("-w", "--word-regexp", action="store_true", help="整词匹配")
    p.add_argument("-n", "--line-no", action="store_true", help="显示行号")
    p.add_argument("-c", "--count", action="store_true", help="显示匹配行数")
    p.add_argument("-l", "--files-with-matches", action="store_true", help="只显示文件名")
    p.add_argument("-L", "--files-without-match", action="store_true", help="只显示不匹配的文件")
    p.add_argument("--column", action="store_true", help="显示列号")
    p.add_argument("--extended-regexp", action="store_true", help="扩展正则")
    p.add_argument("-o", "--only-matching", action="store_true", help="只显示匹配部分")
    p.add_argument("--max-count", type=int, help="最多匹配次数")
    p.add_argument("--context", type=int, help="显示前后上下文行数")
    p.add_argument("-B", "--before-context", type=int, help="显示前导上下文")
    p.add_argument("-A", "--after-context", type=int, help="显示后续上下文")
    p.add_argument("branch", nargs="?", help="在指定分支搜索")

    # ── cherry-pick ──────────────────────────────
    p = subparsers.add_parser("cherry-pick", help="选取性应用提交")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("commit", nargs="?", help="要 cherry-pick 的提交")
    p.add_argument("--no-commit", "-n", action="store_true", help="执行但不自动提交")
    p.add_argument("--mainline", type=int, help="主提交号（用于合并提交）")
    p.add_argument("--edit", "-e", action="store_true", help="编辑提交信息")
    p.add_argument("--cleanup", choices=["default", "scissors", "whitespace", "verbatim", "none"],
                   default="default", help="清理模式")
    p.add_argument("--no-verify", action="store_true", help="跳过 pre-commit hook")
    p.add_argument("--include", nargs="+", help="包含指定提交")
    p.add_argument("--exclude", nargs="+", help="排除指定提交")
    p.add_argument("--ff", action="store_true", help="允许快进")
    p.add_argument("--force", "-f", action="store_true", help="强制继续")
    p.add_argument("-q", "--quiet", action="store_true", help="静默模式")
    p.add_argument("--continue", dest="continue_", action="store_true", help="继续 cherry-pick")
    p.add_argument("--abort", action="store_true", help="放弃并恢复")
    p.add_argument("--skip", action="store_true", help="跳过当前提交")

    # ── revert ───────────────────────────────────
    p = subparsers.add_parser("revert", help="生成反向提交（安全撤销）")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("commit", nargs="?", help="要 revert 的提交")
    p.add_argument("--no-commit", "-n", action="store_true", help="执行但不自动提交")
    p.add_argument("--edit", "-e", action="store_true", help="编辑提交信息")
    p.add_argument("--no-verify", action="store_true", help="跳过 pre-commit hook")
    p.add_argument("--cleanup", choices=["default", "scissors", "whitespace", "verbatim", "none"],
                   default="default", help="清理模式")
    p.add_argument("-q", "--quiet", action="store_true", help="静默模式")
    p.add_argument("--ff", action="store_true", help="允许快进")
    p.add_argument("--include", nargs="+", help="包含指定提交")
    p.add_argument("--exclude", nargs="+", help="排除指定提交")
    p.add_argument("--continue", dest="continue_", action="store_true", help="继续 revert")
    p.add_argument("--abort", action="store_true", help="放弃并恢复")
    p.add_argument("--skip", action="store_true", help="跳过当前提交")

    # ── bisect ───────────────────────────────────
    p = subparsers.add_parser("bisect", help="二分查找定位 bug")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("bad", nargs="?", help="已知有问题的提交（默认 HEAD）")
    p.add_argument("good", nargs="?", help="已知正常的提交")
    p.add_argument("--start", action="store_true", help="启动二分查找")
    p.add_argument("--reset", action="store_true", help="重置并退出 bisect")
    p.add_argument("--skip", action="store_true", help="跳过当前提交")
    p.add_argument("--visualize", action="store_true", help="可视化 bisect")
    p.add_argument("--log", action="store_true", help="显示 bisect 日志")
    p.add_argument("--run", action="store_true", help="自动运行 bisect")
    p.add_argument("--command", help="自动 bisect 的测试命令")
    p.add_argument("--terms", choices=["old", "new"], help="显示术语")
    p.add_argument("revision", nargs="?", help="指定版本")

    # ── lfs ─────────────────────────────────────
    p = subparsers.add_parser("lfs", help="Git LFS 操作")
    p.add_argument("repo_path", help="本地仓库路径")
    p.add_argument("--install", action="store_true", help="初始化 LFS")
    p.add_argument("--track", action="store_true", help="跟踪文件模式")
    p.add_argument("--untrack", nargs="+", help="取消跟踪模式")
    p.add_argument("--fetch", action="store_true", help="LFS Fetch")
    p.add_argument("--pull", action="store_true", help="LFS Pull")
    p.add_argument("--push", action="store_true", help="LFS Push")
    p.add_argument("--remote", help="指定远端")
    p.add_argument("--branch", help="指定分支")
    p.add_argument("--ls-files", action="store_true", help="列出 LFS 文件")
    p.add_argument("--track-list", action="store_true", help="列出跟踪模式")
    p.add_argument("--scan", action="store_true", help="扫描 LFS 对象")
    p.add_argument("patterns", nargs="*", help="文件模式（如 *.psd, *.zip）")

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(0)

    # 命令分发
    dispatch = {
        "clone": cmd_clone, "pull": cmd_pull, "fetch": cmd_fetch,
        "branch": cmd_branch, "checkout": cmd_checkout,
        "merge": cmd_merge, "rebase": cmd_rebase,
        "add": cmd_add, "commit": cmd_commit, "reset": cmd_reset, "stash": cmd_stash,
        "diff": cmd_diff, "status": cmd_status, "log": cmd_log,
        "show": cmd_show, "blame": cmd_blame,
        "tag": cmd_tag, "remote": cmd_remote,
        "clean": cmd_clean, "gc": cmd_gc,
        "lfs": cmd_lfs,
        "reflog": cmd_reflog, "describe": cmd_describe,
        "worktree": cmd_worktree, "grep": cmd_grep,
        "cherry-pick": cmd_cherry_pick, "revert": cmd_revert,
        "bisect": cmd_bisect,
    }
    dispatch[args.command](args)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)