Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

git_diff.go 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  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/git"
  20. "code.gitea.io/gitea/modules/base"
  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. for idx, lof := range hunk[:headerLines] {
  351. newHunk[idx] = lof
  352. }
  353. // transfer last n lines
  354. for _, lof := range hunk[len(hunk)-numbersOfLine-1:] {
  355. newHunk = append(newHunk, lof)
  356. }
  357. // calculate newBegin, ... by counting lines
  358. for i := len(hunk) - 1; i >= len(hunk)-numbersOfLine; i-- {
  359. switch hunk[i][0] {
  360. case '+':
  361. newBegin--
  362. newNumOfLines++
  363. case '-':
  364. oldBegin--
  365. oldNumOfLines++
  366. default:
  367. oldBegin--
  368. newBegin--
  369. newNumOfLines++
  370. oldNumOfLines++
  371. }
  372. }
  373. // construct the new hunk header
  374. newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
  375. oldBegin, oldNumOfLines, newBegin, newNumOfLines)
  376. return strings.Join(newHunk, "\n")
  377. }
  378. const cmdDiffHead = "diff --git "
  379. // ParsePatch builds a Diff object from a io.Reader and some
  380. // parameters.
  381. // TODO: move this function to gogits/git-module
  382. func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*Diff, error) {
  383. var (
  384. diff = &Diff{Files: make([]*DiffFile, 0)}
  385. curFile = &DiffFile{}
  386. curSection = &DiffSection{
  387. Lines: make([]*DiffLine, 0, 10),
  388. }
  389. leftLine, rightLine int
  390. lineCount int
  391. curFileLinesCount int
  392. curFileLFSPrefix bool
  393. )
  394. input := bufio.NewReader(reader)
  395. isEOF := false
  396. for !isEOF {
  397. var linebuf bytes.Buffer
  398. for {
  399. b, err := input.ReadByte()
  400. if err != nil {
  401. if err == io.EOF {
  402. isEOF = true
  403. break
  404. } else {
  405. return nil, fmt.Errorf("ReadByte: %v", err)
  406. }
  407. }
  408. if b == '\n' {
  409. break
  410. }
  411. if linebuf.Len() < maxLineCharacters {
  412. linebuf.WriteByte(b)
  413. } else if linebuf.Len() == maxLineCharacters {
  414. curFile.IsIncomplete = true
  415. }
  416. }
  417. line := linebuf.String()
  418. if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") || len(line) == 0 {
  419. continue
  420. }
  421. trimLine := strings.Trim(line, "+- ")
  422. if trimLine == LFSMetaFileIdentifier {
  423. curFileLFSPrefix = true
  424. }
  425. if curFileLFSPrefix && strings.HasPrefix(trimLine, LFSMetaFileOidPrefix) {
  426. oid := strings.TrimPrefix(trimLine, LFSMetaFileOidPrefix)
  427. if len(oid) == 64 {
  428. m := &LFSMetaObject{Oid: oid}
  429. count, err := x.Count(m)
  430. if err == nil && count > 0 {
  431. curFile.IsBin = true
  432. curFile.IsLFSFile = true
  433. curSection.Lines = nil
  434. }
  435. }
  436. }
  437. curFileLinesCount++
  438. lineCount++
  439. // Diff data too large, we only show the first about maxLines lines
  440. if curFileLinesCount >= maxLines {
  441. curFile.IsIncomplete = true
  442. }
  443. switch {
  444. case line[0] == ' ':
  445. diffLine := &DiffLine{Type: DiffLinePlain, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
  446. leftLine++
  447. rightLine++
  448. curSection.Lines = append(curSection.Lines, diffLine)
  449. continue
  450. case line[0] == '@':
  451. curSection = &DiffSection{}
  452. curFile.Sections = append(curFile.Sections, curSection)
  453. ss := strings.Split(line, "@@")
  454. diffLine := &DiffLine{Type: DiffLineSection, Content: line}
  455. curSection.Lines = append(curSection.Lines, diffLine)
  456. // Parse line number.
  457. ranges := strings.Split(ss[1][1:], " ")
  458. leftLine, _ = com.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int()
  459. if len(ranges) > 1 {
  460. rightLine, _ = com.StrTo(strings.Split(ranges[1], ",")[0]).Int()
  461. } else {
  462. log.Warn("Parse line number failed: %v", line)
  463. rightLine = leftLine
  464. }
  465. continue
  466. case line[0] == '+':
  467. curFile.Addition++
  468. diff.TotalAddition++
  469. diffLine := &DiffLine{Type: DiffLineAdd, Content: line, RightIdx: rightLine}
  470. rightLine++
  471. curSection.Lines = append(curSection.Lines, diffLine)
  472. continue
  473. case line[0] == '-':
  474. curFile.Deletion++
  475. diff.TotalDeletion++
  476. diffLine := &DiffLine{Type: DiffLineDel, Content: line, LeftIdx: leftLine}
  477. if leftLine > 0 {
  478. leftLine++
  479. }
  480. curSection.Lines = append(curSection.Lines, diffLine)
  481. case strings.HasPrefix(line, "Binary"):
  482. curFile.IsBin = true
  483. continue
  484. }
  485. // Get new file.
  486. if strings.HasPrefix(line, cmdDiffHead) {
  487. middle := -1
  488. // Note: In case file name is surrounded by double quotes (it happens only in git-shell).
  489. // e.g. diff --git "a/xxx" "b/xxx"
  490. hasQuote := line[len(cmdDiffHead)] == '"'
  491. if hasQuote {
  492. middle = strings.Index(line, ` "b/`)
  493. } else {
  494. middle = strings.Index(line, " b/")
  495. }
  496. beg := len(cmdDiffHead)
  497. a := line[beg+2 : middle]
  498. b := line[middle+3:]
  499. if hasQuote {
  500. var err error
  501. a, err = strconv.Unquote(a)
  502. if err != nil {
  503. return nil, fmt.Errorf("Unquote: %v", err)
  504. }
  505. b, err = strconv.Unquote(b)
  506. if err != nil {
  507. return nil, fmt.Errorf("Unquote: %v", err)
  508. }
  509. }
  510. curFile = &DiffFile{
  511. Name: b,
  512. OldName: a,
  513. Index: len(diff.Files) + 1,
  514. Type: DiffFileChange,
  515. Sections: make([]*DiffSection, 0, 10),
  516. IsRenamed: a != b,
  517. }
  518. diff.Files = append(diff.Files, curFile)
  519. if len(diff.Files) >= maxFiles {
  520. diff.IsIncomplete = true
  521. io.Copy(ioutil.Discard, reader)
  522. break
  523. }
  524. curFileLinesCount = 0
  525. curFileLFSPrefix = false
  526. // Check file diff type and is submodule.
  527. for {
  528. line, err := input.ReadString('\n')
  529. if err != nil {
  530. if err == io.EOF {
  531. isEOF = true
  532. } else {
  533. return nil, fmt.Errorf("ReadString: %v", err)
  534. }
  535. }
  536. switch {
  537. case strings.HasPrefix(line, "new file"):
  538. curFile.Type = DiffFileAdd
  539. curFile.IsCreated = true
  540. case strings.HasPrefix(line, "deleted"):
  541. curFile.Type = DiffFileDel
  542. curFile.IsDeleted = true
  543. case strings.HasPrefix(line, "index"):
  544. curFile.Type = DiffFileChange
  545. case strings.HasPrefix(line, "similarity index 100%"):
  546. curFile.Type = DiffFileRename
  547. }
  548. if curFile.Type > 0 {
  549. if strings.HasSuffix(line, " 160000\n") {
  550. curFile.IsSubmodule = true
  551. }
  552. break
  553. }
  554. }
  555. }
  556. }
  557. // FIXME: detect encoding while parsing.
  558. var buf bytes.Buffer
  559. for _, f := range diff.Files {
  560. buf.Reset()
  561. for _, sec := range f.Sections {
  562. for _, l := range sec.Lines {
  563. buf.WriteString(l.Content)
  564. buf.WriteString("\n")
  565. }
  566. }
  567. charsetLabel, err := base.DetectEncoding(buf.Bytes())
  568. if charsetLabel != "UTF-8" && err == nil {
  569. encoding, _ := charset.Lookup(charsetLabel)
  570. if encoding != nil {
  571. d := encoding.NewDecoder()
  572. for _, sec := range f.Sections {
  573. for _, l := range sec.Lines {
  574. if c, _, err := transform.String(d, l.Content); err == nil {
  575. l.Content = c
  576. }
  577. }
  578. }
  579. }
  580. }
  581. }
  582. return diff, nil
  583. }
  584. // GetDiffRange builds a Diff between two commits of a repository.
  585. // passing the empty string as beforeCommitID returns a diff from the
  586. // parent commit.
  587. func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
  588. return GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID, maxLines, maxLineCharacters, maxFiles, "")
  589. }
  590. // GetDiffRangeWithWhitespaceBehavior builds a Diff between two commits of a repository.
  591. // Passing the empty string as beforeCommitID returns a diff from the parent commit.
  592. // The whitespaceBehavior is either an empty string or a git flag
  593. func GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int, whitespaceBehavior string) (*Diff, error) {
  594. gitRepo, err := git.OpenRepository(repoPath)
  595. if err != nil {
  596. return nil, err
  597. }
  598. commit, err := gitRepo.GetCommit(afterCommitID)
  599. if err != nil {
  600. return nil, err
  601. }
  602. var cmd *exec.Cmd
  603. if len(beforeCommitID) == 0 && commit.ParentCount() == 0 {
  604. cmd = exec.Command("git", "show", afterCommitID)
  605. } else {
  606. actualBeforeCommitID := beforeCommitID
  607. if len(actualBeforeCommitID) == 0 {
  608. parentCommit, _ := commit.Parent(0)
  609. actualBeforeCommitID = parentCommit.ID.String()
  610. }
  611. diffArgs := []string{"diff", "-M"}
  612. if len(whitespaceBehavior) != 0 {
  613. diffArgs = append(diffArgs, whitespaceBehavior)
  614. }
  615. diffArgs = append(diffArgs, actualBeforeCommitID)
  616. diffArgs = append(diffArgs, afterCommitID)
  617. cmd = exec.Command("git", diffArgs...)
  618. }
  619. cmd.Dir = repoPath
  620. cmd.Stderr = os.Stderr
  621. stdout, err := cmd.StdoutPipe()
  622. if err != nil {
  623. return nil, fmt.Errorf("StdoutPipe: %v", err)
  624. }
  625. if err = cmd.Start(); err != nil {
  626. return nil, fmt.Errorf("Start: %v", err)
  627. }
  628. pid := process.GetManager().Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cmd)
  629. defer process.GetManager().Remove(pid)
  630. diff, err := ParsePatch(maxLines, maxLineCharacters, maxFiles, stdout)
  631. if err != nil {
  632. return nil, fmt.Errorf("ParsePatch: %v", err)
  633. }
  634. if err = cmd.Wait(); err != nil {
  635. return nil, fmt.Errorf("Wait: %v", err)
  636. }
  637. return diff, nil
  638. }
  639. // RawDiffType type of a raw diff.
  640. type RawDiffType string
  641. // RawDiffType possible values.
  642. const (
  643. RawDiffNormal RawDiffType = "diff"
  644. RawDiffPatch RawDiffType = "patch"
  645. )
  646. // GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
  647. // TODO: move this function to gogits/git-module
  648. func GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Writer) error {
  649. return GetRawDiffForFile(repoPath, "", commitID, diffType, "", writer)
  650. }
  651. // GetRawDiffForFile dumps diff results of file in given commit ID to io.Writer.
  652. // TODO: move this function to gogits/git-module
  653. func GetRawDiffForFile(repoPath, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
  654. repo, err := git.OpenRepository(repoPath)
  655. if err != nil {
  656. return fmt.Errorf("OpenRepository: %v", err)
  657. }
  658. commit, err := repo.GetCommit(endCommit)
  659. if err != nil {
  660. return fmt.Errorf("GetCommit: %v", err)
  661. }
  662. fileArgs := make([]string, 0)
  663. if len(file) > 0 {
  664. fileArgs = append(fileArgs, "--", file)
  665. }
  666. var cmd *exec.Cmd
  667. switch diffType {
  668. case RawDiffNormal:
  669. if len(startCommit) != 0 {
  670. cmd = exec.Command("git", append([]string{"diff", "-M", startCommit, endCommit}, fileArgs...)...)
  671. } else if commit.ParentCount() == 0 {
  672. cmd = exec.Command("git", append([]string{"show", endCommit}, fileArgs...)...)
  673. } else {
  674. c, _ := commit.Parent(0)
  675. cmd = exec.Command("git", append([]string{"diff", "-M", c.ID.String(), endCommit}, fileArgs...)...)
  676. }
  677. case RawDiffPatch:
  678. if len(startCommit) != 0 {
  679. query := fmt.Sprintf("%s...%s", endCommit, startCommit)
  680. cmd = exec.Command("git", append([]string{"format-patch", "--no-signature", "--stdout", "--root", query}, fileArgs...)...)
  681. } else if commit.ParentCount() == 0 {
  682. cmd = exec.Command("git", append([]string{"format-patch", "--no-signature", "--stdout", "--root", endCommit}, fileArgs...)...)
  683. } else {
  684. c, _ := commit.Parent(0)
  685. query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
  686. cmd = exec.Command("git", append([]string{"format-patch", "--no-signature", "--stdout", query}, fileArgs...)...)
  687. }
  688. default:
  689. return fmt.Errorf("invalid diffType: %s", diffType)
  690. }
  691. stderr := new(bytes.Buffer)
  692. cmd.Dir = repoPath
  693. cmd.Stdout = writer
  694. cmd.Stderr = stderr
  695. if err = cmd.Run(); err != nil {
  696. return fmt.Errorf("Run: %v - %s", err, stderr)
  697. }
  698. return nil
  699. }
  700. // GetDiffCommit builds a Diff representing the given commitID.
  701. func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
  702. return GetDiffRange(repoPath, "", commitID, maxLines, maxLineCharacters, maxFiles)
  703. }