diff options
Diffstat (limited to 'pkg')
46 files changed, 1909 insertions, 1045 deletions
diff --git a/pkg/app/app.go b/pkg/app/app.go index a16fbcc1f..e12461e28 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -14,7 +14,6 @@ import ( "github.com/spf13/afero" appTypes "github.com/jesseduffield/lazygit/pkg/app/types" - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/common" @@ -119,7 +118,14 @@ func NewApp(config config.AppConfigurer, test integrationTypes.IntegrationTest, return app, err } - showRecentRepos, err := app.setupRepo() + // If we're not in a repo, repoPaths will be nil. The error is moot for us + // at this stage, since we'll try to init a new repo in setupRepo(), below + repoPaths, err := git_commands.GetRepoPaths(app.OSCommand.Cmd, gitVersion) + if err != nil { + return app, err + } + + showRecentRepos, err := app.setupRepo(repoPaths) if err != nil { return app, err } @@ -168,14 +174,16 @@ func openRecentRepo(app *App) bool { return false } -func (app *App) setupRepo() (bool, error) { +func (app *App) setupRepo( + repoPaths *git_commands.RepoPaths, +) (bool, error) { if env.GetGitDirEnv() != "" { - // we've been given the git dir directly. We'll verify this dir when initializing our Git object + // we've been given the git dir directly. Skip setup return false, nil } // if we are not in a git repo, we ask if we want to `git init` - if err := commands.VerifyInGitRepo(app.OSCommand); err != nil { + if repoPaths == nil { cwd, err := os.Getwd() if err != nil { return false, err @@ -221,6 +229,7 @@ func (app *App) setupRepo() (bool, error) { if err := app.OSCommand.Cmd.New(args).Run(); err != nil { return false, err } + return false, nil } @@ -238,10 +247,7 @@ func (app *App) setupRepo() (bool, error) { } // Run this afterward so that the previous repo creation steps can run without this interfering - if isBare, err := git_commands.IsBareRepo(app.OSCommand); isBare { - if err != nil { - return false, err - } + if repoPaths.IsBareRepo() { fmt.Print(app.Tr.BareRepo) diff --git a/pkg/commands/git_commands/branch.go b/pkg/commands/git_commands/branch.go index 79e418d7f..6c9aa8740 100644 --- a/pkg/commands/git_commands/branch.go +++ b/pkg/commands/git_commands/branch.go @@ -7,10 +7,12 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/mgutz/str" + "github.com/samber/lo" ) type BranchCommands struct { *GitCommon + allBranchesLogCmdIndex uint8 // keeps track of current all branches log command } func NewBranchCommands(gitCommon *GitCommon) *BranchCommands { @@ -28,6 +30,15 @@ func (self *BranchCommands) New(name string, base string) error { return self.cmd.New(cmdArgs).Run() } +func (self *BranchCommands) NewWithoutTracking(name string, base string) error { + cmdArgs := NewGitCmd("checkout"). + Arg("-b", name, base). + Arg("--no-track"). + ToArgv() + + return self.cmd.New(cmdArgs).Run() +} + // CreateWithUpstream creates a new branch with a given upstream, but without // checking it out func (self *BranchCommands) CreateWithUpstream(name string, upstream string) error { @@ -235,5 +246,17 @@ func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error { } func (self *BranchCommands) AllBranchesLogCmdObj() oscommands.ICmdObj { - return self.cmd.New(str.ToArgv(self.UserConfig.Git.AllBranchesLogCmd)).DontLog() + // Only choose between non-empty, non-identical commands + candidates := lo.Uniq(lo.WithoutEmpty(append([]string{ + self.UserConfig.Git.AllBranchesLogCmd, + }, + self.UserConfig.Git.AllBranchesLogCmds..., + ))) + + n := len(candidates) + + i := self.allBranchesLogCmdIndex + self.allBranchesLogCmdIndex = uint8((int(i) + 1) % n) + + return self.cmd.New(str.ToArgv(candidates[i])).DontLog() } diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 517be276e..4153dfeb9 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -271,6 +271,7 @@ func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) oscommand Arg("-p"). Arg(hash). ArgIf(self.AppState.IgnoreWhitespaceInDiffView, "--ignore-all-space"). + Arg(fmt.Sprintf("--find-renames=%d%%", self.AppState.RenameSimilarityThreshold)). ArgIf(filterPath != "", "--", filterPath). Dir(self.repoPaths.worktreePath). ToArgv() diff --git a/pkg/commands/git_commands/commit_test.go b/pkg/commands/git_commands/commit_test.go index c3708422e..239d7fa8f 100644 --- a/pkg/commands/git_commands/commit_test.go +++ b/pkg/commands/git_commands/commit_test.go @@ -228,54 +228,69 @@ func TestCommitCreateAmendCommit(t *testing.T) { func TestCommitShowCmdObj(t *testing.T) { type scenario struct { - testName string - filterPath string - contextSize int - ignoreWhitespace bool - extDiffCmd string - expected []string + testName string + filterPath string + contextSize int + similarityThreshold int + ignoreWhitespace bool + extDiffCmd string + expected []string } scenarios := []scenario{ { - testName: "Default case without filter path", - filterPath: "", - contextSize: 3, - ignoreWhitespace: false, - extDiffCmd: "", - expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890"}, + testName: "Default case without filter path", + filterPath: "", + contextSize: 3, + similarityThreshold: 50, + ignoreWhitespace: false, + extDiffCmd: "", + expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890", "--find-renames=50%"}, }, { - testName: "Default case with filter path", - filterPath: "file.txt", - contextSize: 3, - ignoreWhitespace: false, - extDiffCmd: "", - expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890", "--", "file.txt"}, + testName: "Default case with filter path", + filterPath: "file.txt", + contextSize: 3, + similarityThreshold: 50, + ignoreWhitespace: false, + extDiffCmd: "", + expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890", "--find-renames=50%", "--", "file.txt"}, }, { - testName: "Show diff with custom context size", - filterPath: "", - contextSize: 77, - ignoreWhitespace: false, - extDiffCmd: "", - expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=77", "--stat", "--decorate", "-p", "1234567890"}, + testName: "Show diff with custom context size", + filterPath: "", + contextSize: 77, + similarityThreshold: 50, + ignoreWhitespace: false, + extDiffCmd: "", + expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=77", "--stat", "--decorate", "-p", "1234567890", "--find-renames=50%"}, }, { - testName: "Show diff, ignoring whitespace", - filterPath: "", - contextSize: 77, - ignoreWhitespace: true, - extDiffCmd: "", - expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=77", "--stat", "--decorate", "-p", "1234567890", "--ignore-all-space"}, + testName: "Show diff with custom similarity threshold", + filterPath: "", + contextSize: 3, + similarityThreshold: 33, + ignoreWhitespace: false, + extDiffCmd: "", + expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890", "--find-renames=33%"}, }, { - testName: "Show diff with external diff command", - filterPath: "", - contextSize: 3, - ignoreWhitespace: false, - extDiffCmd: "difft --color=always", - expected: []string{"-C", "/path/to/worktree", "-c", "diff.external=difft --color=always", "-c", "diff.noprefix=false", "show", "--ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890"}, + testName: "Show diff, ignoring whitespace", + filterPath: "", + contextSize: 77, + similarityThreshold: 50, + ignoreWhitespace: true, + extDiffCmd: "", + expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=77", "--stat", "--decorate", "-p", "1234567890", "--ignore-all-space", "--find-renames=50%"}, + }, + { + testName: "Show diff with external diff command", + filterPath: "", + contextSize: 3, + similarityThreshold: 50, + ignoreWhitespace: false, + extDiffCmd: "difft --color=always", + expected: []string{"-C", "/path/to/worktree", "-c", "diff.external=difft --color=always", "-c", "diff.noprefix=false", "show", "--ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890", "--find-renames=50%"}, }, } @@ -286,6 +301,7 @@ func TestCommitShowCmdObj(t *testing.T) { appState := &config.AppState{} appState.IgnoreWhitespaceInDiffView = s.ignoreWhitespace appState.DiffContextSize = s.contextSize + appState.RenameSimilarityThreshold = s.similarityThreshold runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expected, "", nil) repoPaths := RepoPaths{ diff --git a/pkg/commands/git_commands/file_loader.go b/pkg/commands/git_commands/file_loader.go index 73d7fdc64..72329543a 100644 --- a/pkg/commands/git_commands/file_loader.go +++ b/pkg/commands/git_commands/file_loader.go @@ -100,15 +100,19 @@ type FileStatus struct { PreviousName string } -func (c *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) { +func (self *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) { cmdArgs := NewGitCmd("status"). Arg(opts.UntrackedFilesArg). Arg("--porcelain"). Arg("-z"). - ArgIf(opts.NoRenames, "--no-renames"). + ArgIfElse( + opts.NoRenames, + "--no-renames", + fmt.Sprintf("--find-renames=%d%%", self.AppState.RenameSimilarityThreshold), + ). ToArgv() - statusLines, _, err := c.cmd.New(cmdArgs).DontLog().RunWithOutputs() + statusLines, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs() if err != nil { return []FileStatus{}, err } diff --git a/pkg/commands/git_commands/file_loader_test.go b/pkg/commands/git_commands/file_loader_test.go index 73fac7ef4..5a9f15700 100644 --- a/pkg/commands/git_commands/file_loader_test.go +++ b/pkg/commands/git_commands/file_loader_test.go @@ -5,27 +5,31 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/config" "github.com/stretchr/testify/assert" ) func TestFileGetStatusFiles(t *testing.T) { type scenario struct { - testName string - runner oscommands.ICmdObjRunner - expectedFiles []*models.File + testName string + similarityThreshold int + runner oscommands.ICmdObjRunner + expectedFiles []*models.File } scenarios := []scenario{ { "No files found", + 50, oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z"}, "", nil), + ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "", nil), []*models.File{}, }, { "Several files found", + 50, oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z"}, + ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt", nil, ), @@ -94,8 +98,9 @@ func TestFileGetStatusFiles(t *testing.T) { }, { "File with new line char", + 50, oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z"}, "MM a\nb.txt", nil), + ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "MM a\nb.txt", nil), []*models.File{ { Name: "a\nb.txt", @@ -113,8 +118,9 @@ func TestFileGetStatusFiles(t *testing.T) { }, { "Renamed files", + 50, oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z"}, + ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt", nil, ), @@ -149,8 +155,9 @@ func TestFileGetStatusFiles(t *testing.T) { }, { "File with arrow in name", + 50, oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z"}, + ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, `?? a -> b.txt`, nil, ), @@ -175,8 +182,11 @@ func TestFileGetStatusFiles(t *testing.T) { t.Run(s.testName, func(t *testing.T) { cmd := oscommands.NewDummyCmdObjBuilder(s.runner) + appState := &config.AppState{} + appState.RenameSimilarityThreshold = s.similarityThreshold + loader := &FileLoader{ - GitCommon: buildGitCommon(commonDeps{}), + GitCommon: buildGitCommon(commonDeps{appState: appState}), cmd: cmd, config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"}, getFileType: func(string) string { return "file" }, diff --git a/pkg/commands/git_commands/repo_paths.go b/pkg/commands/git_commands/repo_paths.go index b0e1970db..c2e77d446 100644 --- a/pkg/commands/git_commands/repo_paths.go +++ b/pkg/commands/git_commands/repo_paths.go @@ -2,6 +2,7 @@ package git_commands import ( ioFs "io/fs" + "os" "path" "path/filepath" "strings" @@ -18,6 +19,7 @@ type RepoPaths struct { repoPath string repoGitDirPath string repoName string + isBareRepo bool } var gitPathFormatVersion GitVersion = GitVersion{2, 31, 0, ""} @@ -54,6 +56,10 @@ func (self *RepoPaths) RepoName() string { return self.repoName } +func (self *RepoPaths) IsBareRepo() bool { + return self.isBareRepo +} + // Returns the repo paths for a typical repo func MockRepoPaths(currentPath string) *RepoPaths { return &RepoPaths{ @@ -62,6 +68,7 @@ func MockRepoPaths(currentPath string) *RepoPaths { repoPath: currentPath, repoGitDirPath: path.Join(currentPath, ".git"), repoName: "lazygit", + isBareRepo: false, } } @@ -69,7 +76,19 @@ func GetRepoPaths( cmd oscommands.ICmdObjBuilder, version *GitVersion, ) (*RepoPaths, error) { - gitDirOutput, err := callGitRevParse(cmd, version, "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree") + cwd, err := os.Getwd() + if err != nil { + return nil, err + } + return GetRepoPathsForDir(cwd, cmd, version) +} + +func GetRepoPathsForDir( + dir string, + cmd oscommands.ICmdObjBuilder, + version *GitVersion, +) (*RepoPaths, error) { + gitDirOutput, err := callGitRevParseWithDir(cmd, version, dir, "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree") if err != nil { return nil, err } @@ -84,13 +103,14 @@ func GetRepoPaths( return nil, err } } + isBareRepo := gitDirResults[3] == "true" // If we're in a submodule, --show-superproject-working-tree will return - // a value, meaning gitDirResults will be length 4. In that case + // a value, meaning gitDirResults will be length 5. In that case // return the worktree path as the repoPath. Otherwise we're in a // normal repo or a worktree so return the parent of the git common // dir (repoGitDirPath) - isSubmodule := len(gitDirResults) == 4 + isSubmodule := len(gitDirResults) == 5 var repoPath string if isSubmodule { @@ -106,17 +126,10 @@ func GetRepoPaths( repoPath: repoPath, repoGitDirPath: repoGitDirPath, repoName: repoName, + isBareRepo: isBareRepo, }, nil } -func callGitRevParse( - cmd oscommands.ICmdObjBuilder, - version *GitVersion, - gitRevArgs ...string, -) (string, error) { - return callGitRevParseWithDir(cmd, version, "", gitRevArgs...) -} - func callGitRevParseWithDir( cmd oscommands.ICmdObjBuilder, version *GitVersion, diff --git a/pkg/commands/git_commands/repo_paths_test.go b/pkg/commands/git_commands/repo_paths_test.go index 97cfc8119..9ee41a3fc 100644 --- a/pkg/commands/git_commands/repo_paths_test.go +++ b/pkg/commands/git_commands/repo_paths_test.go @@ -36,10 +36,12 @@ func TestGetRepoPaths(t *testing.T) { "/path/to/repo/.git", // --git-common-dir "/path/to/repo/.git", + // --is-bare-repository + "false", // --show-superproject-working-tree } runner.ExpectGitArgs( - append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"), + append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"), strings.Join(expectedOutput, "\n"), nil) }, @@ -50,6 +52,38 @@ func TestGetRepoPaths(t *testing.T) { repoPath: "/path/to/repo", repoGitDirPath: "/path/to/repo/.git", repoName: "repo", + isBareRepo: false, + }, + Err: nil, + }, + { + Name: "bare repo", + BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) { + // setup for main worktree + expectedOutput := []string{ + // --show-toplevel + "/path/to/repo", + // --git-dir + "/path/to/bare_repo/bare.git", + // --git-common-dir + "/path/to/bare_repo/bare.git", + // --is-bare-repository + "true", + // --show-superproject-working-tree + } + runner.ExpectGitArgs( + append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"), + strings.Join(expectedOutput, "\n"), + nil) + }, + Path: "/path/to/repo", + Expected: &RepoPaths{ + worktreePath: "/path/to/repo", + worktreeGitDirPath: "/path/to/bare_repo/bare.git", + repoPath: "/path/to/bare_repo", + repoGitDirPath: "/path/to/bare_repo/bare.git", + repoName: "bare_repo", + isBareRepo: true, }, Err: nil, }, @@ -63,11 +97,13 @@ func TestGetRepoPaths(t *testing.T) { "/path/to/repo/.git/modules/submodule1", // --git-common-dir "/path/to/repo/.git/modules/submodule1", + // --is-bare-repository + "false", // --show-superproject-working-tree "/path/to/repo", } runner.ExpectGitArgs( - append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"), + append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"), strings.Join(expectedOutput, "\n"), nil) }, @@ -78,6 +114,7 @@ func TestGetRepoPaths(t *testing.T) { repoPath: "/path/to/repo/submodule1", repoGitDirPath: "/path/to/repo/.git/modules/submodule1", repoName: "submodule1", + isBareRepo: false, }, Err: nil, }, @@ -85,7 +122,7 @@ func TestGetRepoPaths(t *testing.T) { Name: "git rev-parse returns an error", BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) { runner.ExpectGitArgs( - append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"), + append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"), "", errors.New("fatal: invalid gitfile format: /path/to/repo/worktree2/.git")) }, @@ -94,7 +131,7 @@ func TestGetRepoPaths(t *testing.T) { Err: func(getRevParseArgs argFn) error { args := strings.Join(getRevParseArgs(), " ") return errors.New( - fmt.Sprintf("'git %v --show-toplevel --absolute-git-dir --git-common-dir --show-superproject-working-tree' failed: fatal: invalid gitfile format: /path/to/repo/worktree2/.git", args), + fmt.Sprintf("'git %v --show-toplevel --absolute-git-dir --git-common-dir --is-bare-repository --show-superproject-working-tree' failed: fatal: invalid gitfile format: /path/to/repo/worktree2/.git", args), ) }, }, @@ -120,7 +157,7 @@ func TestGetRepoPaths(t *testing.T) { // prepare the filesystem for the scenario s.BeforeFunc(runner, getRevParseArgs) - repoPaths, err := GetRepoPaths(cmd, version) + repoPaths, err := GetRepoPathsForDir("", cmd, version) // check the error and the paths if s.Err != nil { diff --git a/pkg/commands/git_commands/stash.go b/pkg/commands/git_commands/stash.go index 5eeaa6a68..047985e38 100644 --- a |