From 061a1ea8595142a568878594c7626f6bf60504bc Mon Sep 17 00:00:00 2001 From: bigfroggit Date: Sun, 6 Sep 2026 18:14:19 +0800 Subject: [PATCH] Fix compilation errors. --- pkg/commands/git.go | 1 + pkg/commands/git_commands/common.go | 5 +- pkg/commands/git_commands/svn_commands.go | 76 +++++++++++++++---- pkg/commands/models/tag.go | 4 +- pkg/gui/controllers/branches_controller.go | 7 +- pkg/gui/controllers/helpers/refresh_helper.go | 23 ++++-- .../controllers/remote_branches_controller.go | 14 ++-- pkg/gui/controllers/remotes_controller.go | 20 +++-- 8 files changed, 110 insertions(+), 40 deletions(-) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index ccbbcb219..cb9968173 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -121,6 +121,7 @@ func NewGitCommandAux( gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, repoPaths, configCommands, diffRendererConfigManager) svnCommands := git_commands.NewSvnCommands(gitCommon, cmd) + gitCommon.Svn = svnCommands fileLoader := git_commands.NewFileLoader(gitCommon, cmd, configCommands) statusCommands := git_commands.NewStatusCommands(gitCommon) flowCommands := git_commands.NewFlowCommands(gitCommon) diff --git a/pkg/commands/git_commands/common.go b/pkg/commands/git_commands/common.go index 15cf94875..1c90fb4dc 100644 --- a/pkg/commands/git_commands/common.go +++ b/pkg/commands/git_commands/common.go @@ -17,6 +17,7 @@ type GitCommon struct { config *ConfigCommands diffRendererConfigManager *config.DiffRendererConfigManager IsGitSvnRepo bool + Svn *SvnCommands } func (self *GitCommon) detectGitSvnRepo() { @@ -33,7 +34,9 @@ func (self *GitCommon) detectGitSvnRepo() { svnDir := filepath.Join(self.repoPaths.RepoGitDirPath(), "svn") if info, err := os.Stat(svnDir); err == nil && info.IsDir() { self.IsGitSvnRepo = true - self.Common.Log.Info("Detected Git-SVN repository (found .git/svn)") + if self.Common != nil { + self.Common.Log.Info("Detected Git-SVN repository (found .git/svn)") + } } else { self.IsGitSvnRepo = false } diff --git a/pkg/commands/git_commands/svn_commands.go b/pkg/commands/git_commands/svn_commands.go index ce8942dc5..29f56492f 100644 --- a/pkg/commands/git_commands/svn_commands.go +++ b/pkg/commands/git_commands/svn_commands.go @@ -3,6 +3,7 @@ package git_commands import ( "fmt" "strings" + "sync" "time" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" @@ -25,10 +26,22 @@ type SvnCommands struct { svnRefMappingsCache *[]SvnRefMapping svnUrlCache string svnUrlCacheExpiry time.Time + // CheckBranchStatus 结果缓存(key 为 refType),60秒过期, + // 避免每次界面刷新都发起 svn list 网络请求; + // branches 与 tags 在不同 worker协程并发调用, + // Go map 并发读写会触发不可恢复的 fatal error,故必须加锁 + statusCache map[string]map[string]models.SvnBranchStatus + statusCacheExpiry map[string]time.Time + statusCacheMutex sync.Mutex } func NewSvnCommands(gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder) *SvnCommands { - return &SvnCommands{GitCommon: gitCommon, cmd: cmd} + return &SvnCommands{ + GitCommon: gitCommon, + cmd: cmd, + statusCache: make(map[string]map[string]models.SvnBranchStatus), + statusCacheExpiry: make(map[string]time.Time), + } } // GetSvnUrl 从git config获取SVN仓库URL @@ -244,51 +257,81 @@ func (self *SvnCommands) Fetch() error { // CheckBranchStatus 检测本地 refs 和 SVN 服务器的差异 // refType: "branches" 或 "tags" -// 返回值:map[branchPath]models.SvnBranchStatus, branchPath 如 "branches/proj1/xxx" -// SVN list 使用 --non-interactive 防止网络阻塞,结果不缓存(每次进入时重新检测) +// 返回值:map[refRelPath]models.SvnBranchStatus, refRelPath 为 ref 相对路 +//(完整 ref 去掉 “refs/remotes/git-svn/” 前缀,如 “branches/proj1/xxx”、”tags/R1.0.0”), +// 与 RemoteBranch.Name / TrimPrefix(tag.FullRefName(), “refs/remotes/git-svn/”)格式一致 +// 结果缓存 60 秒; svn list 全部失败时返回错误(错误不缓存,下次调用自动重试)径 func (self *SvnCommands) CheckBranchStatus(task gocui.Task, refType string) (map[string]models.SvnBranchStatus, error) { + // 整个方法加锁: branches/tags 两个调用方在不同 worker 协程,Go map并发读写 + // 会直接触发不可恢复的 fatal error;加锁同时保护函数内 + // GetSvnUrl/GetSvnRefMappings 既有缓存在此路径上的并发访问 + self.statusCacheMutex.Lock() + defer self.statusCacheMutex.Unlock() + + // 0. 命中缓存直接返回(60 秒内),避免重复发起 svn list 网络请求 + if cached, ok := self.statusCache[refType]; ok && time.Now().Before(self.statusCacheExpiry[refType]) { + return cached, nil + } + svnUrl, err := self.GetSvnUrl() if err != nil { return nil, err } - // 1. 获取本地 refs + mappings, _ := self.GetSvnRefMappings() + + // 1. 获取本地 refs(遍历该类型所有 mapping 的 RefsPath, + // 兼容refs 端影射到非标准路径的配置,如 tags = tags/*:refs/remotes/git-svn/releases/*) localRefs := make(map[string]bool) - refsPath := "refs/remotes/git-svn/" + refType - output, err := self.cmd.New( - NewGitCmd("for-each-ref").Arg("--format=%(refname)").Arg(refsPath).ToArgv(), - ).DontLog().RunWithOutput() - if err == nil { + for _, m := range mappings { + if m.Type != refType { + continue; + } + output, refErr := self.cmd.New( + NewGitCmd("for-each-ref").Arg("--format=%(refname)").Arg(m.RefsPath).ToArgv(), + ).DontLog().RunWithOutput() + if refErr != nil { + continue; + } for _, line := range strings.Split(strings.TrimSpace(output), "\n") { if line := strings.TrimSpace(line); line != "" { - relPath := strings.TrimPrefix(line, refsPath+"/") - localRefs[relPath] = true + // Key 统一为 ref 相对路径 + localRefs[strings.TrimPrefix(line, "refs/remotes/git-svn/")] = true } } } // 2. 获取 SVN 服务器上的分支列表(遍历所有 SvnRefMappings) - mappings, _ := self.GetSvnRefMappings() svnBranches := make(map[string]bool) + svnListAttempted := false + svnListOk := false for _, m := range mappings { if m.Type != refType { continue } + svnListAttempted = true // svn list 使用 --non-interactive 防止网络不通时永久阻塞 svnListOutput, listErr := self.cmd.New( NewGitCmd("svn").Arg("list").Arg("--non-interactive").Arg(svnUrl+"/"+m.SvnPath).ToArgv(), ).DontLog().RunWithOutput() if listErr == nil { + svnListOk = true for _, line := range strings.Split(strings.TrimSpace(svnListOutput), "\n") { if line := strings.TrimSpace(line); line != "" { name := strings.TrimSuffix(line, "/") - relPath := m.SvnPath + "/" + name - svnBranches[relPath] = true + // SVN 路径正向影射为 ref 相对路径,与 localRefs 的 key 格式统一 + svnBranches[strings.TrimPrefix(m.RefsPath, "refs/remotes/git-svn/")+"/"+name] = true } } } } + // svn list 全部失败(网络不通、认证失败等)时返回错误并中止, + // 避免把”全部 Stale”的误导性结果当作真实状态展示(错误不缓存,下次自动重时) + if svnListAttempted && !svnListOk { + return nil, fmt.Errorf("svn list failed for all %s paths (network or auth error?)", refType) + } + // 3. 对比差异 result := make(map[string]models.SvnBranchStatus) allPaths := make(map[string]bool) @@ -315,6 +358,11 @@ func (self *SvnCommands) CheckBranchStatus(task gocui.Task, refType string) (map result[path] = status } + // 写入缓存(60 秒)。到达此处时 SVN 侧数据必然可信: + // 若有过 svn list 且全部失败,上方已提前返回 + self.statusCache[refType] = result + self.statusCacheExpiry[refType] = time.Now().Add(60 * time.Second) + return result, nil } diff --git a/pkg/commands/models/tag.go b/pkg/commands/models/tag.go index 51675d6ce..0fd76bdfa 100644 --- a/pkg/commands/models/tag.go +++ b/pkg/commands/models/tag.go @@ -19,9 +19,9 @@ type Tag struct { func (t *Tag) FullRefName() string { if t.FullRefNameOverride != "" { - return "refs/tags/" + t.RefName() + return t.FullRefNameOverride } - return "" + return "refs/tags/" + t.RefName() } func (t *Tag) RefName() string { diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index a38e72fb3..a08872944 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -782,7 +782,7 @@ func (self *BranchesController) newBranch(selectedBranch *models.Branch) error { Items: []*types.MenuItem{ { LabelColumns: []string{self.c.Tr.NewBranch}, - Key: 'l', + Keys: menuKey('l'), OnPress: func() error { return self.c.Helpers().Refs.NewBranch( selectedBranch.FullRefName(), @@ -793,7 +793,7 @@ func (self *BranchesController) newBranch(selectedBranch *models.Branch) error { }, { LabelColumns: []string{self.c.Tr.NewSvnBranch}, - Key: 's', + Keys: menuKey('s'), OnPress: func() error { return self.newSvnBranch(selectedBranch) }, @@ -972,11 +972,10 @@ func (self *BranchesController)newSvnBranch(selectedBranch *models.Branch) error } if err := self.c.Git().Svn.Fetch(); err != nil { - self.c.ErrorMsg(fmt.Sprintf(self.c.Tr.SvnFetchFailed)) + return fmt.Errorf(self.c.Tr.SvnFetchFailed) } self.c.Refresh(types.RefreshOptions{ - Mode: types.SYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}, }) return nil diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 6c28fed74..f7290a19e 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -1126,7 +1126,7 @@ func (self *RefreshHelper) refreshTags(env refreshEnv) error { // SVN 自动 stale 检测 (tags) if self.c.Git().Sync.GitCommon.IsSvnRepo() { - self.checkSvnTagStatusAsync(tags) + self.checkSvnTagStatusAsync() } self.refreshView(self.c.Contexts().Tags, env) @@ -1889,21 +1889,28 @@ func (self *RefreshHelper) savePullRequestsToCache(prs []*models.GithubPullReque } } -func (self *RefreshHelper) checkSvnTagStatusAsync(tags []*models.Tag) { +func (self *RefreshHelper) checkSvnTagStatusAsync() { self.c.WithWaitingStatus(self.c.Tr.CheckingSvnStatus, func (task gocui.Task) error { statuses, err := self.c.Git().Svn.CheckBranchStatus(task, "tags") if err != nil { return err } - for _, tag := range tags { - if tag.IsSvnTag() { - if status, ok := statuses[tag.Name]; ok { + self.c.OnUIThread(func() error { + for _, tag := range self.c.Model().Tags { + if !tag.IsSvnTag() { + continue + } + // FullRefName() 对 SVN tag 返回 FullRefNameOverride, + // trim 后即 ref 相对路径,与 statuses 的 key 格式一致 + relPath := strings.TrimPrefix(tag.FullRefName(), "refs/remotes/git-svn/") + if status, ok := statuses[relPath]; ok { tag.StaleStatus = status } } - } - return self.c.Refresh(types.RefreshOptions{ - Scope: []types.RefreshableView{types.TAGS}, + // 仅重绘视图,不 Refresh(TAGS),打断无限刷新链 + self.c.PostRefreshUpdate(self.c.Contexts().Tags) + return nil }) + return nil }) } diff --git a/pkg/gui/controllers/remote_branches_controller.go b/pkg/gui/controllers/remote_branches_controller.go index 4ffa12c7d..d5a51342b 100644 --- a/pkg/gui/controllers/remote_branches_controller.go +++ b/pkg/gui/controllers/remote_branches_controller.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -225,17 +226,17 @@ func (self *RemoteBranchesController) deleteSvnRemoteBranches(selectedBranches [ return self.c.Menu(types.CreateMenuOptions{ Title: menuTitle, - Items: []*types.MenuTitle{ + Items: []*types.MenuItem{ { LabelColumns: []string{self.c.Tr.DeleteSvnLocalRef}, - Key: 'l', + Keys: menuKey('l'), OnPress: func() error { return self.deleteSvnLocalRefs(selectedBranches) }, }, { LabelColumns: []string{self.c.Tr.DeleteSvnBoth}, - Key: 'b', + Keys: menuKey('b'), OnPress: func() error { return self.confirmDeleteSvnBoth(selectedBranches) }, @@ -254,7 +255,6 @@ func (self *RemoteBranchesController) deleteSvnLocalRefs(selectedBranches []*mod } self.c.Contexts().RemoteBranches.CollapseRangeSelectionToTop() self.c.Refresh(types.RefreshOptions{ - Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}, }) return nil @@ -279,14 +279,16 @@ func (self *RemoteBranchesController) confirmDeleteSvnBoth(selectedBranches []*m return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task,) error { for _, branch := range selectedBranches { if err := self.c.Git().Svn.DeleteServerBranch(task, branch.Name); err != nil { - return fmt.Errorf(self.c.Tr.SvnOperationFailed, map[string]string{"error": err.Error()}) + return fmt.Errorf("%s", utils.ResolvePlaceholderString( + self.c.Tr.SvnOperationFailed, + map[string]string{"error": err.Error()}, + )) } refName := branch.RemoteName + "/" + branch.Name _ = self.c.Git().Svn.DeleteLocalRef(refName) } self.c.Contexts().RemoteBranches.CollapseRangeSelectionToTop() self.c.Refresh(types.RefreshOptions{ - Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}, }) return nil diff --git a/pkg/gui/controllers/remotes_controller.go b/pkg/gui/controllers/remotes_controller.go index 394ba1db1..4a8f45d24 100644 --- a/pkg/gui/controllers/remotes_controller.go +++ b/pkg/gui/controllers/remotes_controller.go @@ -398,7 +398,7 @@ func (self *RemotesController) fetchAndCheckout(remote *models.Remote, branchNam } func (self *RemotesController) notGitSvnRemote() *types.DisabledReason { - remote := self.Context().GetSelected() + remote := self.context().GetSelected() if remote != nil && remote.Name == "git-svn" { return &types.DisabledReason{Text: "Cannot modify git-svn remote"} } @@ -411,10 +411,20 @@ func (self *RemotesController) checkSvnBranchStatusAsync() { if err != nil { return err } - // 将结果写入 RemoteBranch 模型的 StaleStatus 字段 - // 通过 Refresh 触发 presentation 层重新渲染 - return self.c.Refresh(types.RefreshOptions{ - Scope: []types.RefreshableView{types.REMOTES}, + self.c.OnUIThread(func() error { + for _, branch := range self.c.Model().RemoteBranches { + if branch.RemoteName != "git-svn" { + continue + } + // key 为 ref 相对路径,与 RemoteBranch.Name 格式一致 + if status, ok := statuses[branch.Name]; ok { + branch.StaleStatus = status + } + } + // 仅重绘视图,不 Refresh 重建模型(避免 StaleStatus 被冲掉) + self.c.PostRefreshUpdate(self.c.Contexts().RemoteBranches) + return nil }) + return nil }) }