mirror of
				https://github.com/jesseduffield/lazygit.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			281 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			281 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package git_commands
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	ioFs "io/fs"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/go-errors/errors"
 | |
| 	"github.com/jesseduffield/lazygit/pkg/env"
 | |
| 	"github.com/samber/lo"
 | |
| 	"github.com/spf13/afero"
 | |
| )
 | |
| 
 | |
| type RepoPaths struct {
 | |
| 	currentPath        string
 | |
| 	worktreePath       string
 | |
| 	worktreeGitDirPath string
 | |
| 	repoPath           string
 | |
| 	repoGitDirPath     string
 | |
| 	repoName           string
 | |
| }
 | |
| 
 | |
| // Current working directory of the program. Currently, this will always
 | |
| // be the same as WorktreePath(), but in future we may support running
 | |
| // lazygit from inside a subdirectory of the worktree.
 | |
| func (self *RepoPaths) CurrentPath() string {
 | |
| 	return self.currentPath
 | |
| }
 | |
| 
 | |
| // Path to the current worktree. If we're in the main worktree, this will
 | |
| // be the same as RepoPath()
 | |
| func (self *RepoPaths) WorktreePath() string {
 | |
| 	return self.worktreePath
 | |
| }
 | |
| 
 | |
| // Path of the worktree's git dir.
 | |
| // If we're in the main worktree, this will be the .git dir under the RepoPath().
 | |
| // If we're in a linked worktree, it will be the directory pointed at by the worktree's .git file
 | |
| func (self *RepoPaths) WorktreeGitDirPath() string {
 | |
| 	return self.worktreeGitDirPath
 | |
| }
 | |
| 
 | |
| // Path of the repo. If we're in a the main worktree, this will be the same as WorktreePath()
 | |
| // If we're in a bare repo, it will be the parent folder of the bare repo
 | |
| func (self *RepoPaths) RepoPath() string {
 | |
| 	return self.repoPath
 | |
| }
 | |
| 
 | |
| // path of the git-dir for the repo.
 | |
| // If this is a bare repo, it will be the location of the bare repo
 | |
| // If this is a non-bare repo, it will be the location of the .git dir in
 | |
| // the main worktree.
 | |
| func (self *RepoPaths) RepoGitDirPath() string {
 | |
| 	return self.repoGitDirPath
 | |
| }
 | |
| 
 | |
| // Name of the repo. Basename of the folder containing the repo.
 | |
| func (self *RepoPaths) RepoName() string {
 | |
| 	return self.repoName
 | |
| }
 | |
| 
 | |
| // Returns the repo paths for a typical repo
 | |
