Skip to content

Commit 3d8b5ad

Browse files
authored
Fix a couple of CommentAsPatch issues. (#14804)
* CutDiffAroundLine makes the incorrect assumption that `---` and `+++` always represent part of the header of a diff. This PR adds a flag to its parsing to prevent this problem and adds a streaming parsing technique to CutDiffAroundLine using an io.pipe instead of just sending data to an unbounded buffer. Fix #14711 Signed-off-by: Andrew Thornton <[email protected]> * Handle unquoted comment patch files When making comment patches unfortunately the patch does not always quote the filename This makes the diff --git header ambiguous again. This PR finally adds handling for ambiguity in to parse patch Fix #14812 Signed-off-by: Andrew Thornton <[email protected]> * Add in testing for no error There is no way currently for CutDiffAroundLine in this test to cause an error however, it should still be tested. Signed-off-by: Andrew Thornton <[email protected]>
1 parent 904a26c commit 3d8b5ad

File tree

6 files changed

+270
-41
lines changed

6 files changed

+270
-41
lines changed

modules/git/diff.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,30 +125,39 @@ var hunkRegex = regexp.MustCompile(`^@@ -(?P<beginOld>[0-9]+)(,(?P<endOld>[0-9]+
125125

126126
const cmdDiffHead = "diff --git "
127127

128-
func isHeader(lof string) bool {
129-
return strings.HasPrefix(lof, cmdDiffHead) || strings.HasPrefix(lof, "---") || strings.HasPrefix(lof, "+++")
128+
func isHeader(lof string, inHunk bool) bool {
129+
return strings.HasPrefix(lof, cmdDiffHead) || (!inHunk && (strings.HasPrefix(lof, "---") || strings.HasPrefix(lof, "+++")))
130130
}
131131

132132
// CutDiffAroundLine cuts a diff of a file in way that only the given line + numberOfLine above it will be shown
133133
// it also recalculates hunks and adds the appropriate headers to the new diff.
134134
// Warning: Only one-file diffs are allowed.
135-
func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLine int) string {
135+
func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLine int) (string, error) {
136136
if line == 0 || numbersOfLine == 0 {
137137
// no line or num of lines => no diff
138-
return ""
138+
return "", nil
139139
}
140+
140141
scanner := bufio.NewScanner(originalDiff)
141142
hunk := make([]string, 0)
143+
142144
// begin is the start of the hunk containing searched line
143145
// end is the end of the hunk ...
144146
// currentLine is the line number on the side of the searched line (differentiated by old)
145147
// otherLine is the line number on the opposite side of the searched line (differentiated by old)
146148
var begin, end, currentLine, otherLine int64
147149
var headerLines int
150+
151+
inHunk := false
152+
148153
for scanner.Scan() {
149154
lof := scanner.Text()
150155
// Add header to enable parsing
151-
if isHeader(lof) {
156+
157+
if isHeader(lof, inHunk) {
158+
if strings.HasPrefix(lof, cmdDiffHead) {
159+
inHunk = false
160+
}
152161
hunk = append(hunk, lof)
153162
headerLines++
154163
}
@@ -157,6 +166,7 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
157166
}
158167
// Detect "hunk" with contains commented lof
159168
if strings.HasPrefix(lof, "@@") {
169+
inHunk = true
160170
// Already got our hunk. End of hunk detected!
161171
if len(hunk) > headerLines {
162172
break
@@ -213,15 +223,19 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
213223
}
214224
}
215225
}
226+
err := scanner.Err()
227+
if err != nil {
228+
return "", err
229+
}
216230

217231
// No hunk found
218232
if currentLine == 0 {
219-
return ""
233+
return "", nil
220234
}
221235
// headerLines + hunkLine (1) = totalNonCodeLines
222236
if len(hunk)-headerLines-1 <= numbersOfLine {
223237
// No need to cut the hunk => return existing hunk
224-
return strings.Join(hunk, "\n")
238+
return strings.Join(hunk, "\n"), nil
225239
}
226240
var oldBegin, oldNumOfLines, newBegin, newNumOfLines int64
227241
if old {
@@ -256,5 +270,5 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
256270
// construct the new hunk header
257271
newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
258272
oldBegin, oldNumOfLines, newBegin, newNumOfLines)
259-
return strings.Join(newHunk, "\n")
273+
return strings.Join(newHunk, "\n"), nil
260274
}

modules/git/diff_test.go

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,28 @@ const exampleDiff = `diff --git a/README.md b/README.md
2323
+ cut off
2424
+ cut off`
2525

26+
const breakingDiff = `diff --git a/aaa.sql b/aaa.sql
27+
index d8e4c92..19dc8ad 100644
28+
--- a/aaa.sql
29+
+++ b/aaa.sql
30+
@@ -1,9 +1,10 @@
31+
--some comment
32+
--- some comment 5
33+
+--some coment 2
34+
+-- some comment 3
35+
create or replace procedure test(p1 varchar2)
36+
is
37+
begin
38+
---new comment
39+
dbms_output.put_line(p1);
40+
+--some other comment
41+
end;
42+
/
43+
`
44+
2645
func TestCutDiffAroundLine(t *testing.T) {
27-
result := CutDiffAroundLine(strings.NewReader(exampleDiff), 4, false, 3)
46+
result, err := CutDiffAroundLine(strings.NewReader(exampleDiff), 4, false, 3)
47+
assert.NoError(t, err)
2848
resultByLine := strings.Split(result, "\n")
2949
assert.Len(t, resultByLine, 7)
3050
// Check if headers got transferred
@@ -37,18 +57,50 @@ func TestCutDiffAroundLine(t *testing.T) {
3757
assert.Equal(t, "+ Build Status", resultByLine[4])
3858

3959
// Must be same result as before since old line 3 == new line 5
40-
newResult := CutDiffAroundLine(strings.NewReader(exampleDiff), 3, true, 3)
60+
newResult, err := CutDiffAroundLine(strings.NewReader(exampleDiff), 3, true, 3)
61+
assert.NoError(t, err)
4162
assert.Equal(t, result, newResult, "Must be same result as before since old line 3 == new line 5")
4263

43-
newResult = CutDiffAroundLine(strings.NewReader(exampleDiff), 6, false, 300)
64+
newResult, err = CutDiffAroundLine(strings.NewReader(exampleDiff), 6, false, 300)
65+
assert.NoError(t, err)
4466
assert.Equal(t, exampleDiff, newResult)
4567

46-
emptyResult := CutDiffAroundLine(strings.NewReader(exampleDiff), 6, false, 0)
68+
emptyResult, err := CutDiffAroundLine(strings.NewReader(exampleDiff), 6, false, 0)
69+
assert.NoError(t, err)
4770
assert.Empty(t, emptyResult)
4871

4972
// Line is out of scope
50-
emptyResult = CutDiffAroundLine(strings.NewReader(exampleDiff), 434, false, 0)
73+
emptyResult, err = CutDiffAroundLine(strings.NewReader(exampleDiff), 434, false, 0)
74+
assert.NoError(t, err)
5175
assert.Empty(t, emptyResult)
76+
77+
// Handle minus diffs properly
78+
minusDiff, err := CutDiffAroundLine(strings.NewReader(breakingDiff), 2, false, 4)
79+
assert.NoError(t, err)
80+
81+
expected := `diff --git a/aaa.sql b/aaa.sql
82+
--- a/aaa.sql
83+
+++ b/aaa.sql
84+
@@ -1,9 +1,10 @@
85+
--some comment
86+
--- some comment 5
87+
+--some coment 2`
88+
assert.Equal(t, expected, minusDiff)
89+
90+
// Handle minus diffs properly
91+
minusDiff, err = CutDiffAroundLine(strings.NewReader(breakingDiff), 3, false, 4)
92+
assert.NoError(t, err)
93+
94+
expected = `diff --git a/aaa.sql b/aaa.sql
95+
--- a/aaa.sql
96+
+++ b/aaa.sql
97+
@@ -1,9 +1,10 @@
98+
--some comment
99+
--- some comment 5
100+
+--some coment 2
101+
+-- some comment 3`
102+
103+
assert.Equal(t, expected, minusDiff)
52104
}
53105

54106
func BenchmarkCutDiffAroundLine(b *testing.B) {
@@ -69,7 +121,7 @@ func ExampleCutDiffAroundLine() {
69121
Docker Pulls
70122
+ cut off
71123
+ cut off`
72-
result := CutDiffAroundLine(strings.NewReader(diff), 4, false, 3)
124+
result, _ := CutDiffAroundLine(strings.NewReader(diff), 4, false, 3)
73125
println(result)
74126
}
75127

modules/migrations/gitea_uploader.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
package migrations
77

88
import (
9-
"bytes"
109
"context"
1110
"fmt"
1211
"io"
@@ -802,13 +801,20 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
802801
}
803802

804803
var patch string
805-
patchBuf := new(bytes.Buffer)
806-
if err := git.GetRepoRawDiffForFile(g.gitRepo, pr.MergeBase, headCommitID, git.RawDiffNormal, comment.TreePath, patchBuf); err != nil {
807-
// We should ignore the error since the commit maybe removed when force push to the pull request
808-
log.Warn("GetRepoRawDiffForFile failed when migrating [%s, %s, %s, %s]: %v", g.gitRepo.Path, pr.MergeBase, headCommitID, comment.TreePath, err)
809-
} else {
810-
patch = git.CutDiffAroundLine(patchBuf, int64((&models.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
811-
}
804+
reader, writer := io.Pipe()
805+
defer func() {
806+
_ = reader.Close()
807+
_ = writer.Close()
808+
}()
809+
go func() {
810+
if err := git.GetRepoRawDiffForFile(g.gitRepo, pr.MergeBase, headCommitID, git.RawDiffNormal, comment.TreePath, writer); err != nil {
811+
// We should ignore the error since the commit maybe removed when force push to the pull request
812+
log.Warn("GetRepoRawDiffForFile failed when migrating [%s, %s, %s, %s]: %v", g.gitRepo.Path, pr.MergeBase, headCommitID, comment.TreePath, err)
813+
}
814+
_ = writer.Close()
815+
}()
816+
817+
patch, _ = git.CutDiffAroundLine(reader, int64((&models.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
812818

813819
var c = models.Comment{
814820
Type: models.CommentTypeCode,

services/gitdiff/gitdiff.go

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@ type DiffFile struct {
583583
IsBin bool
584584
IsLFSFile bool
585585
IsRenamed bool
586+
IsAmbiguous bool
586587
IsSubmodule bool
587588
Sections []*DiffSection
588589
IsIncomplete bool
@@ -776,12 +777,32 @@ parsingLoop:
776777
if strings.HasSuffix(line, " 160000\n") {
777778
curFile.IsSubmodule = true
778779
}
780+
case strings.HasPrefix(line, "rename from "):
781+
curFile.IsRenamed = true
782+
curFile.Type = DiffFileRename
783+
if curFile.IsAmbiguous {
784+
curFile.OldName = line[len("rename from ") : len(line)-1]
785+
}
786+
case strings.HasPrefix(line, "rename to "):
787+
curFile.IsRenamed = true
788+
curFile.Type = DiffFileRename
789+
if curFile.IsAmbiguous {
790+
curFile.Name = line[len("rename to ") : len(line)-1]
791+
curFile.IsAmbiguous = false
792+
}
779793
case strings.HasPrefix(line, "copy from "):
780794
curFile.IsRenamed = true
781795
curFile.Type = DiffFileCopy
796+
if curFile.IsAmbiguous {
797+
curFile.OldName = line[len("copy from ") : len(line)-1]
798+
}
782799
case strings.HasPrefix(line, "copy to "):
783800
curFile.IsRenamed = true
784801
curFile.Type = DiffFileCopy
802+
if curFile.IsAmbiguous {
803+
curFile.Name = line[len("copy to ") : len(line)-1]
804+
curFile.IsAmbiguous = false
805+
}
785806
case strings.HasPrefix(line, "new file"):
786807
curFile.Type = DiffFileAdd
787808
curFile.IsCreated = true
@@ -803,9 +824,35 @@ parsingLoop:
803824
case strings.HasPrefix(line, "Binary"):
804825
curFile.IsBin = true
805826
case strings.HasPrefix(line, "--- "):
806-
// Do nothing with this line
827+
// Handle ambiguous filenames
828+
if curFile.IsAmbiguous {
829+
if len(line) > 6 && line[4] == 'a' {
830+
curFile.OldName = line[6 : len(line)-1]
831+
if line[len(line)-2] == '\t' {
832+
curFile.OldName = curFile.OldName[:len(curFile.OldName)-1]
833+
}
834+
} else {
835+
curFile.OldName = ""
836+
}
837+
}
838+
// Otherwise do nothing with this line
807839
case strings.HasPrefix(line, "+++ "):
808-
// Do nothing with this line
840+
// Handle ambiguous filenames
841+
if curFile.IsAmbiguous {
842+
if len(line) > 6 && line[4] == 'b' {
843+
curFile.Name = line[6 : len(line)-1]
844+
if line[len(line)-2] == '\t' {
845+
curFile.Name = curFile.Name[:len(curFile.Name)-1]
846+
}
847+
if curFile.OldName == "" {
848+
curFile.OldName = curFile.Name
849+
}
850+
} else {
851+
curFile.Name = curFile.OldName
852+
}
853+
curFile.IsAmbiguous = false
854+
}
855+
// Otherwise do nothing with this line, but now switch to parsing hunks
809856
lineBytes, isFragment, err := parseHunks(curFile, maxLines, maxLineCharacters, input)
810857
diff.TotalAddition += curFile.Addition
811858
diff.TotalDeletion += curFile.Deletion
@@ -1056,13 +1103,33 @@ func createDiffFile(diff *Diff, line string) *DiffFile {
10561103

10571104
rd := strings.NewReader(line[len(cmdDiffHead):] + " ")
10581105
curFile.Type = DiffFileChange
1059-
curFile.OldName = readFileName(rd)
1060-
curFile.Name = readFileName(rd)
1106+
oldNameAmbiguity := false
1107+
newNameAmbiguity := false
1108+
1109+
curFile.OldName, oldNameAmbiguity = readFileName(rd)
1110+
curFile.Name, newNameAmbiguity = readFileName(rd)
1111+
if oldNameAmbiguity && newNameAmbiguity {
1112+
curFile.IsAmbiguous = true
1113+
// OK we should bet that the oldName and the newName are the same if they can be made to be same
1114+
// So we need to start again ...
1115+
if (len(line)-len(cmdDiffHead)-1)%2 == 0 {
1116+
// diff --git a/b b/b b/b b/b b/b b/b
1117+
//
1118+
midpoint := (len(line) + len(cmdDiffHead) - 1) / 2
1119+
new, old := line[len(cmdDiffHead):midpoint], line[midpoint+1:]
1120+
if len(new) > 2 && len(old) > 2 && new[2:] == old[2:] {
1121+
curFile.OldName = old[2:]
1122+
curFile.Name = old[2:]
1123+
}
1124+
}
1125+
}
1126+
10611127
curFile.IsRenamed = curFile.Name != curFile.OldName
10621128
return curFile
10631129
}
10641130

1065-
func readFileName(rd *strings.Reader) string {
1131+
func readFileName(rd *strings.Reader) (string, bool) {
1132+
ambiguity := false
10661133
var name string
10671134
char, _ := rd.ReadByte()
10681135
_ = rd.UnreadByte()
@@ -1072,9 +1139,24 @@ func readFileName(rd *strings.Reader) string {
10721139
name = name[1:]
10731140
}
10741141
} else {
1142+
// This technique is potentially ambiguous it may not be possible to uniquely identify the filenames from the diff line alone
1143+
ambiguity = true
10751144
fmt.Fscanf(rd, "%s ", &name)
1145+
char, _ := rd.ReadByte()
1146+
_ = rd.UnreadByte()
1147+
for !(char == 0 || char == '"' || char == 'b') {
1148+
var suffix string
1149+
fmt.Fscanf(rd, "%s ", &suffix)
1150+
name += " " + suffix
1151+
char, _ = rd.ReadByte()
1152+
_ = rd.UnreadByte()
1153+
}
1154+
}
1155+
if len(name) < 2 {
1156+
log.Error("Unable to determine name from reader: %v", rd)
1157+
return "", true
10761158
}
1077-
return name[2:]
1159+
return name[2:], ambiguity
10781160
}
10791161

10801162
// GetDiffRange builds a Diff between two commits of a repository.
@@ -1191,6 +1273,7 @@ func CommentAsDiff(c *models.Comment) (*Diff, error) {
11911273
diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
11921274
setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch))
11931275
if err != nil {
1276+
log.Error("Unable to parse patch: %v", err)
11941277
return nil, err
11951278
}
11961279
if len(diff.Files) == 0 {

0 commit comments

Comments
 (0)