2019-04-20 04:47:00 +02:00
// 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 repo
import (
"fmt"
gotemplate "html/template"
2021-04-05 17:30:52 +02:00
"net/http"
2021-11-16 18:18:25 +00:00
"net/url"
2019-04-20 04:47:00 +02:00
"strings"
2021-12-10 09:27:50 +08:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 17:49:20 +08:00
user_model "code.gitea.io/gitea/models/user"
2019-04-20 04:47:00 +02:00
"code.gitea.io/gitea/modules/base"
2022-01-07 01:18:52 +00:00
"code.gitea.io/gitea/modules/charset"
2019-04-20 04:47:00 +02:00
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
2021-11-17 20:37:00 +00:00
"code.gitea.io/gitea/modules/log"
2020-12-03 19:46:11 +01:00
"code.gitea.io/gitea/modules/templates"
2019-08-15 22:46:21 +08:00
"code.gitea.io/gitea/modules/timeutil"
2021-11-16 18:18:25 +00:00
"code.gitea.io/gitea/modules/util"
2019-04-20 04:47:00 +02:00
)
const (
tplBlame base . TplName = "repo/home"
)
2021-06-28 01:13:20 +02: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-01-07 01:18:52 +00:00
EscapeStatus charset . EscapeStatus
2021-06-28 01:13:20 +02:00
}
2019-04-20 04:47:00 +02: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-16 18:18:25 +00:00
treeLink += "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
2019-04-20 04:47:00 +02: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-06-30 17:34:03 -04:00
2021-11-16 18:18:25 +00:00
ctx . Data [ "RawFileLink" ] = rawLink + "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
2019-04-20 04:47:00 +02:00
ctx . Data [ "PageIsViewCode" ] = true
ctx . Data [ "IsBlame" ] = true
ctx . Data [ "FileSize" ] = blob . Size ( )
ctx . Data [ "FileName" ] = blob . Name ( )
2020-06-15 20:39:39 +02:00
ctx . Data [ "NumLines" ] , err = blob . GetBlobLineCount ( )
if err != nil {
ctx . NotFound ( "GetBlobLineCount" , err )
return
}
2021-12-10 09:27:50 +08:00
blameReader , err := git . CreateBlameReader ( ctx , repo_model . RepoPath ( userName , repoName ) , commitID , fileName )
2019-04-20 04:47:00 +02:00
if err != nil {
ctx . NotFound ( "CreateBlameReader" , err )
return
}
defer blameReader . Close ( )
2019-06-27 02:15:26 +08:00
blameParts := make ( [ ] git . BlamePart , 0 )
2019-04-20 04:47:00 +02: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 01:13:20 +02: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 17:49:20 +08:00
func processBlameParts ( ctx * context . Context , blameParts [ ] git . BlamePart ) ( map [ string ] * user_model . UserCommit , map [ string ] string ) {
2021-06-28 01:13:20 +02:00
// store commit data by SHA to look up avatar info etc
2021-11-24 17:49:20 +08:00
commitNames := make ( map [ string ] * user_model . UserCommit )
2021-06-28 01:13:20 +02: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-09 20:08:51 +02:00
commits := make ( [ ] * git . Commit , 0 , len ( blameParts ) )
2021-06-28 01:13:20 +02:00
commitCache := map [ string ] * git . Commit { }
commitCache [ ctx . Repo . Commit . ID . String ( ) ] = ctx . Repo . Commit
2019-04-20 04:47:00 +02:00
for _ , part := range blameParts {
sha := part . Sha
if _ , ok := commitNames [ sha ] ; ok {
continue
}
2021-06-28 01:13:20 +02: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 04:47:00 +02:00
}
}
2021-08-09 20:08:51 +02:00
commits = append ( commits , commit )
2019-04-20 04:47:00 +02:00
}
2021-06-28 01:13:20 +02:00
// populate commit email addresses to later look up avatars.
2021-11-24 17:49:20 +08:00
for _ , c := range user_model . ValidateCommitsWithEmails ( commits ) {
2019-04-20 04:47:00 +02:00
commitNames [ c . ID . String ( ) ] = c
}
2021-06-28 01:13:20 +02:00
return commitNames , previousCommits
2019-04-20 04:47:00 +02:00
}
2021-11-24 17:49:20 +08:00
func renderBlame ( ctx * context . Context , blameParts [ ] git . BlamePart , commitNames map [ string ] * user_model . UserCommit , previousCommits map [ string ] string ) {
2019-04-20 04:47:00 +02:00
repoLink := ctx . Repo . RepoLink
2021-11-17 20:37:00 +00: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 ,
Attributes : [ ] string { "linguist-language" , "gitlab-language" } ,
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-20 18:46:10 +01:00
lines := make ( [ ] string , 0 )
2021-06-28 01:13:20 +02:00
rows := make ( [ ] * blameRow , 0 )
2022-01-07 01:18:52 +00:00
escapeStatus := charset . EscapeStatus { }
2019-04-20 04:47:00 +02:00
2022-01-20 18:46:10 +01:00
i := 0
commitCnt := 0
2021-06-28 01:13:20 +02:00
for _ , part := range blameParts {
2019-04-20 04:47:00 +02:00
for index , line := range part . Lines {
i ++
lines = append ( lines , line )
2021-06-28 01:13:20 +02:00
br := & blameRow {
RowNumber : i ,
2019-04-20 04:47:00 +02:00
}
2021-06-28 01:13:20 +02:00
2019-04-20 04:47:00 +02:00
commit := commitNames [ part . Sha ]
2021-06-28 01:13:20 +02:00
previousSha := previousCommits [ part . Sha ]
2019-04-20 04:47:00 +02:00
if index == 0 {
2021-06-28 01:13:20 +02:00
// Count commit number
commitCnt ++
2019-04-20 04:47:00 +02:00
// User avatar image
2022-02-08 11:02:30 +08:00
commitSince := timeutil . TimeSinceUnix ( timeutil . TimeStamp ( commit . Author . When . Unix ( ) ) , ctx . Locale . Language ( ) )
2020-12-03 19:46:11 +01:00
var avatar string
2019-04-20 04:47:00 +02:00
if commit . User != nil {
2020-12-03 19:46:11 +01:00
avatar = string ( templates . Avatar ( commit . User , 18 , "mr-3" ) )
2019-04-20 04:47:00 +02:00
} else {
2020-12-03 19:46:11 +01:00
avatar = string ( templates . AvatarByEmail ( commit . Author . Email , commit . Author . Name , 18 , "mr-3" ) )
2019-04-20 04:47:00 +02:00
}
2020-12-03 19:46:11 +01:00
2021-06-28 01:13:20 +02:00
br . Avatar = gotemplate . HTML ( avatar )
br . RepoLink = repoLink
br . PartSha = part . Sha
br . PreviousSha = previousSha
2021-11-16 18:18:25 +00: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 08:25:24 +00:00
br . CommitMessage = commit . CommitMessage
2021-06-28 01:13:20 +02:00
br . CommitSince = commitSince
2019-04-20 04:47:00 +02:00
}
2019-05-11 23:27:39 +03:00
if i != len ( lines ) - 1 {
2019-04-20 04:47:00 +02:00
line += "\n"
}
2020-06-30 17:34:03 -04:00
fileName := fmt . Sprintf ( "%v" , ctx . Data [ "FileName" ] )
2021-11-17 20:37:00 +00:00
line = highlight . Code ( fileName , language , line )
2021-06-28 01:13:20 +02:00
2022-01-07 01:18:52 +00:00
br . EscapeStatus , line = charset . EscapeControlString ( line )
2021-06-28 01:13:20 +02:00
br . Code = gotemplate . HTML ( line )
rows = append ( rows , br )
2022-01-07 01:18:52 +00:00
escapeStatus = escapeStatus . Or ( br . EscapeStatus )
2019-04-20 04:47:00 +02:00
}
}
2022-01-07 01:18:52 +00:00
ctx . Data [ "EscapeStatus" ] = escapeStatus
2021-06-28 01:13:20 +02:00
ctx . Data [ "BlameRows" ] = rows
ctx . Data [ "CommitCnt" ] = commitCnt
2019-04-20 04:47:00 +02:00
}