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