API endpoint for changing/creating/deleting multiple files (#24887)
This PR creates an API endpoint for creating/updating/deleting multiple files in one API call similar to the solution provided by [GitLab](https://docs.gitlab.com/ee/api/commits.html#create-a-commit-with-multiple-files-and-actions). To archive this, the CreateOrUpdateRepoFile and DeleteRepoFIle functions in files service are unified into one function supporting multiple files and actions. Resolves #14619
This commit is contained in:
@@ -64,6 +64,35 @@ func (o *UpdateFileOptions) Branch() string {
|
|||||||
return o.FileOptions.BranchName
|
return o.FileOptions.BranchName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChangeFileOperation for creating, updating or deleting a file
|
||||||
|
type ChangeFileOperation struct {
|
||||||
|
// indicates what to do with the file
|
||||||
|
// required: true
|
||||||
|
// enum: create,update,delete
|
||||||
|
Operation string `json:"operation" binding:"Required"`
|
||||||
|
// path to the existing or new file
|
||||||
|
Path string `json:"path" binding:"MaxSize(500)"`
|
||||||
|
// content must be base64 encoded
|
||||||
|
// required: true
|
||||||
|
Content string `json:"content"`
|
||||||
|
// sha is the SHA for the file that already exists, required for update, delete
|
||||||
|
SHA string `json:"sha"`
|
||||||
|
// old path of the file to move
|
||||||
|
FromPath string `json:"from_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeFilesOptions options for creating, updating or deleting multiple files
|
||||||
|
// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
|
||||||
|
type ChangeFilesOptions struct {
|
||||||
|
FileOptions
|
||||||
|
Files []*ChangeFileOperation `json:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branch returns branch name
|
||||||
|
func (o *ChangeFilesOptions) Branch() string {
|
||||||
|
return o.FileOptions.BranchName
|
||||||
|
}
|
||||||
|
|
||||||
// FileOptionInterface provides a unified interface for the different file options
|
// FileOptionInterface provides a unified interface for the different file options
|
||||||
type FileOptionInterface interface {
|
type FileOptionInterface interface {
|
||||||
Branch() string
|
Branch() string
|
||||||
@@ -126,6 +155,13 @@ type FileResponse struct {
|
|||||||
Verification *PayloadCommitVerification `json:"verification"`
|
Verification *PayloadCommitVerification `json:"verification"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilesResponse contains information about multiple files from a repo
|
||||||
|
type FilesResponse struct {
|
||||||
|
Files []*ContentsResponse `json:"files"`
|
||||||
|
Commit *FileCommitResponse `json:"commit"`
|
||||||
|
Verification *PayloadCommitVerification `json:"verification"`
|
||||||
|
}
|
||||||
|
|
||||||
// FileDeleteResponse contains information about a repo's file that was deleted
|
// FileDeleteResponse contains information about a repo's file that was deleted
|
||||||
type FileDeleteResponse struct {
|
type FileDeleteResponse struct {
|
||||||
Content interface{} `json:"content"` // to be set to nil
|
Content interface{} `json:"content"` // to be set to nil
|
||||||
|
|||||||
@@ -1173,6 +1173,7 @@ func Routes(ctx gocontext.Context) *web.Route {
|
|||||||
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(auth_model.AccessTokenScopeRepo), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
|
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(auth_model.AccessTokenScopeRepo), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
|
||||||
m.Group("/contents", func() {
|
m.Group("/contents", func() {
|
||||||
m.Get("", repo.GetContentsList)
|
m.Get("", repo.GetContentsList)
|
||||||
|
m.Post("", reqToken(auth_model.AccessTokenScopeRepo), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, repo.ChangeFiles)
|
||||||
m.Get("/*", repo.GetContents)
|
m.Get("/*", repo.GetContents)
|
||||||
m.Group("/*", func() {
|
m.Group("/*", func() {
|
||||||
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
|
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
|
||||||
|
|||||||
+159
-20
@@ -12,6 +12,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
@@ -407,6 +408,96 @@ func canReadFiles(r *context.Repository) bool {
|
|||||||
return r.Permission.CanRead(unit.TypeCode)
|
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
|
// CreateFile handles API call for creating a file
|
||||||
func CreateFile(ctx *context.APIContext) {
|
func CreateFile(ctx *context.APIContext) {
|
||||||
// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
|
// 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
|
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &files_service.UpdateRepoFileOptions{
|
opts := &files_service.ChangeRepoFilesOptions{
|
||||||
Content: apiOpts.Content,
|
Files: []*files_service.ChangeRepoFile{
|
||||||
IsNewFile: true,
|
{
|
||||||
Message: apiOpts.Message,
|
Operation: "create",
|
||||||
TreePath: ctx.Params("*"),
|
TreePath: ctx.Params("*"),
|
||||||
|
Content: apiOpts.Content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Message: apiOpts.Message,
|
||||||
OldBranch: apiOpts.BranchName,
|
OldBranch: apiOpts.BranchName,
|
||||||
NewBranch: apiOpts.NewBranchName,
|
NewBranch: apiOpts.NewBranchName,
|
||||||
Committer: &files_service.IdentityOptions{
|
Committer: &files_service.IdentityOptions{
|
||||||
@@ -482,12 +577,13 @@ func CreateFile(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.Message == "" {
|
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)
|
handleCreateOrUpdateFileError(ctx, err)
|
||||||
} else {
|
} else {
|
||||||
|
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||||||
ctx.JSON(http.StatusCreated, fileResponse)
|
ctx.JSON(http.StatusCreated, fileResponse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -540,13 +636,17 @@ func UpdateFile(ctx *context.APIContext) {
|
|||||||
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &files_service.UpdateRepoFileOptions{
|
opts := &files_service.ChangeRepoFilesOptions{
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "update",
|
||||||
Content: apiOpts.Content,
|
Content: apiOpts.Content,
|
||||||
SHA: apiOpts.SHA,
|
SHA: apiOpts.SHA,
|
||||||
IsNewFile: false,
|
|
||||||
Message: apiOpts.Message,
|
|
||||||
FromTreePath: apiOpts.FromPath,
|
FromTreePath: apiOpts.FromPath,
|
||||||
TreePath: ctx.Params("*"),
|
TreePath: ctx.Params("*"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Message: apiOpts.Message,
|
||||||
OldBranch: apiOpts.BranchName,
|
OldBranch: apiOpts.BranchName,
|
||||||
NewBranch: apiOpts.NewBranchName,
|
NewBranch: apiOpts.NewBranchName,
|
||||||
Committer: &files_service.IdentityOptions{
|
Committer: &files_service.IdentityOptions{
|
||||||
@@ -571,12 +671,13 @@ func UpdateFile(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.Message == "" {
|
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)
|
handleCreateOrUpdateFileError(ctx, err)
|
||||||
} else {
|
} else {
|
||||||
|
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||||||
ctx.JSON(http.StatusOK, fileResponse)
|
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
|
// 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) {
|
if !canWriteFiles(ctx, opts.OldBranch) {
|
||||||
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
|
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
|
||||||
UserID: ctx.Doer.ID,
|
UserID: ctx.Doer.ID,
|
||||||
@@ -608,13 +709,45 @@ func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := base64.StdEncoding.DecodeString(opts.Content)
|
for _, file := range opts.Files {
|
||||||
|
content, err := base64.StdEncoding.DecodeString(file.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
opts.Content = string(content)
|
file.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
|
// DeleteFile Delete a file in a repository
|
||||||
@@ -670,12 +803,17 @@ func DeleteFile(ctx *context.APIContext) {
|
|||||||
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
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,
|
Message: apiOpts.Message,
|
||||||
OldBranch: apiOpts.BranchName,
|
OldBranch: apiOpts.BranchName,
|
||||||
NewBranch: apiOpts.NewBranchName,
|
NewBranch: apiOpts.NewBranchName,
|
||||||
SHA: apiOpts.SHA,
|
|
||||||
TreePath: ctx.Params("*"),
|
|
||||||
Committer: &files_service.IdentityOptions{
|
Committer: &files_service.IdentityOptions{
|
||||||
Name: apiOpts.Committer.Name,
|
Name: apiOpts.Committer.Name,
|
||||||
Email: apiOpts.Committer.Email,
|
Email: apiOpts.Committer.Email,
|
||||||
@@ -698,10 +836,10 @@ func DeleteFile(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.Message == "" {
|
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) {
|
if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
|
||||||
ctx.Error(http.StatusNotFound, "DeleteFile", err)
|
ctx.Error(http.StatusNotFound, "DeleteFile", err)
|
||||||
return
|
return
|
||||||
@@ -718,6 +856,7 @@ func DeleteFile(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
|
ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
|
||||||
} else {
|
} else {
|
||||||
|
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||||||
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
|
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ type swaggerParameterBodies struct {
|
|||||||
// in:body
|
// in:body
|
||||||
EditAttachmentOptions api.EditAttachmentOptions
|
EditAttachmentOptions api.EditAttachmentOptions
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
ChangeFilesOptions api.ChangeFilesOptions
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
CreateFileOptions api.CreateFileOptions
|
CreateFileOptions api.CreateFileOptions
|
||||||
|
|
||||||
|
|||||||
@@ -296,6 +296,13 @@ type swaggerFileResponse struct {
|
|||||||
Body api.FileResponse `json:"body"`
|
Body api.FileResponse `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilesResponse
|
||||||
|
// swagger:response FilesResponse
|
||||||
|
type swaggerFilesResponse struct {
|
||||||
|
// in: body
|
||||||
|
Body api.FilesResponse `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
// ContentsResponse
|
// ContentsResponse
|
||||||
// swagger:response ContentsResponse
|
// swagger:response ContentsResponse
|
||||||
type swaggerContentsResponse struct {
|
type swaggerContentsResponse struct {
|
||||||
|
|||||||
@@ -272,18 +272,27 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
|
|||||||
message += "\n\n" + form.CommitMessage
|
message += "\n\n" + form.CommitMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := files_service.CreateOrUpdateRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UpdateRepoFileOptions{
|
operation := "update"
|
||||||
|
if isNewFile {
|
||||||
|
operation = "create"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||||
LastCommitID: form.LastCommit,
|
LastCommitID: form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: ctx.Repo.BranchName,
|
||||||
NewBranch: branchName,
|
NewBranch: branchName,
|
||||||
|
Message: message,
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: operation,
|
||||||
FromTreePath: ctx.Repo.TreePath,
|
FromTreePath: ctx.Repo.TreePath,
|
||||||
TreePath: form.TreePath,
|
TreePath: form.TreePath,
|
||||||
Message: message,
|
|
||||||
Content: strings.ReplaceAll(form.Content, "\r", ""),
|
Content: strings.ReplaceAll(form.Content, "\r", ""),
|
||||||
IsNewFile: isNewFile,
|
},
|
||||||
|
},
|
||||||
Signoff: form.Signoff,
|
Signoff: form.Signoff,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
// This is where we handle all the errors thrown by files_service.CreateOrUpdateRepoFile
|
// This is where we handle all the errors thrown by files_service.ChangeRepoFiles
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
|
||||||
} else if git_model.IsErrLFSFileLocked(err) {
|
} else if git_model.IsErrLFSFileLocked(err) {
|
||||||
@@ -478,11 +487,16 @@ func DeleteFilePost(ctx *context.Context) {
|
|||||||
message += "\n\n" + form.CommitMessage
|
message += "\n\n" + form.CommitMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.DeleteRepoFileOptions{
|
if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||||
LastCommitID: form.LastCommit,
|
LastCommitID: form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: ctx.Repo.BranchName,
|
||||||
NewBranch: branchName,
|
NewBranch: branchName,
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "delete",
|
||||||
TreePath: ctx.Repo.TreePath,
|
TreePath: ctx.Repo.TreePath,
|
||||||
|
},
|
||||||
|
},
|
||||||
Message: message,
|
Message: message,
|
||||||
Signoff: form.Signoff,
|
Signoff: form.Signoff,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|||||||
@@ -1,204 +0,0 @@
|
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package files
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DeleteRepoFileOptions holds the repository delete file options
|
|
||||||
type DeleteRepoFileOptions struct {
|
|
||||||
LastCommitID string
|
|
||||||
OldBranch string
|
|
||||||
NewBranch string
|
|
||||||
TreePath string
|
|
||||||
Message string
|
|
||||||
SHA string
|
|
||||||
Author *IdentityOptions
|
|
||||||
Committer *IdentityOptions
|
|
||||||
Dates *CommitDateOptions
|
|
||||||
Signoff bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRepoFile deletes a file in the given repository
|
|
||||||
func DeleteRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *DeleteRepoFileOptions) (*api.FileResponse, error) {
|
|
||||||
// If no branch name is set, assume the repo's default branch
|
|
||||||
if opts.OldBranch == "" {
|
|
||||||
opts.OldBranch = repo.DefaultBranch
|
|
||||||
}
|
|
||||||
if opts.NewBranch == "" {
|
|
||||||
opts.NewBranch = opts.OldBranch
|
|
||||||
}
|
|
||||||
|
|
||||||
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer closer.Close()
|
|
||||||
|
|
||||||
// oldBranch must exist for this operation
|
|
||||||
if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// A NewBranch can be specified for the file to be created/updated in a new branch.
|
|
||||||
// Check to make sure the branch does not already exist, otherwise we can't proceed.
|
|
||||||
// If we aren't branching to a new branch, make sure user can commit to the given branch
|
|
||||||
if opts.NewBranch != opts.OldBranch {
|
|
||||||
newBranch, err := gitRepo.GetBranch(opts.NewBranch)
|
|
||||||
if err != nil && !git.IsErrBranchNotExist(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if newBranch != nil {
|
|
||||||
return nil, models.ErrBranchAlreadyExists{
|
|
||||||
BranchName: opts.NewBranch,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, opts.TreePath); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the path given in opts.treeName is valid (not a git path)
|
|
||||||
treePath := CleanUploadFileName(opts.TreePath)
|
|
||||||
if treePath == "" {
|
|
||||||
return nil, models.ErrFilenameInvalid{
|
|
||||||
Path: opts.TreePath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message := strings.TrimSpace(opts.Message)
|
|
||||||
|
|
||||||
author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
|
|
||||||
|
|
||||||
t, err := NewTemporaryUploadRepository(ctx, repo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer t.Close()
|
|
||||||
if err := t.Clone(opts.OldBranch); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := t.SetDefaultIndex(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the commit of the original branch
|
|
||||||
commit, err := t.GetBranchCommit(opts.OldBranch)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err // Couldn't get a commit for the branch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assigned LastCommitID in opts if it hasn't been set
|
|
||||||
if opts.LastCommitID == "" {
|
|
||||||
opts.LastCommitID = commit.ID.String()
|
|
||||||
} else {
|
|
||||||
lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("DeleteRepoFile: Invalid last commit ID: %w", err)
|
|
||||||
}
|
|
||||||
opts.LastCommitID = lastCommitID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the files in the index
|
|
||||||
filesInIndex, err := t.LsFiles(opts.TreePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("DeleteRepoFile: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the file we want to delete in the index
|
|
||||||
inFilelist := false
|
|
||||||
for _, file := range filesInIndex {
|
|
||||||
if file == opts.TreePath {
|
|
||||||
inFilelist = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !inFilelist {
|
|
||||||
return nil, models.ErrRepoFileDoesNotExist{
|
|
||||||
Path: opts.TreePath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the entry of treePath and check if the SHA given is the same as the file
|
|
||||||
entry, err := commit.GetTreeEntryByPath(treePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if opts.SHA != "" {
|
|
||||||
// If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
|
|
||||||
if opts.SHA != entry.ID.String() {
|
|
||||||
return nil, models.ErrSHADoesNotMatch{
|
|
||||||
Path: treePath,
|
|
||||||
GivenSHA: opts.SHA,
|
|
||||||
CurrentSHA: entry.ID.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if opts.LastCommitID != "" {
|
|
||||||
// If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw
|
|
||||||
// an error, but only if we aren't creating a new branch.
|
|
||||||
if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch {
|
|
||||||
// CommitIDs don't match, but we don't want to throw a ErrCommitIDDoesNotMatch unless
|
|
||||||
// this specific file has been edited since opts.LastCommitID
|
|
||||||
if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if changed {
|
|
||||||
return nil, models.ErrCommitIDDoesNotMatch{
|
|
||||||
GivenCommitID: opts.LastCommitID,
|
|
||||||
CurrentCommitID: opts.LastCommitID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The file wasn't modified, so we are good to delete it
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// When deleting a file, a lastCommitID or SHA needs to be given to make sure other commits haven't been
|
|
||||||
// made. We throw an error if one wasn't provided.
|
|
||||||
return nil, models.ErrSHAOrCommitIDNotProvided{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the file from the index
|
|
||||||
if err := t.RemoveFilesFromIndex(opts.TreePath); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now write the tree
|
|
||||||
treeHash, err := t.WriteTree()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now commit the tree
|
|
||||||
var commitHash string
|
|
||||||
if opts.Dates != nil {
|
|
||||||
commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
|
|
||||||
} else {
|
|
||||||
commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then push this tree to NewBranch
|
|
||||||
if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
commit, err = t.GetCommit(commitHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := GetFileResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,22 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch string, treeNames []string) (*api.FilesResponse, error) {
|
||||||
|
files := []*api.ContentsResponse{}
|
||||||
|
for _, file := range treeNames {
|
||||||
|
fileContents, _ := GetContents(ctx, repo, file, branch, false) // ok if fails, then will be nil
|
||||||
|
files = append(files, fileContents)
|
||||||
|
}
|
||||||
|
fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
|
||||||
|
verification := GetPayloadCommitVerification(ctx, commit)
|
||||||
|
filesResponse := &api.FilesResponse{
|
||||||
|
Files: files,
|
||||||
|
Commit: fileCommitResponse,
|
||||||
|
Verification: verification,
|
||||||
|
}
|
||||||
|
return filesResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetFileResponseFromCommit Constructs a FileResponse from a Commit object
|
// GetFileResponseFromCommit Constructs a FileResponse from a Commit object
|
||||||
func GetFileResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) {
|
func GetFileResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) {
|
||||||
fileContents, _ := GetContents(ctx, repo, treeName, branch, false) // ok if fails, then will be nil
|
fileContents, _ := GetContents(ctx, repo, treeName, branch, false) // ok if fails, then will be nil
|
||||||
@@ -30,6 +46,20 @@ func GetFileResponseFromCommit(ctx context.Context, repo *repo_model.Repository,
|
|||||||
return fileResponse, nil
|
return fileResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// constructs a FileResponse with the file at the index from FilesResponse
|
||||||
|
func GetFileResponseFromFilesResponse(filesResponse *api.FilesResponse, index int) *api.FileResponse {
|
||||||
|
content := &api.ContentsResponse{}
|
||||||
|
if len(filesResponse.Files) > index {
|
||||||
|
content = filesResponse.Files[index]
|
||||||
|
}
|
||||||
|
fileResponse := &api.FileResponse{
|
||||||
|
Content: content,
|
||||||
|
Commit: filesResponse.Commit,
|
||||||
|
Verification: filesResponse.Verification,
|
||||||
|
}
|
||||||
|
return fileResponse
|
||||||
|
}
|
||||||
|
|
||||||
// GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
|
// GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
|
||||||
func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) {
|
func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) {
|
||||||
if repo == nil {
|
if repo == nil {
|
||||||
|
|||||||
+315
-230
File diff suppressed because it is too large
Load Diff
Generated
+161
@@ -4063,6 +4063,57 @@
|
|||||||
"$ref": "#/responses/notFound"
|
"$ref": "#/responses/notFound"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Create or update multiple files in a repository",
|
||||||
|
"operationId": "repoChangeFiles",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/repos/{owner}/{repo}/contents/{filepath}": {
|
"/repos/{owner}/{repo}/contents/{filepath}": {
|
||||||
@@ -15891,6 +15942,90 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
|
"ChangeFileOperation": {
|
||||||
|
"description": "ChangeFileOperation for creating, updating or deleting a file",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"operation",
|
||||||
|
"content"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"description": "content must be base64 encoded",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Content"
|
||||||
|
},
|
||||||
|
"from_path": {
|
||||||
|
"description": "old path of the file to move",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "FromPath"
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"description": "indicates what to do with the file",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"create",
|
||||||
|
"update",
|
||||||
|
"delete"
|
||||||
|
],
|
||||||
|
"x-go-name": "Operation"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"description": "path to the existing or new file",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Path"
|
||||||
|
},
|
||||||
|
"sha": {
|
||||||
|
"description": "sha is the SHA for the file that already exists, required for update, delete",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "SHA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
|
"ChangeFilesOptions": {
|
||||||
|
"description": "ChangeFilesOptions options for creating, updating or deleting multiple files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"$ref": "#/definitions/Identity"
|
||||||
|
},
|
||||||
|
"branch": {
|
||||||
|
"description": "branch (optional) to base this file from. if not given, the default branch is used",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "BranchName"
|
||||||
|
},
|
||||||
|
"committer": {
|
||||||
|
"$ref": "#/definitions/Identity"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"$ref": "#/definitions/CommitDateOptions"
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ChangeFileOperation"
|
||||||
|
},
|
||||||
|
"x-go-name": "Files"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"description": "message (optional) for the commit of this file. if not supplied, a default message will be used",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Message"
|
||||||
|
},
|
||||||
|
"new_branch": {
|
||||||
|
"description": "new_branch (optional) will make a new branch from `branch` before creating the file",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "NewBranchName"
|
||||||
|
},
|
||||||
|
"signoff": {
|
||||||
|
"description": "Add a Signed-off-by trailer by the committer at the end of the commit log message.",
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "Signoff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
"ChangedFile": {
|
"ChangedFile": {
|
||||||
"description": "ChangedFile store information about files affected by the pull request",
|
"description": "ChangedFile store information about files affected by the pull request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -18326,6 +18461,26 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
|
"FilesResponse": {
|
||||||
|
"description": "FilesResponse contains information about multiple files from a repo",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"commit": {
|
||||||
|
"$ref": "#/definitions/FileCommitResponse"
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ContentsResponse"
|
||||||
|
},
|
||||||
|
"x-go-name": "Files"
|
||||||
|
},
|
||||||
|
"verification": {
|
||||||
|
"$ref": "#/definitions/PayloadCommitVerification"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
"GPGKey": {
|
"GPGKey": {
|
||||||
"description": "GPGKey a user GPG key to sign commit and tag in repository",
|
"description": "GPGKey a user GPG key to sign commit and tag in repository",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -21996,6 +22151,12 @@
|
|||||||
"$ref": "#/definitions/FileResponse"
|
"$ref": "#/definitions/FileResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"FilesResponse": {
|
||||||
|
"description": "FilesResponse",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/FilesResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
"GPGKey": {
|
"GPGKey": {
|
||||||
"description": "GPGKey",
|
"description": "GPGKey",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|||||||
@@ -11,18 +11,22 @@ import (
|
|||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FileResponse, error) {
|
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FilesResponse, error) {
|
||||||
opts := &files_service.UpdateRepoFileOptions{
|
opts := &files_service.ChangeRepoFilesOptions{
|
||||||
OldBranch: branchName,
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "create",
|
||||||
TreePath: treePath,
|
TreePath: treePath,
|
||||||
Content: content,
|
Content: content,
|
||||||
IsNewFile: true,
|
},
|
||||||
|
},
|
||||||
|
OldBranch: branchName,
|
||||||
Author: nil,
|
Author: nil,
|
||||||
Committer: nil,
|
Committer: nil,
|
||||||
}
|
}
|
||||||
return files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, user, opts)
|
return files_service.ChangeRepoFiles(git.DefaultContext, repo, user, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFile(user *user_model.User, repo *repo_model.Repository, treePath string) (*api.FileResponse, error) {
|
func createFile(user *user_model.User, repo *repo_model.Repository, treePath string) (*api.FilesResponse, error) {
|
||||||
return createFileInBranch(user, repo, treePath, repo.DefaultBranch, "This is a NEW file")
|
return createFileInBranch(user, repo, treePath, repo.DefaultBranch, "This is a NEW file")
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -367,22 +367,30 @@ func TestConflictChecking(t *testing.T) {
|
|||||||
assert.NotEmpty(t, baseRepo)
|
assert.NotEmpty(t, baseRepo)
|
||||||
|
|
||||||
// create a commit on new branch.
|
// create a commit on new branch.
|
||||||
_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
|
_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "create",
|
||||||
TreePath: "important_file",
|
TreePath: "important_file",
|
||||||
Message: "Add a important file",
|
|
||||||
Content: "Just a non-important file",
|
Content: "Just a non-important file",
|
||||||
IsNewFile: true,
|
},
|
||||||
|
},
|
||||||
|
Message: "Add a important file",
|
||||||
OldBranch: "main",
|
OldBranch: "main",
|
||||||
NewBranch: "important-secrets",
|
NewBranch: "important-secrets",
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// create a commit on main branch.
|
// create a commit on main branch.
|
||||||
_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
|
_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "create",
|
||||||
TreePath: "important_file",
|
TreePath: "important_file",
|
||||||
Message: "Add a important file",
|
|
||||||
Content: "Not the same content :P",
|
Content: "Not the same content :P",
|
||||||
IsNewFile: true,
|
},
|
||||||
|
},
|
||||||
|
Message: "Add a important file",
|
||||||
OldBranch: "main",
|
OldBranch: "main",
|
||||||
NewBranch: "main",
|
NewBranch: "main",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -101,11 +101,15 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
|
|||||||
assert.NotEmpty(t, headRepo)
|
assert.NotEmpty(t, headRepo)
|
||||||
|
|
||||||
// create a commit on base Repo
|
// create a commit on base Repo
|
||||||
_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, actor, &files_service.UpdateRepoFileOptions{
|
_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, actor, &files_service.ChangeRepoFilesOptions{
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "create",
|
||||||
TreePath: "File_A",
|
TreePath: "File_A",
|
||||||
Message: "Add File A",
|
|
||||||
Content: "File A",
|
Content: "File A",
|
||||||
IsNewFile: true,
|
},
|
||||||
|
},
|
||||||
|
Message: "Add File A",
|
||||||
OldBranch: "master",
|
OldBranch: "master",
|
||||||
NewBranch: "master",
|
NewBranch: "master",
|
||||||
Author: &files_service.IdentityOptions{
|
Author: &files_service.IdentityOptions{
|
||||||
@@ -124,11 +128,15 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// create a commit on head Repo
|
// create a commit on head Repo
|
||||||
_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, headRepo, actor, &files_service.UpdateRepoFileOptions{
|
_, err = files_service.ChangeRepoFiles(git.DefaultContext, headRepo, actor, &files_service.ChangeRepoFilesOptions{
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "create",
|
||||||
TreePath: "File_B",
|
TreePath: "File_B",
|
||||||
Message: "Add File on PR branch",
|
|
||||||
Content: "File B",
|
Content: "File B",
|
||||||
IsNewFile: true,
|
},
|
||||||
|
},
|
||||||
|
Message: "Add File on PR branch",
|
||||||
OldBranch: "master",
|
OldBranch: "master",
|
||||||
NewBranch: "newBranch",
|
NewBranch: "newBranch",
|
||||||
Author: &files_service.IdentityOptions{
|
Author: &files_service.IdentityOptions{
|
||||||
|
|||||||
+207
-76
File diff suppressed because it is too large
Load Diff
@@ -1,201 +0,0 @@
|
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
|
||||||
"code.gitea.io/gitea/models/unittest"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
|
||||||
"code.gitea.io/gitea/modules/test"
|
|
||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getDeleteRepoFileOptions(repo *repo_model.Repository) *files_service.DeleteRepoFileOptions {
|
|
||||||
return &files_service.DeleteRepoFileOptions{
|
|
||||||
LastCommitID: "",
|
|
||||||
OldBranch: repo.DefaultBranch,
|
|
||||||
NewBranch: repo.DefaultBranch,
|
|
||||||
TreePath: "README.md",
|
|
||||||
Message: "Deletes README.md",
|
|
||||||
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
|
|
||||||
Author: &files_service.IdentityOptions{
|
|
||||||
Name: "Bob Smith",
|
|
||||||
Email: "bob@smith.com",
|
|
||||||
},
|
|
||||||
Committer: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExpectedDeleteFileResponse(u *url.URL) *api.FileResponse {
|
|
||||||
// Just returns fields that don't change, i.e. fields with commit SHAs and dates can't be determined
|
|
||||||
return &api.FileResponse{
|
|
||||||
Content: nil,
|
|
||||||
Commit: &api.FileCommitResponse{
|
|
||||||
Author: &api.CommitUser{
|
|
||||||
Identity: api.Identity{
|
|
||||||
Name: "Bob Smith",
|
|
||||||
Email: "bob@smith.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Committer: &api.CommitUser{
|
|
||||||
Identity: api.Identity{
|
|
||||||
Name: "Bob Smith",
|
|
||||||
Email: "bob@smith.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Message: "Deletes README.md\n",
|
|
||||||
},
|
|
||||||
Verification: &api.PayloadCommitVerification{
|
|
||||||
Verified: false,
|
|
||||||
Reason: "gpg.error.not_signed_commit",
|
|
||||||
Signature: "",
|
|
||||||
Payload: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteRepoFile(t *testing.T) {
|
|
||||||
onGiteaRun(t, testDeleteRepoFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDeleteRepoFile(t *testing.T, u *url.URL) {
|
|
||||||
// setup
|
|
||||||
unittest.PrepareTestEnv(t)
|
|
||||||
ctx := test.MockContext(t, "user2/repo1")
|
|
||||||
ctx.SetParams(":id", "1")
|
|
||||||
test.LoadRepo(t, ctx, 1)
|
|
||||||
test.LoadRepoCommit(t, ctx)
|
|
||||||
test.LoadUser(t, ctx, 2)
|
|
||||||
test.LoadGitRepo(t, ctx)
|
|
||||||
defer ctx.Repo.GitRepo.Close()
|
|
||||||
repo := ctx.Repo.Repository
|
|
||||||
doer := ctx.Doer
|
|
||||||
opts := getDeleteRepoFileOptions(repo)
|
|
||||||
|
|
||||||
t.Run("Delete README.md file", func(t *testing.T) {
|
|
||||||
fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
expectedFileResponse := getExpectedDeleteFileResponse(u)
|
|
||||||
assert.NotNil(t, fileResponse)
|
|
||||||
assert.Nil(t, fileResponse.Content)
|
|
||||||
assert.EqualValues(t, expectedFileResponse.Commit.Message, fileResponse.Commit.Message)
|
|
||||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, fileResponse.Commit.Author.Identity)
|
|
||||||
assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, fileResponse.Commit.Committer.Identity)
|
|
||||||
assert.EqualValues(t, expectedFileResponse.Verification, fileResponse.Verification)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Verify README.md has been deleted", func(t *testing.T) {
|
|
||||||
fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
|
|
||||||
assert.Nil(t, fileResponse)
|
|
||||||
expectedError := "repository file does not exist [path: " + opts.TreePath + "]"
|
|
||||||
assert.EqualError(t, err, expectedError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test opts with branch names removed, same results
|
|
||||||
func TestDeleteRepoFileWithoutBranchNames(t *testing.T) {
|
|
||||||
onGiteaRun(t, testDeleteRepoFileWithoutBranchNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDeleteRepoFileWithoutBranchNames(t *testing.T, u *url.URL) {
|
|
||||||
// setup
|
|
||||||
unittest.PrepareTestEnv(t)
|
|
||||||
ctx := test.MockContext(t, "user2/repo1")
|
|
||||||
ctx.SetParams(":id", "1")
|
|
||||||
test.LoadRepo(t, ctx, 1)
|
|
||||||
test.LoadRepoCommit(t, ctx)
|
|
||||||
test.LoadUser(t, ctx, 2)
|
|
||||||
test.LoadGitRepo(t, ctx)
|
|
||||||
defer ctx.Repo.GitRepo.Close()
|
|
||||||
|
|
||||||
repo := ctx.Repo.Repository
|
|
||||||
doer := ctx.Doer
|
|
||||||
opts := getDeleteRepoFileOptions(repo)
|
|
||||||
opts.OldBranch = ""
|
|
||||||
opts.NewBranch = ""
|
|
||||||
|
|
||||||
t.Run("Delete README.md without Branch Name", func(t *testing.T) {
|
|
||||||
fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
expectedFileResponse := getExpectedDeleteFileResponse(u)
|
|
||||||
assert.NotNil(t, fileResponse)
|
|
||||||
assert.Nil(t, fileResponse.Content)
|
|
||||||
assert.EqualValues(t, expectedFileResponse.Commit.Message, fileResponse.Commit.Message)
|
|
||||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, fileResponse.Commit.Author.Identity)
|
|
||||||
assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, fileResponse.Commit.Committer.Identity)
|
|
||||||
assert.EqualValues(t, expectedFileResponse.Verification, fileResponse.Verification)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteRepoFileErrors(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
unittest.PrepareTestEnv(t)
|
|
||||||
ctx := test.MockContext(t, "user2/repo1")
|
|
||||||
ctx.SetParams(":id", "1")
|
|
||||||
test.LoadRepo(t, ctx, 1)
|
|
||||||
test.LoadRepoCommit(t, ctx)
|
|
||||||
test.LoadUser(t, ctx, 2)
|
|
||||||
test.LoadGitRepo(t, ctx)
|
|
||||||
defer ctx.Repo.GitRepo.Close()
|
|
||||||
|
|
||||||
repo := ctx.Repo.Repository
|
|
||||||
doer := ctx.Doer
|
|
||||||
|
|
||||||
t.Run("Bad branch", func(t *testing.T) {
|
|
||||||
opts := getDeleteRepoFileOptions(repo)
|
|
||||||
opts.OldBranch = "bad_branch"
|
|
||||||
fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Nil(t, fileResponse)
|
|
||||||
expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
|
|
||||||
assert.EqualError(t, err, expectedError)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Bad SHA", func(t *testing.T) {
|
|
||||||
opts := getDeleteRepoFileOptions(repo)
|
|
||||||
origSHA := opts.SHA
|
|
||||||
opts.SHA = "bad_sha"
|
|
||||||
fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
|
|
||||||
assert.Nil(t, fileResponse)
|
|
||||||
assert.Error(t, err)
|
|
||||||
expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]"
|
|
||||||
assert.EqualError(t, err, expectedError)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("New branch already exists", func(t *testing.T) {
|
|
||||||
opts := getDeleteRepoFileOptions(repo)
|
|
||||||
opts.NewBranch = "develop"
|
|
||||||
fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
|
|
||||||
assert.Nil(t, fileResponse)
|
|
||||||
assert.Error(t, err)
|
|
||||||
expectedError := "branch already exists [name: " + opts.NewBranch + "]"
|
|
||||||
assert.EqualError(t, err, expectedError)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("TreePath is empty:", func(t *testing.T) {
|
|
||||||
opts := getDeleteRepoFileOptions(repo)
|
|
||||||
opts.TreePath = ""
|
|
||||||
fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
|
|
||||||
assert.Nil(t, fileResponse)
|
|
||||||
assert.Error(t, err)
|
|
||||||
expectedError := "path contains a malformed path component [path: ]"
|
|
||||||
assert.EqualError(t, err, expectedError)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("TreePath is a git directory:", func(t *testing.T) {
|
|
||||||
opts := getDeleteRepoFileOptions(repo)
|
|
||||||
opts.TreePath = ".git"
|
|
||||||
fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
|
|
||||||
assert.Nil(t, fileResponse)
|
|
||||||
assert.Error(t, err)
|
|
||||||
expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]"
|
|
||||||
assert.EqualError(t, err, expectedError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user