diff options
author | Mark Kopenga <mkopenga@gmail.com> | 2018-12-06 08:31:12 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-12-06 08:31:12 +0100 |
commit | 1b6d34e76a4d5d132f95072139fb3aba760eb771 (patch) | |
tree | fb6cc607a67f0d3d2ef0b9d66fafd439864e6d9a | |
parent | 67115436347dec591d12a1a31d1c971915cf0b05 (diff) | |
parent | 1a6a69a8f1f7c44978a384ba56321149f973223d (diff) |
Merge branch 'master' into https-ask-for-username-password
33 files changed, 1283 insertions, 65 deletions
diff --git a/pkg/commands/commit.go b/pkg/commands/commit.go index 54e94ef60..4c304271d 100644 --- a/pkg/commands/commit.go +++ b/pkg/commands/commit.go @@ -13,6 +13,7 @@ type Commit struct { DisplayString string } +// GetDisplayStrings is a function. func (c *Commit) GetDisplayStrings() []string { red := color.New(color.FgRed) yellow := color.New(color.FgGreen) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 2d2710a42..584558d7e 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -267,6 +267,7 @@ func (c *GitCommand) NewBranch(name string) error { return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name)) } +// CurrentBranchName is a function. func (c *GitCommand) CurrentBranchName() (string, error) { branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD") if err != nil { @@ -490,7 +491,6 @@ func (c *GitCommand) getMergeBase() (string, error) { output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch)) if err != nil { // swallowing error because it's not a big deal; probably because there are no commits yet - c.Log.Error(err) } return output, nil } @@ -576,9 +576,10 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool { } // Diff returns the diff of a file -func (c *GitCommand) Diff(file *File) string { +func (c *GitCommand) Diff(file *File, plain bool) string { cachedArg := "" trackedArg := "--" + colorArg := "--color" fileName := c.OSCommand.Quote(file.Name) if file.HasStagedChanges && !file.HasUnstagedChanges { cachedArg = "--cached" @@ -586,9 +587,25 @@ func (c *GitCommand) Diff(file *File) string { if !file.Tracked && !file.HasStagedChanges { trackedArg = "--no-index /dev/null" } - command := fmt.Sprintf("git diff --color %s %s %s", cachedArg, trackedArg, fileName) + if plain { + colorArg = "" + } + + command := fmt.Sprintf("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName) // for now we assume an error means the file was deleted s, _ := c.OSCommand.RunCommandWithOutput(command) return s } + +func (c *GitCommand) ApplyPatch(patch string) (string, error) { + filename, err := c.OSCommand.CreateTempFile("patch", patch) + if err != nil { + c.Log.Error(err) + return "", err + } + + defer func() { _ = c.OSCommand.RemoveFile(filename) }() + + return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", filename)) +} diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index 0410ceb2c..da6a90c98 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -23,26 +23,32 @@ type fileInfoMock struct { sys interface{} } +// Name is a function. func (f fileInfoMock) Name() string { return f.name } +// Size is a function. func (f fileInfoMock) Size() int64 { return f.size } +// Mode is a function. func (f fileInfoMock) Mode() os.FileMode { return f.fileMode } +// ModTime is a function. func (f fileInfoMock) ModTime() time.Time { return f.fileModTime } +// IsDir is a function. func (f fileInfoMock) IsDir() bool { return f.isDir } +// Sys is a function. func (f fileInfoMock) Sys() interface{} { return f.sys } @@ -64,6 +70,7 @@ func newDummyGitCommand() *GitCommand { } } +// TestVerifyInGitRepo is a function. func TestVerifyInGitRepo(t *testing.T) { type scenario struct { testName string @@ -100,6 +107,7 @@ func TestVerifyInGitRepo(t *testing.T) { } } +// TestNavigateToRepoRootDirectory is a function. func TestNavigateToRepoRootDirectory(t *testing.T) { type scenario struct { testName string @@ -156,6 +164,7 @@ func TestNavigateToRepoRootDirectory(t *testing.T) { } } +// TestSetupRepositoryAndWorktree is a function. func TestSetupRepositoryAndWorktree(t *testing.T) { type scenario struct { testName string @@ -224,6 +233,7 @@ func TestSetupRepositoryAndWorktree(t *testing.T) { } } +// TestNewGitCommand is a function. func TestNewGitCommand(t *testing.T) { actual, err := os.Getwd() assert.NoError(t, err) @@ -271,6 +281,7 @@ func TestNewGitCommand(t *testing.T) { } } +// TestGitCommandGetStashEntries is a function. func TestGitCommandGetStashEntries(t *testing.T) { type scenario struct { testName string @@ -323,6 +334,7 @@ func TestGitCommandGetStashEntries(t *testing.T) { } } +// TestGitCommandGetStashEntryDiff is a function. func TestGitCommandGetStashEntryDiff(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -337,6 +349,7 @@ func TestGitCommandGetStashEntryDiff(t *testing.T) { assert.NoError(t, err) } +// TestGitCommandGetStatusFiles is a function. func TestGitCommandGetStatusFiles(t *testing.T) { type scenario struct { testName string @@ -423,6 +436,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) { } } +// TestGitCommandStashDo is a function. func TestGitCommandStashDo(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -435,6 +449,7 @@ func TestGitCommandStashDo(t *testing.T) { assert.NoError(t, gitCmd.StashDo(1, "drop")) } +// TestGitCommandStashSave is a function. func TestGitCommandStashSave(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -447,6 +462,7 @@ func TestGitCommandStashSave(t *testing.T) { assert.NoError(t, gitCmd.StashSave("A stash message")) } +// TestGitCommandCommitAmend is a function. func TestGitCommandCommitAmend(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -460,6 +476,7 @@ func TestGitCommandCommitAmend(t *testing.T) { assert.NoError(t, err) } +// TestGitCommandMergeStatusFiles is a function. func TestGitCommandMergeStatusFiles(t *testing.T) { type scenario struct { testName string @@ -540,6 +557,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) { } } +// TestGitCommandUpstreamDifferentCount is a function. func TestGitCommandUpstreamDifferentCount(t *testing.T) { type scenario struct { testName string @@ -597,6 +615,7 @@ func TestGitCommandUpstreamDifferentCount(t *testing.T) { } } +// TestGitCommandGetCommitsToPush is a function. func TestGitCommandGetCommitsToPush(t *testing.T) { type scenario struct { testName string @@ -635,6 +654,7 @@ func TestGitCommandGetCommitsToPush(t *testing.T) { } } +// TestGitCommandRenameCommit is a function. func TestGitCommandRenameCommit(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -647,6 +667,7 @@ func TestGitCommandRenameCommit(t *testing.T) { assert.NoError(t, gitCmd.RenameCommit("test")) } +// TestGitCommandResetToCommit is a function. func TestGitCommandResetToCommit(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -659,6 +680,7 @@ func TestGitCommandResetToCommit(t *testing.T) { assert.NoError(t, gitCmd.ResetToCommit("78976bc")) } +// TestGitCommandNewBranch is a function. func TestGitCommandNewBranch(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -671,6 +693,7 @@ func TestGitCommandNewBranch(t *testing.T) { assert.NoError(t, gitCmd.NewBranch("test")) } +// TestGitCommandDeleteBranch is a function. func TestGitCommandDeleteBranch(t *testing.T) { type scenario struct { testName string @@ -720,6 +743,7 @@ func TestGitCommandDeleteBranch(t *testing.T) { } } +// TestGitCommandMerge is a function. func TestGitCommandMerge(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -732,6 +756,7 @@ func TestGitCommandMerge(t *testing.T) { assert.NoError(t, gitCmd.Merge("test")) } +// TestGitCommandUsingGpg is a function. func TestGitCommandUsingGpg(t *testing.T) { type scenario struct { testName string @@ -825,6 +850,7 @@ func TestGitCommandUsingGpg(t *testing.T) { } } +// TestGitCommandCommit is a function. func TestGitCommandCommit(t *testing.T) { type scenario struct { testName string @@ -894,6 +920,7 @@ func TestGitCommandCommit(t *testing.T) { } } +// TestGitCommandCommitAmendFromFiles is a function. func TestGitCommandCommitAmendFromFiles(t *testing.T) { type scenario struct { testName string @@ -963,6 +990,7 @@ func TestGitCommandCommitAmendFromFiles(t *testing.T) { } } +// TestGitCommandPush is a function. func TestGitCommandPush(t *testing.T) { type scenario struct { testName string @@ -1025,6 +1053,7 @@ func TestGitCommandPush(t *testing.T) { } } +// TestGitCommandSquashPreviousTwoCommits is a function. func TestGitCommandSquashPreviousTwoCommits(t *testing.T) { type scenario struct { testName string @@ -1088,6 +1117,7 @@ func TestGitCommandSquashPreviousTwoCommits(t *testing.T) { } } +// TestGitCommandSquashFixupCommit is a function. func TestGitCommandSquashFixupCommit(t *testing.T) { type scenario struct { testName string @@ -1151,6 +1181,7 @@ func TestGitCommandSquashFixupCommit(t *testing.T) { } } +// TestGitCommandCatFile is a function. func TestGitCommandCatFile(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -1165,6 +1196,7 @@ func TestGitCommandCatFile(t *testing.T) { assert.Equal(t, "test", o) } +// TestGitCommandStageFile is a function. func TestGitCommandStageFile(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -1177,6 +1209,7 @@ func TestGitCommandStageFile(t *testing.T) { assert.NoError(t, gitCmd.StageFile("test.txt")) } +// TestGitCommandUnstageFile is a function. func TestGitCommandUnstageFile(t *testing.T) { type scenario struct { testName string @@ -1223,6 +1256,7 @@ func TestGitCommandUnstageFile(t *testing.T) { } } +// TestGitCommandIsInMergeState is a function. func TestGitCommandIsInMergeState(t *testing.T) { type scenario struct { testName string @@ -1291,6 +1325,7 @@ func TestGitCommandIsInMergeState(t *testing.T) { } } +// TestGitCommandRemoveFile is a function. func TestGitCommandRemoveFile(t *testing.T) { type scenario struct { testName string @@ -1492,6 +1527,7 @@ func TestGitCommandRemoveFile(t *testing.T) { } } +// TestGitCommandShow is a function. func TestGitCommandShow(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -1505,6 +1541,7 @@ func TestGitCommandShow(t *testing.T) { assert.NoError(t, err) } +// TestGitCommandCheckout is a function. func TestGitCommandCheckout(t *testing.T) { type scenario struct { testName string @@ -1551,6 +1588,7 @@ func TestGitCommandCheckout(t *testing.T) { } } +// TestGitCommandGetBranchGraph is a function. func TestGitCommandGetBranchGraph(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd { @@ -1564,6 +1602,7 @@ func TestGitCommandGetBranchGraph(t *testing.T) { assert.NoError(t, err) } +// TestGitCommandGetCommits is a function. func TestGitCommandGetCommits(t *testing.T) { type scenario struct { testName string @@ -1685,6 +1724,7 @@ func TestGitCommandGetCommits(t *testing.T) { } } +// TestGitCommandGetLog is a function. func TestGitCommandGetLog(t *testing.T) { type scenario struct { testName string @@ -1727,11 +1767,13 @@ func TestGitCommandGetLog(t *testing.T) { } } +// TestGitCommandDiff is a function. func TestGitCommandDiff(t *testing.T) { type scenario struct { testName string command func(string, ...string) *exec.Cmd file *File + plain bool } scenarios := []scenario{ @@ -1748,6 +1790,22 @@ func TestGitCommandDiff(t *testing.T) { HasStagedChanges: false, Tracked: true, }, + false, + }, + { + "Default case", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"diff", "--", "test.txt"}, args) + + return exec.Command("echo") + }, + &File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: true, + }, + true, }, { "All changes staged", @@ -1763,6 +1821,7 @@ func TestGitCommandDiff(t *testing.T) { HasUnstagedChanges: false, Tracked: true, }, + false, }, { "File not tracked and file has no staged changes", @@ -1777,6 +1836,7 @@ func TestGitCommandDiff(t *testing.T) { HasStagedChanges: false, Tracked: false, }, + false, }, } @@ -1784,11 +1844,12 @@ func TestGitCommandDiff(t *testing.T) { t.Run(s.testName, func(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = s.command - gitCmd.Diff(s.file) + gitCmd.Diff(s.file, s.plain) }) } } +// TestGitCommandGetMergeBase is a function. func TestGitCommandGetMergeBase(t *testing.T) { type scenario struct { testName string @@ -1878,6 +1939,7 @@ func TestGitCommandGetMergeBase(t *testing.T) { } } +// TestGitCommandCurrentBranchName is a function. func TestGitCommandCurrentBranchName(t *testing.T) { type scenario struct { testName string @@ -1939,3 +2001,61 @@ func TestGitCommandCurrentBranchName(t *testing.T) { }) } } + +func TestGitCommandApplyPatch(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(string, error) + } + + scenarios := []scenario{ + { + "valid case", + func(cmd string, args ...string) *exec.Cmd { + assert.Equal(t, "git", cmd) + assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2]) + filename := args[2] + content, err := ioutil.ReadFile(filename) + assert.NoError(t, err) + + assert.Equal(t, "test", string(content)) + + return exec.Command("echo", "done") + }, + func(output string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "done\n", output) + }, + }, + { + "command returns error", + func(cmd string, args ...string) *exec.Cmd { + assert.Equal(t, "git", cmd) + assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2]) + filename := args[2] + // TODO: Ideally we want to mock out OSCommand here so that we're not + // double handling testing it's CreateTempFile functionality, + // but it is going to take a bit of work to make a proper mock for it + // so I'm leaving it for another PR + content, err := ioutil.ReadFile(filename) + assert.NoError(t, err) + + assert.Equal(t, "test", string(content)) + + return exec.Command("test") + }, + func(output string, err error) { + assert.Error(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := newDummyGitCommand() + gitCmd.OSCommand.command = s.command + s.test(gitCmd.ApplyPatch("test")) + }) + } +} diff --git a/pkg/commands/os.go b/pkg/commands/os.go index f64f0732e..faf6c5aec 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -2,6 +2,7 @@ package commands import ( "errors" + "io/ioutil" "os" "os/exec" "regexp" @@ -157,7 +158,7 @@ func (c *OSCommand) OpenFile(filename string) error { return err } -// OpenFile opens a file with the given +// OpenLink opens a file with the given func (c *OSCommand) OpenLink(link string) error { commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand") templateValues := map[string]string{ @@ -224,3 +225,28 @@ func (c *OSCommand) AppendLineToFile(filename, line string) error { _, err = f.WriteString("\n" + line) return err } + +// CreateTempFile writes a string to a new temp file and returns the file's name +func (c *OSCommand) CreateTempFile(filename, content string) (string, error) { + tmpfile, err := ioutil.TempFile("", filename) + if err != nil { + c.Log.Error(err) + return "", err + } + + if _, err := tmpfile.Write([]byte(content)); err != nil { + c.Log.Error(err) + return "", err + } + if err := tmpfile.Close(); err != nil { + c.Log.Error(err) + return "", err + } + + return tmpfile.Name(), nil +} + +// RemoveFile removes a file at the specified path +func (c *OSCommand) RemoveFile(filename string) error { + return os.Remove(filename) +} diff --git a/pkg/commands/os_test.go b/pkg/commands/os_test.go index aeef4a6e5..a08c4b57d 100644 --- a/pkg/commands/os_test.go +++ b/pkg/commands/os_test.go @@ -1,6 +1,7 @@ package commands import ( + "io/ioutil" "os" "os/exec" "testing" @@ -29,6 +30,7 @@ func newDummyAppConfig() *config.AppConfig { return appConfig } +// TestOSCommandRunCommandWithOutput is a function. func TestOSCommandRunCommandWithOutput(t *testing.T) { type scenario struct { command string @@ -56,6 +58,7 @@ func TestOSCommandRunCommandWithOutput(t *testing.T) { } } +// TestOSCommandRunCommand is a function. func TestOSCommandRunCommand(t *testing.T) { type scenario struct { command string @@ -76,6 +79,7 @@ func TestOSCommandRunCommand(t *testing.T) { } } +// TestOSCommandOpenFile is a function. func TestOSCommandOpenFile(t *testing.T) { type scenario struct { filename string @@ -126,6 +130,7 @@ func TestOSCommandOpenFile(t *testing.T) { } } +// TestOSCommandEditFile is a function. func TestOSCommandEditFile(t *testing.T) { type scenario struct { filename string @@ -255,6 +260,7 @@ func TestOSCommandEditFile(t *testing.T) { } } +// TestOSCommandQuote is a function. func TestOSCommandQuote(t *testing.T) { osCommand := newDummyOSCommand() @@ -278,7 +284,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) { assert.EqualValues(t, expected, actual) } -// TestOSCommandQuoteSingleQuote tests the quote function with " quotes explicitly for Linux +// TestOSCommandQuoteDoubleQuote tests the quote function with " quotes explicitly for Linux func TestOSCommandQuoteDoubleQuote(t *testing.T) { osCommand := newDummyOSCommand() @@ -291,6 +297,7 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) { assert.EqualValues(t, expected, actual) } +// TestOSCommandUnquote is a function. func TestOSCommandUnquote(t *testing.T) { osCommand := newDummyOSCommand() @@ -301,6 +308,7 @@ func TestOSCommandUnquote(t *testing.T) { assert.EqualValues(t, expected, actual) } +// TestOSCommandFileType is a function. func TestOSCommandFileType(t *testing.T) { type scenario struct { path string @@ -357,3 +365,34 @@ func TestOSCommandFileType(t *testing.T) { _ = os.RemoveAll(s.path) } } + +func TestOSCommandCreateTempFile(t *testing.T) { + type scenario struct { + testName string + filename string + content string + test func(string, error) + } + + scenarios := []scenario{ + { + "valid case", + "filename", + "content", + func(path string, err error) { + assert.NoError(t, err) + + content, err := ioutil.ReadFile(path) + assert.NoError(t, err) + + assert.Equal(t, "content", string(content)) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + s.test(newDummyOSCommand().CreateTempFile(s.filename, s.content)) + }) + } +} diff --git a/pkg/commands/pull_request_test.go b/pkg/commands/pull_request_test.go index a551ee081..774baf92e 100644 --- a/pkg/commands/pull_request_test.go +++ b/pkg/commands/pull_request_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) +// TestGetRepoInfoFromURL is a function. func TestGetRepoInfoFromURL(t *testing.T) { type scenario struct { testName string @@ -41,6 +42,7 @@ func TestGetRepoInfoFromURL(t *testing.T) { } } +// TestCreatePullRequest is a function. func TestCreatePullRequest(t *testing.T) { type scenario struct { testName string diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go new file mode 100644 index 000000000..3c523232e --- /dev/null +++ b/pkg/git/patch_modifier.go @@ -0,0 +1,156 @@ +package git + +import ( + "errors" + "regexp" + "strconv" + "strings" + + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/sirupsen/logrus" +) + +type PatchModifier struct { + Log *logrus.Entry + Tr *i18n.Localizer +} + +// NewPatchModifier builds a new branch list builder +func NewPatchModifier(log *logrus.Entry) (*PatchModifier, error) { + return &PatchModifier{ + Log: log, + }, nil +} + +// ModifyPatchForHunk takes the original patch, which may contain several hunks, +// and removes any hunks that aren't the selected hunk +func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, currentLine int) (string, error) { + // get hunk start and end + lines := strings.Split(patch, "\n") + hunkStartIndex := utils.PrevIndex(hunkStarts, currentLine) + hunkStart := hunkStarts[hunkStartIndex] + nextHunkStartIndex := utils.NextIndex(hunkStarts, currentLine) + var hunkEnd int + if nextHunkStartIndex == 0 { + hunkEnd = len(lines) - 1 + } else { + hunkEnd = hunkStarts[nextHunkStartIndex] + } + + headerLength, err := p.getHeaderLength(lines) + if err != nil { + return "", err + } + + output := strings.Join(lines[0:headerLength], "\n") + "\n" + output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n" + + return output, nil +} + +func (p *PatchModifier) getHeaderLength(patchLines []string) (int, error) { + for index, line := range patchLines { + if strings.HasPrefix(line, "@@") { + return index, nil + } + } + return 0, errors.New(p.Tr.SLocalize("CantFindHunks")) +} + +// ModifyPatchForLine takes the original patch, which may contain several hunks, +// and the line number of the line we want to stage +func (p *PatchModi |