選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

git_diff.go 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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. "io"
  10. "io/ioutil"
  11. "os"
  12. "os/exec"
  13. "strings"
  14. "html/template"
  15. "html"
  16. "github.com/Unknwon/com"
  17. "golang.org/x/net/html/charset"
  18. "golang.org/x/text/transform"
  19. "github.com/gogits/git-module"
  20. "github.com/gogits/gogs/modules/base"
  21. "github.com/gogits/gogs/modules/log"
  22. "github.com/gogits/gogs/modules/process"
  23. "github.com/sergi/go-diff/diffmatchpatch"
  24. )
  25. // Diff line types.
  26. const (
  27. DIFF_LINE_PLAIN = iota + 1
  28. DIFF_LINE_ADD
  29. DIFF_LINE_DEL
  30. DIFF_LINE_SECTION
  31. )
  32. const (
  33. DIFF_FILE_ADD = iota + 1
  34. DIFF_FILE_CHANGE
  35. DIFF_FILE_DEL
  36. DIFF_FILE_RENAME
  37. )
  38. type DiffLine struct {
  39. LeftIdx int
  40. RightIdx int
  41. Type int
  42. Content string
  43. ParsedContent template.HTML
  44. }
  45. func (d DiffLine) GetType() int {
  46. return d.Type
  47. }
  48. type DiffSection struct {
  49. Name string
  50. Lines []*DiffLine
  51. }
  52. func diffToHtml(diffRecord []diffmatchpatch.Diff, lineType int) template.HTML {
  53. result := ""
  54. for _, s := range diffRecord {
  55. if s.Type == diffmatchpatch.DiffInsert && lineType == DIFF_LINE_ADD {
  56. result = result + "<span class=\"added-code\">"+html.EscapeString(s.Text)+"</span>"
  57. } else if s.Type == diffmatchpatch.DiffDelete && lineType == DIFF_LINE_DEL {
  58. result = result + "<span class=\"removed-code\">"+html.EscapeString(s.Text)+"</span>"
  59. } else if s.Type == diffmatchpatch.DiffEqual {
  60. result = result + html.EscapeString(s.Text)
  61. }
  62. }
  63. return template.HTML(result)
  64. }
  65. func (diffSection *DiffSection) GetLeftLine(idx int, sliceIdx int) *DiffLine {
  66. for i, diffLine := range diffSection.Lines {
  67. if diffLine.LeftIdx == idx && diffLine.RightIdx == 0 {
  68. // ignore the the lines are too far from each other
  69. if i > sliceIdx-5 && i < sliceIdx+5 {
  70. return diffLine
  71. } else {
  72. return nil
  73. }
  74. }
  75. }
  76. return nil
  77. }
  78. func (diffSection *DiffSection) GetRightLine(idx int, sliceIdx int) *DiffLine {
  79. for i, diffLine := range diffSection.Lines {
  80. if diffLine.RightIdx == idx && diffLine.LeftIdx == 0 {
  81. // ignore the the lines are too far from each other
  82. if i > sliceIdx-5 && i < sliceIdx+5 {
  83. return diffLine
  84. } else {
  85. return nil
  86. }
  87. }
  88. }
  89. return nil
  90. }
  91. // computes diff of each diff line and set the HTML on diffLine.ParsedContent
  92. func (diffSection *DiffSection) ComputeLinesDiff() {
  93. for i, diffLine := range diffSection.Lines {
  94. var compareDiffLine *DiffLine
  95. var diff1, diff2 string
  96. // default content: as is
  97. diffLine.ParsedContent = template.HTML(html.EscapeString(diffLine.Content[1:]))
  98. // just compute diff for adds and removes
  99. if diffLine.Type != DIFF_LINE_ADD && diffLine.Type != DIFF_LINE_DEL {
  100. continue
  101. }
  102. // try to find equivalent diff line. ignore, otherwise
  103. if diffLine.Type == DIFF_LINE_ADD {
  104. compareDiffLine = diffSection.GetLeftLine(diffLine.RightIdx, i)
  105. if compareDiffLine == nil {
  106. continue
  107. }
  108. diff1 = compareDiffLine.Content
  109. diff2 = diffLine.Content
  110. } else {
  111. compareDiffLine = diffSection.GetRightLine(diffLine.LeftIdx, i)
  112. if compareDiffLine == nil {
  113. continue
  114. }
  115. diff1 = diffLine.Content
  116. diff2 = compareDiffLine.Content
  117. }
  118. dmp := diffmatchpatch.New()
  119. diffRecord := dmp.DiffMain(diff1[1:], diff2[1:], true)
  120. diffRecord = dmp.DiffCleanupSemantic(diffRecord)
  121. diffLine.ParsedContent = diffToHtml(diffRecord, diffLine.Type)
  122. }
  123. }
  124. type DiffFile struct {
  125. Name string
  126. OldName string
  127. Index int
  128. Addition, Deletion int
  129. Type int
  130. IsCreated bool
  131. IsDeleted bool
  132. IsBin bool
  133. IsRenamed bool
  134. Sections []*DiffSection
  135. }
  136. type Diff struct {
  137. TotalAddition, TotalDeletion int
  138. Files []*DiffFile
  139. }
  140. func (diff *Diff) NumFiles() int {
  141. return len(diff.Files)
  142. }
  143. const DIFF_HEAD = "diff --git "
  144. func ParsePatch(maxlines int, reader io.Reader) (*Diff, error) {
  145. var (
  146. diff = &Diff{Files: make([]*DiffFile, 0)}
  147. curFile *DiffFile
  148. curSection = &DiffSection{
  149. Lines: make([]*DiffLine, 0, 10),
  150. }
  151. leftLine, rightLine int
  152. lineCount int
  153. )
  154. input := bufio.NewReader(reader)
  155. isEOF := false
  156. for {
  157. if isEOF {
  158. break
  159. }
  160. line, err := input.ReadString('\n')
  161. if err != nil {
  162. if err == io.EOF {
  163. isEOF = true
  164. } else {
  165. return nil, fmt.Errorf("ReadString: %v", err)
  166. }
  167. }
  168. if len(line) > 0 && line[len(line)-1] == '\n' {
  169. // Remove line break.
  170. line = line[:len(line)-1]
  171. }
  172. if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") {
  173. continue
  174. } else if len(line) == 0 {
  175. continue
  176. }
  177. lineCount++
  178. // Diff data too large, we only show the first about maxlines lines
  179. if lineCount >= maxlines {
  180. log.Warn("Diff data too large")
  181. io.Copy(ioutil.Discard, reader)
  182. diff.Files = nil
  183. return diff, nil
  184. }
  185. switch {
  186. case line[0] == ' ':
  187. diffLine := &DiffLine{Type: DIFF_LINE_PLAIN, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
  188. leftLine++
  189. rightLine++
  190. curSection.Lines = append(curSection.Lines, diffLine)
  191. continue
  192. case line[0] == '@':
  193. curSection = &DiffSection{}
  194. curFile.Sections = append(curFile.Sections, curSection)
  195. ss := strings.Split(line, "@@")
  196. diffLine := &DiffLine{Type: DIFF_LINE_SECTION, Content: line}
  197. curSection.Lines = append(curSection.Lines, diffLine)
  198. // Parse line number.
  199. ranges := strings.Split(ss[1][1:], " ")
  200. leftLine, _ = com.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int()
  201. if len(ranges) > 1 {
  202. rightLine, _ = com.StrTo(strings.Split(ranges[1], ",")[0]).Int()
  203. } else {
  204. log.Warn("Parse line number failed: %v", line)
  205. rightLine = leftLine
  206. }
  207. continue
  208. case line[0] == '+':
  209. curFile.Addition++
  210. diff.TotalAddition++
  211. diffLine := &DiffLine{Type: DIFF_LINE_ADD, Content: line, RightIdx: rightLine}
  212. rightLine++
  213. curSection.Lines = append(curSection.Lines, diffLine)
  214. continue
  215. case line[0] == '-':
  216. curFile.Deletion++
  217. diff.TotalDeletion++
  218. diffLine := &DiffLine{Type: DIFF_LINE_DEL, Content: line, LeftIdx: leftLine}
  219. if leftLine > 0 {
  220. leftLine++
  221. }
  222. curSection.Lines = append(curSection.Lines, diffLine)
  223. case strings.HasPrefix(line, "Binary"):
  224. curFile.IsBin = true
  225. continue
  226. }
  227. // Get new file.
  228. if strings.HasPrefix(line, DIFF_HEAD) {
  229. middle := -1
  230. // Note: In case file name is surrounded by double quotes (it happens only in git-shell).
  231. // e.g. diff --git "a/xxx" "b/xxx"
  232. hasQuote := line[len(DIFF_HEAD)] == '"'
  233. if hasQuote {
  234. middle = strings.Index(line, ` "b/`)
  235. } else {
  236. middle = strings.Index(line, " b/")
  237. }
  238. beg := len(DIFF_HEAD)
  239. a := line[beg+2 : middle]
  240. b := line[middle+3:]
  241. if hasQuote {
  242. a = string(git.UnescapeChars([]byte(a[1 : len(a)-1])))
  243. b = string(git.UnescapeChars([]byte(b[1 : len(b)-1])))
  244. }
  245. curFile = &DiffFile{
  246. Name: a,
  247. Index: len(diff.Files) + 1,
  248. Type: DIFF_FILE_CHANGE,
  249. Sections: make([]*DiffSection, 0, 10),
  250. }
  251. diff.Files = append(diff.Files, curFile)
  252. // Check file diff type.
  253. for {
  254. line, err := input.ReadString('\n')
  255. if err != nil {
  256. if err == io.EOF {
  257. isEOF = true
  258. } else {
  259. return nil, fmt.Errorf("ReadString: %v", err)
  260. }
  261. }
  262. switch {
  263. case strings.HasPrefix(line, "new file"):
  264. curFile.Type = DIFF_FILE_ADD
  265. curFile.IsCreated = true
  266. case strings.HasPrefix(line, "deleted"):
  267. curFile.Type = DIFF_FILE_DEL
  268. curFile.IsDeleted = true
  269. case strings.HasPrefix(line, "index"):
  270. curFile.Type = DIFF_FILE_CHANGE
  271. case strings.HasPrefix(line, "similarity index 100%"):
  272. curFile.Type = DIFF_FILE_RENAME
  273. curFile.IsRenamed = true
  274. curFile.OldName = curFile.Name
  275. curFile.Name = b
  276. }
  277. if curFile.Type > 0 {
  278. break
  279. }
  280. }
  281. }
  282. }
  283. // FIXME: detect encoding while parsing.
  284. var buf bytes.Buffer
  285. for _, f := range diff.Files {
  286. buf.Reset()
  287. for _, sec := range f.Sections {
  288. for _, l := range sec.Lines {
  289. buf.WriteString(l.Content)
  290. buf.WriteString("\n")
  291. }
  292. }
  293. charsetLabel, err := base.DetectEncoding(buf.Bytes())
  294. if charsetLabel != "UTF-8" && err == nil {
  295. encoding, _ := charset.Lookup(charsetLabel)
  296. if encoding != nil {
  297. d := encoding.NewDecoder()
  298. for _, sec := range f.Sections {
  299. for _, l := range sec.Lines {
  300. if c, _, err := transform.String(d, l.Content); err == nil {
  301. l.Content = c
  302. }
  303. }
  304. }
  305. }
  306. }
  307. }
  308. return diff, nil
  309. }
  310. func GetDiffRange(repoPath, beforeCommitID string, afterCommitID string, maxlines int) (*Diff, error) {
  311. repo, err := git.OpenRepository(repoPath)
  312. if err != nil {
  313. return nil, err
  314. }
  315. commit, err := repo.GetCommit(afterCommitID)
  316. if err != nil {
  317. return nil, err
  318. }
  319. var cmd *exec.Cmd
  320. // if "after" commit given
  321. if len(beforeCommitID) == 0 {
  322. // First commit of repository.
  323. if commit.ParentCount() == 0 {
  324. cmd = exec.Command("git", "show", afterCommitID)
  325. } else {
  326. c, _ := commit.Parent(0)
  327. cmd = exec.Command("git", "diff", "-M", c.ID.String(), afterCommitID)
  328. }
  329. } else {
  330. cmd = exec.Command("git", "diff", "-M", beforeCommitID, afterCommitID)
  331. }
  332. cmd.Dir = repoPath
  333. cmd.Stderr = os.Stderr
  334. stdout, err := cmd.StdoutPipe()
  335. if err != nil {
  336. return nil, fmt.Errorf("StdoutPipe: %v", err)
  337. }
  338. if err = cmd.Start(); err != nil {
  339. return nil, fmt.Errorf("Start: %v", err)
  340. }
  341. pid := process.Add(fmt.Sprintf("GetDiffRange (%s)", repoPath), cmd)
  342. defer process.Remove(pid)
  343. diff, err := ParsePatch(maxlines, stdout)
  344. if err != nil {
  345. return nil, fmt.Errorf("ParsePatch: %v", err)
  346. }
  347. if err = cmd.Wait(); err != nil {
  348. return nil, fmt.Errorf("Wait: %v", err)
  349. }
  350. return diff, nil
  351. }
  352. func GetDiffCommit(repoPath, commitId string, maxlines int) (*Diff, error) {
  353. return GetDiffRange(repoPath, "", commitId, maxlines)
  354. }