1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-06-19 00:28:03 +02:00

start breaking up git struct

This commit is contained in:
Jesse Duffield
2022-01-02 10:34:33 +11:00
parent 4a1d23dc27
commit f503ff1ecb
76 changed files with 2234 additions and 1758 deletions

View File

@ -6,24 +6,47 @@ import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string, base string) error {
return c.Cmd.New(fmt.Sprintf("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))).Run()
// this takes something like:
// * (HEAD detached at 264fc6f5)
// remotes
// and returns '264fc6f5' as the second match
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
type BranchCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewBranchCommands(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *BranchCommands {
return &BranchCommands{
Common: common,
cmd: cmd,
}
}
// New creates a new branch
func (self *BranchCommands) New(name string, base string) error {
return self.cmd.New(fmt.Sprintf("git checkout -b %s %s", self.cmd.Quote(name), self.cmd.Quote(base))).Run()
}
// CurrentBranchName get the current branch name and displayname.
// the first returned string is the name and the second is the displayname
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
func (c *GitCommand) CurrentBranchName() (string, string, error) {
branchName, err := c.Cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput()
func (self *BranchCommands) CurrentBranchName() (string, string, error) {
branchName, err := self.cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput()
if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName)
return trimmedBranchName, trimmedBranchName, nil
}
output, err := c.Cmd.New("git branch --contains").DontLog().RunWithOutput()
output, err := self.cmd.New("git branch --contains").DontLog().RunWithOutput()
if err != nil {
return "", "", err
}
@ -39,15 +62,15 @@ func (c *GitCommand) CurrentBranchName() (string, string, error) {
return "HEAD", "HEAD", nil
}
// DeleteBranch delete branch
func (c *GitCommand) DeleteBranch(branch string, force bool) error {
// Delete delete branch
func (self *BranchCommands) Delete(branch string, force bool) error {
command := "git branch -d"
if force {
command = "git branch -D"
}
return c.Cmd.New(fmt.Sprintf("%s %s", command, c.OSCommand.Quote(branch))).Run()
return self.cmd.New(fmt.Sprintf("%s %s", command, self.cmd.Quote(branch))).Run()
}
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
@ -56,13 +79,13 @@ type CheckoutOptions struct {
EnvVars []string
}
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
func (self *BranchCommands) Checkout(branch string, options CheckoutOptions) error {
forceArg := ""
if options.Force {
forceArg = " --force"
}
return c.Cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch))).
return self.cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, self.cmd.Quote(branch))).
// prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0").
@ -70,104 +93,84 @@ func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
Run()
}
// GetBranchGraph gets the color-formatted graph of the log for the given branch
// GetGraph 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.GetBranchGraphCmdObj(branchName).DontLog().RunWithOutput()
func (self *BranchCommands) GetGraph(branchName string) (string, error) {
return self.GetGraphCmdObj(branchName).DontLog().RunWithOutput()
}
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
output, err := c.Cmd.New(fmt.Sprintf("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))).DontLog().RunWithOutput()
func (self *BranchCommands) GetGraphCmdObj(branchName string) oscommands.ICmdObj {
branchLogCmdTemplate := self.UserConfig.Git.BranchLogCmd
templateValues := map[string]string{
"branchName": self.cmd.Quote(branchName),
}
return self.cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)).DontLog()
}
func (self *BranchCommands) SetCurrentBranchUpstream(upstream string) error {
return self.cmd.New("git branch --set-upstream-to=" + self.cmd.Quote(upstream)).Run()
}
func (self *BranchCommands) GetUpstream(branchName string) (string, error) {
output, err := self.cmd.New(fmt.Sprintf("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", self.cmd.Quote(branchName))).DontLog().RunWithOutput()
return strings.TrimSpace(output), err
}
func (c *GitCommand) GetBranchGraphCmdObj(branchName string) oscommands.ICmdObj {
branchLogCmdTemplate := c.UserConfig.Git.BranchLogCmd
templateValues := map[string]string{
"branchName": c.OSCommand.Quote(branchName),
}
return c.Cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)).DontLog()
func (self *BranchCommands) SetUpstream(remoteName string, remoteBranchName string, branchName string) error {
return self.cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))).Run()
}
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
return c.Cmd.New("git branch -u " + c.OSCommand.Quote(upstream)).Run()
func (self *BranchCommands) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
return self.GetCommitDifferences("HEAD", "HEAD@{u}")
}
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
return c.Cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))).Run()
}
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
return c.GetCommitDifferences("HEAD", "HEAD@{u}")
}
func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) {
return c.GetCommitDifferences(branchName, branchName+"@{u}")
func (self *BranchCommands) GetUpstreamDifferenceCount(branchName string) (string, string) {
return self.GetCommitDifferences(branchName, branchName+"@{u}")
}
// GetCommitDifferences checks how many pushables/pullables there are for the
// current branch
func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
func (self *BranchCommands) GetCommitDifferences(from, to string) (string, string) {
command := "git rev-list %s..%s --count"
pushableCount, err := c.Cmd.New(fmt.Sprintf(command, to, from)).DontLog().RunWithOutput()
pushableCount, err := self.cmd.New(fmt.Sprintf(command, to, from)).DontLog().RunWithOutput()
if err != nil {
return "?", "?"
}
pullableCount, err := c.Cmd.New(fmt.Sprintf(command, from, to)).DontLog().RunWithOutput()
pullableCount, err := self.cmd.New(fmt.Sprintf(command, from, to)).DontLog().RunWithOutput()
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
func (self *BranchCommands) IsHeadDetached() bool {
err := self.cmd.New("git symbolic-ref -q HEAD").DontLog().Run()
return err != nil
}
func (self *BranchCommands) Rename(oldName string, newName string) error {
return self.cmd.New(fmt.Sprintf("git branch --move %s %s", self.cmd.Quote(oldName), self.cmd.Quote(newName))).Run()
}
func (self *BranchCommands) GetRawBranches() (string, error) {
return self.cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`).DontLog().RunWithOutput()
}
type MergeOpts struct {
FastForwardOnly bool
}
// Merge merge
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error {
mergeArg := ""
if c.UserConfig.Git.Merging.Args != "" {
mergeArg = " " + c.UserConfig.Git.Merging.Args
if self.UserConfig.Git.Merging.Args != "" {
mergeArg = " " + self.UserConfig.Git.Merging.Args
}
command := fmt.Sprintf("git merge --no-edit%s %s", mergeArg, c.OSCommand.Quote(branchName))
command := fmt.Sprintf("git merge --no-edit%s %s", mergeArg, self.cmd.Quote(branchName))
if opts.FastForwardOnly {
command = fmt.Sprintf("%s --ff-only", command)
}
return c.OSCommand.Cmd.New(command).Run()
}
// AbortMerge abort merge
func (c *GitCommand) AbortMerge() error {
return c.Cmd.New("git merge --abort").Run()
}
func (c *GitCommand) IsHeadDetached() bool {
err := c.Cmd.New("git symbolic-ref -q HEAD").DontLog().Run()
return err != nil
}
// ResetHardHead runs `git reset --hard`
func (c *GitCommand) ResetHard(ref string) error {
return c.Cmd.New("git reset --hard " + c.OSCommand.Quote(ref)).Run()
}
// ResetSoft runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoft(ref string) error {
return c.Cmd.New("git reset --soft " + c.OSCommand.Quote(ref)).Run()
}
func (c *GitCommand) ResetMixed(ref string) error {
return c.Cmd.New("git reset --mixed " + c.OSCommand.Quote(ref)).Run()
}
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
return c.Cmd.New(fmt.Sprintf("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))).Run()
}
func (c *GitCommand) GetRawBranches() (string, error) {
return c.Cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`).DontLog().RunWithOutput()
return self.cmd.New(command).Run()
}

View File

@ -42,7 +42,7 @@ func TestGitCommandGetCommitDifferences(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
pushables, pullables := gitCmd.GetCommitDifferences("HEAD", "@{u}")
pushables, pullables := gitCmd.Branch.GetCommitDifferences("HEAD", "@{u}")
assert.EqualValues(t, s.expectedPushables, pushables)
assert.EqualValues(t, s.expectedPullables, pullables)
s.runner.CheckForMissingCalls()
@ -55,7 +55,7 @@ func TestGitCommandNewBranch(t *testing.T) {
Expect(`git checkout -b "test" "master"`, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.NewBranch("test", "master"))
assert.NoError(t, gitCmd.Branch.New("test", "master"))
runner.CheckForMissingCalls()
}
@ -90,7 +90,7 @@ func TestGitCommandDeleteBranch(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.DeleteBranch("test", s.force))
s.test(gitCmd.Branch.Delete("test", s.force))
s.runner.CheckForMissingCalls()
})
}
@ -101,7 +101,7 @@ func TestGitCommandMerge(t *testing.T) {
Expect(`git merge --no-edit "test"`, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
assert.NoError(t, gitCmd.Branch.Merge("test", MergeOpts{}))
runner.CheckForMissingCalls()
}
@ -135,7 +135,7 @@ func TestGitCommandCheckout(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
s.test(gitCmd.Branch.Checkout("test", CheckoutOptions{Force: s.force}))
s.runner.CheckForMissingCalls()
})
}
@ -146,7 +146,7 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--",
}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
_, err := gitCmd.GetBranchGraph("test")
_, err := gitCmd.Branch.GetGraph("test")
assert.NoError(t, err)
}
@ -215,36 +215,8 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.CurrentBranchName())
s.test(gitCmd.Branch.CurrentBranchName())
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandResetHard(t *testing.T) {
type scenario struct {
testName string
ref string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"valid case",
"HEAD",
oscommands.NewFakeRunner(t).
Expect(`git reset --hard "HEAD"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.ResetHard(s.ref))
})
}
}

View File

@ -4,18 +4,34 @@ import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.Cmd.New("git commit --allow-empty --amend --only -m " + c.OSCommand.Quote(name)).Run()
type CommitCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewCommitCommands(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *CommitCommands {
return &CommitCommands{
Common: common,
cmd: cmd,
}
}
// RewordLastCommit renames the topmost commit with the given name
func (self *CommitCommands) RewordLastCommit(name string) error {
return self.cmd.New("git commit --allow-empty --amend --only -m " + self.cmd.Quote(name)).Run()
}
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string, strength string, envVars []string) error {
return c.Cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)).
func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars []string) error {
return self.cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)).
// prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0").
@ -23,11 +39,11 @@ func (c *GitCommand) ResetToCommit(sha string, strength string, envVars []string
Run()
}
func (c *GitCommand) CommitCmdObj(message string, flags string) oscommands.ICmdObj {
func (self *CommitCommands) CommitCmdObj(message string, flags string) oscommands.ICmdObj {
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line))
lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line))
}
flagsStr := ""
@ -35,71 +51,56 @@ func (c *GitCommand) CommitCmdObj(message string, flags string) oscommands.ICmdO
flagsStr = fmt.Sprintf(" %s", flags)
}
return c.Cmd.New(fmt.Sprintf("git commit%s%s", flagsStr, lineArgs))
return self.cmd.New(fmt.Sprintf("git commit%s%s", flagsStr, lineArgs))
}
// Get the subject of the HEAD commit
func (c *GitCommand) GetHeadCommitMessage() (string, error) {
message, err := c.Cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput()
func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
message, err := self.cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput()
return strings.TrimSpace(message), err
}
func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) {
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
messageWithHeader, err := c.Cmd.New(cmdStr).DontLog().RunWithOutput()
messageWithHeader, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
return strings.TrimSpace(message), err
}
func (c *GitCommand) GetCommitMessageFirstLine(sha string) (string, error) {
return c.Cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).DontLog().RunWithOutput()
func (self *CommitCommands) GetCommitMessageFirstLine(sha string) (string, error) {
return self.cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).DontLog().RunWithOutput()
}
// AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() error {
return c.AmendHeadCmdObj().Run()
func (self *CommitCommands) AmendHead() error {
return self.AmendHeadCmdObj().Run()
}
func (c *GitCommand) AmendHeadCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git commit --amend --no-edit --allow-empty")
func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
return self.cmd.New("git commit --amend --no-edit --allow-empty")
}
func (c *GitCommand) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
contextSize := c.UserConfig.Git.DiffContextSize
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
contextSize := self.UserConfig.Git.DiffContextSize
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath))
filterPathArg = fmt.Sprintf(" -- %s", self.cmd.Quote(filterPath))
}
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", c.colorArg(), contextSize, sha, filterPathArg)
return c.Cmd.New(cmdStr).DontLog()
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", self.UserConfig.Git.Paging.ColorArg, contextSize, sha, filterPathArg)
return self.cmd.New(cmdStr).DontLog()
}
// Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error {
return c.Cmd.New(fmt.Sprintf("git revert %s", sha)).Run()
func (self *CommitCommands) Revert(sha string) error {
return self.cmd.New(fmt.Sprintf("git revert %s", sha)).Run()
}
func (c *GitCommand) RevertMerge(sha string, parentNumber int) error {
return c.Cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run()
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
todo := ""
for _, commit := range commits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmdObj, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
if err != nil {
return err
}
return cmdObj.Run()
func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error {
return self.cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run()
}
// CreateFixupCommit creates a commit that fixes up a previous commit
func (c *GitCommand) CreateFixupCommit(sha string) error {
return c.Cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
func (self *CommitCommands) CreateFixupCommit(sha string) error {
return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
}

View File

@ -7,12 +7,12 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGitCommandRenameCommit(t *testing.T) {
func TestGitCommandRewordCommit(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.RenameCommit("test"))
assert.NoError(t, gitCmd.Commit.RewordLastCommit("test"))
runner.CheckForMissingCalls()
}
@ -21,7 +21,7 @@ func TestGitCommandResetToCommit(t *testing.T) {
ExpectGitArgs([]string{"reset", "--hard", "78976bc"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", []string{}))
assert.NoError(t, gitCmd.Commit.ResetToCommit("78976bc", "hard", []string{}))
runner.CheckForMissingCalls()
}
@ -57,7 +57,7 @@ func TestGitCommandCommitObj(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
cmdStr := gitCmd.CommitCmdObj(s.message, s.flags).ToString()
cmdStr := gitCmd.Commit.CommitCmdObj(s.message, s.flags).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}
@ -86,7 +86,7 @@ func TestGitCommandCreateFixupCommit(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.CreateFixupCommit(s.sha))
s.test(gitCmd.Commit.CreateFixupCommit(s.sha))
s.runner.CheckForMissingCalls()
})
}
@ -125,7 +125,7 @@ func TestGitCommandShowCmdObj(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
cmdStr := gitCmd.ShowCmdObj("1234567890", s.filterPath).ToString()
cmdStr := gitCmd.Commit.ShowCmdObj("1234567890", s.filterPath).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}

View File

@ -5,24 +5,42 @@ import (
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) ConfiguredPager() string {
type ConfigCommands struct {
*common.Common
gitConfig git_config.IGitConfig
}
func NewConfigCommands(
common *common.Common,
gitConfig git_config.IGitConfig,
) *ConfigCommands {
return &ConfigCommands{
Common: common,
gitConfig: gitConfig,
}
}
func (self *ConfigCommands) ConfiguredPager() string {
if os.Getenv("GIT_PAGER") != "" {
return os.Getenv("GIT_PAGER")
}
if os.Getenv("PAGER") != "" {
return os.Getenv("PAGER")
}
output := c.GitConfig.Get("core.pager")
output := self.gitConfig.Get("core.pager")
return strings.Split(output, "\n")[0]
}
func (c *GitCommand) GetPager(width int) string {
useConfig := c.UserConfig.Git.Paging.UseConfig
func (self *ConfigCommands) GetPager(width int) string {
useConfig := self.UserConfig.Git.Paging.UseConfig
if useConfig {
pager := c.ConfiguredPager()
pager := self.ConfiguredPager()
return strings.Split(pager, "| less")[0]
}
@ -30,21 +48,35 @@ func (c *GitCommand) GetPager(width int) string {
"columnWidth": strconv.Itoa(width/2 - 6),
}
pagerTemplate := c.UserConfig.Git.Paging.Pager
pagerTemplate := self.UserConfig.Git.Paging.Pager
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
func (c *GitCommand) colorArg() string {
return c.UserConfig.Git.Paging.ColorArg
}
// 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 {
overrideGpg := c.UserConfig.Git.OverrideGpg
func (self *ConfigCommands) UsingGpg() bool {
overrideGpg := self.UserConfig.Git.OverrideGpg
if overrideGpg {
return false
}
return c.GitConfig.GetBool("commit.gpgsign")
return self.gitConfig.GetBool("commit.gpgsign")
}
func (self *ConfigCommands) GetCoreEditor() string {
return self.gitConfig.Get("core.editor")
}
// GetRemoteURL returns current repo remote url
func (self *ConfigCommands) GetRemoteURL() string {
return self.gitConfig.Get("remote.origin.url")
}
func (self *ConfigCommands) GetShowUntrackedFiles() string {
return self.gitConfig.Get("status.showUntrackedFiles")
}
// this determines whether the user has configured to push to the remote branch of the same name as the current or not
func (self *ConfigCommands) GetPushToCurrent() bool {
return self.gitConfig.Get("push.default") == "current"
}

View File

@ -1,10 +1,6 @@
package commands
import (
"io"
"io/ioutil"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@ -16,16 +12,13 @@ func NewDummyGitCommand() *GitCommand {
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
runner := &oscommands.FakeCmdObjRunner{}
builder := oscommands.NewDummyCmdObjBuilder(runner)
return &GitCommand{
Common: utils.NewDummyCommon(),
Cmd: builder,
OSCommand: osCommand,
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
GetCmdWriter: func() io.Writer { return ioutil.Discard },
}
return NewGitCommandAux(
utils.NewDummyCommon(),
osCommand,
utils.NewDummyGitConfig(),
".git",
nil,
)
}
func NewDummyGitCommandWithRunner(runner oscommands.ICmdObjRunner) *GitCommand {

View File

@ -1,23 +1,43 @@
package commands
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) {
type FileCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
config *ConfigCommands
os FileOSCommand
}
type FileOSCommand interface {
Getenv(string) string
}
func NewFileCommands(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
config *ConfigCommands,
osCommand FileOSCommand,
) *FileCommands {
return &FileCommands{
Common: common,
cmd: cmd,
config: config,
os: osCommand,
}
}
// Cat obtains the content of a file
func (self *FileCommands) Cat(fileName string) (string, error) {
buf, err := ioutil.ReadFile(fileName)
if err != nil {
return "", nil
@ -25,335 +45,24 @@ func (c *GitCommand) CatFile(fileName string) (string, error) {
return string(buf), nil
}
func (c *GitCommand) OpenMergeToolCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git mergetool")
}
func (c *GitCommand) OpenMergeTool() error {
return c.OpenMergeToolCmdObj().Run()
}
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
return c.Cmd.New("git add -- " + c.OSCommand.Quote(fileName)).Run()
}
// StageAll stages all files
func (c *GitCommand) StageAll() error {
return c.Cmd.New("git add -A").Run()
}
// UnstageAll unstages all files
func (c *GitCommand) UnstageAll() error {
return c.Cmd.New("git reset").Run()
}
// UnStageFile unstages a file
// we accept an array of filenames for the cases where a file has been renamed i.e.
// we accept the current name and the previous name
func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error {
command := "git rm --cached --force -- %s"
if reset {
command = "git reset HEAD -- %s"
}
for _, name := range fileNames {
err := c.Cmd.New(fmt.Sprintf(command, c.OSCommand.Quote(name))).Run()
if err != nil {
return err
}
}
return nil
}
func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) {
if !file.IsRename() {
return nil, nil, errors.New("Expected renamed file")
}
// we've got a file that represents a rename from one file to another. Here we will refetch
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file.
filesWithoutRenames := loaders.
NewFileLoader(c.Common, c.Cmd, c.GitConfig).
GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
if f.Name == file.PreviousName {
beforeFile = f
}
if f.Name == file.Name {
afterFile = f
}
}
if beforeFile == nil || afterFile == nil {
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
}
if beforeFile.IsRename() || afterFile.IsRename() {
// probably won't happen but we want to ensure we don't get an infinite loop
return nil, nil, errors.New("Nested rename found")
}
return beforeFile, afterFile, nil
}
// DiscardAllFileChanges directly
func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
if file.IsRename() {
beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file)
if err != nil {
return err
}
if err := c.DiscardAllFileChanges(beforeFile); err != nil {
return err
}
if err := c.DiscardAllFileChanges(afterFile); err != nil {
return err
}
return nil
}
quotedFileName := c.OSCommand.Quote(file.Name)
if file.ShortStatus == "AA" {
if err := c.Cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil {
return err
}
if err := c.Cmd.New("git add -- " + quotedFileName).Run(); err != nil {
return err
}
return nil
}
if file.ShortStatus == "DU" {
return c.Cmd.New("git rm -- " + quotedFileName).Run()
}
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges || file.HasMergeConflicts {
if err := c.Cmd.New("git reset -- " + quotedFileName).Run(); err != nil {
return err
}
}
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
return nil
}
if file.Added {
return c.OSCommand.RemoveFile(file.Name)
}
return c.DiscardUnstagedFileChanges(file)
}
func (c *GitCommand) DiscardAllDirChanges(node *filetree.FileNode) error {
// this could be more efficient but we would need to handle all the edge cases
return node.ForEachFile(c.DiscardAllFileChanges)
}
func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
if err := c.RemoveUntrackedDirFiles(node); err != nil {
return err
}
quotedPath := c.OSCommand.Quote(node.GetPath())
if err := c.Cmd.New("git checkout -- " + quotedPath).Run(); err != nil {
return err
}
return nil
}
func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
untrackedFilePaths := node.GetPathsMatching(
func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() },
)
for _, path := range untrackedFilePaths {
err := os.Remove(path)
if err != nil {
return err
}
}
return nil
}
// DiscardUnstagedFileChanges directly
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
return c.Cmd.New("git checkout -- " + quotedFileName).Run()
}
// Ignore adds a file to the gitignore for the repo
func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)
}
// WorktreeFileDiff returns the diff of a file
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
// for now we assume an error means the file was deleted
s, _ := c.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput()
return s
}
func (c *GitCommand) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj {
cachedArg := ""
trackedArg := "--"
colorArg := c.colorArg()
quotedPath := c.OSCommand.Quote(node.GetPath())
ignoreWhitespaceArg := ""
contextSize := c.UserConfig.Git.DiffContextSize
if cached {
cachedArg = "--cached"
}
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
trackedArg = "--no-index -- /dev/null"
}
if plain {
colorArg = "never"
}
if ignoreWhitespace {
ignoreWhitespaceArg = "--ignore-all-space"
}
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s %s %s %s %s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
return c.Cmd.New(cmdStr).DontLog()
}
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
filepath := filepath.Join(oscommands.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
c.Log.Infof("saving temporary patch to %s", filepath)
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
return err
}
flagStr := ""
for _, flag := range flags {
flagStr += " --" + flag
}
return c.Cmd.New(fmt.Sprintf("git apply%s %s", flagStr, c.OSCommand.Quote(filepath))).Run()
}
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
return c.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
}
func (c *GitCommand) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
colorArg := c.colorArg()
contextSize := c.UserConfig.Git.DiffContextSize
if plain {
colorArg = "never"
}
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
return c.Cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s %s %s %s -- %s", contextSize, colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName))).DontLog()
}
// CheckoutFile checks out the file for the given commit
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
return c.Cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))).Run()
}
// DiscardOldFileChanges discards changes to a file from an old commit
func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); 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.Cmd.New("git cat-file -e HEAD^:" + c.OSCommand.Quote(fileName)).DontLog().Run(); err != nil {
if err := c.OSCommand.Remove(fileName); err != nil {
return err
}
if err := c.StageFile(fileName); err != nil {
return err
}
} else if err := c.CheckoutFile("HEAD^", fileName); err != nil {
return err
}
// amend the commit
err := c.AmendHead()
if err != nil {
return err
}
// continue
return c.GenericMergeOrRebaseAction("rebase", "continue")
}
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
return c.Cmd.New("git checkout -- .").Run()
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (c *GitCommand) RemoveTrackedFiles(name string) error {
return c.Cmd.New("git rm -r --cached -- " + c.OSCommand.Quote(name)).Run()
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *GitCommand) RemoveUntrackedFiles() error {
return c.Cmd.New("git clean -fd").Run()
}
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *GitCommand) ResetAndClean() error {
submoduleConfigs, err := c.GetSubmoduleConfigs()
if err != nil {
return err
}
if len(submoduleConfigs) > 0 {
if err := c.ResetSubmodules(submoduleConfigs); err != nil {
return err
}
}
if err := c.ResetHard("HEAD"); err != nil {
return err
}
return c.RemoveUntrackedFiles()
}
func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, error) {
func (c *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string, error) {
editor := c.UserConfig.OS.EditCommand
if editor == "" {
editor = c.GitConfig.Get("core.editor")
editor = c.config.GetCoreEditor()
}
if editor == "" {
editor = c.OSCommand.Getenv("GIT_EDITOR")
editor = c.os.Getenv("GIT_EDITOR")
}
if editor == "" {
editor = c.OSCommand.Getenv("VISUAL")
editor = c.os.Getenv("VISUAL")
}
if editor == "" {
editor = c.OSCommand.Getenv("EDITOR")
editor = c.os.Getenv("EDITOR")
}
if editor == "" {
if err := c.OSCommand.Cmd.New("which vi").DontLog().Run(); err == nil {
if err := c.cmd.New("which vi").DontLog().Run(); err == nil {
editor = "vi"
}
}
@ -363,7 +72,7 @@ func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, er
templateValues := map[string]string{
"editor": editor,
"filename": c.OSCommand.Quote(filename),
"filename": c.cmd.Quote(filename),
"line": strconv.Itoa(lineNumber),
}

View File

@ -1,602 +1,14 @@
package commands
import (
"fmt"
"io/ioutil"
"regexp"
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
func TestGitCommandStageFile(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"add", "--", "test.txt"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.StageFile("test.txt"))
runner.CheckForMissingCalls()
}
func TestGitCommandUnstageFile(t *testing.T) {
type scenario struct {
testName string
reset bool
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "Remove an untracked file from staging",
reset: false,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"rm", "--cached", "--force", "--", "test.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "Remove a tracked file from staging",
reset: true,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"reset", "HEAD", "--", "test.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.UnStageFile([]string{"test.txt"}, s.reset))
})
}
}
// these tests don't cover everything, in part because we already have an integration
// test which does cover everything. I don't want to unnecessarily assert on the 'how'
// when the 'what' is what matters
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
type scenario struct {
testName string
file *models.File
removeFile func(string) error
runner *oscommands.FakeCmdObjRunner
expectedError string
}
scenarios := []scenario{
{
testName: "An error occurred when resetting",
file: &models.File{
Name: "test",
HasStagedChanges: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"reset", "--", "test"}, "", errors.New("error")),
expectedError: "error",
},
{
testName: "An error occurred when removing file",
file: &models.File{
Name: "test",
Tracked: false,
Added: true,
},
removeFile: func(string) error {
return fmt.Errorf("an error occurred when removing file")
},
runner: oscommands.NewFakeRunner(t),
expectedError: "an error occurred when removing file",
},
{
testName: "An error occurred with checkout",
file: &models.File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"checkout", "--", "test"}, "", errors.New("error")),
expectedError: "error",
},
{
testName: "Checkout only",
file: &models.File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Reset and checkout staged changes",
file: &models.File{
Name: "test",
Tracked: true,
HasStagedChanges: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil).
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Reset and checkout merge conflicts",
file: &models.File{
Name: "test",
Tracked: true,
HasMergeConflicts: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil).
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Reset and remove",
file: &models.File{
Name: "test",
Tracked: false,
Added: true,
HasStagedChanges: true,
},
removeFile: func(filename string) error {
assert.Equal(t, "test", filename)
return nil
},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Remove only",
file: &models.File{
Name: "test",
Tracked: false,
Added: true,
HasStagedChanges: false,
},
removeFile: func(filename string) error {
assert.Equal(t, "test", filename)
return nil
},
runner: oscommands.NewFakeRunner(t),
expectedError: "",
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
gitCmd.OSCommand.SetRemoveFile(s.removeFile)
err := gitCmd.DiscardAllFileChanges(s.file)
if s.expectedError == "" {
assert.Nil(t, err)
} else {
assert.Equal(t, s.expectedError, err.Error())
}
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandDiff(t *testing.T) {
type scenario struct {
testName string
file *models.File
plain bool
cached bool
ignoreWhitespace bool
contextSize int
runner *oscommands.FakeCmdObjRunner
}
const expectedResult = "pretend this is an actual git diff"
scenarios := []scenario{
{
testName: "Default case",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "cached",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: true,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--cached", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "plain",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: true,
cached: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=never", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "File not tracked and file has no staged changes",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: false,
},
plain: false,
cached: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, expectedResult, nil),
},
{
testName: "Default case (ignore whitespace)",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: false,
ignoreWhitespace: true,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "Show diff with custom context size",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: false,
ignoreWhitespace: false,
contextSize: 17,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=17", "--color=always", "--", "test.txt"}, expectedResult, nil),
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
result := gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached, s.ignoreWhitespace)
assert.Equal(t, expectedResult, result)
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandShowFileDiff(t *testing.T) {
type scenario struct {
testName string
from string
to string
reverse bool
plain bool
contextSize int
runner *oscommands.FakeCmdObjRunner
}
const expectedResult = "pretend this is an actual git diff"
scenarios := []scenario{
{
testName: "Default case",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "Show diff with custom context size",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
contextSize: 123,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=123", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
result, err := gitCmd.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain)
assert.NoError(t, err)
assert.Equal(t, expectedResult, result)
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandCheckoutFile(t *testing.T) {
type scenario struct {
testName string
commitSha string
fileName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "typical case",
commitSha: "11af912",
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout 11af912 -- "test999.txt"`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "returns error if there is one",
commitSha: "11af912",
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout 11af912 -- "test999.txt"`, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.CheckoutFile(s.commitSha, s.fileName))
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandApplyPatch(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
expectFn := func(regexStr string, errToReturn error) func(cmdObj oscommands.ICmdObj) (string, error) {
return func(cmdObj oscommands.ICmdObj) (string, error) {
re := regexp.MustCompile(regexStr)
matches := re.FindStringSubmatch(cmdObj.ToString())
assert.Equal(t, 2, len(matches))
filename := matches[1]
content, err := ioutil.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
return "", errToReturn
}
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn(`git apply --cached "(.*)"`, nil)),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "command returns error",
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn(`git apply --cached "(.*)"`, errors.New("error"))),
test: func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.ApplyPatch("test", "cached"))
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
type scenario struct {
testName string
gitConfigMockResponses map[string]string
commits []*models.Commit
commitIndex int
fileName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "returns error when index outside of range of commits",
gitConfigMockResponses: nil,
commits: []*models.Commit{},
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t),
test: func(err error) {
assert.Error(t, err)
},
},
{
testName: "returns error when using gpg",
gitConfigMockResponses: map[string]string{"commit.gpgsign": "true"},
commits: []*models.Commit{{Name: "commit", Sha: "123456"}},
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t),
test: func(err error) {
assert.Error(t, err)
},
},
{
testName: "checks out file if it already existed",
gitConfigMockResponses: nil,
commits: []*models.Commit{
{Name: "commit", Sha: "123456"},
{Name: "commit2", Sha: "abcdef"},
},
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty abcdef`, "", nil).
Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil).
Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil).
Expect(`git commit --amend --no-edit --allow-empty`, "", nil).
Expect(`git rebase --continue`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
// test for when the file was created within the commit requires a refactor to support proper mocks
// currently we'd need to mock out the os.Remove function and that's gonna introduce tech debt
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
type scenario struct {
testName string
file *models.File
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "valid case",
file: &models.File{Name: "test.txt"},
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout -- "test.txt"`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.DiscardUnstagedFileChanges(s.file))
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout -- .`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.DiscardAnyUnstagedFileChanges())
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
Expect(`git clean -fd`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.RemoveUntrackedFiles())
s.runner.CheckForMissingCalls()
})
}
}
func TestEditFileCmdStr(t *testing.T) {
type scenario struct {
filename string
@ -736,9 +148,9 @@ func TestEditFileCmdStr(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
gitCmd.UserConfig.OS.EditCommand = s.configEditCommand
gitCmd.UserConfig.OS.EditCommandTemplate = s.configEditCommandTemplate
gitCmd.OSCommand.Getenv = s.getenv
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
s.test(gitCmd.EditFileCmdStr(s.filename, 1))
gitCmd.OSCommand.GetenvFn = s.getenv
gitCmd.gitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
s.test(gitCmd.File.GetEditCmdStr(s.filename, 1))
s.runner.CheckForMissingCalls()
}
}

View File

@ -1,7 +1,6 @@
package commands
import (
"io"
"io/ioutil"
"os"
"path/filepath"
@ -19,11 +18,31 @@ import (
"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
Loaders Loaders
Cmd oscommands.ICmdObjBuilder
Submodule *SubmoduleCommands
Tag *TagCommands
WorkingTree *WorkingTreeCommands
File *FileCommands
Branch *BranchCommands
Commit *CommitCommands
Rebase *RebaseCommands
Stash *StashCommands
Status *StatusCommands
Config *ConfigCommands
Patch *PatchCommands
Remote *RemoteCommands
Sync *SyncCommands
}
type Loaders struct {
Commits *loaders.CommitLoader
@ -36,44 +55,17 @@ type Loaders struct {
Tags *loaders.TagLoader
}
// GitCommand is our main git interface
type GitCommand struct {
*common.Common
OSCommand *oscommands.OSCommand
Repo *gogit.Repository
DotGitDir string
onSuccessfulContinue func() error
PatchManager *patch.PatchManager
GitConfig git_config.IGitConfig
Loaders Loaders
// 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
Cmd oscommands.ICmdObjBuilder
}
// NewGitCommand it runs git commands
func NewGitCommand(
cmn *common.Common,
osCommand *oscommands.OSCommand,
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 {
repo, err := setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr)
if err != nil {
return nil, err
}
@ -82,33 +74,81 @@ func NewGitCommand(
return nil, err
}
return NewGitCommandAux(
cmn,
osCommand,
gitConfig,
dotGitDir,
repo,
), nil
}
func NewGitCommandAux(
cmn *common.Common,
osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig,
dotGitDir string,
repo *gogit.Repository,
) *GitCommand {
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
gitCommand := &GitCommand{
Common: cmn,
OSCommand: osCommand,
Repo: repo,
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
GitConfig: gitConfig,
GetCmdWriter: func() io.Writer { return ioutil.Discard },
Cmd: cmd,
configCommands := NewConfigCommands(cmn, gitConfig)
statusCommands := NewStatusCommands(cmn, osCommand, repo, dotGitDir)
fileLoader := loaders.NewFileLoader(cmn, cmd, configCommands)
remoteCommands := NewRemoteCommands(cmn, cmd)
branchCommands := NewBranchCommands(cmn, cmd)
syncCommands := NewSyncCommands(cmn, cmd)
tagCommands := NewTagCommands(cmn, cmd)
commitCommands := NewCommitCommands(cmn, cmd)
fileCommands := NewFileCommands(cmn, cmd, configCommands, osCommand)
submoduleCommands := NewSubmoduleCommands(cmn, cmd, dotGitDir)
workingTreeCommands := NewWorkingTreeCommands(cmn, cmd, submoduleCommands, osCommand, fileLoader)
rebaseCommands := NewRebaseCommands(
cmn,
cmd,
osCommand,
commitCommands,
workingTreeCommands,
configCommands,
dotGitDir,
)
stashCommands := NewStashCommands(cmn, cmd, osCommand, fileLoader, workingTreeCommands)
// TODO: have patch manager take workingTreeCommands in its entirety
patchManager := patch.NewPatchManager(cmn.Log, workingTreeCommands.ApplyPatch, workingTreeCommands.ShowFileDiff)
patchCommands := NewPatchCommands(cmn, cmd, rebaseCommands, commitCommands, configCommands, statusCommands, patchManager)
return &GitCommand{
Common: cmn,
OSCommand: osCommand,
Repo: repo,
Cmd: cmd,
Submodule: submoduleCommands,
Tag: tagCommands,
WorkingTree: workingTreeCommands,
File: fileCommands,
Branch: branchCommands,
Commit: commitCommands,
Rebase: rebaseCommands,
Config: configCommands,
Stash: stashCommands,
Status: statusCommands,
Patch: patchCommands,
Remote: remoteCommands,
Sync: syncCommands,
Loaders: Loaders{
Commits: loaders.NewCommitLoader(cmn, cmd, dotGitDir, branchCommands.CurrentBranchName, statusCommands.RebaseMode),
Branches: loaders.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchName),
Files: fileLoader,
CommitFiles: loaders.NewCommitFileLoader(cmn, cmd),
Remotes: loaders.NewRemoteLoader(cmn, cmd, repo.Remotes),
ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd),
Stash: loaders.NewStashLoader(cmn, cmd),
Tags: loaders.NewTagLoader(cmn, cmd),
},
}
gitCommand.Loaders = Loaders{
Commits: loaders.NewCommitLoader(cmn, gitCommand),
Branches: loaders.NewBranchLoader(cmn, gitCommand),
Files: loaders.NewFileLoader(cmn, cmd, gitConfig),
CommitFiles: loaders.NewCommitFileLoader(cmn, cmd),
Remotes: loaders.NewRemoteLoader(cmn, cmd, gitCommand.Repo.Remotes),
ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd),
Stash: loaders.NewStashLoader(cmn, cmd),
Tags: loaders.NewTagLoader(cmn, cmd),
}
gitCommand.PatchManager = patch.NewPatchManager(gitCommand.Log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
return gitCommand, nil
}
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
@ -224,11 +264,3 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
return osCommand.Cmd.New("git rev-parse --git-dir").DontLog().Run()
}
func (c *GitCommand) GetDotGitDir() string {
return c.DotGitDir
}
func (c *GitCommand) GetCmd() oscommands.ICmdObjBuilder {
return c.Cmd
}

View File

@ -27,19 +27,15 @@ type BranchLoader struct {
getCurrentBranchName func() (string, string, error)
}
type BranchLoaderGitCommand interface {
GetRawBranches() (string, error)
CurrentBranchName() (string, string, error)
}
func NewBranchLoader(
cmn *common.Common,
gitCommand BranchLoaderGitCommand,
getRawBranches func() (string, error),
getCurrentBranchName func() (string, string, error),
) *BranchLoader {
return &BranchLoader{
Common: cmn,
getRawBranches: gitCommand.GetRawBranches,
getCurrentBranchName: gitCommand.CurrentBranchName,
getRawBranches: getRawBranches,
getCurrentBranchName: getCurrentBranchName,
}
}

View File

@ -36,26 +36,22 @@ type CommitLoader struct {
dotGitDir string
}
type CommitLoaderGitCommand interface {
CurrentBranchName() (string, string, error)
RebaseMode() (enums.RebaseMode, error)
GetCmd() oscommands.ICmdObjBuilder
GetDotGitDir() string
}
// making our dependencies explicit for the sake of easier testing
func NewCommitLoader(
cmn *common.Common,
gitCommand CommitLoaderGitCommand,
cmd oscommands.ICmdObjBuilder,
dotGitDir string,
getCurrentBranchName func() (string, string, error),
getRebaseMode func() (enums.RebaseMode, error),
) *CommitLoader {
return &CommitLoader{
Common: cmn,
cmd: gitCommand.GetCmd(),
getCurrentBranchName: gitCommand.CurrentBranchName,
getRebaseMode: gitCommand.RebaseMode,
cmd: cmd,
getCurrentBranchName: getCurrentBranchName,
getRebaseMode: getRebaseMode,
readFile: ioutil.ReadFile,
walkFiles: filepath.Walk,
dotGitDir: gitCommand.GetDotGitDir(),
dotGitDir: dotGitDir,
}
}

View File

@ -11,19 +11,24 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
type FileLoaderConfig interface {
GetShowUntrackedFiles() string
}
type FileLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
config FileLoaderConfig
gitConfig git_config.IGitConfig
getFileType func(string) string
}
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, gitConfig git_config.IGitConfig) *FileLoader {
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader {
return &FileLoader{
Common: cmn,
cmd: cmd,
gitConfig: gitConfig,
getFileType: oscommands.FileType,
config: config,
}
}
@ -33,7 +38,7 @@ type GetStatusFileOptions struct {
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := self.gitConfig.Get("status.showUntrackedFiles")
untrackedFilesSetting := self.config.GetShowUntrackedFiles()
if untrackedFilesSetting == "" {
untrackedFilesSetting = "all"

View File

@ -34,6 +34,11 @@ type ICmdObj interface {
// This returns false if DontLog() was called
ShouldLog() bool
PromptOnCredentialRequest() ICmdObj
FailOnCredentialRequest() ICmdObj
GetCredentialStrategy() CredentialStrategy
}
type CmdObj struct {
@ -44,8 +49,28 @@ type CmdObj struct {
// if set to true, we don't want to log the command to the user.
dontLog bool
// if set to true, it means we might be asked to enter a username/password by this command.
credentialStrategy CredentialStrategy
}
type CredentialStrategy int
const (
// do not expect a credential request. If we end up getting one
// we'll be in trouble because the command will hang indefinitely
NONE CredentialStrategy = iota
// expect a credential request and if we get one, prompt the user to enter their username/password
PROMPT
// in this case we will check for a credential request (i.e. the command pauses to ask for
// username/password) and if we get one, we just submit a newline, forcing the
// command to fail. We use this e.g. for a background `git fetch` to prevent it
// from hanging indefinitely.
FAIL
)
var _ ICmdObj = &CmdObj{}
func (self *CmdObj) GetCmd() *exec.Cmd {
return self.cmd
}
@ -84,3 +109,19 @@ func (self *CmdObj) RunWithOutput() (string, error) {
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
return self.runner.RunAndProcessLines(self, onLine)
}
func (self *CmdObj) PromptOnCredentialRequest() ICmdObj {
self.credentialStrategy = PROMPT
return self
}
func (self *CmdObj) FailOnCredentialRequest() ICmdObj {
self.credentialStrategy = FAIL
return self
}
func (self *CmdObj) GetCredentialStrategy() CredentialStrategy {
return self.credentialStrategy
}

View File

@ -15,18 +15,46 @@ type ICmdObjRunner interface {
}
type cmdObjRunner struct {
log *logrus.Entry
logCmdObj func(ICmdObj)
log *logrus.Entry
guiIO *guiIO
}
var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
switch cmdObj.GetCredentialStrategy() {
case PROMPT:
return self.RunCommandWithOutputLive(cmdObj, self.guiIO.promptForCredentialFn)
case FAIL:
return self.RunCommandWithOutputLive(cmdObj, func(s string) string { return "\n" })
}
// we should never land here
return errors.New("runWithCredentialHandling called but cmdObj does not have a a credential strategy")
}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
if cmdObj.GetCredentialStrategy() == NONE {
_, err := self.RunWithOutput(cmdObj)
return err
} else {
return self.runWithCredentialHandling(cmdObj)
}
}
func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) {
self.guiIO.logCommandFn(cmdObj.ToString(), true)
}
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if cmdObj.GetCredentialStrategy() != NONE {
err := self.runWithCredentialHandling(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutput
// but ignore the output, hence why we've got this check here.
return "", err
}
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
@ -39,6 +67,10 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
}
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
if cmdObj.GetCredentialStrategy() != NONE {
return errors.New("cannot call RunAndProcessLines with credential strategy. If you're seeing this then a contributor to Lazygit has accidentally called this method! Please raise an issue")
}
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}

