|
|
|
|
@@ -12,6 +12,7 @@ import (
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"path"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"code.gitea.io/gitea/models"
|
|
|
|
|
@@ -407,6 +408,96 @@ func canReadFiles(r *context.Repository) bool {
|
|
|
|
|
return r.Permission.CanRead(unit.TypeCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ChangeFiles handles API call for creating or updating multiple files
|
|
|
|
|
func ChangeFiles(ctx *context.APIContext) {
|
|
|
|
|
// swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
|
|
|
|
|
// ---
|
|
|
|
|
// summary: Create or update 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"
|
|
|
|
|
|
|
|
|
|
apiOpts := web.GetForm(ctx).(*api.ChangeFilesOptions)
|
|
|
|
|
|
|
|
|
|
if apiOpts.BranchName == "" {
|
|
|
|
|
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
files := []*files_service.ChangeRepoFile{}
|
|
|
|
|
for _, file := range apiOpts.Files {
|
|
|
|
|
changeRepoFile := &files_service.ChangeRepoFile{
|
|
|
|
|
Operation: file.Operation,
|
|
|
|
|
TreePath: file.Path,
|
|
|
|
|
FromTreePath: file.FromPath,
|
|
|
|
|
Content: file.Content,
|
|
|
|
|
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
|
|
|
|
|
@@ -453,11 +544,15 @@ func CreateFile(ctx *context.APIContext) {
|
|
|
|
|
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opts := &files_service.UpdateRepoFileOptions{
|
|
|
|
|
Content: apiOpts.Content,
|
|
|
|
|
IsNewFile: true,
|
|
|
|
|
opts := &files_service.ChangeRepoFilesOptions{
|
|
|
|
|
Files: []*files_service.ChangeRepoFile{
|
|
|
|
|
{
|
|
|
|
|
Operation: "create",
|
|
|
|
|
TreePath: ctx.Params("*"),
|
|
|
|
|
Content: apiOpts.Content,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Message: apiOpts.Message,
|
|
|
|
|
TreePath: ctx.Params("*"),
|
|
|
|
|
OldBranch: apiOpts.BranchName,
|
|
|
|
|
NewBranch: apiOpts.NewBranchName,
|
|
|
|
|
Committer: &files_service.IdentityOptions{
|
|
|
|
|
@@ -482,12 +577,13 @@ func CreateFile(ctx *context.APIContext) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.Message == "" {
|
|
|
|
|
opts.Message = ctx.Tr("repo.editor.add", opts.TreePath)
|
|
|
|
|
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
|
|
|
|
|
if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
|
|
|
|
|
handleCreateOrUpdateFileError(ctx, err)
|
|
|
|
|
} else {
|
|
|
|
|
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
|
|
|
|
ctx.JSON(http.StatusCreated, fileResponse)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -540,15 +636,19 @@ func UpdateFile(ctx *context.APIContext) {
|
|
|
|
|
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opts := &files_service.UpdateRepoFileOptions{
|
|
|
|
|
Content: apiOpts.Content,
|
|
|
|
|
SHA: apiOpts.SHA,
|
|
|
|
|
IsNewFile: false,
|
|
|
|
|
Message: apiOpts.Message,
|
|
|
|
|
FromTreePath: apiOpts.FromPath,
|
|
|
|
|
TreePath: ctx.Params("*"),
|
|
|
|
|
OldBranch: apiOpts.BranchName,
|
|
|
|
|
NewBranch: apiOpts.NewBranchName,
|
|
|
|
|
opts := &files_service.ChangeRepoFilesOptions{
|
|
|
|
|
Files: []*files_service.ChangeRepoFile{
|
|
|
|
|
{
|
|
|
|
|
Operation: "update",
|
|
|
|
|
Content: apiOpts.Content,
|
|
|
|
|
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,
|
|
|
|
|
@@ -571,12 +671,13 @@ func UpdateFile(ctx *context.APIContext) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.Message == "" {
|
|
|
|
|
opts.Message = ctx.Tr("repo.editor.update", opts.TreePath)
|
|
|
|
|
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
|
|
|
|
|
if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
|
|
|
|
|
handleCreateOrUpdateFileError(ctx, err)
|
|
|
|
|
} else {
|
|
|
|
|
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
|
|
|
|
ctx.JSON(http.StatusOK, fileResponse)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -600,7 +701,7 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Called from both CreateFile or UpdateFile to handle both
|
|
|
|
|
func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoFileOptions) (*api.FileResponse, error) {
|
|
|
|
|
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,
|
|
|
|
|
@@ -608,13 +709,45 @@ func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoF
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
content, err := base64.StdEncoding.DecodeString(opts.Content)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
for _, file := range opts.Files {
|
|
|
|
|
content, err := base64.StdEncoding.DecodeString(file.Content)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
file.Content = string(content)
|
|
|
|
|
}
|
|
|
|
|
opts.Content = string(content)
|
|
|
|
|
|
|
|
|
|
return files_service.CreateOrUpdateRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts)
|
|
|
|
|
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
|
|
|
|
|
@@ -670,12 +803,17 @@ func DeleteFile(ctx *context.APIContext) {
|
|
|
|
|
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opts := &files_service.DeleteRepoFileOptions{
|
|
|
|
|
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,
|
|
|
|
|
SHA: apiOpts.SHA,
|
|
|
|
|
TreePath: ctx.Params("*"),
|
|
|
|
|
Committer: &files_service.IdentityOptions{
|
|
|
|
|
Name: apiOpts.Committer.Name,
|
|
|
|
|
Email: apiOpts.Committer.Email,
|
|
|
|
|
@@ -698,10 +836,10 @@ func DeleteFile(ctx *context.APIContext) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.Message == "" {
|
|
|
|
|
opts.Message = ctx.Tr("repo.editor.delete", opts.TreePath)
|
|
|
|
|
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if fileResponse, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
|
|
|
|
|
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
|
|
|
|
|
@@ -718,6 +856,7 @@ func DeleteFile(ctx *context.APIContext) {
|
|
|
|
|
}
|
|
|
|
|
ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
|
|
|
|
|
} else {
|
|
|
|
|
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
|
|
|
|
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|