Skip to content

Commit

Permalink
staging lines and hunks
Browse files Browse the repository at this point in the history
  • Loading branch information
jesseduffield committed Dec 5, 2018
1 parent 658e5a9 commit c0f9795
Show file tree
Hide file tree
Showing 21 changed files with 699 additions and 95 deletions.
21 changes: 4 additions & 17 deletions pkg/commands/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package commands
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
Expand Down Expand Up @@ -486,7 +485,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
}
Expand Down Expand Up @@ -595,24 +593,13 @@ func (c *GitCommand) Diff(file *File, plain bool) string {
}

func (c *GitCommand) ApplyPatch(patch string) (string, error) {

content := []byte(patch)
tmpfile, err := ioutil.TempFile("", "patch")
filename, err := c.OSCommand.CreateTempFile("patch", patch)
if err != nil {
c.Log.Error(err)
return "", errors.New("Could not create patch file") // TODO: i18n
}

defer os.Remove(tmpfile.Name()) // clean up

if _, err := tmpfile.Write(content); err != nil {
c.Log.Error(err)
return "", err
}
if err := tmpfile.Close(); err != nil {
c.Log.Error(err)
return "", err
}

return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", tmpfile.Name()))
defer func() { _ = c.OSCommand.RemoveFile(filename) }()

return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", filename))
}
79 changes: 78 additions & 1 deletion pkg/commands/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1770,6 +1770,7 @@ func TestGitCommandDiff(t *testing.T) {
testName string
command func(string, ...string) *exec.Cmd
file *File
plain bool
}

scenarios := []scenario{
Expand All @@ -1786,6 +1787,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",
Expand All @@ -1801,6 +1818,7 @@ func TestGitCommandDiff(t *testing.T) {
HasUnstagedChanges: false,
Tracked: true,
},
false,
},
{
"File not tracked and file has no staged changes",
Expand All @@ -1815,14 +1833,15 @@ func TestGitCommandDiff(t *testing.T) {
HasStagedChanges: false,
Tracked: false,
},
false,
},
}

for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
gitCmd.Diff(s.file, false)
gitCmd.Diff(s.file, s.plain)
})
}
}
Expand Down Expand Up @@ -1979,3 +1998,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"))
})
}
}
26 changes: 26 additions & 0 deletions pkg/commands/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"errors"
"io/ioutil"
"os"
"os/exec"
"strings"
Expand Down Expand Up @@ -176,3 +177,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)
}
32 changes: 32 additions & 0 deletions pkg/commands/os_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"io/ioutil"
"os"
"os/exec"
"testing"
Expand Down Expand Up @@ -364,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))
})
}
}
59 changes: 48 additions & 11 deletions pkg/git/patch_modifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import (
"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
Expand All @@ -20,11 +23,49 @@ func NewPatchModifier(log *logrus.Entry) (*PatchModifier, error) {
}, nil
}

// ModifyPatch takes the original patch, which may contain several hunks,
// 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 *PatchModifier) ModifyPatch(patch string, lineNumber int) (string, error) {
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
lines := strings.Split(patch, "\n")
headerLength := 4
headerLength, err := p.getHeaderLength(lines)
if err != nil {
return "", err
}
output := strings.Join(lines[0:headerLength], "\n") + "\n"

hunkStart, err := p.getHunkStart(lines, lineNumber)
Expand Down Expand Up @@ -55,7 +96,8 @@ func (p *PatchModifier) getHunkStart(patchLines []string, lineNumber int) (int,
return hunkStart, nil
}
}
return 0, errors.New("Could not find hunk")

return 0, errors.New(p.Tr.SLocalize("CantFindHunk"))
}

func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, lineNumber int) ([]string, error) {
Expand Down Expand Up @@ -101,13 +143,8 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
// @@ -14,8 +14,9 @@ import (
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
// current counter is the number after the second comma
re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
matches := re.FindStringSubmatch(currentHeader)
if len(matches) < 2 {
re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
matches = re.FindStringSubmatch(currentHeader)
}
prevLengthString := matches[1]
re := regexp.MustCompile(`(\d+) @@`)
prevLengthString := re.FindStringSubmatch(currentHeader)[1]

prevLength, err := strconv.Atoi(prevLengthString)
if err != nil {
Expand Down
25 changes: 23 additions & 2 deletions pkg/git/patch_modifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func newDummyPatchModifier() *PatchModifier {
Log: newDummyLog(),
}
}
func TestModifyPatch(t *testing.T) {
func TestModifyPatchForLine(t *testing.T) {
type scenario struct {
testName string
patchFilename string
Expand All @@ -43,6 +43,27 @@ func TestModifyPatch(t *testing.T) {
false,
"testdata/testPatchAfter2.diff",
},
{
"Adding one line in top hunk in diff with multiple hunks",
"testdata/testPatchBefore2.diff",
20,
false,
"testdata/testPatchAfter3.diff",
},
{
"Adding one line in top hunk in diff with multiple hunks",
"testdata/testPatchBefore2.diff",
53,
false,
"testdata/testPatchAfter4.diff",
},
{
"adding unstaged file with a single line",
"testdata/addedFile.diff",
6,
false,
"testdata/addedFile.diff",
},
}

for _, s := range scenarios {
Expand All @@ -52,7 +73,7 @@ func TestModifyPatch(t *testing.T) {
if err != nil {
panic("Cannot open file at " + s.patchFilename)
}
afterPatch, err := p.ModifyPatch(string(beforePatch), s.lineNumber)
afterPatch, err := p.ModifyPatchForLine(string(beforePatch), s.lineNumber)
if s.shouldError {
assert.Error(t, err)
} else {
Expand Down
Loading

0 comments on commit c0f9795

Please sign in to comment.