diff --git a/modules/structs/pull.go b/modules/structs/pull.go index 7cc58217a0f17..449a62f517e67 100644 --- a/modules/structs/pull.go +++ b/modules/structs/pull.go @@ -188,4 +188,6 @@ type ChangedFile struct { ContentsURL string `json:"contents_url,omitempty"` // The raw URL to download the file RawURL string `json:"raw_url,omitempty"` + // The patch text for the file changes + Patch string `json:"patch,omitempty"` } diff --git a/services/convert/convert.go b/services/convert/convert.go index 0de38221409bb..ea371aff8b7e5 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -812,6 +812,7 @@ func ToChangedFile(f *gitdiff.DiffFile, repo *repo_model.Repository, commit stri HTMLURL: fmt.Sprint(repo.HTMLURL(), "/src/commit/", commit, "/", util.PathEscapeSegments(f.GetDiffFileName())), ContentsURL: fmt.Sprint(repo.APIURL(), "/contents/", util.PathEscapeSegments(f.GetDiffFileName()), "?ref=", commit), RawURL: fmt.Sprint(repo.HTMLURL(), "/raw/commit/", commit, "/", util.PathEscapeSegments(f.GetDiffFileName())), + Patch: gitdiff.RenderUnifiedDiff(f), } return file diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index db0f565b526a0..fd8fbd23caf00 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1058,6 +1058,88 @@ func createDiffFile(line string) *DiffFile { return curFile } +func RenderUnifiedDiff(file *DiffFile) string { + var sb strings.Builder + + oldPath := file.OldName + if oldPath == "" { + oldPath = file.Name + } + newPath := file.Name + + // File header + fmt.Fprintf(&sb, "diff --git a/%s b/%s\n", oldPath, newPath) + fmt.Fprintf(&sb, "--- a/%s\n", oldPath) + fmt.Fprintf(&sb, "+++ b/%s\n", newPath) + + if file.IsBin { + return ("Binary files differ\n\n") + } + + for _, section := range file.Sections { + // Compute hunk header + leftStart, leftCount := hunkRange(section, true) + rightStart, rightCount := hunkRange(section, false) + fmt.Fprintf(&sb, "@@ -%d,%d +%d,%d @@\n", leftStart, leftCount, rightStart, rightCount) + + for _, line := range section.Lines { + prefix := " " + switch line.Type { + case DiffLineAdd: + prefix = "+" + case DiffLineDel: + prefix = "-" + } + sb.WriteString(prefix + line.Content) + if !strings.HasSuffix(line.Content, "\n") { + sb.WriteString("\n") + } + } + } + sb.WriteString("\n") + + return sb.String() +} + +// hunkRange calculates the start and length for either old or new file in a section +func hunkRange(section *DiffSection, left bool) (start, count int) { + lines := section.Lines + if len(lines) == 0 { + return 0, 0 + } + + if left { + for _, l := range lines { + if l.LeftIdx > 0 { + start = l.LeftIdx + break + } + } + for _, l := range lines { + if l.LeftIdx > 0 { + count++ + } + } + } else { + for _, l := range lines { + if l.RightIdx > 0 { + start = l.RightIdx + break + } + } + for _, l := range lines { + if l.RightIdx > 0 { + count++ + } + } + } + + if count == 0 { + count = 1 + } + return start, count +} + func readFileName(rd *strings.Reader) (string, bool) { ambiguity := false var name string diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 325f0b78a0564..221cccd31ba03 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -22121,6 +22121,11 @@ "type": "string", "x-go-name": "HTMLURL" }, + "patch": { + "description": "The patch text for the file changes", + "type": "string", + "x-go-name": "Patch" + }, "previous_filename": { "description": "The previous filename if the file was renamed", "type": "string", diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go index 8376fd2717d3b..bc4c9497a4009 100644 --- a/tests/integration/api_pull_test.go +++ b/tests/integration/api_pull_test.go @@ -80,6 +80,7 @@ func TestAPIViewPulls(t *testing.T) { assert.Equal(t, 1, files[0].Changes) assert.Equal(t, 0, files[0].Deletions) assert.Equal(t, "added", files[0].Status) + assert.Equal(t, gitdiff.RenderUnifiedDiff(patch.Files[0]), files[0].Patch) } })) }