Adds side-by-side diff for images (#6784)

* Adds side-by-side diff for images

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Explain blank imports

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Use complete word for width and height labels on image compare

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Update index.css from master

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Moves ImageInfo to git commit file

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Assign ImageInfo function for template and sets correct target for BeforeSourcePath

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Adds missing comment

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Return error if ImageInfo failed

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Avoid template panic when ImageInfo failed for some reason

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Show file size on image diff

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Removes unused helper function

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Reverts copyright year change

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Close file reader

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Update commit.go

Sets correct data key

* Moves reader.Close() up a few lines

* Updates index.css

* Updates CSS file

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Transfers adjustments for image compare to compare.go file

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Adjusts variable name

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Apply lesshint recommendations

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Do not show old image on image compare if it is not in index of base commit

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>

* Change file size text

Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>
This commit is contained in:
Mario Lubenka 2019-09-16 11:03:22 +02:00 committed by Lunny Xiao
parent a5f87feefd
commit a37236314c
10 changed files with 262 additions and 15 deletions

View file

@ -10,6 +10,11 @@ import (
"bytes" "bytes"
"container/list" "container/list"
"fmt" "fmt"
"image"
"image/color"
_ "image/gif" // for processing gif images
_ "image/jpeg" // for processing jpeg images
_ "image/png" // for processing png images
"io" "io"
"net/http" "net/http"
"strconv" "strconv"
@ -158,6 +163,43 @@ func (c *Commit) IsImageFile(name string) bool {
return isImage return isImage
} }
// ImageMetaData represents metadata of an image file
type ImageMetaData struct {
ColorModel color.Model
Width int
Height int
ByteSize int64
}
// ImageInfo returns information about the dimensions of an image
func (c *Commit) ImageInfo(name string) (*ImageMetaData, error) {
if !c.IsImageFile(name) {
return nil, nil
}
blob, err := c.GetBlobByPath(name)
if err != nil {
return nil, err
}
reader, err := blob.DataAsync()
if err != nil {
return nil, err
}
defer reader.Close()
config, _, err := image.DecodeConfig(reader)
if err != nil {
return nil, err
}
metadata := ImageMetaData{
ColorModel: config.ColorModel,
Width: config.Width,
Height: config.Height,
ByteSize: blob.Size(),
}
return &metadata, nil
}
// GetCommitByPath return the commit of relative path object. // GetCommitByPath return the commit of relative path object.
func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) { func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) {
return c.repo.getCommitByPathWithID(c.ID, relpath) return c.repo.getCommitByPathWithID(c.ID, relpath)
@ -310,6 +352,13 @@ func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, erro
return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String()) return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
} }
// HasFile returns true if the file given exists on this commit
// This does only mean it's there - it does not mean the file was changed during the commit.
func (c *Commit) HasFile(filename string) (bool, error) {
result, err := c.repo.LsFiles(filename)
return result[0] == filename, err
}
// GetSubModules get all the sub modules of current revision git tree // GetSubModules get all the sub modules of current revision git tree
func (c *Commit) GetSubModules() (*ObjectCache, error) { func (c *Commit) GetSubModules() (*ObjectCache, error) {
if c.submoduleCache != nil { if c.submoduleCache != nil {

View file

@ -1358,6 +1358,11 @@ diff.whitespace_ignore_at_eol = Ignore changes in whitespace at EOL
diff.stats_desc = <strong> %d changed files</strong> with <strong>%d additions</strong> and <strong>%d deletions</strong> diff.stats_desc = <strong> %d changed files</strong> with <strong>%d additions</strong> and <strong>%d deletions</strong>
diff.bin = BIN diff.bin = BIN
diff.view_file = View File diff.view_file = View File
diff.file_before = Before
diff.file_after = After
diff.file_image_width = Width
diff.file_image_height = Height
diff.file_byte_size = Size
diff.file_suppressed = File diff suppressed because it is too large diff.file_suppressed = File diff suppressed because it is too large
diff.too_many_files = Some files were not shown because too many files changed in this diff diff.too_many_files = Some files were not shown because too many files changed in this diff
diff.comment.placeholder = Leave a comment diff.comment.placeholder = Leave a comment

View file

@ -140,6 +140,16 @@ a{cursor:pointer}
.ui .migrate{color:#888!important;opacity:.5} .ui .migrate{color:#888!important;opacity:.5}
.ui .migrate a{color:#444!important} .ui .migrate a{color:#444!important}
.ui .migrate a:hover{color:#000!important} .ui .migrate a:hover{color:#000!important}
.ui .border{border:1px solid}
.ui .border.red{border-color:#d95c5c!important}
.ui .border.blue{border-color:#428bca!important}
.ui .border.black{border-color:#444}
.ui .border.grey{border-color:#767676!important}
.ui .border.light.grey{border-color:#888!important}
.ui .border.green{border-color:#6cc644!important}
.ui .border.purple{border-color:#6e5494!important}
.ui .border.yellow{border-color:#fbbd08!important}
.ui .border.gold{border-color:#a1882b!important}
.ui .branch-tag-choice{line-height:20px} .ui .branch-tag-choice{line-height:20px}
@media only screen and (max-width:767px){.ui.pagination.menu .item.navigation span.navigation_label,.ui.pagination.menu .item:not(.active):not(.navigation){display:none} @media only screen and (max-width:767px){.ui.pagination.menu .item.navigation span.navigation_label,.ui.pagination.menu .item:not(.active):not(.navigation){display:none}
} }
@ -670,6 +680,7 @@ footer .ui.left,footer .ui.right{line-height:40px}
.repository .diff-file-box .code-diff td{padding:0 0 0 10px!important;border-top:0} .repository .diff-file-box .code-diff td{padding:0 0 0 10px!important;border-top:0}
.repository .diff-file-box .code-diff .lines-num{border-color:#d4d4d5;border-right-width:1px;border-right-style:solid;padding:0 5px!important} .repository .diff-file-box .code-diff .lines-num{border-color:#d4d4d5;border-right-width:1px;border-right-style:solid;padding:0 5px!important}
.repository .diff-file-box .code-diff tbody tr td.halfwidth{width:49%} .repository .diff-file-box .code-diff tbody tr td.halfwidth{width:49%}
.repository .diff-file-box .code-diff tbody tr td.center{text-align:center}
.repository .diff-file-box .code-diff tbody tr .removed-code{background-color:#f99} .repository .diff-file-box .code-diff tbody tr .removed-code{background-color:#f99}
.repository .diff-file-box .code-diff tbody tr .added-code{background-color:#9f9} .repository .diff-file-box .code-diff tbody tr .added-code{background-color:#9f9}
.repository .diff-file-box .code-diff tbody tr [data-line-num]::before{content:attr(data-line-num);text-align:right} .repository .diff-file-box .code-diff tbody tr [data-line-num]::before{content:attr(data-line-num);text-align:right}

View file

@ -600,6 +600,45 @@ code,
} }
} }
.border {
border: 1px solid;
&.red {
border-color: #d95c5c !important;
}
&.blue {
border-color: #428bca !important;
}
&.black {
border-color: #444444;
}
&.grey {
border-color: #767676 !important;
}
&.light.grey {
border-color: #888888 !important;
}
&.green {
border-color: #6cc644 !important;
}
&.purple {
border-color: #6e5494 !important;
}
&.yellow {
border-color: #fbbd08 !important;
}
&.gold {
border-color: #a1882b !important;
}
}
.branch-tag-choice { .branch-tag-choice {
line-height: 20px; line-height: 20px;
} }

View file

@ -1364,6 +1364,10 @@
width: 49%; width: 49%;
} }
td.center {
text-align: center;
}
.removed-code { .removed-code {
background-color: #ff9999; background-color: #ff9999;
} }

View file

@ -240,6 +240,23 @@ func Diff(ctx *context.Context) {
ctx.Data["Username"] = userName ctx.Data["Username"] = userName
ctx.Data["Reponame"] = repoName ctx.Data["Reponame"] = repoName
ctx.Data["IsImageFile"] = commit.IsImageFile ctx.Data["IsImageFile"] = commit.IsImageFile
ctx.Data["ImageInfo"] = func(name string) *git.ImageMetaData {
result, err := commit.ImageInfo(name)
if err != nil {
log.Error("ImageInfo failed: %v", err)
return nil
}
return result
}
ctx.Data["ImageInfoBase"] = ctx.Data["ImageInfo"]
if commit.ParentCount() > 0 {
parentCommit, err := ctx.Repo.GitRepo.GetCommit(parents[0])
if err != nil {
ctx.NotFound("GetParentCommit", err)
return
}
ctx.Data["ImageInfo"] = parentCommit.ImageInfo
}
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID) ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
ctx.Data["Commit"] = commit ctx.Data["Commit"] = commit
ctx.Data["Verification"] = models.ParseCommitWithSignature(commit) ctx.Data["Verification"] = models.ParseCommitWithSignature(commit)
@ -248,6 +265,7 @@ func Diff(ctx *context.Context) {
ctx.Data["Parents"] = parents ctx.Data["Parents"] = parents
ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0 ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0
ctx.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", "commit", commitID) ctx.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", "commit", commitID)
ctx.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "raw", "commit", commitID)
note := &git.Note{} note := &git.Note{}
err = git.GetNote(ctx.Repo.GitRepo, commitID, note) err = git.GetNote(ctx.Repo.GitRepo, commitID, note)
@ -259,8 +277,8 @@ func Diff(ctx *context.Context) {
if commit.ParentCount() > 0 { if commit.ParentCount() > 0 {
ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", "commit", parents[0]) ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", "commit", parents[0])
ctx.Data["BeforeRawPath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "raw", "commit", parents[0])
} }
ctx.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "raw", "commit", commitID)
ctx.Data["BranchName"], err = commit.GetBranchName() ctx.Data["BranchName"], err = commit.GetBranchName()
if err != nil { if err != nil {
ctx.ServerError("commit.GetBranchName", err) ctx.ServerError("commit.GetBranchName", err)

View file

@ -247,6 +247,26 @@ func PrepareCompareDiff(
return false return false
} }
baseGitRepo := ctx.Repo.GitRepo
baseCommitID := baseBranch
if ctx.Data["BaseIsCommit"] == false {
if ctx.Data["BaseIsTag"] == true {
baseCommitID, err = baseGitRepo.GetTagCommitID(baseBranch)
} else {
baseCommitID, err = baseGitRepo.GetBranchCommitID(baseBranch)
}
if err != nil {
ctx.ServerError("GetRefCommitID", err)
return false
}
}
baseCommit, err := baseGitRepo.GetCommit(baseCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return false
}
compareInfo.Commits = models.ValidateCommitsWithEmails(compareInfo.Commits) compareInfo.Commits = models.ValidateCommitsWithEmails(compareInfo.Commits)
compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits) compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits)
compareInfo.Commits = models.ParseCommitsWithStatus(compareInfo.Commits, headRepo) compareInfo.Commits = models.ParseCommitsWithStatus(compareInfo.Commits, headRepo)
@ -272,11 +292,43 @@ func PrepareCompareDiff(
ctx.Data["Username"] = headUser.Name ctx.Data["Username"] = headUser.Name
ctx.Data["Reponame"] = headRepo.Name ctx.Data["Reponame"] = headRepo.Name
ctx.Data["IsImageFile"] = headCommit.IsImageFile ctx.Data["IsImageFile"] = headCommit.IsImageFile
ctx.Data["ImageInfo"] = func(name string) *git.ImageMetaData {
result, err := headCommit.ImageInfo(name)
if err != nil {
log.Error("ImageInfo failed: %v", err)
return nil
}
return result
}
ctx.Data["FileExistsInBaseCommit"] = func(filename string) bool {
result, err := baseCommit.HasFile(filename)
if err != nil {
log.Error(
"Error while checking if file \"%s\" exists in base commit \"%s\" (repo: %s): %v",
filename,
baseCommit,
baseGitRepo.Path,
err)
return false
}
return result
}
ctx.Data["ImageInfoBase"] = func(name string) *git.ImageMetaData {
result, err := baseCommit.ImageInfo(name)
if err != nil {
log.Error("ImageInfo failed: %v", err)
return nil
}
return result
}
headTarget := path.Join(headUser.Name, repo.Name) headTarget := path.Join(headUser.Name, repo.Name)
baseTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
ctx.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", "commit", headCommitID) ctx.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", "commit", headCommitID)
ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", "commit", compareInfo.MergeBase)
ctx.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(headTarget, "raw", "commit", headCommitID) ctx.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(headTarget, "raw", "commit", headCommitID)
ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(baseTarget, "src", "commit", baseCommitID)
ctx.Data["BeforeRawPath"] = setting.AppSubURL + "/" + path.Join(baseTarget, "raw", "commit", baseCommitID)
return false return false
} }

View file

@ -535,6 +535,11 @@ func ViewPullFiles(ctx *context.Context) {
ctx.Data["Diff"] = diff ctx.Data["Diff"] = diff
ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0 ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0
baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
commit, err := gitRepo.GetCommit(endCommitID) commit, err := gitRepo.GetCommit(endCommitID)
if err != nil { if err != nil {
ctx.ServerError("GetCommit", err) ctx.ServerError("GetCommit", err)
@ -542,9 +547,29 @@ func ViewPullFiles(ctx *context.Context) {
} }
ctx.Data["IsImageFile"] = commit.IsImageFile ctx.Data["IsImageFile"] = commit.IsImageFile
ctx.Data["ImageInfoBase"] = func(name string) *git.ImageMetaData {
result, err := baseCommit.ImageInfo(name)
if err != nil {
log.Error("ImageInfo failed: %v", err)
return nil
}
return result
}
ctx.Data["ImageInfo"] = func(name string) *git.ImageMetaData {
result, err := commit.ImageInfo(name)
if err != nil {
log.Error("ImageInfo failed: %v", err)
return nil
}
return result
}
baseTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
ctx.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", "commit", endCommitID) ctx.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", "commit", endCommitID)
ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", "commit", startCommitID)
ctx.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(headTarget, "raw", "commit", endCommitID) ctx.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(headTarget, "raw", "commit", endCommitID)
ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(baseTarget, "src", "commit", startCommitID)
ctx.Data["BeforeRawPath"] = setting.AppSubURL + "/" + path.Join(baseTarget, "raw", "commit", startCommitID)
ctx.Data["RequireHighlightJS"] = true ctx.Data["RequireHighlightJS"] = true
ctx.Data["RequireTribute"] = true ctx.Data["RequireTribute"] = true
if ctx.Data["Assignees"], err = ctx.Repo.Repository.GetAssignees(); err != nil { if ctx.Data["Assignees"], err = ctx.Repo.Repository.GetAssignees(); err != nil {

View file

@ -107,14 +107,12 @@
<div class="ui attached unstackable table segment"> <div class="ui attached unstackable table segment">
{{if ne $file.Type 4}} {{if ne $file.Type 4}}
{{$isImage := (call $.IsImageFile $file.Name)}} {{$isImage := (call $.IsImageFile $file.Name)}}
{{if and $isImage}}
<div class="center">
<img src="{{$.RawPath}}/{{EscapePound .Name}}">
</div>
{{else}}
<div class="file-body file-code code-view code-diff {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}}"> <div class="file-body file-code code-view code-diff {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}}">
<table> <table>
<tbody> <tbody>
{{if $isImage}}
{{template "repo/diff/image_diff" dict "file" . "root" $}}
{{else}}
{{if $.IsSplitStyle}} {{if $.IsSplitStyle}}
{{$highlightClass := $file.GetHighlightClass}} {{$highlightClass := $file.GetHighlightClass}}
{{range $j, $section := $file.Sections}} {{range $j, $section := $file.Sections}}
@ -164,11 +162,11 @@
{{else}} {{else}}
{{template "repo/diff/section_unified" dict "file" . "root" $}} {{template "repo/diff/section_unified" dict "file" . "root" $}}
{{end}} {{end}}
{{end}}
</tbody> </tbody>
</table> </table>
</div> </div>
{{end}} {{end}}
{{end}}
</div> </div>
</div> </div>
{{end}} {{end}}

View file

@ -0,0 +1,46 @@
{{ $imagePathOld := printf "%s/%s" .root.BeforeRawPath (EscapePound .file.OldName) }}
{{ $imagePathNew := printf "%s/%s" .root.RawPath (EscapePound .file.Name) }}
<tr>
<th class="halfwidth center">
{{.root.i18n.Tr "repo.diff.file_before"}}
</th>
<th class="halfwidth center">
{{.root.i18n.Tr "repo.diff.file_after"}}
</th>
</tr>
<tr>
<td class="halfwidth center">
{{ $oldImageExists := (call .root.FileExistsInBaseCommit .file.OldName) }}
{{if $oldImageExists}}
<a href="{{$imagePathOld}}" target="_blank">
<img src="{{$imagePathOld}}" class="border red" />
</a>
{{end}}
</td>
<td class="halfwidth center">
<a href="{{$imagePathNew}}" target="_blank">
<img src="{{$imagePathNew}}" class="border green" />
</a>
</td>
</tr>
{{ $imageInfoBase := (call .root.ImageInfoBase .file.OldName) }}
{{ $imageInfoHead := (call .root.ImageInfo .file.Name) }}
{{if and $imageInfoBase $imageInfoHead }}
<tr>
<td class="halfwidth center">
{{.root.i18n.Tr "repo.diff.file_image_width"}}: <span class="text {{if not (eq $imageInfoBase.Width $imageInfoHead.Width)}}red{{end}}">{{$imageInfoBase.Width}}</span>
&nbsp;|&nbsp;
{{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{if not (eq $imageInfoBase.Height $imageInfoHead.Height)}}red{{end}}">{{$imageInfoBase.Height}}</span>
&nbsp;|&nbsp;
{{.root.i18n.Tr "repo.diff.file_byte_size"}}: <span class="text {{if not (eq $imageInfoBase.ByteSize $imageInfoHead.ByteSize)}}red{{end}}">{{FileSize $imageInfoBase.ByteSize}}</span>
</td>
<td class="halfwidth center">
{{.root.i18n.Tr "repo.diff.file_image_width"}}: <span class="text {{if not (eq $imageInfoBase.Width $imageInfoHead.Width)}}green{{end}}">{{$imageInfoHead.Width}}</span>
&nbsp;|&nbsp;
{{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{if not (eq $imageInfoBase.Height $imageInfoHead.Height)}}green{{end}}">{{$imageInfoHead.Height}}</span>
&nbsp;|&nbsp;
{{.root.i18n.Tr "repo.diff.file_byte_size"}}: <span class="text {{if not (eq $imageInfoBase.ByteSize $imageInfoHead.ByteSize)}}green{{end}}">{{FileSize $imageInfoHead.ByteSize}}</span>
</td>
</tr>
{{end}}