1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-11-30 09:16:47 +02:00
lazygit/pkg/commands/git.go

528 lines
15 KiB
Go
Raw Normal View History

package commands
import (
2018-08-12 12:22:20 +02:00
"errors"
"fmt"
"os"
2018-08-12 12:22:20 +02:00
"os/exec"
2018-08-12 11:50:55 +02:00
"strings"
"github.com/jesseduffield/lazygit/pkg/i18n"
2018-08-12 13:04:47 +02:00
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
2018-08-12 11:50:55 +02:00
gitconfig "github.com/tcnksm/go-gitconfig"
gogit "gopkg.in/src-d/go-git.v4"
)
2018-09-04 06:16:19 +02:00
func verifyInGitRepo(runCmd func(string) error) error {
return runCmd("git status")
2018-08-29 22:55:57 +02:00
}
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
2018-08-29 22:55:57 +02:00
for {
f, err := stat(".git")
2018-08-29 22:55:57 +02:00
if err == nil && f.IsDir() {
return nil
}
if !os.IsNotExist(err) {
return err
}
2018-08-29 22:55:57 +02:00
if err = chdir(".."); err != nil {
2018-08-29 22:55:57 +02:00
return err
}
}
}
func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repository, error), sLocalize func(string) string) (repository *gogit.Repository, worktree *gogit.Worktree, err error) {
repository, err = openGitRepository(".")
if err != nil {
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
return nil, nil, errors.New(sLocalize("GitconfigParseErr"))
}
2018-08-29 22:55:57 +02:00
return
}
worktree, err = repository.Worktree()
if err != nil {
return
2018-08-29 22:55:57 +02:00
}
return
}
// GitCommand is our main git interface
type GitCommand struct {
Log *logrus.Entry
OSCommand *OSCommand
Worktree *gogit.Worktree
Repo *gogit.Repository
Tr *i18n.Localizer
getGlobalGitConfig func(string) (string, error)
getLocalGitConfig func(string) (string, error)
}
// NewGitCommand it runs git commands
func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer) (*GitCommand, error) {
var worktree *gogit.Worktree
var repo *gogit.Repository
fs := []func() error{
func() error {
2018-09-04 06:16:19 +02:00
return verifyInGitRepo(osCommand.RunCommand)
},
func() error {
return navigateToRepoRootDirectory(os.Stat, os.Chdir)
},
func() error {
var err error
repo, worktree, err = setupRepositoryAndWorktree(gogit.PlainOpen, tr.SLocalize)
return err
},
}
for _, f := range fs {
if err := f(); err != nil {
return nil, err
}
}
return &GitCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Worktree: worktree,
Repo: repo,
getGlobalGitConfig: gitconfig.Global,
getLocalGitConfig: gitconfig.Local,
}, nil
}
2018-08-12 12:22:20 +02:00
// GetStashEntries stash entryies
func (c *GitCommand) GetStashEntries() []StashEntry {
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
2018-08-27 22:41:23 +02:00
stashEntries := []StashEntry{}
2018-08-12 13:04:47 +02:00
for i, line := range utils.SplitLines(rawString) {
2018-08-12 12:22:20 +02:00
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
func stashEntryFromLine(line string, index int) StashEntry {
return StashEntry{
Name: line,
Index: index,
DisplayString: line,
}
}
// GetStashEntryDiff stash diff
func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
2018-08-12 12:22:20 +02:00
}
// GetStatusFiles git status files
2018-08-13 12:26:02 +02:00
func (c *GitCommand) GetStatusFiles() []File {
2018-08-12 13:04:47 +02:00
statusOutput, _ := c.GitStatus()
statusStrings := utils.SplitLines(statusOutput)
2018-08-27 23:20:01 +02:00
files := []File{}
2018-08-12 12:22:20 +02:00
for _, statusString := range statusStrings {
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
2018-08-28 11:12:35 +02:00
filename := c.OSCommand.Unquote(statusString[3:])
_, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change]
2018-09-09 20:08:46 +02:00
_, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
2018-08-13 12:26:02 +02:00
file := File{
2018-08-28 11:12:35 +02:00
Name: filename,
2018-08-12 12:22:20 +02:00
DisplayString: statusString,
2018-09-09 20:08:46 +02:00
HasStagedChanges: !hasNoStagedChanges,
2018-08-12 12:22:20 +02:00
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
2018-08-12 12:22:20 +02:00
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: change == "UU",
2018-08-28 11:12:35 +02:00
Type: c.OSCommand.FileType(filename),
2018-08-12 12:22:20 +02:00
}
2018-08-13 12:26:02 +02:00
files = append(files, file)
2018-08-12 12:22:20 +02:00
}
2018-08-13 12:26:02 +02:00
c.Log.Info(files) // TODO: use a dumper-esque log here
return files
2018-08-12 12:22:20 +02:00
}
// StashDo modify stash
func (c *GitCommand) StashDo(index int, method string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git stash %s stash@{%d}", method, index))
2018-08-12 12:22:20 +02:00
}
// StashSave save stash
// TODO: before calling this, check if there is anything to save
func (c *GitCommand) StashSave(message string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git stash save %s", c.OSCommand.Quote(message)))
2018-08-12 12:22:20 +02:00
}
// MergeStatusFiles merge status files
2018-08-13 12:26:02 +02:00
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
if len(oldFiles) == 0 {
return newFiles
2018-08-12 12:22:20 +02:00
}
2018-08-28 21:09:56 +02:00
headResults := []File{}
tailResults := []File{}
2018-08-12 12:22:20 +02:00
2018-08-28 21:09:56 +02:00
for _, newFile := range newFiles {
var isHeadResult bool
for _, oldFile := range oldFiles {
2018-08-13 12:26:02 +02:00
if oldFile.Name == newFile.Name {
2018-08-28 21:09:56 +02:00
isHeadResult = true
2018-08-12 12:22:20 +02:00
break
}
}
2018-08-28 21:09:56 +02:00
if isHeadResult {
headResults = append(headResults, newFile)
continue
2018-08-12 12:22:20 +02:00
}
2018-08-28 21:09:56 +02:00
tailResults = append(tailResults, newFile)
2018-08-12 12:22:20 +02:00
}
2018-08-28 21:09:56 +02:00
return append(headResults, tailResults...)
2018-08-12 12:22:20 +02:00
}
2018-08-12 13:04:47 +02:00
// GetBranchName branch name
func (c *GitCommand) GetBranchName() (string, error) {
return c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
2018-08-12 13:04:47 +02:00
}
2018-08-12 11:50:55 +02:00
// ResetHard does the equivalent of `git reset --hard HEAD`
func (c *GitCommand) ResetHard() error {
2018-08-12 13:04:47 +02:00
return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset})
2018-08-12 11:50:55 +02:00
}
// UpstreamDifferenceCount checks how many pushables/pullables there are for the
// current branch
func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
pushableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --count")
2018-08-12 11:50:55 +02:00
if err != nil {
return "?", "?"
}
pullableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list head..@{u} --count")
2018-08-12 11:50:55 +02:00
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch
func (c *GitCommand) GetCommitsToPush() []string {
pushables, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit")
2018-08-12 11:50:55 +02:00
if err != nil {
2018-09-06 23:18:19 +02:00
return []string{}
2018-08-12 11:50:55 +02:00
}
2018-08-12 13:04:47 +02:00
return utils.SplitLines(pushables)
2018-08-12 11:50:55 +02:00
}
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name)))
2018-08-12 11:50:55 +02:00
}
2018-08-12 12:22:20 +02:00
// Fetch fetch git repo
func (c *GitCommand) Fetch() error {
return c.OSCommand.RunCommand("git fetch")
2018-08-12 11:50:55 +02:00
}
2018-08-12 12:22:20 +02:00
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git reset %s", sha))
2018-08-12 11:50:55 +02:00
}
2018-08-12 12:22:20 +02:00
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name))
2018-08-12 11:50:55 +02:00
}
2018-08-12 12:22:20 +02:00
// DeleteBranch delete branch
func (c *GitCommand) DeleteBranch(branch string, force bool) error {
command := "git branch -d"
if force {
command = "git branch -D"
}
return c.OSCommand.RunCommand(fmt.Sprintf("%s %s", command, branch))
2018-08-12 11:50:55 +02:00
}
2018-08-12 12:22:20 +02:00
// ListStash list stash
2018-08-12 11:50:55 +02:00
func (c *GitCommand) ListStash() (string, error) {
return c.OSCommand.RunCommandWithOutput("git stash list")
2018-08-12 11:50:55 +02:00
}
2018-08-12 12:22:20 +02:00
// Merge merge
func (c *GitCommand) Merge(branchName string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git merge --no-edit %s", branchName))
2018-08-12 11:50:55 +02:00
}
2018-08-12 12:22:20 +02:00
// AbortMerge abort merge
func (c *GitCommand) AbortMerge() error {
return c.OSCommand.RunCommand("git merge --abort")
2018-08-12 12:22:20 +02:00
}
// usingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password
func (c *GitCommand) usingGpg() bool {
gpgsign, _ := c.getGlobalGitConfig("commit.gpgsign")
if gpgsign == "" {
gpgsign, _ = c.getLocalGitConfig("commit.gpgsign")
}
value := strings.ToLower(gpgsign)
return value == "true" || value == "1" || value == "yes" || value == "on"
}
// Commit commits to git
func (c *GitCommand) Commit(message string) (*exec.Cmd, error) {
command := fmt.Sprintf("git commit -m %s", c.OSCommand.Quote(message))
if c.usingGpg() {
2018-08-21 22:33:25 +02:00
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
2018-08-12 11:50:55 +02:00
}
return nil, c.OSCommand.RunCommand(command)
2018-08-12 11:50:55 +02:00
}
2018-08-13 12:26:02 +02:00
// Pull pull from repo
func (c *GitCommand) Pull() error {
2018-08-12 12:22:20 +02:00
return c.OSCommand.RunCommand("git pull --no-edit")
2018-08-12 11:50:55 +02:00
}
2018-08-13 12:26:02 +02:00
// Push push to a branch
func (c *GitCommand) Push(branchName string, force bool) error {
forceFlag := ""
if force {
forceFlag = "--force-with-lease "
}
return c.OSCommand.RunCommand("git push " + forceFlag + "-u origin " + branchName)
2018-08-12 11:50:55 +02:00
}
2018-08-12 12:22:20 +02:00
// SquashPreviousTwoCommits squashes a commit down to the one below it
// retaining the message of the higher commit
func (c *GitCommand) SquashPreviousTwoCommits(message string) error {
// TODO: test this
2018-08-14 10:30:06 +02:00
err := c.OSCommand.RunCommand("git reset --soft HEAD^")
if err != nil {
return err
}
// TODO: if password is required, we need to return a subprocess
return c.OSCommand.RunCommand("git commit --amend -m " + c.OSCommand.Quote(message))
2018-08-12 11:50:55 +02:00
}
2018-08-12 12:22:20 +02:00
// SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it,
// retaining the commit message of the lower commit
func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error {
2018-08-12 11:50:55 +02:00
var err error
commands := []string{
"git checkout -q " + shaValue,
"git reset --soft " + shaValue + "^",
"git commit --amend -C " + shaValue + "^",
"git rebase --onto HEAD " + shaValue + " " + branchName,
}
ret := ""
for _, command := range commands {
2018-08-12 13:04:47 +02:00
c.Log.Info(command)
output, err := c.OSCommand.RunCommandWithOutput(command)
2018-08-12 11:50:55 +02:00
ret += output
if err != nil {
2018-08-12 13:04:47 +02:00
c.Log.Info(ret)
2018-08-12 11:50:55 +02:00
break
}
}
if err != nil {
// We are already in an error state here so we're just going to append
// the output of these commands
output, _ := c.OSCommand.RunCommandWithOutput("git branch -d " + shaValue)
2018-08-12 12:22:20 +02:00
ret += output
output, _ = c.OSCommand.RunCommandWithOutput("git checkout " + branchName)
2018-08-12 12:22:20 +02:00
ret += output
2018-08-12 11:50:55 +02:00
}
if err != nil {
return errors.New(ret)
}
return nil
2018-08-12 11:50:55 +02:00
}
2018-08-12 12:22:20 +02:00
// CatFile obtain the contents of a file
2018-08-19 12:13:29 +02:00
func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("cat " + c.OSCommand.Quote(fileName))
2018-08-12 12:22:20 +02:00
}
// StageFile stages a file
2018-08-19 12:13:29 +02:00
func (c *GitCommand) StageFile(fileName string) error {
return c.OSCommand.RunCommand("git add " + c.OSCommand.Quote(fileName))
2018-08-12 12:22:20 +02:00
}
// StageAll stages all files
func (c *GitCommand) StageAll() error {
return c.OSCommand.RunCommand("git add -A")
}
// UnstageAll stages all files
func (c *GitCommand) UnstageAll() error {
return c.OSCommand.RunCommand("git reset")
}
2018-08-12 12:22:20 +02:00
// UnStageFile unstages a file
2018-08-19 12:13:29 +02:00
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
2018-08-12 12:22:20 +02:00
var command string
if tracked {
command = "git reset HEAD "
} else {
command = "git rm --cached "
}
2018-08-19 12:13:29 +02:00
return c.OSCommand.RunCommand(command + c.OSCommand.Quote(fileName))
2018-08-12 12:22:20 +02:00
}
// GitStatus returns the plaintext short status of the repo
func (c *GitCommand) GitStatus() (string, error) {
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --short")
2018-08-12 12:22:20 +02:00
}
// IsInMergeState states whether we are still mid-merge
func (c *GitCommand) IsInMergeState() (bool, error) {
output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
2018-08-12 12:22:20 +02:00
if err != nil {
return false, err
}
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
}
// RemoveFile directly
2018-08-13 12:26:02 +02:00
func (c *GitCommand) RemoveFile(file File) error {
2018-08-12 12:22:20 +02:00
// if the file isn't tracked, we assume you want to delete it
2018-08-18 12:14:44 +02:00
if file.HasStagedChanges {
if err := c.OSCommand.RunCommand("git reset -- " + file.Name); err != nil {
return err
}
}
2018-08-12 12:22:20 +02:00
if !file.Tracked {
2018-08-19 12:13:29 +02:00
return os.RemoveAll(file.Name)
2018-08-12 12:22:20 +02:00
}
// if the file is tracked, we assume you want to just check it out
2018-08-18 12:14:44 +02:00
return c.OSCommand.RunCommand("git checkout -- " + file.Name)
2018-08-12 12:22:20 +02:00
}
// Checkout checks out a branch, with --force if you set the force arg to true
func (c *GitCommand) Checkout(branch string, force bool) error {
2018-08-12 12:22:20 +02:00
forceArg := ""
if force {
forceArg = "--force "
}
2018-08-12 13:04:47 +02:00
return c.OSCommand.RunCommand("git checkout " + forceArg + branch)
2018-08-12 12:22:20 +02:00
}
2018-08-13 12:26:02 +02:00
// AddPatch prepares a subprocess for adding a patch by patch
2018-08-12 12:22:20 +02:00
// this will eventually be swapped out for a better solution inside the Gui
2018-08-21 22:33:25 +02:00
func (c *GitCommand) AddPatch(filename string) *exec.Cmd {
2018-08-13 12:26:02 +02:00
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", filename)
}
// PrepareCommitSubProcess prepares a subprocess for `git commit`
2018-08-21 22:33:25 +02:00
func (c *GitCommand) PrepareCommitSubProcess() *exec.Cmd {
2018-08-13 12:26:02 +02:00
return c.OSCommand.PrepareSubProcess("git", "commit")
2018-08-12 12:22:20 +02:00
}
// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty`
func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty")
}
2018-08-12 12:22:20 +02:00
// GetBranchGraph gets the color-formatted graph of the log for the given branch
// Currently it limits the result to 100 commits, but when we get async stuff
// working we can do lazy loading
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branchName)
2018-08-12 12:22:20 +02:00
}
func includesString(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
2018-08-12 13:04:47 +02:00
// GetCommits obtains the commits of the current branch
func (c *GitCommand) GetCommits() []Commit {
2018-08-13 12:26:02 +02:00
pushables := c.GetCommitsToPush()
log := c.GetLog()
2018-09-06 23:18:19 +02:00
commits := []Commit{}
2018-08-12 13:04:47 +02:00
// now we can split it up and turn it into commits
2018-08-13 12:26:02 +02:00
lines := utils.SplitLines(log)
2018-08-12 13:04:47 +02:00
for _, line := range lines {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
pushed := includesString(pushables, sha)
commits = append(commits, Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Pushed: pushed,
DisplayString: strings.Join(splitLine, " "),
})
}
return commits
}
// GetLog gets the git log (currently limited to 30 commits for performance
// until we work out lazy loading
func (c *GitCommand) GetLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
2018-08-12 13:04:47 +02:00
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}
// Ignore adds a file to the gitignore for the repo
2018-08-19 12:41:04 +02:00
func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)
2018-08-12 13:04:47 +02:00
}
// Show shows the diff of a commit
func (c *GitCommand) Show(sha string) string {
result, err := c.OSCommand.RunCommandWithOutput("git show --color " + sha)
2018-08-12 13:04:47 +02:00
if err != nil {
panic(err)
}
return result
}
// Diff returns the diff of a file
2018-08-13 12:26:02 +02:00
func (c *GitCommand) Diff(file File) string {
2018-08-12 13:04:47 +02:00
cachedArg := ""
2018-08-19 12:13:29 +02:00
fileName := c.OSCommand.Quote(file.Name)
2018-08-12 13:04:47 +02:00
if file.HasStagedChanges && !file.HasUnstagedChanges {
2018-08-14 10:48:08 +02:00
cachedArg = "--cached"
2018-08-12 13:04:47 +02:00
}
trackedArg := "--"
2018-08-12 13:04:47 +02:00
if !file.Tracked && !file.HasStagedChanges {
2018-08-14 10:48:08 +02:00
trackedArg = "--no-index /dev/null"
2018-08-12 13:04:47 +02:00
}
command := fmt.Sprintf("%s %s %s %s", "git diff --color ", cachedArg, trackedArg, fileName)
2018-08-14 10:48:08 +02:00
2018-08-12 13:04:47 +02:00
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(command)
2018-08-12 13:04:47 +02:00
return s
}