2017-09-14 08:51:32 +02:00
// Copyright 2017 The Gitea Authors. All rights reserved.
2014-07-26 06:24:27 +02:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2022-11-27 19:20:29 +01:00
// SPDX-License-Identifier: MIT
2014-07-26 06:24:27 +02:00
package repo
import (
"bytes"
2021-10-08 15:08:22 +02:00
gocontext "context"
2017-01-29 21:13:57 +01:00
"encoding/base64"
2016-08-09 21:56:00 +02:00
"fmt"
2021-01-22 18:49:13 +01:00
"io"
2021-04-05 17:30:52 +02:00
"net/http"
2019-10-13 15:23:14 +02:00
"net/url"
2014-07-26 06:24:27 +02:00
"path"
"strings"
2021-10-08 15:08:22 +02:00
"time"
2014-07-26 06:24:27 +02:00
2022-08-25 04:31:57 +02:00
activities_model "code.gitea.io/gitea/models/activities"
admin_model "code.gitea.io/gitea/models/admin"
2021-12-10 09:14:24 +01:00
asymkey_model "code.gitea.io/gitea/models/asymkey"
2021-09-24 13:32:56 +02:00
"code.gitea.io/gitea/models/db"
2022-06-12 17:51:54 +02:00
git_model "code.gitea.io/gitea/models/git"
2021-12-10 02:27:50 +01:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-09 20:57:58 +01:00
unit_model "code.gitea.io/gitea/models/unit"
2021-11-24 10:49:20 +01:00
user_model "code.gitea.io/gitea/models/user"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/base"
2019-08-15 14:07:28 +02:00
"code.gitea.io/gitea/modules/charset"
2022-10-12 07:18:26 +02:00
"code.gitea.io/gitea/modules/container"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/context"
2019-03-27 10:33:00 +01:00
"code.gitea.io/gitea/modules/git"
2016-12-06 18:58:31 +01:00
"code.gitea.io/gitea/modules/highlight"
2016-12-26 02:16:37 +01:00
"code.gitea.io/gitea/modules/lfs"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/log"
2017-04-21 09:01:08 +02:00
"code.gitea.io/gitea/modules/markup"
2022-06-06 10:01:49 +02:00
repo_module "code.gitea.io/gitea/modules/repository"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/setting"
2021-06-30 21:14:53 +02:00
"code.gitea.io/gitea/modules/structs"
2021-06-05 14:32:19 +02:00
"code.gitea.io/gitea/modules/typesniffer"
2021-10-24 23:12:43 +02:00
"code.gitea.io/gitea/modules/util"
2022-03-13 17:40:47 +01:00
"code.gitea.io/gitea/routers/web/feed"
2014-07-26 06:24:27 +02:00
)
const (
2021-10-08 15:08:22 +02:00
tplRepoEMPTY base . TplName = "repo/empty"
tplRepoHome base . TplName = "repo/home"
tplRepoViewList base . TplName = "repo/view_list"
tplWatchers base . TplName = "repo/watchers"
tplForks base . TplName = "repo/forks"
tplMigrating base . TplName = "repo/migrate/migrating"
2014-07-26 06:24:27 +02:00
)
2023-02-12 08:08:10 +01:00
// locate a README for a tree in one of the supported paths.
//
// entries is passed to reduce calls to ListEntries(), so
// this has precondition:
//
// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
//
2020-02-22 00:04:20 +01:00
// FIXME: There has to be a more efficient way of doing this
2023-03-15 22:51:39 +01:00
func findReadmeFileInEntries ( ctx * context . Context , entries [ ] * git . TreeEntry ) ( string , * git . TreeEntry , error ) {
2022-08-01 00:36:58 +02:00
// Create a list of extensions in priority order
// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
// 2. Txt files - e.g. README.txt
// 3. No extension - e.g. README
exts := append ( localizedExtensions ( ".md" , ctx . Language ( ) ) , ".txt" , "" ) // sorted by priority
extCount := len ( exts )
2023-03-15 22:51:39 +01:00
readmeFiles := make ( [ ] * git . TreeEntry , extCount + 1 )
2023-02-12 08:08:10 +01:00
docsEntries := make ( [ ] * git . TreeEntry , 3 ) // (one of docs/, .gitea/ or .github/)
2020-02-22 00:04:20 +01:00
for _ , entry := range entries {
if entry . IsDir ( ) {
2023-02-12 08:08:10 +01:00
// as a special case for the top-level repo introduction README,
// fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ...
// (note that docsEntries is ignored unless we are at the root)
lowerName := strings . ToLower ( entry . Name ( ) )
switch lowerName {
case "docs" :
if entry . Name ( ) == "docs" || docsEntries [ 0 ] == nil {
docsEntries [ 0 ] = entry
}
case ".gitea" :
if entry . Name ( ) == ".gitea" || docsEntries [ 1 ] == nil {
docsEntries [ 1 ] = entry
}
case ".github" :
if entry . Name ( ) == ".github" || docsEntries [ 2 ] == nil {
docsEntries [ 2 ] = entry
}
}
2020-02-22 00:04:20 +01:00
continue
}
2023-02-13 21:01:09 +01:00
if i , ok := util . IsReadmeFileExtension ( entry . Name ( ) , exts ... ) ; ok {
2023-02-12 08:08:10 +01:00
log . Debug ( "Potential readme file: %s" , entry . Name ( ) )
2023-03-15 22:51:39 +01:00
if readmeFiles [ i ] == nil || base . NaturalSortLess ( readmeFiles [ i ] . Name ( ) , entry . Blob ( ) . Name ( ) ) {
if entry . IsLink ( ) {
target , err := entry . FollowLinks ( )
2020-02-22 00:04:20 +01:00
if err != nil && ! git . IsErrBadLink ( err ) {
2023-03-15 22:51:39 +01:00
return "" , nil , err
} else if target != nil && ( target . IsExecutable ( ) || target . IsRegular ( ) ) {
readmeFiles [ i ] = entry
2020-02-22 00:04:20 +01:00
}
2023-03-15 22:51:39 +01:00
} else {
readmeFiles [ i ] = entry
2020-02-22 00:04:20 +01:00
}
}
}
}
2023-03-15 22:51:39 +01:00
var readmeFile * git . TreeEntry
2020-02-22 00:04:20 +01:00
for _ , f := range readmeFiles {
if f != nil {
readmeFile = f
break
}
}
2023-02-12 08:08:10 +01:00
if ctx . Repo . TreePath == "" && readmeFile == nil {
for _ , subTreeEntry := range docsEntries {
if subTreeEntry == nil {
continue
}
subTree := subTreeEntry . Tree ( )
if subTree == nil {
// this should be impossible; if subTreeEntry exists so should this.
continue
}
var err error
childEntries , err := subTree . ListEntries ( )
if err != nil {
2023-03-15 22:51:39 +01:00
return "" , nil , err
2023-02-12 08:08:10 +01:00
}
2023-03-15 22:51:39 +01:00
subfolder , readmeFile , err := findReadmeFileInEntries ( ctx , childEntries )
2023-02-12 08:08:10 +01:00
if err != nil && ! git . IsErrNotExist ( err ) {
2023-03-15 22:51:39 +01:00
return "" , nil , err
2023-02-12 08:08:10 +01:00
}
if readmeFile != nil {
2023-03-15 22:51:39 +01:00
return path . Join ( subTreeEntry . Name ( ) , subfolder ) , readmeFile , nil
2023-02-12 08:08:10 +01:00
}
}
}
2023-03-15 22:51:39 +01:00
return "" , readmeFile , nil
2020-02-22 00:04:20 +01:00
}
2016-08-30 11:08:38 +02:00
func renderDirectory ( ctx * context . Context , treeLink string ) {
2021-10-08 15:08:22 +02:00
entries := renderDirectoryFiles ( ctx , 1 * time . Second )
if ctx . Written ( ) {
2014-07-26 06:24:27 +02:00
return
}
2021-12-15 08:50:11 +01:00
if ctx . Repo . TreePath != "" {
2022-10-13 10:31:10 +02:00
ctx . Data [ "HideRepoInfo" ] = true
2021-12-15 08:50:11 +01:00
ctx . Data [ "Title" ] = ctx . Tr ( "repo.file.title" , ctx . Repo . Repository . Name + "/" + path . Base ( ctx . Repo . TreePath ) , ctx . Repo . RefName )
}
2022-04-26 22:31:15 +02:00
// Check permission to add or upload new file.
if ctx . Repo . CanWrite ( unit_model . TypeCode ) && ctx . Repo . IsViewBranch {
ctx . Data [ "CanAddFile" ] = ! ctx . Repo . Repository . IsArchived
ctx . Data [ "CanUploadFile" ] = setting . Repository . Upload . Enabled && ! ctx . Repo . Repository . IsArchived
}
2023-02-12 08:08:10 +01:00
if ctx . Written ( ) {
return
}
2023-03-15 22:51:39 +01:00
subfolder , readmeFile , err := findReadmeFileInEntries ( ctx , entries )
2023-02-12 08:08:10 +01:00
if err != nil {
ctx . ServerError ( "findReadmeFileInEntries" , err )
return
}
2022-04-26 22:31:15 +02:00
2023-03-15 22:51:39 +01:00
renderReadmeFile ( ctx , subfolder , readmeFile , treeLink )
2022-04-26 22:31:15 +02:00
}
2022-08-01 00:36:58 +02:00
// localizedExtensions prepends the provided language code with and without a
Fix various typos (#21103)
Found via `codespell -q 3 -S
./options/locale,./options/license,./public/vendor,./web_src/fomantic -L
actived,allways,attachements,ba,befores,commiter,pullrequest,pullrequests,readby,splitted,te,unknwon`
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
2022-09-07 20:40:36 +02:00
// regional identifier to the provided extension.
2022-08-01 00:36:58 +02:00
// Note: the language code will always be lower-cased, if a region is present it must be separated with a `-`
// Note: ext should be prefixed with a `.`
func localizedExtensions ( ext , languageCode string ) ( localizedExts [ ] string ) {
if len ( languageCode ) < 1 {
return [ ] string { ext }
}
lowerLangCode := "." + strings . ToLower ( languageCode )
if strings . Contains ( lowerLangCode , "-" ) {
underscoreLangCode := strings . ReplaceAll ( lowerLangCode , "-" , "_" )
indexOfDash := strings . Index ( lowerLangCode , "-" )
2022-10-24 08:12:15 +02:00
// e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md]
return [ ] string { lowerLangCode + ext , underscoreLangCode + ext , lowerLangCode [ : indexOfDash ] + ext , "_" + lowerLangCode [ 1 : indexOfDash ] + ext , ext }
2022-08-01 00:36:58 +02:00
}
// e.g. [.en.md, .md]
return [ ] string { lowerLangCode + ext , ext }
}
2022-12-14 11:11:11 +01:00
type fileInfo struct {
isTextFile bool
isLFSFile bool
fileSize int64
lfsMeta * lfs . Pointer
st typesniffer . SniffedType
}
2016-08-30 11:08:38 +02:00
2022-12-14 11:11:11 +01:00
func getFileReader ( repoID int64 , blob * git . Blob ) ( [ ] byte , io . ReadCloser , * fileInfo , error ) {
dataRc , err := blob . DataAsync ( )
2022-04-26 22:31:15 +02:00
if err != nil {
2022-12-14 11:11:11 +01:00
return nil , nil , nil , err
2022-04-26 22:31:15 +02:00
}
buf := make ( [ ] byte , 1024 )
n , _ := util . ReadAtMost ( dataRc , buf )
buf = buf [ : n ]
st := typesniffer . DetectContentType ( buf )
isTextFile := st . IsText ( )
// FIXME: what happens when README file is an image?
2022-12-14 11:11:11 +01:00
if ! isTextFile || ! setting . LFS . StartServer {
return buf , dataRc , & fileInfo { isTextFile , false , blob . Size ( ) , nil , st } , nil
}
2022-04-26 22:31:15 +02:00
2022-12-14 11:11:11 +01:00
pointer , _ := lfs . ReadPointerFromBuffer ( buf )
if ! pointer . IsValid ( ) { // fallback to plain file
return buf , dataRc , & fileInfo { isTextFile , false , blob . Size ( ) , nil , st } , nil
}
2019-02-21 21:57:16 +01:00
2023-01-09 04:50:54 +01:00
meta , err := git_model . GetLFSMetaObjectByOid ( db . DefaultContext , repoID , pointer . Oid )
2022-12-21 02:21:26 +01:00
if err != nil && err != git_model . ErrLFSObjectNotExist { // fallback to plain file
2022-12-14 11:11:11 +01:00
return buf , dataRc , & fileInfo { isTextFile , false , blob . Size ( ) , nil , st } , nil
}
2019-02-21 21:57:16 +01:00
2022-12-14 11:11:11 +01:00
dataRc . Close ( )
if err != nil {
return nil , nil , nil , err
}
2019-02-21 21:57:16 +01:00
2022-12-14 11:11:11 +01:00
dataRc , err = lfs . ReadMetaObject ( pointer )
if err != nil {
return nil , nil , nil , err
2022-04-26 22:31:15 +02:00
}
2019-02-21 21:57:16 +01:00
2022-12-14 11:11:11 +01:00
buf = make ( [ ] byte , 1024 )
n , err = util . ReadAtMost ( dataRc , buf )
if err != nil {
dataRc . Close ( )
return nil , nil , nil , err
}
buf = buf [ : n ]
st = typesniffer . DetectContentType ( buf )
return buf , dataRc , & fileInfo { st . IsText ( ) , true , meta . Size , & meta . Pointer , st } , nil
}
2023-03-15 22:51:39 +01:00
func renderReadmeFile ( ctx * context . Context , subfolder string , readmeFile * git . TreeEntry , readmeTreelink string ) {
target := readmeFile
if readmeFile != nil && readmeFile . IsLink ( ) {
target , _ = readmeFile . FollowLinks ( )
}
if target == nil {
// if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
// simply skip rendering the README
return
}
2022-12-14 11:11:11 +01:00
ctx . Data [ "RawFileLink" ] = ""
ctx . Data [ "ReadmeInList" ] = true
ctx . Data [ "ReadmeExist" ] = true
2023-03-15 22:51:39 +01:00
ctx . Data [ "FileIsSymlink" ] = readmeFile . IsLink ( )
2022-12-14 11:11:11 +01:00
2023-03-15 22:51:39 +01:00
buf , dataRc , fInfo , err := getFileReader ( ctx . Repo . Repository . ID , target . Blob ( ) )
2022-12-14 11:11:11 +01:00
if err != nil {
ctx . ServerError ( "getFileReader" , err )
2022-04-26 22:31:15 +02:00
return
}
2022-12-14 11:11:11 +01:00
defer dataRc . Close ( )
ctx . Data [ "FileIsText" ] = fInfo . isTextFile
2023-03-15 22:51:39 +01:00
ctx . Data [ "FileName" ] = path . Join ( subfolder , readmeFile . Name ( ) )
2022-12-14 11:11:11 +01:00
ctx . Data [ "IsLFSFile" ] = fInfo . isLFSFile
2019-02-21 21:57:16 +01:00
2022-12-14 11:11:11 +01:00
if fInfo . isLFSFile {
2023-03-15 22:51:39 +01:00
filenameBase64 := base64 . RawURLEncoding . EncodeToString ( [ ] byte ( readmeFile . Name ( ) ) )
2023-02-11 07:34:11 +01:00
ctx . Data [ "RawFileLink" ] = fmt . Sprintf ( "%s.git/info/lfs/objects/%s/%s" , ctx . Repo . Repository . Link ( ) , url . PathEscape ( fInfo . lfsMeta . Oid ) , url . PathEscape ( filenameBase64 ) )
2022-04-26 22:31:15 +02:00
}
2022-01-07 02:18:52 +01:00
2022-12-14 11:11:11 +01:00
if ! fInfo . isTextFile {
return
}
if fInfo . fileSize >= setting . UI . MaxDisplayFileSize {
2022-04-26 22:31:15 +02:00
// Pretend that this is a normal text file to display 'This file is too large to be shown'
ctx . Data [ "IsFileTooLarge" ] = true
ctx . Data [ "IsTextFile" ] = true
2022-12-14 11:11:11 +01:00
ctx . Data [ "FileSize" ] = fInfo . fileSize
2022-04-26 22:31:15 +02:00
return
2016-08-30 11:08:38 +02:00
}
2016-08-15 08:02:14 +02:00
2022-04-26 22:31:15 +02:00
rd := charset . ToUTF8WithFallbackReader ( io . MultiReader ( bytes . NewReader ( buf ) , dataRc ) )
2023-03-15 22:51:39 +01:00
if markupType := markup . Type ( readmeFile . Name ( ) ) ; markupType != "" {
2022-04-26 22:31:15 +02:00
ctx . Data [ "IsMarkup" ] = true
2022-06-20 12:02:49 +02:00
ctx . Data [ "MarkupType" ] = markupType
2022-08-13 20:32:34 +02:00
ctx . Data [ "EscapeStatus" ] , ctx . Data [ "FileContent" ] , err = markupRender ( ctx , & markup . RenderContext {
2022-06-16 05:33:23 +02:00
Ctx : ctx ,
2023-03-15 22:51:39 +01:00
RelativePath : path . Join ( ctx . Repo . TreePath , readmeFile . Name ( ) ) , // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
URLPrefix : path . Join ( readmeTreelink , subfolder ) ,
2022-06-16 05:33:23 +02:00
Metas : ctx . Repo . Repository . ComposeDocumentMetas ( ) ,
GitRepo : ctx . Repo . GitRepo ,
2022-08-13 20:32:34 +02:00
} , rd )
2022-04-26 22:31:15 +02:00
if err != nil {
2023-03-15 22:51:39 +01:00
log . Error ( "Render failed for %s in %-v: %v Falling back to rendering source" , readmeFile . Name ( ) , ctx . Repo . Repository , err )
2022-04-26 22:31:15 +02:00
buf := & bytes . Buffer { }
2022-12-17 21:22:25 +01:00
ctx . Data [ "EscapeStatus" ] , _ = charset . EscapeControlStringReader ( rd , buf , ctx . Locale )
ctx . Data [ "FileContent" ] = buf . String ( )
2022-04-26 22:31:15 +02:00
}
} else {
2022-12-17 21:22:25 +01:00
ctx . Data [ "IsPlainText" ] = true
2022-04-26 22:31:15 +02:00
buf := & bytes . Buffer { }
2022-12-17 21:22:25 +01:00
ctx . Data [ "EscapeStatus" ] , err = charset . EscapeControlStringReader ( rd , buf , ctx . Locale )
2022-04-26 22:31:15 +02:00
if err != nil {
log . Error ( "Read failed: %v" , err )
}
2022-08-13 20:32:34 +02:00
ctx . Data [ "FileContent" ] = buf . String ( )
2016-08-30 11:08:38 +02:00
}
}
2014-09-30 10:39:53 +02:00
2016-08-30 11:08:38 +02:00
func renderFile ( ctx * context . Context , entry * git . TreeEntry , treeLink , rawLink string ) {
ctx . Data [ "IsViewFile" ] = true
2022-10-13 10:31:10 +02:00
ctx . Data [ "HideRepoInfo" ] = true
2016-08-30 11:08:38 +02:00
blob := entry . Blob ( )
2022-12-14 11:11:11 +01:00
buf , dataRc , fInfo , err := getFileReader ( ctx . Repo . Repository . ID , blob )
2016-08-30 11:08:38 +02:00
if err != nil {
2022-12-14 11:11:11 +01:00
ctx . ServerError ( "getFileReader" , err )
2016-08-30 11:08:38 +02:00
return
}
2017-11-29 02:50:39 +01:00
defer dataRc . Close ( )
2014-07-26 06:24:27 +02:00
2021-12-15 08:50:11 +01:00
ctx . Data [ "Title" ] = ctx . Tr ( "repo.file.title" , ctx . Repo . Repository . Name + "/" + path . Base ( ctx . Repo . TreePath ) , ctx . Repo . RefName )
2020-02-22 00:04:20 +01:00
ctx . Data [ "FileIsSymlink" ] = entry . IsLink ( )
2016-08-30 11:08:38 +02:00
ctx . Data [ "FileName" ] = blob . Name ( )
2021-11-16 19:18:25 +01:00
ctx . Data [ "RawFileLink" ] = rawLink + "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
2016-08-30 11:08:38 +02:00
2022-09-12 00:16:56 +02:00
if ctx . Repo . TreePath == ".editorconfig" {
2022-09-14 20:54:30 +02:00
_ , editorconfigErr := ctx . Repo . GetEditorconfig ( ctx . Repo . Commit )
2022-09-12 00:16:56 +02:00
ctx . Data [ "FileError" ] = editorconfigErr
2023-03-28 20:22:07 +02:00
} else if ctx . Repo . IsIssueConfig ( ctx . Repo . TreePath ) {
_ , issueConfigErr := ctx . Repo . GetIssueConfig ( ctx . Repo . TreePath , ctx . Repo . Commit )
ctx . Data [ "FileError" ] = issueConfigErr
2022-09-12 00:16:56 +02:00
}
2021-08-11 02:31:13 +02:00
isDisplayingSource := ctx . FormString ( "display" ) == "source"
2021-01-13 04:45:19 +01:00
isDisplayingRendered := ! isDisplayingSource
2022-12-14 11:11:11 +01:00
if fInfo . isLFSFile {
ctx . Data [ "RawFileLink" ] = ctx . Repo . RepoLink + "/media/" + ctx . Repo . BranchNameSubURL ( ) + "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
2016-12-26 02:16:37 +01:00
}
2021-02-05 02:29:42 +01:00
2022-12-14 11:11:11 +01:00
isRepresentableAsText := fInfo . st . IsRepresentableAsText ( )
2021-02-05 02:29:42 +01:00
if ! isRepresentableAsText {
// If we can't show plain text, always try to render.
isDisplayingSource = false
isDisplayingRendered = true
}
2022-12-14 11:11:11 +01:00
ctx . Data [ "IsLFSFile" ] = fInfo . isLFSFile
ctx . Data [ "FileSize" ] = fInfo . fileSize
ctx . Data [ "IsTextFile" ] = fInfo . isTextFile
2021-02-05 02:29:42 +01:00
ctx . Data [ "IsRepresentableAsText" ] = isRepresentableAsText
ctx . Data [ "IsDisplayingSource" ] = isDisplayingSource
ctx . Data [ "IsDisplayingRendered" ] = isDisplayingRendered
2022-11-21 10:59:42 +01:00
2022-12-14 11:11:11 +01:00
isTextSource := fInfo . isTextFile || isDisplayingSource
2022-11-21 10:59:42 +01:00
ctx . Data [ "IsTextSource" ] = isTextSource
if isTextSource {
ctx . Data [ "CanCopyContent" ] = true
}
2021-02-05 02:29:42 +01:00
2019-10-29 22:32:21 +01:00
// Check LFS Lock
2023-01-09 04:50:54 +01:00
lfsLock , err := git_model . GetTreePathLock ( ctx , ctx . Repo . Repository . ID , ctx . Repo . TreePath )
2019-10-29 22:32:21 +01:00
ctx . Data [ "LFSLock" ] = lfsLock
if err != nil {
ctx . ServerError ( "GetTreePathLock" , err )
return
}
if lfsLock != nil {
2022-12-03 03:48:26 +01:00
u , err := user_model . GetUserByID ( ctx , lfsLock . OwnerID )
2021-11-24 10:49:20 +01:00
if err != nil {
ctx . ServerError ( "GetTreePathLock" , err )
return
}
2022-10-12 03:03:15 +02:00
ctx . Data [ "LFSLockOwner" ] = u . Name
2022-02-16 17:22:25 +01:00
ctx . Data [ "LFSLockOwnerHomeLink" ] = u . HomeLink ( )
2019-10-29 22:32:21 +01:00
ctx . Data [ "LFSLockHint" ] = ctx . Tr ( "repo.editor.this_file_locked" )
}
2016-12-26 02:16:37 +01:00
2016-08-30 11:08:38 +02:00
// Assume file is not editable first.
2022-12-14 11:11:11 +01:00
if fInfo . isLFSFile {
2019-02-12 16:09:43 +01:00
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.cannot_edit_lfs_files" )
2021-01-13 04:45:19 +01:00
} else if ! isRepresentableAsText {
2016-08-30 11:08:38 +02:00
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.cannot_edit_non_text_files" )
}
switch {
2021-01-13 04:45:19 +01:00
case isRepresentableAsText :
2022-12-14 11:11:11 +01:00
if fInfo . st . IsSvgImage ( ) {
2021-01-13 04:45:19 +01:00
ctx . Data [ "IsImageFile" ] = true
2022-11-21 10:59:42 +01:00
ctx . Data [ "CanCopyContent" ] = true
2021-01-13 04:45:19 +01:00
ctx . Data [ "HasSourceRenderedToggle" ] = true
}
2022-12-14 11:11:11 +01:00
if fInfo . fileSize >= setting . UI . MaxDisplayFileSize {
2016-08-30 11:08:38 +02:00
ctx . Data [ "IsFileTooLarge" ] = true
break
2014-07-26 06:24:27 +02:00
}
2021-04-20 00:25:08 +02:00
rd := charset . ToUTF8WithFallbackReader ( io . MultiReader ( bytes . NewReader ( buf ) , dataRc ) )
2022-04-10 17:01:35 +02:00
shouldRenderSource := ctx . FormString ( "display" ) == "source"
2023-02-13 21:01:09 +01:00
readmeExist := util . IsReadmeFileName ( blob . Name ( ) )
2016-08-30 11:08:38 +02:00
ctx . Data [ "ReadmeExist" ] = readmeExist
2022-04-10 17:01:35 +02:00
markupType := markup . Type ( blob . Name ( ) )
2022-06-08 23:46:39 +02:00
// If the markup is detected by custom markup renderer it should not be reset later on
// to not pass it down to the render context.
detected := false
if markupType == "" {
detected = true
markupType = markup . DetectRendererType ( blob . Name ( ) , bytes . NewReader ( buf ) )
}
2022-04-10 17:01:35 +02:00
if markupType != "" {
ctx . Data [ "HasSourceRenderedToggle" ] = true
}
if markupType != "" && ! shouldRenderSource {
2017-10-17 01:17:22 +02:00
ctx . Data [ "IsMarkup" ] = true
2019-08-16 00:09:50 +02:00
ctx . Data [ "MarkupType" ] = markupType
2022-06-08 23:46:39 +02:00
if ! detected {
markupType = ""
}
2022-06-16 05:33:23 +02:00
metas := ctx . Repo . Repository . ComposeDocumentMetas ( )
metas [ "BranchNameSubURL" ] = ctx . Repo . BranchNameSubURL ( )
2022-08-13 20:32:34 +02:00
ctx . Data [ "EscapeStatus" ] , ctx . Data [ "FileContent" ] , err = markupRender ( ctx , & markup . RenderContext {
2022-06-16 05:33:23 +02:00
Ctx : ctx ,
Type : markupType ,
RelativePath : ctx . Repo . TreePath ,
URLPrefix : path . Dir ( treeLink ) ,
Metas : metas ,
GitRepo : ctx . Repo . GitRepo ,
2022-08-13 20:32:34 +02:00
} , rd )
2021-04-20 00:25:08 +02:00
if err != nil {
ctx . ServerError ( "Render" , err )
return
}
2022-06-16 05:33:23 +02:00
// to prevent iframe load third-party url
ctx . Resp . Header ( ) . Add ( "Content-Security-Policy" , "frame-src 'self'" )
2016-08-30 11:08:38 +02:00
} else {
2021-09-22 07:38:34 +02:00
buf , _ := io . ReadAll ( rd )
2022-07-30 21:17:43 +02:00
// empty: 0 lines; "a": one line; "a\n": two lines; "a\nb": two lines;
// the NumLines is only used for the display on the UI: "xxx lines"
if len ( buf ) == 0 {
ctx . Data [ "NumLines" ] = 0
} else {
ctx . Data [ "NumLines" ] = bytes . Count ( buf , [ ] byte { '\n' } ) + 1
}
2019-10-29 17:05:26 +01:00
ctx . Data [ "NumLinesSet" ] = true
2021-11-17 21:37:00 +01: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 03:30:43 +01:00
Attributes : [ ] string { "linguist-language" , "gitlab-language" } ,
2021-11-17 21:37:00 +01: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-11-19 12:08:06 +01:00
fileContent , lexerName , err := highlight . File ( blob . Name ( ) , language , buf )
ctx . Data [ "LexerName" ] = lexerName
2022-07-30 21:17:43 +02:00
if err != nil {
log . Error ( "highlight.File failed, fallback to plain text: %v" , err )
fileContent = highlight . PlainText ( buf )
}
2022-08-13 20:32:34 +02:00
status := & charset . EscapeStatus { }
statuses := make ( [ ] * charset . EscapeStatus , len ( fileContent ) )
2022-01-07 02:18:52 +01:00
for i , line := range fileContent {
2022-08-13 20:32:34 +02:00
statuses [ i ] , fileContent [ i ] = charset . EscapeControlHTML ( line , ctx . Locale )
status = status . Or ( statuses [ i ] )
2022-01-07 02:18:52 +01:00
}
2022-08-13 20:32:34 +02:00
ctx . Data [ "EscapeStatus" ] = status
2022-01-07 02:18:52 +01:00
ctx . Data [ "FileContent" ] = fileContent
ctx . Data [ "LineEscapeStatus" ] = statuses
2014-09-26 14:55:13 +02:00
}
2022-12-14 11:11:11 +01:00
if ! fInfo . isLFSFile {
2022-04-28 17:45:33 +02:00
if ctx . Repo . CanEnableEditor ( ctx . Doer ) {
2022-03-22 08:03:22 +01:00
if lfsLock != nil && lfsLock . OwnerID != ctx . Doer . ID {
2019-10-29 22:32:21 +01:00
ctx . Data [ "CanEditFile" ] = false
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.this_file_locked" )
} else {
ctx . Data [ "CanEditFile" ] = true
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.edit_this_file" )
}
2019-02-12 16:09:43 +01:00
} else if ! ctx . Repo . IsViewBranch {
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.must_be_on_a_branch" )
2022-04-28 17:45:33 +02:00
} else if ! ctx . Repo . CanWriteToBranch ( ctx . Doer , ctx . Repo . BranchName ) {
2019-02-12 16:09:43 +01:00
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.fork_before_edit" )
}
2016-08-28 10:41:44 +02:00
}
2016-08-30 11:08:38 +02:00
2022-12-14 11:11:11 +01:00
case fInfo . st . IsPDF ( ) :
2016-08-30 11:08:38 +02:00
ctx . Data [ "IsPDFFile" ] = true
2022-12-14 11:11:11 +01:00
case fInfo . st . IsVideo ( ) :
2016-12-20 09:09:11 +01:00
ctx . Data [ "IsVideoFile" ] = true
2022-12-14 11:11:11 +01:00
case fInfo . st . IsAudio ( ) :
2018-10-30 03:17:26 +01:00
ctx . Data [ "IsAudioFile" ] = true
2022-12-14 11:11:11 +01:00
case fInfo . st . IsImage ( ) && ( setting . UI . SVG . Enabled || ! fInfo . st . IsSvgImage ( ) ) :
2016-08-30 11:08:38 +02:00
ctx . Data [ "IsImageFile" ] = true
2022-11-21 10:59:42 +01:00
ctx . Data [ "CanCopyContent" ] = true
2019-10-21 08:54:18 +02:00
default :
2022-12-14 11:11:11 +01:00
if fInfo . fileSize >= setting . UI . MaxDisplayFileSize {
2019-10-21 08:54:18 +02:00
ctx . Data [ "IsFileTooLarge" ] = true
break
}
if markupType := markup . Type ( blob . Name ( ) ) ; markupType != "" {
2021-04-20 00:25:08 +02:00
rd := io . MultiReader ( bytes . NewReader ( buf ) , dataRc )
2019-10-21 08:54:18 +02:00
ctx . Data [ "IsMarkup" ] = true
ctx . Data [ "MarkupType" ] = markupType
2022-08-13 20:32:34 +02:00
ctx . Data [ "EscapeStatus" ] , ctx . Data [ "FileContent" ] , err = markupRender ( ctx , & markup . RenderContext {
2022-06-16 05:33:23 +02:00
Ctx : ctx ,
RelativePath : ctx . Repo . TreePath ,
URLPrefix : path . Dir ( treeLink ) ,
Metas : ctx . Repo . Repository . ComposeDocumentMetas ( ) ,
GitRepo : ctx . Repo . GitRepo ,
2022-08-13 20:32:34 +02:00
} , rd )
2021-04-20 00:25:08 +02:00
if err != nil {
ctx . ServerError ( "Render" , err )
return
}
2019-10-21 08:54:18 +02:00
}
2014-07-26 06:24:27 +02:00
}
2022-04-28 17:45:33 +02:00
if ctx . Repo . CanEnableEditor ( ctx . Doer ) {
2022-03-22 08:03:22 +01:00
if lfsLock != nil && lfsLock . OwnerID != ctx . Doer . ID {
2019-10-29 22:32:21 +01:00
ctx . Data [ "CanDeleteFile" ] = false
ctx . Data [ "DeleteFileTooltip" ] = ctx . Tr ( "repo.editor.this_file_locked" )
} else {
ctx . Data [ "CanDeleteFile" ] = true
ctx . Data [ "DeleteFileTooltip" ] = ctx . Tr ( "repo.editor.delete_this_file" )
}
2016-08-30 11:08:38 +02:00
} else if ! ctx . Repo . IsViewBranch {
ctx . Data [ "DeleteFileTooltip" ] = ctx . Tr ( "repo.editor.must_be_on_a_branch" )
2022-04-28 17:45:33 +02:00
} else if ! ctx . Repo . CanWriteToBranch ( ctx . Doer , ctx . Repo . BranchName ) {
2016-08-30 11:08:38 +02:00
ctx . Data [ "DeleteFileTooltip" ] = ctx . Tr ( "repo.editor.must_have_write_access" )
}
}
2022-08-13 20:32:34 +02:00
func markupRender ( ctx * context . Context , renderCtx * markup . RenderContext , input io . Reader ) ( escaped * charset . EscapeStatus , output string , err error ) {
markupRd , markupWr := io . Pipe ( )
defer markupWr . Close ( )
done := make ( chan struct { } )
go func ( ) {
sb := & strings . Builder { }
// We allow NBSP here this is rendered
escaped , _ = charset . EscapeControlReader ( markupRd , sb , ctx . Locale , charset . RuneNBSP )
output = sb . String ( )
close ( done )
} ( )
err = markup . Render ( renderCtx , input , markupWr )
_ = markupWr . CloseWithError ( err )
<- done
return escaped , output , err
}
2019-10-13 15:23:14 +02:00
func safeURL ( address string ) string {
u , err := url . Parse ( address )
if err != nil {
return address
}
u . User = nil
return u . String ( )
}
2021-10-08 15:08:22 +02:00
func checkHomeCodeViewable ( ctx * context . Context ) {
2018-11-28 12:26:14 +01:00
if len ( ctx . Repo . Units ) > 0 {
2019-10-13 15:23:14 +02:00
if ctx . Repo . Repository . IsBeingCreated ( ) {
2022-08-25 04:31:57 +02:00
task , err := admin_model . GetMigratingTask ( ctx . Repo . Repository . ID )
2019-10-13 15:23:14 +02:00
if err != nil {
2022-08-25 04:31:57 +02:00
if admin_model . IsErrTaskDoesNotExist ( err ) {
2021-11-13 12:28:50 +01:00
ctx . Data [ "Repo" ] = ctx . Repo
ctx . Data [ "CloneAddr" ] = ""
ctx . Data [ "Failed" ] = true
ctx . HTML ( http . StatusOK , tplMigrating )
return
}
2019-10-13 15:23:14 +02:00
ctx . ServerError ( "models.GetMigratingTask" , err )
return
}
cfg , err := task . MigrateConfig ( )
if err != nil {
ctx . ServerError ( "task.MigrateConfig" , err )
return
}
ctx . Data [ "Repo" ] = ctx . Repo
ctx . Data [ "MigrateTask" ] = task
ctx . Data [ "CloneAddr" ] = safeURL ( cfg . CloneAddr )
2021-06-30 21:14:53 +02:00
ctx . Data [ "Failed" ] = task . Status == structs . TaskStatusFailed
2021-04-05 17:30:52 +02:00
ctx . HTML ( http . StatusOK , tplMigrating )
2019-10-13 15:23:14 +02:00
return
}
2021-03-01 01:47:30 +01:00
if ctx . IsSigned {
// Set repo notification-status read if unread
2022-08-25 04:31:57 +02:00
if err := activities_model . SetRepoReadBy ( ctx , ctx . Repo . Repository . ID , ctx . Doer . ID ) ; err != nil {
2021-03-01 01:47:30 +01:00
ctx . ServerError ( "ReadBy" , err )
return
}
}
2021-11-09 20:57:58 +01:00
var firstUnit * unit_model . Unit
2018-11-28 12:26:14 +01:00
for _ , repoUnit := range ctx . Repo . Units {
2021-11-09 20:57:58 +01:00
if repoUnit . Type == unit_model . TypeCode {
2017-10-01 15:50:56 +02:00
return
}
2021-11-09 20:57:58 +01:00
unit , ok := unit_model . Units [ repoUnit . Type ]
2017-10-01 15:50:56 +02:00
if ok && ( firstUnit == nil || ! firstUnit . IsLessThan ( unit ) ) {
firstUnit = & unit
}
2017-05-18 16:54:24 +02:00
}
2017-10-01 15:50:56 +02:00
if firstUnit != nil {
2021-11-16 19:18:25 +01:00
ctx . Redirect ( fmt . Sprintf ( "%s%s" , ctx . Repo . Repository . Link ( ) , firstUnit . URI ) )
2017-05-18 16:54:24 +02:00
return
}
}
2018-01-10 22:34:17 +01:00
ctx . NotFound ( "Home" , fmt . Errorf ( ctx . Tr ( "units.error.no_unit_allowed_repo" ) ) )
2017-05-18 16:54:24 +02:00
}
2022-11-11 18:02:50 +01:00
func checkCitationFile ( ctx * context . Context , entry * git . TreeEntry ) {
if entry . Name ( ) != "" {
return
}
tree , err := ctx . Repo . Commit . SubTree ( ctx . Repo . TreePath )
if err != nil {
ctx . NotFoundOrServerError ( "Repo.Commit.SubTree" , git . IsErrNotExist , err )
return
}
allEntries , err := tree . ListEntries ( )
if err != nil {
ctx . ServerError ( "ListEntries" , err )
return
}
for _ , entry := range allEntries {
if entry . Name ( ) == "CITATION.cff" || entry . Name ( ) == "CITATION.bib" {
ctx . Data [ "CitiationExist" ] = true
// Read Citation file contents
blob := entry . Blob ( )
dataRc , err := blob . DataAsync ( )
if err != nil {
ctx . ServerError ( "DataAsync" , err )
return
}
defer dataRc . Close ( )
buf := make ( [ ] byte , 1024 )
n , err := util . ReadAtMost ( dataRc , buf )
if err != nil {
ctx . ServerError ( "ReadAtMost" , err )
return
}
buf = buf [ : n ]
ctx . PageData [ "citationFileContent" ] = string ( buf )
break
}
}
}
2021-10-08 15:08:22 +02:00
// Home render repository home page
func Home ( ctx * context . Context ) {
2022-11-21 06:14:58 +01:00
if setting . EnableFeed {
isFeed , _ , showFeedType := feed . GetFeedType ( ctx . Params ( ":reponame" ) , ctx . Req )
if isFeed {
feed . ShowRepoFeed ( ctx , ctx . Repo . Repository , showFeedType )
return
}
2022-03-13 17:40:47 +01:00
2022-11-21 06:14:58 +01:00
ctx . Data [ "EnableFeed" ] = true
2023-02-11 07:34:11 +01:00
ctx . Data [ "FeedURL" ] = ctx . Repo . Repository . Link ( )
2022-11-21 06:14:58 +01:00
}
2022-03-13 17:40:47 +01:00
2021-10-08 15:08:22 +02:00
checkHomeCodeViewable ( ctx )
if ctx . Written ( ) {
return
}
renderCode ( ctx )
}
// LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
func LastCommit ( ctx * context . Context ) {
checkHomeCodeViewable ( ctx )
if ctx . Written ( ) {
return
}
renderDirectoryFiles ( ctx , 0 )
if ctx . Written ( ) {
return
}
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 ) - 2 ]
}
}
branchLink := ctx . Repo . RepoLink + "/src/" + ctx . Repo . BranchNameSubURL ( )
ctx . Data [ "BranchLink" ] = branchLink
ctx . HTML ( http . StatusOK , tplRepoViewList )
}
func renderDirectoryFiles ( ctx * context . Context , timeout time . Duration ) git . Entries {
tree , err := ctx . Repo . Commit . SubTree ( ctx . Repo . TreePath )
if err != nil {
ctx . NotFoundOrServerError ( "Repo.Commit.SubTree" , git . IsErrNotExist , err )
return nil
}
2021-11-16 19:18:25 +01:00
ctx . Data [ "LastCommitLoaderURL" ] = ctx . Repo . RepoLink + "/lastcommit/" + url . PathEscape ( ctx . Repo . CommitID ) + "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
2021-10-08 15:08:22 +02:00
// 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 nil
}
if ! entry . IsDir ( ) {
ctx . NotFoundOrServerError ( "Repo.Commit.GetTreeEntryByPath" , git . IsErrNotExist , err )
return nil
}
allEntries , err := tree . ListEntries ( )
if err != nil {
ctx . ServerError ( "ListEntries" , err )
return nil
}
allEntries . CustomSort ( base . NaturalSortLess )
commitInfoCtx := gocontext . Context ( ctx )
if timeout > 0 {
var cancel gocontext . CancelFunc
commitInfoCtx , cancel = gocontext . WithTimeout ( ctx , timeout )
defer cancel ( )
}
2022-10-12 07:18:26 +02:00
selected := make ( container . Set [ string ] )
selected . AddMultiple ( ctx . FormStrings ( "f[]" ) ... )
2021-10-08 15:08:22 +02:00
entries := allEntries
if len ( selected ) > 0 {
entries = make ( git . Entries , 0 , len ( selected ) )
for _ , entry := range allEntries {
2022-10-12 07:18:26 +02:00
if selected . Contains ( entry . Name ( ) ) {
2021-10-08 15:08:22 +02:00
entries = append ( entries , entry )
}
}
}
var latestCommit * git . Commit
2022-07-25 17:39:42 +02:00
ctx . Data [ "Files" ] , latestCommit , err = entries . GetCommitsInfo ( commitInfoCtx , ctx . Repo . Commit , ctx . Repo . TreePath )
2021-10-08 15:08:22 +02:00
if err != nil {
ctx . ServerError ( "GetCommitsInfo" , err )
return nil
}
// Show latest commit info of repository in table header,
// or of directory if not in root directory.
ctx . Data [ "LatestCommit" ] = latestCommit
if latestCommit != nil {
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 14:37:34 +01:00
verification := asymkey_model . ParseCommitWithSignature ( ctx , latestCommit )
2021-10-08 15:08:22 +02:00
2021-12-10 09:14:24 +01:00
if err := asymkey_model . CalculateTrustStatus ( verification , ctx . Repo . Repository . GetTrustModel ( ) , func ( user * user_model . User ) ( bool , error ) {
2022-06-12 17:51:54 +02:00
return repo_model . IsOwnerMemberCollaborator ( ctx . Repo . Repository , user . ID )
2021-12-10 09:14:24 +01:00
} , nil ) ; err != nil {
2021-10-08 15:08:22 +02:00
ctx . ServerError ( "CalculateTrustStatus" , err )
return nil
}
ctx . Data [ "LatestCommitVerification" ] = verification
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 14:37:34 +01:00
ctx . Data [ "LatestCommitUser" ] = user_model . ValidateCommitWithEmail ( ctx , latestCommit )
2021-10-08 15:08:22 +02:00
2022-07-15 15:01:32 +02:00
statuses , _ , err := git_model . GetLatestCommitStatus ( ctx , ctx . Repo . Repository . ID , latestCommit . ID . String ( ) , db . ListOptions { } )
if err != nil {
log . Error ( "GetLatestCommitStatus: %v" , err )
}
2021-10-08 15:08:22 +02:00
2022-07-15 15:01:32 +02:00
ctx . Data [ "LatestCommitStatus" ] = git_model . CalcCommitStatus ( statuses )
ctx . Data [ "LatestCommitStatuses" ] = statuses
}
2021-10-08 15:08:22 +02:00
branchLink := ctx . Repo . RepoLink + "/src/" + ctx . Repo . BranchNameSubURL ( )
treeLink := branchLink
if len ( ctx . Repo . TreePath ) > 0 {
2021-11-16 19:18:25 +01:00
treeLink += "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
2021-10-08 15:08:22 +02:00
}
ctx . Data [ "TreeLink" ] = treeLink
ctx . Data [ "SSHDomain" ] = setting . SSH . Domain
return allEntries
}
2020-02-11 10:34:17 +01:00
func renderLanguageStats ( ctx * context . Context ) {
2021-12-10 02:27:50 +01:00
langs , err := repo_model . GetTopLanguageStats ( ctx . Repo . Repository , 5 )
2020-02-11 10:34:17 +01:00
if err != nil {
ctx . ServerError ( "Repo.GetTopLanguageStats" , err )
return
}
ctx . Data [ "LanguageStats" ] = langs
}
2020-05-05 23:51:49 +02:00
func renderRepoTopics ( ctx * context . Context ) {
2021-12-12 16:48:20 +01:00
topics , _ , err := repo_model . FindTopics ( & repo_model . FindTopicOptions {
2020-05-05 23:51:49 +02:00
RepoID : ctx . Repo . Repository . ID ,
} )
if err != nil {
ctx . ServerError ( "models.FindTopics" , err )
return
}
ctx . Data [ "Topics" ] = topics
}
2017-05-18 16:54:24 +02:00
func renderCode ( ctx * context . Context ) {
2017-03-18 11:59:07 +01:00
ctx . Data [ "PageIsViewCode" ] = true
2019-01-18 01:01:04 +01:00
if ctx . Repo . Repository . IsEmpty {
2022-07-27 18:46:34 +02:00
reallyEmpty := true
var err error
if ctx . Repo . GitRepo != nil {
reallyEmpty , err = ctx . Repo . GitRepo . IsEmpty ( )
if err != nil {
ctx . ServerError ( "GitRepo.IsEmpty" , err )
return
}
2021-12-24 16:36:26 +01:00
}
if reallyEmpty {
ctx . HTML ( http . StatusOK , tplRepoEMPTY )
return
}
// the repo is not really empty, so we should update the modal in database
// such problem may be caused by:
// 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually
// and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos.
// it's possible for a repository to be non-empty by that flag but still 500
// because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed.
ctx . Repo . Repository . IsEmpty = false
2022-05-20 16:08:52 +02:00
if err = repo_model . UpdateRepositoryCols ( ctx , ctx . Repo . Repository , "is_empty" ) ; err != nil {
2021-12-24 16:36:26 +01:00
ctx . ServerError ( "UpdateRepositoryCols" , err )
return
}
2022-06-06 10:01:49 +02:00
if err = repo_module . UpdateRepoSize ( ctx , ctx . Repo . Repository ) ; err != nil {
2021-12-24 16:36:26 +01:00
ctx . ServerError ( "UpdateRepoSize" , err )
return
}
2017-03-18 11:59:07 +01:00
}
2016-08-30 11:08:38 +02:00
title := ctx . Repo . Repository . Owner . Name + "/" + ctx . Repo . Repository . Name
if len ( ctx . Repo . Repository . Description ) > 0 {
title += ": " + ctx . Repo . Repository . Description
}
ctx . Data [ "Title" ] = title
2017-10-30 03:04:25 +01:00
branchLink := ctx . Repo . RepoLink + "/src/" + ctx . Repo . BranchNameSubURL ( )
2016-08-30 11:08:38 +02:00
treeLink := branchLink
2017-10-30 03:04:25 +01:00
rawLink := ctx . Repo . RepoLink + "/raw/" + ctx . Repo . BranchNameSubURL ( )
2016-08-30 11:08:38 +02:00
if len ( ctx . Repo . TreePath ) > 0 {
2021-11-16 19:18:25 +01:00
treeLink += "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
2016-08-30 11:08:38 +02:00
}
2018-04-11 04:51:44 +02:00
// Get Topics of this repo
2020-05-05 23:51:49 +02:00
renderRepoTopics ( ctx )
if ctx . Written ( ) {
2018-04-11 04:51:44 +02:00
return
}
2016-08-30 11:08:38 +02:00
// 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
}
2022-11-11 18:02:50 +01:00
if ! ctx . Repo . Repository . IsEmpty {
checkCitationFile ( ctx , entry )
if ctx . Written ( ) {
return
}
}
2020-02-11 10:34:17 +01:00
renderLanguageStats ( ctx )
if ctx . Written ( ) {
return
}
2016-08-30 11:08:38 +02:00
if entry . IsDir ( ) {
renderDirectory ( ctx , treeLink )
} else {
renderFile ( ctx , entry , treeLink , rawLink )
}
if ctx . Written ( ) {
return
}
2014-07-26 06:24:27 +02:00
2016-08-30 11:08:38 +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 ] , "/" ) )
2014-07-26 06:24:27 +02:00
}
ctx . Data [ "HasParentPath" ] = true
2016-08-09 21:56:00 +02:00
if len ( paths ) - 2 >= 0 {
ctx . Data [ "ParentPath" ] = "/" + paths [ len ( paths ) - 2 ]
2014-07-26 06:24:27 +02:00
}
}
2016-08-09 21:56:00 +02:00
ctx . Data [ "Paths" ] = paths
2016-08-28 00:25:01 +02:00
ctx . Data [ "TreeLink" ] = treeLink
2016-08-30 11:08:38 +02:00
ctx . Data [ "TreeNames" ] = treeNames
2014-07-26 06:24:27 +02:00
ctx . Data [ "BranchLink" ] = branchLink
2021-04-05 17:30:52 +02:00
ctx . HTML ( http . StatusOK , tplRepoHome )
2014-07-26 06:24:27 +02:00
}
2015-11-17 05:28:46 +01:00
2021-07-08 13:38:13 +02:00
// RenderUserCards render a page show users according the input template
2021-11-24 10:49:20 +01:00
func RenderUserCards ( ctx * context . Context , total int , getter func ( opts db . ListOptions ) ( [ ] * user_model . User , error ) , tpl base . TplName ) {
2021-07-29 03:42:15 +02:00
page := ctx . FormInt ( "page" )
2015-11-17 05:28:46 +01:00
if page <= 0 {
page = 1
}
2022-06-12 17:51:54 +02:00
pager := context . NewPagination ( total , setting . ItemsPerPage , page , 5 )
2015-11-17 05:28:46 +01:00
ctx . Data [ "Page" ] = pager
2021-09-24 13:32:56 +02:00
items , err := getter ( db . ListOptions {
2021-02-04 18:23:46 +01:00
Page : pager . Paginater . Current ( ) ,
2022-06-12 17:51:54 +02:00
PageSize : setting . ItemsPerPage ,
2021-02-04 18:23:46 +01:00
} )
2015-11-17 05:28:46 +01:00
if err != nil {
2018-01-10 22:34:17 +01:00
ctx . ServerError ( "getter" , err )
2015-11-17 05:28:46 +01:00
return
}
2015-12-21 13:24:11 +01:00
ctx . Data [ "Cards" ] = items
2015-11-17 05:28:46 +01:00
2021-04-05 17:30:52 +02:00
ctx . HTML ( http . StatusOK , tpl )
2015-11-17 05:28:46 +01:00
}
2016-11-21 11:03:29 +01:00
// Watchers render repository's watch users
2016-03-11 17:56:52 +01:00
func Watchers ( ctx * context . Context ) {
2015-11-17 05:28:46 +01:00
ctx . Data [ "Title" ] = ctx . Tr ( "repo.watchers" )
2015-12-21 13:24:11 +01:00
ctx . Data [ "CardsTitle" ] = ctx . Tr ( "repo.watchers" )
2015-11-17 05:28:46 +01:00
ctx . Data [ "PageIsWatchers" ] = true
2019-04-20 06:15:19 +02:00
2021-12-10 02:27:50 +01:00
RenderUserCards ( ctx , ctx . Repo . Repository . NumWatches , func ( opts db . ListOptions ) ( [ ] * user_model . User , error ) {
2021-12-12 16:48:20 +01:00
return repo_model . GetRepoWatchers ( ctx . Repo . Repository . ID , opts )
2021-12-10 02:27:50 +01:00
} , tplWatchers )
2015-11-17 05:28:46 +01:00
}
2016-11-21 11:03:29 +01:00
// Stars render repository's starred users
2016-03-11 17:56:52 +01:00
func Stars ( ctx * context . Context ) {
2015-11-17 05:28:46 +01:00
ctx . Data [ "Title" ] = ctx . Tr ( "repo.stargazers" )
2015-12-21 13:24:11 +01:00
ctx . Data [ "CardsTitle" ] = ctx . Tr ( "repo.stargazers" )
2015-11-17 05:28:46 +01:00
ctx . Data [ "PageIsStargazers" ] = true
2021-11-24 10:49:20 +01:00
RenderUserCards ( ctx , ctx . Repo . Repository . NumStars , func ( opts db . ListOptions ) ( [ ] * user_model . User , error ) {
2021-12-12 16:48:20 +01:00
return repo_model . GetStargazers ( ctx . Repo . Repository , opts )
2021-11-22 16:21:55 +01:00
} , tplWatchers )
2015-11-17 05:28:46 +01:00
}
2015-11-17 05:33:40 +01:00
2016-11-21 11:03:29 +01:00
// Forks render repository's forked users
2016-03-11 17:56:52 +01:00
func Forks ( ctx * context . Context ) {
2015-11-17 05:33:40 +01:00
ctx . Data [ "Title" ] = ctx . Tr ( "repos.forks" )
2021-11-18 15:45:56 +01:00
page := ctx . FormInt ( "page" )
if page <= 0 {
page = 1
}
2022-06-12 17:51:54 +02:00
pager := context . NewPagination ( ctx . Repo . Repository . NumForks , setting . ItemsPerPage , page , 5 )
2021-11-18 15:45:56 +01:00
ctx . Data [ "Page" ] = pager
2021-12-12 16:48:20 +01:00
forks , err := repo_model . GetForks ( ctx . Repo . Repository , db . ListOptions {
2021-11-18 15:45:56 +01:00
Page : pager . Paginater . Current ( ) ,
2022-06-12 17:51:54 +02:00
PageSize : setting . ItemsPerPage ,
2021-11-18 15:45:56 +01:00
} )
2015-11-17 05:33:40 +01:00
if err != nil {
2018-01-10 22:34:17 +01:00
ctx . ServerError ( "GetForks" , err )
2015-11-17 05:33:40 +01:00
return
}
for _ , fork := range forks {
2023-02-18 13:11:03 +01:00
if err = fork . LoadOwner ( ctx ) ; err != nil {
ctx . ServerError ( "LoadOwner" , err )
2015-11-17 05:33:40 +01:00
return
}
}
2021-11-18 15:45:56 +01:00
2015-11-17 05:33:40 +01:00
ctx . Data [ "Forks" ] = forks
2021-04-05 17:30:52 +02:00
ctx . HTML ( http . StatusOK , tplForks )
2015-11-17 05:33:40 +01:00
}