git_diff.go 12 KB

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