Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ebcd10a23 | ||
|
|
c787c99622 | ||
|
|
3ec453cc38 | ||
|
|
3d6874f79e | ||
|
|
d6dbd8834b | ||
|
|
beb40688ce | ||
|
|
5154bb5564 | ||
|
|
af15e86df6 | ||
|
|
6354cbfc94 | ||
|
|
cb6b21ef98 | ||
|
|
6e0becb748 |
93
CHANGELOG.md
Normal file
93
CHANGELOG.md
Normal 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 远程仓库管理
|
||||
- 所有远程操作支持指定 remote(Push/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
121
README.md
@@ -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 Code’s Git panel.
|
||||
|
||||

|
||||

|
||||
@@ -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
|
||||
|
||||
|
||||
121
README_CN.md
121
README_CN.md
@@ -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 |
|
||||
|
||||
## 📄 开源协议
|
||||
|
||||
|
||||
@@ -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: .
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
38
config.yaml
38
config.yaml
@@ -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
|
||||
|
||||
@@ -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
376
frontend/app/components/GitCheck.vue
Normal file
376
frontend/app/components/GitCheck.vue
Normal 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
@@ -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" />
|
||||
|
||||
@@ -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";
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
241
internal/app/gitsetup.go
Normal file
241
internal/app/gitsetup.go
Normal 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
|
||||
}
|
||||
8
internal/app/hidewindow_other.go
Normal file
8
internal/app/hidewindow_other.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build !windows
|
||||
|
||||
package app
|
||||
|
||||
import "os/exec"
|
||||
|
||||
// hideWindowCmd 非 Windows 平台无需处理
|
||||
func hideWindowCmd(cmd *exec.Cmd) {}
|
||||
14
internal/app/hidewindow_windows.go
Normal file
14
internal/app/hidewindow_windows.go
Normal 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
420
internal/app/migration.go
Normal 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
|
||||
}
|
||||
596
internal/app/online_migrate.go
Normal file
596
internal/app/online_migrate.go
Normal 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("正在迁移 %s(Gitea 原生迁移)...", 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
1126
internal/app/platform.go
Normal file
File diff suppressed because it is too large
Load Diff
1244
internal/app/platform_extra.go
Normal file
1244
internal/app/platform_extra.go
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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
82
internal/proxy_darwin.go
Normal 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
122
internal/proxy_linux.go
Normal 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
43
internal/proxy_windows.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user