2014-11-16 20:27:04 -05:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2018-11-28 19:26:14 +08:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2014-11-16 20:27:04 -05:00
2015-12-04 17:16:42 -05:00
package repo
2014-11-16 21:32:26 -05:00
import (
2022-06-04 15:17:53 +02:00
"bytes"
2019-04-17 10:06:35 -06:00
"encoding/base64"
2022-08-29 11:45:20 +02:00
"errors"
2020-05-31 22:59:34 +02:00
"fmt"
2022-06-04 15:17:53 +02:00
"io"
2019-04-17 10:06:35 -06:00
"net/http"
2023-05-29 11:41:35 +02:00
"strings"
2019-12-24 03:33:52 +01:00
"time"
2019-04-17 10:06:35 -06:00
2022-06-12 23:51:54 +08:00
git_model "code.gitea.io/gitea/models/git"
2019-03-27 17:33:00 +08:00
"code.gitea.io/gitea/modules/git"
2022-06-04 15:17:53 +02:00
"code.gitea.io/gitea/modules/httpcache"
2026-03-25 17:37:48 +01:00
"code.gitea.io/gitea/modules/httplib"
2025-04-21 19:20:11 +02:00
"code.gitea.io/gitea/modules/json"
2022-06-04 15:17:53 +02:00
"code.gitea.io/gitea/modules/lfs"
2022-05-09 16:54:51 +01:00
"code.gitea.io/gitea/modules/setting"
2022-06-04 15:17:53 +02:00
"code.gitea.io/gitea/modules/storage"
2019-05-11 18:21:34 +08:00
api "code.gitea.io/gitea/modules/structs"
2025-04-21 19:20:11 +02:00
"code.gitea.io/gitea/modules/util"
2021-01-26 23:36:53 +08:00
"code.gitea.io/gitea/modules/web"
2025-04-21 19:20:11 +02:00
"code.gitea.io/gitea/routers/api/v1/utils"
2021-06-09 07:33:54 +08:00
"code.gitea.io/gitea/routers/common"
2024-02-27 15:12:22 +08:00
"code.gitea.io/gitea/services/context"
2024-12-20 10:05:29 -08:00
pull_service "code.gitea.io/gitea/services/pull"
2021-11-24 15:56:24 +08:00
files_service "code.gitea.io/gitea/services/repository/files"
2014-11-16 21:32:26 -05:00
)
2022-07-21 21:18:41 +02:00
const giteaObjectTypeHeader = "X-Gitea-Object-Type"
2016-11-24 15:04:31 +08:00
// GetRawFile get a file by path on a repository
2016-03-13 18:49:16 -04:00
func GetRawFile ( ctx * context . APIContext ) {
2017-11-12 23:02:25 -08:00
// swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
// ---
// summary: Get a file from a repository
// produces:
2024-07-25 14:06:19 +02:00
// - application/octet-stream
2017-11-12 23:02:25 -08:00
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
2024-11-05 14:35:54 +08:00
// description: path of the file to get, it should be "{ref}/{filepath}". If there is no ref could be inferred, it will be treated as the default branch
2017-11-12 23:02:25 -08:00
// type: string
// required: true
2021-02-09 00:15:47 +00:00
// - name: ref
// in: query
2025-06-26 02:25:20 +08:00
// description: "The name of the commit/branch/tag. Default to the repository’ s default branch"
2021-02-09 00:15:47 +00:00
// type: string
// required: false
2017-11-12 23:02:25 -08:00
// responses:
2018-05-31 22:51:49 -07:00
// 200:
2022-04-28 14:57:56 +00:00
// description: Returns raw file content.
2024-07-25 14:06:19 +02:00
// schema:
// type: file
2019-12-20 18:07:12 +01:00
// "404":
// "$ref": "#/responses/notFound"
2019-01-18 00:01:04 +00:00
if ctx . Repo . Repository . IsEmpty {
2025-02-17 14:13:17 +08:00
ctx . APIErrorNotFound ( )
2017-06-11 04:57:28 +02:00
return
}
2022-07-21 21:18:41 +02:00
blob , entry , lastModified := getBlobForEntry ( ctx )
2022-05-09 16:54:51 +01:00
if ctx . Written ( ) {
return
}
2022-07-21 21:18:41 +02:00
ctx . RespHeader ( ) . Set ( giteaObjectTypeHeader , string ( files_service . GetObjectTypeFromTreeEntry ( entry ) ) )
2025-03-13 07:04:50 +08:00
if err := common . ServeBlob ( ctx . Base , ctx . Repo . Repository , ctx . Repo . TreePath , blob , lastModified ) ; err != nil {
2025-02-18 04:41:03 +08:00
ctx . APIErrorInternal ( err )
2022-05-09 16:54:51 +01:00
}
}
2022-06-04 15:17:53 +02:00
// GetRawFileOrLFS get a file by repo's path, redirecting to LFS if necessary.
func GetRawFileOrLFS ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/media/{filepath} repository repoGetRawFileOrLFS
// ---
// summary: Get a file or it's LFS object from a repository
2024-07-25 14:06:19 +02:00
// produces:
// - application/octet-stream
2022-06-04 15:17:53 +02:00
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
2024-11-05 14:35:54 +08:00
// description: path of the file to get, it should be "{ref}/{filepath}". If there is no ref could be inferred, it will be treated as the default branch
2022-06-04 15:17:53 +02:00
// type: string
// required: true
// - name: ref
// in: query
2025-06-26 02:25:20 +08:00
// description: "The name of the commit/branch/tag. Default to the repository’ s default branch"
2022-06-04 15:17:53 +02:00
// type: string
// required: false
// responses:
// 200:
// description: Returns raw file content.
2024-07-25 14:06:19 +02:00
// schema:
// type: file
2022-06-04 15:17:53 +02:00
// "404":
// "$ref": "#/responses/notFound"
if ctx . Repo . Repository . IsEmpty {
2025-02-17 14:13:17 +08:00
ctx . APIErrorNotFound ( )
2022-06-04 15:17:53 +02:00
return
}
2022-07-21 21:18:41 +02:00
blob , entry , lastModified := getBlobForEntry ( ctx )
2022-06-04 15:17:53 +02:00
if ctx . Written ( ) {
return
}
2022-07-21 21:18:41 +02:00
ctx . RespHeader ( ) . Set ( giteaObjectTypeHeader , string ( files_service . GetObjectTypeFromTreeEntry ( entry ) ) )
2022-06-04 15:17:53 +02:00
// LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
2025-06-26 02:25:20 +08:00
if blob . Size ( ) > lfs . MetaFileMaxSize {
2022-06-04 15:17:53 +02:00
// First handle caching for the blob
2026-03-01 21:32:35 +08:00
if httpcache . HandleGenericETagPrivateCache ( ctx . Req , ctx . Resp , ` " ` + blob . ID . String ( ) + ` " ` , lastModified ) {
2022-06-04 15:17:53 +02:00
return
}
2025-06-26 02:25:20 +08:00
// If not cached - serve!
2025-03-13 07:04:50 +08:00
if err := common . ServeBlob ( ctx . Base , ctx . Repo . Repository , ctx . Repo . TreePath , blob , lastModified ) ; err != nil {
2025-02-17 14:13:17 +08:00
ctx . APIErrorInternal ( err )
2022-06-04 15:17:53 +02:00
}
return
}
2025-06-26 02:25:20 +08:00
// OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes,
// we can simply read this in one go (This saves reading it twice)
2026-03-25 17:37:48 +01:00
lfsPointerBuf , err := blob . GetBlobBytes ( lfs . MetaFileMaxSize )
2022-06-04 15:17:53 +02:00
if err != nil {
2025-02-17 14:13:17 +08:00
ctx . APIErrorInternal ( err )
2022-06-04 15:17:53 +02:00
return
}
// Check if the blob represents a pointer
2026-03-25 17:37:48 +01:00
pointer , _ := lfs . ReadPointerFromBuffer ( lfsPointerBuf )
2022-06-04 15:17:53 +02:00
2023-05-09 15:34:36 +08:00
// if it's not a pointer, just serve the data directly
2022-06-04 15:17:53 +02:00
if ! pointer . IsValid ( ) {
2026-03-25 17:37:48 +01:00
_ , _ = ctx . Resp . Write ( lfsPointerBuf )
2022-06-04 15:17:53 +02:00
return
}
2023-05-09 15:34:36 +08:00
// Now check if there is a MetaObject for this pointer
2023-01-09 11:50:54 +08:00
meta , err := git_model . GetLFSMetaObjectByOid ( ctx , ctx . Repo . Repository . ID , pointer . Oid )
2022-06-04 15:17:53 +02:00
2023-05-09 15:34:36 +08:00
// If there isn't one, just serve the data directly
2025-01-06 22:45:20 +08:00
if errors . Is ( err , git_model . ErrLFSObjectNotExist ) {
2026-03-25 17:37:48 +01:00
_ , _ = ctx . Resp . Write ( lfsPointerBuf )
2022-06-04 15:17:53 +02:00
return
} else if err != nil {
2025-02-17 14:13:17 +08:00
ctx . APIErrorInternal ( err )
2022-06-04 15:17:53 +02:00
return
}
// Handle caching for the LFS object OID
2026-03-01 21:32:35 +08:00
if httpcache . HandleGenericETagPrivateCache ( ctx . Req , ctx . Resp , ` " ` + pointer . Oid + ` " ` , meta . UpdatedUnix . AsTimePtr ( ) ) {
2022-06-04 15:17:53 +02:00
return
}
2024-05-30 15:33:50 +08:00
if setting . LFS . Storage . ServeDirect ( ) {
2022-06-04 15:17:53 +02:00
// If we have a signed url (S3, object storage), redirect to this directly.
2026-03-22 05:26:13 +01:00
u , err := storage . LFS . ServeDirectURL ( pointer . RelativePath ( ) , blob . Name ( ) , ctx . Req . Method , nil )
2022-06-04 15:17:53 +02:00
if u != nil && err == nil {
ctx . Redirect ( u . String ( ) )
return
}
}
2026-03-25 17:37:48 +01:00
lfsDataFile , err := lfs . ReadMetaObject ( meta . Pointer )
2022-06-04 15:17:53 +02:00
if err != nil {
2025-02-17 14:13:17 +08:00
ctx . APIErrorInternal ( err )
2022-06-04 15:17:53 +02:00
return
}
2026-03-25 17:37:48 +01:00
defer lfsDataFile . Close ( )
httplib . ServeUserContentByFile ( ctx . Base . Req , ctx . Base . Resp , lfsDataFile , httplib . ServeHeaderOptions { Filename : ctx . Repo . TreePath } )
2022-06-04 15:17:53 +02:00
}
2023-07-07 07:31:56 +02:00
func getBlobForEntry ( ctx * context . APIContext ) ( blob * git . Blob , entry * git . TreeEntry , lastModified * time . Time ) {
2022-05-09 16:54:51 +01:00
entry , err := ctx . Repo . Commit . GetTreeEntryByPath ( ctx . Repo . TreePath )
2014-11-16 21:32:26 -05:00
if err != nil {
2015-12-09 20:46:05 -05:00
if git . IsErrNotExist ( err ) {
2025-02-17 14:13:17 +08:00
ctx . APIErrorNotFound ( )
2014-11-16 21:32:26 -05:00
} else {
2025-02-18 04:41:03 +08:00
ctx . APIErrorInternal ( err )
2014-11-16 21:32:26 -05:00
}
2023-07-07 07:31:56 +02:00
return nil , nil , nil
2014-11-16 21:32:26 -05:00
}
2022-05-09 16:54:51 +01:00
if entry . IsDir ( ) || entry . IsSubModule ( ) {
2025-02-17 14:13:17 +08:00
ctx . APIErrorNotFound ( "getBlobForEntry" , nil )
2023-07-07 07:31:56 +02:00
return nil , nil , nil
2014-11-16 21:32:26 -05:00
}
2022-05-09 16:54:51 +01:00
2024-12-29 19:30:01 -08:00
latestCommit , err := ctx . Repo . GitRepo . GetTreePathLatestCommit ( ctx . Repo . Commit . ID . String ( ) , ctx . Repo . TreePath )
2022-05-09 16:54:51 +01:00
if err != nil {
2025-02-18 04:41:03 +08:00
ctx . APIErrorInternal ( err )
2023-07-07 07:31:56 +02:00
return nil , nil , nil
2022-05-09 16:54:51 +01:00
}
2024-12-29 19:30:01 -08:00
when := & latestCommit . Committer . When
2022-05-09 16:54:51 +01:00
2024-12-29 19:30:01 -08:00
return entry . Blob ( ) , entry , when
2014-11-16 21:32:26 -05:00
}
2015-09-02 09:54:35 -04:00
2016-11-24 15:04:31 +08:00
// GetArchive get archive of a repository
2016-03-13 18:49:16 -04:00
func GetArchive ( ctx * context . APIContext ) {
2018-06-12 16:59:22 +02:00
// swagger:operation GET /repos/{owner}/{repo}/archive/{archive} repository repoGetArchive
2017-11-12 23:02:25 -08:00
// ---
// summary: Get an archive of a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: archive
// in: path
2020-09-06 18:23:47 +02:00
// description: the git reference for download with attached archive format (e.g. master.zip)
2017-11-12 23:02:25 -08:00
// type: string
// required: true
2026-01-16 10:31:12 +01:00
// - name: path
// in: query
// type: array
// items:
// type: string
// description: subpath of the repository to download
// collectionFormat: multi
2017-11-12 23:02:25 -08:00
// responses:
2018-05-31 22:51:49 -07:00
// 200:
// description: success
2019-12-20 18:07:12 +01:00
// "404":
// "$ref": "#/responses/notFound"
2026-01-16 10:31:12 +01:00
serveRepoArchive ( ctx , ctx . PathParam ( "*" ) , ctx . FormStrings ( "path" ) )
2015-09-02 09:54:35 -04:00
}
2016-08-30 20:18:40 -03:00
2016-11-24 15:04:31 +08:00
// GetEditorconfig get editor config of a repository
2016-08-30 20:18:40 -03:00
func GetEditorconfig ( ctx * context . APIContext ) {
2017-11-12 23:02:25 -08:00
// swagger:operation GET /repos/{owner}/{repo}/editorconfig/{filepath} repository repoGetEditorConfig
// ---
// summary: Get the EditorConfig definitions of a file in a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: filepath of file to get
// type: string
// required: true
2022-04-21 17:17:57 +02:00
// - name: ref
// in: query
2025-04-21 19:20:11 +02:00
// description: "The name of the commit/branch/tag. Default to the repository’ s default branch."
2022-04-21 17:17:57 +02:00
// type: string
// required: false
2017-11-12 23:02:25 -08:00
// responses:
2018-05-31 22:51:49 -07:00
// 200:
// description: success
2019-12-20 18:07:12 +01:00
// "404":
// "$ref": "#/responses/notFound"
2023-04-06 22:01:20 +02:00
ec , _ , err := ctx . Repo . GetEditorconfig ( ctx . Repo . Commit )
2016-08-30 20:18:40 -03:00
if err != nil {
if git . IsErrNotExist ( err ) {
2025-02-17 14:13:17 +08:00
ctx . APIErrorNotFound ( err )
2016-08-30 20:18:40 -03:00
} else {
2025-02-18 04:41:03 +08:00
ctx . APIErrorInternal ( err )
2016-08-30 20:18:40 -03:00
}
return
}
2024-06-19 06:32:45 +08:00
fileName := ctx . PathParam ( "filename" )
2019-10-17 02:15:02 +02:00
def , err := ec . GetDefinitionForFilename ( fileName )
2016-08-30 20:18:40 -03:00
if def == nil {
2025-02-17 14:13:17 +08:00
ctx . APIErrorNotFound ( err )
2016-08-30 20:18:40 -03:00
return
}
2019-04-17 10:06:35 -06:00
ctx . JSON ( http . StatusOK , def )
}
2024-02-19 22:50:03 +08:00
func base64Reader ( s string ) ( io . ReadSeeker , error ) {
2023-07-19 02:14:47 +08:00
b , err := base64 . StdEncoding . DecodeString ( s )
if err != nil {
return nil , err
}
return bytes . NewReader ( b ) , nil
}
2025-06-26 02:25:20 +08:00
func ReqChangeRepoFileOptionsAndCheck ( ctx * context . APIContext ) {
commonOpts := web . GetForm ( ctx ) . ( api . FileOptionsInterface ) . GetFileOptions ( )
commonOpts . BranchName = util . IfZero ( commonOpts . BranchName , ctx . Repo . Repository . DefaultBranch )
commonOpts . NewBranchName = util . IfZero ( commonOpts . NewBranchName , commonOpts . BranchName )
if ! ctx . Repo . CanWriteToBranch ( ctx , ctx . Doer , commonOpts . NewBranchName ) && ! ctx . IsUserSiteAdmin ( ) {
ctx . APIError ( http . StatusForbidden , "user should have a permission to write to the target branch" )
return
}
changeFileOpts := & files_service . ChangeRepoFilesOptions {
Message : commonOpts . Message ,
OldBranch : commonOpts . BranchName ,
NewBranch : commonOpts . NewBranchName ,
2025-10-07 00:23:14 -04:00
ForcePush : commonOpts . ForcePush ,
2025-06-26 02:25:20 +08:00
Committer : & files_service . IdentityOptions {
GitUserName : commonOpts . Committer . Name ,
GitUserEmail : commonOpts . Committer . Email ,
} ,
Author : & files_service . IdentityOptions {
GitUserName : commonOpts . Author . Name ,
GitUserEmail : commonOpts . Author . Email ,
} ,
Dates : & files_service . CommitDateOptions {
Author : commonOpts . Dates . Author ,
Committer : commonOpts . Dates . Committer ,
} ,
Signoff : commonOpts . Signoff ,
}
2025-11-06 00:22:24 +05:30
if changeFileOpts . Dates . Author . IsZero ( ) {
changeFileOpts . Dates . Author = time . Now ( )
2025-06-26 02:25:20 +08:00
}
2025-11-06 00:22:24 +05:30
if changeFileOpts . Dates . Committer . IsZero ( ) {
changeFileOpts . Dates . Committer = time . Now ( )
2025-06-26 02:25:20 +08:00
}
ctx . Data [ "__APIChangeRepoFilesOptions" ] = changeFileOpts
}
func getAPIChangeRepoFileOptions [ T api . FileOptionsInterface ] ( ctx * context . APIContext ) ( apiOpts T , opts * files_service . ChangeRepoFilesOptions ) {
return web . GetForm ( ctx ) . ( T ) , ctx . Data [ "__APIChangeRepoFilesOptions" ] . ( * files_service . ChangeRepoFilesOptions )
}
2023-06-07 17:49:58 +02:00
// ChangeFiles handles API call for modifying multiple files
2023-05-29 11:41:35 +02:00
func ChangeFiles ( ctx * context . APIContext ) {
// swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
// ---
2023-06-07 17:49:58 +02:00
// summary: Modify multiple files in a repository
2023-05-29 11:41:35 +02:00
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/ChangeFilesOptions"
// responses:
// "201":
// "$ref": "#/responses/FilesResponse"
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
2023-09-22 01:43:29 +02:00
// "423":
// "$ref": "#/responses/repoArchivedError"
2025-06-26 02:25:20 +08:00
apiOpts , opts := getAPIChangeRepoFileOptions [ * api . ChangeFilesOptions ] ( ctx )
if ctx . Written ( ) {
return
2023-05-29 11:41:35 +02:00
}
for _ , file := range apiOpts . Files {
2023-07-19 02:14:47 +08:00
contentReader , err := base64Reader ( file . ContentBase64 )
if err != nil {
2025-02-17 14:13:17 +08:00
ctx . APIError ( http . StatusUnprocessableEntity , err )
2023-07-19 02:14:47 +08:00
return
}
2025-06-26 02:25:20 +08:00
// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options
// But the LastCommitID is not provided in the API options, need to fully fix them in API
2023-05-29 11:41:35 +02:00
changeRepoFile := & files_service . ChangeRepoFile {
2023-07-19 02:14:47 +08:00
Operation : file . Operation ,
TreePath : file . Path ,
FromTreePath : file . FromPath ,
ContentReader : contentReader ,
SHA : file . SHA ,
2023-05-29 11:41:35 +02:00
}
2025-06-26 02:25:20 +08:00
opts . Files = append ( opts . Files , changeRepoFile )
2023-05-29 11:41:35 +02:00
}
if opts . Message == "" {
2025-06-26 02:25:20 +08:00
opts . Message = changeFilesCommitMessage ( ctx , opts . Files )
2023-05-29 11:41:35 +02:00
}
2025-06-26 02:25:20 +08:00
if filesResponse , err := files_service . ChangeRepoFiles ( ctx , ctx . Repo . Repository , ctx . Doer , opts ) ; err != nil {
handleChangeRepoFilesError ( ctx , err )
2023-05-29 11:41:35 +02:00
} else {
ctx . JSON ( http . StatusCreated , filesResponse )
}
}
2019-04-17 10:06:35 -06:00
// CreateFile handles API call for creating a file
2021-01-26 23:36:53 +08:00
func CreateFile ( ctx * context . APIContext ) {
2019-04-17 10:06:35 -06:00
// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
// ---
// summary: Create a file in a repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: path of the file to create
// type: string
// required: true
// - name: body
// in: body
2019-05-30 13:57:55 -04:00
// required: true
2019-04-17 10:06:35 -06:00
// schema:
// "$ref": "#/definitions/CreateFileOptions"
// responses:
// "201":
// "$ref": "#/responses/FileResponse"
2020-05-31 22:59:34 +02:00
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
2023-09-22 01:43:29 +02:00
// "423":
// "$ref": "#/responses/repoArchivedError"
2020-05-31 22:59:34 +02:00
2025-06-26 02:25:20 +08:00
apiOpts , opts := getAPIChangeRepoFileOptions [ * api . CreateFileOptions ] ( ctx )
if ctx . Written ( ) {
return
2020-04-20 18:47:05 +02:00
}
2023-07-19 02:14:47 +08:00
contentReader , err := base64Reader ( apiOpts . ContentBase64 )
if err != nil {
2025-02-17 14:13:17 +08:00
ctx . APIError ( http . StatusUnprocessableEntity , err )
2023-07-19 02:14:47 +08:00
return
}
2025-06-26 02:25:20 +08:00
opts . Files = append ( opts . Files , & files_service . ChangeRepoFile {
Operation : "create" ,
TreePath : ctx . PathParam ( "*" ) ,
ContentReader : contentReader ,
} )
2019-06-29 11:19:24 -04:00
if opts . Message == "" {
2023-05-29 11:41:35 +02:00
opts . Message = changeFilesCommitMessage ( ctx , opts . Files )
2019-06-29 11:19:24 -04:00
}
2025-06-26 02:25:20 +08:00
if filesResponse , err := files_service . ChangeRepoFiles ( ctx , ctx . Repo . Repository , ctx . Doer , opts ) ; err != nil {
handleChangeRepoFilesError ( ctx , err )
2019-04-17 10:06:35 -06:00
} else {
2023-05-29 11:41:35 +02:00
fileResponse := files_service . GetFileResponseFromFilesResponse ( filesResponse , 0 )
2019-04-17 10:06:35 -06:00
ctx . JSON ( http . StatusCreated , fileResponse )
}
}
// UpdateFile handles API call for updating a file
2021-01-26 23:36:53 +08:00
func UpdateFile ( ctx * context . APIContext ) {
2019-04-17 10:06:35 -06:00
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
// ---
2025-10-24 12:46:54 +08:00
// summary: Update a file in a repository if SHA is set, or create the file if SHA is not set
2019-04-17 10:06:35 -06:00
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: path of the file to update
// type: string
// required: true
// - name: body
// in: body
2019-05-30 13:57:55 -04:00
// required: true
2019-04-17 10:06:35 -06:00
// schema:
// "$ref": "#/definitions/UpdateFileOptions"
// responses:
// "200":
// "$ref": "#/responses/FileResponse"
2025-10-24 12:46:54 +08:00
// "201":
// "$ref": "#/responses/FileResponse"
2020-05-31 22:59:34 +02:00
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
2023-09-22 01:43:29 +02:00
// "423":
// "$ref": "#/responses/repoArchivedError"
2019-04-17 10:06:35 -06:00
2025-06-26 02:25:20 +08:00
apiOpts , opts := getAPIChangeRepoFileOptions [ * api . UpdateFileOptions ] ( ctx )
if ctx . Written ( ) {
return
2020-04-20 18:47:05 +02:00
}
2023-07-19 02:14:47 +08:00
contentReader , err := base64Reader ( apiOpts . ContentBase64 )
if err != nil {
2025-02-17 14:13:17 +08:00
ctx . APIError ( http . StatusUnprocessableEntity , err )
2023-07-19 02:14:47 +08:00
return
}
2025-10-24 12:46:54 +08:00
willCreate := apiOpts . SHA == ""
2025-06-26 02:25:20 +08:00
opts . Files = append ( opts . Files , & files_service . ChangeRepoFile {
2025-10-24 12:46:54 +08:00
Operation : util . Iif ( willCreate , "create" , "update" ) ,
2025-06-26 02:25:20 +08:00
ContentReader : contentReader ,
SHA : apiOpts . SHA ,
FromTreePath : apiOpts . FromPath ,
TreePath : ctx . PathParam ( "*" ) ,
} )
2019-06-29 11:19:24 -04:00
if opts . Message == "" {
2023-05-29 11:41:35 +02:00
opts . Message = changeFilesCommitMessage ( ctx , opts . Files )
2019-06-29 11:19:24 -04:00
}
2025-06-26 02:25:20 +08:00
if filesResponse , err := files_service . ChangeRepoFiles ( ctx , ctx . Repo . Repository , ctx . Doer , opts ) ; err != nil {
handleChangeRepoFilesError ( ctx , err )
2019-04-17 10:06:35 -06:00
} else {
2023-05-29 11:41:35 +02:00
fileResponse := files_service . GetFileResponseFromFilesResponse ( filesResponse , 0 )
2025-10-24 12:46:54 +08:00
ctx . JSON ( util . Iif ( willCreate , http . StatusCreated , http . StatusOK ) , fileResponse )
2019-04-17 10:06:35 -06:00
}
}
2025-06-26 02:25:20 +08:00
func handleChangeRepoFilesError ( ctx * context . APIContext , err error ) {
2025-10-07 00:23:14 -04:00
if git . IsErrPushRejected ( err ) {
err := err . ( * git . ErrPushRejected )
ctx . APIError ( http . StatusForbidden , err . Message )
return
}
2024-12-20 10:05:29 -08:00
if files_service . IsErrUserCannotCommit ( err ) || pull_service . IsErrFilePathProtected ( err ) {
2025-02-17 14:13:17 +08:00
ctx . APIError ( http . StatusForbidden , err )
2020-05-31 22:59:34 +02:00
return
}
2024-12-20 10:05:29 -08:00
if git_model . IsErrBranchAlreadyExists ( err ) || files_service . IsErrFilenameInvalid ( err ) || pull_service . IsErrSHADoesNotMatch ( err ) ||
2025-06-26 02:25:20 +08:00
files_service . IsErrFilePathInvalid ( err ) || files_service . IsErrRepoFileAlreadyExists ( err ) ||
files_service . IsErrCommitIDDoesNotMatch ( err ) || files_service . IsErrSHAOrCommitIDNotProvided ( err ) {
2025-02-17 14:13:17 +08:00
ctx . APIError ( http . StatusUnprocessableEntity , err )
2020-05-31 22:59:34 +02:00
return
}
2025-06-26 02:25:20 +08:00
if errors . Is ( err , util . ErrNotExist ) {
ctx . APIError ( http . StatusNotFound , err )
return
2019-04-17 10:06:35 -06:00
}
2025-06-26 02:25:20 +08:00
ctx . APIErrorInternal ( err )
2023-05-29 11:41:35 +02:00
}
// format commit message if empty
func changeFilesCommitMessage ( ctx * context . APIContext , files [ ] * files_service . ChangeRepoFile ) string {
var (
createFiles [ ] string
updateFiles [ ] string
deleteFiles [ ] string
)
for _ , file := range files {
switch file . Operation {
case "create" :
createFiles = append ( createFiles , file . TreePath )
2025-06-26 02:25:20 +08:00
case "update" , "upload" , "rename" : // upload and rename works like "update", there is no translation for them at the moment
2023-05-29 11:41:35 +02:00
updateFiles = append ( updateFiles , file . TreePath )
case "delete" :
deleteFiles = append ( deleteFiles , file . TreePath )
}
}
message := ""
if len ( createFiles ) != 0 {
2024-02-15 05:48:45 +08:00
message += ctx . Locale . TrString ( "repo.editor.add" , strings . Join ( createFiles , ", " ) + "\n" )
2023-05-29 11:41:35 +02:00
}
if len ( updateFiles ) != 0 {
2024-02-15 05:48:45 +08:00
message += ctx . Locale . TrString ( "repo.editor.update" , strings . Join ( updateFiles , ", " ) + "\n" )
2023-05-29 11:41:35 +02:00
}
if len ( deleteFiles ) != 0 {
2024-02-15 05:48:45 +08:00
message += ctx . Locale . TrString ( "repo.editor.delete" , strings . Join ( deleteFiles , ", " ) )
2023-05-29 11:41:35 +02:00
}
return strings . Trim ( message , "\n" )
2019-04-17 10:06:35 -06:00
}
2022-01-10 04:32:37 -05:00
// DeleteFile Delete a file in a repository
2021-01-26 23:36:53 +08:00
func DeleteFile ( ctx * context . APIContext ) {
2019-04-17 10:06:35 -06:00
// swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
// ---
// summary: Delete a file in a repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: path of the file to delete
// type: string
// required: true
// - name: body
// in: body
2019-05-30 13:57:55 -04:00
// required: true
2019-04-17 10:06:35 -06:00
// schema:
// "$ref": "#/definitions/DeleteFileOptions"
// responses:
// "200":
// "$ref": "#/responses/FileDeleteResponse"
2020-04-15 06:18:51 +01:00
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/error"
2025-06-26 02:25:20 +08:00
// "422":
// "$ref": "#/responses/error"
2023-09-22 01:43:29 +02:00
// "423":
// "$ref": "#/responses/repoArchivedError"
2019-12-20 18:07:12 +01:00
2025-06-26 02:25:20 +08:00
apiOpts , opts := getAPIChangeRepoFileOptions [ * api . DeleteFileOptions ] ( ctx )
if ctx . Written ( ) {
2019-04-17 10:06:35 -06:00
return
}
2025-06-26 02:25:20 +08:00
opts . Files = append ( opts . Files , & files_service . ChangeRepoFile {
Operation : "delete" ,
SHA : apiOpts . SHA ,
TreePath : ctx . PathParam ( "*" ) ,
} )
2019-06-29 11:19:24 -04:00
if opts . Message == "" {
2023-05-29 11:41:35 +02:00
opts . Message = changeFilesCommitMessage ( ctx , opts . Files )
2019-06-29 11:19:24 -04:00
}
2023-05-29 11:41:35 +02:00
if filesResponse , err := files_service . ChangeRepoFiles ( ctx , ctx . Repo . Repository , ctx . Doer , opts ) ; err != nil {
2025-06-26 02:25:20 +08:00
handleChangeRepoFilesError ( ctx , err )
2019-04-17 10:06:35 -06:00
} else {
2023-05-29 11:41:35 +02:00
fileResponse := files_service . GetFileResponseFromFilesResponse ( filesResponse , 0 )
2020-04-15 06:18:51 +01:00
ctx . JSON ( http . StatusOK , fileResponse ) // FIXME on APIv2: return http.StatusNoContent
2019-04-17 10:06:35 -06:00
}
}
2025-04-21 19:20:11 +02:00
func resolveRefCommit ( ctx * context . APIContext , ref string , minCommitIDLen ... int ) * utils . RefCommit {
ref = util . IfZero ( ref , ctx . Repo . Repository . DefaultBranch )
refCommit , err := utils . ResolveRefCommit ( ctx , ctx . Repo . Repository , ref , minCommitIDLen ... )
if errors . Is ( err , util . ErrNotExist ) {
ctx . APIErrorNotFound ( err )
} else if err != nil {
ctx . APIErrorInternal ( err )
}
return refCommit
}
2025-06-25 10:34:21 +08:00
func GetContentsExt ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/contents-ext/{filepath} repository repoGetContentsExt
// ---
// summary: The extended "contents" API, to get file metadata and/or content, or list a directory.
// description: It guarantees that only one of the response fields is set if the request succeeds.
// Users can pass "includes=file_content" or "includes=lfs_metadata" to retrieve more fields.
2025-06-26 02:25:20 +08:00
// "includes=file_content" only works for single file, if you need to retrieve file contents in batch,
// use "file-contents" API after listing the directory.
2025-06-25 10:34:21 +08:00
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
2025-07-03 09:45:42 +08:00
// description: path of the dir, file, symlink or submodule in the repo. Swagger requires path parameter to be "required",
// you can leave it empty or pass a single dot (".") to get the root directory.
2025-06-25 10:34:21 +08:00
// type: string
// required: true
// - name: ref
// in: query
// description: the name of the commit/branch/tag, default to the repository’ s default branch.
// type: string
// required: false
// - name: includes
// in: query
// description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields.
2025-07-03 09:45:42 +08:00
// Option "file_content" will try to retrieve the file content, "lfs_metadata" will try to retrieve LFS metadata,
// "commit_metadata" will try to retrieve commit metadata, and "commit_message" will try to retrieve commit message.
2025-06-25 10:34:21 +08:00
// type: string
// required: false
// responses:
// "200":
// "$ref": "#/responses/ContentsExtResponse"
// "404":
// "$ref": "#/responses/notFound"
2025-07-03 09:45:42 +08:00
if treePath := ctx . PathParam ( "*" ) ; treePath == "." || treePath == "/" {
ctx . SetPathParam ( "*" , "" ) // workaround for swagger, it requires path parameter to be "required", but we need to list root directory
}
2025-06-25 10:34:21 +08:00
opts := files_service . GetContentsOrListOptions { TreePath : ctx . PathParam ( "*" ) }
for includeOpt := range strings . SplitSeq ( ctx . FormString ( "includes" ) , "," ) {
if includeOpt == "" {
continue
}
switch includeOpt {
case "file_content" :
opts . IncludeSingleFileContent = true
case "lfs_metadata" :
opts . IncludeLfsMetadata = true
2025-07-03 09:45:42 +08:00
case "commit_metadata" :
opts . IncludeCommitMetadata = true
case "commit_message" :
opts . IncludeCommitMessage = true
2025-06-25 10:34:21 +08:00
default :
ctx . APIError ( http . StatusBadRequest , fmt . Sprintf ( "unknown include option %q" , includeOpt ) )
return
}
}
ctx . JSON ( http . StatusOK , getRepoContents ( ctx , opts ) )
}
2019-06-29 16:51:10 -04:00
func GetContents ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
2019-04-17 10:06:35 -06:00
// ---
2025-06-25 10:34:21 +08:00
// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir.
2025-06-26 02:25:20 +08:00
// description: This API follows GitHub's design, and it is not easy to use. Recommend users to use the "contents-ext" API instead.
2019-04-17 10:06:35 -06:00
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
2019-06-29 16:51:10 -04:00
// description: path of the dir, file, symlink or submodule in the repo
2019-04-17 10:06:35 -06:00
// type: string
// required: true
// - name: ref
// in: query
2025-04-21 19:20:11 +02:00
// description: "The name of the commit/branch/tag. Default to the repository’ s default branch."
2019-04-17 10:06:35 -06:00
// type: string
2019-06-29 16:51:10 -04:00
// required: false
2019-04-17 10:06:35 -06:00
// responses:
// "200":
2019-06-29 16:51:10 -04:00
// "$ref": "#/responses/ContentsResponse"
2020-04-15 06:18:51 +01:00
// "404":
// "$ref": "#/responses/notFound"
2025-07-03 09:45:42 +08:00
ret := getRepoContents ( ctx , files_service . GetContentsOrListOptions {
TreePath : ctx . PathParam ( "*" ) ,
IncludeSingleFileContent : true ,
IncludeCommitMetadata : true ,
} )
2025-04-21 19:20:11 +02:00
if ctx . Written ( ) {
2019-04-17 10:06:35 -06:00
return
}
2025-06-25 10:34:21 +08:00
ctx . JSON ( http . StatusOK , util . Iif [ any ] ( ret . FileContents != nil , ret . FileContents , ret . DirContents ) )
}
2019-04-17 10:06:35 -06:00
2025-06-25 10:34:21 +08:00
func getRepoContents ( ctx * context . APIContext , opts files_service . GetContentsOrListOptions ) * api . ContentsExtResponse {
refCommit := resolveRefCommit ( ctx , ctx . FormTrim ( "ref" ) )
if ctx . Written ( ) {
return nil
}
ret , err := files_service . GetContentsOrList ( ctx , ctx . Repo . Repository , ctx . Repo . GitRepo , refCommit , opts )
if err != nil {
2020-04-15 06:18:51 +01:00
if git . IsErrNotExist ( err ) {
2025-02-17 14:13:17 +08:00
ctx . APIErrorNotFound ( "GetContentsOrList" , err )
2025-06-25 10:34:21 +08:00
return nil
2020-04-15 06:18:51 +01:00
}
2025-02-18 04:41:03 +08:00
ctx . APIErrorInternal ( err )
2019-04-17 10:06:35 -06:00
}
2025-06-25 10:34:21 +08:00
return & ret
2016-08-30 20:18:40 -03:00
}
2019-06-29 16:51:10 -04:00
func GetContentsList ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
// ---
2025-06-25 10:34:21 +08:00
// summary: Gets the metadata of all the entries of the root dir.
2025-06-26 02:25:20 +08:00
// description: This API follows GitHub's design, and it is not easy to use. Recommend users to use our "contents-ext" API instead.
2019-06-29 16:51:10 -04:00
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: ref
// in: query
2025-04-21 19:20:11 +02:00
// description: "The name of the commit/branch/tag. Default to the repository’ s default branch."
2019-06-29 16:51:10 -04:00
// type: string
// required: false
// responses:
// "200":
// "$ref": "#/responses/ContentsListResponse"
2020-04-15 06:18:51 +01:00
// "404":
// "$ref": "#/responses/notFound"
2019-06-29 16:51:10 -04:00
// same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
GetContents ( ctx )
}
2025-04-21 19:20:11 +02:00
func GetFileContentsGet ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/file-contents repository repoGetFileContents
// ---
// summary: Get the metadata and contents of requested files
2025-06-26 02:25:20 +08:00
// description: See the POST method. This GET method supports using JSON encoded request body in query parameter.
2025-04-21 19:20:11 +02:00
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: ref
// in: query
// description: "The name of the commit/branch/tag. Default to the repository’ s default branch."
// type: string
// required: false
// - name: body
// in: query
// description: "The JSON encoded body (see the POST request): {\"files\": [\"filename1\", \"filename2\"]}"
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ContentsListResponse"
// "404":
// "$ref": "#/responses/notFound"
2025-06-26 02:25:20 +08:00
// The POST method requires "write" permission, so we also support this "GET" method
2025-04-21 19:20:11 +02:00
handleGetFileContents ( ctx )
}
func GetFileContentsPost ( ctx * context . APIContext ) {
// swagger:operation POST /repos/{owner}/{repo}/file-contents repository repoGetFileContentsPost
// ---
// summary: Get the metadata and contents of requested files
// description: Uses automatic pagination based on default page size and
// max response size and returns the maximum allowed number of files.
// Files which could not be retrieved are null. Files which are too large
// are being returned with `encoding == null`, `content == null` and `size > 0`,
// they can be requested separately by using the `download_url`.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: ref
// in: query
// description: "The name of the commit/branch/tag. Default to the repository’ s default branch."
// type: string
// required: false
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/GetFilesOptions"
// responses:
// "200":
// "$ref": "#/responses/ContentsListResponse"
// "404":
// "$ref": "#/responses/notFound"
// This is actually a "read" request, but we need to accept a "files" list, then POST method seems easy to use.
// But the permission system requires that the caller must have "write" permission to use POST method.
2025-06-26 02:25:20 +08:00
// At the moment, there is no other way to get around the permission check, so there is a "GET" workaround method above.
2025-04-21 19:20:11 +02:00
handleGetFileContents ( ctx )
}
func handleGetFileContents ( ctx * context . APIContext ) {
opts , ok := web . GetForm ( ctx ) . ( * api . GetFilesOptions )
if ! ok {
err := json . Unmarshal ( util . UnsafeStringToBytes ( ctx . FormString ( "body" ) ) , & opts )
if err != nil {
ctx . APIError ( http . StatusBadRequest , "invalid body parameter" )
return
}
}
refCommit := resolveRefCommit ( ctx , ctx . FormTrim ( "ref" ) )
if ctx . Written ( ) {
return
}
2025-06-25 10:34:21 +08:00
filesResponse := files_service . GetContentsListFromTreePaths ( ctx , ctx . Repo . Repository , ctx . Repo . GitRepo , refCommit , opts . Files )
2025-04-21 19:20:11 +02:00
ctx . JSON ( http . StatusOK , util . SliceNilAsEmpty ( filesResponse ) )
}