1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-12 11:15:00 +02:00
lazygit/pkg/commands/git.go

1009 lines
30 KiB
Go
Raw Normal View History

package commands
import (
"fmt"
"io/ioutil"
"os"
2018-08-12 12:22:20 +02:00
"os/exec"
2018-08-12 11:50:55 +02:00
"strings"
2019-02-18 12:29:43 +02:00
"github.com/mgutz/str"
"github.com/go-errors/errors"
2019-02-18 12:29:43 +02:00
"github.com/jesseduffield/lazygit/pkg/config"
"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 {
2019-05-12 09:04:32 +02:00
_, err := stat(".git")
2018-08-29 22:55:57 +02:00
if err == nil {
2019-05-12 09:04:32 +02:00
return nil
2018-08-29 22:55:57 +02:00
}
if !os.IsNotExist(err) {
return WrapError(err)
}
2018-08-29 22:55:57 +02:00
if err = chdir(".."); err != nil {
return WrapError(err)
2018-08-29 22:55:57 +02:00
}
}
}
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
2019-02-18 12:29:43 +02:00
Config config.AppConfigurer
getGlobalGitConfig func(string) (string, error)
getLocalGitConfig func(string) (string, error)
removeFile func(string) error
2019-05-12 09:04:32 +02:00
DotGitDir string
}
// NewGitCommand it runs git commands
2019-02-18 12:29:43 +02:00
func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer, config config.AppConfigurer) (*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
}
}
2019-05-12 09:04:32 +02:00
dotGitDir, err := findDotGitDir(os.Stat, ioutil.ReadFile)
if err != nil {
return nil, err
}
return &GitCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Worktree: worktree,
Repo: repo,
2019-02-18 12:29:43 +02:00
Config: config,
getGlobalGitConfig: gitconfig.Global,
getLocalGitConfig: gitconfig.Local,
2018-09-20 01:48:56 +02:00
removeFile: os.RemoveAll,
2019-05-12 09:04:32 +02:00
DotGitDir: dotGitDir,
}, nil
}
2019-05-12 09:04:32 +02:00
func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) {
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
}
2019-05-22 15:34:29 +02:00
// GetStashEntries stash entries
func (c *GitCommand) GetStashEntries() []*StashEntry {
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
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{
2018-08-12 12:22:20 +02:00
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
func (c *GitCommand) GetStatusFiles() []*File {
2018-08-12 13:04:47 +02:00
statusOutput, _ := c.GitStatus()
statusStrings := utils.SplitLines(statusOutput)
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]
hasMergeConflicts := change == "UU" || change == "AA" || change == "DU"
hasInlineMergeConflicts := change == "UU" || change == "AA"
file := &File{
Name: filename,
DisplayString: statusString,
HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: hasMergeConflicts,
HasInlineMergeConflicts: hasInlineMergeConflicts,
Type: c.OSCommand.FileType(filename),
ShortStatus: change,
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
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
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
2018-08-13 12:26:02 +02:00
if len(oldFiles) == 0 {
return newFiles
2018-08-12 12:22:20 +02:00
}
2018-09-12 10:24:03 +02:00
appendedIndexes := []int{}
2018-08-12 12:22:20 +02:00
2018-09-12 10:24:03 +02:00
// retain position of files we already could see
result := []*File{}
2018-09-12 10:24:03 +02:00
for _, oldFile := range oldFiles {
for newIndex, newFile := range newFiles {
2018-08-13 12:26:02 +02:00
if oldFile.Name == newFile.Name {
2018-09-12 10:24:03 +02:00
result = append(result, newFile)
appendedIndexes = append(appendedIndexes, newIndex)
2018-08-12 12:22:20 +02:00
break
}
}
2018-09-12 10:24:03 +02:00
}
2018-08-12 12:22:20 +02:00
2018-09-12 10:24:03 +02:00
// append any new files to the end
for index, newFile := range newFiles {
if !includesInt(appendedIndexes, index) {
result = append(result, newFile)
2018-08-12 12:22:20 +02:00
}
}
2018-09-12 10:24:03 +02:00
return result
}
func includesInt(list []int, a int) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
2018-08-12 12:22:20 +02:00
}
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *GitCommand) ResetAndClean() error {
if err := c.ResetHardHead(); err != nil {
return err
}
return c.RemoveUntrackedFiles()
2018-08-12 11:50:55 +02:00
}
2018-12-07 09:52:31 +02:00
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
return c.GetCommitDifferences("HEAD", "@{u}")
}
func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) {
upstream := "origin" // hardcoded for now
return c.GetCommitDifferences(branchName, fmt.Sprintf("%s/%s", upstream, branchName))
}
// GetCommitDifferences checks how many pushables/pullables there are for the
2018-08-12 11:50:55 +02:00
// current branch
2018-12-07 09:52:31 +02:00
func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
command := "git rev-list %s..%s --count"
pushableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, to, from))
2018-08-12 11:50:55 +02:00
if err != nil {
return "?", "?"
}
2018-12-07 09:52:31 +02:00
pullableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, from, to))
2018-08-12 11:50:55 +02:00
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
// 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
}
// RebaseBranch interactive rebases onto a branch
func (c *GitCommand) RebaseBranch(branchName string) error {
cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
2018-11-29 18:57:28 +02:00
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
2018-11-29 18:57:28 +02:00
}
2018-08-12 12:22:20 +02:00
// Fetch fetch git repo
2018-12-09 14:04:19 +02:00
func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCredentials bool) error {
2018-12-02 15:58:18 +02:00
return c.OSCommand.DetectUnamePass("git fetch", func(question string) string {
2018-12-09 14:04:19 +02:00
if canAskForCredentials {
2018-12-02 15:58:18 +02:00
return unamePassQuestion(question)
2018-11-25 14:15:36 +02:00
}
return "\n"
2018-11-25 14:15:36 +02:00
})
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, strength string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git reset --%s %s", strength, 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-11-30 02:47:14 +02:00
// CurrentBranchName is a function.
func (c *GitCommand) CurrentBranchName() (string, error) {
2018-11-14 10:08:42 +02:00
branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
2018-09-25 12:31:19 +02:00
if err != nil {
2018-11-14 10:08:42 +02:00
branchName, err = c.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD")
if err != nil {
return "", err
}
2018-09-25 12:31:19 +02:00
}
2018-11-14 10:08:42 +02:00
return utils.TrimTrailingNewline(branchName), nil
}
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.getLocalGitConfig("commit.gpgsign")
if gpgsign == "" {
gpgsign, _ = c.getGlobalGitConfig("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, flags string) (*exec.Cmd, error) {
command := fmt.Sprintf("git commit %s -m %s", flags, c.OSCommand.Quote(message))
if c.usingGpg() {
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
2018-09-12 15:20:35 +02:00
}
return nil, c.OSCommand.RunCommand(command)
}
// AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
command := "git commit --amend --no-edit"
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-09-10 22:38:25 +02:00
// Pull pulls from repo
func (c *GitCommand) Pull(ask func(string) string) error {
return c.OSCommand.DetectUnamePass("git pull --no-edit", ask)
2018-08-12 11:50:55 +02:00
}
// Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool, ask func(string) string) error {
forceFlag := ""
if force {
forceFlag = "--force-with-lease "
}
2019-02-16 01:18:38 +02:00
cmd := fmt.Sprintf("git push %s-u origin %s", forceFlag, branchName)
return c.OSCommand.DetectUnamePass(cmd, ask)
2018-08-12 11:50:55 +02:00
}
// CatFile obtains the content of a file
2018-08-19 12:13:29 +02:00
func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", 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(fmt.Sprintf("git add %s", 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 {
command := "git rm --cached %s"
2018-08-12 12:22:20 +02:00
if tracked {
command = "git reset HEAD %s"
2018-08-12 12:22:20 +02:00
}
2019-02-20 10:47:01 +02:00
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(fmt.Sprintf(command, c.OSCommand.Quote(name))); err != nil {
return err
}
}
return nil
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 --porcelain")
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
}
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (c *GitCommand) RebaseMode() (string, error) {
2019-05-12 09:04:32 +02:00
exists, err := c.OSCommand.FileExists(fmt.Sprintf("%s/rebase-apply", c.DotGitDir))
if err != nil {
return "", err
}
if exists {
return "normal", nil
}
2019-05-12 09:04:32 +02:00
exists, err = c.OSCommand.FileExists(fmt.Sprintf("%s/rebase-merge", c.DotGitDir))
if exists {
return "interactive", err
} else {
return "", err
}
2018-12-02 18:33:16 +02:00
}
2019-03-18 11:44:33 +02:00
// DiscardAllFileChanges directly
func (c *GitCommand) DiscardAllFileChanges(file *File) error {
2018-08-12 12:22:20 +02:00
// if the file isn't tracked, we assume you want to delete it
2019-02-20 11:01:29 +02:00
quotedFileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges || file.HasMergeConflicts {
2019-02-20 11:01:29 +02:00
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", quotedFileName)); err != nil {
2018-08-18 12:14:44 +02:00
return err
}
}
2018-08-12 12:22:20 +02:00
if !file.Tracked {
return c.removeFile(file.Name)
2018-08-12 12:22:20 +02:00
}
2019-03-18 11:44:33 +02:00
return c.DiscardUnstagedFileChanges(file)
}
// DiscardUnstagedFileChanges directly
func (c *GitCommand) DiscardUnstagedFileChanges(file *File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
2019-02-20 11:01:29 +02:00
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", quotedFileName))
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 "
}
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout %s %s", 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 {
2019-02-20 11:01:29 +02:00
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", c.OSCommand.Quote(filename))
2018-08-13 12:26:02 +02:00
}
// 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(fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName))
2018-08-12 12:22:20 +02:00
}
2018-08-12 13:04:47 +02:00
// 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, error) {
2018-12-08 07:54:54 +02:00
show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
if err != nil {
return "", err
}
// if this is a merge commit, we need to go a step further and get the diff between the two branches we merged
revList, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git rev-list -1 --merges %s^...%s", sha, sha))
if err != nil {
// turns out we get an error here when it's the first commit. We'll just return the original show
return show, nil
}
if len(revList) == 0 {
return show, nil
}
// we want to pull out 1a6a69a and 3b51d7c from this:
// commit ccc771d8b13d5b0d4635db4463556366470fd4f6
// Merge: 1a6a69a 3b51d7c
lines := utils.SplitLines(show)
if len(lines) < 2 {
return show, nil
}
secondLineWords := strings.Split(lines[1], " ")
if len(secondLineWords) < 3 {
return show, nil
}
mergeDiff, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git diff --color %s...%s", secondLineWords[1], secondLineWords[2]))
if err != nil {
return "", err
}
return show + mergeDiff, nil
2018-08-12 13:04:47 +02:00
}
// GetRemoteURL returns current repo remote url
func (c *GitCommand) GetRemoteURL() string {
url, _ := c.OSCommand.RunCommandWithOutput("git config --get remote.origin.url")
return utils.TrimTrailingNewline(url)
}
// CheckRemoteBranchExists Returns remote branch
func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
_, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(
"git show-ref --verify -- refs/remotes/origin/%s",
branch.Name,
))
return err == nil
}
2018-08-12 13:04:47 +02:00
// Diff returns the diff of a file
func (c *GitCommand) Diff(file *File, plain bool) string {
2018-08-12 13:04:47 +02:00
cachedArg := ""
trackedArg := "--"
colorArg := "--color"
2019-02-20 10:47:01 +02:00
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
fileName := c.OSCommand.Quote(split[len(split)-1])
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
}
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
}
if plain {
colorArg = ""
}
command := fmt.Sprintf("git diff %s %s %s %s", colorArg, 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
}
func (c *GitCommand) ApplyPatch(patch string) (string, error) {
2018-12-05 10:33:46 +02:00
filename, err := c.OSCommand.CreateTempFile("patch", patch)
if err != nil {
c.Log.Error(err)
return "", err
}
2019-03-18 11:44:33 +02:00
defer func() { _ = c.OSCommand.Remove(filename) }()
2018-12-05 10:33:46 +02:00
2019-02-20 11:01:29 +02:00
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", c.OSCommand.Quote(filename)))
}
2018-12-07 09:52:31 +02:00
func (c *GitCommand) FastForward(branchName string) error {
upstream := "origin" // hardcoding for now
return c.OSCommand.RunCommand(fmt.Sprintf("git fetch %s %s:%s", upstream, branchName, branchName))
}
2019-02-16 12:01:17 +02:00
func (c *GitCommand) RunSkipEditorCommand(command string) error {
cmd := c.OSCommand.ExecutableFromString(command)
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"EDITOR="+c.OSCommand.GetLazygitPath(),
)
return c.OSCommand.RunExecutable(cmd)
}
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
2019-02-16 12:01:17 +02:00
// By default we skip the editor in the case where a commit will be made
func (c *GitCommand) GenericMerge(commandType string, command string) error {
return c.RunSkipEditorCommand(
fmt.Sprintf(
"git %s --%s",
commandType,
command,
),
)
2019-02-16 12:01:17 +02:00
}
2019-02-18 12:29:43 +02:00
func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) {
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
2019-02-18 12:29:43 +02:00
if err != nil {
return nil, err
2019-02-18 12:29:43 +02:00
}
return c.PrepareInteractiveRebaseCommand(sha, todo, false)
}
func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error {
// we must ensure that we have at least two commits after the selected one
if len(commits) <= index+2 {
// assuming they aren't picking the bottom commit
return errors.New(c.Tr.SLocalize("NoRoom"))
2019-02-18 12:29:43 +02:00
}
todo := ""
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
for _, commit := range orderedCommits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action string) error {
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action)
if err != nil {
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) {
ex := c.OSCommand.GetLazygitPath()
2019-02-18 12:29:43 +02:00
debug := "FALSE"
2019-07-19 03:46:07 +02:00
if c.OSCommand.Config.GetDebug() {
2019-02-18 12:29:43 +02:00
debug = "TRUE"
}
splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash %s", baseSha))
2019-02-18 12:29:43 +02:00
cmd := c.OSCommand.command(splitCmd[0], splitCmd[1:]...)
2019-02-18 12:29:43 +02:00
gitSequenceEditor := ex
if todo == "" {
gitSequenceEditor = "true"
}
2019-02-18 12:29:43 +02:00
cmd.Env = os.Environ()
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
2019-02-18 12:29:43 +02:00
"LAZYGIT_REBASE_TODO="+todo,
"DEBUG="+debug,
"LANG=en_US.UTF-8", // Force using EN as language
"LC_ALL=en_US.UTF-8", // Force using EN as language
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
2019-02-18 12:29:43 +02:00
)
if overrideEditor {
cmd.Env = append(cmd.Env, "EDITOR="+ex)
}
return cmd, nil
}
func (c *GitCommand) HardReset(baseSha string) error {
return c.OSCommand.RunCommand("git reset --hard " + baseSha)
}
func (c *GitCommand) SoftReset(baseSha string) error {
return c.OSCommand.RunCommand("git reset --soft " + baseSha)
}
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, actionIndex int, action string) (string, string, error) {
baseIndex := actionIndex + 1
if len(commits) <= baseIndex {
return "", "", errors.New(c.Tr.SLocalize("CannotRebaseOntoFirstCommit"))
}
if action == "squash" || action == "fixup" {
baseIndex++
if len(commits) <= baseIndex {
return "", "", errors.New(c.Tr.SLocalize("CannotSquashOntoSecondCommit"))
}
2019-02-18 12:29:43 +02:00
}
todo := ""
for i, commit := range commits[0:baseIndex] {
a := "pick"
if i == actionIndex {
a = action
2019-02-18 12:29:43 +02:00
}
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
2019-02-18 12:29:43 +02:00
}
return todo, commits[baseIndex].Sha, nil
2019-02-18 12:29:43 +02:00
}
// AmendTo amends the given commit with whatever files are staged
func (c *GitCommand) AmendTo(sha string) error {
2019-04-07 03:35:34 +02:00
if err := c.CreateFixupCommit(sha); err != nil {
return err
}
2019-04-07 03:35:34 +02:00
return c.SquashAllAboveFixupCommits(sha)
}
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
func (c *GitCommand) EditRebaseTodo(index int, action string) error {
2019-05-12 09:04:32 +02:00
fileName := fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.DotGitDir)
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content)
// we have the most recent commit at the bottom whereas the todo file has
// it at the bottom, so we need to subtract our index from the commit count
contentIndex := commitCount - 1 - index
splitLine := strings.Split(content[contentIndex], " ")
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
result := strings.Join(content, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0644)
}
func (c *GitCommand) getTodoCommitCount(content []string) int {
// count lines that are not blank and are not comments
commitCount := 0
for _, line := range content {
if line != "" && !strings.HasPrefix(line, "#") {
commitCount++
}
}
return commitCount
}
// MoveTodoDown moves a rebase todo item down by one position
func (c *GitCommand) MoveTodoDown(index int) error {
2019-05-12 09:04:32 +02:00
fileName := fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.DotGitDir)
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content)
contentIndex := commitCount - 1 - index
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
result := strings.Join(rearrangedContent, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0644)
}
// Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git revert %s", sha))
}
2019-02-24 04:51:52 +02:00
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
2019-02-24 04:51:52 +02:00
todo := ""
for _, commit := range commits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
2019-02-24 04:51:52 +02:00
}
cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
// GetCommitFiles get the specified commit files
2019-03-16 01:15:46 +02:00
func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) {
cmd := fmt.Sprintf("git show --pretty= --name-only %s", commitSha)
files, err := c.OSCommand.RunCommandWithOutput(cmd)
if err != nil {
return nil, err
}
commitFiles := make([]*CommitFile, 0)
for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") {
commitFiles = append(commitFiles, &CommitFile{
2019-03-16 01:15:46 +02:00
Sha: commitSha,
Name: file,
DisplayString: file,
})
}
return commitFiles, nil
}
// ShowCommitFile get the diff of specified commit file
2019-03-16 01:15:46 +02:00
func (c *GitCommand) ShowCommitFile(commitSha, fileName string) (string, error) {
cmd := fmt.Sprintf("git show --color %s -- %s", commitSha, fileName)
return c.OSCommand.RunCommandWithOutput(cmd)
}
2019-03-11 00:53:46 +02:00
// CheckoutFile checks out the file for the given commit
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
cmd := fmt.Sprintf("git checkout %s %s", commitSha, fileName)
return c.OSCommand.RunCommand(cmd)
}
// DiscardOldFileChanges discards changes to a file from an old commit
func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, fileName string) error {
2019-03-12 10:51:38 +02:00
if len(commits)-1 < commitIndex {
return errors.New("index outside of range of commits")
}
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if c.usingGpg() {
2019-03-12 10:20:19 +02:00
return errors.New(c.Tr.SLocalize("DisabledForGPG"))
}
todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
if err != nil {
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
return err
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := c.OSCommand.RunCommand(fmt.Sprintf("git cat-file -e HEAD^:%s", fileName)); err != nil {
2019-03-18 11:44:33 +02:00
if err := c.OSCommand.Remove(fileName); err != nil {
return err
}
if err := c.StageFile(fileName); err != nil {
return err
}
2019-07-19 04:00:02 +02:00
} else if err := c.CheckoutFile("HEAD^", fileName); err != nil {
return err
}
// amend the commit
cmd, err = c.AmendHead()
if cmd != nil {
2019-03-12 10:20:19 +02:00
return errors.New("received unexpected pointer to cmd")
}
if err != nil {
return err
}
// continue
return c.GenericMerge("rebase", "continue")
}
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
return c.OSCommand.RunCommand("git checkout -- .")
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *GitCommand) RemoveUntrackedFiles() error {
return c.OSCommand.RunCommand("git clean -fd")
}
// ResetHardHead runs `git reset --hard HEAD`
func (c *GitCommand) ResetHardHead() error {
return c.OSCommand.RunCommand("git reset --hard HEAD")
}
// ResetSoftHead runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoftHead() error {
return c.OSCommand.RunCommand("git reset --soft HEAD")
}
// DiffCommits show diff between commits
func (c *GitCommand) DiffCommits(sha1, sha2 string) (string, error) {
cmd := fmt.Sprintf("git diff --color %s %s", sha1, sha2)
return c.OSCommand.RunCommandWithOutput(cmd)
}
2019-04-07 03:35:34 +02:00
// CreateFixupCommit creates a commit that fixes up a previous commit
func (c *GitCommand) CreateFixupCommit(sha string) error {
cmd := fmt.Sprintf("git commit --fixup=%s", sha)
return c.OSCommand.RunCommand(cmd)
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error {
return c.RunSkipEditorCommand(
fmt.Sprintf(
"git rebase --interactive --autostash --autosquash %s^",
sha,
),
)
}
// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
func (c *GitCommand) StashSaveStagedChanges(message string) error {
if err := c.OSCommand.RunCommand("git stash --keep-index"); err != nil {
return err
}
if err := c.StashSave(message); err != nil {
return err
}
if err := c.OSCommand.RunCommand("git stash apply stash@{1}"); err != nil {
return err
}
if err := c.OSCommand.PipeCommands("git stash show -p", "git apply -R"); err != nil {
return err
}
if err := c.OSCommand.RunCommand("git stash drop stash@{1}"); err != nil {
return err
}
// if you had staged an untracked file, that will now appear as 'AD' in git status
// meaning it's deleted in your working tree but added in your index. Given that it's
// now safely stashed, we need to remove it.
files := c.GetStatusFiles()
for _, file := range files {
if file.ShortStatus == "AD" {
if err := c.UnStageFile(file.Name, false); err != nil {
return err
}
}
}
return nil
}