View File

@ -6,7 +6,7 @@ import (
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform)
osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
return osCmd
}
@ -27,7 +27,7 @@ var dummyPlatform = &Platform{
}
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform)
osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
osCommand.Cmd = NewDummyCmdObjBuilder(runner)
return osCommand

View File

@ -13,12 +13,12 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// DetectUnamePass detect a username / password / passphrase question in a command
// RunAndDetectCredentialRequest detect a username / password / passphrase question in a command
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUserForCredential func(string) string) error {
func (self *cmdObjRunner) RunAndDetectCredentialRequest(cmdObj ICmdObj, promptUserForCredential func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(cmdObj, writer, func(word string) string {
err := self.RunCommandWithOutputLive(cmdObj, func(word string) string {
ttyText = ttyText + " " + word
prompts := map[string]string{
@ -37,13 +37,7 @@ func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUser
return ""
})
return errMessage
}
// Due to a lack of pty support on windows we have RunCommandWithOutputLiveWrapper being defined
// separate for windows and other OS's
func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, writer io.Writer, handleOutput func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, cmdObj, writer, handleOutput)
return err
}
type cmdHandler struct {
@ -56,23 +50,22 @@ type cmdHandler struct {
// Output is a function that executes by every word that gets read by bufio
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't write anything to stdin
func RunCommandWithOutputLiveAux(
c *OSCommand,
func (self *cmdObjRunner) RunCommandWithOutputLiveAux(
cmdObj ICmdObj,
writer io.Writer,
// handleOutput takes a word from stdout and returns a string to be written to stdin.
// See DetectUnamePass above for how this is used to check for a username/password request
// See RunAndDetectCredentialRequest above for how this is used to check for a username/password request
handleOutput func(string) string,
startCmd func(cmd *exec.Cmd) (*cmdHandler, error),
) error {
c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand")
cmdWriter := self.guiIO.newCmdWriterFn()
self.log.WithField("command", cmdObj.ToString()).Info("RunCommand")
if cmdObj.ShouldLog() {
c.LogCommand(cmdObj.ToString(), true)
self.logCmdObj(cmdObj)
}
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
var stderr bytes.Buffer
cmd.Stderr = io.MultiWriter(writer, &stderr)
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
handler, err := startCmd(cmd)
if err != nil {
@ -81,11 +74,11 @@ func RunCommandWithOutputLiveAux(
defer func() {
if closeErr := handler.close(); closeErr != nil {
c.Log.Error(closeErr)
self.log.Error(closeErr)
}
}()
tr := io.TeeReader(handler.stdoutPipe, writer)
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
go utils.Safe(func() {
scanner := bufio.NewScanner(tr)

View File

@ -4,22 +4,19 @@
package oscommands
import (
"io"
"os/exec"
"github.com/creack/pty"
)
func RunCommandWithOutputLiveWrapper(
c *OSCommand,
// we define this separately for windows and non-windows given that windows does
// not have great PTY support and we need a PTY to handle a credential request
func (self *cmdObjRunner) RunCommandWithOutputLive(
cmdObj ICmdObj,
writer io.Writer,
output func(string) string,
) error {
return RunCommandWithOutputLiveAux(
c,
return self.RunCommandWithOutputLiveAux(
cmdObj,
writer,
output,
func(cmd *exec.Cmd) (*cmdHandler, error) {
ptmx, err := pty.Start(cmd)

View File

@ -26,18 +26,14 @@ func (b *Buffer) Write(p []byte) (n int, err error) {
return b.b.Write(p)
}
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
// RunCommandWithOutputLive runs a command live but because of windows compatibility this command can't be ran there
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
func RunCommandWithOutputLiveWrapper(
c *OSCommand,
func (self *cmdObjRunner) RunCommandWithOutputLive(
cmdObj ICmdObj,
writer io.Writer,
output func(string) string,
) error {
return RunCommandWithOutputLiveAux(
c,
return self.RunCommandWithOutputLiveAux(
cmdObj,
writer,
output,
func(cmd *exec.Cmd) (*cmdHandler, error) {
stdoutReader, stdoutWriter := io.Pipe()

View File

@ -0,0 +1,49 @@
package oscommands
import (
"io"
"io/ioutil"
"github.com/sirupsen/logrus"
)
// this struct captures some IO stuff
type guiIO struct {
// this is for logging anything we want. It'll be written to a log file for the sake
// of debugging.
log *logrus.Entry
// this is for us to log the command we're about to run e.g. 'git push'. The GUI
// will write this to a log panel so that the user can see which commands are being
// run.
// The isCommandLineCommand arg is there so that we can style the log differently
// depending on whether we're directly outputting a command we're about to run that
// will be run on the command line, or if we're using something from Go's standard lib.
logCommandFn func(str string, isCommandLineCommand bool)
// this is for us to directly write the output of a command. We will do this for
// certain commands like 'git push'. The GUI will write this to a command output panel.
// We need a new cmd writer per command, hence it being a function.
newCmdWriterFn func() io.Writer
// this allows us to request info from the user like username/password, in the event
// that a command requests it.
// the 'credential' arg is something like 'username' or 'password'
promptForCredentialFn func(credential string) string
}
func NewGuiIO(log *logrus.Entry, logCommandFn func(string, bool), newCmdWriterFn func() io.Writer, promptForCredentialFn func(string) string) *guiIO {
return &guiIO{
log: log,
logCommandFn: logCommandFn,
newCmdWriterFn: newCmdWriterFn,
promptForCredentialFn: promptForCredentialFn,
}
}
func NewNullGuiIO(log *logrus.Entry) *guiIO {
return &guiIO{
log: log,
logCommandFn: func(string, bool) {},
newCmdWriterFn: func() io.Writer { return ioutil.Discard },
promptForCredentialFn: func(string) string { return "" },
}
}

View File

@ -20,7 +20,7 @@ import (
type OSCommand struct {
*common.Common
Platform *Platform
Getenv func(string) string
GetenvFn func(string) string
// callback to run before running a command, i.e. for the purposes of logging.
// the string argument is the command string e.g. 'git add .' and the bool is
@ -43,24 +43,20 @@ type Platform struct {
}
// NewOSCommand os command runner
func NewOSCommand(common *common.Common, platform *Platform) *OSCommand {
func NewOSCommand(common *common.Common, platform *Platform, guiIO *guiIO) *OSCommand {
c := &OSCommand{
Common: common,
Platform: platform,
Getenv: os.Getenv,
GetenvFn: os.Getenv,
removeFile: os.RemoveAll,
}
runner := &cmdObjRunner{log: common.Log, logCmdObj: c.LogCmdObj}
runner := &cmdObjRunner{log: common.Log, guiIO: guiIO}
c.Cmd = &CmdObjBuilder{runner: runner, platform: platform}
return c
}
func (c *OSCommand) LogCmdObj(cmdObj ICmdObj) {
c.LogCommand(cmdObj.ToString(), true)
}
func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) {
c.Log.WithField("command", cmdStr).Info("RunCommand")
@ -270,6 +266,10 @@ func (c *OSCommand) RemoveFile(path string) error {
return c.removeFile(path)
}
func (c *OSCommand) Getenv(key string) string {
return c.GetenvFn(key)
}
func GetTempDir() string {
return filepath.Join(os.TempDir(), "lazygit")
}

View File

@ -5,64 +5,98 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
)
type PatchCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
rebase *RebaseCommands
commit *CommitCommands
config *ConfigCommands
stash *StashCommands
status *StatusCommands
PatchManager *patch.PatchManager
}
func NewPatchCommands(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
rebaseCommands *RebaseCommands,
commitCommands *CommitCommands,
configCommands *ConfigCommands,
statusCommands *StatusCommands,
patchManager *patch.PatchManager,
) *PatchCommands {
return &PatchCommands{
Common: common,
cmd: cmd,
rebase: rebaseCommands,
commit: commitCommands,
config: configCommands,
status: statusCommands,
PatchManager: patchManager,
}
}
// DeletePatchesFromCommit applies a patch in reverse for a commit
func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int, p *patch.PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int) error {
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// apply each patch in reverse
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
return err
}
// time to amend the selected commit
if err := c.AmendHead(); err != nil {
if err := self.commit.AmendHead(); err != nil {
return err
}
c.onSuccessfulContinue = func() error {
c.PatchManager.Reset()
self.rebase.onSuccessfulContinue = func() error {
self.PatchManager.Reset()
return nil
}
// continue
return c.GenericMergeOrRebaseAction("rebase", "continue")
return self.rebase.GenericMergeOrRebaseAction("rebase", "continue")
}
func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceCommitIdx int, destinationCommitIdx int, p *patch.PatchManager) error {
func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, sourceCommitIdx int, destinationCommitIdx int) error {
if sourceCommitIdx < destinationCommitIdx {
if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
return err
}
// apply each patch forward
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the destination commit
if err := c.AmendHead(); err != nil {
if err := self.commit.AmendHead(); err != nil {
return err
}
c.onSuccessfulContinue = func() error {
c.PatchManager.Reset()
self.rebase.onSuccessfulContinue = func() error {
self.PatchManager.Reset()
return nil
}
// continue
return c.GenericMergeOrRebaseAction("rebase", "continue")
return self.rebase.GenericMergeOrRebaseAction("rebase", "continue")
}
if len(commits)-1 < sourceCommitIdx {
@ -72,8 +106,8 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
// 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() {
return errors.New(c.Tr.DisabledForGPG)
if self.config.UsingGpg() {
return errors.New(self.Tr.DisabledForGPG)
}
baseIndex := sourceCommitIdx + 1
@ -86,7 +120,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
cmdObj, err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
if err != nil {
return err
}
@ -96,62 +130,62 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
}
// apply each patch in reverse
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the source commit
if err := c.AmendHead(); err != nil {
if err := self.commit.AmendHead(); err != nil {
return err
}
if c.onSuccessfulContinue != nil {
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.onSuccessfulContinue = func() error {
self.rebase.onSuccessfulContinue = func() error {
// now we should be up to the destination, so let's apply forward these patches to that.
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the destination commit
if err := c.AmendHead(); err != nil {
if err := self.commit.AmendHead(); err != nil {
return err
}
c.onSuccessfulContinue = func() error {
c.PatchManager.Reset()
self.rebase.onSuccessfulContinue = func() error {
self.PatchManager.Reset()
return nil
}
return c.GenericMergeOrRebaseAction("rebase", "continue")
return self.rebase.GenericMergeOrRebaseAction("rebase", "continue")
}
return c.GenericMergeOrRebaseAction("rebase", "continue")
return self.rebase.GenericMergeOrRebaseAction("rebase", "continue")
}
func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, stash bool) error {
if stash {
if err := c.StashSave(c.Tr.StashPrefix + commits[commitIdx].Sha); err != nil {
if err := self.stash.Save(self.Tr.StashPrefix + commits[commitIdx].Sha); err != nil {
return err
}
}
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := p.ApplyPatches(true); err != nil {
if c.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := self.PatchManager.ApplyPatches(true); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
}
@ -159,19 +193,19 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int,
}
// amend the commit
if err := c.AmendHead(); err != nil {
if err := self.commit.AmendHead(); err != nil {
return err
}
if c.onSuccessfulContinue != nil {
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.onSuccessfulContinue = func() error {
self.rebase.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if c.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := self.PatchManager.ApplyPatches(false); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
}
@ -179,54 +213,54 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int,
}
if stash {
if err := c.StashDo(0, "apply"); err != nil {
if err := self.stash.Apply(0); err != nil {
return err
}
}
c.PatchManager.Reset()
self.PatchManager.Reset()
return nil
}
return c.GenericMergeOrRebaseAction("rebase", "continue")
return self.rebase.GenericMergeOrRebaseAction("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx int, p *patch.PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx int) error {
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the commit
if err := c.AmendHead(); err != nil {
if err := self.commit.AmendHead(); err != nil {
return err
}
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
return err
}
head_message, _ := c.GetHeadCommitMessage()
head_message, _ := self.commit.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
err := c.CommitCmdObj(new_message, "").Run()
err := self.commit.CommitCmdObj(new_message, "").Run()
if err != nil {
return err
}
if c.onSuccessfulContinue != nil {
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.PatchManager.Reset()
return c.GenericMergeOrRebaseAction("rebase", "continue")
self.PatchManager.Reset()
return self.rebase.GenericMergeOrRebaseAction("rebase", "continue")
}

View File

@ -9,22 +9,56 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
type RebaseCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
osCommand *oscommands.OSCommand
commit *CommitCommands
workingTree *WorkingTreeCommands
config *ConfigCommands
dotGitDir string
onSuccessfulContinue func() error
}
func NewRebaseCommands(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
osCommand *oscommands.OSCommand,
commitCommands *CommitCommands,
workingTreeCommands *WorkingTreeCommands,
configCommands *ConfigCommands,
dotGitDir string,
) *RebaseCommands {
return &RebaseCommands{
Common: common,
cmd: cmd,
osCommand: osCommand,
commit: commitCommands,
workingTree: workingTreeCommands,
config: configCommands,
dotGitDir: dotGitDir,
}
}
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
todo, sha, err := self.GenerateGenericRebaseTodo(commits, index, "reword")
if err != nil {
return nil, err
}
return c.PrepareInteractiveRebaseCommand(sha, todo, false)
return self.PrepareInteractiveRebaseCommand(sha, todo, false)
}
func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error {
func (self *RebaseCommands) MoveCommitDown(commits []*models.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.NoRoom)
return errors.New(self.Tr.NoRoom)
}
todo := ""
@ -33,7 +67,7 @@ func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
cmdObj, err := self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
if err != nil {
return err
}
@ -41,13 +75,13 @@ func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error {
return cmdObj.Run()
}
func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error {
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action)
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
todo, sha, err := self.GenerateGenericRebaseTodo(commits, index, action)
if err != nil {
return err
}
cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
cmdObj, err := self.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
@ -58,24 +92,24 @@ func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, acti
// 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) (oscommands.ICmdObj, error) {
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (oscommands.ICmdObj, error) {
ex := oscommands.GetLazygitPath()
debug := "FALSE"
if c.Debug {
if self.Debug {
debug = "TRUE"
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
c.Log.WithField("command", cmdStr).Info("RunCommand")
self.Log.WithField("command", cmdStr).Info("RunCommand")
cmdObj := c.Cmd.New(cmdStr)
cmdObj := self.cmd.New(cmdStr)
gitSequenceEditor := ex
if todo == "" {
gitSequenceEditor = "true"
} else {
c.OSCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
self.osCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
}
cmdObj.AddEnvVars(
@ -94,18 +128,18 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
return cmdObj, nil
}
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
func (self *RebaseCommands) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
baseIndex := actionIndex + 1
if len(commits) <= baseIndex {
return "", "", errors.New(c.Tr.CannotRebaseOntoFirstCommit)
return "", "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
}
if action == "squash" || action == "fixup" {
baseIndex++
if len(commits) <= baseIndex {
return "", "", errors.New(c.Tr.CannotSquashOntoSecondCommit)
return "", "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
}
}
@ -129,24 +163,24 @@ func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionI
}
// AmendTo amends the given commit with whatever files are staged
func (c *GitCommand) AmendTo(sha string) error {
if err := c.CreateFixupCommit(sha); err != nil {
func (self *RebaseCommands) AmendTo(sha string) error {
if err := self.commit.CreateFixupCommit(sha); err != nil {
return err
}
return c.SquashAllAboveFixupCommits(sha)
return self.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 {
fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo")
func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content)
commitCount := self.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
@ -158,7 +192,7 @@ func (c *GitCommand) EditRebaseTodo(index int, action string) error {
return ioutil.WriteFile(fileName, []byte(result), 0644)
}
func (c *GitCommand) getTodoCommitCount(content []string) int {
func (self *RebaseCommands) getTodoCommitCount(content []string) int {
// count lines that are not blank and are not comments
commitCount := 0
for _, line := range content {
@ -170,15 +204,15 @@ func (c *GitCommand) getTodoCommitCount(content []string) int {
}
// MoveTodoDown moves a rebase todo item down by one position
func (c *GitCommand) MoveTodoDown(index int) error {
fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo")
func (self *RebaseCommands) MoveTodoDown(index int) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content)
commitCount := self.getTodoCommitCount(content)
contentIndex := commitCount - 1 - index
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
@ -189,8 +223,8 @@ func (c *GitCommand) MoveTodoDown(index int) error {
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error {
return c.runSkipEditorCommand(
func (self *RebaseCommands) SquashAllAboveFixupCommits(sha string) error {
return self.runSkipEditorCommand(
fmt.Sprintf(
"git rebase --interactive --autostash --autosquash %s^",
sha,
@ -199,8 +233,8 @@ func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error {
}
// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current
// commit and pick all others. After this you'll want to call `c.GenericMergeOrRebaseAction("rebase", "continue")`
func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error {
// commit and pick all others. After this you'll want to call `self.GenericMergeOrRebaseAction("rebase", "continue")`
func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error {
if len(commits)-1 < commitIndex {
return errors.New("index outside of range of commits")
}
@ -208,16 +242,16 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c
// 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() {
return errors.New(c.Tr.DisabledForGPG)
if self.config.UsingGpg() {
return errors.New(self.Tr.DisabledForGPG)
}
todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
todo, sha, err := self.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
if err != nil {
return err
}
cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
cmdObj, err := self.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
@ -226,8 +260,8 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c
}
// RebaseBranch interactive rebases onto a branch
func (c *GitCommand) RebaseBranch(branchName string) error {
cmdObj, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
func (self *RebaseCommands) RebaseBranch(branchName string) error {
cmdObj, err := self.PrepareInteractiveRebaseCommand(branchName, "", false)
if err != nil {
return err
}
@ -237,8 +271,8 @@ func (c *GitCommand) RebaseBranch(branchName string) error {
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
// By default we skip the editor in the case where a commit will be made
func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command string) error {
err := c.runSkipEditorCommand(
func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, command string) error {
err := self.runSkipEditorCommand(
fmt.Sprintf(
"git %s --%s",
commandType,
@ -249,25 +283,25 @@ func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command stri
if !strings.Contains(err.Error(), "no rebase in progress") {
return err
}
c.Log.Warn(err)
self.Log.Warn(err)
}
// sometimes we need to do a sequence of things in a rebase but the user needs to
// fix merge conflicts along the way. When this happens we queue up the next step
// so that after the next successful rebase continue we can continue from where we left off
if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil {
f := c.onSuccessfulContinue
c.onSuccessfulContinue = nil
if commandType == "rebase" && command == "continue" && self.onSuccessfulContinue != nil {
f := self.onSuccessfulContinue
self.onSuccessfulContinue = nil
return f()
}
if command == "abort" {
c.onSuccessfulContinue = nil
self.onSuccessfulContinue = nil
}
return nil
}
func (c *GitCommand) runSkipEditorCommand(command string) error {
cmdObj := c.Cmd.New(command)
func (self *RebaseCommands) runSkipEditorCommand(command string) error {
cmdObj := self.cmd.New(command)
lazyGitPath := oscommands.GetLazygitPath()
return cmdObj.
AddEnvVars(
@ -278,3 +312,46 @@ func (c *GitCommand) runSkipEditorCommand(command string) error {
).
Run()
}
// DiscardOldFileChanges discards changes to a file from an old commit
func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error {
if err := self.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := self.cmd.New("git cat-file -e HEAD^:" + self.cmd.Quote(fileName)).Run(); err != nil {
if err := self.osCommand.Remove(fileName); err != nil {
return err
}
if err := self.workingTree.StageFile(fileName); err != nil {
return err
}
} else if err := self.workingTree.CheckoutFile("HEAD^", fileName); err != nil {
return err
}
// amend the commit
err := self.commit.AmendHead()
if err != nil {
return err
}
// continue
return self.GenericMergeOrRebaseAction("rebase", "continue")
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
todo := ""
for _, commit := range commits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmdObj, err := self.PrepareInteractiveRebaseCommand("HEAD", todo, false)
if err != nil {
return err
}
return cmdObj.Run()
}

View File

@ -5,6 +5,8 @@ import (
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
@ -42,7 +44,7 @@ func TestGitCommandRebaseBranch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.RebaseBranch(s.arg))
s.test(gitCmd.Rebase.RebaseBranch(s.arg))
})
}
}
@ -70,7 +72,74 @@ func TestGitCommandSkipEditorCommand(t *testing.T) {
return "", nil
})
gitCmd := NewDummyGitCommandWithRunner(runner)
err := gitCmd.runSkipEditorCommand(commandStr)
err := gitCmd.Rebase.runSkipEditorCommand(commandStr)
assert.NoError(t, err)
runner.CheckForMissingCalls()
}
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
type scenario struct {
testName string
gitConfigMockResponses map[string]string
commits []*models.Commit
commitIndex int
fileName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "returns error when index outside of range of commits",
gitConfigMockResponses: nil,
commits: []*models.Commit{},
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t),
test: func(err error) {
assert.Error(t, err)
},
},
{
testName: "returns error when using gpg",
gitConfigMockResponses: map[string]string{"commit.gpgsign": "true"},
commits: []*models.Commit{{Name: "commit", Sha: "123456"}},
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t),
test: func(err error) {
assert.Error(t, err)
},
},
{
testName: "checks out file if it already existed",
gitConfigMockResponses: nil,
commits: []*models.Commit{
{Name: "commit", Sha: "123456"},
{Name: "commit2", Sha: "abcdef"},
},
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty abcdef`, "", nil).
Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil).
Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil).
Expect(`git commit --amend --no-edit --allow-empty`, "", nil).
Expect(`git rebase --continue`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
// test for when the file was created within the commit requires a refactor to support proper mocks
// currently we'd need to mock out the os.Remove function and that's gonna introduce tech debt
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
gitCmd.gitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
s.test(gitCmd.Rebase.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
s.runner.CheckForMissingCalls()
})
}
}

View File

@ -4,49 +4,60 @@ import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
func (c *GitCommand) AddRemote(name string, url string) error {
return c.Cmd.
New(fmt.Sprintf("git remote add %s %s", c.Cmd.Quote(name), c.Cmd.Quote(url))).
type RemoteCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewRemoteCommands(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *RemoteCommands {
return &RemoteCommands{
Common: common,
cmd: cmd,
}
}
func (self *RemoteCommands) AddRemote(name string, url string) error {
return self.cmd.
New(fmt.Sprintf("git remote add %s %s", self.cmd.Quote(name), self.cmd.Quote(url))).
Run()
}
func (c *GitCommand) RemoveRemote(name string) error {
return c.Cmd.
New(fmt.Sprintf("git remote remove %s", c.Cmd.Quote(name))).
func (self *RemoteCommands) RemoveRemote(name string) error {
return self.cmd.
New(fmt.Sprintf("git remote remove %s", self.cmd.Quote(name))).
Run()
}
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
return c.Cmd.
New(fmt.Sprintf("git remote rename %s %s", c.Cmd.Quote(oldRemoteName), c.Cmd.Quote(newRemoteName))).
func (self *RemoteCommands) RenameRemote(oldRemoteName string, newRemoteName string) error {
return self.cmd.
New(fmt.Sprintf("git remote rename %s %s", self.cmd.Quote(oldRemoteName), self.cmd.Quote(newRemoteName))).
Run()
}
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return c.Cmd.
New(fmt.Sprintf("git remote set-url %s %s", c.Cmd.Quote(remoteName), c.Cmd.Quote(updatedUrl))).
func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return self.cmd.
New(fmt.Sprintf("git remote set-url %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(updatedUrl))).
Run()
}
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s --delete %s", c.Cmd.Quote(remoteName), c.Cmd.Quote(branchName))
cmdObj := c.Cmd.
New(command)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
func (c *GitCommand) DetectUnamePass(cmdObj oscommands.ICmdObj, promptUserForCredential func(string) string) error {
return c.OSCommand.DetectUnamePass(cmdObj, c.GetCmdWriter(), promptUserForCredential)
func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error {
command := fmt.Sprintf("git push %s --delete %s", self.cmd.Quote(remoteName), self.cmd.Quote(branchName))
return self.cmd.New(command).PromptOnCredentialRequest().Run()
}
// CheckRemoteBranchExists Returns remote branch
func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
_, err := c.Cmd.
func (self *RemoteCommands) CheckRemoteBranchExists(branchName string) bool {
_, err := self.cmd.
New(
fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s",
c.Cmd.Quote(branchName),
self.cmd.Quote(branchName),
),
).
DontLog().
@ -54,8 +65,3 @@ func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
return err == nil
}
// GetRemoteURL returns current repo remote url
func (c *GitCommand) GetRemoteURL() string {
return c.GitConfig.Get("remote.origin.url")
}

View File

@ -5,59 +5,91 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
// StashDo modify stash
func (c *GitCommand) StashDo(index int, method string) error {
return c.Cmd.New(fmt.Sprintf("git stash %s stash@{%d}", method, index)).Run()
type StashCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
fileLoader *loaders.FileLoader
osCommand *oscommands.OSCommand
workingTree *WorkingTreeCommands
}
// StashSave save stash
func NewStashCommands(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
osCommand *oscommands.OSCommand,
fileLoader *loaders.FileLoader,
workingTree *WorkingTreeCommands,
) *StashCommands {
return &StashCommands{
Common: common,
cmd: cmd,
fileLoader: fileLoader,
osCommand: osCommand,
workingTree: workingTree,
}
}
func (self *StashCommands) Drop(index int) error {
return self.cmd.New(fmt.Sprintf("git stash drop stash@{%d}", index)).Run()
}
func (self *StashCommands) Pop(index int) error {
return self.cmd.New(fmt.Sprintf("git stash pop stash@{%d}", index)).Run()
}
func (self *StashCommands) Apply(index int) error {
return self.cmd.New(fmt.Sprintf("git stash apply stash@{%d}", index)).Run()
}
// Save save stash
// TODO: before calling this, check if there is anything to save
func (c *GitCommand) StashSave(message string) error {
return c.Cmd.New("git stash save " + c.OSCommand.Quote(message)).Run()
func (self *StashCommands) Save(message string) error {
return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run()
}
func (c *GitCommand) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
cmdStr := fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", c.colorArg(), c.UserConfig.Git.DiffContextSize, index)
func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
cmdStr := fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", self.UserConfig.Git.Paging.ColorArg, self.UserConfig.Git.DiffContextSize, index)
return c.Cmd.New(cmdStr).DontLog()
return self.cmd.New(cmdStr).DontLog()
}
// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
// SaveStagedChanges 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 {
func (self *StashCommands) SaveStagedChanges(message string) error {
// wrap in 'writing', which uses a mutex
if err := c.Cmd.New("git stash --keep-index").Run(); err != nil {
if err := self.cmd.New("git stash --keep-index").Run(); err != nil {
return err
}
if err := c.StashSave(message); err != nil {
if err := self.Save(message); err != nil {
return err
}
if err := c.Cmd.New("git stash apply stash@{1}").Run(); err != nil {
if err := self.cmd.New("git stash apply stash@{1}").Run(); err != nil {
return err
}
if err := c.OSCommand.PipeCommands("git stash show -p", "git apply -R"); err != nil {
if err := self.osCommand.PipeCommands("git stash show -p", "git apply -R"); err != nil {
return err
}
if err := c.Cmd.New("git stash drop stash@{1}").Run(); err != nil {
if err := self.cmd.New("git stash drop stash@{1}").Run(); 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 := loaders.
NewFileLoader(c.Common, c.Cmd, c.GitConfig).
files := self.fileLoader.
GetStatusFiles(loaders.GetStatusFileOptions{})
for _, file := range files {
if file.ShortStatus == "AD" {
if err := c.UnStageFile(file.Names(), false); err != nil {
if err := self.workingTree.UnStageFile(file.Names(), false); err != nil {
return err
}
}

View File

@ -7,12 +7,30 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGitCommandStashDo(t *testing.T) {
func TestGitCommandStashDrop(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.StashDo(1, "drop"))
assert.NoError(t, gitCmd.Stash.Drop(1))
runner.CheckForMissingCalls()
}
func TestGitCommandStashApply(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"stash", "apply", "stash@{1}"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.Stash.Apply(1))
runner.CheckForMissingCalls()
}
func TestGitCommandStashPop(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"stash", "pop", "stash@{1}"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.Stash.Pop(1))
runner.CheckForMissingCalls()
}
@ -21,7 +39,7 @@ func TestGitCommandStashSave(t *testing.T) {
ExpectGitArgs([]string{"stash", "save", "A stash message"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.StashSave("A stash message"))
assert.NoError(t, gitCmd.Stash.Save("A stash message"))
runner.CheckForMissingCalls()
}
@ -52,7 +70,7 @@ func TestGitCommandShowStashEntryCmdObj(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
cmdStr := gitCmd.ShowStashEntryCmdObj(s.index).ToString()
cmdStr := gitCmd.Stash.ShowStashEntryCmdObj(s.index).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}

View File

@ -4,20 +4,43 @@ import (
"path/filepath"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
)
type StatusCommands struct {
*common.Common
osCommand *oscommands.OSCommand
repo *gogit.Repository
dotGitDir string
}
func NewStatusCommands(
common *common.Common,
osCommand *oscommands.OSCommand,
repo *gogit.Repository,
dotGitDir string,
) *StatusCommands {
return &StatusCommands{
Common: common,
osCommand: osCommand,
repo: repo,
dotGitDir: dotGitDir,
}
}
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (c *GitCommand) RebaseMode() (enums.RebaseMode, error) {
exists, err := c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-apply"))
func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) {
exists, err := self.osCommand.FileExists(filepath.Join(self.dotGitDir, "rebase-apply"))
if err != nil {
return enums.REBASE_MODE_NONE, err
}
if exists {
return enums.REBASE_MODE_NORMAL, nil
}
exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge"))
exists, err = self.osCommand.FileExists(filepath.Join(self.dotGitDir, "rebase-merge"))
if exists {
return enums.REBASE_MODE_INTERACTIVE, err
} else {
@ -25,12 +48,12 @@ func (c *GitCommand) RebaseMode() (enums.RebaseMode, error) {
}
}
func (c *GitCommand) WorkingTreeState() enums.RebaseMode {
rebaseMode, _ := c.RebaseMode()
func (self *StatusCommands) WorkingTreeState() enums.RebaseMode {
rebaseMode, _ := self.RebaseMode()
if rebaseMode != enums.REBASE_MODE_NONE {
return enums.REBASE_MODE_REBASING
}
merging, _ := c.IsInMergeState()
merging, _ := self.IsInMergeState()
if merging {
return enums.REBASE_MODE_MERGING
}
@ -38,12 +61,12 @@ func (c *GitCommand) WorkingTreeState() enums.RebaseMode {
}
// IsInMergeState states whether we are still mid-merge
func (c *GitCommand) IsInMergeState() (bool, error) {
return c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "MERGE_HEAD"))
func (self *StatusCommands) IsInMergeState() (bool, error) {
return self.osCommand.FileExists(filepath.Join(self.dotGitDir, "MERGE_HEAD"))
}
func (c *GitCommand) IsBareRepo() bool {
func (self *StatusCommands) IsBareRepo() bool {
// note: could use `git rev-parse --is-bare-repository` if we wanna drop go-git
_, err := c.Repo.Worktree()
_, err := self.repo.Worktree()
return err == gogit.ErrIsBareRepository
}

View File

@ -10,6 +10,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
// .gitmodules looks like this:
@ -17,7 +18,22 @@ import (
// path = blah/mysubmodule
// url = git@github.com:subbo.git
func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
type SubmoduleCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
dotGitDir string
}
func NewSubmoduleCommands(common *common.Common, cmd oscommands.ICmdObjBuilder, dotGitDir string) *SubmoduleCommands {
return &SubmoduleCommands{
Common: common,
cmd: cmd,
dotGitDir: dotGitDir,
}
}
func (self *SubmoduleCommands) GetConfigs() ([]*models.SubmoduleConfig, error) {
file, err := os.Open(".gitmodules")
if err != nil {
if os.IsNotExist(err) {
@ -63,36 +79,36 @@ func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
return configs, nil
}
func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
// because the intention here is to have no dirty worktree state
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) {
c.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
self.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
return nil
}
return c.Cmd.New("git -C " + c.Cmd.Quote(submodule.Path) + " stash --include-untracked").Run()
return self.cmd.New("git -C " + self.cmd.Quote(submodule.Path) + " stash --include-untracked").Run()
}
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
return c.Cmd.New("git submodule update --init --force -- " + c.Cmd.Quote(submodule.Path)).Run()
func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
return self.cmd.New("git submodule update --init --force -- " + self.cmd.Quote(submodule.Path)).Run()
}
func (c *GitCommand) SubmoduleUpdateAll() error {
func (self *SubmoduleCommands) UpdateAll() error {
// not doing an --init here because the user probably doesn't want that
return c.Cmd.New("git submodule update --force").Run()
return self.cmd.New("git submodule update --force").Run()
}
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
if err := c.Cmd.New("git submodule deinit --force -- " + c.Cmd.Quote(submodule.Path)).Run(); err != nil {
if err := self.cmd.New("git submodule deinit --force -- " + self.cmd.Quote(submodule.Path)).Run(); err != nil {
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
if err := c.Cmd.New("git config --file .gitmodules --remove-section submodule." + c.Cmd.Quote(submodule.Name)).Run(); err != nil {
if err := self.cmd.New("git config --file .gitmodules --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
return err
}
if err := c.Cmd.New("git config --remove-section submodule." + c.Cmd.Quote(submodule.Name)).Run(); err != nil {
if err := self.cmd.New("git config --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
return err
}
@ -102,69 +118,69 @@ func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
}
}
if err := c.Cmd.New("git rm --force -r " + submodule.Path).Run(); err != nil {
if err := self.cmd.New("git rm --force -r " + submodule.Path).Run(); err != nil {
// if the directory isn't there then that's fine
c.Log.Error(err)
self.Log.Error(err)
}
return os.RemoveAll(filepath.Join(c.DotGitDir, "modules", submodule.Path))
return os.RemoveAll(filepath.Join(self.dotGitDir, "modules", submodule.Path))
}
func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
return c.Cmd.
func (self *SubmoduleCommands) Add(name string, path string, url string) error {
return self.cmd.
New(
fmt.Sprintf(
"git submodule add --force --name %s -- %s %s ",
c.Cmd.Quote(name),
c.Cmd.Quote(url),
c.Cmd.Quote(path),
self.cmd.Quote(name),
self.cmd.Quote(url),
self.cmd.Quote(path),
)).
Run()
}
func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error {
func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string) error {
// the set-url command is only for later git versions so we're doing it manually here
if err := c.Cmd.New("git config --file .gitmodules submodule." + c.Cmd.Quote(name) + ".url " + c.Cmd.Quote(newUrl)).Run(); err != nil {
if err := self.cmd.New("git config --file .gitmodules submodule." + self.cmd.Quote(name) + ".url " + self.cmd.Quote(newUrl)).Run(); err != nil {
return err
}
if err := c.Cmd.New("git submodule sync -- " + c.Cmd.Quote(path)).Run(); err != nil {
if err := self.cmd.New("git submodule sync -- " + self.cmd.Quote(path)).Run(); err != nil {
return err
}
return nil
}
func (c *GitCommand) SubmoduleInit(path string) error {
return c.Cmd.New("git submodule init -- " + c.Cmd.Quote(path)).Run()
func (self *SubmoduleCommands) Init(path string) error {
return self.cmd.New("git submodule init -- " + self.cmd.Quote(path)).Run()
}
func (c *GitCommand) SubmoduleUpdate(path string) error {
return c.Cmd.New("git submodule update --init -- " + c.Cmd.Quote(path)).Run()
func (self *SubmoduleCommands) Update(path string) error {
return self.cmd.New("git submodule update --init -- " + self.cmd.Quote(path)).Run()
}
func (c *GitCommand) SubmoduleBulkInitCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git submodule init")
func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule init")
}
func (c *GitCommand) SubmoduleBulkUpdateCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git submodule update")
func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule update")
}
func (c *GitCommand) SubmoduleForceBulkUpdateCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git submodule update --force")
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule update --force")
}
func (c *GitCommand) SubmoduleBulkDeinitCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git submodule deinit --all --force")
func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule deinit --all --force")
}
func (c *GitCommand) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
func (self *SubmoduleCommands) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
for _, submodule := range submodules {
if err := c.SubmoduleStash(submodule); err != nil {
if err := self.Stash(submodule); err != nil {
return err
}
}
return c.SubmoduleUpdateAll()
return self.UpdateAll()
}

View File

@ -5,8 +5,25 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type SyncCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewSyncCommands(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *SyncCommands {
return &SyncCommands{
Common: common,
cmd: cmd,
}
}
// Push pushes to a branch
type PushOpts struct {
Force bool
@ -15,7 +32,7 @@ type PushOpts struct {
SetUpstream bool
}
func (c *GitCommand) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
cmdStr := "git push"
if opts.Force {
@ -27,71 +44,62 @@ func (c *GitCommand) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
}
if opts.UpstreamRemote != "" {
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamRemote)
cmdStr += " " + self.cmd.Quote(opts.UpstreamRemote)
}
if opts.UpstreamBranch != "" {
if opts.UpstreamRemote == "" {
return nil, errors.New(c.Tr.MustSpecifyOriginError)
return nil, errors.New(self.Tr.MustSpecifyOriginError)
}
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch)
cmdStr += " " + self.cmd.Quote(opts.UpstreamBranch)
}
cmdObj := c.Cmd.New(cmdStr)
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest()
return cmdObj, nil
}
func (c *GitCommand) Push(opts PushOpts, promptUserForCredential func(string) string) error {
cmdObj, err := c.PushCmdObj(opts)
func (self *SyncCommands) Push(opts PushOpts) error {
cmdObj, err := self.PushCmdObj(opts)
if err != nil {
return err
}
return c.DetectUnamePass(cmdObj, promptUserForCredential)
return cmdObj.Run()
}
type FetchOptions struct {
PromptUserForCredential func(string) string
RemoteName string
BranchName string
Background bool
RemoteName string
BranchName string
}
// Fetch fetch git repo
func (c *GitCommand) Fetch(opts FetchOptions) error {
func (self *SyncCommands) Fetch(opts FetchOptions) error {
cmdStr := "git fetch"
if opts.RemoteName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName))
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.RemoteName))
}
if opts.BranchName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.BranchName))
}
cmdObj := c.Cmd.New(cmdStr)
userInitiated := opts.PromptUserForCredential != nil
if !userInitiated {
cmdObj.DontLog()
cmdObj := self.cmd.New(cmdStr)
if opts.Background {
cmdObj.DontLog().FailOnCredentialRequest()
} else {
cmdObj.PromptOnCredentialRequest()
}
return c.DetectUnamePass(cmdObj, func(question string) string {
if userInitiated {
return opts.PromptUserForCredential(question)
}
return "\n"
})
return cmdObj.Run()
}
type PullOptions struct {
PromptUserForCredential func(string) string
RemoteName string
BranchName string
FastForwardOnly bool
RemoteName string
BranchName string
FastForwardOnly bool
}
func (c *GitCommand) Pull(opts PullOptions) error {
if opts.PromptUserForCredential == nil {
return errors.New("PromptUserForCredential is required")
}
func (self *SyncCommands) Pull(opts PullOptions) error {
cmdStr := "git pull --no-edit"
if opts.FastForwardOnly {
@ -99,26 +107,23 @@ func (c *GitCommand) Pull(opts PullOptions) error {
}
if opts.RemoteName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName))
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.RemoteName))
}
if opts.BranchName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.BranchName))
}
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured.
cmdObj := c.Cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
return self.cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().Run()
}
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))
return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
}
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName))
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
func (self *SyncCommands) FetchRemote(remoteName string) error {
cmdStr := fmt.Sprintf("git fetch %s", self.cmd.Quote(remoteName))
return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
}

View File

@ -87,7 +87,7 @@ func TestGitCommandPush(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(oscommands.NewFakeRunner(t))
s.test(gitCmd.PushCmdObj(s.opts))
s.test(gitCmd.Sync.PushCmdObj(s.opts))
})
}
}

View File

@ -2,22 +2,36 @@ package commands
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
return c.Cmd.New(fmt.Sprintf("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)).Run()
type TagCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func (c *GitCommand) CreateAnnotatedTag(tagName, commitSha, msg string) error {
return c.Cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, c.OSCommand.Quote(msg))).Run()
func NewTagCommands(common *common.Common, cmd oscommands.ICmdObjBuilder) *TagCommands {
return &TagCommands{
Common: common,
cmd: cmd,
}
}
func (c *GitCommand) DeleteTag(tagName string) error {
return c.Cmd.New(fmt.Sprintf("git tag -d %s", c.OSCommand.Quote(tagName))).Run()
func (self *TagCommands) CreateLightweight(tagName string, commitSha string) error {
return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), commitSha)).Run()
}
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName))
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
func (self *TagCommands) CreateAnnotated(tagName, commitSha, msg string) error {
return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, self.cmd.Quote(msg))).Run()
}
func (self *TagCommands) Delete(tagName string) error {
return self.cmd.New(fmt.Sprintf("git tag -d %s", self.cmd.Quote(tagName))).Run()
}
func (self *TagCommands) Push(remoteName string, tagName string) error {
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().Run()
}

View File

@ -0,0 +1,347 @@
package commands
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type WorkingTreeCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
os WorkingTreeOSCommand
submodule *SubmoduleCommands
fileLoader *loaders.FileLoader
}
type WorkingTreeOSCommand interface {
RemoveFile(string) error
CreateFileWithContent(string, string) error
AppendLineToFile(string, string) error
}
func NewWorkingTreeCommands(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
submodulesCommands *SubmoduleCommands,
osCommand WorkingTreeOSCommand,
fileLoader *loaders.FileLoader,
) *WorkingTreeCommands {
return &WorkingTreeCommands{
Common: common,
cmd: cmd,
os: osCommand,
submodule: submodulesCommands,
fileLoader: fileLoader,
}
}
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj {
return self.cmd.New("git mergetool")
}
func (self *WorkingTreeCommands) OpenMergeTool() error {
return self.OpenMergeToolCmdObj().Run()
}
// StageFile stages a file
func (self *WorkingTreeCommands) StageFile(fileName string) error {
return self.cmd.New("git add -- " + self.cmd.Quote(fileName)).Run()
}
// StageAll stages all files
func (self *WorkingTreeCommands) StageAll() error {
return self.cmd.New("git add -A").Run()
}
// UnstageAll unstages all files
func (self *WorkingTreeCommands) UnstageAll() error {
return self.cmd.New("git reset").Run()
}
// UnStageFile unstages a file
// we accept an array of filenames for the cases where a file has been renamed i.e.
// we accept the current name and the previous name
func (self *WorkingTreeCommands) UnStageFile(fileNames []string, reset bool) error {
command := "git rm --cached --force -- %s"
if reset {
command = "git reset HEAD -- %s"
}
for _, name := range fileNames {
err := self.cmd.New(fmt.Sprintf(command, self.cmd.Quote(name))).Run()
if err != nil {
return err
}
}
return nil
}
func (c *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) {
if !file.IsRename() {
return nil, nil, errors.New("Expected renamed file")
}
// we've got a file that represents a rename from one file to another. Here we will refetch
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file.
filesWithoutRenames := c.fileLoader.GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
if f.Name == file.PreviousName {
beforeFile = f
}
if f.Name == file.Name {
afterFile = f
}
}
if beforeFile == nil || afterFile == nil {
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
}
if beforeFile.IsRename() || afterFile.IsRename() {
// probably won't happen but we want to ensure we don't get an infinite loop
return nil, nil, errors.New("Nested rename found")
}
return beforeFile, afterFile, nil
}
// DiscardAllFileChanges directly
func (c *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error {
if file.IsRename() {
beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file)
if err != nil {
return err
}
if err := c.DiscardAllFileChanges(beforeFile); err != nil {
return err
}
if err := c.DiscardAllFileChanges(afterFile); err != nil {
return err
}
return nil
}
quotedFileName := c.cmd.Quote(file.Name)
if file.ShortStatus == "AA" {
if err := c.cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil {
return err
}
if err := c.cmd.New("git add -- " + quotedFileName).Run(); err != nil {
return err
}
return nil
}
if file.ShortStatus == "DU" {
return c.cmd.New("git rm -- " + quotedFileName).Run()
}
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges || file.HasMergeConflicts {
if err := c.cmd.New("git reset -- " + quotedFileName).Run(); err != nil {
return err
}
}
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
return nil
}
if file.Added {
return c.os.RemoveFile(file.Name)
}
return c.DiscardUnstagedFileChanges(file)
}
func (c *WorkingTreeCommands) DiscardAllDirChanges(node *filetree.FileNode) error {
// this could be more efficient but we would need to handle all the edge cases
return node.ForEachFile(c.DiscardAllFileChanges)
}
func (c *WorkingTreeCommands) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
if err := c.RemoveUntrackedDirFiles(node); err != nil {
return err
}
quotedPath := c.cmd.Quote(node.GetPath())
if err := c.cmd.New("git checkout -- " + quotedPath).Run(); err != nil {
return err
}
return nil
}
func (c *WorkingTreeCommands) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
untrackedFilePaths := node.GetPathsMatching(
func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() },
)
for _, path := range untrackedFilePaths {
err := os.Remove(path)
if err != nil {
return err
}
}
return nil
}
// DiscardUnstagedFileChanges directly
func (c *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := c.cmd.Quote(file.Name)
return c.cmd.New("git checkout -- " + quotedFileName).Run()
}
// Ignore adds a file to the gitignore for the repo
func (c *WorkingTreeCommands) Ignore(filename string) error {
return c.os.AppendLineToFile(".gitignore", filename)
}
// WorktreeFileDiff returns the diff of a file
func (c *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
// for now we assume an error means the file was deleted
s, _ := c.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput()
return s
}
func (c *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj {
cachedArg := ""
trackedArg := "--"
colorArg := c.UserConfig.Git.Paging.ColorArg
quotedPath := c.cmd.Quote(node.GetPath())
ignoreWhitespaceArg := ""
contextSize := c.UserConfig.Git.DiffContextSize
if cached {
cachedArg = "--cached"
}
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
trackedArg = "--no-index -- /dev/null"
}
if plain {
colorArg = "never"
}
if ignoreWhitespace {
ignoreWhitespaceArg = "--ignore-all-space"
}
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s %s %s %s %s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
return c.cmd.New(cmdStr).DontLog()
}
func (c *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error {
filepath := filepath.Join(oscommands.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
c.Log.Infof("saving temporary patch to %s", filepath)
if err := c.os.CreateFileWithContent(filepath, patch); err != nil {
return err
}
flagStr := ""
for _, flag := range flags {
flagStr += " --" + flag
}
return c.cmd.New(fmt.Sprintf("git apply%s %s", flagStr, c.cmd.Quote(filepath))).Run()
}
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (c *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
return c.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
}
func (c *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
colorArg := c.UserConfig.Git.Paging.ColorArg
contextSize := c.UserConfig.Git.DiffContextSize
if plain {
colorArg = "never"
}
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
return c.cmd.
New(
fmt.Sprintf(
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s %s %s %s -- %s",
contextSize, colorArg, from, to, reverseFlag, c.cmd.Quote(fileName)),
).
DontLog()
}
// CheckoutFile checks out the file for the given commit
func (c *WorkingTreeCommands) CheckoutFile(commitSha, fileName string) error {
return c.cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, c.cmd.Quote(fileName))).Run()
}
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (c *WorkingTreeCommands) DiscardAnyUnstagedFileChanges() error {
return c.cmd.New("git checkout -- .").Run()
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (c *WorkingTreeCommands) RemoveTrackedFiles(name string) error {
return c.cmd.New("git rm -r --cached -- " + c.cmd.Quote(name)).Run()
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *WorkingTreeCommands) RemoveUntrackedFiles() error {
return c.cmd.New("git clean -fd").Run()
}
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *WorkingTreeCommands) ResetAndClean() error {
submoduleConfigs, err := c.submodule.GetConfigs()
if err != nil {
return err
}
if len(submoduleConfigs) > 0 {
if err := c.submodule.ResetSubmodules(submoduleConfigs); err != nil {
return err
}
}
if err := c.ResetHard("HEAD"); err != nil {
return err
}
return c.RemoveUntrackedFiles()
}
// ResetHardHead runs `git reset --hard`
func (self *WorkingTreeCommands) ResetHard(ref string) error {
return self.cmd.New("git reset --hard " + self.cmd.Quote(ref)).Run()
}
// ResetSoft runs `git reset --soft HEAD`
func (self *WorkingTreeCommands) ResetSoft(ref string) error {
return self.cmd.New("git reset --soft " + self.cmd.Quote(ref)).Run()
}
func (self *WorkingTreeCommands) ResetMixed(ref string) error {
return self.cmd.New("git reset --mixed " + self.cmd.Quote(ref)).Run()
}

View File

@ -0,0 +1,558 @@
package commands
import (
"fmt"
"io/ioutil"
"regexp"
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
func TestGitCommandStageFile(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "add", "--", "test.txt"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.WorkingTree.StageFile("test.txt"))
runner.CheckForMissingCalls()
}
func TestGitCommandUnstageFile(t *testing.T) {
type scenario struct {
testName string
reset bool
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "Remove an untracked file from staging",
reset: false,
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "rm", "--cached", "--force", "--", "test.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "Remove a tracked file from staging",
reset: true,
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "reset", "HEAD", "--", "test.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.WorkingTree.UnStageFile([]string{"test.txt"}, s.reset))
})
}
}
// these tests don't cover everything, in part because we already have an integration
// test which does cover everything. I don't want to unnecessarily assert on the 'how'
// when the 'what' is what matters
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
type scenario struct {
testName string
file *models.File
removeFile func(string) error
runner *oscommands.FakeCmdObjRunner
expectedError string
}
scenarios := []scenario{
{
testName: "An error occurred when resetting",
file: &models.File{
Name: "test",
HasStagedChanges: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "reset", "--", "test"}, "", errors.New("error")),
expectedError: "error",
},
{
testName: "An error occurred when removing file",
file: &models.File{
Name: "test",
Tracked: false,
Added: true,
},
removeFile: func(string) error {
return fmt.Errorf("an error occurred when removing file")
},
runner: oscommands.NewFakeRunner(t),
expectedError: "an error occurred when removing file",
},
{
testName: "An error occurred with checkout",
file: &models.File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "checkout", "--", "test"}, "", errors.New("error")),
expectedError: "error",
},
{
testName: "Checkout only",
file: &models.File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "checkout", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Reset and checkout staged changes",
file: &models.File{
Name: "test",
Tracked: true,
HasStagedChanges: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "reset", "--", "test"}, "", nil).
ExpectArgs([]string{"git", "checkout", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Reset and checkout merge conflicts",
file: &models.File{
Name: "test",
Tracked: true,
HasMergeConflicts: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "reset", "--", "test"}, "", nil).
ExpectArgs([]string{"git", "checkout", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Reset and remove",
file: &models.File{
Name: "test",
Tracked: false,
Added: true,
HasStagedChanges: true,
},
removeFile: func(filename string) error {
assert.Equal(t, "test", filename)
return nil
},
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "reset", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Remove only",
file: &models.File{
Name: "test",
Tracked: false,
Added: true,
HasStagedChanges: false,
},
removeFile: func(filename string) error {
assert.Equal(t, "test", filename)
return nil
},
runner: oscommands.NewFakeRunner(t),
expectedError: "",
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
gitCmd.OSCommand.SetRemoveFile(s.removeFile)
err := gitCmd.WorkingTree.DiscardAllFileChanges(s.file)
if s.expectedError == "" {
assert.Nil(t, err)
} else {
assert.Equal(t, s.expectedError, err.Error())
}
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandDiff(t *testing.T) {
type scenario struct {
testName string
file *models.File
plain bool
cached bool
ignoreWhitespace bool
contextSize int
runner *oscommands.FakeCmdObjRunner
}
const expectedResult = "pretend this is an actual git diff"
scenarios := []scenario{
{
testName: "Default case",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "cached",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: true,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--cached", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "plain",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: true,
cached: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=never", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "File not tracked and file has no staged changes",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: false,
},
plain: false,
cached: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, expectedResult, nil),
},
{
testName: "Default case (ignore whitespace)",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: false,
ignoreWhitespace: true,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "Show diff with custom context size",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: false,
ignoreWhitespace: false,
contextSize: 17,
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=17", "--color=always", "--", "test.txt"}, expectedResult, nil),
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
result := gitCmd.WorkingTree.WorktreeFileDiff(s.file, s.plain, s.cached, s.ignoreWhitespace)
assert.Equal(t, expectedResult, result)
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandShowFileDiff(t *testing.T) {
type scenario struct {
testName string
from string
to string
reverse bool
plain bool
contextSize int
runner *oscommands.FakeCmdObjRunner
}
const expectedResult = "pretend this is an actual git diff"
scenarios := []scenario{
{
testName: "Default case",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "Show diff with custom context size",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
contextSize: 123,
runner: oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=123", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
result, err := gitCmd.WorkingTree.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain)
assert.NoError(t, err)
assert.Equal(t, expectedResult, result)
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandCheckoutFile(t *testing.T) {
type scenario struct {
testName string
commitSha string
fileName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "typical case",
commitSha: "11af912",
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout 11af912 -- "test999.txt"`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "returns error if there is one",
commitSha: "11af912",
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout 11af912 -- "test999.txt"`, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.WorkingTree.CheckoutFile(s.commitSha, s.fileName))
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandApplyPatch(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
expectFn := func(regexStr string, errToReturn error) func(cmdObj oscommands.ICmdObj) (string, error) {
return func(cmdObj oscommands.ICmdObj) (string, error) {
re := regexp.MustCompile(regexStr)
matches := re.FindStringSubmatch(cmdObj.ToString())
assert.Equal(t, 2, len(matches))
filename := matches[1]
content, err := ioutil.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
return "", errToReturn
}
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn(`git apply --cached "(.*)"`, nil)),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "command returns error",
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn(`git apply --cached "(.*)"`, errors.New("error"))),
test: func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.WorkingTree.ApplyPatch("test", "cached"))
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
type scenario struct {
testName string
file *models.File
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "valid case",
file: &models.File{Name: "test.txt"},
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout -- "test.txt"`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.WorkingTree.DiscardUnstagedFileChanges(s.file))
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout -- .`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.WorkingTree.DiscardAnyUnstagedFileChanges())
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
Expect(`git clean -fd`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.WorkingTree.RemoveUntrackedFiles())
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandResetHard(t *testing.T) {
type scenario struct {
testName string
ref string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"valid case",
"HEAD",
oscommands.NewFakeRunner(t).
Expect(`git reset --hard "HEAD"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.WorkingTree.ResetHard(s.ref))
})
}
}