3 Commits

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

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

🔨 改进:
- 源平台开启代理时自动跳过 Gitea 原生迁移,改用本地代理中转
- Push 超时从 30s 提升到 10 分钟
- 设置 http.postBuffer=500MB,push --all 失败自动逐分支推送
- 新增 RunWithProxyTimeout 方法
2026-03-12 15:07:41 +08:00
zyj
c787c99622 feat: v0.4.0 — 配置架构重构 + 一键多端推送 + 项目级代理控制\n\n架构重构:\n- 配置从 Platform→User→Project 重构为扁平的 Projects+Groups+Credentials 模型\n- 侧边栏完全重写,适配新分组→项目树结构\n- 新增分组(Group)管理和凭据(Credential)管理\n\n新功能:\n- 一键多端推送:Push All 按钮,将当前分支/标签推送到所有远程仓库\n- 项目级代理控制:每个项目独立设置代理(跟随全局/强制开启/强制关闭)\n- GitClient 新增 RunWithProxy 方法,支持按调用级别覆盖全局代理\n- 所有网络操作(Pull/Push/Fetch)均支持项目级代理设置\n\n其他:\n- 新增 CHANGELOG.md 版本更新日志\n- 更新 README/README_CN 文档,反映最新 72+ API 方法\n- 版本号升级到 0.4.0(config.yml/info.json/Info.plist)
Some checks failed
Release / build-windows (push) Has been cancelled
2026-03-12 11:41:26 +08:00
zyj
3ec453cc38 feat: 多 remote 远程仓库支持 (v0.3.1)\n\n后端:\n- 新增 RemoteList/AddRemote/RemoveRemote 远程仓库管理\n- 所有远程操作(Push/Pull/Fetch/PushTag/DeleteTag/DeleteRemoteBranch等)支持指定 remote\n- 新增 PushTo/PullFrom/FetchRemote/PushTagTo/DeleteRemoteTagFrom 等方法\n- BatchPull/BatchPush 支持 remote 参数\n\n前端:\n- 顶部信息栏新增 remote 选择器下拉菜单\n- 支持切换当前操作的远程仓库\n- 支持添加/删除远程仓库\n- Pull/Push/Fetch/标签推送/远程分支删除等操作均使用选中的 remote
Some checks failed
Release / build-windows (push) Has been cancelled
2026-03-12 10:57:27 +08:00
22 changed files with 6982 additions and 790 deletions

93
CHANGELOG.md Normal file
View File

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

121
README.md
View File

