Fix all functions now, and can run normally.

This commit is contained in:
bigfroggit 2026-09-10 22:51:55 +08:00
parent caddeb12d5
commit 22a78ccec9
11 changed files with 189 additions and 55 deletions

View file

@ -46,7 +46,8 @@ func isRetryableError(output string, err error) bool {
text += "\n" + err.Error()
}
return strings.Contains(text, "index.lock") ||
strings.Contains(text, "cannot lock ref")
strings.Contains(text, "cannot lock ref") ||
strings.Contains(text, "resource temporarily unavailable")
}
func (self *gitCmdObjRunner) Run(cmdObj *oscommands.CmdObj) error {

View file

@ -51,7 +51,7 @@ func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string
}
func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName string, branchNames []string) error {
if remoteName == "git-svn" {
if self.IsSvnRepo() && remoteName == self.Svn.GetSvnRemoteName() {
return fmt.Errorf("cannot delete git-svn remote branch via git push; use svn delete instead")
}
cmdArgs := NewGitCmd("push").

View file

@ -56,23 +56,24 @@ func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
}
// SVN 仓库:注入 git-svn 虚拟 remote
// go-git 的 repo.Remotes() 不包含 git-svn但 git for-each-ref 已经扫描到了
// refs/remotes/git-svn/*,需要手动注入使其显示在 UI 中
// go-git 的 repo.Remotes() 不包含 svn-remote,但 git for-each-ref 已经扫描到了
// refs/remotes/<svn-remote-name>/*,需要手动注入使其显示在 UI 中
if self.gitCommon != nil && self.gitCommon.IsSvnRepo() {
svnRemoteName := self.gitCommon.Svn.GetSvnRemoteName()
tagsPaths, _ := self.gitCommon.Svn.GetTagsRefsPaths()
svnBranches := remoteBranchesByRemoteName["git-svn"]
svnBranches := remoteBranchesByRemoteName[svnRemoteName]
// 过滤掉属于 tags 的分支(应由 Tags 界面管理)
var filteredBranches []*models.RemoteBranch
for _, b := range svnBranches {
if !self.isTagRef(b.Name, tagsPaths) {
if !self.isTagRef(b.Name, tagsPaths, svnRemoteName) {
filteredBranches = append(filteredBranches, b)
}
}
if len(filteredBranches) > 0 || len(svnBranches) > 0 {
remotes = append(remotes, &models.Remote{
Name: "git-svn",
Name: svnRemoteName,
Urls: []string{"(git-svn)"},
Branches: filteredBranches,
})
@ -190,9 +191,9 @@ func (self *RemoteLoader) getRemoteBranchesByRemoteName() (map[string][]*models.
return remoteBranchesByRemoteName, nil
}
func (self *RemoteLoader) isTagRef(refName string, tagsPaths []string) bool {
func (self *RemoteLoader) isTagRef(refName string, tagsPaths []string, svnRemoteName string) bool {
for _, path := range tagsPaths {
fullRef := "refs/remotes/git-svn/" + refName
fullRef := "refs/remotes/" + svnRemoteName + "/" + refName
if strings.HasPrefix(fullRef, path) {
return true
}

View file

@ -44,6 +44,23 @@ func NewSvnCommands(gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder) *SvnCom
}
}
// GetSvnRemoteName 从 ref 影射中提取实际的 SVN 远程名。
// 例如 RefsPath 为 “refs/remotes/svn/trunk” 时返回 “svn”。
func (self *SvnCommands) GetSvnRemoteName() string {
mappings, err := self.GetSvnRefMappings()
if err != nil || len(mappings) == 0 {
return "svn"
}
for _, m := range mappings {
parts := strings.SplitN(m.RefsPath, "/", 4)
if len(parts) >= 3 && parts[0] == "refs" && parts[1] == "remotes" {
return parts[2]
}
}
return "svn"
}
// GetSvnUrl 从git config获取SVN仓库URL
// 返回值如 https://svn.example.com/repo
// 结果缓存60秒
@ -189,16 +206,19 @@ func (self *SvnCommands) GetSvnUpstream(branchName string) (string, string, erro
return "", "", err
}
remoteName := self.GetSvnRemoteName()
refsPrefix := "refs/remotes/" + remoteName + "/"
for _, m := range mappings {
if relPath == m.SvnPath {
upstreamBranch := strings.TrimPrefix(m.RefsPath, "refs/remotes/git-svn/")
return "git-svn", upstreamBranch, nil
upstreamBranch := strings.TrimPrefix(m.RefsPath, refsPrefix)
return remoteName, upstreamBranch, nil
}
if strings.HasPrefix(relPath, m.SvnPath+"/") {
remaining := strings.TrimPrefix(relPath, m.SvnPath)
fullRef := m.RefsPath + remaining
upstreamBranch := strings.TrimPrefix(fullRef, "refs/remotes/git-svn/")
return "git-svn", upstreamBranch, nil
upstreamBranch := strings.TrimPrefix(fullRef, refsPrefix)
return remoteName, upstreamBranch, nil
}
}
@ -242,10 +262,11 @@ func (self *SvnCommands) DeleteServerBranch(task gocui.Task, branchPath string)
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
}
// DeleteLocalRef 仅删除本地远程跟踪引用refs/remotes/git-svn/xxx
// 不影响 SVN 服务器,安全操作
// DeleteLocalRef 仅删除本地远程跟踪引用refs/remotes/<svn-remote>/xxx
// 不影响 SVN 服务器,安全操作。使用 git update-ref -d 而非 git branch -D -r
// 因为后者只适用于 branch 格式的 ref 名,不适用于 tags 路径。
func (self *SvnCommands) DeleteLocalRef(refName string) error {
cmdArgs := NewGitCmd("branch").Arg("-D").Arg("-r").Arg(refName).ToArgv()
cmdArgs := NewGitCmd("update-ref").Arg("-d").Arg(refName).ToArgv()
return self.cmd.New(cmdArgs).Run()
}
@ -255,6 +276,53 @@ func (self *SvnCommands) Fetch() error {
return self.cmd.New(cmdArgs).Run()
}
// InvalidateStatusCache 清除 CheckBranchStatus 的缓存。
// 应在 git svn fetch 之后调用,确保下次检测使用最新数据。
func (self *SvnCommands) InvalidateStatusCache() {
self.statusCacheMutex.Lock()
defer self.statusCacheMutex.Unlock()
self.statusCache = make(map[string]map[string]models.SvnBranchStatus)
self.statusCacheExpiry = make(map[string]time.Time)
}
// PruneStaleRefs 删除 SVN 服务器上已不存在的本地远程跟踪引用。
// refType: “branches” 或 “tags”
// 返回已删除的 ref 相对路径列表(与 RemoteBranch.Name 格式一致)。
func (self *SvnCommands) PruneStaleRefs(refType string) ([]string, error) {
statuses, err := self.CheckBranchStatus(nil, refType)
if err != nil {
return nil, err
}
var pruned []string
refsPrefix := "refs/remotes/" + self.GetSvnRemoteName() + "/"
for path, status := range statuses {
if status == models.SvnBranchStatusStale {
fullRef := refsPrefix + path
if err := self.DeleteLocalRef(fullRef); err != nil {
self.GitCommon.Log.Warnf("failed to prune stale ref %s: %v", fullRef, err)
continue
}
pruned = append(pruned, path)
}
}
return pruned, nil
}
// GetMissingRefs 返回 SVN 服务器上存在但本地未 fetch 的 ref 路径列表。
// refType: “branches” 或 “tags”
func (self *SvnCommands) GetMissingRefs(refType string) ([]string, error) {
statuses, err := self.CheckBranchStatus(nil, refType)
if err != nil {
return nil, err
}
var missing []string
for path, status := range statuses {
if status == models.SvnBranchStatusMissing {
missing = append(missing, path)
}
}
return missing, nil
}
// CheckBranchStatus 检测本地 refs 和 SVN 服务器的差异
// refType: "branches" 或 "tags"
// 返回值map[refRelPath]models.SvnBranchStatus, refRelPath 为 ref 相对路
@ -279,9 +347,10 @@ func (self *SvnCommands) CheckBranchStatus(task gocui.Task, refType string) (map
}
mappings, _ := self.GetSvnRefMappings()
refsPrefix := "refs/remotes/" + self.GetSvnRemoteName() + "/"
// 1. 获取本地 refs遍历该类型所有 mapping 的 RefsPath
// 兼容refs 端影射到非标准路径的配置,如 tags = tags/*:refs/remotes/git-svn/releases/*
// 兼容refs 端影射到非标准路径的配置,如 tags = tags/*:refs/remotes/svn/releases/*
localRefs := make(map[string]bool)
for _, m := range mappings {
if m.Type != refType {
@ -296,7 +365,35 @@ func (self *SvnCommands) CheckBranchStatus(task gocui.Task, refType string) (map
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
if line := strings.TrimSpace(line); line != "" {
// Key 统一为 ref 相对路径
localRefs[strings.TrimPrefix(line, "refs/remotes/git-svn/")] = true
localRefs[strings.TrimPrefix(line, refsPrefix)] = true
}
}
}
// 排除属于其他类型 mapping 的 ref避免误判为 Stale。
// 例如 branches 的 RefsPath 为 refs/remotes/svn, 会匹配到 trunk 和 tags/*。
// 但它们分别属于 trunk 和 tags类型不应出现在 branches 的 localRefs 中。
// 仅当其他 mapping 的 RefsPath 落在当前 refType 某个 mapping 的 RefsPath 范围内时
// 才需要排除——因为只有此时 for-each-ref 才可能返回属于其他类型的 ref。
// 反之若当前 refType 的 RefsPath 更具体(如 tags 的 refs/remotes/svn/tags
// for-each-ref 不会返回其他类型的 ref无需排除。
for _, other := range mappings {
if other.Type == refType {
continue
}
isSubPath := false
for _, m := range mappings {
if m.Type == refType && (other.RefsPath == m.RefsPath || strings.HasPrefix(other.RefsPath, m.RefsPath+"/")) {
isSubPath = true
break
}
}
if isSubPath {
continue
}
for ref := range localRefs {
fullRef := refsPrefix + ref
if fullRef == other.RefsPath || strings.HasPrefix(fullRef, other.RefsPath+"/") {
delete(localRefs, ref)
}
}
}
@ -320,16 +417,24 @@ func (self *SvnCommands) CheckBranchStatus(task gocui.Task, refType string) (map
if line := strings.TrimSpace(line); line != "" {
name := strings.TrimSuffix(line, "/")
// SVN 路径正向影射为 ref 相对路径,与 localRefs 的 key 格式统一
svnBranches[strings.TrimPrefix(m.RefsPath, "refs/remotes/git-svn/")+"/"+name] = true
// m.RefsPath 去掉通配符后无尾斜杠(如 refs/remotes/svn
// 需先补 “/” 再 TrimPrefix refsPrefix (如 refs/remotes/svn/
// 否则 TrimPrefix 不匹配key 变成完整 ref 路径而非相对路径。
svnBranches[strings.TrimPrefix(m.RefsPath+"/", refsPrefix)+name] = true
}
}
} else {
self.GitCommon.Log.Warnf("svn list failed for %s: %v", svnUrl+"/"+m.SvnPath, listErr)
}
}
// svn list 全部失败(网络不通、认证失败等)时返回错误并中止,
// 避免把”全部 Stale”的误导性结果当作真实状态展示错误不缓存下次自动重时
// Svn list failed for all path: svn may be unavailable, network or auth issue.
// Return empty result with nil error to avoid error dialog.
// Stale status keeps default (Unknown), does not affect core functionality.
// Error is not cached, next call will retry automatically.
if svnListAttempted && !svnListOk {
return nil, fmt.Errorf("svn list failed for all %s paths (network or auth error?)", refType)
self.GitCommon.Log.Warnf("svn list failed for all %s paths, skipping stale detection", refType)
return map[string]models.SvnBranchStatus{}, nil
}
// 3. 对比差异

View file

@ -147,9 +147,11 @@ func (self *SyncCommands) FastForward(
}
func (self *SyncCommands) FetchRemote(task gocui.Task, remoteName string) error {
if self.IsGitSvnRepo && remoteName == "git-svn" {
if self.IsGitSvnRepo && remoteName == self.Svn.GetSvnRemoteName() {
cmdArgs := NewGitCmd("svn").Arg("fetch").ToArgv()
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
err := self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
self.Svn.InvalidateStatusCache()
return err
}
cmdArgs := self.fetchCommandBuilder(false).
Arg(remoteName).

View file

@ -56,7 +56,7 @@ func (self *BranchesHelper) ConfirmLocalDelete(branches []*models.Branch) error
}
func (self *BranchesHelper) ConfirmDeleteRemote(remoteBranches []*models.RemoteBranch, resetRemoteBranchesSelection bool) error {
if len(remoteBranches) > 0 && remoteBranches[0].RemoteName == "git-svn" {
if len(remoteBranches) > 0 && self.c.Git().Sync.GitCommon.IsSvnRepo() && remoteBranches[0].RemoteName == self.c.Git().Svn.GetSvnRemoteName() {
return errors.New("cannot use standard remote delete for git-svn remote branches; use SVN-specific deletion")
}
var title string

View file

@ -1891,23 +1891,52 @@ func (self *RefreshHelper) savePullRequestsToCache(prs []*models.GithubPullReque
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")
svnRemoteName := self.c.Git().Svn.GetSvnRemoteName()
pruned, err := self.c.Git().Svn.PruneStaleRefs("tags")
if err != nil {
return err
}
prunedSet := make(map[string]bool)
for _, p := range pruned {
prunedSet[p] = true
}
missing, err := self.c.Git().Svn.GetMissingRefs("tags")
if err != nil {
return err
}
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
refsPrefix := "refs/remotes/" + svnRemoteName + "/"
if len(prunedSet) > 0 {
self.c.Model().Tags = lo.Filter(self.c.Model().Tags, func(t *models.Tag, _ int) bool {
if !t.IsSvnTag() {
return true
}
relPath := strings.TrimPrefix(t.FullRefName(), refsPrefix)
return prunedSet[relPath]
})
}
existingNames := make(map[string]bool)
for _, t := range self.c.Model().Tags {
if t.IsSvnTag() {
relPath := strings.TrimPrefix(t.FullRefName(), refsPrefix)
existingNames[relPath] = true
}
}
for _, m := range missing {
if !existingNames[m] {
self.c.Model().Tags = append(self.c.Model().Tags,
&models.Tag{
Name: m,
Message: "(SVN tag, not fetched)",
FullRefNameOverride: refsPrefix + m,
StaleStatus: models.SvnBranchStatusMissing,
})
}
}
// 仅重绘视图,不 Refresh(TAGS),打断无限刷新链
self.c.PostRefreshUpdate(self.c.Contexts().Tags)
return nil
})

View file

@ -141,7 +141,7 @@ func (self *RemoteBranchesController) context() *context.RemoteBranchesContext {
}
func (self *RemoteBranchesController) delete(selectedBranches []*models.RemoteBranch) error {
if len(selectedBranches) > 0 && selectedBranches[0].RemoteName == "git-svn" {
if len(selectedBranches) > 0 && self.c.Git().Sync.GitCommon.IsSvnRepo() && selectedBranches[0].RemoteName == self.c.Git().Svn.GetSvnRemoteName() {
return self.deleteSvnRemoteBranches(selectedBranches)
}
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranches, true)

View file

@ -146,7 +146,7 @@ func (self *RemotesController) enter(remote *models.Remote) error {
self.c.PostRefreshUpdate(remoteBranchesContext)
// SVN 自动 stale 检测
if remote.Name == "git-svn" && self.c.Git().Sync.GitCommon.IsSvnRepo() {
if remote.Name == self.c.Git().Svn.GetSvnRemoteName() && self.c.Git().Sync.GitCommon.IsSvnRepo() {
self.checkSvnBranchStatusAsync()
}
self.c.Context().Push(remoteBranchesContext, types.OnFocusOpts{})
@ -374,6 +374,9 @@ func (self *RemotesController) fetchAndCheckout(remote *models.Remote, branchNam
refreshOptions := types.RefreshOptions{
Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES},
}
if self.c.Git().Sync.GitCommon.IsSvnRepo() {
refreshOptions.Scope = append(refreshOptions.Scope, types.TAGS)
}
if branchName != "" {
err = self.c.Git().Branch.New(branchName, remote.Name+"/"+branchName)
if err == nil {
@ -393,13 +396,17 @@ func (self *RemotesController) fetchAndCheckout(remote *models.Remote, branchNam
}
}
self.c.RefreshFromWorker(refreshOptions)
if self.c.Git().Sync.GitCommon.IsSvnRepo() && remote.Name == self.c.Git().Svn.GetSvnRemoteName() {
self.checkSvnBranchStatusAsync()
}
return err
})
}
func (self *RemotesController) notGitSvnRemote() *types.DisabledReason {
remote := self.context().GetSelected()
if remote != nil && remote.Name == "git-svn" {
if remote != nil && self.c.Git().Sync.GitCommon.IsSvnRepo() && remote.Name == self.c.Git().Svn.GetSvnRemoteName() {
return &types.DisabledReason{Text: "Cannot modify git-svn remote"}
}
return nil

View file

@ -23,12 +23,7 @@ func getRemoteBranchDisplayStrings(b *models.RemoteBranch, diffed bool) []string
}
name := b.Name
// SVN stale 标记
switch b.StaleStatus {
case models.SvnBranchStatusStale:
name = b.Name + "⚠"
textStyle = style.FgRed
case models.SvnBranchStatusMissing:
if b.StaleStatus == models.SvnBranchStatusMissing {
name = b.Name + "(not fetched)"
textStyle = style.FgWhite
}

View file

@ -44,16 +44,10 @@ func getTagDisplayStrings(
}
name := t.Name
// SVN stale 标记
if t.IsSvnTag() {
switch t.StaleStatus {
case models.SvnBranchStatusStale:
name = t.Name + "⚠"
textStyle = style.FgRed
case models.SvnBranchStatusMissing:
name = t.Name + "(not fetched)"
textStyle = style.FgWhite
}
if t.IsSvnTag() && t.StaleStatus == models.SvnBranchStatusMissing {
name = t.Name + "(not fetched)"
textStyle = style.FgWhite
}
descriptionColor := style.FgYellow