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

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