History

Move git diff codes from models to services/gitdiff (#7889)

* move git diff codes from models to services/gitdiff * fix template * fix test * fix template

Lunny Xiao committed 7 years ago

Commit c03d75f1 parent b660a7315 files · +164 −111Browse files

Showing the changes to services/gitdiff/gitdiff.go (1 of 15 files). Show all

Unified · Split

services/gitdiff/gitdiff.go+828−0
@@ -0,0 +1,828 @@
1+// Copyright 2014 The Gogs Authors. All rights reserved.
2+// Copyright 2019 The Gitea Authors. All rights reserved.
3+// Use of this source code is governed by a MIT-style
4+// license that can be found in the LICENSE file.
5+
6+package gitdiff
7+
8+import (
9+ "bufio"
10+ "bytes"
11+ "fmt"
12+ "html"
13+ "html/template"
14+ "io"
15+ "io/ioutil"
16+ "os"
17+ "os/exec"
18+ "regexp"
19+ "sort"
20+ "strconv"
21+ "strings"
22+
23+ "code.gitea.io/gitea/models"
24+ "code.gitea.io/gitea/modules/charset"
25+ "code.gitea.io/gitea/modules/git"
26+ "code.gitea.io/gitea/modules/highlight"
27+ "code.gitea.io/gitea/modules/log"
28+ "code.gitea.io/gitea/modules/process"
29+ "code.gitea.io/gitea/modules/setting"
30+
31+ "github.com/sergi/go-diff/diffmatchpatch"
32+ "github.com/unknwon/com"
33+ stdcharset "golang.org/x/net/html/charset"
34+ "golang.org/x/text/transform"
35+)
36+
37+// DiffLineType represents the type of a DiffLine.
38+type DiffLineType uint8
39+
40+// DiffLineType possible values.
41+const (
42+ DiffLinePlain DiffLineType = iota + 1
43+ DiffLineAdd
44+ DiffLineDel
45+ DiffLineSection
46+)
47+
48+// DiffFileType represents the type of a DiffFile.
49+type DiffFileType uint8
50+
51+// DiffFileType possible values.
52+const (
53+ DiffFileAdd DiffFileType = iota + 1
54+ DiffFileChange
55+ DiffFileDel
56+ DiffFileRename
57+)
58+
59+// DiffLine represents a line difference in a DiffSection.
60+type DiffLine struct {
61+ LeftIdx int
62+ RightIdx int
63+ Type DiffLineType
64+ Content string
65+ Comments []*models.Comment
66+}
67+
68+// GetType returns the type of a DiffLine.
69+func (d *DiffLine) GetType() int {
70+ return int(d.Type)
71+}
72+
73+// CanComment returns whether or not a line can get commented
74+func (d *DiffLine) CanComment() bool {
75+ return len(d.Comments) == 0 && d.Type != DiffLineSection
76+}
77+
78+// GetCommentSide returns the comment side of the first comment, if not set returns empty string
79+func (d *DiffLine) GetCommentSide() string {
80+ if len(d.Comments) == 0 {
81+ return ""
82+ }
83+ return d.Comments[0].DiffSide()
84+}
85+
86+// GetLineTypeMarker returns the line type marker
87+func (d *DiffLine) GetLineTypeMarker() string {
88+ if strings.IndexByte(" +-", d.Content[0]) > -1 {
89+ return d.Content[0:1]
90+ }
91+ return ""
92+}
93+
94+// escape a line's content or return <br> needed for copy/paste purposes
95+func getLineContent(content string) string {
96+ if len(content) > 0 {
97+ return html.EscapeString(content)
98+ }
99+ return "<br>"
100+}
101+
102+// DiffSection represents a section of a DiffFile.
103+type DiffSection struct {
104+ Name string
105+ Lines []*DiffLine
106+}
107+
108+var (
109+ addedCodePrefix = []byte(`<span class="added-code">`)
110+ removedCodePrefix = []byte(`<span class="removed-code">`)
111+ codeTagSuffix = []byte(`</span>`)
112+)
113+
114+func diffToHTML(diffs []diffmatchpatch.Diff, lineType DiffLineType) template.HTML {
115+ buf := bytes.NewBuffer(nil)
116+
117+ for i := range diffs {
118+ switch {
119+ case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd:
120+ buf.Write(addedCodePrefix)
121+ buf.WriteString(getLineContent(diffs[i].Text))
122+ buf.Write(codeTagSuffix)
123+ case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel:
124+ buf.Write(removedCodePrefix)
125+ buf.WriteString(getLineContent(diffs[i].Text))
126+ buf.Write(codeTagSuffix)
127+ case diffs[i].Type == diffmatchpatch.DiffEqual:
128+ buf.WriteString(getLineContent(diffs[i].Text))
129+ }
130+ }
131+
132+ return template.HTML(buf.Bytes())
133+}
134+
135+// GetLine gets a specific line by type (add or del) and file line number
136+func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine {
137+ var (
138+ difference = 0
139+ addCount = 0
140+ delCount = 0
141+ matchDiffLine *DiffLine
142+ )
143+
144+LOOP:
145+ for _, diffLine := range diffSection.Lines {
146+ switch diffLine.Type {
147+ case DiffLineAdd:
148+ addCount++
149+ case DiffLineDel:
150+ delCount++
151+ default:
152+ if matchDiffLine != nil {
153+ break LOOP
154+ }
155+ difference = diffLine.RightIdx - diffLine.LeftIdx
156+ addCount = 0
157+ delCount = 0
158+ }
159+
160+ switch lineType {
161+ case DiffLineDel:
162+ if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference {
163+ matchDiffLine = diffLine
164+ }
165+ case DiffLineAdd:
166+ if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference {
167+ matchDiffLine = diffLine
168+ }
169+ }
170+ }
171+
172+ if addCount == delCount {
173+ return matchDiffLine
174+ }
175+ return nil
176+}
177+
178+var diffMatchPatch = diffmatchpatch.New()
179+
180+func init() {
181+ diffMatchPatch.DiffEditCost = 100
182+}
183+
184+// GetComputedInlineDiffFor computes inline diff for the given line.
185+func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML {
186+ if setting.Git.DisableDiffHighlight {
187+ return template.HTML(getLineContent(diffLine.Content[1:]))
188+ }
189+ var (
190+ compareDiffLine *DiffLine
191+ diff1 string
192+ diff2 string
193+ )
194+
195+ // try to find equivalent diff line. ignore, otherwise
196+ switch diffLine.Type {
197+ case DiffLineAdd:
198+ compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
199+ if compareDiffLine == nil {
200+ return template.HTML(getLineContent(diffLine.Content[1:]))
201+ }
202+ diff1 = compareDiffLine.Content
203+ diff2 = diffLine.Content
204+ case DiffLineDel:
205+ compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
206+ if compareDiffLine == nil {
207+ return template.HTML(getLineContent(diffLine.Content[1:]))
208+ }
209+ diff1 = diffLine.Content
210+ diff2 = compareDiffLine.Content
211+ default:
212+ if strings.IndexByte(" +-", diffLine.Content[0]) > -1 {
213+ return template.HTML(getLineContent(diffLine.Content[1:]))
214+ }
215+ return template.HTML(getLineContent(diffLine.Content))
216+ }
217+
218+ diffRecord := diffMatchPatch.DiffMain(diff1[1:], diff2[1:], true)
219+ diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
220+
221+ return diffToHTML(diffRecord, diffLine.Type)
222+}
223+
224+// DiffFile represents a file diff.
225+type DiffFile struct {
226+ Name string
227+ OldName string
228+ Index int
229+ Addition, Deletion int
230+ Type DiffFileType
231+ IsCreated bool
232+ IsDeleted bool
233+ IsBin bool
234+ IsLFSFile bool
235+ IsRenamed bool
236+ IsSubmodule bool
237+ Sections []*DiffSection
238+ IsIncomplete bool
239+}
240+
241+// GetType returns type of diff file.
242+func (diffFile *DiffFile) GetType() int {
243+ return int(diffFile.Type)
244+}
245+
246+// GetHighlightClass returns highlight class for a filename.
247+func (diffFile *DiffFile) GetHighlightClass() string {
248+ return highlight.FileNameToHighlightClass(diffFile.Name)
249+}
250+
251+// Diff represents a difference between two git trees.
252+type Diff struct {
253+ TotalAddition, TotalDeletion int
254+ Files []*DiffFile
255+ IsIncomplete bool
256+}
257+
258+// LoadComments loads comments into each line
259+func (diff *Diff) LoadComments(issue *models.Issue, currentUser *models.User) error {
260+ allComments, err := models.FetchCodeComments(issue, currentUser)
261+ if err != nil {
262+ return err
263+ }
264+ for _, file := range diff.Files {
265+ if lineCommits, ok := allComments[file.Name]; ok {
266+ for _, section := range file.Sections {
267+ for _, line := range section.Lines {
268+ if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {
269+ line.Comments = append(line.Comments, comments...)
270+ }
271+ if comments, ok := lineCommits[int64(line.RightIdx)]; ok {
272+ line.Comments = append(line.Comments, comments...)
273+ }
274+ sort.SliceStable(line.Comments, func(i, j int) bool {
275+ return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
276+ })
277+ }
278+ }
279+ }
280+ }
281+ return nil
282+}
283+
284+// NumFiles returns number of files changes in a diff.
285+func (diff *Diff) NumFiles() int {
286+ return len(diff.Files)
287+}
288+
289+// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
290+var hunkRegex = regexp.MustCompile(`^@@ -(?P<beginOld>[0-9]+)(,(?P<endOld>[0-9]+))? \+(?P<beginNew>[0-9]+)(,(?P<endNew>[0-9]+))? @@`)
291+
292+func isHeader(lof string) bool {
293+ return strings.HasPrefix(lof, cmdDiffHead) || strings.HasPrefix(lof, "---") || strings.HasPrefix(lof, "+++")
294+}
295+
296+// CutDiffAroundLine cuts a diff of a file in way that only the given line + numberOfLine above it will be shown
297+// it also recalculates hunks and adds the appropriate headers to the new diff.
298+// Warning: Only one-file diffs are allowed.
299+func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLine int) string {
300+ if line == 0 || numbersOfLine == 0 {
301+ // no line or num of lines => no diff
302+ return ""
303+ }
304+ scanner := bufio.NewScanner(originalDiff)
305+ hunk := make([]string, 0)
306+ // begin is the start of the hunk containing searched line
307+ // end is the end of the hunk ...
308+ // currentLine is the line number on the side of the searched line (differentiated by old)
309+ // otherLine is the line number on the opposite side of the searched line (differentiated by old)
310+ var begin, end, currentLine, otherLine int64
311+ var headerLines int
312+ for scanner.Scan() {
313+ lof := scanner.Text()
314+ // Add header to enable parsing
315+ if isHeader(lof) {
316+ hunk = append(hunk, lof)
317+ headerLines++
318+ }
319+ if currentLine > line {
320+ break
321+ }
322+ // Detect "hunk" with contains commented lof
323+ if strings.HasPrefix(lof, "@@") {
324+ // Already got our hunk. End of hunk detected!
325+ if len(hunk) > headerLines {
326+ break
327+ }
328+ // A map with named groups of our regex to recognize them later more easily
329+ submatches := hunkRegex.FindStringSubmatch(lof)
330+ groups := make(map[string]string)
331+ for i, name := range hunkRegex.SubexpNames() {
332+ if i != 0 && name != "" {
333+ groups[name] = submatches[i]
334+ }
335+ }
336+ if old {
337+ begin = com.StrTo(groups["beginOld"]).MustInt64()
338+ end = com.StrTo(groups["endOld"]).MustInt64()
339+ // init otherLine with begin of opposite side
340+ otherLine = com.StrTo(groups["beginNew"]).MustInt64()
341+ } else {
342+ begin = com.StrTo(groups["beginNew"]).MustInt64()
343+ if groups["endNew"] != "" {
344+ end = com.StrTo(groups["endNew"]).MustInt64()
345+ } else {
346+ end = 0
347+ }
348+ // init otherLine with begin of opposite side
349+ otherLine = com.StrTo(groups["beginOld"]).MustInt64()
350+ }
351+ end += begin // end is for real only the number of lines in hunk
352+ // lof is between begin and end
353+ if begin <= line && end >= line {
354+ hunk = append(hunk, lof)
355+ currentLine = begin
356+ continue
357+ }
358+ } else if len(hunk) > headerLines {
359+ hunk = append(hunk, lof)
360+ // Count lines in context
361+ switch lof[0] {
362+ case '+':
363+ if !old {
364+ currentLine++
365+ } else {
366+ otherLine++
367+ }
368+ case '-':
369+ if old {
370+ currentLine++
371+ } else {
372+ otherLine++
373+ }
374+ default:
375+ currentLine++
376+ otherLine++
377+ }
378+ }
379+ }
380+
381+ // No hunk found
382+ if currentLine == 0 {
383+ return ""
384+ }
385+ // headerLines + hunkLine (1) = totalNonCodeLines
386+ if len(hunk)-headerLines-1 <= numbersOfLine {
387+ // No need to cut the hunk => return existing hunk
388+ return strings.Join(hunk, "\n")
389+ }
390+ var oldBegin, oldNumOfLines, newBegin, newNumOfLines int64
391+ if old {
392+ oldBegin = currentLine
393+ newBegin = otherLine
394+ } else {
395+ oldBegin = otherLine
396+ newBegin = currentLine
397+ }
398+ // headers + hunk header
399+ newHunk := make([]string, headerLines)
400+ // transfer existing headers
401+ copy(newHunk, hunk[:headerLines])
402+ // transfer last n lines
403+ newHunk = append(newHunk, hunk[len(hunk)-numbersOfLine-1:]...)
404+ // calculate newBegin, ... by counting lines
405+ for i := len(hunk) - 1; i >= len(hunk)-numbersOfLine; i-- {
406+ switch hunk[i][0] {
407+ case '+':
408+ newBegin--
409+ newNumOfLines++
410+ case '-':
411+ oldBegin--
412+ oldNumOfLines++
413+ default:
414+ oldBegin--
415+ newBegin--
416+ newNumOfLines++
417+ oldNumOfLines++
418+ }
419+ }
420+ // construct the new hunk header
421+ newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
422+ oldBegin, oldNumOfLines, newBegin, newNumOfLines)
423+ return strings.Join(newHunk, "\n")
424+}
425+
426+const cmdDiffHead = "diff --git "
427+
428+// ParsePatch builds a Diff object from a io.Reader and some
429+// parameters.
430+// TODO: move this function to gogits/git-module
431+func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*Diff, error) {
432+ var (
433+ diff = &Diff{Files: make([]*DiffFile, 0)}
434+
435+ curFile = &DiffFile{}
436+ curSection = &DiffSection{
437+ Lines: make([]*DiffLine, 0, 10),
438+ }
439+
440+ leftLine, rightLine int
441+ lineCount int
442+ curFileLinesCount int
443+ curFileLFSPrefix bool
444+ )
445+
446+ input := bufio.NewReader(reader)
447+ isEOF := false
448+ for !isEOF {
449+ var linebuf bytes.Buffer
450+ for {
451+ b, err := input.ReadByte()
452+ if err != nil {
453+ if err == io.EOF {
454+ isEOF = true
455+ break
456+ } else {
457+ return nil, fmt.Errorf("ReadByte: %v", err)
458+ }
459+ }
460+ if b == '\n' {
461+ break
462+ }
463+ if linebuf.Len() < maxLineCharacters {
464+ linebuf.WriteByte(b)
465+ } else if linebuf.Len() == maxLineCharacters {
466+ curFile.IsIncomplete = true
467+ }
468+ }
469+ line := linebuf.String()
470+
471+ if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") || len(line) == 0 {
472+ continue
473+ }
474+
475+ trimLine := strings.Trim(line, "+- ")
476+
477+ if trimLine == models.LFSMetaFileIdentifier {
478+ curFileLFSPrefix = true
479+ }
480+
481+ if curFileLFSPrefix && strings.HasPrefix(trimLine, models.LFSMetaFileOidPrefix) {
482+ oid := strings.TrimPrefix(trimLine, models.LFSMetaFileOidPrefix)
483+
484+ if len(oid) == 64 {
485+ m := &models.LFSMetaObject{Oid: oid}
486+ count, err := models.Count(m)
487+
488+ if err == nil && count > 0 {
489+ curFile.IsBin = true
490+ curFile.IsLFSFile = true
491+ curSection.Lines = nil
492+ }
493+ }
494+ }
495+
496+ curFileLinesCount++
497+ lineCount++
498+
499+ // Diff data too large, we only show the first about maxLines lines
500+ if curFileLinesCount >= maxLines {
501+ curFile.IsIncomplete = true
502+ }
503+ switch {
504+ case line[0] == ' ':
505+ diffLine := &DiffLine{Type: DiffLinePlain, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
506+ leftLine++
507+ rightLine++
508+ curSection.Lines = append(curSection.Lines, diffLine)
509+ continue
510+ case line[0] == '@':
511+ curSection = &DiffSection{}
512+ curFile.Sections = append(curFile.Sections, curSection)
513+ ss := strings.Split(line, "@@")
514+ diffLine := &DiffLine{Type: DiffLineSection, Content: line}
515+ curSection.Lines = append(curSection.Lines, diffLine)
516+
517+ // Parse line number.
518+ ranges := strings.Split(ss[1][1:], " ")
519+ leftLine, _ = com.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int()
520+ if len(ranges) > 1 {
521+ rightLine, _ = com.StrTo(strings.Split(ranges[1], ",")[0]).Int()
522+ } else {
523+ log.Warn("Parse line number failed: %v", line)
524+ rightLine = leftLine
525+ }
526+ continue
527+ case line[0] == '+':
528+ curFile.Addition++
529+ diff.TotalAddition++
530+ diffLine := &DiffLine{Type: DiffLineAdd, Content: line, RightIdx: rightLine}
531+ rightLine++
532+ curSection.Lines = append(curSection.Lines, diffLine)
533+ continue
534+ case line[0] == '-':
535+ curFile.Deletion++
536+ diff.TotalDeletion++
537+ diffLine := &DiffLine{Type: DiffLineDel, Content: line, LeftIdx: leftLine}
538+ if leftLine > 0 {
539+ leftLine++
540+ }
541+ curSection.Lines = append(curSection.Lines, diffLine)
542+ case strings.HasPrefix(line, "Binary"):
543+ curFile.IsBin = true
544+ continue
545+ }
546+
547+ // Get new file.
548+ if strings.HasPrefix(line, cmdDiffHead) {
549+ var middle int
550+
551+ // Note: In case file name is surrounded by double quotes (it happens only in git-shell).
552+ // e.g. diff --git "a/xxx" "b/xxx"
553+ hasQuote := line[len(cmdDiffHead)] == '"'
554+ if hasQuote {
555+ middle = strings.Index(line, ` "b/`)
556+ } else {
557+ middle = strings.Index(line, " b/")
558+ }
559+
560+ beg := len(cmdDiffHead)
561+ a := line[beg+2 : middle]
562+ b := line[middle+3:]
563+
564+ if hasQuote {
565+ // Keep the entire string in double quotes for now
566+ a = line[beg:middle]
567+ b = line[middle+1:]
568+
569+ var err error
570+ a, err = strconv.Unquote(a)
571+ if err != nil {
572+ return nil, fmt.Errorf("Unquote: %v", err)
573+ }
574+ b, err = strconv.Unquote(b)
575+ if err != nil {
576+ return nil, fmt.Errorf("Unquote: %v", err)
577+ }
578+ // Now remove the /a /b
579+ a = a[2:]
580+ b = b[2:]
581+
582+ }
583+
584+ curFile = &DiffFile{
585+ Name: b,
586+ OldName: a,
587+ Index: len(diff.Files) + 1,
588+ Type: DiffFileChange,
589+ Sections: make([]*DiffSection, 0, 10),
590+ IsRenamed: a != b,
591+ }
592+ diff.Files = append(diff.Files, curFile)
593+ if len(diff.Files) >= maxFiles {
594+ diff.IsIncomplete = true
595+ _, err := io.Copy(ioutil.Discard, reader)
596+ if err != nil {
597+ return nil, fmt.Errorf("Copy: %v", err)
598+ }
599+ break
600+ }
601+ curFileLinesCount = 0
602+ curFileLFSPrefix = false
603+
604+ // Check file diff type and is submodule.
605+ for {
606+ line, err := input.ReadString('\n')
607+ if err != nil {
608+ if err == io.EOF {
609+ isEOF = true
610+ } else {
611+ return nil, fmt.Errorf("ReadString: %v", err)
612+ }
613+ }
614+
615+ switch {
616+ case strings.HasPrefix(line, "new file"):
617+ curFile.Type = DiffFileAdd
618+ curFile.IsCreated = true
619+ case strings.HasPrefix(line, "deleted"):
620+ curFile.Type = DiffFileDel
621+ curFile.IsDeleted = true
622+ case strings.HasPrefix(line, "index"):
623+ curFile.Type = DiffFileChange
624+ case strings.HasPrefix(line, "similarity index 100%"):
625+ curFile.Type = DiffFileRename
626+ }
627+ if curFile.Type > 0 {
628+ if strings.HasSuffix(line, " 160000\n") {
629+ curFile.IsSubmodule = true
630+ }
631+ break
632+ }
633+ }
634+ }
635+ }
636+
637+ // FIXME: detect encoding while parsing.
638+ var buf bytes.Buffer
639+ for _, f := range diff.Files {
640+ buf.Reset()
641+ for _, sec := range f.Sections {
642+ for _, l := range sec.Lines {
643+ buf.WriteString(l.Content)
644+ buf.WriteString("\n")
645+ }
646+ }
647+ charsetLabel, err := charset.DetectEncoding(buf.Bytes())
648+ if charsetLabel != "UTF-8" && err == nil {
649+ encoding, _ := stdcharset.Lookup(charsetLabel)
650+ if encoding != nil {
651+ d := encoding.NewDecoder()
652+ for _, sec := range f.Sections {
653+ for _, l := range sec.Lines {
654+ if c, _, err := transform.String(d, l.Content); err == nil {
655+ l.Content = c
656+ }
657+ }
658+ }
659+ }
660+ }
661+ }
662+
663+ return diff, nil
664+}
665+
666+// GetDiffRange builds a Diff between two commits of a repository.
667+// passing the empty string as beforeCommitID returns a diff from the
668+// parent commit.
669+func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
670+ return GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID, maxLines, maxLineCharacters, maxFiles, "")
671+}
672+
673+// GetDiffRangeWithWhitespaceBehavior builds a Diff between two commits of a repository.
674+// Passing the empty string as beforeCommitID returns a diff from the parent commit.
675+// The whitespaceBehavior is either an empty string or a git flag
676+func GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int, whitespaceBehavior string) (*Diff, error) {
677+ gitRepo, err := git.OpenRepository(repoPath)
678+ if err != nil {
679+ return nil, err
680+ }
681+
682+ commit, err := gitRepo.GetCommit(afterCommitID)
683+ if err != nil {
684+ return nil, err
685+ }
686+
687+ var cmd *exec.Cmd
688+ if len(beforeCommitID) == 0 && commit.ParentCount() == 0 {
689+ cmd = exec.Command(git.GitExecutable, "show", afterCommitID)
690+ } else {
691+ actualBeforeCommitID := beforeCommitID
692+ if len(actualBeforeCommitID) == 0 {
693+ parentCommit, _ := commit.Parent(0)
694+ actualBeforeCommitID = parentCommit.ID.String()
695+ }
696+ diffArgs := []string{"diff", "-M"}
697+ if len(whitespaceBehavior) != 0 {
698+ diffArgs = append(diffArgs, whitespaceBehavior)
699+ }
700+ diffArgs = append(diffArgs, actualBeforeCommitID)
701+ diffArgs = append(diffArgs, afterCommitID)
702+ cmd = exec.Command(git.GitExecutable, diffArgs...)
703+ }
704+ cmd.Dir = repoPath
705+ cmd.Stderr = os.Stderr
706+
707+ stdout, err := cmd.StdoutPipe()
708+ if err != nil {
709+ return nil, fmt.Errorf("StdoutPipe: %v", err)
710+ }
711+
712+ if err = cmd.Start(); err != nil {
713+ return nil, fmt.Errorf("Start: %v", err)
714+ }
715+
716+ pid := process.GetManager().Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cmd)
717+ defer process.GetManager().Remove(pid)
718+
719+ diff, err := ParsePatch(maxLines, maxLineCharacters, maxFiles, stdout)
720+ if err != nil {
721+ return nil, fmt.Errorf("ParsePatch: %v", err)
722+ }
723+
724+ if err = cmd.Wait(); err != nil {
725+ return nil, fmt.Errorf("Wait: %v", err)
726+ }
727+
728+ return diff, nil
729+}
730+
731+// RawDiffType type of a raw diff.
732+type RawDiffType string
733+
734+// RawDiffType possible values.
735+const (
736+ RawDiffNormal RawDiffType = "diff"
737+ RawDiffPatch RawDiffType = "patch"
738+)
739+
740+// GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
741+// TODO: move this function to gogits/git-module
742+func GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Writer) error {
743+ return GetRawDiffForFile(repoPath, "", commitID, diffType, "", writer)
744+}
745+
746+// GetRawDiffForFile dumps diff results of file in given commit ID to io.Writer.
747+// TODO: move this function to gogits/git-module
748+func GetRawDiffForFile(repoPath, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
749+ repo, err := git.OpenRepository(repoPath)
750+ if err != nil {
751+ return fmt.Errorf("OpenRepository: %v", err)
752+ }
753+
754+ commit, err := repo.GetCommit(endCommit)
755+ if err != nil {
756+ return fmt.Errorf("GetCommit: %v", err)
757+ }
758+ fileArgs := make([]string, 0)
759+ if len(file) > 0 {
760+ fileArgs = append(fileArgs, "--", file)
761+ }
762+ var cmd *exec.Cmd
763+ switch diffType {
764+ case RawDiffNormal:
765+ if len(startCommit) != 0 {
766+ cmd = exec.Command(git.GitExecutable, append([]string{"diff", "-M", startCommit, endCommit}, fileArgs...)...)
767+ } else if commit.ParentCount() == 0 {
768+ cmd = exec.Command(git.GitExecutable, append([]string{"show", endCommit}, fileArgs...)...)
769+ } else {
770+ c, _ := commit.Parent(0)
771+ cmd = exec.Command(git.GitExecutable, append([]string{"diff", "-M", c.ID.String(), endCommit}, fileArgs...)...)
772+ }
773+ case RawDiffPatch:
774+ if len(startCommit) != 0 {
775+ query := fmt.Sprintf("%s...%s", endCommit, startCommit)
776+ cmd = exec.Command(git.GitExecutable, append([]string{"format-patch", "--no-signature", "--stdout", "--root", query}, fileArgs...)...)
777+ } else if commit.ParentCount() == 0 {
778+ cmd = exec.Command(git.GitExecutable, append([]string{"format-patch", "--no-signature", "--stdout", "--root", endCommit}, fileArgs...)...)
779+ } else {
780+ c, _ := commit.Parent(0)
781+ query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
782+ cmd = exec.Command(git.GitExecutable, append([]string{"format-patch", "--no-signature", "--stdout", query}, fileArgs...)...)
783+ }
784+ default:
785+ return fmt.Errorf("invalid diffType: %s", diffType)
786+ }
787+
788+ stderr := new(bytes.Buffer)
789+
790+ cmd.Dir = repoPath
791+ cmd.Stdout = writer
792+ cmd.Stderr = stderr
793+ if err = cmd.Run(); err != nil {
794+ return fmt.Errorf("Run: %v - %s", err, stderr)
795+ }
796+ return nil
797+}
798+
799+// GetDiffCommit builds a Diff representing the given commitID.
800+func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
801+ return GetDiffRange(repoPath, "", commitID, maxLines, maxLineCharacters, maxFiles)
802+}
803+
804+// CommentAsDiff returns c.Patch as *Diff
805+func CommentAsDiff(c *models.Comment) (*Diff, error) {
806+ diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
807+ setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch))
808+ if err != nil {
809+ return nil, err
810+ }
811+ if len(diff.Files) == 0 {
812+ return nil, fmt.Errorf("no file found for comment ID: %d", c.ID)
813+ }
814+ secs := diff.Files[0].Sections
815+ if len(secs) == 0 {
816+ return nil, fmt.Errorf("no sections found for comment ID: %d", c.ID)
817+ }
818+ return diff, nil
819+}
820+
821+// CommentMustAsDiff executes AsDiff and logs the error instead of returning
822+func CommentMustAsDiff(c *models.Comment) *Diff {
823+ diff, err := CommentAsDiff(c)
824+ if err != nil {
825+ log.Warn("CommentMustAsDiff: %v", err)
826+ }
827+ return diff
828+}