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

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