ec8f162b33
(cherry picked from commit 0a3388f93f53f53cce79ebcc194d67979cbe61cc) (cherry picked from commit 7eba0a440a3138443d0b7461560f9f6f1c46b256) (cherry picked from commit eb9646c7ef0b1a9e73bf22806acbc2caeaee7ff3) (cherry picked from commit f1972578f5cb72ad9e1ef1ded9aa324b1d669864) Conflicts: (cherry picked from commit 7f96222fb4b4a773193a11d4ba76f449013c30ef) (cherry picked from commit e3c7c9fe7b5809cdb75f1eb629c82b27986f7e98) (cherry picked from commit 84fdead90242e68493efb96a7fee1e3b5fd417e9) (cherry picked from commit 85148e11961b86269b7cda30294a4cdc817787d0) (cherry picked from commit c0086bd70d39d9a1075fa624cdbfdf5ef26f7a2c) (cherry picked from commit d1e31ef31817e5e811f6bea7aee5582383d53b12) (cherry picked from commit 681d3ed5c43d45f802dc13c58c5b821da938374d) (cherry picked from commit 76a3001f5bc5a064054514c179ba14008952e914) (cherry picked from commit a55a9567d36f6b60557df3df3a0801c0fccec963) (cherry picked from commit aa7adc167d4e4ee7879b88fe799f2afca2fd5531) (cherry picked from commit d5354cb52c26ab0090388d019efe61770e7ec452) (cherry picked from commit 472c48999699420d15aaa7c61a42db8136221616) (cherry picked from commit dc816d065b715f2bfdc7fa68fa2d254f3d481b35) (cherry picked from commit 4795f9ea85eac21c70db1159914a7b7b035b7374) (cherry picked from commit ddd4ae53433d4adab823aa974c0f1c39d0394414) (cherry picked from commit 0e95f2a36b1c0f480f94fef85d341792c7a7b759) (cherry picked from commit 47a41a4e7b3c5d3cf247dafef17a64415069f171) (cherry picked from commit d4416fcd3ee0f1d74c2b03fbe40c15b83f80b56d) (cherry picked from commit f279d153b61677e2a766859c7a7688b0327e7c9a) (cherry picked from commit 959f908ffdc9dd2fb07aefdf7cb85ba34f9071a3) (cherry picked from commit 82df95310176a083abb9eaa51de9e9754d4f72c6) (cherry picked from commit 8f8c9fd9e3deca0af0f087fc44fcd271e8440a7b) (cherry picked from commit d4a0db7706f30a394192c6023072ad5185a7d600) (cherry picked from commit 44594d623942128072bd86a20b5b90603517005e) (cherry picked from commit 62b1de579ebfe326340b218cc9a980b705b8e6a2) (cherry picked from commit 507abee353f120247276fe9165f09976421f926d) (cherry picked from commit 8c36ac42c7091661b2f3fec70fc88a74043628fc) (cherry picked from commit 72f74067f4c781e1c4936f0da2653a35a6f1c82b) (cherry picked from commit 8e5a9e187b50a6eb8523c6dda4ce1a23c08a0b5a) (cherry picked from commit cff8f43c7e4ab86a468ea49f7b945412d9911151) (cherry picked from commit 493cc93e6d4b738a5b81d1a0db831bf9f430e9de) (cherry picked from commit c94352b4d460e631c223b45516e42765357bf67d) (cherry picked from commit 3f622c51a70200bf67d209d0cec78db2f25b65db) (cherry picked from commit 84c62ad447a94ebd5d1906e41c29efd3a0816c70) (cherry picked from commit ba186c8ee40e95cd4f40b6205d673dfe20528825) (cherry picked from commit 4238ef291dffb68b350aa2f410e934d52cb0a1eb) (cherry picked from commit 3ef1bd61b9a45271e69c1529875e84b379073318) (cherry picked from commit f304086bb693d3a0a58871ba8cb1354ba290695c) (cherry picked from commit 64a2edabcb7bc9e3961ea45f83cf6488e4cd99f2) (cherry picked from commit 6accea6ba7f2b0f98b6bef5b1483ee39ad68edba) (cherry picked from commit 2296e93a42939af90dee718bc8ff691009238c47) (cherry picked from commit 2bf0be3b1bff62923dd04e1a02956ced1eda97af) (cherry picked from commit fb4652b197ec64280689e6d065492f916f7e26f3) (cherry picked from commit 7d462cf353ef87007025bbd76aebef429be10562) (cherry picked from commit f5319660ddecea208163b5bcd37444322e571b51)
981 lines
27 KiB
Go
981 lines
27 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package repo
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/base64"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"path"
|
||
"strings"
|
||
"time"
|
||
|
||
"code.gitea.io/gitea/models"
|
||
git_model "code.gitea.io/gitea/models/git"
|
||
repo_model "code.gitea.io/gitea/models/repo"
|
||
"code.gitea.io/gitea/models/unit"
|
||
"code.gitea.io/gitea/modules/context"
|
||
"code.gitea.io/gitea/modules/git"
|
||
"code.gitea.io/gitea/modules/gitrepo"
|
||
"code.gitea.io/gitea/modules/httpcache"
|
||
"code.gitea.io/gitea/modules/lfs"
|
||
"code.gitea.io/gitea/modules/log"
|
||
"code.gitea.io/gitea/modules/setting"
|
||
"code.gitea.io/gitea/modules/storage"
|
||
api "code.gitea.io/gitea/modules/structs"
|
||
"code.gitea.io/gitea/modules/web"
|
||
"code.gitea.io/gitea/routers/common"
|
||
archiver_service "code.gitea.io/gitea/services/repository/archiver"
|
||
files_service "code.gitea.io/gitea/services/repository/files"
|
||
)
|
||
|
||
const (
|
||
giteaObjectTypeHeader = "X-Gitea-Object-Type"
|
||
forgejoObjectTypeHeader = "X-Forgejo-Object-Type"
|
||
)
|
||
|
||
// GetRawFile get a file by path on a repository
|
||
func GetRawFile(ctx *context.APIContext) {
|
||
// swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
|
||
// ---
|
||
// summary: Get a file from 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 the file to get
|
||
// 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:
|
||
// description: Returns raw file content.
|
||
// "404":
|
||
// "$ref": "#/responses/notFound"
|
||
|
||
if ctx.Repo.Repository.IsEmpty {
|
||
ctx.NotFound()
|
||
return
|
||
}
|
||
|
||
blob, entry, lastModified := getBlobForEntry(ctx)
|
||
if ctx.Written() {
|
||
return
|
||
}
|
||
|
||
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
|
||
ctx.RespHeader().Set(forgejoObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
|
||
|
||
if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
|
||
ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
|
||
}
|
||
}
|
||
|
||
// 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
|
||
// 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 the file to get
|
||
// 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:
|
||
// description: Returns raw file content.
|
||
// "404":
|
||
// "$ref": "#/responses/notFound"
|
||
|
||
if ctx.Repo.Repository.IsEmpty {
|
||
ctx.NotFound()
|
||
return
|
||
}
|
||
|
||
blob, entry, lastModified := getBlobForEntry(ctx)
|
||
if ctx.Written() {
|
||
return
|
||
}
|
||
|
||
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
|
||
ctx.RespHeader().Set(forgejoObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
|
||
|
||
// 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!
|
||
if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
|
||
ctx.ServerError("ServeBlob", err)
|
||
}
|
||
return
|
||
}
|
||
|
||
// OK, now the blob is known to have at most 1024 bytes we can simply read this in in one go (This saves reading it twice)
|
||
dataRc, err := blob.DataAsync()
|
||
if err != nil {
|
||
ctx.ServerError("DataAsync", err)
|
||
return
|
||
}
|
||
|
||
// FIXME: code from #19689, what if the file is large ... OOM ...
|
||
buf, err := io.ReadAll(dataRc)
|
||
if err != nil {
|
||
_ = dataRc.Close()
|
||
ctx.ServerError("DataAsync", err)
|
||
return
|
||
}
|
||
|
||
if err := dataRc.Close(); err != nil {
|
||
log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err)
|
||
}
|
||
|
||
// Check if the blob represents a pointer
|
||
pointer, _ := lfs.ReadPointer(bytes.NewReader(buf))
|
||
|
||
// if it's not a pointer, just serve the data directly
|
||
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!
|
||
common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
|
||
return
|
||
}
|
||
|
||
// Now check if there is a MetaObject for this pointer
|
||
meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
|
||
|
||
// If there isn't one, just serve the data directly
|
||
if err == git_model.ErrLFSObjectNotExist {
|
||
// Handle caching for the blob SHA (not the LFS object OID)
|
||
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
||
return
|
||
}
|
||
|
||
common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
|
||
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
|
||
}
|
||
|
||
if setting.LFS.Storage.MinioConfig.ServeDirect {
|
||
// If we have a signed url (S3, object storage), redirect to this directly.
|
||
u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name())
|
||
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()
|
||
|
||
common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
|
||
}
|
||
|
||
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
|
||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
||
if err != nil {
|
||
if git.IsErrNotExist(err) {
|
||
ctx.NotFound()
|
||
} else {
|
||
ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath", err)
|
||
}
|
||
return nil, nil, nil
|
||
}
|
||
|
||
if entry.IsDir() || entry.IsSubModule() {
|
||
ctx.NotFound("getBlobForEntry", nil)
|
||
return nil, nil, nil
|
||
}
|
||
|
||
info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
|
||
if err != nil {
|
||
ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err)
|
||
return nil, nil, nil
|
||
}
|
||
|
||
if len(info) == 1 {
|
||
// Not Modified
|
||
lastModified = &info[0].Commit.Committer.When
|
||
}
|
||
blob = entry.Blob()
|
||
|
||
return blob, entry, lastModified
|
||
}
|
||
|
||
// GetArchive get archive of a repository
|
||
func GetArchive(ctx *context.APIContext) {
|
||
// swagger:operation GET /repos/{owner}/{repo}/archive/{archive} repository repoGetArchive
|
||
// ---
|
||
// 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
|
||
// description: the git reference for download with attached archive format (e.g. master.zip)
|
||
// type: string
|
||
// required: true
|
||
// responses:
|
||
// 200:
|
||
// description: success
|
||
// "404":
|
||
// "$ref": "#/responses/notFound"
|
||
|
||
if ctx.Repo.GitRepo == nil {
|
||
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
|
||
if err != nil {
|
||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||
return
|
||
}
|
||
ctx.Repo.GitRepo = gitRepo
|
||
defer gitRepo.Close()
|
||
}
|
||
|
||
archiveDownload(ctx)
|
||
}
|
||
|
||
func archiveDownload(ctx *context.APIContext) {
|
||
uri := ctx.Params("*")
|
||
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
|
||
|
||
rPath := archiver.RelativePath()
|
||
if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
|
||
// If we have a signed url (S3, object storage), redirect to this directly.
|
||
u, err := storage.RepoArchives.URL(rPath, downloadName)
|
||
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()
|
||
|
||
ctx.ServeContent(fr, &context.ServeHeaderOptions{
|
||
Filename: downloadName,
|
||
LastModified: archiver.CreatedUnix.AsLocalTime(),
|
||
})
|
||
}
|
||
|
||
// GetEditorconfig get editor config of a repository
|
||
func GetEditorconfig(ctx *context.APIContext) {
|
||
// 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
|
||
// - 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:
|
||
// description: success
|
||
// "404":
|
||
// "$ref": "#/responses/notFound"
|
||
|
||
ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
|
||
if err != nil {
|
||
if git.IsErrNotExist(err) {
|
||
ctx.NotFound(err)
|
||
} else {
|
||
ctx.Error(http.StatusInternalServerError, "GetEditorconfig", err)
|
||
}
|
||
return
|
||
}
|
||
|
||
fileName := ctx.Params("filename")
|
||
def, err := ec.GetDefinitionForFilename(fileName)
|
||
if def == nil {
|
||
ctx.NotFound(err)
|
||
return
|
||
}
|
||
ctx.JSON(http.StatusOK, def)
|
||
}
|
||
|
||
// canWriteFiles returns true if repository is editable and user has proper access level.
|
||
func canWriteFiles(ctx *context.APIContext, branch string) bool {
|
||
return ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, branch) &&
|
||
!ctx.Repo.Repository.IsMirror &&
|
||
!ctx.Repo.Repository.IsArchived
|
||
}
|
||
|
||
// canReadFiles returns true if repository is readable and user has proper access level.
|
||
func canReadFiles(r *context.Repository) bool {
|
||
return r.Permission.CanRead(unit.TypeCode)
|
||
}
|
||
|
||
func base64Reader(s string) (io.Reader, error) {
|
||
b, err := base64.StdEncoding.DecodeString(s)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return bytes.NewReader(b), nil
|
||
}
|
||
|
||
// ChangeFiles handles API call for modifying multiple files
|
||
func ChangeFiles(ctx *context.APIContext) {
|
||
// swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
|
||
// ---
|
||
// summary: Modify multiple files 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: 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"
|
||
// "423":
|
||
// "$ref": "#/responses/repoArchivedError"
|
||
|
||
apiOpts := web.GetForm(ctx).(*api.ChangeFilesOptions)
|
||
|
||
if apiOpts.BranchName == "" {
|
||
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
||
}
|
||
|
||
var files []*files_service.ChangeRepoFile
|
||
for _, file := range apiOpts.Files {
|
||
contentReader, err := base64Reader(file.ContentBase64)
|
||
if err != nil {
|
||
ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err)
|
||
return
|
||
}
|
||
changeRepoFile := &files_service.ChangeRepoFile{
|
||
Operation: file.Operation,
|
||
TreePath: file.Path,
|
||
FromTreePath: file.FromPath,
|
||
ContentReader: contentReader,
|
||
SHA: file.SHA,
|
||
}
|
||
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)
|
||
}
|
||
}
|
||
|
||
// CreateFile handles API call for creating a file
|
||
func CreateFile(ctx *context.APIContext) {
|
||
// 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
|
||
// required: true
|
||
// schema:
|
||
// "$ref": "#/definitions/CreateFileOptions"
|
||
// responses:
|
||
// "201":
|
||
// "$ref": "#/responses/FileResponse"
|
||
// "403":
|
||
// "$ref": "#/responses/error"
|
||
// "404":
|
||
// "$ref": "#/responses/notFound"
|
||
// "422":
|
||
// "$ref": "#/responses/error"
|
||
// "423":
|
||
// "$ref": "#/responses/repoArchivedError"
|
||
|
||
apiOpts := web.GetForm(ctx).(*api.CreateFileOptions)
|
||
|
||
if apiOpts.BranchName == "" {
|
||
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
||
}
|
||
|
||
contentReader, err := base64Reader(apiOpts.ContentBase64)
|
||
if err != nil {
|
||
ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err)
|
||
return
|
||
}
|
||
|
||
opts := &files_service.ChangeRepoFilesOptions{
|
||
Files: []*files_service.ChangeRepoFile{
|
||
{
|
||
Operation: "create",
|
||
TreePath: ctx.Params("*"),
|
||
ContentReader: contentReader,
|
||
},
|
||
},
|
||
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, opts.Files)
|
||
}
|
||
|
||
if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
|
||
handleCreateOrUpdateFileError(ctx, err)
|
||
} else {
|
||
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||
ctx.JSON(http.StatusCreated, fileResponse)
|
||
}
|
||
}
|
||
|
||
// UpdateFile handles API call for updating a file
|
||
func UpdateFile(ctx *context.APIContext) {
|
||
// 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
|
||
// required: true
|
||
// schema:
|
||
// "$ref": "#/definitions/UpdateFileOptions"
|
||
// responses:
|
||
// "200":
|
||
// "$ref": "#/responses/FileResponse"
|
||
// "403":
|
||
// "$ref": "#/responses/error"
|
||
// "404":
|
||
// "$ref": "#/responses/notFound"
|
||
// "422":
|
||
// "$ref": "#/responses/error"
|
||
// "423":
|
||
// "$ref": "#/responses/repoArchivedError"
|
||
apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions)
|
||
if ctx.Repo.Repository.IsEmpty {
|
||
ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
|
||
}
|
||
|
||
if apiOpts.BranchName == "" {
|
||
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
||
}
|
||
|
||
contentReader, err := base64Reader(apiOpts.ContentBase64)
|
||
if err != nil {
|
||
ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err)
|
||
return
|
||
}
|
||
|
||
opts := &files_service.ChangeRepoFilesOptions{
|
||
Files: []*files_service.ChangeRepoFile{
|
||
{
|
||
Operation: "update",
|
||
ContentReader: contentReader,
|
||
SHA: apiOpts.SHA,
|
||
FromTreePath: apiOpts.FromPath,
|
||
TreePath: ctx.Params("*"),
|
||
},
|
||
},
|
||
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, opts.Files)
|
||
}
|
||
|
||
if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
|
||
handleCreateOrUpdateFileError(ctx, err)
|
||
} else {
|
||
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||
ctx.JSON(http.StatusOK, fileResponse)
|
||
}
|
||
}
|
||
|
||
func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
|
||
if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) {
|
||
ctx.Error(http.StatusForbidden, "Access", err)
|
||
return
|
||
}
|
||
if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) ||
|
||
models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) {
|
||
ctx.Error(http.StatusUnprocessableEntity, "Invalid", err)
|
||
return
|
||
}
|
||
if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) {
|
||
ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err)
|
||
return
|
||
}
|
||
|
||
ctx.Error(http.StatusInternalServerError, "UpdateFile", err)
|
||
}
|
||
|
||
// Called from both CreateFile or UpdateFile to handle both
|
||
func createOrUpdateFiles(ctx *context.APIContext, opts *files_service.ChangeRepoFilesOptions) (*api.FilesResponse, error) {
|
||
if !canWriteFiles(ctx, opts.OldBranch) {
|
||
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
|
||
UserID: ctx.Doer.ID,
|
||
RepoName: ctx.Repo.Repository.LowerName,
|
||
}
|
||
}
|
||
|
||
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 {
|
||
message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
|
||
}
|
||
if len(updateFiles) != 0 {
|
||
message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
|
||
}
|
||
if len(deleteFiles) != 0 {
|
||
message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", "))
|
||
}
|
||
return strings.Trim(message, "\n")
|
||
}
|
||
|
||
// DeleteFile Delete a file in a repository
|
||
func DeleteFile(ctx *context.APIContext) {
|
||
// 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
|
||
// required: true
|
||
// schema:
|
||
// "$ref": "#/definitions/DeleteFileOptions"
|
||
// responses:
|
||
// "200":
|
||
// "$ref": "#/responses/FileDeleteResponse"
|
||
// "400":
|
||
// "$ref": "#/responses/error"
|
||
// "403":
|
||
// "$ref": "#/responses/error"
|
||
// "404":
|
||
// "$ref": "#/responses/error"
|
||
// "423":
|
||
// "$ref": "#/responses/repoArchivedError"
|
||
|
||
apiOpts := web.GetForm(ctx).(*api.DeleteFileOptions)
|
||
if !canWriteFiles(ctx, apiOpts.BranchName) {
|
||
ctx.Error(http.StatusForbidden, "DeleteFile", repo_model.ErrUserDoesNotHaveAccessToRepo{
|
||
UserID: ctx.Doer.ID,
|
||
RepoName: ctx.Repo.Repository.LowerName,
|
||
})
|
||
return
|
||
}
|
||
|
||
if apiOpts.BranchName == "" {
|
||
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
||
}
|
||
|
||
opts := &files_service.ChangeRepoFilesOptions{
|
||
Files: []*files_service.ChangeRepoFile{
|
||
{
|
||
Operation: "delete",
|
||
SHA: apiOpts.SHA,
|
||
TreePath: ctx.Params("*"),
|
||
},
|
||
},
|
||
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, opts.Files)
|
||
}
|
||
|
||
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
|
||
if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
|
||
ctx.Error(http.StatusNotFound, "DeleteFile", err)
|
||
return
|
||
} else if git_model.IsErrBranchAlreadyExists(err) ||
|
||
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
|
||
}
|
||
ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
|
||
} else {
|
||
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
|
||
}
|
||
}
|
||
|
||
// 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
|
||
// ---
|
||
// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a 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: filepath
|
||
// in: path
|
||
// description: path of the dir, file, symlink or submodule in 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/ContentsResponse"
|
||
// "404":
|
||
// "$ref": "#/responses/notFound"
|
||
|
||
if !canReadFiles(ctx.Repo) {
|
||
ctx.Error(http.StatusInternalServerError, "GetContentsOrList", repo_model.ErrUserDoesNotHaveAccessToRepo{
|
||
UserID: ctx.Doer.ID,
|
||
RepoName: ctx.Repo.Repository.LowerName,
|
||
})
|
||
return
|
||
}
|
||
|
||
treePath := ctx.Params("*")
|
||
ref := ctx.FormTrim("ref")
|
||
|
||
if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref); err != nil {
|
||
if git.IsErrNotExist(err) {
|
||
ctx.NotFound("GetContentsOrList", err)
|
||
return
|
||
}
|
||
ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)
|
||
} else {
|
||
ctx.JSON(http.StatusOK, fileList)
|
||
}
|
||
}
|
||
|
||
// 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"
|
||
// "404":
|
||
// "$ref": "#/responses/notFound"
|
||
|
||
// same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
|
||
GetContents(ctx)
|
||
}
|