mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-12-12 11:15:00 +02:00
263 lines
6.9 KiB
Go
263 lines
6.9 KiB
Go
package commands
|
|
|
|
import (
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-errors/errors"
|
|
|
|
gogit "github.com/jesseduffield/go-git/v5"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
|
"github.com/jesseduffield/lazygit/pkg/common"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/jesseduffield/lazygit/pkg/env"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
)
|
|
|
|
// this takes something like:
|
|
// * (HEAD detached at 264fc6f5)
|
|
// remotes
|
|
// and returns '264fc6f5' as the second match
|
|
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
|
|
|
|
// GitCommand is our main git interface
|
|
type GitCommand struct {
|
|
*common.Common
|
|
OSCommand *oscommands.OSCommand
|
|
Repo *gogit.Repository
|
|
Config config.AppConfigurer
|
|
DotGitDir string
|
|
onSuccessfulContinue func() error
|
|
PatchManager *patch.PatchManager
|
|
GitConfig git_config.IGitConfig
|
|
|
|
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
|
|
PushToCurrent bool
|
|
|
|
// this is just a view that we write to when running certain commands.
|
|
// Coincidentally at the moment it's the same view that OnRunCommand logs to
|
|
// but that need not always be the case.
|
|
GetCmdWriter func() io.Writer
|
|
}
|
|
|
|
// NewGitCommand it runs git commands
|
|
func NewGitCommand(
|
|
cmn *common.Common,
|
|
osCommand *oscommands.OSCommand,
|
|
config config.AppConfigurer,
|
|
gitConfig git_config.IGitConfig,
|
|
) (*GitCommand, error) {
|
|
var repo *gogit.Repository
|
|
|
|
pushToCurrent := gitConfig.Get("push.default") == "current"
|
|
|
|
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var err error
|
|
if repo, err = setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dotGitDir, err := findDotGitDir(os.Stat, ioutil.ReadFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gitCommand := &GitCommand{
|
|
Common: cmn,
|
|
OSCommand: osCommand,
|
|
Repo: repo,
|
|
Config: config,
|
|
DotGitDir: dotGitDir,
|
|
PushToCurrent: pushToCurrent,
|
|
GitConfig: gitConfig,
|
|
GetCmdWriter: func() io.Writer { return ioutil.Discard },
|
|
}
|
|
|
|
gitCommand.PatchManager = patch.NewPatchManager(gitCommand.Log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
|
|
|
|
return gitCommand, nil
|
|
}
|
|
|
|
func (c *GitCommand) WithSpan(span string) *GitCommand {
|
|
// sometimes .WithSpan(span) will be called where span actually is empty, in
|
|
// which case we don't need to log anything so we can just return early here
|
|
// with the original struct
|
|
if span == "" {
|
|
return c
|
|
}
|
|
|
|
newGitCommand := &GitCommand{}
|
|
*newGitCommand = *c
|
|
newGitCommand.OSCommand = c.OSCommand.WithSpan(span)
|
|
|
|
// NOTE: unlike the other things here which create shallow clones, this will
|
|
// actually update the PatchManager on the original struct to have the new span.
|
|
// This means each time we call ApplyPatch in PatchManager, we need to ensure
|
|
// we've called .WithSpan() ahead of time with the new span value
|
|
newGitCommand.PatchManager.ApplyPatch = newGitCommand.ApplyPatch
|
|
|
|
return newGitCommand
|
|
}
|
|
|
|
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
|
|
gitDir := env.GetGitDirEnv()
|
|
if gitDir != "" {
|
|
// we've been given the git directory explicitly so no need to navigate to it
|
|
_, err := stat(gitDir)
|
|
if err != nil {
|
|
return utils.WrapError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// we haven't been given the git dir explicitly so we assume it's in the current working directory as `.git/` (or an ancestor directory)
|
|
|
|
for {
|
|
_, err := stat(".git")
|
|
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
if !os.IsNotExist(err) {
|
|
return utils.WrapError(err)
|
|
}
|
|
|
|
if err = chdir(".."); err != nil {
|
|
return utils.WrapError(err)
|
|
}
|
|
|
|
currentPath, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
atRoot := currentPath == filepath.Dir(currentPath)
|
|
if atRoot {
|
|
// we should never really land here: the code that creates GitCommand should
|
|
// verify we're in a git directory
|
|
return errors.New("Must open lazygit in a git repository")
|
|
}
|
|
}
|
|
}
|
|
|
|
// resolvePath takes a path containing a symlink and returns the true path
|
|
func resolvePath(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)
|
|
}
|
|
|
|
func setupRepository(openGitRepository func(string) (*gogit.Repository, error), gitConfigParseErrorStr string) (*gogit.Repository, error) {
|
|
unresolvedPath := env.GetGitDirEnv()
|
|
if unresolvedPath == "" {
|
|
var err error
|
|
unresolvedPath, err = os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
path, err := resolvePath(unresolvedPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repository, err := openGitRepository(path)
|
|
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
|
|
return nil, errors.New(gitConfigParseErrorStr)
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
return repository, err
|
|
}
|
|
|
|
func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) {
|
|
if env.GetGitDirEnv() != "" {
|
|
return env.GetGitDirEnv(), nil
|
|
}
|
|
|
|
f, err := stat(".git")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if f.IsDir() {
|
|
return ".git", nil
|
|
}
|
|
|
|
fileBytes, err := readFile(".git")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fileContent := string(fileBytes)
|
|
if !strings.HasPrefix(fileContent, "gitdir: ") {
|
|
return "", errors.New(".git is a file which suggests we are in a submodule but the file's contents do not contain a gitdir pointing to the actual .git directory")
|
|
}
|
|
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
|
|
}
|
|
|
|
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
|
|
return osCommand.Run(osCommand.NewCmdObj("git rev-parse --git-dir"))
|
|
}
|
|
|
|
func (c *GitCommand) Run(cmdObj oscommands.ICmdObj) error {
|
|
_, err := c.RunWithOutput(cmdObj)
|
|
return err
|
|
}
|
|
|
|
func (c *GitCommand) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) {
|
|
// TODO: have this retry logic in other places we run the command
|
|
waitTime := 50 * time.Millisecond
|
|
retryCount := 5
|
|
attempt := 0
|
|
|
|
for {
|
|
output, err := c.OSCommand.RunWithOutput(cmdObj)
|
|
if err != nil {
|
|
// if we have an error based on the index lock, we should wait a bit and then retry
|
|
if strings.Contains(output, ".git/index.lock") {
|
|
c.Log.Error(output)
|
|
c.Log.Info("index.lock prevented command from running. Retrying command after a small wait")
|
|
attempt++
|
|
time.Sleep(waitTime)
|
|
if attempt < retryCount {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
return output, err
|
|
}
|
|
}
|
|
|
|
func (c *GitCommand) NewCmdObj(cmdStr string) oscommands.ICmdObj {
|
|
return c.OSCommand.NewCmdObj(cmdStr).AddEnvVars("GIT_OPTIONAL_LOCKS=0")
|
|
}
|
|
|
|
func (c *GitCommand) NewCmdObjWithLog(cmdStr string) oscommands.ICmdObj {
|
|
cmdObj := c.NewCmdObj(cmdStr)
|
|
c.OSCommand.LogCmdObj(cmdObj)
|
|
return cmdObj
|
|
}
|