1634 lines · 49.3 KB · Go
RawDownload
// Copyright 2014 The Gogs Authors. All rights reserved.// Copyright 2019 The Gitea 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 gitdiff import (	"bufio"	"bytes"	"context"	"fmt"	"html"	"html/template"	"io"	"net/url"	"os"	"regexp"	"sort"	"strings"	"time" 	"code.gitea.io/gitea/models"	"code.gitea.io/gitea/models/db"	git_model "code.gitea.io/gitea/models/git"	pull_model "code.gitea.io/gitea/models/pull"	user_model "code.gitea.io/gitea/models/user"	"code.gitea.io/gitea/modules/analyze"	"code.gitea.io/gitea/modules/base"	"code.gitea.io/gitea/modules/charset"	"code.gitea.io/gitea/modules/git"	"code.gitea.io/gitea/modules/highlight"	"code.gitea.io/gitea/modules/lfs"	"code.gitea.io/gitea/modules/log"	"code.gitea.io/gitea/modules/setting" 	"github.com/sergi/go-diff/diffmatchpatch"	stdcharset "golang.org/x/net/html/charset"	"golang.org/x/text/encoding"	"golang.org/x/text/transform") // DiffLineType represents the type of a DiffLine.type DiffLineType uint8 // DiffLineType possible values.const (	DiffLinePlain DiffLineType = iota + 1	DiffLineAdd	DiffLineDel	DiffLineSection) // DiffFileType represents the type of a DiffFile.type DiffFileType uint8 // DiffFileType possible values.const (	DiffFileAdd DiffFileType = iota + 1	DiffFileChange	DiffFileDel	DiffFileRename	DiffFileCopy) // DiffLineExpandDirection represents the DiffLineSection expand directiontype DiffLineExpandDirection uint8 // DiffLineExpandDirection possible values.const (	DiffLineExpandNone DiffLineExpandDirection = iota + 1	DiffLineExpandSingle	DiffLineExpandUpDown	DiffLineExpandUp	DiffLineExpandDown) // DiffLine represents a line difference in a DiffSection.type DiffLine struct {	LeftIdx     int	RightIdx    int	Match       int	Type        DiffLineType	Content     string	Comments    []*models.Comment	SectionInfo *DiffLineSectionInfo} // DiffLineSectionInfo represents diff line section meta datatype DiffLineSectionInfo struct {	Path          string	LastLeftIdx   int	LastRightIdx  int	LeftIdx       int	RightIdx      int	LeftHunkSize  int	RightHunkSize int} // BlobExcerptChunkSize represent max lines of excerptconst BlobExcerptChunkSize = 20 // GetType returns the type of a DiffLine.func (d *DiffLine) GetType() int {	return int(d.Type)} // CanComment returns whether or not a line can get commentedfunc (d *DiffLine) CanComment() bool {	return len(d.Comments) == 0 && d.Type != DiffLineSection} // GetCommentSide returns the comment side of the first comment, if not set returns empty stringfunc (d *DiffLine) GetCommentSide() string {	if len(d.Comments) == 0 {		return ""	}	return d.Comments[0].DiffSide()} // GetLineTypeMarker returns the line type markerfunc (d *DiffLine) GetLineTypeMarker() string {	if strings.IndexByte(" +-", d.Content[0]) > -1 {		return d.Content[0:1]	}	return ""} // GetBlobExcerptQuery builds query string to get blob excerptfunc (d *DiffLine) GetBlobExcerptQuery() string {	query := fmt.Sprintf(		"last_left=%d&last_right=%d&"+			"left=%d&right=%d&"+			"left_hunk_size=%d&right_hunk_size=%d&"+			"path=%s",		d.SectionInfo.LastLeftIdx, d.SectionInfo.LastRightIdx,		d.SectionInfo.LeftIdx, d.SectionInfo.RightIdx,		d.SectionInfo.LeftHunkSize, d.SectionInfo.RightHunkSize,		url.QueryEscape(d.SectionInfo.Path))	return query} // GetExpandDirection gets DiffLineExpandDirectionfunc (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {	if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 {		return DiffLineExpandNone	}	if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 {		return DiffLineExpandUp	} else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 {		return DiffLineExpandUpDown	} else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 {		return DiffLineExpandDown	}	return DiffLineExpandSingle} func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {	leftLine, leftHunk, rightLine, righHunk := git.ParseDiffHunkString(line) 	return &DiffLineSectionInfo{		Path:          treePath,		LastLeftIdx:   lastLeftIdx,		LastRightIdx:  lastRightIdx,		LeftIdx:       leftLine,		RightIdx:      rightLine,		LeftHunkSize:  leftHunk,		RightHunkSize: righHunk,	}} // escape a line's content or return <br> needed for copy/paste purposesfunc getLineContent(content string) DiffInline {	if len(content) > 0 {		return DiffInlineWithUnicodeEscape(template.HTML(html.EscapeString(content)))	}	return DiffInline{Content: "<br>"}} // DiffSection represents a section of a DiffFile.type DiffSection struct {	file     *DiffFile	FileName string	Name     string	Lines    []*DiffLine} var (	addedCodePrefix   = []byte(`<span class="added-code">`)	removedCodePrefix = []byte(`<span class="removed-code">`)	codeTagSuffix     = []byte(`</span>`)) var (	unfinishedtagRegex = regexp.MustCompile(`<[^>]*$`)	trailingSpanRegex  = regexp.MustCompile(`<span\s*[[:alpha:]="]*?[>]?$`)	entityRegex        = regexp.MustCompile(`&[#]*?[0-9[:alpha:]]*$`)) // shouldWriteInline represents combinations where we manually write inline changesfunc shouldWriteInline(diff diffmatchpatch.Diff, lineType DiffLineType) bool {	if true &&		diff.Type == diffmatchpatch.DiffEqual ||		diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd ||		diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel {		return true	}	return false} func fixupBrokenSpans(diffs []diffmatchpatch.Diff) []diffmatchpatch.Diff {	// Create a new array to store our fixed up blocks	fixedup := make([]diffmatchpatch.Diff, 0, len(diffs)) 	// semantically label some numbers	const insert, delete, equal = 0, 1, 2 	// record the positions of the last type of each block in the fixedup blocks	last := []int{-1, -1, -1}	operation := []diffmatchpatch.Operation{diffmatchpatch.DiffInsert, diffmatchpatch.DiffDelete, diffmatchpatch.DiffEqual} 	// create a writer for insert and deletes	toWrite := []strings.Builder{		{},		{},	} 	// make some flags for insert and delete	unfinishedTag := []bool{false, false}	unfinishedEnt := []bool{false, false} 	// store stores the provided text in the writer for the typ	store := func(text string, typ int) {		(&(toWrite[typ])).WriteString(text)	} 	// hasStored returns true if there is stored content	hasStored := func(typ int) bool {		return (&toWrite[typ]).Len() > 0	} 	// stored will return that content	stored := func(typ int) string {		return (&toWrite[typ]).String()	} 	// empty will empty the stored content	empty := func(typ int) {		(&toWrite[typ]).Reset()	} 	// pop will remove the stored content appending to a diff block for that typ	pop := func(typ int, fixedup []diffmatchpatch.Diff) []diffmatchpatch.Diff {		if hasStored(typ) {			if last[typ] > last[equal] {				fixedup[last[typ]].Text += stored(typ)			} else {				fixedup = append(fixedup, diffmatchpatch.Diff{					Type: operation[typ],					Text: stored(typ),				})			}			empty(typ)		}		return fixedup	} 	// Now we walk the provided diffs and check the type of each block in turn	for _, diff := range diffs { 		typ := delete // flag for handling insert or delete typs		switch diff.Type {		case diffmatchpatch.DiffEqual:			// First check if there is anything stored			if hasStored(insert) || hasStored(delete) {				// There are two reasons for storing content:				// 1. Unfinished Entity <- Could be more efficient here by not doing this if we're looking for a tag				if unfinishedEnt[insert] || unfinishedEnt[delete] {					// we look for a ';' to finish an entity					idx := strings.IndexRune(diff.Text, ';')					if idx >= 0 {						// if we find a ';' store the preceding content to both insert and delete						store(diff.Text[:idx+1], insert)						store(diff.Text[:idx+1], delete) 						// and remove it from this block						diff.Text = diff.Text[idx+1:] 						// reset the ent flags						unfinishedEnt[insert] = false						unfinishedEnt[delete] = false					} else {						// otherwise store it all on insert and delete						store(diff.Text, insert)						store(diff.Text, delete)						// and empty this block						diff.Text = ""					}				}				// 2. Unfinished Tag				if unfinishedTag[insert] || unfinishedTag[delete] {					// we look for a '>' to finish a tag					idx := strings.IndexRune(diff.Text, '>')					if idx >= 0 {						store(diff.Text[:idx+1], insert)						store(diff.Text[:idx+1], delete)						diff.Text = diff.Text[idx+1:]						unfinishedTag[insert] = false						unfinishedTag[delete] = false					} else {						store(diff.Text, insert)						store(diff.Text, delete)						diff.Text = ""					}				} 				// If we've completed the required tag/entities				if !(unfinishedTag[insert] || unfinishedTag[delete] || unfinishedEnt[insert] || unfinishedEnt[delete]) {					// pop off the stack					fixedup = pop(insert, fixedup)					fixedup = pop(delete, fixedup)				} 				// If that has left this diff block empty then shortcut				if len(diff.Text) == 0 {					continue				}			} 			// check if this block ends in an unfinished tag?			idx := unfinishedtagRegex.FindStringIndex(diff.Text)			if idx != nil {				unfinishedTag[insert] = true				unfinishedTag[delete] = true			} else {				// otherwise does it end in an unfinished entity?				idx = entityRegex.FindStringIndex(diff.Text)				if idx != nil {					unfinishedEnt[insert] = true					unfinishedEnt[delete] = true				}			} 			// If there is an unfinished component			if idx != nil {				// Store the fragment				store(diff.Text[idx[0]:], insert)				store(diff.Text[idx[0]:], delete)				// and remove it from this block				diff.Text = diff.Text[:idx[0]]			} 			// If that hasn't left the block empty			if len(diff.Text) > 0 {				// store the position of the last equal block and store it in our diffs				last[equal] = len(fixedup)				fixedup = append(fixedup, diff)			}			continue		case diffmatchpatch.DiffInsert:			typ = insert			fallthrough		case diffmatchpatch.DiffDelete:			// First check if there is anything stored for this type			if hasStored(typ) {				// if there is prepend it to this block, empty the storage and reset our flags				diff.Text = stored(typ) + diff.Text				empty(typ)				unfinishedEnt[typ] = false				unfinishedTag[typ] = false			} 			// check if this block ends in an unfinished tag			idx := unfinishedtagRegex.FindStringIndex(diff.Text)			if idx != nil {				unfinishedTag[typ] = true			} else {				// otherwise does it end in an unfinished entity				idx = entityRegex.FindStringIndex(diff.Text)				if idx != nil {					unfinishedEnt[typ] = true				}			} 			// If there is an unfinished component			if idx != nil {				// Store the fragment				store(diff.Text[idx[0]:], typ)				// and remove it from this block				diff.Text = diff.Text[:idx[0]]			} 			// If that hasn't left the block empty			if len(diff.Text) > 0 {				// if the last block of this type was after the last equal block				if last[typ] > last[equal] {					// store this blocks content on that block					fixedup[last[typ]].Text += diff.Text				} else {					// otherwise store the position of the last block of this type and store the block					last[typ] = len(fixedup)					fixedup = append(fixedup, diff)				}			}			continue		}	} 	// pop off any remaining stored content	fixedup = pop(insert, fixedup)	fixedup = pop(delete, fixedup) 	return fixedup} func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineType) DiffInline {	buf := bytes.NewBuffer(nil)	match := "" 	diffs = fixupBrokenSpans(diffs) 	for _, diff := range diffs {		if shouldWriteInline(diff, lineType) {			if len(match) > 0 {				diff.Text = match + diff.Text				match = ""			}			// Chroma HTML syntax highlighting is done before diffing individual lines in order to maintain consistency.			// Since inline changes might split in the middle of a chroma span tag or HTML entity, make we manually put it back together			// before writing so we don't try insert added/removed code spans in the middle of one of those			// and create broken HTML. This is done by moving incomplete HTML forward until it no longer matches our pattern of			// a line ending with an incomplete HTML entity or partial/opening <span>. 			// EX:			// diffs[{Type: dmp.DiffDelete, Text: "language</span><span "},			// {Type: dmp.DiffEqual, Text: "c"},			// {Type: dmp.DiffDelete, Text: "lass="p">}] 			// After first iteration			// diffs[{Type: dmp.DiffDelete, Text: "language</span>"}, //write out			// {Type: dmp.DiffEqual, Text: "<span c"},			// {Type: dmp.DiffDelete, Text: "lass="p">,</span>}] 			// After second iteration			// {Type: dmp.DiffEqual, Text: ""}, // write out			// {Type: dmp.DiffDelete, Text: "<span class="p">,</span>}] 			// Final			// {Type: dmp.DiffDelete, Text: "<span class="p">,</span>}]			// end up writing <span class="removed-code"><span class="p">,</span></span>			// Instead of <span class="removed-code">lass="p",</span></span> 			m := trailingSpanRegex.FindStringSubmatchIndex(diff.Text)			if m != nil {				match = diff.Text[m[0]:m[1]]				diff.Text = strings.TrimSuffix(diff.Text, match)			}			m = entityRegex.FindStringSubmatchIndex(diff.Text)			if m != nil {				match = diff.Text[m[0]:m[1]]				diff.Text = strings.TrimSuffix(diff.Text, match)			}			// Print an existing closing span first before opening added/remove-code span so it doesn't unintentionally close it			if strings.HasPrefix(diff.Text, "</span>") {				buf.WriteString("</span>")				diff.Text = strings.TrimPrefix(diff.Text, "</span>")			}			// If we weren't able to fix it then this should avoid broken HTML by not inserting more spans below			// The previous/next diff section will contain the rest of the tag that is missing here			if strings.Count(diff.Text, "<") != strings.Count(diff.Text, ">") {				buf.WriteString(diff.Text)				continue			}		}		switch {		case diff.Type == diffmatchpatch.DiffEqual:			buf.WriteString(diff.Text)		case diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd:			buf.Write(addedCodePrefix)			buf.WriteString(diff.Text)			buf.Write(codeTagSuffix)		case diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel:			buf.Write(removedCodePrefix)			buf.WriteString(diff.Text)			buf.Write(codeTagSuffix)		}	}	return DiffInlineWithUnicodeEscape(template.HTML(buf.String()))} // GetLine gets a 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:

Lines 1–500 of 1634.