11 Commits

Author SHA1 Message Date
zyj
3ebcd10a23 release: v0.4.1 - GitLab支持、迁移优化、Bug修复
Some checks failed
Release / build-windows (push) Has been cancelled
 新功能:
- GitLab 平台完整支持(16+7 个 API 方法、前端图标/选择器)
- Release 同步支持手动输入 Token

🐛 Bug 修复:
- 自建平台凭证匹配(BaseURL 优先精确匹配)
- Gitea 迁移 API service 字段类型修复(HTTP 422)
- 构建时不再覆盖已有 config.yaml(数据丢失问题)
- 错误信息显示实际 URL

🔨 改进:
- 源平台开启代理时自动跳过 Gitea 原生迁移,改用本地代理中转
- Push 超时从 30s 提升到 10 分钟
- 设置 http.postBuffer=500MB,push --all 失败自动逐分支推送
- 新增 RunWithProxyTimeout 方法
2026-03-12 15:07:41 +08:00
zyj
c787c99622 feat: v0.4.0 — 配置架构重构 + 一键多端推送 + 项目级代理控制\n\n架构重构:\n- 配置从 Platform→User→Project 重构为扁平的 Projects+Groups+Credentials 模型\n- 侧边栏完全重写,适配新分组→项目树结构\n- 新增分组(Group)管理和凭据(Credential)管理\n\n新功能:\n- 一键多端推送:Push All 按钮,将当前分支/标签推送到所有远程仓库\n- 项目级代理控制:每个项目独立设置代理(跟随全局/强制开启/强制关闭)\n- GitClient 新增 RunWithProxy 方法,支持按调用级别覆盖全局代理\n- 所有网络操作(Pull/Push/Fetch)均支持项目级代理设置\n\n其他:\n- 新增 CHANGELOG.md 版本更新日志\n- 更新 README/README_CN 文档,反映最新 72+ API 方法\n- 版本号升级到 0.4.0(config.yml/info.json/Info.plist)
Some checks failed
Release / build-windows (push) Has been cancelled
2026-03-12 11:41:26 +08:00
zyj
3ec453cc38 feat: 多 remote 远程仓库支持 (v0.3.1)\n\n后端:\n- 新增 RemoteList/AddRemote/RemoveRemote 远程仓库管理\n- 所有远程操作(Push/Pull/Fetch/PushTag/DeleteTag/DeleteRemoteBranch等)支持指定 remote\n- 新增 PushTo/PullFrom/FetchRemote/PushTagTo/DeleteRemoteTagFrom 等方法\n- BatchPull/BatchPush 支持 remote 参数\n\n前端:\n- 顶部信息栏新增 remote 选择器下拉菜单\n- 支持切换当前操作的远程仓库\n- 支持添加/删除远程仓库\n- Pull/Push/Fetch/标签推送/远程分支删除等操作均使用选中的 remote
Some checks failed
Release / build-windows (push) Has been cancelled
2026-03-12 10:57:27 +08:00
zyj
3d6874f79e chore: 升级版本号到 v0.3.0\n\n- Windows info.json: 0.2.0 → 0.3.0\n- macOS Info.plist: 0.2.0 → 0.3.0\n- build/config.yml: 0.1.0 → 0.3.0(修复上次遗漏) 2026-03-11 10:31:20 +08:00
zyj
d6dbd8834b fix: 修复 GitCheck 组件 @wailsio/runtime Events 导入错误 2026-03-11 10:28:58 +08:00
zyj
beb40688ce feat: 启动时检测 Git 安装状态,支持自动下载安装\n\n- 启动时自动检测 Git 是否已安装\n- 未安装时显示全屏提示界面\n- 支持用户选择安装路径\n- 自动从 GitHub 下载 Git for Windows 并静默安装\n- 实时显示下载进度和安装状态\n- 安装完成后自动重新初始化 Git 客户端" 2026-03-11 10:18:26 +08:00
zyj
5154bb5564 chore: 升级版本号到 v0.2.0,修复 macOS/Linux 配置\n\n- 升级所有平台版本号 0.1.0 → 0.2.0\n- 修复 macOS Info.plist 残留的旧项目名 Peter Agent → GitPilot\n- 修复 Linux desktop 文件残留的旧项目名" 2026-03-11 10:10:37 +08:00
zyj
af15e86df6 feat: 新增冲突处理、提交搜索、批量操作功能\n\n冲突处理:\n- 检测合并状态并显示冲突提示栏\n- 冲突文件列表展示和内容编辑器\n- 支持保存冲突文件、标记已解决、中止合并、完成合并\n\n提交搜索:\n- 支持按提交信息关键字和作者搜索\n- 搜索结果与完整历史切换显示\n\n批量操作:\n- 侧边栏新增批量操作入口\n- 展示所有项目概览(分支/变更/未推送状态)\n- 支持批量 Pull/Push 并显示执行结果" 2026-03-11 10:06:28 +08:00
zyj
6354cbfc94 feat: 新增分支管理、Stash贮藏、远程分支、设置页面\n\n分支管理:\n- 创建新分支\n- 删除本地分支(安全/强制)\n- 合并分支到当前分支\n\n远程分支:\n- 查看远程分支列表\n- 检出远程分支到本地\n- 删除远程分支\n\nStash 贮藏:\n- 保存当前变更(支持自定义消息)\n- 应用/弹出/删除贮藏\n- 贮藏列表管理\n\n设置:\n- Git 全局配置 (user.name / user.email)\n- 设置弹窗 UI" 2026-03-11 09:37:58 +08:00
zyj
cb6b21ef98 refactor: 拆分 proxy.go 为平台特定文件,兼容 macOS 和 Linux\n\n- proxy.go: 保留通用类型和辅助函数\n- proxy_windows.go: 通过注册表读取 Windows 系统代理\n- proxy_darwin.go: 通过 scutil --proxy 读取 macOS 系统代理\n- proxy_linux.go: 通过 gsettings/kreadconfig 读取 Linux 系统代理" 2026-03-10 11:55:34 +08:00
zyj
6e0becb748 新增远程项目克隆功能 2026-03-10 10:25:45 +08:00
33 changed files with 10414 additions and 779 deletions

93
CHANGELOG.md Normal file
View File

@@ -0,0 +1,93 @@
# Changelog
## v0.4.1 (2026-03-12)
### ✨ 新功能
- **GitLab 平台支持**:新增 GitLab 作为第四个平台GitHub / Gitee / Gitea / GitLab
- 完整实现 PlatformAPI 全部 16 个接口方法CreateRepo / ListRepos / ListReleases / CreateRelease / UploadAsset 等)
- 实现 7 个扩展迁移方法Labels / Milestones / Issues / PullRequests
- 前端新增 GitLab 图标、平台选择项、BaseURL 输入支持
- **Release 同步 Token 输入**:同步发布时可手动输入源/目标 Token不再依赖配置文件中的凭证
### 🐛 Bug 修复
- **凭证匹配修复**自建平台Gitea/GitLab优先按 BaseURL 精确匹配,解决自建 GitLab 被误识别为 Gitea 的问题
- **Gitea 迁移 API 修复**`service` 字段从整数改为字符串,修复 HTTP 422 错误
- **配置文件覆盖修复**:构建时仅在 `bin/config.yaml` 不存在时复制初始模板,避免每次 `wails3 dev` 丢失已有配置
- **错误信息优化**:迁移相关错误信息显示实际 URL 而非推断的平台名
### 🔨 改进
- **智能迁移路径**:源平台开启代理时自动跳过 Gitea 原生迁移,改用本地代理中转(解决国内服务器无法访问 GitHub 的问题)
- **推送超时优化**:在线迁移的 push 操作超时从 30 秒提升到 10 分钟
- **大仓库推送**:设置 `http.postBuffer=500MB`push --all 失败时自动回退为逐分支推送,避免 HTTP 413
- **Git 操作**:新增 `RunWithProxyTimeout` 方法,支持自定义超时的 git 命令执行
---
## v0.4.0 (2026-03-12)
### 🔧 架构重构
- **配置架构重构**:从 `Platform → User → Project` 层级结构重构为扁平的 `Projects + Groups + Credentials` 模型
- 项目支持多远程仓库指向不同平台,不再受限于单一平台层级
- 新增分组Group管理支持自定义分组和图标
- 新增凭据Credential管理支持 GitHub / Gitee / Gitea 等多平台凭据
- 侧边栏完全重写,适配新的分组→项目树结构
### ✨ 新功能
- **一键多端推送**Push All 按钮,一键将当前分支推送到所有远程仓库
- 顶部操作栏新增 "Push All" 按钮(仅在有多个远程仓库时启用)
- 标签管理新增 "推送到所有远程" 按钮
- 推送结果逐一展示每个远程仓库的成功/失败状态
- **项目代理开关**:每个项目可独立控制是否使用系统代理
- 三种模式:跟随全局 / 强制使用代理 / 强制不使用代理
- 顶部操作栏新增代理设置下拉菜单
- Pull / Push / Fetch / Push All 等所有网络操作均尊重项目级代理设置
### 🔨 后端改进
- `GitClient` 新增 `RunWithProxy()` 方法,支持按调用级别覆盖全局代理设置
- 所有网络操作Pull/Push/Fetch/PushToAllRemotes/PushTagToAllRemotes均支持项目级代理
- 配置管理方法重构Group CRUD / Credential CRUD / Project 分组管理
- 新增 `PushToAllRemotes()` / `PushTagToAllRemotes()` 服务方法
- 新增 `GetProjectProxy()` / `SetProjectProxy()` 服务方法
---
## v0.3.1 (2026-02-27)
### ✨ 新功能
- **多 Remote 远程仓库支持**
- 新增 RemoteList / AddRemote / RemoveRemote 远程仓库管理
- 所有远程操作支持指定 remotePush/Pull/Fetch/PushTag/DeleteTag 等)
- 顶部信息栏新增 remote 选择器下拉菜单
- 支持切换当前操作的远程仓库、添加/删除远程仓库
---
## v0.3.0 (2026-02-25)
### ✨ 新功能
- **Git 安装检测**:启动时检测 Git 是否已安装,支持自动下载安装 Git for Windows
- **冲突处理**:合并冲突检测、冲突文件编辑器、标记已解决、中止/完成合并
- **提交搜索**:支持按提交信息关键字和作者搜索
- **批量操作**:侧边栏批量 Pull/Push 所有项目
---
## v0.2.0 (2026-02-20)
### ✨ 新功能
- 分支管理(创建/删除/合并)
- 远程分支(查看/检出/删除)
- Stash 贮藏管理
- Git 全局配置设置user.name / user.email
- Tag 标签管理
- CI/CD 发布流水线
- NSIS 安装包
---
## v0.1.0
- 初始版本
- 基本 Git 操作Clone / Pull / Push / Commit
- 文件变更查看和 Diff 展示
- 系统代理自动检测

121
README.md
View File

