mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Enhance lazygit function for git-svn
This commit is contained in:
parent
074146926a
commit
5cefdfd75e
|
|
@ -35,6 +35,7 @@ type GitCommand struct {
|
|||
Tag *git_commands.TagCommands
|
||||
WorkingTree *git_commands.WorkingTreeCommands
|
||||
Bisect *git_commands.BisectCommands
|
||||
Svn *git_commands.SvnCommands
|
||||
Worktree *git_commands.WorktreeCommands
|
||||
Version *git_commands.GitVersion
|
||||
RepoPaths *git_commands.RepoPaths
|
||||
|
|
@ -119,6 +120,7 @@ func NewGitCommandAux(
|
|||
|
||||
gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, repoPaths, configCommands, diffRendererConfigManager)
|
||||
|
||||
svnCommands := git_commands.NewSvnCommands(gitCommon, cmd)
|
||||
fileLoader := git_commands.NewFileLoader(gitCommon, cmd, configCommands)
|
||||
statusCommands := git_commands.NewStatusCommands(gitCommon)
|
||||
flowCommands := git_commands.NewFlowCommands(gitCommon)
|
||||
|
|
@ -145,14 +147,14 @@ func NewGitCommandAux(
|
|||
gitHubCommands := git_commands.NewGitHubCommands(gitCommon)
|
||||
hostingServiceCommands := git_commands.NewHostingServiceCommand(gitCommon)
|
||||
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands, svnCommands)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.WorkingTreeState, gitCommon)
|
||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd)
|
||||
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, gitCommon)
|
||||
worktreeLoader := git_commands.NewWorktreeLoader(gitCommon)
|
||||
stashLoader := git_commands.NewStashLoader(cmn, cmd)
|
||||
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
||||
tagLoader := git_commands.NewTagLoader(cmn, cmd, gitCommon)
|
||||
|
||||
return &GitCommand{
|
||||
Blame: blameCommands,
|
||||
|
|
@ -172,6 +174,7 @@ func NewGitCommandAux(
|
|||
Sync: syncCommands,
|
||||
Tag: tagCommands,
|
||||
Bisect: bisectCommands,
|
||||
Svn: svnCommands,
|
||||
WorkingTree: workingTreeCommands,
|
||||
Worktree: worktreeCommands,
|
||||
Version: version,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ type BranchLoader struct {
|
|||
cmd oscommands.ICmdObjBuilder
|
||||
getCurrentBranchInfo func() (BranchInfo, error)
|
||||
config BranchLoaderConfigCommands
|
||||
svn *SvnCommands
|
||||
}
|
||||
|
||||
func NewBranchLoader(
|
||||
|
|
@ -53,6 +54,7 @@ func NewBranchLoader(
|
|||
cmd oscommands.ICmdObjBuilder,
|
||||
getCurrentBranchInfo func() (BranchInfo, error),
|
||||
config BranchLoaderConfigCommands,
|
||||
svn *SvnCommands,
|
||||
) *BranchLoader {
|
||||
return &BranchLoader{
|
||||
Common: cmn,
|
||||
|
|
@ -60,6 +62,7 @@ func NewBranchLoader(
|
|||
cmd: cmd,
|
||||
getCurrentBranchInfo: getCurrentBranchInfo,
|
||||
config: config,
|
||||
svn: svn,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +128,34 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit,
|
|||
if match != nil {
|
||||
branch.UpstreamRemote = match.Remote
|
||||
branch.UpstreamBranch = match.Merge
|
||||
} else if !branch.DetachedHead && self.GitCommon.IsSvnRepo() && self.svn != nil {
|
||||
// git-svn: .git/config 中无 tracking 配置,通过 git-svn-id 反推
|
||||
remote, upstreamBranch, err := self.svn.GetSvnUpstream(branch.Name)
|
||||
if err == nil && remote != "" {
|
||||
branch.UpstreamRemote = remote
|
||||
branch.UpstreamBranch = upstreamBranch
|
||||
|
||||
// 计算 ahead/behind (使状态指示器显示✓,而非?)
|
||||
upstreamRef := fmt.Sprintf("refs/remotes/%s/%s", remote, upstreamBranch)
|
||||
revOutput, revErr := self.cmd.New(
|
||||
NewGitCmd("rev-list").
|
||||
Arg("--left-right").
|
||||
Arg(fmt.Sprintf("%s...%s", branch.FullRefName(), upstreamRef)).
|
||||
ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if revErr == nil {
|
||||
parts := strings.Split(strings.TrimSpace(revOutput), "\t")
|
||||
if len(parts) == 2 {
|
||||
branch.AheadForPull = strings.TrimSpace(parts[0])
|
||||
branch.BehindForPull = strings.TrimSpace(parts[1])
|
||||
branch.AheadForPush = branch.AheadForPull
|
||||
branch.BehindForPush = branch.BehindForPull
|
||||
}
|
||||
} else {
|
||||
// 远程引用不存在(已被删除),标记为 gone
|
||||
branch.UpstreamGone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the branch already existed, take over its BehindBaseBranch value
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package git_commands
|
|||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string
|
|||
}
|
||||
|
||||
func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName string, branchNames []string) error {
|
||||
if remoteName == "git-svn" {
|
||||
return fmt.Errorf("cannot delete git-svn remote branch via git push; use svn delete instead")
|
||||
}
|
||||
cmdArgs := NewGitCmd("push").
|
||||
Arg(remoteName, "--delete").
|
||||
Arg(lo.Map(branchNames, func(b string, _ int) string { return "refs/heads/" + b })...).
|
||||
|
|
|
|||
|
|
@ -16,15 +16,18 @@ import (
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +55,30 @@ func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
|
|||
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
|
||||
|
|
@ -162,3 +189,13 @@ func (self *RemoteLoader) getRemoteBranchesByRemoteName() (map[string][]*models.
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
322
pkg/commands/git_commands/svn_commands.go
Normal file
322
pkg/commands/git_commands/svn_commands.go
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
// SvnRefMapping 表示git-svn配置中的一组路径映射
|
||||
// 例如branches = branches/proj1/*:refs/remotes/git-svn/branches/*
|
||||
// SvnPath 为 "branches/proj1/*", RefsPath 为 "refs/remotes/git-svn/branches/*", Type 为 "branches"
|
||||
type SvnRefMapping struct {
|
||||
SvnPath string // SVN 路径,含通配符,如"branches/proj1/*"
|
||||
RefsPath string // Git refs 路径前缀,如”"refs/remotes/git-svn/branches"
|
||||
Type string // 类型:"trunk" | "branches" | "tags"
|
||||
}
|
||||
|
||||
type SvnCommands struct {
|
||||
*GitCommon
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
// 缓存 SVN ref 映射,避免重复解析git config
|
||||
svnRefMappingsCache *[] SvnRefMapping
|
||||
svnUrlCache string
|
||||
svnUrlCacheExpiry time.Time
|
||||
}
|
||||
|
||||
func NewSvnCommands(gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder) *SvnCommands {
|
||||
return &SvnCommands{GitCommon: gitCommon, cmd: cmd}
|
||||
}
|
||||
|
||||
// GetSvnUrl 从git config获取SVN仓库URL
|
||||
// 返回值如 https://svn.example.com/repo
|
||||
// 结果缓存60秒
|
||||
func (self *SvnCommands) GetSvnUrl() (string, error) {
|
||||
if self.svnUrlCache != "" && time.Now().Before(self.svnUrlCacheExpiry) {
|
||||
return self.svnUrlCache, nil
|
||||
}
|
||||
|
||||
output, err := self.cmd.New(
|
||||
NewGitCmd("config").Arg("--get", "svn-remote.svn.url").ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
self.svnUrlCache = strings.TrimSpace(output)
|
||||
self.svnUrlCacheExpiry = time.Now().Add(60 * time.Second)
|
||||
return self.svnUrlCache, nil
|
||||
}
|
||||
|
||||
// GetSvnRefMappings 解析 svn-remote.svn 配置,返回trunk/branches/tags的refs路径前缀
|
||||
// 这是识别SVN tags的核心方法,能处理非标准 tags 目录名(如 "release/proj1")
|
||||
// 示例配置:
|
||||
// [svn-remote "svn"]
|
||||
// url = https://svn.example.com/repo
|
||||
// fetch = trunk/proj1:refs/remotes/git-svn/trunk
|
||||
// branches = branches/proj1/*:refs/remotes/git-svn/branches/*
|
||||
// tags = release/proj1/*:refs/remotes/git-svn/tags/*
|
||||
// 返回:
|
||||
// {SvnPath: "trunk/proj1", RefsPath: "refs/remotes/git-svn/trunk", Type: "trunk"}
|
||||
// {SvnPath: "branches/proj1/*", RefsPath: "refs/remotes/git-svn/branches", Type: "branches"}
|
||||
// {SvnPath: "release/proj1/*", RefsPath: "refs/remotes/git-svn/tags", Type: "tags"}
|
||||
func (self *SvnCommands) GetSvnRefMappings() ([]SvnRefMapping, error) {
|
||||
if self.svnRefMappingsCache != nil {
|
||||
return *self.svnRefMappingsCache, nil
|
||||
}
|
||||
|
||||
mappings := []SvnRefMapping{}
|
||||
|
||||
// 1. fetch (trunk) - 格式:trunk/proj1:refs/remotes/git-svn/trunk
|
||||
fetchVal, err := self.cmd.New(
|
||||
NewGitCmd("config").Arg("--get", "svn-remote.svn.fetch").ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err == nil && strings.TrimSpace(fetchVal) != "" {
|
||||
mappings = append(mappings, self.parseSvnRefMapping(strings.TrimSpace(fetchVal), "trunk")...)
|
||||
}
|
||||
|
||||
// 2. branches (可能多组) - git config --get-all
|
||||
branchesOutput, _ := self.cmd.New(
|
||||
NewGitCmd("config").Arg("--get-all", "svn-remote.svn.branches").ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
for _, line := range strings.Split(strings.TrimSpace(branchesOutput), "\n") {
|
||||
if line := strings.TrimSpace(line); line != "" {
|
||||
mappings = append(mappings, self.parseSvnRefMapping(line, "branches")...)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. tags (可能多组)
|
||||
tagsOutput, _ := self.cmd.New(
|
||||
NewGitCmd("config").Arg("--get-all", "svn-remote.svn.tags").ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
for _, line := range strings.Split(strings.TrimSpace(tagsOutput), "\n") {
|
||||
if line := strings.TrimSpace(line); line != "" {
|
||||
mappings = append(mappings, self.parseSvnRefMapping(line, "tags")...)
|
||||
}
|
||||
}
|
||||
|
||||
self.svnRefMappingsCache = &mappings
|
||||
return mappings, nil
|
||||
}
|
||||
|
||||
// parseSvnRefMapping 解析单条映射
|
||||
// 输入格式: "branches/proj1/*:refs/remotes/git-svn/branches/*"
|
||||
// 输出:SvnPath="branches/proj1*", RefsPath="refs/remotes/git-svn/branches", Type="branches"
|
||||
func (self *SvnCommands) parseSvnRefMapping(line, defaultType string) []SvnRefMapping {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil
|
||||
}
|
||||
svnPath := strings.TrimSpace(parts[0])
|
||||
refsPath := strings.TrimSpace(parts[1])
|
||||
|
||||
// 去掉末尾的 /* 通配符
|
||||
svnPathBase := strings.TrimSuffix(svnPath, "/*")
|
||||
refsPathBase := strings.TrimSuffix(refsPath, "/*")
|
||||
|
||||
return []SvnRefMapping{{
|
||||
SvnPath: svnPathBase,
|
||||
RefsPath: refsPathBase,
|
||||
Type: defaultType,
|
||||
}}
|
||||
}
|
||||
|
||||
// GetTagsRefsPaths 返回所有tags类型的 refs 路径前缀列表
|
||||
// 用于 TagLoader 扫描 SVN tags
|
||||
func (self *SvnCommands) GetTagsRefsPaths() ([]string, error) {
|
||||
mappings, err := self.GetSvnRefMappings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var paths []string
|
||||
for _, m := range mappings {
|
||||
if m.Type == "tags" {
|
||||
paths = append(paths, m.RefsPath)
|
||||
}
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// GetSvnUpstream 通过 commit message 中的 git-svn-id 反推本地分支对应的 SVN 远程分支
|
||||
// 用于 BranchLoader.Load() 循环中回填 upstream 信息
|
||||
func (self *SvnCommands) GetSvnUpstream(branchName string) (string, string, error) {
|
||||
output, err := self.cmd.New(
|
||||
NewGitCmd("log").
|
||||
Arg("--grep=git-svn-id").
|
||||
Arg("--format=%B").
|
||||
Arg("-1").
|
||||
Arg(branchName).
|
||||
ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil || strings.TrimSpace(output) == "" {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
svnCommitUrl, ok := self.parseSvnIdLine(output)
|
||||
if !ok {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
svnRootUrl, err := self.GetSvnUrl()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
svnRootUrl = strings.TrimSuffix(svnRootUrl, "/")
|
||||
relPath := strings.TrimPrefix(svnCommitUrl, svnRootUrl)
|
||||
relPath = strings.TrimPrefix(relPath, "/")
|
||||
if relPath == "" {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
mappings, err := self.GetSvnRefMappings()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
for _, m := range mappings {
|
||||
if relPath == m.SvnPath {
|
||||
upstreamBranch := strings.TrimPrefix(m.RefsPath, "refs/remotes/git-svn")
|
||||
return "git-svn", 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
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
func (self *SvnCommands) parseSvnIdLine(commitMessage string) (string, bool) {
|
||||
for _, line := range strings.Split(commitMessage, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "git-svn-id: ") {
|
||||
rest := string.TrimPrefix(line, "git-svn-id: ")
|
||||
parts := strings.SplitN(rest, " ", 2)
|
||||
urlWithRev := parts[0]
|
||||
atIdx := strings.LastIndex(urlWithRev, "@")
|
||||
if atIdx > 0 {
|
||||
return urlWithRev[:atIdx], true
|
||||
}
|
||||
return urlWithRev, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// CreateBranch 使用 git svn branch 在 SVN 仓库创建分支
|
||||
// branchName 取决于 clone 时的 --branches 配置
|
||||
// 例如 clone 时指定 --branches=branches/proj1,则输入 "xxx" 创建 branches/proj1/xxx
|
||||
func (self *SvnCommands) CreateBranch(branchName string) error {
|
||||
cmdArgs := NewGitCmd("svn").Arg("branch").Arg("-m").Arg(fmt.Sprintf("Create branch %s", branchName)).Arg(branchName).ToArgv()
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// DeleteServerBranch 从 SVN 服务器删除分支
|
||||
// branchPath 是相对于 SVN 根的路径,如 "branches/proj1/xxx"
|
||||
// 实现:执行 svn delete -m "..." <svn-url>/<branchPath>
|
||||
func (self *SvnCommands) DeleteServerBranch(task gocui.Task, branchPath string) error {
|
||||
svnUrl, err := self.GetSvnUrl()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmdArgs := NewGitCmd("svn").Arg("delete").Arg(fmt.Sprintf("%s/%s", svnUrl, branchPath)).Arg("-m").Arg(fmt.Sprintf("Delete branch %s", branchPath)).ToArgv()
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
|
||||
}
|
||||
|
||||
// DeleteLocalRef 仅删除本地远程跟踪引用(refs/remotes/git-svn/xxx)
|
||||
// 不影响 SVN 服务器,安全操作
|
||||
func (self *SvnCommands) DeleteLocalRef(refName string) error {
|
||||
cmdArgs := NewGitCmd("branch").Arg("-D").Arg("-r").Arg(refName).ToArgv()
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// Fetch 执行 git svn fetch -all 获取 SVN 更新
|
||||
func (self *SvnCommands) Fetch() error {
|
||||
cmdArgs := NewGitCmd("svn").Arg("fetch").Arg("-all").ToArgv()
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// CheckBranchStatus 检测本地 refs 和 SVN 服务器的差异
|
||||
// refType: "branches" 或 "tags"
|
||||
// 返回值:map[branchPath]models.SvnBranchStatus, branchPath 如 "branches/proj1/xxx"
|
||||
// SVN list 使用 --non-interactive 防止网络阻塞,结果不缓存(每次进入时重新检测)
|
||||
func (self *SvnCommands) CheckBranchStatus(task gocui.Task, refType string) (map[string]models.SvnBranchStatus, error) {
|
||||
svnUrl, err != self.GetSvnUrl()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 1. 获取本地 refs
|
||||
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 _, line := range strings.Split(strings.TrimSpace(output), "\n") {
|
||||
if line := strings.TrimSpace(line); line != "" {
|
||||
relPath := strings.TrimPrefix(line, refsPath+"/")
|
||||
localRefs[relPath] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 获取 SVN 服务器上的分支列表(遍历所有 SvnRefMappings)
|
||||
mappings, _ := self.GetSvnRefMappings()
|
||||
svnBranches := make(map[string]bool)
|
||||
for _, m := range mappings {
|
||||
if m.Type != refType {
|
||||
continue
|
||||
}
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 对比差异
|
||||
result := make(map[string]models.SvnBranchStatus)
|
||||
allPaths := make(map[string]bool)
|
||||
for k := range localRefs {
|
||||
allPaths[k] = true
|
||||
}
|
||||
for k := range svnBranches {
|
||||
allPaths[k] = true
|
||||
}
|
||||
|
||||
for path := range allPaths {
|
||||
hasLocal := localRefs[path]
|
||||
hasSvn := svnBranches[path]
|
||||
var status models.SvnBranchStatus
|
||||
if hasLocal && hasSvn {
|
||||
status = models.SvnBranchStatusOk
|
||||
} else if hasLocal && !hasSvn {
|
||||
status = models.SvnBranchStatusStale
|
||||
} else if !hasLocal && hasSvn {
|
||||
status = models.SvnBranchStatusMissing
|
||||
} else {
|
||||
status = models.SvnBranchStatusUnknown
|
||||
}
|
||||
result[path] = status
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -85,6 +85,13 @@ func (self *SyncCommands) Fetch(task gocui.Task) error {
|
|||
}
|
||||
|
||||
func (self *SyncCommands) FetchBackgroundCmdObj() *oscommands.CmdObj {
|
||||
if self.IsGitSvnRepo {
|
||||
cmdArgs := NewGitCmd("svn").Arg("fetch").ToArgv()
|
||||
cmdObj := self.cmd.New(cmdArgs)
|
||||
cmdObj.DontLog().FailOnCredentialRequest()
|
||||
cmdObj.SuppressOutputUnlessError()
|
||||
return cmdObj
|
||||
}
|
||||
cmdArgs := self.fetchCommandBuilder(self.UserConfig().Git.FetchAll).ToArgv()
|
||||
|
||||
cmdObj := self.cmd.New(cmdArgs)
|
||||
|
|
@ -140,6 +147,10 @@ func (self *SyncCommands) FastForward(
|
|||
}
|
||||
|
||||
func (self *SyncCommands) FetchRemote(task gocui.Task, remoteName string) error {
|
||||
if self.IsGitSvnRepo && remoteName == "git-svn" {
|
||||
cmdArgs := NewGitCmd("svn").Arg("fetch").ToArgv()
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
|
||||
}
|
||||
cmdArgs := self.fetchCommandBuilder(false).
|
||||
Arg(remoteName).
|
||||
ToArgv()
|
||||
|
|
|
|||
|
|
@ -13,15 +13,18 @@ import (
|
|||
type TagLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
gitCommon *GitCommon
|
||||
}
|
||||
|
||||
func NewTagLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
gitCommon *GitCommon,
|
||||
) *TagLoader {
|
||||
return &TagLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
gitCommon: gitCommon,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,5 +55,48 @@ func (self *TagLoader) GetTags() ([]*models.Tag, error) {
|
|||
}
|
||||
})
|
||||
|
||||
// SVN 仓库:追究扫描 refs/remotes/git-svn/tags/* 下的引用
|
||||
if self.gitCommon != nil && self.gitCommon.IsSvnRepo() {
|
||||
tagsPaths, err := self.gitCommon.Svn.GetTagsRefsPaths()
|
||||
if err == nil && len(tagsPaths) > 0 {
|
||||
for _, tagsPath := range tagsPaths {
|
||||
svnTags, err := self.getTagsFromPath(tagsPath)
|
||||
if err == nil {
|
||||
tags = append(tags, svnTags...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// getTagsFromPath 从指定 refs 路径下获取所有 tag 引用
|
||||
func (self *TagLoader) getTagsFromPath(basePath string) ([]*models.Tag, error) {
|
||||
var svnTags []*models.Tag
|
||||
cmdArgs := NewGitCmd("for-each-ref").
|
||||
Arg("--sort=-creatordate").
|
||||
Arg("--format=%(refname)").
|
||||
Arg(basePath).
|
||||
ToArgv()
|
||||
|
||||
err := self.cmd.New(cmdArgs).DontLog().RunAndProcessLines(func(line, string) (bool, error){
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
tagName := strings.TrimPrefix(line, basePath+"/")
|
||||
if tagName == "" || tagName == line {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
svnTags = append(svnTags, &models.Tag{
|
||||
Name: tagName,
|
||||
Message: "(SVN tag)",
|
||||
FullRefNameOverride: line,
|
||||
})
|
||||
return false, nil
|
||||
})
|
||||
|
||||
return svnTags, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package models
|
|||
type RemoteBranch struct {
|
||||
Name string
|
||||
RemoteName string
|
||||
StaleStatus SvnBranchStatus
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) FullName() string {
|
||||
|
|
|
|||
11
pkg/commands/models/svn_branch_status.go
Normal file
11
pkg/commands/models/svn_branch_status.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package models
|
||||
|
||||
// SvnBranchStatus 表示 SVN 分支的状态
|
||||
type SvnBranchStatus int
|
||||
|
||||
const (
|
||||
SvnBranchStatusUnknown SvnBranchStatus = itoa
|
||||
SvnBranchStatusOk // 正常: 本地和 SVN 服务器都存在
|
||||
SvnBranchStatusStale // Stale:本地有引用但 SVN 服务器已删除
|
||||
SvnBranchStatusMissing // Missing:本地未 fetch 但 SVN 服务器上有
|
||||
)
|
||||
|
|
@ -1,11 +1,20 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Tag : A git tag
|
||||
type Tag struct {
|
||||
Name string
|
||||
// this is either the first line of the message of an annotated tag, or the
|
||||
// first line of a commit message for a lightweight tag
|
||||
Message string
|
||||
// FullRefNameOverride 当非空时,FullRefName() 返回此值而非默认的 refs/tags/<Name>
|
||||
// 用于 SVN 类型的 tag,其引用路径在 refs/remotes/git-svn/tags/* 而非 refs/tags/*
|
||||
FullRefNameOverride string
|
||||
// StaleStatus 表示 SVN tag 的 stale 状态
|
||||
StaleStatus SvnBranchStatus
|
||||
}
|
||||
|
||||
func (t *Tag) FullRefName() string {
|
||||
|
|
@ -35,3 +44,8 @@ func (t *Tag) URN() string {
|
|||
func (t *Tag) Description() string {
|
||||
return t.Message
|
||||
}
|
||||
|
||||
// IsSvnTag 判断是否为 SVN tag
|
||||
func (t *Tag) IsSvnTag() bool {
|
||||
return t.FullRefNameOverride != "" && strings.HasPrefix(t.FullRefNameOverride, "refs/remotes/")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,7 +359,7 @@ type GitConfig struct {
|
|||
// When copying commit hashes to the clipboard, truncate them to this length. Set to 40 to disable truncation.
|
||||
TruncateCopiedCommitHashesTo int `yaml:"truncateCopiedCommitHashesTo"`
|
||||
// If true, will detect if git repository is created using git-svn, is so, will use git svn dcommit/rebase for push/pull operations.
|
||||
EnableGitSvnCompat bool `toml:"EnableGitSvnCompat"`
|
||||
EnableGitSvnCompat bool `yaml:"EnableGitSvnCompat" jsonschema:"default=true"`
|
||||
}
|
||||
|
||||
type DiffRendererCommandType string
|
||||
|
|
|
|||
|
|
@ -776,6 +776,31 @@ func (self *BranchesController) rename(branch *models.Branch) error {
|
|||
}
|
||||
|
||||
func (self *BranchesController) newBranch(selectedBranch *models.Branch) error {
|
||||
if self.c.Git().Sync.GitCommon.IsSvnRepo() {
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.NewBranch,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
LabelColumns: []string{self.c.Tr.NewBranch},
|
||||
Key: 'l',
|
||||
OnPress: func() error {
|
||||
return self.c.Helpers().Refs.NewBranch(
|
||||
selectedBranch.FullRefName(),
|
||||
selectedBranch.RefName(),
|
||||
"",
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
LabelColumns: []string{self.c.Tr.NewSvnBranch},
|
||||
Key: 's',
|
||||
OnPress: func() error {
|
||||
return self.newSvnBranch(selectedBranch)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return self.c.Helpers().Refs.NewBranch(selectedBranch.FullRefName(), selectedBranch.RefName(), "")
|
||||
}
|
||||
|
||||
|
|
@ -927,3 +952,36 @@ func (self *BranchesController) notMergingIntoYourself(branch *models.Branch) *t
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newSvnBranch 提示用户输入 SVN 分支名,然后创建分支并 fetch 刷新
|
||||
func (self *BranchesController)newSvnBranch(selectedBranch *models.Branch) error {
|
||||
self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.SvnCreateBranchTitle,
|
||||
InitialContent: "",
|
||||
HandleConfirm: func(response string) error {
|
||||
branchName := strings.TrimSpace(response)
|
||||
if branchName == "" {
|
||||
return errors.New("branch name cannot be empty")
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.NewSvnBranch)
|
||||
|
||||
return self.c.WithWaitingStatus(self.c.Tr.SvnFetchingStatus, func(task gocui.Task) error {
|
||||
if err := self.c.Git().Svn.CreateBranch(branchName); err != nil {
|
||||
return fmt.Errorf(self.c.Tr.SvnOperationFailed, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
if err := self.c.Git().Svn.Fetch(); err != nil {
|
||||
self.c.ErrorMsg(fmt.Sprintf(self.c.Tr.SvnFetchFailed))
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.SYNC,
|
||||
Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES},
|
||||
})
|
||||
return nil
|
||||
})
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ 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" {
|
||||
return errors.New("cannot use standard remote delete for git-svn remote branches; use SVN-specific deletion")
|
||||
}
|
||||
var title string
|
||||
if len(remoteBranches) == 1 {
|
||||
title = utils.ResolvePlaceholderString(
|
||||
|
|
|
|||
|
|
@ -1124,6 +1124,11 @@ func (self *RefreshHelper) refreshTags(env refreshEnv) error {
|
|||
self.c.Model().Tags = tags
|
||||
})
|
||||
|
||||
// SVN 自动 stale 检测 (tags)
|
||||
if self.c.Git().Sync.GitCommon.IsSvnRepo() {
|
||||
self.checkSvnTagStatusAsync(tags)
|
||||
}
|
||||
|
||||
self.refreshView(self.c.Contexts().Tags, env)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1883,3 +1888,22 @@ func (self *RefreshHelper) savePullRequestsToCache(prs []*models.GithubPullReque
|
|||
self.c.Log.Warnf("error saving GitHub pull request cache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) checkSvnTagStatusAsync(tags []*models.Tag) {
|
||||
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 {
|
||||
tag.StaleStatus = status
|
||||
}
|
||||
}
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{
|
||||
Scope: []types.RefreshableView{types.TAGS},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
|
|
@ -139,6 +140,9 @@ func (self *RemoteBranchesController) context() *context.RemoteBranchesContext {
|
|||
}
|
||||
|
||||
func (self *RemoteBranchesController) delete(selectedBranches []*models.RemoteBranch) error {
|
||||
if len(selectedBranches) > 0 && selectedBranches[0].RemoteName == "git-svn" {
|
||||
return self.deleteSvnRemoteBranches(selectedBranches)
|
||||
}
|
||||
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranches, true)
|
||||
}
|
||||
|
||||
|
|
@ -207,3 +211,87 @@ func (self *RemoteBranchesController) newLocalBranch(selectedBranch *models.Remo
|
|||
func (self *RemoteBranchesController) checkoutBranch(selectedBranch *models.RemoteBranch) error {
|
||||
return self.c.Helpers().Refs.CheckoutRemoteBranch(selectedBranch.FullName(), selectedBranch.Name)
|
||||
}
|
||||
|
||||
func (self *RemoteBranchesController) deleteSvnRemoteBranches(selectedBranches []*models.RemoteBranch) error {
|
||||
var menuTitle string
|
||||
if len(selectedBranches) == 1 {
|
||||
menuTitle = utils.ResolvePlaceholderString(
|
||||
self.c.Tr.SvnDeleteBranchTitle,
|
||||
map[string]string{"selectedBranchName": selectedBranches[0].Name},
|
||||
)
|
||||
} else {
|
||||
menuTitle = self.c.Tr.SvnDeleteBranchTitle
|
||||
}
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: menuTitle,
|
||||
Items: []*types.menuTitle{
|
||||
{
|
||||
LabelColumns: []string{self.c.Tr.DeleteSvnLocalRef},
|
||||
Key: 'l',
|
||||
OnPress: func() error {
|
||||
return self.deleteSvnLocalRefs(selectedBranches)
|
||||
},
|
||||
},
|
||||
{
|
||||
LabelColumns: []string{self.c.Tr.DeleteSvnBoth},
|
||||
Key: 'b',
|
||||
OnPress: func() error {
|
||||
return self.confirmDeleteSvnBoth(selectedBranches)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RemoteBranchesController) deleteSvnLocalRefs(selectedBranches []*models.RemoteBranch) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error {
|
||||
for _, branch := range selectedBranches {
|
||||
refName := branch.RemoteName + "/" + branch.Name
|
||||
if err := self.c.Git().Svn.DeleteSvnLocalRef(refName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
self.c.Contexts().RemoteBranches.CollapseRangeSelectionToTop()
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.ASYNC,
|
||||
Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES},
|
||||
})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RemoteBranchesController) confirmDeleteSvnBoth(selectedBranches []models.RemoteBranch) error {
|
||||
var prompt string
|
||||
if len(selectedBranches) == 1 {
|
||||
prompt = utils.ResolvePlaceholderString(
|
||||
self.c.Tr.DeleteSvnBothConfirm,
|
||||
map[string]string{"branchPath": selectedBranches[0].Name},
|
||||
)
|
||||
} else {
|
||||
prompt = fmt.Sprintf("This will delete %d branches from both local refs and SVN server. Continue?", len(selectedBranches))
|
||||
}
|
||||
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
Title: self.c.Tr.SvnDeleteBranchTitle,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
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()})
|
||||
}
|
||||
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
|
||||
})
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*typ
|
|||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Remove),
|
||||
Handler: self.withItem(self.remove),
|
||||
GetDisabledReason: self.require(self.singleItemSelected()),
|
||||
GetDisabledReason: self.require(self.singleItemSelected(), self.notGitSvnRemote),
|
||||
Description: self.c.Tr.Remove,
|
||||
Tooltip: self.c.Tr.RemoveRemoteTooltip,
|
||||
DisplayOnScreen: true,
|
||||
|
|
@ -68,7 +68,7 @@ func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*typ
|
|||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Edit),
|
||||
Handler: self.withItem(self.edit),
|
||||
GetDisabledReason: self.require(self.singleItemSelected()),
|
||||
GetDisabledReason: self.require(self.singleItemSelected(), self.notGitSvnRemote),
|
||||
Description: self.c.Tr.Edit,
|
||||
Tooltip: self.c.Tr.EditRemoteTooltip,
|
||||
DisplayOnScreen: true,
|
||||
|
|
@ -145,6 +145,10 @@ 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() {
|
||||
self.checkSvnBranchStatusAsync()
|
||||
}
|
||||
self.c.Context().Push(remoteBranchesContext, types.OnFocusOpts{})
|
||||
return nil
|
||||
}
|
||||
|
|
@ -392,3 +396,25 @@ func (self *RemotesController) fetchAndCheckout(remote *models.Remote, branchNam
|
|||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RemotesController) notGitSvnRemote() *types.DisabledReason {
|
||||
remote := self.Context().GetSelected()
|
||||
if remote != nil && remote.Name == "git-svn" {
|
||||
return &types.DisabledReason{Text: "Cannot modify git-svn remote"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *RemotesController) checkSvnBranchStatusAsync() {
|
||||
self.c.WithWaitingStatus(self.c.Tr.CheckingSvnStatus, func (task gocui.Task) error {
|
||||
statuses, err := self.c.Git().Svn.CheckBranchStatus(task, "branches")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 将结果写入 RemoteBranch 模型的 StaleStatus 字段
|
||||
// 通过 Refresh 触发 presentation 层重新渲染
|
||||
return self.c.Refresh(types.RefreshOptions{
|
||||
Scope: []types.RefreshableView{types.REMOTES},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,12 +181,6 @@ func (self *SyncController) pullWithLock(task gocui.Task, opts PullFilesOptions)
|
|||
},
|
||||
)
|
||||
|
||||
if self.c.Git().Sync.GitCommon.IsSvnRepo() {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Git-SVN rebase failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseAndSelectHeadCommit(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package presentation
|
|||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
|
@ -21,6 +22,17 @@ func getRemoteBranchDisplayStrings(b *models.RemoteBranch, diffed bool) []string
|
|||
textStyle = theme.DiffTerminalColor
|
||||
}
|
||||
|
||||
name := b.Name
|
||||
// SVN stale 标记
|
||||
switch b.StaleStatus {
|
||||
case models.SvnBranchStatusStale:
|
||||
name = b.Name + "⚠"
|
||||
textStyle = style.FgRed
|
||||
case models.SvnBranchStatusMissing:
|
||||
name = b.Name + "(not fetched)"
|
||||
textStyle = style.FgWhite
|
||||
}
|
||||
|
||||
res := make([]string, 0, 2)
|
||||
if icons.IsIconEnabled() {
|
||||
res = append(res, textStyle.Sprint(icons.IconForRemoteBranch(b)))
|
||||
|
|
|
|||
|
|
@ -42,6 +42,20 @@ func getTagDisplayStrings(
|
|||
if icons.IsIconEnabled() {
|
||||
res = append(res, textStyle.Sprint(icons.IconForTag(t)))
|
||||
}
|
||||
|
||||
name := t.Name
|
||||
// SVN stale 标记
|
||||
if t.IsSvnTag() {
|
||||
switch t.StaleStatus {
|
||||
case models.SvnBranchStatusStale:
|
||||
name = b.Name + "⚠"
|
||||
textStyle = style.FgRed
|
||||
case models.SvnBranchStatusMissing:
|
||||
name = b.Name + "(not fetched)"
|
||||
textStyle = style.FgWhite
|
||||
}
|
||||
}
|
||||
|
||||
descriptionColor := style.FgYellow
|
||||
descriptionStr := descriptionColor.Sprint(t.Description())
|
||||
itemOperationStr := ItemOperationToString(itemOperation, tr)
|
||||
|
|
|
|||
|
|
@ -973,6 +973,23 @@ type TranslationSet struct {
|
|||
UseCurrentChanges string
|
||||
UseIncomingChanges string
|
||||
UseBothChanges string
|
||||
// --- SVN 相关 ---
|
||||
NewSvnBranch string
|
||||
NewSvnBranchPrompt string
|
||||
SvnBranchCreateSuccess string
|
||||
SvnFetchFailed string
|
||||
|
||||
DeleteSvnLocalRef string
|
||||
DeleteSvnBoth string
|
||||
DeleteSvnBothConfirm string
|
||||
SvnDeleteLocalRefSuccess string
|
||||
SvnDeleteBothSuccess string
|
||||
SvnOperationFailed string
|
||||
|
||||
SvnCreateBranchTitle string
|
||||
SvnDeleteBranchTitle string
|
||||
SvnFetchingStatus string
|
||||
CheckingSvnStatus string
|
||||
}
|
||||
|
||||
type Bisect struct {
|
||||
|
|
@ -2125,6 +2142,24 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
UseIncomingChanges: "Use incoming changes",
|
||||
UseBothChanges: "Use both",
|
||||
|
||||
// --- SVN 相关 ---
|
||||
NewSvnBranch "New SVN remote branch",
|
||||
NewSvnBranchPrompt "SVN branch name (relative to --branches config, e.g. my-feature",
|
||||
SvnBranchCreateSuccess "SVN branch created. Fetching from SVN ...",
|
||||
SvnFetchFailed "SVN branch created but fetch failed. Run 'git svn fetch' manually.",
|
||||
|
||||
DeleteSvnLocalRef "Delete local ref only (safe)",
|
||||
DeleteSvnBoth "Delete from SVN server and local ref (dangerous)",
|
||||
DeleteSvnBothConfirm "This will PERMANENTLY delete '{{.branchPath}}' from both local refs and SVN server. Continue?",
|
||||
SvnDeleteLocalRefSuccess "Local ref deleted.",
|
||||
SvnDeleteBothSuccess "SVN branch deleted from both local and SVN server.",
|
||||
SvnOperationFailed "SVN operation failed: {{.error}}",
|
||||
|
||||
SvnCreateBranchTitle "Create SVN Remote Branch",
|
||||
SvnDeleteBranchTitle "Delete SVN Remote Branch",
|
||||
SvnFetchingStatus "Fetching from SVN ...",
|
||||
CheckingSvnStatus "Checking SVN branch status ...",
|
||||
|
||||
Actions: Actions{
|
||||
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
||||
CheckoutCommit: "Checkout commit",
|
||||
|
|
|
|||
Loading…
Reference in a new issue