2019-11-04 10:47:25 +02:00
|
|
|
package commands
|
2019-02-19 00:18:30 +02:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2019-03-02 11:00:26 +02:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
2019-02-19 00:18:30 +02:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/fatih/color"
|
2019-02-20 13:52:17 +02:00
|
|
|
"github.com/jesseduffield/lazygit/pkg/i18n"
|
2019-02-19 00:18:30 +02:00
|
|
|
"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 {
|
2019-02-24 08:34:19 +02:00
|
|
|
Log *logrus.Entry
|
2019-11-04 10:47:25 +02:00
|
|
|
GitCommand *GitCommand
|
|
|
|
OSCommand *OSCommand
|
2019-02-24 08:34:19 +02:00
|
|
|
Tr *i18n.Localizer
|
2019-11-04 10:47:25 +02:00
|
|
|
CherryPickedCommits []*Commit
|
|
|
|
DiffEntries []*Commit
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewCommitListBuilder builds a new commit list builder
|
2019-11-04 10:47:25 +02:00
|
|
|
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit, diffEntries []*Commit) (*CommitListBuilder, error) {
|
2019-02-19 00:18:30 +02:00
|
|
|
return &CommitListBuilder{
|
2019-02-24 08:34:19 +02:00
|
|
|
Log: log,
|
|
|
|
GitCommand: gitCommand,
|
|
|
|
OSCommand: osCommand,
|
|
|
|
Tr: tr,
|
|
|
|
CherryPickedCommits: cherryPickedCommits,
|
2019-03-28 11:58:34 +02:00
|
|
|
DiffEntries: diffEntries,
|
2019-02-19 00:18:30 +02:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2020-02-24 14:09:55 +02:00
|
|
|
// nameAndTag takes a line from a git log and extracts the sha, message and tag (if present)
|
|
|
|
// example inputs:
|
|
|
|
// 66e6369c284e96ed5af5 (tag: v0.14.4) allow fastforwarding the current branch
|
|
|
|
// 32e650e0bb3f4327749f (HEAD -> show-tags) this is my commit
|
|
|
|
// 32e650e0bb3f4327749e this is my other commit
|
|
|
|
func (c *CommitListBuilder) commitLineParts(line string) (string, string, []string) {
|
|
|
|
re := regexp.MustCompile(`(\w+) (.*)`)
|
|
|
|
shaMatch := re.FindStringSubmatch(line)
|
|
|
|
|
|
|
|
if len(shaMatch) <= 1 {
|
|
|
|
return line, "", nil
|
|
|
|
}
|
|
|
|
sha := shaMatch[1]
|
|
|
|
rest := shaMatch[2]
|
|
|
|
|
|
|
|
if !strings.HasPrefix(rest, "(") {
|
|
|
|
return sha, rest, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
re = regexp.MustCompile(`\((.*)\) (.*)`)
|
|
|
|
|
|
|
|
parensMatch := re.FindStringSubmatch(rest)
|
|
|
|
if len(parensMatch) <= 1 {
|
|
|
|
return sha, rest, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
notes := parensMatch[1]
|
|
|
|
message := parensMatch[2]
|
|
|
|
re = regexp.MustCompile(`tag: ([^,]+)`)
|
|
|
|
tagMatch := re.FindStringSubmatch(notes)
|
|
|
|
if len(tagMatch) <= 1 {
|
|
|
|
return sha, message, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
tag := tagMatch[1]
|
|
|
|
return sha, message, []string{tag}
|
|
|
|
}
|
|
|
|
|
2019-02-19 00:18:30 +02:00
|
|
|
// GetCommits obtains the commits of the current branch
|
2020-01-11 09:23:35 +02:00
|
|
|
func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
|
2019-11-04 10:47:25 +02:00
|
|
|
commits := []*Commit{}
|
|
|
|
var rebasingCommits []*Commit
|
2019-03-02 11:00:26 +02:00
|
|
|
rebaseMode, err := c.GitCommand.RebaseMode()
|
2019-02-19 00:18:30 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-03-02 11:00:26 +02:00
|
|
|
if rebaseMode != "" {
|
2019-02-19 14:36:29 +02:00
|
|
|
// here we want to also prepend the commits that we're in the process of rebasing
|
2019-03-02 11:00:26 +02:00
|
|
|
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
|
2019-02-19 14:36:29 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(rebasingCommits) > 0 {
|
|
|
|
commits = append(commits, rebasingCommits...)
|
|
|
|
}
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
unpushedCommits := c.getUnpushedCommits()
|
2020-01-11 09:23:35 +02:00
|
|
|
log := c.getLog(limit)
|
2019-02-19 00:18:30 +02:00
|
|
|
|
|
|
|
// now we can split it up and turn it into commits
|
|
|
|
for _, line := range utils.SplitLines(log) {
|
2020-02-24 14:09:55 +02:00
|
|
|
sha, name, tags := c.commitLineParts(line)
|
2019-02-19 00:18:30 +02:00
|
|
|
_, unpushed := unpushedCommits[sha]
|
|
|
|
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
|
2019-11-04 10:47:25 +02:00
|
|
|
commits = append(commits, &Commit{
|
2019-02-19 00:18:30 +02:00
|
|
|
Sha: sha,
|
2020-02-24 14:09:55 +02:00
|
|
|
Name: name,
|
2019-02-19 00:18:30 +02:00
|
|
|
Status: status,
|
2020-02-24 14:09:55 +02:00
|
|
|
DisplayString: line,
|
|
|
|
Tags: tags,
|
2019-02-19 00:18:30 +02:00
|
|
|
})
|
|
|
|
}
|
2019-03-02 11:00:26 +02:00
|
|
|
if rebaseMode != "" {
|
2019-02-19 00:18:30 +02:00
|
|
|
currentCommit := commits[len(rebasingCommits)]
|
|
|
|
blue := color.New(color.FgYellow)
|
2019-02-20 13:52:17 +02:00
|
|
|
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.SLocalize("YouAreHere"))
|
2019-02-19 00:18:30 +02:00
|
|
|
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
|
|
|
}
|
2019-02-24 04:51:52 +02:00
|
|
|
|
|
|
|
commits, err = c.setCommitMergedStatuses(commits)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
commits, err = c.setCommitCherryPickStatuses(commits)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-03-28 11:58:34 +02:00
|
|
|
for _, commit := range commits {
|
|
|
|
for _, entry := range c.DiffEntries {
|
|
|
|
if entry.Sha == commit.Sha {
|
|
|
|
commit.Status = "selected"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-24 04:51:52 +02:00
|
|
|
return commits, nil
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2019-03-02 11:00:26 +02:00
|
|
|
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
2019-11-04 10:47:25 +02:00
|
|
|
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*Commit, error) {
|
2019-03-02 11:00:26 +02:00
|
|
|
switch rebaseMode {
|
|
|
|
case "normal":
|
|
|
|
return c.getNormalRebasingCommits()
|
|
|
|
case "interactive":
|
|
|
|
return c.getInteractiveRebasingCommits()
|
|
|
|
default:
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-04 10:47:25 +02:00
|
|
|
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*Commit, error) {
|
2019-03-02 11:00:26 +02:00
|
|
|
rewrittenCount := 0
|
2019-05-12 09:04:32 +02:00
|
|
|
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-apply/rewritten", c.GitCommand.DotGitDir))
|
2019-03-02 11:00:26 +02:00
|
|
|
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
|
2019-11-04 10:47:25 +02:00
|
|
|
commits := []*Commit{}
|
2019-05-12 09:04:32 +02:00
|
|
|
err = filepath.Walk(fmt.Sprintf("%s/rebase-apply", c.GitCommand.DotGitDir), func(path string, f os.FileInfo, err error) error {
|
2019-03-02 11:00:26 +02:00
|
|
|
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
|
|
|
|
}
|
2019-11-04 10:47:25 +02:00
|
|
|
commits = append([]*Commit{commit}, commits...)
|
2019-03-02 11:00:26 +02:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return commits, nil
|
|
|
|
}
|
|
|
|
|
2019-02-19 00:18:30 +02:00
|
|
|
// 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
|
|
|
|
|
2019-03-02 11:00:26 +02:00
|
|
|
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
|
2019-02-19 00:18:30 +02:00
|
|
|
// and extracts out the sha and names of commits that we still have to go
|
|
|
|
// in the rebase:
|
2019-11-04 10:47:25 +02:00
|
|
|
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
|
2019-05-12 09:04:32 +02:00
|
|
|
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.GitCommand.DotGitDir))
|
2019-02-19 14:36:29 +02:00
|
|
|
if err != nil {
|
2019-05-22 15:34:29 +02:00
|
|
|
c.Log.Info(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
|
2019-02-19 14:36:29 +02:00
|
|
|
// we assume an error means the file doesn't exist so we just return
|
|
|
|
return nil, nil
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2019-11-04 10:47:25 +02:00
|
|
|
commits := []*Commit{}
|
2019-02-19 14:36:29 +02:00
|
|
|
lines := strings.Split(string(bytesContent), "\n")
|
|
|
|
for _, line := range lines {
|
2019-02-24 04:51:52 +02:00
|
|
|
if line == "" || line == "noop" {
|
2019-02-19 14:36:29 +02:00
|
|
|
return commits, nil
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
2019-02-19 14:36:29 +02:00
|
|
|
splitLine := strings.Split(line, " ")
|
2019-11-04 10:47:25 +02:00
|
|
|
commits = append([]*Commit{{
|
2020-02-13 08:59:14 +02:00
|
|
|
Sha: splitLine[1],
|
2019-02-19 14:36:29 +02:00
|
|
|
Name: strings.Join(splitLine[2:], " "),
|
|
|
|
Status: "rebasing",
|
|
|
|
Action: splitLine[0],
|
|
|
|
}}, commits...)
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2019-02-19 14:36:29 +02:00
|
|
|
return nil, nil
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2019-03-02 11:00:26 +02:00
|
|
|
// 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
|
2019-11-04 10:47:25 +02:00
|
|
|
func (c *CommitListBuilder) commitFromPatch(content string) (*Commit, error) {
|
2019-03-02 11:00:26 +02:00
|
|
|
lines := strings.Split(content, "\n")
|
2020-02-13 08:59:14 +02:00
|
|
|
sha := strings.Split(lines[0], " ")[1]
|
2019-03-02 11:00:26 +02:00
|
|
|
name := strings.TrimPrefix(lines[3], "Subject: ")
|
2019-11-04 10:47:25 +02:00
|
|
|
return &Commit{
|
2019-03-02 11:00:26 +02:00
|
|
|
Sha: sha,
|
|
|
|
Name: name,
|
|
|
|
Status: "rebasing",
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2019-11-04 10:47:25 +02:00
|
|
|
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
|
2019-02-19 00:18:30 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-11-04 10:47:25 +02:00
|
|
|
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*Commit) ([]*Commit, error) {
|
2019-02-24 04:51:52 +02:00
|
|
|
for _, commit := range commits {
|
2019-02-24 08:34:19 +02:00
|
|
|
for _, cherryPickedCommit := range c.CherryPickedCommits {
|
|
|
|
if commit.Sha == cherryPickedCommit.Sha {
|
2019-02-24 04:51:52 +02:00
|
|
|
commit.Copied = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return commits, nil
|
|
|
|
}
|
|
|
|
|
2019-02-19 00:18:30 +02:00
|
|
|
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"
|
|
|
|
}
|
|
|
|
|
2019-07-19 03:55:33 +02:00
|
|
|
// swallowing error because it's not a big deal; probably because there are no commits yet
|
2019-11-21 12:09:14 +02:00
|
|
|
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base HEAD %s", baseBranch)
|
2019-02-19 00:18:30 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-01-11 09:23:35 +02:00
|
|
|
// getLog gets the git log.
|
|
|
|
func (c *CommitListBuilder) getLog(limit bool) string {
|
|
|
|
limitFlag := ""
|
|
|
|
if limit {
|
|
|
|
limitFlag = "-30"
|
|
|
|
}
|
|
|
|
|
2020-02-24 14:09:55 +02:00
|
|
|
result, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --decorate --oneline %s --abbrev=%d", limitFlag, 20))
|
2019-02-19 00:18:30 +02:00
|
|
|
if err != nil {
|
|
|
|
// assume if there is an error there are no commits yet for this branch
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|