From 1f2346802b50e4d03b138550536f5a35b229290b Mon Sep 17 00:00:00 2001 From: pedrampdd Date: Fri, 8 May 2026 17:28:27 +0330 Subject: [PATCH] Expand ~ in the worktree creation prompt path When creating a new worktree via the prompt, the user-entered path is passed directly to "git worktree add" via os/exec. Because the command is exec'd rather than run through a shell, "~" is taken literally and git creates a directory called "~" containing the rest of the path, instead of the user's home directory. Add a small helper utils.ExpandHomeDir that turns a leading "~" or "~/" into the user's home directory (using os.UserHomeDir) and apply it to the path captured by the New worktree path prompt. Paths that do not begin with "~" are returned unchanged, so absolute paths and relative paths like "../foo" continue to work the way they did before. The "~user" form is rejected explicitly so we don't silently do the wrong thing. Adds a table-driven unit test for the helper covering the empty input, untouched paths, "~" alone, "~/foo/bar", "~user/foo", and a mid-path "~" segment. Fixes #4708 --- .../controllers/helpers/worktree_helper.go | 6 +- pkg/utils/path.go | 37 ++++++++++ pkg/utils/path_test.go | 72 +++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 pkg/utils/path.go create mode 100644 pkg/utils/path_test.go diff --git a/pkg/gui/controllers/helpers/worktree_helper.go b/pkg/gui/controllers/helpers/worktree_helper.go index 6cb22084b31..d1ac50cac20 100644 --- a/pkg/gui/controllers/helpers/worktree_helper.go +++ b/pkg/gui/controllers/helpers/worktree_helper.go @@ -114,7 +114,11 @@ func (self *WorktreeHelper) NewWorktreeCheckout(base string, canCheckoutBase boo self.c.Prompt(types.PromptOpts{ Title: self.c.Tr.NewWorktreePath, HandleConfirm: func(path string) error { - opts.Path = path + expanded, err := utils.ExpandHomeDir(path) + if err != nil { + return err + } + opts.Path = expanded if detached { return f() diff --git a/pkg/utils/path.go b/pkg/utils/path.go new file mode 100644 index 00000000000..994695548b9 --- /dev/null +++ b/pkg/utils/path.go @@ -0,0 +1,37 @@ +package utils + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +// ExpandHomeDir expands a leading "~" or "~/" in path to the user's +// home directory. Paths that do not begin with "~" are returned +// unchanged, including the empty string. +// +// We deliberately only support "~" for the current user (no +// "~user" syntax) because that is what the shells lazygit users +// expect to see lazygit honour, and the cross-platform story for +// other-user expansion is messy. +// +// If path begins with "~" but the home directory cannot be +// determined, the unexpanded path is returned along with the error. +func ExpandHomeDir(path string) (string, error) { + if path == "" || path[0] != '~' { + return path, nil + } + // Only "~" alone or "~/..." — not "~user/...". + if path != "~" && !strings.HasPrefix(path, "~"+string(filepath.Separator)) && !strings.HasPrefix(path, "~/") { + return path, errors.New("only the current user's home directory can be expanded with ~") + } + home, err := os.UserHomeDir() + if err != nil { + return path, err + } + if path == "~" { + return home, nil + } + return filepath.Join(home, path[2:]), nil +} diff --git a/pkg/utils/path_test.go b/pkg/utils/path_test.go new file mode 100644 index 00000000000..e6c5b4d692a --- /dev/null +++ b/pkg/utils/path_test.go @@ -0,0 +1,72 @@ +package utils + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExpandHomeDir(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil { + t.Fatalf("UserHomeDir failed: %v", err) + } + + scenarios := []struct { + name string + input string + expected string + wantErr bool + }{ + { + name: "empty stays empty", + input: "", + expected: "", + }, + { + name: "no tilde left untouched", + input: filepath.Join("path", "to", "thing"), + expected: filepath.Join("path", "to", "thing"), + }, + { + name: "absolute path left untouched", + input: "/absolute/path", + expected: "/absolute/path", + }, + { + name: "tilde alone expands to home", + input: "~", + expected: home, + }, + { + name: "tilde slash expands to home/rest", + input: "~/foo/bar", + expected: filepath.Join(home, "foo", "bar"), + }, + { + name: "tilde without separator (~user style) is rejected", + input: "~bob/foo", + expected: "~bob/foo", + wantErr: true, + }, + { + name: "tilde mid-path is not expanded", + input: filepath.Join("foo", "~", "bar"), + expected: filepath.Join("foo", "~", "bar"), + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + got, err := ExpandHomeDir(s.input) + if s.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, s.expected, got) + }) + } +}