@@ -4,7 +4,7 @@
A cross-platform desktop Git repository management tool built with **Go + Wails v3 + Nuxt 4 + Vue 3**. A cross-platform desktop Git repository management tool built with **Go + Wails v3 + Nuxt 4 + Vue 3**.
Manage multiple Git repositories across different platforms (GitHub, Gitee, Gitea, etc.) from a single unified interface — like a lightweight, standalone version of VS Code's Git panel. Manage multiple Git repositories across different platforms (GitHub, Gitee, Gitea, etc.) from a single unified interface — like a lightweight, standalone version of VS Codes Git panel.
![Go](https://img.shields.io/badge/Go-1.25-00ADD8?logo=go&logoColor=white) ![Go](https://img.shields.io/badge/Go-1.25-00ADD8?logo=go&logoColor=white)
![Wails](https://img.shields.io/badge/Wails-v3-red?logo=go&logoColor=white) ![Wails](https://img.shields.io/badge/Wails-v3-red?logo=go&logoColor=white)
@@ -14,10 +14,12 @@ Manage multiple Git repositories across different platforms (GitHub, Gitee, Gite
## ✨ Features ## ✨ Features
### Multi-Platform Management ### Project & Group Management
- Manage repositories from **GitHub**, **Gitee**, **Gitea**, and custom Git platforms - Manage repositories from **GitHub**, **Gitee**, **Gitea**, and custom Git platforms
- Organize projects by **Platform → User → Project** hierarchy - Organize projects into custom **Groups** (e.g. `github/user`, `gitea`, `work`)
- Add / remove platforms, users, and projects through the GUI - 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 ### VS Code-Style Git Operations
- **View changed files** with a collapsible directory tree structure - **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 - **Diff viewer** with syntax-highlighted additions and deletions
- **Binary file detection** — skips preview for images, executables, etc. - **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 ### Commit History
- Browse commit history with author, time, and message - 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** - Select a file to view its **per-file diff**
- **Version rollback** — hard / mixed / soft reset with confirmation dialog - **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 ### 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 - **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 ### Desktop Experience
- **System tray** — close window hides to tray, click tray icon to restore - **System tray** — close window hides to tray, click tray icon to restore
- **Resizable panels** — drag splitters between sidebar, file list, and viewer - **Resizable panels** — drag splitters between sidebar, file list, and viewer
- **Dark theme** — Catppuccin Mocha color scheme with Ant Design Vue - **Dark theme** — Catppuccin Mocha color scheme with Ant Design Vue
- **Native folder picker** — select project paths via OS file dialog - **Native folder picker** — select project paths via OS file dialog
- Git global config editor (user.name / user.email)
## 🏗️ Tech Stack ## 🏗️ Tech Stack
| Layer | Technology | | Layer | Technology |
|-------|-----------| |-------|-----------|
| Backend | Go 1.25, Wails v3 (alpha.74) | | Backend | Go 1.25, Wails v3 |
| Frontend | Nuxt 4, Vue 3.5, Vite 7 | | Frontend | Nuxt 4, Vue 3.5, Vite 7 |
| UI Library | Ant Design Vue 4, @ant-design/icons-vue | | 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 | | Config | YAML-based persistent configuration |
| Build | Taskfile v3 | | Build | Taskfile v3, NSIS installer (Windows) |
## 📁 Project Structure ## 📁 Project Structure
``` ```
GitPilot/ GitPilot/
├── main.go # Wails app entry, system tray, window management ├── cmd/main.go # Wails app entry, system tray, window management
├── config.yaml # Platform/user/project configuration ├── config.yaml # Projects / Groups / Credentials configuration
├── CHANGELOG.md # Version history
├── Taskfile.yml # Build tasks ├── Taskfile.yml # Build tasks
├── go.mod ├── go.mod
├── cmd/ # CLI utilities
├── config/ ├── config/
│ └── config.go # YAML config loader/saver │ └── config.go # YAML config types & loader/saver
├── internal/ ├── internal/
│ ├── proxy.go # System proxy detection │ ├── proxy.go # System proxy detection (Windows/macOS/Linux)
│ ├── git/ │ ├── git/
│ │ └── client.go # Git CLI wrapper (34 operations) │ │ └── client.go # Git CLI wrapper with RunWithProxy support
│ └── app/ │ └── 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/ ├── frontend/
│ ├── nuxt.config.ts │ ├── nuxt.config.ts
│ ├── package.json │ ├── package.json
@@ -81,10 +128,12 @@ GitPilot/
│ ├── layouts/ │ ├── layouts/
│ │ └── default.vue # Ant Design dark theme provider │ │ └── default.vue # Ant Design dark theme provider
│ └── components/ │ └── components/
│ ├── Sidebar.vue # Platform/user/project tree sidebar │ ├── Sidebar.vue # Group/project tree, batch ops, credentials
│ ├── ContentArea.vue # Git status, diff, history, branches │ ├── ContentArea.vue # Git status, diff, history, branches, tags
── FileTreeNode.vue # Recursive file tree with actions ── FileTreeNode.vue # Recursive file tree with stage/discard
└── build/ # Wails build configuration │ ├── CommitFileTreeNode.vue # Commit file tree viewer
│ └── GitCheck.vue # Git installation check screen
└── build/ # Wails build config, NSIS installer scripts
``` ```
## 🚀 Getting Started ## 🚀 Getting Started
@@ -108,14 +157,9 @@ cd GitPilot
cd frontend && npm install && cd .. cd frontend && npm install && cd ..
# Generate Wails bindings # Generate Wails bindings
wails3 generate bindings wails3 generate bindings -d frontend/bindings
# Build frontend
cd frontend && npx nuxi generate && cd ..
# Run in development mode # Run in development mode
task dev
# or
wails3 dev -config ./build/config.yml -port 9245 wails3 dev -config ./build/config.yml -port 9245
``` ```
@@ -129,19 +173,28 @@ The binary will be output to the `bin/` directory.
## 📋 API Overview ## 📋 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 | | Category | Methods |
|----------|---------| |----------|---------|
| **Projects** | GetProjectTree, AddProject, RemoveProject, SelectDirectory | | **Project Management** | GetProjectTree, AddProject, RemoveProject, CloneProject, MoveProjectToGroup, SelectDirectory |
| **Platforms** | AddPlatform, UpdatePlatform, RemovePlatform, GetPlatformInfo | | **Group Management** | GetGroups, AddGroup, UpdateGroup, RemoveGroup |
| **Users** | AddUser, UpdateUser, RemoveUser, GetUserInfo | | **Credential Management** | GetCredentials, AddCredential, UpdateCredential, RemoveCredential |
| **Git Status** | GetProjectStatus, GetProjectChangedFiles, GetFileContent, GetFileDiff, GetFileDiffStaged | | **Git Status** | GetProjectStatus, GetProjectChangedFiles, GetFileContent, GetFileDiff, GetFileDiffStaged |
| **Staging** | StageFiles, UnstageFiles, StageAll, UnstageAll | | **Staging** | StageFiles, UnstageFiles, StageAll, UnstageAll |
| **Commit** | CommitChanges, DiscardFiles | | **Commit** | CommitChanges, DiscardFiles |
| **Remote** | PullProject, PushProject, FetchProject | | **Remote Operations** | PullProject, PushProject, FetchProject, PushToAllRemotes, PushTagToAllRemotes |
| **History** | GetCommitLog, GetCommitDiff, GetCommitFiles, GetCommitFileDiff | | **Remote Management** | GetRemotes, AddRemote, RemoveRemote |
| **Branches** | GetBranches, SwitchBranch, ResetProject | | **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 ## 📄 License

View File

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

View File

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

View File

@@ -10,11 +10,11 @@
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.gitpilot.app</string> <string>com.gitpilot.app</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.3.0</string> <string>0.4.1</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>Git Repository Management Tool</string> <string>Git Repository Management Tool</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.3.0</string> <string>0.4.1</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>icons</string> <string>icons</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

@@ -18,9 +18,9 @@ tasks:
cmds: cmds:
- task: generate:syso - task: generate:syso
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe - 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] 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] platforms: [linux, darwin]
- cmd: powershell Remove-item *.syso - cmd: powershell Remove-item *.syso
platforms: [windows] platforms: [windows]

View File

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

View File

@@ -1,22 +1,22 @@
platforms:
gitea:
base_url: http://192.168.1.10:3000
users: []
gitee:
base_url: ""
users:
- username: zhuyuj
token: ""
projects: []
github:
base_url: ""
users:
- username: zhuy1228
token: ""
projects: projects:
- name: GitPilot - name: GitPilot
path: F:\Project\GitPilot path: F:\Project\GitPilot
enabled: false 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: settings:
concurrency: 6 concurrency: 6
network_check: true network_check: true

View File

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

View File

@@ -85,11 +85,31 @@ const newTagName = ref('')
const newTagMessage = ref('') const newTagMessage = ref('')
const createTagLoading = ref(false) const createTagLoading = ref(false)
// ---- Release 同步 ----
const releases = ref([])
const releasesLoading = ref(false)
const selectedRelease = ref(null)
const releaseSyncSource = ref('')
const releaseSyncTarget = ref('')
const releaseSyncLoading = ref(false)
const releaseSyncResults = ref([])
const releaseSrcToken = ref('')
const releaseTgtToken = ref('')
// ---- 分支管理 ---- // ---- 分支管理 ----
const newBranchName = ref('') const newBranchName = ref('')
const createBranchLoading = ref(false) const createBranchLoading = ref(false)
const remoteBranches = ref([]) const remoteBranches = ref([])
// ---- Remote 远程仓库 ----
const remotes = ref([])
const currentRemote = ref('origin')
const showRemoteDropdown = ref(false)
const showAddRemote = ref(false)
const newRemoteName = ref('')
const newRemoteUrl = ref('')
const addRemoteLoading = ref(false)
// ---- Stash 贮藏管理 ---- // ---- Stash 贮藏管理 ----
const stashList = ref([]) const stashList = ref([])
const stashLoading = ref(false) const stashLoading = ref(false)
@@ -103,6 +123,9 @@ const showSettings = ref(false)
const settingsLoading = ref(false) const settingsLoading = ref(false)
const gitConfig = ref({ userName: '', userEmail: '' }) const gitConfig = ref({ userName: '', userEmail: '' })
// ---- 代理开关 ----
const projectProxy = ref(null) // null: 跟随全局, true: 强制使用, false: 强制不使用
// ---- 冲突处理 ---- // ---- 冲突处理 ----
const isMerging = ref(false) const isMerging = ref(false)
const conflictFiles = ref([]) const conflictFiles = ref([])
@@ -248,8 +271,17 @@ async function loadStatus() {
status.value = { status.value = {
branch: result.branch || '', branch: result.branch || '',
remoteUrl: result.remoteUrl || result.remoteURL || '', remoteUrl: result.remoteUrl || result.remoteURL || '',
remotes: Array.isArray(result.remotes) ? result.remotes : [],
currentRemote: result.currentRemote || 'origin',
changedFiles: [], changedFiles: [],
} }
// 同步 remotes
remotes.value = status.value.remotes
if (remotes.value.length && !remotes.value.find(r => r.name === currentRemote.value)) {
currentRemote.value = remotes.value[0].name
}
// 同步代理设置
projectProxy.value = result.useProxy ?? null
} else { } else {
errorMsg.value = '获取项目状态返回空值: ' + JSON.stringify(result) errorMsg.value = '获取项目状态返回空值: ' + JSON.stringify(result)
loadingBase.value = false loadingBase.value = false
@@ -512,11 +544,11 @@ async function gitAction(action) {
actionLoading.value = action actionLoading.value = action
try { try {
if (action === 'pull') { if (action === 'pull') {
await AppService.PullProject(props.project.path) await AppService.PullProject(props.project.path, currentRemote.value)
} else if (action === 'push') { } else if (action === 'push') {
await AppService.PushProject(props.project.path) await AppService.PushProject(props.project.path, currentRemote.value)
} else { } else {
await AppService.FetchProject(props.project.path) await AppService.FetchProject(props.project.path, currentRemote.value)
} }
await loadStatus() await loadStatus()
} catch (e) { } catch (e) {
@@ -526,6 +558,85 @@ async function gitAction(action) {
} }
} }
// ---- 多端推送 ----
async function pushToAllRemotes() {
if (!props.project?.path) return
actionLoading.value = 'pushAll'
try {
const results = await AppService.PushToAllRemotes(props.project.path)
const successCount = results.filter(r => r.success).length
const failCount = results.length - successCount
if (failCount === 0) {
message.success(`已成功推送到全部 ${successCount} 个远程仓库`)
} else {
Modal.info({
title: '多端推送结果',
content: h('div', results.map(r =>
h('div', { style: `padding: 4px 0; color: ${r.success ? 'var(--success, #a6e3a1)' : '#f38ba8'}` }, [
h('span', { style: 'font-weight: 600; margin-right: 8px;' }, r.remote + ':'),
h('span', r.success ? '✓ 成功' : '✗ ' + r.message),
])
)),
})
}
await loadStatus()
} catch (e) {
console.error('多端推送失败:', e)
Modal.error({ title: '多端推送失败', content: String(e) })
} finally {
actionLoading.value = ''
}
}
async function pushTagToAllRemotes(tag) {
if (!props.project?.path) return
try {
const results = await AppService.PushTagToAllRemotes(props.project.path, tag.name)
const successCount = results.filter(r => r.success).length
const failCount = results.length - successCount
if (failCount === 0) {
message.success(`标签 ${tag.name} 已推送到全部 ${successCount} 个远程仓库`)
} else {
Modal.info({
title: '标签多端推送结果',
content: h('div', results.map(r =>
h('div', { style: `padding: 4px 0; color: ${r.success ? 'var(--success, #a6e3a1)' : '#f38ba8'}` }, [
h('span', { style: 'font-weight: 600; margin-right: 8px;' }, r.remote + ':'),
h('span', r.success ? '✓ 成功' : '✗ ' + r.message),
])
)),
})
}
} catch (e) {
console.error('标签多端推送失败:', e)
Modal.error({ title: '推送失败', content: String(e) })
}
}
// ---- 代理开关 ----
async function toggleProxy(checked) {
if (!props.project?.path) return
const useProxy = checked ? true : false
try {
await AppService.SetProjectProxy(props.project.path, useProxy)
projectProxy.value = useProxy
} catch (e) {
console.error('设置代理失败:', e)
Modal.error({ title: '设置代理失败', content: String(e) })
}
}
async function resetProxy() {
if (!props.project?.path) return
try {
await AppService.SetProjectProxy(props.project.path, null)
projectProxy.value = null
message.success('已恢复跟随全局设置')
} catch (e) {
console.error('重置代理失败:', e)
}
}
function getStatusColor(status) { function getStatusColor(status) {
const colors = { 'M': 'orange', 'A': 'green', 'D': 'red', '?': 'default' } const colors = { 'M': 'orange', 'A': 'green', 'D': 'red', '?': 'default' }
return colors[status] || 'default' return colors[status] || 'default'
@@ -675,7 +786,7 @@ async function mergeBranch(branchName) {
async function loadRemoteBranches() { async function loadRemoteBranches() {
if (!props.project?.path) return if (!props.project?.path) return
try { try {
const list = await AppService.GetRemoteBranches(props.project.path) const list = await AppService.GetRemoteBranches(props.project.path, currentRemote.value)
remoteBranches.value = Array.isArray(list) ? list : [] remoteBranches.value = Array.isArray(list) ? list : []
} catch (e) { } catch (e) {
console.error('获取远程分支失败:', e) console.error('获取远程分支失败:', e)
@@ -709,7 +820,7 @@ async function deleteRemoteBranch(remoteBranch) {
okButtonProps: { danger: true }, okButtonProps: { danger: true },
async onOk() { async onOk() {
try { try {
await AppService.DeleteRemoteBranch(props.project.path, remoteBranch) await AppService.DeleteRemoteBranch(props.project.path, remoteBranch, currentRemote.value)
await loadRemoteBranches() await loadRemoteBranches()
message.success(`远程分支 ${remoteBranch} 已删除`) message.success(`远程分支 ${remoteBranch} 已删除`)
} catch (e) { } catch (e) {
@@ -1130,7 +1241,7 @@ async function deleteTag(tag) {
okButtonProps: { danger: true }, okButtonProps: { danger: true },
async onOk() { async onOk() {
try { try {
await AppService.DeleteTag(props.project.path, tag.name) await AppService.DeleteTag(props.project.path, tag.name, currentRemote.value)
await loadTags() await loadTags()
} catch (e) { } catch (e) {
console.error('删除标签失败:', e) console.error('删除标签失败:', e)
@@ -1143,7 +1254,7 @@ async function deleteTag(tag) {
async function pushTag(tag) { async function pushTag(tag) {
if (!props.project?.path) return if (!props.project?.path) return
try { try {
await AppService.PushTag(props.project.path, tag.name) await AppService.PushTag(props.project.path, tag.name, currentRemote.value)
Modal.success({ title: '推送成功', content: `标签 ${tag.name} 已推送到远程` }) Modal.success({ title: '推送成功', content: `标签 ${tag.name} 已推送到远程` })
} catch (e) { } catch (e) {
console.error('推送标签失败:', e) console.error('推送标签失败:', e)
@@ -1156,6 +1267,112 @@ watch(activeTab, (tab) => {
if (tab === 'tags' && !tags.value.length && !tagsLoading.value) { if (tab === 'tags' && !tags.value.length && !tagsLoading.value) {
loadTags() loadTags()
} }
if (tab === 'releases' && !releases.value.length && releaseSyncSource.value) {
loadReleases()
}
})
// ---- Release 同步 ----
async function loadReleases() {
if (!props.project?.path || !releaseSyncSource.value) return
releasesLoading.value = true
selectedRelease.value = null
try {
const list = await AppService.GetRemoteReleases(props.project.path, releaseSyncSource.value, releaseSrcToken.value)
releases.value = Array.isArray(list) ? list : []
} catch (e) {
console.error('获取 Release 失败:', e)
releases.value = []
message.error('获取 Release 失败: ' + String(e))
} finally {
releasesLoading.value = false
}
}
async function doSyncReleases() {
if (!props.project?.path || !releaseSyncSource.value || !releaseSyncTarget.value) return
releaseSyncLoading.value = true
releaseSyncResults.value = []
try {
const results = await AppService.SyncReleases(props.project.path, releaseSyncSource.value, releaseSyncTarget.value, releaseSrcToken.value, releaseTgtToken.value)
releaseSyncResults.value = Array.isArray(results) ? results : []
const successCount = releaseSyncResults.value.filter(r => r.success).length
message.info(`Release 同步完成:${successCount}/${releaseSyncResults.value.length}`)
} catch (e) {
console.error('Release 同步失败:', e)
Modal.error({ title: 'Release 同步失败', content: String(e) })
} finally {
releaseSyncLoading.value = false
}
}
function formatFileSize(bytes) {
if (!bytes || bytes <= 0) return ''
const units = ['B', 'KB', 'MB', 'GB']
let i = 0
let size = bytes
while (size >= 1024 && i < units.length - 1) {
size /= 1024
i++
}
return size.toFixed(i === 0 ? 0 : 1) + ' ' + units[i]
}
// ---- Remote 远程仓库管理 ----
async function switchRemote(remoteName) {
currentRemote.value = remoteName
showRemoteDropdown.value = false
// 切换 remote 后刷新远程分支
await loadRemoteBranches()
}
async function addRemote() {
if (!props.project?.path || !newRemoteName.value.trim() || !newRemoteUrl.value.trim()) return
addRemoteLoading.value = true
try {
await AppService.AddRemote(props.project.path, newRemoteName.value.trim(), newRemoteUrl.value.trim())
newRemoteName.value = ''
newRemoteUrl.value = ''
showAddRemote.value = false
message.success('远程仓库添加成功')
await loadStatus()
} catch (e) {
Modal.error({ title: '添加远程仓库失败', content: String(e) })
} finally {
addRemoteLoading.value = false
}
}
async function removeRemote(remoteName) {
if (!props.project?.path) return
Modal.confirm({
title: '确认删除远程仓库',
icon: h(ExclamationCircleOutlined),
content: h('div', [
h('p', '确定要删除远程仓库吗?'),
h('p', { style: 'font-family: monospace; color: #f38ba8; font-size: 15px;' }, remoteName),
]),
okText: '删除',
cancelText: '取消',
okButtonProps: { danger: true },
async onOk() {
try {
await AppService.RemoveRemote(props.project.path, remoteName)
message.success(`远程仓库 ${remoteName} 已删除`)
if (currentRemote.value === remoteName) {
currentRemote.value = 'origin'
}
await loadStatus()
} catch (e) {
Modal.error({ title: '删除远程仓库失败', content: String(e) })
}
},
})
}
const currentRemoteUrl = computed(() => {
const r = remotes.value.find(r => r.name === currentRemote.value)
return r ? r.url : status.value?.remoteUrl || ''
}) })
</script> </script>
@@ -1271,10 +1488,85 @@ watch(activeTab, (tab) => {
</div> </div>
</template> </template>
</a-dropdown> </a-dropdown>
<span v-if="status.remoteUrl" class="remote-url" :title="status.remoteUrl">{{ status.remoteUrl }}</span> <span v-if="status.remoteUrl" class="remote-url" :title="currentRemoteUrl">
<a-dropdown :open="showRemoteDropdown" @openChange="v => showRemoteDropdown = v" :trigger="['click']">
<span class="remote-selector" @click.prevent="showRemoteDropdown = !showRemoteDropdown">
<GlobalOutlined style="margin-right: 4px;" />
{{ currentRemote }}
<CaretDownOutlined style="font-size: 10px; margin-left: 2px;" />
</span>
<template #overlay>
<div class="remote-dropdown">
<div class="branch-dropdown-title">远程仓库</div>
<div class="branch-dropdown-list">
<div
v-for="r in remotes"
:key="r.name"
class="branch-dropdown-item"
:class="{ active: r.name === currentRemote }"
>
<div class="branch-item-main" @click="switchRemote(r.name)">
<GlobalOutlined style="font-size: 12px; margin-right: 6px;" />
<span class="branch-item-name">{{ r.name }}</span>
<span v-if="r.name === currentRemote" style="margin-left: auto; color: var(--success, #a6e3a1);"></span>
</div>
<div class="branch-item-actions" @click.stop>
<a-tooltip :title="r.url">
<span class="branch-action-btn" style="cursor: default; opacity: 0.6; font-size: 10px;">URL</span>
</a-tooltip>
<a-tooltip title="删除远程仓库" v-if="r.name !== 'origin'">
<span class="branch-action-btn danger" @click="removeRemote(r.name)"><DeleteOutlined /></span>
</a-tooltip>
</div>
</div>
<div v-if="!remotes.length" style="padding: 12px; text-align: center; color: var(--text-muted);">
无远程仓库
</div>
</div>
<!-- 添加远程仓库 -->
<div v-if="!showAddRemote" class="remote-add-btn" @click="showAddRemote = true">
<PlusOutlined /> 添加远程仓库
</div>
<div v-else class="remote-add-form">
<a-input v-model:value="newRemoteName" placeholder="名称 (如 upstream)" size="small" style="margin-bottom: 4px;" />
<a-input v-model:value="newRemoteUrl" placeholder="仓库地址" size="small" style="margin-bottom: 4px;" />
<div style="display: flex; gap: 4px;">
<a-button size="small" type="primary" :loading="addRemoteLoading" :disabled="!newRemoteName.trim() || !newRemoteUrl.trim()" @click="addRemote" style="flex: 1;">添加</a-button>
<a-button size="small" @click="showAddRemote = false; newRemoteName = ''; newRemoteUrl = ''">取消</a-button>
</div>
</div>
</div>
</template>
</a-dropdown>
<span class="remote-url-text" :title="currentRemoteUrl">{{ currentRemoteUrl }}</span>
</span>
</template> </template>
</div> </div>
<a-space class="project-actions"> <a-space class="project-actions">
<a-tooltip :title="projectProxy === null ? '代理: 跟随全局' : projectProxy ? '代理: 已开启' : '代理: 已关闭'">
<a-dropdown :trigger="['click']">
<a-button size="small" :type="projectProxy === true ? 'primary' : 'default'" :ghost="projectProxy === true">
<template #icon><GlobalOutlined /></template>
</a-button>
<template #overlay>
<div class="proxy-dropdown">
<div class="proxy-dropdown-title">代理设置</div>
<div class="proxy-dropdown-item" :class="{ active: projectProxy === null }" @click="resetProxy()">
<span>跟随全局</span>
<span v-if="projectProxy === null" style="color: var(--success, #a6e3a1);"></span>
</div>
<div class="proxy-dropdown-item" :class="{ active: projectProxy === true }" @click="toggleProxy(true)">
<span>强制使用代理</span>
<span v-if="projectProxy === true" style="color: var(--success, #a6e3a1);"></span>
</div>
<div class="proxy-dropdown-item" :class="{ active: projectProxy === false }" @click="toggleProxy(false)">
<span>强制不使用代理</span>
<span v-if="projectProxy === false" style="color: var(--success, #a6e3a1);"></span>
</div>
</div>
</template>
</a-dropdown>
</a-tooltip>
<a-button size="small" :loading="actionLoading === 'fetch'" :disabled="loadingBase" @click="gitAction('fetch')"> <a-button size="small" :loading="actionLoading === 'fetch'" :disabled="loadingBase" @click="gitAction('fetch')">
<template #icon><SyncOutlined /></template> <template #icon><SyncOutlined /></template>
Fetch Fetch
@@ -1287,6 +1579,12 @@ watch(activeTab, (tab) => {
<template #icon><CloudUploadOutlined /></template> <template #icon><CloudUploadOutlined /></template>
Push Push
</a-button> </a-button>
<a-tooltip title="推送到所有远程仓库">
<a-button size="small" :loading="actionLoading === 'pushAll'" :disabled="loadingBase || remotes.length < 2" @click="pushToAllRemotes">
<template #icon><SendOutlined /></template>
Push All
</a-button>
</a-tooltip>
<a-button size="small" :disabled="loadingBase" @click="loadStatus"> <a-button size="small" :disabled="loadingBase" @click="loadStatus">
<template #icon><ReloadOutlined /></template> <template #icon><ReloadOutlined /></template>
</a-button> </a-button>
@@ -1311,6 +1609,9 @@ watch(activeTab, (tab) => {
<div class="file-list-tab" :class="{ active: activeTab === 'tags' }" @click="activeTab = 'tags'"> <div class="file-list-tab" :class="{ active: activeTab === 'tags' }" @click="activeTab = 'tags'">
<TagsOutlined /> 标签 <TagsOutlined /> 标签
</div> </div>
<div class="file-list-tab" :class="{ active: activeTab === 'releases' }" @click="activeTab = 'releases'">
<DownloadOutlined /> 发布
</div>
</div> </div>
<!-- ===== 变更面板 ===== --> <!-- ===== 变更面板 ===== -->
@@ -1689,11 +1990,16 @@ watch(activeTab, (tab) => {
<TagOutlined class="tag-icon" /> <TagOutlined class="tag-icon" />
<span class="tag-name">{{ tag.name }}</span> <span class="tag-name">{{ tag.name }}</span>
<div class="tag-action-btns" @click.stop> <div class="tag-action-btns" @click.stop>
<a-tooltip title="推送到远程"> <a-tooltip title="推送到当前远程">
<a-button type="text" size="small" class="tag-action-btn push" @click="pushTag(tag)"> <a-button type="text" size="small" class="tag-action-btn push" @click="pushTag(tag)">
<template #icon><SendOutlined /></template> <template #icon><SendOutlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip title="推送到所有远程" v-if="remotes.length > 1">
<a-button type="text" size="small" class="tag-action-btn push" @click="pushTagToAllRemotes(tag)">
<template #icon><CloudUploadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip title="删除标签"> <a-tooltip title="删除标签">
<a-button type="text" size="small" class="tag-action-btn delete" @click="deleteTag(tag)"> <a-button type="text" size="small" class="tag-action-btn delete" @click="deleteTag(tag)">
<template #icon><DeleteOutlined /></template> <template #icon><DeleteOutlined /></template>
@@ -1719,6 +2025,94 @@ watch(activeTab, (tab) => {
</div> </div>
</div> </div>
</template> </template>
<!-- ===== 发布同步面板 ===== -->
<template v-else-if="activeTab === 'releases'">
<div class="file-list-content tags-panel">
<!-- /目标 remote 选择 -->
<div style="padding: 8px 10px; border-bottom: 1px solid var(--border-color);">
<div style="display: flex; gap: 6px; align-items: center; margin-bottom: 6px;">
<span style="font-size: 11px; color: var(--text-muted); width: 30px; flex-shrink: 0;"></span>
<a-select v-model:value="releaseSyncSource" size="small" style="flex: 1;" placeholder="源 remote">
<a-select-option v-for="r in remotes" :key="r.name" :value="r.name">{{ r.name }}</a-select-option>
</a-select>
<a-button size="small" :loading="releasesLoading" @click="loadReleases" :disabled="!releaseSyncSource">
<template #icon><ReloadOutlined /></template>
</a-button>
</div>
<div style="display: flex; gap: 6px; align-items: center; margin-bottom: 6px;">
<span style="font-size: 11px; color: var(--text-muted); width: 30px; flex-shrink: 0;">目标</span>
<a-select v-model:value="releaseSyncTarget" size="small" style="flex: 1;" placeholder="目标 remote">
<a-select-option v-for="r in remotes" :key="r.name" :value="r.name" :disabled="r.name === releaseSyncSource">{{ r.name }}</a-select-option>
</a-select>
<a-button
size="small"
type="primary"
:loading="releaseSyncLoading"
:disabled="!releaseSyncSource || !releaseSyncTarget || releaseSyncSource === releaseSyncTarget"
@click="doSyncReleases"
>
<template #icon><SyncOutlined /></template>
同步
</a-button>
</div>
<div style="display: flex; gap: 6px; align-items: center; margin-bottom: 6px;">
<span style="font-size: 11px; color: var(--text-muted); width: 30px; flex-shrink: 0;">认证</span>
<a-input-password v-model:value="releaseSrcToken" size="small" style="flex: 1;" placeholder="源 Token可选" />
<a-input-password v-model:value="releaseTgtToken" size="small" style="flex: 1;" placeholder="目标 Token可选" />
</div>
</div>
<!-- Release 列表 -->
<div class="tag-list-section">
<template v-if="releasesLoading">
<div v-for="i in 4" :key="i" style="padding: 10px 12px;">
<div style="height: 14px; background: var(--bg-hover, #333); border-radius: 4px; animation: pulse 1.5s infinite;" :style="{ width: (40 + i * 8) + '%' }"></div>
</div>
</template>
<div v-else-if="!releases.length && releaseSyncSource" style="padding: 24px; text-align: center; color: var(--text-muted);">
<DownloadOutlined :style="{ fontSize: '28px', marginBottom: '8px' }" />
<div>暂无 Release</div>
</div>
<div v-else-if="!releaseSyncSource" style="padding: 24px; text-align: center; color: var(--text-muted);">
<DownloadOutlined :style="{ fontSize: '28px', marginBottom: '8px' }" />
<div>选择源 remote 以查看 Release</div>
</div>
<template v-else>
<div
v-for="rel in releases"
:key="rel.tagName"
class="tag-item"
:class="{ active: selectedRelease?.tagName === rel.tagName }"
@click="selectedRelease = rel"
>
<div class="tag-main-row">
<TagOutlined class="tag-icon" />
<span class="tag-name">{{ rel.name || rel.tagName }}</span>
<a-tag v-if="rel.draft" size="small" color="orange">草稿</a-tag>
<a-tag v-if="rel.prerelease" size="small" color="blue">预发布</a-tag>
<a-badge v-if="rel.assets?.length" :count="rel.assets.length" :number-style="{ backgroundColor: 'var(--accent)', fontSize: '10px', height: '16px', lineHeight: '16px', minWidth: '16px', padding: '0 4px' }" />
</div>
<div class="tag-meta">
<span class="tag-hash">{{ rel.tagName }}</span>
<span v-if="rel.publishedAt || rel.createdAt" class="tag-time">{{ formatTime(rel.publishedAt || rel.createdAt) }}</span>
</div>
</div>
</template>
</div>
<!-- 同步结果 -->
<div v-if="releaseSyncResults.length" style="padding: 8px 10px; border-top: 1px solid var(--border-color);">
<div style="font-size: 11px; color: var(--text-muted); margin-bottom: 4px;">同步结果</div>
<div v-for="r in releaseSyncResults" :key="r.tagName" style="display: flex; align-items: center; gap: 4px; padding: 2px 0; font-size: 11px;">
<CheckCircleOutlined v-if="r.success" style="color: #a6e3a1; flex-shrink: 0;" />
<CloseCircleOutlined v-else style="color: #f38ba8; flex-shrink: 0;" />
<span style="font-weight: 500;">{{ r.tagName }}</span>
<span style="color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ r.message }}</span>
</div>
</div>
</div>
</template>
</div> </div>
<!-- 拖拽分隔条 --> <!-- 拖拽分隔条 -->
@@ -1807,6 +2201,42 @@ watch(activeTab, (tab) => {
<span style="color: var(--text-muted); font-size: 12px; margin-top: 4px;"> {{ tags.length }} 个标签</span> <span style="color: var(--text-muted); font-size: 12px; margin-top: 4px;"> {{ tags.length }} 个标签</span>
</div> </div>
</template> </template>
<!-- Release 详情模式 -->
<template v-else-if="activeTab === 'releases'">
<div v-if="selectedRelease" class="release-detail-panel">
<div class="release-detail-header">
<h3 style="margin: 0; font-size: 16px;">{{ selectedRelease.name || selectedRelease.tagName }}</h3>
<div style="display: flex; gap: 6px; margin-top: 4px;">
<a-tag color="green">{{ selectedRelease.tagName }}</a-tag>
<a-tag v-if="selectedRelease.draft" color="orange">草稿</a-tag>
<a-tag v-if="selectedRelease.prerelease" color="blue">预发布</a-tag>
<span v-if="selectedRelease.publishedAt || selectedRelease.createdAt" style="color: var(--text-muted); font-size: 12px; line-height: 22px;">
{{ formatTime(selectedRelease.publishedAt || selectedRelease.createdAt) }}
</span>
</div>
</div>
<div v-if="selectedRelease.body" class="release-detail-body">
<pre style="white-space: pre-wrap; font-size: 13px; line-height: 1.6; margin: 0; font-family: inherit;">{{ selectedRelease.body }}</pre>
</div>
<div v-if="selectedRelease.assets?.length" class="release-detail-assets">
<div style="font-weight: 500; font-size: 13px; margin-bottom: 8px;">附件 ({{ selectedRelease.assets.length }})</div>
<div
v-for="asset in selectedRelease.assets"
:key="asset.name"
class="release-asset-item"
>
<DownloadOutlined style="color: var(--accent); flex-shrink: 0;" />
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ asset.name }}</span>
<span style="color: var(--text-muted); font-size: 11px; flex-shrink: 0;">{{ formatFileSize(asset.size) }}</span>
</div>
</div>
</div>
<div v-else class="empty-state small">
<DownloadOutlined :style="{ fontSize: '48px', color: 'var(--text-muted)', marginBottom: '8px' }" />
<span style="color: var(--text-muted)">选择一个 Release 查看详情</span>
<span style="color: var(--text-muted); font-size: 12px; margin-top: 4px;"> {{ releases.length }} Release</span>
</div>
</template>
</div> </div>
</div> </div>
</template> </template>
@@ -1898,12 +2328,97 @@ watch(activeTab, (tab) => {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 4px;
}
.remote-selector {
display: inline-flex;
align-items: center;
cursor: pointer;
padding: 1px 6px;
border-radius: 4px;
color: var(--accent, #89b4fa);
font-weight: 500;
transition: background 0.2s;
}
.remote-selector:hover {
background: var(--hover-bg, rgba(137, 180, 250, 0.1));
}
.remote-url-text {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.remote-dropdown {
background: var(--dropdown-bg, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 8px;
padding: 8px 0;
min-width: 280px;
max-width: 400px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.remote-add-btn {
padding: 8px 12px;
color: var(--accent, #89b4fa);
cursor: pointer;
font-size: 12px;
border-top: 1px solid var(--border-color, #313244);
margin-top: 4px;
}
.remote-add-btn:hover {
background: var(--hover-bg, rgba(137, 180, 250, 0.08));
}
.remote-add-form {
padding: 8px 12px;
border-top: 1px solid var(--border-color, #313244);
margin-top: 4px;
} }
.project-actions { .project-actions {
flex-shrink: 0; flex-shrink: 0;
} }
/* 代理设置下拉 */
.proxy-dropdown {
background: var(--dropdown-bg, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 8px;
padding: 4px 0;
min-width: 180px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.proxy-dropdown-title {
padding: 6px 12px;
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--border-color, #313244);
}
.proxy-dropdown-item {
padding: 8px 12px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
font-size: 13px;
color: var(--text-primary, #cdd6f4);
transition: background 0.15s;
}
.proxy-dropdown-item:hover {
background: var(--hover-bg, rgba(137, 180, 250, 0.08));
}
.proxy-dropdown-item.active {
color: var(--accent, #89b4fa);
}
/* 主体区域 */ /* 主体区域 */
.project-body { .project-body {
flex: 1; flex: 1;
@@ -2731,4 +3246,43 @@ watch(activeTab, (tab) => {
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
flex-shrink: 0; flex-shrink: 0;
} }
/* Release 详情面板 */
.release-detail-panel {
padding: 16px;
overflow-y: auto;
height: 100%;
}
.release-detail-header {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.release-detail-body {
margin-bottom: 16px;
padding: 12px;
background: var(--bg-secondary, #181825);
border-radius: 6px;
border: 1px solid var(--border-color);
}
.release-detail-assets {
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.release-asset-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 4px;
font-size: 13px;
}
.release-asset-item:hover {
background: var(--bg-hover, #313244);
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -3,5 +3,6 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
export { export {
Group,
Settings Settings
} from "./models.js"; } from "./models.js";

View File

@@ -6,6 +6,48 @@
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime"; 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 { export class Settings {
/** /**
* Creates a new Settings instance. * Creates a new Settings instance.

View File

@@ -32,57 +32,85 @@ export function AbortMerge(path) {
} }
/** /**
* AddPlatform 添加新平台 * AddCredential 添加凭证
* @param {string} name * @param {string} platform
* @param {string} baseURL * @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
* @param {string} path
* @returns {$CancellablePromise<void>}
*/
export function AddProject(platform, username, name, path) {
return $Call.ByID(2299402672, platform, username, name, path);
}
/**
* AddUser 添加用户到指定平台
* @param {string} platform
* @param {string} username * @param {string} username
* @param {string} token * @param {string} token
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
*/ */
export function AddUser(platform, username, token) { export function AddCredential(platform, baseURL, username, token) {
return $Call.ByID(2264426704, platform, 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 批量拉取指定项目 * BatchPull 批量拉取指定项目
* @param {string[]} paths * @param {string[]} paths
* @param {string} remote
* @returns {$CancellablePromise<$models.BatchPullResult[]>} * @returns {$CancellablePromise<$models.BatchPullResult[]>}
*/ */
export function BatchPull(paths) { export function BatchPull(paths, remote) {
return $Call.ByID(758996647, paths).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(758996647, paths, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result); return $$createType3($result);
})); }));
} }
/** /**
* BatchPush 批量推送指定项目 * BatchPush 批量推送指定项目
* @param {string[]} paths * @param {string[]} paths
* @param {string} remote
* @returns {$CancellablePromise<$models.BatchPullResult[]>} * @returns {$CancellablePromise<$models.BatchPullResult[]>}
*/ */
export function BatchPush(paths) { export function BatchPush(paths, remote) {
return $Call.ByID(794082076, paths).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(794082076, paths, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result); return $$createType3($result);
})); }));
} }
@@ -92,7 +120,7 @@ export function BatchPush(paths) {
*/ */
export function CheckGitInstalled() { export function CheckGitInstalled() {
return $Call.ByID(464620426).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(464620426).then(/** @type {($result: any) => any} */(($result) => {
return $$createType2($result); return $$createType4($result);
})); }));
} }
@@ -107,16 +135,15 @@ export function CheckoutRemoteBranch(path, remoteBranch) {
} }
/** /**
* CloneProject 克隆远程仓库到本地目录,并添加到项目 * CloneProject 克隆远程仓库到本地目录,并添加到项目列表
* @param {string} platform
* @param {string} username
* @param {string} repoURL * @param {string} repoURL
* @param {string} parentDir * @param {string} parentDir
* @param {string} name * @param {string} name
* @param {string} group
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
*/ */
export function CloneProject(platform, username, repoURL, parentDir, name) { export function CloneProject(repoURL, parentDir, name, group) {
return $Call.ByID(2347019414, platform, username, repoURL, parentDir, name); return $Call.ByID(2347019414, repoURL, parentDir, name, group);
} }
/** /**
@@ -165,20 +192,22 @@ export function DeleteBranch(path, name, force) {
* DeleteRemoteBranch 删除远程分支 * DeleteRemoteBranch 删除远程分支
* @param {string} path * @param {string} path
* @param {string} branch * @param {string} branch
* @param {string} remote
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
*/ */
export function DeleteRemoteBranch(path, branch) { export function DeleteRemoteBranch(path, branch, remote) {
return $Call.ByID(2626609049, path, branch); return $Call.ByID(2626609049, path, branch, remote);
} }
/** /**
* DeleteTag 删除标签(本地+远程) * DeleteTag 删除标签(本地+远程)
* @param {string} path * @param {string} path
* @param {string} name * @param {string} name
* @param {string} remote
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
*/ */
export function DeleteTag(path, name) { export function DeleteTag(path, name, remote) {
return $Call.ByID(873653241, path, name); return $Call.ByID(873653241, path, name, remote);
} }
/** /**
@@ -192,12 +221,13 @@ export function DiscardFiles(path, files) {
} }
/** /**
* FetchProject 拉取远程信息 * FetchProject 拉取远程信息(指定 remote空则 fetch --all
* @param {string} path * @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<string>} * @returns {$CancellablePromise<string>}
*/ */
export function FetchProject(path) { export function FetchProject(path, remote) {
return $Call.ByID(3541106829, path); return $Call.ByID(3541106829, path, remote);
} }
/** /**
@@ -206,7 +236,7 @@ export function FetchProject(path) {
*/ */
export function GetAllProjectOverview() { export function GetAllProjectOverview() {
return $Call.ByID(2453130151).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2453130151).then(/** @type {($result: any) => any} */(($result) => {
return $$createType4($result); return $$createType6($result);
})); }));
} }
@@ -216,7 +246,7 @@ export function GetAllProjectOverview() {
*/ */
export function GetAppSettings() { export function GetAppSettings() {
return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => {
return $$createType6($result); return $$createType8($result);
})); }));
} }
@@ -227,7 +257,7 @@ export function GetAppSettings() {
*/ */
export function GetBranches(path) { export function GetBranches(path) {
return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType8($result); return $$createType10($result);
})); }));
} }
@@ -260,7 +290,7 @@ export function GetCommitFileDiff(path, hash, filePath) {
*/ */
export function GetCommitFiles(path, hash) { export function GetCommitFiles(path, hash) {
return $Call.ByID(2420707876, path, hash).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2420707876, path, hash).then(/** @type {($result: any) => any} */(($result) => {
return $$createType10($result); return $$createType12($result);
})); }));
} }
@@ -272,7 +302,7 @@ export function GetCommitFiles(path, hash) {
*/ */
export function GetCommitLog(path, count) { export function GetCommitLog(path, count) {
return $Call.ByID(1281870789, path, count).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(1281870789, path, count).then(/** @type {($result: any) => any} */(($result) => {
return $$createType12($result); return $$createType14($result);
})); }));
} }
@@ -293,7 +323,17 @@ export function GetConflictFileContent(projectPath, filePath) {
*/ */
export function GetConflictFiles(path) { export function GetConflictFiles(path) {
return $Call.ByID(2446806137, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2446806137, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType14($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);
})); }));
} }
@@ -333,18 +373,17 @@ export function GetFileDiffStaged(projectPath, filePath) {
*/ */
export function GetGitGlobalConfig() { export function GetGitGlobalConfig() {
return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => {
return $$createType16($result); return $$createType20($result);
})); }));
} }
/** /**
* GetPlatformInfo 获取平台信息 * GetGroups 获取所有分组
* @param {string} name * @returns {$CancellablePromise<config$0.Group[]>}
* @returns {$CancellablePromise<$models.PlatformInfo | null>}
*/ */
export function GetPlatformInfo(name) { export function GetGroups() {
return $Call.ByID(2668095547, name).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(1600884836).then(/** @type {($result: any) => any} */(($result) => {
return $$createType18($result); return $$createType22($result);
})); }));
} }
@@ -355,10 +394,19 @@ export function GetPlatformInfo(name) {
*/ */
export function GetProjectChangedFiles(path) { export function GetProjectChangedFiles(path) {
return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType20($result); return $$createType24($result);
})); }));
} }
/**
* GetProjectProxy 获取项目代理设置nil 表示跟随全局)
* @param {string} path
* @returns {$CancellablePromise<boolean | null>}
*/
export function GetProjectProxy(path) {
return $Call.ByID(2515497911, path);
}
/** /**
* GetProjectStatus 获取项目 git 状态 * GetProjectStatus 获取项目 git 状态
* @param {string} path * @param {string} path
@@ -366,28 +414,54 @@ export function GetProjectChangedFiles(path) {
*/ */
export function GetProjectStatus(path) { export function GetProjectStatus(path) {
return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType22($result); return $$createType26($result);
})); }));
} }
/** /**
* GetProjectTree 获取项目树,供前端侧边栏渲染 * GetProjectTree 获取项目树,按分组组织
* @returns {$CancellablePromise<$models.TreeNode[]>} * @returns {$CancellablePromise<$models.TreeNode[]>}
*/ */
export function GetProjectTree() { export function GetProjectTree() {
return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => {
return $$createType24($result); return $$createType28($result);
})); }));
} }
/** /**
* GetRemoteBranches 获取远程分支列表 * GetRemoteBranches 获取远程分支列表
* @param {string} path * @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<$models.BranchInfo[]>} * @returns {$CancellablePromise<$models.BranchInfo[]>}
*/ */
export function GetRemoteBranches(path) { export function GetRemoteBranches(path, remote) {
return $Call.ByID(994796370, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(994796370, path, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType8($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);
})); }));
} }
@@ -398,7 +472,7 @@ export function GetRemoteBranches(path) {
*/ */
export function GetStashList(path) { export function GetStashList(path) {
return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType26($result); return $$createType34($result);
})); }));
} }
@@ -409,19 +483,7 @@ export function GetStashList(path) {
*/ */
export function GetTags(path) { export function GetTags(path) {
return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType28($result); return $$createType36($result);
}));
}
/**
* GetUserInfo 获取用户信息
* @param {string} platform
* @param {string} username
* @returns {$CancellablePromise<$models.UserInfo | null>}
*/
export function GetUserInfo(platform, username) {
return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => {
return $$createType30($result);
})); }));
} }
@@ -455,61 +517,167 @@ export function MergeBranch(path, branch) {
} }
/** /**
* PullProject 拉取项目(当前分支) * MigrateProject 将项目迁移到目标平台
* @param {string} path * targetCredPlatform + targetCredUsername 用于定位目标凭证
* @returns {$CancellablePromise<string>} * 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 PullProject(path) { export function MigrateProject(projectPath, targetCredPlatform, targetCredUsername, repoName, remoteName, description, $private) {
return $Call.ByID(4145813996, path); return $Call.ByID(618007206, projectPath, targetCredPlatform, targetCredUsername, repoName, remoteName, description, $private).then(/** @type {($result: any) => any} */(($result) => {
return $$createType37($result);
}));
} }
/** /**
* PushProject 推送项目(当前分支) * MoveProjectToGroup 移动项目到指定分组
* @param {string} path * @param {string} path
* @param {string} group
* @returns {$CancellablePromise<void>}
*/
export function MoveProjectToGroup(path, group) {
return $Call.ByID(1699187666, path, group);
}
/**
* 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>} * @returns {$CancellablePromise<string>}
*/ */
export function PushProject(path) { export function PullProject(path, remote) {
return $Call.ByID(3887901113, path); 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 推送标签到远程 * PushTag 推送标签到远程
* @param {string} path * @param {string} path
* @param {string} name * @param {string} name
* @param {string} remote
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
*/ */
export function PushTag(path, name) { export function PushTag(path, name, remote) {
return $Call.ByID(1424810524, path, name); return $Call.ByID(1424810524, path, name, remote);
} }
/** /**
* RemovePlatform 删除平台 * PushTagToAllRemotes 一键推送标签到所有远程仓库
* @param {string} name * @param {string} path
* @returns {$CancellablePromise<void>} * @param {string} tagName
* @returns {$CancellablePromise<$models.PushAllResult[]>}
*/ */
export function RemovePlatform(name) { export function PushTagToAllRemotes(path, tagName) {
return $Call.ByID(2222448051, name); return $Call.ByID(2343581171, path, tagName).then(/** @type {($result: any) => any} */(($result) => {
return $$createType43($result);
}));
} }
/** /**
* RemoveProject 从指定平台/用户下删除项目 * PushToAllRemotes 一键推送到所有远程仓库(当前分支)
* @param {string} platform * @param {string} path
* @param {string} username * @returns {$CancellablePromise<$models.PushAllResult[]>}
* @param {string} name
* @returns {$CancellablePromise<void>}
*/ */
export function RemoveProject(platform, username, name) { export function PushToAllRemotes(path) {
return $Call.ByID(2748109581, platform, username, name); return $Call.ByID(1027392731, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType43($result);
}));
} }
/** /**
* RemoveUser 从平台删除用户 * RemoveCredential 删除凭证
* @param {string} platform * @param {string} platform
* @param {string} username * @param {string} username
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
*/ */
export function RemoveUser(platform, username) { export function RemoveCredential(platform, username) {
return $Call.ByID(1409517275, 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);
} }
/** /**
@@ -565,7 +733,7 @@ export function SaveConflictFile(projectPath, filePath, content) {
*/ */
export function SearchCommitLog(path, keyword, author, maxCount) { export function SearchCommitLog(path, keyword, author, maxCount) {
return $Call.ByID(2874450917, path, keyword, author, maxCount).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2874450917, path, keyword, author, maxCount).then(/** @type {($result: any) => any} */(($result) => {
return $$createType12($result); return $$createType14($result);
})); }));
} }
@@ -603,6 +771,17 @@ export function SetGitGlobalConfig(name, email) {
return $Call.ByID(3875064981, 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 暂存所有变更文件 * StageAll 暂存所有变更文件
* @param {string} path * @param {string} path
@@ -672,6 +851,22 @@ export function SwitchBranch(path, branch) {
return $Call.ByID(4262224900, 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 取消暂存所有文件 * UnstageAll 取消暂存所有文件
* @param {string} path * @param {string} path
@@ -701,56 +896,72 @@ export function UpdateAppSettings(logLevel) {
} }
/** /**
* UpdatePlatform 修改平台信息base_url * UpdateCredential 更新凭证
* @param {string} name
* @param {string} baseURL
* @returns {$CancellablePromise<void>}
*/
export function UpdatePlatform(name, baseURL) {
return $Call.ByID(3258014550, name, baseURL);
}
/**
* UpdateUser 修改用户信息用户名、token
* @param {string} platform * @param {string} platform
* @param {string} oldUsername * @param {string} username
* @param {string} newUsername * @param {string} baseURL
* @param {string} token * @param {string} token
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
*/ */
export function UpdateUser(platform, oldUsername, newUsername, token) { export function UpdateCredential(platform, username, baseURL, token) {
return $Call.ByID(1631729342, platform, oldUsername, newUsername, 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 // Private type creation functions
const $$createType0 = $models.BatchPullResult.createFrom; const $$createType0 = $models.MigrateResult.createFrom;
const $$createType1 = $Create.Array($$createType0); const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $models.GitStatus.createFrom; const $$createType2 = $models.BatchPullResult.createFrom;
const $$createType3 = $models.ProjectOverview.createFrom; const $$createType3 = $Create.Array($$createType2);
const $$createType4 = $Create.Array($$createType3); const $$createType4 = $models.GitStatus.createFrom;
const $$createType5 = config$0.Settings.createFrom; const $$createType5 = $models.ProjectOverview.createFrom;
const $$createType6 = $Create.Nullable($$createType5); const $$createType6 = $Create.Array($$createType5);
const $$createType7 = $models.BranchInfo.createFrom; const $$createType7 = config$0.Settings.createFrom;
const $$createType8 = $Create.Array($$createType7); const $$createType8 = $Create.Nullable($$createType7);
const $$createType9 = $models.CommitFileInfo.createFrom; const $$createType9 = $models.BranchInfo.createFrom;
const $$createType10 = $Create.Array($$createType9); const $$createType10 = $Create.Array($$createType9);
const $$createType11 = $models.CommitLog.createFrom; const $$createType11 = $models.CommitFileInfo.createFrom;
const $$createType12 = $Create.Array($$createType11); const $$createType12 = $Create.Array($$createType11);
const $$createType13 = $models.ConflictFileInfo.createFrom; const $$createType13 = $models.CommitLog.createFrom;
const $$createType14 = $Create.Array($$createType13); const $$createType14 = $Create.Array($$createType13);
const $$createType15 = $models.GitConfig.createFrom; const $$createType15 = $models.ConflictFileInfo.createFrom;
const $$createType16 = $Create.Nullable($$createType15); const $$createType16 = $Create.Array($$createType15);
const $$createType17 = $models.PlatformInfo.createFrom; const $$createType17 = $models.CredentialInfo.createFrom;
const $$createType18 = $Create.Nullable($$createType17); const $$createType18 = $Create.Array($$createType17);
const $$createType19 = $models.FileInfo.createFrom; const $$createType19 = $models.GitConfig.createFrom;
const $$createType20 = $Create.Array($$createType19); const $$createType20 = $Create.Nullable($$createType19);
const $$createType21 = $models.ProjectStatus.createFrom; const $$createType21 = config$0.Group.createFrom;
const $$createType22 = $Create.Nullable($$createType21); const $$createType22 = $Create.Array($$createType21);
const $$createType23 = $models.TreeNode.createFrom; const $$createType23 = $models.FileInfo.createFrom;
const $$createType24 = $Create.Array($$createType23); const $$createType24 = $Create.Array($$createType23);
const $$createType25 = $models.StashInfo.createFrom; const $$createType25 = $models.ProjectStatus.createFrom;
const $$createType26 = $Create.Array($$createType25); const $$createType26 = $Create.Nullable($$createType25);
const $$createType27 = $models.TagInfo.createFrom; const $$createType27 = $models.TreeNode.createFrom;
const $$createType28 = $Create.Array($$createType27); const $$createType28 = $Create.Array($$createType27);
const $$createType29 = $models.UserInfo.createFrom; const $$createType29 = $models.ReleaseInfo.createFrom;
const $$createType30 = $Create.Nullable($$createType29); const $$createType30 = $Create.Array($$createType29);
const $$createType31 = $models.RemoteItem.createFrom;
const $$createType32 = $Create.Array($$createType31);
const $$createType33 = $models.StashInfo.createFrom;
const $$createType34 = $Create.Array($$createType33);
const $$createType35 = $models.TagInfo.createFrom;
const $$createType36 = $Create.Array($$createType35);
const $$createType37 = $Create.Nullable($$createType0);
const $$createType38 = $models.OnlineMigrateCompareResult.createFrom;
const $$createType39 = $Create.Nullable($$createType38);
const $$createType40 = $models.OnlineMigrateItemResult.createFrom;
const $$createType41 = $Create.Array($$createType40);
const $$createType42 = $models.PushAllResult.createFrom;
const $$createType43 = $Create.Array($$createType42);
const $$createType44 = $models.ReleaseSyncResult.createFrom;
const $$createType45 = $Create.Array($$createType44);

View File

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

View File

@@ -6,6 +6,58 @@
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime"; import { Create as $Create } from "@wailsio/runtime";
/**
* AssetInfo Release 附件
*/
export class AssetInfo {
/**
* Creates a new AssetInfo instance.
* @param {Partial<AssetInfo>} [$$source = {}] - The source object to create the AssetInfo.
*/
constructor($$source = {}) {
if (!("id" in $$source)) {
/**
* @member
* @type {number}
*/
this["id"] = 0;
}
if (!("name" in $$source)) {
/**
* @member
* @type {string}
*/
this["name"] = "";
}
if (!("size" in $$source)) {
/**
* @member
* @type {number}
*/
this["size"] = 0;
}
if (!("downloadUrl" in $$source)) {
/**
* @member
* @type {string}
*/
this["downloadUrl"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new AssetInfo instance from a string or object.
* @param {any} [$$source = {}]
* @returns {AssetInfo}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new AssetInfo(/** @type {Partial<AssetInfo>} */($$parsedSource));
}
}
/** /**
* BatchPullResult 批量 pull 结果 * BatchPullResult 批量 pull 结果
*/ */
@@ -238,6 +290,58 @@ export class ConflictFileInfo {
} }
} }
/**
* CredentialInfo 凭证信息(前端展示用)
*/
export class CredentialInfo {
/**
* Creates a new CredentialInfo instance.
* @param {Partial<CredentialInfo>} [$$source = {}] - The source object to create the CredentialInfo.
*/
constructor($$source = {}) {
if (!("platform" in $$source)) {
/**
* @member
* @type {string}
*/
this["platform"] = "";
}
if (!("baseUrl" in $$source)) {
/**
* @member
* @type {string}
*/
this["baseUrl"] = "";
}
if (!("username" in $$source)) {
/**
* @member
* @type {string}
*/
this["username"] = "";
}
if (!("token" in $$source)) {
/**
* @member
* @type {string}
*/
this["token"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new CredentialInfo instance from a string or object.
* @param {any} [$$source = {}]
* @returns {CredentialInfo}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new CredentialInfo(/** @type {Partial<CredentialInfo>} */($$parsedSource));
}
}
/** /**
* FileInfo 文件信息 * FileInfo 文件信息
*/ */
@@ -374,12 +478,197 @@ export class GitStatus {
} }
/** /**
* PlatformInfo 平台信息 * MigrateOptions 迁移选项
*/ */
export class PlatformInfo { export class MigrateOptions {
/** /**
* Creates a new PlatformInfo instance. * Creates a new MigrateOptions instance.
* @param {Partial<PlatformInfo>} [$$source = {}] - The source object to create the PlatformInfo. * @param {Partial<MigrateOptions>} [$$source = {}] - The source object to create the MigrateOptions.
*/
constructor($$source = {}) {
if (!("labels" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["labels"] = false;
}
if (!("issues" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["issues"] = false;
}
if (!("pullRequests" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["pullRequests"] = false;
}
if (!("releases" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["releases"] = false;
}
if (!("milestones" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["milestones"] = false;
}
Object.assign(this, $$source);
}
/**
* Creates a new MigrateOptions instance from a string or object.
* @param {any} [$$source = {}]
* @returns {MigrateOptions}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new MigrateOptions(/** @type {Partial<MigrateOptions>} */($$parsedSource));
}
}
/**
* MigrateResult 迁移结果
*/
export class MigrateResult {
/**
* Creates a new MigrateResult instance.
* @param {Partial<MigrateResult>} [$$source = {}] - The source object to create the MigrateResult.
*/
constructor($$source = {}) {
if (!("path" in $$source)) {
/**
* @member
* @type {string}
*/
this["path"] = "";
}
if (!("name" in $$source)) {
/**
* @member
* @type {string}
*/
this["name"] = "";
}
if (!("success" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["success"] = false;
}
if (!("message" in $$source)) {
/**
* @member
* @type {string}
*/
this["message"] = "";
}
if (/** @type {any} */(false)) {
/**
* @member
* @type {string | undefined}
*/
this["cloneUrl"] = undefined;
}
if (/** @type {any} */(false)) {
/**
* @member
* @type {string | undefined}
*/
this["remoteName"] = undefined;
}
Object.assign(this, $$source);
}
/**
* Creates a new MigrateResult instance from a string or object.
* @param {any} [$$source = {}]
* @returns {MigrateResult}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new MigrateResult(/** @type {Partial<MigrateResult>} */($$parsedSource));
}
}
/**
* OnlineMigrateCompareResult 在线迁移对比结果
*/
export class OnlineMigrateCompareResult {
/**
* Creates a new OnlineMigrateCompareResult instance.
* @param {Partial<OnlineMigrateCompareResult>} [$$source = {}] - The source object to create the OnlineMigrateCompareResult.
*/
constructor($$source = {}) {
if (!("sourceRepos" in $$source)) {
/**
* 源平台所有仓库
* @member
* @type {RepoInfo[]}
*/
this["sourceRepos"] = [];
}
if (!("targetRepos" in $$source)) {
/**
* 目标平台所有仓库
* @member
* @type {RepoInfo[]}
*/
this["targetRepos"] = [];
}
if (!("missingRepos" in $$source)) {
/**
* 目标平台缺少的仓库
* @member
* @type {RepoInfo[]}
*/
this["missingRepos"] = [];
}
Object.assign(this, $$source);
}
/**
* Creates a new OnlineMigrateCompareResult instance from a string or object.
* @param {any} [$$source = {}]
* @returns {OnlineMigrateCompareResult}
*/
static createFrom($$source = {}) {
const $$createField0_0 = $$createType1;
const $$createField1_0 = $$createType1;
const $$createField2_0 = $$createType1;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("sourceRepos" in $$parsedSource) {
$$parsedSource["sourceRepos"] = $$createField0_0($$parsedSource["sourceRepos"]);
}
if ("targetRepos" in $$parsedSource) {
$$parsedSource["targetRepos"] = $$createField1_0($$parsedSource["targetRepos"]);
}
if ("missingRepos" in $$parsedSource) {
$$parsedSource["missingRepos"] = $$createField2_0($$parsedSource["missingRepos"]);
}
return new OnlineMigrateCompareResult(/** @type {Partial<OnlineMigrateCompareResult>} */($$parsedSource));
}
}
/**
* OnlineMigrateItemResult 单个仓库在线迁移结果
*/
export class OnlineMigrateItemResult {
/**
* Creates a new OnlineMigrateItemResult instance.
* @param {Partial<OnlineMigrateItemResult>} [$$source = {}] - The source object to create the OnlineMigrateItemResult.
*/ */
constructor($$source = {}) { constructor($$source = {}) {
if (!("name" in $$source)) { if (!("name" in $$source)) {
@@ -389,25 +678,39 @@ export class PlatformInfo {
*/ */
this["name"] = ""; this["name"] = "";
} }
if (!("baseUrl" in $$source)) { if (/** @type {any} */(false)) {
/**
* @member
* @type {string | undefined}
*/
this["cloneUrl"] = undefined;
}
if (!("success" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["success"] = false;
}
if (!("message" in $$source)) {
/** /**
* @member * @member
* @type {string} * @type {string}
*/ */
this["baseUrl"] = ""; this["message"] = "";
} }
Object.assign(this, $$source); Object.assign(this, $$source);
} }
/** /**
* Creates a new PlatformInfo instance from a string or object. * Creates a new OnlineMigrateItemResult instance from a string or object.
* @param {any} [$$source = {}] * @param {any} [$$source = {}]
* @returns {PlatformInfo} * @returns {OnlineMigrateItemResult}
*/ */
static createFrom($$source = {}) { static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new PlatformInfo(/** @type {Partial<PlatformInfo>} */($$parsedSource)); return new OnlineMigrateItemResult(/** @type {Partial<OnlineMigrateItemResult>} */($$parsedSource));
} }
} }
@@ -507,6 +810,20 @@ export class ProjectStatus {
*/ */
this["remoteUrl"] = ""; this["remoteUrl"] = "";
} }
if (!("remotes" in $$source)) {
/**
* @member
* @type {RemoteItem[]}
*/
this["remotes"] = [];
}
if (!("currentRemote" in $$source)) {
/**
* @member
* @type {string}
*/
this["currentRemote"] = "";
}
if (!("changedFiles" in $$source)) { if (!("changedFiles" in $$source)) {
/** /**
* @member * @member
@@ -514,6 +831,14 @@ export class ProjectStatus {
*/ */
this["changedFiles"] = []; this["changedFiles"] = [];
} }
if (!("useProxy" in $$source)) {
/**
* nil=跟随全局, true=开启, false=关闭
* @member
* @type {boolean | null}
*/
this["useProxy"] = null;
}
Object.assign(this, $$source); Object.assign(this, $$source);
} }
@@ -524,15 +849,332 @@ export class ProjectStatus {
* @returns {ProjectStatus} * @returns {ProjectStatus}
*/ */
static createFrom($$source = {}) { static createFrom($$source = {}) {
const $$createField2_0 = $$createType1; const $$createField2_0 = $$createType3;
const $$createField4_0 = $$createType5;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("remotes" in $$parsedSource) {
$$parsedSource["remotes"] = $$createField2_0($$parsedSource["remotes"]);
}
if ("changedFiles" in $$parsedSource) { if ("changedFiles" in $$parsedSource) {
$$parsedSource["changedFiles"] = $$createField2_0($$parsedSource["changedFiles"]); $$parsedSource["changedFiles"] = $$createField4_0($$parsedSource["changedFiles"]);
} }
return new ProjectStatus(/** @type {Partial<ProjectStatus>} */($$parsedSource)); return new ProjectStatus(/** @type {Partial<ProjectStatus>} */($$parsedSource));
} }
} }
/**
* PushAllResult 一键多端推送结果
*/
export class PushAllResult {
/**
* Creates a new PushAllResult instance.
* @param {Partial<PushAllResult>} [$$source = {}] - The source object to create the PushAllResult.
*/
constructor($$source = {}) {
if (!("remote" in $$source)) {
/**
* @member
* @type {string}
*/
this["remote"] = "";
}
if (!("success" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["success"] = false;
}
if (!("message" in $$source)) {
/**
* @member
* @type {string}
*/
this["message"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new PushAllResult instance from a string or object.
* @param {any} [$$source = {}]
* @returns {PushAllResult}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new PushAllResult(/** @type {Partial<PushAllResult>} */($$parsedSource));
}
}
/**
* ReleaseInfo 通用 Release 信息
*/
export class ReleaseInfo {
/**
* Creates a new ReleaseInfo instance.
* @param {Partial<ReleaseInfo>} [$$source = {}] - The source object to create the ReleaseInfo.
*/
constructor($$source = {}) {
if (!("id" in $$source)) {
/**
* @member
* @type {number}
*/
this["id"] = 0;
}
if (!("tagName" in $$source)) {
/**
* @member
* @type {string}
*/
this["tagName"] = "";
}
if (!("name" in $$source)) {
/**
* @member
* @type {string}
*/
this["name"] = "";
}
if (!("body" in $$source)) {
/**
* @member
* @type {string}
*/
this["body"] = "";
}
if (!("draft" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["draft"] = false;
}
if (!("prerelease" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["prerelease"] = false;
}
if (!("createdAt" in $$source)) {
/**
* @member
* @type {string}
*/
this["createdAt"] = "";
}
if (!("publishedAt" in $$source)) {
/**
* @member
* @type {string}
*/
this["publishedAt"] = "";
}
if (!("assets" in $$source)) {
/**
* @member
* @type {AssetInfo[]}
*/
this["assets"] = [];
}
Object.assign(this, $$source);
}
/**
* Creates a new ReleaseInfo instance from a string or object.
* @param {any} [$$source = {}]
* @returns {ReleaseInfo}
*/
static createFrom($$source = {}) {
const $$createField8_0 = $$createType7;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("assets" in $$parsedSource) {
$$parsedSource["assets"] = $$createField8_0($$parsedSource["assets"]);
}
return new ReleaseInfo(/** @type {Partial<ReleaseInfo>} */($$parsedSource));
}
}
/**
* ReleaseSyncResult 单条 Release 同步结果
*/
export class ReleaseSyncResult {
/**
* Creates a new ReleaseSyncResult instance.
* @param {Partial<ReleaseSyncResult>} [$$source = {}] - The source object to create the ReleaseSyncResult.
*/
constructor($$source = {}) {
if (!("tagName" in $$source)) {
/**
* @member
* @type {string}
*/
this["tagName"] = "";
}
if (!("name" in $$source)) {
/**
* @member
* @type {string}
*/
this["name"] = "";
}
if (!("success" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["success"] = false;
}
if (!("message" in $$source)) {
/**
* @member
* @type {string}
*/
this["message"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new ReleaseSyncResult instance from a string or object.
* @param {any} [$$source = {}]
* @returns {ReleaseSyncResult}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ReleaseSyncResult(/** @type {Partial<ReleaseSyncResult>} */($$parsedSource));
}
}
/**
* RemoteItem 远程仓库信息
*/
export class RemoteItem {
/**
* Creates a new RemoteItem instance.
* @param {Partial<RemoteItem>} [$$source = {}] - The source object to create the RemoteItem.
*/
constructor($$source = {}) {
if (!("name" in $$source)) {
/**
* @member
* @type {string}
*/
this["name"] = "";
}
if (!("url" in $$source)) {
/**
* @member
* @type {string}
*/
this["url"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new RemoteItem instance from a string or object.
* @param {any} [$$source = {}]
* @returns {RemoteItem}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new RemoteItem(/** @type {Partial<RemoteItem>} */($$parsedSource));
}
}
/**
* RepoInfo 仓库信息
*/
export class RepoInfo {
/**
* Creates a new RepoInfo instance.
* @param {Partial<RepoInfo>} [$$source = {}] - The source object to create the RepoInfo.
*/
constructor($$source = {}) {
if (!("name" in $$source)) {
/**
* @member
* @type {string}
*/
this["name"] = "";
}
if (!("fullName" in $$source)) {
/**
* @member
* @type {string}
*/
this["fullName"] = "";
}
if (!("description" in $$source)) {
/**
* @member
* @type {string}
*/
this["description"] = "";
}
if (!("cloneUrl" in $$source)) {
/**
* @member
* @type {string}
*/
this["cloneUrl"] = "";
}
if (!("sshUrl" in $$source)) {
/**
* @member
* @type {string}
*/
this["sshUrl"] = "";
}
if (!("private" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["private"] = false;
}
if (!("fork" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["fork"] = false;
}
if (!("empty" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["empty"] = false;
}
if (!("updatedAt" in $$source)) {
/**
* @member
* @type {string}
*/
this["updatedAt"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new RepoInfo instance from a string or object.
* @param {any} [$$source = {}]
* @returns {RepoInfo}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new RepoInfo(/** @type {Partial<RepoInfo>} */($$parsedSource));
}
}
/** /**
* StashInfo 贮藏信息 * StashInfo 贮藏信息
*/ */
@@ -662,7 +1304,7 @@ export class TreeNode {
} }
if (!("type" in $$source)) { if (!("type" in $$source)) {
/** /**
* platform, user, project * group, project
* @member * @member
* @type {string} * @type {string}
*/ */
@@ -675,6 +1317,13 @@ export class TreeNode {
*/ */
this["path"] = undefined; this["path"] = undefined;
} }
if (/** @type {any} */(false)) {
/**
* @member
* @type {string | undefined}
*/
this["icon"] = undefined;
}
if (/** @type {any} */(false)) { if (/** @type {any} */(false)) {
/** /**
* @member * @member
@@ -692,55 +1341,23 @@ export class TreeNode {
* @returns {TreeNode} * @returns {TreeNode}
*/ */
static createFrom($$source = {}) { static createFrom($$source = {}) {
const $$createField4_0 = $$createType3; const $$createField5_0 = $$createType9;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("children" in $$parsedSource) { if ("children" in $$parsedSource) {
$$parsedSource["children"] = $$createField4_0($$parsedSource["children"]); $$parsedSource["children"] = $$createField5_0($$parsedSource["children"]);
} }
return new TreeNode(/** @type {Partial<TreeNode>} */($$parsedSource)); return new TreeNode(/** @type {Partial<TreeNode>} */($$parsedSource));
} }
} }
/**
* UserInfo 用户信息
*/
export class UserInfo {
/**
* Creates a new UserInfo instance.
* @param {Partial<UserInfo>} [$$source = {}] - The source object to create the UserInfo.
*/
constructor($$source = {}) {
if (!("username" in $$source)) {
/**
* @member
* @type {string}
*/
this["username"] = "";
}
if (!("token" in $$source)) {
/**
* @member
* @type {string}
*/
this["token"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new UserInfo instance from a string or object.
* @param {any} [$$source = {}]
* @returns {UserInfo}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new UserInfo(/** @type {Partial<UserInfo>} */($$parsedSource));
}
}
// Private type creation functions // Private type creation functions
const $$createType0 = FileInfo.createFrom; const $$createType0 = RepoInfo.createFrom;
const $$createType1 = $Create.Array($$createType0); const $$createType1 = $Create.Array($$createType0);
const $$createType2 = TreeNode.createFrom; const $$createType2 = RemoteItem.createFrom;
const $$createType3 = $Create.Array($$createType2); const $$createType3 = $Create.Array($$createType2);
const $$createType4 = FileInfo.createFrom;
const $$createType5 = $Create.Array($$createType4);
const $$createType6 = AssetInfo.createFrom;
const $$createType7 = $Create.Array($$createType6);
const $$createType8 = TreeNode.createFrom;
const $$createType9 = $Create.Array($$createType8);

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

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

View File

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

1126
internal/app/platform.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,9 @@ func New() *AppService {
if err != nil { if err != nil {
log.Printf("加载配置失败: %v, 使用默认配置", err) log.Printf("加载配置失败: %v, 使用默认配置", err)
cfg = &config.AppConfig{ cfg = &config.AppConfig{
Platforms: make(map[string]config.Platform), Projects: []config.ProjectItem{},
Groups: []config.Group{},
Credentials: []config.Credential{},
Settings: config.Settings{Concurrency: 6, NetworkCheck: true, LogLevel: "info"}, Settings: config.Settings{Concurrency: 6, NetworkCheck: true, LogLevel: "info"},
} }
} }
@@ -54,51 +56,91 @@ func (s *AppService) SelectDirectory() (string, error) {
return path, nil return path, nil
} }
// --- 平台/项目 树形结构 --- // --- 项目/分组 树形结构 ---
// TreeNode 前端侧边栏树节点 // TreeNode 前端侧边栏树节点
type TreeNode struct { type TreeNode struct {
Key string `json:"key"` Key string `json:"key"`
Label string `json:"label"` Label string `json:"label"`
Type string `json:"type"` // platform, user, project Type string `json:"type"` // group, project
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
Icon string `json:"icon,omitempty"`
Children []TreeNode `json:"children,omitempty"` Children []TreeNode `json:"children,omitempty"`
} }
// GetProjectTree 获取项目树,供前端侧边栏渲染 // GetProjectTree 获取项目树,按分组组织
func (s *AppService) GetProjectTree() []TreeNode { func (s *AppService) GetProjectTree() []TreeNode {
// 按分组归类项目
groupProjects := make(map[string][]config.ProjectItem)
for _, proj := range s.config.Projects {
g := proj.Group
if g == "" {
g = "未分组"
}
groupProjects[g] = append(groupProjects[g], proj)
}
// 构建分组 → icon 映射
groupIcon := make(map[string]string)
for _, g := range s.config.Groups {
groupIcon[g.Name] = g.Icon
}
var tree []TreeNode var tree []TreeNode
for platformName, platform := range s.config.Platforms { // 先输出已定义的分组(保持顺序)
platformNode := TreeNode{ rendered := make(map[string]bool)
Key: platformName, for _, g := range s.config.Groups {
Label: platformName, projects, ok := groupProjects[g.Name]
Type: "platform", if !ok {
projects = nil
} }
for _, user := range platform.Users { node := TreeNode{
userNode := TreeNode{ Key: "group:" + g.Name,
Key: platformName + "/" + user.Username, Label: g.Name,
Label: user.Username, Type: "group",
Type: "user", Icon: g.Icon,
} }
for _, proj := range user.Projects { for _, proj := range projects {
userNode.Children = append(userNode.Children, TreeNode{ node.Children = append(node.Children, TreeNode{
Key: platformName + "/" + user.Username + "/" + proj.Name, Key: "project:" + proj.Path,
Label: proj.Name, Label: proj.Name,
Type: "project", Type: "project",
Path: proj.Path, Path: proj.Path,
}) })
} }
platformNode.Children = append(platformNode.Children, userNode) tree = append(tree, node)
rendered[g.Name] = true
} }
tree = append(tree, platformNode)
// 输出"未分组"或不在 groups 列表里的项目
for groupName, projects := range groupProjects {
if rendered[groupName] {
continue
} }
node := TreeNode{
Key: "group:" + groupName,
Label: groupName,
Type: "group",
Icon: "folder",
}
for _, proj := range projects {
node.Children = append(node.Children, TreeNode{
Key: "project:" + proj.Path,
Label: proj.Name,
Type: "project",
Path: proj.Path,
})
}
tree = append(tree, node)
}
return tree return tree
} }
// --- 项目管理 --- // --- 项目管理 ---
// CloneProject 克隆远程仓库到本地目录,并添加到项目 // CloneProject 克隆远程仓库到本地目录,并添加到项目列表
func (s *AppService) CloneProject(platform, username, repoURL, parentDir, name string) error { func (s *AppService) CloneProject(repoURL, parentDir, name, group string) error {
if repoURL == "" { if repoURL == "" {
return fmt.Errorf("仓库地址不能为空") return fmt.Errorf("仓库地址不能为空")
} }
@@ -109,198 +151,219 @@ func (s *AppService) CloneProject(platform, username, repoURL, parentDir, name s
return fmt.Errorf("项目名称不能为空") return fmt.Errorf("项目名称不能为空")
} }
// 目标路径: parentDir/name
targetPath := parentDir + "/" + name targetPath := parentDir + "/" + name
// 执行 git clone
_, err := s.gitClient.Clone(repoURL, targetPath) _, err := s.gitClient.Clone(repoURL, targetPath)
if err != nil { if err != nil {
return fmt.Errorf("克隆失败: %w", err) return fmt.Errorf("克隆失败: %w", err)
} }
// 克隆成功后添加到项目树 return s.AddProject(name, targetPath, group)
return s.AddProject(platform, username, name, targetPath)
} }
// AddProject 添加项目到指定平台/用户下 // AddProject 添加项目
func (s *AppService) AddProject(platform, username, name, path string) error { func (s *AppService) AddProject(name, path, group string) error {
p, ok := s.config.Platforms[platform] // 检查路径重复
if !ok { for _, proj := range s.config.Projects {
return fmt.Errorf("平台 %s 不存在", platform) if proj.Path == path {
} return fmt.Errorf("项目路径 %s 已存在", path)
for i, user := range p.Users {
if user.Username == username {
// 检查重复
for _, proj := range user.Projects {
if proj.Name == name {
return fmt.Errorf("项目 %s 已存在", name)
} }
} }
s.config.Platforms[platform].Users[i].Projects = append( s.config.Projects = append(s.config.Projects, config.ProjectItem{
s.config.Platforms[platform].Users[i].Projects, Name: name,
config.Project{Name: name, Path: path}, Path: path,
) Group: group,
})
// 如果分组不存在,自动创建
if group != "" {
found := false
for _, g := range s.config.Groups {
if g.Name == group {
found = true
break
}
}
if !found {
s.config.Groups = append(s.config.Groups, config.Group{Name: group, Icon: "folder"})
}
}
return config.SaveConfig(s.config)
}
// RemoveProject 删除项目(按路径匹配)
func (s *AppService) RemoveProject(path string) error {
for i, proj := range s.config.Projects {
if proj.Path == path {
s.config.Projects = append(s.config.Projects[:i], s.config.Projects[i+1:]...)
return config.SaveConfig(s.config) return config.SaveConfig(s.config)
} }
} }
return fmt.Errorf("用户 %s 不存在于平台 %s", username, platform) return fmt.Errorf("项目不存在: %s", path)
} }
// RemoveProject 从指定平台/用户下删除项目 // MoveProjectToGroup 移动项目到指定分组
func (s *AppService) RemoveProject(platform, username, name string) error { func (s *AppService) MoveProjectToGroup(path, group string) error {
p, ok := s.config.Platforms[platform] for i, proj := range s.config.Projects {
if !ok { if proj.Path == path {
return fmt.Errorf("平台 %s 不存在", platform) s.config.Projects[i].Group = group
// 自动创建分组
if group != "" {
found := false
for _, g := range s.config.Groups {
if g.Name == group {
found = true
break
}
}
if !found {
s.config.Groups = append(s.config.Groups, config.Group{Name: group, Icon: "folder"})
}
} }
for i, user := range p.Users {
if user.Username == username {
projects := user.Projects
for j, proj := range projects {
if proj.Name == name {
s.config.Platforms[platform].Users[i].Projects = append(projects[:j], projects[j+1:]...)
return config.SaveConfig(s.config) return config.SaveConfig(s.config)
} }
} }
return fmt.Errorf("项目 %s 不存在", name) return fmt.Errorf("项目不存在: %s", path)
}
}
return fmt.Errorf("用户 %s 不存在于平台 %s", username, platform)
} }
// --- 平台管理 --- // --- 分组管理 ---
// AddPlatform 添加新平台 // GetGroups 获取所有分组
func (s *AppService) AddPlatform(name, baseURL string) error { func (s *AppService) GetGroups() []config.Group {
return s.config.Groups
}
// AddGroup 添加分组
func (s *AppService) AddGroup(name, icon string) error {
if name == "" { if name == "" {
return fmt.Errorf("平台名称不能为空") return fmt.Errorf("分组名称不能为空")
} }
if _, ok := s.config.Platforms[name]; ok { for _, g := range s.config.Groups {
return fmt.Errorf("平台 %s 已存在", name) if g.Name == name {
return fmt.Errorf("分组 %s 已存在", name)
} }
s.config.Platforms[name] = config.Platform{
BaseURL: baseURL,
Users: []config.User{},
} }
if icon == "" {
icon = "folder"
}
s.config.Groups = append(s.config.Groups, config.Group{Name: name, Icon: icon})
return config.SaveConfig(s.config) return config.SaveConfig(s.config)
} }
// UpdatePlatform 修改平台信息base_url // UpdateGroup 修改分组
func (s *AppService) UpdatePlatform(name, baseURL string) error { func (s *AppService) UpdateGroup(oldName, newName, icon string) error {
p, ok := s.config.Platforms[name] if newName == "" {
if !ok { return fmt.Errorf("分组名称不能为空")
return fmt.Errorf("平台 %s 不存在", name) }
for i, g := range s.config.Groups {
if g.Name == oldName {
// 如果改名,检查冲突并同步更新项目的分组引用
if oldName != newName {
for _, g2 := range s.config.Groups {
if g2.Name == newName {
return fmt.Errorf("分组 %s 已存在", newName)
}
}
for j, proj := range s.config.Projects {
if proj.Group == oldName {
s.config.Projects[j].Group = newName
}
}
}
s.config.Groups[i].Name = newName
if icon != "" {
s.config.Groups[i].Icon = icon
} }
p.BaseURL = baseURL
s.config.Platforms[name] = p
return config.SaveConfig(s.config) return config.SaveConfig(s.config)
} }
// RemovePlatform 删除平台
func (s *AppService) RemovePlatform(name string) error {
if _, ok := s.config.Platforms[name]; !ok {
return fmt.Errorf("平台 %s 不存在", name)
} }
delete(s.config.Platforms, name) return fmt.Errorf("分组 %s 不存在", oldName)
}
// RemoveGroup 删除分组(分组下的项目变为"未分组"
func (s *AppService) RemoveGroup(name string) error {
for i, g := range s.config.Groups {
if g.Name == name {
s.config.Groups = append(s.config.Groups[:i], s.config.Groups[i+1:]...)
// 将该分组下项目归入未分组
for j, proj := range s.config.Projects {
if proj.Group == name {
s.config.Projects[j].Group = ""
}
}
return config.SaveConfig(s.config) return config.SaveConfig(s.config)
} }
}
return fmt.Errorf("分组 %s 不存在", name)
}
// --- 用户管理 --- // --- 凭证管理 ---
// AddUser 添加用户到指定平台 // CredentialInfo 凭证信息(前端展示用)
func (s *AppService) AddUser(platform, username, token string) error { type CredentialInfo struct {
p, ok := s.config.Platforms[platform] Platform string `json:"platform"`
if !ok { BaseURL string `json:"baseUrl"`
return fmt.Errorf("平台 %s 不存在", platform) Username string `json:"username"`
Token string `json:"token"`
}
// GetCredentials 获取所有凭证
func (s *AppService) GetCredentials() []CredentialInfo {
var result []CredentialInfo
for _, c := range s.config.Credentials {
result = append(result, CredentialInfo{
Platform: c.Platform,
BaseURL: c.BaseURL,
Username: c.Username,
Token: c.Token,
})
}
return result
}
// AddCredential 添加凭证
func (s *AppService) AddCredential(platform, baseURL, username, token string) error {
if platform == "" {
return fmt.Errorf("平台不能为空")
} }
if username == "" { if username == "" {
return fmt.Errorf("用户名不能为空") return fmt.Errorf("用户名不能为空")
} }
for _, user := range p.Users { // 检查重复(同平台同用户名)
if user.Username == username { for _, c := range s.config.Credentials {
return fmt.Errorf("用户 %s 已存在于平台 %s", username, platform) if c.Platform == platform && c.Username == username {
return fmt.Errorf("凭证已存在: %s/%s", platform, username)
} }
} }
p.Users = append(p.Users, config.User{ s.config.Credentials = append(s.config.Credentials, config.Credential{
Platform: platform,
BaseURL: baseURL,
Username: username, Username: username,
Token: token, Token: token,
Projects: []config.Project{},
}) })
s.config.Platforms[platform] = p
return config.SaveConfig(s.config) return config.SaveConfig(s.config)
} }
// UpdateUser 修改用户信息用户名、token // UpdateCredential 更新凭证
func (s *AppService) UpdateUser(platform, oldUsername, newUsername, token string) error { func (s *AppService) UpdateCredential(platform, username, baseURL, token string) error {
p, ok := s.config.Platforms[platform] for i, c := range s.config.Credentials {
if !ok { if c.Platform == platform && c.Username == username {
return fmt.Errorf("平台 %s 不存在", platform) s.config.Credentials[i].BaseURL = baseURL
} s.config.Credentials[i].Token = token
if newUsername == "" {
return fmt.Errorf("新用户名不能为空")
}
for i, user := range p.Users {
if user.Username == oldUsername {
// 如果改了用户名,检查新名字是否冲突
if oldUsername != newUsername {
for _, u := range p.Users {
if u.Username == newUsername {
return fmt.Errorf("用户 %s 已存在于平台 %s", newUsername, platform)
}
}
}
s.config.Platforms[platform].Users[i].Username = newUsername
s.config.Platforms[platform].Users[i].Token = token
return config.SaveConfig(s.config) return config.SaveConfig(s.config)
} }
} }
return fmt.Errorf("用户 %s 不存在于平台 %s", oldUsername, platform) return fmt.Errorf("凭证不存在: %s/%s", platform, username)
} }
// RemoveUser 从平台删除用户 // RemoveCredential 删除凭证
func (s *AppService) RemoveUser(platform, username string) error { func (s *AppService) RemoveCredential(platform, username string) error {
p, ok := s.config.Platforms[platform] for i, c := range s.config.Credentials {
if !ok { if c.Platform == platform && c.Username == username {
return fmt.Errorf("平台 %s 不存在", platform) s.config.Credentials = append(s.config.Credentials[:i], s.config.Credentials[i+1:]...)
}
for i, user := range p.Users {
if user.Username == username {
s.config.Platforms[platform] = config.Platform{
BaseURL: p.BaseURL,
Users: append(p.Users[:i], p.Users[i+1:]...),
}
return config.SaveConfig(s.config) return config.SaveConfig(s.config)
} }
} }
return fmt.Errorf("用户 %s 不存在于平台 %s", username, platform) return fmt.Errorf("凭证不存在: %s/%s", platform, username)
}
// GetUserInfo 获取用户信息
func (s *AppService) GetUserInfo(platform, username string) (*UserInfo, error) {
p, ok := s.config.Platforms[platform]
if !ok {
return nil, fmt.Errorf("平台 %s 不存在", platform)
}
for _, user := range p.Users {
if user.Username == username {
return &UserInfo{
Username: user.Username,
Token: user.Token,
}, nil
}
}
return nil, fmt.Errorf("用户 %s 不存在于平台 %s", username, platform)
}
// GetPlatformInfo 获取平台信息
func (s *AppService) GetPlatformInfo(name string) (*PlatformInfo, error) {
p, ok := s.config.Platforms[name]
if !ok {
return nil, fmt.Errorf("平台 %s 不存在", name)
}
return &PlatformInfo{
Name: name,
BaseURL: p.BaseURL,
}, nil
} }
// --- Git 操作 --- // --- Git 操作 ---
@@ -309,7 +372,16 @@ func (s *AppService) GetPlatformInfo(name string) (*PlatformInfo, error) {
type ProjectStatus struct { type ProjectStatus struct {
Branch string `json:"branch"` Branch string `json:"branch"`
RemoteURL string `json:"remoteUrl"` RemoteURL string `json:"remoteUrl"`
Remotes []RemoteItem `json:"remotes"`
CurrentRemote string `json:"currentRemote"`
ChangedFiles []FileInfo `json:"changedFiles"` ChangedFiles []FileInfo `json:"changedFiles"`
UseProxy *bool `json:"useProxy"` // nil=跟随全局, true=开启, false=关闭
}
// RemoteItem 远程仓库信息
type RemoteItem struct {
Name string `json:"name"`
URL string `json:"url"`
} }
// FileInfo 文件信息 // FileInfo 文件信息
@@ -320,18 +392,6 @@ type FileInfo struct {
Staged bool `json:"staged"` Staged bool `json:"staged"`
} }
// UserInfo 用户信息
type UserInfo struct {
Username string `json:"username"`
Token string `json:"token"`
}
// PlatformInfo 平台信息
type PlatformInfo struct {
Name string `json:"name"`
BaseURL string `json:"baseUrl"`
}
// GetProjectStatus 获取项目 git 状态 // GetProjectStatus 获取项目 git 状态
func (s *AppService) GetProjectStatus(path string) (*ProjectStatus, error) { func (s *AppService) GetProjectStatus(path string) (*ProjectStatus, error) {
// 先校验路径是否存在 // 先校验路径是否存在
@@ -346,10 +406,35 @@ func (s *AppService) GetProjectStatus(path string) (*ProjectStatus, error) {
remoteURL, _ := s.gitClient.RemoteURL(path) remoteURL, _ := s.gitClient.RemoteURL(path)
// 获取所有 remote 列表
var remotes []RemoteItem
remoteList, remoteErr := s.gitClient.RemoteList(path)
if remoteErr == nil {
for _, r := range remoteList {
remotes = append(remotes, RemoteItem{Name: r.Name, URL: r.URL})
}
}
currentRemote := "origin"
if len(remotes) > 0 {
currentRemote = remotes[0].Name
}
// 获取项目代理设置
var useProxy *bool
for _, proj := range s.config.Projects {
if proj.Path == path {
useProxy = proj.UseProxy
break
}
}
return &ProjectStatus{ return &ProjectStatus{
Branch: strings.TrimSpace(branch), Branch: strings.TrimSpace(branch),
RemoteURL: remoteURL, RemoteURL: remoteURL,
Remotes: remotes,
CurrentRemote: currentRemote,
ChangedFiles: []FileInfo{}, ChangedFiles: []FileInfo{},
UseProxy: useProxy,
}, nil }, nil
} }
@@ -515,22 +600,124 @@ func (s *AppService) GetFileDiffStaged(projectPath, filePath string) (string, er
return s.gitClient.DiffStagedFile(projectPath, filePath) return s.gitClient.DiffStagedFile(projectPath, filePath)
} }
// PullProject 拉取项目(当前分支) // PullProject 拉取项目(当前分支,指定 remote
func (s *AppService) PullProject(path string) (string, error) { func (s *AppService) PullProject(path, remote string) (string, error) {
if remote == "" {
remote = "origin"
}
branch, err := s.gitClient.Branch(path) branch, err := s.gitClient.Branch(path)
if err != nil { if err != nil {
return "", fmt.Errorf("获取当前分支失败: %w", err) return "", fmt.Errorf("获取当前分支失败: %w", err)
} }
return s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch)) proxy := s.GetProjectProxy(path)
return s.gitClient.RunWithProxy(path, proxy, "pull", remote, strings.TrimSpace(branch))
} }
// PushProject 推送项目(当前分支) // PushProject 推送项目(当前分支,指定 remote
func (s *AppService) PushProject(path string) (string, error) { func (s *AppService) PushProject(path, remote string) (string, error) {
if remote == "" {
remote = "origin"
}
branch, err := s.gitClient.Branch(path) branch, err := s.gitClient.Branch(path)
if err != nil { if err != nil {
return "", fmt.Errorf("获取当前分支失败: %w", err) return "", fmt.Errorf("获取当前分支失败: %w", err)
} }
return s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch)) proxy := s.GetProjectProxy(path)
return s.gitClient.RunWithProxy(path, proxy, "push", remote, strings.TrimSpace(branch))
}
// PushAllResult 一键多端推送结果
type PushAllResult struct {
Remote string `json:"remote"`
Success bool `json:"success"`
Message string `json:"message"`
}
// PushToAllRemotes 一键推送到所有远程仓库(当前分支)
func (s *AppService) PushToAllRemotes(path string) ([]PushAllResult, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", path)
}
branch, err := s.gitClient.Branch(path)
if err != nil {
return nil, fmt.Errorf("获取当前分支失败: %w", err)
}
branch = strings.TrimSpace(branch)
remoteList, err := s.gitClient.RemoteList(path)
if err != nil {
return nil, fmt.Errorf("获取远程仓库列表失败: %w", err)
}
proxy := s.GetProjectProxy(path)
var results []PushAllResult
for _, r := range remoteList {
result := PushAllResult{Remote: r.Name}
_, pushErr := s.gitClient.RunWithProxy(path, proxy, "push", r.Name, branch)
if pushErr != nil {
result.Message = pushErr.Error()
} else {
result.Success = true
result.Message = "推送成功"
}
results = append(results, result)
}
return results, nil
}
// PushTagToAllRemotes 一键推送标签到所有远程仓库
func (s *AppService) PushTagToAllRemotes(path, tagName string) ([]PushAllResult, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", path)
}
tagName = strings.TrimSpace(tagName)
if tagName == "" {
return nil, fmt.Errorf("标签名不能为空")
}
remoteList, err := s.gitClient.RemoteList(path)
if err != nil {
return nil, fmt.Errorf("获取远程仓库列表失败: %w", err)
}
proxy := s.GetProjectProxy(path)
var results []PushAllResult
for _, r := range remoteList {
result := PushAllResult{Remote: r.Name}
_, pushErr := s.gitClient.RunWithProxy(path, proxy, "push", r.Name, tagName)
if pushErr != nil {
result.Message = pushErr.Error()
} else {
result.Success = true
result.Message = "推送成功"
}
results = append(results, result)
}
return results, nil
}
// --- 项目代理开关 ---
// GetProjectProxy 获取项目代理设置nil 表示跟随全局)
func (s *AppService) GetProjectProxy(path string) *bool {
for _, proj := range s.config.Projects {
if proj.Path == path {
return proj.UseProxy
}
}
return nil
}
// SetProjectProxy 设置项目代理开关
// useProxy: true=强制启用, false=强制禁用, nil(传空)=跟随全局
func (s *AppService) SetProjectProxy(path string, useProxy *bool) error {
for i, proj := range s.config.Projects {
if proj.Path == path {
s.config.Projects[i].UseProxy = useProxy
return config.SaveConfig(s.config)
}
}
return fmt.Errorf("项目不存在: %s", path)
} }
// GetCommitDiff 获取指定提交的 diff // GetCommitDiff 获取指定提交的 diff
@@ -581,9 +768,13 @@ func (s *AppService) GetCommitFileDiff(path, hash, filePath string) (string, err
return s.gitClient.CommitFileDiff(path, hash, filePath) return s.gitClient.CommitFileDiff(path, hash, filePath)
} }
// FetchProject 拉取远程信息 // FetchProject 拉取远程信息(指定 remote空则 fetch --all
func (s *AppService) FetchProject(path string) (string, error) { func (s *AppService) FetchProject(path, remote string) (string, error) {
return s.gitClient.Fetch(path) proxy := s.GetProjectProxy(path)
if remote == "" {
return s.gitClient.RunWithProxy(path, proxy, "fetch")
}
return s.gitClient.RunWithProxy(path, proxy, "fetch", remote)
} }
// CommitLog 提交记录 // CommitLog 提交记录
@@ -791,7 +982,7 @@ func (s *AppService) CreateTag(path, name, message string) error {
} }
// DeleteTag 删除标签(本地+远程) // DeleteTag 删除标签(本地+远程)
func (s *AppService) DeleteTag(path, name string) error { func (s *AppService) DeleteTag(path, name, remote string) error {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path) return fmt.Errorf("项目路径不存在: %s", path)
} }
@@ -799,17 +990,20 @@ func (s *AppService) DeleteTag(path, name string) error {
if name == "" { if name == "" {
return fmt.Errorf("标签名不能为空") return fmt.Errorf("标签名不能为空")
} }
if remote == "" {
remote = "origin"
}
// 删除本地标签 // 删除本地标签
if _, err := s.gitClient.DeleteTag(path, name); err != nil { if _, err := s.gitClient.DeleteTag(path, name); err != nil {
return fmt.Errorf("删除本地标签失败: %w", err) return fmt.Errorf("删除本地标签失败: %w", err)
} }
// 尝试删除远程标签(忽略错误,可能未推送过) // 尝试删除远程标签(忽略错误,可能未推送过)
s.gitClient.DeleteRemoteTag(path, name) s.gitClient.DeleteRemoteTagFrom(path, remote, name)
return nil return nil
} }
// PushTag 推送标签到远程 // PushTag 推送标签到远程
func (s *AppService) PushTag(path, name string) error { func (s *AppService) PushTag(path, name, remote string) error {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path) return fmt.Errorf("项目路径不存在: %s", path)
} }
@@ -817,7 +1011,10 @@ func (s *AppService) PushTag(path, name string) error {
if name == "" { if name == "" {
return fmt.Errorf("标签名不能为空") return fmt.Errorf("标签名不能为空")
} }
_, err := s.gitClient.PushTag(path, name) if remote == "" {
remote = "origin"
}
_, err := s.gitClient.PushTagTo(path, remote, name)
return err return err
} }
@@ -869,11 +1066,17 @@ func (s *AppService) MergeBranch(path, branch string) (string, error) {
// --- 远程分支管理 --- // --- 远程分支管理 ---
// GetRemoteBranches 获取远程分支列表 // GetRemoteBranches 获取远程分支列表
func (s *AppService) GetRemoteBranches(path string) ([]BranchInfo, error) { func (s *AppService) GetRemoteBranches(path, remote string) ([]BranchInfo, error) {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", path) return nil, fmt.Errorf("项目路径不存在: %s", path)
} }
out, err := s.gitClient.RemoteBranchList(path) var out string
var err error
if remote == "" {
out, err = s.gitClient.RemoteBranchList(path)
} else {
out, err = s.gitClient.RemoteBranchListByRemote(path, remote)
}
if err != nil { if err != nil {
return nil, fmt.Errorf("获取远程分支列表失败: %w", err) return nil, fmt.Errorf("获取远程分支列表失败: %w", err)
} }
@@ -907,7 +1110,7 @@ func (s *AppService) CheckoutRemoteBranch(path, remoteBranch string) error {
} }
// DeleteRemoteBranch 删除远程分支 // DeleteRemoteBranch 删除远程分支
func (s *AppService) DeleteRemoteBranch(path, branch string) error { func (s *AppService) DeleteRemoteBranch(path, branch, remote string) error {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path) return fmt.Errorf("项目路径不存在: %s", path)
} }
@@ -915,12 +1118,15 @@ func (s *AppService) DeleteRemoteBranch(path, branch string) error {
if branch == "" { if branch == "" {
return fmt.Errorf("分支名不能为空") return fmt.Errorf("分支名不能为空")
} }
if remote == "" {
remote = "origin"
}
// origin/feature -> feature // origin/feature -> feature
localName := branch localName := branch
if idx := strings.Index(branch, "/"); idx != -1 { if idx := strings.Index(branch, "/"); idx != -1 {
localName = branch[idx+1:] localName = branch[idx+1:]
} }
_, err := s.gitClient.DeleteRemoteBranch(path, localName) _, err := s.gitClient.DeleteRemoteBranchFrom(path, remote, localName)
return err return err
} }
@@ -1050,6 +1256,54 @@ func (s *AppService) UpdateAppSettings(logLevel string) error {
return config.SaveConfig(s.config) return config.SaveConfig(s.config)
} }
// --- 远程仓库管理 ---
// GetRemotes 获取项目所有远程仓库列表
func (s *AppService) GetRemotes(path string) ([]RemoteItem, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", path)
}
remoteList, err := s.gitClient.RemoteList(path)
if err != nil {
return nil, fmt.Errorf("获取远程仓库列表失败: %w", err)
}
var remotes []RemoteItem
for _, r := range remoteList {
remotes = append(remotes, RemoteItem{Name: r.Name, URL: r.URL})
}
return remotes, nil
}
// AddRemote 添加远程仓库
func (s *AppService) AddRemote(path, name, url string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
name = strings.TrimSpace(name)
url = strings.TrimSpace(url)
if name == "" {
return fmt.Errorf("远程名称不能为空")
}
if url == "" {
return fmt.Errorf("远程地址不能为空")
}
_, err := s.gitClient.AddRemote(path, name, url)
return err
}
// RemoveRemote 删除远程仓库
func (s *AppService) RemoveRemote(path, name string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
name = strings.TrimSpace(name)
if name == "" {
return fmt.Errorf("远程名称不能为空")
}
_, err := s.gitClient.RemoveRemote(path, name)
return err
}
// --- 冲突处理 --- // --- 冲突处理 ---
// ConflictFileInfo 冲突文件信息 // ConflictFileInfo 冲突文件信息
@@ -1174,11 +1428,9 @@ type ProjectOverview struct {
// GetAllProjectOverview 获取所有项目的概览状态 // GetAllProjectOverview 获取所有项目的概览状态
func (s *AppService) GetAllProjectOverview() []ProjectOverview { func (s *AppService) GetAllProjectOverview() []ProjectOverview {
var results []ProjectOverview var results []ProjectOverview
for platformName, platform := range s.config.Platforms { for _, proj := range s.config.Projects {
for _, user := range platform.Users {
for _, proj := range user.Projects {
overview := ProjectOverview{ overview := ProjectOverview{
Key: platformName + "/" + user.Username + "/" + proj.Name, Key: proj.Path,
Name: proj.Name, Name: proj.Name,
Path: proj.Path, Path: proj.Path,
} }
@@ -1197,8 +1449,6 @@ func (s *AppService) GetAllProjectOverview() []ProjectOverview {
} }
results = append(results, overview) results = append(results, overview)
} }
}
}
return results return results
} }
@@ -1211,7 +1461,10 @@ type BatchPullResult struct {
} }
// BatchPull 批量拉取指定项目 // BatchPull 批量拉取指定项目
func (s *AppService) BatchPull(paths []string) []BatchPullResult { func (s *AppService) BatchPull(paths []string, remote string) []BatchPullResult {
if remote == "" {
remote = "origin"
}
var results []BatchPullResult var results []BatchPullResult
for _, path := range paths { for _, path := range paths {
result := BatchPullResult{Path: path} result := BatchPullResult{Path: path}
@@ -1229,7 +1482,7 @@ func (s *AppService) BatchPull(paths []string) []BatchPullResult {
results = append(results, result) results = append(results, result)
continue continue
} }
_, err = s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch)) _, err = s.gitClient.PullFrom(path, remote, strings.TrimSpace(branch))
if err != nil { if err != nil {
result.Message = err.Error() result.Message = err.Error()
} else { } else {
@@ -1242,7 +1495,10 @@ func (s *AppService) BatchPull(paths []string) []BatchPullResult {
} }
// BatchPush 批量推送指定项目 // BatchPush 批量推送指定项目
func (s *AppService) BatchPush(paths []string) []BatchPullResult { func (s *AppService) BatchPush(paths []string, remote string) []BatchPullResult {
if remote == "" {
remote = "origin"
}
var results []BatchPullResult var results []BatchPullResult
for _, path := range paths { for _, path := range paths {
result := BatchPullResult{Path: path} result := BatchPullResult{Path: path}
@@ -1259,7 +1515,7 @@ func (s *AppService) BatchPush(paths []string) []BatchPullResult {
results = append(results, result) results = append(results, result)
continue continue
} }
_, err = s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch)) _, err = s.gitClient.PushTo(path, remote, strings.TrimSpace(branch))
if err != nil { if err != nil {
result.Message = err.Error() result.Message = err.Error()
} else { } else {

View File

@@ -29,10 +29,49 @@ func NewGitClient() *GitClient {
// Run 执行 git 命令,支持超时和代理设置 // Run 执行 git 命令,支持超时和代理设置
func (g *GitClient) Run(path string, args ...string) (string, error) { 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) ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
defer cancel() defer cancel()
header := []string{"-C", path, "-c", "core.quotePath=false"} 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) header = append(header, "-c", "http.proxy="+g.Proxy, "-c", "https.proxy="+g.Proxy)
} }
argsArr := append(header, args...) argsArr := append(header, args...)
@@ -60,16 +99,40 @@ func (g *GitClient) Push(path string) (string, error) {
return g.Run(path, "push") 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) { func (g *GitClient) Status(path string) (string, error) {
return g.Run(path, "status") return g.Run(path, "status")
} }
func (g *GitClient) Clone(repoURL, path string) (string, error) { func (g *GitClient) Clone(repoURL, path string) (string, error) {
return g.CloneWithProxy(repoURL, path, nil)
}
// CloneWithProxy 克隆仓库useProxy 可独立控制代理
func (g *GitClient) CloneWithProxy(repoURL, path string, useProxy *bool) (string, error) {
// Clone 操作可能需要较长时间使用独立的超时设置10 分钟) // Clone 操作可能需要较长时间使用独立的超时设置10 分钟)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel() defer cancel()
args := []string{"-c", "core.quotePath=false"} args := []string{"-c", "core.quotePath=false"}
if g.Enabled { 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, "-c", "http.proxy="+g.Proxy, "-c", "https.proxy="+g.Proxy)
} }
args = append(args, "clone", "--progress", repoURL, path) args = append(args, "clone", "--progress", repoURL, path)
@@ -93,6 +156,47 @@ func (g *GitClient) Fetch(path string) (string, error) {
return g.Run(path, "fetch", "--all") 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) { func (g *GitClient) RemoteURL(path string) (string, error) {
out, err := g.Run(path, "config", "--get", "remote.origin.url") out, err := g.Run(path, "config", "--get", "remote.origin.url")
if err != nil { if err != nil {
@@ -141,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) { func (g *GitClient) UnpushedCommits(path, branch string) (string, error) {
return g.Run(path, "rev-list", "origin/"+branch+"..HEAD") 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 撤回指定提交(创建一个反向提交) // RevertCommit 撤回指定提交(创建一个反向提交)
func (g *GitClient) RevertCommit(path, hash string) (string, error) { func (g *GitClient) RevertCommit(path, hash string) (string, error) {
return g.Run(path, "revert", "--no-edit", hash) return g.Run(path, "revert", "--no-edit", hash)
@@ -167,16 +276,26 @@ func (g *GitClient) DeleteTag(path, name string) (string, error) {
return g.Run(path, "tag", "-d", name) return g.Run(path, "tag", "-d", name)
} }
// PushTag 推送标签到远程 // PushTag 推送标签到远程(默认 origin
func (g *GitClient) PushTag(path, name string) (string, error) { func (g *GitClient) PushTag(path, name string) (string, error) {
return g.Run(path, "push", "origin", name) 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) { func (g *GitClient) DeleteRemoteTag(path, name string) (string, error) {
return g.Run(path, "push", "origin", "--delete", name) 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 获取所有本地分支 // BranchList 获取所有本地分支
func (g *GitClient) BranchList(path string) (string, error) { func (g *GitClient) BranchList(path string) (string, error) {
return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)") return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)")
@@ -368,16 +487,26 @@ func (g *GitClient) MergeBranch(path, branch string) (string, error) {
return g.Run(path, "merge", branch) return g.Run(path, "merge", branch)
} }
// DeleteRemoteBranch 删除远程分支 // DeleteRemoteBranch 删除远程分支(默认 origin
func (g *GitClient) DeleteRemoteBranch(path, branch string) (string, error) { func (g *GitClient) DeleteRemoteBranch(path, branch string) (string, error) {
return g.Run(path, "push", "origin", "--delete", branch) 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 获取所有远程分支 // RemoteBranchList 获取所有远程分支
func (g *GitClient) RemoteBranchList(path string) (string, error) { func (g *GitClient) RemoteBranchList(path string) (string, error) {
return g.Run(path, "branch", "-r", "--format=%(refname:short)") 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 从远程分支检出新本地分支 // CheckoutNewBranch 从远程分支检出新本地分支
func (g *GitClient) CheckoutNewBranch(path, localBranch, remoteBranch string) (string, error) { func (g *GitClient) CheckoutNewBranch(path, localBranch, remoteBranch string) (string, error) {
return g.Run(path, "checkout", "-b", localBranch, remoteBranch) return g.Run(path, "checkout", "-b", localBranch, remoteBranch)