diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2019-11-04 19:47:25 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2019-11-05 19:22:01 +1100 |
commit | d5e443e8e3609fe38586aed942a3dae3343dbe47 (patch) | |
tree | 6d7465b9abd8df3ae903e6d95898054ac3a6d8b4 /pkg/commands | |
parent | a3c84296bf2fbc8b132d5b2285eedba09813fbee (diff) |
Support building and moving patches
WIP
Diffstat (limited to 'pkg/commands')
-rw-r--r-- | pkg/commands/branch_list_builder.go | 164 | ||||
-rw-r--r-- | pkg/commands/commit_file.go | 31 | ||||
-rw-r--r-- | pkg/commands/commit_list_builder.go | 295 | ||||
-rw-r--r-- | pkg/commands/commit_list_builder_test.go | 314 | ||||
-rw-r--r-- | pkg/commands/git.go | 123 | ||||
-rw-r--r-- | pkg/commands/git_test.go | 13 | ||||
-rw-r--r-- | pkg/commands/patch_manager.go | 194 | ||||
-rw-r--r-- | pkg/commands/patch_modifier.go | 260 | ||||
-rw-r--r-- | pkg/commands/patch_modifier_test.go | 511 | ||||
-rw-r--r-- | pkg/commands/patch_parser.go | 209 | ||||
-rw-r--r-- | pkg/commands/patch_rebases.go | 153 |
11 files changed, 2216 insertions, 51 deletions
diff --git a/pkg/commands/branch_list_builder.go b/pkg/commands/branch_list_builder.go new file mode 100644 index 000000000..d7a232055 --- /dev/null +++ b/pkg/commands/branch_list_builder.go @@ -0,0 +1,164 @@ +package commands + +import ( + "regexp" + "strings" + + "github.com/jesseduffield/lazygit/pkg/utils" + + "github.com/sirupsen/logrus" + + "gopkg.in/src-d/go-git.v4/plumbing" +) + +// 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 + +// BranchListBuilder returns a list of Branch objects for the current repo +type BranchListBuilder struct { + Log *logrus.Entry + GitCommand *GitCommand +} + +// NewBranchListBuilder builds a new branch list builder +func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand) (*BranchListBuilder, error) { + return &BranchListBuilder{ + Log: log, + GitCommand: gitCommand, + }, nil +} + +func (b *BranchListBuilder) obtainCurrentBranch() *Branch { + branchName, err := b.GitCommand.CurrentBranchName() + if err != nil { + panic(err.Error()) + } + + return &Branch{Name: strings.TrimSpace(branchName)} +} + +func (b *BranchListBuilder) obtainReflogBranches() []*Branch { + branches := make([]*Branch, 0) + rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") + if err != nil { + return branches + } + + branchLines := utils.SplitLines(rawString) + for _, line := range branchLines { + timeNumber, timeUnit, branchName := branchInfoFromLine(line) + timeUnit = abbreviatedTimeUnit(timeUnit) + branch := &Branch{Name: branchName, Recency: timeNumber + timeUnit} + branches = append(branches, branch) + } + return uniqueByName(branches) +} + +func (b *BranchListBuilder) obtainSafeBranches() []*Branch { + branches := make([]*Branch, 0) + + bIter, err := b.GitCommand.Repo.Branches() + if err != nil { + panic(err) + } + bIter.ForEach(func(b *plumbing.Reference) error { + name := b.Name().Short() + branches = append(branches, &Branch{Name: name}) + return nil + }) + + return branches +} + +func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*Branch, included bool) []*Branch { + for _, newBranch := range newBranches { + if included == branchIncluded(newBranch.Name, existingBranches) { + finalBranches = append(finalBranches, newBranch) + } + } + return finalBranches +} + +func sanitisedReflogName(reflogBranch *Branch, safeBranches []*Branch) string { + for _, safeBranch := range safeBranches { + if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) { + return safeBranch.Name + } + } + return reflogBranch.Name +} + +// Build the list of branches for the current repo +func (b *BranchListBuilder) Build() []*Branch { + branches := make([]*Branch, 0) + head := b.obtainCurrentBranch() + safeBranches := b.obtainSafeBranches() + + reflogBranches := b.obtainReflogBranches() + for i, reflogBranch := range reflogBranches { + reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches) + } + + branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true) + branches = b.appendNewBranches(branches, safeBranches, branches, false) + + if len(branches) == 0 || branches[0].Name != head.Name { + branches = append([]*Branch{head}, branches...) + } + + branches[0].Recency = " *" + + return branches +} + +func branchIncluded(branchName string, branches []*Branch) bool { + for _, existingBranch := range branches { + if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) { + return true + } + } + return false +} + +func uniqueByName(branches []*Branch) []*Branch { + finalBranches := make([]*Branch, 0) + for _, branch := range branches { + if branchIncluded(branch.Name, finalBranches) { + continue + } + finalBranches = append(finalBranches, branch) + } + return finalBranches +} + +// A line will have the form '10 days ago master' so we need to strip out the +// useful information from that into timeNumber, timeUnit, and branchName +func branchInfoFromLine(line string) (string, string, string) { + r := regexp.MustCompile("\\|.*\\s") + line = r.ReplaceAllString(line, " ") + words := strings.Split(line, " ") + return words[0], words[1], words[len(words)-1] +} + +func abbreviatedTimeUnit(timeUnit string) string { + r := regexp.MustCompile("s$") + timeUnit = r.ReplaceAllString(timeUnit, "") + timeUnitMap := map[string]string{ + "hour": "h", + "minute": "m", + "second": "s", + "week": "w", + "year": "y", + "day": "d", + "month": "m", + } + return timeUnitMap[timeUnit] +} diff --git a/pkg/commands/commit_file.go b/pkg/commands/commit_file.go index 8bc6a11c2..ddd09b23b 100644 --- a/pkg/commands/commit_file.go +++ b/pkg/commands/commit_file.go @@ -1,13 +1,42 @@ package commands +import ( + "github.com/fatih/color" + "github.com/jesseduffield/lazygit/pkg/theme" +) + // CommitFile : A git commit file type CommitFile struct { Sha string Name string DisplayString string + Status int // one of 'WHOLE' 'PART' 'NONE' } +const ( + // UNSELECTED is for when the commit file has not been added to the patch in any way + UNSELECTED = iota + // WHOLE is for when you want to add the whole diff of a file to the patch, + // including e.g. if it was deleted + WHOLE = iota + // PART is for when you're only talking about specific lines that have been modified + PART +) + // GetDisplayStrings is a function. func (f *CommitFile) GetDisplayStrings(isFocused bool) []string { - return []string{f.DisplayString} + yellow := color.New(color.FgYellow) + green := color.New(color.FgGreen) + defaultColor := color.New(theme.DefaultTextColor) + + var colour *color.Color + switch f.Status { + case UNSELECTED: + colour = defaultColor + case WHOLE: + colour = green + case PART: + colour = yellow + } + return []string{colour.Sprint(f.DisplayString)} } diff --git a/pkg/commands/commit_list_builder.go b/pkg/commands/commit_list_builder.go new file mode 100644 index 000000000..aab6de6a3 --- /dev/null +++ b/pkg/commands/commit_list_builder.go @@ -0,0 +1,295 @@ +package commands + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/fatih/color" + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/sirupsen/logrus" +) + +// 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 + +// 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 + +// CommitListBuilder returns a list of Branch objects for the current repo +type CommitListBuilder struct { + Log *logrus.Entry + GitCommand *GitCommand + OSCommand *OSCommand + Tr *i18n.Localizer + CherryPickedCommits []*Commit + DiffEntries []*Commit +} + +// NewCommitListBuilder builds a new commit list builder +func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit, diffEntries []*Commit) (*CommitListBuilder, error) { + return &CommitListBuilder{ + Log: log, + GitCommand: gitCommand, + OSCommand: osCommand, + Tr: tr, + CherryPickedCommits: cherryPickedCommits, + DiffEntries: diffEntries, + }, nil +} + +// GetCommits obtains the commits of the current branch +func (c *CommitListBuilder) GetCommits() ([]*Commit, error) { + commits := []*Commit{} + var rebasingCommits []*Commit + rebaseMode, err := c.GitCommand.RebaseMode() + if err != nil { + return nil, err + } + if rebaseMode != "" { + // here we want to also prepend the commits that we're in the process of rebasing + rebasingCommits, err = c.getRebasingCommits(rebaseMode) + if err != nil { + return nil, err + } + if len(rebasingCommits) > 0 { + commits = append(commits, rebasingCommits...) + } + } + + unpushedCommits := c.getUnpushedCommits() + log := c.getLog() + + // now we can split it up and turn it into commits + for _, line := range utils.SplitLines(log) { + splitLine := strings.Split(line, " ") + sha := splitLine[0] + _, unpushed := unpushedCommits[sha] + status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed] + commits = append(commits, &Commit{ + Sha: sha, + Name: strings.Join(splitLine[1:], " "), + Status: status, + DisplayString: strings.Join(splitLine, " "), + }) + } + if rebaseMode != "" { + currentCommit := commits[len(rebasingCommits)] + blue := color.New(color.FgYellow) + youAreHere := blue.Sprintf("<-- %s ---", c.Tr.SLocalize("YouAreHere")) + currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name) + } + + commits, err = c.setCommitMergedStatuses(commits) + if err != nil { + return nil, err + } + + commits, err = c.setCommitCherryPickStatuses(commits) + if err != nil { + return nil, err + } + + for _, commit := range commits { + for _, entry := range c.DiffEntries { + if entry.Sha == commit.Sha { + commit.Status = "selected" + } + } + } + + return commits, nil +} + +// getRebasingCommits obtains the commits that we're in the process of rebasing +func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*Commit, error) { + switch rebaseMode { + case "normal": + return c.getNormalRebasingCommits() + case "interactive": + return c.getInteractiveRebasingCommits() + default: + return nil, nil + } +} + +func (c *CommitListBuilder) getNormalRebasingCommits() ([]*Commit, error) { + rewrittenCount := 0 + bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-apply/rewritten", c.GitCommand.DotGitDir)) + 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 := []*Commit{} + err = filepath.Walk(fmt.Sprintf("%s/rebase-apply", c.GitCommand.DotGitDir), 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 := ioutil.ReadFile(path) + if err != nil { + return err + } + content := string(bytesContent) + commit, err := c.commitFromPatch(content) + if err != nil { + return err + } + commits = append([]*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 (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) { + bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.GitCommand.DotGitDir)) + if err != nil { + c.Log.Info(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 := []*Commit{} + lines := strings.Split(string(bytesContent), "\n") + for _, line := range lines { + if line == "" || line == "noop" { + return commits, nil + } + splitLine := strings.Split(line, " ") + commits = append([]*Commit{{ + Sha: splitLine[1][0:7], + Name: strings.Join(splitLine[2:], " "), + Status: "rebasing", + Action: splitLine[0], + }}, commits...) + } + + return nil, 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 (c *CommitListBuilder) commitFromPatch(content string) (*Commit, error) { + lines := strings.Split(content, "\n") + sha := strings.Split(lines[0], " ")[1][0:7] + name := strings.TrimPrefix(lines[3], "Subject: ") + return &Commit{ + Sha: sha, + Name: name, + Status: "rebasing", + }, nil +} + +func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) { + ancestor, err := c.getMergeBase() + 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 (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*Commit) ([]*Commit, error) { + for _, commit := range commits { + for _, cherryPickedCommit := range c.CherryPickedCommits { + if commit.Sha == cherryPickedCommit.Sha { + commit.Copied = true + } + } + } + return commits, nil +} + +func (c *CommitListBuilder) getMergeBase() (string, error) { + currentBranch, err := c.GitCommand.CurrentBranchName() + 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, _ := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch)) + return output, nil +} + +// getUnpushedCommits Returns the sha's of the commits that have not yet been pushed +// to the remote branch of the current branch, a map is returned to ease look up +func (c *CommitListBuilder) getUnpushedCommits() map[string]bool { + pushables := map[string]bool{} + o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit") + if err != nil { + return pushables + } + for _, p := range utils.SplitLines(o) { + pushables[p] = true + } + + return pushables +} + +// getLog gets the git log (currently limited to 30 commits for performance +// until we work out lazy loading +func (c *CommitListBuilder) getLog() string { + // currently limiting to 30 for performance reasons + // TODO: add lazyloading when you scroll down + result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30") + if err != nil { + // assume if there is an error there are no commits yet for this branch + return "" + } + + return result +} diff --git a/pkg/commands/commit_list_builder_test.go b/pkg/commands/commit_list_builder_test.go new file mode 100644 index 000000000..cdd360ce8 --- /dev/null +++ b/pkg/commands/commit_list_builder_test.go @@ -0,0 +1,314 @@ +package commands + +import ( + "os/exec" + "testing" + + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/stretchr/testify/assert" +) + +// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing +func NewDummyCommitListBuilder() *CommitListBuilder { + osCommand := NewDummyOSCommand() + + return &CommitListBuilder{ + Log: NewDummyLog(), + GitCommand: NewDummyGitCommandWithOSCommand(osCommand), + OSCommand: osCommand, + Tr: i18n.NewLocalizer(NewDummyLog()), + CherryPickedCommits: []*Commit{}, + } +} + +// TestCommitListBuilderGetUnpushedCommits is a function. +func TestCommitListBuilderGetUnpushedCommits(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(map[string]bool) + } + + scenarios := []scenario{ + { + "Can't retrieve pushable commits", + func(string, ...string) *exec.Cmd { + return exec.Command("test") + }, + func(pushables map[string]bool) { + assert.EqualValues(t, map[string]bool{}, pushables) + }, + }, + { + "Retrieve pushable commits", + func(cmd string, args ...string) *exec.Cmd { + return exec.Command("echo", "8a2bb0e\n78976bc") + }, + func(pushables map[string]bool) { + assert.Len(t, pushables, 2) + assert.EqualValues(t, map[string]bool{"8a2bb0e": true, "78976bc": true}, pushables) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + c := NewDummyCommitListBuilder() + c.OSCommand.SetCommand(s.command) + s.test(c.getUnpushedCommits()) + }) + } +} + +// TestCommitListBuilderGetMergeBase is a function. +func TestCommitListBuilderGetMergeBase(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(string, error) + } + + scenarios := []scenario{ + { + "swallows an error if the call to merge-base returns an error", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + + switch args[0] { + case "symbolic-ref": + assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args) + return exec.Command("echo", "master") + case "merge-base": + assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args) + return exec.Command("test") + } + return nil + }, + func(output string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "", output) + }, + }, + { + "returns the commit when master", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + + switch args[0] { + case "symbolic-ref": + assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args) + return exec.Command("echo", "master") + case "merge-base": + assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args) + return exec.Command("echo", "blah") + } + return nil + }, + func(output string, err error) { + assert.NoError(t, err) + assert.Equal(t, "blah\n", output) + }, + }, + { + "checks against develop when a feature branch", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + + switch args[0] { + case "symbolic-ref": + assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args) + return exec.Command("echo", "feature/test") + case "merge-base": + assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args) + return exec.Command("echo", "blah") + } + return nil + }, + func(output string, err error) { + assert.NoError(t, err) + assert.Equal(t, "blah\n", output) + }, + }, + { + "bubbles up error if there is one", + func(cmd string, args ...string) *exec.Cmd { + return exec.Command("test") + }, + func(output string, err error) { + assert.Error(t, err) + assert.Equal(t, "", output) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + c := NewDummyCommitListBuilder() + c.OSCommand.SetCommand(s.command) + s.test(c.getMergeBase()) + }) + } +} + +// TestCommitListBuilderGetLog is a function. +func TestCommitListBuilderGetLog(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(string) + } + + scenarios := []scenario{ + { + "Retrieves logs", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args) + + return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line") + }, + func(output string) { + assert.EqualValues(t, "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line\n", output) + }, + }, + { + "An error occurred when retrieving logs", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args) + return exec.Command("test") + }, + func(output string) { + assert.Empty(t, output) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + c := NewDummyCommitListBuilder() + c.OSCommand.SetCommand(s.command) + s.test(c.getLog()) + }) + } +} + +// TestCommitListBuilderGetCommits is a function. +func TestCommitListBuilderGetCommits(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func([]*Commit, error) + } + + scenarios := []scenario{ + { + "No data found", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + + switch args[0] { + case "rev-list": + assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args) + return exec.Command("echo") + case "log": + assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args) + return exec.Command("echo") + case "merge-base": + assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args) + return exec.Command("test") + case "symbolic-ref": + assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args) + return exec.Command("echo", "master") + } + + return nil + }, + func(commits []*Commit, err error) { + assert.NoError(t, err) + assert.Len(t, commits, 0) + }, + }, + { + "GetCommits returns 2 commits, 1 unpushed, the other merged", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + + switch args[0] { + case "rev-list": + assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args) + return exec.Command("echo", "8a2bb0e") + case "log": + assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args) + return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2") + case "merge-base": + assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args) + return exec.Command("echo", "78976bc") + case "symbolic-ref": + assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args) + return exec.Command("echo", "master") + } + + return nil + }, + func(commits []*Commit, err error) { + assert.NoError(t, err) + assert.Len(t, commits, 2) + assert.EqualValues(t, []*Commit{ + { + Sha: "8a2bb0e", + Name: "commit 1", + Status: "unpushed", + DisplayString: "8a2bb0e commit 1", + }, + { + Sha: "78976bc", + Name: "commit 2", + Status: "merged", + DisplayString: "78976bc commit 2", + }, + }, commits) + }, + }, + { + "GetCommits bubbles up an error from setCommitMergedStatuses", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + + switch args[0] { + case "rev-list": + assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args) + return exec.Command("echo", "8a2bb0e") + case "log": + assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args) + return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2") + case "merge-base": + assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args) + return exec.Command("echo", "78976bc") + case "symbolic-ref": + assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args) + // here's where we are returning the error + return exec.Command("test") + case "rev-parse": + assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args) + // here too + return exec.Command("test") + } + + return nil + }, + func(commits []*Commit, err error) { + assert.Error(t, err) + assert.Len(t, commits, 0) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + c := NewDummyCommitListBuilder() + c.OSCommand.SetCommand(s.command) + s.test(c.GetCommits()) + }) + } +} diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 6e86fe0b5..815c84423 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -63,16 +63,17 @@ func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repositor // GitCommand is our main git interface type GitCommand struct { - Log *logrus.Entry - OSCommand *OSCommand - Worktree *gogit.Worktree - Repo *gogit.Repository - Tr *i18n.Localizer - Config config.AppConfigurer - getGlobalGitConfig func(string) (string, error) - getLocalGitConfig func(string) (string, error) - removeFile func(string) error - DotGitDir string + Log *logrus.Entry + OSCommand *OSCommand + Worktree *gogit.Worktree + Repo *gogit.Repository + Tr *i18n.Localizer + Config config.AppConfigurer + getGlobalGitConfig func(string) (string, error) + getLocalGitConfig func(string) (string, error) + removeFile func(string) error + DotGitDir string + onSuccessfulContinue func() error } // NewGitCommand it runs git commands @@ -376,7 +377,7 @@ func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) { // AmendHead amends HEAD with whatever is staged in your working tree func (c *GitCommand) AmendHead() (*exec.Cmd, error) { - command := "git commit --amend --no-edit" + command := "git commit --amend --no-edit --allow-empty" if c.usingGpg() { return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil } @@ -530,7 +531,7 @@ func (c *GitCommand) Ignore(filename string) error { // Show shows the diff of a commit func (c *GitCommand) Show(sha string) (string, error) { - show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha)) + show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color --no-renames %s", sha)) if err != nil { return "", err } @@ -605,11 +606,11 @@ func (c *GitCommand) Diff(file *File, plain bool, cached bool) string { return s } -func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool) (string, error) { +func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool, extraFlags string) error { filename, err := c.OSCommand.CreateTempFile("patch", patch) if err != nil { c.Log.Error(err) - return "", err + return err } defer func() { _ = c.OSCommand.Remove(filename) }() @@ -624,7 +625,7 @@ func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool) (string cachedFlag = "--cached" } - return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply %s %s %s", cachedFlag, reverseFlag, c.OSCommand.Quote(filename))) + return c.OSCommand.RunCommand(fmt.Sprintf("git apply %s %s %s %s", cachedFlag, reverseFlag, extraFlags, c.OSCommand.Quote(filename))) } func (c *GitCommand) FastForward(branchName string) error { @@ -645,13 +646,29 @@ func (c *GitCommand) RunSkipEditorCommand(command string) error { // GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue" // By default we skip the editor in the case where a commit will be made func (c *GitCommand) GenericMerge(commandType string, command string) error { - return c.RunSkipEditorCommand( + err := c.RunSkipEditorCommand( fmt.Sprintf( "git %s --%s", commandType, command, ), ) + if err != nil { + return err + } + + // sometimes we need to do a sequence of things in a rebase but the user needs to + // fix merge conflicts along the way. When this happens we queue up the next step + // so that after the next successful rebase continue w |