You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

git_diff.go 22KB

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