2014-04-13 09:35:36 +08:00
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"bufio"
2014-12-09 02:18:25 -05:00
"bytes"
2014-06-19 01:08:03 -04:00
"fmt"
2016-01-09 14:51:17 +08:00
"html"
"html/template"
2014-04-13 09:35:36 +08:00
"io"
2015-12-14 09:38:21 -05:00
"io/ioutil"
2014-04-13 09:35:36 +08:00
"os"
"os/exec"
"strings"
2015-12-02 01:10:13 -05:00
"github.com/Unknwon/com"
2016-01-09 14:51:17 +08:00
"github.com/sergi/go-diff/diffmatchpatch"
2014-12-22 11:01:52 +02:00
"golang.org/x/net/html/charset"
"golang.org/x/text/transform"
2015-12-15 17:25:45 -05:00
"github.com/gogits/git-module"
2014-07-26 00:24:27 -04:00
2014-12-09 02:18:25 -05:00
"github.com/gogits/gogs/modules/base"
2014-04-13 09:35:36 +08:00
"github.com/gogits/gogs/modules/log"
2014-06-19 01:08:03 -04:00
"github.com/gogits/gogs/modules/process"
2016-01-31 14:19:02 -02:00
"github.com/gogits/gogs/modules/template/highlight"
2014-04-13 09:35:36 +08:00
)
2016-01-06 18:00:40 -02:00
type DiffLineType uint8
2014-04-13 09:35:36 +08:00
const (
2016-01-07 11:27:35 -02:00
DIFF_LINE_PLAIN DiffLineType = iota + 1
DIFF_LINE_ADD
DIFF_LINE_DEL
DIFF_LINE_SECTION
2014-04-13 09:35:36 +08:00
)
2016-01-06 18:00:40 -02:00
type DiffFileType uint8
2014-04-13 09:35:36 +08:00
const (
2016-01-07 11:27:35 -02:00
DIFF_FILE_ADD DiffFileType = iota + 1
DIFF_FILE_CHANGE
DIFF_FILE_DEL
DIFF_FILE_RENAME
2014-04-13 09:35:36 +08:00
)
type DiffLine struct {
2016-06-29 12:11:00 -03:00
LeftIdx int
RightIdx int
Type DiffLineType
Content string
2014-04-13 09:35:36 +08:00
}
2016-01-06 18:00:40 -02:00
func ( d * DiffLine ) GetType ( ) int {
return int ( d . Type )
2014-04-13 09:35:36 +08:00
}
type DiffSection struct {
Name string
Lines [ ] * DiffLine
}
2016-01-09 14:51:17 +08:00
var (
addedCodePrefix = [ ] byte ( "<span class=\"added-code\">" )
removedCodePrefix = [ ] byte ( "<span class=\"removed-code\">" )
codeTagSuffix = [ ] byte ( "</span>" )
)
func diffToHTML ( diffs [ ] diffmatchpatch . Diff , lineType DiffLineType ) template . HTML {
var buf bytes . Buffer
for i := range diffs {
if diffs [ i ] . Type == diffmatchpatch . DiffInsert && lineType == DIFF_LINE_ADD {
buf . Write ( addedCodePrefix )
buf . WriteString ( html . EscapeString ( diffs [ i ] . Text ) )
buf . Write ( codeTagSuffix )
} else if diffs [ i ] . Type == diffmatchpatch . DiffDelete && lineType == DIFF_LINE_DEL {
buf . Write ( removedCodePrefix )
buf . WriteString ( html . EscapeString ( diffs [ i ] . Text ) )
buf . Write ( codeTagSuffix )
} else if diffs [ i ] . Type == diffmatchpatch . DiffEqual {
buf . WriteString ( html . EscapeString ( diffs [ i ] . Text ) )
2016-01-03 19:26:46 -02:00
}
}
2016-01-09 14:51:17 +08:00
return template . HTML ( buf . Bytes ( ) )
2016-01-03 19:26:46 -02:00
}
2016-01-08 10:50:25 -02:00
// get an specific line by type (add or del) and file line number
func ( diffSection * DiffSection ) GetLine ( lineType DiffLineType , idx int ) * DiffLine {
difference := 0
for _ , diffLine := range diffSection . Lines {
if diffLine . Type == DIFF_LINE_PLAIN {
// get the difference of line numbers between ADD and DEL versions
difference = diffLine . RightIdx - diffLine . LeftIdx
continue
2016-01-03 19:26:46 -02:00
}
2016-01-08 10:50:25 -02:00
if lineType == DIFF_LINE_DEL {
2016-01-09 14:51:17 +08:00
if diffLine . RightIdx == 0 && diffLine . LeftIdx == idx - difference {
2016-01-08 10:50:25 -02:00
return diffLine
}
} else if lineType == DIFF_LINE_ADD {
2016-01-09 14:51:17 +08:00
if diffLine . LeftIdx == 0 && diffLine . RightIdx == idx + difference {
2016-01-03 19:26:46 -02:00
return diffLine
}
}
}
return nil
}
2016-01-27 18:54:08 -02:00
// computes inline diff for the given line
func ( diffSection * DiffSection ) GetComputedInlineDiffFor ( diffLine * DiffLine ) template . HTML {
var compareDiffLine * DiffLine
var diff1 , diff2 string
2016-01-03 19:26:46 -02:00
2016-01-27 18:54:08 -02:00
getDefaultReturn := func ( ) template . HTML {
return template . HTML ( html . EscapeString ( diffLine . Content [ 1 : ] ) )
}
2016-01-09 15:04:28 +08:00
2016-01-27 18:54:08 -02:00
// just compute diff for adds and removes
if diffLine . Type != DIFF_LINE_ADD && diffLine . Type != DIFF_LINE_DEL {
return getDefaultReturn ( )
}
2016-01-03 19:26:46 -02:00
2016-01-27 18:54:08 -02:00
// try to find equivalent diff line. ignore, otherwise
if diffLine . Type == DIFF_LINE_ADD {
compareDiffLine = diffSection . GetLine ( DIFF_LINE_DEL , diffLine . RightIdx )
if compareDiffLine == nil {
return getDefaultReturn ( )
}
diff1 = compareDiffLine . Content
diff2 = diffLine . Content
} else {
compareDiffLine = diffSection . GetLine ( DIFF_LINE_ADD , diffLine . LeftIdx )
if compareDiffLine == nil {
return getDefaultReturn ( )
2016-01-03 19:26:46 -02:00
}
2016-01-27 18:54:08 -02:00
diff1 = diffLine . Content
diff2 = compareDiffLine . Content
}
2016-01-03 19:26:46 -02:00
2016-01-27 18:54:08 -02:00
dmp := diffmatchpatch . New ( )
diffRecord := dmp . DiffMain ( diff1 [ 1 : ] , diff2 [ 1 : ] , true )
diffRecord = dmp . DiffCleanupSemantic ( diffRecord )
2016-01-03 19:26:46 -02:00
2016-01-27 18:54:08 -02:00
return diffToHTML ( diffRecord , diffLine . Type )
2016-01-03 19:26:46 -02:00
}
2014-04-13 09:35:36 +08:00
type DiffFile struct {
Name string
2015-11-02 19:55:24 -05:00
OldName string
2014-05-13 12:40:32 -04:00
Index int
2014-04-13 09:35:36 +08:00
Addition , Deletion int
2016-01-06 18:00:40 -02:00
Type DiffFileType
2015-02-06 17:02:32 +08:00
IsCreated bool
IsDeleted bool
2014-04-15 20:01:20 -04:00
IsBin bool
2015-11-02 19:55:24 -05:00
IsRenamed bool
2014-04-13 09:35:36 +08:00
Sections [ ] * DiffSection
2016-06-29 12:11:00 -03:00
IsIncomplete bool
2014-04-13 09:35:36 +08:00
}
2016-01-06 18:00:40 -02:00
func ( diffFile * DiffFile ) GetType ( ) int {
return int ( diffFile . Type )
}
2016-01-31 14:19:02 -02:00
func ( diffFile * DiffFile ) GetHighlightClass ( ) string {
2016-02-21 18:45:24 -03:00
return highlight . FileNameToHighlightClass ( diffFile . Name )
2016-01-31 14:19:02 -02:00
}
2014-04-13 09:35:36 +08:00
type Diff struct {
TotalAddition , TotalDeletion int
Files [ ] * DiffFile
2016-06-29 12:11:00 -03:00
IsIncomplete bool
2014-04-13 09:35:36 +08:00
}
func ( diff * Diff ) NumFiles ( ) int {
return len ( diff . Files )
}
const DIFF_HEAD = "diff --git "
2016-06-29 12:11:00 -03:00
func ParsePatch ( maxLines , maxLineCharacteres , maxFiles int , reader io . Reader ) ( * Diff , error ) {
2014-04-13 09:35:36 +08:00
var (
2015-12-02 01:10:13 -05:00
diff = & Diff { Files : make ( [ ] * DiffFile , 0 ) }
2014-04-13 09:35:36 +08:00
curFile * DiffFile
curSection = & DiffSection {
Lines : make ( [ ] * DiffLine , 0 , 10 ) ,
}
leftLine , rightLine int
2015-12-02 01:10:13 -05:00
lineCount int
2016-06-29 12:11:00 -03:00
curFileLinesCount int
2014-04-13 09:35:36 +08:00
)
2015-12-02 01:10:13 -05:00
input := bufio . NewReader ( reader )
isEOF := false
2016-06-29 12:11:00 -03:00
for ! isEOF {
2015-12-02 01:10:13 -05:00
line , err := input . ReadString ( '\n' )
if err != nil {
if err == io . EOF {
isEOF = true
} else {
return nil , fmt . Errorf ( "ReadString: %v" , err )
}
}
if len ( line ) > 0 && line [ len ( line ) - 1 ] == '\n' {
// Remove line break.
line = line [ : len ( line ) - 1 ]
2014-04-13 09:35:36 +08:00
}
2016-06-29 12:11:00 -03:00
if strings . HasPrefix ( line , "+++ " ) || strings . HasPrefix ( line , "--- " ) || len ( line ) == 0 {
2014-09-17 12:03:03 +08:00
continue
}
2016-06-29 12:11:00 -03:00
curFileLinesCount ++
2015-12-02 01:10:13 -05:00
lineCount ++
2014-04-13 09:35:36 +08:00
2014-09-17 12:03:03 +08:00
// Diff data too large, we only show the first about maxlines lines
2016-06-29 12:11:00 -03:00
if curFileLinesCount >= maxLines || len ( line ) >= maxLineCharacteres {
curFile . IsIncomplete = true
2014-04-13 09:35:36 +08:00
}
2014-04-15 20:01:20 -04:00
switch {
case line [ 0 ] == ' ' :
2014-04-13 09:35:36 +08:00
diffLine := & DiffLine { Type : DIFF_LINE_PLAIN , Content : line , LeftIdx : leftLine , RightIdx : rightLine }
leftLine ++
rightLine ++
curSection . Lines = append ( curSection . Lines , diffLine )
continue
2014-04-15 20:01:20 -04:00
case line [ 0 ] == '@' :
2014-04-13 09:35:36 +08:00
curSection = & DiffSection { }
curFile . Sections = append ( curFile . Sections , curSection )
ss := strings . Split ( line , "@@" )
diffLine := & DiffLine { Type : DIFF_LINE_SECTION , Content : line }
curSection . Lines = append ( curSection . Lines , diffLine )
// Parse line number.
2015-07-29 17:55:01 +03:00
ranges := strings . Split ( ss [ 1 ] [ 1 : ] , " " )
2014-07-26 00:24:27 -04:00
leftLine , _ = com . StrTo ( strings . Split ( ranges [ 0 ] , "," ) [ 0 ] [ 1 : ] ) . Int ( )
2015-07-29 17:55:01 +03:00
if len ( ranges ) > 1 {
rightLine , _ = com . StrTo ( strings . Split ( ranges [ 1 ] , "," ) [ 0 ] ) . Int ( )
} else {
log . Warn ( "Parse line number failed: %v" , line )
rightLine = leftLine
}
2014-04-13 09:35:36 +08:00
continue
2014-04-15 20:01:20 -04:00
case line [ 0 ] == '+' :
2014-04-13 09:35:36 +08:00
curFile . Addition ++
diff . TotalAddition ++
diffLine := & DiffLine { Type : DIFF_LINE_ADD , Content : line , RightIdx : rightLine }
rightLine ++
curSection . Lines = append ( curSection . Lines , diffLine )
continue
2014-04-15 20:01:20 -04:00
case line [ 0 ] == '-' :
2014-04-13 09:35:36 +08:00
curFile . Deletion ++
diff . TotalDeletion ++
diffLine := & DiffLine { Type : DIFF_LINE_DEL , Content : line , LeftIdx : leftLine }
if leftLine > 0 {
leftLine ++
}
curSection . Lines = append ( curSection . Lines , diffLine )
2014-04-15 20:01:20 -04:00
case strings . HasPrefix ( line , "Binary" ) :
curFile . IsBin = true
2014-04-13 09:35:36 +08:00
continue
}
// Get new file.
if strings . HasPrefix ( line , DIFF_HEAD ) {
2015-11-02 19:55:24 -05:00
middle := - 1
2015-11-20 01:18:50 -05:00
// Note: In case file name is surrounded by double quotes (it happens only in git-shell).
// e.g. diff --git "a/xxx" "b/xxx"
hasQuote := line [ len ( DIFF_HEAD ) ] == '"'
2015-11-02 19:55:24 -05:00
if hasQuote {
middle = strings . Index ( line , ` "b/ ` )
} else {
middle = strings . Index ( line , " b/" )
}
2014-04-13 09:35:36 +08:00
2015-11-02 19:55:24 -05:00
beg := len ( DIFF_HEAD )
a := line [ beg + 2 : middle ]
b := line [ middle + 3 : ]
if hasQuote {
2015-11-20 01:18:50 -05:00
a = string ( git . UnescapeChars ( [ ] byte ( a [ 1 : len ( a ) - 1 ] ) ) )
b = string ( git . UnescapeChars ( [ ] byte ( b [ 1 : len ( b ) - 1 ] ) ) )
2015-08-20 16:08:26 +08:00
}
2014-04-13 09:35:36 +08:00
curFile = & DiffFile {
2015-11-02 19:55:24 -05:00
Name : a ,
2014-05-13 12:40:32 -04:00
Index : len ( diff . Files ) + 1 ,
2014-04-13 09:35:36 +08:00
Type : DIFF_FILE_CHANGE ,
Sections : make ( [ ] * DiffSection , 0 , 10 ) ,
}
diff . Files = append ( diff . Files , curFile )
2016-06-29 12:11:00 -03:00
if len ( diff . Files ) >= maxFiles {
diff . IsIncomplete = true
io . Copy ( ioutil . Discard , reader )
break
}
curFileLinesCount = 0
2014-04-13 09:35:36 +08:00
// Check file diff type.
2015-12-02 01:10:13 -05:00
for {
line , err := input . ReadString ( '\n' )
if err != nil {
if err == io . EOF {
isEOF = true
} else {
return nil , fmt . Errorf ( "ReadString: %v" , err )
}
}
2014-04-13 09:35:36 +08:00
switch {
2015-12-02 01:10:13 -05:00
case strings . HasPrefix ( line , "new file" ) :
2014-04-13 09:35:36 +08:00
curFile . Type = DIFF_FILE_ADD
2015-02-06 17:02:32 +08:00
curFile . IsCreated = true
2015-12-02 01:10:13 -05:00
case strings . HasPrefix ( line , "deleted" ) :
2014-04-13 09:35:36 +08:00
curFile . Type = DIFF_FILE_DEL
2015-02-06 17:02:32 +08:00
curFile . IsDeleted = true
2015-12-02 01:10:13 -05:00
case strings . HasPrefix ( line , "index" ) :
2014-04-13 09:35:36 +08:00
curFile . Type = DIFF_FILE_CHANGE
2015-12-02 01:10:13 -05:00
case strings . HasPrefix ( line , "similarity index 100%" ) :
2015-11-02 19:55:24 -05:00
curFile . Type = DIFF_FILE_RENAME
curFile . IsRenamed = true
curFile . OldName = curFile . Name
curFile . Name = b
2014-04-13 09:35:36 +08:00
}
if curFile . Type > 0 {
break
}
}
}
}
2015-12-02 01:10:13 -05:00
// FIXME: detect encoding while parsing.
var buf bytes . Buffer
2015-07-29 17:55:01 +03:00
for _ , f := range diff . Files {
buf . Reset ( )
for _ , sec := range f . Sections {
for _ , l := range sec . Lines {
buf . WriteString ( l . Content )
buf . WriteString ( "\n" )
}
}
2015-12-31 22:13:47 -05:00
charsetLabel , err := base . DetectEncoding ( buf . Bytes ( ) )
if charsetLabel != "UTF-8" && err == nil {
2015-07-29 17:55:01 +03:00
encoding , _ := charset . Lookup ( charsetLabel )
if encoding != nil {
d := encoding . NewDecoder ( )
2014-12-09 02:18:25 -05:00
for _ , sec := range f . Sections {
for _ , l := range sec . Lines {
2014-12-22 11:01:52 +02:00
if c , _ , err := transform . String ( d , l . Content ) ; err == nil {
l . Content = c
}
2014-12-09 02:18:25 -05:00
}
}
}
}
}
2014-04-13 09:35:36 +08:00
return diff , nil
}
2016-06-29 12:11:00 -03:00
func GetDiffRange ( repoPath , beforeCommitID string , afterCommitID string , maxLines , maxLineCharacteres , maxFiles int ) ( * Diff , error ) {
2014-04-13 09:35:36 +08:00
repo , err := git . OpenRepository ( repoPath )
if err != nil {
return nil , err
}
2015-12-02 01:10:13 -05:00
commit , err := repo . GetCommit ( afterCommitID )
2014-04-13 09:35:36 +08:00
if err != nil {
return nil , err
}
2014-05-28 22:15:15 -04:00
var cmd * exec . Cmd
2014-08-26 08:20:18 -04:00
// if "after" commit given
2015-12-02 01:10:13 -05:00
if len ( beforeCommitID ) == 0 {
2014-08-26 08:20:18 -04:00
// First commit of repository.
if commit . ParentCount ( ) == 0 {
2015-12-02 01:10:13 -05:00
cmd = exec . Command ( "git" , "show" , afterCommitID )
2014-08-26 08:20:18 -04:00
} else {
c , _ := commit . Parent ( 0 )
2015-12-02 01:10:13 -05:00
cmd = exec . Command ( "git" , "diff" , "-M" , c . ID . String ( ) , afterCommitID )
2014-08-26 08:20:18 -04:00
}
2014-05-28 22:15:15 -04:00
} else {
2015-12-02 01:10:13 -05:00
cmd = exec . Command ( "git" , "diff" , "-M" , beforeCommitID , afterCommitID )
2014-04-13 09:35:36 +08:00
}
2014-05-28 22:15:15 -04:00
cmd . Dir = repoPath
cmd . Stderr = os . Stderr
2014-07-06 17:32:36 -04:00
2015-12-02 01:10:13 -05:00
stdout , err := cmd . StdoutPipe ( )
if err != nil {
return nil , fmt . Errorf ( "StdoutPipe: %v" , err )
}
if err = cmd . Start ( ) ; err != nil {
return nil , fmt . Errorf ( "Start: %v" , err )
}
2014-07-06 17:32:36 -04:00
2015-12-02 01:10:13 -05:00
pid := process . Add ( fmt . Sprintf ( "GetDiffRange (%s)" , repoPath ) , cmd )
defer process . Remove ( pid )
2016-06-29 12:11:00 -03:00
diff , err := ParsePatch ( maxLines , maxLineCharacteres , maxFiles , stdout )
2015-12-02 01:10:13 -05:00
if err != nil {
return nil , fmt . Errorf ( "ParsePatch: %v" , err )
}
if err = cmd . Wait ( ) ; err != nil {
return nil , fmt . Errorf ( "Wait: %v" , err )
}
return diff , nil
2014-04-13 09:35:36 +08:00
}
2014-08-26 08:20:18 -04:00
2016-06-29 12:11:00 -03:00
func GetDiffCommit ( repoPath , commitId string , maxLines , maxLineCharacteres , maxFiles int ) ( * Diff , error ) {
return GetDiffRange ( repoPath , "" , commitId , maxLines , maxLineCharacteres , maxFiles )
2014-08-26 08:20:18 -04:00
}