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

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