diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 5047ab114..e89551fe3 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -2,16 +2,12 @@ package commands import ( "fmt" - "io/ioutil" "os" "os/exec" - "path/filepath" - "regexp" "strings" "github.com/mgutz/str" - "github.com/fatih/color" "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/config" @@ -250,21 +246,6 @@ func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) { return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) } -// 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) 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 -} - // RenameCommit renames the topmost commit with the given name func (c *GitCommand) RenameCommit(name string) error { return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name))) @@ -527,226 +508,6 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) { return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName)) } -func (c *GitCommand) getMergeBase() (string, error) { - currentBranch, err := c.CurrentBranchName() - if err != nil { - return "", err - } - - baseBranch := "master" - if strings.HasPrefix(currentBranch, "feature/") { - baseBranch = "develop" - } - - output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch)) - if err != nil { - // swallowing error because it's not a big deal; probably because there are no commits yet - } - return output, nil -} - -// GetRebasingCommits obtains the commits that we're in the process of rebasing -func (c *GitCommand) GetRebasingCommits() ([]*Commit, error) { - rebaseMode, err := c.RebaseMode() - if err != nil { - return nil, err - } - switch rebaseMode { - case "normal": - return c.GetNormalRebasingCommits() - case "interactive": - return c.GetInteractiveRebasingCommits() - default: - return nil, nil - } -} - -func (c *GitCommand) GetNormalRebasingCommits() ([]*Commit, error) { - 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 -} - -// 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 *GitCommand) GetInteractiveRebasingCommits() ([]*Commit, error) { - bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo") - var content []string - if err == nil { - content = strings.Split(string(bytesContent), "\n") - if len(content) > 0 && content[len(content)-1] == "" { - content = content[0 : len(content)-1] - } - } - - // for each of them, grab the matching commit name in the backup - bytesContent, err = ioutil.ReadFile(".git/rebase-merge/git-rebase-todo.backup") - var backupContent []string - if err == nil { - backupContent = strings.Split(string(bytesContent), "\n") - } - - commits := []*Commit{} - for _, todoLine := range content { - commit := c.extractCommit(todoLine, backupContent) - if commit != nil { - commits = append([]*Commit{commit}, commits...) - } - } - - return commits, nil -} - -func (c *GitCommand) extractCommit(todoLine string, backupContent []string) *Commit { - for _, backupLine := range backupContent { - split := strings.Split(todoLine, " ") - prefix := strings.Join(split[0:2], " ") - if strings.HasPrefix(backupLine, prefix) { - return &Commit{ - Sha: split[2], - Name: strings.TrimPrefix(backupLine, prefix+" "), - Status: "rebasing", - } - } - } - return 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) { - 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() - - // 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 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) -} - -func (c *GitCommand) 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 -} - -// GetLog gets the git log (currently limited to 30 commits for performance -// until we work out lazy loading -func (c *GitCommand) 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 -} - // Ignore adds a file to the gitignore for the repo func (c *GitCommand) Ignore(filename string) error { return c.OSCommand.AppendLineToFile(".gitignore", filename) diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go index 60ec4e060..b96a8ec67 100644 --- a/pkg/git/branch_list_builder.go +++ b/pkg/git/branch_list_builder.go @@ -20,6 +20,9 @@ import ( // 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 diff --git a/pkg/git/commit_list_builder.go b/pkg/git/commit_list_builder.go new file mode 100644 index 000000000..703dbd990 --- /dev/null +++ b/pkg/git/commit_list_builder.go @@ -0,0 +1,275 @@ +package git + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/fatih/color" + "github.com/jesseduffield/lazygit/pkg/commands" + "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 *commands.GitCommand + OSCommand *commands.OSCommand +} + +// NewCommitListBuilder builds a new commit list builder +func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand) (*CommitListBuilder, error) { + return &CommitListBuilder{ + Log: log, + GitCommand: gitCommand, + OSCommand: osCommand, + }, nil +} + +// GetCommits obtains the commits of the current branch +func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) { + commits := []*commands.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() + + // 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, &commands.Commit{ + Sha: sha, + Name: strings.Join(splitLine[1:], " "), + 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) +} + +// getRebasingCommits obtains the commits that we're in the process of rebasing +func (c *CommitListBuilder) getRebasingCommits() ([]*commands.Commit, error) { + rebaseMode, err := c.GitCommand.RebaseMode() + if err != nil { + return nil, err + } + switch rebaseMode { + case "normal": + return c.getNormalRebasingCommits() + case "interactive": + return c.getInteractiveRebasingCommits() + default: + return nil, nil + } +} + +func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, error) { + 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 := []*commands.Commit{} + err = filepath.Walk(".git/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 := ioutil.ReadFile(path) + if err != nil { + return err + } + content := string(bytesContent) + commit, err := c.commitFromPatch(content) + if err != nil { + return err + } + commits = append([]*commands.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() ([]*commands.Commit, error) { + bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo") + var content []string + if err == nil { + content = strings.Split(string(bytesContent), "\n") + if len(content) > 0 && content[len(content)-1] == "" { + content = content[0 : len(content)-1] + } + } + + // for each of them, grab the matching commit name in the backup + bytesContent, err = ioutil.ReadFile(".git/rebase-merge/git-rebase-todo.backup") + var backupContent []string + if err == nil { + backupContent = strings.Split(string(bytesContent), "\n") + } + + commits := []*commands.Commit{} + for _, todoLine := range content { + commit := c.extractCommit(todoLine, backupContent) + if commit != nil { + commits = append([]*commands.Commit{commit}, commits...) + } + } + + return commits, nil +} + +func (c *CommitListBuilder) extractCommit(todoLine string, backupContent []string) *commands.Commit { + for _, backupLine := range backupContent { + split := strings.Split(todoLine, " ") + prefix := strings.Join(split[0:2], " ") + if strings.HasPrefix(backupLine, prefix) { + return &commands.Commit{ + Sha: split[2], + Name: strings.TrimPrefix(backupLine, prefix+" "), + Status: "rebasing", + } + } + } + return 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 *CommitListBuilder) commitFromPatch(content string) (*commands.Commit, error) { + lines := strings.Split(content, "\n") + sha := strings.Split(lines[0], " ")[1][0:7] + name := strings.TrimPrefix(lines[3], "Subject: ") + return &commands.Commit{ + Sha: sha, + Name: name, + Status: "rebasing", + }, nil +} + +func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) ([]*commands.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) getMergeBase() (string, error) { + currentBranch, err := c.GitCommand.CurrentBranchName() + if err != nil { + return "", err + } + + baseBranch := "master" + if strings.HasPrefix(currentBranch, "feature/") { + baseBranch = "develop" + } + + output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch)) + if err != nil { + // swallowing error because it's not a big deal; probably because there are no commits yet + } + 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/gui/commits_panel.go b/pkg/gui/commits_panel.go index 374f7ccf0..f1d443731 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/git" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -39,7 +40,11 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) refreshCommits(g *gocui.Gui) error { g.Update(func(*gocui.Gui) error { - commits, err := gui.GitCommand.GetCommits() + builder, err := git.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand) + if err != nil { + return err + } + commits, err := builder.GetCommits() if err != nil { return err }