2024-12-06 23:29:04 +09:00
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"bytes"
"fmt"
"image"
"io"
"path"
"strings"
git_model "code.gitea.io/gitea/models/git"
issue_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
2025-04-11 06:41:29 -07:00
"code.gitea.io/gitea/modules/git/attribute"
2024-12-06 23:29:04 +09:00
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
2026-04-18 16:02:18 +08:00
"code.gitea.io/gitea/modules/markup"
2024-12-06 23:29:04 +09:00
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
)
2025-04-04 16:19:19 -07:00
func prepareLatestCommitInfo ( ctx * context . Context ) bool {
commit , err := ctx . Repo . Commit . GetCommitByPath ( ctx . Repo . TreePath )
if err != nil {
ctx . ServerError ( "GetCommitByPath" , err )
return false
}
return loadLatestCommitData ( ctx , commit )
}
2025-06-30 16:12:25 +08:00
func prepareFileViewLfsAttrs ( ctx * context . Context ) ( * attribute . Attributes , bool ) {
attrsMap , err := attribute . CheckAttributes ( ctx , ctx . Repo . GitRepo , ctx . Repo . CommitID , attribute . CheckAttributeOpts {
Filenames : [ ] string { ctx . Repo . TreePath } ,
Attributes : [ ] string { attribute . LinguistGenerated , attribute . LinguistVendored , attribute . LinguistLanguage , attribute . GitlabLanguage } ,
} )
if err != nil {
ctx . ServerError ( "attribute.CheckAttributes" , err )
return nil , false
}
attrs := attrsMap [ ctx . Repo . TreePath ]
if attrs == nil {
// this case shouldn't happen, just in case.
setting . PanicInDevOrTesting ( "no attributes found for %s" , ctx . Repo . TreePath )
attrs = attribute . NewAttributes ( )
}
ctx . Data [ "IsVendored" ] , ctx . Data [ "IsGenerated" ] = attrs . GetVendored ( ) . Value ( ) , attrs . GetGenerated ( ) . Value ( )
return attrs , true
}
2026-01-26 10:34:38 +08:00
func handleFileViewRenderMarkup ( ctx * context . Context , prefetchBuf [ ] byte , utf8Reader io . Reader ) bool {
rctx := renderhelper . NewRenderContextRepoFile ( ctx , ctx . Repo . Repository , renderhelper . RepoFileOptions {
CurrentRefPath : ctx . Repo . RefTypeNameSubURL ( ) ,
CurrentTreePath : path . Dir ( ctx . Repo . TreePath ) ,
} ) . WithRelativePath ( ctx . Repo . TreePath )
renderer := rctx . DetectMarkupRenderer ( prefetchBuf )
if renderer == nil {
return false // not supported markup
2025-06-30 16:12:25 +08:00
}
2026-01-26 10:34:38 +08:00
metas := ctx . Repo . Repository . ComposeRepoFileMetas ( ctx )
metas [ "RefTypeNameSubURL" ] = ctx . Repo . RefTypeNameSubURL ( )
rctx . WithMetas ( metas )
2025-06-30 16:12:25 +08:00
ctx . Data [ "HasSourceRenderedToggle" ] = true
if ctx . FormString ( "display" ) == "source" {
return false
}
var err error
2026-01-26 10:34:38 +08:00
ctx . Data [ "EscapeStatus" ] , ctx . Data [ "FileContent" ] , err = markupRenderToHTML ( ctx , rctx , renderer , utf8Reader )
2025-06-30 16:12:25 +08:00
if err != nil {
ctx . ServerError ( "Render" , err )
return true
}
2026-04-18 16:02:18 +08:00
opts , ok := markup . GetExternalRendererOptions ( renderer )
usingIframe := ok && opts . DisplayInIframe
ctx . Data [ "MarkupType" ] = rctx . RenderOptions . MarkupType
ctx . Data [ "RenderAsMarkup" ] = util . Iif ( usingIframe , "markup-iframe" , "markup-inplace" )
2025-06-30 16:12:25 +08:00
return true
}
2026-01-26 10:34:38 +08:00
func handleFileViewRenderSource ( ctx * context . Context , attrs * attribute . Attributes , fInfo * fileInfo , utf8Reader io . Reader ) bool {
filename := ctx . Repo . TreePath
2025-06-30 16:12:25 +08:00
if ctx . FormString ( "display" ) == "rendered" || ! fInfo . st . IsRepresentableAsText ( ) {
return false
}
if ! fInfo . st . IsText ( ) {
if ctx . FormString ( "display" ) == "" {
// not text but representable as text, e.g. SVG
// since there is no "display" is specified, let other renders to handle
return false
}
ctx . Data [ "HasSourceRenderedToggle" ] = true
}
buf , _ := io . ReadAll ( utf8Reader )
// 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 := attrs . GetLanguage ( ) . Value ( )
2026-04-03 12:10:01 +08:00
fileContent , lexerName := highlight . RenderFullFile ( filename , language , buf )
2025-06-30 16:12:25 +08:00
ctx . Data [ "LexerName" ] = lexerName
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
return true
}
func handleFileViewRenderImage ( ctx * context . Context , fInfo * fileInfo , prefetchBuf [ ] byte ) bool {
if ! fInfo . st . IsImage ( ) {
return false
}
if fInfo . st . IsSvgImage ( ) && ! setting . UI . SVG . Enabled {
return false
}
if fInfo . st . IsSvgImage ( ) {
ctx . Data [ "HasSourceRenderedToggle" ] = true
} else {
img , _ , err := image . DecodeConfig ( bytes . NewReader ( prefetchBuf ) )
if err == nil { // ignore the error for the formats that are not supported by image.DecodeConfig
ctx . Data [ "ImageSize" ] = fmt . Sprintf ( "%dx%dpx" , img . Width , img . Height )
}
}
return true
}
func prepareFileView ( ctx * context . Context , entry * git . TreeEntry ) {
2024-12-06 23:29:04 +09:00
ctx . Data [ "IsViewFile" ] = true
ctx . Data [ "HideRepoInfo" ] = true
2025-04-04 16:19:19 -07:00
if ! prepareLatestCommitInfo ( ctx ) {
2024-12-06 23:29:04 +09:00
return
}
2025-04-04 16:19:19 -07:00
blob := entry . Blob ( )
2024-12-06 23:29:04 +09:00
2025-10-29 14:38:09 +01:00
ctx . Data [ "Title" ] = ctx . Tr ( "repo.file.title" , ctx . Repo . Repository . Name + "/" + ctx . Repo . TreePath , ctx . Repo . RefFullName . ShortName ( ) )
2024-12-06 23:29:04 +09:00
ctx . Data [ "FileIsSymlink" ] = entry . IsLink ( )
2025-04-14 03:27:31 +08:00
ctx . Data [ "FileTreePath" ] = ctx . Repo . TreePath
2025-01-12 11:39:46 +08:00
ctx . Data [ "RawFileLink" ] = ctx . Repo . RepoLink + "/raw/" + ctx . Repo . RefTypeNameSubURL ( ) + "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
2024-12-06 23:29:04 +09:00
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 )
}
2026-04-10 04:57:04 +08:00
if workFlowErr := actions . ValidateWorkflowContent ( content ) ; workFlowErr != nil {
2024-12-06 23:29:04 +09:00
ctx . Data [ "FileError" ] = ctx . Locale . Tr ( "actions.runs.invalid_workflow_helper" , workFlowErr . Error ( ) )
}
2025-03-13 19:36:14 -07:00
} else if issue_service . IsCodeOwnerFile ( ctx . Repo . TreePath ) {
2024-12-06 23:29:04 +09:00
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" )
}
}
}
2025-04-04 16:19:19 -07:00
// Don't call any other repository functions depends on git.Repository until the dataRc closed to
2025-06-30 16:12:25 +08:00
// avoid creating an unnecessary temporary cat file.
2025-04-04 16:19:19 -07:00
buf , dataRc , fInfo , err := getFileReader ( ctx , ctx . Repo . Repository . ID , blob )
if err != nil {
ctx . ServerError ( "getFileReader" , err )
return
}
defer dataRc . Close ( )
2025-06-30 16:12:25 +08:00
if fInfo . isLFSFile ( ) {
2025-01-12 11:39:46 +08:00
ctx . Data [ "RawFileLink" ] = ctx . Repo . RepoLink + "/media/" + ctx . Repo . RefTypeNameSubURL ( ) + "/" + util . PathEscapeSegments ( ctx . Repo . TreePath )
2024-12-06 23:29:04 +09:00
}
2025-06-30 16:12:25 +08:00
if ! prepareFileViewEditorButtons ( ctx ) {
return
2024-12-06 23:29:04 +09:00
}
2025-06-30 16:12:25 +08:00
ctx . Data [ "IsLFSFile" ] = fInfo . isLFSFile ( )
2025-10-16 15:12:54 +05:30
ctx . Data [ "FileSize" ] = fInfo . blobOrLfsSize
2025-06-30 16:12:25 +08:00
ctx . Data [ "IsRepresentableAsText" ] = fInfo . st . IsRepresentableAsText ( )
2024-12-06 23:29:04 +09:00
ctx . Data [ "IsExecutable" ] = entry . IsExecutable ( )
2025-06-30 16:12:25 +08:00
ctx . Data [ "CanCopyContent" ] = fInfo . st . IsRepresentableAsText ( ) || fInfo . st . IsImage ( )
2024-12-06 23:29:04 +09:00
2025-06-30 16:12:25 +08:00
attrs , ok := prepareFileViewLfsAttrs ( ctx )
if ! ok {
2024-12-06 23:29:04 +09:00
return
}
2025-06-30 16:12:25 +08:00
// TODO: in the future maybe we need more accurate flags, for example:
// * IsRepresentableAsText: some files are text, some are not
// * IsRenderableXxx: some files are rendered by backend "markup" engine, some are rendered by frontend (pdf, 3d)
// * DefaultViewMode: when there is no "display" query parameter, which view mode should be used by default, source or rendered
2025-04-11 06:41:29 -07:00
2025-11-14 08:31:11 +08:00
contentReader := io . MultiReader ( bytes . NewReader ( buf ) , dataRc )
if fInfo . st . IsRepresentableAsText ( ) {
contentReader = charset . ToUTF8WithFallbackReader ( contentReader , charset . ConvertOpts { } )
}
2024-12-06 23:29:04 +09:00
switch {
2025-10-16 15:12:54 +05:30
case fInfo . blobOrLfsSize >= setting . UI . MaxDisplayFileSize :
2025-06-30 16:12:25 +08:00
ctx . Data [ "IsFileTooLarge" ] = true
2026-01-26 10:34:38 +08:00
case handleFileViewRenderMarkup ( ctx , buf , contentReader ) :
case handleFileViewRenderSource ( ctx , attrs , fInfo , contentReader ) :
2025-06-30 16:12:25 +08:00
// it also sets ctx.Data["FileContent"] and more
ctx . Data [ "IsDisplayingSource" ] = true
case handleFileViewRenderImage ( ctx , fInfo , buf ) :
ctx . Data [ "IsImageFile" ] = true
2024-12-06 23:29:04 +09:00
case fInfo . st . IsVideo ( ) :
ctx . Data [ "IsVideoFile" ] = true
case fInfo . st . IsAudio ( ) :
ctx . Data [ "IsAudioFile" ] = true
default :
2025-06-30 16:12:25 +08:00
// unable to render anything, show the "view raw" or let frontend handle it
2024-12-06 23:29:04 +09:00
}
2025-06-13 02:12:45 +08:00
}
2025-06-30 16:12:25 +08:00
func prepareFileViewEditorButtons ( ctx * context . Context ) bool {
2025-06-13 02:12:45 +08:00
// archived or mirror repository, the buttons should not be shown
2025-06-22 14:43:43 +02:00
if ! ctx . Repo . Repository . CanEnableEditor ( ) {
2025-06-30 16:12:25 +08:00
return true
2025-06-13 02:12:45 +08:00
}
// The buttons should not be shown if it's not a branch
if ! ctx . Repo . RefFullName . IsBranch ( ) {
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.must_be_on_a_branch" )
2024-12-06 23:29:04 +09:00
ctx . Data [ "DeleteFileTooltip" ] = ctx . Tr ( "repo.editor.must_be_on_a_branch" )
2025-06-30 16:12:25 +08:00
return true
2025-06-13 02:12:45 +08:00
}
if ! ctx . Repo . CanWriteToBranch ( ctx , ctx . Doer , ctx . Repo . BranchName ) {
2025-06-22 14:43:43 +02:00
ctx . Data [ "CanEditFile" ] = true
2025-06-17 02:15:07 +02:00
ctx . Data [ "EditFileTooltip" ] = ctx . Tr ( "repo.editor.fork_before_edit" )
2025-06-22 14:43:43 +02:00
ctx . Data [ "CanDeleteFile" ] = true
2024-12-06 23:29:04 +09:00
ctx . Data [ "DeleteFileTooltip" ] = ctx . Tr ( "repo.editor.must_have_write_access" )
2025-06-30 16:12:25 +08:00
return true
}
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 false
}
if lfsLock != nil {
u , err := user_model . GetUserByID ( ctx , lfsLock . OwnerID )
if err != nil {
ctx . ServerError ( "GetTreePathLock" , err )
return false
}
ctx . Data [ "LFSLockOwner" ] = u . Name
ctx . Data [ "LFSLockOwnerHomeLink" ] = u . HomeLink ( )
ctx . Data [ "LFSLockHint" ] = ctx . Tr ( "repo.editor.this_file_locked" )
2025-06-13 02:12:45 +08:00
}
// it's a lfs file and the user is not the owner of the lock
2025-06-17 02:15:07 +02:00
isLFSLocked := lfsLock != nil && lfsLock . OwnerID != ctx . Doer . ID
ctx . Data [ "CanEditFile" ] = ! isLFSLocked
ctx . Data [ "EditFileTooltip" ] = util . Iif ( isLFSLocked , ctx . Tr ( "repo.editor.this_file_locked" ) , ctx . Tr ( "repo.editor.edit_this_file" ) )
ctx . Data [ "CanDeleteFile" ] = ! isLFSLocked
ctx . Data [ "DeleteFileTooltip" ] = util . Iif ( isLFSLocked , ctx . Tr ( "repo.editor.this_file_locked" ) , ctx . Tr ( "repo.editor.delete_this_file" ) )
2025-06-30 16:12:25 +08:00
return true
2024-12-06 23:29:04 +09:00
}