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 21KB

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