@@ -4,7 +4,7 @@
A cross-platform desktop Git repository management tool built with **Go + Wails v3 + Nuxt 4 + Vue 3**.
Manage multiple Git repositories across different platforms (GitHub, Gitee, Gitea, etc.) from a single unified interface — like a lightweight, standalone version of VS Code's Git panel.
Manage multiple Git repositories across different platforms (GitHub, Gitee, Gitea, etc.) from a single unified interface — like a lightweight, standalone version of VS Codes Git panel.
![Go](https://img.shields.io/badge/Go-1.25-00ADD8?logo=go&logoColor=white)
![Wails](https://img.shields.io/badge/Wails-v3-red?logo=go&logoColor=white)
@@ -14,10 +14,12 @@ Manage multiple Git repositories across different platforms (GitHub, Gitee, Gite
## ✨ Features
### Multi-Platform Management
### Project & Group Management
- Manage repositories from **GitHub**, **Gitee**, **Gitea**, and custom Git platforms
- Organize projects by **Platform → User → Project** hierarchy
- Add / remove platforms, users, and projects through the GUI
- Organize projects into custom **Groups** (e.g. `github/user`, `gitea`, `work`)
- Manage **Credentials** per platform (GitHub / Gitee / Gitea with custom base URL)
- **Clone** remote repositories directly from the GUI with group assignment
- Add / remove / move projects between groups
### VS Code-Style Git Operations
- **View changed files** with a collapsible directory tree structure
@@ -27,51 +29,96 @@ Manage multiple Git repositories across different platforms (GitHub, Gitee, Gite
- **Diff viewer** with syntax-highlighted additions and deletions
- **Binary file detection** — skips preview for images, executables, etc.
### Multi-Remote Support
- Manage multiple remotes per project (origin, upstream, etc.)
- **Remote selector** dropdown to switch active remote
- Add / remove remotes through the GUI
- **Push to All Remotes** — one-click push current branch to every remote
- **Push Tag to All Remotes** — push tags to all remotes at once
### Per-Project Proxy Control
- Each project can independently control proxy usage
- Three modes: **Follow Global** / **Force Proxy On** / **Force Proxy Off**
- All network operations (Pull / Push / Fetch / Push All) respect per-project proxy settings
- System proxy auto-detection from Windows registry / macOS scutil / Linux gsettings
### Commit History
- Browse commit history with author, time, and message
- Click a commit to view its **changed file list** (like VS Code)
- Click a commit to view its **changed file list**
- Select a file to view its **per-file diff**
- **Version rollback** — hard / mixed / soft reset with confirmation dialog
- **Revert commit** — create a reverse commit
- **Search commits** — filter by message keyword and/or author
### Branch Management
- View all local branches with current branch indicator
- View all local and remote branches
- **Create / Delete / Merge** branches
- **Switch branches** via dropdown selector
- **Pull / Push / Fetch** with current branch awareness
- **Checkout remote branches** to local
- **Delete remote branches**
### Tag Management
- View all tags with hash, message, and timestamp
- **Create tags** (lightweight or annotated)
- **Push tags** to selected remote or all remotes
- **Delete tags** (local and remote)
### Stash Management
- **Save** current changes with optional message
- **Apply / Pop / Drop** stash entries
- View stash list with descriptions
### Merge Conflict Resolution
- Detect merge state and display conflict indicator
- View and edit conflict files in a built-in editor
- **Mark as resolved / Abort merge / Complete merge**
### Batch Operations
- View all projects overview (branch, changes, push status)
- **Batch Pull / Push** all projects at once
- Results displayed per project
### Git Installation Detection
- Auto-detect Git installation on startup
- If not installed, show a full-screen guide
- **Auto-download and install** Git for Windows with progress tracking
### Desktop Experience
- **System tray** — close window hides to tray, click tray icon to restore
- **Resizable panels** — drag splitters between sidebar, file list, and viewer
- **Dark theme** — Catppuccin Mocha color scheme with Ant Design Vue
- **Native folder picker** — select project paths via OS file dialog
- Git global config editor (user.name / user.email)
## 🏗️ Tech Stack
| Layer | Technology |
|-------|-----------|
| Backend | Go 1.25, Wails v3 (alpha.74) |
| Backend | Go 1.25, Wails v3 |
| Frontend | Nuxt 4, Vue 3.5, Vite 7 |
| UI Library | Ant Design Vue 4, @ant-design/icons-vue |
| Git Operations | Git CLI with proxy support, 30s timeout |
| Git Operations | Git CLI wrapper with per-project proxy support |
| Config | YAML-based persistent configuration |
| Build | Taskfile v3 |
| Build | Taskfile v3, NSIS installer (Windows) |
## 📁 Project Structure
```
GitPilot/
├── main.go # Wails app entry, system tray, window management
├── config.yaml # Platform/user/project configuration
├── cmd/main.go # Wails app entry, system tray, window management
├── config.yaml # Projects / Groups / Credentials configuration
├── CHANGELOG.md # Version history
├── Taskfile.yml # Build tasks
├── go.mod
├── cmd/ # CLI utilities
├── config/
│ └── config.go # YAML config loader/saver
│ └── config.go # YAML config types & loader/saver
├── internal/
│ ├── proxy.go # System proxy detection
│ ├── proxy.go # System proxy detection (Windows/macOS/Linux)
│ ├── git/
│ │ └── client.go # Git CLI wrapper (34 operations)
│ │ └── client.go # Git CLI wrapper with RunWithProxy support
│ └── app/
── service.go # AppService — 34 methods exposed to frontend
── service.go # AppService — 72 methods exposed to frontend
│ └── gitsetup.go # Git installation detection & auto-install
├── frontend/
│ ├── nuxt.config.ts
│ ├── package.json
@@ -81,10 +128,12 @@ GitPilot/
│ ├── layouts/
│ │ └── default.vue # Ant Design dark theme provider
│ └── components/
│ ├── Sidebar.vue # Platform/user/project tree sidebar
│ ├── ContentArea.vue # Git status, diff, history, branches
── FileTreeNode.vue # Recursive file tree with actions
└── build/ # Wails build configuration
│ ├── Sidebar.vue # Group/project tree, batch ops, credentials
│ ├── ContentArea.vue # Git status, diff, history, branches, tags
── FileTreeNode.vue # Recursive file tree with stage/discard
│ ├── CommitFileTreeNode.vue # Commit file tree viewer
│ └── GitCheck.vue # Git installation check screen
└── build/ # Wails build config, NSIS installer scripts
```
## 🚀 Getting Started
@@ -108,14 +157,9 @@ cd GitPilot
cd frontend && npm install && cd ..
# Generate Wails bindings
wails3 generate bindings
# Build frontend
cd frontend && npx nuxi generate && cd ..
wails3 generate bindings -d frontend/bindings
# Run in development mode
task dev
# or
wails3 dev -config ./build/config.yml -port 9245
```
@@ -129,19 +173,28 @@ The binary will be output to the `bin/` directory.
## 📋 API Overview
GitPilot exposes **34 backend methods** to the frontend via Wails bindings:
GitPilot exposes **72+ backend methods** to the frontend via Wails bindings:
| Category | Methods |
|----------|---------|
| **Projects** | GetProjectTree, AddProject, RemoveProject, SelectDirectory |
| **Platforms** | AddPlatform, UpdatePlatform, RemovePlatform, GetPlatformInfo |
| **Users** | AddUser, UpdateUser, RemoveUser, GetUserInfo |
| **Project Management** | GetProjectTree, AddProject, RemoveProject, CloneProject, MoveProjectToGroup, SelectDirectory |
| **Group Management** | GetGroups, AddGroup, UpdateGroup, RemoveGroup |
| **Credential Management** | GetCredentials, AddCredential, UpdateCredential, RemoveCredential |
| **Git Status** | GetProjectStatus, GetProjectChangedFiles, GetFileContent, GetFileDiff, GetFileDiffStaged |
| **Staging** | StageFiles, UnstageFiles, StageAll, UnstageAll |
| **Commit** | CommitChanges, DiscardFiles |
| **Remote** | PullProject, PushProject, FetchProject |
| **History** | GetCommitLog, GetCommitDiff, GetCommitFiles, GetCommitFileDiff |
| **Branches** | GetBranches, SwitchBranch, ResetProject |
| **Remote Operations** | PullProject, PushProject, FetchProject, PushToAllRemotes, PushTagToAllRemotes |
| **Remote Management** | GetRemotes, AddRemote, RemoveRemote |
| **Proxy Control** | GetProjectProxy, SetProjectProxy |
| **Commit History** | GetCommitLog, GetCommitDiff, GetCommitFiles, GetCommitFileDiff, RevertCommit, SearchCommitLog |
| **Branch Management** | GetBranches, SwitchBranch, CreateBranch, DeleteBranch, MergeBranch, ResetProject |
| **Remote Branches** | GetRemoteBranches, CheckoutRemoteBranch, DeleteRemoteBranch |
| **Tags** | GetTags, CreateTag, DeleteTag, PushTag |
| **Stash** | StashSave, GetStashList, StashApply, StashPop, StashDrop |
| **Conflict Resolution** | IsMerging, GetConflictFiles, GetConflictFileContent, SaveConflictFile, ResolveConflictFile, AbortMerge |
| **Batch Operations** | GetAllProjectOverview, BatchPull, BatchPush |
| **Settings** | GetGitGlobalConfig, SetGitGlobalConfig, GetAppSettings, UpdateAppSettings |
| **Git Setup** | CheckGitInstalled, SelectGitInstallDir, InstallGit |
## 📄 License

View File

@@ -14,10 +14,12 @@
## ✨ 功能特性
### 多平台管理
### 项目与分组管理
- 管理来自 **GitHub**、**Gitee**、**Gitea** 及自建 Git 平台的仓库
- **平台 → 用户 → 项目** 层级组织
- 通过图形界面添加/删除平台、用户和项目
-自定义**分组**组织项目(如 `github/user``gitea``work`
- 按平台管理**凭据**GitHub / Gitee / Gitea支持自定义 Base URL
- 直接在界面中**克隆**远程仓库并分配分组
- 添加 / 删除 / 移动项目到不同分组
### VS Code 风格的 Git 操作
- 以可折叠的**目录树结构**查看变更文件
@@ -27,51 +29,96 @@
- **Diff 查看器** — 语法高亮显示增删内容
- **二进制文件检测** — 自动跳过图片、可执行文件等的预览
### 多远程仓库支持
- 每个项目支持管理多个远程仓库origin、upstream 等)
- **远程仓库选择器** — 下拉切换当前操作的远程仓库
- 通过界面添加 / 删除远程仓库
- **一键推送到所有远程** — 将当前分支推送到全部远程仓库
- **标签多端推送** — 一键将标签推送到所有远程仓库
### 项目级代理控制
- 每个项目可独立控制是否使用代理
- 三种模式:**跟随全局** / **强制使用代理** / **强制不使用代理**
- 所有网络操作Pull / Push / Fetch / Push All均尊重项目级代理设置
- 系统代理自动检测Windows 注册表 / macOS scutil / Linux gsettings
### 提交历史
- 浏览提交历史,显示作者、时间和提交信息
- 点击提交查看该版本的**变更文件列表**(与 VS Code 一致)
- 点击提交查看该版本的**变更文件列表**
- 选中文件查看**单文件 diff**
- **版本回滚** — 支持硬回滚/混合回滚/软回滚,操作前弹出确认对话框
- **撤销提交** — 生成反向提交
- **提交搜索** — 按提交信息关键字和/或作者过滤
### 分支管理
- 查看所有本地分支,标记当前分支
- 查看所有本地分支和远程分支
- **创建 / 删除 / 合并**分支
- 通过下拉菜单**切换分支**
- **拉取/推送/获取远程信息**,自动识别当前分支
- **检出远程分支**到本地
- **删除远程分支**
### 标签管理
- 查看所有标签(显示哈希、描述、时间戳)
- **创建标签**(轻量标签或附注标签)
- **推送标签**到指定远程或所有远程
- **删除标签**(本地和远程同步删除)
### 贮藏管理
- **保存**当前变更(支持自定义描述)
- **应用 / 弹出 / 删除**贮藏条目
- 查看贮藏列表及描述
### 合并冲突处理
- 自动检测合并状态并显示冲突提示
- 在内置编辑器中查看和编辑冲突文件
- **标记已解决 / 中止合并 / 完成合并**
### 批量操作
- 查看所有项目概览(分支、变更数、推送状态)
- **批量 Pull / Push** 所有项目
- 按项目显示操作结果
### Git 安装检测
- 启动时自动检测 Git 是否已安装
- 未安装时显示全屏引导界面
- 支持**自动下载安装** Git for Windows实时显示进度
### 桌面体验
- **系统托盘** — 关闭窗口自动隐藏到托盘,点击托盘图标恢复窗口
- **系统托盘** — 关闭窗口自动隐藏到托盘,点击图标恢复
- **可拖拽调整面板** — 侧边栏、文件列表、查看器之间的分隔条可拖拽
- **暗色主题** — Catppuccin Mocha 配色方案 + Ant Design Vue
- **原生文件夹选择器** — 通过系统对话框选择项目路径
- Git 全局配置编辑user.name / user.email
## 🏗️ 技术栈
| 层级 | 技术 |
|------|------|
| 后端 | Go 1.25, Wails v3 (alpha.74) |
| 后端 | Go 1.25, Wails v3 |
| 前端 | Nuxt 4, Vue 3.5, Vite 7 |
| UI 组件库 | Ant Design Vue 4, @ant-design/icons-vue |
| Git 操作 | Git CLI 封装,支持代理30 秒超时 |
| Git 操作 | Git CLI 封装,支持项目级代理控制 |
| 配置 | 基于 YAML 的持久化配置 |
| 构建 | Taskfile v3 |
| 构建 | Taskfile v3, NSIS 安装包Windows |
## 📁 项目结构
```
GitPilot/
├── main.go # Wails 应用入口,系统托盘,窗口管理
├── config.yaml # 平台/用户/项目配置文件
├── cmd/main.go # Wails 应用入口,系统托盘,窗口管理
├── config.yaml # 项目 / 分组 / 凭据配置文件
├── CHANGELOG.md # 版本更新日志
├── Taskfile.yml # 构建任务
├── go.mod
├── cmd/ # 命令行工具
├── config/
│ └── config.go # YAML 配置加载/保存
│ └── config.go # YAML 配置类型 & 加载/保存
├── internal/
│ ├── proxy.go # 系统代理检测
│ ├── proxy.go # 系统代理检测Windows/macOS/Linux
│ ├── git/
│ │ └── client.go # Git CLI 封装34 个操作)
│ │ └── client.go # Git CLI 封装,支持 RunWithProxy
│ └── app/
── service.go # AppService — 34 个方法暴露给前端
── service.go # AppService — 72 个方法暴露给前端
│ └── gitsetup.go # Git 安装检测 & 自动安装
├── frontend/
│ ├── nuxt.config.ts
│ ├── package.json
@@ -81,10 +128,12 @@ GitPilot/
│ ├── layouts/
│ │ └── default.vue # Ant Design 暗色主题配置
│ └── components/
│ ├── Sidebar.vue # 平台/用户/项目树形侧边栏
│ ├── ContentArea.vue # Git 状态、Diff、历史、分支
── FileTreeNode.vue # 递归文件树组件(含操作按钮
└── build/ # Wails 构建配置
│ ├── Sidebar.vue # 分组/项目树、批量操作、凭据管理
│ ├── ContentArea.vue # Git 状态、Diff、历史、分支、标签
── FileTreeNode.vue # 递归文件树(含暂存/丢弃操作
├── CommitFileTreeNode.vue # 提交文件树查看器
│ └── GitCheck.vue # Git 安装检测界面
└── build/ # Wails 构建配置NSIS 安装脚本
```
## 🚀 快速开始
@@ -108,14 +157,9 @@ cd GitPilot
cd frontend && npm install && cd ..
# 生成 Wails 绑定
wails3 generate bindings
# 构建前端
cd frontend && npx nuxi generate && cd ..
wails3 generate bindings -d frontend/bindings
# 开发模式运行
task dev
# 或者
wails3 dev -config ./build/config.yml -port 9245
```
@@ -129,19 +173,28 @@ task build
## 📋 API 概览
GitPilot 通过 Wails 绑定向前端暴露了 **34 个后端方法**
GitPilot 通过 Wails 绑定向前端暴露了 **72+ 个后端方法**
| 类别 | 方法 |
|------|------|
| **项目管理** | GetProjectTree, AddProject, RemoveProject, SelectDirectory |
| **平台管理** | AddPlatform, UpdatePlatform, RemovePlatform, GetPlatformInfo |
| **用户管理** | AddUser, UpdateUser, RemoveUser, GetUserInfo |
| **项目管理** | GetProjectTree, AddProject, RemoveProject, CloneProject, MoveProjectToGroup, SelectDirectory |
| **分组管理** | GetGroups, AddGroup, UpdateGroup, RemoveGroup |
| **凭据管理** | GetCredentials, AddCredential, UpdateCredential, RemoveCredential |
| **Git 状态** | GetProjectStatus, GetProjectChangedFiles, GetFileContent, GetFileDiff, GetFileDiffStaged |
| **暂存操作** | StageFiles, UnstageFiles, StageAll, UnstageAll |
| **提交操作** | CommitChanges, DiscardFiles |
| **远程操作** | PullProject, PushProject, FetchProject |
| **提交历史** | GetCommitLog, GetCommitDiff, GetCommitFiles, GetCommitFileDiff |
| **分支管理** | GetBranches, SwitchBranch, ResetProject |
| **远程操作** | PullProject, PushProject, FetchProject, PushToAllRemotes, PushTagToAllRemotes |
| **远程仓库管理** | GetRemotes, AddRemote, RemoveRemote |
| **代理控制** | GetProjectProxy, SetProjectProxy |
| **提交历史** | GetCommitLog, GetCommitDiff, GetCommitFiles, GetCommitFileDiff, RevertCommit, SearchCommitLog |
| **分支管理** | GetBranches, SwitchBranch, CreateBranch, DeleteBranch, MergeBranch, ResetProject |
| **远程分支** | GetRemoteBranches, CheckoutRemoteBranch, DeleteRemoteBranch |
| **标签管理** | GetTags, CreateTag, DeleteTag, PushTag |
| **贮藏管理** | StashSave, GetStashList, StashApply, StashPop, StashDrop |
| **冲突处理** | IsMerging, GetConflictFiles, GetConflictFileContent, SaveConflictFile, ResolveConflictFile, AbortMerge |
| **批量操作** | GetAllProjectOverview, BatchPull, BatchPush |
| **设置** | GetGitGlobalConfig, SetGitGlobalConfig, GetAppSettings, UpdateAppSettings |
| **Git 安装** | CheckGitInstalled, SelectGitInstallDir, InstallGit |
## 📄 开源协议

View File

@@ -9,7 +9,7 @@ info:
description: "Git Repository Management Tool"
copyright: "(c) 2025, GitPilot"
comments: "GitPilot - Git Repository Manager"
version: "0.1.0"
version: "0.4.1"
dev_mode:
root_path: .

View File

@@ -4,17 +4,17 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>Peter Agent</string>
<string>GitPilot</string>
<key>CFBundleExecutable</key>
<string>peter-agent</string>
<string>gitpilot</string>
<key>CFBundleIdentifier</key>
<string>com.peteragent.app</string>
<string>com.gitpilot.app</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<string>0.2.0</string>
<key>CFBundleGetInfoString</key>
<string>AI Agent Application</string>
<string>Git Repository Management Tool</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>0.2.0</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
@@ -22,7 +22,7 @@
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>© 2025, Peter Agent</string>
<string>© 2025, GitPilot</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>

View File

@@ -4,17 +4,17 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>Peter Agent</string>
<string>GitPilot</string>
<key>CFBundleExecutable</key>
<string>peter-agent</string>
<string>gitpilot</string>
<key>CFBundleIdentifier</key>
<string>com.peteragent.app</string>
<string>com.gitpilot.app</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<string>0.4.1</string>
<key>CFBundleGetInfoString</key>
<string>AI Agent Application</string>
<string>Git Repository Management Tool</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>0.4.1</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
@@ -22,6 +22,6 @@
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>© 2025, Peter Agent</string>
<string>© 2025, GitPilot</string>
</dict>
</plist>

View File

@@ -1,13 +1,13 @@
[Desktop Entry]
Version=1.0
Name=Peter Agent
Comment=AI Agent Application
Name=GitPilot
Comment=Git Repository Management Tool
# The Exec line includes %u to pass the URL to the application
Exec=/usr/local/bin/peter-agent %u
Exec=/usr/local/bin/gitpilot %u
Terminal=false
Type=Application
Icon=peter-agent
Icon=gitpilot
Categories=Utility;
StartupWMClass=peter-agent
StartupWMClass=gitpilot

View File

@@ -18,9 +18,9 @@ tasks:
cmds:
- task: generate:syso
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
- cmd: powershell Copy-Item config.yaml -Destination {{.BIN_DIR}}/config.yaml -Force
- cmd: powershell -Command "if (-not (Test-Path '{{.BIN_DIR}}/config.yaml')) { Copy-Item config.yaml -Destination '{{.BIN_DIR}}/config.yaml' }"
platforms: [windows]
- cmd: cp config.yaml {{.BIN_DIR}}/config.yaml
- cmd: test -f {{.BIN_DIR}}/config.yaml || cp config.yaml {{.BIN_DIR}}/config.yaml
platforms: [linux, darwin]
- cmd: powershell Remove-item *.syso
platforms: [windows]

View File

@@ -1,10 +1,10 @@
{
"fixed": {
"file_version": "0.1.0"
"file_version": "0.4.1"
},
"info": {
"0000": {
"ProductVersion": "0.1.0",
"ProductVersion": "0.4.1",
"CompanyName": "GitPilot",
"FileDescription": "Git Repository Management Tool",
"LegalCopyright": "© 2025, GitPilot",

View File

@@ -1,22 +1,22 @@
platforms:
gitea:
base_url: http://192.168.1.10:3000
users: []
gitee:
base_url: ""
users:
- username: zhuyuj
token: ""
projects: []
github:
base_url: ""
users:
- username: zhuy1228
token: ""
projects:
- name: GitPilot
path: F:\Project\GitPilot
enabled: false
projects:
- name: GitPilot
path: F:\Project\GitPilot
group: github/zhuy1228
groups:
- name: github/zhuy1228
icon: github
- name: gitee/zhuyuj
icon: gitee
- name: gitea
icon: gitea
credentials:
- platform: github
username: zhuy1228
- platform: gitee
username: zhuyuj
- platform: gitea
base_url: http://localhost:3000
username: "zhuyujun"
settings:
concurrency: 6
network_check: true

View File

@@ -8,32 +8,41 @@ import (
"gopkg.in/yaml.v3"
)
type Project struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
Enabled bool `yaml:"enabled"` // 可选,默认 true
// ProjectItem 项目配置(以路径为核心)
type ProjectItem struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
Group string `yaml:"group,omitempty"` // 所属分组名,可为空表示"未分组"
UseProxy *bool `yaml:"use_proxy,omitempty"` // 是否走系统代理nil 表示跟随全局
}
type User struct {
Username string `yaml:"username"`
Token string `yaml:"token"` // 可选:未来扩展 API 功能
Projects []Project `yaml:"projects"`
// Group 项目分组
type Group struct {
Name string `yaml:"name"`
Icon string `yaml:"icon,omitempty"` // 可选图标标识
}
type Platform struct {
BaseURL string `yaml:"base_url"` // GitHub/Gitee 可为空Gitea 需要
Users []User `yaml:"users"`
// Credential 凭证信息(独立于项目)
type Credential struct {
Platform string `yaml:"platform"` // github / gitee / gitea / ...
BaseURL string `yaml:"base_url,omitempty"` // 自建平台地址
Username string `yaml:"username"`
Token string `yaml:"token,omitempty"`
}
// Settings 应用设置
type Settings struct {
Concurrency int `yaml:"concurrency"`
NetworkCheck bool `yaml:"network_check"`
LogLevel string `yaml:"log_level"`
}
// AppConfig 应用配置(扁平化结构)
type AppConfig struct {
Platforms map[string]Platform `yaml:"platforms"`
Settings Settings `yaml:"settings"`
Projects []ProjectItem `yaml:"projects"`
Groups []Group `yaml:"groups"`
Credentials []Credential `yaml:"credentials"`
Settings Settings `yaml:"settings"`
}
// configPath 返回 config.yaml 的绝对路径(与可执行文件同目录)
@@ -53,18 +62,26 @@ func LoadConfig() (*AppConfig, error) {
}
defer file.Close()
var appConfig *AppConfig
err = yaml.NewDecoder(file).Decode(&appConfig)
if err != nil {
var appConfig AppConfig
if err := yaml.NewDecoder(file).Decode(&appConfig); err != nil {
return nil, err
}
return appConfig, nil
if appConfig.Projects == nil {
appConfig.Projects = []ProjectItem{}
}
if appConfig.Groups == nil {
appConfig.Groups = []Group{}
}
if appConfig.Credentials == nil {
appConfig.Credentials = []Credential{}
}
return &appConfig, nil
}
// SaveConfig 将配置保存到 config.yaml 文件
func SaveConfig(config *AppConfig) error {
func SaveConfig(cfg *AppConfig) error {
file, err := os.OpenFile(configPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return err
@@ -72,10 +89,8 @@ func SaveConfig(config *AppConfig) error {
defer file.Close()
enc := yaml.NewEncoder(file)
enc.SetIndent(2)
if err := enc.Encode(config); err != nil {
if err := enc.Encode(cfg); err != nil {
return fmt.Errorf("写入配置文件失败: %w", err)
}
return enc.Close()
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,376 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import {
WarningOutlined,
DownloadOutlined,
FolderOpenOutlined,
CheckCircleOutlined,
LoadingOutlined,
CloseCircleOutlined,
} from '@ant-design/icons-vue'
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
import { Events } from '@wailsio/runtime'
const emit = defineEmits(['ready'])
const checking = ref(true)
const gitInstalled = ref(false)
const gitVersion = ref('')
const gitPath = ref('')
// 安装相关
const installDir = ref('C:\\Program Files\\Git')
const installing = ref(false)
const installPhase = ref('') // downloading | installing | done | error
const installPercent = ref(0)
const installMessage = ref('')
let unsubscribe = null
onMounted(async () => {
// 监听安装进度事件
unsubscribe = Events.On('git-install-progress', (event) => {
const data = event.data?.[0] || event.data
if (data) {
installPhase.value = data.phase
installPercent.value = data.percent
installMessage.value = data.message
if (data.phase === 'done') {
installing.value = false
gitInstalled.value = true
setTimeout(() => emit('ready'), 1500)
} else if (data.phase === 'error') {
installing.value = false
}
}
})
// 检查 Git
try {
const status = await AppService.CheckGitInstalled()
gitInstalled.value = status.installed
gitVersion.value = status.version || ''
gitPath.value = status.path || ''
if (status.installed) {
emit('ready')
}
} catch (e) {
console.error('检查 Git 失败:', e)
gitInstalled.value = false
} finally {
checking.value = false
}
})
onBeforeUnmount(() => {
if (unsubscribe) unsubscribe()
})
async function selectInstallDir() {
try {
const dir = await AppService.SelectGitInstallDir()
if (dir) installDir.value = dir
} catch (e) {
console.error('选择目录失败:', e)
}
}
async function startInstall() {
installing.value = true
installPhase.value = 'downloading'
installPercent.value = 0
installMessage.value = '准备下载...'
try {
await AppService.InstallGit(installDir.value)
} catch (e) {
console.error('安装 Git 失败:', e)
installPhase.value = 'error'
installMessage.value = '安装失败: ' + String(e)
installing.value = false
}
}
</script>
<template>
<!-- 检查中 -->
<div v-if="checking" class="git-check-overlay">
<div class="git-check-card">
<LoadingOutlined :style="{ fontSize: '32px', color: 'var(--accent, #89b4fa)' }" spin />
<div class="git-check-title">正在检查 Git 环境...</div>
</div>
</div>
<!-- Git 未安装 -->
<div v-else-if="!gitInstalled" class="git-check-overlay">
<div class="git-check-card install-card">
<div class="git-check-icon">
<WarningOutlined :style="{ fontSize: '48px', color: '#fab387' }" />
</div>
<div class="git-check-title">未检测到 Git</div>
<div class="git-check-desc">GitPilot 需要 Git 才能正常工作请安装 Git 后使用</div>
<!-- 安装进度 -->
<template v-if="installing || installPhase === 'done'">
<div class="install-progress">
<div v-if="installPhase === 'downloading'" class="progress-section">
<div class="progress-bar-bg">
<div class="progress-bar-fill" :style="{ width: installPercent + '%' }"></div>
</div>
<div class="progress-text">
<DownloadOutlined /> {{ installMessage }}
</div>
</div>
<div v-else-if="installPhase === 'installing'" class="progress-section">
<div class="progress-bar-bg">
<div class="progress-bar-fill installing-anim" style="width: 100%"></div>
</div>
<div class="progress-text">
<LoadingOutlined spin /> {{ installMessage }}
</div>
</div>
<div v-else-if="installPhase === 'done'" class="progress-section done">
<CheckCircleOutlined :style="{ fontSize: '24px', color: '#a6e3a1' }" />
<div class="progress-text success">{{ installMessage }}</div>
</div>
<div v-else-if="installPhase === 'error'" class="progress-section error">
<CloseCircleOutlined :style="{ fontSize: '24px', color: '#f38ba8' }" />
<div class="progress-text error-text">{{ installMessage }}</div>
</div>
</div>
</template>
<!-- 安装表单 -->
<template v-else>
<div class="install-form">
<div class="install-field">
<label>安装路径</label>
<div class="install-dir-input">
<input
v-model="installDir"
class="dir-input"
placeholder="C:\Program Files\Git"
/>
<button class="dir-btn" @click="selectInstallDir" title="选择目录">
<FolderOpenOutlined />
</button>
</div>
</div>
</div>
<div class="install-actions">
<button class="install-btn primary" @click="startInstall">
<DownloadOutlined /> 自动下载并安装 Git
</button>
</div>
<div class="install-hint">
将从 GitHub 下载 Git for Windows 安装包 65MB需保持网络连接
</div>
</template>
</div>
</div>
</template>
<style scoped>
.git-check-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-primary, #1e1e2e);
}
.git-check-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 40px;
max-width: 480px;
width: 100%;
}
.install-card {
background: var(--bg-secondary, #181825);
border: 1px solid var(--border-color, #313244);
border-radius: 12px;
padding: 40px 36px;
}
.git-check-icon {
margin-bottom: 4px;
}
.git-check-title {
font-size: 20px;
font-weight: 700;
color: var(--text-primary, #cdd6f4);
}
.git-check-desc {
font-size: 14px;
color: var(--text-muted, #6c7086);
text-align: center;
line-height: 1.6;
}
.install-form {
width: 100%;
margin-top: 8px;
}
.install-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.install-field label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary, #a6adc8);
}
.install-dir-input {
display: flex;
gap: 6px;
}
.dir-input {
flex: 1;
padding: 8px 12px;
background: var(--bg-primary, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 6px;
color: var(--text-primary, #cdd6f4);
font-size: 13px;
font-family: 'Consolas', 'Courier New', monospace;
outline: none;
transition: border-color 0.15s;
}
.dir-input:focus {
border-color: var(--accent, #89b4fa);
}
.dir-btn {
padding: 8px 12px;
background: var(--bg-primary, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 6px;
color: var(--accent, #89b4fa);
cursor: pointer;
font-size: 15px;
display: flex;
align-items: center;
transition: all 0.15s;
}
.dir-btn:hover {
background: var(--bg-hover, #313244);
}
.install-actions {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 4px;
}
.install-btn {
padding: 10px 20px;
border-radius: 8px;
border: 1px solid var(--border-color, #313244);
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.15s;
}
.install-btn.primary {
background: var(--accent, #89b4fa);
color: #1e1e2e;
border-color: var(--accent, #89b4fa);
}
.install-btn.primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.install-hint {
font-size: 12px;
color: var(--text-muted, #6c7086);
text-align: center;
line-height: 1.5;
}
/* 安装进度 */
.install-progress {
width: 100%;
margin-top: 8px;
}
.progress-section {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.progress-section.done,
.progress-section.error {
gap: 12px;
}
.progress-bar-bg {
width: 100%;
height: 6px;
background: var(--bg-primary, #1e1e2e);
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--accent, #89b4fa);
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-bar-fill.installing-anim {
background: linear-gradient(90deg, var(--accent, #89b4fa) 0%, #b4befe 50%, var(--accent, #89b4fa) 100%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.progress-text {
font-size: 13px;
color: var(--text-secondary, #a6adc8);
display: flex;
align-items: center;
gap: 6px;
}
.progress-text.success {
color: #a6e3a1;
font-weight: 600;
}
.progress-text.error-text {
color: #f38ba8;
font-size: 12px;
text-align: center;
word-break: break-all;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,11 @@
import { ref, onBeforeUnmount } from 'vue'
const selectedProject = ref(null)
const gitReady = ref(false)
function onGitReady() {
gitReady.value = true
}
function onSelectProject(project) {
selectedProject.value = project
@@ -43,7 +48,11 @@ onBeforeUnmount(() => {
</script>
<template>
<div class="main-layout">
<!-- Git 环境检查 -->
<GitCheck v-if="!gitReady" @ready="onGitReady" />
<!-- 主界面 -->
<div v-else class="main-layout">
<Sidebar :selected-project="selectedProject" :style="{ width: sidebarWidth + 'px' }" @select-project="onSelectProject" />
<div class="resize-handle" @mousedown="startSidebarResize"></div>
<ContentArea :project="selectedProject" />

View File

@@ -0,0 +1,8 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export {
Group,
Settings
} from "./models.js";

View File

@@ -0,0 +1,91 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime";
/**
* Group 项目分组
*/
export class Group {
/**
* Creates a new Group instance.
* @param {Partial<Group>} [$$source = {}] - The source object to create the Group.
*/
constructor($$source = {}) {
if (!("Name" in $$source)) {
/**
* @member
* @type {string}
*/
this["Name"] = "";
}
if (!("Icon" in $$source)) {
/**
* 可选图标标识
* @member
* @type {string}
*/
this["Icon"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new Group instance from a string or object.
* @param {any} [$$source = {}]
* @returns {Group}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Group(/** @type {Partial<Group>} */($$parsedSource));
}
}
/**
* Settings 应用设置
*/
export class Settings {
/**
* Creates a new Settings instance.
* @param {Partial<Settings>} [$$source = {}] - The source object to create the Settings.
*/
constructor($$source = {}) {
if (!("Concurrency" in $$source)) {
/**
* @member
* @type {number}
*/
this["Concurrency"] = 0;
}
if (!("NetworkCheck" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["NetworkCheck"] = false;
}
if (!("LogLevel" in $$source)) {
/**
* @member
* @type {string}
*/
this["LogLevel"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new Settings instance from a string or object.
* @param {any} [$$source = {}]
* @returns {Settings}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Settings(/** @type {Partial<Settings>} */($$parsedSource));
}
}

View File

@@ -14,42 +14,136 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../../wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as config$0 from "../../config/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
/**
* AddPlatform 添加新平台
* @param {string} name
* @param {string} baseURL
* @returns {$CancellablePromise<void>}
*/
export function AddPlatform(name, baseURL) {
return $Call.ByID(526003212, name, baseURL);
}
/**
* AddProject 添加项目到指定平台/用户下
* @param {string} platform
* @param {string} username
* @param {string} name
* AbortMerge 中止合并
* @param {string} path
* @returns {$CancellablePromise<void>}
*/
export function AddProject(platform, username, name, path) {
return $Call.ByID(2299402672, platform, username, name, path);
export function AbortMerge(path) {
return $Call.ByID(3035150106, path);
}
/**
* AddUser 添加用户到指定平台
* AddCredential 添加凭证
* @param {string} platform
* @param {string} baseURL
* @param {string} username
* @param {string} token
* @returns {$CancellablePromise<void>}
*/
export function AddUser(platform, username, token) {
return $Call.ByID(2264426704, platform, username, token);
export function AddCredential(platform, baseURL, username, token) {
return $Call.ByID(1627284202, platform, baseURL, username, token);
}
/**
* AddGroup 添加分组
* @param {string} name
* @param {string} icon
* @returns {$CancellablePromise<void>}
*/
export function AddGroup(name, icon) {
return $Call.ByID(4237035530, name, icon);
}
/**
* AddProject 添加项目
* @param {string} name
* @param {string} path
* @param {string} group
* @returns {$CancellablePromise<void>}
*/
export function AddProject(name, path, group) {
return $Call.ByID(2299402672, name, path, group);
}
/**
* AddRemote 添加远程仓库
* @param {string} path
* @param {string} name
* @param {string} url
* @returns {$CancellablePromise<void>}
*/
export function AddRemote(path, name, url) {
return $Call.ByID(3697916441, path, name, url);
}
/**
* BatchMigrateProjects 批量迁移项目到目标平台
* @param {string[]} projectPaths
* @param {string} targetCredPlatform
* @param {string} targetCredUsername
* @param {string} description
* @param {boolean} $private
* @returns {$CancellablePromise<$models.MigrateResult[]>}
*/
export function BatchMigrateProjects(projectPaths, targetCredPlatform, targetCredUsername, description, $private) {
return $Call.ByID(1641390103, projectPaths, targetCredPlatform, targetCredUsername, description, $private).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result);
}));
}
/**
* BatchPull 批量拉取指定项目
* @param {string[]} paths
* @param {string} remote
* @returns {$CancellablePromise<$models.BatchPullResult[]>}
*/
export function BatchPull(paths, remote) {
return $Call.ByID(758996647, paths, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType3($result);
}));
}
/**
* BatchPush 批量推送指定项目
* @param {string[]} paths
* @param {string} remote
* @returns {$CancellablePromise<$models.BatchPullResult[]>}
*/
export function BatchPush(paths, remote) {
return $Call.ByID(794082076, paths, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType3($result);
}));
}
/**
* CheckGitInstalled 检查 Git 是否已安装
* @returns {$CancellablePromise<$models.GitStatus>}
*/
export function CheckGitInstalled() {
return $Call.ByID(464620426).then(/** @type {($result: any) => any} */(($result) => {
return $$createType4($result);
}));
}
/**
* CheckoutRemoteBranch 检出远程分支到本地
* @param {string} path
* @param {string} remoteBranch
* @returns {$CancellablePromise<void>}
*/
export function CheckoutRemoteBranch(path, remoteBranch) {
return $Call.ByID(3682237522, path, remoteBranch);
}
/**
* CloneProject 克隆远程仓库到本地目录,并添加到项目列表
* @param {string} repoURL
* @param {string} parentDir
* @param {string} name
* @param {string} group
* @returns {$CancellablePromise<void>}
*/
export function CloneProject(repoURL, parentDir, name, group) {
return $Call.ByID(2347019414, repoURL, parentDir, name, group);
}
/**
@@ -62,6 +156,16 @@ export function CommitChanges(path, message) {
return $Call.ByID(921984546, path, message);
}
/**
* CreateBranch 创建新分支
* @param {string} path
* @param {string} name
* @returns {$CancellablePromise<void>}
*/
export function CreateBranch(path, name) {
return $Call.ByID(1422152924, path, name);
}
/**
* CreateTag 创建标签
* @param {string} path
@@ -73,14 +177,37 @@ export function CreateTag(path, name, message) {
return $Call.ByID(4209616956, path, name, message);
}
/**
* DeleteBranch 删除本地分支
* @param {string} path
* @param {string} name
* @param {boolean} force
* @returns {$CancellablePromise<void>}
*/
export function DeleteBranch(path, name, force) {
return $Call.ByID(580272035, path, name, force);
}
/**
* DeleteRemoteBranch 删除远程分支
* @param {string} path
* @param {string} branch
* @param {string} remote
* @returns {$CancellablePromise<void>}
*/
export function DeleteRemoteBranch(path, branch, remote) {
return $Call.ByID(2626609049, path, branch, remote);
}
/**
* DeleteTag 删除标签(本地+远程)
* @param {string} path
* @param {string} name
* @param {string} remote
* @returns {$CancellablePromise<void>}
*/
export function DeleteTag(path, name) {
return $Call.ByID(873653241, path, name);
export function DeleteTag(path, name, remote) {
return $Call.ByID(873653241, path, name, remote);
}
/**
@@ -94,12 +221,33 @@ export function DiscardFiles(path, files) {
}
/**
* FetchProject 拉取远程信息
* FetchProject 拉取远程信息(指定 remote空则 fetch --all
* @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<string>}
*/
export function FetchProject(path) {
return $Call.ByID(3541106829, path);
export function FetchProject(path, remote) {
return $Call.ByID(3541106829, path, remote);
}
/**
* GetAllProjectOverview 获取所有项目的概览状态
* @returns {$CancellablePromise<$models.ProjectOverview[]>}
*/
export function GetAllProjectOverview() {
return $Call.ByID(2453130151).then(/** @type {($result: any) => any} */(($result) => {
return $$createType6($result);
}));
}
/**
* GetAppSettings 获取应用设置
* @returns {$CancellablePromise<config$0.Settings | null>}
*/
export function GetAppSettings() {
return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => {
return $$createType8($result);
}));
}
/**
@@ -109,7 +257,7 @@ export function FetchProject(path) {
*/
export function GetBranches(path) {
return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result);
return $$createType10($result);
}));
}
@@ -142,7 +290,7 @@ export function GetCommitFileDiff(path, hash, filePath) {
*/
export function GetCommitFiles(path, hash) {
return $Call.ByID(2420707876, path, hash).then(/** @type {($result: any) => any} */(($result) => {
return $$createType3($result);
return $$createType12($result);
}));
}
@@ -154,7 +302,38 @@ export function GetCommitFiles(path, hash) {
*/
export function GetCommitLog(path, count) {
return $Call.ByID(1281870789, path, count).then(/** @type {($result: any) => any} */(($result) => {
return $$createType5($result);
return $$createType14($result);
}));
}
/**
* GetConflictFileContent 获取冲突文件内容(包含冲突标记)
* @param {string} projectPath
* @param {string} filePath
* @returns {$CancellablePromise<string>}
*/
export function GetConflictFileContent(projectPath, filePath) {
return $Call.ByID(298498517, projectPath, filePath);
}
/**
* GetConflictFiles 获取冲突文件列表
* @param {string} path
* @returns {$CancellablePromise<$models.ConflictFileInfo[]>}
*/
export function GetConflictFiles(path) {
return $Call.ByID(2446806137, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType16($result);
}));
}
/**
* GetCredentials 获取所有凭证
* @returns {$CancellablePromise<$models.CredentialInfo[]>}
*/
export function GetCredentials() {
return $Call.ByID(4214051290).then(/** @type {($result: any) => any} */(($result) => {
return $$createType18($result);
}));
}
@@ -189,13 +368,22 @@ export function GetFileDiffStaged(projectPath, filePath) {
}
/**
* GetPlatformInfo 获取平台信息
* @param {string} name
* @returns {$CancellablePromise<$models.PlatformInfo | null>}
* GetGitGlobalConfig 获取 git 全局配置
* @returns {$CancellablePromise<$models.GitConfig | null>}
*/
export function GetPlatformInfo(name) {
return $Call.ByID(2668095547, name).then(/** @type {($result: any) => any} */(($result) => {
return $$createType7($result);
export function GetGitGlobalConfig() {
return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => {
return $$createType20($result);
}));
}
/**
* GetGroups 获取所有分组
* @returns {$CancellablePromise<config$0.Group[]>}
*/
export function GetGroups() {
return $Call.ByID(1600884836).then(/** @type {($result: any) => any} */(($result) => {
return $$createType22($result);
}));
}
@@ -206,10 +394,19 @@ export function GetPlatformInfo(name) {
*/
export function GetProjectChangedFiles(path) {
return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType9($result);
return $$createType24($result);
}));
}
/**
* GetProjectProxy 获取项目代理设置nil 表示跟随全局)
* @param {string} path
* @returns {$CancellablePromise<boolean | null>}
*/
export function GetProjectProxy(path) {
return $Call.ByID(2515497911, path);
}
/**
* GetProjectStatus 获取项目 git 状态
* @param {string} path
@@ -217,17 +414,65 @@ export function GetProjectChangedFiles(path) {
*/
export function GetProjectStatus(path) {
return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType11($result);
return $$createType26($result);
}));
}
/**
* GetProjectTree 获取项目树,供前端侧边栏渲染
* GetProjectTree 获取项目树,按分组组织
* @returns {$CancellablePromise<$models.TreeNode[]>}
*/
export function GetProjectTree() {
return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => {
return $$createType13($result);
return $$createType28($result);
}));
}
/**
* GetRemoteBranches 获取远程分支列表
* @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<$models.BranchInfo[]>}
*/
export function GetRemoteBranches(path, remote) {
return $Call.ByID(994796370, path, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType10($result);
}));
}
/**
* GetRemoteReleases 获取指定远程仓库的 Release 列表
* overrideToken: 前端传入的 Token优先于凭证中的 Token
* @param {string} projectPath
* @param {string} remoteName
* @param {string} overrideToken
* @returns {$CancellablePromise<$models.ReleaseInfo[]>}
*/
export function GetRemoteReleases(projectPath, remoteName, overrideToken) {
return $Call.ByID(280863080, projectPath, remoteName, overrideToken).then(/** @type {($result: any) => any} */(($result) => {
return $$createType30($result);
}));
}
/**
* GetRemotes 获取项目所有远程仓库列表
* @param {string} path
* @returns {$CancellablePromise<$models.RemoteItem[]>}
*/
export function GetRemotes(path) {
return $Call.ByID(975520027, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType32($result);
}));
}
/**
* GetStashList 获取贮藏列表
* @param {string} path
* @returns {$CancellablePromise<$models.StashInfo[]>}
*/
export function GetStashList(path) {
return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType34($result);
}));
}
@@ -238,78 +483,201 @@ export function GetProjectTree() {
*/
export function GetTags(path) {
return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType15($result);
return $$createType36($result);
}));
}
/**
* GetUserInfo 获取用户信息
* @param {string} platform
* @param {string} username
* @returns {$CancellablePromise<$models.UserInfo | null>}
* InstallGit 下载并安装 Git
* installDir: 用户选择的安装目录Windows 有效)
* @param {string} installDir
* @returns {$CancellablePromise<void>}
*/
export function GetUserInfo(platform, username) {
return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => {
return $$createType17($result);
export function InstallGit(installDir) {
return $Call.ByID(1052641569, installDir);
}
/**
* IsMerging 检查是否处于合并状态
* @param {string} path
* @returns {$CancellablePromise<boolean>}
*/
export function IsMerging(path) {
return $Call.ByID(693809703, path);
}
/**
* MergeBranch 合并指定分支到当前分支
* @param {string} path
* @param {string} branch
* @returns {$CancellablePromise<string>}
*/
export function MergeBranch(path, branch) {
return $Call.ByID(278161076, path, branch);
}
/**
* MigrateProject 将项目迁移到目标平台
* targetCredPlatform + targetCredUsername 用于定位目标凭证
* repoName: 目标仓库名(空则用项目名)
* remoteName: 新增的远程名(空则用平台名)
* private: 是否私有仓库
* description: 仓库描述
* @param {string} projectPath
* @param {string} targetCredPlatform
* @param {string} targetCredUsername
* @param {string} repoName
* @param {string} remoteName
* @param {string} description
* @param {boolean} $private
* @returns {$CancellablePromise<$models.MigrateResult | null>}
*/
export function MigrateProject(projectPath, targetCredPlatform, targetCredUsername, repoName, remoteName, description, $private) {
return $Call.ByID(618007206, projectPath, targetCredPlatform, targetCredUsername, repoName, remoteName, description, $private).then(/** @type {($result: any) => any} */(($result) => {
return $$createType37($result);
}));
}
/**
* PullProject 拉取项目(当前分支)
* MoveProjectToGroup 移动项目到指定分组
* @param {string} path
* @returns {$CancellablePromise<string>}
* @param {string} group
* @returns {$CancellablePromise<void>}
*/
export function PullProject(path) {
return $Call.ByID(4145813996, path);
export function MoveProjectToGroup(path, group) {
return $Call.ByID(1699187666, path, group);
}
/**
* PushProject 推送项目(当前分支)
* OnlineMigrateCompare 对比两个平台的仓库列表
* @param {string} srcPlatform
* @param {string} srcBaseURL
* @param {string} srcUsername
* @param {string} srcToken
* @param {string} tgtPlatform
* @param {string} tgtBaseURL
* @param {string} tgtUsername
* @param {string} tgtToken
* @returns {$CancellablePromise<$models.OnlineMigrateCompareResult | null>}
*/
export function OnlineMigrateCompare(srcPlatform, srcBaseURL, srcUsername, srcToken, tgtPlatform, tgtBaseURL, tgtUsername, tgtToken) {
return $Call.ByID(3514758503, srcPlatform, srcBaseURL, srcUsername, srcToken, tgtPlatform, tgtBaseURL, tgtUsername, tgtToken).then(/** @type {($result: any) => any} */(($result) => {
return $$createType39($result);
}));
}
/**
* OnlineMigrateExecute 执行在线迁移
* @param {string} srcPlatform
* @param {string} srcBaseURL
* @param {string} srcUsername
* @param {string} srcToken
* @param {string} tgtPlatform
* @param {string} tgtBaseURL
* @param {string} tgtUsername
* @param {string} tgtToken
* @param {string[]} repoNames
* @param {boolean} srcUseProxy
* @param {boolean} tgtUseProxy
* @param {$models.MigrateOptions} opts
* @returns {$CancellablePromise<$models.OnlineMigrateItemResult[]>}
*/
export function OnlineMigrateExecute(srcPlatform, srcBaseURL, srcUsername, srcToken, tgtPlatform, tgtBaseURL, tgtUsername, tgtToken, repoNames, srcUseProxy, tgtUseProxy, opts) {
return $Call.ByID(357541815, srcPlatform, srcBaseURL, srcUsername, srcToken, tgtPlatform, tgtBaseURL, tgtUsername, tgtToken, repoNames, srcUseProxy, tgtUseProxy, opts).then(/** @type {($result: any) => any} */(($result) => {
return $$createType41($result);
}));
}
/**
* PullProject 拉取项目(当前分支,指定 remote
* @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<string>}
*/
export function PushProject(path) {
return $Call.ByID(3887901113, path);
export function PullProject(path, remote) {
return $Call.ByID(4145813996, path, remote);
}
/**
* PushProject 推送项目(当前分支,指定 remote
* @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<string>}
*/
export function PushProject(path, remote) {
return $Call.ByID(3887901113, path, remote);
}
/**
* PushTag 推送标签到远程
* @param {string} path
* @param {string} name
* @param {string} remote
* @returns {$CancellablePromise<void>}
*/
export function PushTag(path, name) {
return $Call.ByID(1424810524, path, name);
export function PushTag(path, name, remote) {
return $Call.ByID(1424810524, path, name, remote);
}
/**
* RemovePlatform 删除平台
* @param {string} name
* @returns {$CancellablePromise<void>}
* PushTagToAllRemotes 一键推送标签到所有远程仓库
* @param {string} path
* @param {string} tagName
* @returns {$CancellablePromise<$models.PushAllResult[]>}
*/
export function RemovePlatform(name) {
return $Call.ByID(2222448051, name);
export function PushTagToAllRemotes(path, tagName) {
return $Call.ByID(2343581171, path, tagName).then(/** @type {($result: any) => any} */(($result) => {
return $$createType43($result);
}));
}
/**
* RemoveProject 从指定平台/用户下删除项目
* @param {string} platform
* @param {string} username
* @param {string} name
* @returns {$CancellablePromise<void>}
* PushToAllRemotes 一键推送到所有远程仓库(当前分支)
* @param {string} path
* @returns {$CancellablePromise<$models.PushAllResult[]>}
*/
export function RemoveProject(platform, username, name) {
return $Call.ByID(2748109581, platform, username, name);
export function PushToAllRemotes(path) {
return $Call.ByID(1027392731, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType43($result);
}));
}
/**
* RemoveUser 从平台删除用户
* RemoveCredential 删除凭证
* @param {string} platform
* @param {string} username
* @returns {$CancellablePromise<void>}
*/
export function RemoveUser(platform, username) {
return $Call.ByID(1409517275, platform, username);
export function RemoveCredential(platform, username) {
return $Call.ByID(414538393, platform, username);
}
/**
* RemoveGroup 删除分组(分组下的项目变为"未分组"
* @param {string} name
* @returns {$CancellablePromise<void>}
*/
export function RemoveGroup(name) {
return $Call.ByID(3268966203, name);
}
/**
* RemoveProject 删除项目(按路径匹配)
* @param {string} path
* @returns {$CancellablePromise<void>}
*/
export function RemoveProject(path) {
return $Call.ByID(2748109581, path);
}
/**
* RemoveRemote 删除远程仓库
* @param {string} path
* @param {string} name
* @returns {$CancellablePromise<void>}
*/
export function RemoveRemote(path, name) {
return $Call.ByID(2915623022, path, name);
}
/**
@@ -324,6 +692,16 @@ export function ResetProject(path, hash, mode) {
return $Call.ByID(2061148684, path, hash, mode);
}
/**
* ResolveConflictFile 将冲突文件标记为已解决
* @param {string} path
* @param {string[]} files
* @returns {$CancellablePromise<void>}
*/
export function ResolveConflictFile(path, files) {
return $Call.ByID(1893119072, path, files);
}
/**
* RevertCommit 撤回指定提交(生成一个反向提交)
* @param {string} path
@@ -334,6 +712,31 @@ export function RevertCommit(path, hash) {
return $Call.ByID(1334788539, path, hash);
}
/**
* SaveConflictFile 保存冲突文件内容(手动解决冲突后保存)
* @param {string} projectPath
* @param {string} filePath
* @param {string} content
* @returns {$CancellablePromise<void>}
*/
export function SaveConflictFile(projectPath, filePath, content) {
return $Call.ByID(3340289161, projectPath, filePath, content);
}
/**
* SearchCommitLog 搜索提交历史
* @param {string} path
* @param {string} keyword
* @param {string} author
* @param {number} maxCount
* @returns {$CancellablePromise<$models.CommitLog[]>}
*/
export function SearchCommitLog(path, keyword, author, maxCount) {
return $Call.ByID(2874450917, path, keyword, author, maxCount).then(/** @type {($result: any) => any} */(($result) => {
return $$createType14($result);
}));
}
/**
* SelectDirectory 打开系统文件夹选择器,返回选中的路径
* @returns {$CancellablePromise<string>}
@@ -342,6 +745,14 @@ export function SelectDirectory() {
return $Call.ByID(2318416763);
}
/**
* SelectGitInstallDir 打开文件夹选择器让用户选择 Git 安装路径
* @returns {$CancellablePromise<string>}
*/
export function SelectGitInstallDir() {
return $Call.ByID(2941074074);
}
/**
* @param {application$0.App | null} app
* @returns {$CancellablePromise<void>}
@@ -350,6 +761,27 @@ export function SetApplication(app) {
return $Call.ByID(383695022, app);
}
/**
* SetGitGlobalConfig 设置 git 全局配置
* @param {string} name
* @param {string} email
* @returns {$CancellablePromise<void>}
*/
export function SetGitGlobalConfig(name, email) {
return $Call.ByID(3875064981, name, email);
}
/**
* SetProjectProxy 设置项目代理开关
* useProxy: true=强制启用, false=强制禁用, nil(传空)=跟随全局
* @param {string} path
* @param {boolean | null} useProxy
* @returns {$CancellablePromise<void>}
*/
export function SetProjectProxy(path, useProxy) {
return $Call.ByID(4094650651, path, useProxy);
}
/**
* StageAll 暂存所有变更文件
* @param {string} path
@@ -369,6 +801,46 @@ export function StageFiles(path, files) {
return $Call.ByID(4064506881, path, files);
}
/**
* StashApply 应用贮藏(不删除)
* @param {string} path
* @param {number} index
* @returns {$CancellablePromise<void>}
*/
export function StashApply(path, index) {
return $Call.ByID(721583959, path, index);
}
/**
* StashDrop 删除贮藏
* @param {string} path
* @param {number} index
* @returns {$CancellablePromise<void>}
*/
export function StashDrop(path, index) {
return $Call.ByID(3821556536, path, index);
}
/**
* StashPop 应用贮藏并删除
* @param {string} path
* @param {number} index
* @returns {$CancellablePromise<void>}
*/
export function StashPop(path, index) {
return $Call.ByID(4117964460, path, index);
}
/**
* StashSave 保存当前变更到贮藏
* @param {string} path
* @param {string} message
* @returns {$CancellablePromise<void>}
*/
export function StashSave(path, message) {
return $Call.ByID(1178263140, path, message);
}
/**
* SwitchBranch 切换分支
* @param {string} path
@@ -379,6 +851,22 @@ export function SwitchBranch(path, branch) {
return $Call.ByID(4262224900, path, branch);
}
/**
* SyncReleases 将源远程仓库的 Release 同步到目标远程仓库
* 仅同步目标不存在的 Release包括附件
* @param {string} projectPath
* @param {string} sourceRemote
* @param {string} targetRemote
* @param {string} srcOverrideToken
* @param {string} tgtOverrideToken
* @returns {$CancellablePromise<$models.ReleaseSyncResult[]>}
*/
export function SyncReleases(projectPath, sourceRemote, targetRemote, srcOverrideToken, tgtOverrideToken) {
return $Call.ByID(3895607505, projectPath, sourceRemote, targetRemote, srcOverrideToken, tgtOverrideToken).then(/** @type {($result: any) => any} */(($result) => {
return $$createType45($result);
}));
}
/**
* UnstageAll 取消暂存所有文件
* @param {string} path
@@ -399,43 +887,81 @@ export function UnstageFiles(path, files) {
}
/**
* UpdatePlatform 修改平台信息base_url
* @param {string} name
* @param {string} baseURL
* UpdateAppSettings 更新应用设置
* @param {string} logLevel
* @returns {$CancellablePromise<void>}
*/
export function UpdatePlatform(name, baseURL) {
return $Call.ByID(3258014550, name, baseURL);
export function UpdateAppSettings(logLevel) {
return $Call.ByID(718820989, logLevel);
}
/**
* UpdateUser 修改用户信息用户名、token
* UpdateCredential 更新凭证
* @param {string} platform
* @param {string} oldUsername
* @param {string} newUsername
* @param {string} username
* @param {string} baseURL
* @param {string} token
* @returns {$CancellablePromise<void>}
*/
export function UpdateUser(platform, oldUsername, newUsername, token) {
return $Call.ByID(1631729342, platform, oldUsername, newUsername, token);
export function UpdateCredential(platform, username, baseURL, token) {
return $Call.ByID(2448329924, platform, username, baseURL, token);
}
/**
* UpdateGroup 修改分组
* @param {string} oldName
* @param {string} newName
* @param {string} icon
* @returns {$CancellablePromise<void>}
*/
export function UpdateGroup(oldName, newName, icon) {
return $Call.ByID(1322206248, oldName, newName, icon);
}
// Private type creation functions
const $$createType0 = $models.BranchInfo.createFrom;
const $$createType0 = $models.MigrateResult.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $models.CommitFileInfo.createFrom;
const $$createType2 = $models.BatchPullResult.createFrom;
const $$createType3 = $Create.Array($$createType2);
const $$createType4 = $models.CommitLog.createFrom;
const $$createType5 = $Create.Array($$createType4);
const $$createType6 = $models.PlatformInfo.createFrom;
const $$createType7 = $Create.Nullable($$createType6);
const $$createType8 = $models.FileInfo.createFrom;
const $$createType9 = $Create.Array($$createType8);
const $$createType10 = $models.ProjectStatus.createFrom;
const $$createType11 = $Create.Nullable($$createType10);
const $$createType12 = $models.TreeNode.createFrom;
const $$createType13 = $Create.Array($$createType12);
const $$createType14 = $models.TagInfo.createFrom;
const $$createType15 = $Create.Array($$createType14);
const $$createType16 = $models.UserInfo.createFrom;
const $$createType17 = $Create.Nullable($$createType16);
const $$createType4 = $models.GitStatus.createFrom;
const $$createType5 = $models.ProjectOverview.createFrom;
const $$createType6 = $Create.Array($$createType5);
const $$createType7 = config$0.Settings.createFrom;
const $$createType8 = $Create.Nullable($$createType7);
const $$createType9 = $models.BranchInfo.createFrom;
const $$createType10 = $Create.Array($$createType9);
const $$createType11 = $models.CommitFileInfo.createFrom;
const $$createType12 = $Create.Array($$createType11);
const $$createType13 = $models.CommitLog.createFrom;
const $$createType14 = $Create.Array($$createType13);
const $$createType15 = $models.ConflictFileInfo.createFrom;
const $$createType16 = $Create.Array($$createType15);
const $$createType17 = $models.CredentialInfo.createFrom;
const $$createType18 = $Create.Array($$createType17);
const $$createType19 = $models.GitConfig.createFrom;
const $$createType20 = $Create.Nullable($$createType19);
const $$createType21 = config$0.Group.createFrom;
const $$createType22 = $Create.Array($$createType21);
const $$createType23 = $models.FileInfo.createFrom;
const $$createType24 = $Create.Array($$createType23);
const $$createType25 = $models.ProjectStatus.createFrom;
const $$createType26 = $Create.Nullable($$createType25);
const $$createType27 = $models.TreeNode.createFrom;
const $$createType28 = $Create.Array($$createType27);
const $$createType29 = $models.ReleaseInfo.createFrom;
const $$createType30 = $Create.Array($$createType29);
const $$createType31 = $models.RemoteItem.createFrom;
const $$createType32 = $Create.Array($$createType31);
const $$createType33 = $models.StashInfo.createFrom;
const $$createType34 = $Create.Array($$createType33);
const $$createType35 = $models.TagInfo.createFrom;
const $$createType36 = $Create.Array($$createType35);
const $$createType37 = $Create.Nullable($$createType0);
const $$createType38 = $models.OnlineMigrateCompareResult.createFrom;
const $$createType39 = $Create.Nullable($$createType38);
const $$createType40 = $models.OnlineMigrateItemResult.createFrom;
const $$createType41 = $Create.Array($$createType40);
const $$createType42 = $models.PushAllResult.createFrom;
const $$createType43 = $Create.Array($$createType42);
const $$createType44 = $models.ReleaseSyncResult.createFrom;
const $$createType45 = $Create.Array($$createType44);

View File

@@ -8,13 +8,28 @@ export {
};
export {
AssetInfo,
BatchPullResult,
BranchInfo,
CommitFileInfo,
CommitLog,
ConflictFileInfo,
CredentialInfo,
FileInfo,
PlatformInfo,
GitConfig,
GitStatus,
MigrateOptions,
MigrateResult,
OnlineMigrateCompareResult,
OnlineMigrateItemResult,
ProjectOverview,
ProjectStatus,
PushAllResult,
ReleaseInfo,
ReleaseSyncResult,
RemoteItem,
RepoInfo,
StashInfo,
TagInfo,
TreeNode,
UserInfo
TreeNode
} from "./models.js";

241
internal/app/gitsetup.go Normal file
View File

@@ -0,0 +1,241 @@
package app
import (
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/zhuy1228/GitPilot/internal/git"
)
// GitStatus Git 安装状态
type GitStatus struct {
Installed bool `json:"installed"`
Version string `json:"version"`
Path string `json:"path"`
}
// GitInstallProgress 安装进度
type GitInstallProgress struct {
Phase string `json:"phase"` // downloading, installing, done, error
Percent float64 `json:"percent"` // 下载百分比 0-100
Message string `json:"message"`
}
// CheckGitInstalled 检查 Git 是否已安装
func (s *AppService) CheckGitInstalled() GitStatus {
path, err := exec.LookPath("git")
if err != nil {
log.Println("Git 未安装:", err)
return GitStatus{Installed: false}
}
// 获取版本号
cmd := exec.Command("git", "--version")
hideWindowCmd(cmd)
output, err := cmd.Output()
if err != nil {
log.Println("获取 Git 版本失败:", err)
return GitStatus{Installed: true, Path: path}
}
version := strings.TrimSpace(string(output))
// "git version 2.43.0.windows.1" -> "2.43.0"
version = strings.TrimPrefix(version, "git version ")
if idx := strings.Index(version, ".windows"); idx > 0 {
version = version[:idx]
}
return GitStatus{
Installed: true,
Version: version,
Path: path,
}
}
// SelectGitInstallDir 打开文件夹选择器让用户选择 Git 安装路径
func (s *AppService) SelectGitInstallDir() (string, error) {
if s.app == nil {
return "", fmt.Errorf("应用未初始化")
}
path, err := s.app.Dialog.OpenFile().
CanChooseDirectories(true).
CanChooseFiles(false).
SetTitle("选择 Git 安装路径").
PromptForSingleSelection()
if err != nil {
return "", fmt.Errorf("打开文件夹选择器失败: %w", err)
}
return path, nil
}
// getGitDownloadURL 根据平台返回 Git 下载地址
func getGitDownloadURL() string {
switch runtime.GOOS {
case "windows":
if runtime.GOARCH == "arm64" {
return "https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-arm64.exe"
}
return "https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-64-bit.exe"
case "darwin":
return "" // macOS 建议使用 brew 或 Xcode CLI
default:
return "" // Linux 建议使用包管理器
}
}
// InstallGit 下载并安装 Git
// installDir: 用户选择的安装目录Windows 有效)
func (s *AppService) InstallGit(installDir string) error {
if runtime.GOOS != "windows" {
return fmt.Errorf("自动安装仅支持 Windows请使用系统包管理器安装 Git")
}
url := getGitDownloadURL()
if url == "" {
return fmt.Errorf("无法获取 Git 下载地址")
}
// 1. 下载安装包
s.emitInstallProgress("downloading", 0, "正在下载 Git 安装包...")
log.Println("开始下载 Git:", url)
tmpDir := os.TempDir()
installerPath := filepath.Join(tmpDir, "Git-Installer.exe")
err := s.downloadFile(url, installerPath)
if err != nil {
s.emitInstallProgress("error", 0, "下载失败: "+err.Error())
return fmt.Errorf("下载 Git 安装包失败: %w", err)
}
defer os.Remove(installerPath)
// 2. 静默安装
s.emitInstallProgress("installing", 100, "正在安装 Git请稍候...")
log.Println("开始安装 Git 到:", installDir)
// Git for Windows 静默安装参数
args := []string{
"/VERYSILENT",
"/NORESTART",
"/NOCANCEL",
"/SP-",
"/CLOSEAPPLICATIONS",
"/RESTARTAPPLICATIONS",
"/COMPONENTS=icons,ext,ext\\shellhere,ext\\guihere,gitlfs,assoc,assoc_sh,autoupdate",
}
if installDir != "" {
args = append(args, "/DIR="+installDir)
}
cmd := exec.Command(installerPath, args...)
hideWindowCmd(cmd)
err = cmd.Run()
if err != nil {
s.emitInstallProgress("error", 0, "安装失败: "+err.Error())
return fmt.Errorf("安装 Git 失败: %w", err)
}
// 3. 验证安装
time.Sleep(2 * time.Second) // 等待 PATH 更新
status := s.CheckGitInstalled()
if !status.Installed {
// 尝试在指定目录查找
gitExe := filepath.Join(installDir, "bin", "git.exe")
if _, err := os.Stat(gitExe); err == nil {
s.emitInstallProgress("done", 100, "Git 安装成功!路径: "+gitExe)
// 重新初始化 gitClient
s.reinitGitClient()
return nil
}
s.emitInstallProgress("error", 0, "安装完成但未检测到 Git请重启应用重试")
return fmt.Errorf("安装完成但未检测到 Git")
}
s.emitInstallProgress("done", 100, "Git 安装成功!版本: "+status.Version)
s.reinitGitClient()
return nil
}
// reinitGitClient 重新初始化 Git 客户端
func (s *AppService) reinitGitClient() {
s.gitClient = git.NewGitClient()
}
// emitInstallProgress 向前端发送安装进度事件
func (s *AppService) emitInstallProgress(phase string, percent float64, message string) {
if s.app != nil {
s.app.Event.Emit("git-install-progress", GitInstallProgress{
Phase: phase,
Percent: percent,
Message: message,
})
}
}
// downloadFile 下载文件并通过事件报告进度
func (s *AppService) downloadFile(url, dest string) error {
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
totalSize := resp.ContentLength
var downloaded int64
buf := make([]byte, 32*1024)
lastReport := time.Now()
for {
n, readErr := resp.Body.Read(buf)
if n > 0 {
_, writeErr := out.Write(buf[:n])
if writeErr != nil {
return writeErr
}
downloaded += int64(n)
// 每 300ms 报告一次进度
if time.Since(lastReport) > 300*time.Millisecond {
var pct float64
if totalSize > 0 {
pct = float64(downloaded) / float64(totalSize) * 100
}
sizeMB := float64(downloaded) / 1024 / 1024
msg := fmt.Sprintf("正在下载... %.1f MB", sizeMB)
if totalSize > 0 {
totalMB := float64(totalSize) / 1024 / 1024
msg = fmt.Sprintf("正在下载... %.1f / %.1f MB", sizeMB, totalMB)
}
s.emitInstallProgress("downloading", pct, msg)
lastReport = time.Now()
}
}
if readErr != nil {
if readErr == io.EOF {
break
}
return readErr
}
}
return nil
}

View File

@@ -0,0 +1,8 @@
//go:build !windows
package app
import "os/exec"
// hideWindowCmd 非 Windows 平台无需处理
func hideWindowCmd(cmd *exec.Cmd) {}

View File

@@ -0,0 +1,14 @@
package app
import (
"os/exec"
"syscall"
)
// hideWindowCmd 在 Windows 上隐藏子进程的控制台窗口
func hideWindowCmd(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000,
}
}

420
internal/app/migration.go Normal file
View File

@@ -0,0 +1,420 @@
package app
import (
"bytes"
"fmt"
"log"
"os"
"strings"
)
// --- 迁移相关类型 ---
// MigrateResult 迁移结果
type MigrateResult struct {
Path string `json:"path"`
Name string `json:"name"`
Success bool `json:"success"`
Message string `json:"message"`
CloneURL string `json:"cloneUrl,omitempty"`
RemoteName string `json:"remoteName,omitempty"`
}
// --- Release 同步相关类型 ---
// ReleaseSyncResult 单条 Release 同步结果
type ReleaseSyncResult struct {
TagName string `json:"tagName"`
Name string `json:"name"`
Success bool `json:"success"`
Message string `json:"message"`
}
// --- 辅助:根据 remote URL 匹配 credential ---
// detectPlatformFromURL 从 remote URL 推断平台类型
func detectPlatformFromURL(remoteURL string) string {
u := strings.ToLower(remoteURL)
if strings.Contains(u, "github.com") {
return "github"
}
if strings.Contains(u, "gitee.com") {
return "gitee"
}
if strings.Contains(u, "gitlab.com") {
return "gitlab"
}
// 默认当作 Gitea自建
return "gitea"
}
// extractBaseURL 从 remote URL 提取 base URL用于 Gitea 自建平台匹配)
func extractBaseURL(remoteURL string) string {
// https://host:port/owner/repo.git => https://host:port
// git@host:owner/repo.git => 不适用
if strings.HasPrefix(remoteURL, "git@") {
return ""
}
idx := strings.Index(remoteURL, "://")
if idx < 0 {
return ""
}
rest := remoteURL[idx+3:]
slashIdx := strings.Index(rest, "/")
if slashIdx < 0 {
return ""
}
return remoteURL[:idx+3+slashIdx]
}
// findCredentialForRemote 根据 remote URL 自动查找匹配的凭证
func (s *AppService) findCredentialForRemote(remoteURL string) (platform, baseURL, username, token string, found bool) {
plat := detectPlatformFromURL(remoteURL)
remoteBase := extractBaseURL(remoteURL)
// 1. 对于自建平台gitea/gitlab 或未知),优先按 BaseURL 精确匹配
if plat == "gitea" || plat == "gitlab" {
if remoteBase != "" {
for _, c := range s.config.Credentials {
cp := strings.ToLower(c.Platform)
if (cp == "gitea" || cp == "gitlab") && c.BaseURL != "" {
if strings.EqualFold(strings.TrimRight(c.BaseURL, "/"), strings.TrimRight(remoteBase, "/")) {
return c.Platform, c.BaseURL, c.Username, c.Token, true
}
}
}
}
}
// 2. 按平台名称匹配github / gitee / gitlab.com 等公有平台)
for _, c := range s.config.Credentials {
if strings.ToLower(c.Platform) == plat {
return c.Platform, c.BaseURL, c.Username, c.Token, true
}
}
return "", "", "", "", false
}
// --- 迁移服务方法 ---
// MigrateProject 将项目迁移到目标平台
// targetCredPlatform + targetCredUsername 用于定位目标凭证
// repoName: 目标仓库名(空则用项目名)
// remoteName: 新增的远程名(空则用平台名)
// private: 是否私有仓库
// description: 仓库描述
func (s *AppService) MigrateProject(projectPath, targetCredPlatform, targetCredUsername, repoName, remoteName, description string, private bool) (*MigrateResult, error) {
result := &MigrateResult{Path: projectPath}
// 校验项目路径
if _, err := os.Stat(projectPath); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", projectPath)
}
// 查找目标凭证
var cred *CredentialInfo
for _, c := range s.config.Credentials {
if strings.EqualFold(c.Platform, targetCredPlatform) && c.Username == targetCredUsername {
cred = &CredentialInfo{Platform: c.Platform, BaseURL: c.BaseURL, Username: c.Username, Token: c.Token}
break
}
}
if cred == nil {
return nil, fmt.Errorf("未找到目标凭证: %s/%s", targetCredPlatform, targetCredUsername)
}
if cred.Token == "" {
return nil, fmt.Errorf("目标凭证缺少 Token: %s/%s", targetCredPlatform, targetCredUsername)
}
// 创建平台 API 客户端
api, err := NewPlatformAPI(cred.Platform, cred.BaseURL, cred.Username, cred.Token)
if err != nil {
return nil, fmt.Errorf("创建平台 API 失败: %w", err)
}
// 确定仓库名
if repoName == "" {
// 从项目名或路径推断
for _, p := range s.config.Projects {
if p.Path == projectPath {
repoName = p.Name
break
}
}
if repoName == "" {
parts := strings.Split(strings.ReplaceAll(projectPath, "\\", "/"), "/")
repoName = parts[len(parts)-1]
}
}
// 确定远程名
if remoteName == "" {
remoteName = strings.ToLower(cred.Platform)
// 如果已存在同名 remote加上用户名
remoteList, _ := s.gitClient.RemoteList(projectPath)
for _, r := range remoteList {
if r.Name == remoteName {
remoteName = strings.ToLower(cred.Platform) + "-" + cred.Username
break
}
}
}
result.Name = repoName
result.RemoteName = remoteName
// 1. 通过 API 在目标平台创建仓库
cloneURL, err := api.CreateRepo(repoName, description, private)
if err != nil {
result.Message = fmt.Sprintf("创建仓库失败: %v", err)
return result, nil
}
result.CloneURL = cloneURL
log.Printf("迁移: 在 %s 上创建仓库 %s => %s", cred.Platform, repoName, cloneURL)
// 2. 添加 remote
_, err = s.gitClient.AddRemote(projectPath, remoteName, cloneURL)
if err != nil {
result.Message = fmt.Sprintf("仓库已创建(%s),但添加远程仓库失败: %v", cloneURL, err)
return result, nil
}
// 3. 推送所有分支到新 remote
proxy := s.GetProjectProxy(projectPath)
_, err = s.gitClient.RunWithProxy(projectPath, proxy, "push", remoteName, "--all")
if err != nil {
result.Message = fmt.Sprintf("推送分支失败: %v", err)
return result, nil
}
// 4. 推送所有标签到新 remote
_, err = s.gitClient.RunWithProxy(projectPath, proxy, "push", remoteName, "--tags")
if err != nil {
result.Message = fmt.Sprintf("分支已推送,但推送标签失败: %v", err)
return result, nil
}
result.Success = true
result.Message = "迁移成功"
return result, nil
}
// BatchMigrateProjects 批量迁移项目到目标平台
func (s *AppService) BatchMigrateProjects(projectPaths []string, targetCredPlatform, targetCredUsername, description string, private bool) []MigrateResult {
var results []MigrateResult
for _, path := range projectPaths {
result, err := s.MigrateProject(path, targetCredPlatform, targetCredUsername, "", "", description, private)
if err != nil {
results = append(results, MigrateResult{
Path: path,
Message: err.Error(),
})
} else {
results = append(results, *result)
}
}
return results
}
// --- Release 同步服务方法 ---
// GetRemoteReleases 获取指定远程仓库的 Release 列表
// overrideToken: 前端传入的 Token优先于凭证中的 Token
func (s *AppService) GetRemoteReleases(projectPath, remoteName, overrideToken string) ([]ReleaseInfo, error) {
if _, err := os.Stat(projectPath); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", projectPath)
}
// 获取 remote URL
remoteList, err := s.gitClient.RemoteList(projectPath)
if err != nil {
return nil, fmt.Errorf("获取远程仓库列表失败: %w", err)
}
var remoteURL string
for _, r := range remoteList {
if r.Name == remoteName {
remoteURL = r.URL
break
}
}
if remoteURL == "" {
return nil, fmt.Errorf("未找到远程仓库: %s", remoteName)
}
// 查找凭证
plat, baseURL, username, token, found := s.findCredentialForRemote(remoteURL)
if !found {
return nil, fmt.Errorf("未找到匹配的凭证,请先配置匹配 %s 的凭证", remoteURL)
}
if overrideToken != "" {
token = overrideToken
}
// 创建 API 客户端
api, err := NewPlatformAPI(plat, baseURL, username, token)
if err != nil {
return nil, fmt.Errorf("创建平台 API 失败: %w", err)
}
// 解析 owner/repo
owner, repo, err := ParseOwnerRepo(remoteURL)
if err != nil {
return nil, fmt.Errorf("解析远程地址失败: %w", err)
}
return api.ListReleases(owner, repo)
}
// SyncReleases 将源远程仓库的 Release 同步到目标远程仓库
// 仅同步目标不存在的 Release包括附件
func (s *AppService) SyncReleases(projectPath, sourceRemote, targetRemote, srcOverrideToken, tgtOverrideToken string) ([]ReleaseSyncResult, error) {
if _, err := os.Stat(projectPath); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", projectPath)
}
// 获取 remote URLs
remoteList, err := s.gitClient.RemoteList(projectPath)
if err != nil {
return nil, fmt.Errorf("获取远程仓库列表失败: %w", err)
}
var sourceURL, targetURL string
for _, r := range remoteList {
if r.Name == sourceRemote {
sourceURL = r.URL
}
if r.Name == targetRemote {
targetURL = r.URL
}
}
if sourceURL == "" {
return nil, fmt.Errorf("未找到源远程仓库: %s", sourceRemote)
}
if targetURL == "" {
return nil, fmt.Errorf("未找到目标远程仓库: %s", targetRemote)
}
// 查找源和目标凭证
srcPlat, srcBase, srcUser, srcToken, srcFound := s.findCredentialForRemote(sourceURL)
if !srcFound {
return nil, fmt.Errorf("未找到源平台的凭证,请先配置匹配 %s 的凭证", sourceURL)
}
if srcOverrideToken != "" {
srcToken = srcOverrideToken
}
tgtPlat, tgtBase, tgtUser, tgtToken, tgtFound := s.findCredentialForRemote(targetURL)
if !tgtFound {
return nil, fmt.Errorf("未找到目标平台的凭证,请先配置匹配 %s 的凭证", targetURL)
}
if tgtOverrideToken != "" {
tgtToken = tgtOverrideToken
}
// 创建 API 客户端
srcAPI, err := NewPlatformAPI(srcPlat, srcBase, srcUser, srcToken)
if err != nil {
return nil, fmt.Errorf("创建源平台 API 失败: %w", err)
}
tgtAPI, err := NewPlatformAPI(tgtPlat, tgtBase, tgtUser, tgtToken)
if err != nil {
return nil, fmt.Errorf("创建目标平台 API 失败: %w", err)
}
// 解析 owner/repo
srcOwner, srcRepo, err := ParseOwnerRepo(sourceURL)
if err != nil {
return nil, fmt.Errorf("解析源远程地址失败: %w", err)
}
tgtOwner, tgtRepo, err := ParseOwnerRepo(targetURL)
if err != nil {
return nil, fmt.Errorf("解析目标远程地址失败: %w", err)
}
// 获取源 Release 列表
srcReleases, err := srcAPI.ListReleases(srcOwner, srcRepo)
if err != nil {
return nil, fmt.Errorf("获取源 Release 列表失败: %w", err)
}
// 获取目标 Release 列表,用于跳过已存在的
tgtReleases, err := tgtAPI.ListReleases(tgtOwner, tgtRepo)
if err != nil {
// 目标可能还没有 Release不报错
tgtReleases = []ReleaseInfo{}
}
existingTags := make(map[string]bool)
for _, r := range tgtReleases {
existingTags[r.TagName] = true
}
var results []ReleaseSyncResult
// 先确保所有 tags 已推送到目标
proxy := s.GetProjectProxy(projectPath)
s.gitClient.RunWithProxy(projectPath, proxy, "push", targetRemote, "--tags")
// 逐个同步 Release
for _, srcRel := range srcReleases {
result := ReleaseSyncResult{
TagName: srcRel.TagName,
Name: srcRel.Name,
}
// 跳过已存在的
if existingTags[srcRel.TagName] {
result.Success = true
result.Message = "已存在,跳过"
results = append(results, result)
continue
}
// 创建 Release
created, err := tgtAPI.CreateRelease(tgtOwner, tgtRepo, ReleaseCreate{
TagName: srcRel.TagName,
Name: srcRel.Name,
Body: srcRel.Body,
Draft: srcRel.Draft,
Prerelease: srcRel.Prerelease,
})
if err != nil {
result.Message = fmt.Sprintf("创建 Release 失败: %v", err)
results = append(results, result)
continue
}
// 同步附件
assetErrors := []string{}
for _, asset := range srcRel.Assets {
if asset.DownloadURL == "" {
continue
}
// 下载附件
data, err := DownloadAsset(asset.DownloadURL, srcToken, srcPlat)
if err != nil {
assetErrors = append(assetErrors, fmt.Sprintf("%s: 下载失败(%v)", asset.Name, err))
continue
}
// 上传到目标
err = tgtAPI.UploadAsset(tgtOwner, tgtRepo, created.ID, asset.Name, bytes.NewReader(data))
if err != nil {
assetErrors = append(assetErrors, fmt.Sprintf("%s: 上传失败(%v)", asset.Name, err))
}
}
if len(assetErrors) > 0 {
result.Success = true
result.Message = "Release 已创建,但部分附件同步失败: " + strings.Join(assetErrors, "; ")
} else {
result.Success = true
if len(srcRel.Assets) > 0 {
result.Message = fmt.Sprintf("同步成功(含 %d 个附件)", len(srcRel.Assets))
} else {
result.Message = "同步成功"
}
}
results = append(results, result)
}
return results, nil
}

View File

@@ -0,0 +1,596 @@
package app
import (
"bytes"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
)
// --- 在线迁移相关类型 ---
// OnlineMigrateCompareRequest 在线迁移对比请求
type OnlineMigrateCompareRequest struct {
SrcPlatform string `json:"srcPlatform"` // github / gitee / gitea
SrcBaseURL string `json:"srcBaseUrl"`
SrcUsername string `json:"srcUsername"`
SrcToken string `json:"srcToken"`
TgtPlatform string `json:"tgtPlatform"`
TgtBaseURL string `json:"tgtBaseUrl"`
TgtUsername string `json:"tgtUsername"`
TgtToken string `json:"tgtToken"`
}
// MigrateOptions 迁移选项
type MigrateOptions struct {
Labels bool `json:"labels"`
Issues bool `json:"issues"`
PullRequests bool `json:"pullRequests"`
Releases bool `json:"releases"`
Milestones bool `json:"milestones"`
}
// OnlineMigrateCompareResult 在线迁移对比结果
type OnlineMigrateCompareResult struct {
SourceRepos []RepoInfo `json:"sourceRepos"` // 源平台所有仓库
TargetRepos []RepoInfo `json:"targetRepos"` // 目标平台所有仓库
MissingRepos []RepoInfo `json:"missingRepos"` // 目标平台缺少的仓库
}
// OnlineMigrateItemResult 单个仓库在线迁移结果
type OnlineMigrateItemResult struct {
Name string `json:"name"`
CloneURL string `json:"cloneUrl,omitempty"`
Success bool `json:"success"`
Message string `json:"message"`
}
// OnlineMigrateProgress 迁移进度事件数据
type OnlineMigrateProgress struct {
Current int `json:"current"` // 当前正在处理第几个仓库(从 1 开始)
Total int `json:"total"` // 仓库总数
RepoName string `json:"repoName"` // 当前仓库名
Phase string `json:"phase"` // clone | create | push | labels | milestones | issues | releases | pullRequests | done | error
Percent float64 `json:"percent"` // 整体进度百分比 0-100
Message string `json:"message"` // 可读状态描述
}
// emitMigrateProgress 向前端发送迁移进度
func (s *AppService) emitMigrateProgress(current, total int, repoName, phase, message string) {
pct := float64(0)
if total > 0 {
phaseWeights := map[string]float64{
"clone": 0.3,
"create": 0.05,
"push": 0.3,
"labels": 0.05,
"milestones": 0.05,
"issues": 0.1,
"releases": 0.1,
"pullRequests": 0.05,
"done": 1.0,
"error": 0.0,
}
w := phaseWeights[phase]
pct = (float64(current-1) + w) / float64(total) * 100
if pct > 100 {
pct = 100
}
}
if s.app != nil {
s.app.Event.Emit("online-migrate-progress", OnlineMigrateProgress{
Current: current,
Total: total,
RepoName: repoName,
Phase: phase,
Percent: pct,
Message: message,
})
}
}
// --- 在线迁移服务方法 ---
// OnlineMigrateCompare 对比两个平台的仓库列表
func (s *AppService) OnlineMigrateCompare(
srcPlatform, srcBaseURL, srcUsername, srcToken,
tgtPlatform, tgtBaseURL, tgtUsername, tgtToken string,
) (*OnlineMigrateCompareResult, error) {
srcAPI, err := NewPlatformAPI(srcPlatform, srcBaseURL, srcUsername, srcToken)
if err != nil {
return nil, fmt.Errorf("创建源平台 API 失败: %w", err)
}
tgtAPI, err := NewPlatformAPI(tgtPlatform, tgtBaseURL, tgtUsername, tgtToken)
if err != nil {
return nil, fmt.Errorf("创建目标平台 API 失败: %w", err)
}
srcRepos, err := srcAPI.ListRepos()
if err != nil {
return nil, fmt.Errorf("获取源平台仓库列表失败: %w", err)
}
tgtRepos, err := tgtAPI.ListRepos()
if err != nil {
return nil, fmt.Errorf("获取目标平台仓库列表失败: %w", err)
}
tgtNames := make(map[string]bool)
for _, r := range tgtRepos {
tgtNames[strings.ToLower(r.Name)] = true
}
var missing []RepoInfo
for _, r := range srcRepos {
if !tgtNames[strings.ToLower(r.Name)] {
missing = append(missing, r)
}
}
return &OnlineMigrateCompareResult{
SourceRepos: srcRepos,
TargetRepos: tgtRepos,
MissingRepos: missing,
}, nil
}
// OnlineMigrateExecute 执行在线迁移
func (s *AppService) OnlineMigrateExecute(
srcPlatform, srcBaseURL, srcUsername, srcToken string,
tgtPlatform, tgtBaseURL, tgtUsername, tgtToken string,
repoNames []string,
srcUseProxy, tgtUseProxy bool,
opts MigrateOptions,
) []OnlineMigrateItemResult {
total := len(repoNames)
srcAPI, err := NewPlatformAPI(srcPlatform, srcBaseURL, srcUsername, srcToken)
if err != nil {
return []OnlineMigrateItemResult{{Name: "(全局)", Message: fmt.Sprintf("创建源平台 API 失败: %v", err)}}
}
tgtAPI, err := NewPlatformAPI(tgtPlatform, tgtBaseURL, tgtUsername, tgtToken)
if err != nil {
return []OnlineMigrateItemResult{{Name: "(全局)", Message: fmt.Sprintf("创建目标平台 API 失败: %v", err)}}
}
srcRepos, err := srcAPI.ListRepos()
if err != nil {
return []OnlineMigrateItemResult{{Name: "(全局)", Message: fmt.Sprintf("获取源平台仓库列表失败: %v", err)}}
}
repoMap := make(map[string]RepoInfo)
for _, r := range srcRepos {
repoMap[strings.ToLower(r.Name)] = r
}
// 检查目标是否是 Gitea可以用原生迁移 API
// 但如果源平台需要代理,说明 Gitea 服务器可能无法直接访问源平台(如国内服务器访问 GitHub
// 此时应跳过原生迁移,改用本地中转(本地代理克隆 → 推送到 Gitea
giteaTgt, isGiteaTarget := tgtAPI.(*GiteaAPI)
useNativeGiteaMigrate := isGiteaTarget && !srcUseProxy
// 创建临时目录(非原生 Gitea 迁移时使用)
var tmpDir string
if !useNativeGiteaMigrate {
tmpDir, err = os.MkdirTemp("", "gitpilot-migrate-*")
if err != nil {
return []OnlineMigrateItemResult{{Name: "(全局)", Message: fmt.Sprintf("创建临时目录失败: %v", err)}}
}
defer os.RemoveAll(tmpDir)
}
srcProxy := srcUseProxy
tgtProxy := tgtUseProxy
var results []OnlineMigrateItemResult
for idx, name := range repoNames {
current := idx + 1
result := OnlineMigrateItemResult{Name: name}
srcRepo, ok := repoMap[strings.ToLower(name)]
if !ok {
result.Message = "在源平台未找到该仓库"
s.emitMigrateProgress(current, total, name, "error", result.Message)
results = append(results, result)
continue
}
cloneURL := srcRepo.CloneURL
if cloneURL == "" {
result.Message = "源仓库无 clone URL"
s.emitMigrateProgress(current, total, name, "error", result.Message)
results = append(results, result)
continue
}
// 解析源仓库的 owner/repo
srcOwner, srcRepoName, parseErr := ParseOwnerRepo(cloneURL)
if parseErr != nil {
srcOwner = srcUsername
srcRepoName = name
}
// --- Gitea 目标且源不需代理: 使用原生迁移 API ---
if useNativeGiteaMigrate {
s.emitMigrateProgress(current, total, name, "clone", fmt.Sprintf("正在迁移 %sGitea 原生迁移)...", name))
targetCloneURL, migrateErr := giteaTgt.MigrateRepo(cloneURL, name, srcToken, srcPlatform, opts)
if migrateErr != nil {
result.Message = fmt.Sprintf("Gitea 迁移失败: %v", migrateErr)
s.emitMigrateProgress(current, total, name, "error", result.Message)
results = append(results, result)
continue
}
result.CloneURL = targetCloneURL
result.Success = true
result.Message = "迁移成功(含选中的标签/工单/发布/里程碑/合并请求)"
s.emitMigrateProgress(current, total, name, "done", result.Message)
results = append(results, result)
log.Printf("在线迁移(Gitea): %s 完成 → %s", name, targetCloneURL)
continue
}
// --- 非 Gitea 目标: 手动迁移 ---
authCloneURL := injectTokenToURL(cloneURL, srcUsername, srcToken, srcPlatform)
localPath := filepath.Join(tmpDir, name)
// 1. Clone
s.emitMigrateProgress(current, total, name, "clone", fmt.Sprintf("正在克隆 %s...", name))
_, err = s.gitClient.CloneWithProxy(authCloneURL, localPath, &srcProxy)
if err != nil {
result.Message = fmt.Sprintf("克隆失败: %v", err)
s.emitMigrateProgress(current, total, name, "error", result.Message)
results = append(results, result)
continue
}
// 2. 创建目标仓库
s.emitMigrateProgress(current, total, name, "create", fmt.Sprintf("正在创建目标仓库 %s...", name))
targetCloneURL, err := tgtAPI.CreateRepo(name, srcRepo.Description, srcRepo.Private)
if err != nil {
result.Message = fmt.Sprintf("创建目标仓库失败: %v", err)
s.emitMigrateProgress(current, total, name, "error", result.Message)
results = append(results, result)
continue
}
result.CloneURL = targetCloneURL
tgtOwner, tgtRepoName, tgtParseErr := ParseOwnerRepo(targetCloneURL)
if tgtParseErr != nil {
tgtOwner = tgtUsername
tgtRepoName = name
}
pushURL := injectTokenToURL(targetCloneURL, tgtUsername, tgtToken, tgtPlatform)
// 3. 添加 remote
_, err = s.gitClient.Run(localPath, "remote", "add", "target", pushURL)
if err != nil {
result.Message = fmt.Sprintf("添加 remote 失败: %v", err)
s.emitMigrateProgress(current, total, name, "error", result.Message)
results = append(results, result)
continue
}
// 4. Push 分支和标签
s.emitMigrateProgress(current, total, name, "push", fmt.Sprintf("正在推送 %s 的分支和标签...", name))
pushFailed := false
// 先尝试 --all 一次性推送
_, err = s.gitClient.RunWithProxyTimeout(localPath, &tgtProxy, 10*time.Minute,
"-c", "http.postBuffer=524288000", "push", "target", "--all")
if err != nil {
// --all 推送失败(可能 HTTP 413改为逐分支推送
log.Printf("push --all 失败,改为逐分支推送: %v", err)
s.emitMigrateProgress(current, total, name, "push", fmt.Sprintf("正在逐分支推送 %s...", name))
branchOut, brErr := s.gitClient.Run(localPath, "branch", "-a", "--format=%(refname:short)")
if brErr != nil {
result.Message = fmt.Sprintf("获取分支列表失败: %v", brErr)
s.emitMigrateProgress(current, total, name, "error", result.Message)
results = append(results, result)
continue
}
branches := strings.Split(strings.TrimSpace(branchOut), "\n")
var pushErrs []string
for _, br := range branches {
br = strings.TrimSpace(br)
if br == "" || strings.HasPrefix(br, "origin/HEAD") {
continue
}
// 将 origin/xxx 转换为本地 refspec
refspec := br
if strings.HasPrefix(br, "origin/") {
refspec = "refs/remotes/" + br + ":refs/heads/" + strings.TrimPrefix(br, "origin/")
}
_, pushErr := s.gitClient.RunWithProxyTimeout(localPath, &tgtProxy, 10*time.Minute,
"-c", "http.postBuffer=524288000", "push", "target", refspec)
if pushErr != nil {
pushErrs = append(pushErrs, fmt.Sprintf("%s(%v)", br, pushErr))
}
}
if len(pushErrs) > 0 {
// 如果所有分支都失败,才算整体失败
if len(pushErrs) == len(branches) {
result.Message = fmt.Sprintf("推送分支全部失败: %s", strings.Join(pushErrs, "; "))
s.emitMigrateProgress(current, total, name, "error", result.Message)
results = append(results, result)
pushFailed = true
} else {
log.Printf("部分分支推送失败: %s", strings.Join(pushErrs, "; "))
}
}
}
if pushFailed {
continue
}
// 推送标签
s.gitClient.RunWithProxyTimeout(localPath, &tgtProxy, 10*time.Minute,
"-c", "http.postBuffer=524288000", "push", "target", "--tags")
// --- 迁移扩展数据 ---
var migrateNotes []string
// 5. 标签
if opts.Labels {
s.emitMigrateProgress(current, total, name, "labels", fmt.Sprintf("正在迁移 %s 的标签...", name))
if labErr := s.migrateLabels(srcAPI, tgtAPI, srcOwner, srcRepoName, tgtOwner, tgtRepoName); labErr != nil {
migrateNotes = append(migrateNotes, fmt.Sprintf("标签: %v", labErr))
} else {
migrateNotes = append(migrateNotes, "标签 ✓")
}
}
// 6. 里程碑
var milestoneMap map[string]int64
if opts.Milestones {
s.emitMigrateProgress(current, total, name, "milestones", fmt.Sprintf("正在迁移 %s 的里程碑...", name))
var msErr error
milestoneMap, msErr = s.migrateMilestones(srcAPI, tgtAPI, srcOwner, srcRepoName, tgtOwner, tgtRepoName)
if msErr != nil {
migrateNotes = append(migrateNotes, fmt.Sprintf("里程碑: %v", msErr))
} else {
migrateNotes = append(migrateNotes, "里程碑 ✓")
}
}
// 7. 工单
if opts.Issues {
s.emitMigrateProgress(current, total, name, "issues", fmt.Sprintf("正在迁移 %s 的工单...", name))
if issErr := s.migrateIssues(srcAPI, tgtAPI, srcOwner, srcRepoName, tgtOwner, tgtRepoName, milestoneMap); issErr != nil {
migrateNotes = append(migrateNotes, fmt.Sprintf("工单: %v", issErr))
} else {
migrateNotes = append(migrateNotes, "工单 ✓")
}
}
// 8. 发布
if opts.Releases {
s.emitMigrateProgress(current, total, name, "releases", fmt.Sprintf("正在迁移 %s 的发布...", name))
if relErr := s.migrateReleases(srcAPI, tgtAPI, srcOwner, srcRepoName, tgtOwner, tgtRepoName, srcToken, srcPlatform); relErr != nil {
migrateNotes = append(migrateNotes, fmt.Sprintf("发布: %v", relErr))
} else {
migrateNotes = append(migrateNotes, "发布 ✓")
}
}
// 9. 合并请求(以 Issue 方式记录)
if opts.PullRequests {
s.emitMigrateProgress(current, total, name, "pullRequests", fmt.Sprintf("正在迁移 %s 的合并请求...", name))
if prErr := s.migratePullRequests(srcAPI, tgtAPI, srcOwner, srcRepoName, tgtOwner, tgtRepoName, milestoneMap); prErr != nil {
migrateNotes = append(migrateNotes, fmt.Sprintf("合并请求: %v", prErr))
} else {
migrateNotes = append(migrateNotes, "合并请求 ✓")
}
}
result.Success = true
if len(migrateNotes) > 0 {
result.Message = "迁移完成 | " + strings.Join(migrateNotes, " | ")
} else {
result.Message = "迁移成功"
}
s.emitMigrateProgress(current, total, name, "done", result.Message)
results = append(results, result)
log.Printf("在线迁移: %s 完成 → %s", name, targetCloneURL)
os.RemoveAll(localPath)
}
return results
}
// --- 迁移子步骤 ---
func (s *AppService) migrateLabels(srcAPI, tgtAPI PlatformAPI, srcOwner, srcRepo, tgtOwner, tgtRepo string) error {
srcLabels, err := srcAPI.ListLabels(srcOwner, srcRepo)
if err != nil {
return fmt.Errorf("获取源标签失败: %w", err)
}
if len(srcLabels) == 0 {
return nil
}
tgtLabels, _ := tgtAPI.ListLabels(tgtOwner, tgtRepo)
existing := make(map[string]bool)
for _, l := range tgtLabels {
existing[strings.ToLower(l.Name)] = true
}
var errs []string
for _, label := range srcLabels {
if existing[strings.ToLower(label.Name)] {
continue
}
if err := tgtAPI.CreateLabel(tgtOwner, tgtRepo, label); err != nil {
errs = append(errs, fmt.Sprintf("%s(%v)", label.Name, err))
}
}
if len(errs) > 0 {
return fmt.Errorf("部分失败: %s", strings.Join(errs, "; "))
}
return nil
}
func (s *AppService) migrateMilestones(srcAPI, tgtAPI PlatformAPI, srcOwner, srcRepo, tgtOwner, tgtRepo string) (map[string]int64, error) {
msMap := make(map[string]int64)
srcMS, err := srcAPI.ListMilestones(srcOwner, srcRepo)
if err != nil {
return msMap, fmt.Errorf("获取源里程碑失败: %w", err)
}
if len(srcMS) == 0 {
return msMap, nil
}
tgtMS, _ := tgtAPI.ListMilestones(tgtOwner, tgtRepo)
for _, ms := range tgtMS {
msMap[ms.Title] = ms.ID
}
var errs []string
for _, ms := range srcMS {
if _, exists := msMap[ms.Title]; exists {
continue
}
created, err := tgtAPI.CreateMilestone(tgtOwner, tgtRepo, ms)
if err != nil {
errs = append(errs, fmt.Sprintf("%s(%v)", ms.Title, err))
} else if created != nil {
msMap[created.Title] = created.ID
}
}
if len(errs) > 0 {
return msMap, fmt.Errorf("部分失败: %s", strings.Join(errs, "; "))
}
return msMap, nil
}
func (s *AppService) migrateIssues(srcAPI, tgtAPI PlatformAPI, srcOwner, srcRepo, tgtOwner, tgtRepo string, milestoneMap map[string]int64) error {
srcIssues, err := srcAPI.ListIssues(srcOwner, srcRepo)
if err != nil {
return fmt.Errorf("获取源工单失败: %w", err)
}
if len(srcIssues) == 0 {
return nil
}
if milestoneMap == nil {
milestoneMap = make(map[string]int64)
}
var errs []string
for _, issue := range srcIssues {
if err := tgtAPI.CreateIssue(tgtOwner, tgtRepo, issue, milestoneMap); err != nil {
errs = append(errs, fmt.Sprintf("#%d(%v)", issue.Number, err))
if len(errs) >= 5 {
errs = append(errs, "...更多错误已省略")
break
}
}
}
if len(errs) > 0 {
return fmt.Errorf("部分失败: %s", strings.Join(errs, "; "))
}
return nil
}
func (s *AppService) migrateReleases(srcAPI, tgtAPI PlatformAPI, srcOwner, srcRepo, tgtOwner, tgtRepo, srcToken, srcPlatform string) error {
srcRel, err := srcAPI.ListReleases(srcOwner, srcRepo)
if err != nil {
return fmt.Errorf("获取源发布失败: %w", err)
}
if len(srcRel) == 0 {
return nil
}
tgtRel, _ := tgtAPI.ListReleases(tgtOwner, tgtRepo)
existing := make(map[string]bool)
for _, r := range tgtRel {
existing[r.TagName] = true
}
var errs []string
for _, rel := range srcRel {
if existing[rel.TagName] {
continue
}
created, err := tgtAPI.CreateRelease(tgtOwner, tgtRepo, ReleaseCreate{
TagName: rel.TagName, Name: rel.Name, Body: rel.Body, Draft: rel.Draft, Prerelease: rel.Prerelease,
})
if err != nil {
errs = append(errs, fmt.Sprintf("%s(%v)", rel.TagName, err))
continue
}
for _, asset := range rel.Assets {
if asset.DownloadURL == "" {
continue
}
data, dlErr := DownloadAsset(asset.DownloadURL, srcToken, srcPlatform)
if dlErr != nil {
continue
}
tgtAPI.UploadAsset(tgtOwner, tgtRepo, created.ID, asset.Name, bytes.NewReader(data))
}
}
if len(errs) > 0 {
return fmt.Errorf("部分失败: %s", strings.Join(errs, "; "))
}
return nil
}
func (s *AppService) migratePullRequests(srcAPI, tgtAPI PlatformAPI, srcOwner, srcRepo, tgtOwner, tgtRepo string, milestoneMap map[string]int64) error {
srcPRs, err := srcAPI.ListPullRequests(srcOwner, srcRepo)
if err != nil {
return fmt.Errorf("获取源合并请求失败: %w", err)
}
if len(srcPRs) == 0 {
return nil
}
if milestoneMap == nil {
milestoneMap = make(map[string]int64)
}
var errs []string
for _, pr := range srcPRs {
stateEmoji := "🟢"
if pr.State == "closed" {
stateEmoji = "🔴"
} else if pr.State == "merged" {
stateEmoji = "🟣"
}
body := fmt.Sprintf(
"%s **[迁移的合并请求]** `%s` → `%s` | 状态: %s %s | 作者: @%s\n\n---\n\n%s",
stateEmoji, pr.Head, pr.Base, stateEmoji, pr.State, pr.User, pr.Body,
)
issue := IssueInfo{
Title: fmt.Sprintf("[PR#%d] %s", pr.Number, pr.Title),
Body: body,
State: "closed",
}
if err := tgtAPI.CreateIssue(tgtOwner, tgtRepo, issue, milestoneMap); err != nil {
errs = append(errs, fmt.Sprintf("PR#%d(%v)", pr.Number, err))
if len(errs) >= 5 {
errs = append(errs, "...更多错误已省略")
break
}
}
}
if len(errs) > 0 {
return fmt.Errorf("部分失败: %s", strings.Join(errs, "; "))
}
return nil
}
// injectTokenToURL 将 token 注入到 HTTPS clone URL 中用于认证
func injectTokenToURL(cloneURL, username, token, platform string) string {
if token == "" {
return cloneURL
}
if !strings.HasPrefix(cloneURL, "https://") && !strings.HasPrefix(cloneURL, "http://") {
return cloneURL
}
idx := strings.Index(cloneURL, "://")
if idx < 0 {
return cloneURL
}
scheme := cloneURL[:idx+3]
rest := cloneURL[idx+3:]
return fmt.Sprintf("%s%s:%s@%s", scheme, username, token, rest)
}

1126
internal/app/platform.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -29,10 +29,49 @@ func NewGitClient() *GitClient {
// Run 执行 git 命令,支持超时和代理设置
func (g *GitClient) Run(path string, args ...string) (string, error) {
return g.RunWithProxy(path, nil, args...)
}
// RunWithProxy 执行 git 命令useProxy 可覆盖全局代理设置
// useProxy == nil 时跟随 GitClient 自身的 Enabled 设置
func (g *GitClient) RunWithProxy(path string, useProxy *bool, args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
defer cancel()
header := []string{"-C", path, "-c", "core.quotePath=false"}
if g.Enabled {
enableProxy := g.Enabled
if useProxy != nil {
enableProxy = *useProxy
}
if enableProxy {
header = append(header, "-c", "http.proxy="+g.Proxy, "-c", "https.proxy="+g.Proxy)
}
argsArr := append(header, args...)
log.Println(argsArr)
cmd := exec.CommandContext(ctx, "git", argsArr...)
hideWindow(cmd)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if ctx.Err() == context.DeadlineExceeded {
return "", fmt.Errorf("git command timeout: git %v", args)
}
if err != nil {
return "", fmt.Errorf("git command error: %v, stderr: %s", err, stderr.String())
}
return stdout.String(), nil
}
// RunWithProxyTimeout 执行 git 命令,支持自定义超时
func (g *GitClient) RunWithProxyTimeout(path string, useProxy *bool, timeout time.Duration, args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
header := []string{"-C", path, "-c", "core.quotePath=false"}
enableProxy := g.Enabled
if useProxy != nil {
enableProxy = *useProxy
}
if enableProxy {
header = append(header, "-c", "http.proxy="+g.Proxy, "-c", "https.proxy="+g.Proxy)
}
argsArr := append(header, args...)
@@ -60,18 +99,104 @@ func (g *GitClient) Push(path string) (string, error) {
return g.Run(path, "push")
}
// PushTo 推送到指定远程
func (g *GitClient) PushTo(path, remote, branch string) (string, error) {
return g.Run(path, "push", remote, branch)
}
// PullFrom 从指定远程拉取
func (g *GitClient) PullFrom(path, remote, branch string) (string, error) {
return g.Run(path, "pull", remote, branch)
}
// FetchRemote 拉取指定远程信息
func (g *GitClient) FetchRemote(path, remote string) (string, error) {
return g.Run(path, "fetch", remote)
}
func (g *GitClient) Status(path string) (string, error) {
return g.Run(path, "status")
}
func (g *GitClient) Clone(repoURL, path string) (string, error) {
return g.Run(".", "clone", repoURL, path)
return g.CloneWithProxy(repoURL, path, nil)
}
// CloneWithProxy 克隆仓库useProxy 可独立控制代理
func (g *GitClient) CloneWithProxy(repoURL, path string, useProxy *bool) (string, error) {
// Clone 操作可能需要较长时间使用独立的超时设置10 分钟)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
args := []string{"-c", "core.quotePath=false"}
enableProxy := g.Enabled
if useProxy != nil {
enableProxy = *useProxy
}
if enableProxy {
args = append(args, "-c", "http.proxy="+g.Proxy, "-c", "https.proxy="+g.Proxy)
}
args = append(args, "clone", "--progress", repoURL, path)
log.Println(args)
cmd := exec.CommandContext(ctx, "git", args...)
hideWindow(cmd)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if ctx.Err() == context.DeadlineExceeded {
return "", fmt.Errorf("git clone timeout")
}
if err != nil {
return "", fmt.Errorf("git clone error: %v, stderr: %s", err, stderr.String())
}
return stdout.String(), nil
}
func (g *GitClient) Fetch(path string) (string, error) {
return g.Run(path, "fetch", "--all")
}
// RemoteList 获取所有远程仓库列表 (git remote -v)
type RemoteInfo struct {
Name string
URL string
}
func (g *GitClient) RemoteList(path string) ([]RemoteInfo, error) {
out, err := g.Run(path, "remote", "-v")
if err != nil {
return nil, err
}
seen := make(map[string]bool)
var remotes []RemoteInfo
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
name := parts[0]
if seen[name] {
continue
}
seen[name] = true
remotes = append(remotes, RemoteInfo{Name: name, URL: parts[1]})
}
return remotes, nil
}
// AddRemote 添加远程仓库
func (g *GitClient) AddRemote(path, name, url string) (string, error) {
return g.Run(path, "remote", "add", name, url)
}
// RemoveRemote 删除远程仓库
func (g *GitClient) RemoveRemote(path, name string) (string, error) {
return g.Run(path, "remote", "remove", name)
}
func (g *GitClient) RemoteURL(path string) (string, error) {
out, err := g.Run(path, "config", "--get", "remote.origin.url")
if err != nil {
@@ -120,11 +245,16 @@ func (g *GitClient) Log(path string, count int) (string, error) {
)
}
// UnpushedCommits 获取当前分支上未推送到远程的提交哈希列表
// UnpushedCommits 获取当前分支上未推送到远程的提交哈希列表(默认 origin
func (g *GitClient) UnpushedCommits(path, branch string) (string, error) {
return g.Run(path, "rev-list", "origin/"+branch+"..HEAD")
}
// UnpushedCommitsTo 获取当前分支上未推送到指定远程的提交哈希列表
func (g *GitClient) UnpushedCommitsTo(path, remote, branch string) (string, error) {
return g.Run(path, "rev-list", remote+"/"+branch+"..HEAD")
}
// RevertCommit 撤回指定提交(创建一个反向提交)
func (g *GitClient) RevertCommit(path, hash string) (string, error) {
return g.Run(path, "revert", "--no-edit", hash)
@@ -146,16 +276,26 @@ func (g *GitClient) DeleteTag(path, name string) (string, error) {
return g.Run(path, "tag", "-d", name)
}
// PushTag 推送标签到远程
// PushTag 推送标签到远程(默认 origin
func (g *GitClient) PushTag(path, name string) (string, error) {
return g.Run(path, "push", "origin", name)
}
// DeleteRemoteTag 删除远程标签
// PushTagTo 推送标签到指定远程
func (g *GitClient) PushTagTo(path, remote, name string) (string, error) {
return g.Run(path, "push", remote, name)
}
// DeleteRemoteTag 删除远程标签(默认 origin
func (g *GitClient) DeleteRemoteTag(path, name string) (string, error) {
return g.Run(path, "push", "origin", "--delete", name)
}
// DeleteRemoteTagFrom 删除指定远程的标签
func (g *GitClient) DeleteRemoteTagFrom(path, remote, name string) (string, error) {
return g.Run(path, "push", remote, "--delete", name)
}
// BranchList 获取所有本地分支
func (g *GitClient) BranchList(path string) (string, error) {
return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)")
@@ -327,6 +467,184 @@ func parseNameStatus(output string) []FileChange {
return changes
}
// CreateBranch 创建新分支
func (g *GitClient) CreateBranch(path, name string) (string, error) {
return g.Run(path, "branch", name)
}
// DeleteBranch 删除本地分支 (-d 安全删除)
func (g *GitClient) DeleteBranch(path, name string) (string, error) {
return g.Run(path, "branch", "-d", name)
}
// ForceDeleteBranch 强制删除本地分支 (-D)
func (g *GitClient) ForceDeleteBranch(path, name string) (string, error) {
return g.Run(path, "branch", "-D", name)
}
// MergeBranch 合并指定分支到当前分支
func (g *GitClient) MergeBranch(path, branch string) (string, error) {
return g.Run(path, "merge", branch)
}
// DeleteRemoteBranch 删除远程分支(默认 origin
func (g *GitClient) DeleteRemoteBranch(path, branch string) (string, error) {
return g.Run(path, "push", "origin", "--delete", branch)
}
// DeleteRemoteBranchFrom 删除指定远程的分支
func (g *GitClient) DeleteRemoteBranchFrom(path, remote, branch string) (string, error) {
return g.Run(path, "push", remote, "--delete", branch)
}
// RemoteBranchList 获取所有远程分支
func (g *GitClient) RemoteBranchList(path string) (string, error) {
return g.Run(path, "branch", "-r", "--format=%(refname:short)")
}
// RemoteBranchListByRemote 获取指定远程的分支
func (g *GitClient) RemoteBranchListByRemote(path, remote string) (string, error) {
return g.Run(path, "branch", "-r", "--list", remote+"/*", "--format=%(refname:short)")
}
// CheckoutNewBranch 从远程分支检出新本地分支
func (g *GitClient) CheckoutNewBranch(path, localBranch, remoteBranch string) (string, error) {
return g.Run(path, "checkout", "-b", localBranch, remoteBranch)
}
// StashSave 保存当前工作区变更到贮藏
func (g *GitClient) StashSave(path, message string) (string, error) {
if message != "" {
return g.Run(path, "stash", "push", "-m", message)
}
return g.Run(path, "stash", "push")
}
// StashList 获取贮藏列表
func (g *GitClient) StashList(path string) (string, error) {
return g.Run(path, "stash", "list", "--format=%gd\t%s\t%at")
}
// StashApply 应用贮藏(不删除)
func (g *GitClient) StashApply(path string, index int) (string, error) {
return g.Run(path, "stash", "apply", fmt.Sprintf("stash@{%d}", index))
}
// StashPop 应用贮藏并删除
func (g *GitClient) StashPop(path string, index int) (string, error) {
return g.Run(path, "stash", "pop", fmt.Sprintf("stash@{%d}", index))
}
// StashDrop 删除贮藏
func (g *GitClient) StashDrop(path string, index int) (string, error) {
return g.Run(path, "stash", "drop", fmt.Sprintf("stash@{%d}", index))
}
// GetGitGlobalConfig 获取 git 全局配置
func (g *GitClient) GetGitGlobalConfig(key string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "git", "config", "--global", "--get", key)
hideWindow(cmd)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return "", nil // key 不存在不算错误
}
return strings.TrimSpace(stdout.String()), nil
}
// SetGitGlobalConfig 设置 git 全局配置
func (g *GitClient) SetGitGlobalConfig(key, value string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "git", "config", "--global", key, value)
hideWindow(cmd)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return "", fmt.Errorf("git config error: %v, stderr: %s", err, stderr.String())
}
return strings.TrimSpace(stdout.String()), nil
}
// --- 冲突处理 ---
// ConflictFiles 获取冲突文件列表 (git diff --name-only --diff-filter=U)
func (g *GitClient) ConflictFiles(path string) (string, error) {
return g.Run(path, "diff", "--name-only", "--diff-filter=U")
}
// GetConflictContent 获取冲突文件的完整内容(含冲突标记)
func (g *GitClient) GetConflictContent(path, filePath string) (string, error) {
return g.Run(path, "show", ":0:"+filePath)
}
// MarkResolved 将冲突文件标记为已解决 (git add <file>)
func (g *GitClient) MarkResolved(path string, files ...string) (string, error) {
args := append([]string{"add"}, files...)
return g.Run(path, args...)
}
// AbortMerge 中止合并 (git merge --abort)
func (g *GitClient) AbortMerge(path string) (string, error) {
return g.Run(path, "merge", "--abort")
}
// MergeStatus 检查是否处于合并状态
func (g *GitClient) MergeStatus(path string) bool {
// 检查 .git/MERGE_HEAD 文件是否存在
_, err := g.Run(path, "rev-parse", "--verify", "MERGE_HEAD")
return err == nil
}
// --- 提交搜索 ---
// SearchCommits 搜索提交历史 (git log --grep / --author / --after / --before)
func (g *GitClient) SearchCommits(path string, keyword, author string, maxCount int) (string, error) {
args := []string{"log"}
if maxCount > 0 {
args = append(args, fmt.Sprintf("--max-count=%d", maxCount))
}
if keyword != "" {
args = append(args, "--grep="+keyword, "-i")
}
if author != "" {
args = append(args, "--author="+author)
}
args = append(args, "--format=%H%n%h%n%an%n%ae%n%at%n%s%n---END---")
return g.Run(path, args...)
}
// --- 批量操作 ---
// QuickStatus 快速获取分支名和是否有变更(轻量级)
func (g *GitClient) QuickStatus(path string) (branch string, hasChanges bool, unpushed int, err error) {
branchOut, err := g.Branch(path)
if err != nil {
return "", false, 0, err
}
branch = strings.TrimSpace(branchOut)
// 检查是否有变更
statusOut, err := g.Run(path, "status", "--porcelain")
if err == nil {
hasChanges = strings.TrimSpace(statusOut) != ""
}
// 检查未推送提交数
unpushedOut, err := g.Run(path, "rev-list", "--count", "origin/"+branch+"..HEAD")
if err == nil {
fmt.Sscanf(strings.TrimSpace(unpushedOut), "%d", &unpushed)
}
return branch, hasChanges, unpushed, nil
}
// StatusText 返回变更状态的中文描述
func (fc FileChange) StatusText() string {
switch {

View File

@@ -4,8 +4,6 @@ import (
"fmt"
"os"
"strings"
"golang.org/x/sys/windows/registry"
)
// ProxyInfo 保存当前系统代理信息
@@ -25,8 +23,8 @@ type ProxyInfo struct {
}
// GetCurrentProxy 获取本机当前的代理设置
// 优先读取 Windows 系统代理(注册表),同时也读取环境变量中的代理配置
// 支持检测 Clash、V2Ray 等常见代理工具的配置
// 优先读取系统代理(Windows 注册表 / macOS scutil / Linux 环境变量),
// 同时也读取环境变量中的代理配置
func GetCurrentProxy() (*ProxyInfo, error) {
info := &ProxyInfo{}
@@ -34,9 +32,8 @@ func GetCurrentProxy() (*ProxyInfo, error) {
info.HTTPProxy = getEnvProxy("HTTP_PROXY", "http_proxy")
info.HTTPSProxy = getEnvProxy("HTTPS_PROXY", "https_proxy")
// 2. 从 Windows 注册表读取系统代理设置
if err := readWindowsSystemProxy(info); err != nil {
// 注册表读取失败不算致命错误,环境变量中可能仍有代理信息
// 2. 从系统设置读取代理(平台相关)
if err := readSystemProxy(info); err != nil {
fmt.Printf("读取系统代理设置时出错: %v\n", err)
}
@@ -54,42 +51,6 @@ func GetCurrentProxy() (*ProxyInfo, error) {
return info, nil
}
// readWindowsSystemProxy 从 Windows 注册表读取系统代理设置
func readWindowsSystemProxy(info *ProxyInfo) error {
key, err := registry.OpenKey(
registry.CURRENT_USER,
`Software\Microsoft\Windows\CurrentVersion\Internet Settings`,
registry.QUERY_VALUE,
)
if err != nil {
return fmt.Errorf("无法打开注册表键: %w", err)
}
defer key.Close()
// 读取代理是否启用 (ProxyEnable: 0=关闭, 1=开启)
proxyEnable, _, err := key.GetIntegerValue("ProxyEnable")
if err != nil {
return fmt.Errorf("无法读取 ProxyEnable: %w", err)
}
info.Enabled = proxyEnable == 1
// 读取代理服务器地址
proxyServer, _, err := key.GetStringValue("ProxyServer")
if err == nil && proxyServer != "" {
info.Server = proxyServer
// 根据端口推断代理协议
info.Protocol = guessProtocol(proxyServer)
}
// 读取代理绕过列表
bypass, _, err := key.GetStringValue("ProxyOverride")
if err == nil {
info.Bypass = bypass
}
return nil
}
// getEnvProxy 依次检查多个环境变量名,返回第一个非空值
func getEnvProxy(names ...string) string {
for _, name := range names {
@@ -104,11 +65,9 @@ func getEnvProxy(names ...string) string {
// 例如 "http://127.0.0.1:7890" -> "127.0.0.1:7890"
func extractServerFromURL(proxyURL string) string {
s := proxyURL
// 去掉协议前缀
if idx := strings.Index(s, "://"); idx != -1 {
s = s[idx+3:]
}
// 去掉尾部斜杠
s = strings.TrimRight(s, "/")
return s
}
@@ -127,14 +86,14 @@ func extractProtocolFromURL(proxyURL string) string {
// V2Ray 默认: 10808(socks5), 10809(http)
func guessProtocol(server string) string {
knownPorts := map[string]string{
"7890": "http", // Clash HTTP
"7891": "socks5", // Clash SOCKS5
"7892": "http", // Clash mixed
"10808": "socks5", // V2Ray SOCKS5
"10809": "http", // V2Ray HTTP
"1080": "socks5", // 通用 SOCKS5
"1081": "http", // 通用 HTTP
"8080": "http", // 通用 HTTP
"7890": "http",
"7891": "socks5",
"7892": "http",
"10808": "socks5",
"10809": "http",
"1080": "socks5",
"1081": "http",
"8080": "http",
}
parts := strings.Split(server, ":")

82
internal/proxy_darwin.go Normal file
View File

@@ -0,0 +1,82 @@
//go:build darwin
package internal
import (
"fmt"
"os/exec"
"strings"
)
// readSystemProxy 从 macOS 系统偏好设置读取代理配置
// 使用 scutil --proxy 命令获取系统代理信息
func readSystemProxy(info *ProxyInfo) error {
out, err := exec.Command("scutil", "--proxy").Output()
if err != nil {
return fmt.Errorf("执行 scutil --proxy 失败: %w", err)
}
lines := strings.Split(string(out), "\n")
settings := make(map[string]string)
for _, line := range lines {
line = strings.TrimSpace(line)
parts := strings.SplitN(line, " : ", 2)
if len(parts) == 2 {
settings[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}
// 检查 HTTP 代理
if settings["HTTPEnable"] == "1" {
host := settings["HTTPProxy"]
port := settings["HTTPPort"]
if host != "" {
info.Enabled = true
info.Server = host
if port != "" && port != "0" {
info.Server = host + ":" + port
}
info.Protocol = "http"
}
}
// 检查 HTTPS 代理
if !info.Enabled && settings["HTTPSEnable"] == "1" {
host := settings["HTTPSProxy"]
port := settings["HTTPSPort"]
if host != "" {
info.Enabled = true
info.Server = host
if port != "" && port != "0" {
info.Server = host + ":" + port
}
info.Protocol = "http"
}
}
// 检查 SOCKS 代理
if !info.Enabled && settings["SOCKSEnable"] == "1" {
host := settings["SOCKSProxy"]
port := settings["SOCKSPort"]
if host != "" {
info.Enabled = true
info.Server = host
if port != "" && port != "0" {
info.Server = host + ":" + port
}
info.Protocol = "socks5"
}
}
// 如果通过以上方式检测到了代理,根据端口再次猜测协议
if info.Enabled && info.Server != "" {
info.Protocol = guessProtocol(info.Server)
}
// 读取绕过列表
if exceptions, ok := settings["ExceptionsList"]; ok {
info.Bypass = exceptions
}
return nil
}

122
internal/proxy_linux.go Normal file
View File

@@ -0,0 +1,122 @@
//go:build linux
package internal
import (
"fmt"
"os/exec"
"strings"
)
// readSystemProxy 从 Linux 系统读取代理配置
// 优先尝试 GNOME (gsettings),失败则回退到环境变量
func readSystemProxy(info *ProxyInfo) error {
// 尝试通过 gsettings 读取 GNOME 系统代理
if err := readGnomeProxy(info); err == nil && info.Enabled {
return nil
}
// 尝试通过 KDE 配置读取代理
if err := readKDEProxy(info); err == nil && info.Enabled {
return nil
}
// Linux 上环境变量已由 GetCurrentProxy 中读取,此处无需额外操作
return nil
}
// readGnomeProxy 通过 gsettings 读取 GNOME 桌面环境的代理配置
func readGnomeProxy(info *ProxyInfo) error {
// 检查 gsettings 是否可用
if _, err := exec.LookPath("gsettings"); err != nil {
return fmt.Errorf("gsettings 不可用: %w", err)
}
// 读取代理模式
mode, err := gsettingsGet("org.gnome.system.proxy", "mode")
if err != nil {
return err
}
if mode != "'manual'" {
return nil // 代理未启用
}
// 读取 HTTP 代理
host, err := gsettingsGet("org.gnome.system.proxy.http", "host")
if err == nil && host != "" && host != "''" {
host = strings.Trim(host, "'")
port, _ := gsettingsGet("org.gnome.system.proxy.http", "port")
port = strings.TrimSpace(port)
if port != "" && port != "0" {
info.Enabled = true
info.Server = host + ":" + port
info.Protocol = guessProtocol(info.Server)
return nil
}
}
// 读取 SOCKS 代理
host, err = gsettingsGet("org.gnome.system.proxy.socks", "host")
if err == nil && host != "" && host != "''" {
host = strings.Trim(host, "'")
port, _ := gsettingsGet("org.gnome.system.proxy.socks", "port")
port = strings.TrimSpace(port)
if port != "" && port != "0" {
info.Enabled = true
info.Server = host + ":" + port
info.Protocol = "socks5"
return nil
}
}
return nil
}
// readKDEProxy 通过 kreadconfig5/kreadconfig6 读取 KDE 代理配置
func readKDEProxy(info *ProxyInfo) error {
// 尝试 kreadconfig5 或 kreadconfig6
var cmd string
if _, err := exec.LookPath("kreadconfig6"); err == nil {
cmd = "kreadconfig6"
} else if _, err := exec.LookPath("kreadconfig5"); err == nil {
cmd = "kreadconfig5"
} else {
return fmt.Errorf("kreadconfig 不可用")
}
// 读取代理类型
proxyType, err := kdeGet(cmd, "Proxy Settings", "ProxyType")
if err != nil || proxyType != "1" {
return nil // 0 = 无代理, 1 = 手动
}
// 读取 HTTP 代理
httpProxy, err := kdeGet(cmd, "Proxy Settings", "httpProxy")
if err == nil && httpProxy != "" {
info.Enabled = true
info.Server = extractServerFromURL(httpProxy)
info.Protocol = guessProtocol(info.Server)
return nil
}
return nil
}
// gsettingsGet 执行 gsettings get 命令
func gsettingsGet(schema, key string) (string, error) {
out, err := exec.Command("gsettings", "get", schema, key).Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// kdeGet 执行 kreadconfig 命令读取 KDE 配置
func kdeGet(cmd, group, key string) (string, error) {
out, err := exec.Command(cmd, "--file", "kioslaverc", "--group", group, "--key", key).Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}

43
internal/proxy_windows.go Normal file
View File

@@ -0,0 +1,43 @@
package internal
import (
"fmt"
"golang.org/x/sys/windows/registry"
)
// readSystemProxy 从 Windows 注册表读取系统代理设置
func readSystemProxy(info *ProxyInfo) error {
key, err := registry.OpenKey(
registry.CURRENT_USER,
`Software\Microsoft\Windows\CurrentVersion\Internet Settings`,
registry.QUERY_VALUE,
)
if err != nil {
return fmt.Errorf("无法打开注册表键: %w", err)
}
defer key.Close()
// 读取代理是否启用 (ProxyEnable: 0=关闭, 1=开启)
proxyEnable, _, err := key.GetIntegerValue("ProxyEnable")
if err != nil {
return fmt.Errorf("无法读取 ProxyEnable: %w", err)
}
info.Enabled = proxyEnable == 1
// 读取代理服务器地址
proxyServer, _, err := key.GetStringValue("ProxyServer")
if err == nil && proxyServer != "" {
info.Server = proxyServer
// 根据端口推断代理协议
info.Protocol = guessProtocol(proxyServer)
}
// 读取代理绕过列表
bypass, _, err := key.GetStringValue("ProxyOverride")
if err == nil {
info.Bypass = bypass
}
return nil
}