2014-11-16 18:27:04 -07:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2018-11-28 04:26:14 -07:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2022-11-27 11:20:29 -07:00
// SPDX-License-Identifier: MIT
2014-11-16 18:27:04 -07:00
2015-12-04 15:16:42 -07:00
package repo
2014-11-16 19:32:26 -07:00
import (
2022-06-04 07:17:53 -06:00
"bytes"
2019-04-17 10:06:35 -06:00
"encoding/base64"
2022-08-29 03:45:20 -06:00
"errors"
2020-05-31 14:59:34 -06:00
"fmt"
2022-06-04 07:17:53 -06:00
"io"
2019-04-17 10:06:35 -06:00
"net/http"
2022-05-09 09:54:51 -06:00
"path"
2023-05-29 03:41:35 -06:00
"strings"
2019-12-23 19:33:52 -07:00
"time"
2019-04-17 10:06:35 -06:00
2016-11-10 09:24:48 -07:00
"code.gitea.io/gitea/models"
2022-06-12 09:51:54 -06:00
git_model "code.gitea.io/gitea/models/git"
2021-12-09 18:27:50 -07:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-09 12:57:58 -07:00
"code.gitea.io/gitea/models/unit"
2019-03-27 03:33:00 -06:00
"code.gitea.io/gitea/modules/git"
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-27 13:09:51 -07:00
"code.gitea.io/gitea/modules/gitrepo"
2022-06-04 07:17:53 -06:00
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
2022-05-09 09:54:51 -06:00
"code.gitea.io/gitea/modules/setting"
2022-06-04 07:17:53 -06:00
"code.gitea.io/gitea/modules/storage"
2019-05-11 04:21:34 -06:00
api "code.gitea.io/gitea/modules/structs"
2021-01-26 08:36:53 -07:00
"code.gitea.io/gitea/modules/web"
2021-06-08 17:33:54 -06:00
"code.gitea.io/gitea/routers/common"
2024-02-27 00:12:22 -07:00
"code.gitea.io/gitea/services/context"
2022-08-29 03:45:20 -06:00
archiver_service "code.gitea.io/gitea/services/repository/archiver"
2021-11-24 00:56:24 -07:00
files_service "code.gitea.io/gitea/services/repository/files"
2014-11-16 19:32:26 -07:00
)
2022-07-21 13:18:41 -06:00
const giteaObjectTypeHeader = "X-Gitea-Object-Type"
2016-11-24 00:04:31 -07:00
// GetRawFile get a file by path on a repository
2016-03-13 16:49:16 -06:00
func GetRawFile ( ctx * context . APIContext ) {
2017-11-13 00:02:25 -07:00
// swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
// ---
// summary: Get a file from a repository
// produces:
2024-07-25 06:06:19 -06:00
// - application/octet-stream
2017-11-13 00:02:25 -07: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-04 23:35:54 -07: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-13 00:02:25 -07:00
// type: string
// required: true
2021-02-08 17:15:47 -07:00
// - name: ref
// in: query
2024-11-04 23:35:54 -07:00
// description: "The name of the commit/branch/tag. Default the repository’ s default branch"
2021-02-08 17:15:47 -07:00
// type: string
// required: false
2017-11-13 00:02:25 -07:00
// responses:
2018-05-31 23:51:49 -06:00
// 200:
2022-04-28 08:57:56 -06:00
// description: Returns raw file content.
2024-07-25 06:06:19 -06:00
// schema:
// type: file
2019-12-20 10:07:12 -07:00
// "404":
// "$ref": "#/responses/notFound"
2019-01-17 17:01:04 -07:00
if ctx . Repo . Repository . IsEmpty {
2019-03-18 20:29:43 -06:00
ctx . NotFound ( )
2017-06-10 20:57:28 -06:00
return
}
2022-07-21 13:18:41 -06:00
blob , entry , lastModified := getBlobForEntry ( ctx )
2022-05-09 09:54:51 -06:00
if ctx . Written ( ) {
return
}
2022-07-21 13:18:41 -06:00
ctx . RespHeader ( ) . Set ( giteaObjectTypeHeader , string ( files_service . GetObjectTypeFromTreeEntry ( entry ) ) )
2023-05-20 19:50:53 -06:00
if err := common . ServeBlob ( ctx . Base , ctx . Repo . TreePath , blob , lastModified ) ; err != nil {
2022-05-09 09:54:51 -06:00
ctx . Error ( http . StatusInternalServerError , "ServeBlob" , err )
}
}
2022-06-04 07:17:53 -06: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 06:06:19 -06:00
// produces:
// - application/octet-stream
2022-06-04 07:17:53 -06: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-04 23:35:54 -07: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 07:17:53 -06:00
// type: string
// required: true
// - name: ref
// in: query
2024-11-04 23:35:54 -07:00
// description: "The name of the commit/branch/tag. Default the repository’ s default branch"
2022-06-04 07:17:53 -06:00
// type: string
// required: false
// responses:
// 200:
// description: Returns raw file content.
2024-07-25 06:06:19 -06:00
// schema:
// type: file
2022-06-04 07:17:53 -06:00
// "404":
// "$ref": "#/responses/notFound"
if ctx . Repo . Repository . IsEmpty {
ctx . NotFound ( )
return
}
2022-07-21 13:18:41 -06:00
blob , entry , lastModified := getBlobForEntry ( ctx )
2022-06-04 07:17:53 -06:00
if ctx . Written ( ) {
return
}
2022-07-21 13:18:41 -06:00
ctx . RespHeader ( ) . Set ( giteaObjectTypeHeader , string ( files_service . GetObjectTypeFromTreeEntry ( entry ) ) )
2022-06-04 07:17:53 -06:00
// LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
if blob . Size ( ) > 1024 {
// First handle caching for the blob
if httpcache . HandleGenericETagTimeCache ( ctx . Req , ctx . Resp , ` " ` + blob . ID . String ( ) + ` " ` , lastModified ) {
return
}
// OK not cached - serve!
2023-05-20 19:50:53 -06:00
if err := common . ServeBlob ( ctx . Base , ctx . Repo . TreePath , blob , lastModified ) ; err != nil {
2022-06-04 07:17:53 -06:00
ctx . ServerError ( "ServeBlob" , err )
}
return
}
2024-03-26 01:48:53 -06:00
// OK, now the blob is known to have at most 1024 bytes we can simply read this in one go (This saves reading it twice)
2022-06-04 07:17:53 -06:00
dataRc , err := blob . DataAsync ( )
if err != nil {
ctx . ServerError ( "DataAsync" , err )
return
}
2023-05-09 01:34:36 -06:00
// FIXME: code from #19689, what if the file is large ... OOM ...
2022-06-04 07:17:53 -06:00
buf , err := io . ReadAll ( dataRc )
if err != nil {
_ = dataRc . Close ( )
ctx . ServerError ( "DataAsync" , err )
return
}
if err := dataRc . Close ( ) ; err != nil {
2023-05-20 19:50:53 -06:00
log . Error ( "Error whilst closing blob %s reader in %-v. Error: %v" , blob . ID , ctx . Repo . Repository , err )
2022-06-04 07:17:53 -06:00
}
// Check if the blob represents a pointer
pointer , _ := lfs . ReadPointer ( bytes . NewReader ( buf ) )
2023-05-09 01:34:36 -06:00
// if it's not a pointer, just serve the data directly
2022-06-04 07:17:53 -06:00
if ! pointer . IsValid ( ) {
// First handle caching for the blob
if httpcache . HandleGenericETagTimeCache ( ctx . Req , ctx . Resp , ` " ` + blob . ID . String ( ) + ` " ` , lastModified ) {
return
}
// OK not cached - serve!
2023-05-20 19:50:53 -06:00
common . ServeContentByReader ( ctx . Base , ctx . Repo . TreePath , blob . Size ( ) , bytes . NewReader ( buf ) )
2022-06-04 07:17:53 -06:00
return
}
2023-05-09 01:34:36 -06:00
// Now check if there is a MetaObject for this pointer
2023-01-08 20:50:54 -07:00
meta , err := git_model . GetLFSMetaObjectByOid ( ctx , ctx . Repo . Repository . ID , pointer . Oid )
2022-06-04 07:17:53 -06:00
2023-05-09 01:34:36 -06:00
// If there isn't one, just serve the data directly
2022-06-12 09:51:54 -06:00
if err == git_model . ErrLFSObjectNotExist {
2022-06-04 07:17:53 -06:00
// Handle caching for the blob SHA (not the LFS object OID)
if httpcache . HandleGenericETagTimeCache ( ctx . Req , ctx . Resp , ` " ` + blob . ID . String ( ) + ` " ` , lastModified ) {
return
}
2023-05-20 19:50:53 -06:00
common . ServeContentByReader ( ctx . Base , ctx . Repo . TreePath , blob . Size ( ) , bytes . NewReader ( buf ) )
2022-06-04 07:17:53 -06:00
return
} else if err != nil {
ctx . ServerError ( "GetLFSMetaObjectByOid" , err )
return
}
// Handle caching for the LFS object OID
if httpcache . HandleGenericETagCache ( ctx . Req , ctx . Resp , ` " ` + pointer . Oid + ` " ` ) {
return
}
2024-05-30 01:33:50 -06:00
if setting . LFS . Storage . ServeDirect ( ) {
2022-06-04 07:17:53 -06:00
// If we have a signed url (S3, object storage), redirect to this directly.
2024-10-31 09:28:25 -06:00
u , err := storage . LFS . URL ( pointer . RelativePath ( ) , blob . Name ( ) , nil )
2022-06-04 07:17:53 -06:00
if u != nil && err == nil {
ctx . Redirect ( u . String ( ) )
return
}
}
lfsDataRc , err := lfs . ReadMetaObject ( meta . Pointer )
if err != nil {
ctx . ServerError ( "ReadMetaObject" , err )
return
}
defer lfsDataRc . Close ( )
2023-05-20 19:50:53 -06:00
common . ServeContentByReadSeeker ( ctx . Base , ctx . Repo . TreePath , lastModified , lfsDataRc )
2022-06-04 07:17:53 -06:00
}
2023-07-06 23:31:56 -06:00
func getBlobForEntry ( ctx * context . APIContext ) ( blob * git . Blob , entry * git . TreeEntry , lastModified * time . Time ) {
2022-05-09 09:54:51 -06:00
entry , err := ctx . Repo . Commit . GetTreeEntryByPath ( ctx . Repo . TreePath )
2014-11-16 19:32:26 -07:00
if err != nil {
2015-12-09 18:46:05 -07:00
if git . IsErrNotExist ( err ) {
2019-03-18 20:29:43 -06:00
ctx . NotFound ( )
2014-11-16 19:32:26 -07:00
} else {
2022-05-09 09:54:51 -06:00
ctx . Error ( http . StatusInternalServerError , "GetTreeEntryByPath" , err )
2014-11-16 19:32:26 -07:00
}
2023-07-06 23:31:56 -06:00
return nil , nil , nil
2014-11-16 19:32:26 -07:00
}
2022-05-09 09:54:51 -06:00
if entry . IsDir ( ) || entry . IsSubModule ( ) {
ctx . NotFound ( "getBlobForEntry" , nil )
2023-07-06 23:31:56 -06:00
return nil , nil , nil
2014-11-16 19:32:26 -07:00
}
2022-05-09 09:54:51 -06:00
2022-07-25 09:39:42 -06:00
info , _ , err := git . Entries ( [ ] * git . TreeEntry { entry } ) . GetCommitsInfo ( ctx , ctx . Repo . Commit , path . Dir ( "/" + ctx . Repo . TreePath ) [ 1 : ] )
2022-05-09 09:54:51 -06:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , "GetCommitsInfo" , err )
2023-07-06 23:31:56 -06:00
return nil , nil , nil
2022-05-09 09:54:51 -06:00
}
if len ( info ) == 1 {
// Not Modified
2023-07-06 23:31:56 -06:00
lastModified = & info [ 0 ] . Commit . Committer . When
2022-05-09 09:54:51 -06:00
}
blob = entry . Blob ( )
2022-07-21 13:18:41 -06:00
return blob , entry , lastModified
2014-11-16 19:32:26 -07:00
}
2015-09-02 07:54:35 -06:00
2016-11-24 00:04:31 -07:00
// GetArchive get archive of a repository
2016-03-13 16:49:16 -06:00
func GetArchive ( ctx * context . APIContext ) {
2018-06-12 08:59:22 -06:00
// swagger:operation GET /repos/{owner}/{repo}/archive/{archive} repository repoGetArchive
2017-11-13 00:02:25 -07: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 10:23:47 -06:00
// description: the git reference for download with attached archive format (e.g. master.zip)
2017-11-13 00:02:25 -07:00
// type: string
// required: true
// responses:
2018-05-31 23:51:49 -06:00
// 200:
// description: success
2019-12-20 10:07:12 -07:00
// "404":
// "$ref": "#/responses/notFound"
2021-09-17 18:54:15 -06:00
if ctx . Repo . GitRepo == nil {
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-27 13:09:51 -07:00
gitRepo , err := gitrepo . OpenRepository ( ctx , ctx . Repo . Repository )
2021-09-17 18:54:15 -06:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , "OpenRepository" , err )
return
}
ctx . Repo . GitRepo = gitRepo
defer gitRepo . Close ( )
2015-09-02 07:54:35 -06:00
}
2022-08-29 03:45:20 -06:00
archiveDownload ( ctx )
}
func archiveDownload ( ctx * context . APIContext ) {
2024-06-18 16:32:45 -06:00
uri := ctx . PathParam ( "*" )
2022-08-29 03:45:20 -06:00
aReq , err := archiver_service . NewRequest ( ctx . Repo . Repository . ID , ctx . Repo . GitRepo , uri )
if err != nil {
if errors . Is ( err , archiver_service . ErrUnknownArchiveFormat { } ) {
ctx . Error ( http . StatusBadRequest , "unknown archive format" , err )
} else if errors . Is ( err , archiver_service . RepoRefNotFoundError { } ) {
ctx . Error ( http . StatusNotFound , "unrecognized reference" , err )
} else {
ctx . ServerError ( "archiver_service.NewRequest" , err )
}
return
}
archiver , err := aReq . Await ( ctx )
if err != nil {
ctx . ServerError ( "archiver.Await" , err )
return
}
download ( ctx , aReq . GetArchiveName ( ) , archiver )
}
func download ( ctx * context . APIContext , archiveName string , archiver * repo_model . RepoArchiver ) {
downloadName := ctx . Repo . Repository . Name + "-" + archiveName
2024-05-28 09:30:34 -06:00
// Add nix format link header so tarballs lock correctly:
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
ctx . Resp . Header ( ) . Add ( "Link" , fmt . Sprintf ( ` <%s/archive/%s.tar.gz?rev=%s>; rel="immutable" ` ,
ctx . Repo . Repository . APIURL ( ) ,
archiver . CommitID , archiver . CommitID ) )
2022-08-29 03:45:20 -06:00
rPath := archiver . RelativePath ( )
2024-05-30 01:33:50 -06:00
if setting . RepoArchive . Storage . ServeDirect ( ) {
2022-08-29 03:45:20 -06:00
// If we have a signed url (S3, object storage), redirect to this directly.
2024-10-31 09:28:25 -06:00
u , err := storage . RepoArchives . URL ( rPath , downloadName , nil )
2022-08-29 03:45:20 -06:00
if u != nil && err == nil {
ctx . Redirect ( u . String ( ) )
return
}
}
// If we have matched and access to release or issue
fr , err := storage . RepoArchives . Open ( rPath )
if err != nil {
ctx . ServerError ( "Open" , err )
return
}
defer fr . Close ( )
2022-11-24 07:25:13 -07:00
ctx . ServeContent ( fr , & context . ServeHeaderOptions {
Filename : downloadName ,
LastModified : archiver . CreatedUnix . AsLocalTime ( ) ,
} )
2015-09-02 07:54:35 -06:00
}
2016-08-30 17:18:40 -06:00
2016-11-24 00:04:31 -07:00
// GetEditorconfig get editor config of a repository
2016-08-30 17:18:40 -06:00
func GetEditorconfig ( ctx * context . APIContext ) {
2017-11-13 00:02:25 -07: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 09:17:57 -06:00
// - name: ref
// in: query
// description: "The name of the commit/branch/tag. Default the repository’ s default branch (usually master)"
// type: string
// required: false
2017-11-13 00:02:25 -07:00
// responses:
2018-05-31 23:51:49 -06:00
// 200:
// description: success
2019-12-20 10:07:12 -07:00
// "404":
// "$ref": "#/responses/notFound"
2023-04-06 14:01:20 -06:00
ec , _ , err := ctx . Repo . GetEditorconfig ( ctx . Repo . Commit )
2016-08-30 17:18:40 -06:00
if err != nil {
if git . IsErrNotExist ( err ) {
2019-03-18 20:29:43 -06:00
ctx . NotFound ( err )
2016-08-30 17:18:40 -06:00
} else {
2019-04-17 10:06:35 -06:00
ctx . Error ( http . StatusInternalServerError , "GetEditorconfig" , err )
2016-08-30 17:18:40 -06:00
}
return
}
2024-06-18 16:32:45 -06:00
fileName := ctx . PathParam ( "filename" )
2019-10-16 18:15:02 -06:00
def , err := ec . GetDefinitionForFilename ( fileName )
2016-08-30 17:18:40 -06:00
if def == nil {
2019-03-18 20:29:43 -06:00
ctx . NotFound ( err )
2016-08-30 17:18:40 -06:00
return
}
2019-04-17 10:06:35 -06:00
ctx . JSON ( http . StatusOK , def )
}
2020-04-24 10:20:22 -06:00
// canWriteFiles returns true if repository is editable and user has proper access level.
2022-04-28 09:45:33 -06:00
func canWriteFiles ( ctx * context . APIContext , branch string ) bool {
2023-07-22 08:14:27 -06:00
return ctx . Repo . CanWriteToBranch ( ctx , ctx . Doer , branch ) &&
2022-04-28 09:45:33 -06:00
! ctx . Repo . Repository . IsMirror &&
! ctx . Repo . Repository . IsArchived
2019-04-17 10:06:35 -06:00
}
2020-04-24 10:20:22 -06:00
// canReadFiles returns true if repository is readable and user has proper access level.
func canReadFiles ( r * context . Repository ) bool {
2021-11-09 12:57:58 -07:00
return r . Permission . CanRead ( unit . TypeCode )
2019-04-17 10:06:35 -06:00
}
2024-02-19 07:50:03 -07:00
func base64Reader ( s string ) ( io . ReadSeeker , error ) {
2023-07-18 12:14:47 -06:00
b , err := base64 . StdEncoding . DecodeString ( s )
if err != nil {
return nil , err
}
return bytes . NewReader ( b ) , nil
}
2023-06-07 09:49:58 -06:00
// ChangeFiles handles API call for modifying multiple files
2023-05-29 03:41:35 -06:00
func ChangeFiles ( ctx * context . APIContext ) {
// swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
// ---
2023-06-07 09:49:58 -06:00
// summary: Modify multiple files in a repository
2023-05-29 03:41: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: 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-21 17:43:29 -06:00
// "423":
// "$ref": "#/responses/repoArchivedError"
2023-05-29 03:41:35 -06:00
apiOpts := web . GetForm ( ctx ) . ( * api . ChangeFilesOptions )
if apiOpts . BranchName == "" {
apiOpts . BranchName = ctx . Repo . Repository . DefaultBranch
}
2023-07-18 12:14:47 -06:00
var files [ ] * files_service . ChangeRepoFile
2023-05-29 03:41:35 -06:00
for _ , file := range apiOpts . Files {
2023-07-18 12:14:47 -06:00
contentReader , err := base64Reader ( file . ContentBase64 )
if err != nil {
ctx . Error ( http . StatusUnprocessableEntity , "Invalid base64 content" , err )
return
}
2023-05-29 03:41:35 -06:00
changeRepoFile := & files_service . ChangeRepoFile {
2023-07-18 12:14:47 -06:00
Operation : file . Operation ,
TreePath : file . Path ,
FromTreePath : file . FromPath ,
ContentReader : contentReader ,
SHA : file . SHA ,
2023-05-29 03:41:35 -06:00
}
files = append ( files , changeRepoFile )
}
opts := & files_service . ChangeRepoFilesOptions {
Files : files ,
Message : apiOpts . Message ,
OldBranch : apiOpts . BranchName ,
NewBranch : apiOpts . NewBranchName ,
Committer : & files_service . IdentityOptions {
Name : apiOpts . Committer . Name ,
Email : apiOpts . Committer . Email ,
} ,
Author : & files_service . IdentityOptions {
Name : apiOpts . Author . Name ,
Email : apiOpts . Author . Email ,
} ,
Dates : & files_service . CommitDateOptions {
Author : apiOpts . Dates . Author ,
Committer : apiOpts . Dates . Committer ,
} ,
Signoff : apiOpts . Signoff ,
}
if opts . Dates . Author . IsZero ( ) {
opts . Dates . Author = time . Now ( )
}
if opts . Dates . Committer . IsZero ( ) {
opts . Dates . Committer = time . Now ( )
}
if opts . Message == "" {
opts . Message = changeFilesCommitMessage ( ctx , files )
}
if filesResponse , err := createOrUpdateFiles ( ctx , opts ) ; err != nil {
handleCreateOrUpdateFileError ( ctx , err )
} 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 08:36:53 -07: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 11:57:55 -06:00
// required: true
2019-04-17 10:06:35 -06:00
// schema:
// "$ref": "#/definitions/CreateFileOptions"
// responses:
// "201":
// "$ref": "#/responses/FileResponse"
2020-05-31 14:59:34 -06:00
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
2023-09-21 17:43:29 -06:00
// "423":
// "$ref": "#/responses/repoArchivedError"
2020-05-31 14:59:34 -06:00
2021-01-26 08:36:53 -07:00
apiOpts := web . GetForm ( ctx ) . ( * api . CreateFileOptions )
2019-04-17 10:06:35 -06:00
2020-04-20 10:47:05 -06:00
if apiOpts . BranchName == "" {
apiOpts . BranchName = ctx . Repo . Repository . DefaultBranch
}
2023-07-18 12:14:47 -06:00
contentReader , err := base64Reader ( apiOpts . ContentBase64 )
if err != nil {
ctx . Error ( http . StatusUnprocessableEntity , "Invalid base64 content" , err )
return
}
2023-05-29 03:41:35 -06:00
opts := & files_service . ChangeRepoFilesOptions {
Files : [ ] * files_service . ChangeRepoFile {
{
2023-07-18 12:14:47 -06:00
Operation : "create" ,
2024-06-18 16:32:45 -06:00
TreePath : ctx . PathParam ( "*" ) ,
2023-07-18 12:14:47 -06:00
ContentReader : contentReader ,
2023-05-29 03:41:35 -06:00
} ,
} ,
2019-04-17 10:06:35 -06:00
Message : apiOpts . Message ,
OldBranch : apiOpts . BranchName ,
NewBranch : apiOpts . NewBranchName ,
2021-11-24 00:56:24 -07:00
Committer : & files_service . IdentityOptions {
2019-04-17 10:06:35 -06:00
Name : apiOpts . Committer . Name ,
Email : apiOpts . Committer . Email ,
} ,
2021-11-24 00:56:24 -07:00
Author : & files_service . IdentityOptions {
2019-04-17 10:06:35 -06:00
Name : apiOpts . Author . Name ,
Email : apiOpts . Author . Email ,
} ,
2021-11-24 00:56:24 -07:00
Dates : & files_service . CommitDateOptions {
2019-12-23 19:33:52 -07:00
Author : apiOpts . Dates . Author ,
Committer : apiOpts . Dates . Committer ,
} ,
2021-01-29 01:57:45 -07:00
Signoff : apiOpts . Signoff ,
2019-12-23 19:33:52 -07:00
}
if opts . Dates . Author . IsZero ( ) {
opts . Dates . Author = time . Now ( )
}
if opts . Dates . Committer . IsZero ( ) {
opts . Dates . Committer = time . Now ( )
2019-04-17 10:06:35 -06:00
}
2019-06-29 09:19:24 -06:00
if opts . Message == "" {
2023-05-29 03:41:35 -06:00
opts . Message = changeFilesCommitMessage ( ctx , opts . Files )
2019-06-29 09:19:24 -06:00
}
2023-05-29 03:41:35 -06:00
if filesResponse , err := createOrUpdateFiles ( ctx , opts ) ; err != nil {
2020-05-31 14:59:34 -06:00
handleCreateOrUpdateFileError ( ctx , err )
2019-04-17 10:06:35 -06:00
} else {
2023-05-29 03:41:35 -06: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 08:36:53 -07:00
func UpdateFile ( ctx * context . APIContext ) {
2019-04-17 10:06:35 -06:00
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
// ---
// summary: Update 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 update
// type: string
// required: true
// - name: body
// in: body
2019-05-30 11:57:55 -06:00
// required: true
2019-04-17 10:06:35 -06:00
// schema:
// "$ref": "#/definitions/UpdateFileOptions"
// responses:
// "200":
// "$ref": "#/responses/FileResponse"
2020-05-31 14:59:34 -06:00
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
2023-09-21 17:43:29 -06:00
// "423":
// "$ref": "#/responses/repoArchivedError"
2021-01-26 08:36:53 -07:00
apiOpts := web . GetForm ( ctx ) . ( * api . UpdateFileOptions )
2020-05-31 14:59:34 -06:00
if ctx . Repo . Repository . IsEmpty {
ctx . Error ( http . StatusUnprocessableEntity , "RepoIsEmpty" , fmt . Errorf ( "repo is empty" ) )
2024-02-27 08:09:13 -07:00
return
2020-05-31 14:59:34 -06:00
}
2019-04-17 10:06:35 -06:00
2020-04-20 10:47:05 -06:00
if apiOpts . BranchName == "" {
apiOpts . BranchName = ctx . Repo . Repository . DefaultBranch
}
2023-07-18 12:14:47 -06:00
contentReader , err := base64Reader ( apiOpts . ContentBase64 )
if err != nil {
ctx . Error ( http . StatusUnprocessableEntity , "Invalid base64 content" , err )
return
}
2023-05-29 03:41:35 -06:00
opts := & files_service . ChangeRepoFilesOptions {
Files : [ ] * files_service . ChangeRepoFile {
{
2023-07-18 12:14:47 -06:00
Operation : "update" ,
ContentReader : contentReader ,
SHA : apiOpts . SHA ,
FromTreePath : apiOpts . FromPath ,
2024-06-18 16:32:45 -06:00
TreePath : ctx . PathParam ( "*" ) ,
2023-05-29 03:41:35 -06:00
} ,
} ,
Message : apiOpts . Message ,
OldBranch : apiOpts . BranchName ,
NewBranch : apiOpts . NewBranchName ,
2021-11-24 00:56:24 -07:00
Committer : & files_service . IdentityOptions {
2019-04-17 10:06:35 -06:00
Name : apiOpts . Committer . Name ,
Email : apiOpts . Committer . Email ,
} ,
2021-11-24 00:56:24 -07:00
Author : & files_service . IdentityOptions {
2019-04-17 10:06:35 -06:00
Name : apiOpts . Author . Name ,
Email : apiOpts . Author . Email ,
} ,
2021-11-24 00:56:24 -07:00
Dates : & files_service . CommitDateOptions {
2019-12-23 19:33:52 -07:00
Author : apiOpts . Dates . Author ,
Committer : apiOpts . Dates . Committer ,
} ,
2021-01-29 01:57:45 -07:00
Signoff : apiOpts . Signoff ,
2019-12-23 19:33:52 -07:00
}
if opts . Dates . Author . IsZero ( ) {
opts . Dates . Author = time . Now ( )
}
if opts . Dates . Committer . IsZero ( ) {
opts . Dates . Committer = time . Now ( )
2019-04-17 10:06:35 -06:00
}
2019-06-29 09:19:24 -06:00
if opts . Message == "" {
2023-05-29 03:41:35 -06:00
opts . Message = changeFilesCommitMessage ( ctx , opts . Files )
2019-06-29 09:19:24 -06:00
}
2023-05-29 03:41:35 -06:00
if filesResponse , err := createOrUpdateFiles ( ctx , opts ) ; err != nil {
2020-05-31 14:59:34 -06:00
handleCreateOrUpdateFileError ( ctx , err )
2019-04-17 10:06:35 -06:00
} else {
2023-05-29 03:41:35 -06:00
fileResponse := files_service . GetFileResponseFromFilesResponse ( filesResponse , 0 )
2019-04-17 10:06:35 -06:00
ctx . JSON ( http . StatusOK , fileResponse )
}
}
2020-05-31 14:59:34 -06:00
func handleCreateOrUpdateFileError ( ctx * context . APIContext , err error ) {
if models . IsErrUserCannotCommit ( err ) || models . IsErrFilePathProtected ( err ) {
ctx . Error ( http . StatusForbidden , "Access" , err )
return
}
2023-06-29 04:03:20 -06:00
if git_model . IsErrBranchAlreadyExists ( err ) || models . IsErrFilenameInvalid ( err ) || models . IsErrSHADoesNotMatch ( err ) ||
2020-05-31 14:59:34 -06:00
models . IsErrFilePathInvalid ( err ) || models . IsErrRepoFileAlreadyExists ( err ) {
ctx . Error ( http . StatusUnprocessableEntity , "Invalid" , err )
return
}
2023-06-29 04:03:20 -06:00
if git_model . IsErrBranchNotExist ( err ) || git . IsErrBranchNotExist ( err ) {
2020-06-07 11:30:58 -06:00
ctx . Error ( http . StatusNotFound , "BranchDoesNotExist" , err )
return
}
2020-05-31 14:59:34 -06:00
ctx . Error ( http . StatusInternalServerError , "UpdateFile" , err )
}
2019-04-17 10:06:35 -06:00
// Called from both CreateFile or UpdateFile to handle both
2023-05-29 03:41:35 -06:00
func createOrUpdateFiles ( ctx * context . APIContext , opts * files_service . ChangeRepoFilesOptions ) ( * api . FilesResponse , error ) {
2022-04-28 09:45:33 -06:00
if ! canWriteFiles ( ctx , opts . OldBranch ) {
2022-06-13 03:37:59 -06:00
return nil , repo_model . ErrUserDoesNotHaveAccessToRepo {
2022-03-22 01:03:22 -06:00
UserID : ctx . Doer . ID ,
2019-04-17 10:06:35 -06:00
RepoName : ctx . Repo . Repository . LowerName ,
}
}
2023-05-29 03:41:35 -06:00
return files_service . ChangeRepoFiles ( ctx , ctx . Repo . Repository , ctx . Doer , opts )
}
// 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 )
case "update" :
updateFiles = append ( updateFiles , file . TreePath )
case "delete" :
deleteFiles = append ( deleteFiles , file . TreePath )
}
}
message := ""
if len ( createFiles ) != 0 {
2024-02-14 14:48:45 -07:00
message += ctx . Locale . TrString ( "repo.editor.add" , strings . Join ( createFiles , ", " ) + "\n" )
2023-05-29 03:41:35 -06:00
}
if len ( updateFiles ) != 0 {
2024-02-14 14:48:45 -07:00
message += ctx . Locale . TrString ( "repo.editor.update" , strings . Join ( updateFiles , ", " ) + "\n" )
2023-05-29 03:41:35 -06:00
}
if len ( deleteFiles ) != 0 {
2024-02-14 14:48:45 -07:00
message += ctx . Locale . TrString ( "repo.editor.delete" , strings . Join ( deleteFiles , ", " ) )
2023-05-29 03:41:35 -06:00
}
return strings . Trim ( message , "\n" )
2019-04-17 10:06:35 -06:00
}
2022-01-10 02:32:37 -07:00
// DeleteFile Delete a file in a repository
2021-01-26 08:36:53 -07: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 11:57:55 -06:00
// required: true
2019-04-17 10:06:35 -06:00
// schema:
// "$ref": "#/definitions/DeleteFileOptions"
// responses:
// "200":
// "$ref": "#/responses/FileDeleteResponse"
2020-04-14 23:18:51 -06:00
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/error"
2023-09-21 17:43:29 -06:00
// "423":
// "$ref": "#/responses/repoArchivedError"
2019-12-20 10:07:12 -07:00
2021-01-26 08:36:53 -07:00
apiOpts := web . GetForm ( ctx ) . ( * api . DeleteFileOptions )
2022-04-28 09:45:33 -06:00
if ! canWriteFiles ( ctx , apiOpts . BranchName ) {
2022-06-13 03:37:59 -06:00
ctx . Error ( http . StatusForbidden , "DeleteFile" , repo_model . ErrUserDoesNotHaveAccessToRepo {
2022-03-22 01:03:22 -06:00
UserID : ctx . Doer . ID ,
2019-04-17 10:06:35 -06:00
RepoName : ctx . Repo . Repository . LowerName ,
} )
return
}
2020-04-20 10:47:05 -06:00
if apiOpts . BranchName == "" {
apiOpts . BranchName = ctx . Repo . Repository . DefaultBranch
}
2023-05-29 03:41:35 -06:00
opts := & files_service . ChangeRepoFilesOptions {
Files : [ ] * files_service . ChangeRepoFile {
{
Operation : "delete" ,
SHA : apiOpts . SHA ,
2024-06-18 16:32:45 -06:00
TreePath : ctx . PathParam ( "*" ) ,
2023-05-29 03:41:35 -06:00
} ,
} ,
2019-04-17 10:06:35 -06:00
Message : apiOpts . Message ,
OldBranch : apiOpts . BranchName ,
NewBranch : apiOpts . NewBranchName ,
2021-11-24 00:56:24 -07:00
Committer : & files_service . IdentityOptions {
2019-04-17 10:06:35 -06:00
Name : apiOpts . Committer . Name ,
Email : apiOpts . Committer . Email ,
} ,
2021-11-24 00:56:24 -07:00
Author : & files_service . IdentityOptions {
2019-04-17 10:06:35 -06:00
Name : apiOpts . Author . Name ,
Email : apiOpts . Author . Email ,
} ,
2021-11-24 00:56:24 -07:00
Dates : & files_service . CommitDateOptions {
2019-12-23 19:33:52 -07:00
Author : apiOpts . Dates . Author ,
Committer : apiOpts . Dates . Committer ,
} ,
2021-01-29 01:57:45 -07:00
Signoff : apiOpts . Signoff ,
2019-12-23 19:33:52 -07:00
}
if opts . Dates . Author . IsZero ( ) {
opts . Dates . Author = time . Now ( )
}
if opts . Dates . Committer . IsZero ( ) {
opts . Dates . Committer = time . Now ( )
2019-04-17 10:06:35 -06:00
}
2019-06-29 09:19:24 -06:00
if opts . Message == "" {
2023-05-29 03:41:35 -06:00
opts . Message = changeFilesCommitMessage ( ctx , opts . Files )
2019-06-29 09:19:24 -06:00
}
2023-05-29 03:41:35 -06:00
if filesResponse , err := files_service . ChangeRepoFiles ( ctx , ctx . Repo . Repository , ctx . Doer , opts ) ; err != nil {
2020-04-14 23:18:51 -06:00
if git . IsErrBranchNotExist ( err ) || models . IsErrRepoFileDoesNotExist ( err ) || git . IsErrNotExist ( err ) {
ctx . Error ( http . StatusNotFound , "DeleteFile" , err )
return
2023-06-29 04:03:20 -06:00
} else if git_model . IsErrBranchAlreadyExists ( err ) ||
2020-04-14 23:18:51 -06:00
models . IsErrFilenameInvalid ( err ) ||
models . IsErrSHADoesNotMatch ( err ) ||
models . IsErrCommitIDDoesNotMatch ( err ) ||
models . IsErrSHAOrCommitIDNotProvided ( err ) {
ctx . Error ( http . StatusBadRequest , "DeleteFile" , err )
return
} else if models . IsErrUserCannotCommit ( err ) {
ctx . Error ( http . StatusForbidden , "DeleteFile" , err )
return
}
2019-04-17 10:06:35 -06:00
ctx . Error ( http . StatusInternalServerError , "DeleteFile" , err )
} else {
2023-05-29 03:41:35 -06:00
fileResponse := files_service . GetFileResponseFromFilesResponse ( filesResponse , 0 )
2020-04-14 23:18:51 -06:00
ctx . JSON ( http . StatusOK , fileResponse ) // FIXME on APIv2: return http.StatusNoContent
2019-04-17 10:06:35 -06:00
}
}
2019-06-29 14:51:10 -06:00
// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
func GetContents ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
2019-04-17 10:06:35 -06:00
// ---
2019-06-29 14:51:10 -06:00
// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
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 14:51:10 -06: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
// description: "The name of the commit/branch/tag. Default the repository’ s default branch (usually master)"
// type: string
2019-06-29 14:51:10 -06:00
// required: false
2019-04-17 10:06:35 -06:00
// responses:
// "200":
2019-06-29 14:51:10 -06:00
// "$ref": "#/responses/ContentsResponse"
2020-04-14 23:18:51 -06:00
// "404":
// "$ref": "#/responses/notFound"
2019-04-17 10:06:35 -06:00
2020-04-24 10:20:22 -06:00
if ! canReadFiles ( ctx . Repo ) {
2022-06-13 03:37:59 -06:00
ctx . Error ( http . StatusInternalServerError , "GetContentsOrList" , repo_model . ErrUserDoesNotHaveAccessToRepo {
2022-03-22 01:03:22 -06:00
UserID : ctx . Doer . ID ,
2019-04-17 10:06:35 -06:00
RepoName : ctx . Repo . Repository . LowerName ,
} )
return
}
2024-06-18 16:32:45 -06:00
treePath := ctx . PathParam ( "*" )
2021-07-28 19:42:15 -06:00
ref := ctx . FormTrim ( "ref" )
2019-04-17 10:06:35 -06:00
2022-01-19 16:26:57 -07:00
if fileList , err := files_service . GetContentsOrList ( ctx , ctx . Repo . Repository , treePath , ref ) ; err != nil {
2020-04-14 23:18:51 -06:00
if git . IsErrNotExist ( err ) {
ctx . NotFound ( "GetContentsOrList" , err )
return
}
2019-06-29 14:51:10 -06:00
ctx . Error ( http . StatusInternalServerError , "GetContentsOrList" , err )
2019-04-17 10:06:35 -06:00
} else {
2019-06-29 14:51:10 -06:00
ctx . JSON ( http . StatusOK , fileList )
2019-04-17 10:06:35 -06:00
}
2016-08-30 17:18:40 -06:00
}
2019-06-29 14:51:10 -06:00
// GetContentsList Get the metadata of all the entries of the root dir
func GetContentsList ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
// ---
// summary: Gets the metadata of all the entries of the root dir
// 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 the repository’ s default branch (usually master)"
// type: string
// required: false
// responses:
// "200":
// "$ref": "#/responses/ContentsListResponse"
2020-04-14 23:18:51 -06:00
// "404":
// "$ref": "#/responses/notFound"
2019-06-29 14:51:10 -06:00
// same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
GetContents ( ctx )
}