mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 02:07:09 -04:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||||
@@ -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,7 @@ 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
|
||||||
|
|
||||||
Loaders Loaders
|
Loaders Loaders
|
||||||
}
|
}
|
||||||
@@ -50,6 +51,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 +129,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 +158,7 @@ func NewGitCommandAux(
|
|||||||
Tag: tagCommands,
|
Tag: tagCommands,
|
||||||
Bisect: bisectCommands,
|
Bisect: bisectCommands,
|
||||||
WorkingTree: workingTreeCommands,
|
WorkingTree: workingTreeCommands,
|
||||||
|
Worktree: worktreeCommands,
|
||||||
Loaders: Loaders{
|
Loaders: Loaders{
|
||||||
BranchLoader: branchLoader,
|
BranchLoader: branchLoader,
|
||||||
CommitFileLoader: commitFileLoader,
|
CommitFileLoader: commitFileLoader,
|
||||||
@@ -161,6 +166,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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package git_commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -117,6 +118,11 @@ outer:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *BranchLoader) obtainBranches() []*models.Branch {
|
func (self *BranchLoader) obtainBranches() []*models.Branch {
|
||||||
|
currentDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
output, err := self.getRawBranches()
|
output, err := self.getRawBranches()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -138,7 +144,7 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return obtainBranch(split), true
|
return obtainBranch(split, currentDir), true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,28 +172,32 @@ var branchFields = []string{
|
|||||||
"upstream:track",
|
"upstream:track",
|
||||||
"subject",
|
"subject",
|
||||||
fmt.Sprintf("objectname:short=%d", utils.COMMIT_HASH_SHORT_SIZE),
|
fmt.Sprintf("objectname:short=%d", utils.COMMIT_HASH_SHORT_SIZE),
|
||||||
|
"worktreepath",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain branch information from parsed line output of getRawBranches()
|
// Obtain branch information from parsed line output of getRawBranches()
|
||||||
func obtainBranch(split []string) *models.Branch {
|
func obtainBranch(split []string, currentDir string) *models.Branch {
|
||||||
headMarker := split[0]
|
headMarker := split[0]
|
||||||
fullName := split[1]
|
fullName := split[1]
|
||||||
upstreamName := split[2]
|
upstreamName := split[2]
|
||||||
track := split[3]
|
track := split[3]
|
||||||
subject := split[4]
|
subject := split[4]
|
||||||
commitHash := split[5]
|
commitHash := split[5]
|
||||||
|
branchDir := split[6]
|
||||||
|
checkedOutByOtherWorktree := len(branchDir) > 0 && branchDir != currentDir
|
||||||
|
|
||||||
name := strings.TrimPrefix(fullName, "heads/")
|
name := strings.TrimPrefix(fullName, "heads/")
|
||||||
pushables, pullables, gone := parseUpstreamInfo(upstreamName, track)
|
pushables, pullables, gone := parseUpstreamInfo(upstreamName, track)
|
||||||
|
|
||||||
return &models.Branch{
|
return &models.Branch{
|
||||||
Name: name,
|
Name: name,
|
||||||
Pushables: pushables,
|
Pushables: pushables,
|
||||||
Pullables: pullables,
|
Pullables: pullables,
|
||||||
UpstreamGone: gone,
|
UpstreamGone: gone,
|
||||||
Head: headMarker == "*",
|
Head: headMarker == "*",
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
CommitHash: commitHash,
|
CommitHash: commitHash,
|
||||||
|
CheckedOutByOtherWorktree: checkedOutByOtherWorktree,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func TestObtainBranch(t *testing.T) {
|
|||||||
|
|
||||||
for _, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
t.Run(s.testName, func(t *testing.T) {
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
branch := obtainBranch(s.input)
|
branch := obtainBranch(s.input, "current-dir")
|
||||||
assert.EqualValues(t, s.expectedBranch, branch)
|
assert.EqualValues(t, s.expectedBranch, branch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeCommands) New(worktreePath string, committish string) error {
|
||||||
|
cmdArgs := NewGitCmd("worktree").Arg("add", worktreePath, committish).ToArgv()
|
||||||
|
|
||||||
|
return self.cmd.New(cmdArgs).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) IsCurrentWorktree(w *models.Worktree) bool {
|
||||||
|
return IsCurrentWorktree(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsCurrentWorktree(w *models.Worktree) bool {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return EqualPath(pwd, w.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeCommands) IsWorktreePathMissing(w *models.Worktree) bool {
|
||||||
|
if _, err := os.Stat(w.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", 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package git_commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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:], "/")
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ type Branch struct {
|
|||||||
Subject string
|
Subject string
|
||||||
// commit hash
|
// commit hash
|
||||||
CommitHash string
|
CommitHash string
|
||||||
|
|
||||||
|
CheckedOutByOtherWorktree bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Branch) FullRefName() string {
|
func (b *Branch) FullRefName() string {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
c.Model().Worktrees,
|
||||||
|
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()
|
||||||
|
}
|
||||||
+21
-3
@@ -45,7 +45,18 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
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)
|
reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo)
|
||||||
|
worktreeHelper := helpers.NewWorktreeHelper(helperCommon, reposHelper)
|
||||||
|
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 +95,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 +110,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 +150,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 +190,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,
|
||||||
@@ -298,6 +312,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,37 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
|
|||||||
return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch)
|
return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if selectedBranch.CheckedOutByOtherWorktree {
|
||||||
|
worktreeForRef, ok := self.worktreeForRef(selectedBranch.Name)
|
||||||
|
if ok && !self.c.Git().Worktree.IsCurrentWorktree(worktreeForRef) {
|
||||||
|
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) worktreeForRef(ref string) (*models.Worktree, bool) {
|
||||||
|
for _, worktree := range self.c.Model().Worktrees {
|
||||||
|
if worktree.Branch == ref {
|
||||||
|
return worktree, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,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 +38,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 +48,7 @@ func NewRefreshHelper(
|
|||||||
patchBuildingHelper: patchBuildingHelper,
|
patchBuildingHelper: patchBuildingHelper,
|
||||||
stagingHelper: stagingHelper,
|
stagingHelper: stagingHelper,
|
||||||
mergeConflictsHelper: mergeConflictsHelper,
|
mergeConflictsHelper: mergeConflictsHelper,
|
||||||
|
worktreeHelper: worktreeHelper,
|
||||||
fileWatcher: fileWatcher,
|
fileWatcher: fileWatcher,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +80,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 +132,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 +178,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 +566,17 @@ func (self *RefreshHelper) refreshRemotes() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *RefreshHelper) refreshWorktrees() error {
|
||||||
|
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())
|
||||||
@@ -587,6 +607,10 @@ func (self *RefreshHelper) refreshStatus() {
|
|||||||
|
|
||||||
name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
|
name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
|
||||||
repoName := utils.GetCurrentRepoName()
|
repoName := utils.GetCurrentRepoName()
|
||||||
|
mainWorktreeName := self.worktreeHelper.GetMainWorktreeName()
|
||||||
|
if repoName != mainWorktreeName {
|
||||||
|
repoName = fmt.Sprintf("%s(%s)", mainWorktreeName, style.FgBlue.Sprint(repoName))
|
||||||
|
}
|
||||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
status += fmt.Sprintf("%s → %s ", repoName, name)
|
||||||
|
|
||||||
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,7 +138,11 @@ 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 {
|
||||||
@@ -146,7 +151,7 @@ func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool) error {
|
|||||||
|
|
||||||
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 +176,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,93 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IWorktreeHelper interface {
|
||||||
|
GetMainWorktreeName() string
|
||||||
|
GetCurrentWorktreeName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorktreeHelper struct {
|
||||||
|
c *HelperCommon
|
||||||
|
reposHelper *ReposHelper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorktreeHelper(c *HelperCommon, reposHelper *ReposHelper) *WorktreeHelper {
|
||||||
|
return &WorktreeHelper{
|
||||||
|
c: c,
|
||||||
|
reposHelper: reposHelper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) GetMainWorktreeName() string {
|
||||||
|
for _, worktree := range self.c.Model().Worktrees {
|
||||||
|
if worktree.Main() {
|
||||||
|
return worktree.Name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) IsCurrentWorktree(w *models.Worktree) bool {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
log.Fatalln(fmt.Errorf("failed to check if worktree path `%s` exists\n%w", w.Path, err).Error())
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) NewWorktree() error {
|
||||||
|
return self.c.Prompt(types.PromptOpts{
|
||||||
|
Title: self.c.Tr.NewWorktreePath,
|
||||||
|
HandleConfirm: func(path string) error {
|
||||||
|
return self.c.Prompt(types.PromptOpts{
|
||||||
|
Title: self.c.Tr.NewWorktreePath,
|
||||||
|
HandleConfirm: func(committish string) 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(sanitizedBranchName(path), committish); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) Switch(worktree *models.Worktree, contextKey types.ContextKey) error {
|
||||||
|
if self.c.Git().Worktree.IsCurrentWorktree(worktree) {
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.AlreadyInWorktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.c.LogAction(self.c.Tr.SwitchToWorktree)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
self.c.State().GetRepoPathStack().Clear()
|
||||||
|
|
||||||
|
return self.reposHelper.DispatchSwitchTo(worktree.Path, true, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
|
||||||
|
}
|
||||||
@@ -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,166 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"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"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.CantDeleteCurrentWorktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.removeWithForce(worktree, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreesController) removeWithForce(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.RemovingWorktree)
|
||||||
|
if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
|
||||||
|
errMessage := err.Error()
|
||||||
|
if !force {
|
||||||
|
return self.removeWithForce(worktree, true)
|
||||||
|
}
|
||||||
|
return self.c.ErrorMsg(errMessage)
|
||||||
|
}
|
||||||
|
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES}})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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, context.WORKTREES_CONTEXT_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
+17
-2
@@ -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
|
||||||
}
|
}
|
||||||
@@ -544,6 +555,10 @@ func (gui *Gui) viewTabMap() map[string][]context.TabView {
|
|||||||
Tab: gui.c.Tr.TagsTitle,
|
Tab: gui.c.Tr.TagsTitle,
|
||||||
ViewName: "tags",
|
ViewName: "tags",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Tab: gui.c.Tr.WorktreesTitle,
|
||||||
|
ViewName: "worktrees",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"commits": {
|
"commits": {
|
||||||
{
|
{
|
||||||
@@ -614,7 +629,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{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,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)
|
||||||
@@ -49,6 +50,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 b.CheckedOutByOtherWorktree {
|
||||||
|
worktreeIcon := lo.Ternary(icons.IsIconEnabled(), icons.LINKED_WORKTREE_ICON, "(worktree)")
|
||||||
|
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 +63,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,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(*models.Worktree) bool, isMissing func(*models.Worktree) bool) [][]string {
|
||||||
|
return lo.Map(worktrees, func(worktree *models.Worktree, _ int) []string {
|
||||||
|
return GetWorktreeDisplayString(
|
||||||
|
isCurrent(worktree),
|
||||||
|
isMissing(worktree),
|
||||||
|
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
|
||||||
|
|||||||
@@ -480,6 +480,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 +541,27 @@ type TranslationSet struct {
|
|||||||
FilterPrefix string
|
FilterPrefix string
|
||||||
ExitSearchMode string
|
ExitSearchMode string
|
||||||
ExitTextFilterMode string
|
ExitTextFilterMode string
|
||||||
|
SwitchToWorktree string
|
||||||
|
RemoveWorktree string
|
||||||
|
RemoveWorktreeTitle 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
|
||||||
|
NewWorktreeBranch string
|
||||||
|
Name string
|
||||||
|
Branch string
|
||||||
|
Path string
|
||||||
Actions Actions
|
Actions Actions
|
||||||
Bisect Bisect
|
Bisect Bisect
|
||||||
}
|
}
|
||||||
@@ -666,6 +688,8 @@ type Actions struct {
|
|||||||
ResetBisect string
|
ResetBisect string
|
||||||
BisectSkip string
|
BisectSkip string
|
||||||
BisectMark string
|
BisectMark string
|
||||||
|
RemoveWorktree string
|
||||||
|
AddWorktree string
|
||||||
}
|
}
|
||||||
|
|
||||||
const englishIntroPopupMessage = `
|
const englishIntroPopupMessage = `
|
||||||
@@ -1175,6 +1199,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 +1258,27 @@ 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}}' is not fully merged. Are you sure you want to remove it?",
|
||||||
|
RemovingWorktree: "Deleting 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",
|
||||||
|
NewWorktreeBranch: "New worktree branch (leave blank to use the current branch)",
|
||||||
|
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 +1386,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",
|
||||||
|
|||||||
Reference in New Issue
Block a user