diff --git a/pkg/commands/commit.go b/pkg/commands/commit.go index 4c304271d..6ee972f9f 100644 --- a/pkg/commands/commit.go +++ b/pkg/commands/commit.go @@ -8,23 +8,30 @@ import ( type Commit struct { Sha string Name string - Pushed bool - Merged bool + Status string // one of "unpushed", "pushed", "merged", or "rebasing" DisplayString string } // GetDisplayStrings is a function. func (c *Commit) GetDisplayStrings() []string { red := color.New(color.FgRed) - yellow := color.New(color.FgGreen) - green := color.New(color.FgYellow) + yellow := color.New(color.FgYellow) + green := color.New(color.FgGreen) white := color.New(color.FgWhite) + blue := color.New(color.FgBlue) - shaColor := yellow - if c.Pushed { + var shaColor *color.Color + switch c.Status { + case "unpushed": shaColor = red - } else if !c.Merged { + case "pushed": + shaColor = yellow + case "merged": shaColor = green + case "rebasing": + shaColor = blue + default: + shaColor = white } return []string{shaColor.Sprint(c.Sha), white.Sprint(c.Name)} diff --git a/pkg/commands/git.go b/pkg/commands/git.go index bbd388303..4fed44d2a 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -3,10 +3,14 @@ package commands import ( "errors" "fmt" + "io/ioutil" "os" "os/exec" + "path/filepath" + "regexp" "strings" + "github.com/fatih/color" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" @@ -149,7 +153,7 @@ func (c *GitCommand) GetStatusFiles() []*File { HasUnstagedChanges: unstagedChange != " ", Tracked: !untracked, Deleted: unstagedChange == "D" || stagedChange == "D", - HasMergeConflicts: change == "UU", + HasMergeConflicts: change == "UU" || change == "AA", Type: c.OSCommand.FileType(filename), } files = append(files, file) @@ -240,9 +244,9 @@ func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) { return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) } -// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed +// 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 *GitCommand) GetCommitsToPush() map[string]bool { +func (c *GitCommand) GetUnpushedCommits() map[string]bool { pushables := map[string]bool{} o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit") if err != nil { @@ -273,6 +277,10 @@ func (c *GitCommand) ContinueRebaseBranch() error { return c.OSCommand.RunCommand("git rebase --continue") } +func (c *GitCommand) SkipRebaseBranch() error { + return c.OSCommand.RunCommand("git rebase --skip") +} + func (c *GitCommand) AbortRebaseBranch() error { return c.OSCommand.RunCommand("git rebase --abort") } @@ -455,11 +463,11 @@ func (c *GitCommand) IsInMergeState() (bool, error) { } func (c *GitCommand) IsInRebaseState() (bool, error) { - output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all") + exists, err := c.OSCommand.FileExists(".git/rebase-apply") if err != nil { return false, err } - return strings.Contains(output, "rebase in progress"), nil + return exists, nil } // RemoveFile directly @@ -527,24 +535,105 @@ func (c *GitCommand) getMergeBase() (string, error) { return output, nil } +// GetRebasingCommits obtains the commits that we're in the process of rebasing +func (c *GitCommand) GetRebasingCommits() ([]*Commit, error) { + rebasing, err := c.IsInRebaseState() + if err != nil { + return nil, err + } + if !rebasing { + return nil, nil + } + + rewrittenCount := 0 + bytesContent, err := ioutil.ReadFile(".git/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 := []*Commit{} + err = filepath.Walk(".git/rebase-apply", func(path string, f os.FileInfo, err error) error { + if rewrittenCount > 0 { + rewrittenCount -= 1 + 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 +} + +// assuming the file starts like this: +// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001 +// From: Lazygit Tester +// Date: Wed, 5 Dec 2018 21:03:23 +1100 +// Subject: second commit on master +func (c *GitCommand) 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 +} + // GetCommits obtains the commits of the current branch func (c *GitCommand) GetCommits() ([]*Commit, error) { - pushables := c.GetCommitsToPush() + commits := []*Commit{} + // here we want to also prepend the commits that we're in the process of rebasing + rebasingCommits, err := c.GetRebasingCommits() + if err != nil { + return nil, err + } + if len(rebasingCommits) > 0 { + commits = append(commits, rebasingCommits...) + } + + unpushedCommits := c.GetUnpushedCommits() log := c.GetLog() - lines := utils.SplitLines(log) - commits := make([]*Commit, len(lines)) // now we can split it up and turn it into commits - for i, line := range lines { + for _, line := range utils.SplitLines(log) { splitLine := strings.Split(line, " ") sha := splitLine[0] - _, pushed := pushables[sha] - commits[i] = &Commit{ + _, unpushed := unpushedCommits[sha] + status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed] + commits = append(commits, &Commit{ Sha: sha, Name: strings.Join(splitLine[1:], " "), - Pushed: pushed, + Status: status, DisplayString: strings.Join(splitLine, " "), - } + }) + } + if len(rebasingCommits) > 0 { + currentCommit := commits[len(rebasingCommits)] + blue := color.New(color.FgYellow) + youAreHere := blue.Sprint("<-- YOU ARE HERE ---") + currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name) } return c.setCommitMergedStatuses(commits) } @@ -562,7 +651,12 @@ func (c *GitCommand) setCommitMergedStatuses(commits []*Commit) ([]*Commit, erro if strings.HasPrefix(ancestor, commit.Sha) { passedAncestor = true } - commits[i].Merged = passedAncestor + if commit.Status != "pushed" { + continue + } + if passedAncestor { + commits[i].Status = "merged" + } } return commits, nil } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 6b28a69bb..829034eb8 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -202,3 +202,14 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) { func (c *OSCommand) RemoveFile(filename string) error { return os.Remove(filename) } + +// FileExists checks whether a file exists at the specified path +func (c *OSCommand) FileExists(path string) (bool, error) { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index f4a5e28f5..165a75a4a 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -96,7 +96,7 @@ func (gui *Gui) handleBranchesPrevLine(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleRebase(g *gocui.Gui, v *gocui.View) error { - selectedBranch := gui.getSelectedBranch(v).Name + selectedBranch := gui.getSelectedBranch().Name checkedOutBranch := gui.State.Branches[0].Name title := "Rebasing" prompt := fmt.Sprintf("Are you sure you want to rebase %s onto %s?", checkedOutBranch, selectedBranch) @@ -109,11 +109,14 @@ func (gui *Gui) handleRebase(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.RebaseBranch(selectedBranch); err != nil { gui.Log.Errorln(err) - if err := gui.createConfirmationPanel(g, v, "Rebase failed", "Rebasing failed, would you like to resolve it?", + if err := gui.createConfirmationPanel(g, v, "Rebase failed", "Damn, conflicts! To abort press 'esc', otherwise press 'enter'", func(g *gocui.Gui, v *gocui.View) error { return nil }, func(g *gocui.Gui, v *gocui.View) error { - return gui.GitCommand.AbortRebaseBranch() + if err := gui.GitCommand.AbortRebaseBranch(); err != nil { + return err + } + return gui.refreshSidePanels(g) }); err != nil { gui.Log.Errorln(err) } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 95b4f80a4..2059ac97f 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -118,11 +118,7 @@ func (gui *Gui) stageSelectedFile(g *gocui.Gui) error { return gui.GitCommand.StageFile(file.Name) } -func (gui *Gui) handleSwitchToStagingPanel(g *gocui.Gui, v *gocui.View) error { - stagingView, err := g.View("staging") - if err != nil { - return err - } +func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error { file, err := gui.getSelectedFile(g) if err != nil { if err != gui.Errors.ErrNoFiles { @@ -130,10 +126,17 @@ func (gui *Gui) handleSwitchToStagingPanel(g *gocui.Gui, v *gocui.View) error { } return nil } + if file.HasMergeConflicts { + return gui.handleSwitchToMerge(g, v) + } if !file.HasUnstagedChanges { gui.Log.WithField("staging", "staging").Info("making error panel") return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements")) } + stagingView, err := g.View("staging") + if err != nil { + return err + } if err := gui.switchFocus(g, v, stagingView); err != nil { return err } @@ -256,7 +259,7 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { - if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts { + if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" { return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit")) } commitMessageView := gui.getCommitMessageView(g) @@ -270,7 +273,7 @@ func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { } func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error { - if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts { + if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" { return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit")) } title := strings.Title(gui.Tr.SLocalize("AmendLastCommit")) @@ -294,7 +297,7 @@ func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) erro // handleCommitEditorPress - handle when the user wants to commit changes via // their editor rather than via the popup panel func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error { - if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts { + if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" { return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit")) } gui.PrepareSubProcess(g, "git", "commit") @@ -347,15 +350,27 @@ func (gui *Gui) refreshStateFiles() { files := gui.GitCommand.GetStatusFiles() gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files) gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files)) - gui.updateHasMergeConflictStatus() + gui.updateWorkTreeState() } -func (gui *Gui) updateHasMergeConflictStatus() error { +func (gui *Gui) updateWorkTreeState() error { merging, err := gui.GitCommand.IsInMergeState() if err != nil { return err } - gui.State.HasMergeConflicts = merging + if merging { + gui.State.WorkingTreeState = "merging" + return nil + } + rebasing, err := gui.GitCommand.IsInRebaseState() + if err != nil { + return err + } + if rebasing { + gui.State.WorkingTreeState = "rebasing" + return nil + } + gui.State.WorkingTreeState = "normal" return nil } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 26ebd5f2b..ef41482c9 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -125,6 +125,7 @@ type guiState struct { Platform commands.Platform Updating bool Panels *panelStates + WorkingTreeState string // one of "merging", "rebasing", "normal" } // NewGui builds a new gui handler diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 5bc08db59..745e2fa7f 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -216,7 +216,7 @@ func (gui *Gui) GetKeybindings() []*Binding { ViewName: "files", Key: gocui.KeyEnter, Modifier: gocui.ModNone, - Handler: gui.handleSwitchToStagingPanel, + Handler: gui.handleEnterFile, Description: gui.Tr.SLocalize("StageLines"), KeyReadable: "enter", }, { diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go index 7c2922c9b..404266d29 100644 --- a/pkg/gui/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -20,11 +20,12 @@ func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) { conflicts := make([]commands.Conflict, 0) var newConflict commands.Conflict for i, line := range utils.SplitLines(content) { - if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" { + trimmedLine := strings.TrimPrefix(line, "++") + if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" { newConflict = commands.Conflict{Start: i} - } else if line == "=======" { + } else if trimmedLine == "=======" { newConflict.Middle = i - } else if strings.HasPrefix(line, ">>>>>>> ") { + } else if strings.HasPrefix(trimmedLine, ">>>>>>> ") { newConflict.End = i conflicts = append(conflicts, newConflict) } @@ -258,10 +259,16 @@ func (gui *Gui) handleCompleteMerge(g *gocui.Gui) error { gui.refreshFiles(g) if rebase, err := gui.GitCommand.IsInRebaseState(); rebase && err == nil { if err := gui.GitCommand.ContinueRebaseBranch(); err != nil { - gui.Log.Errorln(err) + if strings.Contains(err.Error(), "No changes - did you forget to use") { + if err := gui.GitCommand.SkipRebaseBranch(); err != nil { + gui.Log.Errorln(err) + } + } else { + gui.Log.Errorln(err) + } } if err := gui.refreshSidePanels(g); err != nil { - gui.Log.Errorln(err) + return err } } return gui.switchFocus(g, nil, filesView) diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 8d3b34dfe..16017abba 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -22,11 +22,11 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error { pushables, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount() fmt.Fprint(v, "↑"+pushables+"↓"+pullables) branches := gui.State.Branches - if err := gui.updateHasMergeConflictStatus(); err != nil { + if err := gui.updateWorkTreeState(); err != nil { return err } - if gui.State.HasMergeConflicts { - fmt.Fprint(v, utils.ColoredString(" (merging)", color.FgYellow)) + if gui.State.WorkingTreeState != "normal" { + fmt.Fprint(v, utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow)) } if len(branches) == 0 { diff --git a/test/repos/merge_conflict.sh b/test/repos/merge_conflict.sh index 357597719..9a8e9f442 100755 --- a/test/repos/merge_conflict.sh +++ b/test/repos/merge_conflict.sh @@ -24,30 +24,62 @@ git add file1 git add directory git commit -m "first commit" -git checkout -b develop +git checkout -b develop echo "once upon a time there was a dog" >> file1 add_spacing file1 echo "once upon a time there was another dog" >> file1 git add file1 - echo "test2" > directory/file echo "test2" > directory/file2 git add directory - git commit -m "first commit on develop" -git checkout master +git checkout master echo "once upon a time there was a cat" >> file1 add_spacing file1 echo "once upon a time there was another cat" >> file1 git add file1 - echo "test3" > directory/file echo "test3" > directory/file2 git add directory +git commit -m "first commit on master" -git commit -m "first commit on develop" -git merge develop # should have a merge conflict here +git checkout develop +echo "once upon a time there was a mouse" >> file3 +git add file3 +git commit -m "second commit on develop" + + +git checkout master +echo "once upon a time there was a horse" >> file3 +git add file3 +git commit -m "second commit on master" + + +git checkout develop +echo "once upon a time there was a mouse" >> file4 +git add file4 +git commit -m "third commit on develop" + + +git checkout master +echo "once upon a time there was a horse" >> file4 +git add file4 +git commit -m "third commit on master" + + +git checkout develop +echo "once upon a time there was a mouse" >> file5 +git add file5 +git commit -m "fourth commit on develop" + + +git checkout master +echo "once upon a time there was a horse" >> file5 +git add file5 +git commit -m "fourth commit on master" + +# git merge develop # should have a merge conflict here