2014-04-13 05:35:36 +04: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 10:18:25 +03:00
"bytes"
2014-06-19 09:08:03 +04:00
"fmt"
2014-04-13 05:35:36 +04:00
"io"
2015-12-14 17:38:21 +03:00
"io/ioutil"
2014-04-13 05:35:36 +04:00
"os"
"os/exec"
"strings"
2016-01-04 00:26:46 +03:00
"html/template"
"html"
2014-04-13 05:35:36 +04:00
2015-12-02 09:10:13 +03:00
"github.com/Unknwon/com"
2014-12-22 12:01:52 +03:00
"golang.org/x/net/html/charset"
"golang.org/x/text/transform"
2016-01-07 16:27:35 +03:00
"github.com/sergi/go-diff/diffmatchpatch"
2014-12-22 12:01:52 +03:00
2015-12-16 01:25:45 +03:00
"github.com/gogits/git-module"
2014-07-26 08:24:27 +04:00
2014-12-09 10:18:25 +03:00
"github.com/gogits/gogs/modules/base"
2014-04-13 05:35:36 +04:00
"github.com/gogits/gogs/modules/log"
2014-06-19 09:08:03 +04:00
"github.com/gogits/gogs/modules/process"
2014-04-13 05:35:36 +04:00
)
2016-01-06 23:00:40 +03:00
type DiffLineType uint8
2014-04-13 05:35:36 +04:00
const (
2016-01-07 16:27:35 +03:00
DIFF_LINE_PLAIN DiffLineType = iota + 1
DIFF_LINE_ADD
DIFF_LINE_DEL
DIFF_LINE_SECTION
2014-04-13 05:35:36 +04:00
)
2016-01-06 23:00:40 +03:00
type DiffFileType uint8
2014-04-13 05:35:36 +04:00
const (
2016-01-07 16:27:35 +03:00
DIFF_FILE_ADD DiffFileType = iota + 1
DIFF_FILE_CHANGE
DIFF_FILE_DEL
DIFF_FILE_RENAME
2014-04-13 05:35:36 +04:00
)
type DiffLine struct {
LeftIdx int
RightIdx int
2016-01-06 23:00:40 +03:00
Type DiffLineType
2014-04-13 05:35:36 +04:00
Content string
2016-01-04 00:26:46 +03:00
ParsedContent template . HTML
2014-04-13 05:35:36 +04:00
}
2016-01-06 23:00:40 +03:00
func ( d * DiffLine ) GetType ( ) int {
return int ( d . Type )
2014-04-13 05:35:36 +04:00
}
type DiffSection struct {
Name string
Lines [ ] * DiffLine
}
2016-01-06 23:00:40 +03:00
func diffToHtml ( diffRecord [ ] diffmatchpatch . Diff , lineType DiffLineType ) template . HTML {
2016-01-04 00:26:46 +03:00
result := ""
for _ , s := range diffRecord {
if s . Type == diffmatchpatch . DiffInsert && lineType == DIFF_LINE_ADD {
result = result + "<span class=\"added-code\">" + html . EscapeString ( s . Text ) + "</span>"
} else if s . Type == diffmatchpatch . DiffDelete && lineType == DIFF_LINE_DEL {
result = result + "<span class=\"removed-code\">" + html . EscapeString ( s . Text ) + "</span>"
} else if s . Type == diffmatchpatch . DiffEqual {
result = result + html . EscapeString ( s . Text )
}
}
return template . HTML ( result )
}
2016-01-08 15:50:25 +03: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-04 00:26:46 +03:00
}
2016-01-08 15:50:25 +03:00
if lineType == DIFF_LINE_DEL {
if diffLine . RightIdx == 0 && diffLine . LeftIdx == idx - difference {
return diffLine
}
} else if lineType == DIFF_LINE_ADD {
if diffLine . LeftIdx == 0 && diffLine . RightIdx == idx + difference {
2016-01-04 00:26:46 +03:00
return diffLine
}
}
}
return nil
}
// computes diff of each diff line and set the HTML on diffLine.ParsedContent
func ( diffSection * DiffSection ) ComputeLinesDiff ( ) {
2016-01-08 15:50:25 +03:00
for _ , diffLine := range diffSection . Lines {
2016-01-04 00:26:46 +03:00
var compareDiffLine * DiffLine
var diff1 , diff2 string
// default content: as is
diffLine . ParsedContent = template . HTML ( html . EscapeString ( diffLine . Content [ 1 : ] ) )
// just compute diff for adds and removes
if diffLine . Type != DIFF_LINE_ADD && diffLine . Type != DIFF_LINE_DEL {
continue
}
// try to find equivalent diff line. ignore, otherwise
if diffLine . Type == DIFF_LINE_ADD {
2016-01-08 15:50:25 +03:00
compareDiffLine = diffSection . GetLine ( DIFF_LINE_DEL , diffLine . RightIdx )
2016-01-04 00:26:46 +03:00
if compareDiffLine == nil {
continue
}
diff1 = compareDiffLine . Content
diff2 = diffLine . Content
} else {
2016-01-08 15:50:25 +03:00
compareDiffLine = diffSection . GetLine ( DIFF_LINE_ADD , diffLine . LeftIdx )
2016-01-04 00:26:46 +03:00
if compareDiffLine == nil {
continue
}
diff1 = diffLine . Content
diff2 = compareDiffLine . Content
}
dmp := diffmatchpatch . New ( )
diffRecord := dmp . DiffMain ( diff1 [ 1 : ] , diff2 [ 1 : ] , true )
diffRecord = dmp . DiffCleanupSemantic ( diffRecord )
diffLine . ParsedContent = diffToHtml ( diffRecord , diffLine . Type )
}
}
2014-04-13 05:35:36 +04:00
type DiffFile struct {
Name string
2015-11-03 03:55:24 +03:00
OldName string
2014-05-13 20:40:32 +04:00
Index int
2014-04-13 05:35:36 +04:00
Addition , Deletion int
2016-01-06 23:00:40 +03:00
Type DiffFileType
2015-02-06 12:02:32 +03:00
IsCreated bool
IsDeleted bool
2014-04-16 04:01:20 +04:00
IsBin bool
2015-11-03 03:55:24 +03:00
IsRenamed bool
2014-04-13 05:35:36 +04:00
Sections [ ] * DiffSection
}
2016-01-06 23:00:40 +03:00
func ( diffFile * DiffFile ) GetType ( ) int {
return int ( diffFile . Type )
}
2014-04-13 05:35:36 +04:00
type Diff struct {
TotalAddition , TotalDeletion int
Files [ ] * DiffFile
}
func ( diff * Diff ) NumFiles ( ) int {
return len ( diff . Files )
}
const DIFF_HEAD = "diff --git "
2015-12-02 09:10:13 +03:00
func ParsePatch ( maxlines int , reader io . Reader ) ( * Diff , error ) {
2014-04-13 05:35:36 +04:00
var (
2015-12-02 09:10:13 +03:00
diff = & Diff { Files : make ( [ ] * DiffFile , 0 ) }
2014-04-13 05:35:36 +04:00
curFile * DiffFile
curSection = & DiffSection {
Lines : make ( [ ] * DiffLine , 0 , 10 ) ,
}
leftLine , rightLine int
2015-12-02 09:10:13 +03:00
lineCount int
2014-04-13 05:35:36 +04:00
)
2015-12-02 09:10:13 +03:00
input := bufio . NewReader ( reader )
isEOF := false
for {
if isEOF {
break
}
2015-11-04 01:25:39 +03:00
2015-12-02 09:10:13 +03: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 05:35:36 +04:00
}
2015-12-02 09:10:13 +03:00
if strings . HasPrefix ( line , "+++ " ) || strings . HasPrefix ( line , "--- " ) {
continue
} else if len ( line ) == 0 {
2014-09-17 08:03:03 +04:00
continue
}
2015-12-02 09:10:13 +03:00
lineCount ++
2014-04-13 05:35:36 +04:00
2014-09-17 08:03:03 +04:00
// Diff data too large, we only show the first about maxlines lines
2015-12-02 09:10:13 +03:00
if lineCount >= maxlines {
2014-04-13 05:35:36 +04:00
log . Warn ( "Diff data too large" )
2015-12-14 17:38:21 +03:00
io . Copy ( ioutil . Discard , reader )
2015-10-18 00:25:45 +03:00
diff . Files = nil
return diff , nil
2014-04-13 05:35:36 +04:00
}
2014-04-16 04:01:20 +04:00
switch {
case line [ 0 ] == ' ' :
2014-04-13 05:35:36 +04:00
diffLine := & DiffLine { Type : DIFF_LINE_PLAIN , Content : line , LeftIdx : leftLine , RightIdx : rightLine }
leftLine ++
rightLine ++
curSection . Lines = append ( curSection . Lines , diffLine )
continue
2014-04-16 04:01:20 +04:00
case line [ 0 ] == '@' :
2014-04-13 05:35:36 +04: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 08: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 05:35:36 +04:00
continue
2014-04-16 04:01:20 +04:00
case line [ 0 ] == '+' :
2014-04-13 05:35:36 +04: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-16 04:01:20 +04:00
case line [ 0 ] == '-' :
2014-04-13 05:35:36 +04: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-16 04:01:20 +04:00
case strings . HasPrefix ( line , "Binary" ) :
curFile . IsBin = true
2014-04-13 05:35:36 +04:00
continue
}
// Get new file.
if strings . HasPrefix ( line , DIFF_HEAD ) {
2015-11-03 03:55:24 +03:00
middle := - 1
2015-11-20 09:18:50 +03: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-03 03:55:24 +03:00
if hasQuote {
middle = strings . Index ( line , ` "b/ ` )
} else {
middle = strings . Index ( line , " b/" )
}
2014-04-13 05:35:36 +04:00
2015-11-03 03:55:24 +03:00
beg := len ( DIFF_HEAD )
a := line [ beg + 2 : middle ]
b := line [ middle + 3 : ]
if hasQuote {
2015-11-20 09:18:50 +03:00
a = string ( git . UnescapeChars ( [ ] byte ( a [ 1 : len ( a ) - 1 ] ) ) )
b = string ( git . UnescapeChars ( [ ] byte ( b [ 1 : len ( b ) - 1 ] ) ) )
2015-08-20 11:08:26 +03:00
}
2014-04-13 05:35:36 +04:00
curFile = & DiffFile {
2015-11-03 03:55:24 +03:00
Name : a ,
2014-05-13 20:40:32 +04:00
Index : len ( diff . Files ) + 1 ,
2014-04-13 05:35:36 +04:00
Type : DIFF_FILE_CHANGE ,
Sections : make ( [ ] * DiffSection , 0 , 10 ) ,
}
diff . Files = append ( diff . Files , curFile )
// Check file diff type.
2015-12-02 09:10:13 +03: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 05:35:36 +04:00
switch {
2015-12-02 09:10:13 +03:00
case strings . HasPrefix ( line , "new file" ) :
2014-04-13 05:35:36 +04:00
curFile . Type = DIFF_FILE_ADD
2015-02-06 12:02:32 +03:00
curFile . IsCreated = true
2015-12-02 09:10:13 +03:00
case strings . HasPrefix ( line , "deleted" ) :
2014-04-13 05:35:36 +04:00
curFile . Type = DIFF_FILE_DEL
2015-02-06 12:02:32 +03:00
curFile . IsDeleted = true
2015-12-02 09:10:13 +03:00
case strings . HasPrefix ( line , "index" ) :
2014-04-13 05:35:36 +04:00
curFile . Type = DIFF_FILE_CHANGE
2015-12-02 09:10:13 +03:00
case strings . HasPrefix ( line , "similarity index 100%" ) :
2015-11-03 03:55:24 +03:00
curFile . Type = DIFF_FILE_RENAME
curFile . IsRenamed = true
curFile . OldName = curFile . Name
curFile . Name = b
2014-04-13 05:35:36 +04:00
}
if curFile . Type > 0 {
break
}
}
}
}
2015-12-02 09:10:13 +03: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" )
}
}
2016-01-01 06:13:47 +03: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 10:18:25 +03:00
for _ , sec := range f . Sections {
for _ , l := range sec . Lines {
2014-12-22 12:01:52 +03:00
if c , _ , err := transform . String ( d , l . Content ) ; err == nil {
l . Content = c
}
2014-12-09 10:18:25 +03:00
}
}
}
}
}
2014-04-13 05:35:36 +04:00
return diff , nil
}
2015-12-02 09:10:13 +03:00
func GetDiffRange ( repoPath , beforeCommitID string , afterCommitID string , maxlines int ) ( * Diff , error ) {
2014-04-13 05:35:36 +04:00
repo , err := git . OpenRepository ( repoPath )
if err != nil {
return nil , err
}
2015-12-02 09:10:13 +03:00
commit , err := repo . GetCommit ( afterCommitID )
2014-04-13 05:35:36 +04:00
if err != nil {
return nil , err
}
2014-05-29 06:15:15 +04:00
var cmd * exec . Cmd
2014-08-26 16:20:18 +04:00
// if "after" commit given
2015-12-02 09:10:13 +03:00
if len ( beforeCommitID ) == 0 {
2014-08-26 16:20:18 +04:00
// First commit of repository.
if commit . ParentCount ( ) == 0 {
2015-12-02 09:10:13 +03:00
cmd = exec . Command ( "git" , "show" , afterCommitID )
2014-08-26 16:20:18 +04:00
} else {
c , _ := commit . Parent ( 0 )
2015-12-02 09:10:13 +03:00
cmd = exec . Command ( "git" , "diff" , "-M" , c . ID . String ( ) , afterCommitID )
2014-08-26 16:20:18 +04:00
}
2014-05-29 06:15:15 +04:00
} else {
2015-12-02 09:10:13 +03:00
cmd = exec . Command ( "git" , "diff" , "-M" , beforeCommitID , afterCommitID )
2014-04-13 05:35:36 +04:00
}
2014-05-29 06:15:15 +04:00
cmd . Dir = repoPath
cmd . Stderr = os . Stderr
2014-07-07 01:32:36 +04:00
2015-12-02 09:10:13 +03: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-07 01:32:36 +04:00
2015-12-02 09:10:13 +03:00
pid := process . Add ( fmt . Sprintf ( "GetDiffRange (%s)" , repoPath ) , cmd )
defer process . Remove ( pid )
diff , err := ParsePatch ( maxlines , stdout )
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 05:35:36 +04:00
}
2014-08-26 16:20:18 +04:00
2014-09-17 08:03:03 +04:00
func GetDiffCommit ( repoPath , commitId string , maxlines int ) ( * Diff , error ) {
return GetDiffRange ( repoPath , "" , commitId , maxlines )
2014-08-26 16:20:18 +04:00
}