2023-07-28 18:27:14 +10:00
package git_commands
import (
"fmt"
2023-07-29 17:02:04 +10:00
ioFs "io/fs"
2023-07-28 18:27:14 +10:00
"os"
2023-07-29 13:15:58 +10:00
"path"
2023-07-28 18:27:14 +10:00
"path/filepath"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/samber/lo"
2023-07-29 17:02:04 +10:00
"github.com/spf13/afero"
2023-07-28 18:27:14 +10:00
)
2023-07-29 17:02:04 +10:00
type RepoPaths struct {
2023-07-28 18:27:14 +10:00
currentPath string
worktreePath string
worktreeGitDirPath string
repoPath string
repoGitDirPath string
repoName string
}
2023-07-29 17:02:04 +10:00
// 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 {
2023-07-28 18:27:14 +10:00
return self . currentPath
}
2023-07-29 17:02:04 +10:00
// Path to the current worktree. If we're in the main worktree, this will
// be the same as RepoPath()
func ( self * RepoPaths ) WorktreePath ( ) string {
2023-07-28 18:27:14 +10:00
return self . worktreePath
}
2023-07-29 17:02:04 +10:00
// 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 {
2023-07-28 18:27:14 +10:00
return self . worktreeGitDirPath
}
2023-07-29 17:02:04 +10:00
// 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 {
2023-07-28 18:27:14 +10:00
return self . repoPath
}
2023-07-29 17:02:04 +10:00
// 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 {
2023-07-28 18:27:14 +10:00
return self . repoGitDirPath
}
2023-07-29 17:02:04 +10:00
// Name of the repo. Basename of the folder containing the repo.
func ( self * RepoPaths ) RepoName ( ) string {
2023-07-28 18:27:14 +10:00
return self . repoName
}
2023-07-29 17:02:04 +10:00
// 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" ,
2023-07-28 18:27:14 +10:00
}
2023-07-29 17:02:04 +10:00
}
2023-07-28 18:27:14 +10:00
2023-07-29 17:02:04 +10:00
func GetRepoPaths (
fs afero . Fs ,
currentPath string ,
) ( * RepoPaths , error ) {
return getRepoPathsAux ( afero . NewOsFs ( ) , resolveSymlink , currentPath )
}
2023-07-29 13:15:58 +10:00
2023-07-29 17:02:04 +10:00
func getRepoPathsAux (
fs afero . Fs ,
resolveSymlinkFn func ( string ) ( string , error ) ,
currentPath string ,
) ( * RepoPaths , error ) {
2023-07-28 18:27:14 +10:00
worktreePath := currentPath
2023-07-29 17:02:04 +10:00
repoGitDirPath , repoPath , err := getCurrentRepoGitDirPath ( fs , resolveSymlinkFn , currentPath )
2023-07-28 18:27:14 +10:00
if err != nil {
2023-07-29 17:02:04 +10:00
return nil , errors . Errorf ( "failed to get repo git dir path: %v" , err )
2023-07-28 18:27:14 +10:00
}
2023-08-07 22:08:12 +10:00
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 )
}
2023-07-28 18:27:14 +10:00
}
2023-08-07 22:08:12 +10:00
2023-07-29 13:15:58 +10:00
repoName := path . Base ( repoPath )
2023-07-28 18:27:14 +10:00
2023-07-29 17:02:04 +10:00
return & RepoPaths {
2023-07-28 18:27:14 +10:00
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)
2023-08-07 22:08:12 +10:00
func getWorktreeGitDirPath ( fs afero . Fs , worktreePath string ) ( string , error ) {
2023-07-28 18:27:14 +10:00
// if .git is a file, we're in a linked worktree, otherwise we're in
// the main worktree
2023-07-29 13:15:58 +10:00
dotGitPath := path . Join ( worktreePath , ".git" )
2023-07-29 17:02:04 +10:00
gitFileInfo , err := fs . Stat ( dotGitPath )
2023-07-28 18:27:14 +10:00
if err != nil {
return "" , err
}
if gitFileInfo . IsDir ( ) {
return dotGitPath , nil
}
2023-07-29 17:02:04 +10:00
return linkedWorktreeGitDirPath ( fs , worktreePath )
2023-07-28 18:27:14 +10:00
}
2023-07-29 17:02:04 +10:00
func linkedWorktreeGitDirPath ( fs afero . Fs , worktreePath string ) ( string , error ) {
2023-07-29 13:15:58 +10:00
dotGitPath := path . Join ( worktreePath , ".git" )
2023-07-29 17:02:04 +10:00
gitFileContents , err := afero . ReadFile ( fs , dotGitPath )
2023-07-28 18:27:14 +10:00
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: " )
return gitDir , nil
}
2023-07-29 17:02:04 +10:00
func getCurrentRepoGitDirPath (
fs afero . Fs ,
resolveSymlinkFn func ( string ) ( string , error ) ,
currentPath string ,
) ( string , string , error ) {
2023-07-28 18:27:14 +10:00
var unresolvedGitPath string
if env . GetGitDirEnv ( ) != "" {
unresolvedGitPath = env . GetGitDirEnv ( )
} else {
2023-07-29 13:15:58 +10:00
unresolvedGitPath = path . Join ( currentPath , ".git" )
2023-07-28 18:27:14 +10:00
}
2023-07-29 17:02:04 +10:00
gitPath , err := resolveSymlinkFn ( unresolvedGitPath )
2023-07-28 18:27:14 +10:00
if err != nil {
return "" , "" , err
}
// check if .git is a file or a directory
2023-07-29 17:02:04 +10:00
gitFileInfo , err := fs . Stat ( gitPath )
2023-07-28 18:27:14 +10:00
if err != nil {
return "" , "" , err
}
if gitFileInfo . IsDir ( ) {
// must be in the main worktree
2023-07-29 13:15:58 +10:00
return gitPath , path . Dir ( gitPath ) , nil
2023-07-28 18:27:14 +10:00
}
// either in a submodule, or worktree
2023-07-29 17:02:04 +10:00
worktreeGitPath , err := linkedWorktreeGitDirPath ( fs , currentPath )
2023-07-28 18:27:14 +10:00
if err != nil {
return "" , "" , errors . Errorf ( "could not find git dir for %s: %v" , currentPath , err )
}
// confirm whether the next directory up is the worktrees/submodules directory
2023-07-29 13:15:58 +10:00
parent := path . Dir ( worktreeGitPath )
if path . Base ( parent ) != "worktrees" && path . Base ( parent ) != "modules" {
2023-07-28 18:27:14 +10:00
return "" , "" , errors . Errorf ( "could not find git dir for %s" , currentPath )
}
// if it's a submodule, we treat it as its own repo
2023-07-29 13:15:58 +10:00
if path . Base ( parent ) == "modules" {
2023-07-28 18:27:14 +10:00
return worktreeGitPath , currentPath , nil
}
2023-07-29 13:15:58 +10:00
gitDirPath := path . Dir ( parent )
return gitDirPath , path . Dir ( gitDirPath ) , nil
2023-07-28 18:27:14 +10:00
}
// 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 )
}
2023-07-29 17:02:04 +10:00
// 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
}