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

gitdiff.go 40KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331
  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. "strings"
  21. "code.gitea.io/gitea/models"
  22. "code.gitea.io/gitea/modules/charset"
  23. "code.gitea.io/gitea/modules/git"
  24. "code.gitea.io/gitea/modules/highlight"
  25. "code.gitea.io/gitea/modules/lfs"
  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. stdcharset "golang.org/x/net/html/charset"
  31. "golang.org/x/text/transform"
  32. )
  33. // DiffLineType represents the type of a DiffLine.
  34. type DiffLineType uint8
  35. // DiffLineType possible values.
  36. const (
  37. DiffLinePlain DiffLineType = iota + 1
  38. DiffLineAdd
  39. DiffLineDel
  40. DiffLineSection
  41. )
  42. // DiffFileType represents the type of a DiffFile.
  43. type DiffFileType uint8
  44. // DiffFileType possible values.
  45. const (
  46. DiffFileAdd DiffFileType = iota + 1
  47. DiffFileChange
  48. DiffFileDel
  49. DiffFileRename
  50. DiffFileCopy
  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. // BlobExcerptChunkSize represent max lines of excerpt
  82. const BlobExcerptChunkSize = 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 > BlobExcerptChunkSize && 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(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
  133. leftLine, leftHunk, rightLine, righHunk := git.ParseDiffHunkString(line)
  134. return &DiffLineSectionInfo{
  135. Path: treePath,
  136. LastLeftIdx: lastLeftIdx,
  137. LastRightIdx: lastRightIdx,
  138. LeftIdx: leftLine,
  139. RightIdx: rightLine,
  140. LeftHunkSize: leftHunk,
  141. RightHunkSize: righHunk,
  142. }
  143. }
  144. // escape a line's content or return <br> needed for copy/paste purposes
  145. func getLineContent(content string) string {
  146. if len(content) > 0 {
  147. return html.EscapeString(content)
  148. }
  149. return "<br>"
  150. }
  151. // DiffSection represents a section of a DiffFile.
  152. type DiffSection struct {
  153. FileName string
  154. Name string
  155. Lines []*DiffLine
  156. }
  157. var (
  158. addedCodePrefix = []byte(`<span class="added-code">`)
  159. removedCodePrefix = []byte(`<span class="removed-code">`)
  160. codeTagSuffix = []byte(`</span>`)
  161. )
  162. var unfinishedtagRegex = regexp.MustCompile(`<[^>]*$`)
  163. var trailingSpanRegex = regexp.MustCompile(`<span\s*[[:alpha:]="]*?[>]?$`)
  164. var entityRegex = regexp.MustCompile(`&[#]*?[0-9[:alpha:]]*$`)
  165. // shouldWriteInline represents combinations where we manually write inline changes
  166. func shouldWriteInline(diff diffmatchpatch.Diff, lineType DiffLineType) bool {
  167. if true &&
  168. diff.Type == diffmatchpatch.DiffEqual ||
  169. diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd ||
  170. diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel {
  171. return true
  172. }
  173. return false
  174. }
  175. func fixupBrokenSpans(diffs []diffmatchpatch.Diff) []diffmatchpatch.Diff {
  176. // Create a new array to store our fixed up blocks
  177. fixedup := make([]diffmatchpatch.Diff, 0, len(diffs))
  178. // semantically label some numbers
  179. const insert, delete, equal = 0, 1, 2
  180. // record the positions of the last type of each block in the fixedup blocks
  181. last := []int{-1, -1, -1}
  182. operation := []diffmatchpatch.Operation{diffmatchpatch.DiffInsert, diffmatchpatch.DiffDelete, diffmatchpatch.DiffEqual}
  183. // create a writer for insert and deletes
  184. toWrite := []strings.Builder{
  185. {},
  186. {},
  187. }
  188. // make some flags for insert and delete
  189. unfinishedTag := []bool{false, false}
  190. unfinishedEnt := []bool{false, false}
  191. // store stores the provided text in the writer for the typ
  192. store := func(text string, typ int) {
  193. (&(toWrite[typ])).WriteString(text)
  194. }
  195. // hasStored returns true if there is stored content
  196. hasStored := func(typ int) bool {
  197. return (&toWrite[typ]).Len() > 0
  198. }
  199. // stored will return that content
  200. stored := func(typ int) string {
  201. return (&toWrite[typ]).String()
  202. }
  203. // empty will empty the stored content
  204. empty := func(typ int) {
  205. (&toWrite[typ]).Reset()
  206. }
  207. // pop will remove the stored content appending to a diff block for that typ
  208. pop := func(typ int, fixedup []diffmatchpatch.Diff) []diffmatchpatch.Diff {
  209. if hasStored(typ) {
  210. if last[typ] > last[equal] {
  211. fixedup[last[typ]].Text += stored(typ)
  212. } else {
  213. fixedup = append(fixedup, diffmatchpatch.Diff{
  214. Type: operation[typ],
  215. Text: stored(typ),
  216. })
  217. }
  218. empty(typ)
  219. }
  220. return fixedup
  221. }
  222. // Now we walk the provided diffs and check the type of each block in turn
  223. for _, diff := range diffs {
  224. typ := delete // flag for handling insert or delete typs
  225. switch diff.Type {
  226. case diffmatchpatch.DiffEqual:
  227. // First check if there is anything stored
  228. if hasStored(insert) || hasStored(delete) {
  229. // There are two reasons for storing content:
  230. // 1. Unfinished Entity <- Could be more efficient here by not doing this if we're looking for a tag
  231. if unfinishedEnt[insert] || unfinishedEnt[delete] {
  232. // we look for a ';' to finish an entity
  233. idx := strings.IndexRune(diff.Text, ';')
  234. if idx >= 0 {
  235. // if we find a ';' store the preceding content to both insert and delete
  236. store(diff.Text[:idx+1], insert)
  237. store(diff.Text[:idx+1], delete)
  238. // and remove it from this block
  239. diff.Text = diff.Text[idx+1:]
  240. // reset the ent flags
  241. unfinishedEnt[insert] = false
  242. unfinishedEnt[delete] = false
  243. } else {
  244. // otherwise store it all on insert and delete
  245. store(diff.Text, insert)
  246. store(diff.Text, delete)
  247. // and empty this block
  248. diff.Text = ""
  249. }
  250. }
  251. // 2. Unfinished Tag
  252. if unfinishedTag[insert] || unfinishedTag[delete] {
  253. // we look for a '>' to finish a tag
  254. idx := strings.IndexRune(diff.Text, '>')
  255. if idx >= 0 {
  256. store(diff.Text[:idx+1], insert)
  257. store(diff.Text[:idx+1], delete)
  258. diff.Text = diff.Text[idx+1:]
  259. unfinishedTag[insert] = false
  260. unfinishedTag[delete] = false
  261. } else {
  262. store(diff.Text, insert)
  263. store(diff.Text, delete)
  264. diff.Text = ""
  265. }
  266. }
  267. // If we've completed the required tag/entities
  268. if !(unfinishedTag[insert] || unfinishedTag[delete] || unfinishedEnt[insert] || unfinishedEnt[delete]) {
  269. // pop off the stack
  270. fixedup = pop(insert, fixedup)
  271. fixedup = pop(delete, fixedup)
  272. }
  273. // If that has left this diff block empty then shortcut
  274. if len(diff.Text) == 0 {
  275. continue
  276. }
  277. }
  278. // check if this block ends in an unfinished tag?
  279. idx := unfinishedtagRegex.FindStringIndex(diff.Text)
  280. if idx != nil {
  281. unfinishedTag[insert] = true
  282. unfinishedTag[delete] = true
  283. } else {
  284. // otherwise does it end in an unfinished entity?
  285. idx = entityRegex.FindStringIndex(diff.Text)
  286. if idx != nil {
  287. unfinishedEnt[insert] = true
  288. unfinishedEnt[delete] = true
  289. }
  290. }
  291. // If there is an unfinished component
  292. if idx != nil {
  293. // Store the fragment
  294. store(diff.Text[idx[0]:], insert)
  295. store(diff.Text[idx[0]:], delete)
  296. // and remove it from this block
  297. diff.Text = diff.Text[:idx[0]]
  298. }
  299. // If that hasn't left the block empty
  300. if len(diff.Text) > 0 {
  301. // store the position of the last equal block and store it in our diffs
  302. last[equal] = len(fixedup)
  303. fixedup = append(fixedup, diff)
  304. }
  305. continue
  306. case diffmatchpatch.DiffInsert:
  307. typ = insert
  308. fallthrough
  309. case diffmatchpatch.DiffDelete:
  310. // First check if there is anything stored for this type
  311. if hasStored(typ) {
  312. // if there is prepend it to this block, empty the storage and reset our flags
  313. diff.Text = stored(typ) + diff.Text
  314. empty(typ)
  315. unfinishedEnt[typ] = false
  316. unfinishedTag[typ] = false
  317. }
  318. // check if this block ends in an unfinished tag
  319. idx := unfinishedtagRegex.FindStringIndex(diff.Text)
  320. if idx != nil {
  321. unfinishedTag[typ] = true
  322. } else {
  323. // otherwise does it end in an unfinished entity
  324. idx = entityRegex.FindStringIndex(diff.Text)
  325. if idx != nil {
  326. unfinishedEnt[typ] = true
  327. }
  328. }
  329. // If there is an unfinished component
  330. if idx != nil {
  331. // Store the fragment
  332. store(diff.Text[idx[0]:], typ)
  333. // and remove it from this block
  334. diff.Text = diff.Text[:idx[0]]
  335. }
  336. // If that hasn't left the block empty
  337. if len(diff.Text) > 0 {
  338. // if the last block of this type was after the last equal block
  339. if last[typ] > last[equal] {
  340. // store this blocks content on that block
  341. fixedup[last[typ]].Text += diff.Text
  342. } else {
  343. // otherwise store the position of the last block of this type and store the block
  344. last[typ] = len(fixedup)
  345. fixedup = append(fixedup, diff)
  346. }
  347. }
  348. continue
  349. }
  350. }
  351. // pop off any remaining stored content
  352. fixedup = pop(insert, fixedup)
  353. fixedup = pop(delete, fixedup)
  354. return fixedup
  355. }
  356. func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineType) template.HTML {
  357. buf := bytes.NewBuffer(nil)
  358. match := ""
  359. diffs = fixupBrokenSpans(diffs)
  360. for _, diff := range diffs {
  361. if shouldWriteInline(diff, lineType) {
  362. if len(match) > 0 {
  363. diff.Text = match + diff.Text
  364. match = ""
  365. }
  366. // Chroma HTML syntax highlighting is done before diffing individual lines in order to maintain consistency.
  367. // Since inline changes might split in the middle of a chroma span tag or HTML entity, make we manually put it back together
  368. // before writing so we don't try insert added/removed code spans in the middle of one of those
  369. // and create broken HTML. This is done by moving incomplete HTML forward until it no longer matches our pattern of
  370. // a line ending with an incomplete HTML entity or partial/opening <span>.
  371. // EX:
  372. // diffs[{Type: dmp.DiffDelete, Text: "language</span><span "},
  373. // {Type: dmp.DiffEqual, Text: "c"},
  374. // {Type: dmp.DiffDelete, Text: "lass="p">}]
  375. // After first iteration
  376. // diffs[{Type: dmp.DiffDelete, Text: "language</span>"}, //write out
  377. // {Type: dmp.DiffEqual, Text: "<span c"},
  378. // {Type: dmp.DiffDelete, Text: "lass="p">,</span>}]
  379. // After second iteration
  380. // {Type: dmp.DiffEqual, Text: ""}, // write out
  381. // {Type: dmp.DiffDelete, Text: "<span class="p">,</span>}]
  382. // Final
  383. // {Type: dmp.DiffDelete, Text: "<span class="p">,</span>}]
  384. // end up writing <span class="removed-code"><span class="p">,</span></span>
  385. // Instead of <span class="removed-code">lass="p",</span></span>
  386. m := trailingSpanRegex.FindStringSubmatchIndex(diff.Text)
  387. if m != nil {
  388. match = diff.Text[m[0]:m[1]]
  389. diff.Text = strings.TrimSuffix(diff.Text, match)
  390. }
  391. m = entityRegex.FindStringSubmatchIndex(diff.Text)
  392. if m != nil {
  393. match = diff.Text[m[0]:m[1]]
  394. diff.Text = strings.TrimSuffix(diff.Text, match)
  395. }
  396. // Print an existing closing span first before opening added/remove-code span so it doesn't unintentionally close it
  397. if strings.HasPrefix(diff.Text, "</span>") {
  398. buf.WriteString("</span>")
  399. diff.Text = strings.TrimPrefix(diff.Text, "</span>")
  400. }
  401. // If we weren't able to fix it then this should avoid broken HTML by not inserting more spans below
  402. // The previous/next diff section will contain the rest of the tag that is missing here
  403. if strings.Count(diff.Text, "<") != strings.Count(diff.Text, ">") {
  404. buf.WriteString(diff.Text)
  405. continue
  406. }
  407. }
  408. switch {
  409. case diff.Type == diffmatchpatch.DiffEqual:
  410. buf.WriteString(diff.Text)
  411. case diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd:
  412. buf.Write(addedCodePrefix)
  413. buf.WriteString(diff.Text)
  414. buf.Write(codeTagSuffix)
  415. case diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel:
  416. buf.Write(removedCodePrefix)
  417. buf.WriteString(diff.Text)
  418. buf.Write(codeTagSuffix)
  419. }
  420. }
  421. return template.HTML(buf.Bytes())
  422. }
  423. // GetLine gets a specific line by type (add or del) and file line number
  424. func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine {
  425. var (
  426. difference = 0
  427. addCount = 0
  428. delCount = 0
  429. matchDiffLine *DiffLine
  430. )
  431. LOOP:
  432. for _, diffLine := range diffSection.Lines {
  433. switch diffLine.Type {
  434. case DiffLineAdd:
  435. addCount++
  436. case DiffLineDel:
  437. delCount++
  438. default:
  439. if matchDiffLine != nil {
  440. break LOOP
  441. }
  442. difference = diffLine.RightIdx - diffLine.LeftIdx
  443. addCount = 0
  444. delCount = 0
  445. }
  446. switch lineType {
  447. case DiffLineDel:
  448. if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference {
  449. matchDiffLine = diffLine
  450. }
  451. case DiffLineAdd:
  452. if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference {
  453. matchDiffLine = diffLine
  454. }
  455. }
  456. }
  457. if addCount == delCount {
  458. return matchDiffLine
  459. }
  460. return nil
  461. }
  462. var diffMatchPatch = diffmatchpatch.New()
  463. func init() {
  464. diffMatchPatch.DiffEditCost = 100
  465. }
  466. // GetComputedInlineDiffFor computes inline diff for the given line.
  467. func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML {
  468. if setting.Git.DisableDiffHighlight {
  469. return template.HTML(getLineContent(diffLine.Content[1:]))
  470. }
  471. var (
  472. compareDiffLine *DiffLine
  473. diff1 string
  474. diff2 string
  475. )
  476. // try to find equivalent diff line. ignore, otherwise
  477. switch diffLine.Type {
  478. case DiffLineSection:
  479. return template.HTML(getLineContent(diffLine.Content[1:]))
  480. case DiffLineAdd:
  481. compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
  482. if compareDiffLine == nil {
  483. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
  484. }
  485. diff1 = compareDiffLine.Content
  486. diff2 = diffLine.Content
  487. case DiffLineDel:
  488. compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
  489. if compareDiffLine == nil {
  490. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
  491. }
  492. diff1 = diffLine.Content
  493. diff2 = compareDiffLine.Content
  494. default:
  495. if strings.IndexByte(" +-", diffLine.Content[0]) > -1 {
  496. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
  497. }
  498. return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content))
  499. }
  500. diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, diff1[1:]), highlight.Code(diffSection.FileName, diff2[1:]), true)
  501. diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
  502. return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type)
  503. }
  504. // DiffFile represents a file diff.
  505. type DiffFile struct {
  506. Name string
  507. OldName string
  508. Index int
  509. Addition, Deletion int
  510. Type DiffFileType
  511. IsCreated bool
  512. IsDeleted bool
  513. IsBin bool
  514. IsLFSFile bool
  515. IsRenamed bool
  516. IsAmbiguous bool
  517. IsSubmodule bool
  518. Sections []*DiffSection
  519. IsIncomplete bool
  520. IsProtected bool
  521. }
  522. // GetType returns type of diff file.
  523. func (diffFile *DiffFile) GetType() int {
  524. return int(diffFile.Type)
  525. }
  526. // GetTailSection creates a fake DiffLineSection if the last section is not the end of the file
  527. func (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommitID, rightCommitID string) *DiffSection {
  528. if len(diffFile.Sections) == 0 || diffFile.Type != DiffFileChange || diffFile.IsBin || diffFile.IsLFSFile {
  529. return nil
  530. }
  531. leftCommit, err := gitRepo.GetCommit(leftCommitID)
  532. if err != nil {
  533. return nil
  534. }
  535. rightCommit, err := gitRepo.GetCommit(rightCommitID)
  536. if err != nil {
  537. return nil
  538. }
  539. lastSection := diffFile.Sections[len(diffFile.Sections)-1]
  540. lastLine := lastSection.Lines[len(lastSection.Lines)-1]
  541. leftLineCount := getCommitFileLineCount(leftCommit, diffFile.Name)
  542. rightLineCount := getCommitFileLineCount(rightCommit, diffFile.Name)
  543. if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx {
  544. return nil
  545. }
  546. tailDiffLine := &DiffLine{
  547. Type: DiffLineSection,
  548. Content: " ",
  549. SectionInfo: &DiffLineSectionInfo{
  550. Path: diffFile.Name,
  551. LastLeftIdx: lastLine.LeftIdx,
  552. LastRightIdx: lastLine.RightIdx,
  553. LeftIdx: leftLineCount,
  554. RightIdx: rightLineCount,
  555. }}
  556. tailSection := &DiffSection{FileName: diffFile.Name, Lines: []*DiffLine{tailDiffLine}}
  557. return tailSection
  558. }
  559. func getCommitFileLineCount(commit *git.Commit, filePath string) int {
  560. blob, err := commit.GetBlobByPath(filePath)
  561. if err != nil {
  562. return 0
  563. }
  564. lineCount, err := blob.GetBlobLineCount()
  565. if err != nil {
  566. return 0
  567. }
  568. return lineCount
  569. }
  570. // Diff represents a difference between two git trees.
  571. type Diff struct {
  572. NumFiles, TotalAddition, TotalDeletion int
  573. Files []*DiffFile
  574. IsIncomplete bool
  575. }
  576. // LoadComments loads comments into each line
  577. func (diff *Diff) LoadComments(issue *models.Issue, currentUser *models.User) error {
  578. allComments, err := models.FetchCodeComments(issue, currentUser)
  579. if err != nil {
  580. return err
  581. }
  582. for _, file := range diff.Files {
  583. if lineCommits, ok := allComments[file.Name]; ok {
  584. for _, section := range file.Sections {
  585. for _, line := range section.Lines {
  586. if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {
  587. line.Comments = append(line.Comments, comments...)
  588. }
  589. if comments, ok := lineCommits[int64(line.RightIdx)]; ok {
  590. line.Comments = append(line.Comments, comments...)
  591. }
  592. sort.SliceStable(line.Comments, func(i, j int) bool {
  593. return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
  594. })
  595. }
  596. }
  597. }
  598. }
  599. return nil
  600. }
  601. const cmdDiffHead = "diff --git "
  602. // ParsePatch builds a Diff object from a io.Reader and some parameters.
  603. func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*Diff, error) {
  604. var curFile *DiffFile
  605. diff := &Diff{Files: make([]*DiffFile, 0)}
  606. sb := strings.Builder{}
  607. // OK let's set a reasonable buffer size.
  608. // This should be let's say at least the size of maxLineCharacters or 4096 whichever is larger.
  609. readerSize := maxLineCharacters
  610. if readerSize < 4096 {
  611. readerSize = 4096
  612. }
  613. input := bufio.NewReaderSize(reader, readerSize)
  614. line, err := input.ReadString('\n')
  615. if err != nil {
  616. if err == io.EOF {
  617. return diff, nil
  618. }
  619. return diff, err
  620. }
  621. parsingLoop:
  622. for {
  623. // 1. A patch file always begins with `diff --git ` + `a/path b/path` (possibly quoted)
  624. // if it does not we have bad input!
  625. if !strings.HasPrefix(line, cmdDiffHead) {
  626. return diff, fmt.Errorf("Invalid first file line: %s", line)
  627. }
  628. // TODO: Handle skipping first n files
  629. if len(diff.Files) >= maxFiles {
  630. diff.IsIncomplete = true
  631. _, err := io.Copy(ioutil.Discard, reader)
  632. if err != nil {
  633. // By the definition of io.Copy this never returns io.EOF
  634. return diff, fmt.Errorf("Copy: %v", err)
  635. }
  636. break parsingLoop
  637. }
  638. curFile = createDiffFile(diff, line)
  639. diff.Files = append(diff.Files, curFile)
  640. // 2. It is followed by one or more extended header lines:
  641. //
  642. // old mode <mode>
  643. // new mode <mode>
  644. // deleted file mode <mode>
  645. // new file mode <mode>
  646. // copy from <path>
  647. // copy to <path>
  648. // rename from <path>
  649. // rename to <path>
  650. // similarity index <number>
  651. // dissimilarity index <number>
  652. // index <hash>..<hash> <mode>
  653. //
  654. // * <mode> 6-digit octal numbers including the file type and file permission bits.
  655. // * <path> does not include the a/ and b/ prefixes
  656. // * <number> percentage of unchanged lines for similarity, percentage of changed
  657. // lines dissimilarity as integer rounded down with terminal %. 100% => equal files.
  658. // * The index line includes the blob object names before and after the change.
  659. // The <mode> is included if the file mode does not change; otherwise, separate
  660. // lines indicate the old and the new mode.
  661. // 3. Following this header the "standard unified" diff format header may be encountered: (but not for every case...)
  662. //
  663. // --- a/<path>
  664. // +++ b/<path>
  665. //
  666. // With multiple hunks
  667. //
  668. // @@ <hunk descriptor> @@
  669. // +added line
  670. // -removed line
  671. // unchanged line
  672. //
  673. // 4. Binary files get:
  674. //
  675. // Binary files a/<path> and b/<path> differ
  676. //
  677. // but one of a/<path> and b/<path> could be /dev/null.
  678. curFileLoop:
  679. for {
  680. line, err = input.ReadString('\n')
  681. if err != nil {
  682. if err != io.EOF {
  683. return diff, err
  684. }
  685. break parsingLoop
  686. }
  687. switch {
  688. case strings.HasPrefix(line, cmdDiffHead):
  689. break curFileLoop
  690. case strings.HasPrefix(line, "old mode ") ||
  691. strings.HasPrefix(line, "new mode "):
  692. if strings.HasSuffix(line, " 160000\n") {
  693. curFile.IsSubmodule = true
  694. }
  695. case strings.HasPrefix(line, "rename from "):
  696. curFile.IsRenamed = true
  697. curFile.Type = DiffFileRename
  698. if curFile.IsAmbiguous {
  699. curFile.OldName = line[len("rename from ") : len(line)-1]
  700. }
  701. case strings.HasPrefix(line, "rename to "):
  702. curFile.IsRenamed = true
  703. curFile.Type = DiffFileRename
  704. if curFile.IsAmbiguous {
  705. curFile.Name = line[len("rename to ") : len(line)-1]
  706. curFile.IsAmbiguous = false
  707. }
  708. case strings.HasPrefix(line, "copy from "):
  709. curFile.IsRenamed = true
  710. curFile.Type = DiffFileCopy
  711. if curFile.IsAmbiguous {
  712. curFile.OldName = line[len("copy from ") : len(line)-1]
  713. }
  714. case strings.HasPrefix(line, "copy to "):
  715. curFile.IsRenamed = true
  716. curFile.Type = DiffFileCopy
  717. if curFile.IsAmbiguous {
  718. curFile.Name = line[len("copy to ") : len(line)-1]
  719. curFile.IsAmbiguous = false
  720. }
  721. case strings.HasPrefix(line, "new file"):
  722. curFile.Type = DiffFileAdd
  723. curFile.IsCreated = true
  724. if strings.HasSuffix(line, " 160000\n") {
  725. curFile.IsSubmodule = true
  726. }
  727. case strings.HasPrefix(line, "deleted"):
  728. curFile.Type = DiffFileDel
  729. curFile.IsDeleted = true
  730. if strings.HasSuffix(line, " 160000\n") {
  731. curFile.IsSubmodule = true
  732. }
  733. case strings.HasPrefix(line, "index"):
  734. if strings.HasSuffix(line, " 160000\n") {
  735. curFile.IsSubmodule = true
  736. }
  737. case strings.HasPrefix(line, "similarity index 100%"):
  738. curFile.Type = DiffFileRename
  739. case strings.HasPrefix(line, "Binary"):
  740. curFile.IsBin = true
  741. case strings.HasPrefix(line, "--- "):
  742. // Handle ambiguous filenames
  743. if curFile.IsAmbiguous {
  744. if len(line) > 6 && line[4] == 'a' {
  745. curFile.OldName = line[6 : len(line)-1]
  746. if line[len(line)-2] == '\t' {
  747. curFile.OldName = curFile.OldName[:len(curFile.OldName)-1]
  748. }
  749. } else {
  750. curFile.OldName = ""
  751. }
  752. }
  753. // Otherwise do nothing with this line
  754. case strings.HasPrefix(line, "+++ "):
  755. // Handle ambiguous filenames
  756. if curFile.IsAmbiguous {
  757. if len(line) > 6 && line[4] == 'b' {
  758. curFile.Name = line[6 : len(line)-1]
  759. if line[len(line)-2] == '\t' {
  760. curFile.Name = curFile.Name[:len(curFile.Name)-1]
  761. }
  762. if curFile.OldName == "" {
  763. curFile.OldName = curFile.Name
  764. }
  765. } else {
  766. curFile.Name = curFile.OldName
  767. }
  768. curFile.IsAmbiguous = false
  769. }
  770. // Otherwise do nothing with this line, but now switch to parsing hunks
  771. lineBytes, isFragment, err := parseHunks(curFile, maxLines, maxLineCharacters, input)
  772. diff.TotalAddition += curFile.Addition
  773. diff.TotalDeletion += curFile.Deletion
  774. if err != nil {
  775. if err != io.EOF {
  776. return diff, err
  777. }
  778. break parsingLoop
  779. }
  780. sb.Reset()
  781. _, _ = sb.Write(lineBytes)
  782. for isFragment {
  783. lineBytes, isFragment, err = input.ReadLine()
  784. if err != nil {
  785. // Now by the definition of ReadLine this cannot be io.EOF
  786. return diff, fmt.Errorf("Unable to ReadLine: %v", err)
  787. }
  788. _, _ = sb.Write(lineBytes)
  789. }
  790. line = sb.String()
  791. sb.Reset()
  792. break curFileLoop
  793. }
  794. }
  795. }
  796. // FIXME: There are numerous issues with this:
  797. // - we might want to consider detecting encoding while parsing but...
  798. // - we're likely to fail to get the correct encoding here anyway as we won't have enough information
  799. // - and this doesn't really account for changes in encoding
  800. var buf bytes.Buffer
  801. for _, f := range diff.Files {
  802. buf.Reset()
  803. for _, sec := range f.Sections {
  804. for _, l := range sec.Lines {
  805. if l.Type == DiffLineSection {
  806. continue
  807. }
  808. buf.WriteString(l.Content[1:])
  809. buf.WriteString("\n")
  810. }
  811. }
  812. charsetLabel, err := charset.DetectEncoding(buf.Bytes())
  813. if charsetLabel != "UTF-8" && err == nil {
  814. encoding, _ := stdcharset.Lookup(charsetLabel)
  815. if encoding != nil {
  816. d := encoding.NewDecoder()
  817. for _, sec := range f.Sections {
  818. for _, l := range sec.Lines {
  819. if l.Type == DiffLineSection {
  820. continue
  821. }
  822. if c, _, err := transform.String(d, l.Content[1:]); err == nil {
  823. l.Content = l.Content[0:1] + c
  824. }
  825. }
  826. }
  827. }
  828. }
  829. }
  830. diff.NumFiles = len(diff.Files)
  831. return diff, nil
  832. }
  833. func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio.Reader) (lineBytes []byte, isFragment bool, err error) {
  834. sb := strings.Builder{}
  835. var (
  836. curSection *DiffSection
  837. curFileLinesCount int
  838. curFileLFSPrefix bool
  839. )
  840. leftLine, rightLine := 1, 1
  841. for {
  842. for isFragment {
  843. curFile.IsIncomplete = true
  844. _, isFragment, err = input.ReadLine()
  845. if err != nil {
  846. // Now by the definition of ReadLine this cannot be io.EOF
  847. err = fmt.Errorf("Unable to ReadLine: %v", err)
  848. return
  849. }
  850. }
  851. sb.Reset()
  852. lineBytes, isFragment, err = input.ReadLine()
  853. if err != nil {
  854. if err == io.EOF {
  855. return
  856. }
  857. err = fmt.Errorf("Unable to ReadLine: %v", err)
  858. return
  859. }
  860. if lineBytes[0] == 'd' {
  861. // End of hunks
  862. return
  863. }
  864. switch lineBytes[0] {
  865. case '@':
  866. if curFileLinesCount >= maxLines {
  867. curFile.IsIncomplete = true
  868. continue
  869. }
  870. _, _ = sb.Write(lineBytes)
  871. for isFragment {
  872. // This is very odd indeed - we're in a section header and the line is too long
  873. // This really shouldn't happen...
  874. lineBytes, isFragment, err = input.ReadLine()
  875. if err != nil {
  876. // Now by the definition of ReadLine this cannot be io.EOF
  877. err = fmt.Errorf("Unable to ReadLine: %v", err)
  878. return
  879. }
  880. _, _ = sb.Write(lineBytes)
  881. }
  882. line := sb.String()
  883. // Create a new section to represent this hunk
  884. curSection = &DiffSection{}
  885. curFile.Sections = append(curFile.Sections, curSection)
  886. lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1)
  887. diffLine := &DiffLine{
  888. Type: DiffLineSection,
  889. Content: line,
  890. SectionInfo: lineSectionInfo,
  891. }
  892. curSection.Lines = append(curSection.Lines, diffLine)
  893. curSection.FileName = curFile.Name
  894. // update line number.
  895. leftLine = lineSectionInfo.LeftIdx
  896. rightLine = lineSectionInfo.RightIdx
  897. continue
  898. case '\\':
  899. if curFileLinesCount >= maxLines {
  900. curFile.IsIncomplete = true
  901. continue
  902. }
  903. // This is used only to indicate that the current file does not have a terminal newline
  904. if !bytes.Equal(lineBytes, []byte("\\ No newline at end of file")) {
  905. err = fmt.Errorf("Unexpected line in hunk: %s", string(lineBytes))
  906. return
  907. }
  908. // Technically this should be the end the file!
  909. // FIXME: we should be putting a marker at the end of the file if there is no terminal new line
  910. continue
  911. case '+':
  912. curFileLinesCount++
  913. curFile.Addition++
  914. if curFileLinesCount >= maxLines {
  915. curFile.IsIncomplete = true
  916. continue
  917. }
  918. diffLine := &DiffLine{Type: DiffLineAdd, RightIdx: rightLine}
  919. rightLine++
  920. if curSection == nil {
  921. // Create a new section to represent this hunk
  922. curSection = &DiffSection{}
  923. curFile.Sections = append(curFile.Sections, curSection)
  924. }
  925. curSection.Lines = append(curSection.Lines, diffLine)
  926. case '-':
  927. curFileLinesCount++
  928. curFile.Deletion++
  929. if curFileLinesCount >= maxLines {
  930. curFile.IsIncomplete = true
  931. continue
  932. }
  933. diffLine := &DiffLine{Type: DiffLineDel, LeftIdx: leftLine}
  934. if leftLine > 0 {
  935. leftLine++
  936. }
  937. if curSection == nil {
  938. // Create a new section to represent this hunk
  939. curSection = &DiffSection{}
  940. curFile.Sections = append(curFile.Sections, curSection)
  941. }
  942. curSection.Lines = append(curSection.Lines, diffLine)
  943. case ' ':
  944. curFileLinesCount++
  945. if curFileLinesCount >= maxLines {
  946. curFile.IsIncomplete = true
  947. continue
  948. }
  949. diffLine := &DiffLine{Type: DiffLinePlain, LeftIdx: leftLine, RightIdx: rightLine}
  950. leftLine++
  951. rightLine++
  952. if curSection == nil {
  953. // Create a new section to represent this hunk
  954. curSection = &DiffSection{}
  955. curFile.Sections = append(curFile.Sections, curSection)
  956. }
  957. curSection.Lines = append(curSection.Lines, diffLine)
  958. default:
  959. // This is unexpected
  960. err = fmt.Errorf("Unexpected line in hunk: %s", string(lineBytes))
  961. return
  962. }
  963. line := string(lineBytes)
  964. if isFragment {
  965. curFile.IsIncomplete = true
  966. for isFragment {
  967. lineBytes, isFragment, err = input.ReadLine()
  968. if err != nil {
  969. // Now by the definition of ReadLine this cannot be io.EOF
  970. err = fmt.Errorf("Unable to ReadLine: %v", err)
  971. return
  972. }
  973. }
  974. }
  975. if len(line) > maxLineCharacters {
  976. curFile.IsIncomplete = true
  977. line = line[:maxLineCharacters]
  978. }
  979. curSection.Lines[len(curSection.Lines)-1].Content = line
  980. // handle LFS
  981. if line[1:] == lfs.MetaFileIdentifier {
  982. curFileLFSPrefix = true
  983. } else if curFileLFSPrefix && strings.HasPrefix(line[1:], lfs.MetaFileOidPrefix) {
  984. oid := strings.TrimPrefix(line[1:], lfs.MetaFileOidPrefix)
  985. if len(oid) == 64 {
  986. m := &models.LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}}
  987. count, err := models.Count(m)
  988. if err == nil && count > 0 {
  989. curFile.IsBin = true
  990. curFile.IsLFSFile = true
  991. curSection.Lines = nil
  992. }
  993. }
  994. }
  995. }
  996. }
  997. func createDiffFile(diff *Diff, line string) *DiffFile {
  998. // The a/ and b/ filenames are the same unless rename/copy is involved.
  999. // Especially, even for a creation or a deletion, /dev/null is not used
  1000. // in place of the a/ or b/ filenames.
  1001. //
  1002. // When rename/copy is involved, file1 and file2 show the name of the
  1003. // source file of the rename/copy and the name of the file that rename/copy
  1004. // produces, respectively.
  1005. //
  1006. // Path names are quoted if necessary.
  1007. //
  1008. // This means that you should always be able to determine the file name even when there
  1009. // there is potential ambiguity...
  1010. //
  1011. // but we can be simpler with our heuristics by just forcing git to prefix things nicely
  1012. curFile := &DiffFile{
  1013. Index: len(diff.Files) + 1,
  1014. Type: DiffFileChange,
  1015. Sections: make([]*DiffSection, 0, 10),
  1016. }
  1017. rd := strings.NewReader(line[len(cmdDiffHead):] + " ")
  1018. curFile.Type = DiffFileChange
  1019. oldNameAmbiguity := false
  1020. newNameAmbiguity := false
  1021. curFile.OldName, oldNameAmbiguity = readFileName(rd)
  1022. curFile.Name, newNameAmbiguity = readFileName(rd)
  1023. if oldNameAmbiguity && newNameAmbiguity {
  1024. curFile.IsAmbiguous = true
  1025. // OK we should bet that the oldName and the newName are the same if they can be made to be same
  1026. // So we need to start again ...
  1027. if (len(line)-len(cmdDiffHead)-1)%2 == 0 {
  1028. // diff --git a/b b/b b/b b/b b/b b/b
  1029. //
  1030. midpoint := (len(line) + len(cmdDiffHead) - 1) / 2
  1031. new, old := line[len(cmdDiffHead):midpoint], line[midpoint+1:]
  1032. if len(new) > 2 && len(old) > 2 && new[2:] == old[2:] {
  1033. curFile.OldName = old[2:]
  1034. curFile.Name = old[2:]
  1035. }
  1036. }
  1037. }
  1038. curFile.IsRenamed = curFile.Name != curFile.OldName
  1039. return curFile
  1040. }
  1041. func readFileName(rd *strings.Reader) (string, bool) {
  1042. ambiguity := false
  1043. var name string
  1044. char, _ := rd.ReadByte()
  1045. _ = rd.UnreadByte()
  1046. if char == '"' {
  1047. fmt.Fscanf(rd, "%q ", &name)
  1048. if name[0] == '\\' {
  1049. name = name[1:]
  1050. }
  1051. } else {
  1052. // This technique is potentially ambiguous it may not be possible to uniquely identify the filenames from the diff line alone
  1053. ambiguity = true
  1054. fmt.Fscanf(rd, "%s ", &name)
  1055. char, _ := rd.ReadByte()
  1056. _ = rd.UnreadByte()
  1057. for !(char == 0 || char == '"' || char == 'b') {
  1058. var suffix string
  1059. fmt.Fscanf(rd, "%s ", &suffix)
  1060. name += " " + suffix
  1061. char, _ = rd.ReadByte()
  1062. _ = rd.UnreadByte()
  1063. }
  1064. }
  1065. if len(name) < 2 {
  1066. log.Error("Unable to determine name from reader: %v", rd)
  1067. return "", true
  1068. }
  1069. return name[2:], ambiguity
  1070. }
  1071. // GetDiffRange builds a Diff between two commits of a repository.
  1072. // passing the empty string as beforeCommitID returns a diff from the
  1073. // parent commit.
  1074. func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
  1075. return GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID, maxLines, maxLineCharacters, maxFiles, "")
  1076. }
  1077. // GetDiffRangeWithWhitespaceBehavior builds a Diff between two commits of a repository.
  1078. // Passing the empty string as beforeCommitID returns a diff from the parent commit.
  1079. // The whitespaceBehavior is either an empty string or a git flag
  1080. func GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int, whitespaceBehavior string) (*Diff, error) {
  1081. gitRepo, err := git.OpenRepository(repoPath)
  1082. if err != nil {
  1083. return nil, err
  1084. }
  1085. defer gitRepo.Close()
  1086. commit, err := gitRepo.GetCommit(afterCommitID)
  1087. if err != nil {
  1088. return nil, err
  1089. }
  1090. // FIXME: graceful: These commands should likely have a timeout
  1091. ctx, cancel := context.WithCancel(git.DefaultContext)
  1092. defer cancel()
  1093. var cmd *exec.Cmd
  1094. if (len(beforeCommitID) == 0 || beforeCommitID == git.EmptySHA) && commit.ParentCount() == 0 {
  1095. diffArgs := []string{"diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M"}
  1096. if len(whitespaceBehavior) != 0 {
  1097. diffArgs = append(diffArgs, whitespaceBehavior)
  1098. }
  1099. // append empty tree ref
  1100. diffArgs = append(diffArgs, "4b825dc642cb6eb9a060e54bf8d69288fbee4904")
  1101. diffArgs = append(diffArgs, afterCommitID)
  1102. cmd = exec.CommandContext(ctx, git.GitExecutable, diffArgs...)
  1103. } else {
  1104. actualBeforeCommitID := beforeCommitID
  1105. if len(actualBeforeCommitID) == 0 {
  1106. parentCommit, _ := commit.Parent(0)
  1107. actualBeforeCommitID = parentCommit.ID.String()
  1108. }
  1109. diffArgs := []string{"diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M"}
  1110. if len(whitespaceBehavior) != 0 {
  1111. diffArgs = append(diffArgs, whitespaceBehavior)
  1112. }
  1113. diffArgs = append(diffArgs, actualBeforeCommitID)
  1114. diffArgs = append(diffArgs, afterCommitID)
  1115. cmd = exec.CommandContext(ctx, git.GitExecutable, diffArgs...)
  1116. beforeCommitID = actualBeforeCommitID
  1117. }
  1118. cmd.Dir = repoPath
  1119. cmd.Stderr = os.Stderr
  1120. stdout, err := cmd.StdoutPipe()
  1121. if err != nil {
  1122. return nil, fmt.Errorf("StdoutPipe: %v", err)
  1123. }
  1124. if err = cmd.Start(); err != nil {
  1125. return nil, fmt.Errorf("Start: %v", err)
  1126. }
  1127. pid := process.GetManager().Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cancel)
  1128. defer process.GetManager().Remove(pid)
  1129. diff, err := ParsePatch(maxLines, maxLineCharacters, maxFiles, stdout)
  1130. if err != nil {
  1131. return nil, fmt.Errorf("ParsePatch: %v", err)
  1132. }
  1133. for _, diffFile := range diff.Files {
  1134. tailSection := diffFile.GetTailSection(gitRepo, beforeCommitID, afterCommitID)
  1135. if tailSection != nil {
  1136. diffFile.Sections = append(diffFile.Sections, tailSection)
  1137. }
  1138. }
  1139. if err = cmd.Wait(); err != nil {
  1140. return nil, fmt.Errorf("Wait: %v", err)
  1141. }
  1142. shortstatArgs := []string{beforeCommitID + "..." + afterCommitID}
  1143. if len(beforeCommitID) == 0 || beforeCommitID == git.EmptySHA {
  1144. shortstatArgs = []string{git.EmptyTreeSHA, afterCommitID}
  1145. }
  1146. diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(repoPath, shortstatArgs...)
  1147. if err != nil && strings.Contains(err.Error(), "no merge base") {
  1148. // git >= 2.28 now returns an error if base and head have become unrelated.
  1149. // previously it would return the results of git diff --shortstat base head so let's try that...
  1150. shortstatArgs = []string{beforeCommitID, afterCommitID}
  1151. diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(repoPath, shortstatArgs...)
  1152. }
  1153. if err != nil {
  1154. return nil, err
  1155. }
  1156. return diff, nil
  1157. }
  1158. // GetDiffCommit builds a Diff representing the given commitID.
  1159. func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
  1160. return GetDiffRangeWithWhitespaceBehavior(repoPath, "", commitID, maxLines, maxLineCharacters, maxFiles, "")
  1161. }
  1162. // GetDiffCommitWithWhitespaceBehavior builds a Diff representing the given commitID.
  1163. // The whitespaceBehavior is either an empty string or a git flag
  1164. func GetDiffCommitWithWhitespaceBehavior(repoPath, commitID string, maxLines, maxLineCharacters, maxFiles int, whitespaceBehavior string) (*Diff, error) {
  1165. return GetDiffRangeWithWhitespaceBehavior(repoPath, "", commitID, maxLines, maxLineCharacters, maxFiles, whitespaceBehavior)
  1166. }
  1167. // CommentAsDiff returns c.Patch as *Diff
  1168. func CommentAsDiff(c *models.Comment) (*Diff, error) {
  1169. diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
  1170. setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch))
  1171. if err != nil {
  1172. log.Error("Unable to parse patch: %v", err)
  1173. return nil, err
  1174. }
  1175. if len(diff.Files) == 0 {
  1176. return nil, fmt.Errorf("no file found for comment ID: %d", c.ID)
  1177. }
  1178. secs := diff.Files[0].Sections
  1179. if len(secs) == 0 {
  1180. return nil, fmt.Errorf("no sections found for comment ID: %d", c.ID)
  1181. }
  1182. return diff, nil
  1183. }
  1184. // CommentMustAsDiff executes AsDiff and logs the error instead of returning
  1185. func CommentMustAsDiff(c *models.Comment) *Diff {
  1186. if c == nil {
  1187. return nil
  1188. }
  1189. defer func() {
  1190. if err := recover(); err != nil {
  1191. log.Error("PANIC whilst retrieving diff for comment[%d] Error: %v\nStack: %s", c.ID, err, log.Stack(2))
  1192. }
  1193. }()
  1194. diff, err := CommentAsDiff(c)
  1195. if err != nil {
  1196. log.Warn("CommentMustAsDiff: %v", err)
  1197. }
  1198. return diff
  1199. }
  1200. // GetWhitespaceFlag returns git diff flag for treating whitespaces
  1201. func GetWhitespaceFlag(whiteSpaceBehavior string) string {
  1202. whitespaceFlags := map[string]string{
  1203. "ignore-all": "-w",
  1204. "ignore-change": "-b",
  1205. "ignore-eol": "--ignore-space-at-eol",
  1206. "": ""}
  1207. return whitespaceFlags[whiteSpaceBehavior]
  1208. }