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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  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. var addSpan bool
  162. for i := range diffs {
  163. switch {
  164. case diffs[i].Type == diffmatchpatch.DiffEqual:
  165. // Looking for the case where our 3rd party diff library previously detected a string difference
  166. // in the middle of a span class because we highlight them first. This happens when added/deleted code
  167. // also changes the chroma class name. If found, just move the openining span code forward into the next section
  168. if addSpan {
  169. diffs[i].Text = "<span class=\"" + diffs[i].Text
  170. }
  171. if strings.HasSuffix(diffs[i].Text, "<span class=\"") {
  172. addSpan = true
  173. buf.WriteString(strings.TrimSuffix(diffs[i].Text, "<span class=\""))
  174. } else {
  175. addSpan = false
  176. buf.WriteString(getLineContent(diffs[i].Text))
  177. }
  178. case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd:
  179. if addSpan {
  180. addSpan = false
  181. diffs[i].Text = "<span class=\"" + diffs[i].Text
  182. }
  183. // Print existing closing span first before opening added-code span so it doesn't unintentionally close it
  184. if strings.HasPrefix(diffs[i].Text, "</span>") {
  185. buf.WriteString("</span>")
  186. diffs[i].Text = strings.TrimPrefix(diffs[i].Text, "</span>")
  187. }
  188. if strings.HasSuffix(diffs[i].Text, "<span class=\"") {
  189. addSpan = true
  190. diffs[i].Text = strings.TrimSuffix(diffs[i].Text, "<span class=\"")
  191. }
  192. buf.Write(addedCodePrefix)
  193. buf.WriteString(getLineContent(diffs[i].Text))
  194. buf.Write(codeTagSuffix)
  195. case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel:
  196. if addSpan {
  197. addSpan = false
  198. diffs[i].Text = "<span class=\"" + diffs[i].Text
  199. }
  200. if strings.HasPrefix(diffs[i].Text, "</span>") {
  201. buf.WriteString("</span>")
  202. diffs[i].Text = strings.TrimPrefix(diffs[i].Text, "</span>")
  203. }
  204. if strings.HasSuffix(diffs[i].Text, "<span class=\"") {
  205. addSpan = true
  206. diffs[i].Text = strings.TrimSuffix(diffs[i].Text, "<span class=\"")
  207. }
  208. buf.Write(removedCodePrefix)
  209. buf.WriteString(getLineContent(diffs[i].Text))
  210. buf.Write(codeTagSuffix)
  211. }
  212. }
  213. return template.HTML(buf.Bytes())
  214. }
  215. // GetLine gets a specific line by type (add or del) and file line number
  216. func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine {
  217. var (
  218. difference = 0
  219. addCount = 0
  220. delCount = 0
  221. matchDiffLine *DiffLine
  222. )
  223. LOOP:
  224. for _, diffLine := range diffSection.Lines {
  225. switch diffLine.Type {
  226. case DiffLineAdd:
  227. addCount++
  228. case DiffLineDel:
  229. delCount++
  230. default:
  231. if matchDiffLine != nil {
  232. break LOOP
  233. }
  234. difference = diffLine.RightIdx - diffLine.LeftIdx
  235. addCount = 0
  236. delCount = 0
  237. }
  238. switch lineType {
  239. case DiffLineDel:
  240. if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference {
  241. matchDiffLine = diffLine
  242. }
  243. case DiffLineAdd:
  244. if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference {
  245. matchDiffLine = diffLine
  246. }
  247. }
  248. }
  249. if addCount == delCount {
  250. return matchDiffLine
  251. }
  252. return nil
  253. }
  254. var diffMatchPatch = diffmatchpatch.New()
  255. func init() {
  256. diffMatchPatch.DiffEditCost = 100
  257. }
  258. // GetComputedInlineDiffFor computes inline diff for the given line.
  259. func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML {
  260. if setting.Git.DisableDiffHighlight {
  261. return template.HTML(getLineContent(diffLine.Content[1:]))
  262. }
  263. var (
  264. compareDiffLine *DiffLine
  265. diff1 string
  266. diff2 string
  267. )
  268. // try to find equivalent diff line. ignore, otherwise
  269. switch diffLine.Type {
  270. case DiffLineSection:
  271. return template.HTML(getLineContent(diffLine.Content[1:]))
  272. case DiffLineAdd:
  273. compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
  274. if compareDiffLine == nil {
  275. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
  276. }
  277. diff1 = compareDiffLine.Content
  278. diff2 = diffLine.Content
  279. case DiffLineDel:
  280. compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
  281. if compareDiffLine == nil {
  282. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
  283. }
  284. diff1 = diffLine.Content
  285. diff2 = compareDiffLine.Content
  286. default:
  287. if strings.IndexByte(" +-", diffLine.Content[0]) > -1 {
  288. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
  289. }
  290. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content))
  291. }
  292. diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, diff1[1:]), highlight.Code(diffSection.FileName, diff2[1:]), true)
  293. diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
  294. return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type)
  295. }
  296. // DiffFile represents a file diff.
  297. type DiffFile struct {
  298. Name string
  299. OldName string
  300. Index int
  301. Addition, Deletion int
  302. Type DiffFileType
  303. IsCreated bool
  304. IsDeleted bool
  305. IsBin bool
  306. IsLFSFile bool
  307. IsRenamed bool
  308. IsSubmodule bool
  309. Sections []*DiffSection
  310. IsIncomplete bool
  311. }
  312. // GetType returns type of diff file.
  313. func (diffFile *DiffFile) GetType() int {
  314. return int(diffFile.Type)
  315. }
  316. // GetTailSection creates a fake DiffLineSection if the last section is not the end of the file
  317. func (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommitID, rightCommitID string) *DiffSection {
  318. if len(diffFile.Sections) == 0 || diffFile.Type != DiffFileChange || diffFile.IsBin || diffFile.IsLFSFile {
  319. return nil
  320. }
  321. leftCommit, err := gitRepo.GetCommit(leftCommitID)
  322. if err != nil {
  323. return nil
  324. }
  325. rightCommit, err := gitRepo.GetCommit(rightCommitID)
  326. if err != nil {
  327. return nil
  328. }
  329. lastSection := diffFile.Sections[len(diffFile.Sections)-1]
  330. lastLine := lastSection.Lines[len(lastSection.Lines)-1]
  331. leftLineCount := getCommitFileLineCount(leftCommit, diffFile.Name)
  332. rightLineCount := getCommitFileLineCount(rightCommit, diffFile.Name)
  333. if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx {
  334. return nil
  335. }
  336. tailDiffLine := &DiffLine{
  337. Type: DiffLineSection,
  338. Content: " ",
  339. SectionInfo: &DiffLineSectionInfo{
  340. Path: diffFile.Name,
  341. LastLeftIdx: lastLine.LeftIdx,
  342. LastRightIdx: lastLine.RightIdx,
  343. LeftIdx: leftLineCount,
  344. RightIdx: rightLineCount,
  345. }}
  346. tailSection := &DiffSection{FileName: diffFile.Name, Lines: []*DiffLine{tailDiffLine}}
  347. return tailSection
  348. }
  349. func getCommitFileLineCount(commit *git.Commit, filePath string) int {
  350. blob, err := commit.GetBlobByPath(filePath)
  351. if err != nil {
  352. return 0
  353. }
  354. lineCount, err := blob.GetBlobLineCount()
  355. if err != nil {
  356. return 0
  357. }
  358. return lineCount
  359. }
  360. // Diff represents a difference between two git trees.
  361. type Diff struct {
  362. NumFiles, TotalAddition, TotalDeletion int
  363. Files []*DiffFile
  364. IsIncomplete bool
  365. }
  366. // LoadComments loads comments into each line
  367. func (diff *Diff) LoadComments(issue *models.Issue, currentUser *models.User) error {
  368. allComments, err := models.FetchCodeComments(issue, currentUser)
  369. if err != nil {
  370. return err
  371. }
  372. for _, file := range diff.Files {
  373. if lineCommits, ok := allComments[file.Name]; ok {
  374. for _, section := range file.Sections {
  375. for _, line := range section.Lines {
  376. if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {
  377. line.Comments = append(line.Comments, comments...)
  378. }
  379. if comments, ok := lineCommits[int64(line.RightIdx)]; ok {
  380. line.Comments = append(line.Comments, comments...)
  381. }
  382. sort.SliceStable(line.Comments, func(i, j int) bool {
  383. return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
  384. })
  385. }
  386. }
  387. }
  388. }
  389. return nil
  390. }
  391. const cmdDiffHead = "diff --git "
  392. // ParsePatch builds a Diff object from a io.Reader and some
  393. // parameters.
  394. // TODO: move this function to gogits/git-module
  395. func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*Diff, error) {
  396. var (
  397. diff = &Diff{Files: make([]*DiffFile, 0)}
  398. curFile = &DiffFile{}
  399. curSection = &DiffSection{
  400. Lines: make([]*DiffLine, 0, 10),
  401. }
  402. leftLine, rightLine int
  403. lineCount int
  404. curFileLinesCount int
  405. curFileLFSPrefix bool
  406. )
  407. input := bufio.NewReader(reader)
  408. isEOF := false
  409. for !isEOF {
  410. var linebuf bytes.Buffer
  411. for {
  412. b, err := input.ReadByte()
  413. if err != nil {
  414. if err == io.EOF {
  415. isEOF = true
  416. break
  417. } else {
  418. return nil, fmt.Errorf("ReadByte: %v", err)
  419. }
  420. }
  421. if b == '\n' {
  422. break
  423. }
  424. if linebuf.Len() < maxLineCharacters {
  425. linebuf.WriteByte(b)
  426. } else if linebuf.Len() == maxLineCharacters {
  427. curFile.IsIncomplete = true
  428. }
  429. }
  430. line := linebuf.String()
  431. if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") || len(line) == 0 {
  432. continue
  433. }
  434. trimLine := strings.Trim(line, "+- ")
  435. if trimLine == models.LFSMetaFileIdentifier {
  436. curFileLFSPrefix = true
  437. }
  438. if curFileLFSPrefix && strings.HasPrefix(trimLine, models.LFSMetaFileOidPrefix) {
  439. oid := strings.TrimPrefix(trimLine, models.LFSMetaFileOidPrefix)
  440. if len(oid) == 64 {
  441. m := &models.LFSMetaObject{Oid: oid}
  442. count, err := models.Count(m)
  443. if err == nil && count > 0 {
  444. curFile.IsBin = true
  445. curFile.IsLFSFile = true
  446. curSection.Lines = nil
  447. }
  448. }
  449. }
  450. curFileLinesCount++
  451. lineCount++
  452. // Diff data too large, we only show the first about maxLines lines
  453. if curFileLinesCount >= maxLines {
  454. curFile.IsIncomplete = true
  455. }
  456. switch {
  457. case line[0] == ' ':
  458. diffLine := &DiffLine{Type: DiffLinePlain, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
  459. leftLine++
  460. rightLine++
  461. curSection.Lines = append(curSection.Lines, diffLine)
  462. curSection.FileName = curFile.Name
  463. continue
  464. case line[0] == '@':
  465. curSection = &DiffSection{}
  466. curFile.Sections = append(curFile.Sections, curSection)
  467. lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1)
  468. diffLine := &DiffLine{
  469. Type: DiffLineSection,
  470. Content: line,
  471. SectionInfo: lineSectionInfo,
  472. }
  473. curSection.Lines = append(curSection.Lines, diffLine)
  474. curSection.FileName = curFile.Name
  475. // update line number.
  476. leftLine = lineSectionInfo.LeftIdx
  477. rightLine = lineSectionInfo.RightIdx
  478. continue
  479. case line[0] == '+':
  480. curFile.Addition++
  481. diff.TotalAddition++
  482. diffLine := &DiffLine{Type: DiffLineAdd, Content: line, RightIdx: rightLine}
  483. rightLine++
  484. curSection.Lines = append(curSection.Lines, diffLine)
  485. curSection.FileName = curFile.Name
  486. continue
  487. case line[0] == '-':
  488. curFile.Deletion++
  489. diff.TotalDeletion++
  490. diffLine := &DiffLine{Type: DiffLineDel, Content: line, LeftIdx: leftLine}
  491. if leftLine > 0 {
  492. leftLine++
  493. }
  494. curSection.Lines = append(curSection.Lines, diffLine)
  495. curSection.FileName = curFile.Name
  496. case strings.HasPrefix(line, "Binary"):
  497. curFile.IsBin = true
  498. continue
  499. }
  500. // Get new file.
  501. if strings.HasPrefix(line, cmdDiffHead) {
  502. if len(diff.Files) >= maxFiles {
  503. diff.IsIncomplete = true
  504. _, err := io.Copy(ioutil.Discard, reader)
  505. if err != nil {
  506. return nil, fmt.Errorf("Copy: %v", err)
  507. }
  508. break
  509. }
  510. var middle int
  511. // Note: In case file name is surrounded by double quotes (it happens only in git-shell).
  512. // e.g. diff --git "a/xxx" "b/xxx"
  513. hasQuote := line[len(cmdDiffHead)] == '"'
  514. if hasQuote {
  515. middle = strings.Index(line, ` "b/`)
  516. } else {
  517. middle = strings.Index(line, " b/")
  518. }
  519. beg := len(cmdDiffHead)
  520. a := line[beg+2 : middle]
  521. b := line[middle+3:]
  522. if hasQuote {
  523. // Keep the entire string in double quotes for now
  524. a = line[beg:middle]
  525. b = line[middle+1:]
  526. var err error
  527. a, err = strconv.Unquote(a)
  528. if err != nil {
  529. return nil, fmt.Errorf("Unquote: %v", err)
  530. }
  531. b, err = strconv.Unquote(b)
  532. if err != nil {
  533. return nil, fmt.Errorf("Unquote: %v", err)
  534. }
  535. // Now remove the /a /b
  536. a = a[2:]
  537. b = b[2:]
  538. }
  539. curFile = &DiffFile{
  540. Name: b,
  541. OldName: a,
  542. Index: len(diff.Files) + 1,
  543. Type: DiffFileChange,
  544. Sections: make([]*DiffSection, 0, 10),
  545. IsRenamed: a != b,
  546. }
  547. diff.Files = append(diff.Files, curFile)
  548. curFileLinesCount = 0
  549. leftLine = 1
  550. rightLine = 1
  551. curFileLFSPrefix = false
  552. // Check file diff type and is submodule.
  553. for {
  554. line, err := input.ReadString('\n')
  555. if err != nil {
  556. if err == io.EOF {
  557. isEOF = true
  558. } else {
  559. return nil, fmt.Errorf("ReadString: %v", err)
  560. }
  561. }
  562. switch {
  563. case strings.HasPrefix(line, "new file"):
  564. curFile.Type = DiffFileAdd
  565. curFile.IsCreated = true
  566. case strings.HasPrefix(line, "deleted"):
  567. curFile.Type = DiffFileDel
  568. curFile.IsDeleted = true
  569. case strings.HasPrefix(line, "index"):
  570. curFile.Type = DiffFileChange
  571. case strings.HasPrefix(line, "similarity index 100%"):
  572. curFile.Type = DiffFileRename
  573. }
  574. if curFile.Type > 0 {
  575. if strings.HasSuffix(line, " 160000\n") {
  576. curFile.IsSubmodule = true
  577. }
  578. break
  579. }
  580. }
  581. }
  582. }
  583. // FIXME: detect encoding while parsing.
  584. var buf bytes.Buffer
  585. for _, f := range diff.Files {
  586. buf.Reset()
  587. for _, sec := range f.Sections {
  588. for _, l := range sec.Lines {
  589. buf.WriteString(l.Content)
  590. buf.WriteString("\n")
  591. }
  592. }
  593. charsetLabel, err := charset.DetectEncoding(buf.Bytes())
  594. if charsetLabel != "UTF-8" && err == nil {
  595. encoding, _ := stdcharset.Lookup(charsetLabel)
  596. if encoding != nil {
  597. d := encoding.NewDecoder()
  598. for _, sec := range f.Sections {
  599. for _, l := range sec.Lines {
  600. if c, _, err := transform.String(d, l.Content); err == nil {
  601. l.Content = c
  602. }
  603. }
  604. }
  605. }
  606. }
  607. }
  608. diff.NumFiles = len(diff.Files)
  609. return diff, nil
  610. }
  611. // GetDiffRange builds a Diff between two commits of a repository.
  612. // passing the empty string as beforeCommitID returns a diff from the
  613. // parent commit.
  614. func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
  615. return GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID, maxLines, maxLineCharacters, maxFiles, "")
  616. }
  617. // GetDiffRangeWithWhitespaceBehavior builds a Diff between two commits of a repository.
  618. // Passing the empty string as beforeCommitID returns a diff from the parent commit.
  619. // The whitespaceBehavior is either an empty string or a git flag
  620. func GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int, whitespaceBehavior string) (*Diff, error) {
  621. gitRepo, err := git.OpenRepository(repoPath)
  622. if err != nil {
  623. return nil, err
  624. }
  625. defer gitRepo.Close()
  626. commit, err := gitRepo.GetCommit(afterCommitID)
  627. if err != nil {
  628. return nil, err
  629. }
  630. // FIXME: graceful: These commands should likely have a timeout
  631. ctx, cancel := context.WithCancel(git.DefaultContext)
  632. defer cancel()
  633. var cmd *exec.Cmd
  634. if (len(beforeCommitID) == 0 || beforeCommitID == git.EmptySHA) && commit.ParentCount() == 0 {
  635. cmd = exec.CommandContext(ctx, git.GitExecutable, "show", afterCommitID)
  636. } else {
  637. actualBeforeCommitID := beforeCommitID
  638. if len(actualBeforeCommitID) == 0 {
  639. parentCommit, _ := commit.Parent(0)
  640. actualBeforeCommitID = parentCommit.ID.String()
  641. }
  642. diffArgs := []string{"diff", "-M"}
  643. if len(whitespaceBehavior) != 0 {
  644. diffArgs = append(diffArgs, whitespaceBehavior)
  645. }
  646. diffArgs = append(diffArgs, actualBeforeCommitID)
  647. diffArgs = append(diffArgs, afterCommitID)
  648. cmd = exec.CommandContext(ctx, git.GitExecutable, diffArgs...)
  649. beforeCommitID = actualBeforeCommitID
  650. }
  651. cmd.Dir = repoPath
  652. cmd.Stderr = os.Stderr
  653. stdout, err := cmd.StdoutPipe()
  654. if err != nil {
  655. return nil, fmt.Errorf("StdoutPipe: %v", err)
  656. }
  657. if err = cmd.Start(); err != nil {
  658. return nil, fmt.Errorf("Start: %v", err)
  659. }
  660. pid := process.GetManager().Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cancel)
  661. defer process.GetManager().Remove(pid)
  662. diff, err := ParsePatch(maxLines, maxLineCharacters, maxFiles, stdout)
  663. if err != nil {
  664. return nil, fmt.Errorf("ParsePatch: %v", err)
  665. }
  666. for _, diffFile := range diff.Files {
  667. tailSection := diffFile.GetTailSection(gitRepo, beforeCommitID, afterCommitID)
  668. if tailSection != nil {
  669. diffFile.Sections = append(diffFile.Sections, tailSection)
  670. }
  671. }
  672. if err = cmd.Wait(); err != nil {
  673. return nil, fmt.Errorf("Wait: %v", err)
  674. }
  675. shortstatArgs := []string{beforeCommitID + "..." + afterCommitID}
  676. if len(beforeCommitID) == 0 || beforeCommitID == git.EmptySHA {
  677. shortstatArgs = []string{git.EmptyTreeSHA, afterCommitID}
  678. }
  679. diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(repoPath, shortstatArgs...)
  680. if err != nil {
  681. return nil, err
  682. }
  683. return diff, nil
  684. }
  685. // GetDiffCommit builds a Diff representing the given commitID.
  686. func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
  687. return GetDiffRange(repoPath, "", commitID, maxLines, maxLineCharacters, maxFiles)
  688. }
  689. // CommentAsDiff returns c.Patch as *Diff
  690. func CommentAsDiff(c *models.Comment) (*Diff, error) {
  691. diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
  692. setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch))
  693. if err != nil {
  694. return nil, err
  695. }
  696. if len(diff.Files) == 0 {
  697. return nil, fmt.Errorf("no file found for comment ID: %d", c.ID)
  698. }
  699. secs := diff.Files[0].Sections
  700. if len(secs) == 0 {
  701. return nil, fmt.Errorf("no sections found for comment ID: %d", c.ID)
  702. }
  703. return diff, nil
  704. }
  705. // CommentMustAsDiff executes AsDiff and logs the error instead of returning
  706. func CommentMustAsDiff(c *models.Comment) *Diff {
  707. diff, err := CommentAsDiff(c)
  708. if err != nil {
  709. log.Warn("CommentMustAsDiff: %v", err)
  710. }
  711. return diff
  712. }