feat: 多 remote 远程仓库支持 (v0.3.1)\n\n后端:\n- 新增 RemoteList/AddRemote/RemoveRemote 远程仓库管理\n- 所有远程操作(Push/Pull/Fetch/PushTag/DeleteTag/DeleteRemoteBranch等)支持指定 remote\n- 新增 PushTo/PullFrom/FetchRemote/PushTagTo/DeleteRemoteTagFrom 等方法\n- BatchPull/BatchPush 支持 remote 参数\n\n前端:\n- 顶部信息栏新增 remote 选择器下拉菜单\n- 支持切换当前操作的远程仓库\n- 支持添加/删除远程仓库\n- Pull/Push/Fetch/标签推送/远程分支删除等操作均使用选中的 remote
Some checks failed
Release / build-windows (push) Has been cancelled

This commit is contained in:
zyj
2026-03-12 10:57:27 +08:00
parent 3d6874f79e
commit 3ec453cc38
10 changed files with 536 additions and 78 deletions

View File

@@ -307,9 +307,17 @@ func (s *AppService) GetPlatformInfo(name string) (*PlatformInfo, error) {
// ProjectStatus 项目状态信息
type ProjectStatus struct {
Branch string `json:"branch"`
RemoteURL string `json:"remoteUrl"`
ChangedFiles []FileInfo `json:"changedFiles"`
Branch string `json:"branch"`
RemoteURL string `json:"remoteUrl"`
Remotes []RemoteItem `json:"remotes"`
CurrentRemote string `json:"currentRemote"`
ChangedFiles []FileInfo `json:"changedFiles"`
}
// RemoteItem 远程仓库信息
type RemoteItem struct {
Name string `json:"name"`
URL string `json:"url"`
}
// FileInfo 文件信息
@@ -346,10 +354,25 @@ func (s *AppService) GetProjectStatus(path string) (*ProjectStatus, error) {
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
}
return &ProjectStatus{
Branch: strings.TrimSpace(branch),
RemoteURL: remoteURL,
ChangedFiles: []FileInfo{},
Branch: strings.TrimSpace(branch),
RemoteURL: remoteURL,
Remotes: remotes,
CurrentRemote: currentRemote,
ChangedFiles: []FileInfo{},
}, nil
}
@@ -515,22 +538,28 @@ func (s *AppService) GetFileDiffStaged(projectPath, filePath string) (string, er
return s.gitClient.DiffStagedFile(projectPath, filePath)
}
// PullProject 拉取项目(当前分支)
func (s *AppService) PullProject(path string) (string, error) {
// PullProject 拉取项目(当前分支,指定 remote
func (s *AppService) PullProject(path, remote string) (string, error) {
if remote == "" {
remote = "origin"
}
branch, err := s.gitClient.Branch(path)
if err != nil {
return "", fmt.Errorf("获取当前分支失败: %w", err)
}
return s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch))
return s.gitClient.PullFrom(path, remote, strings.TrimSpace(branch))
}
// PushProject 推送项目(当前分支)
func (s *AppService) PushProject(path string) (string, error) {
// PushProject 推送项目(当前分支,指定 remote
func (s *AppService) PushProject(path, remote string) (string, error) {
if remote == "" {
remote = "origin"
}
branch, err := s.gitClient.Branch(path)
if err != nil {
return "", fmt.Errorf("获取当前分支失败: %w", err)
}
return s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch))
return s.gitClient.PushTo(path, remote, strings.TrimSpace(branch))
}
// GetCommitDiff 获取指定提交的 diff
@@ -581,9 +610,12 @@ func (s *AppService) GetCommitFileDiff(path, hash, filePath string) (string, err
return s.gitClient.CommitFileDiff(path, hash, filePath)
}
// FetchProject 拉取远程信息
func (s *AppService) FetchProject(path string) (string, error) {
return s.gitClient.Fetch(path)
// FetchProject 拉取远程信息(指定 remote空则 fetch --all
func (s *AppService) FetchProject(path, remote string) (string, error) {
if remote == "" {
return s.gitClient.Fetch(path)
}
return s.gitClient.FetchRemote(path, remote)
}
// CommitLog 提交记录
@@ -791,7 +823,7 @@ func (s *AppService) CreateTag(path, name, message string) error {
}
// 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) {
return fmt.Errorf("项目路径不存在: %s", path)
}
@@ -799,17 +831,20 @@ func (s *AppService) DeleteTag(path, name string) error {
if name == "" {
return fmt.Errorf("标签名不能为空")
}
if remote == "" {
remote = "origin"
}
// 删除本地标签
if _, err := s.gitClient.DeleteTag(path, name); err != nil {
return fmt.Errorf("删除本地标签失败: %w", err)
}
// 尝试删除远程标签(忽略错误,可能未推送过)
s.gitClient.DeleteRemoteTag(path, name)
s.gitClient.DeleteRemoteTagFrom(path, remote, name)
return nil
}
// 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) {
return fmt.Errorf("项目路径不存在: %s", path)
}
@@ -817,7 +852,10 @@ func (s *AppService) PushTag(path, name string) error {
if name == "" {
return fmt.Errorf("标签名不能为空")
}
_, err := s.gitClient.PushTag(path, name)
if remote == "" {
remote = "origin"
}
_, err := s.gitClient.PushTagTo(path, remote, name)
return err
}
@@ -869,11 +907,17 @@ func (s *AppService) MergeBranch(path, branch string) (string, error) {
// --- 远程分支管理 ---
// 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) {
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 {
return nil, fmt.Errorf("获取远程分支列表失败: %w", err)
}
@@ -907,7 +951,7 @@ func (s *AppService) CheckoutRemoteBranch(path, remoteBranch string) error {
}
// 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) {
return fmt.Errorf("项目路径不存在: %s", path)
}
@@ -915,12 +959,15 @@ func (s *AppService) DeleteRemoteBranch(path, branch string) error {
if branch == "" {
return fmt.Errorf("分支名不能为空")
}
if remote == "" {
remote = "origin"
}
// origin/feature -> feature
localName := branch
if idx := strings.Index(branch, "/"); idx != -1 {
localName = branch[idx+1:]
}
_, err := s.gitClient.DeleteRemoteBranch(path, localName)
_, err := s.gitClient.DeleteRemoteBranchFrom(path, remote, localName)
return err
}
@@ -1050,6 +1097,54 @@ func (s *AppService) UpdateAppSettings(logLevel string) error {
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 冲突文件信息
@@ -1211,7 +1306,10 @@ type BatchPullResult struct {
}
// BatchPull 批量拉取指定项目
func (s *AppService) BatchPull(paths []string) []BatchPullResult {
func (s *AppService) BatchPull(paths []string, remote string) []BatchPullResult {
if remote == "" {
remote = "origin"
}
var results []BatchPullResult
for _, path := range paths {
result := BatchPullResult{Path: path}
@@ -1229,7 +1327,7 @@ func (s *AppService) BatchPull(paths []string) []BatchPullResult {
results = append(results, result)
continue
}
_, err = s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch))
_, err = s.gitClient.PullFrom(path, remote, strings.TrimSpace(branch))
if err != nil {
result.Message = err.Error()
} else {
@@ -1242,7 +1340,10 @@ func (s *AppService) BatchPull(paths []string) []BatchPullResult {
}
// BatchPush 批量推送指定项目
func (s *AppService) BatchPush(paths []string) []BatchPullResult {
func (s *AppService) BatchPush(paths []string, remote string) []BatchPullResult {
if remote == "" {
remote = "origin"
}
var results []BatchPullResult
for _, path := range paths {
result := BatchPullResult{Path: path}
@@ -1259,7 +1360,7 @@ func (s *AppService) BatchPush(paths []string) []BatchPullResult {
results = append(results, result)
continue
}
_, err = s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch))
_, err = s.gitClient.PushTo(path, remote, strings.TrimSpace(branch))
if err != nil {
result.Message = err.Error()
} else {

View File

@@ -60,6 +60,21 @@ func (g *GitClient) Push(path string) (string, error) {
return g.Run(path, "push")
}
// PushTo 推送到指定远程
func (g *GitClient) PushTo(path, remote, branch string) (string, error) {
return g.Run(path, "push", remote, branch)
}
// PullFrom 从指定远程拉取
func (g *GitClient) PullFrom(path, remote, branch string) (string, error) {
return g.Run(path, "pull", remote, branch)
}
// FetchRemote 拉取指定远程信息
func (g *GitClient) FetchRemote(path, remote string) (string, error) {
return g.Run(path, "fetch", remote)
}
func (g *GitClient) Status(path string) (string, error) {
return g.Run(path, "status")
}
@@ -93,6 +108,47 @@ func (g *GitClient) Fetch(path string) (string, error) {
return g.Run(path, "fetch", "--all")
}
// RemoteList 获取所有远程仓库列表 (git remote -v)
type RemoteInfo struct {
Name string
URL string
}
func (g *GitClient) RemoteList(path string) ([]RemoteInfo, error) {
out, err := g.Run(path, "remote", "-v")
if err != nil {
return nil, err
}
seen := make(map[string]bool)
var remotes []RemoteInfo
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
name := parts[0]
if seen[name] {
continue
}
seen[name] = true
remotes = append(remotes, RemoteInfo{Name: name, URL: parts[1]})
}
return remotes, nil
}
// AddRemote 添加远程仓库
func (g *GitClient) AddRemote(path, name, url string) (string, error) {
return g.Run(path, "remote", "add", name, url)
}
// RemoveRemote 删除远程仓库
func (g *GitClient) RemoveRemote(path, name string) (string, error) {
return g.Run(path, "remote", "remove", name)
}
func (g *GitClient) RemoteURL(path string) (string, error) {
out, err := g.Run(path, "config", "--get", "remote.origin.url")
if err != nil {
@@ -141,11 +197,16 @@ func (g *GitClient) Log(path string, count int) (string, error) {
)
}
// UnpushedCommits 获取当前分支上未推送到远程的提交哈希列表
// UnpushedCommits 获取当前分支上未推送到远程的提交哈希列表(默认 origin
func (g *GitClient) UnpushedCommits(path, branch string) (string, error) {
return g.Run(path, "rev-list", "origin/"+branch+"..HEAD")
}
// UnpushedCommitsTo 获取当前分支上未推送到指定远程的提交哈希列表
func (g *GitClient) UnpushedCommitsTo(path, remote, branch string) (string, error) {
return g.Run(path, "rev-list", remote+"/"+branch+"..HEAD")
}
// RevertCommit 撤回指定提交(创建一个反向提交)
func (g *GitClient) RevertCommit(path, hash string) (string, error) {
return g.Run(path, "revert", "--no-edit", hash)
@@ -167,16 +228,26 @@ func (g *GitClient) DeleteTag(path, name string) (string, error) {
return g.Run(path, "tag", "-d", name)
}
// PushTag 推送标签到远程
// PushTag 推送标签到远程(默认 origin
func (g *GitClient) PushTag(path, name string) (string, error) {
return g.Run(path, "push", "origin", name)
}
// DeleteRemoteTag 删除远程标签
// PushTagTo 推送标签到指定远程
func (g *GitClient) PushTagTo(path, remote, name string) (string, error) {
return g.Run(path, "push", remote, name)
}
// DeleteRemoteTag 删除远程标签(默认 origin
func (g *GitClient) DeleteRemoteTag(path, name string) (string, error) {
return g.Run(path, "push", "origin", "--delete", name)
}
// DeleteRemoteTagFrom 删除指定远程的标签
func (g *GitClient) DeleteRemoteTagFrom(path, remote, name string) (string, error) {
return g.Run(path, "push", remote, "--delete", name)
}
// BranchList 获取所有本地分支
func (g *GitClient) BranchList(path string) (string, error) {
return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)")
@@ -368,16 +439,26 @@ func (g *GitClient) MergeBranch(path, branch string) (string, error) {
return g.Run(path, "merge", branch)
}
// DeleteRemoteBranch 删除远程分支
// DeleteRemoteBranch 删除远程分支(默认 origin
func (g *GitClient) DeleteRemoteBranch(path, branch string) (string, error) {
return g.Run(path, "push", "origin", "--delete", branch)
}
// DeleteRemoteBranchFrom 删除指定远程的分支
func (g *GitClient) DeleteRemoteBranchFrom(path, remote, branch string) (string, error) {
return g.Run(path, "push", remote, "--delete", branch)
}
// RemoteBranchList 获取所有远程分支
func (g *GitClient) RemoteBranchList(path string) (string, error) {
return g.Run(path, "branch", "-r", "--format=%(refname:short)")
}
// RemoteBranchListByRemote 获取指定远程的分支
func (g *GitClient) RemoteBranchListByRemote(path, remote string) (string, error) {
return g.Run(path, "branch", "-r", "--list", remote+"/*", "--format=%(refname:short)")
}
// CheckoutNewBranch 从远程分支检出新本地分支
func (g *GitClient) CheckoutNewBranch(path, localBranch, remoteBranch string) (string, error) {
return g.Run(path, "checkout", "-b", localBranch, remoteBranch)