diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go new file mode 100644 index 000000000..0e31af952 --- /dev/null +++ b/pkg/git/patch_modifier.go @@ -0,0 +1,110 @@ +package git + +import ( + "errors" + "regexp" + "strconv" + "strings" + + "github.com/sirupsen/logrus" +) + +type PatchModifier struct { + Log *logrus.Entry +} + +// NewPatchModifier builds a new branch list builder +func NewPatchModifier(log *logrus.Entry) (*PatchModifier, error) { + return &PatchModifier{ + Log: log, + }, nil +} + +// ModifyPatch 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) { + lines := strings.Split(patch, "\n") + headerLength := 4 + output := strings.Join(lines[0:headerLength], "\n") + "\n" + + hunkStart, err := p.getHunkStart(lines, lineNumber) + if err != nil { + return "", err + } + + hunk, err := p.getModifiedHunk(lines, hunkStart, lineNumber) + if err != nil { + return "", err + } + + output += strings.Join(hunk, "\n") + + return output, nil +} + +// getHunkStart returns the line number of the hunk we're going to be modifying +// in order to stage our line +func (p *PatchModifier) getHunkStart(patchLines []string, lineNumber int) (int, error) { + // find the hunk that we're modifying + hunkStart := 0 + for index, line := range patchLines { + if strings.HasPrefix(line, "@@") { + hunkStart = index + } + if index == lineNumber { + return hunkStart, nil + } + } + return 0, errors.New("Could not find hunk") +} + +func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, lineNumber int) ([]string, error) { + lineChanges := 0 + // strip the hunk down to just the line we want to stage + newHunk := []string{} + for offsetIndex, line := range patchLines[hunkStart:] { + index := offsetIndex + hunkStart + if index != lineNumber { + // we include other removals but treat them like context + if strings.HasPrefix(line, "-") { + newHunk = append(newHunk, " "+line[1:]) + lineChanges += 1 + continue + } + // we don't include other additions + if strings.HasPrefix(line, "+") { + lineChanges -= 1 + continue + } + } + newHunk = append(newHunk, line) + } + + var err error + newHunk[0], err = p.updatedHeader(newHunk[0], lineChanges) + if err != nil { + return nil, err + } + + return newHunk, nil +} + +// updatedHeader returns the hunk header with the updated line range +// we need to update the hunk length to reflect the changes we made +// if the hunk has three additions but we're only staging one, then +// @@ -14,8 +14,11 @@ import ( +// becomes +// @@ -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+)`) + prevLengthString := re.FindStringSubmatch(currentHeader)[1] + + prevLength, err := strconv.Atoi(prevLengthString) + if err != nil { + return "", err + } + re = regexp.MustCompile(`\d+ @@`) + newLength := strconv.Itoa(prevLength + lineChanges) + return re.ReplaceAllString(currentHeader, newLength+" @@"), nil +} diff --git a/pkg/git/patch_modifier_test.go b/pkg/git/patch_modifier_test.go new file mode 100644 index 000000000..af7be3751 --- /dev/null +++ b/pkg/git/patch_modifier_test.go @@ -0,0 +1,68 @@ +package git + +import ( + "io/ioutil" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func newDummyLog() *logrus.Entry { + log := logrus.New() + log.Out = ioutil.Discard + return log.WithField("test", "test") +} + +func newDummyPatchModifier() *PatchModifier { + return &PatchModifier{ + Log: newDummyLog(), + } +} +func TestModifyPatch(t *testing.T) { + type scenario struct { + testName string + patchFilename string + lineNumber int + shouldError bool + expectedPatchFilename string + } + + scenarios := []scenario{ + { + "Removing one line", + "testdata/testPatchBefore.diff", + 8, + false, + "testdata/testPatchAfter1.diff", + }, + { + "Adding one line", + "testdata/testPatchBefore.diff", + 10, + false, + "testdata/testPatchAfter2.diff", + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + p := newDummyPatchModifier() + beforePatch, err := ioutil.ReadFile(s.patchFilename) + if err != nil { + panic("Cannot open file at " + s.patchFilename) + } + afterPatch, err := p.ModifyPatch(string(beforePatch), s.lineNumber) + if s.shouldError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + expected, err := ioutil.ReadFile(s.expectedPatchFilename) + if err != nil { + panic("Cannot open file at " + s.expectedPatchFilename) + } + assert.Equal(t, string(expected), afterPatch) + } + }) + } +} diff --git a/pkg/git/testdata/testPatchAfter1.diff b/pkg/git/testdata/testPatchAfter1.diff new file mode 100644 index 000000000..88066e1c2 --- /dev/null +++ b/pkg/git/testdata/testPatchAfter1.diff @@ -0,0 +1,13 @@ +diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go +index 60ec4e0..db4485d 100644 +--- a/pkg/git/branch_list_builder.go ++++ b/pkg/git/branch_list_builder.go +@@ -14,8 +14,7 @@ import ( + + // context: + // we want to only show 'safe' branches (ones that haven't e.g. been deleted) +-// which `git branch -a` gives us, but we also want the recency data that + // git reflog gives us. + // So we get the HEAD, then append get the reflog branches that intersect with + // our safe branches, then add the remaining safe branches, ensuring uniqueness + // along the way diff --git a/pkg/git/testdata/testPatchAfter2.diff b/pkg/git/testdata/testPatchAfter2.diff new file mode 100644 index 000000000..0a17c2b67 --- /dev/null +++ b/pkg/git/testdata/testPatchAfter2.diff @@ -0,0 +1,14 @@ +diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go +index 60ec4e0..db4485d 100644 +--- a/pkg/git/branch_list_builder.go ++++ b/pkg/git/branch_list_builder.go +@@ -14,8 +14,9 @@ import ( + + // context: + // we want to only show 'safe' branches (ones that haven't e.g. been deleted) + // which `git branch -a` gives us, but we also want the recency data that + // git reflog gives us. ++// test 2 - if I remove this, I decrement the end counter + // So we get the HEAD, then append get the reflog branches that intersect with + // our safe branches, then add the remaining safe branches, ensuring uniqueness + // along the way diff --git a/pkg/git/testdata/testPatchBefore.diff b/pkg/git/testdata/testPatchBefore.diff new file mode 100644 index 000000000..14e4b0e23 --- /dev/null +++ b/pkg/git/testdata/testPatchBefore.diff @@ -0,0 +1,15 @@ +diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go +index 60ec4e0..db4485d 100644 +--- a/pkg/git/branch_list_builder.go ++++ b/pkg/git/branch_list_builder.go +@@ -14,8 +14,8 @@ import ( + + // context: + // we want to only show 'safe' branches (ones that haven't e.g. been deleted) +-// which `git branch -a` gives us, but we also want the recency data that +-// git reflog gives us. ++// test 2 - if I remove this, I decrement the end counter ++// test + // So we get the HEAD, then append get the reflog branches that intersect with + // our safe branches, then add the remaining safe branches, ensuring uniqueness + // along the way