| func MockRepoPaths(currentPath string) *RepoPaths {
 | |
| 	return &RepoPaths{
 | |
| 		currentPath:        currentPath,
 | |
| 		worktreePath:       currentPath,
 | |
| 		worktreeGitDirPath: path.Join(currentPath, ".git"),
 | |
| 		repoPath:           currentPath,
 | |
| 		repoGitDirPath:     path.Join(currentPath, ".git"),
 | |
| 		repoName:           "lazygit",
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func GetRepoPaths(
 | |
| 	fs afero.Fs,
 | |
| 	currentPath string,
 | |
| ) (*RepoPaths, error) {
 | |
| 	return getRepoPathsAux(afero.NewOsFs(), resolveSymlink, currentPath)
 | |
| }
 | |
| 
 | |
| func getRepoPathsAux(
 | |
| 	fs afero.Fs,
 | |
| 	resolveSymlinkFn func(string) (string, error),
 | |
| 	currentPath string,
 | |
| ) (*RepoPaths, error) {
 | |
| 	worktreePath := currentPath
 | |
| 	repoGitDirPath, repoPath, err := getCurrentRepoGitDirPath(fs, resolveSymlinkFn, currentPath)
 | |
| 	if err != nil {
 | |
| 		return nil, errors.Errorf("failed to get repo git dir path: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	var worktreeGitDirPath string
 | |
| 	if env.GetWorkTreeEnv() != "" {
 | |
| 		// This env is set when you pass --work-tree to lazygit. In that case,
 | |
| 		// we're not dealing with a linked work-tree, we're dealing with a 'specified'
 | |
| 		// worktree (for lack of a better term). In this case, the worktree has no
 | |
| 		// .git file and it just contains a bunch of files: it has no idea it's
 | |
| 		// pointed to by a bare repo. As such it does not have its own git dir within
 | |
| 		// the bare repo's git dir. Instead, we just use the bare repo's git dir.
 | |
| 		worktreeGitDirPath = repoGitDirPath
 | |
| 	} else {
 | |
| 		var err error
 | |
| 		worktreeGitDirPath, err = getWorktreeGitDirPath(fs, currentPath)
 | |
| 		if err != nil {
 | |
| 			return nil, errors.Errorf("failed to get worktree git dir path: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	repoName := path.Base(repoPath)
 | |
| 
 | |
| 	return &RepoPaths{
 | |
| 		currentPath:        currentPath,
 | |
| 		worktreePath:       worktreePath,
 | |
| 		worktreeGitDirPath: worktreeGitDirPath,
 | |
| 		repoPath:           repoPath,
 | |
| 		repoGitDirPath:     repoGitDirPath,
 | |
| 		repoName:           repoName,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| // Returns the path of the git-dir for the worktree. For linked worktrees, the worktree has
 | |
| // a .git file that points to the git-dir (which itself lives in the git-dir
 | |
| // of the repo)
 | |
| func getWorktreeGitDirPath(fs afero.Fs, worktreePath string) (string, error) {
 | |
| 	// if .git is a file, we're in a linked worktree, otherwise we're in
 | |
| 	// the main worktree
 | |
| 	dotGitPath := path.Join(worktreePath, ".git")
 | |
| 	gitFileInfo, err := fs.Stat(dotGitPath)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	if gitFileInfo.IsDir() {
 | |
| 		return dotGitPath, nil
 | |
| 	}
 | |
| 
 | |
| 	return linkedWorktreeGitDirPath(fs, worktreePath)
 | |
| }
 | |
| 
 | |
| func linkedWorktreeGitDirPath(fs afero.Fs, worktreePath string) (string, error) {
 | |
| 	dotGitPath := path.Join(worktreePath, ".git")
 | |
| 	gitFileContents, err := afero.ReadFile(fs, dotGitPath)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// The file will have `gitdir: /path/to/.git/worktrees/<worktree-name>`
 | |
| 	gitDirLine := lo.Filter(strings.Split(string(gitFileContents), "\n"), func(line string, _ int) bool {
 | |
| 		return strings.HasPrefix(line, "gitdir: ")
 | |
| 	})
 | |
| 
 | |
| 	if len(gitDirLine) == 0 {
 | |
| 		return "", errors.New(fmt.Sprintf("%s is a file which suggests we are in a submodule or a worktree but the file's contents do not contain a gitdir pointing to the actual .git directory", dotGitPath))
 | |
| 	}
 | |
| 
 | |
| 	gitDir := strings.TrimPrefix(gitDirLine[0], "gitdir: ")
 | |
| 
 | |
| 	gitDir = filepath.Clean(gitDir)
 | |
| 	// For windows support
 | |
| 	gitDir = filepath.ToSlash(gitDir)
 | |
| 
 | |
| 	return gitDir, nil
 | |
| }
 | |
| 
 | |
| func getCurrentRepoGitDirPath(
 | |
| 	fs afero.Fs,
 | |
| 	resolveSymlinkFn func(string) (string, error),
 | |
| 	currentPath string,
 | |
| ) (string, string, error) {
 | |
| 	var unresolvedGitPath string
 | |
| 	if env.GetGitDirEnv() != "" {
 | |
| 		unresolvedGitPath = env.GetGitDirEnv()
 | |
| 	} else {
 | |
| 		unresolvedGitPath = path.Join(currentPath, ".git")
 | |
| 	}
 | |
| 
 | |
| 	gitPath, err := resolveSymlinkFn(unresolvedGitPath)
 | |
| 	if err != nil {
 | |
| 		return "", "", err
 | |
| 	}
 | |
| 
 | |
| 	// check if .git is a file or a directory
 | |
| 	gitFileInfo, err := fs.Stat(gitPath)
 | |
| 	if err != nil {
 | |
| 		return "", "", err
 | |
| 	}
 | |
| 
 | |
| 	if gitFileInfo.IsDir() {
 | |
| 		// must be in the main worktree
 | |
| 		return gitPath, path.Dir(gitPath), nil
 | |
| 	}
 | |
| 
 | |
| 	// either in a submodule, or worktree
 | |
| 	worktreeGitPath, err := linkedWorktreeGitDirPath(fs, currentPath)
 | |
| 	if err != nil {
 | |
| 		return "", "", errors.Errorf("could not find git dir for %s: %v", currentPath, err)
 | |
| 	}
 | |
| 
 | |
| 	_, err = fs.Stat(worktreeGitPath)
 | |
| 	if err != nil {
 | |
| 		if os.IsNotExist(err) {
 | |
| 			// hardcoding error to get around windows-specific error message
 | |
| 			return "", "", errors.Errorf("could not find git dir for %s. %s does not exist", currentPath, worktreeGitPath)
 | |
| 		}
 | |
| 		return "", "", errors.Errorf("could not find git dir for %s: %v", currentPath, err)
 | |
| 	}
 | |
| 
 | |
| 	// confirm whether the next directory up is the worktrees directory
 | |
| 	parent := path.Dir(worktreeGitPath)
 | |
| 	if path.Base(parent) == "worktrees" {
 | |
| 		gitDirPath := path.Dir(parent)
 | |
| 		return gitDirPath, path.Dir(gitDirPath), nil
 | |
| 	}
 | |
| 
 | |
| 	// Unlike worktrees, submodules can be nested arbitrarily deep, so we check
 | |
| 	// if the `modules` directory is anywhere up the chain.
 | |
| 	if strings.Contains(worktreeGitPath, "/modules/") {
 | |
| 		// For submodules, we just return the path directly
 | |
| 		return worktreeGitPath, currentPath, nil
 | |
| 	}
 | |
| 
 | |
| 	// If this error causes issues, we could relax the constraint and just always
 | |
| 	// return the path
 | |
| 	return "", "", errors.Errorf("could not find git dir for %s: the path '%s' is not under `worktrees` or `modules` directories", currentPath, worktreeGitPath)
 | |
| }
 | |
| 
 | |
| // takes a path containing a symlink and returns the true path
 | |
| func resolveSymlink(path string) (string, error) {
 | |
| 	l, err := os.Lstat(path)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	if l.Mode()&os.ModeSymlink == 0 {
 | |
| 		return path, nil
 | |
| 	}
 | |
| 
 | |
| 	return filepath.EvalSymlinks(path)
 | |
| }
 | |
| 
 | |
| // Returns the paths of linked worktrees
 | |
| func linkedWortkreePaths(fs afero.Fs, repoGitDirPath string) []string {
 | |
| 	result := []string{}
 | |
| 	// For each directory in this path we're going to cat the `gitdir` file and append its contents to our result
 | |
| 	// That file points us to the `.git` file in the worktree.
 | |
| 	worktreeGitDirsPath := path.Join(repoGitDirPath, "worktrees")
 | |
| 
 | |
| 	// ensure the directory exists
 | |
| 	_, err := fs.Stat(worktreeGitDirsPath)
 | |
| 	if err != nil {
 | |
| 		return result
 | |
| 	}
 | |
| 
 | |
| 	_ = afero.Walk(fs, worktreeGitDirsPath, func(currPath string, info ioFs.FileInfo, err error) error {
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if !info.IsDir() {
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		gitDirPath := path.Join(currPath, "gitdir")
 | |
| 		gitDirBytes, err := afero.ReadFile(fs, gitDirPath)
 | |
| 		if err != nil {
 | |
| 			// ignoring error
 | |
| 			return nil
 | |
| 		}
 | |
| 		trimmedGitDir := strings.TrimSpace(string(gitDirBytes))
 | |
| 		// removing the .git part
 | |
| 		worktreeDir := path.Dir(trimmedGitDir)
 | |
| 		result = append(result, worktreeDir)
 | |
| 		return nil
 | |
| 	})
 | |
| 
 | |
| 	return result
 | |
| }
 |