本站源代码
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.

829 lines
23KB

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