diff options
Diffstat (limited to 'pkg/commands/git_commands')
18 files changed, 1962 insertions, 11 deletions
diff --git a/pkg/commands/git_commands/branch_loader.go b/pkg/commands/git_commands/branch_loader.go new file mode 100644 index 000000000..08811374d --- /dev/null +++ b/pkg/commands/git_commands/branch_loader.go @@ -0,0 +1,204 @@ +package git_commands + +import ( + "regexp" + "strings" + + "github.com/jesseduffield/generics/set" + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/go-git/v5/config" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/common" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +// context: +// we want to only show 'safe' branches (ones that haven't e.g. been deleted) +// which `git branch -a` gives us, but we also want the recency data that +// git reflog gives us. +// So we get the HEAD, then append get the reflog branches that intersect with +// our safe branches, then add the remaining safe branches, ensuring uniqueness +// along the way + +// if we find out we need to use one of these functions in the git.go file, we +// can just pull them out of here and put them there and then call them from in here + +type BranchLoaderConfigCommands interface { + Branches() (map[string]*config.Branch, error) +} + +// BranchLoader returns a list of Branch objects for the current repo +type BranchLoader struct { + *common.Common + getRawBranches func() (string, error) + getCurrentBranchName func() (string, string, error) + config BranchLoaderConfigCommands +} + +func NewBranchLoader( + cmn *common.Common, + getRawBranches func() (string, error), + getCurrentBranchName func() (string, string, error), + config BranchLoaderConfigCommands, +) *BranchLoader { + return &BranchLoader{ + Common: cmn, + getRawBranches: getRawBranches, + getCurrentBranchName: getCurrentBranchName, + config: config, + } +} + +// Load the list of branches for the current repo +func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) { + branches := self.obtainBranches() + + reflogBranches := self.obtainReflogBranches(reflogCommits) + + // loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches + branchesWithRecency := make([]*models.Branch, 0) +outer: + for _, reflogBranch := range reflogBranches { + for j, branch := range branches { + if branch.Head { + continue + } + if strings.EqualFold(reflogBranch.Name, branch.Name) { + branch.Recency = reflogBranch.Recency + branchesWithRecency = append(branchesWithRecency, branch) + branches = slices.Remove(branches, j) + continue outer + } + } + } + + branches = slices.Prepend(branches, branchesWithRecency...) + + foundHead := false + for i, branch := range branches { + if branch.Head { + foundHead = true + branch.Recency = " *" + branches = slices.Move(branches, i, 0) + break + } + } + if !foundHead { + currentBranchName, currentBranchDisplayName, err := self.getCurrentBranchName() + if err != nil { + return nil, err + } + branches = slices.Prepend(branches, &models.Branch{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}) + } + + configBranches, err := self.config.Branches() + if err != nil { + return nil, err + } + + for _, branch := range branches { + match := configBranches[branch.Name] + if match != nil { + branch.UpstreamRemote = match.Remote + branch.UpstreamBranch = match.Merge.Short() + } + } + + return branches, nil +} + +func (self *BranchLoader) obtainBranches() []*models.Branch { + output, err := self.getRawBranches() + if err != nil { + panic(err) + } + + trimmedOutput := strings.TrimSpace(output) + outputLines := strings.Split(trimmedOutput, "\n") + + return slices.FilterMap(outputLines, func(line string) (*models.Branch, bool) { + if line == "" { + return nil, false + } + + split := strings.Split(line, "\x00") + if len(split) != 4 { + // Ignore line if it isn't separated into 4 parts + // This is probably a warning message, for more info see: + // https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439 + return nil, false + } + + return obtainBranch(split), true + }) +} + +// Obtain branch information from parsed line output of getRawBranches() +// split contains the '|' separated tokens in the line of output +func obtainBranch(split []string) *models.Branch { + name := strings.TrimPrefix(split[1], "heads/") + branch := &models.Branch{ + Name: name, + Pullables: "?", + Pushables: "?", + Head: split[0] == "*", + } + + upstreamName := split[2] + if upstreamName == "" { + // if we're here then it means we do not have a local version of the remote. + // The branch might still be tracking a remote though, we just don't know + // how many commits ahead/behind it is + return branch + } + + track := split[3] + if track == "[gone]" { + branch.UpstreamGone = true + } else { + re := regexp.MustCompile(`ahead (\d+)`) + match := re.FindStringSubmatch(track) + if len(match) > 1 { + branch.Pushables = match[1] + } else { + branch.Pushables = "0" + } + + re = regexp.MustCompile(`behind (\d+)`) + match = re.FindStringSubmatch(track) + if len(match) > 1 { + branch.Pullables = match[1] + } else { + branch.Pullables = "0" + } + } + + return branch +} + +// TODO: only look at the new reflog commits, and otherwise store the recencies in +// int form against the branch to recalculate the time ago +func (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch { + foundBranches := set.New[string]() + re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`) + reflogBranches := make([]*models.Branch, 0, len(reflogCommits)) + + for _, commit := range reflogCommits { + match := re.FindStringSubmatch(commit.Name) + if len(match) != 3 { + continue + } + + recency := utils.UnixToTimeAgo(commit.UnixTimestamp) + for _, branchName := range match[1:] { + if !foundBranches.Includes(branchName) { + foundBranches.Add(branchName) + reflogBranches = append(reflogBranches, &models.Branch{ + Recency: recency, + Name: branchName, + }) + } + } + } + return reflogBranches +} diff --git a/pkg/commands/git_commands/branch_loader_test.go b/pkg/commands/git_commands/branch_loader_test.go new file mode 100644 index 000000000..c147c1484 --- /dev/null +++ b/pkg/commands/git_commands/branch_loader_test.go @@ -0,0 +1,52 @@ +package git_commands + +// "*|feat/detect-purge|origin/feat/detect-purge|[ahead 1]" +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/stretchr/testify/assert" +) + +func TestObtainBanch(t *testing.T) { + type scenario struct { + testName string + input []string + expectedBranch *models.Branch + } + + scenarios := []scenario{ + { + testName: "TrimHeads", + input: []string{"", "heads/a_branch", "", ""}, + expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false}, + }, + { + testName: "NoUpstream", + input: []string{"", "a_branch", "", ""}, + expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false}, + }, + { + testName: "IsHead", + input: []string{"*", "a_branch", "", ""}, + expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: true}, + }, + { + testName: "IsBehindAndAhead", + input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]"}, + expectedBranch: &models.Branch{Name: "a_branch", Pushables: "3", Pullables: "2", Head: false}, + }, + { + testName: "RemoteBranchIsGone", + input: []string{"", "a_branch", "a_remote/a_branch", "[gone]"}, + expectedBranch: &models.Branch{Name: "a_branch", UpstreamGone: true, Pushables: "?", Pullables: "?", Head: false}, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + branch := obtainBranch(s.input) + assert.EqualValues(t, s.expectedBranch, branch) + }) + } +} diff --git a/pkg/commands/git_commands/commit_file_loader.go b/pkg/commands/git_commands/commit_file_loader.go new file mode 100644 index 000000000..0b606ae86 --- /dev/null +++ b/pkg/commands/git_commands/commit_file_loader.go @@ -0,0 +1,56 @@ +package git_commands + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/generics/slices" + "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 CommitFileLoader struct { + *common.Common + cmd oscommands.ICmdObjBuilder +} + +func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *CommitFileLoader { + return &CommitFileLoader{ + Common: common, + cmd: cmd, + } +} + +// GetFilesInDiff get the specified commit files +func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) { + reverseFlag := "" + if reverse { + reverseFlag = " -R " + } + + filenames, err := self.cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).DontLog().RunWithOutput() + if err != nil { + return nil, err + } + + return getCommitFilesFromFilenames(filenames), nil +} + +// filenames string is something like "MM\x00file1\x00MU\x00file2\x00AA\x00file3\x00" +// so we need to split it by the null character and then map each status-name pair to a commit file +func getCommitFilesFromFilenames(filenames string) []*models.CommitFile { + lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00") + if len(lines) == 1 { + return []*models.CommitFile{} + } + + // typical result looks like 'A my_file' meaning my_file was added + return slices.Map(lo.Chunk(lines, 2), func(chunk []string) *models.CommitFile { + return &models.CommitFile{ + ChangeStatus: chunk[0], + Name: chunk[1], + } + }) +} diff --git a/pkg/commands/git_commands/commit_file_loader_test.go b/pkg/commands/git_commands/commit_file_loader_test.go new file mode 100644 index 000000000..8928f5204 --- /dev/null +++ b/pkg/commands/git_commands/commit_file_loader_test.go @@ -0,0 +1,71 @@ +package git_commands + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/stretchr/testify/assert" +) + +func TestGetCommitFilesFromFilenames(t *testing.T) { + tests := []struct { + testName string + input string + output []*models.CommitFile + }{ + { + testName: "no files", + input: "", + output: []*models.CommitFile{}, + }, + { + testName: "one file", + input: "MM\x00Myfile\x00", + output: []*models.CommitFile{ + { + Name: "Myfile", + ChangeStatus: "MM", + }, + }, + }, + { + testName: "two files", + input: "MM\x00Myfile\x00M \x00MyOtherFile\x00", + output: []*models.CommitFile{ + { + Name: "Myfile", + ChangeStatus: "MM", + }, + { + Name: "MyOtherFile", + ChangeStatus: "M ", + }, + }, + }, + { + testName: "three files", + input: "MM\x00Myfile\x00M \x00MyOtherFile\x00 M\x00YetAnother\x00", + output: []*models.CommitFile{ + { + Name: "Myfile", + ChangeStatus: "MM", + }, + { + Name: "MyOtherFile", + ChangeStatus: "M ", + }, + { + Name: "YetAnother", + ChangeStatus: " M", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + result := getCommitFilesFromFilenames(test.input) + assert.Equal(t, test.output, result) + }) + } +} diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go new file mode 100644 index 000000000..21395fad4 --- /dev/null +++ b/pkg/commands/git_commands/commit_loader.go @@ -0,0 +1,459 @@ +package git_commands + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/fsmiamoto/git-todo-parser/todo" + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/common" + "github.com/jesseduffield/lazygit/pkg/gui/style" +) + +// context: +// here we get the commits from git log but format them to show whether they're +// unpushed/pushed/merged into the base branch or not, or if they're yet to +// be processed as part of a rebase (these won't appear in git log but we +// grab them from the rebase-related files in the .git directory to show them + +// CommitLoader returns a list of Commit objects for the current repo +type CommitLoader struct { + *common.Common + cmd oscommands.ICmdObjBuilder + + getCurrentBranchName func() (string, string, error) + getRebaseMode func() (enums.RebaseMode, error) + readFile func(filename string) ([]byte, error) + walkFiles func(root string, fn filepath.WalkFunc) error + dotGitDir string +} + +// making our dependencies explicit for the sake of easier testing +func NewCommitLoader( + cmn *common.Common, + cmd oscommands.ICmdObjBuilder, + dotGitDir string, + getCurrentBranchName func() (string, string, error), + getRebaseMode func() (enums.RebaseMode, error), +) *CommitLoader { + return &CommitLoader{ + Common: cmn, + cmd: cmd, + getCurrentBranchName: getCurrentBranchName, + getRebaseMode: getRebaseMode, + readFile: os.ReadFile, + walkFiles: filepath.Walk, + dotGitDir: dotGitDir, + } +} + +type GetCommitsOptions struct { + Limit bool + FilterPath string + IncludeRebaseCommits bool + RefName string // e.g. "HEAD" or "my_branch" + // determines if we show the whole git graph i.e. pass the '--all' flag + All bool +} + +// GetCommits obtains the commits of the current branch +func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) { + commits := []*models.Commit{} + var rebasingCommits []*models.Commit + rebaseMode, err := self.getRebaseMode() + if err != nil { + return nil, err + } + + if opts.IncludeRebaseCommits && opts.FilterPath == "" { + var err error + rebasingCommits, err = self.MergeRebasingCommits(commits) + if err != nil { + return nil, err + } + commits = append(commits, rebasingCommits...) + } + + passedFirstPushedCommit := false + firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName) + if err != nil { + // must have no upstream branch so we'll consider everything as pushed + passedFirstPushedCommit = true + } + + err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) { + commit := self.extractCommitFromLine(line) + if commit.Sha == firstPushedCommit { + passedFirstPushedCommit = true + } + commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit] + commits = append(commits, commit) + return false, nil + }) + if err != nil { + return nil, err + } + + if len(commits) == 0 { + return commits, nil + } + + if rebaseMode != enums.REBASE_MODE_NONE { + currentCommit := commits[len(rebasingCommits)] + youAreHere := style.FgYellow.Sprintf("<-- %s ---", self.Tr.YouAreHere) + currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name) + } + + commits, err = self.setCommitMergedStatuses(opts.RefName, commits) + if err != nil { + return nil, err + } + + return commits, nil +} + +func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) { + // chances are we have as many commits as last time so we'll set the capacity to be the old length + result := make([]*models.Commit, 0, len(commits)) + for i, commit := range commits { + if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones + result = append(result, commits[i:]...) + break + } + } + + rebaseMode, err := self.getRebaseMode() + if err != nil { + return nil, err + } + + if rebaseMode == enums.REBASE_MODE_NONE { + // not in rebase mode so return original commits + return result, nil + } + + rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode) + if err != nil { + return nil, err + } + if len(rebasingCommits) > 0 { + result = append(rebasingCommits, result...) + } + + return result, nil +} + +// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present +// then puts them into a commit object +// example input: +// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag +func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit { + split := strings.SplitN(line, "\x00", 7) + + sha := split[0] + unixTimestamp := split[1] + authorName := split[2] + authorEmail := split[3] + extraInfo := strings.TrimSpace(split[4]) + parentHashes := split[5] + message := split[6] + + tags := []string{} + + if extraInfo != "" { + re := regexp.MustCompile(`tag: ([^,\)]+)`) + tagMatch := re.FindStringSubmatch(extraInfo) + if len(tagMatch) > 1 { + tags = append(tags, tagMatch[1]) + } + } + + unitTimestampInt, _ := strconv.Atoi(unixTimestamp) + + parents := []string{} + if len(parentHashes) > 0 { + parents = strings.Split(parentHashes, " ") + } + + return &models.Commit{ + Sha: sha, + Name: message, + Tags: tags, + ExtraInfo: extraInfo, + UnixTimestamp: int64(unitTimestampInt), + AuthorName: authorName, + AuthorEmail: authorEmail, + Parents: parents, + } +} + +func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) { + commits, err := self.getRebasingCommits(rebaseMode) + if err != nil { + return nil, err + } + + if len(commits) == 0 { + return nil, nil + } + + commitShas := slices.Map(commits, func(commit *models.Commit) string { + return commit.Sha + }) + + // note that we're not filtering these as we do non-rebasing commits just because + // I suspect that will cause some damage + cmdObj := self.cmd.New( + fmt.Sprintf( + "git -c log.showSignature=false show %s --no-patch --oneline %s --abbrev=%d", + strings.Join(commitShas, " "), + prettyFormat, + 20, + ), + ).DontLog() + + hydratedCommits := make([]*models.Commit, 0, len(commits)) + i := 0 + err = cmdObj.RunAndProcessLines(func(line string) (bool, error) { + commit := self.extractCommitFromLine(line) + matchingCommit := commits[i] + commit.Action = matchingCommit.Action + commit.Status = matchingCommit.Status + hydratedCommits = append(hydratedCommits, commit) + i++ + return false, nil + }) + if err != nil { + return nil, err + } + return hydratedCommits, nil +} + +// getRebasingCommits obtains the commits that we're in the process of rebasing +func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) { + switch rebaseMode { + case enums.REBASE_MODE_MERGING: + return self.getNormalRebasingCommits() + case enums.REBASE_MODE_INTERACTIVE: + return self.getInteractiveRebasingCommits() + default: + return nil, nil + } +} + +func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) { + rewrittenCount := 0 + bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten")) + if err == nil { + content := string(bytesContent) + rewrittenCount = len(strings.Split(content, "\n")) + } + + // we know we're rebasing, so lets get all the files whose names have numbers + commits := []*models.Commit{} + err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error { + if rewrittenCount > 0 { + rewrittenCount-- + return nil + } + if err != nil { + return err + } + re := regexp.MustCompile(`^\d+$`) + if !re.MatchString(f.Name()) { + return nil + } + bytesContent, err := self.readFile(path) + if err != nil { + return err + } + content := string(bytesContent) + commit := self.commitFromPatch(content) + commits = append([]*models.Commit{commit}, commits...) + return nil + }) + if err != nil { + return nil, err + } + + return commits, nil +} + +// git-rebase-todo example: +// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae +// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931 + +// git-rebase-todo.backup example: +// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master +// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master +// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master + +// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files +// and extracts out the sha and names of commits that we still have to go +// in the rebase: +func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) { + bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")) + if err != nil { + self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error())) + // we assume an error means the file doesn't exist so we just return + return nil, nil + } + + commits := []*models.Commit{} + + todos, err := todo.Parse(bytes.NewBuffer(bytesContent)) + if err != nil { + self.Log.Error(fmt.Sprintf("error occurred while parsing git-rebase-todo file: %s", err.Error())) + return nil, nil + } + + for _, t := range todos { + if t.Commit == "" { + // Command does not have a commit associated, skip + continue + } + commits = slices.Prepend(commits, &models.Commit{ + Sha: t.Commit, + Name: t.Msg, + Status: "rebasing", + Action: t.Command.String(), + }) + } + + return commits, nil +} + +// assuming the file starts like this: +// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001 +// From: Lazygit Tester <test@example.com> +// Date: Wed, 5 Dec 2018 21:03:23 +1100 +// Subject: second commit on master +func (self *CommitLoader) commitFromPatch(content string) *models.Commit { + lines := strings.Split(content, "\n") + sha := strings.Split(lines[0], " ")[1] + name := strings.TrimPrefix(lines[3], "Subject: ") + return &models.Commit{ + Sha: sha, + Name: name, + Status: "rebasing", + } +} + +func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) { + ancestor, err := self.getMergeBase(refName) + if err != nil { + return nil, err + } + if ancestor == "" { + return commits, nil + } + passedAncestor := false + for i, commit := range commits { + if strings.HasPrefix(ancestor, commit.Sha) { + passedAncestor = true + } + if commit.Status != "pushed" { + continue + } + if passedAncestor { + commits[i].Status = "merged" + } + } + return commits, nil +} + +func (self *CommitLoader) getMergeBase(refName string) (string, error) { + currentBranch, _, err := self.getCurrentBranchName() + if err != nil { + return "", err + } + + baseBranch := "master" + if strings.HasPrefix(currentBranch, "feature/") { + baseBranch = "develop" + } + + // swallowing error because it's not a big deal; probably because there are no commits yet + output, _ := self.cmd.New(fmt.Sprintf("git merge-base %s %s", self.cmd.Quote(refName), self.cmd.Quote(baseBranch))).DontLog().RunWithOutput() + return ignoringWarnings(output), nil +} + +func ignoringWarnings(commandOutput string) string { + trimmedOutput := strings.TrimSpace(commandOutput) + split := strings.Split(trimmedOutput, "\n") + // need to get last line in case the first line is a warning about how the error is ambiguous. + // At some point we should find a way to make it unambiguous + lastLine := split[len(split)-1] + + return lastLine +} + +// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream. +// all commits above this are deemed unpushed and marked as such. +func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) { + output, err := self.cmd. + New( + fmt.Sprintf("git merge-base %s %s@{u}", self.cmd.Quote(refName), self.cmd.Quote(refName)), + ). + DontLog(). + RunWithOutput() + if err != nil { + return "", err + } + + return ignoringWarnings(output), nil +} + +// getLog gets the git log. +func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj { + limitFlag := "" + if opts.Limit { + limitFlag = " -300" + } + + filterFlag := "" + if opts.FilterPath != "" { + filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath)) + } + + config := self.UserConfig.Git.Log + + orderFlag := "--" + config.Order + allFlag := "" + if opts.All { + allFlag = " --all" + } + + return self.cmd.New( + fmt.Sprintf( + "git -c log.showSignature=false log %s %s %s --oneline %s%s --abbrev=%d%s", + self.cmd.Quote(opts.RefName), + orderFlag, + allFlag, + prettyFormat, + limitFlag, + 40, + filterFlag, + ), + ).DontLog() +} + +var prettyFormat = fmt.Sprintf( + "--pretty=format:\"%%H%s%%at%s%%aN%s%%ae%s%%d%s%%p%s%%s\"", + NULL_CODE, + NULL_CODE, + NULL_CODE, + NULL_CODE, + NULL_CODE, + NULL_CODE, +) + +const NULL_CODE = "%x00" diff --git a/pkg/commands/git_commands/commit_loader_test.go b/pkg/commands/git_commands/commit_loader_test.go new file mode 100644 index 000000000..7d45101ff --- /dev/null +++ b/pkg/commands/git_commands/commit_loader_test.go @@ -0,0 +1,206 @@ +package git_commands + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/stretchr/testify/assert" +) + +var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode +b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging +e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com||d8084cd558925eb7c9c3|refactor +d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP +65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP +26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP +3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP +053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1) + +func TestGetCommits(t *testing.T) { + type scenario struct { + testName string + runner *oscommands.FakeCmdObjRunner + expectedCommits []*models.Commit + expectedError error + rebaseMode enums.RebaseMode + cur |