Compare commits
2 Commits
v0.3.0
...
c787c99622
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c787c99622 | ||
|
|
3ec453cc38 |
70
CHANGELOG.md
Normal file
70
CHANGELOG.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v0.4.0 (2026-03-12)
|
||||||
|
|
||||||
|
### 🔧 架构重构
|
||||||
|
- **配置架构重构**:从 `Platform → User → Project` 层级结构重构为扁平的 `Projects + Groups + Credentials` 模型
|
||||||
|
- 项目支持多远程仓库指向不同平台,不再受限于单一平台层级
|
||||||
|
- 新增分组(Group)管理,支持自定义分组和图标
|
||||||
|
- 新增凭据(Credential)管理,支持 GitHub / Gitee / Gitea 等多平台凭据
|
||||||
|
- 侧边栏完全重写,适配新的分组→项目树结构
|
||||||
|
|
||||||
|
### ✨ 新功能
|
||||||
|
- **一键多端推送**:Push All 按钮,一键将当前分支推送到所有远程仓库
|
||||||
|
- 顶部操作栏新增 "Push All" 按钮(仅在有多个远程仓库时启用)
|
||||||
|
- 标签管理新增 "推送到所有远程" 按钮
|
||||||
|
- 推送结果逐一展示每个远程仓库的成功/失败状态
|
||||||
|
- **项目代理开关**:每个项目可独立控制是否使用系统代理
|
||||||
|
- 三种模式:跟随全局 / 强制使用代理 / 强制不使用代理
|
||||||
|
- 顶部操作栏新增代理设置下拉菜单
|
||||||
|
- Pull / Push / Fetch / Push All 等所有网络操作均尊重项目级代理设置
|
||||||
|
|
||||||
|
### 🔨 后端改进
|
||||||
|
- `GitClient` 新增 `RunWithProxy()` 方法,支持按调用级别覆盖全局代理设置
|
||||||
|
- 所有网络操作(Pull/Push/Fetch/PushToAllRemotes/PushTagToAllRemotes)均支持项目级代理
|
||||||
|
- 配置管理方法重构:Group CRUD / Credential CRUD / Project 分组管理
|
||||||
|
- 新增 `PushToAllRemotes()` / `PushTagToAllRemotes()` 服务方法
|
||||||
|
- 新增 `GetProjectProxy()` / `SetProjectProxy()` 服务方法
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.3.1 (2026-02-27)
|
||||||
|
|
||||||
|
### ✨ 新功能
|
||||||
|
- **多 Remote 远程仓库支持**
|
||||||
|
- 新增 RemoteList / AddRemote / RemoveRemote 远程仓库管理
|
||||||
|
- 所有远程操作支持指定 remote(Push/Pull/Fetch/PushTag/DeleteTag 等)
|
||||||
|
- 顶部信息栏新增 remote 选择器下拉菜单
|
||||||
|
- 支持切换当前操作的远程仓库、添加/删除远程仓库
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.3.0 (2026-02-25)
|
||||||
|
|
||||||
|
### ✨ 新功能
|
||||||
|
- **Git 安装检测**:启动时检测 Git 是否已安装,支持自动下载安装 Git for Windows
|
||||||
|
- **冲突处理**:合并冲突检测、冲突文件编辑器、标记已解决、中止/完成合并
|
||||||
|
- **提交搜索**:支持按提交信息关键字和作者搜索
|
||||||
|
- **批量操作**:侧边栏批量 Pull/Push 所有项目
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.2.0 (2026-02-20)
|
||||||
|
|
||||||
|
### ✨ 新功能
|
||||||
|
- 分支管理(创建/删除/合并)
|
||||||
|
- 远程分支(查看/检出/删除)
|
||||||
|
- Stash 贮藏管理
|
||||||
|
- Git 全局配置设置(user.name / user.email)
|
||||||
|
- Tag 标签管理
|
||||||
|
- CI/CD 发布流水线
|
||||||
|
- NSIS 安装包
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
- 初始版本
|
||||||
|
- 基本 Git 操作(Clone / Pull / Push / Commit)
|
||||||
|
- 文件变更查看和 Diff 展示
|
||||||
|
- 系统代理自动检测
|
||||||
121
README.md
121
README.md
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
A cross-platform desktop Git repository management tool built with **Go + Wails v3 + Nuxt 4 + Vue 3**.
|
A cross-platform desktop Git repository management tool built with **Go + Wails v3 + Nuxt 4 + Vue 3**.
|
||||||
|
|
||||||
Manage multiple Git repositories across different platforms (GitHub, Gitee, Gitea, etc.) from a single unified interface — like a lightweight, standalone version of VS Code's Git panel.
|
Manage multiple Git repositories across different platforms (GitHub, Gitee, Gitea, etc.) from a single unified interface — like a lightweight, standalone version of VS Code’s Git panel.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -14,10 +14,12 @@ Manage multiple Git repositories across different platforms (GitHub, Gitee, Gite
|
|||||||
|
|
||||||
## ✨ Features
|
## ✨ 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
|
||||||
|
|
||||||
|
|||||||
121
README_CN.md
121
README_CN.md
@@ -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 |
|
||||||
|
|
||||||
## 📄 开源协议
|
## 📄 开源协议
|
||||||
|
|
||||||
|
|||||||
@@ -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.0"
|
||||||
|
|
||||||
dev_mode:
|
dev_mode:
|
||||||
root_path: .
|
root_path: .
|
||||||
|
|||||||
@@ -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.0</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.0</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>icons</string>
|
<string>icons</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"fixed": {
|
"fixed": {
|
||||||
"file_version": "0.3.0"
|
"file_version": "0.4.0"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"0000": {
|
"0000": {
|
||||||
"ProductVersion": "0.3.0",
|
"ProductVersion": "0.4.0",
|
||||||
"CompanyName": "GitPilot",
|
"CompanyName": "GitPilot",
|
||||||
"FileDescription": "Git Repository Management Tool",
|
"FileDescription": "Git Repository Management Tool",
|
||||||
"LegalCopyright": "© 2025, GitPilot",
|
"LegalCopyright": "© 2025, GitPilot",
|
||||||
|
|||||||
36
config.yaml
36
config.yaml
@@ -1,22 +1,22 @@
|
|||||||
platforms:
|
projects:
|
||||||
gitea:
|
- name: GitPilot
|
||||||
|
path: F:\Project\GitPilot
|
||||||
|
group: github/zhuy1228
|
||||||
|
groups:
|
||||||
|
- name: github/zhuy1228
|
||||||
|
icon: github
|
||||||
|
- name: gitee/zhuyuj
|
||||||
|
icon: gitee
|
||||||
|
- name: gitea
|
||||||
|
icon: gitea
|
||||||
|
credentials:
|
||||||
|
- platform: github
|
||||||
|
username: zhuy1228
|
||||||
|
- platform: gitee
|
||||||
|
username: zhuyuj
|
||||||
|
- platform: gitea
|
||||||
base_url: http://192.168.1.10:3000
|
base_url: http://192.168.1.10:3000
|
||||||
users: []
|
username: ""
|
||||||
gitee:
|
|
||||||
base_url: ""
|
|
||||||
users:
|
|
||||||
- username: zhuyuj
|
|
||||||
token: ""
|
|
||||||
projects: []
|
|
||||||
github:
|
|
||||||
base_url: ""
|
|
||||||
users:
|
|
||||||
- username: zhuy1228
|
|
||||||
token: ""
|
|
||||||
projects:
|
|
||||||
- name: GitPilot
|
|
||||||
path: F:\Project\GitPilot
|
|
||||||
enabled: false
|
|
||||||
settings:
|
settings:
|
||||||
concurrency: 6
|
concurrency: 6
|
||||||
network_check: true
|
network_check: true
|
||||||
|
|||||||
@@ -8,32 +8,41 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Project struct {
|
// ProjectItem 项目配置(以路径为核心)
|
||||||
Name string `yaml:"name"`
|
type ProjectItem struct {
|
||||||
Path string `yaml:"path"`
|
Name string `yaml:"name"`
|
||||||
Enabled bool `yaml:"enabled"` // 可选,默认 true
|
Path string `yaml:"path"`
|
||||||
|
Group string `yaml:"group,omitempty"` // 所属分组名,可为空表示"未分组"
|
||||||
|
UseProxy *bool `yaml:"use_proxy,omitempty"` // 是否走系统代理,nil 表示跟随全局
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
// Group 项目分组
|
||||||
Username string `yaml:"username"`
|
type Group struct {
|
||||||
Token string `yaml:"token"` // 可选:未来扩展 API 功能
|
Name string `yaml:"name"`
|
||||||
Projects []Project `yaml:"projects"`
|
Icon string `yaml:"icon,omitempty"` // 可选图标标识
|
||||||
}
|
}
|
||||||
|
|
||||||
type Platform struct {
|
// Credential 凭证信息(独立于项目)
|
||||||
BaseURL string `yaml:"base_url"` // GitHub/Gitee 可为空,Gitea 需要
|
type Credential struct {
|
||||||
Users []User `yaml:"users"`
|
Platform string `yaml:"platform"` // github / gitee / gitea / ...
|
||||||
|
BaseURL string `yaml:"base_url,omitempty"` // 自建平台地址
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Token string `yaml:"token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings 应用设置
|
||||||
type Settings struct {
|
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"`
|
||||||
Settings Settings `yaml:"settings"`
|
Groups []Group `yaml:"groups"`
|
||||||
|
Credentials []Credential `yaml:"credentials"`
|
||||||
|
Settings Settings `yaml:"settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// configPath 返回 config.yaml 的绝对路径(与可执行文件同目录)
|
// configPath 返回 config.yaml 的绝对路径(与可执行文件同目录)
|
||||||
@@ -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()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ 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 +112,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 +260,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 +533,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 +547,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 +775,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 +809,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 +1230,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 +1243,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)
|
||||||
@@ -1157,6 +1257,63 @@ watch(activeTab, (tab) => {
|
|||||||
loadTags()
|
loadTags()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---- 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>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -1271,10 +1428,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 +1519,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>
|
||||||
@@ -1689,11 +1927,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>
|
||||||
@@ -1898,12 +2141,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;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { ref, onMounted, computed, h } from 'vue'
|
|||||||
import {
|
import {
|
||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
FolderOpenOutlined,
|
FolderOpenOutlined,
|
||||||
UserOutlined,
|
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
@@ -15,6 +14,9 @@ import {
|
|||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
BranchesOutlined,
|
BranchesOutlined,
|
||||||
|
KeyOutlined,
|
||||||
|
GroupOutlined,
|
||||||
|
SwapOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
||||||
@@ -31,13 +33,27 @@ const selectedKeys = ref([])
|
|||||||
|
|
||||||
// 添加项目弹窗
|
// 添加项目弹窗
|
||||||
const showAddDialog = ref(false)
|
const showAddDialog = ref(false)
|
||||||
const addForm = ref({ platform: '', username: '', name: '', path: '' })
|
const addForm = ref({ name: '', path: '', group: '' })
|
||||||
|
|
||||||
// 克隆项目弹窗
|
// 克隆项目弹窗
|
||||||
const showCloneDialog = ref(false)
|
const showCloneDialog = ref(false)
|
||||||
const cloneForm = ref({ platform: '', username: '', repoURL: '', parentDir: '', name: '' })
|
const cloneForm = ref({ repoURL: '', parentDir: '', name: '', group: '' })
|
||||||
const cloneLoading = ref(false)
|
const cloneLoading = ref(false)
|
||||||
|
|
||||||
|
// 分组弹窗
|
||||||
|
const showGroupDialog = ref(false)
|
||||||
|
const groupDialogMode = ref('add')
|
||||||
|
const groupForm = ref({ name: '', icon: '', oldName: '' })
|
||||||
|
|
||||||
|
// 凭证弹窗
|
||||||
|
const showCredentialDialog = ref(false)
|
||||||
|
const credentialDialogMode = ref('add')
|
||||||
|
const credentialForm = ref({ platform: '', baseUrl: '', username: '', token: '', oldPlatform: '', oldUsername: '' })
|
||||||
|
|
||||||
|
// 凭证列表弹窗
|
||||||
|
const showCredentialList = ref(false)
|
||||||
|
const credentials = ref([])
|
||||||
|
|
||||||
// ---- 批量操作 ----
|
// ---- 批量操作 ----
|
||||||
const showBatchModal = ref(false)
|
const showBatchModal = ref(false)
|
||||||
const batchOverviews = ref([])
|
const batchOverviews = ref([])
|
||||||
@@ -54,24 +70,23 @@ const batchResultMap = computed(() => {
|
|||||||
// 右键菜单
|
// 右键菜单
|
||||||
const contextMenu = ref({ visible: false, x: 0, y: 0, type: '', data: {} })
|
const contextMenu = ref({ visible: false, x: 0, y: 0, type: '', data: {} })
|
||||||
|
|
||||||
// 平台弹窗
|
// 获取所有分组名(用于下拉选择)
|
||||||
const showPlatformDialog = ref(false)
|
const groupNames = computed(() => {
|
||||||
const platformDialogMode = ref('add')
|
const names = []
|
||||||
const platformForm = ref({ name: '', baseUrl: '', oldName: '' })
|
;(tree.value || []).forEach(node => {
|
||||||
|
if (node.type === 'group') {
|
||||||
|
names.push(node.label)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return names
|
||||||
|
})
|
||||||
|
|
||||||
// 用户弹窗
|
// 构建 key → 原始节点 的映射
|
||||||
const showUserDialog = ref(false)
|
|
||||||
const userDialogMode = ref('add')
|
|
||||||
const userForm = ref({ platform: '', username: '', token: '', oldUsername: '' })
|
|
||||||
|
|
||||||
// 构建 key → 原始节点 的映射,避免 a-tree event node 丢失自定义字段
|
|
||||||
const nodeMap = computed(() => {
|
const nodeMap = computed(() => {
|
||||||
const map = {}
|
const map = {}
|
||||||
;(tree.value || []).forEach(platform => {
|
;(tree.value || []).forEach(group => {
|
||||||
platform.children?.forEach(user => {
|
group.children?.forEach(proj => {
|
||||||
user.children?.forEach(proj => {
|
map[proj.key] = proj
|
||||||
map[proj.key] = proj
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
@@ -79,26 +94,18 @@ const nodeMap = computed(() => {
|
|||||||
|
|
||||||
// 转换后端树数据为 a-tree 格式
|
// 转换后端树数据为 a-tree 格式
|
||||||
const treeData = computed(() => {
|
const treeData = computed(() => {
|
||||||
return (tree.value || []).map(platform => ({
|
return (tree.value || []).map(group => ({
|
||||||
key: platform.key,
|
key: group.key,
|
||||||
title: platform.label,
|
title: group.label,
|
||||||
type: 'platform',
|
type: 'group',
|
||||||
|
icon: group.icon,
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
children: (platform.children || []).map(user => ({
|
children: (group.children || []).map(proj => ({
|
||||||
key: user.key,
|
key: proj.key,
|
||||||
title: user.label,
|
title: proj.label,
|
||||||
type: 'user',
|
type: 'project',
|
||||||
platformKey: platform.key,
|
path: proj.path,
|
||||||
isLeaf: false,
|
isLeaf: true,
|
||||||
children: (user.children || []).map(proj => ({
|
|
||||||
key: proj.key,
|
|
||||||
title: proj.label,
|
|
||||||
type: 'project',
|
|
||||||
path: proj.path,
|
|
||||||
platformKey: platform.key,
|
|
||||||
username: user.label,
|
|
||||||
isLeaf: true,
|
|
||||||
}))
|
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@@ -110,11 +117,6 @@ async function loadTree() {
|
|||||||
const keys = []
|
const keys = []
|
||||||
tree.value.forEach(node => {
|
tree.value.forEach(node => {
|
||||||
keys.push(node.key)
|
keys.push(node.key)
|
||||||
if (node.children) {
|
|
||||||
node.children.forEach(child => {
|
|
||||||
keys.push(child.key)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
expandedKeys.value = keys
|
expandedKeys.value = keys
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -125,10 +127,8 @@ async function loadTree() {
|
|||||||
function onTreeSelect(keys, { node }) {
|
function onTreeSelect(keys, { node }) {
|
||||||
if (node.type === 'project') {
|
if (node.type === 'project') {
|
||||||
selectedKeys.value = keys
|
selectedKeys.value = keys
|
||||||
// 从 nodeMap 中取 path,避免 a-tree 事件节点丢失自定义字段
|
|
||||||
const raw = nodeMap.value[node.key]
|
const raw = nodeMap.value[node.key]
|
||||||
const path = raw?.path || node.path || ''
|
const path = raw?.path || node.path || ''
|
||||||
console.log('[Sidebar] select project:', node.key, 'path:', path)
|
|
||||||
emit('select-project', { key: node.key, label: node.title || raw?.label, path, type: 'project' })
|
emit('select-project', { key: node.key, label: node.title || raw?.label, path, type: 'project' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,15 +138,11 @@ function onRightClick({ event, node }) {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const type = node.type
|
const type = node.type
|
||||||
const data = {}
|
const data = {}
|
||||||
if (type === 'platform') {
|
if (type === 'group') {
|
||||||
data.key = node.key
|
data.groupName = node.title
|
||||||
} else if (type === 'user') {
|
|
||||||
data.platformKey = node.platformKey
|
|
||||||
data.username = node.title
|
|
||||||
} else if (type === 'project') {
|
} else if (type === 'project') {
|
||||||
data.platformKey = node.platformKey
|
|
||||||
data.username = node.username
|
|
||||||
data.name = node.title
|
data.name = node.title
|
||||||
|
data.path = node.path
|
||||||
}
|
}
|
||||||
contextMenu.value = { visible: true, x: event.clientX, y: event.clientY, type, data }
|
contextMenu.value = { visible: true, x: event.clientX, y: event.clientY, type, data }
|
||||||
}
|
}
|
||||||
@@ -164,40 +160,36 @@ function onContextMenuClick({ key: action }) {
|
|||||||
const { type, data } = contextMenu.value
|
const { type, data } = contextMenu.value
|
||||||
hideContextMenu()
|
hideContextMenu()
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'add-platform': openAddPlatformDialog(); break
|
case 'add-group': openAddGroupDialog(); break
|
||||||
case 'add-user': openAddUserDialog(data.key || data.platformKey); break
|
case 'edit-group': openEditGroupDialog(data.groupName); break
|
||||||
case 'edit-platform': openEditPlatformDialog(data.key); break
|
case 'remove-group': removeGroup(data.groupName); break
|
||||||
case 'remove-platform': removePlatform(data.key); break
|
case 'add-project': openAddDialog(data.groupName); break
|
||||||
case 'add-project': openAddDialog(data.platformKey, data.username); break
|
case 'clone-project': openCloneDialog(data.groupName); break
|
||||||
case 'clone-project': openCloneDialog(data.platformKey, data.username); break
|
case 'remove-project': removeProject(data.path); break
|
||||||
case 'edit-user': openEditUserDialog(data.platformKey, data.username); break
|
case 'manage-credentials': openCredentialList(); break
|
||||||
case 'remove-user': removeUser(data.platformKey, data.username); break
|
|
||||||
case 'remove-project': removeProject(data.platformKey, data.username, data.name); break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextMenuItems = computed(() => {
|
const contextMenuItems = computed(() => {
|
||||||
const { type } = contextMenu.value
|
const { type } = contextMenu.value
|
||||||
if (type === 'empty') {
|
if (type === 'empty') {
|
||||||
return [{ key: 'add-platform', label: '添加平台', icon: h(PlusOutlined) }]
|
|
||||||
}
|
|
||||||
if (type === 'platform') {
|
|
||||||
return [
|
return [
|
||||||
{ key: 'add-user', label: '添加用户', icon: h(UserOutlined) },
|
{ key: 'add-group', label: '添加分组', icon: h(GroupOutlined) },
|
||||||
{ key: 'edit-platform', label: '编辑平台', icon: h(EditOutlined) },
|
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
|
||||||
|
{ key: 'clone-project', label: '克隆项目', icon: h(CloudDownloadOutlined) },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ key: 'add-platform', label: '添加平台', icon: h(PlusOutlined) },
|
{ key: 'manage-credentials', label: '凭证管理', icon: h(KeyOutlined) },
|
||||||
{ type: 'divider' },
|
|
||||||
{ key: 'remove-platform', label: '删除平台', danger: true, icon: h(DeleteOutlined) },
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
if (type === 'user') {
|
if (type === 'group') {
|
||||||
return [
|
return [
|
||||||
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
|
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
|
||||||
{ key: 'clone-project', label: '克隆项目', icon: h(CloudDownloadOutlined) },
|
{ key: 'clone-project', label: '克隆项目', icon: h(CloudDownloadOutlined) },
|
||||||
{ key: 'edit-user', label: '编辑用户', icon: h(EditOutlined) },
|
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ key: 'remove-user', label: '删除用户', danger: true, icon: h(DeleteOutlined) },
|
{ key: 'edit-group', label: '编辑分组', icon: h(EditOutlined) },
|
||||||
|
{ key: 'add-group', label: '添加分组', icon: h(GroupOutlined) },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'remove-group', label: '删除分组', danger: true, icon: h(DeleteOutlined) },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
if (type === 'project') {
|
if (type === 'project') {
|
||||||
@@ -211,109 +203,49 @@ const contextMenuItems = computed(() => {
|
|||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- 平台操作 ---
|
// --- 分组操作 ---
|
||||||
function openAddPlatformDialog() {
|
function openAddGroupDialog() {
|
||||||
platformDialogMode.value = 'add'
|
groupDialogMode.value = 'add'
|
||||||
platformForm.value = { name: '', baseUrl: '', oldName: '' }
|
groupForm.value = { name: '', icon: 'folder', oldName: '' }
|
||||||
showPlatformDialog.value = true
|
showGroupDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openEditPlatformDialog(platformKey) {
|
function openEditGroupDialog(groupName) {
|
||||||
try {
|
groupDialogMode.value = 'edit'
|
||||||
const info = await AppService.GetPlatformInfo(platformKey)
|
groupForm.value = { name: groupName, icon: '', oldName: groupName }
|
||||||
platformDialogMode.value = 'edit'
|
showGroupDialog.value = true
|
||||||
platformForm.value = { name: info.name, baseUrl: info.baseUrl || '', oldName: info.name }
|
|
||||||
showPlatformDialog.value = true
|
|
||||||
} catch (e) {
|
|
||||||
console.error('获取平台信息失败:', e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function savePlatform() {
|
async function saveGroup() {
|
||||||
if (!platformForm.value.name) return
|
if (!groupForm.value.name) return
|
||||||
try {
|
try {
|
||||||
if (platformDialogMode.value === 'add') {
|
if (groupDialogMode.value === 'add') {
|
||||||
await AppService.AddPlatform(platformForm.value.name, platformForm.value.baseUrl)
|
await AppService.AddGroup(groupForm.value.name, groupForm.value.icon || 'folder')
|
||||||
} else {
|
} else {
|
||||||
await AppService.UpdatePlatform(platformForm.value.oldName, platformForm.value.baseUrl)
|
await AppService.UpdateGroup(groupForm.value.oldName, groupForm.value.name, groupForm.value.icon)
|
||||||
}
|
}
|
||||||
showPlatformDialog.value = false
|
showGroupDialog.value = false
|
||||||
await loadTree()
|
await loadTree()
|
||||||
emit('tree-updated')
|
emit('tree-updated')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('操作失败:', e)
|
message.error('操作失败: ' + String(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removePlatform(name) {
|
async function removeGroup(name) {
|
||||||
if (!confirm(`确定删除平台 "${name}" 及其所有用户和项目吗?`)) return
|
if (!confirm(`确定删除分组 "${name}" 吗?分组下的项目将移到"未分组"。`)) return
|
||||||
try {
|
try {
|
||||||
await AppService.RemovePlatform(name)
|
await AppService.RemoveGroup(name)
|
||||||
await loadTree()
|
await loadTree()
|
||||||
emit('tree-updated')
|
emit('tree-updated')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('删除失败:', e)
|
message.error('删除失败: ' + String(e))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 用户操作 ---
|
|
||||||
function openAddUserDialog(platformKey) {
|
|
||||||
userDialogMode.value = 'add'
|
|
||||||
userForm.value = { platform: platformKey, username: '', token: '', oldUsername: '' }
|
|
||||||
showUserDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openEditUserDialog(platformKey, username) {
|
|
||||||
try {
|
|
||||||
const info = await AppService.GetUserInfo(platformKey, username)
|
|
||||||
userDialogMode.value = 'edit'
|
|
||||||
userForm.value = {
|
|
||||||
platform: platformKey,
|
|
||||||
username: info.username,
|
|
||||||
token: info.token || '',
|
|
||||||
oldUsername: info.username
|
|
||||||
}
|
|
||||||
showUserDialog.value = true
|
|
||||||
} catch (e) {
|
|
||||||
console.error('获取用户信息失败:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveUser() {
|
|
||||||
if (!userForm.value.username) return
|
|
||||||
try {
|
|
||||||
if (userDialogMode.value === 'add') {
|
|
||||||
await AppService.AddUser(userForm.value.platform, userForm.value.username, userForm.value.token)
|
|
||||||
} else {
|
|
||||||
await AppService.UpdateUser(
|
|
||||||
userForm.value.platform,
|
|
||||||
userForm.value.oldUsername,
|
|
||||||
userForm.value.username,
|
|
||||||
userForm.value.token
|
|
||||||
)
|
|
||||||
}
|
|
||||||
showUserDialog.value = false
|
|
||||||
await loadTree()
|
|
||||||
emit('tree-updated')
|
|
||||||
} catch (e) {
|
|
||||||
console.error('操作失败:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeUser(platform, username) {
|
|
||||||
if (!confirm(`确定删除用户 "${username}" 及其所有项目吗?`)) return
|
|
||||||
try {
|
|
||||||
await AppService.RemoveUser(platform, username)
|
|
||||||
await loadTree()
|
|
||||||
emit('tree-updated')
|
|
||||||
} catch (e) {
|
|
||||||
console.error('删除失败:', e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 项目操作 ---
|
// --- 项目操作 ---
|
||||||
function openAddDialog(platformKey, username) {
|
function openAddDialog(groupName) {
|
||||||
addForm.value = { platform: platformKey, username: username, name: '', path: '' }
|
addForm.value = { name: '', path: '', group: groupName || '' }
|
||||||
showAddDialog.value = true
|
showAddDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +254,6 @@ async function pickDirectory() {
|
|||||||
const path = await AppService.SelectDirectory()
|
const path = await AppService.SelectDirectory()
|
||||||
if (path) {
|
if (path) {
|
||||||
addForm.value.path = path
|
addForm.value.path = path
|
||||||
// 如果项目名称为空,自动用文件夹名称填充
|
|
||||||
if (!addForm.value.name) {
|
if (!addForm.value.name) {
|
||||||
const parts = path.replace(/\\/g, '/').split('/')
|
const parts = path.replace(/\\/g, '/').split('/')
|
||||||
addForm.value.name = parts[parts.length - 1] || ''
|
addForm.value.name = parts[parts.length - 1] || ''
|
||||||
@@ -336,23 +267,18 @@ async function pickDirectory() {
|
|||||||
async function addProject() {
|
async function addProject() {
|
||||||
if (!addForm.value.name || !addForm.value.path) return
|
if (!addForm.value.name || !addForm.value.path) return
|
||||||
try {
|
try {
|
||||||
await AppService.AddProject(
|
await AppService.AddProject(addForm.value.name, addForm.value.path, addForm.value.group)
|
||||||
addForm.value.platform,
|
|
||||||
addForm.value.username,
|
|
||||||
addForm.value.name,
|
|
||||||
addForm.value.path
|
|
||||||
)
|
|
||||||
showAddDialog.value = false
|
showAddDialog.value = false
|
||||||
await loadTree()
|
await loadTree()
|
||||||
emit('tree-updated')
|
emit('tree-updated')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('添加项目失败:', e)
|
message.error('添加项目失败: ' + String(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 克隆项目 ---
|
// --- 克隆项目 ---
|
||||||
function openCloneDialog(platformKey, username) {
|
function openCloneDialog(groupName) {
|
||||||
cloneForm.value = { platform: platformKey, username: username, repoURL: '', parentDir: '', name: '' }
|
cloneForm.value = { repoURL: '', parentDir: '', name: '', group: groupName || '' }
|
||||||
showCloneDialog.value = true
|
showCloneDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +294,6 @@ async function pickCloneDirectory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onRepoURLChange() {
|
function onRepoURLChange() {
|
||||||
// 从仓库 URL 自动提取项目名称
|
|
||||||
if (!cloneForm.value.name && cloneForm.value.repoURL) {
|
if (!cloneForm.value.name && cloneForm.value.repoURL) {
|
||||||
const url = cloneForm.value.repoURL.trim()
|
const url = cloneForm.value.repoURL.trim()
|
||||||
const match = url.match(/\/([^\/]+?)(\.git)?$/)
|
const match = url.match(/\/([^\/]+?)(\.git)?$/)
|
||||||
@@ -383,32 +308,96 @@ async function cloneProject() {
|
|||||||
cloneLoading.value = true
|
cloneLoading.value = true
|
||||||
try {
|
try {
|
||||||
await AppService.CloneProject(
|
await AppService.CloneProject(
|
||||||
cloneForm.value.platform,
|
|
||||||
cloneForm.value.username,
|
|
||||||
cloneForm.value.repoURL,
|
cloneForm.value.repoURL,
|
||||||
cloneForm.value.parentDir,
|
cloneForm.value.parentDir,
|
||||||
cloneForm.value.name
|
cloneForm.value.name,
|
||||||
|
cloneForm.value.group
|
||||||
)
|
)
|
||||||
showCloneDialog.value = false
|
showCloneDialog.value = false
|
||||||
message.success('克隆成功')
|
message.success('克隆成功')
|
||||||
await loadTree()
|
await loadTree()
|
||||||
emit('tree-updated')
|
emit('tree-updated')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('克隆项目失败:', e)
|
|
||||||
message.error('克隆失败: ' + String(e))
|
message.error('克隆失败: ' + String(e))
|
||||||
} finally {
|
} finally {
|
||||||
cloneLoading.value = false
|
cloneLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeProject(platform, username, name) {
|
async function removeProject(path) {
|
||||||
|
const name = path.split(/[/\\]/).pop()
|
||||||
if (!confirm(`确定删除项目 "${name}" 吗?`)) return
|
if (!confirm(`确定删除项目 "${name}" 吗?`)) return
|
||||||
try {
|
try {
|
||||||
await AppService.RemoveProject(platform, username, name)
|
await AppService.RemoveProject(path)
|
||||||
await loadTree()
|
await loadTree()
|
||||||
emit('tree-updated')
|
emit('tree-updated')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('删除项目失败:', e)
|
message.error('删除项目失败: ' + String(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 凭证管理 ---
|
||||||
|
async function openCredentialList() {
|
||||||
|
try {
|
||||||
|
const list = await AppService.GetCredentials()
|
||||||
|
credentials.value = list || []
|
||||||
|
} catch (e) {
|
||||||
|
credentials.value = []
|
||||||
|
}
|
||||||
|
showCredentialList.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAddCredentialDialog() {
|
||||||
|
credentialDialogMode.value = 'add'
|
||||||
|
credentialForm.value = { platform: '', baseUrl: '', username: '', token: '', oldPlatform: '', oldUsername: '' }
|
||||||
|
showCredentialDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditCredentialDialog(cred) {
|
||||||
|
credentialDialogMode.value = 'edit'
|
||||||
|
credentialForm.value = {
|
||||||
|
platform: cred.platform,
|
||||||
|
baseUrl: cred.baseUrl || '',
|
||||||
|
username: cred.username,
|
||||||
|
token: cred.token || '',
|
||||||
|
oldPlatform: cred.platform,
|
||||||
|
oldUsername: cred.username
|
||||||
|
}
|
||||||
|
showCredentialDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCredential() {
|
||||||
|
if (!credentialForm.value.platform || !credentialForm.value.username) return
|
||||||
|
try {
|
||||||
|
if (credentialDialogMode.value === 'add') {
|
||||||
|
await AppService.AddCredential(
|
||||||
|
credentialForm.value.platform,
|
||||||
|
credentialForm.value.baseUrl,
|
||||||
|
credentialForm.value.username,
|
||||||
|
credentialForm.value.token
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await AppService.UpdateCredential(
|
||||||
|
credentialForm.value.oldPlatform,
|
||||||
|
credentialForm.value.oldUsername,
|
||||||
|
credentialForm.value.baseUrl,
|
||||||
|
credentialForm.value.token
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showCredentialDialog.value = false
|
||||||
|
await openCredentialList()
|
||||||
|
} catch (e) {
|
||||||
|
message.error('操作失败: ' + String(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeCredential(platform, username) {
|
||||||
|
if (!confirm(`确定删除凭证 "${platform}/${username}" 吗?`)) return
|
||||||
|
try {
|
||||||
|
await AppService.RemoveCredential(platform, username)
|
||||||
|
await openCredentialList()
|
||||||
|
} catch (e) {
|
||||||
|
message.error('删除失败: ' + String(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,7 +446,7 @@ async function doBatchPull() {
|
|||||||
if (!selectedBatchPaths.value.length) return
|
if (!selectedBatchPaths.value.length) return
|
||||||
batchActionLoading.value = 'pull'
|
batchActionLoading.value = 'pull'
|
||||||
try {
|
try {
|
||||||
const results = await AppService.BatchPull(selectedBatchPaths.value)
|
const results = await AppService.BatchPull(selectedBatchPaths.value, 'origin')
|
||||||
batchResults.value = Array.isArray(results) ? results : []
|
batchResults.value = Array.isArray(results) ? results : []
|
||||||
const successCount = batchResults.value.filter(r => r.success).length
|
const successCount = batchResults.value.filter(r => r.success).length
|
||||||
message.info(`批量 Pull 完成:${successCount}/${batchResults.value.length} 成功`)
|
message.info(`批量 Pull 完成:${successCount}/${batchResults.value.length} 成功`)
|
||||||
@@ -473,7 +462,7 @@ async function doBatchPush() {
|
|||||||
if (!selectedBatchPaths.value.length) return
|
if (!selectedBatchPaths.value.length) return
|
||||||
batchActionLoading.value = 'push'
|
batchActionLoading.value = 'push'
|
||||||
try {
|
try {
|
||||||
const results = await AppService.BatchPush(selectedBatchPaths.value)
|
const results = await AppService.BatchPush(selectedBatchPaths.value, 'origin')
|
||||||
batchResults.value = Array.isArray(results) ? results : []
|
batchResults.value = Array.isArray(results) ? results : []
|
||||||
const successCount = batchResults.value.filter(r => r.success).length
|
const successCount = batchResults.value.filter(r => r.success).length
|
||||||
message.info(`批量 Push 完成:${successCount}/${batchResults.value.length} 成功`)
|
message.info(`批量 Push 完成:${successCount}/${batchResults.value.length} 成功`)
|
||||||
@@ -485,17 +474,15 @@ async function doBatchPush() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 平台 SVG Logo
|
// 分组图标
|
||||||
const platformLogos = {
|
const groupIcons = {
|
||||||
github: `<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>`,
|
github: `<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>`,
|
||||||
gitee: `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z"/></svg>`,
|
gitee: `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z"/></svg>`,
|
||||||
gitea: `<svg viewBox="0 0 640 640" fill="currentColor"><path d="M395.022 297.778c-13.158 0-23.822 10.664-23.822 23.822s10.664 23.822 23.822 23.822 23.822-10.664 23.822-23.822-10.664-23.822-23.822-23.822zM243.2 297.778c-13.158 0-23.822 10.664-23.822 23.822s10.664 23.822 23.822 23.822 23.822-10.664 23.822-23.822-10.664-23.822-23.822-23.822zM319.111 22.756C158.578 22.756 28.444 152.889 28.444 313.422s130.133 290.667 290.667 290.667 290.667-130.133 290.667-290.667S479.644 22.756 319.111 22.756zm165.689 361.244c0 5.333-0.711 10.667-1.778 15.644-20.267 93.867-148.089 167.111-305.067 167.111a360.604 360.604 0 0 1-65.778-5.689c-21.333 18.133-56.889 37.689-101.333 49.422-7.822 2.133-16 3.911-24.889 5.333h-0.711c-4.267 0-7.822-3.556-8.889-7.822v-0.356c-1.067-4.622 2.133-7.467 4.978-10.667 17.067-18.844 36.622-34.844 48-71.467-51.911-36.267-83.911-85.067-83.911-139.733 0-106.311 107.378-192.356 239.644-192.356 118.4 0 220.089 68.267 238.578 160.356 1.778 6.4 3.556 14.578 3.556 22.4-0.356 2.489-0.356 5.333-0.356 7.822z"/></svg>`,
|
gitea: `<svg viewBox="0 0 640 640" fill="currentColor"><path d="M395.022 297.778c-13.158 0-23.822 10.664-23.822 23.822s10.664 23.822 23.822 23.822 23.822-10.664 23.822-23.822-10.664-23.822-23.822-23.822zM243.2 297.778c-13.158 0-23.822 10.664-23.822 23.822s10.664 23.822 23.822 23.822 23.822-10.664 23.822-23.822-10.664-23.822-23.822-23.822zM319.111 22.756C158.578 22.756 28.444 152.889 28.444 313.422s130.133 290.667 290.667 290.667 290.667-130.133 290.667-290.667S479.644 22.756 319.111 22.756zm165.689 361.244c0 5.333-0.711 10.667-1.778 15.644-20.267 93.867-148.089 167.111-305.067 167.111a360.604 360.604 0 0 1-65.778-5.689c-21.333 18.133-56.889 37.689-101.333 49.422-7.822 2.133-16 3.911-24.889 5.333h-0.711c-4.267 0-7.822-3.556-8.889-7.822v-0.356c-1.067-4.622 2.133-7.467 4.978-10.667 17.067-18.844 36.622-34.844 48-71.467-51.911-36.267-83.911-85.067-83.911-139.733 0-106.311 107.378-192.356 239.644-192.356 118.4 0 220.089 68.267 238.578 160.356 1.778 6.4 3.556 14.578 3.556 22.4-0.356 2.489-0.356 5.333-0.356 7.822z"/></svg>`,
|
||||||
default: `<svg viewBox="0 0 16 16" fill="currentColor"><path d="M15 5.6c0-.3-.2-.6-.5-.7L8.3.2a.6.6 0 0 0-.6 0L1.5 4.9c-.3.1-.5.4-.5.7v5.8c0 .3.2.6.5.7l6.2 3.7c.2.1.4.1.6 0l6.2-3.7c.3-.1.5-.4.5-.7V5.6zM8 1.2l5.2 3.1L8 7.5 2.8 4.3 8 1.2zm-6 4.5l5.5 3.2v6.3L2 12V5.7zm7 9.5V9l5.5-3.2V12L9 15.2z"/></svg>`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlatformColor(name) {
|
function getGroupIcon(icon) {
|
||||||
const colors = { github: '#e6edf3', gitee: '#c71d23', gitea: '#609926' }
|
return groupIcons[icon] || null
|
||||||
return colors[name] || 'var(--text-secondary)'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -511,7 +498,7 @@ onMounted(() => {
|
|||||||
<a-button type="text" size="small" @click="openBatchModal" title="批量操作">
|
<a-button type="text" size="small" @click="openBatchModal" title="批量操作">
|
||||||
<template #icon><AppstoreOutlined /></template>
|
<template #icon><AppstoreOutlined /></template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="text" size="small" @click="openAddPlatformDialog" title="添加平台">
|
<a-button type="text" size="small" @click="openAddGroupDialog" title="添加分组">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon><PlusOutlined /></template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -529,21 +516,20 @@ onMounted(() => {
|
|||||||
@rightClick="onRightClick"
|
@rightClick="onRightClick"
|
||||||
@contextmenu.stop
|
@contextmenu.stop
|
||||||
>
|
>
|
||||||
<template #title="{ title, type, key }">
|
<template #title="{ title, type, icon }">
|
||||||
<div class="tree-node-title">
|
<div class="tree-node-title">
|
||||||
<span
|
<span
|
||||||
v-if="type === 'platform'"
|
v-if="type === 'group' && getGroupIcon(icon)"
|
||||||
class="platform-logo"
|
class="group-logo"
|
||||||
:style="{ color: getPlatformColor(key) }"
|
v-html="getGroupIcon(icon)"
|
||||||
v-html="platformLogos[key] || platformLogos.default"
|
|
||||||
></span>
|
></span>
|
||||||
<UserOutlined v-else-if="type === 'user'" class="node-icon" />
|
<GroupOutlined v-else-if="type === 'group'" class="node-icon" />
|
||||||
<FolderOutlined v-else class="node-icon" />
|
<FolderOutlined v-else class="node-icon" />
|
||||||
<span class="node-label">{{ title }}</span>
|
<span class="node-label">{{ title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-tree>
|
</a-tree>
|
||||||
<div v-else class="empty-tip">右键可添加平台</div>
|
<div v-else class="empty-tip">右键添加分组或项目</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右键菜单 -->
|
<!-- 右键菜单 -->
|
||||||
@@ -577,12 +563,6 @@ onMounted(() => {
|
|||||||
:width="420"
|
:width="420"
|
||||||
>
|
>
|
||||||
<a-form layout="vertical" :style="{ marginTop: '16px' }">
|
<a-form layout="vertical" :style="{ marginTop: '16px' }">
|
||||||
<a-form-item label="平台">
|
|
||||||
<a-input :value="addForm.platform" disabled />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="用户">
|
|
||||||
<a-input :value="addForm.username" disabled />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="项目名称">
|
<a-form-item label="项目名称">
|
||||||
<a-input v-model:value="addForm.name" placeholder="my-project" />
|
<a-input v-model:value="addForm.name" placeholder="my-project" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -597,6 +577,11 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item label="分组">
|
||||||
|
<a-select v-model:value="addForm.group" placeholder="选择分组(可选)" allow-clear>
|
||||||
|
<a-select-option v-for="g in groupNames" :key="g" :value="g">{{ g }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
@@ -613,12 +598,6 @@ onMounted(() => {
|
|||||||
:width="480"
|
:width="480"
|
||||||
>
|
>
|
||||||
<a-form layout="vertical" :style="{ marginTop: '16px' }">
|
<a-form layout="vertical" :style="{ marginTop: '16px' }">
|
||||||
<a-form-item label="平台">
|
|
||||||
<a-input :value="cloneForm.platform" disabled />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="用户">
|
|
||||||
<a-input :value="cloneForm.username" disabled />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="仓库地址" required>
|
<a-form-item label="仓库地址" required>
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="cloneForm.repoURL"
|
v-model:value="cloneForm.repoURL"
|
||||||
@@ -640,6 +619,11 @@ onMounted(() => {
|
|||||||
<a-form-item label="项目名称" required>
|
<a-form-item label="项目名称" required>
|
||||||
<a-input v-model:value="cloneForm.name" placeholder="自动从仓库地址提取" />
|
<a-input v-model:value="cloneForm.name" placeholder="自动从仓库地址提取" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item label="分组">
|
||||||
|
<a-select v-model:value="cloneForm.group" placeholder="选择分组(可选)" allow-clear>
|
||||||
|
<a-select-option v-for="g in groupNames" :key="g" :value="g">{{ g }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
<div v-if="cloneForm.parentDir && cloneForm.name" style="font-size: 12px; color: var(--text-muted); margin-top: -8px; margin-bottom: 8px;">
|
<div v-if="cloneForm.parentDir && cloneForm.name" style="font-size: 12px; color: var(--text-muted); margin-top: -8px; margin-bottom: 8px;">
|
||||||
<span>目标路径:</span>
|
<span>目标路径:</span>
|
||||||
<span style="color: var(--accent, #89b4fa);">{{ cloneForm.parentDir }}/{{ cloneForm.name }}</span>
|
<span style="color: var(--accent, #89b4fa);">{{ cloneForm.parentDir }}/{{ cloneForm.name }}</span>
|
||||||
@@ -647,53 +631,109 @@ onMounted(() => {
|
|||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<!-- 平台弹窗 -->
|
<!-- 分组弹窗 -->
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:open="showPlatformDialog"
|
v-model:open="showGroupDialog"
|
||||||
:title="platformDialogMode === 'add' ? '添加平台' : '编辑平台'"
|
:title="groupDialogMode === 'add' ? '添加分组' : '编辑分组'"
|
||||||
@ok="savePlatform"
|
@ok="saveGroup"
|
||||||
:ok-text="platformDialogMode === 'add' ? '添加' : '保存'"
|
:ok-text="groupDialogMode === 'add' ? '添加' : '保存'"
|
||||||
cancel-text="取消"
|
cancel-text="取消"
|
||||||
:width="420"
|
:width="420"
|
||||||
>
|
>
|
||||||
<a-form layout="vertical" :style="{ marginTop: '16px' }">
|
<a-form layout="vertical" :style="{ marginTop: '16px' }">
|
||||||
<a-form-item label="平台名称">
|
<a-form-item label="分组名称">
|
||||||
|
<a-input v-model:value="groupForm.name" placeholder="我的项目" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="图标">
|
||||||
|
<a-select v-model:value="groupForm.icon" placeholder="选择图标">
|
||||||
|
<a-select-option value="folder">📁 文件夹</a-select-option>
|
||||||
|
<a-select-option value="github">🐙 GitHub</a-select-option>
|
||||||
|
<a-select-option value="gitee">🟠 Gitee</a-select-option>
|
||||||
|
<a-select-option value="gitea">🍵 Gitea</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 凭证列表弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="showCredentialList"
|
||||||
|
title="凭证管理"
|
||||||
|
:footer="null"
|
||||||
|
:width="520"
|
||||||
|
>
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<a-button size="small" @click="openAddCredentialDialog">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
添加凭证
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!credentials.length" style="padding: 24px; text-align: center; color: var(--text-muted);">
|
||||||
|
暂无凭证
|
||||||
|
</div>
|
||||||
|
<div v-else class="credential-list">
|
||||||
|
<div v-for="cred in credentials" :key="cred.platform + '/' + cred.username" class="credential-item">
|
||||||
|
<div class="credential-info">
|
||||||
|
<div class="credential-name">
|
||||||
|
<a-tag :color="cred.platform === 'github' ? 'default' : cred.platform === 'gitee' ? 'red' : 'green'" size="small">
|
||||||
|
{{ cred.platform }}
|
||||||
|
</a-tag>
|
||||||
|
<span>{{ cred.username }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="cred.baseUrl" class="credential-url">{{ cred.baseUrl }}</div>
|
||||||
|
</div>
|
||||||
|
<a-space :size="4">
|
||||||
|
<a-button type="text" size="small" @click="openEditCredentialDialog(cred)">
|
||||||
|
<template #icon><EditOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
<a-button type="text" size="small" danger @click="removeCredential(cred.platform, cred.username)">
|
||||||
|
<template #icon><DeleteOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 凭证编辑弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="showCredentialDialog"
|
||||||
|
:title="credentialDialogMode === 'add' ? '添加凭证' : '编辑凭证'"
|
||||||
|
@ok="saveCredential"
|
||||||
|
:ok-text="credentialDialogMode === 'add' ? '添加' : '保存'"
|
||||||
|
cancel-text="取消"
|
||||||
|
:width="420"
|
||||||
|
>
|
||||||
|
<a-form layout="vertical" :style="{ marginTop: '16px' }">
|
||||||
|
<a-form-item label="平台">
|
||||||
|
<a-select
|
||||||
|
v-model:value="credentialForm.platform"
|
||||||
|
placeholder="选择平台"
|
||||||
|
:disabled="credentialDialogMode === 'edit'"
|
||||||
|
>
|
||||||
|
<a-select-option value="github">GitHub</a-select-option>
|
||||||
|
<a-select-option value="gitee">Gitee</a-select-option>
|
||||||
|
<a-select-option value="gitea">Gitea</a-select-option>
|
||||||
|
<a-select-option value="gitlab">GitLab</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="用户名">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="platformForm.name"
|
v-model:value="credentialForm.username"
|
||||||
placeholder="github / gitee / gitea"
|
placeholder="your-username"
|
||||||
:disabled="platformDialogMode === 'edit'"
|
:disabled="credentialDialogMode === 'edit'"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
Base URL <span class="form-hint">(自建平台需要填写)</span>
|
Base URL <span class="form-hint">(自建平台需要填写)</span>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="platformForm.baseUrl" placeholder="http://192.168.1.10:3000" />
|
<a-input v-model:value="credentialForm.baseUrl" placeholder="http://192.168.1.10:3000" />
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</a-modal>
|
|
||||||
|
|
||||||
<!-- 用户弹窗 -->
|
|
||||||
<a-modal
|
|
||||||
v-model:open="showUserDialog"
|
|
||||||
:title="userDialogMode === 'add' ? '添加用户' : '编辑用户'"
|
|
||||||
@ok="saveUser"
|
|
||||||
:ok-text="userDialogMode === 'add' ? '添加' : '保存'"
|
|
||||||
cancel-text="取消"
|
|
||||||
:width="420"
|
|
||||||
>
|
|
||||||
<a-form layout="vertical" :style="{ marginTop: '16px' }">
|
|
||||||
<a-form-item label="平台">
|
|
||||||
<a-input :value="userForm.platform" disabled />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="用户名">
|
|
||||||
<a-input v-model:value="userForm.username" placeholder="your-username" />
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
Token <span class="form-hint">(可选,用于 API 认证)</span>
|
Token <span class="form-hint">(可选,用于 API 认证)</span>
|
||||||
</template>
|
</template>
|
||||||
<a-input-password v-model:value="userForm.token" placeholder="ghp_xxxx / Bearer token" />
|
<a-input-password v-model:value="credentialForm.token" placeholder="ghp_xxxx / Bearer token" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@@ -815,7 +855,7 @@ onMounted(() => {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-logo {
|
.group-logo {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -824,7 +864,7 @@ onMounted(() => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-logo :deep(svg) {
|
.group-logo :deep(svg) {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
@@ -967,4 +1007,41 @@ onMounted(() => {
|
|||||||
0%, 100% { opacity: 0.4; }
|
0%, 100% { opacity: 0.4; }
|
||||||
50% { opacity: 0.8; }
|
50% { opacity: 0.8; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 凭证列表 */
|
||||||
|
.credential-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-item:hover {
|
||||||
|
background: var(--bg-hover, rgba(255,255,255,0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-url {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -32,45 +32,57 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 $$createType1($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -78,10 +90,11 @@ export function BatchPull(paths) {
|
|||||||
/**
|
/**
|
||||||
* 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 $$createType1($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -107,16 +120,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 +177,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 +206,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -297,6 +312,16 @@ export function GetConflictFiles(path) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetCredentials 获取所有凭证
|
||||||
|
* @returns {$CancellablePromise<$models.CredentialInfo[]>}
|
||||||
|
*/
|
||||||
|
export function GetCredentials() {
|
||||||
|
return $Call.ByID(4214051290).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType16($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GetFileContent 获取文件内容
|
* GetFileContent 获取文件内容
|
||||||
* @param {string} projectPath
|
* @param {string} projectPath
|
||||||
@@ -333,18 +358,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 $$createType18($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 $$createType20($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,10 +379,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 $$createType22($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,31 +399,43 @@ 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 $$createType24($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 $$createType26($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 $$createType8($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 $$createType28($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GetStashList 获取贮藏列表
|
* GetStashList 获取贮藏列表
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
@@ -398,7 +443,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 $$createType30($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,19 +454,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 $$createType32($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 +488,105 @@ export function MergeBranch(path, branch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PullProject 拉取项目(当前分支)
|
* MoveProjectToGroup 移动项目到指定分组
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @returns {$CancellablePromise<string>}
|
* @param {string} group
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
*/
|
*/
|
||||||
export function PullProject(path) {
|
export function MoveProjectToGroup(path, group) {
|
||||||
return $Call.ByID(4145813996, path);
|
return $Call.ByID(1699187666, path, group);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PushProject 推送项目(当前分支)
|
* PullProject 拉取项目(当前分支,指定 remote)
|
||||||
* @param {string} path
|
* @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 $$createType34($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 $$createType34($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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -603,6 +680,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
|
||||||
@@ -701,25 +789,26 @@ 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
|
||||||
@@ -738,19 +827,23 @@ const $$createType11 = $models.CommitLog.createFrom;
|
|||||||
const $$createType12 = $Create.Array($$createType11);
|
const $$createType12 = $Create.Array($$createType11);
|
||||||
const $$createType13 = $models.ConflictFileInfo.createFrom;
|
const $$createType13 = $models.ConflictFileInfo.createFrom;
|
||||||
const $$createType14 = $Create.Array($$createType13);
|
const $$createType14 = $Create.Array($$createType13);
|
||||||
const $$createType15 = $models.GitConfig.createFrom;
|
const $$createType15 = $models.CredentialInfo.createFrom;
|
||||||
const $$createType16 = $Create.Nullable($$createType15);
|
const $$createType16 = $Create.Array($$createType15);
|
||||||
const $$createType17 = $models.PlatformInfo.createFrom;
|
const $$createType17 = $models.GitConfig.createFrom;
|
||||||
const $$createType18 = $Create.Nullable($$createType17);
|
const $$createType18 = $Create.Nullable($$createType17);
|
||||||
const $$createType19 = $models.FileInfo.createFrom;
|
const $$createType19 = config$0.Group.createFrom;
|
||||||
const $$createType20 = $Create.Array($$createType19);
|
const $$createType20 = $Create.Array($$createType19);
|
||||||
const $$createType21 = $models.ProjectStatus.createFrom;
|
const $$createType21 = $models.FileInfo.createFrom;
|
||||||
const $$createType22 = $Create.Nullable($$createType21);
|
const $$createType22 = $Create.Array($$createType21);
|
||||||
const $$createType23 = $models.TreeNode.createFrom;
|
const $$createType23 = $models.ProjectStatus.createFrom;
|
||||||
const $$createType24 = $Create.Array($$createType23);
|
const $$createType24 = $Create.Nullable($$createType23);
|
||||||
const $$createType25 = $models.StashInfo.createFrom;
|
const $$createType25 = $models.TreeNode.createFrom;
|
||||||
const $$createType26 = $Create.Array($$createType25);
|
const $$createType26 = $Create.Array($$createType25);
|
||||||
const $$createType27 = $models.TagInfo.createFrom;
|
const $$createType27 = $models.RemoteItem.createFrom;
|
||||||
const $$createType28 = $Create.Array($$createType27);
|
const $$createType28 = $Create.Array($$createType27);
|
||||||
const $$createType29 = $models.UserInfo.createFrom;
|
const $$createType29 = $models.StashInfo.createFrom;
|
||||||
const $$createType30 = $Create.Nullable($$createType29);
|
const $$createType30 = $Create.Array($$createType29);
|
||||||
|
const $$createType31 = $models.TagInfo.createFrom;
|
||||||
|
const $$createType32 = $Create.Array($$createType31);
|
||||||
|
const $$createType33 = $models.PushAllResult.createFrom;
|
||||||
|
const $$createType34 = $Create.Array($$createType33);
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ export {
|
|||||||
CommitFileInfo,
|
CommitFileInfo,
|
||||||
CommitLog,
|
CommitLog,
|
||||||
ConflictFileInfo,
|
ConflictFileInfo,
|
||||||
|
CredentialInfo,
|
||||||
FileInfo,
|
FileInfo,
|
||||||
GitConfig,
|
GitConfig,
|
||||||
GitStatus,
|
GitStatus,
|
||||||
PlatformInfo,
|
|
||||||
ProjectOverview,
|
ProjectOverview,
|
||||||
ProjectStatus,
|
ProjectStatus,
|
||||||
|
PushAllResult,
|
||||||
|
RemoteItem,
|
||||||
StashInfo,
|
StashInfo,
|
||||||
TagInfo,
|
TagInfo,
|
||||||
TreeNode,
|
TreeNode
|
||||||
UserInfo
|
|
||||||
} from "./models.js";
|
} from "./models.js";
|
||||||
|
|||||||
@@ -238,6 +238,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 文件信息
|
||||||
*/
|
*/
|
||||||
@@ -373,44 +425,6 @@ export class GitStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* PlatformInfo 平台信息
|
|
||||||
*/
|
|
||||||
export class PlatformInfo {
|
|
||||||
/**
|
|
||||||
* Creates a new PlatformInfo instance.
|
|
||||||
* @param {Partial<PlatformInfo>} [$$source = {}] - The source object to create the PlatformInfo.
|
|
||||||
*/
|
|
||||||
constructor($$source = {}) {
|
|
||||||
if (!("name" in $$source)) {
|
|
||||||
/**
|
|
||||||
* @member
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this["name"] = "";
|
|
||||||
}
|
|
||||||
if (!("baseUrl" in $$source)) {
|
|
||||||
/**
|
|
||||||
* @member
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this["baseUrl"] = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new PlatformInfo instance from a string or object.
|
|
||||||
* @param {any} [$$source = {}]
|
|
||||||
* @returns {PlatformInfo}
|
|
||||||
*/
|
|
||||||
static createFrom($$source = {}) {
|
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
|
||||||
return new PlatformInfo(/** @type {Partial<PlatformInfo>} */($$parsedSource));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProjectOverview 项目概览信息(轻量级)
|
* ProjectOverview 项目概览信息(轻量级)
|
||||||
*/
|
*/
|
||||||
@@ -507,6 +521,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 +542,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);
|
||||||
}
|
}
|
||||||
@@ -525,14 +561,101 @@ export class ProjectStatus {
|
|||||||
*/
|
*/
|
||||||
static createFrom($$source = {}) {
|
static createFrom($$source = {}) {
|
||||||
const $$createField2_0 = $$createType1;
|
const $$createField2_0 = $$createType1;
|
||||||
|
const $$createField4_0 = $$createType3;
|
||||||
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* StashInfo 贮藏信息
|
* StashInfo 贮藏信息
|
||||||
*/
|
*/
|
||||||
@@ -662,7 +785,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 +798,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 +822,19 @@ export class TreeNode {
|
|||||||
* @returns {TreeNode}
|
* @returns {TreeNode}
|
||||||
*/
|
*/
|
||||||
static createFrom($$source = {}) {
|
static createFrom($$source = {}) {
|
||||||
const $$createField4_0 = $$createType3;
|
const $$createField5_0 = $$createType5;
|
||||||
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 = RemoteItem.createFrom;
|
||||||
const $$createType1 = $Create.Array($$createType0);
|
const $$createType1 = $Create.Array($$createType0);
|
||||||
const $$createType2 = TreeNode.createFrom;
|
const $$createType2 = FileInfo.createFrom;
|
||||||
const $$createType3 = $Create.Array($$createType2);
|
const $$createType3 = $Create.Array($$createType2);
|
||||||
|
const $$createType4 = TreeNode.createFrom;
|
||||||
|
const $$createType5 = $Create.Array($$createType4);
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ 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{},
|
||||||
Settings: config.Settings{Concurrency: 6, NetworkCheck: true, LogLevel: "info"},
|
Groups: []config.Group{},
|
||||||
|
Credentials: []config.Credential{},
|
||||||
|
Settings: config.Settings{Concurrency: 6, NetworkCheck: true, LogLevel: "info"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &AppService{
|
return &AppService{
|
||||||
@@ -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 {
|
||||||
var tree []TreeNode
|
// 按分组归类项目
|
||||||
for platformName, platform := range s.config.Platforms {
|
groupProjects := make(map[string][]config.ProjectItem)
|
||||||
platformNode := TreeNode{
|
for _, proj := range s.config.Projects {
|
||||||
Key: platformName,
|
g := proj.Group
|
||||||
Label: platformName,
|
if g == "" {
|
||||||
Type: "platform",
|
g = "未分组"
|
||||||
}
|
}
|
||||||
for _, user := range platform.Users {
|
groupProjects[g] = append(groupProjects[g], proj)
|
||||||
userNode := TreeNode{
|
|
||||||
Key: platformName + "/" + user.Username,
|
|
||||||
Label: user.Username,
|
|
||||||
Type: "user",
|
|
||||||
}
|
|
||||||
for _, proj := range user.Projects {
|
|
||||||
userNode.Children = append(userNode.Children, TreeNode{
|
|
||||||
Key: platformName + "/" + user.Username + "/" + proj.Name,
|
|
||||||
Label: proj.Name,
|
|
||||||
Type: "project",
|
|
||||||
Path: proj.Path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
platformNode.Children = append(platformNode.Children, userNode)
|
|
||||||
}
|
|
||||||
tree = append(tree, platformNode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建分组 → icon 映射
|
||||||
|
groupIcon := make(map[string]string)
|
||||||
|
for _, g := range s.config.Groups {
|
||||||
|
groupIcon[g.Name] = g.Icon
|
||||||
|
}
|
||||||
|
|
||||||
|
var tree []TreeNode
|
||||||
|
// 先输出已定义的分组(保持顺序)
|
||||||
|
rendered := make(map[string]bool)
|
||||||
|
for _, g := range s.config.Groups {
|
||||||
|
projects, ok := groupProjects[g.Name]
|
||||||
|
if !ok {
|
||||||
|
projects = nil
|
||||||
|
}
|
||||||
|
node := TreeNode{
|
||||||
|
Key: "group:" + g.Name,
|
||||||
|
Label: g.Name,
|
||||||
|
Type: "group",
|
||||||
|
Icon: g.Icon,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
rendered[g.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输出"未分组"或不在 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,207 +151,237 @@ 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 {
|
s.config.Projects = append(s.config.Projects, config.ProjectItem{
|
||||||
if user.Username == username {
|
Name: name,
|
||||||
// 检查重复
|
Path: path,
|
||||||
for _, proj := range user.Projects {
|
Group: group,
|
||||||
if proj.Name == name {
|
})
|
||||||
return fmt.Errorf("项目 %s 已存在", name)
|
// 如果分组不存在,自动创建
|
||||||
}
|
if group != "" {
|
||||||
|
found := false
|
||||||
|
for _, g := range s.config.Groups {
|
||||||
|
if g.Name == group {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
s.config.Platforms[platform].Users[i].Projects = append(
|
}
|
||||||
s.config.Platforms[platform].Users[i].Projects,
|
if !found {
|
||||||
config.Project{Name: name, Path: path},
|
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
|
||||||
}
|
// 自动创建分组
|
||||||
for i, user := range p.Users {
|
if group != "" {
|
||||||
if user.Username == username {
|
found := false
|
||||||
projects := user.Projects
|
for _, g := range s.config.Groups {
|
||||||
for j, proj := range projects {
|
if g.Name == group {
|
||||||
if proj.Name == name {
|
found = true
|
||||||
s.config.Platforms[platform].Users[i].Projects = append(projects[:j], projects[j+1:]...)
|
break
|
||||||
return config.SaveConfig(s.config)
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
s.config.Groups = append(s.config.Groups, config.Group{Name: group, Icon: "folder"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("项目 %s 不存在", name)
|
return config.SaveConfig(s.config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("用户 %s 不存在于平台 %s", username, platform)
|
return fmt.Errorf("项目不存在: %s", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 平台管理 ---
|
// --- 分组管理 ---
|
||||||
|
|
||||||
// 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{
|
if icon == "" {
|
||||||
BaseURL: baseURL,
|
icon = "folder"
|
||||||
Users: []config.User{},
|
|
||||||
}
|
}
|
||||||
|
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)
|
|
||||||
}
|
}
|
||||||
p.BaseURL = baseURL
|
for i, g := range s.config.Groups {
|
||||||
s.config.Platforms[name] = p
|
if g.Name == oldName {
|
||||||
return config.SaveConfig(s.config)
|
// 如果改名,检查冲突并同步更新项目的分组引用
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return config.SaveConfig(s.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("分组 %s 不存在", oldName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemovePlatform 删除平台
|
// RemoveGroup 删除分组(分组下的项目变为"未分组")
|
||||||
func (s *AppService) RemovePlatform(name string) error {
|
func (s *AppService) RemoveGroup(name string) error {
|
||||||
if _, ok := s.config.Platforms[name]; !ok {
|
for i, g := range s.config.Groups {
|
||||||
return fmt.Errorf("平台 %s 不存在", name)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delete(s.config.Platforms, name)
|
return fmt.Errorf("分组 %s 不存在", name)
|
||||||
return config.SaveConfig(s.config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 用户管理 ---
|
// --- 凭证管理 ---
|
||||||
|
|
||||||
// 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 操作 ---
|
||||||
|
|
||||||
// ProjectStatus 项目状态信息
|
// ProjectStatus 项目状态信息
|
||||||
type ProjectStatus struct {
|
type ProjectStatus struct {
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch"`
|
||||||
RemoteURL string `json:"remoteUrl"`
|
RemoteURL string `json:"remoteUrl"`
|
||||||
ChangedFiles []FileInfo `json:"changedFiles"`
|
Remotes []RemoteItem `json:"remotes"`
|
||||||
|
CurrentRemote string `json:"currentRemote"`
|
||||||
|
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,
|
||||||
ChangedFiles: []FileInfo{},
|
Remotes: remotes,
|
||||||
|
CurrentRemote: currentRemote,
|
||||||
|
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,30 +1428,26 @@ 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 {
|
overview := ProjectOverview{
|
||||||
for _, proj := range user.Projects {
|
Key: proj.Path,
|
||||||
overview := ProjectOverview{
|
Name: proj.Name,
|
||||||
Key: platformName + "/" + user.Username + "/" + proj.Name,
|
Path: proj.Path,
|
||||||
Name: proj.Name,
|
|
||||||
Path: proj.Path,
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(proj.Path); os.IsNotExist(err) {
|
|
||||||
overview.Error = "路径不存在"
|
|
||||||
results = append(results, overview)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
branch, hasChanges, unpushed, err := s.gitClient.QuickStatus(proj.Path)
|
|
||||||
if err != nil {
|
|
||||||
overview.Error = err.Error()
|
|
||||||
} else {
|
|
||||||
overview.Branch = branch
|
|
||||||
overview.HasChanges = hasChanges
|
|
||||||
overview.Unpushed = unpushed
|
|
||||||
}
|
|
||||||
results = append(results, overview)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if _, err := os.Stat(proj.Path); os.IsNotExist(err) {
|
||||||
|
overview.Error = "路径不存在"
|
||||||
|
results = append(results, overview)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branch, hasChanges, unpushed, err := s.gitClient.QuickStatus(proj.Path)
|
||||||
|
if err != nil {
|
||||||
|
overview.Error = err.Error()
|
||||||
|
} else {
|
||||||
|
overview.Branch = branch
|
||||||
|
overview.HasChanges = hasChanges
|
||||||
|
overview.Unpushed = unpushed
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -29,10 +29,20 @@ 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)
|
header = append(header, "-c", "http.proxy="+g.Proxy, "-c", "https.proxy="+g.Proxy)
|
||||||
}
|
}
|
||||||
argsArr := append(header, args...)
|
argsArr := append(header, args...)
|
||||||
@@ -60,6 +70,21 @@ 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")
|
||||||
}
|
}
|
||||||
@@ -93,6 +118,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 +207,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 +238,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 +449,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)
|
||||||
|
|||||||
Reference in New Issue
Block a user