mirror of
https://github.com/go-gitea/gitea
synced 2024-12-22 16:44:26 +00:00
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:
parent
245f2c08db
commit
275d4b7e3f
@ -64,6 +64,35 @@ func (o *UpdateFileOptions) Branch() string {
|
||||
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
|
||||
type FileOptionInterface interface {
|
||||
Branch() string
|
||||
@ -126,6 +155,13 @@ type FileResponse struct {
|
||||
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
|
||||
type FileDeleteResponse struct {
|
||||
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.Group("/contents", func() {
|
||||
m.Get("", repo.GetContentsList)
|
||||
m.Post("", reqToken(auth_model.AccessTokenScopeRepo), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, repo.ChangeFiles)
|
||||
m.Get("/*", repo.GetContents)
|
||||
m.Group("/*", func() {
|
||||
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -116,6 +116,9 @@ type swaggerParameterBodies struct {
|
||||
// in:body
|
||||
EditAttachmentOptions api.EditAttachmentOptions
|
||||
|
||||
// in:body
|
||||
ChangeFilesOptions api.ChangeFilesOptions
|
||||
|
||||
// in:body
|
||||
CreateFileOptions api.CreateFileOptions
|
||||
|
||||
|
@ -296,6 +296,13 @@ type swaggerFileResponse struct {
|
||||
Body api.FileResponse `json:"body"`
|
||||
}
|
||||
|
||||
// FilesResponse
|
||||
// swagger:response FilesResponse
|
||||
type swaggerFilesResponse struct {
|
||||
// in: body
|
||||
Body api.FilesResponse `json:"body"`
|
||||
}
|
||||
|
||||
// ContentsResponse
|
||||
// swagger:response ContentsResponse
|
||||
type swaggerContentsResponse struct {
|
||||
|
@ -272,18 +272,27 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
|
||||
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,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: branchName,
|
||||
FromTreePath: ctx.Repo.TreePath,
|
||||
TreePath: form.TreePath,
|
||||
Message: message,
|
||||
Content: strings.ReplaceAll(form.Content, "\r", ""),
|
||||
IsNewFile: isNewFile,
|
||||
Signoff: form.Signoff,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: operation,
|
||||
FromTreePath: ctx.Repo.TreePath,
|
||||
TreePath: form.TreePath,
|
||||
Content: strings.ReplaceAll(form.Content, "\r", ""),
|
||||
},
|
||||
},
|
||||
Signoff: form.Signoff,
|
||||
}); 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) {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
|
||||
} else if git_model.IsErrLFSFileLocked(err) {
|
||||
@ -478,13 +487,18 @@ func DeleteFilePost(ctx *context.Context) {
|
||||
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,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: branchName,
|
||||
TreePath: ctx.Repo.TreePath,
|
||||
Message: message,
|
||||
Signoff: form.Signoff,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "delete",
|
||||
TreePath: ctx.Repo.TreePath,
|
||||
},
|
||||
},
|
||||
Message: message,
|
||||
Signoff: form.Signoff,
|
||||
}); err != nil {
|
||||
// This is where we handle all the errors thrown by repofiles.DeleteRepoFile
|
||||
if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
|
||||
|
@ -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"
|
||||
)
|
||||
|
||||
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
|
||||
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
|
||||
@ -30,6 +46,20 @@ func GetFileResponseFromCommit(ctx context.Context, repo *repo_model.Repository,
|
||||
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
|
||||
func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) {
|
||||
if repo == nil {
|
||||
|
@ -41,23 +41,36 @@ type CommitDateOptions struct {
|
||||
Committer time.Time
|
||||
}
|
||||
|
||||
// UpdateRepoFileOptions holds the repository file update options
|
||||
type UpdateRepoFileOptions struct {
|
||||
type ChangeRepoFile struct {
|
||||
Operation string
|
||||
TreePath string
|
||||
FromTreePath string
|
||||
Content string
|
||||
SHA string
|
||||
Options *RepoFileOptions
|
||||
}
|
||||
|
||||
// UpdateRepoFilesOptions holds the repository files update options
|
||||
type ChangeRepoFilesOptions struct {
|
||||
LastCommitID string
|
||||
OldBranch string
|
||||
NewBranch string
|
||||
TreePath string
|
||||
FromTreePath string
|
||||
Message string
|
||||
Content string
|
||||
SHA string
|
||||
IsNewFile bool
|
||||
Files []*ChangeRepoFile
|
||||
Author *IdentityOptions
|
||||
Committer *IdentityOptions
|
||||
Dates *CommitDateOptions
|
||||
Signoff bool
|
||||
}
|
||||
|
||||
type RepoFileOptions struct {
|
||||
treePath string
|
||||
fromTreePath string
|
||||
encoding string
|
||||
bom bool
|
||||
executable bool
|
||||
}
|
||||
|
||||
func detectEncodingAndBOM(entry *git.TreeEntry, repo *repo_model.Repository) (string, bool) {
|
||||
reader, err := entry.Blob().DataAsync()
|
||||
if err != nil {
|
||||
@ -125,8 +138,8 @@ func detectEncodingAndBOM(entry *git.TreeEntry, repo *repo_model.Repository) (st
|
||||
return encoding, false
|
||||
}
|
||||
|
||||
// CreateOrUpdateRepoFile adds or updates a file in the given repository
|
||||
func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UpdateRepoFileOptions) (*structs.FileResponse, error) {
|
||||
// ChangeRepoFiles adds, updates or removes multiple files in the given repository
|
||||
func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (*structs.FilesResponse, error) {
|
||||
// If no branch name is set, assume default branch
|
||||
if opts.OldBranch == "" {
|
||||
opts.OldBranch = repo.DefaultBranch
|
||||
@ -146,6 +159,38 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
|
||||
return nil, err
|
||||
}
|
||||
|
||||
treePaths := []string{}
|
||||
for _, file := range opts.Files {
|
||||
// If FromTreePath is not set, set it to the opts.TreePath
|
||||
if file.TreePath != "" && file.FromTreePath == "" {
|
||||
file.FromTreePath = file.TreePath
|
||||
}
|
||||
|
||||
// Check that the path given in opts.treePath is valid (not a git path)
|
||||
treePath := CleanUploadFileName(file.TreePath)
|
||||
if treePath == "" {
|
||||
return nil, models.ErrFilenameInvalid{
|
||||
Path: file.TreePath,
|
||||
}
|
||||
}
|
||||
// If there is a fromTreePath (we are copying it), also clean it up
|
||||
fromTreePath := CleanUploadFileName(file.FromTreePath)
|
||||
if fromTreePath == "" && file.FromTreePath != "" {
|
||||
return nil, models.ErrFilenameInvalid{
|
||||
Path: file.FromTreePath,
|
||||
}
|
||||
}
|
||||
|
||||
file.Options = &RepoFileOptions{
|
||||
treePath: treePath,
|
||||
fromTreePath: fromTreePath,
|
||||
encoding: "UTF-8",
|
||||
bom: false,
|
||||
executable: false,
|
||||
}
|
||||
treePaths = append(treePaths, treePath)
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -159,30 +204,10 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
|
||||
if err != nil && !git.IsErrBranchNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
} else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, opts.TreePath); err != nil {
|
||||
} else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, treePaths); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If FromTreePath is not set, set it to the opts.TreePath
|
||||
if opts.TreePath != "" && opts.FromTreePath == "" {
|
||||
opts.FromTreePath = opts.TreePath
|
||||
}
|
||||
|
||||
// Check that the path given in opts.treePath is valid (not a git path)
|
||||
treePath := CleanUploadFileName(opts.TreePath)
|
||||
if treePath == "" {
|
||||
return nil, models.ErrFilenameInvalid{
|
||||
Path: opts.TreePath,
|
||||
}
|
||||
}
|
||||
// If there is a fromTreePath (we are copying it), also clean it up
|
||||
fromTreePath := CleanUploadFileName(opts.FromTreePath)
|
||||
if fromTreePath == "" && opts.FromTreePath != "" {
|
||||
return nil, models.ErrFilenameInvalid{
|
||||
Path: opts.FromTreePath,
|
||||
}
|
||||
}
|
||||
|
||||
message := strings.TrimSpace(opts.Message)
|
||||
|
||||
author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
|
||||
@ -194,6 +219,11 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
|
||||
defer t.Close()
|
||||
hasOldBranch := true
|
||||
if err := t.Clone(opts.OldBranch); err != nil {
|
||||
for _, file := range opts.Files {
|
||||
if file.Operation == "delete" {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
|
||||
return nil, err
|
||||
}
|
||||
@ -209,9 +239,29 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
|
||||
}
|
||||
}
|
||||
|
||||
encoding := "UTF-8"
|
||||
bom := false
|
||||
executable := false
|
||||
for _, file := range opts.Files {
|
||||
if file.Operation == "delete" {
|
||||
// Get the files in the index
|
||||
filesInIndex, err := t.LsFiles(file.TreePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DeleteRepoFile: %w", err)
|
||||
}
|
||||
|
||||
// Find the file we want to delete in the index
|
||||
inFilelist := false
|
||||
for _, indexFile := range filesInIndex {
|
||||
if indexFile == file.TreePath {
|
||||
inFilelist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inFilelist {
|
||||
return nil, models.ErrRepoFileDoesNotExist{
|
||||
Path: file.TreePath,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasOldBranch {
|
||||
// Get the commit of the original branch
|
||||
@ -232,176 +282,27 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
|
||||
|
||||
}
|
||||
|
||||
if !opts.IsNewFile {
|
||||
fromEntry, err := commit.GetTreeEntryByPath(fromTreePath)
|
||||
if err != nil {
|
||||
for _, file := range opts.Files {
|
||||
if err := handleCheckErrors(file, commit, opts, repo); 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 != fromEntry.ID.String() {
|
||||
return nil, models.ErrSHADoesNotMatch{
|
||||
Path: treePath,
|
||||
GivenSHA: opts.SHA,
|
||||
CurrentSHA: fromEntry.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 {
|
||||
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 updating 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{}
|
||||
}
|
||||
encoding, bom = detectEncodingAndBOM(fromEntry, repo)
|
||||
executable = fromEntry.IsExecutable()
|
||||
}
|
||||
}
|
||||
|
||||
// For the path where this file will be created/updated, we need to make
|
||||
// sure no parts of the path are existing files or links except for the last
|
||||
// item in the path which is the file name, and that shouldn't exist IF it is
|
||||
// a new file OR is being moved to a new path.
|
||||
treePathParts := strings.Split(treePath, "/")
|
||||
subTreePath := ""
|
||||
for index, part := range treePathParts {
|
||||
subTreePath = path.Join(subTreePath, part)
|
||||
entry, err := commit.GetTreeEntryByPath(subTreePath)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
// Means there is no item with that name, so we're good
|
||||
break
|
||||
}
|
||||
contentStore := lfs.NewContentStore()
|
||||
for _, file := range opts.Files {
|
||||
switch file.Operation {
|
||||
case "create", "update":
|
||||
if err := CreateOrUpdateFile(ctx, t, file, contentStore, repo.ID, hasOldBranch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if index < len(treePathParts)-1 {
|
||||
if !entry.IsDir() {
|
||||
return nil, models.ErrFilePathInvalid{
|
||||
Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", subTreePath),
|
||||
Path: subTreePath,
|
||||
Name: part,
|
||||
Type: git.EntryModeBlob,
|
||||
}
|
||||
}
|
||||
} else if entry.IsLink() {
|
||||
return nil, models.ErrFilePathInvalid{
|
||||
Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath),
|
||||
Path: subTreePath,
|
||||
Name: part,
|
||||
Type: git.EntryModeSymlink,
|
||||
}
|
||||
} else if entry.IsDir() {
|
||||
return nil, models.ErrFilePathInvalid{
|
||||
Message: fmt.Sprintf("a directory exists where you’re trying to create a file [path: %s]", subTreePath),
|
||||
Path: subTreePath,
|
||||
Name: part,
|
||||
Type: git.EntryModeTree,
|
||||
}
|
||||
} else if fromTreePath != treePath || opts.IsNewFile {
|
||||
// The entry shouldn't exist if we are creating new file or moving to a new path
|
||||
return nil, models.ErrRepoFileAlreadyExists{
|
||||
Path: treePath,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Get the two paths (might be the same if not moving) from the index if they exist
|
||||
filesInIndex, err := t.LsFiles(opts.TreePath, opts.FromTreePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepoFile: %w", err)
|
||||
}
|
||||
// If is a new file (not updating) then the given path shouldn't exist
|
||||
if opts.IsNewFile {
|
||||
for _, file := range filesInIndex {
|
||||
if file == opts.TreePath {
|
||||
return nil, models.ErrRepoFileAlreadyExists{
|
||||
Path: opts.TreePath,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the old path from the tree
|
||||
if fromTreePath != treePath && len(filesInIndex) > 0 {
|
||||
for _, file := range filesInIndex {
|
||||
if file == fromTreePath {
|
||||
if err := t.RemoveFilesFromIndex(opts.FromTreePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content := opts.Content
|
||||
if bom {
|
||||
content = string(charset.UTF8BOM) + content
|
||||
}
|
||||
if encoding != "UTF-8" {
|
||||
charsetEncoding, _ := stdcharset.Lookup(encoding)
|
||||
if charsetEncoding != nil {
|
||||
result, _, err := transform.String(charsetEncoding.NewEncoder(), content)
|
||||
if err != nil {
|
||||
// Look if we can't encode back in to the original we should just stick with utf-8
|
||||
log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", opts.TreePath, opts.FromTreePath, encoding, err)
|
||||
result = content
|
||||
}
|
||||
content = result
|
||||
} else {
|
||||
log.Error("Unknown encoding: %s", encoding)
|
||||
}
|
||||
}
|
||||
// Reset the opts.Content to our adjusted content to ensure that LFS gets the correct content
|
||||
opts.Content = content
|
||||
var lfsMetaObject *git_model.LFSMetaObject
|
||||
|
||||
if setting.LFS.StartServer && hasOldBranch {
|
||||
// Check there is no way this can return multiple infos
|
||||
filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{
|
||||
Attributes: []string{"filter"},
|
||||
Filenames: []string{treePath},
|
||||
CachedOnly: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" {
|
||||
// OK so we are supposed to LFS this data!
|
||||
pointer, err := lfs.GeneratePointer(strings.NewReader(opts.Content))
|
||||
if err != nil {
|
||||
case "delete":
|
||||
// Remove the file from the index
|
||||
if err := t.RemoveFilesFromIndex(file.TreePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID}
|
||||
content = pointer.StringContent()
|
||||
}
|
||||
}
|
||||
// Add the object to the database
|
||||
objectHash, err := t.HashObject(strings.NewReader(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add the object to the index
|
||||
if executable {
|
||||
if err := t.AddObjectToIndex("100755", objectHash, treePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil {
|
||||
return nil, err
|
||||
default:
|
||||
return nil, fmt.Errorf("Invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,27 +323,6 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if lfsMetaObject != nil {
|
||||
// We have an LFS object - create it
|
||||
lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, lfsMetaObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentStore := lfs.NewContentStore()
|
||||
exist, err := contentStore.Exists(lfsMetaObject.Pointer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
if err := contentStore.Put(lfsMetaObject.Pointer, strings.NewReader(opts.Content)); err != nil {
|
||||
if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, lfsMetaObject.Oid); err2 != nil {
|
||||
return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then push this tree to NewBranch
|
||||
if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
|
||||
log.Error("%T %v", err, err)
|
||||
@ -454,7 +334,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := GetFileResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePath)
|
||||
filesReponse, err := GetFilesResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePaths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -463,25 +343,238 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
|
||||
_ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false}, "is_empty")
|
||||
}
|
||||
|
||||
return file, nil
|
||||
return filesReponse, nil
|
||||
}
|
||||
|
||||
// handles the check for various issues for ChangeRepoFiles
|
||||
func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions, repo *repo_model.Repository) error {
|
||||
if file.Operation == "update" || file.Operation == "delete" {
|
||||
fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if file.SHA != "" {
|
||||
// If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
|
||||
if file.SHA != fromEntry.ID.String() {
|
||||
return models.ErrSHADoesNotMatch{
|
||||
Path: file.Options.treePath,
|
||||
GivenSHA: file.SHA,
|
||||
CurrentSHA: fromEntry.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 {
|
||||
if changed, err := commit.FileChangedSinceCommit(file.Options.treePath, opts.LastCommitID); err != nil {
|
||||
return err
|
||||
} else if changed {
|
||||
return models.ErrCommitIDDoesNotMatch{
|
||||
GivenCommitID: opts.LastCommitID,
|
||||
CurrentCommitID: opts.LastCommitID,
|
||||
}
|
||||
}
|
||||
// The file wasn't modified, so we are good to delete it
|
||||
}
|
||||
} else {
|
||||
// When updating 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 models.ErrSHAOrCommitIDNotProvided{}
|
||||
}
|
||||
file.Options.encoding, file.Options.bom = detectEncodingAndBOM(fromEntry, repo)
|
||||
file.Options.executable = fromEntry.IsExecutable()
|
||||
}
|
||||
if file.Operation == "create" || file.Operation == "update" {
|
||||
// For the path where this file will be created/updated, we need to make
|
||||
// sure no parts of the path are existing files or links except for the last
|
||||
// item in the path which is the file name, and that shouldn't exist IF it is
|
||||
// a new file OR is being moved to a new path.
|
||||
treePathParts := strings.Split(file.Options.treePath, "/")
|
||||
subTreePath := ""
|
||||
for index, part := range treePathParts {
|
||||
subTreePath = path.Join(subTreePath, part)
|
||||
entry, err := commit.GetTreeEntryByPath(subTreePath)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
// Means there is no item with that name, so we're good
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
if index < len(treePathParts)-1 {
|
||||
if !entry.IsDir() {
|
||||
return models.ErrFilePathInvalid{
|
||||
Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", subTreePath),
|
||||
Path: subTreePath,
|
||||
Name: part,
|
||||
Type: git.EntryModeBlob,
|
||||
}
|
||||
}
|
||||
} else if entry.IsLink() {
|
||||
return models.ErrFilePathInvalid{
|
||||
Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath),
|
||||
Path: subTreePath,
|
||||
Name: part,
|
||||
Type: git.EntryModeSymlink,
|
||||
}
|
||||
} else if entry.IsDir() {
|
||||
return models.ErrFilePathInvalid{
|
||||
Message: fmt.Sprintf("a directory exists where you’re trying to create a file [path: %s]", subTreePath),
|
||||
Path: subTreePath,
|
||||
Name: part,
|
||||
Type: git.EntryModeTree,
|
||||
}
|
||||
} else if file.Options.fromTreePath != file.Options.treePath || file.Operation == "create" {
|
||||
// The entry shouldn't exist if we are creating new file or moving to a new path
|
||||
return models.ErrRepoFileAlreadyExists{
|
||||
Path: file.Options.treePath,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handle creating or updating a file for ChangeRepoFiles
|
||||
func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error {
|
||||
// Get the two paths (might be the same if not moving) from the index if they exist
|
||||
filesInIndex, err := t.LsFiles(file.TreePath, file.FromTreePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateRepoFile: %w", err)
|
||||
}
|
||||
// If is a new file (not updating) then the given path shouldn't exist
|
||||
if file.Operation == "create" {
|
||||
for _, indexFile := range filesInIndex {
|
||||
if indexFile == file.TreePath {
|
||||
return models.ErrRepoFileAlreadyExists{
|
||||
Path: file.TreePath,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the old path from the tree
|
||||
if file.Options.fromTreePath != file.Options.treePath && len(filesInIndex) > 0 {
|
||||
for _, indexFile := range filesInIndex {
|
||||
if indexFile == file.Options.fromTreePath {
|
||||
if err := t.RemoveFilesFromIndex(file.FromTreePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content := file.Content
|
||||
if file.Options.bom {
|
||||
content = string(charset.UTF8BOM) + content
|
||||
}
|
||||
if file.Options.encoding != "UTF-8" {
|
||||
charsetEncoding, _ := stdcharset.Lookup(file.Options.encoding)
|
||||
if charsetEncoding != nil {
|
||||
result, _, err := transform.String(charsetEncoding.NewEncoder(), content)
|
||||
if err != nil {
|
||||
// Look if we can't encode back in to the original we should just stick with utf-8
|
||||
log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", file.TreePath, file.FromTreePath, file.Options.encoding, err)
|
||||
result = content
|
||||
}
|
||||
content = result
|
||||
} else {
|
||||
log.Error("Unknown encoding: %s", file.Options.encoding)
|
||||
}
|
||||
}
|
||||
// Reset the opts.Content to our adjusted content to ensure that LFS gets the correct content
|
||||
file.Content = content
|
||||
var lfsMetaObject *git_model.LFSMetaObject
|
||||
|
||||
if setting.LFS.StartServer && hasOldBranch {
|
||||
// Check there is no way this can return multiple infos
|
||||
filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{
|
||||
Attributes: []string{"filter"},
|
||||
Filenames: []string{file.Options.treePath},
|
||||
CachedOnly: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if filename2attribute2info[file.Options.treePath] != nil && filename2attribute2info[file.Options.treePath]["filter"] == "lfs" {
|
||||
// OK so we are supposed to LFS this data!
|
||||
pointer, err := lfs.GeneratePointer(strings.NewReader(file.Content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repoID}
|
||||
content = pointer.StringContent()
|
||||
}
|
||||
}
|
||||
|
||||
// Add the object to the database
|
||||
objectHash, err := t.HashObject(strings.NewReader(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add the object to the index
|
||||
if file.Options.executable {
|
||||
if err := t.AddObjectToIndex("100755", objectHash, file.Options.treePath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := t.AddObjectToIndex("100644", objectHash, file.Options.treePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if lfsMetaObject != nil {
|
||||
// We have an LFS object - create it
|
||||
lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, lfsMetaObject)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exist, err := contentStore.Exists(lfsMetaObject.Pointer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
if err := contentStore.Put(lfsMetaObject.Pointer, strings.NewReader(file.Content)); err != nil {
|
||||
if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil {
|
||||
return fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch
|
||||
func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName, treePath string) error {
|
||||
func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName string, treePaths []string) error {
|
||||
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if protectedBranch != nil {
|
||||
protectedBranch.Repo = repo
|
||||
isUnprotectedFile := false
|
||||
glob := protectedBranch.GetUnprotectedFilePatterns()
|
||||
if len(glob) != 0 {
|
||||
isUnprotectedFile = protectedBranch.IsUnprotectedFile(glob, treePath)
|
||||
}
|
||||
if !protectedBranch.CanUserPush(ctx, doer) && !isUnprotectedFile {
|
||||
return models.ErrUserCannotCommit{
|
||||
UserName: doer.LowerName,
|
||||
globUnprotected := protectedBranch.GetUnprotectedFilePatterns()
|
||||
globProtected := protectedBranch.GetProtectedFilePatterns()
|
||||
canUserPush := protectedBranch.CanUserPush(ctx, doer)
|
||||
for _, treePath := range treePaths {
|
||||
isUnprotectedFile := false
|
||||
if len(globUnprotected) != 0 {
|
||||
isUnprotectedFile = protectedBranch.IsUnprotectedFile(globUnprotected, treePath)
|
||||
}
|
||||
if !canUserPush && !isUnprotectedFile {
|
||||
return models.ErrUserCannotCommit{
|
||||
UserName: doer.LowerName,
|
||||
}
|
||||
}
|
||||
if protectedBranch.IsProtectedFile(globProtected, treePath) {
|
||||
return models.ErrFilePathProtected{
|
||||
Path: treePath,
|
||||
}
|
||||
}
|
||||
}
|
||||
if protectedBranch.RequireSignedCommits {
|
||||
@ -495,14 +588,6 @@ func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, do
|
||||
}
|
||||
}
|
||||
}
|
||||
patterns := protectedBranch.GetProtectedFilePatterns()
|
||||
for _, pat := range patterns {
|
||||
if pat.Match(strings.ToLower(treePath)) {
|
||||
return models.ErrFilePathProtected{
|
||||
Path: treePath,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
161
templates/swagger/v1_json.tmpl
generated
161
templates/swagger/v1_json.tmpl
generated
@ -4063,6 +4063,57 @@
|
||||
"$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}": {
|
||||
@ -15891,6 +15942,90 @@
|
||||
},
|
||||
"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": {
|
||||
"description": "ChangedFile store information about files affected by the pull request",
|
||||
"type": "object",
|
||||
@ -18326,6 +18461,26 @@
|
||||
},
|
||||
"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": {
|
||||
"description": "GPGKey a user GPG key to sign commit and tag in repository",
|
||||
"type": "object",
|
||||
@ -21996,6 +22151,12 @@
|
||||
"$ref": "#/definitions/FileResponse"
|
||||
}
|
||||
},
|
||||
"FilesResponse": {
|
||||
"description": "FilesResponse",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/FilesResponse"
|
||||
}
|
||||
},
|
||||
"GPGKey": {
|
||||
"description": "GPGKey",
|
||||
"schema": {
|
||||
|
@ -11,18 +11,22 @@ import (
|
||||
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) {
|
||||
opts := &files_service.UpdateRepoFileOptions{
|
||||
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FilesResponse, error) {
|
||||
opts := &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: treePath,
|
||||
Content: content,
|
||||
},
|
||||
},
|
||||
OldBranch: branchName,
|
||||
TreePath: treePath,
|
||||
Content: content,
|
||||
IsNewFile: true,
|
||||
Author: 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")
|
||||
}
|
||||
|
309
tests/integration/api_repo_files_change_test.go
Normal file
309
tests/integration/api_repo_files_change_test.go
Normal file
@ -0,0 +1,309 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
stdCtx "context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getChangeFilesOptions() *api.ChangeFilesOptions {
|
||||
newContent := "This is new text"
|
||||
updateContent := "This is updated text"
|
||||
newContentEncoded := base64.StdEncoding.EncodeToString([]byte(newContent))
|
||||
updateContentEncoded := base64.StdEncoding.EncodeToString([]byte(updateContent))
|
||||
return &api.ChangeFilesOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: "master",
|
||||
NewBranchName: "master",
|
||||
Message: "My update of new/file.txt",
|
||||
Author: api.Identity{
|
||||
Name: "Anne Doe",
|
||||
Email: "annedoe@example.com",
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: "John Doe",
|
||||
Email: "johndoe@example.com",
|
||||
},
|
||||
},
|
||||
Files: []*api.ChangeFileOperation{
|
||||
{
|
||||
Operation: "create",
|
||||
Content: newContentEncoded,
|
||||
},
|
||||
{
|
||||
Operation: "update",
|
||||
Content: updateContentEncoded,
|
||||
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
||||
},
|
||||
{
|
||||
Operation: "delete",
|
||||
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIChangeFiles(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
|
||||
user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
|
||||
repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
|
||||
fileID := 0
|
||||
|
||||
// Get user2's token
|
||||
session := loginUser(t, user2.Name)
|
||||
token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
|
||||
// Get user4's token
|
||||
session = loginUser(t, user4.Name)
|
||||
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
|
||||
|
||||
// Test changing files in repo1 which user2 owns, try both with branch and empty branch
|
||||
for _, branch := range [...]string{
|
||||
"master", // Branch
|
||||
"", // Empty branch
|
||||
} {
|
||||
fileID++
|
||||
createTreePath := fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath := fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath := fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
createFile(user2, repo1, updateTreePath)
|
||||
createFile(user2, repo1, deleteTreePath)
|
||||
changeFilesOptions := getChangeFilesOptions()
|
||||
changeFilesOptions.BranchName = branch
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo1.Name, token2)
|
||||
req := NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
gitRepo, _ := git.OpenRepository(stdCtx.Background(), repo1.RepoPath())
|
||||
commitID, _ := gitRepo.GetBranchCommitID(changeFilesOptions.NewBranchName)
|
||||
createLasCommit, _ := gitRepo.GetCommitByPath(createTreePath)
|
||||
updateLastCommit, _ := gitRepo.GetCommitByPath(updateTreePath)
|
||||
expectedCreateFileResponse := getExpectedFileResponseForCreate(fmt.Sprintf("%v/%v", user2.Name, repo1.Name), commitID, createTreePath, createLasCommit.ID.String())
|
||||
expectedUpdateFileResponse := getExpectedFileResponseForUpdate(commitID, updateTreePath, updateLastCommit.ID.String())
|
||||
var filesResponse api.FilesResponse
|
||||
DecodeJSON(t, resp, &filesResponse)
|
||||
|
||||
// check create file
|
||||
assert.EqualValues(t, expectedCreateFileResponse.Content, filesResponse.Files[0])
|
||||
|
||||
// check update file
|
||||
assert.EqualValues(t, expectedUpdateFileResponse.Content, filesResponse.Files[1])
|
||||
|
||||
// test commit info
|
||||
assert.EqualValues(t, expectedCreateFileResponse.Commit.SHA, filesResponse.Commit.SHA)
|
||||
assert.EqualValues(t, expectedCreateFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
|
||||
assert.EqualValues(t, expectedCreateFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email)
|
||||
assert.EqualValues(t, expectedCreateFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name)
|
||||
assert.EqualValues(t, expectedCreateFileResponse.Commit.Committer.Email, filesResponse.Commit.Committer.Email)
|
||||
assert.EqualValues(t, expectedCreateFileResponse.Commit.Committer.Name, filesResponse.Commit.Committer.Name)
|
||||
|
||||
// test delete file
|
||||
assert.Nil(t, filesResponse.Files[2])
|
||||
|
||||
gitRepo.Close()
|
||||
}
|
||||
|
||||
// Test changing files in a new branch
|
||||
changeFilesOptions := getChangeFilesOptions()
|
||||
changeFilesOptions.BranchName = repo1.DefaultBranch
|
||||
changeFilesOptions.NewBranchName = "new_branch"
|
||||
fileID++
|
||||
createTreePath := fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath := fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath := fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
createFile(user2, repo1, updateTreePath)
|
||||
createFile(user2, repo1, deleteTreePath)
|
||||
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo1.Name, token2)
|
||||
req := NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
var filesResponse api.FilesResponse
|
||||
DecodeJSON(t, resp, &filesResponse)
|
||||
expectedCreateSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
|
||||
expectedCreateHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/new/file%d.txt", fileID)
|
||||
expectedCreateDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
|
||||
expectedUpdateSHA := "08bd14b2e2852529157324de9c226b3364e76136"
|
||||
expectedUpdateHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/update/file%d.txt", fileID)
|
||||
expectedUpdateDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID)
|
||||
assert.EqualValues(t, expectedCreateSHA, filesResponse.Files[0].SHA)
|
||||
assert.EqualValues(t, expectedCreateHTMLURL, *filesResponse.Files[0].HTMLURL)
|
||||
assert.EqualValues(t, expectedCreateDownloadURL, *filesResponse.Files[0].DownloadURL)
|
||||
assert.EqualValues(t, expectedUpdateSHA, filesResponse.Files[1].SHA)
|
||||
assert.EqualValues(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL)
|
||||
assert.EqualValues(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL)
|
||||
assert.Nil(t, filesResponse.Files[2])
|
||||
|
||||
assert.EqualValues(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message)
|
||||
|
||||
// Test updating a file and renaming it
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.BranchName = repo1.DefaultBranch
|
||||
fileID++
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
createFile(user2, repo1, updateTreePath)
|
||||
changeFilesOptions.Files = []*api.ChangeFileOperation{changeFilesOptions.Files[1]}
|
||||
changeFilesOptions.Files[0].FromPath = updateTreePath
|
||||
changeFilesOptions.Files[0].Path = "rename/" + updateTreePath
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
DecodeJSON(t, resp, &filesResponse)
|
||||
expectedUpdateSHA = "08bd14b2e2852529157324de9c226b3364e76136"
|
||||
expectedUpdateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/master/rename/update/file%d.txt", fileID)
|
||||
expectedUpdateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID)
|
||||
assert.EqualValues(t, expectedUpdateSHA, filesResponse.Files[0].SHA)
|
||||
assert.EqualValues(t, expectedUpdateHTMLURL, *filesResponse.Files[0].HTMLURL)
|
||||
assert.EqualValues(t, expectedUpdateDownloadURL, *filesResponse.Files[0].DownloadURL)
|
||||
|
||||
// Test updating a file without a message
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.Message = ""
|
||||
changeFilesOptions.BranchName = repo1.DefaultBranch
|
||||
fileID++
|
||||
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
createFile(user2, repo1, updateTreePath)
|
||||
createFile(user2, repo1, deleteTreePath)
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
DecodeJSON(t, resp, &filesResponse)
|
||||
expectedMessage := fmt.Sprintf("Add %v\nUpdate %v\nDelete %v\n", createTreePath, updateTreePath, deleteTreePath)
|
||||
assert.EqualValues(t, expectedMessage, filesResponse.Commit.Message)
|
||||
|
||||
// Test updating a file with the wrong SHA
|
||||
fileID++
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
createFile(user2, repo1, updateTreePath)
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.Files = []*api.ChangeFileOperation{changeFilesOptions.Files[1]}
|
||||
changeFilesOptions.Files[0].Path = updateTreePath
|
||||
correctSHA := changeFilesOptions.Files[0].SHA
|
||||
changeFilesOptions.Files[0].SHA = "badsha"
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
resp = MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
expectedAPIError := context.APIError{
|
||||
Message: "sha does not match [given: " + changeFilesOptions.Files[0].SHA + ", expected: " + correctSHA + "]",
|
||||
URL: setting.API.SwaggerURL,
|
||||
}
|
||||
var apiError context.APIError
|
||||
DecodeJSON(t, resp, &apiError)
|
||||
assert.Equal(t, expectedAPIError, apiError)
|
||||
|
||||
// Test creating a file in repo1 by user4 who does not have write access
|
||||
fileID++
|
||||
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
createFile(user2, repo16, updateTreePath)
|
||||
createFile(user2, repo16, deleteTreePath)
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo16.Name, token4)
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// Tests a repo with no token given so will fail
|
||||
fileID++
|
||||
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
createFile(user2, repo16, updateTreePath)
|
||||
createFile(user2, repo16, deleteTreePath)
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo16.Name)
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// Test using access token for a private repo that the user of the token owns
|
||||
fileID++
|
||||
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
createFile(user2, repo16, updateTreePath)
|
||||
createFile(user2, repo16, deleteTreePath)
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo16.Name, token2)
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// Test using org repo "user3/repo3" where user2 is a collaborator
|
||||
fileID++
|
||||
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
createFile(user3, repo3, updateTreePath)
|
||||
createFile(user3, repo3, deleteTreePath)
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user3.Name, repo3.Name, token2)
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// Test using org repo "user3/repo3" with no user token
|
||||
fileID++
|
||||
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
createFile(user3, repo3, updateTreePath)
|
||||
createFile(user3, repo3, deleteTreePath)
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user3.Name, repo3.Name)
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// Test using repo "user2/repo1" where user4 is a NOT collaborator
|
||||
fileID++
|
||||
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
createFile(user2, repo1, updateTreePath)
|
||||
createFile(user2, repo1, deleteTreePath)
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo1.Name, token4)
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
}
|
@ -367,22 +367,30 @@ func TestConflictChecking(t *testing.T) {
|
||||
assert.NotEmpty(t, baseRepo)
|
||||
|
||||
// create a commit on new branch.
|
||||
_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
|
||||
TreePath: "important_file",
|
||||
_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "important_file",
|
||||
Content: "Just a non-important file",
|
||||
},
|
||||
},
|
||||
Message: "Add a important file",
|
||||
Content: "Just a non-important file",
|
||||
IsNewFile: true,
|
||||
OldBranch: "main",
|
||||
NewBranch: "important-secrets",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create a commit on main branch.
|
||||
_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
|
||||
TreePath: "important_file",
|
||||
_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "important_file",
|
||||
Content: "Not the same content :P",
|
||||
},
|
||||
},
|
||||
Message: "Add a important file",
|
||||
Content: "Not the same content :P",
|
||||
IsNewFile: true,
|
||||
OldBranch: "main",
|
||||
NewBranch: "main",
|
||||
})
|
||||
|
@ -101,11 +101,15 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
|
||||
assert.NotEmpty(t, headRepo)
|
||||
|
||||
// create a commit on base Repo
|
||||
_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, actor, &files_service.UpdateRepoFileOptions{
|
||||
TreePath: "File_A",
|
||||
_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, actor, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "File_A",
|
||||
Content: "File A",
|
||||
},
|
||||
},
|
||||
Message: "Add File A",
|
||||
Content: "File A",
|
||||
IsNewFile: true,
|
||||
OldBranch: "master",
|
||||
NewBranch: "master",
|
||||
Author: &files_service.IdentityOptions{
|
||||
@ -124,11 +128,15 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create a commit on head Repo
|
||||
_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, headRepo, actor, &files_service.UpdateRepoFileOptions{
|
||||
TreePath: "File_B",
|
||||
_, err = files_service.ChangeRepoFiles(git.DefaultContext, headRepo, actor, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "File_B",
|
||||
Content: "File B",
|
||||
},
|
||||
},
|
||||
Message: "Add File on PR branch",
|
||||
Content: "File B",
|
||||
IsNewFile: true,
|
||||
OldBranch: "master",
|
||||
NewBranch: "newBranch",
|
||||
Author: &files_service.IdentityOptions{
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
@ -19,33 +20,90 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getCreateRepoFileOptions(repo *repo_model.Repository) *files_service.UpdateRepoFileOptions {
|
||||
return &files_service.UpdateRepoFileOptions{
|
||||
func getCreateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
|
||||
return &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "new/file.txt",
|
||||
Content: "This is a NEW file",
|
||||
},
|
||||
},
|
||||
OldBranch: repo.DefaultBranch,
|
||||
NewBranch: repo.DefaultBranch,
|
||||
TreePath: "new/file.txt",
|
||||
Message: "Creates new/file.txt",
|
||||
Content: "This is a NEW file",
|
||||
IsNewFile: true,
|
||||
Author: nil,
|
||||
Committer: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func getUpdateRepoFileOptions(repo *repo_model.Repository) *files_service.UpdateRepoFileOptions {
|
||||
return &files_service.UpdateRepoFileOptions{
|
||||
func getUpdateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
|
||||
return &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "update",
|
||||
TreePath: "README.md",
|
||||
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
|
||||
Content: "This is UPDATED content for the README file",
|
||||
},
|
||||
},
|
||||
OldBranch: repo.DefaultBranch,
|
||||
NewBranch: repo.DefaultBranch,
|
||||
TreePath: "README.md",
|
||||
Message: "Updates README.md",
|
||||
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
|
||||
Content: "This is UPDATED content for the README file",
|
||||
IsNewFile: false,
|
||||
Author: nil,
|
||||
Committer: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func getDeleteRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
|
||||
return &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "delete",
|
||||
TreePath: "README.md",
|
||||
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
|
||||
},
|
||||
},
|
||||
LastCommitID: "",
|
||||
OldBranch: repo.DefaultBranch,
|
||||
NewBranch: repo.DefaultBranch,
|
||||
Message: "Deletes README.md",
|
||||
Author: &files_service.IdentityOptions{
|
||||
Name: "Bob Smith",
|
||||
Email: "bob@smith.com",
|
||||
},
|
||||
Committer: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func getExpectedFileResponseForRepofilesDelete(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 getExpectedFileResponseForRepofilesCreate(commitID, lastCommitSHA string) *api.FileResponse {
|
||||
treePath := "new/file.txt"
|
||||
encoding := "base64"
|
||||
@ -183,7 +241,7 @@ func getExpectedFileResponseForRepofilesUpdate(commitID, filename, lastCommitSHA
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
|
||||
func TestChangeRepoFilesForCreate(t *testing.T) {
|
||||
// setup
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
ctx := test.MockContext(t, "user2/repo1")
|
||||
@ -196,10 +254,10 @@ func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
doer := ctx.Doer
|
||||
opts := getCreateRepoFileOptions(repo)
|
||||
opts := getCreateRepoFilesOptions(repo)
|
||||
|
||||
// test
|
||||
fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
|
||||
// asserts
|
||||
assert.NoError(t, err)
|
||||
@ -211,16 +269,16 @@ func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
|
||||
expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID, lastCommit.ID.String())
|
||||
assert.NotNil(t, expectedFileResponse)
|
||||
if expectedFileResponse != nil {
|
||||
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
|
||||
assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0])
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
|
||||
func TestChangeRepoFilesForUpdate(t *testing.T) {
|
||||
// setup
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
ctx := test.MockContext(t, "user2/repo1")
|
||||
@ -233,10 +291,10 @@ func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
doer := ctx.Doer
|
||||
opts := getUpdateRepoFileOptions(repo)
|
||||
opts := getUpdateRepoFilesOptions(repo)
|
||||
|
||||
// test
|
||||
fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
|
||||
// asserts
|
||||
assert.NoError(t, err)
|
||||
@ -244,17 +302,17 @@ func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
|
||||
lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
|
||||
expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
|
||||
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
|
||||
lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
|
||||
expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String())
|
||||
assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0])
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
|
||||
func TestChangeRepoFilesForUpdateWithFileMove(t *testing.T) {
|
||||
// setup
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
ctx := test.MockContext(t, "user2/repo1")
|
||||
@ -267,12 +325,12 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
doer := ctx.Doer
|
||||
opts := getUpdateRepoFileOptions(repo)
|
||||
opts.FromTreePath = "README.md"
|
||||
opts.TreePath = "README_new.md" // new file name, README_new.md
|
||||
opts := getUpdateRepoFilesOptions(repo)
|
||||
opts.Files[0].FromTreePath = "README.md"
|
||||
opts.Files[0].TreePath = "README_new.md" // new file name, README_new.md
|
||||
|
||||
// test
|
||||
fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
|
||||
// asserts
|
||||
assert.NoError(t, err)
|
||||
@ -280,32 +338,32 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
|
||||
lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
|
||||
expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
|
||||
lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
|
||||
expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String())
|
||||
// assert that the old file no longer exists in the last commit of the branch
|
||||
fromEntry, err := commit.GetTreeEntryByPath(opts.FromTreePath)
|
||||
fromEntry, err := commit.GetTreeEntryByPath(opts.Files[0].FromTreePath)
|
||||
switch err.(type) {
|
||||
case git.ErrNotExist:
|
||||
// correct, continue
|
||||
default:
|
||||
t.Fatalf("expected git.ErrNotExist, got:%v", err)
|
||||
}
|
||||
toEntry, err := commit.GetTreeEntryByPath(opts.TreePath)
|
||||
toEntry, err := commit.GetTreeEntryByPath(opts.Files[0].TreePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, fromEntry) // Should no longer exist here
|
||||
assert.NotNil(t, toEntry) // Should exist here
|
||||
// assert SHA has remained the same but paths use the new file name
|
||||
assert.EqualValues(t, expectedFileResponse.Content.SHA, fileResponse.Content.SHA)
|
||||
assert.EqualValues(t, expectedFileResponse.Content.Name, fileResponse.Content.Name)
|
||||
assert.EqualValues(t, expectedFileResponse.Content.Path, fileResponse.Content.Path)
|
||||
assert.EqualValues(t, expectedFileResponse.Content.URL, fileResponse.Content.URL)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
|
||||
assert.EqualValues(t, expectedFileResponse.Content.SHA, filesResponse.Files[0].SHA)
|
||||
assert.EqualValues(t, expectedFileResponse.Content.Name, filesResponse.Files[0].Name)
|
||||
assert.EqualValues(t, expectedFileResponse.Content.Path, filesResponse.Files[0].Path)
|
||||
assert.EqualValues(t, expectedFileResponse.Content.URL, filesResponse.Files[0].URL)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
|
||||
})
|
||||
}
|
||||
|
||||
// Test opts with branch names removed, should get same results as above test
|
||||
func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
|
||||
func TestChangeRepoFilesWithoutBranchNames(t *testing.T) {
|
||||
// setup
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
ctx := test.MockContext(t, "user2/repo1")
|
||||
@ -318,12 +376,12 @@ func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
doer := ctx.Doer
|
||||
opts := getUpdateRepoFileOptions(repo)
|
||||
opts := getUpdateRepoFilesOptions(repo)
|
||||
opts.OldBranch = ""
|
||||
opts.NewBranch = ""
|
||||
|
||||
// test
|
||||
fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
|
||||
// asserts
|
||||
assert.NoError(t, err)
|
||||
@ -331,13 +389,86 @@ func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, _ := gitRepo.GetBranchCommit(repo.DefaultBranch)
|
||||
lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
|
||||
expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
|
||||
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
|
||||
lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
|
||||
expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String())
|
||||
assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0])
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateRepoFileErrors(t *testing.T) {
|
||||
func TestChangeRepoFilesForDelete(t *testing.T) {
|
||||
onGiteaRun(t, testDeleteRepoFiles)
|
||||
}
|
||||
|
||||
func testDeleteRepoFiles(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 := getDeleteRepoFilesOptions(repo)
|
||||
|
||||
t.Run("Delete README.md file", func(t *testing.T) {
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
assert.NoError(t, err)
|
||||
expectedFileResponse := getExpectedFileResponseForRepofilesDelete(u)
|
||||
assert.NotNil(t, filesResponse)
|
||||
assert.Nil(t, filesResponse.Files[0])
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity)
|
||||
assert.EqualValues(t, expectedFileResponse.Verification, filesResponse.Verification)
|
||||
})
|
||||
|
||||
t.Run("Verify README.md has been deleted", func(t *testing.T) {
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
assert.Nil(t, filesResponse)
|
||||
expectedError := "repository file does not exist [path: " + opts.Files[0].TreePath + "]"
|
||||
assert.EqualError(t, err, expectedError)
|
||||
})
|
||||
}
|
||||
|
||||
// Test opts with branch names removed, same results
|
||||
func TestChangeRepoFilesForDeleteWithoutBranchNames(t *testing.T) {
|
||||
onGiteaRun(t, testDeleteRepoFilesWithoutBranchNames)
|
||||
}
|
||||
|
||||
func testDeleteRepoFilesWithoutBranchNames(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 := getDeleteRepoFilesOptions(repo)
|
||||
opts.OldBranch = ""
|
||||
opts.NewBranch = ""
|
||||
|
||||
t.Run("Delete README.md without Branch Name", func(t *testing.T) {
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
assert.NoError(t, err)
|
||||
expectedFileResponse := getExpectedFileResponseForRepofilesDelete(u)
|
||||
assert.NotNil(t, filesResponse)
|
||||
assert.Nil(t, filesResponse.Files[0])
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity)
|
||||
assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity)
|
||||
assert.EqualValues(t, expectedFileResponse.Verification, filesResponse.Verification)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChangeRepoFilesErrors(t *testing.T) {
|
||||
// setup
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
ctx := test.MockContext(t, "user2/repo1")
|
||||
@ -352,63 +483,63 @@ func TestCreateOrUpdateRepoFileErrors(t *testing.T) {
|
||||
doer := ctx.Doer
|
||||
|
||||
t.Run("bad branch", func(t *testing.T) {
|
||||
opts := getUpdateRepoFileOptions(repo)
|
||||
opts := getUpdateRepoFilesOptions(repo)
|
||||
opts.OldBranch = "bad_branch"
|
||||
fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, fileResponse)
|
||||
assert.Nil(t, filesResponse)
|
||||
expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
|
||||
assert.EqualError(t, err, expectedError)
|
||||
})
|
||||
|
||||
t.Run("bad SHA", func(t *testing.T) {
|
||||
opts := getUpdateRepoFileOptions(repo)
|
||||
origSHA := opts.SHA
|
||||
opts.SHA = "bad_sha"
|
||||
fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
|
||||
assert.Nil(t, fileResponse)
|
||||
opts := getUpdateRepoFilesOptions(repo)
|
||||
origSHA := opts.Files[0].SHA
|
||||
opts.Files[0].SHA = "bad_sha"
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
assert.Nil(t, filesResponse)
|
||||
assert.Error(t, err)
|
||||
expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]"
|
||||
expectedError := "sha does not match [given: " + opts.Files[0].SHA + ", expected: " + origSHA + "]"
|
||||
assert.EqualError(t, err, expectedError)
|
||||
})
|
||||
|
||||
t.Run("new branch already exists", func(t *testing.T) {
|
||||
opts := getUpdateRepoFileOptions(repo)
|
||||
opts := getUpdateRepoFilesOptions(repo)
|
||||
opts.NewBranch = "develop"
|
||||
fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
|
||||
assert.Nil(t, fileResponse)
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
assert.Nil(t, filesResponse)
|
||||
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 := getUpdateRepoFileOptions(repo)
|
||||
opts.TreePath = ""
|
||||
fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
|
||||
assert.Nil(t, fileResponse)
|
||||
opts := getUpdateRepoFilesOptions(repo)
|
||||
opts.Files[0].TreePath = ""
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
assert.Nil(t, filesResponse)
|
||||
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 := getUpdateRepoFileOptions(repo)
|
||||
opts.TreePath = ".git"
|
||||
fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
|
||||
assert.Nil(t, fileResponse)
|
||||
opts := getUpdateRepoFilesOptions(repo)
|
||||
opts.Files[0].TreePath = ".git"
|
||||
filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
assert.Nil(t, filesResponse)
|
||||
assert.Error(t, err)
|
||||
expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]"
|
||||
expectedError := "path contains a malformed path component [path: " + opts.Files[0].TreePath + "]"
|
||||
assert.EqualError(t, err, expectedError)
|
||||
})
|
||||
|
||||
t.Run("create file that already exists", func(t *testing.T) {
|
||||
opts := getCreateRepoFileOptions(repo)
|
||||
opts.TreePath = "README.md" // already exists
|
||||
fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
|
||||
opts := getCreateRepoFilesOptions(repo)
|
||||
opts.Files[0].TreePath = "README.md" // already exists
|
||||
fileResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
|
||||
assert.Nil(t, fileResponse)
|
||||
assert.Error(t, err)
|
||||
expectedError := "repository file already exists [path: " + opts.TreePath + "]"
|
||||
expectedError := "repository file already exists [path: " + opts.Files[0].TreePath + "]"
|
||||
assert.EqualError(t, err, expectedError)
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user