git_diff.go 13 KB

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