git_diff.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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. func ParsePatch(maxLines, maxLineCharacteres, maxFiles int, reader io.Reader) (*Diff, error) {
  158. var (
  159. diff = &Diff{Files: make([]*DiffFile, 0)}
  160. curFile *DiffFile
  161. curSection = &DiffSection{
  162. Lines: make([]*DiffLine, 0, 10),
  163. }
  164. leftLine, rightLine int
  165. lineCount int
  166. curFileLinesCount int
  167. )
  168. input := bufio.NewReader(reader)
  169. isEOF := false
  170. for !isEOF {
  171. line, err := input.ReadString('\n')
  172. if err != nil {
  173. if err == io.EOF {
  174. isEOF = true
  175. } else {
  176. return nil, fmt.Errorf("ReadString: %v", err)
  177. }
  178. }
  179. if len(line) > 0 && line[len(line)-1] == '\n' {
  180. // Remove line break.
  181. line = line[:len(line)-1]
  182. }
  183. if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") || len(line) == 0 {
  184. continue
  185. }
  186. curFileLinesCount++
  187. lineCount++
  188. // Diff data too large, we only show the first about maxlines lines
  189. if curFileLinesCount >= maxLines || len(line) >= maxLineCharacteres {
  190. curFile.IsIncomplete = true
  191. }
  192. switch {
  193. case line[0] == ' ':
  194. diffLine := &DiffLine{Type: DIFF_LINE_PLAIN, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
  195. leftLine++
  196. rightLine++
  197. curSection.Lines = append(curSection.Lines, diffLine)
  198. continue
  199. case line[0] == '@':
  200. curSection = &DiffSection{}
  201. curFile.Sections = append(curFile.Sections, curSection)
  202. ss := strings.Split(line, "@@")
  203. diffLine := &DiffLine{Type: DIFF_LINE_SECTION, Content: line}
  204. curSection.Lines = append(curSection.Lines, diffLine)
  205. // Parse line number.
  206. ranges := strings.Split(ss[1][1:], " ")
  207. leftLine, _ = com.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int()
  208. if len(ranges) > 1 {
  209. rightLine, _ = com.StrTo(strings.Split(ranges[1], ",")[0]).Int()
  210. } else {
  211. log.Warn("Parse line number failed: %v", line)
  212. rightLine = leftLine
  213. }
  214. continue
  215. case line[0] == '+':
  216. curFile.Addition++
  217. diff.TotalAddition++
  218. diffLine := &DiffLine{Type: DIFF_LINE_ADD, Content: line, RightIdx: rightLine}
  219. rightLine++
  220. curSection.Lines = append(curSection.Lines, diffLine)
  221. continue
  222. case line[0] == '-':
  223. curFile.Deletion++
  224. diff.TotalDeletion++
  225. diffLine := &DiffLine{Type: DIFF_LINE_DEL, Content: line, LeftIdx: leftLine}
  226. if leftLine > 0 {
  227. leftLine++
  228. }
  229. curSection.Lines = append(curSection.Lines, diffLine)
  230. case strings.HasPrefix(line, "Binary"):
  231. curFile.IsBin = true
  232. continue
  233. }
  234. // Get new file.
  235. if strings.HasPrefix(line, DIFF_HEAD) {
  236. middle := -1
  237. // Note: In case file name is surrounded by double quotes (it happens only in git-shell).
  238. // e.g. diff --git "a/xxx" "b/xxx"
  239. hasQuote := line[len(DIFF_HEAD)] == '"'
  240. if hasQuote {
  241. middle = strings.Index(line, ` "b/`)
  242. } else {
  243. middle = strings.Index(line, " b/")
  244. }
  245. beg := len(DIFF_HEAD)
  246. a := line[beg+2 : middle]
  247. b := line[middle+3:]
  248. if hasQuote {
  249. a = string(git.UnescapeChars([]byte(a[1 : len(a)-1])))
  250. b = string(git.UnescapeChars([]byte(b[1 : len(b)-1])))
  251. }
  252. curFile = &DiffFile{
  253. Name: a,
  254. Index: len(diff.Files) + 1,
  255. Type: DIFF_FILE_CHANGE,
  256. Sections: make([]*DiffSection, 0, 10),
  257. }
  258. diff.Files = append(diff.Files, curFile)
  259. if len(diff.Files) >= maxFiles {
  260. diff.IsIncomplete = true
  261. io.Copy(ioutil.Discard, reader)
  262. break
  263. }
  264. curFileLinesCount = 0
  265. // Check file diff type and is submodule.
  266. for {
  267. line, err := input.ReadString('\n')
  268. if err != nil {
  269. if err == io.EOF {
  270. isEOF = true
  271. } else {
  272. return nil, fmt.Errorf("ReadString: %v", err)
  273. }
  274. }
  275. switch {
  276. case strings.HasPrefix(line, "new file"):
  277. curFile.Type = DIFF_FILE_ADD
  278. curFile.IsCreated = true
  279. case strings.HasPrefix(line, "deleted"):
  280. curFile.Type = DIFF_FILE_DEL
  281. curFile.IsDeleted = true
  282. case strings.HasPrefix(line, "index"):
  283. curFile.Type = DIFF_FILE_CHANGE
  284. case strings.HasPrefix(line, "similarity index 100%"):
  285. curFile.Type = DIFF_FILE_RENAME
  286. curFile.IsRenamed = true
  287. curFile.OldName = curFile.Name
  288. curFile.Name = b
  289. }
  290. if curFile.Type > 0 {
  291. if strings.HasSuffix(line, " 160000\n") {
  292. curFile.IsSubmodule = true
  293. }
  294. break
  295. }
  296. }
  297. }
  298. }
  299. // FIXME: detect encoding while parsing.
  300. var buf bytes.Buffer
  301. for _, f := range diff.Files {
  302. buf.Reset()
  303. for _, sec := range f.Sections {
  304. for _, l := range sec.Lines {
  305. buf.WriteString(l.Content)
  306. buf.WriteString("\n")
  307. }
  308. }
  309. charsetLabel, err := base.DetectEncoding(buf.Bytes())
  310. if charsetLabel != "UTF-8" && err == nil {
  311. encoding, _ := charset.Lookup(charsetLabel)
  312. if encoding != nil {
  313. d := encoding.NewDecoder()
  314. for _, sec := range f.Sections {
  315. for _, l := range sec.Lines {
  316. if c, _, err := transform.String(d, l.Content); err == nil {
  317. l.Content = c
  318. }
  319. }
  320. }
  321. }
  322. }
  323. }
  324. return diff, nil
  325. }
  326. func GetDiffRange(repoPath, beforeCommitID string, afterCommitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {
  327. repo, err := git.OpenRepository(repoPath)
  328. if err != nil {
  329. return nil, err
  330. }
  331. commit, err := repo.GetCommit(afterCommitID)
  332. if err != nil {
  333. return nil, err
  334. }
  335. var cmd *exec.Cmd
  336. // if "after" commit given
  337. if len(beforeCommitID) == 0 {
  338. // First commit of repository.
  339. if commit.ParentCount() == 0 {
  340. cmd = exec.Command("git", "show", afterCommitID)
  341. } else {
  342. c, _ := commit.Parent(0)
  343. cmd = exec.Command("git", "diff", "-M", c.ID.String(), afterCommitID)
  344. }
  345. } else {
  346. cmd = exec.Command("git", "diff", "-M", beforeCommitID, afterCommitID)
  347. }
  348. cmd.Dir = repoPath
  349. cmd.Stderr = os.Stderr
  350. stdout, err := cmd.StdoutPipe()
  351. if err != nil {
  352. return nil, fmt.Errorf("StdoutPipe: %v", err)
  353. }
  354. if err = cmd.Start(); err != nil {
  355. return nil, fmt.Errorf("Start: %v", err)
  356. }
  357. pid := process.Add(fmt.Sprintf("GetDiffRange (%s)", repoPath), cmd)
  358. defer process.Remove(pid)
  359. diff, err := ParsePatch(maxLines, maxLineCharacteres, maxFiles, stdout)
  360. if err != nil {
  361. return nil, fmt.Errorf("ParsePatch: %v", err)
  362. }
  363. if err = cmd.Wait(); err != nil {
  364. return nil, fmt.Errorf("Wait: %v", err)
  365. }
  366. return diff, nil
  367. }
  368. func GetDiffCommit(repoPath, commitId string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {
  369. return GetDiffRange(repoPath, "", commitId, maxLines, maxLineCharacteres, maxFiles)
  370. }