Skip to content

Commit e1d8550

Browse files
committed
fix: preserve line order when staging non-contiguous lines in change blocks
1 parent 3201695 commit e1d8550

File tree

2 files changed

+92
-1
lines changed

2 files changed

+92
-1
lines changed

pkg/commands/patch/patch_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,59 @@ func TestAdjustLineNumber(t *testing.T) {
711711
}
712712
}
713713

714+
func TestTransformNonContiguousSelection(t *testing.T) {
715+
const changeBlockDiff = `diff --git a/test.c b/test.c
716+
index 1234567..abcdefg 100644
717+
--- a/test.c
718+
+++ b/test.c
719+
@@ -1,6 +1,6 @@
720+
int main() {
721+
- // init something
722+
- int i = 0;
723+
+ // init variables
724+
+ int x = 0;
725+
726+
return 0;
727+
}
728+
`
729+
730+
scenarios := []struct {
731+
testName string
732+
selectedIndices []int
733+
expected string
734+
}{
735+
{
736+
testName: "non-contiguous selection keeps change pairs together",
737+
selectedIndices: []int{6, 8},
738+
expected: `--- a/test.c
739+
+++ b/test.c
740+
@@ -1,6 +1,6 @@
741+
int main() {
742+
- // init something
743+
+ // init variables
744+
int i = 0;
745+
746+
return 0;
747+
}
748+
`,
749+
},
750+
}
751+
752+
for _, s := range scenarios {
753+
t.Run(s.testName, func(t *testing.T) {
754+
result := Parse(changeBlockDiff).
755+
Transform(TransformOpts{
756+
Reverse: false,
757+
FileNameOverride: "test.c",
758+
IncludedLineIndices: s.selectedIndices,
759+
}).
760+
FormatPlain()
761+
762+
assert.Equal(t, s.expected, result)
763+
})
764+
}
765+
}
766+
714767
func TestIsSingleHunkForWholeFile(t *testing.T) {
715768
scenarios := []struct {
716769
testName string

pkg/commands/patch/transform.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,51 @@ func (self *patchTransformer) transformHunkLines(hunk *Hunk, firstLineIdx int) [
126126
skippedNewlineMessageIndex := -1
127127
newLines := []*PatchLine{}
128128

129-
for i, line := range hunk.bodyLines {
129+
for i := 0; i < len(hunk.bodyLines); i++ {
130+
line := hunk.bodyLines[i]
130131
lineIdx := i + firstLineIdx + 1 // plus one for header line
131132
if line.Content == "" {
132133
break
133134
}
134135
isLineSelected := lo.Contains(self.opts.IncludedLineIndices, lineIdx)
135136

137+
// Check for change block (multiple consecutive deletions)
138+
if line.Kind == DELETION {
139+
deletionCount := 0
140+
for j := i; j < len(hunk.bodyLines) && hunk.bodyLines[j].Kind == DELETION; j++ {
141+
deletionCount++
142+
}
143+
if deletionCount > 1 {
144+
selectedDeletions := []*PatchLine{}
145+
pendingContext := []*PatchLine{}
146+
for j := 0; j < deletionCount; j++ {
147+
delLine := hunk.bodyLines[i+j]
148+
delLineIdx := i + j + firstLineIdx + 1
149+
if lo.Contains(self.opts.IncludedLineIndices, delLineIdx) {
150+
selectedDeletions = append(selectedDeletions, delLine)
151+
} else if (delLine.Kind == DELETION && !self.opts.Reverse) || (delLine.Kind == ADDITION && self.opts.Reverse) {
152+
pendingContext = append(pendingContext, &PatchLine{Kind: CONTEXT, Content: " " + delLine.Content[1:]})
153+
}
154+
}
155+
newLines = append(newLines, selectedDeletions...)
156+
157+
// Process additions in the block
158+
addIdx := i + deletionCount
159+
for addIdx < len(hunk.bodyLines) && hunk.bodyLines[addIdx].Kind == ADDITION {
160+
addLineIdx := addIdx + firstLineIdx + 1
161+
if lo.Contains(self.opts.IncludedLineIndices, addLineIdx) {
162+
newLines = append(newLines, hunk.bodyLines[addIdx])
163+
} else {
164+
skippedNewlineMessageIndex = addLineIdx + 1
165+
}
166+
addIdx++
167+
}
168+
newLines = append(newLines, pendingContext...)
169+
i = addIdx - 1
170+
continue
171+
}
172+
}
173+
136174
if isLineSelected || (line.Kind == NEWLINE_MESSAGE && skippedNewlineMessageIndex != lineIdx) || line.Kind == CONTEXT {
137175
newLines = append(newLines, line)
138176
continue

0 commit comments

Comments
 (0)