mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
The existing git log logic fetched the first 300 commits of a repo and displayed them in the local and sub-commit views. Once a user selected a commit beyond a threshold of 200 commits the whole repository was loaded. This is problematic with large repos e.g. the linux kernel with currently ~138k commits as lazygit slows down substantially with such a large number of commits in memory. This commit replaces the current all or only the first 300 commits logic with an incremental fetching approach: 1. The first 400 commits of repo are loaded by default. 2. If the user selects a commit beyond a threshold (current limit-100) the git log limit is increased by 400 if there are more commits available. If there are more commits available is currently checked by comparing the previous log limit with the real commit count in the model, if the commit count is less then the limit it is assumed that we reached the end of the commit log. Ideally it would be better to call `git rev-list --count xyz` in the right places and compare with this result, but this requires more changes. Adding a "paginated implementation" by utilizing `git log --skip=x --max-count=y` and appending commits to the model instead of replacing the whole collection would be nice, but this requires deeper changes to keep everything consistent. Signed-off-by: Stefan Kerkmann <s.kerkmann@pengutronix.de> Co-authored-by: DeepSeek V4 Flash
652 lines
21 KiB
Go
652 lines
21 KiB
Go
package git_commands
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/jesseduffield/generics/set"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/jesseduffield/lazygit/pkg/common"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
"github.com/stefanhaller/git-todo-parser/todo"
|
|
)
|
|
|
|
// 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
|
|
|
|
// CommitLoader returns a list of Commit objects for the current repo
|
|
type CommitLoader struct {
|
|
*common.Common
|
|
cmd oscommands.ICmdObjBuilder
|
|
|
|
getWorkingTreeState func() models.WorkingTreeState
|
|
readFile func(filename string) ([]byte, error)
|
|
walkFiles func(root string, fn filepath.WalkFunc) error
|
|
dotGitDir string
|
|
*GitCommon
|
|
}
|
|
|
|
// making our dependencies explicit for the sake of easier testing
|
|
func NewCommitLoader(
|
|
cmn *common.Common,
|
|
cmd oscommands.ICmdObjBuilder,
|
|
getWorkingTreeState func() models.WorkingTreeState,
|
|
gitCommon *GitCommon,
|
|
) *CommitLoader {
|
|
return &CommitLoader{
|
|
Common: cmn,
|
|
cmd: cmd,
|
|
getWorkingTreeState: getWorkingTreeState,
|
|
readFile: os.ReadFile,
|
|
walkFiles: filepath.Walk,
|
|
GitCommon: gitCommon,
|
|
}
|
|
}
|
|
|
|
const (
|
|
GIT_LOG_FETCH_COUNT = 400
|
|
GIT_LOG_FETCH_THRESHOLD = GIT_LOG_FETCH_COUNT / 4
|
|
)
|
|
|
|
type GitLogLimit struct {
|
|
Limit int
|
|
}
|
|
|
|
func DefaultGitLogLimit() *GitLogLimit {
|
|
return &GitLogLimit{Limit: GIT_LOG_FETCH_COUNT}
|
|
}
|
|
|
|
func (self *GitLogLimit) CanFetchMoreCommits(lineIdx, logCommitCount int) bool {
|
|
return lineIdx >= logCommitCount-GIT_LOG_FETCH_THRESHOLD && logCommitCount >= self.Limit
|
|
}
|
|
|
|
func (self *GitLogLimit) Increase(realCommitCount int) {
|
|
self.Limit = realCommitCount + GIT_LOG_FETCH_COUNT
|
|
}
|
|
|
|
type GetCommitsOptions struct {
|
|
LogLimit *GitLogLimit
|
|
FilterPath string
|
|
FilterAuthor string
|
|
IncludeRebaseCommits bool
|
|
RefName string // e.g. "HEAD" or "my_branch"
|
|
RefForPushedStatus models.Ref // the ref to use for determining pushed/unpushed status
|
|
// determines if we show the whole git graph i.e. pass the '--all' flag
|
|
All bool
|
|
// If non-empty, show divergence from this ref (left-right log)
|
|
RefToShowDivergenceFrom string
|
|
MainBranches *MainBranches
|
|
HashPool *utils.StringPool
|
|
}
|
|
|
|
// GetCommits obtains the commits of the current branch
|
|
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
|
|
commits := []*models.Commit{}
|
|
|
|
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
|
|
var err error
|
|
commits, err = self.MergeRebasingCommits(opts.HashPool, commits)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
wg := sync.WaitGroup{}
|
|
|
|
wg.Add(2)
|
|
|
|
var logErr error
|
|
go utils.Safe(func() {
|
|
defer wg.Done()
|
|
|
|
var realCommits []*models.Commit
|
|
realCommits, logErr = loadCommits(self.getLogCmd(opts), opts.FilterPath, func(line string) (*models.Commit, bool) {
|
|
return self.extractCommitFromLine(opts.HashPool, line, opts.RefToShowDivergenceFrom != ""), false
|
|
})
|
|
if logErr == nil {
|
|
commits = append(commits, realCommits...)
|
|
}
|
|
})
|
|
|
|
var unmergedCommitHashes *set.Set[string]
|
|
var remoteUnmergedCommitHashes *set.Set[string]
|
|
mainBranches := opts.MainBranches.Get()
|
|
|
|
go utils.Safe(func() {
|
|
defer wg.Done()
|
|
|
|
if len(mainBranches) > 0 {
|
|
unmergedCommitHashes = self.getReachableHashes(opts.RefName, mainBranches)
|
|
if opts.RefToShowDivergenceFrom != "" {
|
|
remoteUnmergedCommitHashes = self.getReachableHashes(opts.RefToShowDivergenceFrom, mainBranches)
|
|
}
|
|
}
|
|
})
|
|
|
|
var unpushedCommitHashes *set.Set[string]
|
|
if opts.RefForPushedStatus != nil {
|
|
unpushedCommitHashes = self.getReachableHashes(opts.RefForPushedStatus.FullRefName(),
|
|
append([]string{opts.RefForPushedStatus.RefName() + "@{u}"}, mainBranches...))
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if logErr != nil {
|
|
return nil, logErr
|
|
}
|
|
|
|
if len(commits) == 0 {
|
|
return commits, nil
|
|
}
|
|
|
|
if opts.RefToShowDivergenceFrom != "" {
|
|
sort.SliceStable(commits, func(i, j int) bool {
|
|
// In the divergence view we want incoming commits to come first
|
|
return commits[i].Divergence > commits[j].Divergence
|
|
})
|
|
|
|
_, localSectionStart, found := lo.FindIndexOf(commits, func(commit *models.Commit) bool {
|
|
return commit.Divergence == models.DivergenceLeft
|
|
})
|
|
if !found {
|
|
localSectionStart = len(commits)
|
|
}
|
|
|
|
setCommitStatuses(unpushedCommitHashes, remoteUnmergedCommitHashes, commits[:localSectionStart])
|
|
setCommitStatuses(unpushedCommitHashes, unmergedCommitHashes, commits[localSectionStart:])
|
|
} else {
|
|
setCommitStatuses(unpushedCommitHashes, unmergedCommitHashes, commits)
|
|
}
|
|
|
|
return commits, nil
|
|
}
|
|
|
|
func (self *CommitLoader) MergeRebasingCommits(hashPool *utils.StringPool, commits []*models.Commit) ([]*models.Commit, error) {
|
|
// 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 {
|
|
if !commit.IsTODO() { // removing the existing rebase commits so we can add the refreshed ones
|
|
result = append(result, commits[i:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
workingTreeState := self.getWorkingTreeState()
|
|
addConflictedRebasingCommit := true
|
|
if workingTreeState.CherryPicking || workingTreeState.Reverting {
|
|
sequencerCommits, err := self.getHydratedSequencerCommits(hashPool, workingTreeState)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(sequencerCommits, result...)
|
|
addConflictedRebasingCommit = false
|
|
}
|
|
|
|
if workingTreeState.Rebasing {
|
|
rebasingCommits, err := self.getHydratedRebasingCommits(hashPool, commits, addConflictedRebasingCommit)
|
|
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 hash, 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
|
|
func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line string, showDivergence bool) *models.Commit {
|
|
split := strings.SplitN(line, "\x00", 8)
|
|
|
|
// Ensure we have the minimum required fields (at least 7 for basic functionality)
|
|
if len(split) < 7 {
|
|
self.Log.Warnf("Malformed git log line: expected at least 7 fields, got %d. Line: %s", len(split), line)
|
|
return nil
|
|
}
|
|
|
|
hash := split[0]
|
|
unixTimestamp := split[1]
|
|
authorName := split[2]
|
|
authorEmail := split[3]
|
|
parentHashes := split[4]
|
|
divergence := models.DivergenceNone
|
|
if showDivergence {
|
|
divergence = lo.Ternary(split[5] == "<", models.DivergenceLeft, models.DivergenceRight)
|
|
}
|
|
extraInfo := strings.TrimSpace(split[6])
|
|
|
|
// message (and the \x00 before it) might not be present if extraInfo is extremely long
|
|
message := ""
|
|
if len(split) > 7 {
|
|
message = split[7]
|
|
}
|
|
|
|
var tags []string
|
|
|
|
if extraInfo != "" {
|
|
extraInfoFields := strings.SplitSeq(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])
|
|
}
|
|
}
|
|
|
|
extraInfo = "(" + extraInfo + ")"
|
|
}
|
|
|
|
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
|
|
|
|
parents := []string{}
|
|
if len(parentHashes) > 0 {
|
|
parents = strings.Split(parentHashes, " ")
|
|
}
|
|
|
|
return models.NewCommit(hashPool, models.NewCommitOpts{
|
|
Hash: hash,
|
|
Name: message,
|
|
Tags: tags,
|
|
ExtraInfo: extraInfo,
|
|
UnixTimestamp: int64(unitTimestampInt),
|
|
AuthorName: authorName,
|
|
AuthorEmail: authorEmail,
|
|
Parents: parents,
|
|
Divergence: divergence,
|
|
})
|
|
}
|
|
|
|
func (self *CommitLoader) getHydratedRebasingCommits(hashPool *utils.StringPool, existingCommits []*models.Commit, addConflictingCommit bool) ([]*models.Commit, error) {
|
|
return self.getHydratedTodoCommits(hashPool, self.getRebasingCommits(hashPool, addConflictingCommit), existingCommits, false)
|
|
}
|
|
|
|
func (self *CommitLoader) getHydratedSequencerCommits(hashPool *utils.StringPool, workingTreeState models.WorkingTreeState) ([]*models.Commit, error) {
|
|
commits := self.getSequencerCommits(hashPool)
|
|
if len(commits) > 0 {
|
|
// If we have any commits in .git/sequencer/todo, then the last one of
|
|
// those is the conflicting one.
|
|
commits[len(commits)-1].Status = models.StatusConflicted
|
|
} else {
|
|
// For single-commit cherry-picks and reverts, git apparently doesn't
|
|
// use the sequencer; in that case, CHERRY_PICK_HEAD or REVERT_HEAD is
|
|
// our conflicting commit, so synthesize it here.
|
|
conflicedCommit := self.getConflictedSequencerCommit(hashPool, workingTreeState)
|
|
if conflicedCommit != nil {
|
|
commits = append(commits, conflicedCommit)
|
|
}
|
|
}
|
|
|
|
return self.getHydratedTodoCommits(hashPool, commits, nil, true)
|
|
}
|
|
|
|
func (self *CommitLoader) getHydratedTodoCommits(
|
|
hashPool *utils.StringPool,
|
|
todoCommits []*models.Commit,
|
|
existingCommits []*models.Commit,
|
|
todoFileHasShortHashes bool,
|
|
) ([]*models.Commit, error) {
|
|
if len(todoCommits) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// A refresh of only the rebasing todos should reuse the already loaded todos to avoid
|
|
// unnecessary git show calls.
|
|
fullCommits := map[string]*models.Commit{}
|
|
for _, commit := range existingCommits {
|
|
if commit.IsTODO() && commit.Hash() != "" {
|
|
// Make a copy of the commit; that's necessary to avoid mutating the original commit
|
|
// when we later reuse it in the loop at the end of this function.
|
|
fullCommits[commit.Hash()] = lo.ToPtr(*commit)
|
|
}
|
|
}
|
|
|
|
commitHashesToFetch := lo.FilterMap(todoCommits, func(commit *models.Commit, _ int) (string, bool) {
|
|
return commit.Hash(), commit.Hash() != "" && fullCommits[commit.Hash()] == nil
|
|
})
|
|
|
|
if len(commitHashesToFetch) > 0 {
|
|
// note that we're not filtering these as we do non-rebasing commits just because
|
|
// I suspect that will cause some damage
|
|
cmdObj := self.cmd.New(
|
|
NewGitCmd("show").
|
|
Config("log.showSignature=false").
|
|
Arg("--no-patch", "--oneline", "--abbrev=20", prettyFormat).
|
|
Arg(commitHashesToFetch...).
|
|
ToArgv(),
|
|
).DontLog()
|
|
|
|
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
|
if line == "" || line[0] != '+' {
|
|
return false, nil
|
|
}
|
|
commit := self.extractCommitFromLine(hashPool, line[1:], false)
|
|
fullCommits[commit.Hash()] = commit
|
|
return false, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
findFullCommit := lo.Ternary(todoFileHasShortHashes,
|
|
func(hash string) *models.Commit {
|
|
for s, c := range fullCommits {
|
|
if strings.HasPrefix(s, hash) {
|
|
return c
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
func(hash string) *models.Commit {
|
|
return fullCommits[hash]
|
|
})
|
|
|
|
hydratedCommits := make([]*models.Commit, 0, len(todoCommits))
|
|
for _, rebasingCommit := range todoCommits {
|
|
if rebasingCommit.Hash() == "" {
|
|
hydratedCommits = append(hydratedCommits, rebasingCommit)
|
|
} else if commit := findFullCommit(rebasingCommit.Hash()); commit != nil {
|
|
commit.Action = rebasingCommit.Action
|
|
commit.ActionFlag = rebasingCommit.ActionFlag
|
|
commit.Status = rebasingCommit.Status
|
|
hydratedCommits = append(hydratedCommits, commit)
|
|
}
|
|
}
|
|
return hydratedCommits, nil
|
|
}
|
|
|
|
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
|
|
|
// git-rebase-todo example:
|
|
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
|
|
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
|
|
func (self *CommitLoader) getRebasingCommits(hashPool *utils.StringPool, addConflictingCommit bool) []*models.Commit {
|
|
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"))
|
|
if err != nil {
|
|
self.Log.Error(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
|
|
}
|
|
|
|
commits := []*models.Commit{}
|
|
|
|
todos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
|
|
if err != nil {
|
|
self.Log.Error(fmt.Sprintf("error occurred while parsing git-rebase-todo file: %s", err.Error()))
|
|
return nil
|
|
}
|
|
|
|
// See if the current commit couldn't be applied because it conflicted; if
|
|
// so, add a fake entry for it
|
|
if addConflictingCommit {
|
|
if conflictedCommit := self.getConflictedCommit(hashPool, todos); conflictedCommit != nil {
|
|
commits = append(commits, conflictedCommit)
|
|
}
|
|
}
|
|
|
|
for _, t := range todos {
|
|
if t.Command == todo.UpdateRef {
|
|
t.Msg = t.Ref
|
|
} else if t.Command == todo.Exec {
|
|
t.Msg = t.ExecCommand
|
|
} else if t.Commit == "" {
|
|
// Command does not have a commit associated, skip
|
|
continue
|
|
}
|
|
commits = utils.Prepend(commits, models.NewCommit(hashPool, models.NewCommitOpts{
|
|
Hash: t.Commit,
|
|
Name: t.Msg,
|
|
Status: models.StatusRebasing,
|
|
Action: t.Command,
|
|
ActionFlag: t.Flag,
|
|
}))
|
|
}
|
|
|
|
return commits
|
|
}
|
|
|
|
func (self *CommitLoader) getConflictedCommit(hashPool *utils.StringPool, todos []todo.Todo) *models.Commit {
|
|
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/done"))
|
|
if err != nil {
|
|
self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error()))
|
|
return nil
|
|
}
|
|
|
|
doneTodos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
|
|
if err != nil {
|
|
self.Log.Error(fmt.Sprintf("error occurred while parsing rebase-merge/done file: %s", err.Error()))
|
|
return nil
|
|
}
|
|
|
|
amendFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend"))
|
|
messageFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/message"))
|
|
|
|
return self.getConflictedCommitImpl(hashPool, todos, doneTodos, amendFileExists, messageFileExists)
|
|
}
|
|
|
|
func (self *CommitLoader) getConflictedCommitImpl(hashPool *utils.StringPool, todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool, messageFileExists bool) *models.Commit {
|
|
// 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 nil
|
|
}
|
|
lastTodo := doneTodos[len(doneTodos)-1]
|
|
if lastTodo.Command == todo.Break || lastTodo.Command == todo.Exec || lastTodo.Command == todo.Reword {
|
|
return nil
|
|
}
|
|
|
|
// 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 nil
|
|
}
|
|
|
|
// 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 nil
|
|
}
|
|
|
|
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 nil
|
|
}
|
|
|
|
if !messageFileExists {
|
|
// As an additional check, see if the "message" file exists; if it
|
|
// doesn't, it must be because a multi-commit cherry-pick or revert
|
|
// was performed in the meantime, which deleted both the amend file
|
|
// and the message file.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// 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 nil
|
|
}
|
|
|
|
// 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 models.NewCommit(hashPool, models.NewCommitOpts{
|
|
Hash: lastTodo.Commit,
|
|
Action: lastTodo.Command,
|
|
Status: models.StatusConflicted,
|
|
})
|
|
}
|
|
|
|
func (self *CommitLoader) getSequencerCommits(hashPool *utils.StringPool) []*models.Commit {
|
|
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "sequencer/todo"))
|
|
if err != nil {
|
|
self.Log.Error(fmt.Sprintf("error occurred reading sequencer/todo: %s", err.Error()))
|
|
// we assume an error means the file doesn't exist so we just return
|
|
return nil
|
|
}
|
|
|
|
commits := []*models.Commit{}
|
|
|
|
todos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
|
|
if err != nil {
|
|
self.Log.Error(fmt.Sprintf("error occurred while parsing sequencer/todo file: %s", err.Error()))
|
|
return nil
|
|
}
|
|
|
|
for _, t := range todos {
|
|
if t.Commit == "" {
|
|
// Command does not have a commit associated, skip
|
|
continue
|
|
}
|
|
commits = utils.Prepend(commits, models.NewCommit(hashPool, models.NewCommitOpts{
|
|
Hash: t.Commit,
|
|
Name: t.Msg,
|
|
Status: models.StatusCherryPickingOrReverting,
|
|
Action: t.Command,
|
|
}))
|
|
}
|
|
|
|
return commits
|
|
}
|
|
|
|
func (self *CommitLoader) getConflictedSequencerCommit(hashPool *utils.StringPool, workingTreeState models.WorkingTreeState) *models.Commit {
|
|
var shaFile string
|
|
var action todo.TodoCommand
|
|
if workingTreeState.CherryPicking {
|
|
shaFile = "CHERRY_PICK_HEAD"
|
|
action = todo.Pick
|
|
} else if workingTreeState.Reverting {
|
|
shaFile = "REVERT_HEAD"
|
|
action = todo.Revert
|
|
} else {
|
|
return nil
|
|
}
|
|
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), shaFile))
|
|
if err != nil {
|
|
self.Log.Error(fmt.Sprintf("error occurred reading %s: %s", shaFile, err.Error()))
|
|
// we assume an error means the file doesn't exist so we just return
|
|
return nil
|
|
}
|
|
lines := strings.Split(string(bytesContent), "\n")
|
|
if len(lines) == 0 {
|
|
return nil
|
|
}
|
|
return models.NewCommit(hashPool, models.NewCommitOpts{
|
|
Hash: lines[0],
|
|
Status: models.StatusConflicted,
|
|
Action: action,
|
|
})
|
|
}
|
|
|
|
func setCommitStatuses(unpushedCommitHashes *set.Set[string], unmergedCommitHashes *set.Set[string], commits []*models.Commit) {
|
|
for i, commit := range commits {
|
|
if commit.IsTODO() {
|
|
continue
|
|
}
|
|
|
|
if unmergedCommitHashes == nil || unmergedCommitHashes.Includes(commit.Hash()) {
|
|
if unpushedCommitHashes != nil && unpushedCommitHashes.Includes(commit.Hash()) {
|
|
commits[i].Status = models.StatusUnpushed
|
|
} else {
|
|
commits[i].Status = models.StatusPushed
|
|
}
|
|
} else {
|
|
commits[i].Status = models.StatusMerged
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *CommitLoader) getReachableHashes(refName string, notRefNames []string) *set.Set[string] {
|
|
output, _, err := self.cmd.New(
|
|
NewGitCmd("rev-list").
|
|
Arg(refName).
|
|
Arg(lo.Map(notRefNames, func(name string, _ int) string {
|
|
return "^" + name
|
|
})...).
|
|
ToArgv(),
|
|
).
|
|
DontLog().
|
|
RunWithOutputs()
|
|
if err != nil {
|
|
return set.New[string]()
|
|
}
|
|
|
|
return set.NewFromSlice(utils.SplitLines(output))
|
|
}
|
|
|
|
// getLogCmd gets the git log.
|
|
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj {
|
|
gitLogOrder := self.UserConfig().Git.Log.Order
|
|
|
|
refSpec := opts.RefName
|
|
if opts.RefToShowDivergenceFrom != "" {
|
|
refSpec += "..." + opts.RefToShowDivergenceFrom
|
|
}
|
|
|
|
var limitArg string
|
|
if opts.LogLimit != nil {
|
|
limitArg = fmt.Sprintf("--max-count=%d", opts.LogLimit.Limit)
|
|
}
|
|
|
|
cmdArgs := NewGitCmd("log").
|
|
Arg(refSpec).
|
|
ArgIf(gitLogOrder != "default", "--"+gitLogOrder).
|
|
ArgIf(opts.All, "--all").
|
|
Arg("--oneline").
|
|
Arg(prettyFormat).
|
|
Arg("--abbrev=40").
|
|
ArgIf(opts.FilterAuthor != "", "--author="+opts.FilterAuthor).
|
|
ArgIf(limitArg != "", limitArg).
|
|
ArgIf(opts.FilterPath != "", "--follow", "--name-status").
|
|
Arg("--no-show-signature").
|
|
ArgIf(opts.RefToShowDivergenceFrom != "", "--left-right").
|
|
Arg("--").
|
|
ArgIf(opts.FilterPath != "", opts.FilterPath).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).DontLog()
|
|
}
|
|
|
|
const prettyFormat = `--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s`
|