mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
202 lines
5.1 KiB
Go
202 lines
5.1 KiB
Go
package git_commands
|
||
|
||
import (
|
||
"fmt"
|
||
"maps"
|
||
"slices"
|
||
"strings"
|
||
"sync"
|
||
|
||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||
"github.com/jesseduffield/lazygit/pkg/common"
|
||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||
)
|
||
|
||
type RemoteLoader struct {
|
||
*common.Common
|
||
cmd oscommands.ICmdObjBuilder
|
||
gitCommon *GitCommon
|
||
}
|
||
|
||
func NewRemoteLoader(
|
||
common *common.Common,
|
||
cmd oscommands.ICmdObjBuilder,
|
||
gitCommon *GitCommon,
|
||
) *RemoteLoader {
|
||
return &RemoteLoader{
|
||
Common: common,
|
||
cmd: cmd,
|
||
gitCommon: gitCommon,
|
||
}
|
||
}
|
||
|
||
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
|
||
wg := sync.WaitGroup{}
|
||
wg.Add(1)
|
||
|
||
var remoteBranchesByRemoteName map[string][]*models.RemoteBranch
|
||
var remoteBranchesErr error
|
||
go utils.Safe(func() {
|
||
defer wg.Done()
|
||
|
||
remoteBranchesByRemoteName, remoteBranchesErr = self.getRemoteBranchesByRemoteName()
|
||
})
|
||
|
||
remotes := self.getRemotesFromConfig()
|
||
|
||
wg.Wait()
|
||
|
||
if remoteBranchesErr != nil {
|
||
return nil, remoteBranchesErr
|
||
}
|
||
|
||
for _, remote := range remotes {
|
||
remote.Branches = remoteBranchesByRemoteName[remote.Name]
|
||
}
|
||
|
||
// SVN 仓库:注入 git-svn 虚拟 remote
|
||
// go-git 的 repo.Remotes() 不包含 git-svn,但 git for-each-ref 已经扫描到了
|
||
// refs/remotes/git-svn/*,需要手动注入使其显示在 UI 中
|
||
if self.gitCommon != nil && self.gitCommon.IsSvnRepo() {
|
||
tagsPaths, _ := self.gitCommon.Svn.GetTagsRefsPaths()
|
||
svnBranches := remoteBranchesByRemoteName["git-svn"]
|
||
|
||
// 过滤掉属于 tags 的分支(应由 Tags 界面管理)
|
||
var filteredBranches []*models.RemoteBranch
|
||
for _, b := range svnBranches {
|
||
if !self.isTagRef(b.Name, tagsPaths) {
|
||
filteredBranches = append(filteredBranches, b)
|
||
}
|
||
}
|
||
|
||
if len(filteredBranches) > 0 || len(svnBranches) > 0 {
|
||
remotes = append(remotes, &models.Remote{
|
||
Name: "git-svn",
|
||
Urls: []string{"(git-svn)"},
|
||
Branches: filteredBranches,
|
||
})
|
||
}
|
||
}
|
||
|
||
// now lets sort our remotes by name alphabetically
|
||
slices.SortFunc(remotes, func(a, b *models.Remote) int {
|
||
// we want origin at the top because we'll be most likely to want it
|
||
if a.Name == "origin" {
|
||
return -1
|
||
}
|
||
if b.Name == "origin" {
|
||
return 1
|
||
}
|
||
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
|
||
})
|
||
|
||
return remotes, nil
|
||
}
|
||
|
||
func (self *RemoteLoader) getRemotesFromConfig() []*models.Remote {
|
||
cmdArgs := NewGitCmd("config").
|
||
Arg("--local", "--get-regexp", `^remote\.[^.]+\.(url|pushurl)$`).ToArgv()
|
||
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||
if err != nil {
|
||
// exit code 1 means no matching keys (no remotes configured)
|
||
return nil
|
||
}
|
||
|
||
remotesByName := make(map[string]*models.Remote)
|
||
|
||
for _, line := range strings.Split(output, "\n") {
|
||
key, url, found := strings.Cut(strings.TrimSpace(line), " ")
|
||
if !found {
|
||
continue
|
||
}
|
||
// key is "remote.<name>.url" or "remote.<name>.pushurl";
|
||
// strip prefix and suffix to get the name
|
||
rest, ok := strings.CutPrefix(key, "remote.")
|
||
if !ok {
|
||
continue
|
||
}
|
||
var remoteName string
|
||
var isPushUrl bool
|
||
if name, ok := strings.CutSuffix(rest, ".pushurl"); ok {
|
||
remoteName, isPushUrl = name, true
|
||
} else if name, ok := strings.CutSuffix(rest, ".url"); ok {
|
||
remoteName, isPushUrl = name, false
|
||
} else {
|
||
continue
|
||
}
|
||
if _, ok := remotesByName[remoteName]; !ok {
|
||
remotesByName[remoteName] = &models.Remote{Name: remoteName}
|
||
}
|
||
if isPushUrl {
|
||
remotesByName[remoteName].PushUrls = append(remotesByName[remoteName].PushUrls, url)
|
||
} else {
|
||
remotesByName[remoteName].Urls = append(remotesByName[remoteName].Urls, url)
|
||
}
|
||
}
|
||
|
||
return slices.Collect(maps.Values(remotesByName))
|
||
}
|
||
|
||
func (self *RemoteLoader) getRemoteBranchesByRemoteName() (map[string][]*models.RemoteBranch, error) {
|
||
remoteBranchesByRemoteName := make(map[string][]*models.RemoteBranch)
|
||
|
||
var sortOrder string
|
||
switch strings.ToLower(self.UserConfig().Git.RemoteBranchSortOrder) {
|
||
case "alphabetical":
|
||
sortOrder = "refname"
|
||
case "date":
|
||
sortOrder = "-committerdate"
|
||
default:
|
||
sortOrder = "refname"
|
||
}
|
||
|
||
cmdArgs := NewGitCmd("for-each-ref").
|
||
Arg(fmt.Sprintf("--sort=%s", sortOrder)).
|
||
Arg("--format=%(refname)").
|
||
Arg("refs/remotes").
|
||
ToArgv()
|
||
|
||
err := self.cmd.New(cmdArgs).DontLog().RunAndProcessLines(func(line string) (bool, error) {
|
||
line = strings.TrimSpace(line)
|
||
|
||
split := strings.SplitN(line, "/", 4)
|
||
if len(split) != 4 {
|
||
return false, nil
|
||
}
|
||
remoteName := split[2]
|
||
name := split[3]
|
||
|
||
if name == "HEAD" {
|
||
return false, nil
|
||
}
|
||
|
||
_, ok := remoteBranchesByRemoteName[remoteName]
|
||
if !ok {
|
||
remoteBranchesByRemoteName[remoteName] = []*models.RemoteBranch{}
|
||
}
|
||
|
||
remoteBranchesByRemoteName[remoteName] = append(remoteBranchesByRemoteName[remoteName],
|
||
&models.RemoteBranch{
|
||
Name: name,
|
||
RemoteName: remoteName,
|
||
})
|
||
return false, nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return remoteBranchesByRemoteName, nil
|
||
}
|
||
|
||
func (self *RemoteLoader) isTagRef(refName string, tagsPaths []string) bool {
|
||
for _, path := range tagsPaths {
|
||
fullRef := "refs/remotes/git-svn/" + refName
|
||
if strings.HasPrefix(fullRef, path) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|