Fix compilation errors.

This commit is contained in:
bigfroggit 2026-09-06 18:14:19 +08:00
parent 233045b9cf
commit 061a1ea859
8 changed files with 110 additions and 40 deletions

View file

@ -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)

View file

@ -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
}

View file

@ -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 为 refType60秒过期
// 避免每次界面刷新都发起 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
}

View file

@ -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 {

View file

@ -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

View file

@ -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
})
}

View file

@ -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

View file

@ -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
})
}