diff --git a/pkg/commands/git_commands/branch_loader_test.go b/pkg/commands/git_commands/branch_loader_test.go index f16dcf5f4..86ee1640d 100644 --- a/pkg/commands/git_commands/branch_loader_test.go +++ b/pkg/commands/git_commands/branch_loader_test.go @@ -81,7 +81,7 @@ func TestObtainBranch(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { - branch := obtainBranch(s.input) + branch := obtainBranch(s.input, "current-dir") assert.EqualValues(t, s.expectedBranch, branch) }) } diff --git a/pkg/commands/git_commands/worktree.go b/pkg/commands/git_commands/worktree.go index 78157ddf2..7f3dd8e01 100644 --- a/pkg/commands/git_commands/worktree.go +++ b/pkg/commands/git_commands/worktree.go @@ -20,8 +20,8 @@ func NewWorktreeCommands(gitCommon *GitCommon) *WorktreeCommands { } } -func (self *WorktreeCommands) New(worktreePath string) error { - cmdArgs := NewGitCmd("worktree").Arg("add", worktreePath).ToArgv() +func (self *WorktreeCommands) New(worktreePath string, committish string) error { + cmdArgs := NewGitCmd("worktree").Arg("add", worktreePath, committish).ToArgv() return self.cmd.New(cmdArgs).Run() } diff --git a/pkg/commands/git_commands/worktree_loader.go b/pkg/commands/git_commands/worktree_loader.go index 3633d31e3..070cdca73 100644 --- a/pkg/commands/git_commands/worktree_loader.go +++ b/pkg/commands/git_commands/worktree_loader.go @@ -1,12 +1,12 @@ package git_commands import ( - "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 { @@ -49,9 +49,115 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) { } } else if strings.HasPrefix(splitLine, "branch ") { branch := strings.SplitN(splitLine, " ", 2)[1] - currentWorktree.Branch = filepath.Base(branch) + currentWorktree.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] + } + return worktrees, nil } + +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:], "/") +} diff --git a/pkg/commands/git_commands/worktree_loader_test.go b/pkg/commands/git_commands/worktree_loader_test.go new file mode 100644 index 000000000..cf3d2a906 --- /dev/null +++ b/pkg/commands/git_commands/worktree_loader_test.go @@ -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) + } +} diff --git a/pkg/commands/models/worktree.go b/pkg/commands/models/worktree.go index 726b59899..fb7dce62d 100644 --- a/pkg/commands/models/worktree.go +++ b/pkg/commands/models/worktree.go @@ -1,14 +1,13 @@ package models -import ( - "path/filepath" -) - -// Worktree : A git worktree +// 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 { @@ -16,7 +15,7 @@ func (w *Worktree) RefName() string { } func (w *Worktree) ID() string { - return w.RefName() + return w.Path } func (w *Worktree) Description() string { @@ -24,7 +23,7 @@ func (w *Worktree) Description() string { } func (w *Worktree) Name() string { - return filepath.Base(w.Path) + return w.NameField } func (w *Worktree) Main() bool { diff --git a/pkg/gui/controllers/helpers/worktree_helper.go b/pkg/gui/controllers/helpers/worktree_helper.go index 481442b2f..bdaf7d2f9 100644 --- a/pkg/gui/controllers/helpers/worktree_helper.go +++ b/pkg/gui/controllers/helpers/worktree_helper.go @@ -58,12 +58,17 @@ func (self *WorktreeHelper) IsWorktreePathMissing(w *models.Worktree) bool { func (self *WorktreeHelper) NewWorktree() error { return self.c.Prompt(types.PromptOpts{ Title: self.c.Tr.NewWorktreePath, - HandleConfirm: func(response string) error { - self.c.LogAction(self.c.Tr.Actions.CreateWorktree) - if err := self.c.Git().Worktree.New(sanitizedBranchName(response)); err != nil { - return err - } - return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) + HandleConfirm: func(path string) error { + return self.c.Prompt(types.PromptOpts{ + Title: self.c.Tr.NewWorktreePath, + HandleConfirm: func(committish string) error { + self.c.LogAction(self.c.Tr.Actions.CreateWorktree) + if err := self.c.Git().Worktree.New(sanitizedBranchName(path), committish); err != nil { + return err + } + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) + }, + }) }, }) } diff --git a/pkg/gui/presentation/worktrees.go b/pkg/gui/presentation/worktrees.go index 55c1da08b..1161afc44 100644 --- a/pkg/gui/presentation/worktrees.go +++ b/pkg/gui/presentation/worktrees.go @@ -38,6 +38,12 @@ func GetWorktreeDisplayString(isCurrent bool, isPathMissing bool, worktree *mode if icons.IsIconEnabled() { res = append(res, textStyle.Sprint(icon)) } - res = append(res, textStyle.Sprint(worktree.Name())) + + name := worktree.Name() + if worktree.Main() { + // TODO: i18n + name += " (main worktree)" + } + res = append(res, textStyle.Sprint(name)) return res } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index c38ea66a2..84581c0b6 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -557,6 +557,7 @@ type TranslationSet struct { MainWorktree string CreateWorktree string NewWorktreePath string + NewWorktreeBranch string Name string Branch string Path string @@ -1276,6 +1277,7 @@ func EnglishTranslationSet() TranslationSet { MainWorktree: "(main)", CreateWorktree: "Create worktree", NewWorktreePath: "New worktree path", + NewWorktreeBranch: "New worktree branch (leave blank to use the current branch)", Name: "Name", Branch: "Branch", Path: "Path",