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.

git_diff.go 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "bufio"
  7. "bytes"
  8. "fmt"
  9. "html"
  10. "html/template"
  11. "io"
  12. "io/ioutil"
  13. "os"
  14. "os/exec"
  15. "strings"
  16. "code.gitea.io/git"
  17. "code.gitea.io/gitea/modules/base"
  18. "code.gitea.io/gitea/modules/log"
  19. "code.gitea.io/gitea/modules/process"
  20. "code.gitea.io/gitea/modules/setting"
  21. "code.gitea.io/gitea/modules/template/highlight"
  22. "github.com/Unknwon/com"
  23. "github.com/sergi/go-diff/diffmatchpatch"
  24. "golang.org/x/net/html/charset"
  25. "golang.org/x/text/transform"
  26. )
  27. // DiffLineType ...
  28. type DiffLineType uint8
  29. // DiffLineType possible values.
  30. const (
  31. DiffLinePlain DiffLineType = iota + 1
  32. DiffLineAdd
  33. DiffLineDel
  34. DiffLineSection
  35. )
  36. // DiffFileType ...
  37. type DiffFileType uint8
  38. // DiffFileType possible values.
  39. const (
  40. DiffFileAdd DiffFileType = iota + 1
  41. DiffFileChange
  42. DiffFileDel
  43. DiffFileRename
  44. )
  45. // DiffLine ...
  46. type DiffLine struct {
  47. LeftIdx int
  48. RightIdx int
  49. Type DiffLineType
  50. Content string
  51. }
  52. // GetType ...
  53. func (d *DiffLine) GetType() int {
  54. return int(d.Type)
  55. }
  56. // DiffSection ...
  57. type DiffSection struct {
  58. Name string
  59. Lines []*DiffLine
  60. }
  61. var (
  62. addedCodePrefix = []byte("<span class=\"added-code\">")
  63. removedCodePrefix = []byte("<span class=\"removed-code\">")
  64. codeTagSuffix = []byte("</span>")
  65. )
  66. // diffToHTML ...
  67. func diffToHTML(diffs []diffmatchpatch.Diff, lineType DiffLineType) template.HTML {
  68. buf := bytes.NewBuffer(nil)
  69. // Reproduce signs which are cutted for inline diff before.
  70. switch lineType {
  71. case DiffLineAdd:
  72. buf.WriteByte('+')
  73. case DiffLineDel:
  74. buf.WriteByte('-')
  75. }
  76. for i := range diffs {
  77. switch {
  78. case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd:
  79. buf.Write(addedCodePrefix)
  80. buf.WriteString(html.EscapeString(diffs[i].Text))
  81. buf.Write(codeTagSuffix)
  82. case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel:
  83. buf.Write(removedCodePrefix)
  84. buf.WriteString(html.EscapeString(diffs[i].Text))
  85. buf.Write(codeTagSuffix)
  86. case diffs[i].Type == diffmatchpatch.DiffEqual:
  87. buf.WriteString(html.EscapeString(diffs[i].Text))
  88. }
  89. }
  90. return template.HTML(buf.Bytes())
  91. }
  92. // GetLine gets a specific line by type (add or del) and file line number
  93. func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine {
  94. var (
  95. difference = 0
  96. addCount = 0
  97. delCount = 0
  98. matchDiffLine *DiffLine
  99. )
  100. LOOP:
  101. for _, diffLine := range diffSection.Lines {
  102. switch diffLine.Type {
  103. case DiffLineAdd:
  104. addCount++
  105. case DiffLineDel:
  106. delCount++
  107. default:
  108. if matchDiffLine != nil {
  109. break LOOP
  110. }
  111. difference = diffLine.RightIdx - diffLine.LeftIdx
  112. addCount = 0
  113. delCount = 0
  114. }
  115. switch lineType {
  116. case DiffLineDel:
  117. if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference {
  118. matchDiffLine = diffLine
  119. }
  120. case DiffLineAdd:
  121. if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference {
  122. matchDiffLine = diffLine
  123. }
  124. }
  125. }
  126. if addCount == delCount {
  127. return matchDiffLine
  128. }
  129. return nil
  130. }
  131. var diffMatchPatch = diffmatchpatch.New()
  132. // init ...
  133. func init() {
  134. diffMatchPatch.DiffEditCost = 100
  135. }
  136. // GetComputedInlineDiffFor computes inline diff for the given line.
  137. func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML {
  138. if setting.Git.DisableDiffHighlight {
  139. return template.HTML(html.EscapeString(diffLine.Content[1:]))
  140. }
  141. var (
  142. compareDiffLine *DiffLine
  143. diff1 string
  144. diff2 string
  145. )
  146. // try to find equivalent diff line. ignore, otherwise
  147. switch diffLine.Type {
  148. case DiffLineAdd:
  149. compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
  150. if compareDiffLine == nil {
  151. return template.HTML(html.EscapeString(diffLine.Content))
  152. }
  153. diff1 = compareDiffLine.Content
  154. diff2 = diffLine.Content
  155. case DiffLineDel:
  156. compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
  157. if compareDiffLine == nil {
  158. return template.HTML(html.EscapeString(diffLine.Content))
  159. }
  160. diff1 = diffLine.Content
  161. diff2 = compareDiffLine.Content
  162. default:
  163. return template.HTML(html.EscapeString(diffLine.Content))
  164. }
  165. diffRecord := diffMatchPatch.DiffMain(diff1[1:], diff2[1:], true)
  166. diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
  167. return diffToHTML(diffRecord, diffLine.Type)
  168. }
  169. // DiffFile ...
  170. type DiffFile struct {
  171. Name string
  172. OldName string
  173. Index int
  174. Addition, Deletion int
  175. Type DiffFileType
  176. IsCreated bool
  177. IsDeleted bool
  178. IsBin bool
  179. IsRenamed bool
  180. IsSubmodule bool
  181. Sections []*DiffSection
  182. IsIncomplete bool
  183. }
  184. // GetType returns type of diff file.
  185. func (diffFile *DiffFile) GetType() int {
  186. return int(diffFile.Type)
  187. }
  188. // GetHighlightClass ...
  189. func (diffFile *DiffFile) GetHighlightClass() string {
  190. return highlight.FileNameToHighlightClass(diffFile.Name)
  191. }
  192. // Diff ...
  193. type Diff struct {
  194. TotalAddition, TotalDeletion int
  195. Files []*DiffFile
  196. IsIncomplete bool
  197. }
  198. // NumFiles ...
  199. func (diff *Diff) NumFiles() int {
  200. return len(diff.Files)
  201. }
  202. const cmdDiffHead = "diff --git "
  203. // ParsePatch ...
  204. // TODO: move this function to gogits/git-module
  205. func ParsePatch(maxLines, maxLineCharacteres, maxFiles int, reader io.Reader) (*Diff, error) {
  206. var (
  207. diff = &Diff{Files: make([]*DiffFile, 0)}
  208. curFile *DiffFile
  209. curSection = &DiffSection{
  210. Lines: make([]*DiffLine, 0, 10),
  211. }
  212. leftLine, rightLine int
  213. lineCount int
  214. curFileLinesCount int
  215. )
  216. input := bufio.NewReader(reader)
  217. isEOF := false
  218. for !isEOF {
  219. line, err := input.ReadString('\n')
  220. if err != nil {
  221. if err == io.EOF {
  222. isEOF = true
  223. } else {
  224. return nil, fmt.Errorf("ReadString: %v", err)
  225. }
  226. }
  227. if len(line) > 0 && line[len(line)-1] == '\n' {
  228. // Remove line break.
  229. line = line[:len(line)-1]
  230. }
  231. if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") || len(line) == 0 {
  232. continue
  233. }
  234. curFileLinesCount++
  235. lineCount++
  236. // Diff data too large, we only show the first about maxlines lines
  237. if curFileLinesCount >= maxLines || len(line) >= maxLineCharacteres {
  238. curFile.IsIncomplete = true
  239. }
  240. switch {
  241. case line[0] == ' ':
  242. diffLine := &DiffLine{Type: DiffLinePlain, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
  243. leftLine++
  244. rightLine++
  245. curSection.Lines = append(curSection.Lines, diffLine)
  246. continue
  247. case line[0] == '@':
  248. curSection = &DiffSection{}
  249. curFile.Sections = append(curFile.Sections, curSection)
  250. ss := strings.Split(line, "@@")
  251. diffLine := &DiffLine{Type: DiffLineSection, Content: line}
  252. curSection.Lines = append(curSection.Lines, diffLine)
  253. // Parse line number.
  254. ranges := strings.Split(ss[1][1:], " ")
  255. leftLine, _ = com.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int()
  256. if len(ranges) > 1 {
  257. rightLine, _ = com.StrTo(strings.Split(ranges[1], ",")[0]).Int()
  258. } else {
  259. log.Warn("Parse line number failed: %v", line)
  260. rightLine = leftLine
  261. }
  262. continue
  263. case line[0] == '+':
  264. curFile.Addition++
  265. diff.TotalAddition++
  266. diffLine := &DiffLine{Type: DiffLineAdd, Content: line, RightIdx: rightLine}
  267. rightLine++
  268. curSection.Lines = append(curSection.Lines, diffLine)
  269. continue
  270. case line[0] == '-':
  271. curFile.Deletion++
  272. diff.TotalDeletion++
  273. diffLine := &DiffLine{Type: DiffLineDel, Content: line, LeftIdx: leftLine}
  274. if leftLine > 0 {
  275. leftLine++
  276. }
  277. curSection.Lines = append(curSection.Lines, diffLine)
  278. case strings.HasPrefix(line, "Binary"):
  279. curFile.IsBin = true
  280. continue
  281. }
  282. // Get new file.
  283. if strings.HasPrefix(line, cmdDiffHead) {
  284. middle := -1
  285. // Note: In case file name is surrounded by double quotes (it happens only in git-shell).
  286. // e.g. diff --git "a/xxx" "b/xxx"
  287. hasQuote := line[len(cmdDiffHead)] == '"'
  288. if hasQuote {
  289. middle = strings.Index(line, ` "b/`)
  290. } else {
  291. middle = strings.Index(line, " b/")
  292. }
  293. beg := len(cmdDiffHead)
  294. a := line[beg+2 : middle]
  295. b := line[middle+3:]
  296. if hasQuote {
  297. a = string(git.UnescapeChars([]byte(a[1 : len(a)-1])))
  298. b = string(git.UnescapeChars([]byte(b[1 : len(b)-1])))
  299. }
  300. curFile = &DiffFile{
  301. Name: a,
  302. Index: len(diff.Files) + 1,
  303. Type: DiffFileChange,
  304. Sections: make([]*DiffSection, 0, 10),
  305. }
  306. diff.Files = append(diff.Files, curFile)
  307. if len(diff.Files) >= maxFiles {
  308. diff.IsIncomplete = true
  309. io.Copy(ioutil.Discard, reader)
  310. break
  311. }
  312. curFileLinesCount = 0
  313. // Check file diff type and is submodule.
  314. for {
  315. line, err := input.ReadString('\n')
  316. if err != nil {
  317. if err == io.EOF {
  318. isEOF = true
  319. } else {
  320. return nil, fmt.Errorf("ReadString: %v", err)
  321. }
  322. }
  323. switch {
  324. case strings.HasPrefix(line, "new file"):
  325. curFile.Type = DiffFileAdd
  326. curFile.IsCreated = true
  327. case strings.HasPrefix(line, "deleted"):
  328. curFile.Type = DiffFileDel
  329. curFile.IsDeleted = true
  330. case strings.HasPrefix(line, "index"):
  331. curFile.Type = DiffFileChange
  332. case strings.HasPrefix(line, "similarity index 100%"):
  333. curFile.Type = DiffFileRename
  334. curFile.IsRenamed = true
  335. curFile.OldName = curFile.Name
  336. curFile.Name = b
  337. }
  338. if curFile.Type > 0 {
  339. if strings.HasSuffix(line, " 160000\n") {
  340. curFile.IsSubmodule = true
  341. }
  342. break
  343. }
  344. }
  345. }
  346. }
  347. // FIXME: detect encoding while parsing.
  348. var buf bytes.Buffer
  349. for _, f := range diff.Files {
  350. buf.Reset()
  351. for _, sec := range f.Sections {
  352. for _, l := range sec.Lines {
  353. buf.WriteString(l.Content)
  354. buf.WriteString("\n")
  355. }
  356. }
  357. charsetLabel, err := base.DetectEncoding(buf.Bytes())
  358. if charsetLabel != "UTF-8" && err == nil {
  359. encoding, _ := charset.Lookup(charsetLabel)
  360. if encoding != nil {
  361. d := encoding.NewDecoder()
  362. for _, sec := range f.Sections {
  363. for _, l := range sec.Lines {
  364. if c, _, err := transform.String(d, l.Content); err == nil {
  365. l.Content = c
  366. }
  367. }
  368. }
  369. }
  370. }
  371. }
  372. return diff, nil
  373. }
  374. // GetDiffRange ...
  375. func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {
  376. gitRepo, err := git.OpenRepository(repoPath)
  377. if err != nil {
  378. return nil, err
  379. }
  380. commit, err := gitRepo.GetCommit(afterCommitID)
  381. if err != nil {
  382. return nil, err
  383. }
  384. var cmd *exec.Cmd
  385. // if "after" commit given
  386. if len(beforeCommitID) == 0 {
  387. // First commit of repository.
  388. if commit.ParentCount() == 0 {
  389. cmd = exec.Command("git", "show", afterCommitID)
  390. } else {
  391. c, _ := commit.Parent(0)
  392. cmd = exec.Command("git", "diff", "-M", c.ID.String(), afterCommitID)
  393. }
  394. } else {
  395. cmd = exec.Command("git", "diff", "-M", beforeCommitID, afterCommitID)
  396. }
  397. cmd.Dir = repoPath
  398. cmd.Stderr = os.Stderr
  399. stdout, err := cmd.StdoutPipe()
  400. if err != nil {
  401. return nil, fmt.Errorf("StdoutPipe: %v", err)
  402. }
  403. if err = cmd.Start(); err != nil {
  404. return nil, fmt.Errorf("Start: %v", err)
  405. }
  406. pid := process.Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cmd)
  407. defer process.Remove(pid)
  408. diff, err := ParsePatch(maxLines, maxLineCharacteres, maxFiles, stdout)
  409. if err != nil {
  410. return nil, fmt.Errorf("ParsePatch: %v", err)
  411. }
  412. if err = cmd.Wait(); err != nil {
  413. return nil, fmt.Errorf("Wait: %v", err)
  414. }
  415. return diff, nil
  416. }
  417. // RawDiffType ...
  418. type RawDiffType string
  419. // RawDiffType possible values.
  420. const (
  421. RawDiffNormal RawDiffType = "diff"
  422. RawDiffPatch RawDiffType = "patch"
  423. )
  424. // GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
  425. // TODO: move this function to gogits/git-module
  426. // GetRawDiff ...
  427. func GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Writer) error {
  428. repo, err := git.OpenRepository(repoPath)
  429. if err != nil {
  430. return fmt.Errorf("OpenRepository: %v", err)
  431. }
  432. commit, err := repo.GetCommit(commitID)
  433. if err != nil {
  434. return fmt.Errorf("GetCommit: %v", err)
  435. }
  436. var cmd *exec.Cmd
  437. switch diffType {
  438. case RawDiffNormal:
  439. if commit.ParentCount() == 0 {
  440. cmd = exec.Command("git", "show", commitID)
  441. } else {
  442. c, _ := commit.Parent(0)
  443. cmd = exec.Command("git", "diff", "-M", c.ID.String(), commitID)
  444. }
  445. case RawDiffPatch:
  446. if commit.ParentCount() == 0 {
  447. cmd = exec.Command("git", "format-patch", "--no-signature", "--stdout", "--root", commitID)
  448. } else {
  449. c, _ := commit.Parent(0)
  450. query := fmt.Sprintf("%s...%s", commitID, c.ID.String())
  451. cmd = exec.Command("git", "format-patch", "--no-signature", "--stdout", query)
  452. }
  453. default:
  454. return fmt.Errorf("invalid diffType: %s", diffType)
  455. }
  456. stderr := new(bytes.Buffer)
  457. cmd.Dir = repoPath
  458. cmd.Stdout = writer
  459. cmd.Stderr = stderr
  460. if err = cmd.Run(); err != nil {
  461. return fmt.Errorf("Run: %v - %s", err, stderr)
  462. }
  463. return nil
  464. }
  465. // GetDiffCommit ...
  466. func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {
  467. return GetDiffRange(repoPath, "", commitID, maxLines, maxLineCharacteres, maxFiles)
  468. }