mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-05 05:26:53 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e57f6ff9c5 | ||
|
|
a748294bc1 | ||
|
|
eb099c13c7 | ||
|
|
2ff12e6820 | ||
|
|
a8aafbc6af | ||
|
|
67e6e4e8fd | ||
|
|
a8d99a8ee7 | ||
|
|
223143c834 | ||
|
|
f6973cf7e4 | ||
|
|
e1e7e9185e | ||
|
|
48d161dd1c | ||
|
|
8ce1ed23ce | ||
|
|
4d1352e3d0 | ||
|
|
a7367ffcc1 | ||
|
|
cda40a7b75 | ||
|
|
76c62eba7c | ||
|
|
835ca6389e | ||
|
|
bdaf0c99ba | ||
|
|
c996c29df0 | ||
|
|
dc9a6e0ea5 | ||
|
|
eaba9dd62d | ||
|
|
3cb13f14bf | ||
|
|
5d52852df3 | ||
|
|
7d4432c4b5 | ||
|
|
a1235fa468 | ||
|
|
8b90811c65 | ||
|
|
2dd2b9f5e3 | ||
|
|
e7484808e5 | ||
|
|
3245350bab | ||
|
|
6e51dd1c85 | ||
|
|
77db982774 | ||
|
|
e16f56e492 | ||
|
|
3055944b5d | ||
|
|
6194b17ebb | ||
|
|
15ffb34474 | ||
|
|
32409dbb2f | ||
|
|
4ec960f07d | ||
|
|
a7bdc6be01 | ||
|
|
271f894106 |
@@ -39,3 +39,5 @@ test/results/**
|
|||||||
|
|
||||||
oryxBuildBinary
|
oryxBuildBinary
|
||||||
__debug_bin
|
__debug_bin
|
||||||
|
|
||||||
|
.worktrees
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
eUTlaNqeTB
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
qtuVMihtEl
|
||||||
@@ -116,6 +116,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
|||||||
"stash": tr.StashTitle,
|
"stash": tr.StashTitle,
|
||||||
"suggestions": tr.SuggestionsCheatsheetTitle,
|
"suggestions": tr.SuggestionsCheatsheetTitle,
|
||||||
"extras": tr.ExtrasTitle,
|
"extras": tr.ExtrasTitle,
|
||||||
|
"worktrees": tr.WorktreesTitle,
|
||||||
}
|
}
|
||||||
|
|
||||||
title, ok := contextTitleMap[str]
|
title, ok := contextTitleMap[str]
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ type GitCommand struct {
|
|||||||
Tag *git_commands.TagCommands
|
Tag *git_commands.TagCommands
|
||||||
WorkingTree *git_commands.WorkingTreeCommands
|
WorkingTree *git_commands.WorkingTreeCommands
|
||||||
Bisect *git_commands.BisectCommands
|
Bisect *git_commands.BisectCommands
|
||||||
|
Worktree *git_commands.WorktreeCommands
|
||||||
|
Version *git_commands.GitVersion
|
||||||
|
|
||||||
Loaders Loaders
|
Loaders Loaders
|
||||||
}
|
}
|
||||||
@@ -50,6 +52,7 @@ type Loaders struct {
|
|||||||
RemoteLoader *git_commands.RemoteLoader
|
RemoteLoader *git_commands.RemoteLoader
|
||||||
StashLoader *git_commands.StashLoader
|
StashLoader *git_commands.StashLoader
|
||||||
TagLoader *git_commands.TagLoader
|
TagLoader *git_commands.TagLoader
|
||||||
|
Worktrees *git_commands.WorktreeLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGitCommand(
|
func NewGitCommand(
|
||||||
@@ -127,12 +130,14 @@ func NewGitCommandAux(
|
|||||||
})
|
})
|
||||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||||
|
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
|
||||||
|
|
||||||
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode, gitCommon)
|
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode, gitCommon)
|
||||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||||
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
|
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
|
||||||
|
worktreeLoader := git_commands.NewWorktreeLoader(cmn, cmd)
|
||||||
stashLoader := git_commands.NewStashLoader(cmn, cmd)
|
stashLoader := git_commands.NewStashLoader(cmn, cmd)
|
||||||
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
||||||
|
|
||||||
@@ -154,6 +159,8 @@ func NewGitCommandAux(
|
|||||||
Tag: tagCommands,
|
Tag: tagCommands,
|
||||||
Bisect: bisectCommands,
|
Bisect: bisectCommands,
|
||||||
WorkingTree: workingTreeCommands,
|
WorkingTree: workingTreeCommands,
|
||||||
|
Worktree: worktreeCommands,
|
||||||
|
Version: version,
|
||||||
Loaders: Loaders{
|
Loaders: Loaders{
|
||||||
BranchLoader: branchLoader,
|
BranchLoader: branchLoader,
|
||||||
CommitFileLoader: commitFileLoader,
|
CommitFileLoader: commitFileLoader,
|
||||||
@@ -161,6 +168,7 @@ func NewGitCommandAux(
|
|||||||
FileLoader: fileLoader,
|
FileLoader: fileLoader,
|
||||||
ReflogCommitLoader: reflogCommitLoader,
|
ReflogCommitLoader: reflogCommitLoader,
|
||||||
RemoteLoader: remoteLoader,
|
RemoteLoader: remoteLoader,
|
||||||
|
Worktrees: worktreeLoader,
|
||||||
StashLoader: stashLoader,
|
StashLoader: stashLoader,
|
||||||
TagLoader: tagLoader,
|
TagLoader: tagLoader,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ func (self *GitCommandBuilder) RepoPath(value string) *GitCommandBuilder {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *GitCommandBuilder) WorktreePath(path string) *GitCommandBuilder {
|
||||||
|
// worktree path comes before the command
|
||||||
|
self.args = append([]string{"--work-tree", path}, self.args...)
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
func (self *GitCommandBuilder) ToArgv() []string {
|
func (self *GitCommandBuilder) ToArgv() []string {
|
||||||
return append([]string{"git"}, self.args...)
|
return append([]string{"git"}, self.args...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,3 +69,7 @@ func (v *GitVersion) IsOlderThan(major, minor, patch int) bool {
|
|||||||
func (v *GitVersion) IsOlderThanVersion(version *GitVersion) bool {
|
func (v *GitVersion) IsOlderThanVersion(version *GitVersion) bool {
|
||||||
return v.IsOlderThan(version.Major, version.Minor, version.Patch)
|
return v.IsOlderThan(version.Major, version.Minor, version.Patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *GitVersion) SupportsWorktrees() bool {
|
||||||
|
return !v.IsOlderThan(2, 5, 0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package git_commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorktreeCommands struct {
|
||||||
|
*GitCommon
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorktreeCommands(gitCommon *GitCommon) *WorktreeCommands {
|
||||||
|
return &WorktreeCommands{
|
||||||
|
GitCommon: gitCommon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewWorktreeOpts struct {
|
||||||
|
// required. The path of the new worktree.
|
||||||
|
Path string
|
||||||
|
// required. The base branch/ref.
|
||||||
|
Base string
|
||||||
|
|
||||||
|
// if true, ends up with a detached head
|
||||||
|
Detach bool
|
||||||
|
|
||||||
|
// optional. if empty, and if detach is false, we will checkout the base
|
||||||
|
Branch string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeCommands) New(opts NewWorktreeOpts) error {
|
||||||
|
if opts.Detach && opts.Branch != "" {
|
||||||
|
panic("cannot specify branch when detaching")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdArgs := NewGitCmd("worktree").Arg("add").
|
||||||
|
ArgIf(opts.Detach, "--detach").
|
||||||
|
ArgIf(opts.Branch != "", "-b", opts.Branch).
|
||||||
|
Arg(opts.Path, opts.Base)
|
||||||
|
|
||||||
|
return self.cmd.New(cmdArgs.ToArgv()).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeCommands) Delete(worktreePath string, force bool) error {
|
||||||
|
cmdArgs := NewGitCmd("worktree").Arg("remove").ArgIf(force, "-f").Arg(worktreePath).ToArgv()
|
||||||
|
|
||||||
|
return self.cmd.New(cmdArgs).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeCommands) Detach(worktreePath string) error {
|
||||||
|
cmdArgs := NewGitCmd("checkout").Arg("--detach").ToArgv()
|
||||||
|
|
||||||
|
return self.cmd.New(cmdArgs).SetWd(worktreePath).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeCommands) IsCurrentWorktree(path string) bool {
|
||||||
|
return IsCurrentWorktree(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsCurrentWorktree(path string) bool {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return EqualPath(pwd, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeCommands) IsWorktreePathMissing(path string) bool {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
log.Fatalln(fmt.Errorf("failed to check if worktree path `%s` exists\n%w", path, err).Error())
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks if two paths are equal
|
||||||
|
// TODO: support relative paths
|
||||||
|
func EqualPath(a string, b string) bool {
|
||||||
|
return a == b
|
||||||
|
}
|
||||||
|
|
||||||
|
func WorktreeForBranch(branch *models.Branch, worktrees []*models.Worktree) (*models.Worktree, bool) {
|
||||||
|
for _, worktree := range worktrees {
|
||||||
|
if worktree.Branch == branch.Name {
|
||||||
|
return worktree, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckedOutByOtherWorktree(branch *models.Branch, worktrees []*models.Worktree) bool {
|
||||||
|
worktree, ok := WorktreeForBranch(branch, worktrees)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !IsCurrentWorktree(worktree.Path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
package git_commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/common"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorktreeLoader struct {
|
||||||
|
*common.Common
|
||||||
|
cmd oscommands.ICmdObjBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorktreeLoader(
|
||||||
|
common *common.Common,
|
||||||
|
cmd oscommands.ICmdObjBuilder,
|
||||||
|
) *WorktreeLoader {
|
||||||
|
return &WorktreeLoader{
|
||||||
|
Common: common,
|
||||||
|
cmd: cmd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
|
||||||
|
cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain", "-z").ToArgv()
|
||||||
|
worktreesOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
splitLines := strings.Split(worktreesOutput, "\x00")
|
||||||
|
|
||||||
|
var worktrees []*models.Worktree
|
||||||
|
var current *models.Worktree
|
||||||
|
for _, splitLine := range splitLines {
|
||||||
|
if len(splitLine) == 0 && current != nil {
|
||||||
|
worktrees = append(worktrees, current)
|
||||||
|
current = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(splitLine, "worktree ") {
|
||||||
|
path := strings.SplitN(splitLine, " ", 2)[1]
|
||||||
|
current = &models.Worktree{
|
||||||
|
IsMain: len(worktrees) == 0,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(splitLine, "branch ") {
|
||||||
|
branch := strings.SplitN(splitLine, " ", 2)[1]
|
||||||
|
current.Branch = strings.TrimPrefix(branch, "refs/heads/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
names := getUniqueNamesFromPaths(lo.Map(worktrees, func(worktree *models.Worktree, _ int) string {
|
||||||
|
return worktree.Path
|
||||||
|
}))
|
||||||
|
|
||||||
|
for index, worktree := range worktrees {
|
||||||
|
worktree.NameField = names[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// move current worktree to the top
|
||||||
|
for i, worktree := range worktrees {
|
||||||
|
if EqualPath(worktree.Path, pwd) {
|
||||||
|
worktrees = append(worktrees[:i], worktrees[i+1:]...)
|
||||||
|
worktrees = append([]*models.Worktree{worktree}, worktrees...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some worktrees are on a branch but are mid-rebase, and in those cases,
|
||||||
|
// `git worktree list` will not show the branch name. We can get the branch
|
||||||
|
// name from the `rebase-merge/head-name` file (if it exists) in the folder
|
||||||
|
// for the worktree in the parent repo's .git/worktrees folder.
|
||||||
|
for _, worktree := range worktrees {
|
||||||
|
// No point checking if we already have a branch name
|
||||||
|
if worktree.Branch != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rebaseBranch, ok := rebaseBranch(worktree.Path)
|
||||||
|
if ok {
|
||||||
|
worktree.Branch = rebaseBranch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return worktrees, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rebaseBranch(worktreePath string) (string, bool) {
|
||||||
|
// need to find the actual path of the worktree in the .git dir
|
||||||
|
gitPath, ok := WorktreeGitPath(worktreePath)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we look inside that git path for a file `rebase-merge/head-name`
|
||||||
|
// if it exists, we update the worktree to say that it has that for a head
|
||||||
|
headNameContents, err := os.ReadFile(filepath.Join(gitPath, "rebase-merge", "head-name"))
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
headName := strings.TrimSpace(string(headNameContents))
|
||||||
|
shortHeadName := strings.TrimPrefix(headName, "refs/heads/")
|
||||||
|
|
||||||
|
return shortHeadName, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func WorktreeGitPath(worktreePath string) (string, bool) {
|
||||||
|
// first we get the path of the worktree, then we look at the contents of the `.git` file in that path
|
||||||
|
// then we look for the line that says `gitdir: /path/to/.git/worktrees/<worktree-name>`
|
||||||
|
// then we return that path
|
||||||
|
gitFileContents, err := os.ReadFile(filepath.Join(worktreePath, ".git"))
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
gitDirLine := lo.Filter(strings.Split(string(gitFileContents), "\n"), func(line string, _ int) bool {
|
||||||
|
return strings.HasPrefix(line, "gitdir: ")
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(gitDirLine) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
gitDir := strings.TrimPrefix(gitDirLine[0], "gitdir: ")
|
||||||
|
return gitDir, true
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathWithIndexT struct {
|
||||||
|
path string
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
type nameWithIndexT struct {
|
||||||
|
name string
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUniqueNamesFromPaths(paths []string) []string {
|
||||||
|
pathsWithIndex := lo.Map(paths, func(path string, index int) pathWithIndexT {
|
||||||
|
return pathWithIndexT{path, index}
|
||||||
|
})
|
||||||
|
|
||||||
|
namesWithIndex := getUniqueNamesFromPathsAux(pathsWithIndex, 0)
|
||||||
|
|
||||||
|
// now sort based on index
|
||||||
|
result := make([]string, len(namesWithIndex))
|
||||||
|
for _, nameWithIndex := range namesWithIndex {
|
||||||
|
result[nameWithIndex.index] = nameWithIndex.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUniqueNamesFromPathsAux(paths []pathWithIndexT, depth int) []nameWithIndexT {
|
||||||
|
// If we have no paths, return an empty array
|
||||||
|
if len(paths) == 0 {
|
||||||
|
return []nameWithIndexT{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have only one path, return the last segment of the path
|
||||||
|
if len(paths) == 1 {
|
||||||
|
path := paths[0]
|
||||||
|
return []nameWithIndexT{{index: path.index, name: sliceAtDepth(path.path, depth)}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// group the paths by their value at the specified depth
|
||||||
|
groups := make(map[string][]pathWithIndexT)
|
||||||
|
for _, path := range paths {
|
||||||
|
value := valueAtDepth(path.path, depth)
|
||||||
|
groups[value] = append(groups[value], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []nameWithIndexT{}
|
||||||
|
for _, group := range groups {
|
||||||
|
if len(group) == 1 {
|
||||||
|
path := group[0]
|
||||||
|
result = append(result, nameWithIndexT{index: path.index, name: sliceAtDepth(path.path, depth)})
|
||||||
|
} else {
|
||||||
|
result = append(result, getUniqueNamesFromPathsAux(group, depth+1)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the path is /a/b/c/d, and the depth is 0, the value is 'd'. If the depth is 1, the value is 'c', etc
|
||||||
|
func valueAtDepth(path string, depth int) string {
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
path = strings.TrimSuffix(path, "/")
|
||||||
|
|
||||||
|
// Split the path into segments
|
||||||
|
segments := strings.Split(path, "/")
|
||||||
|
|
||||||
|
// Get the length of segments
|
||||||
|
length := len(segments)
|
||||||
|
|
||||||
|
// If the depth is greater than the length of segments, return an empty string
|
||||||
|
if depth >= length {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the segment at the specified depth from the end of the path
|
||||||
|
return segments[length-1-depth]
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the path is /a/b/c/d, and the depth is 0, the value is 'd'. If the depth is 1, the value is 'b/c', etc
|
||||||
|
func sliceAtDepth(path string, depth int) string {
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
path = strings.TrimSuffix(path, "/")
|
||||||
|
|
||||||
|
// Split the path into segments
|
||||||
|
segments := strings.Split(path, "/")
|
||||||
|
|
||||||
|
// Get the length of segments
|
||||||
|
length := len(segments)
|
||||||
|
|
||||||
|
// If the depth is greater than or equal to the length of segments, return an empty string
|
||||||
|
if depth >= length {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join the segments from the specified depth till end of the path
|
||||||
|
return strings.Join(segments[length-1-depth:], "/")
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package git_commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetUniqueNamesFromPaths(t *testing.T) {
|
||||||
|
for _, scenario := range []struct {
|
||||||
|
input []string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: []string{},
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []string{
|
||||||
|
"/my/path/feature/one",
|
||||||
|
},
|
||||||
|
expected: []string{
|
||||||
|
"one",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []string{
|
||||||
|
"/my/path/feature/one/",
|
||||||
|
},
|
||||||
|
expected: []string{
|
||||||
|
"one",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []string{
|
||||||
|
"/a/b/c/d",
|
||||||
|
"/a/b/c/e",
|
||||||
|
"/a/b/f/d",
|
||||||
|
"/a/e/c/d",
|
||||||
|
},
|
||||||
|
expected: []string{
|
||||||
|
"b/c/d",
|
||||||
|
"e",
|
||||||
|
"f/d",
|
||||||
|
"e/c/d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
actual := getUniqueNamesFromPaths(scenario.input)
|
||||||
|
assert.EqualValues(t, scenario.expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// A git worktree
|
||||||
|
type Worktree struct {
|
||||||
|
// if false, this is a linked worktree
|
||||||
|
IsMain bool
|
||||||
|
Path string
|
||||||
|
Branch string
|
||||||
|
// based on the path, but uniquified
|
||||||
|
NameField string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worktree) RefName() string {
|
||||||
|
return w.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worktree) ID() string {
|
||||||
|
return w.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worktree) Description() string {
|
||||||
|
return w.RefName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worktree) Name() string {
|
||||||
|
return w.NameField
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worktree) Main() bool {
|
||||||
|
return w.IsMain
|
||||||
|
}
|
||||||
@@ -24,6 +24,9 @@ type ICmdObj interface {
|
|||||||
AddEnvVars(...string) ICmdObj
|
AddEnvVars(...string) ICmdObj
|
||||||
GetEnvVars() []string
|
GetEnvVars() []string
|
||||||
|
|
||||||
|
// sets the working directory
|
||||||
|
SetWd(string) ICmdObj
|
||||||
|
|
||||||
// runs the command and returns an error if any
|
// runs the command and returns an error if any
|
||||||
Run() error
|
Run() error
|
||||||
// runs the command and returns the output as a string, and an error if any
|
// runs the command and returns the output as a string, and an error if any
|
||||||
@@ -142,6 +145,12 @@ func (self *CmdObj) GetEnvVars() []string {
|
|||||||
return self.cmd.Env
|
return self.cmd.Env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *CmdObj) SetWd(wd string) ICmdObj {
|
||||||
|
self.cmd.Dir = wd
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
func (self *CmdObj) DontLog() ICmdObj {
|
func (self *CmdObj) DontLog() ICmdObj {
|
||||||
self.dontLog = true
|
self.dontLog = true
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ type KeybindingConfig struct {
|
|||||||
Status KeybindingStatusConfig `yaml:"status"`
|
Status KeybindingStatusConfig `yaml:"status"`
|
||||||
Files KeybindingFilesConfig `yaml:"files"`
|
Files KeybindingFilesConfig `yaml:"files"`
|
||||||
Branches KeybindingBranchesConfig `yaml:"branches"`
|
Branches KeybindingBranchesConfig `yaml:"branches"`
|
||||||
|
Worktrees KeybindingWorktreesConfig `yaml:"worktrees"`
|
||||||
Commits KeybindingCommitsConfig `yaml:"commits"`
|
Commits KeybindingCommitsConfig `yaml:"commits"`
|
||||||
Stash KeybindingStashConfig `yaml:"stash"`
|
Stash KeybindingStashConfig `yaml:"stash"`
|
||||||
CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"`
|
CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"`
|
||||||
@@ -246,6 +247,10 @@ type KeybindingBranchesConfig struct {
|
|||||||
FetchRemote string `yaml:"fetchRemote"`
|
FetchRemote string `yaml:"fetchRemote"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KeybindingWorktreesConfig struct {
|
||||||
|
ViewWorktreeOptions string `yaml:"viewWorktreeOptions"`
|
||||||
|
}
|
||||||
|
|
||||||
type KeybindingCommitsConfig struct {
|
type KeybindingCommitsConfig struct {
|
||||||
SquashDown string `yaml:"squashDown"`
|
SquashDown string `yaml:"squashDown"`
|
||||||
RenameCommit string `yaml:"renameCommit"`
|
RenameCommit string `yaml:"renameCommit"`
|
||||||
@@ -584,6 +589,9 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
SetUpstream: "u",
|
SetUpstream: "u",
|
||||||
FetchRemote: "f",
|
FetchRemote: "f",
|
||||||
},
|
},
|
||||||
|
Worktrees: KeybindingWorktreesConfig{
|
||||||
|
ViewWorktreeOptions: "w",
|
||||||
|
},
|
||||||
Commits: KeybindingCommitsConfig{
|
Commits: KeybindingCommitsConfig{
|
||||||
SquashDown: "s",
|
SquashDown: "s",
|
||||||
RenameCommit: "r",
|
RenameCommit: "r",
|
||||||
|
|||||||
@@ -376,3 +376,16 @@ func (self *ContextMgr) AllPatchExplorer() []types.IPatchExplorerContext {
|
|||||||
|
|
||||||
return listContexts
|
return listContexts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *ContextMgr) ContextForKey(key types.ContextKey) types.Context {
|
||||||
|
self.RLock()
|
||||||
|
defer self.RUnlock()
|
||||||
|
|
||||||
|
for _, context := range self.allContexts.Flatten() {
|
||||||
|
if context.GetKey() == key {
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
|||||||
c.Modes().Diffing.Ref,
|
c.Modes().Diffing.Ref,
|
||||||
c.Tr,
|
c.Tr,
|
||||||
c.UserConfig,
|
c.UserConfig,
|
||||||
|
c.Model().Worktrees,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// used as a nil value when passing a context key as an arg
|
||||||
|
NO_CONTEXT types.ContextKey = "none"
|
||||||
|
|
||||||
GLOBAL_CONTEXT_KEY types.ContextKey = "global"
|
GLOBAL_CONTEXT_KEY types.ContextKey = "global"
|
||||||
STATUS_CONTEXT_KEY types.ContextKey = "status"
|
STATUS_CONTEXT_KEY types.ContextKey = "status"
|
||||||
SNAKE_CONTEXT_KEY types.ContextKey = "snake"
|
SNAKE_CONTEXT_KEY types.ContextKey = "snake"
|
||||||
FILES_CONTEXT_KEY types.ContextKey = "files"
|
FILES_CONTEXT_KEY types.ContextKey = "files"
|
||||||
LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches"
|
LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches"
|
||||||
REMOTES_CONTEXT_KEY types.ContextKey = "remotes"
|
REMOTES_CONTEXT_KEY types.ContextKey = "remotes"
|
||||||
|
WORKTREES_CONTEXT_KEY types.ContextKey = "worktrees"
|
||||||
REMOTE_BRANCHES_CONTEXT_KEY types.ContextKey = "remoteBranches"
|
REMOTE_BRANCHES_CONTEXT_KEY types.ContextKey = "remoteBranches"
|
||||||
TAGS_CONTEXT_KEY types.ContextKey = "tags"
|
TAGS_CONTEXT_KEY types.ContextKey = "tags"
|
||||||
LOCAL_COMMITS_CONTEXT_KEY types.ContextKey = "commits"
|
LOCAL_COMMITS_CONTEXT_KEY types.ContextKey = "commits"
|
||||||
@@ -49,6 +53,7 @@ var AllContextKeys = []types.ContextKey{
|
|||||||
FILES_CONTEXT_KEY,
|
FILES_CONTEXT_KEY,
|
||||||
LOCAL_BRANCHES_CONTEXT_KEY,
|
LOCAL_BRANCHES_CONTEXT_KEY,
|
||||||
REMOTES_CONTEXT_KEY,
|
REMOTES_CONTEXT_KEY,
|
||||||
|
WORKTREES_CONTEXT_KEY,
|
||||||
REMOTE_BRANCHES_CONTEXT_KEY,
|
REMOTE_BRANCHES_CONTEXT_KEY,
|
||||||
TAGS_CONTEXT_KEY,
|
TAGS_CONTEXT_KEY,
|
||||||
LOCAL_COMMITS_CONTEXT_KEY,
|
LOCAL_COMMITS_CONTEXT_KEY,
|
||||||
@@ -84,6 +89,7 @@ type ContextTree struct {
|
|||||||
LocalCommits *LocalCommitsContext
|
LocalCommits *LocalCommitsContext
|
||||||
CommitFiles *CommitFilesContext
|
CommitFiles *CommitFilesContext
|
||||||
Remotes *RemotesContext
|
Remotes *RemotesContext
|
||||||
|
Worktrees *WorktreesContext
|
||||||
Submodules *SubmodulesContext
|
Submodules *SubmodulesContext
|
||||||
RemoteBranches *RemoteBranchesContext
|
RemoteBranches *RemoteBranchesContext
|
||||||
ReflogCommits *ReflogCommitsContext
|
ReflogCommits *ReflogCommitsContext
|
||||||
@@ -121,6 +127,7 @@ func (self *ContextTree) Flatten() []types.Context {
|
|||||||
self.Files,
|
self.Files,
|
||||||
self.SubCommits,
|
self.SubCommits,
|
||||||
self.Remotes,
|
self.Remotes,
|
||||||
|
self.Worktrees,
|
||||||
self.RemoteBranches,
|
self.RemoteBranches,
|
||||||
self.Tags,
|
self.Tags,
|
||||||
self.Branches,
|
self.Branches,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func NewContextTree(c *ContextCommon) *ContextTree {
|
|||||||
Submodules: NewSubmodulesContext(c),
|
Submodules: NewSubmodulesContext(c),
|
||||||
Menu: NewMenuContext(c),
|
Menu: NewMenuContext(c),
|
||||||
Remotes: NewRemotesContext(c),
|
Remotes: NewRemotesContext(c),
|
||||||
|
Worktrees: NewWorktreesContext(c),
|
||||||
RemoteBranches: NewRemoteBranchesContext(c),
|
RemoteBranches: NewRemoteBranchesContext(c),
|
||||||
LocalCommits: NewLocalCommitsContext(c),
|
LocalCommits: NewLocalCommitsContext(c),
|
||||||
CommitFiles: commitFilesContext,
|
CommitFiles: commitFilesContext,
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorktreesContext struct {
|
||||||
|
*FilteredListViewModel[*models.Worktree]
|
||||||
|
*ListContextTrait
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ types.IListContext = (*WorktreesContext)(nil)
|
||||||
|
|
||||||
|
func NewWorktreesContext(c *ContextCommon) *WorktreesContext {
|
||||||
|
viewModel := NewFilteredListViewModel(
|
||||||
|
func() []*models.Worktree { return c.Model().Worktrees },
|
||||||
|
func(Worktree *models.Worktree) []string {
|
||||||
|
return []string{Worktree.Name()}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
getDisplayStrings := func(startIdx int, length int) [][]string {
|
||||||
|
return presentation.GetWorktreeDisplayStrings(
|
||||||
|
viewModel.GetFilteredList(),
|
||||||
|
c.Git().Worktree.IsCurrentWorktree,
|
||||||
|
c.Git().Worktree.IsWorktreePathMissing,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WorktreesContext{
|
||||||
|
FilteredListViewModel: viewModel,
|
||||||
|
ListContextTrait: &ListContextTrait{
|
||||||
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
|
View: c.Views().Worktrees,
|
||||||
|
WindowName: "branches",
|
||||||
|
Key: WORKTREES_CONTEXT_KEY,
|
||||||
|
Kind: types.SIDE_CONTEXT,
|
||||||
|
Focusable: true,
|
||||||
|
})),
|
||||||
|
list: viewModel,
|
||||||
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
c: c,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesContext) GetSelectedItemId() string {
|
||||||
|
item := self.GetSelected()
|
||||||
|
if item == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.ID()
|
||||||
|
}
|
||||||
+38
-5
@@ -18,10 +18,13 @@ func (gui *Gui) Helpers() *helpers.Helpers {
|
|||||||
|
|
||||||
func (gui *Gui) resetHelpersAndControllers() {
|
func (gui *Gui) resetHelpersAndControllers() {
|
||||||
helperCommon := gui.c
|
helperCommon := gui.c
|
||||||
|
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
|
||||||
|
reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo)
|
||||||
refsHelper := helpers.NewRefsHelper(helperCommon)
|
refsHelper := helpers.NewRefsHelper(helperCommon)
|
||||||
|
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
|
||||||
|
worktreeHelper := helpers.NewWorktreeHelper(helperCommon, reposHelper, refsHelper, suggestionsHelper)
|
||||||
|
|
||||||
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper)
|
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper)
|
||||||
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
|
|
||||||
|
|
||||||
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
|
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
|
||||||
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
|
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
|
||||||
@@ -41,11 +44,20 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
|
|
||||||
gpgHelper := helpers.NewGpgHelper(helperCommon)
|
gpgHelper := helpers.NewGpgHelper(helperCommon)
|
||||||
viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts)
|
viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts)
|
||||||
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
|
|
||||||
patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
|
patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
|
||||||
stagingHelper := helpers.NewStagingHelper(helperCommon)
|
stagingHelper := helpers.NewStagingHelper(helperCommon)
|
||||||
mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon)
|
mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon)
|
||||||
refreshHelper := helpers.NewRefreshHelper(helperCommon, refsHelper, rebaseHelper, patchBuildingHelper, stagingHelper, mergeConflictsHelper, gui.fileWatcher)
|
|
||||||
|
refreshHelper := helpers.NewRefreshHelper(
|
||||||
|
helperCommon,
|
||||||
|
refsHelper,
|
||||||
|
rebaseHelper,
|
||||||
|
patchBuildingHelper,
|
||||||
|
stagingHelper,
|
||||||
|
mergeConflictsHelper,
|
||||||
|
worktreeHelper,
|
||||||
|
gui.fileWatcher,
|
||||||
|
)
|
||||||
diffHelper := helpers.NewDiffHelper(helperCommon)
|
diffHelper := helpers.NewDiffHelper(helperCommon)
|
||||||
cherryPickHelper := helpers.NewCherryPickHelper(
|
cherryPickHelper := helpers.NewCherryPickHelper(
|
||||||
helperCommon,
|
helperCommon,
|
||||||
@@ -84,7 +96,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
Commits: commitsHelper,
|
Commits: commitsHelper,
|
||||||
Snake: helpers.NewSnakeHelper(helperCommon),
|
Snake: helpers.NewSnakeHelper(helperCommon),
|
||||||
Diff: diffHelper,
|
Diff: diffHelper,
|
||||||
Repos: helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo),
|
Repos: reposHelper,
|
||||||
RecordDirectory: recordDirectoryHelper,
|
RecordDirectory: recordDirectoryHelper,
|
||||||
Update: helpers.NewUpdateHelper(helperCommon, gui.Updater),
|
Update: helpers.NewUpdateHelper(helperCommon, gui.Updater),
|
||||||
Window: windowHelper,
|
Window: windowHelper,
|
||||||
@@ -99,7 +111,8 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
modeHelper,
|
modeHelper,
|
||||||
appStatusHelper,
|
appStatusHelper,
|
||||||
),
|
),
|
||||||
Search: helpers.NewSearchHelper(helperCommon),
|
Search: helpers.NewSearchHelper(helperCommon),
|
||||||
|
Worktree: worktreeHelper,
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.CustomCommandsClient = custom_commands.NewClient(
|
gui.CustomCommandsClient = custom_commands.NewClient(
|
||||||
@@ -138,6 +151,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
common,
|
common,
|
||||||
func(branches []*models.RemoteBranch) { gui.State.Model.RemoteBranches = branches },
|
func(branches []*models.RemoteBranch) { gui.State.Model.RemoteBranches = branches },
|
||||||
)
|
)
|
||||||
|
worktreesController := controllers.NewWorktreesController(common)
|
||||||
undoController := controllers.NewUndoController(common)
|
undoController := controllers.NewUndoController(common)
|
||||||
globalController := controllers.NewGlobalController(common)
|
globalController := controllers.NewGlobalController(common)
|
||||||
contextLinesController := controllers.NewContextLinesController(common)
|
contextLinesController := controllers.NewContextLinesController(common)
|
||||||
@@ -177,6 +191,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
for _, context := range []types.Context{
|
for _, context := range []types.Context{
|
||||||
gui.State.Contexts.Status,
|
gui.State.Contexts.Status,
|
||||||
gui.State.Contexts.Remotes,
|
gui.State.Contexts.Remotes,
|
||||||
|
gui.State.Contexts.Worktrees,
|
||||||
gui.State.Contexts.Tags,
|
gui.State.Contexts.Tags,
|
||||||
gui.State.Contexts.Branches,
|
gui.State.Contexts.Branches,
|
||||||
gui.State.Contexts.RemoteBranches,
|
gui.State.Contexts.RemoteBranches,
|
||||||
@@ -227,6 +242,20 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context))
|
controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if gui.c.Git().Version.SupportsWorktrees() {
|
||||||
|
for _, context := range []controllers.CanViewWorktreeOptions{
|
||||||
|
gui.State.Contexts.LocalCommits,
|
||||||
|
gui.State.Contexts.ReflogCommits,
|
||||||
|
gui.State.Contexts.SubCommits,
|
||||||
|
gui.State.Contexts.Stash,
|
||||||
|
gui.State.Contexts.Branches,
|
||||||
|
gui.State.Contexts.RemoteBranches,
|
||||||
|
gui.State.Contexts.Tags,
|
||||||
|
} {
|
||||||
|
controllers.AttachControllers(context, controllers.NewWorktreeOptionsController(common, context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
controllers.AttachControllers(gui.State.Contexts.ReflogCommits,
|
controllers.AttachControllers(gui.State.Contexts.ReflogCommits,
|
||||||
reflogCommitsController,
|
reflogCommitsController,
|
||||||
)
|
)
|
||||||
@@ -298,6 +327,10 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
remotesController,
|
remotesController,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
controllers.AttachControllers(gui.State.Contexts.Worktrees,
|
||||||
|
worktreesController,
|
||||||
|
)
|
||||||
|
|
||||||
controllers.AttachControllers(gui.State.Contexts.Stash,
|
controllers.AttachControllers(gui.State.Contexts.Stash,
|
||||||
stashController,
|
stashController,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -202,10 +202,29 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
|
|||||||
return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch)
|
return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
worktreeForRef, ok := self.worktreeForBranch(selectedBranch)
|
||||||
|
if ok && !self.c.Git().Worktree.IsCurrentWorktree(worktreeForRef.Path) {
|
||||||
|
return self.promptToCheckoutWorktree(worktreeForRef)
|
||||||
|
}
|
||||||
|
|
||||||
self.c.LogAction(self.c.Tr.Actions.CheckoutBranch)
|
self.c.LogAction(self.c.Tr.Actions.CheckoutBranch)
|
||||||
return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{})
|
return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *BranchesController) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) {
|
||||||
|
return git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *BranchesController) promptToCheckoutWorktree(worktree *models.Worktree) error {
|
||||||
|
return self.c.Confirm(types.ConfirmOpts{
|
||||||
|
Title: "Switch to worktree",
|
||||||
|
Prompt: fmt.Sprintf("This branch is checked out by worktree %s. Do you want to switch to that worktree?", worktree.Name()),
|
||||||
|
HandleConfirm: func() error {
|
||||||
|
return self.c.Helpers().Worktree.Switch(worktree.Path, context.LOCAL_BRANCHES_CONTEXT_KEY)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error {
|
func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error {
|
||||||
return self.createPullRequest(selectedBranch.Name, "")
|
return self.createPullRequest(selectedBranch.Name, "")
|
||||||
}
|
}
|
||||||
@@ -298,9 +317,51 @@ func (self *BranchesController) delete(branch *models.Branch) error {
|
|||||||
if checkedOutBranch.Name == branch.Name {
|
if checkedOutBranch.Name == branch.Name {
|
||||||
return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch)
|
return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.checkedOutByOtherWorktree(branch) {
|
||||||
|
return self.promptWorktreeBranchDelete(branch)
|
||||||
|
}
|
||||||
|
|
||||||
return self.deleteWithForce(branch, false)
|
return self.deleteWithForce(branch, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *BranchesController) checkedOutByOtherWorktree(branch *models.Branch) bool {
|
||||||
|
return git_commands.CheckedOutByOtherWorktree(branch, self.c.Model().Worktrees)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *BranchesController) promptWorktreeBranchDelete(selectedBranch *models.Branch) error {
|
||||||
|
worktree, ok := self.worktreeForBranch(selectedBranch)
|
||||||
|
if !ok {
|
||||||
|
self.c.Log.Error("CheckedOutByOtherWorktree out of sync with list of worktrees")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.c.Menu(types.CreateMenuOptions{
|
||||||
|
Title: fmt.Sprintf("Branch %s is checked out by worktree %s", selectedBranch.Name, worktree.Name()),
|
||||||
|
Items: []*types.MenuItem{
|
||||||
|
{
|
||||||
|
Label: "Switch to worktree",
|
||||||
|
OnPress: func() error {
|
||||||
|
return self.c.Helpers().Worktree.Switch(worktree.Path, context.LOCAL_BRANCHES_CONTEXT_KEY)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Detach worktree",
|
||||||
|
Tooltip: "This will run `git checkout --detach` on the worktree so that it stops hogging the branch, but the worktree's working tree will be left alone",
|
||||||
|
OnPress: func() error {
|
||||||
|
return self.c.Helpers().Worktree.Detach(worktree)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Remove worktree",
|
||||||
|
OnPress: func() error {
|
||||||
|
return self.c.Helpers().Worktree.Remove(worktree, false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, force bool) error {
|
func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, force bool) error {
|
||||||
title := self.c.Tr.DeleteBranch
|
title := self.c.Tr.DeleteBranch
|
||||||
var templateStr string
|
var templateStr string
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
|
|||||||
if err := self.c.Git().WorkingTree.DiscardAllDirChanges(node); err != nil {
|
if err := self.c.Git().WorkingTree.DiscardAllDirChanges(node); err != nil {
|
||||||
return self.c.Error(err)
|
return self.c.Error(err)
|
||||||
}
|
}
|
||||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
|
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||||
},
|
},
|
||||||
Key: 'x',
|
Key: 'x',
|
||||||
Tooltip: utils.ResolvePlaceholderString(
|
Tooltip: utils.ResolvePlaceholderString(
|
||||||
@@ -72,7 +72,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
|
|||||||
return self.c.Error(err)
|
return self.c.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
|
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||||
},
|
},
|
||||||
Key: 'u',
|
Key: 'u',
|
||||||
Tooltip: utils.ResolvePlaceholderString(
|
Tooltip: utils.ResolvePlaceholderString(
|
||||||
@@ -107,7 +107,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
|
|||||||
if err := self.c.Git().WorkingTree.DiscardAllFileChanges(file); err != nil {
|
if err := self.c.Git().WorkingTree.DiscardAllFileChanges(file); err != nil {
|
||||||
return self.c.Error(err)
|
return self.c.Error(err)
|
||||||
}
|
}
|
||||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
|
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||||
},
|
},
|
||||||
Key: 'x',
|
Key: 'x',
|
||||||
Tooltip: utils.ResolvePlaceholderString(
|
Tooltip: utils.ResolvePlaceholderString(
|
||||||
@@ -128,7 +128,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
|
|||||||
return self.c.Error(err)
|
return self.c.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
|
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||||
},
|
},
|
||||||
Key: 'u',
|
Key: 'u',
|
||||||
Tooltip: utils.ResolvePlaceholderString(
|
Tooltip: utils.ResolvePlaceholderString(
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type Helpers struct {
|
|||||||
AppStatus *AppStatusHelper
|
AppStatus *AppStatusHelper
|
||||||
WindowArrangement *WindowArrangementHelper
|
WindowArrangement *WindowArrangementHelper
|
||||||
Search *SearchHelper
|
Search *SearchHelper
|
||||||
|
Worktree *WorktreeHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStubHelpers() *Helpers {
|
func NewStubHelpers() *Helpers {
|
||||||
@@ -80,5 +81,6 @@ func NewStubHelpers() *Helpers {
|
|||||||
AppStatus: &AppStatusHelper{},
|
AppStatus: &AppStatusHelper{},
|
||||||
WindowArrangement: &WindowArrangementHelper{},
|
WindowArrangement: &WindowArrangementHelper{},
|
||||||
Search: &SearchHelper{},
|
Search: &SearchHelper{},
|
||||||
|
Worktree: &WorktreeHelper{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package helpers
|
package helpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
@@ -27,6 +25,7 @@ type RefreshHelper struct {
|
|||||||
patchBuildingHelper *PatchBuildingHelper
|
patchBuildingHelper *PatchBuildingHelper
|
||||||
stagingHelper *StagingHelper
|
stagingHelper *StagingHelper
|
||||||
mergeConflictsHelper *MergeConflictsHelper
|
mergeConflictsHelper *MergeConflictsHelper
|
||||||
|
worktreeHelper *WorktreeHelper
|
||||||
fileWatcher types.IFileWatcher
|
fileWatcher types.IFileWatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +36,7 @@ func NewRefreshHelper(
|
|||||||
patchBuildingHelper *PatchBuildingHelper,
|
patchBuildingHelper *PatchBuildingHelper,
|
||||||
stagingHelper *StagingHelper,
|
stagingHelper *StagingHelper,
|
||||||
mergeConflictsHelper *MergeConflictsHelper,
|
mergeConflictsHelper *MergeConflictsHelper,
|
||||||
|
worktreeHelper *WorktreeHelper,
|
||||||
fileWatcher types.IFileWatcher,
|
fileWatcher types.IFileWatcher,
|
||||||
) *RefreshHelper {
|
) *RefreshHelper {
|
||||||
return &RefreshHelper{
|
return &RefreshHelper{
|
||||||
@@ -46,6 +46,7 @@ func NewRefreshHelper(
|
|||||||
patchBuildingHelper: patchBuildingHelper,
|
patchBuildingHelper: patchBuildingHelper,
|
||||||
stagingHelper: stagingHelper,
|
stagingHelper: stagingHelper,
|
||||||
mergeConflictsHelper: mergeConflictsHelper,
|
mergeConflictsHelper: mergeConflictsHelper,
|
||||||
|
worktreeHelper: worktreeHelper,
|
||||||
fileWatcher: fileWatcher,
|
fileWatcher: fileWatcher,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +78,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
|
|||||||
types.REFLOG,
|
types.REFLOG,
|
||||||
types.TAGS,
|
types.TAGS,
|
||||||
types.REMOTES,
|
types.REMOTES,
|
||||||
|
types.WORKTREES,
|
||||||
types.STATUS,
|
types.STATUS,
|
||||||
types.BISECT_INFO,
|
types.BISECT_INFO,
|
||||||
types.STAGING,
|
types.STAGING,
|
||||||
@@ -128,6 +130,10 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
|
|||||||
refresh(func() { _ = self.refreshRemotes() })
|
refresh(func() { _ = self.refreshRemotes() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if scopeSet.Includes(types.WORKTREES) {
|
||||||
|
refresh(func() { _ = self.refreshWorktrees() })
|
||||||
|
}
|
||||||
|
|
||||||
if scopeSet.Includes(types.STAGING) {
|
if scopeSet.Includes(types.STAGING) {
|
||||||
refresh(func() { _ = self.stagingHelper.RefreshStagingPanel(types.OnFocusOpts{}) })
|
refresh(func() { _ = self.stagingHelper.RefreshStagingPanel(types.OnFocusOpts{}) })
|
||||||
}
|
}
|
||||||
@@ -170,6 +176,7 @@ func getScopeNames(scopes []types.RefreshableView) []string {
|
|||||||
types.REFLOG: "reflog",
|
types.REFLOG: "reflog",
|
||||||
types.TAGS: "tags",
|
types.TAGS: "tags",
|
||||||
types.REMOTES: "remotes",
|
types.REMOTES: "remotes",
|
||||||
|
types.WORKTREES: "worktrees",
|
||||||
types.STATUS: "status",
|
types.STATUS: "status",
|
||||||
types.BISECT_INFO: "bisect",
|
types.BISECT_INFO: "bisect",
|
||||||
types.STAGING: "staging",
|
types.STAGING: "staging",
|
||||||
@@ -557,6 +564,22 @@ func (self *RefreshHelper) refreshRemotes() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *RefreshHelper) refreshWorktrees() error {
|
||||||
|
if !self.c.Git().Version.SupportsWorktrees() {
|
||||||
|
self.c.Model().Worktrees = []*models.Worktree{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
worktrees, err := self.c.Git().Loaders.Worktrees.GetWorktrees()
|
||||||
|
if err != nil {
|
||||||
|
return self.c.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.c.Model().Worktrees = worktrees
|
||||||
|
|
||||||
|
return self.c.PostRefreshUpdate(self.c.Contexts().Worktrees)
|
||||||
|
}
|
||||||
|
|
||||||
func (self *RefreshHelper) refreshStashEntries() error {
|
func (self *RefreshHelper) refreshStashEntries() error {
|
||||||
self.c.Model().StashEntries = self.c.Git().Loaders.StashLoader.
|
self.c.Model().StashEntries = self.c.Git().Loaders.StashLoader.
|
||||||
GetStashEntries(self.c.Modes().Filtering.GetPath())
|
GetStashEntries(self.c.Modes().Filtering.GetPath())
|
||||||
@@ -574,20 +597,16 @@ func (self *RefreshHelper) refreshStatus() {
|
|||||||
// need to wait for branches to refresh
|
// need to wait for branches to refresh
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
status := ""
|
|
||||||
|
|
||||||
if currentBranch.IsRealBranch() {
|
|
||||||
status += presentation.ColoredBranchStatus(currentBranch, self.c.Tr) + " "
|
|
||||||
}
|
|
||||||
|
|
||||||
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
||||||
if workingTreeState != enums.REBASE_MODE_NONE {
|
var linkedWorktreeName string
|
||||||
status += style.FgYellow.Sprintf("(%s) ", presentation.FormatWorkingTreeStateLower(self.c.Tr, workingTreeState))
|
if self.c.Git().Version.SupportsWorktrees() {
|
||||||
|
linkedWorktreeName = self.worktreeHelper.GetLinkedWorktreeName()
|
||||||
}
|
}
|
||||||
|
|
||||||
name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
|
repoName := self.worktreeHelper.GetCurrentRepoName()
|
||||||
repoName := utils.GetCurrentRepoName()
|
|
||||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
status := presentation.FormatStatus(repoName, currentBranch, linkedWorktreeName, workingTreeState, self.c.Tr)
|
||||||
|
|
||||||
self.c.SetViewContent(self.c.Views().Status, status)
|
self.c.SetViewContent(self.c.Views().Status, status)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/env"
|
"github.com/jesseduffield/lazygit/pkg/env"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type onNewRepoFn func(startArgs appTypes.StartArgs, reuseState bool) error
|
type onNewRepoFn func(startArgs appTypes.StartArgs, reuseState bool, contextKey types.ContextKey) error
|
||||||
|
|
||||||
// helps switch back and forth between repos
|
// helps switch back and forth between repos
|
||||||
type ReposHelper struct {
|
type ReposHelper struct {
|
||||||
@@ -46,7 +47,7 @@ func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error
|
|||||||
}
|
}
|
||||||
self.c.State().GetRepoPathStack().Push(wd)
|
self.c.State().GetRepoPathStack().Push(wd)
|
||||||
|
|
||||||
return self.DispatchSwitchToRepo(submodule.Path, true)
|
return self.DispatchSwitchToRepo(submodule.Path, true, context.NO_CONTEXT)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *ReposHelper) getCurrentBranch(path string) string {
|
func (self *ReposHelper) getCurrentBranch(path string) string {
|
||||||
@@ -129,7 +130,7 @@ func (self *ReposHelper) CreateRecentReposMenu() error {
|
|||||||
// if we were in a submodule, we want to forget about that stack of repos
|
// if we were in a submodule, we want to forget about that stack of repos
|
||||||
// so that hitting escape in the new repo does nothing
|
// so that hitting escape in the new repo does nothing
|
||||||
self.c.State().GetRepoPathStack().Clear()
|
self.c.State().GetRepoPathStack().Clear()
|
||||||
return self.DispatchSwitchToRepo(path, false)
|
return self.DispatchSwitchToRepo(path, false, context.NO_CONTEXT)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -137,16 +138,22 @@ func (self *ReposHelper) CreateRecentReposMenu() error {
|
|||||||
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems})
|
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool) error {
|
func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool, contextKey types.ContextKey) error {
|
||||||
|
return self.DispatchSwitchTo(path, reuse, self.c.Tr.ErrRepositoryMovedOrDeleted, contextKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ReposHelper) DispatchSwitchTo(path string, reuse bool, errMsg string, contextKey types.ContextKey) error {
|
||||||
env.UnsetGitDirEnvs()
|
env.UnsetGitDirEnvs()
|
||||||
originalPath, err := os.Getwd()
|
originalPath, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.c.LogCommand(fmt.Sprintf("Changing directory to %s", path), false)
|
||||||
|
|
||||||
if err := os.Chdir(path); err != nil {
|
if err := os.Chdir(path); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return self.c.ErrorMsg(self.c.Tr.ErrRepositoryMovedOrDeleted)
|
return self.c.ErrorMsg(errMsg)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -171,5 +178,5 @@ func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool) error {
|
|||||||
self.c.Mutexes().RefreshingFilesMutex.Lock()
|
self.c.Mutexes().RefreshingFilesMutex.Lock()
|
||||||
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
|
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
|
||||||
|
|
||||||
return self.onNewRepo(appTypes.StartArgs{}, reuse)
|
return self.onNewRepo(appTypes.StartArgs{}, reuse, contextKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||||
|
"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/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IWorktreeHelper interface {
|
||||||
|
GetMainWorktreeName() string
|
||||||
|
GetCurrentWorktreeName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorktreeHelper struct {
|
||||||
|
c *HelperCommon
|
||||||
|
reposHelper *ReposHelper
|
||||||
|
refsHelper *RefsHelper
|
||||||
|
suggestionsHelper *SuggestionsHelper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorktreeHelper(c *HelperCommon, reposHelper *ReposHelper, refsHelper *RefsHelper, suggestionsHelper *SuggestionsHelper) *WorktreeHelper {
|
||||||
|
return &WorktreeHelper{
|
||||||
|
c: c,
|
||||||
|
reposHelper: reposHelper,
|
||||||
|
refsHelper: refsHelper,
|
||||||
|
suggestionsHelper: suggestionsHelper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) GetMainWorktreeName() string {
|
||||||
|
for _, worktree := range self.c.Model().Worktrees {
|
||||||
|
if worktree.Main() {
|
||||||
|
return worktree.Name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're on the main worktree, we return an empty string
|
||||||
|
func (self *WorktreeHelper) GetLinkedWorktreeName() string {
|
||||||
|
worktrees := self.c.Model().Worktrees
|
||||||
|
if len(worktrees) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// worktrees always have the current worktree on top
|
||||||
|
currentWorktree := worktrees[0]
|
||||||
|
if currentWorktree.Main() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentWorktree.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) IsCurrentWorktree(w *models.Worktree) bool {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
self.c.Log.Errorf("failed to obtain current working directory: %w", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return pwd == w.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) IsWorktreePathMissing(w *models.Worktree) bool {
|
||||||
|
if _, err := os.Stat(w.Path); err != nil {
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
self.c.Log.Errorf("failed to check if worktree path `%s` exists: %w", w.Path, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) NewWorktree() error {
|
||||||
|
branch := self.refsHelper.GetCheckedOutRef()
|
||||||
|
currentBranchName := branch.RefName()
|
||||||
|
|
||||||
|
f := func(detached bool) error {
|
||||||
|
return self.c.Prompt(types.PromptOpts{
|
||||||
|
Title: self.c.Tr.NewWorktreeBase,
|
||||||
|
InitialContent: currentBranchName,
|
||||||
|
FindSuggestionsFunc: self.suggestionsHelper.GetRefsSuggestionsFunc(),
|
||||||
|
HandleConfirm: func(base string) error {
|
||||||
|
// we assume that the base can be checked out
|
||||||
|
canCheckoutBase := true
|
||||||
|
return self.NewWorktreeCheckout(base, canCheckoutBase, detached)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholders := map[string]string{"ref": "ref"}
|
||||||
|
|
||||||
|
return self.c.Menu(types.CreateMenuOptions{
|
||||||
|
Title: self.c.Tr.WorktreeTitle,
|
||||||
|
Items: []*types.MenuItem{
|
||||||
|
{
|
||||||
|
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFrom, placeholders)},
|
||||||
|
OnPress: func() error {
|
||||||
|
return f(false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFromDetached, placeholders)},
|
||||||
|
OnPress: func() error {
|
||||||
|
return f(true)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) NewWorktreeCheckout(base string, canCheckoutBase bool, detached bool) error {
|
||||||
|
opts := git_commands.NewWorktreeOpts{
|
||||||
|
Base: base,
|
||||||
|
Detach: detached,
|
||||||
|
}
|
||||||
|
|
||||||
|
f := func() error {
|
||||||
|
return self.c.WithWaitingStatus(self.c.Tr.AddingWorktree, func(gocui.Task) error {
|
||||||
|
self.c.LogAction(self.c.Tr.Actions.AddWorktree)
|
||||||
|
if err := self.c.Git().Worktree.New(opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return self.Switch(opts.Path, context.LOCAL_BRANCHES_CONTEXT_KEY)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.c.Prompt(types.PromptOpts{
|
||||||
|
Title: self.c.Tr.NewWorktreePath,
|
||||||
|
HandleConfirm: func(path string) error {
|
||||||
|
opts.Path = path
|
||||||
|
|
||||||
|
if detached {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
|
||||||
|
if canCheckoutBase {
|
||||||
|
title := utils.ResolvePlaceholderString(self.c.Tr.NewBranchNameLeaveBlank, map[string]string{"default": base})
|
||||||
|
// prompt for the new branch name where a blank means we just check out the branch
|
||||||
|
return self.c.Prompt(types.PromptOpts{
|
||||||
|
Title: title,
|
||||||
|
HandleConfirm: func(branchName string) error {
|
||||||
|
opts.Branch = branchName
|
||||||
|
|
||||||
|
return f()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// prompt for the new branch name where a blank means we just check out the branch
|
||||||
|
return self.c.Prompt(types.PromptOpts{
|
||||||
|
Title: self.c.Tr.NewBranchName,
|
||||||
|
HandleConfirm: func(branchName string) error {
|
||||||
|
if branchName == "" {
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.BranchNameCannotBeBlank)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Branch = branchName
|
||||||
|
|
||||||
|
return f()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) Switch(path string, contextKey types.ContextKey) error {
|
||||||
|
if self.c.Git().Worktree.IsCurrentWorktree(path) {
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.AlreadyInWorktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.c.LogAction(self.c.Tr.SwitchToWorktree)
|
||||||
|
|
||||||
|
return self.reposHelper.DispatchSwitchTo(path, true, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) Remove(worktree *models.Worktree, force bool) error {
|
||||||
|
title := self.c.Tr.RemoveWorktreeTitle
|
||||||
|
var templateStr string
|
||||||
|
if force {
|
||||||
|
templateStr = self.c.Tr.ForceRemoveWorktreePrompt
|
||||||
|
} else {
|
||||||
|
templateStr = self.c.Tr.RemoveWorktreePrompt
|
||||||
|
}
|
||||||
|
message := utils.ResolvePlaceholderString(
|
||||||
|
templateStr,
|
||||||
|
map[string]string{
|
||||||
|
"worktreeName": worktree.Name(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.c.Confirm(types.ConfirmOpts{
|
||||||
|
Title: title,
|
||||||
|
Prompt: message,
|
||||||
|
HandleConfirm: func() error {
|
||||||
|
return self.c.WithWaitingStatus(self.c.Tr.RemovingWorktree, func(gocui.Task) error {
|
||||||
|
self.c.LogAction(self.c.Tr.RemoveWorktree)
|
||||||
|
if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
|
||||||
|
errMessage := err.Error()
|
||||||
|
if !strings.Contains(errMessage, "--force") {
|
||||||
|
return self.c.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
return self.Remove(worktree, true)
|
||||||
|
}
|
||||||
|
return self.c.ErrorMsg(errMessage)
|
||||||
|
}
|
||||||
|
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) Detach(worktree *models.Worktree) error {
|
||||||
|
return self.c.WithWaitingStatus(self.c.Tr.DetachingWorktree, func(gocui.Task) error {
|
||||||
|
self.c.LogAction(self.c.Tr.RemovingWorktree)
|
||||||
|
|
||||||
|
err := self.c.Git().Worktree.Detach(worktree.Path)
|
||||||
|
if err != nil {
|
||||||
|
return self.c.Error(err)
|
||||||
|
}
|
||||||
|
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) ViewWorktreeOptions(context types.IListContext, ref string) error {
|
||||||
|
currentBranch := self.refsHelper.GetCheckedOutRef()
|
||||||
|
canCheckoutBase := context == self.c.Contexts().Branches && ref != currentBranch.RefName()
|
||||||
|
|
||||||
|
return self.ViewBranchWorktreeOptions(ref, canCheckoutBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) ViewBranchWorktreeOptions(branchName string, canCheckoutBase bool) error {
|
||||||
|
placeholders := map[string]string{"ref": branchName}
|
||||||
|
|
||||||
|
return self.c.Menu(types.CreateMenuOptions{
|
||||||
|
Title: self.c.Tr.WorktreeTitle,
|
||||||
|
Items: []*types.MenuItem{
|
||||||
|
{
|
||||||
|
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFrom, placeholders)},
|
||||||
|
OnPress: func() error {
|
||||||
|
return self.NewWorktreeCheckout(branchName, canCheckoutBase, false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFromDetached, placeholders)},
|
||||||
|
OnPress: func() error {
|
||||||
|
return self.NewWorktreeCheckout(branchName, canCheckoutBase, true)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) GetCurrentRepoName() string {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if .git is a file or a directory
|
||||||
|
gitPath := filepath.Join(pwd, ".git")
|
||||||
|
gitFileInfo, err := os.Stat(gitPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// must be a worktree or bare repo
|
||||||
|
if !gitFileInfo.IsDir() {
|
||||||
|
worktreeGitPath, ok := git_commands.WorktreeGitPath(pwd)
|
||||||
|
if !ok {
|
||||||
|
return basePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we just jump up three directories to get the repo name
|
||||||
|
return filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(worktreeGitPath))))
|
||||||
|
}
|
||||||
|
|
||||||
|
return basePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
func basePath() string {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
return filepath.Base(pwd)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ func (self *QuitActions) Escape() error {
|
|||||||
|
|
||||||
repoPathStack := self.c.State().GetRepoPathStack()
|
repoPathStack := self.c.State().GetRepoPathStack()
|
||||||
if !repoPathStack.IsEmpty() {
|
if !repoPathStack.IsEmpty() {
|
||||||
return self.c.Helpers().Repos.DispatchSwitchToRepo(repoPathStack.Pop(), true)
|
return self.c.Helpers().Repos.DispatchSwitchToRepo(repoPathStack.Pop(), true, context.NO_CONTEXT)
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.c.UserConfig.QuitOnTopLevelReturn {
|
if self.c.UserConfig.QuitOnTopLevelReturn {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This controller is for all contexts that have items you can create a worktree from
|
||||||
|
|
||||||
|
var _ types.IController = &WorktreeOptionsController{}
|
||||||
|
|
||||||
|
type CanViewWorktreeOptions interface {
|
||||||
|
types.IListContext
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorktreeOptionsController struct {
|
||||||
|
baseController
|
||||||
|
c *ControllerCommon
|
||||||
|
context CanViewWorktreeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorktreeOptionsController(controllerCommon *ControllerCommon, context CanViewWorktreeOptions) *WorktreeOptionsController {
|
||||||
|
return &WorktreeOptionsController{
|
||||||
|
baseController: baseController{},
|
||||||
|
c: controllerCommon,
|
||||||
|
context: context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeOptionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||||
|
bindings := []*types.Binding{
|
||||||
|
{
|
||||||
|
Key: opts.GetKey(opts.Config.Worktrees.ViewWorktreeOptions),
|
||||||
|
Handler: self.checkSelected(self.viewWorktreeOptions),
|
||||||
|
Description: self.c.Tr.ViewWorktreeOptions,
|
||||||
|
OpensMenu: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeOptionsController) checkSelected(callback func(string) error) func() error {
|
||||||
|
return func() error {
|
||||||
|
ref := self.context.GetSelectedItemId()
|
||||||
|
if ref == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeOptionsController) Context() types.Context {
|
||||||
|
return self.context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeOptionsController) viewWorktreeOptions(ref string) error {
|
||||||
|
return self.c.Helpers().Worktree.ViewWorktreeOptions(self.context, ref)
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorktreesController struct {
|
||||||
|
baseController
|
||||||
|
c *ControllerCommon
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ types.IController = &WorktreesController{}
|
||||||
|
|
||||||
|
func NewWorktreesController(
|
||||||
|
common *ControllerCommon,
|
||||||
|
) *WorktreesController {
|
||||||
|
return &WorktreesController{
|
||||||
|
baseController: baseController{},
|
||||||
|
c: common,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||||
|
bindings := []*types.Binding{
|
||||||
|
{
|
||||||
|
Key: opts.GetKey(opts.Config.Universal.Select),
|
||||||
|
Handler: self.checkSelected(self.enter),
|
||||||
|
Description: self.c.Tr.SwitchToWorktree,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: opts.GetKey(opts.Config.Universal.Remove),
|
||||||
|
Handler: self.checkSelected(self.remove),
|
||||||
|
Description: self.c.Tr.RemoveWorktree,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: opts.GetKey(opts.Config.Universal.New),
|
||||||
|
Handler: self.add,
|
||||||
|
Description: self.c.Tr.CreateWorktree,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: opts.GetKey(opts.Config.Universal.OpenFile),
|
||||||
|
Handler: self.checkSelected(self.open),
|
||||||
|
Description: self.c.Tr.OpenInEditor,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) GetOnRenderToMain() func() error {
|
||||||
|
return func() error {
|
||||||
|
var task types.UpdateTask
|
||||||
|
worktree := self.context().GetSelected()
|
||||||
|
if worktree == nil {
|
||||||
|
task = types.NewRenderStringTask(self.c.Tr.NoWorktreesThisRepo)
|
||||||
|
} else {
|
||||||
|
main := ""
|
||||||
|
if worktree.Main() {
|
||||||
|
main = style.FgDefault.Sprintf(" %s", self.c.Tr.MainWorktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
missing := ""
|
||||||
|
if self.c.Git().Worktree.IsWorktreePathMissing(worktree.Path) {
|
||||||
|
missing = style.FgRed.Sprintf(" %s", self.c.Tr.MissingWorktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
w := tabwriter.NewWriter(&builder, 0, 0, 2, ' ', 0)
|
||||||
|
_, _ = fmt.Fprintf(w, "%s:\t%s%s\n", self.c.Tr.Name, style.FgGreen.Sprint(worktree.Name()), main)
|
||||||
|
_, _ = fmt.Fprintf(w, "%s:\t%s\n", self.c.Tr.Branch, style.FgYellow.Sprint(worktree.Branch))
|
||||||
|
_, _ = fmt.Fprintf(w, "%s:\t%s%s\n", self.c.Tr.Path, style.FgCyan.Sprint(worktree.Path), missing)
|
||||||
|
_ = w.Flush()
|
||||||
|
|
||||||
|
task = types.NewRenderStringTask(builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||||
|
Pair: self.c.MainViewPairs().Normal,
|
||||||
|
Main: &types.ViewUpdateOpts{
|
||||||
|
Title: self.c.Tr.WorktreeTitle,
|
||||||
|
Task: task,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) add() error {
|
||||||
|
return self.c.Helpers().Worktree.NewWorktree()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) remove(worktree *models.Worktree) error {
|
||||||
|
if worktree.Main() {
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.CantDeleteMainWorktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.c.Git().Worktree.IsCurrentWorktree(worktree.Path) {
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.CantDeleteCurrentWorktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.c.Helpers().Worktree.Remove(worktree, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) GetOnClick() func() error {
|
||||||
|
return self.checkSelected(self.enter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) enter(worktree *models.Worktree) error {
|
||||||
|
return self.c.Helpers().Worktree.Switch(worktree.Path, context.WORKTREES_CONTEXT_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) open(worktree *models.Worktree) error {
|
||||||
|
return self.c.Helpers().Files.OpenFile(worktree.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) checkSelected(callback func(worktree *models.Worktree) error) func() error {
|
||||||
|
return func() error {
|
||||||
|
worktree := self.context().GetSelected()
|
||||||
|
if worktree == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(worktree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) Context() types.Context {
|
||||||
|
return self.context()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) context() *context.WorktreesContext {
|
||||||
|
return self.c.Contexts().Worktrees
|
||||||
|
}
|
||||||
+38
-16
@@ -274,7 +274,7 @@ func (self *GuiRepoState) GetSplitMainPanel() bool {
|
|||||||
return self.SplitMainPanel
|
return self.SplitMainPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
|
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool, contextKey types.ContextKey) error {
|
||||||
var err error
|
var err error
|
||||||
gui.git, err = commands.NewGitCommand(
|
gui.git, err = commands.NewGitCommand(
|
||||||
gui.Common,
|
gui.Common,
|
||||||
@@ -295,6 +295,17 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if a context key has been given, push that instead, and set its index to 0
|
||||||
|
if contextKey != context.NO_CONTEXT {
|
||||||
|
contextToPush = gui.c.ContextForKey(contextKey)
|
||||||
|
// when we pass a list context, the expectation is that our cursor goes to the top,
|
||||||
|
// because e.g. with worktrees, we'll show the current worktree at the top of the list.
|
||||||
|
listContext, ok := contextToPush.(types.IListContext)
|
||||||
|
if ok {
|
||||||
|
listContext.GetList().SetSelectedLineIdx(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := gui.c.PushContext(contextToPush); err != nil {
|
if err := gui.c.PushContext(contextToPush); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -530,21 +541,32 @@ func (gui *Gui) initGocui(headless bool, test integrationTypes.IntegrationTest)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) viewTabMap() map[string][]context.TabView {
|
func (gui *Gui) viewTabMap() map[string][]context.TabView {
|
||||||
return map[string][]context.TabView{
|
branchesTabs := []context.TabView{
|
||||||
"branches": {
|
{
|
||||||
{
|
Tab: gui.c.Tr.LocalBranchesTitle,
|
||||||
Tab: gui.c.Tr.LocalBranchesTitle,
|
ViewName: "localBranches",
|
||||||
ViewName: "localBranches",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Tab: gui.c.Tr.RemotesTitle,
|
|
||||||
ViewName: "remotes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Tab: gui.c.Tr.TagsTitle,
|
|
||||||
ViewName: "tags",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Tab: gui.c.Tr.RemotesTitle,
|
||||||
|
ViewName: "remotes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tab: gui.c.Tr.TagsTitle,
|
||||||
|
ViewName: "tags",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if gui.c.Git().Version.SupportsWorktrees() {
|
||||||
|
branchesTabs = append(branchesTabs,
|
||||||
|
context.TabView{
|
||||||
|
Tab: gui.c.Tr.WorktreesTitle,
|
||||||
|
ViewName: "worktrees",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string][]context.TabView{
|
||||||
|
"branches": branchesTabs,
|
||||||
"commits": {
|
"commits": {
|
||||||
{
|
{
|
||||||
Tab: gui.c.Tr.CommitsTitle,
|
Tab: gui.c.Tr.CommitsTitle,
|
||||||
@@ -614,7 +636,7 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// onNewRepo must be called after g.SetManager because SetManager deletes keybindings
|
// onNewRepo must be called after g.SetManager because SetManager deletes keybindings
|
||||||
if err := gui.onNewRepo(startArgs, false); err != nil {
|
if err := gui.onNewRepo(startArgs, false, context.NO_CONTEXT); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ func (self *guiCommon) Context() types.IContextMgr {
|
|||||||
return self.gui.State.ContextMgr
|
return self.gui.State.ContextMgr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *guiCommon) ContextForKey(key types.ContextKey) types.Context {
|
||||||
|
return self.gui.State.ContextMgr.ContextForKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
func (self *guiCommon) ActivateContext(context types.Context) error {
|
func (self *guiCommon) ActivateContext(context types.Context) error {
|
||||||
return self.gui.State.ContextMgr.ActivateContext(context, types.OnFocusOpts{})
|
return self.gui.State.ContextMgr.ActivateContext(context, types.OnFocusOpts{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/generics/slices"
|
"github.com/jesseduffield/generics/slices"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/config"
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
var branchPrefixColorCache = make(map[string]style.TextStyle)
|
var branchPrefixColorCache = make(map[string]style.TextStyle)
|
||||||
@@ -22,10 +24,11 @@ func GetBranchListDisplayStrings(
|
|||||||
diffName string,
|
diffName string,
|
||||||
tr *i18n.TranslationSet,
|
tr *i18n.TranslationSet,
|
||||||
userConfig *config.UserConfig,
|
userConfig *config.UserConfig,
|
||||||
|
worktrees []*models.Worktree,
|
||||||
) [][]string {
|
) [][]string {
|
||||||
return slices.Map(branches, func(branch *models.Branch) []string {
|
return slices.Map(branches, func(branch *models.Branch) []string {
|
||||||
diffed := branch.Name == diffName
|
diffed := branch.Name == diffName
|
||||||
return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig)
|
return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig, worktrees)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +39,7 @@ func getBranchDisplayStrings(
|
|||||||
diffed bool,
|
diffed bool,
|
||||||
tr *i18n.TranslationSet,
|
tr *i18n.TranslationSet,
|
||||||
userConfig *config.UserConfig,
|
userConfig *config.UserConfig,
|
||||||
|
worktrees []*models.Worktree,
|
||||||
) []string {
|
) []string {
|
||||||
displayName := b.Name
|
displayName := b.Name
|
||||||
if b.DisplayName != "" {
|
if b.DisplayName != "" {
|
||||||
@@ -49,6 +53,10 @@ func getBranchDisplayStrings(
|
|||||||
|
|
||||||
coloredName := nameTextStyle.Sprint(displayName)
|
coloredName := nameTextStyle.Sprint(displayName)
|
||||||
branchStatus := utils.WithPadding(ColoredBranchStatus(b, tr), 2, utils.AlignLeft)
|
branchStatus := utils.WithPadding(ColoredBranchStatus(b, tr), 2, utils.AlignLeft)
|
||||||
|
if git_commands.CheckedOutByOtherWorktree(b, worktrees) {
|
||||||
|
worktreeIcon := lo.Ternary(icons.IsIconEnabled(), icons.LINKED_WORKTREE_ICON, fmt.Sprintf("(%s)", tr.LcWorktree))
|
||||||
|
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon))
|
||||||
|
}
|
||||||
coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus)
|
coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus)
|
||||||
|
|
||||||
recencyColor := style.FgCyan
|
recencyColor := style.FgCyan
|
||||||
@@ -58,6 +66,7 @@ func getBranchDisplayStrings(
|
|||||||
|
|
||||||
res := make([]string, 0, 6)
|
res := make([]string, 0, 6)
|
||||||
res = append(res, recencyColor.Sprint(b.Recency))
|
res = append(res, recencyColor.Sprint(b.Recency))
|
||||||
|
|
||||||
if icons.IsIconEnabled() {
|
if icons.IsIconEnabled() {
|
||||||
res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b)))
|
res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
BRANCH_ICON = "\U000f062c" //
|
BRANCH_ICON = "\U000f062c" //
|
||||||
DETACHED_HEAD_ICON = "\ue729" //
|
DETACHED_HEAD_ICON = "\ue729" //
|
||||||
TAG_ICON = "\uf02b" //
|
TAG_ICON = "\uf02b" //
|
||||||
COMMIT_ICON = "\U000f0718" //
|
COMMIT_ICON = "\U000f0718" //
|
||||||
MERGE_COMMIT_ICON = "\U000f062d" //
|
MERGE_COMMIT_ICON = "\U000f062d" //
|
||||||
DEFAULT_REMOTE_ICON = "\uf02a2" //
|
DEFAULT_REMOTE_ICON = "\uf02a2" //
|
||||||
STASH_ICON = "\uf01c" //
|
STASH_ICON = "\uf01c" //
|
||||||
|
LINKED_WORKTREE_ICON = "\uf838" //
|
||||||
|
MISSING_LINKED_WORKTREE_ICON = "\uf839" //
|
||||||
)
|
)
|
||||||
|
|
||||||
var remoteIcons = map[string]string{
|
var remoteIcons = map[string]string{
|
||||||
@@ -68,3 +70,10 @@ func IconForRemote(remote *models.Remote) string {
|
|||||||
func IconForStash(stash *models.StashEntry) string {
|
func IconForStash(stash *models.StashEntry) string {
|
||||||
return STASH_ICON
|
return STASH_ICON
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IconForWorktree(missing bool) string {
|
||||||
|
if missing {
|
||||||
|
return MISSING_LINKED_WORKTREE_ICON
|
||||||
|
}
|
||||||
|
return LINKED_WORKTREE_ICON
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package presentation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FormatStatus(repoName string, currentBranch *models.Branch, linkedWorktreeName string, workingTreeState enums.RebaseMode, tr *i18n.TranslationSet) string {
|
||||||
|
status := ""
|
||||||
|
|
||||||
|
if currentBranch.IsRealBranch() {
|
||||||
|
status += ColoredBranchStatus(currentBranch, tr) + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
if workingTreeState != enums.REBASE_MODE_NONE {
|
||||||
|
status += style.FgYellow.Sprintf("(%s) ", FormatWorkingTreeStateLower(tr, workingTreeState))
|
||||||
|
}
|
||||||
|
|
||||||
|
name := GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
|
||||||
|
// If the user is in a linked worktree (i.e. not the main worktree) we'll display that
|
||||||
|
if linkedWorktreeName != "" {
|
||||||
|
icon := ""
|
||||||
|
if icons.IsIconEnabled() {
|
||||||
|
icon = icons.LINKED_WORKTREE_ICON + " "
|
||||||
|
}
|
||||||
|
repoName = fmt.Sprintf("%s(%s%s)", repoName, icon, style.FgCyan.Sprint(linkedWorktreeName))
|
||||||
|
}
|
||||||
|
status += fmt.Sprintf("%s → %s ", repoName, name)
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetWorktreeDisplayStrings(worktrees []*models.Worktree, isCurrent func(string) bool, isMissing func(string) bool) [][]string {
|
||||||
|
return lo.Map(worktrees, func(worktree *models.Worktree, _ int) []string {
|
||||||
|
return GetWorktreeDisplayString(
|
||||||
|
isCurrent(worktree.Path),
|
||||||
|
isMissing(worktree.Path),
|
||||||
|
worktree)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetWorktreeDisplayString(isCurrent bool, isPathMissing bool, worktree *models.Worktree) []string {
|
||||||
|
textStyle := theme.DefaultTextColor
|
||||||
|
|
||||||
|
current := ""
|
||||||
|
currentColor := style.FgCyan
|
||||||
|
if isCurrent {
|
||||||
|
current = " *"
|
||||||
|
currentColor = style.FgGreen
|
||||||
|
}
|
||||||
|
|
||||||
|
icon := icons.IconForWorktree(false)
|
||||||
|
if isPathMissing {
|
||||||
|
textStyle = style.FgRed
|
||||||
|
icon = icons.IconForWorktree(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := []string{}
|
||||||
|
res = append(res, currentColor.Sprint(current))
|
||||||
|
if icons.IsIconEnabled() {
|
||||||
|
res = append(res, textStyle.Sprint(icon))
|
||||||
|
}
|
||||||
|
|
||||||
|
name := worktree.Name()
|
||||||
|
if worktree.Main() {
|
||||||
|
// TODO: i18n
|
||||||
|
name += " (main worktree)"
|
||||||
|
}
|
||||||
|
res = append(res, textStyle.Sprint(name))
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -66,6 +66,7 @@ type IGuiCommon interface {
|
|||||||
IsCurrentContext(Context) bool
|
IsCurrentContext(Context) bool
|
||||||
// TODO: replace the above context-based methods with just using Context() e.g. replace PushContext() with Context().Push()
|
// TODO: replace the above context-based methods with just using Context() e.g. replace PushContext() with Context().Push()
|
||||||
Context() IContextMgr
|
Context() IContextMgr
|
||||||
|
ContextForKey(key ContextKey) Context
|
||||||
|
|
||||||
ActivateContext(context Context) error
|
ActivateContext(context Context) error
|
||||||
|
|
||||||
@@ -196,6 +197,7 @@ type Model struct {
|
|||||||
StashEntries []*models.StashEntry
|
StashEntries []*models.StashEntry
|
||||||
SubCommits []*models.Commit
|
SubCommits []*models.Commit
|
||||||
Remotes []*models.Remote
|
Remotes []*models.Remote
|
||||||
|
Worktrees []*models.Worktree
|
||||||
|
|
||||||
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
||||||
// when in filtering mode we only include the ones that match the given path
|
// when in filtering mode we only include the ones that match the given path
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const (
|
|||||||
REFLOG
|
REFLOG
|
||||||
TAGS
|
TAGS
|
||||||
REMOTES
|
REMOTES
|
||||||
|
WORKTREES
|
||||||
STATUS
|
STATUS
|
||||||
SUBMODULES
|
SUBMODULES
|
||||||
STAGING
|
STAGING
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type Views struct {
|
|||||||
Files *gocui.View
|
Files *gocui.View
|
||||||
Branches *gocui.View
|
Branches *gocui.View
|
||||||
Remotes *gocui.View
|
Remotes *gocui.View
|
||||||
|
Worktrees *gocui.View
|
||||||
Tags *gocui.View
|
Tags *gocui.View
|
||||||
RemoteBranches *gocui.View
|
RemoteBranches *gocui.View
|
||||||
ReflogCommits *gocui.View
|
ReflogCommits *gocui.View
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
|
|||||||
{viewPtr: &gui.Views.Files, name: "files"},
|
{viewPtr: &gui.Views.Files, name: "files"},
|
||||||
{viewPtr: &gui.Views.Tags, name: "tags"},
|
{viewPtr: &gui.Views.Tags, name: "tags"},
|
||||||
{viewPtr: &gui.Views.Remotes, name: "remotes"},
|
{viewPtr: &gui.Views.Remotes, name: "remotes"},
|
||||||
|
{viewPtr: &gui.Views.Worktrees, name: "worktrees"},
|
||||||
{viewPtr: &gui.Views.Branches, name: "localBranches"},
|
{viewPtr: &gui.Views.Branches, name: "localBranches"},
|
||||||
{viewPtr: &gui.Views.RemoteBranches, name: "remoteBranches"},
|
{viewPtr: &gui.Views.RemoteBranches, name: "remoteBranches"},
|
||||||
{viewPtr: &gui.Views.ReflogCommits, name: "reflogCommits"},
|
{viewPtr: &gui.Views.ReflogCommits, name: "reflogCommits"},
|
||||||
@@ -113,6 +114,8 @@ func (gui *Gui) createAllViews() error {
|
|||||||
|
|
||||||
gui.Views.Remotes.Title = gui.c.Tr.RemotesTitle
|
gui.Views.Remotes.Title = gui.c.Tr.RemotesTitle
|
||||||
|
|
||||||
|
gui.Views.Worktrees.Title = gui.c.Tr.WorktreesTitle
|
||||||
|
|
||||||
gui.Views.Tags.Title = gui.c.Tr.TagsTitle
|
gui.Views.Tags.Title = gui.c.Tr.TagsTitle
|
||||||
|
|
||||||
gui.Views.Files.Title = gui.c.Tr.FilesTitle
|
gui.Views.Files.Title = gui.c.Tr.FilesTitle
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ type TranslationSet struct {
|
|||||||
GitconfigParseErr string
|
GitconfigParseErr string
|
||||||
EditFile string
|
EditFile string
|
||||||
OpenFile string
|
OpenFile string
|
||||||
|
OpenInEditor string
|
||||||
IgnoreFile string
|
IgnoreFile string
|
||||||
ExcludeFile string
|
ExcludeFile string
|
||||||
RefreshFiles string
|
RefreshFiles string
|
||||||
@@ -480,6 +481,7 @@ type TranslationSet struct {
|
|||||||
ErrCannotEditDirectory string
|
ErrCannotEditDirectory string
|
||||||
ErrStageDirWithInlineMergeConflicts string
|
ErrStageDirWithInlineMergeConflicts string
|
||||||
ErrRepositoryMovedOrDeleted string
|
ErrRepositoryMovedOrDeleted string
|
||||||
|
ErrWorktreeMovedOrRemoved string
|
||||||
CommandLog string
|
CommandLog string
|
||||||
ToggleShowCommandLog string
|
ToggleShowCommandLog string
|
||||||
FocusCommandLog string
|
FocusCommandLog string
|
||||||
@@ -540,6 +542,36 @@ type TranslationSet struct {
|
|||||||
FilterPrefix string
|
FilterPrefix string
|
||||||
ExitSearchMode string
|
ExitSearchMode string
|
||||||
ExitTextFilterMode string
|
ExitTextFilterMode string
|
||||||
|
SwitchToWorktree string
|
||||||
|
RemoveWorktree string
|
||||||
|
RemoveWorktreeTitle string
|
||||||
|
DetachWorktree string
|
||||||
|
DetachingWorktree string
|
||||||
|
WorktreesTitle string
|
||||||
|
WorktreeTitle string
|
||||||
|
RemoveWorktreePrompt string
|
||||||
|
ForceRemoveWorktreePrompt string
|
||||||
|
RemovingWorktree string
|
||||||
|
AddingWorktree string
|
||||||
|
CantDeleteCurrentWorktree string
|
||||||
|
AlreadyInWorktree string
|
||||||
|
CantDeleteMainWorktree string
|
||||||
|
NoWorktreesThisRepo string
|
||||||
|
MissingWorktree string
|
||||||
|
MainWorktree string
|
||||||
|
CreateWorktree string
|
||||||
|
NewWorktreePath string
|
||||||
|
NewWorktreeBase string
|
||||||
|
BranchNameCannotBeBlank string
|
||||||
|
NewBranchName string
|
||||||
|
NewBranchNameLeaveBlank string
|
||||||
|
ViewWorktreeOptions string
|
||||||
|
CreateWorktreeFrom string
|
||||||
|
CreateWorktreeFromDetached string
|
||||||
|
LcWorktree string
|
||||||
|
Name string
|
||||||
|
Branch string
|
||||||
|
Path string
|
||||||
Actions Actions
|
Actions Actions
|
||||||
Bisect Bisect
|
Bisect Bisect
|
||||||
}
|
}
|
||||||
@@ -666,6 +698,8 @@ type Actions struct {
|
|||||||
ResetBisect string
|
ResetBisect string
|
||||||
BisectSkip string
|
BisectSkip string
|
||||||
BisectMark string
|
BisectMark string
|
||||||
|
RemoveWorktree string
|
||||||
|
AddWorktree string
|
||||||
}
|
}
|
||||||
|
|
||||||
const englishIntroPopupMessage = `
|
const englishIntroPopupMessage = `
|
||||||
@@ -849,6 +883,7 @@ func EnglishTranslationSet() TranslationSet {
|
|||||||
GitconfigParseErr: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`,
|
GitconfigParseErr: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`,
|
||||||
EditFile: `Edit file`,
|
EditFile: `Edit file`,
|
||||||
OpenFile: `Open file`,
|
OpenFile: `Open file`,
|
||||||
|
OpenInEditor: "Open in editor",
|
||||||
IgnoreFile: `Add to .gitignore`,
|
IgnoreFile: `Add to .gitignore`,
|
||||||
ExcludeFile: `Add to .git/info/exclude`,
|
ExcludeFile: `Add to .git/info/exclude`,
|
||||||
RefreshFiles: `Refresh files`,
|
RefreshFiles: `Refresh files`,
|
||||||
@@ -1175,6 +1210,7 @@ func EnglishTranslationSet() TranslationSet {
|
|||||||
ErrStageDirWithInlineMergeConflicts: "Cannot stage/unstage directory containing files with inline merge conflicts. Please fix up the merge conflicts first",
|
ErrStageDirWithInlineMergeConflicts: "Cannot stage/unstage directory containing files with inline merge conflicts. Please fix up the merge conflicts first",
|
||||||
ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯",
|
ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯",
|
||||||
CommandLog: "Command log",
|
CommandLog: "Command log",
|
||||||
|
ErrWorktreeMovedOrRemoved: "Cannot find worktree. It might have been moved or removed ¯\\_(ツ)_/¯",
|
||||||
ToggleShowCommandLog: "Toggle show/hide command log",
|
ToggleShowCommandLog: "Toggle show/hide command log",
|
||||||
FocusCommandLog: "Focus command log",
|
FocusCommandLog: "Focus command log",
|
||||||
CommandLogHeader: "You can hide/focus this panel by pressing '%s'\n",
|
CommandLogHeader: "You can hide/focus this panel by pressing '%s'\n",
|
||||||
@@ -1233,6 +1269,36 @@ func EnglishTranslationSet() TranslationSet {
|
|||||||
SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode",
|
SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode",
|
||||||
SearchPrefix: "Search: ",
|
SearchPrefix: "Search: ",
|
||||||
FilterPrefix: "Filter: ",
|
FilterPrefix: "Filter: ",
|
||||||
|
WorktreesTitle: "Worktrees",
|
||||||
|
WorktreeTitle: "Worktree",
|
||||||
|
SwitchToWorktree: "Switch to worktree",
|
||||||
|
RemoveWorktree: "Remove worktree",
|
||||||
|
RemoveWorktreeTitle: "Remove worktree",
|
||||||
|
RemoveWorktreePrompt: "Are you sure you want to remove worktree '{{.worktreeName}}'?",
|
||||||
|
ForceRemoveWorktreePrompt: "'{{.worktreeName}}' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?",
|
||||||
|
RemovingWorktree: "Deleting worktree",
|
||||||
|
DetachWorktree: "Detach worktree",
|
||||||
|
DetachingWorktree: "Detaching worktree",
|
||||||
|
AddingWorktree: "Adding worktree",
|
||||||
|
CantDeleteCurrentWorktree: "You cannot remove the current worktree!",
|
||||||
|
AlreadyInWorktree: "You are already in the selected worktree",
|
||||||
|
CantDeleteMainWorktree: "You cannot remove the main worktree!",
|
||||||
|
NoWorktreesThisRepo: "No worktrees",
|
||||||
|
MissingWorktree: "(missing)",
|
||||||
|
MainWorktree: "(main)",
|
||||||
|
CreateWorktree: "Create worktree",
|
||||||
|
NewWorktreePath: "New worktree path",
|
||||||
|
NewWorktreeBase: "New worktree base ref",
|
||||||
|
BranchNameCannotBeBlank: "Branch name cannot be blank",
|
||||||
|
NewBranchName: "New branch name",
|
||||||
|
NewBranchNameLeaveBlank: "New branch name (leave blank to checkout {{.default}})",
|
||||||
|
ViewWorktreeOptions: "View worktree options",
|
||||||
|
CreateWorktreeFrom: "Create worktree from {{.ref}}",
|
||||||
|
CreateWorktreeFromDetached: "Create worktree from {{.ref}} (detached)",
|
||||||
|
LcWorktree: "worktree",
|
||||||
|
Name: "Name",
|
||||||
|
Branch: "Branch",
|
||||||
|
Path: "Path",
|
||||||
Actions: Actions{
|
Actions: Actions{
|
||||||
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
||||||
CheckoutCommit: "Checkout commit",
|
CheckoutCommit: "Checkout commit",
|
||||||
@@ -1340,6 +1406,8 @@ func EnglishTranslationSet() TranslationSet {
|
|||||||
ResetBisect: "Reset bisect",
|
ResetBisect: "Reset bisect",
|
||||||
BisectSkip: "Bisect skip",
|
BisectSkip: "Bisect skip",
|
||||||
BisectMark: "Bisect mark",
|
BisectMark: "Bisect mark",
|
||||||
|
RemoveWorktree: "Remove worktree",
|
||||||
|
AddWorktree: "Add worktree",
|
||||||
},
|
},
|
||||||
Bisect: Bisect{
|
Bisect: Bisect{
|
||||||
Mark: "Mark %s as %s",
|
Mark: "Mark %s as %s",
|
||||||
|
|||||||
+1
-3
@@ -31,9 +31,7 @@ var periods = []period{
|
|||||||
{"h", SECONDS_IN_HOUR},
|
{"h", SECONDS_IN_HOUR},
|
||||||
{"d", SECONDS_IN_DAY},
|
{"d", SECONDS_IN_DAY},
|
||||||
{"w", SECONDS_IN_WEEK},
|
{"w", SECONDS_IN_WEEK},
|
||||||
// we're using 'm' for both minutes and months which is ambiguous but
|
{"M", SECONDS_IN_MONTH},
|
||||||
// disambiguating with another character feels like overkill.
|
|
||||||
{"m", SECONDS_IN_MONTH},
|
|
||||||
{"y", SECONDS_IN_YEAR},
|
{"y", SECONDS_IN_YEAR},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user