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.

gitdiff.go 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  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. "context"
  10. "fmt"
  11. "html/template"
  12. "io"
  13. "io/ioutil"
  14. "net/url"
  15. "os"
  16. "os/exec"
  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. stdcharset "golang.org/x/net/html/charset"
  29. "golang.org/x/text/transform"
  30. )
  31. // DiffLineType represents the type of a DiffLine.
  32. type DiffLineType uint8
  33. // DiffLineType possible values.
  34. const (
  35. DiffLinePlain DiffLineType = iota + 1
  36. DiffLineAdd
  37. DiffLineDel
  38. DiffLineSection
  39. )
  40. // DiffFileType represents the type of a DiffFile.
  41. type DiffFileType uint8
  42. // DiffFileType possible values.
  43. const (
  44. DiffFileAdd DiffFileType = iota + 1
  45. DiffFileChange
  46. DiffFileDel
  47. DiffFileRename
  48. )
  49. // DiffLineExpandDirection represents the DiffLineSection expand direction
  50. type DiffLineExpandDirection uint8
  51. // DiffLineExpandDirection possible values.
  52. const (
  53. DiffLineExpandNone DiffLineExpandDirection = iota + 1
  54. DiffLineExpandSingle
  55. DiffLineExpandUpDown
  56. DiffLineExpandUp
  57. DiffLineExpandDown
  58. )
  59. // DiffLine represents a line difference in a DiffSection.
  60. type DiffLine struct {
  61. LeftIdx int
  62. RightIdx int
  63. Type DiffLineType
  64. Content string
  65. Comments []*models.Comment
  66. SectionInfo *DiffLineSectionInfo
  67. }
  68. // DiffLineSectionInfo represents diff line section meta data
  69. type DiffLineSectionInfo struct {
  70. Path string
  71. LastLeftIdx int
  72. LastRightIdx int
  73. LeftIdx int
  74. RightIdx int
  75. LeftHunkSize int
  76. RightHunkSize int
  77. }
  78. // BlobExceprtChunkSize represent max lines of excerpt
  79. const BlobExceprtChunkSize = 20
  80. // GetType returns the type of a DiffLine.
  81. func (d *DiffLine) GetType() int {
  82. return int(d.Type)
  83. }
  84. // CanComment returns whether or not a line can get commented
  85. func (d *DiffLine) CanComment() bool {
  86. return len(d.Comments) == 0 && d.Type != DiffLineSection
  87. }
  88. // GetCommentSide returns the comment side of the first comment, if not set returns empty string
  89. func (d *DiffLine) GetCommentSide() string {
  90. if len(d.Comments) == 0 {
  91. return ""
  92. }
  93. return d.Comments[0].DiffSide()
  94. }
  95. // GetLineTypeMarker returns the line type marker
  96. func (d *DiffLine) GetLineTypeMarker() string {
  97. if strings.IndexByte(" +-", d.Content[0]) > -1 {
  98. return d.Content[0:1]
  99. }
  100. return ""
  101. }
  102. // GetBlobExcerptQuery builds query string to get blob excerpt
  103. func (d *DiffLine) GetBlobExcerptQuery() string {
  104. query := fmt.Sprintf(
  105. "last_left=%d&last_right=%d&"+
  106. "left=%d&right=%d&"+
  107. "left_hunk_size=%d&right_hunk_size=%d&"+
  108. "path=%s",
  109. d.SectionInfo.LastLeftIdx, d.SectionInfo.LastRightIdx,
  110. d.SectionInfo.LeftIdx, d.SectionInfo.RightIdx,
  111. d.SectionInfo.LeftHunkSize, d.SectionInfo.RightHunkSize,
  112. url.QueryEscape(d.SectionInfo.Path))
  113. return query
  114. }
  115. // GetExpandDirection gets DiffLineExpandDirection
  116. func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {
  117. if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 {
  118. return DiffLineExpandNone
  119. }
  120. if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 {
  121. return DiffLineExpandUp
  122. } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx > BlobExceprtChunkSize && d.SectionInfo.RightHunkSize > 0 {
  123. return DiffLineExpandUpDown
  124. } else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 {
  125. return DiffLineExpandDown
  126. }
  127. return DiffLineExpandSingle
  128. }
  129. func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
  130. leftLine, leftHunk, rightLine, righHunk := git.ParseDiffHunkString(line)
  131. return &DiffLineSectionInfo{
  132. Path: treePath,
  133. LastLeftIdx: lastLeftIdx,
  134. LastRightIdx: lastRightIdx,
  135. LeftIdx: leftLine,
  136. RightIdx: rightLine,
  137. LeftHunkSize: leftHunk,
  138. RightHunkSize: righHunk,
  139. }
  140. }
  141. // escape a line's content or return <br> needed for copy/paste purposes
  142. func getLineContent(content string) string {
  143. if len(content) > 0 {
  144. return content
  145. }
  146. return "\n"
  147. }
  148. // DiffSection represents a section of a DiffFile.
  149. type DiffSection struct {
  150. FileName string
  151. Name string
  152. Lines []*DiffLine
  153. }
  154. var (
  155. addedCodePrefix = []byte(`<span class="added-code">`)
  156. removedCodePrefix = []byte(`<span class="removed-code">`)
  157. codeTagSuffix = []byte(`</span>`)
  158. )
  159. func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineType) template.HTML {
  160. buf := bytes.NewBuffer(nil)
  161. for i := range diffs {
  162. switch {
  163. case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd:
  164. buf.Write(addedCodePrefix)
  165. buf.WriteString(highlight.Code(fileName, diffs[i].Text))
  166. buf.Write(codeTagSuffix)
  167. case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel:
  168. buf.Write(removedCodePrefix)
  169. buf.WriteString(highlight.Code(fileName, diffs[i].Text))
  170. buf.Write(codeTagSuffix)
  171. case diffs[i].Type == diffmatchpatch.DiffEqual:
  172. buf.WriteString(highlight.Code(fileName, getLineContent(diffs[i].Text)))
  173. }
  174. }
  175. return template.HTML(buf.Bytes())
  176. }
  177. // GetLine gets a specific line by type (add or del) and file line number
  178. func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine {
  179. var (
  180. difference = 0
  181. addCount = 0
  182. delCount = 0
  183. matchDiffLine *DiffLine
  184. )
  185. LOOP:
  186. for _, diffLine := range diffSection.Lines {
  187. switch diffLine.Type {
  188. case DiffLineAdd:
  189. addCount++
  190. case DiffLineDel:
  191. delCount++
  192. default:
  193. if matchDiffLine != nil {
  194. break LOOP
  195. }
  196. difference = diffLine.RightIdx - diffLine.LeftIdx
  197. addCount = 0
  198. delCount = 0
  199. }
  200. switch lineType {
  201. case DiffLineDel:
  202. if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference {
  203. matchDiffLine = diffLine
  204. }
  205. case DiffLineAdd:
  206. if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference {
  207. matchDiffLine = diffLine
  208. }
  209. }
  210. }
  211. if addCount == delCount {
  212. return matchDiffLine
  213. }
  214. return nil
  215. }
  216. var diffMatchPatch = diffmatchpatch.New()
  217. func init() {
  218. diffMatchPatch.DiffEditCost = 100
  219. }
  220. // GetComputedInlineDiffFor computes inline diff for the given line.
  221. func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML {
  222. if setting.Git.DisableDiffHighlight {
  223. return template.HTML(getLineContent(diffLine.Content[1:]))
  224. }
  225. var (
  226. compareDiffLine *DiffLine
  227. diff1 string
  228. diff2 string
  229. )
  230. // try to find equivalent diff line. ignore, otherwise
  231. switch diffLine.Type {
  232. case DiffLineSection:
  233. return template.HTML(getLineContent(diffLine.Content[1:]))
  234. case DiffLineAdd:
  235. compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
  236. if compareDiffLine == nil {
  237. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]+"\n"))
  238. }
  239. diff1 = compareDiffLine.Content
  240. diff2 = diffLine.Content
  241. case DiffLineDel:
  242. compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
  243. if compareDiffLine == nil {
  244. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]+"\n"))
  245. }
  246. diff1 = diffLine.Content
  247. diff2 = compareDiffLine.Content
  248. default:
  249. if strings.IndexByte(" +-", diffLine.Content[0]) > -1 {
  250. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]+"\n"))
  251. }
  252. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content))
  253. }
  254. diffRecord := diffMatchPatch.DiffMain(diff1[1:], diff2[1:], true)
  255. diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
  256. return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type)
  257. }
  258. // DiffFile represents a file diff.
  259. type DiffFile struct {
  260. Name string
  261. OldName string
  262. Index int
  263. Addition, Deletion int
  264. Type DiffFileType
  265. IsCreated bool
  266. IsDeleted bool
  267. IsBin bool
  268. IsLFSFile bool
  269. IsRenamed bool
  270. IsSubmodule bool
  271. Sections []*DiffSection
  272. IsIncomplete bool
  273. }
  274. // GetType returns type of diff file.
  275. func (diffFile *DiffFile) GetType() int {
  276. return int(diffFile.Type)
  277. }
  278. // GetTailSection creates a fake DiffLineSection if the last section is not the end of the file
  279. func (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommitID, rightCommitID string) *DiffSection {
  280. if len(diffFile.Sections) == 0 || diffFile.Type != DiffFileChange || diffFile.IsBin || diffFile.IsLFSFile {
  281. return nil
  282. }
  283. leftCommit, err := gitRepo.GetCommit(leftCommitID)
  284. if err != nil {
  285. return nil
  286. }
  287. rightCommit, err := gitRepo.GetCommit(rightCommitID)
  288. if err != nil {
  289. return nil
  290. }
  291. lastSection := diffFile.Sections[len(diffFile.Sections)-1]
  292. lastLine := lastSection.Lines[len(lastSection.Lines)-1]
  293. leftLineCount := getCommitFileLineCount(leftCommit, diffFile.Name)
  294. rightLineCount := getCommitFileLineCount(rightCommit, diffFile.Name)
  295. if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx {
  296. return nil
  297. }
  298. tailDiffLine := &DiffLine{
  299. Type: DiffLineSection,
  300. Content: " ",
  301. SectionInfo: &DiffLineSectionInfo{
  302. Path: diffFile.Name,
  303. LastLeftIdx: lastLine.LeftIdx,
  304. LastRightIdx: lastLine.RightIdx,
  305. LeftIdx: leftLineCount,
  306. RightIdx: rightLineCount,
  307. }}
  308. tailSection := &DiffSection{FileName: diffFile.Name, Lines: []*DiffLine{tailDiffLine}}
  309. return tailSection
  310. }
  311. func getCommitFileLineCount(commit *git.Commit, filePath string) int {
  312. blob, err := commit.GetBlobByPath(filePath)
  313. if err != nil {
  314. return 0
  315. }
  316. lineCount, err := blob.GetBlobLineCount()
  317. if err != nil {
  318. return 0
  319. }
  320. return lineCount
  321. }
  322. // Diff represents a difference between two git trees.
  323. type Diff struct {
  324. NumFiles, TotalAddition, TotalDeletion int
  325. Files []*DiffFile
  326. IsIncomplete bool
  327. }
  328. // LoadComments loads comments into each line
  329. func (diff *Diff) LoadComments(issue *models.Issue, currentUser *models.User) error {
  330. allComments, err := models.FetchCodeComments(issue, currentUser)
  331. if err != nil {
  332. return err
  333. }
  334. for _, file := range diff.Files {
  335. if lineCommits, ok := allComments[file.Name]; ok {
  336. for _, section := range file.Sections {
  337. for _, line := range section.Lines {
  338. if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {
  339. line.Comments = append(line.Comments, comments...)
  340. }
  341. if comments, ok := lineCommits[int64(line.RightIdx)]; ok {
  342. line.Comments = append(line.Comments, comments...)
  343. }
  344. sort.SliceStable(line.Comments, func(i, j int) bool {
  345. return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
  346. })
  347. }
  348. }
  349. }
  350. }
  351. return nil
  352. }
  353. const cmdDiffHead = "diff --git "
  354. // ParsePatch builds a Diff object from a io.Reader and some
  355. // parameters.
  356. // TODO: move this function to gogits/git-module
  357. func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*Diff, error) {
  358. var (
  359. diff = &Diff{Files: make([]*DiffFile, 0)}
  360. curFile = &DiffFile{}
  361. curSection = &DiffSection{
  362. Lines: make([]*DiffLine, 0, 10),
  363. }
  364. leftLine, rightLine int
  365. lineCount int
  366. curFileLinesCount int
  367. curFileLFSPrefix bool
  368. )
  369. input := bufio.NewReader(reader)
  370. isEOF := false
  371. for !isEOF {
  372. var linebuf bytes.Buffer
  373. for {
  374. b, err := input.ReadByte()
  375. if err != nil {
  376. if err == io.EOF {
  377. isEOF = true
  378. break
  379. } else {
  380. return nil, fmt.Errorf("ReadByte: %v", err)
  381. }
  382. }
  383. if b == '\n' {
  384. break
  385. }
  386. if linebuf.Len() < maxLineCharacters {
  387. linebuf.WriteByte(b)
  388. } else if linebuf.Len() == maxLineCharacters {
  389. curFile.IsIncomplete = true
  390. }
  391. }
  392. line := linebuf.String()
  393. if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") || len(line) == 0 {
  394. continue
  395. }
  396. trimLine := strings.Trim(line, "+- ")
  397. if trimLine == models.LFSMetaFileIdentifier {
  398. curFileLFSPrefix = true
  399. }
  400. if curFileLFSPrefix && strings.HasPrefix(trimLine, models.LFSMetaFileOidPrefix) {
  401. oid := strings.TrimPrefix(trimLine, models.LFSMetaFileOidPrefix)
  402. if len(oid) == 64 {
  403. m := &models.LFSMetaObject{Oid: oid}
  404. count, err := models.Count(m)
  405. if err == nil && count > 0 {
  406. curFile.IsBin = true
  407. curFile.IsLFSFile = true
  408. curSection.Lines = nil
  409. }
  410. }
  411. }
  412. curFileLinesCount++
  413. lineCount++
  414. // Diff data too large, we only show the first about maxLines lines
  415. if curFileLinesCount >= maxLines {
  416. curFile.IsIncomplete = true
  417. }
  418. switch {
  419. case line[0] == ' ':
  420. diffLine := &DiffLine{Type: DiffLinePlain, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
  421. leftLine++
  422. rightLine++
  423. curSection.Lines = append(curSection.Lines, diffLine)
  424. curSection.FileName = curFile.Name
  425. continue
  426. case line[0] == '@':
  427. curSection = &DiffSection{}
  428. curFile.Sections = append(curFile.Sections, curSection)
  429. lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1)
  430. diffLine := &DiffLine{
  431. Type: DiffLineSection,
  432. Content: line,
  433. SectionInfo: lineSectionInfo,
  434. }
  435. curSection.Lines = append(curSection.Lines, diffLine)
  436. curSection.FileName = curFile.Name
  437. // update line number.
  438. leftLine = lineSectionInfo.LeftIdx
  439. rightLine = lineSectionInfo.RightIdx
  440. continue
  441. case line[0] == '+':
  442. curFile.Addition++
  443. diff.TotalAddition++
  444. diffLine := &DiffLine{Type: DiffLineAdd, Content: line, RightIdx: rightLine}
  445. rightLine++
  446. curSection.Lines = append(curSection.Lines, diffLine)
  447. curSection.FileName = curFile.Name
  448. continue
  449. case line[0] == '-':
  450. curFile.Deletion++
  451. diff.TotalDeletion++
  452. diffLine := &DiffLine{Type: DiffLineDel, Content: line, LeftIdx: leftLine}
  453. if leftLine > 0 {
  454. leftLine++
  455. }
  456. curSection.Lines = append(curSection.Lines, diffLine)
  457. curSection.FileName = curFile.Name
  458. case strings.HasPrefix(line, "Binary"):
  459. curFile.IsBin = true
  460. continue
  461. }
  462. // Get new file.
  463. if strings.HasPrefix(line, cmdDiffHead) {
  464. if len(diff.Files) >= maxFiles {
  465. diff.IsIncomplete = true
  466. _, err := io.Copy(ioutil.Discard, reader)
  467. if err != nil {
  468. return nil, fmt.Errorf("Copy: %v", err)
  469. }
  470. break
  471. }
  472. var middle int
  473. // Note: In case file name is surrounded by double quotes (it happens only in git-shell).
  474. // e.g. diff --git "a/xxx" "b/xxx"
  475. hasQuote := line[len(cmdDiffHead)] == '"'
  476. if hasQuote {
  477. middle = strings.Index(line, ` "b/`)
  478. } else {
  479. middle = strings.Index(line, " b/")
  480. }
  481. beg := len(cmdDiffHead)
  482. a := line[beg+2 : middle]
  483. b := line[middle+3:]
  484. if hasQuote {
  485. // Keep the entire string in double quotes for now
  486. a = line[beg:middle]
  487. b = line[middle+1:]
  488. var err error
  489. a, err = strconv.Unquote(a)
  490. if err != nil {
  491. return nil, fmt.Errorf("Unquote: %v", err)
  492. }
  493. b, err = strconv.Unquote(b)
  494. if err != nil {
  495. return nil, fmt.Errorf("Unquote: %v", err)
  496. }
  497. // Now remove the /a /b
  498. a = a[2:]
  499. b = b[2:]
  500. }
  501. curFile = &DiffFile{
  502. Name: b,
  503. OldName: a,
  504. Index: len(diff.Files) + 1,
  505. Type: DiffFileChange,
  506. Sections: make([]*DiffSection, 0, 10),
  507. IsRenamed: a != b,
  508. }
  509. diff.Files = append(diff.Files, curFile)
  510. curFileLinesCount = 0
  511. leftLine = 1
  512. rightLine = 1
  513. curFileLFSPrefix = false
  514. // Check file diff type and is submodule.
  515. for {
  516. line, err := input.ReadString('\n')
  517. if err != nil {
  518. if err == io.EOF {
  519. isEOF = true
  520. } else {
  521. return nil, fmt.Errorf("ReadString: %v", err)
  522. }
  523. }
  524. switch {
  525. case strings.HasPrefix(line, "new file"):
  526. curFile.Type = DiffFileAdd
  527. curFile.IsCreated = true
  528. case strings.HasPrefix(line, "deleted"):
  529. curFile.Type = DiffFileDel
  530. curFile.IsDeleted = true
  531. case strings.HasPrefix(line, "index"):
  532. curFile.Type = DiffFileChange
  533. case strings.HasPrefix(line, "similarity index 100%"):
  534. curFile.Type = DiffFileRename
  535. }
  536. if curFile.Type > 0 {
  537. if strings.HasSuffix(line, " 160000\n") {
  538. curFile.IsSubmodule = true
  539. }
  540. break
  541. }
  542. }
  543. }
  544. }
  545. // FIXME: detect encoding while parsing.
  546. var buf bytes.Buffer
  547. for _, f := range diff.Files {
  548. buf.Reset()
  549. for _, sec := range f.Sections {
  550. for _, l := range sec.Lines {
  551. buf.WriteString(l.Content)
  552. buf.WriteString("\n")
  553. }
  554. }
  555. charsetLabel, err := charset.DetectEncoding(buf.Bytes())
  556. if charsetLabel != "UTF-8" && err == nil {
  557. encoding, _ := stdcharset.Lookup(charsetLabel)
  558. if encoding != nil {
  559. d := encoding.NewDecoder()
  560. for _, sec := range f.Sections {
  561. for _, l := range sec.Lines {
  562. if c, _, err := transform.String(d, l.Content); err == nil {
  563. l.Content = c
  564. }
  565. }
  566. }
  567. }
  568. }
  569. }
  570. diff.NumFiles = len(diff.Files)
  571. return diff, nil
  572. }
  573. // GetDiffRange builds a Diff between two commits of a repository.
  574. // passing the empty string as beforeCommitID returns a diff from the
  575. // parent commit.
  576. func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
  577. return GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID, maxLines, maxLineCharacters, maxFiles, "")
  578. }
  579. // GetDiffRangeWithWhitespaceBehavior builds a Diff between two commits of a repository.
  580. // Passing the empty string as beforeCommitID returns a diff from the parent commit.
  581. // The whitespaceBehavior is either an empty string or a git flag
  582. func GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int, whitespaceBehavior string) (*Diff, error) {
  583. gitRepo, err := git.OpenRepository(repoPath)
  584. if err != nil {
  585. return nil, err
  586. }
  587. defer gitRepo.Close()
  588. commit, err := gitRepo.GetCommit(afterCommitID)
  589. if err != nil {
  590. return nil, err
  591. }
  592. // FIXME: graceful: These commands should likely have a timeout
  593. ctx, cancel := context.WithCancel(git.DefaultContext)
  594. defer cancel()
  595. var cmd *exec.Cmd
  596. if (len(beforeCommitID) == 0 || beforeCommitID == git.EmptySHA) && commit.ParentCount() == 0 {
  597. cmd = exec.CommandContext(ctx, git.GitExecutable, "show", afterCommitID)
  598. } else {
  599. actualBeforeCommitID := beforeCommitID
  600. if len(actualBeforeCommitID) == 0 {
  601. parentCommit, _ := commit.Parent(0)
  602. actualBeforeCommitID = parentCommit.ID.String()
  603. }
  604. diffArgs := []string{"diff", "-M"}
  605. if len(whitespaceBehavior) != 0 {
  606. diffArgs = append(diffArgs, whitespaceBehavior)
  607. }
  608. diffArgs = append(diffArgs, actualBeforeCommitID)
  609. diffArgs = append(diffArgs, afterCommitID)
  610. cmd = exec.CommandContext(ctx, git.GitExecutable, diffArgs...)
  611. beforeCommitID = actualBeforeCommitID
  612. }
  613. cmd.Dir = repoPath
  614. cmd.Stderr = os.Stderr
  615. stdout, err := cmd.StdoutPipe()
  616. if err != nil {
  617. return nil, fmt.Errorf("StdoutPipe: %v", err)
  618. }
  619. if err = cmd.Start(); err != nil {
  620. return nil, fmt.Errorf("Start: %v", err)
  621. }
  622. pid := process.GetManager().Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cancel)
  623. defer process.GetManager().Remove(pid)
  624. diff, err := ParsePatch(maxLines, maxLineCharacters, maxFiles, stdout)
  625. if err != nil {
  626. return nil, fmt.Errorf("ParsePatch: %v", err)
  627. }
  628. for _, diffFile := range diff.Files {
  629. tailSection := diffFile.GetTailSection(gitRepo, beforeCommitID, afterCommitID)
  630. if tailSection != nil {
  631. diffFile.Sections = append(diffFile.Sections, tailSection)
  632. }
  633. }
  634. if err = cmd.Wait(); err != nil {
  635. return nil, fmt.Errorf("Wait: %v", err)
  636. }
  637. shortstatArgs := []string{beforeCommitID + "..." + afterCommitID}
  638. if len(beforeCommitID) == 0 || beforeCommitID == git.EmptySHA {
  639. shortstatArgs = []string{git.EmptyTreeSHA, afterCommitID}
  640. }
  641. diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(repoPath, shortstatArgs...)
  642. if err != nil {
  643. return nil, err
  644. }
  645. return diff, nil
  646. }
  647. // GetDiffCommit builds a Diff representing the given commitID.
  648. func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
  649. return GetDiffRange(repoPath, "", commitID, maxLines, maxLineCharacters, maxFiles)
  650. }
  651. // CommentAsDiff returns c.Patch as *Diff
  652. func CommentAsDiff(c *models.Comment) (*Diff, error) {
  653. diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
  654. setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch))
  655. if err != nil {
  656. return nil, err
  657. }
  658. if len(diff.Files) == 0 {
  659. return nil, fmt.Errorf("no file found for comment ID: %d", c.ID)
  660. }
  661. secs := diff.Files[0].Sections
  662. if len(secs) == 0 {
  663. return nil, fmt.Errorf("no sections found for comment ID: %d", c.ID)
  664. }
  665. return diff, nil
  666. }
  667. // CommentMustAsDiff executes AsDiff and logs the error instead of returning
  668. func CommentMustAsDiff(c *models.Comment) *Diff {
  669. diff, err := CommentAsDiff(c)
  670. if err != nil {
  671. log.Warn("CommentMustAsDiff: %v", err)
  672. }
  673. return diff
  674. }