mirror of
https://github.com/go-gitea/gitea
synced 2024-11-08 03:04:29 +00:00
db01bf6cc8
1. Restore missing styles for message close icon 2. Move `code-line-button` so that it does not go off-screen on small viewports 3. Make `code-line-button` look and behave like other buttons 4. Make `code-line-button` work in blame 5. Make the active selection span the whole line, not just the code part 6. Tweak colors, make dark theme code bg darker, make line numbers same color in diff and file view. 7. Move code background to parent, fixing border radius and other problems 8. Enable code wrap in blame 9. Improve blame responsiveness 10. Remove `--color-code-sidebar-bg` in blame, now it uses same background as code 11. Rename `--color-active-line` to `--color-highlight-bg` 12. Add `--color-highlight-bg` 13. Fix button group borders on hover and border-right on last button. <img width="1343" alt="Screenshot 2024-03-23 at 22 34 13" src="https://github.com/go-gitea/gitea/assets/115237/fcbb919f-5dc3-43f0-97f6-870d6f412554"> <img width="1334" alt="Screenshot 2024-03-23 at 22 34 26" src="https://github.com/go-gitea/gitea/assets/115237/ca44c3b7-4328-4645-ba49-b0dc6a5ac06d"> <img width="1338" alt="Screenshot 2024-03-23 at 22 34 57" src="https://github.com/go-gitea/gitea/assets/115237/00eb0b5a-1ec7-4669-a94a-4602b9d1c1ac"> <img width="1337" alt="Screenshot 2024-03-23 at 22 34 42" src="https://github.com/go-gitea/gitea/assets/115237/752edc4a-064f-413c-9dff-c086187fcd85"> Fixes: https://github.com/go-gitea/gitea/issues/18074
319 lines
8.3 KiB
Go
319 lines
8.3 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"fmt"
|
|
gotemplate "html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/charset"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/highlight"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/templates"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/modules/util"
|
|
"code.gitea.io/gitea/services/context"
|
|
files_service "code.gitea.io/gitea/services/repository/files"
|
|
)
|
|
|
|
type blameRow struct {
|
|
RowNumber int
|
|
Avatar gotemplate.HTML
|
|
RepoLink string
|
|
PartSha string
|
|
PreviousSha string
|
|
PreviousShaURL string
|
|
IsFirstCommit bool
|
|
CommitURL string
|
|
CommitMessage string
|
|
CommitSince gotemplate.HTML
|
|
Code gotemplate.HTML
|
|
EscapeStatus *charset.EscapeStatus
|
|
}
|
|
|
|
// RefBlame render blame page
|
|
func RefBlame(ctx *context.Context) {
|
|
fileName := ctx.Repo.TreePath
|
|
if len(fileName) == 0 {
|
|
ctx.NotFound("Blame FileName", nil)
|
|
return
|
|
}
|
|
|
|
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
|
treeLink := branchLink
|
|
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
|
|
|
|
if len(ctx.Repo.TreePath) > 0 {
|
|
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
|
}
|
|
|
|
var treeNames []string
|
|
paths := make([]string, 0, 5)
|
|
if len(ctx.Repo.TreePath) > 0 {
|
|
treeNames = strings.Split(ctx.Repo.TreePath, "/")
|
|
for i := range treeNames {
|
|
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
|
|
}
|
|
|
|
ctx.Data["HasParentPath"] = true
|
|
if len(paths)-2 >= 0 {
|
|
ctx.Data["ParentPath"] = "/" + paths[len(paths)-1]
|
|
}
|
|
}
|
|
|
|
// Get current entry user currently looking at.
|
|
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
|
if err != nil {
|
|
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
|
|
return
|
|
}
|
|
|
|
blob := entry.Blob()
|
|
|
|
ctx.Data["Paths"] = paths
|
|
ctx.Data["TreeLink"] = treeLink
|
|
ctx.Data["TreeNames"] = treeNames
|
|
ctx.Data["BranchLink"] = branchLink
|
|
|
|
ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
|
ctx.Data["PageIsViewCode"] = true
|
|
|
|
ctx.Data["IsBlame"] = true
|
|
|
|
ctx.Data["FileSize"] = blob.Size()
|
|
ctx.Data["FileName"] = blob.Name()
|
|
|
|
ctx.Data["NumLines"], err = blob.GetBlobLineCount()
|
|
ctx.Data["NumLinesSet"] = true
|
|
|
|
if err != nil {
|
|
ctx.NotFound("GetBlobLineCount", err)
|
|
return
|
|
}
|
|
|
|
bypassBlameIgnore, _ := strconv.ParseBool(ctx.FormString("bypass-blame-ignore"))
|
|
|
|
result, err := performBlame(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Commit, fileName, bypassBlameIgnore)
|
|
if err != nil {
|
|
ctx.NotFound("CreateBlameReader", err)
|
|
return
|
|
}
|
|
|
|
ctx.Data["UsesIgnoreRevs"] = result.UsesIgnoreRevs
|
|
ctx.Data["FaultyIgnoreRevsFile"] = result.FaultyIgnoreRevsFile
|
|
|
|
// Get Topics of this repo
|
|
renderRepoTopics(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
commitNames := processBlameParts(ctx, result.Parts)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
renderBlame(ctx, result.Parts, commitNames)
|
|
|
|
ctx.HTML(http.StatusOK, tplRepoHome)
|
|
}
|
|
|
|
type blameResult struct {
|
|
Parts []*git.BlamePart
|
|
UsesIgnoreRevs bool
|
|
FaultyIgnoreRevsFile bool
|
|
}
|
|
|
|
func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
|
|
objectFormat := ctx.Repo.GetObjectFormat()
|
|
|
|
blameReader, err := git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, bypassBlameIgnore)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := &blameResult{}
|
|
if err := fillBlameResult(blameReader, r); err != nil {
|
|
_ = blameReader.Close()
|
|
return nil, err
|
|
}
|
|
|
|
err = blameReader.Close()
|
|
if err != nil {
|
|
if len(r.Parts) == 0 && r.UsesIgnoreRevs {
|
|
// try again without ignored revs
|
|
|
|
blameReader, err = git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := &blameResult{
|
|
FaultyIgnoreRevsFile: true,
|
|
}
|
|
if err := fillBlameResult(blameReader, r); err != nil {
|
|
_ = blameReader.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return r, blameReader.Close()
|
|
}
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func fillBlameResult(br *git.BlameReader, r *blameResult) error {
|
|
r.UsesIgnoreRevs = br.UsesIgnoreRevs()
|
|
|
|
previousHelper := make(map[string]*git.BlamePart)
|
|
|
|
r.Parts = make([]*git.BlamePart, 0, 5)
|
|
for {
|
|
blamePart, err := br.NextPart()
|
|
if err != nil {
|
|
return fmt.Errorf("BlameReader.NextPart failed: %w", err)
|
|
}
|
|
if blamePart == nil {
|
|
break
|
|
}
|
|
|
|
if prev, ok := previousHelper[blamePart.Sha]; ok {
|
|
if blamePart.PreviousSha == "" {
|
|
blamePart.PreviousSha = prev.PreviousSha
|
|
blamePart.PreviousPath = prev.PreviousPath
|
|
}
|
|
} else {
|
|
previousHelper[blamePart.Sha] = blamePart
|
|
}
|
|
|
|
r.Parts = append(r.Parts, blamePart)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[string]*user_model.UserCommit {
|
|
// store commit data by SHA to look up avatar info etc
|
|
commitNames := make(map[string]*user_model.UserCommit)
|
|
// and as blameParts can reference the same commits multiple
|
|
// times, we cache the lookup work locally
|
|
commits := make([]*git.Commit, 0, len(blameParts))
|
|
commitCache := map[string]*git.Commit{}
|
|
commitCache[ctx.Repo.Commit.ID.String()] = ctx.Repo.Commit
|
|
|
|
for _, part := range blameParts {
|
|
sha := part.Sha
|
|
if _, ok := commitNames[sha]; ok {
|
|
continue
|
|
}
|
|
|
|
// find the blamePart commit, to look up parent & email address for avatars
|
|
commit, ok := commitCache[sha]
|
|
var err error
|
|
if !ok {
|
|
commit, err = ctx.Repo.GitRepo.GetCommit(sha)
|
|
if err != nil {
|
|
if git.IsErrNotExist(err) {
|
|
ctx.NotFound("Repo.GitRepo.GetCommit", err)
|
|
} else {
|
|
ctx.ServerError("Repo.GitRepo.GetCommit", err)
|
|
}
|
|
return nil
|
|
}
|
|
commitCache[sha] = commit
|
|
}
|
|
|
|
commits = append(commits, commit)
|
|
}
|
|
|
|
// populate commit email addresses to later look up avatars.
|
|
for _, c := range user_model.ValidateCommitsWithEmails(ctx, commits) {
|
|
commitNames[c.ID.String()] = c
|
|
}
|
|
|
|
return commitNames
|
|
}
|
|
|
|
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
|
|
repoLink := ctx.Repo.RepoLink
|
|
|
|
language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
|
|
if err != nil {
|
|
log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
|
|
}
|
|
|
|
lines := make([]string, 0)
|
|
rows := make([]*blameRow, 0)
|
|
escapeStatus := &charset.EscapeStatus{}
|
|
|
|
var lexerName string
|
|
|
|
avatarUtils := templates.NewAvatarUtils(ctx)
|
|
i := 0
|
|
commitCnt := 0
|
|
for _, part := range blameParts {
|
|
for index, line := range part.Lines {
|
|
i++
|
|
lines = append(lines, line)
|
|
|
|
br := &blameRow{
|
|
RowNumber: i,
|
|
}
|
|
|
|
commit := commitNames[part.Sha]
|
|
if index == 0 {
|
|
// Count commit number
|
|
commitCnt++
|
|
|
|
// User avatar image
|
|
commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Locale)
|
|
|
|
var avatar string
|
|
if commit.User != nil {
|
|
avatar = string(avatarUtils.Avatar(commit.User, 18))
|
|
} else {
|
|
avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "gt-mr-3"))
|
|
}
|
|
|
|
br.Avatar = gotemplate.HTML(avatar)
|
|
br.RepoLink = repoLink
|
|
br.PartSha = part.Sha
|
|
br.PreviousSha = part.PreviousSha
|
|
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
|
|
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
|
|
br.CommitMessage = commit.CommitMessage
|
|
br.CommitSince = commitSince
|
|
}
|
|
|
|
if i != len(lines)-1 {
|
|
line += "\n"
|
|
}
|
|
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
|
|
line, lexerNameForLine := highlight.Code(fileName, language, line)
|
|
|
|
// set lexer name to the first detected lexer. this is certainly suboptimal and
|
|
// we should instead highlight the whole file at once
|
|
if lexerName == "" {
|
|
lexerName = lexerNameForLine
|
|
}
|
|
|
|
br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
|
|
rows = append(rows, br)
|
|
escapeStatus = escapeStatus.Or(br.EscapeStatus)
|
|
}
|
|
}
|
|
|
|
ctx.Data["EscapeStatus"] = escapeStatus
|
|
ctx.Data["BlameRows"] = rows
|
|
ctx.Data["CommitCnt"] = commitCnt
|
|
ctx.Data["LexerName"] = lexerName
|
|
}
|