mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 02:24:25 -05:00
Expand a leading ~ in worktree paths to the home directory
Lazygit runs git directly rather than through a shell, so a literal "~" reaches `git worktree add` unexpanded and git creates a directory named "~" instead of using the home directory. Expand the tilde ourselves, both for paths typed into the "Other" location prompt and for the worktree.defaultPath config value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
737fb98967
commit
7a67cea687
@@ -533,6 +533,7 @@ worktree:
|
||||
# location alongside the parent directories of any worktrees you already have.
|
||||
# A relative path is resolved against the repository's root directory, so
|
||||
# "../worktrees" sits beside the repo and ".worktrees" sits inside it.
|
||||
# A leading "~" is expanded to your home directory, so "~/worktrees" works.
|
||||
defaultPath: ""
|
||||
|
||||
# Periodic update checks
|
||||
|
||||
@@ -427,6 +427,7 @@ type CommitPrefixConfig struct {
|
||||
type WorktreeConfig struct {
|
||||
// Default parent directory for new worktrees. It is offered as a candidate location alongside the parent directories of any worktrees you already have.
|
||||
// A relative path is resolved against the repository's root directory, so "../worktrees" sits beside the repo and ".worktrees" sits inside it.
|
||||
// A leading "~" is expanded to your home directory, so "~/worktrees" works.
|
||||
DefaultPath string `yaml:"defaultPath"`
|
||||
}
|
||||
|
||||
|
||||
@@ -378,7 +378,7 @@ func (self *WorktreeHelper) promptForWorktreeLocation(dirName string, prompt str
|
||||
parentDirs := worktreeParentDirCandidates(
|
||||
self.c.Git().RepoPaths.RepoPath(),
|
||||
linkedWorktreePaths,
|
||||
self.c.UserConfig().Worktree.DefaultPath,
|
||||
utils.ExpandTilde(self.c.UserConfig().Worktree.DefaultPath),
|
||||
)
|
||||
|
||||
targets := lo.Map(parentDirs, func(parentDir string, _ int) string {
|
||||
@@ -398,7 +398,9 @@ func (self *WorktreeHelper) promptForWorktreeLocation(dirName string, prompt str
|
||||
self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.NewWorktreePath,
|
||||
InitialContent: targets[0],
|
||||
HandleConfirm: onConfirm,
|
||||
HandleConfirm: func(response string) error {
|
||||
return onConfirm(utils.ExpandTilde(response))
|
||||
},
|
||||
})
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -508,6 +508,7 @@ var tests = []*components.IntegrationTest{
|
||||
worktree.BareRepoWorktreeConfig,
|
||||
worktree.Crud,
|
||||
worktree.CustomCommand,
|
||||
worktree.DefaultPathTilde,
|
||||
worktree.DetachWorktreeFromBranch,
|
||||
worktree.DotfileBareRepo,
|
||||
worktree.DoubleNestedLinkedSubmodule,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package worktree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var DefaultPathTilde = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "A leading ~ in the worktree.defaultPath config is expanded to the home directory",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {
|
||||
config.GetUserConfig().Worktree.DefaultPath = "~/my-worktrees"
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.NewBranch("mybranch")
|
||||
shell.CreateFileAndAdd("README.md", "hello world")
|
||||
shell.Commit("initial commit")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Branches().
|
||||
Focus().
|
||||
NavigateToLine(Contains("mybranch")).
|
||||
Press(keys.Universal.NewWorktree).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("New worktree")).
|
||||
Select(Contains("New branch and worktree from 'mybranch'")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("New branch and worktree name")).
|
||||
Type("newbranch").
|
||||
Confirm()
|
||||
|
||||
// The default path's "~" is expanded to an absolute home-directory
|
||||
// path; without expansion it would stay a literal "~" resolved
|
||||
// against the repo, so the candidate would still contain a "~".
|
||||
home, _ := os.UserHomeDir()
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Worktree location")).
|
||||
ContainsLines(
|
||||
Contains(filepath.Join(home, "my-worktrees", "newbranch")).DoesNotContain("~"),
|
||||
).
|
||||
Cancel()
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -96,3 +97,26 @@ func FilePath(skip int) string {
|
||||
_, path, _, _ := runtime.Caller(skip)
|
||||
return path
|
||||
}
|
||||
|
||||
// ExpandTilde expands a leading "~" that refers to the current user's home
|
||||
// directory: "~" and "~/foo" become e.g. "/home/user" and "/home/user/foo". A
|
||||
// tilde anywhere other than the start, or one immediately followed by a
|
||||
// username ("~other/foo"), is left untouched, as is the path if the home
|
||||
// directory can't be determined. We expand it ourselves because lazygit runs
|
||||
// git directly, with no shell to do it for us.
|
||||
func ExpandTilde(path string) string {
|
||||
if path != "~" && !strings.HasPrefix(path, "~/") &&
|
||||
!(runtime.GOOS == "windows" && strings.HasPrefix(path, `~\`)) {
|
||||
return path
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
|
||||
if path == "~" {
|
||||
return home
|
||||
}
|
||||
return filepath.Join(home, path[2:])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -98,3 +100,28 @@ func TestModuloWithWrap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandTilde(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
assert.NoError(t, err)
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{"bare tilde", "~", home},
|
||||
{"tilde with subpath", "~/worktrees", filepath.Join(home, "worktrees")},
|
||||
{"absolute path is untouched", "/absolute/path", "/absolute/path"},
|
||||
{"relative path is untouched", "relative/path", "relative/path"},
|
||||
{"tilde not at the start is untouched", "/foo/~/bar", "/foo/~/bar"},
|
||||
{"tilde followed by a username is untouched", "~other/worktrees", "~other/worktrees"},
|
||||
{"empty string is untouched", "", ""},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.expected, ExpandTilde(s.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3899,7 +3899,7 @@
|
||||
"properties": {
|
||||
"defaultPath": {
|
||||
"type": "string",
|
||||
"description": "Default parent directory for new worktrees. It is offered as a candidate location alongside the parent directories of any worktrees you already have.\nA relative path is resolved against the repository's root directory, so \"../worktrees\" sits beside the repo and \".worktrees\" sits inside it."
|
||||
"description": "Default parent directory for new worktrees. It is offered as a candidate location alongside the parent directories of any worktrees you already have.\nA relative path is resolved against the repository's root directory, so \"../worktrees\" sits beside the repo and \".worktrees\" sits inside it.\nA leading \"~\" is expanded to your home directory, so \"~/worktrees\" works."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
Reference in New Issue
Block a user