2019-04-20 11:47:00 +09:00
|
|
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
2022-11-28 03:20:29 +09:00
|
|
|
// SPDX-License-Identifier: MIT
|
2019-04-20 11:47:00 +09:00
|
|
|
|
|
|
|
package repo
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
gotemplate "html/template"
|
2021-04-06 00:30:52 +09:00
|
|
|
"net/http"
|
2021-11-17 03:18:25 +09:00
|
|
|
"net/url"
|
2019-04-20 11:47:00 +09:00
|
|
|
"strings"
|
|
|
|
|
2021-12-10 10:27:50 +09:00
|
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
2021-11-24 18:49:20 +09:00
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
2019-04-20 11:47:00 +09:00
|
|
|
"code.gitea.io/gitea/modules/base"
|
2022-01-07 10:18:52 +09:00
|
|
|
"code.gitea.io/gitea/modules/charset"
|
2019-04-20 11:47:00 +09:00
|
|
|
"code.gitea.io/gitea/modules/context"
|
|
|
|
"code.gitea.io/gitea/modules/git"
|
|
|
|
"code.gitea.io/gitea/modules/highlight"
|
2021-11-18 05:37:00 +09:00
|
|
|
"code.gitea.io/gitea/modules/log"
|
2020-12-04 03:46:11 +09:00
|
|
|
"code.gitea.io/gitea/modules/templates"
|
2019-08-15 23:46:21 +09:00
|
|
|
"code.gitea.io/gitea/modules/timeutil"
|
2021-11-17 03:18:25 +09:00
|
|
|
"code.gitea.io/gitea/modules/util"
|
2019-04-20 11:47:00 +09:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
tplBlame base.TplName = "repo/home"
|
|
|
|
)
|
|
|
|
|
2021-06-28 08:13:20 +09:00
|
|
|
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
|
2022-08-14 03:32:34 +09:00
|
|
|
EscapeStatus *charset.EscapeStatus
|
2021-06-28 08:13:20 +09:00
|
|
|
}
|
|
|
|
|
2019-04-20 11:47:00 +09:00
|
|
|
// RefBlame render blame page
|
|
|
|
func RefBlame(ctx *context.Context) {
|
|
|
|
fileName := ctx.Repo.TreePath
|
|
|
|
if len(fileName) == 0 {
|
|
|
|
ctx.NotFound("Blame FileName", nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
userName := ctx.Repo.Owner.Name
|
|
|
|
repoName := ctx.Repo.Repository.Name
|
|
|
|
commitID := ctx.Repo.CommitID
|
|
|
|
|
|
|
|
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
|
|
|
treeLink := branchLink
|
|
|
|
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
|
|
|
|
|
|
|
|
if len(ctx.Repo.TreePath) > 0 {
|
2021-11-17 03:18:25 +09:00
|
|
|
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
2019-04-20 11:47:00 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
blob := entry.Blob()
|
|
|
|
|
|
|
|
ctx.Data["Paths"] = paths
|
|
|
|
ctx.Data["TreeLink"] = treeLink
|
|
|
|
ctx.Data["TreeNames"] = treeNames
|
|
|
|
ctx.Data["BranchLink"] = branchLink
|
2020-07-01 06:34:03 +09:00
|
|
|
|
2021-11-17 03:18:25 +09:00
|
|
|
ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
2019-04-20 11:47:00 +09:00
|
|
|
ctx.Data["PageIsViewCode"] = true
|
|
|
|
|
|
|
|
ctx.Data["IsBlame"] = true
|
|
|
|
|
|
|
|
ctx.Data["FileSize"] = blob.Size()
|
|
|
|
ctx.Data["FileName"] = blob.Name()
|
|
|
|
|
2020-06-16 03:39:39 +09:00
|
|
|
ctx.Data["NumLines"], err = blob.GetBlobLineCount()
|
2022-11-19 20:08:06 +09:00
|
|
|
ctx.Data["NumLinesSet"] = true
|
|
|
|
|
2020-06-16 03:39:39 +09:00
|
|
|
if err != nil {
|
|
|
|
ctx.NotFound("GetBlobLineCount", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-12-10 10:27:50 +09:00
|
|
|
blameReader, err := git.CreateBlameReader(ctx, repo_model.RepoPath(userName, repoName), commitID, fileName)
|
2019-04-20 11:47:00 +09:00
|
|
|
if err != nil {
|
|
|
|
ctx.NotFound("CreateBlameReader", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer blameReader.Close()
|
|
|
|
|
2019-06-27 03:15:26 +09:00
|
|
|
blameParts := make([]git.BlamePart, 0)
|
2019-04-20 11:47:00 +09:00
|
|
|
|
|
|
|
for {
|
|
|
|
blamePart, err := blameReader.NextPart()
|
|
|
|
if err != nil {
|
|
|
|
ctx.NotFound("NextPart", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if blamePart == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
blameParts = append(blameParts, *blamePart)
|
|
|
|
}
|
|
|
|
|
2021-06-28 08:13:20 +09:00
|
|
|
// Get Topics of this repo
|
|
|
|
renderRepoTopics(ctx)
|
|
|
|
if ctx.Written() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
commitNames, previousCommits := processBlameParts(ctx, blameParts)
|
|
|
|
if ctx.Written() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
renderBlame(ctx, blameParts, commitNames, previousCommits)
|
|
|
|
|
|
|
|
ctx.HTML(http.StatusOK, tplBlame)
|
|
|
|
}
|
|
|
|
|
2021-11-24 18:49:20 +09:00
|
|
|
func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) (map[string]*user_model.UserCommit, map[string]string) {
|
2021-06-28 08:13:20 +09:00
|
|
|
// store commit data by SHA to look up avatar info etc
|
2021-11-24 18:49:20 +09:00
|
|
|
commitNames := make(map[string]*user_model.UserCommit)
|
2021-06-28 08:13:20 +09:00
|
|
|
// previousCommits contains links from SHA to parent SHA,
|
|
|
|
// if parent also contains the current TreePath.
|
|
|
|
previousCommits := make(map[string]string)
|
|
|
|
// and as blameParts can reference the same commits multiple
|
|
|
|
// times, we cache the lookup work locally
|
2021-08-10 03:08:51 +09:00
|
|
|
commits := make([]*git.Commit, 0, len(blameParts))
|
2021-06-28 08:13:20 +09:00
|
|
|
commitCache := map[string]*git.Commit{}
|
|
|
|
commitCache[ctx.Repo.Commit.ID.String()] = ctx.Repo.Commit
|
2019-04-20 11:47:00 +09:00
|
|
|
|
|
|
|
for _, part := range blameParts {
|
|
|
|
sha := part.Sha
|
|
|
|
if _, ok := commitNames[sha]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2021-06-28 08:13:20 +09:00
|
|
|
// 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, nil
|
|
|
|
}
|
|
|
|
commitCache[sha] = commit
|
|
|
|
}
|
|
|
|
|
|
|
|
// find parent commit
|
|
|
|
if commit.ParentCount() > 0 {
|
|
|
|
psha := commit.Parents[0]
|
|
|
|
previousCommit, ok := commitCache[psha.String()]
|
|
|
|
if !ok {
|
|
|
|
previousCommit, _ = commit.Parent(0)
|
|
|
|
if previousCommit != nil {
|
|
|
|
commitCache[psha.String()] = previousCommit
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// only store parent commit ONCE, if it has the file
|
|
|
|
if previousCommit != nil {
|
|
|
|
if haz1, _ := previousCommit.HasFile(ctx.Repo.TreePath); haz1 {
|
|
|
|
previousCommits[commit.ID.String()] = previousCommit.ID.String()
|
|
|
|
}
|
2019-04-20 11:47:00 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-10 03:08:51 +09:00
|
|
|
commits = append(commits, commit)
|
2019-04-20 11:47:00 +09:00
|
|
|
}
|
|
|
|
|
2021-06-28 08:13:20 +09:00
|
|
|
// populate commit email addresses to later look up avatars.
|
2021-11-24 18:49:20 +09:00
|
|
|
for _, c := range user_model.ValidateCommitsWithEmails(commits) {
|
2019-04-20 11:47:00 +09:00
|
|
|
commitNames[c.ID.String()] = c
|
|
|
|
}
|
|
|
|
|
2021-06-28 08:13:20 +09:00
|
|
|
return commitNames, previousCommits
|
2019-04-20 11:47:00 +09:00
|
|
|
}
|
|
|
|
|
2021-11-24 18:49:20 +09:00
|
|
|
func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]*user_model.UserCommit, previousCommits map[string]string) {
|
2019-04-20 11:47:00 +09:00
|
|
|
repoLink := ctx.Repo.RepoLink
|
|
|
|
|
2021-11-18 05:37:00 +09:00
|
|
|
language := ""
|
|
|
|
|
|
|
|
indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
|
|
|
|
if err == nil {
|
|
|
|
defer deleteTemporaryFile()
|
|
|
|
|
|
|
|
filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
|
|
|
|
CachedOnly: true,
|
Refactor git command package to improve security and maintainability (#22678)
This PR follows #21535 (and replace #22592)
## Review without space diff
https://github.com/go-gitea/gitea/pull/22678/files?diff=split&w=1
## Purpose of this PR
1. Make git module command completely safe (risky user inputs won't be
passed as argument option anymore)
2. Avoid low-level mistakes like
https://github.com/go-gitea/gitea/pull/22098#discussion_r1045234918
3. Remove deprecated and dirty `CmdArgCheck` function, hide the `CmdArg`
type
4. Simplify code when using git command
## The main idea of this PR
* Move the `git.CmdArg` to the `internal` package, then no other package
except `git` could use it. Then developers could never do
`AddArguments(git.CmdArg(userInput))` any more.
* Introduce `git.ToTrustedCmdArgs`, it's for user-provided and already
trusted arguments. It's only used in a few cases, for example: use git
arguments from config file, help unit test with some arguments.
* Introduce `AddOptionValues` and `AddOptionFormat`, they make code more
clear and simple:
* Before: `AddArguments("-m").AddDynamicArguments(message)`
* After: `AddOptionValues("-m", message)`
* -
* Before: `AddArguments(git.CmdArg(fmt.Sprintf("--author='%s <%s>'",
sig.Name, sig.Email)))`
* After: `AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)`
## FAQ
### Why these changes were not done in #21535 ?
#21535 is mainly a search&replace, it did its best to not change too
much logic.
Making the framework better needs a lot of changes, so this separate PR
is needed as the second step.
### The naming of `AddOptionXxx`
According to git's manual, the `--xxx` part is called `option`.
### How can it guarantee that `internal.CmdArg` won't be not misused?
Go's specification guarantees that. Trying to access other package's
internal package causes compilation error.
And, `golangci-lint` also denies the git/internal package. Only the
`git/command.go` can use it carefully.
### There is still a `ToTrustedCmdArgs`, will it still allow developers
to make mistakes and pass untrusted arguments?
Generally speaking, no. Because when using `ToTrustedCmdArgs`, the code
will be very complex (see the changes for examples). Then developers and
reviewers can know that something might be unreasonable.
### Why there was a `CmdArgCheck` and why it's removed?
At the moment of #21535, to reduce unnecessary changes, `CmdArgCheck`
was introduced as a hacky patch. Now, almost all code could be written
as `cmd := NewCommand(); cmd.AddXxx(...)`, then there is no need for
`CmdArgCheck` anymore.
### Why many codes for `signArg == ""` is deleted?
Because in the old code, `signArg` could never be empty string, it's
either `-S[key-id]` or `--no-gpg-sign`. So the `signArg == ""` is just
dead code.
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-04 11:30:43 +09:00
|
|
|
Attributes: []string{"linguist-language", "gitlab-language"},
|
2021-11-18 05:37:00 +09:00
|
|
|
Filenames: []string{ctx.Repo.TreePath},
|
|
|
|
IndexFile: indexFilename,
|
|
|
|
WorkTree: worktree,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
|
|
|
|
if language == "" || language == "unspecified" {
|
|
|
|
language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
|
|
|
|
}
|
|
|
|
if language == "unspecified" {
|
|
|
|
language = ""
|
|
|
|
}
|
|
|
|
}
|
2022-01-21 02:46:10 +09:00
|
|
|
lines := make([]string, 0)
|
2021-06-28 08:13:20 +09:00
|
|
|
rows := make([]*blameRow, 0)
|
2022-08-14 03:32:34 +09:00
|
|
|
escapeStatus := &charset.EscapeStatus{}
|
2019-04-20 11:47:00 +09:00
|
|
|
|
2022-11-19 20:08:06 +09:00
|
|
|
var lexerName string
|
|
|
|
|
2022-01-21 02:46:10 +09:00
|
|
|
i := 0
|
|
|
|
commitCnt := 0
|
2021-06-28 08:13:20 +09:00
|
|
|
for _, part := range blameParts {
|
2019-04-20 11:47:00 +09:00
|
|
|
for index, line := range part.Lines {
|
|
|
|
i++
|
|
|
|
lines = append(lines, line)
|
|
|
|
|
2021-06-28 08:13:20 +09:00
|
|
|
br := &blameRow{
|
|
|
|
RowNumber: i,
|
2019-04-20 11:47:00 +09:00
|
|
|
}
|
2021-06-28 08:13:20 +09:00
|
|
|
|
2019-04-20 11:47:00 +09:00
|
|
|
commit := commitNames[part.Sha]
|
2021-06-28 08:13:20 +09:00
|
|
|
previousSha := previousCommits[part.Sha]
|
2019-04-20 11:47:00 +09:00
|
|
|
if index == 0 {
|
2021-06-28 08:13:20 +09:00
|
|
|
// Count commit number
|
|
|
|
commitCnt++
|
|
|
|
|
2019-04-20 11:47:00 +09:00
|
|
|
// User avatar image
|
2022-06-26 23:19:22 +09:00
|
|
|
commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Locale)
|
2020-12-04 03:46:11 +09:00
|
|
|
|
|
|
|
var avatar string
|
2019-04-20 11:47:00 +09:00
|
|
|
if commit.User != nil {
|
2020-12-04 03:46:11 +09:00
|
|
|
avatar = string(templates.Avatar(commit.User, 18, "mr-3"))
|
2019-04-20 11:47:00 +09:00
|
|
|
} else {
|
2020-12-04 03:46:11 +09:00
|
|
|
avatar = string(templates.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "mr-3"))
|
2019-04-20 11:47:00 +09:00
|
|
|
}
|
2020-12-04 03:46:11 +09:00
|
|
|
|
2021-06-28 08:13:20 +09:00
|
|
|
br.Avatar = gotemplate.HTML(avatar)
|
|
|
|
br.RepoLink = repoLink
|
|
|
|
br.PartSha = part.Sha
|
|
|
|
br.PreviousSha = previousSha
|
2021-11-17 03:18:25 +09:00
|
|
|
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(previousSha), util.PathEscapeSegments(ctx.Repo.TreePath))
|
|
|
|
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
|
2021-10-31 17:25:24 +09:00
|
|
|
br.CommitMessage = commit.CommitMessage
|
2021-06-28 08:13:20 +09:00
|
|
|
br.CommitSince = commitSince
|
2019-04-20 11:47:00 +09:00
|
|
|
}
|
|
|
|
|
2019-05-12 05:27:39 +09:00
|
|
|
if i != len(lines)-1 {
|
2019-04-20 11:47:00 +09:00
|
|
|
line += "\n"
|
|
|
|
}
|
2020-07-01 06:34:03 +09:00
|
|
|
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
|
2022-11-19 20:08:06 +09:00
|
|
|
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
|
|
|
|
}
|
2021-06-28 08:13:20 +09:00
|
|
|
|
2022-08-14 03:32:34 +09:00
|
|
|
br.EscapeStatus, line = charset.EscapeControlHTML(line, ctx.Locale)
|
2021-06-28 08:13:20 +09:00
|
|
|
br.Code = gotemplate.HTML(line)
|
|
|
|
rows = append(rows, br)
|
2022-01-07 10:18:52 +09:00
|
|
|
escapeStatus = escapeStatus.Or(br.EscapeStatus)
|
2019-04-20 11:47:00 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-07 10:18:52 +09:00
|
|
|
ctx.Data["EscapeStatus"] = escapeStatus
|
2021-06-28 08:13:20 +09:00
|
|
|
ctx.Data["BlameRows"] = rows
|
|
|
|
ctx.Data["CommitCnt"] = commitCnt
|
2022-11-19 20:08:06 +09:00
|
|
|
ctx.Data["LexerName"] = lexerName
|
2019-04-20 11:47:00 +09:00
|
|
|
}
|