Add Tabular Diff for CSV files (#14661)

Implements request #14320 The rendering of CSV files does match the diff style.

* Moved CSV logic into base package.

* Added method to create a tabular diff.

* Added CSV compare context.

* Added CSV diff template.

* Use new table style in CSV markup.

* Added file size limit for CSV rendering.

* Display CSV parser errors in diff.

* Lazy read single file.

* Lazy read rows for full diff.

* Added unit tests for various CSV changes.
This commit is contained in:
KN4CK3R 2021-03-29 22:44:28 +02:00 committed by GitHub
parent d3b8127ad3
commit 0c6137617f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 937 additions and 118 deletions

View file

@ -336,9 +336,8 @@ func Diff(ctx *context.Context) {
return
}
}
setImageCompareContext(ctx, parentCommit, commit)
headTarget := path.Join(userName, repoName)
setPathsCompareContext(ctx, parentCommit, commit, headTarget)
setCompareContext(ctx, parentCommit, commit, headTarget)
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
ctx.Data["Commit"] = commit
verification := models.ParseCommitWithSignature(commit)

View file

@ -6,14 +6,20 @@ package repo
import (
"bufio"
"encoding/csv"
"errors"
"fmt"
"html"
"io/ioutil"
"path"
"path/filepath"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context"
csv_module "code.gitea.io/gitea/modules/csv"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@ -26,6 +32,16 @@ const (
tplBlobExcerpt base.TplName = "repo/diff/blob_excerpt"
)
// setCompareContext sets context data.
func setCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headTarget string) {
ctx.Data["BaseCommit"] = base
ctx.Data["HeadCommit"] = head
setPathsCompareContext(ctx, base, head, headTarget)
setImageCompareContext(ctx, base, head)
setCsvCompareContext(ctx)
}
// setPathsCompareContext sets context data for source and raw paths
func setPathsCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headTarget string) {
sourcePath := setting.AppSubURL + "/%s/src/commit/%s"
@ -65,6 +81,73 @@ func setImageCompareContext(ctx *context.Context, base *git.Commit, head *git.Co
}
}
// setCsvCompareContext sets context data that is required by the CSV compare template
func setCsvCompareContext(ctx *context.Context) {
ctx.Data["IsCsvFile"] = func(diffFile *gitdiff.DiffFile) bool {
extension := strings.ToLower(filepath.Ext(diffFile.Name))
return extension == ".csv" || extension == ".tsv"
}
type CsvDiffResult struct {
Sections []*gitdiff.TableDiffSection
Error string
}
ctx.Data["CreateCsvDiff"] = func(diffFile *gitdiff.DiffFile, baseCommit *git.Commit, headCommit *git.Commit) CsvDiffResult {
if diffFile == nil || baseCommit == nil || headCommit == nil {
return CsvDiffResult{nil, ""}
}
errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))
csvReaderFromCommit := func(c *git.Commit) (*csv.Reader, error) {
blob, err := c.GetBlobByPath(diffFile.Name)
if err != nil {
return nil, err
}
if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < blob.Size() {
return nil, errTooLarge
}
reader, err := blob.DataAsync()
if err != nil {
return nil, err
}
defer reader.Close()
b, err := ioutil.ReadAll(reader)
if err != nil {
return nil, err
}
b = charset.ToUTF8WithFallback(b)
return csv_module.CreateReaderAndGuessDelimiter(b), nil
}
baseReader, err := csvReaderFromCommit(baseCommit)
if err == errTooLarge {
return CsvDiffResult{nil, err.Error()}
}
headReader, err := csvReaderFromCommit(headCommit)
if err == errTooLarge {
return CsvDiffResult{nil, err.Error()}
}
sections, err := gitdiff.CreateCsvDiff(diffFile, baseReader, headReader)
if err != nil {
errMessage, err := csv_module.FormatError(err, ctx.Locale)
if err != nil {
log.Error("RenderCsvDiff failed: %v", err)
return CsvDiffResult{nil, ""}
}
return CsvDiffResult{nil, errMessage}
}
return CsvDiffResult{sections, ""}
}
}
// ParseCompareInfo parse compare info between two commit for preparing comparing references
func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, *git.Repository, *git.CompareInfo, string, string) {
baseRepo := ctx.Repo.Repository
@ -490,9 +573,8 @@ func PrepareCompareDiff(
ctx.Data["Username"] = headUser.Name
ctx.Data["Reponame"] = headRepo.Name
setImageCompareContext(ctx, baseCommit, headCommit)
headTarget := path.Join(headUser.Name, repo.Name)
setPathsCompareContext(ctx, baseCommit, headCommit, headTarget)
setCompareContext(ctx, baseCommit, headCommit, headTarget)
return false
}

View file

@ -591,7 +591,6 @@ func ViewPullFiles(ctx *context.Context) {
gitRepo *git.Repository
)
var headTarget string
var prInfo *git.CompareInfo
if pull.HasMerged {
prInfo = PrepareMergedViewPullInfo(ctx, issue)
@ -618,7 +617,6 @@ func ViewPullFiles(ctx *context.Context) {
startCommitID = prInfo.MergeBase
endCommitID = headCommitID
headTarget = path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["AfterCommitID"] = endCommitID
@ -672,8 +670,8 @@ func ViewPullFiles(ctx *context.Context) {
}
}
setImageCompareContext(ctx, baseCommit, commit)
setPathsCompareContext(ctx, baseCommit, commit, headTarget)
headTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
setCompareContext(ctx, baseCommit, commit, headTarget)
ctx.Data["RequireHighlightJS"] = true
ctx.Data["RequireSimpleMDE"] = true