@ -5,18 +5,13 @@
package repo
import (
"bytes"
gocontext "context"
"encoding/base64"
"errors"
"fmt"
"html/template"
"image"
"io"
"net/http"
"net/url"
"path"
"slices"
"strings"
"time"
@ -29,33 +24,21 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issue_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/feed"
"code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
repo_service "code.gitea.io/gitea/services/repository"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/nektos/act/pkg/model"
_ "golang.org/x/image/bmp" // for processing bmp images
_ "golang.org/x/image/webp" // for processing webp images
@ -70,140 +53,6 @@ const (
tplMigrating base . TplName = "repo/migrate/migrating"
)
// 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()
//
// FIXME: There has to be a more efficient way of doing this
func findReadmeFileInEntries ( ctx * context . Context , entries [ ] * git . TreeEntry , tryWellKnownDirs bool ) ( string , * git . TreeEntry , error ) {
// 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 . Locale . Language ( ) ) , ".txt" , "" ) // sorted by priority
extCount := len ( exts )
readmeFiles := make ( [ ] * git . TreeEntry , extCount + 1 )
docsEntries := make ( [ ] * git . TreeEntry , 3 ) // (one of docs/, .gitea/ or .github/)
for _ , entry := range entries {
if tryWellKnownDirs && entry . IsDir ( ) {
// 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
}
}
continue
}
if i , ok := util . IsReadmeFileExtension ( entry . Name ( ) , exts ... ) ; ok {
log . Debug ( "Potential readme file: %s" , entry . Name ( ) )
if readmeFiles [ i ] == nil || base . NaturalSortLess ( readmeFiles [ i ] . Name ( ) , entry . Blob ( ) . Name ( ) ) {
if entry . IsLink ( ) {
target , err := entry . FollowLinks ( )
if err != nil && ! git . IsErrBadLink ( err ) {
return "" , nil , err
} else if target != nil && ( target . IsExecutable ( ) || target . IsRegular ( ) ) {
readmeFiles [ i ] = entry
}
} else {
readmeFiles [ i ] = entry
}
}
}
}
var readmeFile * git . TreeEntry
for _ , f := range readmeFiles {
if f != nil {
readmeFile = f
break
}
}
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
}
childEntries , err := subTree . ListEntries ( )
if err != nil {
return "" , nil , err
}
subfolder , readmeFile , err := findReadmeFileInEntries ( ctx , childEntries , false )
if err != nil && ! git . IsErrNotExist ( err ) {
return "" , nil , err
}
if readmeFile != nil {
return path . Join ( subTreeEntry . Name ( ) , subfolder ) , readmeFile , nil
}
}
}
return "" , readmeFile , nil
}
func renderDirectory ( ctx * context . Context ) {
entries := renderDirectoryFiles ( ctx , 1 * time . Second )
if ctx . Written ( ) {
return
}
if ctx . Repo . TreePath != "" {
ctx . Data [ "HideRepoInfo" ] = true
ctx . Data [ "Title" ] = ctx . Tr ( "repo.file.title" , ctx . Repo . Repository . Name + "/" + path . Base ( ctx . Repo . TreePath ) , ctx . Repo . RefName )
}
subfolder , readmeFile , err := findReadmeFileInEntries ( ctx , entries , true )
if err != nil {
ctx . ServerError ( "findReadmeFileInEntries" , err )
return
}
renderReadmeFile ( ctx , subfolder , readmeFile )
}
// localizedExtensions prepends the provided language code with and without a
// regional identifier to the provided extension.
// 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 , "-" )
// 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 }
}
// e.g. [.en.md, .md]
return [ ] string { lowerLangCode + ext , ext }
}
type fileInfo struct {
isTextFile bool
isLFSFile bool
@ -261,85 +110,6 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
return buf , dataRc , & fileInfo { st . IsText ( ) , true , meta . Size , & meta . Pointer , st } , nil
}
func renderReadmeFile ( ctx * context . Context , subfolder string , readmeFile * git . TreeEntry ) {
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
}
ctx . Data [ "RawFileLink" ] = ""
ctx . Data [ "ReadmeInList" ] = true
ctx . Data [ "ReadmeExist" ] = true
ctx . Data [ "FileIsSymlink" ] = readmeFile . IsLink ( )
buf , dataRc , fInfo , err := getFileReader ( ctx , ctx . Repo . Repository . ID , target . Blob ( ) )
if err != nil {
ctx . ServerError ( "getFileReader" , err )
return
}
defer dataRc . Close ( )
ctx . Data [ "FileIsText" ] = fInfo . isTextFile
ctx . Data [ "FileName" ] = path . Join ( subfolder , readmeFile . Name ( ) )
ctx . Data [ "FileSize" ] = fInfo . fileSize
ctx . Data [ "IsLFSFile" ] = fInfo . isLFSFile
if fInfo . isLFSFile {
filenameBase64 := base64 . RawURLEncoding . EncodeToString ( [ ] byte ( readmeFile . Name ( ) ) )
ctx . Data [ "RawFileLink" ] = fmt . Sprintf ( "%s.git/info/lfs/objects/%s/%s" , ctx . Repo . Repository . Link ( ) , url . PathEscape ( fInfo . lfsMeta . Oid ) , url . PathEscape ( filenameBase64 ) )
}
if ! fInfo . isTextFile {
return
}
if fInfo . fileSize >= setting . UI . MaxDisplayFileSize {
// 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
return
}
rd := charset . ToUTF8WithFallbackReader ( io . MultiReader ( bytes . NewReader ( buf ) , dataRc ) , charset . ConvertOpts { } )
if markupType := markup . DetectMarkupTypeByFileName ( readmeFile . Name ( ) ) ; markupType != "" {
ctx . Data [ "IsMarkup" ] = true
ctx . Data [ "MarkupType" ] = markupType
rctx := renderhelper . NewRenderContextRepoFile ( ctx , ctx . Repo . Repository , renderhelper . RepoFileOptions {
CurrentRefPath : ctx . Repo . BranchNameSubURL ( ) ,
CurrentTreePath : path . Join ( ctx . Repo . TreePath , subfolder ) ,
} ) .
WithMarkupType ( markupType ) .
WithRelativePath ( path . Join ( ctx . Repo . TreePath , subfolder , readmeFile . Name ( ) ) ) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
ctx . Data [ "EscapeStatus" ] , ctx . Data [ "FileContent" ] , err = markupRender ( ctx , rctx , rd )
if err != nil {
log . Error ( "Render failed for %s in %-v: %v Falling back to rendering source" , readmeFile . Name ( ) , ctx . Repo . Repository , err )
delete ( ctx . Data , "IsMarkup" )
}
}
if ctx . Data [ "IsMarkup" ] != true {
ctx . Data [ "IsPlainText" ] = true
content , err := io . ReadAll ( rd )
if err != nil {
log . Error ( "Read readme content failed: %v" , err )
}
contentEscaped := template . HTMLEscapeString ( util . UnsafeBytesToString ( content ) )
ctx . Data [ "EscapeStatus" ] , ctx . Data [ "FileContent" ] = charset . EscapeControlHTML ( template . HTML ( contentEscaped ) , ctx . Locale )
}
if ! fInfo . isLFSFile && ctx . Repo . CanEnableEditor ( ctx , ctx . Doer ) {
ctx . Data [ "CanEditReadmeFile" ] = true
}
}
func loadLatestCommitData ( ctx * context . Context , latestCommit * git . Commit ) bool {
// Show latest commit info of repository in table header,
// or of directory if not in root directory.
@ -371,287 +141,6 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
return true
}
func renderFile ( ctx * context . Context , entry * git . TreeEntry ) {
ctx . Data [ "IsViewFile" ] = true
ctx . Data [ "HideRepoInfo" ] = true
blob := entry . Blob ( )
buf , dataRc , fInfo , err := getFileReader ( ctx , ctx . Repo . Repository . ID , blob )
if err != nil {
ctx . ServerError ( "getFileReader" , err )
return
}
defer dataRc . Close ( )
ctx . Data [ "Title" ] = ctx . Tr ( "repo.file.title" , ctx . Repo . Repository . Name + "/" + path . Base ( ctx . Repo . TreePath ) , ctx . Repo . RefName )
ctx . Data [ "FileIsSymlink" ] = entry . IsLink ( )
ctx . Data [ "FileName" ] = blob . Name ( )
ctx . Data [ "RawFileLink" ] = ctx . Repo . RepoLink + "/raw/" + ctx . Repo . BranchNameSubURL ( ) + "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
commit , err := ctx . Repo . Commit . GetCommitByPath ( ctx . Repo . TreePath )
if err != nil {
ctx . ServerError ( "GetCommitByPath" , err )
return
}
if ! loadLatestCommitData ( ctx , commit ) {
return
}
if ctx . Repo . TreePath == ".editorconfig" {
_ , editorconfigWarning , editorconfigErr := ctx . Repo . GetEditorconfig ( ctx . Repo . Commit )
if editorconfigWarning != nil {
ctx . Data [ "FileWarning" ] = strings . TrimSpace ( editorconfigWarning . Error ( ) )
}
if editorconfigErr != nil {
ctx . Data [ "FileError" ] = strings . TrimSpace ( editorconfigErr . Error ( ) )
}
} else if issue_service . IsTemplateConfig ( ctx . Repo . TreePath ) {
_ , issueConfigErr := issue_service . GetTemplateConfig ( ctx . Repo . GitRepo , ctx . Repo . TreePath , ctx . Repo . Commit )
if issueConfigErr != nil {
ctx . Data [ "FileError" ] = strings . TrimSpace ( issueConfigErr . Error ( ) )
}
} else if actions . IsWorkflow ( ctx . Repo . TreePath ) {
content , err := actions . GetContentFromEntry ( entry )
if err != nil {
log . Error ( "actions.GetContentFromEntry: %v" , err )
}
_ , workFlowErr := model . ReadWorkflow ( bytes . NewReader ( content ) )
if workFlowErr != nil {
ctx . Data [ "FileError" ] = ctx . Locale . Tr ( "actions.runs.invalid_workflow_helper" , workFlowErr . Error ( ) )
}
} else if slices . Contains ( [ ] string { "CODEOWNERS" , "docs/CODEOWNERS" , ".gitea/CODEOWNERS" } , ctx . Repo . TreePath ) {
if data , err := blob . GetBlobContent ( setting . UI . MaxDisplayFileSize ) ; err == nil {
_ , warnings := issue_model . GetCodeOwnersFromContent ( ctx , data )
if len ( warnings ) > 0 {
ctx . Data [ "FileWarning" ] = strings . Join ( warnings , "\n" )
}
}
}
isDisplayingSource := ctx . FormString ( "display" ) == "source"
isDisplayingRendered := ! isDisplayingSource
if fInfo . isLFSFile {
ctx . Data [ "RawFileLink" ] = ctx . Repo . RepoLink + "/media/" + ctx . Repo . BranchNameSubURL ( ) + "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
}
isRepresentableAsText := fInfo . st . IsRepresentableAsText ( )
if ! isRepresentableAsText {
// If we can't show plain text, always try to render.
isDisplayingSource = false
isDisplayingRendered = true
}
ctx . Data [ "IsLFSFile" ] = fInfo . isLFSFile
ctx . Data [ "FileSize" ] = fInfo . fileSize
ctx . Data [ "IsTextFile" ] = fInfo . isTextFile
ctx . Data [ "IsRepresentableAsText" ] = isRepresentableAsText
ctx . Data [ "IsDisplayingSource" ] = isDisplayingSource
ctx . Data [ "IsDisplayingRendered" ] = isDisplayingRendered
ctx . Data [ "IsExecutable" ] = entry . IsExecutable ( )
isTextSource := fInfo . isTextFile || isDisplayingSource
ctx . Data [ "IsTextSource" ] = isTextSource
if isTextSource {
ctx . Data [ "CanCopyContent" ] = true
}
// Check LFS Lock
lfsLock , err := git_model . GetTreePathLock ( ctx , ctx . Repo . Repository . ID , ctx . Repo . TreePath )
ctx . Data [ "LFSLock" ] = lfsLock
if err != nil {
ctx . ServerError ( "GetTreePathLock" , err )
return
}
if lfsLock != nil {
u , err := user_model . GetUserByID ( ctx , lfsLock . OwnerID )
if err != nil {
ctx . ServerError ( "GetTreePathLock" , err )
return
}
ctx . Data [ "LFSLockOwner" ] = u . Name
ctx . Data [ "LFSLockOwnerHomeLink" ] = u . HomeLink ( )
ctx . Data [ "LFSLockHint" ] = ctx . Tr ( "repo.editor.this_file_locked" )
}
// Assume file is not editable first.
if fInfo . isLFSFile {
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.cannot_edit_lfs_files" )
} else if ! isRepresentableAsText {
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.cannot_edit_non_text_files" )
}
switch {
case isRepresentableAsText :
if fInfo . fileSize >= setting . UI . MaxDisplayFileSize {
ctx . Data [ "IsFileTooLarge" ] = true
break
}
if fInfo . st . IsSvgImage ( ) {
ctx . Data [ "IsImageFile" ] = true
ctx . Data [ "CanCopyContent" ] = true
ctx . Data [ "HasSourceRenderedToggle" ] = true
}
rd := charset . ToUTF8WithFallbackReader ( io . MultiReader ( bytes . NewReader ( buf ) , dataRc ) , charset . ConvertOpts { } )
shouldRenderSource := ctx . FormString ( "display" ) == "source"
readmeExist := util . IsReadmeFileName ( blob . Name ( ) )
ctx . Data [ "ReadmeExist" ] = readmeExist
markupType := markup . DetectMarkupTypeByFileName ( blob . Name ( ) )
if markupType == "" {
markupType = markup . DetectRendererType ( blob . Name ( ) , bytes . NewReader ( buf ) )
}
if markupType != "" {
ctx . Data [ "HasSourceRenderedToggle" ] = true
}
if markupType != "" && ! shouldRenderSource {
ctx . Data [ "IsMarkup" ] = true
ctx . Data [ "MarkupType" ] = markupType
metas := ctx . Repo . Repository . ComposeDocumentMetas ( ctx )
metas [ "BranchNameSubURL" ] = ctx . Repo . BranchNameSubURL ( )
rctx := renderhelper . NewRenderContextRepoFile ( ctx , ctx . Repo . Repository , renderhelper . RepoFileOptions {
CurrentRefPath : ctx . Repo . BranchNameSubURL ( ) ,
CurrentTreePath : path . Dir ( ctx . Repo . TreePath ) ,
} ) .
WithMarkupType ( markupType ) .
WithRelativePath ( ctx . Repo . TreePath ) .
WithMetas ( metas )
ctx . Data [ "EscapeStatus" ] , ctx . Data [ "FileContent" ] , err = markupRender ( ctx , rctx , rd )
if err != nil {
ctx . ServerError ( "Render" , err )
return
}
// to prevent iframe load third-party url
ctx . Resp . Header ( ) . Add ( "Content-Security-Policy" , "frame-src 'self'" )
} else {
buf , _ := io . ReadAll ( rd )
// The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
// empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
// Gitea uses the definition (like most modern editors):
// empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
// When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
// To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
// This 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
}
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 )
}
fileContent , lexerName , err := highlight . File ( blob . Name ( ) , language , buf )
ctx . Data [ "LexerName" ] = lexerName
if err != nil {
log . Error ( "highlight.File failed, fallback to plain text: %v" , err )
fileContent = highlight . PlainText ( buf )
}
status := & charset . EscapeStatus { }
statuses := make ( [ ] * charset . EscapeStatus , len ( fileContent ) )
for i , line := range fileContent {
statuses [ i ] , fileContent [ i ] = charset . EscapeControlHTML ( line , ctx . Locale )
status = status . Or ( statuses [ i ] )
}
ctx . Data [ "EscapeStatus" ] = status
ctx . Data [ "FileContent" ] = fileContent
ctx . Data [ "LineEscapeStatus" ] = statuses
}
if ! fInfo . isLFSFile {
if ctx . Repo . CanEnableEditor ( ctx , ctx . Doer ) {
if lfsLock != nil && lfsLock . OwnerID != ctx . Doer . ID {
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" )
}
} else if ! ctx . Repo . IsViewBranch {
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.must_be_on_a_branch" )
} else if ! ctx . Repo . CanWriteToBranch ( ctx , ctx . Doer , ctx . Repo . BranchName ) {
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.fork_before_edit" )
}
}
case fInfo . st . IsPDF ( ) :
ctx . Data [ "IsPDFFile" ] = true
case fInfo . st . IsVideo ( ) :
ctx . Data [ "IsVideoFile" ] = true
case fInfo . st . IsAudio ( ) :
ctx . Data [ "IsAudioFile" ] = true
case fInfo . st . IsImage ( ) && ( setting . UI . SVG . Enabled || ! fInfo . st . IsSvgImage ( ) ) :
ctx . Data [ "IsImageFile" ] = true
ctx . Data [ "CanCopyContent" ] = true
default :
if fInfo . fileSize >= setting . UI . MaxDisplayFileSize {
ctx . Data [ "IsFileTooLarge" ] = true
break
}
// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
// It is used by "external renders", markupRender will execute external programs to get rendered content.
if markupType := markup . DetectMarkupTypeByFileName ( blob . Name ( ) ) ; markupType != "" {
rd := io . MultiReader ( bytes . NewReader ( buf ) , dataRc )
ctx . Data [ "IsMarkup" ] = true
ctx . Data [ "MarkupType" ] = markupType
rctx := renderhelper . NewRenderContextRepoFile ( ctx , ctx . Repo . Repository , renderhelper . RepoFileOptions {
CurrentRefPath : ctx . Repo . BranchNameSubURL ( ) ,
CurrentTreePath : path . Dir ( ctx . Repo . TreePath ) ,
} ) .
WithMarkupType ( markupType ) .
WithRelativePath ( ctx . Repo . TreePath )
ctx . Data [ "EscapeStatus" ] , ctx . Data [ "FileContent" ] , err = markupRender ( ctx , rctx , rd )
if err != nil {
ctx . ServerError ( "Render" , err )
return
}
}
}
if ctx . Repo . GitRepo != nil {
checker , deferable := ctx . Repo . GitRepo . CheckAttributeReader ( ctx . Repo . CommitID )
if checker != nil {
defer deferable ( )
attrs , err := checker . CheckPath ( ctx . Repo . TreePath )
if err == nil {
ctx . Data [ "IsVendored" ] = git . AttributeToBool ( attrs , git . AttributeLinguistVendored ) . Value ( )
ctx . Data [ "IsGenerated" ] = git . AttributeToBool ( attrs , git . AttributeLinguistGenerated ) . Value ( )
}
}
}
if fInfo . st . IsImage ( ) && ! fInfo . st . IsSvgImage ( ) {
img , _ , err := image . DecodeConfig ( bytes . NewReader ( buf ) )
if err == nil {
// There are Image formats go can't decode
// Instead of throwing an error in that case, we show the size only when we can decode
ctx . Data [ "ImageSize" ] = fmt . Sprintf ( "%dx%dpx" , img . Width , img . Height )
}
}
if ctx . Repo . CanEnableEditor ( ctx , ctx . Doer ) {
if lfsLock != nil && lfsLock . OwnerID != ctx . Doer . ID {
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" )
}
} else if ! ctx . Repo . IsViewBranch {
ctx . Data [ "DeleteFileTooltip" ] = ctx . Tr ( "repo.editor.must_be_on_a_branch" )
} else if ! ctx . Repo . CanWriteToBranch ( ctx , ctx . Doer , ctx . Repo . BranchName ) {
ctx . Data [ "DeleteFileTooltip" ] = ctx . Tr ( "repo.editor.must_have_write_access" )
}
}
func markupRender ( ctx * context . Context , renderCtx * markup . RenderContext , input io . Reader ) ( escaped * charset . EscapeStatus , output template . HTML , err error ) {
markupRd , markupWr := io . Pipe ( )
defer markupWr . Close ( )
@ -728,59 +217,6 @@ func checkHomeCodeViewable(ctx *context.Context) {
ctx . NotFound ( "Home" , errors . New ( ctx . Locale . TrString ( "units.error.no_unit_allowed_repo" ) ) )
}
func checkCitationFile ( ctx * context . Context , entry * git . TreeEntry ) {
if entry . Name ( ) != "" {
return
}
tree , err := ctx . Repo . Commit . SubTree ( ctx . Repo . TreePath )
if err != nil {
HandleGitError ( ctx , "Repo.Commit.SubTree" , 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" {
// Read Citation file contents
if content , err := entry . Blob ( ) . GetBlobContent ( setting . UI . MaxDisplayFileSize ) ; err != nil {
log . Error ( "checkCitationFile: GetBlobContent: %v" , err )
} else {
ctx . Data [ "CitiationExist" ] = true
ctx . PageData [ "citationFileContent" ] = content
break
}
}
}
}
// Home render repository home page
func Home ( ctx * context . Context ) {
if setting . Other . EnableFeed {
isFeed , _ , showFeedType := feed . GetFeedType ( ctx . PathParam ( ":reponame" ) , ctx . Req )
if isFeed {
switch {
case ctx . Link == fmt . Sprintf ( "%s.%s" , ctx . Repo . RepoLink , showFeedType ) :
feed . ShowRepoFeed ( ctx , ctx . Repo . Repository , showFeedType )
case ctx . Repo . TreePath == "" :
feed . ShowBranchFeed ( ctx , ctx . Repo . Repository , showFeedType )
case ctx . Repo . TreePath != "" :
feed . ShowFileFeed ( ctx , ctx . Repo . Repository , showFeedType )
}
return
}
}
checkHomeCodeViewable ( ctx )
if ctx . Written ( ) {
return
}
renderHomeCode ( 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 )
@ -877,220 +313,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
return allEntries
}
func renderLanguageStats ( ctx * context . Context ) {
langs , err := repo_model . GetTopLanguageStats ( ctx , ctx . Repo . Repository , 5 )
if err != nil {
ctx . ServerError ( "Repo.GetTopLanguageStats" , err )
return
}
ctx . Data [ "LanguageStats" ] = langs
}
func renderRepoTopics ( ctx * context . Context ) {
topics , err := db . Find [ repo_model . Topic ] ( ctx , & repo_model . FindTopicOptions {
RepoID : ctx . Repo . Repository . ID ,
} )
if err != nil {
ctx . ServerError ( "models.FindTopics" , err )
return
}
ctx . Data [ "Topics" ] = topics
}
func prepareOpenWithEditorApps ( ctx * context . Context ) {
var tmplApps [ ] map [ string ] any
apps := setting . Config ( ) . Repository . OpenWithEditorApps . Value ( ctx )
if len ( apps ) == 0 {
apps = setting . DefaultOpenWithEditorApps ( )
}
for _ , app := range apps {
schema , _ , _ := strings . Cut ( app . OpenURL , ":" )
var iconHTML template . HTML
if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
iconHTML = svg . RenderHTML ( fmt . Sprintf ( "gitea-%s" , schema ) , 16 , "tw-mr-2" )
} else {
iconHTML = svg . RenderHTML ( "gitea-git" , 16 , "tw-mr-2" ) // TODO: it could support user's customized icon in the future
}
tmplApps = append ( tmplApps , map [ string ] any {
"DisplayName" : app . DisplayName ,
"OpenURL" : app . OpenURL ,
"IconHTML" : iconHTML ,
} )
}
ctx . Data [ "OpenWithEditorApps" ] = tmplApps
}
func renderHomeCode ( ctx * context . Context ) {
ctx . Data [ "PageIsViewCode" ] = true
ctx . Data [ "RepositoryUploadEnabled" ] = setting . Repository . Upload . Enabled
prepareOpenWithEditorApps ( ctx )
if ctx . Repo . Commit == nil || ctx . Repo . Repository . IsEmpty || ctx . Repo . Repository . IsBroken ( ) {
showEmpty := true
var err error
if ctx . Repo . GitRepo != nil {
showEmpty , err = ctx . Repo . GitRepo . IsEmpty ( )
if err != nil {
log . Error ( "GitRepo.IsEmpty: %v" , err )
ctx . Repo . Repository . Status = repo_model . RepositoryBroken
showEmpty = true
ctx . Flash . Error ( ctx . Tr ( "error.occurred" ) , true )
}
}
if showEmpty {
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
if err = repo_model . UpdateRepositoryCols ( ctx , ctx . Repo . Repository , "is_empty" ) ; err != nil {
ctx . ServerError ( "UpdateRepositoryCols" , err )
return
}
if err = repo_module . UpdateRepoSize ( ctx , ctx . Repo . Repository ) ; err != nil {
ctx . ServerError ( "UpdateRepoSize" , err )
return
}
// the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values
link := ctx . Link
if ctx . Req . URL . RawQuery != "" {
link += "?" + ctx . Req . URL . RawQuery
}
ctx . Redirect ( link )
return
}
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
// Get Topics of this repo
renderRepoTopics ( ctx )
if ctx . Written ( ) {
return
}
// 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
}
checkOutdatedBranch ( ctx )
checkCitationFile ( ctx , entry )
if ctx . Written ( ) {
return
}
renderLanguageStats ( ctx )
if ctx . Written ( ) {
return
}
if entry . IsDir ( ) {
renderDirectory ( ctx )
} else {
renderFile ( ctx , entry )
}
if ctx . Written ( ) {
return
}
if ctx . Doer != nil {
if err := ctx . Repo . Repository . GetBaseRepo ( ctx ) ; err != nil {
ctx . ServerError ( "GetBaseRepo" , err )
return
}
opts := & git_model . FindRecentlyPushedNewBranchesOptions {
Repo : ctx . Repo . Repository ,
BaseRepo : ctx . Repo . Repository ,
}
if ctx . Repo . Repository . IsFork {
opts . BaseRepo = ctx . Repo . Repository . BaseRepo
}
baseRepoPerm , err := access_model . GetUserRepoPermission ( ctx , opts . BaseRepo , ctx . Doer )
if err != nil {
ctx . ServerError ( "GetUserRepoPermission" , err )
return
}
if ! opts . Repo . IsMirror && ! opts . BaseRepo . IsMirror &&
opts . BaseRepo . UnitEnabled ( ctx , unit_model . TypePullRequests ) &&
baseRepoPerm . CanRead ( unit_model . TypePullRequests ) {
ctx . Data [ "RecentlyPushedNewBranches" ] , err = git_model . FindRecentlyPushedNewBranches ( ctx , ctx . Doer , opts )
if err != nil {
log . Error ( "FindRecentlyPushedNewBranches failed: %v" , err )
}
}
}
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 ]
}
}
ctx . Data [ "Paths" ] = paths
branchLink := ctx . Repo . RepoLink + "/src/" + ctx . Repo . BranchNameSubURL ( )
treeLink := branchLink
if len ( ctx . Repo . TreePath ) > 0 {
treeLink += "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
}
ctx . Data [ "TreeLink" ] = treeLink
ctx . Data [ "TreeNames" ] = treeNames
ctx . Data [ "BranchLink" ] = branchLink
ctx . Data [ "LicenseFileName" ] = repo_service . LicenseFileName
ctx . HTML ( http . StatusOK , tplRepoHome )
}
func checkOutdatedBranch ( ctx * context . Context ) {
if ! ( ctx . Repo . IsAdmin ( ) || ctx . Repo . IsOwner ( ) ) {
return
}
// get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName`
commit , err := ctx . Repo . GitRepo . GetBranchCommit ( ctx . Repo . BranchName )
if err != nil {
log . Error ( "GetBranchCommitID: %v" , err )
// Don't return an error page, as it can be rechecked the next time the user opens the page.
return
}
dbBranch , err := git_model . GetBranch ( ctx , ctx . Repo . Repository . ID , ctx . Repo . BranchName )
if err != nil {
log . Error ( "GetBranch: %v" , err )
// Don't return an error page, as it can be rechecked the next time the user opens the page.
return
}
if dbBranch . CommitID != commit . ID . String ( ) {
ctx . Flash . Warning ( ctx . Tr ( "repo.error.broken_git_hook" , "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running" ) , true )
}
}
// RenderUserCards render a page show users according the input template
func RenderUserCards ( ctx * context . Context , total int , getter func ( opts db . ListOptions ) ( [ ] * user_model . User , error ) , tpl base . TplName ) {
page := ctx . FormInt ( "page" )