summaryrefslogtreecommitdiffstats
path: root/pkg/commands
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2019-11-04 19:47:25 +1100
committerJesse Duffield <jessedduffield@gmail.com>2019-11-05 19:22:01 +1100
commitd5e443e8e3609fe38586aed942a3dae3343dbe47 (patch)
tree6d7465b9abd8df3ae903e6d95898054ac3a6d8b4 /pkg/commands
parenta3c84296bf2fbc8b132d5b2285eedba09813fbee (diff)
Support building and moving patches
WIP
Diffstat (limited to 'pkg/commands')
-rw-r--r--pkg/commands/branch_list_builder.go164
-rw-r--r--pkg/commands/commit_file.go31
-rw-r--r--pkg/commands/commit_list_builder.go295
-rw-r--r--pkg/commands/commit_list_builder_test.go314
-rw-r--r--pkg/commands/git.go123
-rw-r--r--pkg/commands/git_test.go13
-rw-r--r--pkg/commands/patch_manager.go194
-rw-r--r--pkg/commands/patch_modifier.go260
-rw-r--r--pkg/commands/patch_modifier_test.go511
-rw-r--r--pkg/commands/patch_parser.go209
-rw-r--r--pkg/commands/patch_rebases.go153
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