mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-04 22:34:39 +02:00
We've been sometimes using lo and sometimes using my slices package, and we need to pick one for consistency. Lo is more extensive and better maintained so we're going with that. My slices package was a superset of go's own slices package so in some places I've just used the official one (the methods were just wrappers anyway). I've also moved the remaining methods into the utils package.
254 lines
6.9 KiB
Go
254 lines
6.9 KiB
Go
package git_commands
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/generics/set"
|
|
"github.com/jesseduffield/go-git/v5/config"
|
|
"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"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
// context:
|
|
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
|
|
// which `git branch -a` gives us, but we also want the recency data that
|
|
// git reflog gives us.
|
|
// So we get the HEAD, then append get the reflog branches that intersect with
|
|
// 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
|
|
|
|
type BranchLoaderConfigCommands interface {
|
|
Branches() (map[string]*config.Branch, error)
|
|
}
|
|
|
|
type BranchInfo struct {
|
|
RefName string
|
|
DisplayName string // e.g. '(HEAD detached at 123asdf)'
|
|
DetachedHead bool
|
|
}
|
|
|
|
// BranchLoader returns a list of Branch objects for the current repo
|
|
type BranchLoader struct {
|
|
*common.Common
|
|
cmd oscommands.ICmdObjBuilder
|
|
getCurrentBranchInfo func() (BranchInfo, error)
|
|
config BranchLoaderConfigCommands
|
|
}
|
|
|
|
func NewBranchLoader(
|
|
cmn *common.Common,
|
|
cmd oscommands.ICmdObjBuilder,
|
|
getCurrentBranchInfo func() (BranchInfo, error),
|
|
config BranchLoaderConfigCommands,
|
|
) *BranchLoader {
|
|
return &BranchLoader{
|
|
Common: cmn,
|
|
cmd: cmd,
|
|
getCurrentBranchInfo: getCurrentBranchInfo,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// Load the list of branches for the current repo
|
|
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
|
|
branches := self.obtainBranches()
|
|
|
|
reflogBranches := self.obtainReflogBranches(reflogCommits)
|
|
|
|
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
|
|
branchesWithRecency := make([]*models.Branch, 0)
|
|
outer:
|
|
for _, reflogBranch := range reflogBranches {
|
|
for j, branch := range branches {
|
|
if branch.Head {
|
|
continue
|
|
}
|
|
if strings.EqualFold(reflogBranch.Name, branch.Name) {
|
|
branch.Recency = reflogBranch.Recency
|
|
branchesWithRecency = append(branchesWithRecency, branch)
|
|
branches = utils.Remove(branches, j)
|
|
continue outer
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort branches that don't have a recency value alphabetically
|
|
// (we're really doing this for the sake of deterministic behaviour across git versions)
|
|
slices.SortFunc(branches, func(a *models.Branch, b *models.Branch) bool {
|
|
return a.Name < b.Name
|
|
})
|
|
|
|
branches = utils.Prepend(branches, branchesWithRecency...)
|
|
|
|
foundHead := false
|
|
for i, branch := range branches {
|
|
if branch.Head {
|
|
foundHead = true
|
|
branch.Recency = " *"
|
|
branches = utils.Move(branches, i, 0)
|
|
break
|
|
}
|
|
}
|
|
if !foundHead {
|
|
info, err := self.getCurrentBranchInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
branches = utils.Prepend(branches, &models.Branch{Name: info.RefName, DisplayName: info.DisplayName, Head: true, DetachedHead: info.DetachedHead, Recency: " *"})
|
|
}
|
|
|
|
configBranches, err := self.config.Branches()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, branch := range branches {
|
|
match := configBranches[branch.Name]
|
|
if match != nil {
|
|
branch.UpstreamRemote = match.Remote
|
|
branch.UpstreamBranch = match.Merge.Short()
|
|
}
|
|
}
|
|
|
|
return branches, nil
|
|
}
|
|
|
|
func (self *BranchLoader) obtainBranches() []*models.Branch {
|
|
output, err := self.getRawBranches()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
trimmedOutput := strings.TrimSpace(output)
|
|
outputLines := strings.Split(trimmedOutput, "\n")
|
|
|
|
return lo.FilterMap(outputLines, func(line string, _ int) (*models.Branch, bool) {
|
|
if line == "" {
|
|
return nil, false
|
|
}
|
|
|
|
split := strings.Split(line, "\x00")
|
|
if len(split) != len(branchFields) {
|
|
// Ignore line if it isn't separated into the expected number of parts
|
|
// This is probably a warning message, for more info see:
|
|
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
|
|
return nil, false
|
|
}
|
|
|
|
return obtainBranch(split), true
|
|
})
|
|
}
|
|
|
|
func (self *BranchLoader) getRawBranches() (string, error) {
|
|
format := strings.Join(
|
|
lo.Map(branchFields, func(thing string, _ int) string {
|
|
return "%(" + thing + ")"
|
|
}),
|
|
"%00",
|
|
)
|
|
|
|
cmdArgs := NewGitCmd("for-each-ref").
|
|
Arg("--sort=-committerdate").
|
|
Arg(fmt.Sprintf("--format=%s", format)).
|
|
Arg("refs/heads").
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
|
}
|
|
|
|
var branchFields = []string{
|
|
"HEAD",
|
|
"refname:short",
|
|
"upstream:short",
|
|
"upstream:track",
|
|
"subject",
|
|
fmt.Sprintf("objectname:short=%d", utils.COMMIT_HASH_SHORT_SIZE),
|
|
}
|
|
|
|
// Obtain branch information from parsed line output of getRawBranches()
|
|
func obtainBranch(split []string) *models.Branch {
|
|
headMarker := split[0]
|
|
fullName := split[1]
|
|
upstreamName := split[2]
|
|
track := split[3]
|
|
subject := split[4]
|
|
commitHash := split[5]
|
|
|
|
name := strings.TrimPrefix(fullName, "heads/")
|
|
pushables, pullables, gone := parseUpstreamInfo(upstreamName, track)
|
|
|
|
return &models.Branch{
|
|
Name: name,
|
|
Pushables: pushables,
|
|
Pullables: pullables,
|
|
UpstreamGone: gone,
|
|
Head: headMarker == "*",
|
|
Subject: subject,
|
|
CommitHash: commitHash,
|
|
}
|
|
}
|
|
|
|
func parseUpstreamInfo(upstreamName string, track string) (string, string, bool) {
|
|
if upstreamName == "" {
|
|
// if we're here then it means we do not have a local version of the remote.
|
|
// The branch might still be tracking a remote though, we just don't know
|
|
// how many commits ahead/behind it is
|
|
return "?", "?", false
|
|
}
|
|
|
|
if track == "[gone]" {
|
|
return "?", "?", true
|
|
}
|
|
|
|
pushables := parseDifference(track, `ahead (\d+)`)
|
|
pullables := parseDifference(track, `behind (\d+)`)
|
|
|
|
return pushables, pullables, false
|
|
}
|
|
|
|
func parseDifference(track string, regexStr string) string {
|
|
re := regexp.MustCompile(regexStr)
|
|
match := re.FindStringSubmatch(track)
|
|
if len(match) > 1 {
|
|
return match[1]
|
|
} else {
|
|
return "0"
|
|
}
|
|
}
|
|
|
|
// TODO: only look at the new reflog commits, and otherwise store the recencies in
|
|
// int form against the branch to recalculate the time ago
|
|
func (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
|
|
foundBranches := set.New[string]()
|
|
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
|
|
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
|
|
|
|
for _, commit := range reflogCommits {
|
|
match := re.FindStringSubmatch(commit.Name)
|
|
if len(match) != 3 {
|
|
continue
|
|
}
|
|
|
|
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
|
|
for _, branchName := range match[1:] {
|
|
if !foundBranches.Includes(branchName) {
|
|
foundBranches.Add(branchName)
|
|
reflogBranches = append(reflogBranches, &models.Branch{
|
|
Recency: recency,
|
|
Name: branchName,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return reflogBranches
|
|
}
|