2022-11-11 04:19:29 +02:00
|
|
|
package git_commands
|
2019-02-19 00:18:30 +02:00
|
|
|
|
|
|
|
import (
|
2022-05-22 18:42:56 +02:00
|
|
|
"bytes"
|
2019-02-19 00:18:30 +02:00
|
|
|
"fmt"
|
2019-03-02 11:00:26 +02:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
2020-03-27 10:12:15 +02:00
|
|
|
"strconv"
|
2019-02-19 00:18:30 +02:00
|
|
|
"strings"
|
2023-07-28 15:04:24 +02:00
|
|
|
"sync"
|
2019-02-19 00:18:30 +02:00
|
|
|
|
2022-05-22 18:42:56 +02:00
|
|
|
"github.com/fsmiamoto/git-todo-parser/todo"
|
2020-09-29 12:28:39 +02:00
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
2020-09-29 11:10:57 +02:00
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
2021-12-30 04:35:10 +02:00
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
2021-12-29 05:33:38 +02:00
|
|
|
"github.com/jesseduffield/lazygit/pkg/common"
|
2023-07-28 15:04:24 +02:00
|
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
2023-05-09 21:41:25 +02:00
|
|
|
"github.com/samber/lo"
|
2019-02-19 00:18:30 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2021-12-30 04:35:10 +02:00
|
|
|
// CommitLoader returns a list of Commit objects for the current repo
|
|
|
|
type CommitLoader struct {
|
2021-12-29 05:33:38 +02:00
|
|
|
*common.Common
|
|
|
|
cmd oscommands.ICmdObjBuilder
|
|
|
|
|
2023-05-09 21:41:25 +02:00
|
|
|
getRebaseMode func() (enums.RebaseMode, error)
|
|
|
|
readFile func(filename string) ([]byte, error)
|
|
|
|
walkFiles func(root string, fn filepath.WalkFunc) error
|
|
|
|
dotGitDir string
|
2023-05-21 09:00:29 +02:00
|
|
|
// List of main branches that exist in the repo.
|
2023-05-09 21:41:25 +02:00
|
|
|
// We use these to obtain the merge base of the branch.
|
2023-05-21 09:00:29 +02:00
|
|
|
// When nil, we're yet to obtain the list of existing main branches.
|
|
|
|
// When an empty slice, we've obtained the list and it's empty.
|
|
|
|
mainBranches []string
|
2023-07-02 06:03:16 +02:00
|
|
|
*GitCommon
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2021-12-30 04:44:41 +02:00
|
|
|
// making our dependencies explicit for the sake of easier testing
|
2021-12-30 04:35:10 +02:00
|
|
|
func NewCommitLoader(
|
2021-12-30 04:44:41 +02:00
|
|
|
cmn *common.Common,
|
2022-01-02 01:34:33 +02:00
|
|
|
cmd oscommands.ICmdObjBuilder,
|
|
|
|
getRebaseMode func() (enums.RebaseMode, error),
|
2023-07-02 06:03:16 +02:00
|
|
|
gitCommon *GitCommon,
|
2021-12-30 04:35:10 +02:00
|
|
|
) *CommitLoader {
|
|
|
|
return &CommitLoader{
|
2023-05-21 09:00:29 +02:00
|
|
|
Common: cmn,
|
|
|
|
cmd: cmd,
|
|
|
|
getRebaseMode: getRebaseMode,
|
|
|
|
readFile: os.ReadFile,
|
|
|
|
walkFiles: filepath.Walk,
|
|
|
|
mainBranches: nil,
|
2023-07-02 06:03:16 +02:00
|
|
|
GitCommon: gitCommon,
|
2020-08-22 00:49:02 +02:00
|
|
|
}
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2020-03-28 07:28:35 +02:00
|
|
|
type GetCommitsOptions struct {
|
2020-08-22 00:49:02 +02:00
|
|
|
Limit bool
|
|
|
|
FilterPath string
|
|
|
|
IncludeRebaseCommits bool
|
|
|
|
RefName string // e.g. "HEAD" or "my_branch"
|
2021-11-02 12:16:00 +02:00
|
|
|
// determines if we show the whole git graph i.e. pass the '--all' flag
|
|
|
|
All bool
|
2020-03-28 07:28:35 +02:00
|
|
|
}
|
|
|
|
|
2019-02-19 00:18:30 +02:00
|
|
|
// GetCommits obtains the commits of the current branch
|
2021-12-30 04:35:10 +02:00
|
|
|
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
|
2020-09-29 10:36:54 +02:00
|
|
|
commits := []*models.Commit{}
|
|
|
|
var rebasingCommits []*models.Commit
|
2020-08-22 00:49:02 +02:00
|
|
|
|
2020-08-27 12:50:30 +02:00
|
|
|
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
|
2020-08-22 00:49:02 +02:00
|
|
|
var err error
|
2021-12-30 02:22:29 +02:00
|
|
|
rebasingCommits, err = self.MergeRebasingCommits(commits)
|
2019-02-19 14:36:29 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-08-27 12:50:30 +02:00
|
|
|
commits = append(commits, rebasingCommits...)
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2023-07-28 15:04:24 +02:00
|
|
|
wg := sync.WaitGroup{}
|
|
|
|
|
|
|
|
wg.Add(2)
|
|
|
|
|
|
|
|
var logErr error
|
|
|
|
go utils.Safe(func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
logErr = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
|
|
|
|
commit := self.extractCommitFromLine(line)
|
|
|
|
commits = append(commits, commit)
|
|
|
|
return false, nil
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
var ancestor string
|
|
|
|
go utils.Safe(func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
ancestor = self.getMergeBase(opts.RefName)
|
|
|
|
})
|
|
|
|
|
2020-08-27 09:05:07 +02:00
|
|
|
passedFirstPushedCommit := false
|
2023-07-28 15:04:24 +02:00
|
|
|
// I can get this before
|
2021-12-30 02:22:29 +02:00
|
|
|
firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName)
|
2020-08-27 00:42:42 +02:00
|
|
|
if err != nil {
|
2020-08-27 09:05:07 +02:00
|
|
|
// must have no upstream branch so we'll consider everything as pushed
|
|
|
|
passedFirstPushedCommit = true
|
2020-08-27 00:42:42 +02:00
|
|
|
}
|
|
|
|
|
2023-07-28 15:04:24 +02:00
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
if logErr != nil {
|
|
|
|
return nil, logErr
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, commit := range commits {
|
2022-05-04 12:41:37 +02:00
|
|
|
if commit.Sha == firstPushedCommit {
|
|
|
|
passedFirstPushedCommit = true
|
2020-04-06 19:26:12 +02:00
|
|
|
}
|
2023-08-19 08:47:22 +02:00
|
|
|
if commit.Status != models.StatusRebasing {
|
|
|
|
if passedFirstPushedCommit {
|
|
|
|
commit.Status = models.StatusPushed
|
|
|
|
} else {
|
|
|
|
commit.Status = models.StatusUnpushed
|
|
|
|
}
|
2023-07-28 15:04:24 +02:00
|
|
|
}
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
2020-03-26 12:11:21 +02:00
|
|
|
|
2021-12-30 04:11:58 +02:00
|
|
|
if len(commits) == 0 {
|
|
|
|
return commits, nil
|
|
|
|
}
|
|
|
|
|
2023-07-28 15:04:24 +02:00
|
|
|
if ancestor != "" {
|
2023-07-29 20:36:16 +02:00
|
|
|
commits = setCommitMergedStatuses(ancestor, commits)
|
2023-07-28 15:04:24 +02:00
|
|
|
}
|
2019-02-24 04:51:52 +02:00
|
|
|
|
|
|
|
return commits, nil
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2021-12-30 04:35:10 +02:00
|
|
|
func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
|
2021-12-30 04:11:58 +02:00
|
|
|
// chances are we have as many commits as last time so we'll set the capacity to be the old length
|
|
|
|
result := make([]*models.Commit, 0, len(commits))
|
|
|
|
for i, commit := range commits {
|
2023-03-03 20:45:54 +02:00
|
|
|
if !commit.IsTODO() { // removing the existing rebase commits so we can add the refreshed ones
|
2021-12-30 04:11:58 +02:00
|
|
|
result = append(result, commits[i:]...)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
rebaseMode, err := self.getRebaseMode()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-12-30 04:35:10 +02:00
|
|
|
if rebaseMode == enums.REBASE_MODE_NONE {
|
2021-12-30 04:11:58 +02:00
|
|
|
// not in rebase mode so return original commits
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(rebasingCommits) > 0 {
|
|
|
|
result = append(rebasingCommits, result...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
|
|
|
|
// then puts them into a commit object
|
|
|
|
// example input:
|
|
|
|
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
|
2021-12-30 04:35:10 +02:00
|
|
|
func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
|
2022-05-08 06:23:29 +02:00
|
|
|
split := strings.SplitN(line, "\x00", 7)
|
2021-12-30 04:11:58 +02:00
|
|
|
|
|
|
|
sha := split[0]
|
|
|
|
unixTimestamp := split[1]
|
2022-05-08 06:23:29 +02:00
|
|
|
authorName := split[2]
|
|
|
|
authorEmail := split[3]
|
|
|
|
extraInfo := strings.TrimSpace(split[4])
|
|
|
|
parentHashes := split[5]
|
|
|
|
message := split[6]
|
2021-12-30 04:11:58 +02:00
|
|
|
|
|
|
|
tags := []string{}
|
|
|
|
|
|
|
|
if extraInfo != "" {
|
2023-06-21 19:30:25 +02:00
|
|
|
extraInfoFields := strings.Split(extraInfo, ",")
|
|
|
|
for _, extraInfoField := range extraInfoFields {
|
|
|
|
extraInfoField = strings.TrimSpace(extraInfoField)
|
|
|
|
re := regexp.MustCompile(`tag: (.+)`)
|
|
|
|
tagMatch := re.FindStringSubmatch(extraInfoField)
|
|
|
|
if len(tagMatch) > 1 {
|
|
|
|
tags = append(tags, tagMatch[1])
|
|
|
|
}
|
2021-12-30 04:11:58 +02:00
|
|
|
}
|
2023-06-21 19:30:25 +02:00
|
|
|
|
|
|
|
extraInfo = "(" + extraInfo + ")"
|
2021-12-30 04:11:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
|
|
|
|
|
2022-03-26 15:03:32 +02:00
|
|
|
parents := []string{}
|
|
|
|
if len(parentHashes) > 0 {
|
|
|
|
parents = strings.Split(parentHashes, " ")
|
|
|
|
}
|
|
|
|
|
2021-12-30 04:11:58 +02:00
|
|
|
return &models.Commit{
|
|
|
|
Sha: sha,
|
|
|
|
Name: message,
|
|
|
|
Tags: tags,
|
|
|
|
ExtraInfo: extraInfo,
|
|
|
|
UnixTimestamp: int64(unitTimestampInt),
|
2022-05-08 06:23:29 +02:00
|
|
|
AuthorName: authorName,
|
|
|
|
AuthorEmail: authorEmail,
|
2022-03-26 15:03:32 +02:00
|
|
|
Parents: parents,
|
2021-12-30 04:11:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-30 04:35:10 +02:00
|
|
|
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
|
2021-12-30 02:22:29 +02:00
|
|
|
commits, err := self.getRebasingCommits(rebaseMode)
|
2021-10-30 08:42:52 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-10-30 09:06:39 +02:00
|
|
|
if len(commits) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2023-07-24 05:06:42 +02:00
|
|
|
commitShas := lo.FilterMap(commits, func(commit *models.Commit, _ int) (string, bool) {
|
2023-04-07 19:28:31 +02:00
|
|
|
return commit.Sha, commit.Sha != ""
|
2022-03-19 07:34:46 +02:00
|
|
|
})
|
2021-10-30 08:42:52 +02:00
|
|
|
|
|
|
|
// note that we're not filtering these as we do non-rebasing commits just because
|
|
|
|
// I suspect that will cause some damage
|
2021-12-30 02:22:29 +02:00
|
|
|
cmdObj := self.cmd.New(
|
2023-05-19 12:18:02 +02:00
|
|
|
NewGitCmd("show").
|
|
|
|
Config("log.showSignature=false").
|
|
|
|
Arg("--no-patch", "--oneline", "--abbrev=20", prettyFormat).
|
|
|
|
Arg(commitShas...).
|
2023-05-21 09:00:29 +02:00
|
|
|
ToArgv(),
|
2022-01-05 02:57:32 +02:00
|
|
|
).DontLog()
|
2021-10-30 08:42:52 +02:00
|
|
|
|
2023-04-07 19:28:31 +02:00
|
|
|
fullCommits := map[string]*models.Commit{}
|
2021-12-30 02:22:29 +02:00
|
|
|
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
2022-05-06 16:04:58 +02:00
|
|
|
commit := self.extractCommitFromLine(line)
|
2023-04-07 19:28:31 +02:00
|
|
|
fullCommits[commit.Sha] = commit
|
2021-10-30 08:42:52 +02:00
|
|
|
return false, nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-04-07 19:28:31 +02:00
|
|
|
|
2023-06-27 19:11:38 +02:00
|
|
|
findFullCommit := lo.Ternary(self.version.IsOlderThan(2, 25, 2),
|
|
|
|
func(sha string) *models.Commit {
|
|
|
|
for s, c := range fullCommits {
|
|
|
|
if strings.HasPrefix(s, sha) {
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
func(sha string) *models.Commit {
|
|
|
|
return fullCommits[sha]
|
|
|
|
})
|
|
|
|
|
2023-04-07 19:28:31 +02:00
|
|
|
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
|
|
|
for _, rebasingCommit := range commits {
|
|
|
|
if rebasingCommit.Sha == "" {
|
|
|
|
hydratedCommits = append(hydratedCommits, rebasingCommit)
|
2023-06-27 19:11:38 +02:00
|
|
|
} else if commit := findFullCommit(rebasingCommit.Sha); commit != nil {
|
2023-04-07 19:28:31 +02:00
|
|
|
commit.Action = rebasingCommit.Action
|
|
|
|
commit.Status = rebasingCommit.Status
|
|
|
|
hydratedCommits = append(hydratedCommits, commit)
|
|
|
|
}
|
|
|
|
}
|
2021-10-30 08:42:52 +02:00
|
|
|
return hydratedCommits, nil
|
|
|
|
}
|
|
|
|
|
2019-03-02 11:00:26 +02:00
|
|
|
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
2021-12-30 04:35:10 +02:00
|
|
|
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
|
2019-03-02 11:00:26 +02:00
|
|
|
switch rebaseMode {
|
2021-12-30 04:35:10 +02:00
|
|
|
case enums.REBASE_MODE_MERGING:
|
2021-12-30 02:22:29 +02:00
|
|
|
return self.getNormalRebasingCommits()
|
2021-12-30 04:35:10 +02:00
|
|
|
case enums.REBASE_MODE_INTERACTIVE:
|
2021-12-30 02:22:29 +02:00
|
|
|
return self.getInteractiveRebasingCommits()
|
2019-03-02 11:00:26 +02:00
|
|
|
default:
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-30 04:35:10 +02:00
|
|
|
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
|
2019-03-02 11:00:26 +02:00
|
|
|
rewrittenCount := 0
|
2023-07-28 10:27:14 +02:00
|
|
|
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply/rewritten"))
|
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
|
2020-09-29 10:36:54 +02:00
|
|
|
commits := []*models.Commit{}
|
2023-07-28 10:27:14 +02:00
|
|
|
err = self.walkFiles(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply"), 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
|
|
|
|
}
|
2021-12-30 02:22:29 +02:00
|
|
|
bytesContent, err := self.readFile(path)
|
2019-03-02 11:00:26 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
content := string(bytesContent)
|
2022-01-08 06:46:35 +02:00
|
|
|
commit := self.commitFromPatch(content)
|
2020-09-29 10:36:54 +02:00
|
|
|
commits = append([]*models.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:
|
2021-12-30 04:35:10 +02:00
|
|
|
func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
|
2023-07-28 10:27:14 +02:00
|
|
|
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"))
|
2019-02-19 14:36:29 +02:00
|
|
|
if err != nil {
|
2021-12-30 02:22:29 +02:00
|
|
|
self.Log.Error(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
|
|
|
}
|
|
|
|
|
2020-09-29 10:36:54 +02:00
|
|
|
commits := []*models.Commit{}
|
2022-03-19 10:51:48 +02:00
|
|
|
|
2023-07-02 06:03:16 +02:00
|
|
|
todos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
|
2022-05-22 18:42:56 +02:00
|
|
|
if err != nil {
|
|
|
|
self.Log.Error(fmt.Sprintf("error occurred while parsing git-rebase-todo file: %s", err.Error()))
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2023-02-26 12:51:02 +02:00
|
|
|
// See if the current commit couldn't be applied because it conflicted; if
|
|
|
|
// so, add a fake entry for it
|
|
|
|
if conflictedCommitSha := self.getConflictedCommit(todos); conflictedCommitSha != "" {
|
|
|
|
commits = append(commits, &models.Commit{
|
|
|
|
Sha: conflictedCommitSha,
|
|
|
|
Name: "",
|
|
|
|
Status: models.StatusRebasing,
|
|
|
|
Action: models.ActionConflict,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-22 18:42:56 +02:00
|
|
|
for _, t := range todos {
|
2023-04-05 09:33:11 +02:00
|
|
|
if t.Command == todo.UpdateRef {
|
|
|
|
t.Msg = strings.TrimPrefix(t.Ref, "refs/heads/")
|
|
|
|
} else if t.Commit == "" {
|
2022-05-22 18:42:56 +02:00
|
|
|
// Command does not have a commit associated, skip
|
2020-04-22 03:15:41 +02:00
|
|
|
continue
|
|
|
|
}
|
2023-07-24 05:06:42 +02:00
|
|
|
commits = utils.Prepend(commits, &models.Commit{
|
2022-05-22 18:42:56 +02:00
|
|
|
Sha: t.Commit,
|
|
|
|
Name: t.Msg,
|
2023-04-03 12:40:29 +02:00
|
|
|
Status: models.StatusRebasing,
|
2023-04-03 12:42:29 +02:00
|
|
|
Action: t.Command,
|
2022-03-19 10:51:48 +02:00
|
|
|
})
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2020-04-22 03:21:20 +02:00
|
|
|
return commits, nil
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2023-02-26 12:51:02 +02:00
|
|
|
func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string {
|
2023-07-28 10:27:14 +02:00
|
|
|
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/done"))
|
2023-02-26 12:51:02 +02:00
|
|
|
if err != nil {
|
|
|
|
self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error()))
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2023-07-02 06:03:16 +02:00
|
|
|
doneTodos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
|
2023-02-26 12:51:02 +02:00
|
|
|
if err != nil {
|
|
|
|
self.Log.Error(fmt.Sprintf("error occurred while parsing rebase-merge/done file: %s", err.Error()))
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
amendFileExists := false
|
2023-07-28 10:27:14 +02:00
|
|
|
if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend")); err == nil {
|
2023-02-26 12:51:02 +02:00
|
|
|
amendFileExists = true
|
|
|
|
}
|
|
|
|
|
|
|
|
return self.getConflictedCommitImpl(todos, doneTodos, amendFileExists)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool) string {
|
|
|
|
// Should never be possible, but just to be safe:
|
|
|
|
if len(doneTodos) == 0 {
|
|
|
|
self.Log.Error("no done entries in rebase-merge/done file")
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
lastTodo := doneTodos[len(doneTodos)-1]
|
|
|
|
if lastTodo.Command == todo.Break || lastTodo.Command == todo.Exec || lastTodo.Command == todo.Reword {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// In certain cases, git reschedules commands that failed. One example is if
|
|
|
|
// a patch would overwrite an untracked file (another one is an "exec" that
|
|
|
|
// failed, but we don't care about that here because we dealt with exec
|
|
|
|
// already above). To detect this, compare the last command of the "done"
|
|
|
|
// file against the first command of "git-rebase-todo"; if they are the
|
|
|
|
// same, the command was rescheduled.
|
|
|
|
if len(doneTodos) > 0 && len(todos) > 0 && doneTodos[len(doneTodos)-1] == todos[0] {
|
|
|
|
// Command was rescheduled, no need to display it
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// Older versions of git have a bug whereby, if a command is rescheduled,
|
|
|
|
// the last successful command is appended to the "done" file again. To
|
|
|
|
// detect this, we need to compare the second-to-last done entry against the
|
|
|
|
// first todo entry, and also compare the last done entry against the
|
|
|
|
// last-but-two done entry; this latter check is needed for the following
|
|
|
|
// case:
|
|
|
|
// pick A
|
|
|
|
// exec make test
|
|
|
|
// pick B
|
|
|
|
// exec make test
|
|
|
|
// If pick B fails with conflicts, then the "done" file contains
|
|
|
|
// pick A
|
|
|
|
// exec make test
|
|
|
|
// pick B
|
|
|
|
// and git-rebase-todo contains
|
|
|
|
// exec make test
|
|
|
|
// Without the last condition we would erroneously treat this as the exec
|
|
|
|
// command being rescheduled, so we wouldn't display our fake entry for
|
|
|
|
// "pick B".
|
|
|
|
if len(doneTodos) >= 3 && len(todos) > 0 && doneTodos[len(doneTodos)-2] == todos[0] &&
|
|
|
|
doneTodos[len(doneTodos)-1] == doneTodos[len(doneTodos)-3] {
|
|
|
|
// Command was rescheduled, no need to display it
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
if lastTodo.Command == todo.Edit {
|
|
|
|
if amendFileExists {
|
|
|
|
// Special case for "edit": if the "amend" file exists, the "edit"
|
|
|
|
// command was successful, otherwise it wasn't
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// I don't think this is ever possible, but again, just to be safe:
|
|
|
|
if lastTodo.Commit == "" {
|
|
|
|
self.Log.Error("last command in rebase-merge/done file doesn't have a commit")
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// Any other todo that has a commit associated with it must have failed with
|
|
|
|
// a conflict, otherwise we wouldn't have stopped the rebase:
|
|
|
|
return lastTodo.Commit
|
|
|
|
}
|
|
|
|
|
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
|
2022-01-08 06:46:35 +02:00
|
|
|
func (self *CommitLoader) commitFromPatch(content string) *models.Commit {
|
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: ")
|
2020-09-29 10:36:54 +02:00
|
|
|
return &models.Commit{
|
2019-03-02 11:00:26 +02:00
|
|
|
Sha: sha,
|
|
|
|
Name: name,
|
2023-04-03 12:40:29 +02:00
|
|
|
Status: models.StatusRebasing,
|
2022-01-08 06:46:35 +02:00
|
|
|
}
|
2019-03-02 11:00:26 +02:00
|
|
|
}
|
|
|
|
|
2023-07-29 20:36:16 +02:00
|
|
|
func setCommitMergedStatuses(ancestor string, commits []*models.Commit) []*models.Commit {
|
2019-02-19 00:18:30 +02:00
|
|
|
passedAncestor := false
|
|
|
|
for i, commit := range commits {
|
2023-07-29 20:15:00 +02:00
|
|
|
// some commits aren't really commits and don't have sha's, such as the update-ref todo
|
|
|
|
if commit.Sha != "" && strings.HasPrefix(ancestor, commit.Sha) {
|
2019-02-19 00:18:30 +02:00
|
|
|
passedAncestor = true
|
|
|
|
}
|
2023-03-15 13:21:24 +02:00
|
|
|
if commit.Status != models.StatusPushed && commit.Status != models.StatusUnpushed {
|
2019-02-19 00:18:30 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
if passedAncestor {
|
2023-04-03 12:40:29 +02:00
|
|
|
commits[i].Status = models.StatusMerged
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
}
|
2023-05-09 21:41:25 +02:00
|
|
|
return commits
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2023-05-09 21:41:25 +02:00
|
|
|
func (self *CommitLoader) getMergeBase(refName string) string {
|
2023-05-21 09:00:29 +02:00
|
|
|
if self.mainBranches == nil {
|
|
|
|
self.mainBranches = self.getExistingMainBranches()
|
2023-05-09 21:41:25 +02:00
|
|
|
}
|
|
|
|
|
2023-05-21 09:00:29 +02:00
|
|
|
if len(self.mainBranches) == 0 {
|
2023-05-09 21:41:25 +02:00
|
|
|
return ""
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2023-05-09 21:41:25 +02:00
|
|
|
// We pass all configured main branches to the merge-base call; git will
|
|
|
|
// return the base commit for the closest one.
|
2023-05-19 12:18:02 +02:00
|
|
|
|
|
|
|
output, err := self.cmd.New(
|
2023-05-21 09:00:29 +02:00
|
|
|
NewGitCmd("merge-base").Arg(refName).Arg(self.mainBranches...).
|
|
|
|
ToArgv(),
|
2023-05-19 12:18:02 +02:00
|
|
|
).DontLog().RunWithOutput()
|
2023-05-09 21:41:25 +02:00
|
|
|
if err != nil {
|
|
|
|
// If there's an error, it must be because one of the main branches that
|
|
|
|
// used to exist when we called getExistingMainBranches() was deleted
|
|
|
|
// meanwhile. To fix this for next time, throw away our cache.
|
2023-05-21 09:00:29 +02:00
|
|
|
self.mainBranches = nil
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
2023-05-09 21:41:25 +02:00
|
|
|
return ignoringWarnings(output)
|
|
|
|
}
|
2019-02-19 00:18:30 +02:00
|
|
|
|
2023-05-21 09:00:29 +02:00
|
|
|
func (self *CommitLoader) getExistingMainBranches() []string {
|
2023-07-28 15:04:24 +02:00
|
|
|
var existingBranches []string
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
|
|
mainBranches := self.UserConfig.Git.MainBranches
|
|
|
|
existingBranches = make([]string, len(mainBranches))
|
|
|
|
|
|
|
|
for i, branchName := range mainBranches {
|
|
|
|
wg.Add(1)
|
|
|
|
i := i
|
|
|
|
branchName := branchName
|
|
|
|
go utils.Safe(func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
2023-06-10 10:49:18 +02:00
|
|
|
// Try to determine upstream of local main branch
|
|
|
|
if ref, err := self.cmd.New(
|
|
|
|
NewGitCmd("rev-parse").Arg("--symbolic-full-name", branchName+"@{u}").ToArgv(),
|
|
|
|
).DontLog().RunWithOutput(); err == nil {
|
2023-07-28 15:04:24 +02:00
|
|
|
existingBranches[i] = strings.TrimSpace(ref)
|
|
|
|
return
|
2023-06-10 10:49:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// If this failed, a local branch for this main branch doesn't exist or it
|
|
|
|
// has no upstream configured. Try looking for one in the "origin" remote.
|
|
|
|
ref := "refs/remotes/origin/" + branchName
|
2023-05-21 09:00:29 +02:00
|
|
|
if err := self.cmd.New(
|
|
|
|
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
|
2023-06-10 10:49:18 +02:00
|
|
|
).DontLog().Run(); err == nil {
|
2023-07-28 15:04:24 +02:00
|
|
|
existingBranches[i] = ref
|
|
|
|
return
|
2023-05-21 09:00:29 +02:00
|
|
|
}
|
2023-06-10 10:49:18 +02:00
|
|
|
|
|
|
|
// If this failed as well, try if we have the main branch as a local
|
|
|
|
// branch. This covers the case where somebody is using git locally
|
|
|
|
// for something, but never pushing anywhere.
|
|
|
|
ref = "refs/heads/" + branchName
|
|
|
|
if err := self.cmd.New(
|
|
|
|
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
|
|
|
|
).DontLog().Run(); err == nil {
|
2023-07-28 15:04:24 +02:00
|
|
|
existingBranches[i] = ref
|
2023-06-10 10:49:18 +02:00
|
|
|
}
|
2023-05-21 09:00:29 +02:00
|
|
|
})
|
2023-07-28 15:04:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
existingBranches = lo.Filter(existingBranches, func(branch string, _ int) bool {
|
|
|
|
return branch != ""
|
|
|
|
})
|
|
|
|
|
|
|
|
return existingBranches
|
2020-09-02 12:39:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func ignoringWarnings(commandOutput string) string {
|
|
|
|
trimmedOutput := strings.TrimSpace(commandOutput)
|
|
|
|
split := strings.Split(trimmedOutput, "\n")
|
|
|
|
// need to get last line in case the first line is a warning about how the error is ambiguous.
|
|
|
|
// At some point we should find a way to make it unambiguous
|
|
|
|
lastLine := split[len(split)-1]
|
|
|
|
|
|
|
|
return lastLine
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2020-08-27 00:42:42 +02:00
|
|
|
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
|
|
|
|
// all commits above this are deemed unpushed and marked as such.
|
2021-12-30 04:35:10 +02:00
|
|
|
func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
|
2023-05-21 09:00:29 +02:00
|
|
|
output, err := self.cmd.New(
|
|
|
|
NewGitCmd("merge-base").
|
|
|
|
Arg(refName).
|
|
|
|
Arg(strings.TrimPrefix(refName, "refs/heads/") + "@{u}").
|
|
|
|
ToArgv(),
|
|
|
|
).
|
2022-01-05 02:57:32 +02:00
|
|
|
DontLog().
|
2021-12-30 02:22:29 +02:00
|
|
|
RunWithOutput()
|
2019-02-19 00:18:30 +02:00
|
|
|
if err != nil {
|
2020-08-27 00:42:42 +02:00
|
|
|
return "", err
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
2020-09-02 12:39:24 +02:00
|
|
|
|
|
|
|
return ignoringWarnings(output), nil
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
|
|
|
|
2020-01-11 09:23:35 +02:00
|
|
|
// getLog gets the git log.
|
2021-12-30 04:35:10 +02:00
|
|
|
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
2021-12-30 02:22:29 +02:00
|
|
|
config := self.UserConfig.Git.Log
|
2021-11-02 11:05:23 +02:00
|
|
|
|
2023-05-21 09:00:29 +02:00
|
|
|
cmdArgs := NewGitCmd("log").
|
|
|
|
Arg(opts.RefName).
|
2023-05-19 12:18:02 +02:00
|
|
|
ArgIf(config.Order != "default", "--"+config.Order).
|
|
|
|
ArgIf(opts.All, "--all").
|
|
|
|
Arg("--oneline").
|
|
|
|
Arg(prettyFormat).
|
|
|
|
Arg("--abbrev=40").
|
|
|
|
ArgIf(opts.Limit, "-300").
|
|
|
|
ArgIf(opts.FilterPath != "", "--follow").
|
|
|
|
Arg("--no-show-signature").
|
|
|
|
Arg("--").
|
2023-05-21 09:00:29 +02:00
|
|
|
ArgIf(opts.FilterPath != "", opts.FilterPath).
|
|
|
|
ToArgv()
|
2023-05-19 12:18:02 +02:00
|
|
|
|
2023-05-21 09:00:29 +02:00
|
|
|
return self.cmd.New(cmdArgs).DontLog()
|
2019-02-19 00:18:30 +02:00
|
|
|
}
|
2021-10-30 08:42:52 +02:00
|
|
|
|
2023-06-21 19:30:25 +02:00
|
|
|
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s`
|