2018-08-12 11:31:27 +02:00
|
|
|
package commands
|
|
|
|
|
|
|
|
import (
|
2018-08-12 12:22:20 +02:00
|
|
|
"errors"
|
2018-08-12 11:31:27 +02:00
|
|
|
"fmt"
|
|
|
|
"os"
|
2018-08-12 12:22:20 +02:00
|
|
|
"os/exec"
|
2018-08-12 11:50:55 +02:00
|
|
|
"strings"
|
2018-08-12 11:31:27 +02:00
|
|
|
|
2018-08-28 10:01:53 +02:00
|
|
|
"github.com/jesseduffield/lazygit/pkg/i18n"
|
2018-08-12 13:04:47 +02:00
|
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
2018-08-25 00:51:47 +02:00
|
|
|
"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-08-12 11:31:27 +02:00
|
|
|
)
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2018-09-02 17:15:27 +02:00
|
|
|
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
|
2018-08-29 22:55:57 +02:00
|
|
|
for {
|
2018-09-02 17:15:27 +02:00
|
|
|
f, err := stat(".git")
|
2018-08-29 22:55:57 +02:00
|
|
|
|
|
|
|
if err == nil && f.IsDir() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-09-02 17:15:27 +02:00
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
return err
|
|
|
|
}
|
2018-08-29 22:55:57 +02:00
|
|
|
|
2018-09-02 17:15:27 +02:00
|
|
|
if err = chdir(".."); err != nil {
|
2018-08-29 22:55:57 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-02 17:15:27 +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
|
|
|
|
}
|
|
|
|
|
2018-09-02 17:15:27 +02:00
|
|
|
worktree, err = repository.Worktree()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return
|
2018-08-29 22:55:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-09-02 17:15:27 +02:00
|
|
|
// GitCommand is our main git interface
|
|
|
|
type GitCommand struct {
|
2018-09-10 21:57:08 +02:00
|
|
|
Log *logrus.Entry
|
|
|
|
OSCommand *OSCommand
|
|
|
|
Worktree *gogit.Worktree
|
|
|
|
Repo *gogit.Repository
|
|
|
|
Tr *i18n.Localizer
|
|
|
|
getGlobalGitConfig func(string) (string, error)
|
|
|
|
getLocalGitConfig func(string) (string, error)
|
2018-09-16 22:03:56 +02:00
|
|
|
removeFile func(string) error
|
2018-09-02 17:15:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
2018-09-02 17:15:27 +02:00
|
|
|
},
|
|
|
|
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{
|
2018-09-10 21:57:08 +02:00
|
|
|
Log: log,
|
|
|
|
OSCommand: osCommand,
|
|
|
|
Tr: tr,
|
|
|
|
Worktree: worktree,
|
|
|
|
Repo: repo,
|
|
|
|
getGlobalGitConfig: gitconfig.Global,
|
|
|
|
getLocalGitConfig: gitconfig.Local,
|
2018-09-20 01:48:56 +02:00
|
|
|
removeFile: os.RemoveAll,
|
2018-09-02 17:15:27 +02:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2018-08-12 12:22:20 +02:00
|
|
|
// GetStashEntries stash entryies
|
2018-09-17 13:02:30 +02:00
|
|
|
func (c *GitCommand) GetStashEntries() []*StashEntry {
|
2018-08-14 09:47:33 +02:00
|
|
|
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
|
2018-09-17 13:02:30 +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
|
|
|
|
}
|
|
|
|
|
2018-09-17 13:02:30 +02:00
|
|
|
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) {
|
2018-08-14 09:47:33 +02:00
|
|
|
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-09-17 13:02:30 +02:00
|
|
|
func (c *GitCommand) GetStatusFiles() []*File {
|
2018-08-12 13:04:47 +02:00
|
|
|
statusOutput, _ := c.GitStatus()
|
|
|
|
statusStrings := utils.SplitLines(statusOutput)
|
2018-09-17 13:02:30 +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:])
|
2018-09-06 23:16:56 +02:00
|
|
|
_, 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-09-06 23:16:56 +02:00
|
|
|
|
2018-09-17 13:02:30 +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 != " ",
|
2018-09-06 23:16:56 +02:00
|
|
|
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
|
|
|
return files
|
2018-08-12 12:22:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// StashDo modify stash
|
2018-08-14 09:47:33 +02:00
|
|
|
func (c *GitCommand) StashDo(index int, method string) error {
|
2018-08-28 20:18:34 +02:00
|
|
|
return c.OSCommand.RunCommand(fmt.Sprintf("git stash %s stash@{%d}", method, index))
|
2018-08-12 12:22:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// StashSave save stash
|
2018-08-14 09:47:33 +02:00
|
|
|
// TODO: before calling this, check if there is anything to save
|
|
|
|
func (c *GitCommand) StashSave(message string) error {
|
2018-08-28 20:24:19 +02:00
|
|
|
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-09-17 13:02:30 +02:00
|
|
|
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
|
2018-09-17 13:02:30 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2018-12-05 11:06:47 +02:00
|
|
|
// ResetAndClean removes all unstaged changes and removes all untracked files
|
|
|
|
func (c *GitCommand) ResetAndClean() error {
|
|
|
|
if err := c.OSCommand.RunCommand("git reset --hard HEAD"); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.OSCommand.RunCommand("git clean -fd")
|
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}")
|
2018-08-12 11:50:55 +02:00
|
|
|
}
|
|
|
|
|
2018-12-07 09:52:31 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed
|
2018-09-17 21:19:17 +02:00
|
|
|
// to the remote branch of the current branch, a map is returned to ease look up
|
2018-09-19 11:16:55 +02:00
|
|
|
func (c *GitCommand) GetCommitsToPush() map[string]bool {
|
2018-09-17 21:19:17 +02:00
|
|
|
pushables := map[string]bool{}
|
2018-10-06 07:40:12 +02:00
|
|
|
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
|
2018-08-12 11:50:55 +02:00
|
|
|
if err != nil {
|
2018-09-17 21:19:17 +02:00
|
|
|
return pushables
|
2018-08-12 11:50:55 +02:00
|
|
|
}
|
2018-09-17 21:19:17 +02:00
|
|
|
for _, p := range utils.SplitLines(o) {
|
|
|
|
pushables[p] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
return pushables
|
2018-08-12 11:50:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// RenameCommit renames the topmost commit with the given name
|
2018-08-14 09:47:33 +02:00
|
|
|
func (c *GitCommand) RenameCommit(name string) error {
|
2018-09-06 23:38:49 +02:00
|
|
|
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
|
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
|
|
|
}
|
2018-12-02 15:58:18 +02:00
|
|
|
return "-"
|
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
|
2018-08-14 09:47:33 +02:00
|
|
|
func (c *GitCommand) ResetToCommit(sha string) error {
|
2018-09-06 23:38:49 +02:00
|
|
|
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
|
2018-08-14 09:47:33 +02:00
|
|
|
func (c *GitCommand) NewBranch(name string) error {
|
2018-09-06 23:38:49 +02:00
|
|
|
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.
|
2018-09-25 12:11:33 +02:00
|
|
|
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-09-25 12:11:33 +02:00
|
|
|
}
|
|
|
|
|
2018-08-12 12:22:20 +02:00
|
|
|
// DeleteBranch delete branch
|
2018-08-15 15:02:01 +02:00
|
|
|
func (c *GitCommand) DeleteBranch(branch string, force bool) error {
|
2018-09-06 23:38:49 +02:00
|
|
|
command := "git branch -d"
|
|
|
|
|
2018-08-15 15:02:01 +02:00
|
|
|
if force {
|
2018-09-06 23:38:49 +02:00
|
|
|
command = "git branch -D"
|
2018-08-15 15:02:01 +02:00
|
|
|
}
|
2018-09-06 23:38:49 +02:00
|
|
|
|
|
|
|
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) {
|
2018-08-14 09:47:33 +02:00
|
|
|
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
|
2018-08-14 09:47:33 +02:00
|
|
|
func (c *GitCommand) Merge(branchName string) error {
|
2018-09-06 23:38:49 +02:00
|
|
|
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
|
2018-08-14 09:47:33 +02:00
|
|
|
func (c *GitCommand) AbortMerge() error {
|
|
|
|
return c.OSCommand.RunCommand("git merge --abort")
|
2018-08-12 12:22:20 +02:00
|
|
|
}
|
|
|
|
|
2018-09-10 21:57:08 +02:00
|
|
|
// usingGpg tells us whether the user has gpg enabled so that we can know
|
2018-08-14 00:33:27 +02:00
|
|
|
// whether we need to run a subprocess to allow them to enter their password
|
2018-09-10 21:57:08 +02:00
|
|
|
func (c *GitCommand) usingGpg() bool {
|
2018-09-12 07:50:49 +02:00
|
|
|
gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
|
2018-08-14 00:33:27 +02:00
|
|
|
if gpgsign == "" {
|
2018-09-12 07:50:49 +02:00
|
|
|
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
|
2018-08-14 00:33:27 +02:00
|
|
|
}
|
2018-09-10 21:57:08 +02:00
|
|
|
value := strings.ToLower(gpgsign)
|
|
|
|
|
|
|
|
return value == "true" || value == "1" || value == "yes" || value == "on"
|
2018-08-14 00:33:27 +02:00
|
|
|
}
|
|
|
|
|
2018-09-10 22:01:52 +02:00
|
|
|
// Commit commits to git
|
2018-09-12 15:20:35 +02:00
|
|
|
func (c *GitCommand) Commit(message string, amend bool) (*exec.Cmd, error) {
|
|
|
|
amendParam := ""
|
|
|
|
if amend {
|
2018-10-08 22:19:42 +02:00
|
|
|
amendParam = " --amend"
|
2018-09-12 15:20:35 +02:00
|
|
|
}
|
2018-10-08 22:19:42 +02:00
|
|
|
command := fmt.Sprintf("git commit%s -m %s", amendParam, c.OSCommand.Quote(message))
|
2018-09-10 21:57:08 +02:00
|
|
|
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
|
|
|
}
|
2018-09-10 22:25:02 +02:00
|
|
|
|
2018-08-14 09:47:33 +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
|
2018-11-02 10:54:54 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2018-09-10 22:38:08 +02:00
|
|
|
// Push pushes to a branch
|
2018-10-17 21:12:33 +02:00
|
|
|
func (c *GitCommand) Push(branchName string, force bool, ask func(string) string) error {
|
2018-08-19 13:28:13 +02:00
|
|
|
forceFlag := ""
|
|
|
|
if force {
|
|
|
|
forceFlag = "--force-with-lease "
|
|
|
|
}
|
2018-09-10 22:38:08 +02:00
|
|
|
|
2018-10-17 20:38:13 +02:00
|
|
|
cmd := fmt.Sprintf("git push %s -u origin %s", forceFlag, branchName)
|
2018-10-17 21:12:33 +02:00
|
|
|
return c.OSCommand.DetectUnamePass(cmd, ask)
|
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
|
2018-08-14 09:47:33 +02:00
|
|
|
func (c *GitCommand) SquashPreviousTwoCommits(message string) error {
|
|
|
|
// TODO: test this
|
2018-09-12 20:43:03 +02:00
|
|
|
if err := c.OSCommand.RunCommand("git reset --soft HEAD^"); err != nil {
|
2018-08-14 10:30:06 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
// TODO: if password is required, we need to return a subprocess
|
2018-09-12 20:43:03 +02:00
|
|
|
return c.OSCommand.RunCommand(fmt.Sprintf("git commit --amend -m %s", 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
|
2018-08-14 09:47:33 +02:00
|
|
|
func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error {
|
2018-08-12 11:50:55 +02:00
|
|
|
commands := []string{
|
2018-09-12 22:28:49 +02:00
|
|
|
fmt.Sprintf("git checkout -q %s", shaValue),
|
|
|
|
fmt.Sprintf("git reset --soft %s^", shaValue),
|
|
|
|
fmt.Sprintf("git commit --amend -C %s^", shaValue),
|
|
|
|
fmt.Sprintf("git rebase --onto HEAD %s %s", shaValue, branchName),
|
2018-08-12 11:50:55 +02:00
|
|
|
}
|
|
|
|
for _, command := range commands {
|
2018-08-12 13:04:47 +02:00
|
|
|
c.Log.Info(command)
|
2018-09-12 22:28:49 +02:00
|
|
|
|
|
|
|
if output, err := c.OSCommand.RunCommandWithOutput(command); err != nil {
|
|
|
|
ret := output
|
|
|
|
// We are already in an error state here so we're just going to append
|
|
|
|
// the output of these commands
|
|
|
|
output, _ := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git branch -d %s", shaValue))
|
|
|
|
ret += output
|
|
|
|
output, _ = c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git checkout %s", branchName))
|
|
|
|
ret += output
|
|
|
|
|
2018-08-12 13:04:47 +02:00
|
|
|
c.Log.Info(ret)
|
2018-09-12 22:28:49 +02:00
|
|
|
return errors.New(ret)
|
2018-08-12 11:50:55 +02:00
|
|
|
}
|
|
|
|
}
|
2018-09-12 22:28:49 +02:00
|
|
|
|
2018-08-14 09:47:33 +02:00
|
|
|
return nil
|
2018-08-12 11:50:55 +02:00
|
|
|
}
|
2018-08-12 12:22:20 +02:00
|
|
|
|
2018-09-12 22:28:49 +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) {
|
2018-09-13 21:27:35 +02:00
|
|
|
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 {
|
2018-09-13 21:27:35 +02:00
|
|
|
return c.OSCommand.RunCommand(fmt.Sprintf("git add %s", c.OSCommand.Quote(fileName)))
|
2018-08-12 12:22:20 +02:00
|
|
|
}
|
|
|
|
|
2018-08-25 00:51:47 +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-09-13 21:27:35 +02:00
|
|
|
command := "git rm --cached %s"
|
2018-08-12 12:22:20 +02:00
|
|
|
if tracked {
|
2018-09-13 21:27:35 +02:00
|
|
|
command = "git reset HEAD %s"
|
2018-08-12 12:22:20 +02:00
|
|
|
}
|
2018-09-13 21:27:35 +02:00
|
|
|
return c.OSCommand.RunCommand(fmt.Sprintf(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) {
|
2018-08-14 09:47:33 +02:00
|
|
|
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) {
|
2018-08-14 09:47:33 +02:00
|
|
|
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-09-17 13:02:30 +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 {
|
2018-09-16 22:03:56 +02:00
|
|
|
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", file.Name)); err != nil {
|
2018-08-18 12:14:44 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2018-08-12 12:22:20 +02:00
|
|
|
if !file.Tracked {
|
2018-09-16 22:03:56 +02:00
|
|
|
return c.removeFile(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-09-16 22:03:56 +02:00
|
|
|
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", 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
|
2018-08-14 09:47:33 +02:00
|
|
|
func (c *GitCommand) Checkout(branch string, force bool) error {
|
2018-08-12 12:22:20 +02:00
|
|
|
forceArg := ""
|
|
|
|
if force {
|
|
|
|
forceArg = "--force "
|
|
|
|
}
|
2018-09-16 22:08:23 +02:00
|
|
|
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 {
|
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
|
|
|
}
|
|
|
|
|
2018-08-29 14:27:17 +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) {
|
2018-09-16 22:12:03 +02:00
|
|
|
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
|
|
|
|
2018-09-25 12:11:33 +02:00
|
|
|
func (c *GitCommand) getMergeBase() (string, error) {
|
|
|
|
currentBranch, err := c.CurrentBranchName()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
baseBranch := "master"
|
|
|
|
if strings.HasPrefix(currentBranch, "feature/") {
|
|
|
|
baseBranch = "develop"
|
|
|
|
}
|
|
|
|
|
|
|
|
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
|
2018-09-18 13:32:24 +02:00
|
|
|
if err != nil {
|
2018-09-25 12:11:33 +02:00
|
|
|
// swallowing error because it's not a big deal; probably because there are no commits yet
|
2018-09-18 13:32:24 +02:00
|
|
|
}
|
2018-09-25 12:11:33 +02:00
|
|
|
return output, nil
|
2018-09-18 13:32:24 +02:00
|
|
|
}
|
|
|
|
|
2018-08-12 13:04:47 +02:00
|
|
|
// GetCommits obtains the commits of the current branch
|
2018-09-25 12:11:33 +02:00
|
|
|
func (c *GitCommand) GetCommits() ([]*Commit, error) {
|
2018-08-13 12:26:02 +02:00
|
|
|
pushables := c.GetCommitsToPush()
|
|
|
|
log := c.GetLog()
|
2018-09-20 01:41:29 +02:00
|
|
|
|
2018-08-13 12:26:02 +02:00
|
|
|
lines := utils.SplitLines(log)
|
2018-09-20 01:41:29 +02:00
|
|
|
commits := make([]*Commit, len(lines))
|
2018-08-12 13:04:47 +02:00
|
|
|
// now we can split it up and turn it into commits
|
2018-09-18 13:32:24 +02:00
|
|
|
for i, line := range lines {
|
2018-08-12 13:04:47 +02:00
|
|
|
splitLine := strings.Split(line, " ")
|
|
|
|
sha := splitLine[0]
|
2018-09-17 21:19:17 +02:00
|
|
|
_, pushed := pushables[sha]
|
2018-09-20 01:41:29 +02:00
|
|
|
commits[i] = &Commit{
|
2018-08-12 13:04:47 +02:00
|
|
|
Sha: sha,
|
|
|
|
Name: strings.Join(splitLine[1:], " "),
|
|
|
|
Pushed: pushed,
|
|
|
|
DisplayString: strings.Join(splitLine, " "),
|
2018-09-18 13:32:24 +02:00
|
|
|
}
|
|
|
|
}
|
2018-09-25 12:11:33 +02:00
|
|
|
return c.setCommitMergedStatuses(commits)
|
2018-09-18 13:32:24 +02:00
|
|
|
}
|
|
|
|
|
2018-09-25 12:11:33 +02:00
|
|
|
func (c *GitCommand) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
|
|
|
|
ancestor, err := c.getMergeBase()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-09-18 13:32:24 +02:00
|
|
|
if ancestor == "" {
|
2018-09-25 12:11:33 +02:00
|
|
|
return commits, nil
|
2018-09-18 13:32:24 +02:00
|
|
|
}
|
|
|
|
passedAncestor := false
|
|
|
|
for i, commit := range commits {
|
|
|
|
if strings.HasPrefix(ancestor, commit.Sha) {
|
|
|
|
passedAncestor = true
|
|
|
|
}
|
|
|
|
commits[i].Merged = passedAncestor
|
2018-08-12 13:04:47 +02:00
|
|
|
}
|
2018-09-25 12:11:33 +02:00
|
|
|
return commits, nil
|
2018-08-12 13:04:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2018-08-14 09:47:33 +02:00
|
|
|
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 ""
|
|
|
|
}
|
2018-09-17 21:19:17 +02:00
|
|
|
|
2018-08-12 13:04:47 +02:00
|
|
|
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
|
2018-09-18 20:53:32 +02:00
|
|
|
func (c *GitCommand) Show(sha string) (string, error) {
|
|
|
|
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
|
2018-08-12 13:04:47 +02:00
|
|
|
}
|
|
|
|
|
2018-10-12 14:06:03 +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)
|
|
|
|
}
|
|
|
|
|
2018-10-17 14:20:15 +02:00
|
|
|
// 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
|
2018-12-02 10:57:01 +02:00
|
|
|
func (c *GitCommand) Diff(file *File, plain bool) string {
|
2018-08-12 13:04:47 +02:00
|
|
|
cachedArg := ""
|
2018-09-18 21:09:51 +02:00
|
|
|
trackedArg := "--"
|
2018-12-02 10:57:01 +02:00
|
|
|
colorArg := "--color"
|
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
|
|
|
}
|
|
|
|
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
|
|
|
}
|
2018-12-02 10:57:01 +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
|
2018-08-14 09:47:33 +02:00
|
|
|
s, _ := c.OSCommand.RunCommandWithOutput(command)
|
2018-08-12 13:04:47 +02:00
|
|
|
return s
|
|
|
|
}
|
2018-12-02 10:57:01 +02:00
|
|
|
|
|
|
|
func (c *GitCommand) ApplyPatch(patch string) (string, error) {
|
2018-12-05 10:33:46 +02:00
|
|
|
filename, err := c.OSCommand.CreateTempFile("patch", patch)
|
2018-12-02 10:57:01 +02:00
|
|
|
if err != nil {
|
|
|
|
c.Log.Error(err)
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2018-12-05 10:33:46 +02:00
|
|
|
defer func() { _ = c.OSCommand.RemoveFile(filename) }()
|
|
|
|
|
|
|
|
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", filename))
|
2018-12-02 10:57:01 +02:00
|
|
|
}
|
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))
|
|
|
|
}
|