| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 | // 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 modelsimport (	"bufio"	"bytes"	"fmt"	"html"	"html/template"	"io"	"io/ioutil"	"os"	"os/exec"	"strings"	"github.com/Unknwon/com"	"github.com/sergi/go-diff/diffmatchpatch"	"golang.org/x/net/html/charset"	"golang.org/x/text/transform"	log "gopkg.in/clog.v1"	"github.com/gogits/git-module"	"github.com/gogits/gogs/modules/base"	"github.com/gogits/gogs/modules/process"	"github.com/gogits/gogs/modules/setting"	"github.com/gogits/gogs/modules/template/highlight")type DiffLineType uint8const (	DIFF_LINE_PLAIN DiffLineType = iota + 1	DIFF_LINE_ADD	DIFF_LINE_DEL	DIFF_LINE_SECTION)type DiffFileType uint8const (	DIFF_FILE_ADD DiffFileType = iota + 1	DIFF_FILE_CHANGE	DIFF_FILE_DEL	DIFF_FILE_RENAME)type DiffLine struct {	LeftIdx  int	RightIdx int	Type     DiffLineType	Content  string}func (d *DiffLine) GetType() int {	return int(d.Type)}type DiffSection struct {	Name  string	Lines []*DiffLine}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 {	buf := bytes.NewBuffer(nil)	// Reproduce signs which are cutted for inline diff before.	switch lineType {	case DIFF_LINE_ADD:		buf.WriteByte('+')	case DIFF_LINE_DEL:		buf.WriteByte('-')	}	for i := range diffs {		switch {		case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == DIFF_LINE_ADD:			buf.Write(addedCodePrefix)			buf.WriteString(html.EscapeString(diffs[i].Text))			buf.Write(codeTagSuffix)		case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == DIFF_LINE_DEL:			buf.Write(removedCodePrefix)			buf.WriteString(html.EscapeString(diffs[i].Text))			buf.Write(codeTagSuffix)		case diffs[i].Type == diffmatchpatch.DiffEqual:			buf.WriteString(html.EscapeString(diffs[i].Text))		}	}	return template.HTML(buf.Bytes())}// get an specific line by type (add or del) and file line numberfunc (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine {	var (		difference    = 0		addCount      = 0		delCount      = 0		matchDiffLine *DiffLine	)LOOP:	for _, diffLine := range diffSection.Lines {		switch diffLine.Type {		case DIFF_LINE_ADD:			addCount++		case DIFF_LINE_DEL:			delCount++		default:			if matchDiffLine != nil {				break LOOP			}			difference = diffLine.RightIdx - diffLine.LeftIdx			addCount = 0			delCount = 0		}		switch lineType {		case DIFF_LINE_DEL:			if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference {				matchDiffLine = diffLine			}		case DIFF_LINE_ADD:			if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference {				matchDiffLine = diffLine			}		}	}	if addCount == delCount {		return matchDiffLine	}	return nil}var diffMatchPatch = diffmatchpatch.New()func init() {	diffMatchPatch.DiffEditCost = 100}// computes inline diff for the given linefunc (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML {	if setting.Git.DisableDiffHighlight {		return template.HTML(html.EscapeString(diffLine.Content[1:]))	}	var (		compareDiffLine *DiffLine		diff1           string		diff2           string	)	// try to find equivalent diff line. ignore, otherwise	switch diffLine.Type {	case DIFF_LINE_ADD:		compareDiffLine = diffSection.GetLine(DIFF_LINE_DEL, diffLine.RightIdx)		if compareDiffLine == nil {			return template.HTML(html.EscapeString(diffLine.Content))		}		diff1 = compareDiffLine.Content		diff2 = diffLine.Content	case DIFF_LINE_DEL:		compareDiffLine = diffSection.GetLine(DIFF_LINE_ADD, diffLine.LeftIdx)		if compareDiffLine == nil {			return template.HTML(html.EscapeString(diffLine.Content))		}		diff1 = diffLine.Content		diff2 = compareDiffLine.Content	default:		return template.HTML(html.EscapeString(diffLine.Content))	}	diffRecord := diffMatchPatch.DiffMain(diff1[1:], diff2[1:], true)	diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)	return diffToHTML(diffRecord, diffLine.Type)}type DiffFile struct {	Name               string	OldName            string	Index              int	Addition, Deletion int	Type               DiffFileType	IsCreated          bool	IsDeleted          bool	IsBin              bool	IsRenamed          bool	IsSubmodule        bool	Sections           []*DiffSection	IsIncomplete       bool}func (diffFile *DiffFile) GetType() int {	return int(diffFile.Type)}func (diffFile *DiffFile) GetHighlightClass() string {	return highlight.FileNameToHighlightClass(diffFile.Name)}type Diff struct {	TotalAddition, TotalDeletion int	Files                        []*DiffFile	IsIncomplete                 bool}func (diff *Diff) NumFiles() int {	return len(diff.Files)}const DIFF_HEAD = "diff --git "// TODO: move this function to gogits/git-modulefunc ParsePatch(maxLines, maxLineCharacteres, maxFiles int, reader io.Reader) (*Diff, error) {	var (		diff = &Diff{Files: make([]*DiffFile, 0)}		curFile    *DiffFile		curSection = &DiffSection{			Lines: make([]*DiffLine, 0, 10),		}		leftLine, rightLine int		lineCount           int		curFileLinesCount   int	)	input := bufio.NewReader(reader)	isEOF := false	for !isEOF {		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]		}		if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") || len(line) == 0 {			continue		}		curFileLinesCount++		lineCount++		// Diff data too large, we only show the first about maxlines lines		if curFileLinesCount >= maxLines || len(line) >= maxLineCharacteres {			curFile.IsIncomplete = true		}		switch {		case line[0] == ' ':			diffLine := &DiffLine{Type: DIFF_LINE_PLAIN, Content: line, LeftIdx: leftLine, RightIdx: rightLine}			leftLine++			rightLine++			curSection.Lines = append(curSection.Lines, diffLine)			continue		case line[0] == '@':			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.			ranges := strings.Split(ss[1][1:], " ")			leftLine, _ = com.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int()			if len(ranges) > 1 {				rightLine, _ = com.StrTo(strings.Split(ranges[1], ",")[0]).Int()			} else {				log.Warn("Parse line number failed: %v", line)				rightLine = leftLine			}			continue		case line[0] == '+':			curFile.Addition++			diff.TotalAddition++			diffLine := &DiffLine{Type: DIFF_LINE_ADD, Content: line, RightIdx: rightLine}			rightLine++			curSection.Lines = append(curSection.Lines, diffLine)			continue		case line[0] == '-':			curFile.Deletion++			diff.TotalDeletion++			diffLine := &DiffLine{Type: DIFF_LINE_DEL, Content: line, LeftIdx: leftLine}			if leftLine > 0 {				leftLine++			}			curSection.Lines = append(curSection.Lines, diffLine)		case strings.HasPrefix(line, "Binary"):			curFile.IsBin = true			continue		}		// Get new file.		if strings.HasPrefix(line, DIFF_HEAD) {			middle := -1			// 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)] == '"'			if hasQuote {				middle = strings.Index(line, ` "b/`)			} else {				middle = strings.Index(line, " b/")			}			beg := len(DIFF_HEAD)			a := line[beg+2 : middle]			b := line[middle+3:]			if hasQuote {				a = string(git.UnescapeChars([]byte(a[1 : len(a)-1])))				b = string(git.UnescapeChars([]byte(b[1 : len(b)-1])))			}			curFile = &DiffFile{				Name:     a,				Index:    len(diff.Files) + 1,				Type:     DIFF_FILE_CHANGE,				Sections: make([]*DiffSection, 0, 10),			}			diff.Files = append(diff.Files, curFile)			if len(diff.Files) >= maxFiles {				diff.IsIncomplete = true				io.Copy(ioutil.Discard, reader)				break			}			curFileLinesCount = 0			// Check file diff type and is submodule.			for {				line, err := input.ReadString('\n')				if err != nil {					if err == io.EOF {						isEOF = true					} else {						return nil, fmt.Errorf("ReadString: %v", err)					}				}				switch {				case strings.HasPrefix(line, "new file"):					curFile.Type = DIFF_FILE_ADD					curFile.IsCreated = true				case strings.HasPrefix(line, "deleted"):					curFile.Type = DIFF_FILE_DEL					curFile.IsDeleted = true				case strings.HasPrefix(line, "index"):					curFile.Type = DIFF_FILE_CHANGE				case strings.HasPrefix(line, "similarity index 100%"):					curFile.Type = DIFF_FILE_RENAME					curFile.IsRenamed = true					curFile.OldName = curFile.Name					curFile.Name = b				}				if curFile.Type > 0 {					if strings.HasSuffix(line, " 160000\n") {						curFile.IsSubmodule = true					}					break				}			}		}	}	// FIXME: detect encoding while parsing.	var buf bytes.Buffer	for _, f := range diff.Files {		buf.Reset()		for _, sec := range f.Sections {			for _, l := range sec.Lines {				buf.WriteString(l.Content)				buf.WriteString("\n")			}		}		charsetLabel, err := base.DetectEncoding(buf.Bytes())		if charsetLabel != "UTF-8" && err == nil {			encoding, _ := charset.Lookup(charsetLabel)			if encoding != nil {				d := encoding.NewDecoder()				for _, sec := range f.Sections {					for _, l := range sec.Lines {						if c, _, err := transform.String(d, l.Content); err == nil {							l.Content = c						}					}				}			}		}	}	return diff, nil}func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {	gitRepo, err := git.OpenRepository(repoPath)	if err != nil {		return nil, err	}	commit, err := gitRepo.GetCommit(afterCommitID)	if err != nil {		return nil, err	}	var cmd *exec.Cmd	// if "after" commit given	if len(beforeCommitID) == 0 {		// First commit of repository.		if commit.ParentCount() == 0 {			cmd = exec.Command("git", "show", afterCommitID)		} else {			c, _ := commit.Parent(0)			cmd = exec.Command("git", "diff", "-M", c.ID.String(), afterCommitID)		}	} else {		cmd = exec.Command("git", "diff", "-M", beforeCommitID, afterCommitID)	}	cmd.Dir = repoPath	cmd.Stderr = os.Stderr	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)	}	pid := process.Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cmd)	defer process.Remove(pid)	diff, err := ParsePatch(maxLines, maxLineCharacteres, maxFiles, 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}type RawDiffType stringconst (	RAW_DIFF_NORMAL RawDiffType = "diff"	RAW_DIFF_PATCH  RawDiffType = "patch")// GetRawDiff dumps diff results of repository in given commit ID to io.Writer.// TODO: move this function to gogits/git-modulefunc GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Writer) error {	repo, err := git.OpenRepository(repoPath)	if err != nil {		return fmt.Errorf("OpenRepository: %v", err)	}	commit, err := repo.GetCommit(commitID)	if err != nil {		return fmt.Errorf("GetCommit: %v", err)	}	var cmd *exec.Cmd	switch diffType {	case RAW_DIFF_NORMAL:		if commit.ParentCount() == 0 {			cmd = exec.Command("git", "show", commitID)		} else {			c, _ := commit.Parent(0)			cmd = exec.Command("git", "diff", "-M", c.ID.String(), commitID)		}	case RAW_DIFF_PATCH:		if commit.ParentCount() == 0 {			cmd = exec.Command("git", "format-patch", "--no-signature", "--stdout", "--root", commitID)		} else {			c, _ := commit.Parent(0)			query := fmt.Sprintf("%s...%s", commitID, c.ID.String())			cmd = exec.Command("git", "format-patch", "--no-signature", "--stdout", query)		}	default:		return fmt.Errorf("invalid diffType: %s", diffType)	}	stderr := new(bytes.Buffer)	cmd.Dir = repoPath	cmd.Stdout = writer	cmd.Stderr = stderr	if err = cmd.Run(); err != nil {		return fmt.Errorf("Run: %v - %s", err, stderr)	}	return nil}func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {	return GetDiffRange(repoPath, "", commitID, maxLines, maxLineCharacteres, maxFiles)}
 |