Compare commits
39 Commits
more-secti
...
bronze
| 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 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -39,3 +39,5 @@ test/results/**
|
||||
|
||||
oryxBuildBinary
|
||||
__debug_bin
|
||||
|
||||
.worktrees
|
||||
1
LjQtBGAgQZ
Normal file
1
LjQtBGAgQZ
Normal file
@@ -0,0 +1 @@
|
||||
eUTlaNqeTB
|
||||
1
MEZhsomFwS
Normal file
1
MEZhsomFwS
Normal file
@@ -0,0 +1 @@
|
||||
qtuVMihtEl
|
||||
@@ -116,6 +116,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
"stash": tr.StashTitle,
|
||||
"suggestions": tr.SuggestionsCheatsheetTitle,
|
||||
"extras": tr.ExtrasTitle,
|
||||
"worktrees": tr.WorktreesTitle,
|
||||
}
|
||||
|
||||
title, ok := contextTitleMap[str]
|
||||
|
||||
@@ -37,6 +37,8 @@ type GitCommand struct {
|
||||
Tag *git_commands.TagCommands
|
||||
WorkingTree *git_commands.WorkingTreeCommands
|
||||
Bisect *git_commands.BisectCommands
|
||||
Worktree *git_commands.WorktreeCommands
|
||||
Version *git_commands.GitVersion
|
||||
|
||||
Loaders Loaders
|
||||
}
|
||||
@@ -50,6 +52,7 @@ type Loaders struct {
|
||||
RemoteLoader *git_commands.RemoteLoader
|
||||
StashLoader *git_commands.StashLoader
|
||||
TagLoader *git_commands.TagLoader
|
||||
Worktrees *git_commands.WorktreeLoader
|
||||
}
|
||||
|
||||
func NewGitCommand(
|
||||
@@ -127,12 +130,14 @@ func NewGitCommandAux(
|
||||
})
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
|
||||
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode, gitCommon)
|
||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
|
||||
worktreeLoader := git_commands.NewWorktreeLoader(cmn, cmd)
|
||||
stashLoader := git_commands.NewStashLoader(cmn, cmd)
|
||||
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
||||
|
||||
@@ -154,6 +159,8 @@ func NewGitCommandAux(
|
||||
Tag: tagCommands,
|
||||
Bisect: bisectCommands,
|
||||
WorkingTree: workingTreeCommands,
|
||||
Worktree: worktreeCommands,
|
||||
Version: version,
|
||||
Loaders: Loaders{
|
||||
BranchLoader: branchLoader,
|
||||
CommitFileLoader: commitFileLoader,
|
||||
@@ -161,6 +168,7 @@ func NewGitCommandAux(
|
||||
FileLoader: fileLoader,
|
||||
ReflogCommitLoader: reflogCommitLoader,
|
||||
RemoteLoader: remoteLoader,
|
||||
Worktrees: worktreeLoader,
|
||||
StashLoader: stashLoader,
|
||||
TagLoader: tagLoader,
|
||||
},
|
||||
|
||||
@@ -49,6 +49,13 @@ func (self *GitCommandBuilder) RepoPath(value string) *GitCommandBuilder {
|
||||
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 {
|
||||
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 {
|
||||
return v.IsOlderThan(version.Major, version.Minor, version.Patch)
|
||||
}
|
||||
|
||||
func (v *GitVersion) SupportsWorktrees() bool {
|
||||
return !v.IsOlderThan(2, 5, 0)
|
||||
}
|
||||
|
||||
107
pkg/commands/git_commands/worktree.go
Normal file
107
pkg/commands/git_commands/worktree.go
Normal file
@@ -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)
|
||||
}
|
||||
236
pkg/commands/git_commands/worktree_loader.go
Normal file
236
pkg/commands/git_commands/worktree_loader.go
Normal file
@@ -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:], "/")
|
||||
}
|
||||
52
pkg/commands/git_commands/worktree_loader_test.go
Normal file
52
pkg/commands/git_commands/worktree_loader_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
31
pkg/commands/models/worktree.go
Normal file
31
pkg/commands/models/worktree.go
Normal file
@@ -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
|
||||
GetEnvVars() []string
|
||||
|
||||
// sets the working directory
|
||||
SetWd(string) ICmdObj
|
||||
|
||||
// runs the command and returns an error if any
|
||||
Run() error
|
||||
// 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
|
||||
}
|
||||
|
||||
func (self *CmdObj) SetWd(wd string) ICmdObj {
|
||||
self.cmd.Dir = wd
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) DontLog() ICmdObj {
|
||||
self.dontLog = true
|
||||
return self
|
||||
|
||||
@@ -132,6 +132,7 @@ type KeybindingConfig struct {
|
||||
Status KeybindingStatusConfig `yaml:"status"`
|
||||
Files KeybindingFilesConfig `yaml:"files"`
|
||||
Branches KeybindingBranchesConfig `yaml:"branches"`
|
||||
Worktrees KeybindingWorktreesConfig `yaml:"worktrees"`
|
||||
Commits KeybindingCommitsConfig `yaml:"commits"`
|
||||
Stash KeybindingStashConfig `yaml:"stash"`
|
||||
CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"`
|
||||
@@ -246,6 +247,10 @@ type KeybindingBranchesConfig struct {
|
||||
FetchRemote string `yaml:"fetchRemote"`
|
||||
}
|
||||
|
||||
type KeybindingWorktreesConfig struct {
|
||||
ViewWorktreeOptions string `yaml:"viewWorktreeOptions"`
|
||||
}
|
||||
|
||||
type KeybindingCommitsConfig struct {
|
||||
SquashDown string `yaml:"squashDown"`
|
||||
RenameCommit string `yaml:"renameCommit"`
|
||||
@@ -584,6 +589,9 @@ func GetDefaultConfig() *UserConfig {
|
||||
SetUpstream: "u",
|
||||
FetchRemote: "f",
|
||||
},
|
||||
Worktrees: KeybindingWorktreesConfig{
|
||||
ViewWorktreeOptions: "w",
|
||||
},
|
||||
Commits: KeybindingCommitsConfig{
|
||||
SquashDown: "s",
|
||||
RenameCommit: "r",
|
||||
|
||||
@@ -376,3 +376,16 @@ func (self *ContextMgr) AllPatchExplorer() []types.IPatchExplorerContext {
|
||||
|
||||
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.Tr,
|
||||
c.UserConfig,
|
||||
c.Model().Worktrees,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,16 @@ import (
|
||||
)
|
||||
|
||||
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"
|
||||
STATUS_CONTEXT_KEY types.ContextKey = "status"
|
||||
SNAKE_CONTEXT_KEY types.ContextKey = "snake"
|
||||
FILES_CONTEXT_KEY types.ContextKey = "files"
|
||||
LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches"
|
||||
REMOTES_CONTEXT_KEY types.ContextKey = "remotes"
|
||||
WORKTREES_CONTEXT_KEY types.ContextKey = "worktrees"
|
||||
REMOTE_BRANCHES_CONTEXT_KEY types.ContextKey = "remoteBranches"
|
||||
TAGS_CONTEXT_KEY types.ContextKey = "tags"
|
||||
LOCAL_COMMITS_CONTEXT_KEY types.ContextKey = "commits"
|
||||
@@ -49,6 +53,7 @@ var AllContextKeys = []types.ContextKey{
|
||||
FILES_CONTEXT_KEY,
|
||||
LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
REMOTES_CONTEXT_KEY,
|
||||
WORKTREES_CONTEXT_KEY,
|
||||
REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
TAGS_CONTEXT_KEY,
|
||||
LOCAL_COMMITS_CONTEXT_KEY,
|
||||
@@ -84,6 +89,7 @@ type ContextTree struct {
|
||||
LocalCommits *LocalCommitsContext
|
||||
CommitFiles *CommitFilesContext
|
||||
Remotes *RemotesContext
|
||||
Worktrees *WorktreesContext
|
||||
Submodules *SubmodulesContext
|
||||
RemoteBranches *RemoteBranchesContext
|
||||
ReflogCommits *ReflogCommitsContext
|
||||
@@ -121,6 +127,7 @@ func (self *ContextTree) Flatten() []types.Context {
|
||||
self.Files,
|
||||
self.SubCommits,
|
||||
self.Remotes,
|
||||
self.Worktrees,
|
||||
self.RemoteBranches,
|
||||
self.Tags,
|
||||
self.Branches,
|
||||
|
||||
@@ -29,6 +29,7 @@ func NewContextTree(c *ContextCommon) *ContextTree {
|
||||
Submodules: NewSubmodulesContext(c),
|
||||
Menu: NewMenuContext(c),
|
||||
Remotes: NewRemotesContext(c),
|
||||
Worktrees: NewWorktreesContext(c),
|
||||
RemoteBranches: NewRemoteBranchesContext(c),
|
||||
LocalCommits: NewLocalCommitsContext(c),
|
||||
CommitFiles: commitFilesContext,
|
||||
|
||||
56
pkg/gui/context/worktrees_context.go
Normal file
56
pkg/gui/context/worktrees_context.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -18,10 +18,13 @@ func (gui *Gui) Helpers() *helpers.Helpers {
|
||||
|
||||
func (gui *Gui) resetHelpersAndControllers() {
|
||||
helperCommon := gui.c
|
||||
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
|
||||
reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo)
|
||||
refsHelper := helpers.NewRefsHelper(helperCommon)
|
||||
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
|
||||
worktreeHelper := helpers.NewWorktreeHelper(helperCommon, reposHelper, refsHelper, suggestionsHelper)
|
||||
|
||||
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper)
|
||||
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
|
||||
|
||||
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
|
||||
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
|
||||
@@ -41,11 +44,20 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
|
||||
gpgHelper := helpers.NewGpgHelper(helperCommon)
|
||||
viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts)
|
||||
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
|
||||
patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
|
||||
stagingHelper := helpers.NewStagingHelper(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)
|
||||
cherryPickHelper := helpers.NewCherryPickHelper(
|
||||
helperCommon,
|
||||
@@ -84,7 +96,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
Commits: commitsHelper,
|
||||
Snake: helpers.NewSnakeHelper(helperCommon),
|
||||
Diff: diffHelper,
|
||||
Repos: helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo),
|
||||
Repos: reposHelper,
|
||||
RecordDirectory: recordDirectoryHelper,
|
||||
Update: helpers.NewUpdateHelper(helperCommon, gui.Updater),
|
||||
Window: windowHelper,
|
||||
@@ -99,7 +111,8 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
modeHelper,
|
||||
appStatusHelper,
|
||||
),
|
||||
Search: helpers.NewSearchHelper(helperCommon),
|
||||
Search: helpers.NewSearchHelper(helperCommon),
|
||||
Worktree: worktreeHelper,
|
||||
}
|
||||
|
||||
gui.CustomCommandsClient = custom_commands.NewClient(
|
||||
@@ -138,6 +151,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
common,
|
||||
func(branches []*models.RemoteBranch) { gui.State.Model.RemoteBranches = branches },
|
||||
)
|
||||
worktreesController := controllers.NewWorktreesController(common)
|
||||
undoController := controllers.NewUndoController(common)
|
||||
globalController := controllers.NewGlobalController(common)
|
||||
contextLinesController := controllers.NewContextLinesController(common)
|
||||
@@ -177,6 +191,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
for _, context := range []types.Context{
|
||||
gui.State.Contexts.Status,
|
||||
gui.State.Contexts.Remotes,
|
||||
gui.State.Contexts.Worktrees,
|
||||
gui.State.Contexts.Tags,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
@@ -227,6 +242,20 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
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,
|
||||
reflogCommitsController,
|
||||
)
|
||||
@@ -298,6 +327,10 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
remotesController,
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.Worktrees,
|
||||
worktreesController,
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.Stash,
|
||||
stashController,
|
||||
)
|
||||
|
||||
@@ -202,10 +202,29 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
|
||||
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)
|
||||
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 {
|
||||
return self.createPullRequest(selectedBranch.Name, "")
|
||||
}
|
||||
@@ -298,9 +317,51 @@ func (self *BranchesController) delete(branch *models.Branch) error {
|
||||
if checkedOutBranch.Name == branch.Name {
|
||||
return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch)
|
||||
}
|
||||
|
||||
if self.checkedOutByOtherWorktree(branch) {
|
||||
return self.promptWorktreeBranchDelete(branch)
|
||||
}
|
||||
|
||||
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 {
|
||||
title := self.c.Tr.DeleteBranch
|
||||
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 {
|
||||
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',
|
||||
Tooltip: utils.ResolvePlaceholderString(
|
||||
@@ -72,7 +72,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
|
||||
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',
|
||||
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 {
|
||||
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',
|
||||
Tooltip: utils.ResolvePlaceholderString(
|
||||
@@ -128,7 +128,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
|
||||
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',
|
||||
Tooltip: utils.ResolvePlaceholderString(
|
||||
|
||||
@@ -47,6 +47,7 @@ type Helpers struct {
|
||||
AppStatus *AppStatusHelper
|
||||
WindowArrangement *WindowArrangementHelper
|
||||
Search *SearchHelper
|
||||
Worktree *WorktreeHelper
|
||||
}
|
||||
|
||||
func NewStubHelpers() *Helpers {
|
||||
@@ -80,5 +81,6 @@ func NewStubHelpers() *Helpers {
|
||||
AppStatus: &AppStatusHelper{},
|
||||
WindowArrangement: &WindowArrangementHelper{},
|
||||
Search: &SearchHelper{},
|
||||
Worktree: &WorktreeHelper{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||
"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/utils"
|
||||
)
|
||||
@@ -27,6 +25,7 @@ type RefreshHelper struct {
|
||||
patchBuildingHelper *PatchBuildingHelper
|
||||
stagingHelper *StagingHelper
|
||||
mergeConflictsHelper *MergeConflictsHelper
|
||||
worktreeHelper *WorktreeHelper
|
||||
fileWatcher types.IFileWatcher
|
||||
}
|
||||
|
||||
@@ -37,6 +36,7 @@ func NewRefreshHelper(
|
||||
patchBuildingHelper *PatchBuildingHelper,
|
||||
stagingHelper *StagingHelper,
|
||||
mergeConflictsHelper *MergeConflictsHelper,
|
||||
worktreeHelper *WorktreeHelper,
|
||||
fileWatcher types.IFileWatcher,
|
||||
) *RefreshHelper {
|
||||
return &RefreshHelper{
|
||||
@@ -46,6 +46,7 @@ func NewRefreshHelper(
|
||||
patchBuildingHelper: patchBuildingHelper,
|
||||
stagingHelper: stagingHelper,
|
||||
mergeConflictsHelper: mergeConflictsHelper,
|
||||
worktreeHelper: worktreeHelper,
|
||||
fileWatcher: fileWatcher,
|
||||
}
|
||||
}
|
||||
@@ -77,6 +78,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
|
||||
types.REFLOG,
|
||||
types.TAGS,
|
||||
types.REMOTES,
|
||||
types.WORKTREES,
|
||||
types.STATUS,
|
||||
types.BISECT_INFO,
|
||||
types.STAGING,
|
||||
@@ -128,6 +130,10 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
|
||||
refresh(func() { _ = self.refreshRemotes() })
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.WORKTREES) {
|
||||
refresh(func() { _ = self.refreshWorktrees() })
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.STAGING) {
|
||||
refresh(func() { _ = self.stagingHelper.RefreshStagingPanel(types.OnFocusOpts{}) })
|
||||
}
|
||||
@@ -170,6 +176,7 @@ func getScopeNames(scopes []types.RefreshableView) []string {
|
||||
types.REFLOG: "reflog",
|
||||
types.TAGS: "tags",
|
||||
types.REMOTES: "remotes",
|
||||
types.WORKTREES: "worktrees",
|
||||
types.STATUS: "status",
|
||||
types.BISECT_INFO: "bisect",
|
||||
types.STAGING: "staging",
|
||||
@@ -557,6 +564,22 @@ func (self *RefreshHelper) refreshRemotes() error {
|
||||
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 {
|
||||
self.c.Model().StashEntries = self.c.Git().Loaders.StashLoader.
|
||||
GetStashEntries(self.c.Modes().Filtering.GetPath())
|
||||
@@ -574,20 +597,16 @@ func (self *RefreshHelper) refreshStatus() {
|
||||
// need to wait for branches to refresh
|
||||
return
|
||||
}
|
||||
status := ""
|
||||
|
||||
if currentBranch.IsRealBranch() {
|
||||
status += presentation.ColoredBranchStatus(currentBranch, self.c.Tr) + " "
|
||||
}
|
||||
|
||||
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
||||
if workingTreeState != enums.REBASE_MODE_NONE {
|
||||
status += style.FgYellow.Sprintf("(%s) ", presentation.FormatWorkingTreeStateLower(self.c.Tr, workingTreeState))
|
||||
var linkedWorktreeName string
|
||||
if self.c.Git().Version.SupportsWorktrees() {
|
||||
linkedWorktreeName = self.worktreeHelper.GetLinkedWorktreeName()
|
||||
}
|
||||
|
||||
name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
||||
repoName := self.worktreeHelper.GetCurrentRepoName()
|
||||
|
||||
status := presentation.FormatStatus(repoName, currentBranch, linkedWorktreeName, workingTreeState, self.c.Tr)
|
||||
|
||||
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/models"
|
||||
"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/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"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
|
||||
type ReposHelper struct {
|
||||
@@ -46,7 +47,7 @@ func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error
|
||||
}
|
||||
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 {
|
||||
@@ -129,7 +130,7 @@ func (self *ReposHelper) CreateRecentReposMenu() error {
|
||||
// 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.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})
|
||||
}
|
||||
|
||||
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()
|
||||
originalPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.LogCommand(fmt.Sprintf("Changing directory to %s", path), false)
|
||||
|
||||
if err := os.Chdir(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return self.c.ErrorMsg(self.c.Tr.ErrRepositoryMovedOrDeleted)
|
||||
return self.c.ErrorMsg(errMsg)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -171,5 +178,5 @@ func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool) error {
|
||||
self.c.Mutexes().RefreshingFilesMutex.Lock()
|
||||
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
|
||||
|
||||
return self.onNewRepo(appTypes.StartArgs{}, reuse)
|
||||
return self.onNewRepo(appTypes.StartArgs{}, reuse, contextKey)
|
||||
}
|
||||
|
||||
302
pkg/gui/controllers/helpers/worktree_helper.go
Normal file
302
pkg/gui/controllers/helpers/worktree_helper.go
Normal file
@@ -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 (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
@@ -77,7 +78,7 @@ func (self *QuitActions) Escape() error {
|
||||
|
||||
repoPathStack := self.c.State().GetRepoPathStack()
|
||||
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 {
|
||||
|
||||
59
pkg/gui/controllers/worktree_options_controller.go
Normal file
59
pkg/gui/controllers/worktree_options_controller.go
Normal file
@@ -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)
|
||||
}
|
||||
139
pkg/gui/controllers/worktrees_controller.go
Normal file
139
pkg/gui/controllers/worktrees_controller.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -274,7 +274,7 @@ func (self *GuiRepoState) GetSplitMainPanel() bool {
|
||||
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
|
||||
gui.git, err = commands.NewGitCommand(
|
||||
gui.Common,
|
||||
@@ -295,6 +295,17 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -530,21 +541,32 @@ func (gui *Gui) initGocui(headless bool, test integrationTypes.IntegrationTest)
|
||||
}
|
||||
|
||||
func (gui *Gui) viewTabMap() map[string][]context.TabView {
|
||||
return map[string][]context.TabView{
|
||||
"branches": {
|
||||
{
|
||||
Tab: gui.c.Tr.LocalBranchesTitle,
|
||||
ViewName: "localBranches",
|
||||
},
|
||||
{
|
||||
Tab: gui.c.Tr.RemotesTitle,
|
||||
ViewName: "remotes",
|
||||
},
|
||||
{
|
||||
Tab: gui.c.Tr.TagsTitle,
|
||||
ViewName: "tags",
|
||||
},
|
||||
branchesTabs := []context.TabView{
|
||||
{
|
||||
Tab: gui.c.Tr.LocalBranchesTitle,
|
||||
ViewName: "localBranches",
|
||||
},
|
||||
{
|
||||
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": {
|
||||
{
|
||||
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
|
||||
if err := gui.onNewRepo(startArgs, false); err != nil {
|
||||
if err := gui.onNewRepo(startArgs, false, context.NO_CONTEXT); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,10 @@ func (self *guiCommon) Context() types.IContextMgr {
|
||||
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 {
|
||||
return self.gui.State.ContextMgr.ActivateContext(context, types.OnFocusOpts{})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var branchPrefixColorCache = make(map[string]style.TextStyle)
|
||||
@@ -22,10 +24,11 @@ func GetBranchListDisplayStrings(
|
||||
diffName string,
|
||||
tr *i18n.TranslationSet,
|
||||
userConfig *config.UserConfig,
|
||||
worktrees []*models.Worktree,
|
||||
) [][]string {
|
||||
return slices.Map(branches, func(branch *models.Branch) []string {
|
||||
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,
|
||||
tr *i18n.TranslationSet,
|
||||
userConfig *config.UserConfig,
|
||||
worktrees []*models.Worktree,
|
||||
) []string {
|
||||
displayName := b.Name
|
||||
if b.DisplayName != "" {
|
||||
@@ -49,6 +53,10 @@ func getBranchDisplayStrings(
|
||||
|
||||
coloredName := nameTextStyle.Sprint(displayName)
|
||||
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)
|
||||
|
||||
recencyColor := style.FgCyan
|
||||
@@ -58,6 +66,7 @@ func getBranchDisplayStrings(
|
||||
|
||||
res := make([]string, 0, 6)
|
||||
res = append(res, recencyColor.Sprint(b.Recency))
|
||||
|
||||
if icons.IsIconEnabled() {
|
||||
res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b)))
|
||||
}
|
||||
|
||||
@@ -7,13 +7,15 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
BRANCH_ICON = "\U000f062c" //
|
||||
DETACHED_HEAD_ICON = "\ue729" //
|
||||
TAG_ICON = "\uf02b" //
|
||||
COMMIT_ICON = "\U000f0718" //
|
||||
MERGE_COMMIT_ICON = "\U000f062d" //
|
||||
DEFAULT_REMOTE_ICON = "\uf02a2" //
|
||||
STASH_ICON = "\uf01c" //
|
||||
BRANCH_ICON = "\U000f062c" //
|
||||
DETACHED_HEAD_ICON = "\ue729" //
|
||||
TAG_ICON = "\uf02b" //
|
||||
COMMIT_ICON = "\U000f0718" //
|
||||
MERGE_COMMIT_ICON = "\U000f062d" //
|
||||
DEFAULT_REMOTE_ICON = "\uf02a2" //
|
||||
STASH_ICON = "\uf01c" //
|
||||
LINKED_WORKTREE_ICON = "\uf838" //
|
||||
MISSING_LINKED_WORKTREE_ICON = "\uf839" //
|
||||
)
|
||||
|
||||
var remoteIcons = map[string]string{
|
||||
@@ -68,3 +70,10 @@ func IconForRemote(remote *models.Remote) string {
|
||||
func IconForStash(stash *models.StashEntry) string {
|
||||
return STASH_ICON
|
||||
}
|
||||
|
||||
func IconForWorktree(missing bool) string {
|
||||
if missing {
|
||||
return MISSING_LINKED_WORKTREE_ICON
|
||||
}
|
||||
return LINKED_WORKTREE_ICON
|
||||
}
|
||||
|
||||
36
pkg/gui/presentation/status.go
Normal file
36
pkg/gui/presentation/status.go
Normal file
@@ -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
|
||||
}
|
||||
49
pkg/gui/presentation/worktrees.go
Normal file
49
pkg/gui/presentation/worktrees.go
Normal file
@@ -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
|
||||
// TODO: replace the above context-based methods with just using Context() e.g. replace PushContext() with Context().Push()
|
||||
Context() IContextMgr
|
||||
ContextForKey(key ContextKey) Context
|
||||
|
||||
ActivateContext(context Context) error
|
||||
|
||||
@@ -196,6 +197,7 @@ type Model struct {
|
||||
StashEntries []*models.StashEntry
|
||||
SubCommits []*models.Commit
|
||||
Remotes []*models.Remote
|
||||
Worktrees []*models.Worktree
|
||||
|
||||
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
||||
// when in filtering mode we only include the ones that match the given path
|
||||
|
||||
@@ -13,6 +13,7 @@ const (
|
||||
REFLOG
|
||||
TAGS
|
||||
REMOTES
|
||||
WORKTREES
|
||||
STATUS
|
||||
SUBMODULES
|
||||
STAGING
|
||||
|
||||
@@ -8,6 +8,7 @@ type Views struct {
|
||||
Files *gocui.View
|
||||
Branches *gocui.View
|
||||
Remotes *gocui.View
|
||||
Worktrees *gocui.View
|
||||
Tags *gocui.View
|
||||
RemoteBranches *gocui.View
|
||||
ReflogCommits *gocui.View
|
||||
|
||||
@@ -29,6 +29,7 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
|
||||
{viewPtr: &gui.Views.Files, name: "files"},
|
||||
{viewPtr: &gui.Views.Tags, name: "tags"},
|
||||
{viewPtr: &gui.Views.Remotes, name: "remotes"},
|
||||
{viewPtr: &gui.Views.Worktrees, name: "worktrees"},
|
||||
{viewPtr: &gui.Views.Branches, name: "localBranches"},
|
||||
{viewPtr: &gui.Views.RemoteBranches, name: "remoteBranches"},
|
||||
{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.Worktrees.Title = gui.c.Tr.WorktreesTitle
|
||||
|
||||
gui.Views.Tags.Title = gui.c.Tr.TagsTitle
|
||||
|
||||
gui.Views.Files.Title = gui.c.Tr.FilesTitle
|
||||
|
||||
@@ -156,6 +156,7 @@ type TranslationSet struct {
|
||||
GitconfigParseErr string
|
||||
EditFile string
|
||||
OpenFile string
|
||||
OpenInEditor string
|
||||
IgnoreFile string
|
||||
ExcludeFile string
|
||||
RefreshFiles string
|
||||
@@ -480,6 +481,7 @@ type TranslationSet struct {
|
||||
ErrCannotEditDirectory string
|
||||
ErrStageDirWithInlineMergeConflicts string
|
||||
ErrRepositoryMovedOrDeleted string
|
||||
ErrWorktreeMovedOrRemoved string
|
||||
CommandLog string
|
||||
ToggleShowCommandLog string
|
||||
FocusCommandLog string
|
||||
@@ -540,6 +542,36 @@ type TranslationSet struct {
|
||||
FilterPrefix string
|
||||
ExitSearchMode 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
|
||||
Bisect Bisect
|
||||
}
|
||||
@@ -666,6 +698,8 @@ type Actions struct {
|
||||
ResetBisect string
|
||||
BisectSkip string
|
||||
BisectMark string
|
||||
RemoveWorktree string
|
||||
AddWorktree string
|
||||
}
|
||||
|
||||
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.`,
|
||||
EditFile: `Edit file`,
|
||||
OpenFile: `Open file`,
|
||||
OpenInEditor: "Open in editor",
|
||||
IgnoreFile: `Add to .gitignore`,
|
||||
ExcludeFile: `Add to .git/info/exclude`,
|
||||
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",
|
||||
ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯",
|
||||
CommandLog: "Command log",
|
||||
ErrWorktreeMovedOrRemoved: "Cannot find worktree. It might have been moved or removed ¯\\_(ツ)_/¯",
|
||||
ToggleShowCommandLog: "Toggle show/hide command log",
|
||||
FocusCommandLog: "Focus command log",
|
||||
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",
|
||||
SearchPrefix: "Search: ",
|
||||
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{
|
||||
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
||||
CheckoutCommit: "Checkout commit",
|
||||
@@ -1340,6 +1406,8 @@ func EnglishTranslationSet() TranslationSet {
|
||||
ResetBisect: "Reset bisect",
|
||||
BisectSkip: "Bisect skip",
|
||||
BisectMark: "Bisect mark",
|
||||
RemoveWorktree: "Remove worktree",
|
||||
AddWorktree: "Add worktree",
|
||||
},
|
||||
Bisect: Bisect{
|
||||
Mark: "Mark %s as %s",
|
||||
|
||||
@@ -31,9 +31,7 @@ var periods = []period{
|
||||
{"h", SECONDS_IN_HOUR},
|
||||
{"d", SECONDS_IN_DAY},
|
||||
{"w", SECONDS_IN_WEEK},
|
||||
// we're using 'm' for both minutes and months which is ambiguous but
|
||||
// disambiguating with another character feels like overkill.
|
||||
{"m", SECONDS_IN_MONTH},
|
||||
{"M", SECONDS_IN_MONTH},
|
||||
{"y", SECONDS_IN_YEAR},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user