mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-02-01 13:17:53 +02:00
7b302d8c29
Afero is a package that lets you mock out a filesystem with an in-memory filesystem. It allows us to easily create the files required for a given test without worrying about a cleanup step or different tests tripping on eachother when run in parallel. Later on I'll standardise on using afero over the vanilla os package
257 lines
6.9 KiB
Go
257 lines
6.9 KiB
Go
package git_commands
|
|
|
|
import (
|
|
iofs "io/fs"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
type WorktreeLoader struct {
|
|
*GitCommon
|
|
}
|
|
|
|
func NewWorktreeLoader(gitCommon *GitCommon) *WorktreeLoader {
|
|
return &WorktreeLoader{GitCommon: gitCommon}
|
|
}
|
|
|
|
func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
|
|
currentRepoPath := self.repoPaths.RepoPath()
|
|
worktreePath := self.repoPaths.WorktreePath()
|
|
|
|
cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain").ToArgv()
|
|
worktreesOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
splitLines := strings.Split(
|
|
utils.NormalizeLinefeeds(worktreesOutput), "\n",
|
|
)
|
|
|
|
var worktrees []*models.Worktree
|
|
var current *models.Worktree
|
|
for _, splitLine := range splitLines {
|
|
// worktrees are defined over multiple lines and are separated by blank lines
|
|
// so if we reach a blank line we're done with the current worktree
|
|
if len(splitLine) == 0 && current != nil {
|
|
worktrees = append(worktrees, current)
|
|
current = nil
|
|
continue
|
|
}
|
|
|
|
// ignore bare repo (not sure why it's even appearing in this list: it's not a worktree)
|
|
if splitLine == "bare" {
|
|
current = nil
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(splitLine, "worktree ") {
|
|
path := strings.SplitN(splitLine, " ", 2)[1]
|
|
isMain := path == currentRepoPath
|
|
isCurrent := path == worktreePath
|
|
isPathMissing := self.pathExists(path)
|
|
|
|
var gitDir string
|
|
gitDir, err := worktreeGitDirPath(self.Fs, path)
|
|
if err != nil {
|
|
self.Log.Warnf("Could not find git dir for worktree %s: %v", path, err)
|
|
}
|
|
|
|
current = &models.Worktree{
|
|
IsMain: isMain,
|
|
IsCurrent: isCurrent,
|
|
IsPathMissing: isPathMissing,
|
|
Path: path,
|
|
GitDir: gitDir,
|
|
}
|
|
} else if strings.HasPrefix(splitLine, "branch ") {
|
|
branch := strings.SplitN(splitLine, " ", 2)[1]
|
|
current.Branch = strings.TrimPrefix(branch, "refs/heads/")
|
|
}
|
|
}
|
|
|
|
names := getUniqueNamesFromPaths(lo.Map(worktrees, func(worktree *models.Worktree, _ int) string {
|
|
return worktree.Path
|
|
}))
|
|
|
|
for index, worktree := range worktrees {
|
|
worktree.Name = names[index]
|
|
}
|
|
|
|
// move current worktree to the top
|
|
for i, worktree := range worktrees {
|
|
if worktree.IsCurrent {
|
|
worktrees = append(worktrees[:i], worktrees[i+1:]...)
|
|
worktrees = append([]*models.Worktree{worktree}, worktrees...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Some worktrees are on a branch but are mid-rebase, and in those cases,
|
|
// `git worktree list` will not show the branch name. We can get the branch
|
|
// name from the `rebase-merge/head-name` file (if it exists) in the folder
|
|
// for the worktree in the parent repo's .git/worktrees folder.
|
|
for _, worktree := range worktrees {
|
|
// No point checking if we already have a branch name
|
|
if worktree.Branch != "" {
|
|
continue
|
|
}
|
|
|
|
// If we couldn't find the git directory, we can't find the branch name
|
|
if worktree.GitDir == "" {
|
|
continue
|
|
}
|
|
|
|
rebasedBranch, ok := self.rebasedBranch(worktree)
|
|
if ok {
|
|
worktree.Branch = rebasedBranch
|
|
continue
|
|
}
|
|
|
|
bisectedBranch, ok := self.bisectedBranch(worktree)
|
|
if ok {
|
|
worktree.Branch = bisectedBranch
|
|
continue
|
|
}
|
|
}
|
|
|
|
return worktrees, nil
|
|
}
|
|
|
|
func (self *WorktreeLoader) pathExists(path string) bool {
|
|
if _, err := self.Fs.Stat(path); err != nil {
|
|
if errors.Is(err, iofs.ErrNotExist) {
|
|
return true
|
|
}
|
|
self.Log.Errorf("failed to check if worktree path `%s` exists\n%v", path, err)
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (self *WorktreeLoader) rebasedBranch(worktree *models.Worktree) (string, bool) {
|
|
for _, dir := range []string{"rebase-merge", "rebase-apply"} {
|
|
if bytesContent, err := afero.ReadFile(self.Fs, filepath.Join(worktree.GitDir, dir, "head-name")); err == nil {
|
|
headName := strings.TrimSpace(string(bytesContent))
|
|
shortHeadName := strings.TrimPrefix(headName, "refs/heads/")
|
|
return shortHeadName, true
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
func (self *WorktreeLoader) bisectedBranch(worktree *models.Worktree) (string, bool) {
|
|
bisectStartPath := filepath.Join(worktree.GitDir, "BISECT_START")
|
|
startContent, err := afero.ReadFile(self.Fs, bisectStartPath)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
return strings.TrimSpace(string(startContent)), true
|
|
}
|
|
|
|
type pathWithIndexT struct {
|
|
path string
|
|
index int
|
|
}
|
|
|
|
type nameWithIndexT struct {
|
|
name string
|
|
index int
|
|
}
|
|
|
|
func getUniqueNamesFromPaths(paths []string) []string {
|
|
pathsWithIndex := lo.Map(paths, func(path string, index int) pathWithIndexT {
|
|
return pathWithIndexT{path, index}
|
|
})
|
|
|
|
namesWithIndex := getUniqueNamesFromPathsAux(pathsWithIndex, 0)
|
|
|
|
// now sort based on index
|
|
result := make([]string, len(namesWithIndex))
|
|
for _, nameWithIndex := range namesWithIndex {
|
|
result[nameWithIndex.index] = nameWithIndex.name
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func getUniqueNamesFromPathsAux(paths []pathWithIndexT, depth int) []nameWithIndexT {
|
|
// If we have no paths, return an empty array
|
|
if len(paths) == 0 {
|
|
return []nameWithIndexT{}
|
|
}
|
|
|
|
// If we have only one path, return the last segment of the path
|
|
if len(paths) == 1 {
|
|
path := paths[0]
|
|
return []nameWithIndexT{{index: path.index, name: sliceAtDepth(path.path, depth)}}
|
|
}
|
|
|
|
// group the paths by their value at the specified depth
|
|
groups := make(map[string][]pathWithIndexT)
|
|
for _, path := range paths {
|
|
value := valueAtDepth(path.path, depth)
|
|
groups[value] = append(groups[value], path)
|
|
}
|
|
|
|
result := []nameWithIndexT{}
|
|
for _, group := range groups {
|
|
if len(group) == 1 {
|
|
path := group[0]
|
|
result = append(result, nameWithIndexT{index: path.index, name: sliceAtDepth(path.path, depth)})
|
|
} else {
|
|
result = append(result, getUniqueNamesFromPathsAux(group, depth+1)...)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// if the path is /a/b/c/d, and the depth is 0, the value is 'd'. If the depth is 1, the value is 'c', etc
|
|
func valueAtDepth(path string, depth int) string {
|
|
path = strings.TrimPrefix(path, "/")
|
|
path = strings.TrimSuffix(path, "/")
|
|
|
|
// Split the path into segments
|
|
segments := strings.Split(path, "/")
|
|
|
|
// Get the length of segments
|
|
length := len(segments)
|
|
|
|
// If the depth is greater than the length of segments, return an empty string
|
|
if depth >= length {
|
|
return ""
|
|
}
|
|
|
|
// Return the segment at the specified depth from the end of the path
|
|
return segments[length-1-depth]
|
|
}
|
|
|
|
// if the path is /a/b/c/d, and the depth is 0, the value is 'd'. If the depth is 1, the value is 'b/c', etc
|
|
func sliceAtDepth(path string, depth int) string {
|
|
path = strings.TrimPrefix(path, "/")
|
|
path = strings.TrimSuffix(path, "/")
|
|
|
|
// Split the path into segments
|
|
segments := strings.Split(path, "/")
|
|
|
|
// Get the length of segments
|
|
length := len(segments)
|
|
|
|
// If the depth is greater than or equal to the length of segments, return an empty string
|
|
if depth >= length {
|
|
return ""
|
|
}
|
|
|
|
// Join the segments from the specified depth till end of the path
|
|
return strings.Join(segments[length-1-depth:], "/")
|
|
}
|