1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-14 11:23:09 +02:00
lazygit/pkg/commands/git_commands/branch_loader.go
Joel Baranick 9a79154d05 Hide worktrees in the worktree panel if they point at a non-existing filesystem location.
Remove unneeded check when filtering out branches from non-current worktrees from the branch panel.
Add link icon for linked worktrees
2023-07-30 18:35:21 +10:00

262 lines
7.1 KiB
Go

package git_commands
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/generics/slices"
"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"
)
// 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 = slices.Remove(branches, j)
continue outer
}
}
}
branches = slices.Prepend(branches, branchesWithRecency...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = slices.Move(branches, i, 0)
break
}
}
if !foundHead {
info, err := self.getCurrentBranchInfo()
if err != nil {
return nil, err
}
branches = slices.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 {
currentDir, err := os.Getwd()
if err != nil {
panic(err)
}
output, err := self.getRawBranches()
if err != nil {
panic(err)
}
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
return slices.FilterMap(outputLines, func(line string) (*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
}
branchDir := split[6]
if len(branchDir) > 0 && branchDir != currentDir {
// Ignore line because it is a branch checked out in a different worktree
// Branches which are not checked out will not have a path, so we should not ignore them.
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),
"worktreepath",
}
// 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
}