mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08: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:
		@@ -64,6 +64,35 @@ func (o *UpdateFileOptions) Branch() string {
 | 
				
			|||||||
	return o.FileOptions.BranchName
 | 
						return o.FileOptions.BranchName
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ChangeFileOperation for creating, updating or deleting a file
 | 
				
			||||||
 | 
					type ChangeFileOperation struct {
 | 
				
			||||||
 | 
						// indicates what to do with the file
 | 
				
			||||||
 | 
						// required: true
 | 
				
			||||||
 | 
						// enum: create,update,delete
 | 
				
			||||||
 | 
						Operation string `json:"operation" binding:"Required"`
 | 
				
			||||||
 | 
						// path to the existing or new file
 | 
				
			||||||
 | 
						Path string `json:"path" binding:"MaxSize(500)"`
 | 
				
			||||||
 | 
						// content must be base64 encoded
 | 
				
			||||||
 | 
						// required: true
 | 
				
			||||||
 | 
						Content string `json:"content"`
 | 
				
			||||||
 | 
						// sha is the SHA for the file that already exists, required for update, delete
 | 
				
			||||||
 | 
						SHA string `json:"sha"`
 | 
				
			||||||
 | 
						// old path of the file to move
 | 
				
			||||||
 | 
						FromPath string `json:"from_path"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ChangeFilesOptions options for creating, updating or deleting multiple files
 | 
				
			||||||
 | 
					// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
 | 
				
			||||||
 | 
					type ChangeFilesOptions struct {
 | 
				
			||||||
 | 
						FileOptions
 | 
				
			||||||
 | 
						Files []*ChangeFileOperation `json:"files"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Branch returns branch name
 | 
				
			||||||
 | 
					func (o *ChangeFilesOptions) Branch() string {
 | 
				
			||||||
 | 
						return o.FileOptions.BranchName
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FileOptionInterface provides a unified interface for the different file options
 | 
					// FileOptionInterface provides a unified interface for the different file options
 | 
				
			||||||
type FileOptionInterface interface {
 | 
					type FileOptionInterface interface {
 | 
				
			||||||
	Branch() string
 | 
						Branch() string
 | 
				
			||||||
@@ -126,6 +155,13 @@ type FileResponse struct {
 | 
				
			|||||||
	Verification *PayloadCommitVerification `json:"verification"`
 | 
						Verification *PayloadCommitVerification `json:"verification"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FilesResponse contains information about multiple files from a repo
 | 
				
			||||||
 | 
					type FilesResponse struct {
 | 
				
			||||||
 | 
						Files        []*ContentsResponse        `json:"files"`
 | 
				
			||||||
 | 
						Commit       *FileCommitResponse        `json:"commit"`
 | 
				
			||||||
 | 
						Verification *PayloadCommitVerification `json:"verification"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FileDeleteResponse contains information about a repo's file that was deleted
 | 
					// FileDeleteResponse contains information about a repo's file that was deleted
 | 
				
			||||||
type FileDeleteResponse struct {
 | 
					type FileDeleteResponse struct {
 | 
				
			||||||
	Content      interface{}                `json:"content"` // to be set to nil
 | 
						Content      interface{}                `json:"content"` // to be set to nil
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1173,6 +1173,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 | 
				
			|||||||
				m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(auth_model.AccessTokenScopeRepo), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
 | 
									m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(auth_model.AccessTokenScopeRepo), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
 | 
				
			||||||
				m.Group("/contents", func() {
 | 
									m.Group("/contents", func() {
 | 
				
			||||||
					m.Get("", repo.GetContentsList)
 | 
										m.Get("", repo.GetContentsList)
 | 
				
			||||||
 | 
										m.Post("", reqToken(auth_model.AccessTokenScopeRepo), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, repo.ChangeFiles)
 | 
				
			||||||
					m.Get("/*", repo.GetContents)
 | 
										m.Get("/*", repo.GetContents)
 | 
				
			||||||
					m.Group("/*", func() {
 | 
										m.Group("/*", func() {
 | 
				
			||||||
						m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
 | 
											m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ import (
 | 
				
			|||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
@@ -407,6 +408,96 @@ func canReadFiles(r *context.Repository) bool {
 | 
				
			|||||||
	return r.Permission.CanRead(unit.TypeCode)
 | 
						return r.Permission.CanRead(unit.TypeCode)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ChangeFiles handles API call for creating or updating multiple files
 | 
				
			||||||
 | 
					func ChangeFiles(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Create or update multiple files in a repository
 | 
				
			||||||
 | 
						// consumes:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: owner
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: owner of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: repo
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: body
 | 
				
			||||||
 | 
						//   in: body
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						//   schema:
 | 
				
			||||||
 | 
						//     "$ref": "#/definitions/ChangeFilesOptions"
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "201":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/FilesResponse"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/error"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/error"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiOpts := web.GetForm(ctx).(*api.ChangeFilesOptions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if apiOpts.BranchName == "" {
 | 
				
			||||||
 | 
							apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						files := []*files_service.ChangeRepoFile{}
 | 
				
			||||||
 | 
						for _, file := range apiOpts.Files {
 | 
				
			||||||
 | 
							changeRepoFile := &files_service.ChangeRepoFile{
 | 
				
			||||||
 | 
								Operation:    file.Operation,
 | 
				
			||||||
 | 
								TreePath:     file.Path,
 | 
				
			||||||
 | 
								FromTreePath: file.FromPath,
 | 
				
			||||||
 | 
								Content:      file.Content,
 | 
				
			||||||
 | 
								SHA:          file.SHA,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							files = append(files, changeRepoFile)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opts := &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
 | 
							Files:     files,
 | 
				
			||||||
 | 
							Message:   apiOpts.Message,
 | 
				
			||||||
 | 
							OldBranch: apiOpts.BranchName,
 | 
				
			||||||
 | 
							NewBranch: apiOpts.NewBranchName,
 | 
				
			||||||
 | 
							Committer: &files_service.IdentityOptions{
 | 
				
			||||||
 | 
								Name:  apiOpts.Committer.Name,
 | 
				
			||||||
 | 
								Email: apiOpts.Committer.Email,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Author: &files_service.IdentityOptions{
 | 
				
			||||||
 | 
								Name:  apiOpts.Author.Name,
 | 
				
			||||||
 | 
								Email: apiOpts.Author.Email,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Dates: &files_service.CommitDateOptions{
 | 
				
			||||||
 | 
								Author:    apiOpts.Dates.Author,
 | 
				
			||||||
 | 
								Committer: apiOpts.Dates.Committer,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Signoff: apiOpts.Signoff,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.Dates.Author.IsZero() {
 | 
				
			||||||
 | 
							opts.Dates.Author = time.Now()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.Dates.Committer.IsZero() {
 | 
				
			||||||
 | 
							opts.Dates.Committer = time.Now()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if opts.Message == "" {
 | 
				
			||||||
 | 
							opts.Message = changeFilesCommitMessage(ctx, files)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
 | 
				
			||||||
 | 
							handleCreateOrUpdateFileError(ctx, err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							ctx.JSON(http.StatusCreated, filesResponse)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateFile handles API call for creating a file
 | 
					// CreateFile handles API call for creating a file
 | 
				
			||||||
func CreateFile(ctx *context.APIContext) {
 | 
					func CreateFile(ctx *context.APIContext) {
 | 
				
			||||||
	// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
 | 
						// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
 | 
				
			||||||
@@ -453,11 +544,15 @@ func CreateFile(ctx *context.APIContext) {
 | 
				
			|||||||
		apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
 | 
							apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	opts := &files_service.UpdateRepoFileOptions{
 | 
						opts := &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
		Content:   apiOpts.Content,
 | 
							Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
		IsNewFile: true,
 | 
								{
 | 
				
			||||||
 | 
									Operation: "create",
 | 
				
			||||||
 | 
									TreePath:  ctx.Params("*"),
 | 
				
			||||||
 | 
									Content:   apiOpts.Content,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		Message:   apiOpts.Message,
 | 
							Message:   apiOpts.Message,
 | 
				
			||||||
		TreePath:  ctx.Params("*"),
 | 
					 | 
				
			||||||
		OldBranch: apiOpts.BranchName,
 | 
							OldBranch: apiOpts.BranchName,
 | 
				
			||||||
		NewBranch: apiOpts.NewBranchName,
 | 
							NewBranch: apiOpts.NewBranchName,
 | 
				
			||||||
		Committer: &files_service.IdentityOptions{
 | 
							Committer: &files_service.IdentityOptions{
 | 
				
			||||||
@@ -482,12 +577,13 @@ func CreateFile(ctx *context.APIContext) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if opts.Message == "" {
 | 
						if opts.Message == "" {
 | 
				
			||||||
		opts.Message = ctx.Tr("repo.editor.add", opts.TreePath)
 | 
							opts.Message = changeFilesCommitMessage(ctx, opts.Files)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
 | 
						if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
 | 
				
			||||||
		handleCreateOrUpdateFileError(ctx, err)
 | 
							handleCreateOrUpdateFileError(ctx, err)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
 | 
				
			||||||
		ctx.JSON(http.StatusCreated, fileResponse)
 | 
							ctx.JSON(http.StatusCreated, fileResponse)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -540,15 +636,19 @@ func UpdateFile(ctx *context.APIContext) {
 | 
				
			|||||||
		apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
 | 
							apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	opts := &files_service.UpdateRepoFileOptions{
 | 
						opts := &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
		Content:      apiOpts.Content,
 | 
							Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
		SHA:          apiOpts.SHA,
 | 
								{
 | 
				
			||||||
		IsNewFile:    false,
 | 
									Operation:    "update",
 | 
				
			||||||
		Message:      apiOpts.Message,
 | 
									Content:      apiOpts.Content,
 | 
				
			||||||
		FromTreePath: apiOpts.FromPath,
 | 
									SHA:          apiOpts.SHA,
 | 
				
			||||||
		TreePath:     ctx.Params("*"),
 | 
									FromTreePath: apiOpts.FromPath,
 | 
				
			||||||
		OldBranch:    apiOpts.BranchName,
 | 
									TreePath:     ctx.Params("*"),
 | 
				
			||||||
		NewBranch:    apiOpts.NewBranchName,
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Message:   apiOpts.Message,
 | 
				
			||||||
 | 
							OldBranch: apiOpts.BranchName,
 | 
				
			||||||
 | 
							NewBranch: apiOpts.NewBranchName,
 | 
				
			||||||
		Committer: &files_service.IdentityOptions{
 | 
							Committer: &files_service.IdentityOptions{
 | 
				
			||||||
			Name:  apiOpts.Committer.Name,
 | 
								Name:  apiOpts.Committer.Name,
 | 
				
			||||||
			Email: apiOpts.Committer.Email,
 | 
								Email: apiOpts.Committer.Email,
 | 
				
			||||||
@@ -571,12 +671,13 @@ func UpdateFile(ctx *context.APIContext) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if opts.Message == "" {
 | 
						if opts.Message == "" {
 | 
				
			||||||
		opts.Message = ctx.Tr("repo.editor.update", opts.TreePath)
 | 
							opts.Message = changeFilesCommitMessage(ctx, opts.Files)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
 | 
						if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
 | 
				
			||||||
		handleCreateOrUpdateFileError(ctx, err)
 | 
							handleCreateOrUpdateFileError(ctx, err)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
 | 
				
			||||||
		ctx.JSON(http.StatusOK, fileResponse)
 | 
							ctx.JSON(http.StatusOK, fileResponse)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -600,7 +701,7 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Called from both CreateFile or UpdateFile to handle both
 | 
					// Called from both CreateFile or UpdateFile to handle both
 | 
				
			||||||
func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoFileOptions) (*api.FileResponse, error) {
 | 
					func createOrUpdateFiles(ctx *context.APIContext, opts *files_service.ChangeRepoFilesOptions) (*api.FilesResponse, error) {
 | 
				
			||||||
	if !canWriteFiles(ctx, opts.OldBranch) {
 | 
						if !canWriteFiles(ctx, opts.OldBranch) {
 | 
				
			||||||
		return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
 | 
							return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
 | 
				
			||||||
			UserID:   ctx.Doer.ID,
 | 
								UserID:   ctx.Doer.ID,
 | 
				
			||||||
@@ -608,13 +709,45 @@ func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoF
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	content, err := base64.StdEncoding.DecodeString(opts.Content)
 | 
						for _, file := range opts.Files {
 | 
				
			||||||
	if err != nil {
 | 
							content, err := base64.StdEncoding.DecodeString(file.Content)
 | 
				
			||||||
		return nil, err
 | 
							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
 | 
					// DeleteFile Delete a file in a repository
 | 
				
			||||||
@@ -670,12 +803,17 @@ func DeleteFile(ctx *context.APIContext) {
 | 
				
			|||||||
		apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
 | 
							apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	opts := &files_service.DeleteRepoFileOptions{
 | 
						opts := &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
 | 
							Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Operation: "delete",
 | 
				
			||||||
 | 
									SHA:       apiOpts.SHA,
 | 
				
			||||||
 | 
									TreePath:  ctx.Params("*"),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		Message:   apiOpts.Message,
 | 
							Message:   apiOpts.Message,
 | 
				
			||||||
		OldBranch: apiOpts.BranchName,
 | 
							OldBranch: apiOpts.BranchName,
 | 
				
			||||||
		NewBranch: apiOpts.NewBranchName,
 | 
							NewBranch: apiOpts.NewBranchName,
 | 
				
			||||||
		SHA:       apiOpts.SHA,
 | 
					 | 
				
			||||||
		TreePath:  ctx.Params("*"),
 | 
					 | 
				
			||||||
		Committer: &files_service.IdentityOptions{
 | 
							Committer: &files_service.IdentityOptions{
 | 
				
			||||||
			Name:  apiOpts.Committer.Name,
 | 
								Name:  apiOpts.Committer.Name,
 | 
				
			||||||
			Email: apiOpts.Committer.Email,
 | 
								Email: apiOpts.Committer.Email,
 | 
				
			||||||
@@ -698,10 +836,10 @@ func DeleteFile(ctx *context.APIContext) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if opts.Message == "" {
 | 
						if opts.Message == "" {
 | 
				
			||||||
		opts.Message = ctx.Tr("repo.editor.delete", opts.TreePath)
 | 
							opts.Message = changeFilesCommitMessage(ctx, opts.Files)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if fileResponse, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
 | 
						if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
 | 
				
			||||||
		if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
 | 
							if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
 | 
				
			||||||
			ctx.Error(http.StatusNotFound, "DeleteFile", err)
 | 
								ctx.Error(http.StatusNotFound, "DeleteFile", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@@ -718,6 +856,7 @@ func DeleteFile(ctx *context.APIContext) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
 | 
							ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
 | 
				
			||||||
		ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
 | 
							ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -116,6 +116,9 @@ type swaggerParameterBodies struct {
 | 
				
			|||||||
	// in:body
 | 
						// in:body
 | 
				
			||||||
	EditAttachmentOptions api.EditAttachmentOptions
 | 
						EditAttachmentOptions api.EditAttachmentOptions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// in:body
 | 
				
			||||||
 | 
						ChangeFilesOptions api.ChangeFilesOptions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// in:body
 | 
						// in:body
 | 
				
			||||||
	CreateFileOptions api.CreateFileOptions
 | 
						CreateFileOptions api.CreateFileOptions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -296,6 +296,13 @@ type swaggerFileResponse struct {
 | 
				
			|||||||
	Body api.FileResponse `json:"body"`
 | 
						Body api.FileResponse `json:"body"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FilesResponse
 | 
				
			||||||
 | 
					// swagger:response FilesResponse
 | 
				
			||||||
 | 
					type swaggerFilesResponse struct {
 | 
				
			||||||
 | 
						// in: body
 | 
				
			||||||
 | 
						Body api.FilesResponse `json:"body"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ContentsResponse
 | 
					// ContentsResponse
 | 
				
			||||||
// swagger:response ContentsResponse
 | 
					// swagger:response ContentsResponse
 | 
				
			||||||
type swaggerContentsResponse struct {
 | 
					type swaggerContentsResponse struct {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -272,18 +272,27 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
 | 
				
			|||||||
		message += "\n\n" + form.CommitMessage
 | 
							message += "\n\n" + form.CommitMessage
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err := files_service.CreateOrUpdateRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UpdateRepoFileOptions{
 | 
						operation := "update"
 | 
				
			||||||
 | 
						if isNewFile {
 | 
				
			||||||
 | 
							operation = "create"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
		LastCommitID: form.LastCommit,
 | 
							LastCommitID: form.LastCommit,
 | 
				
			||||||
		OldBranch:    ctx.Repo.BranchName,
 | 
							OldBranch:    ctx.Repo.BranchName,
 | 
				
			||||||
		NewBranch:    branchName,
 | 
							NewBranch:    branchName,
 | 
				
			||||||
		FromTreePath: ctx.Repo.TreePath,
 | 
					 | 
				
			||||||
		TreePath:     form.TreePath,
 | 
					 | 
				
			||||||
		Message:      message,
 | 
							Message:      message,
 | 
				
			||||||
		Content:      strings.ReplaceAll(form.Content, "\r", ""),
 | 
							Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
		IsNewFile:    isNewFile,
 | 
								{
 | 
				
			||||||
		Signoff:      form.Signoff,
 | 
									Operation:    operation,
 | 
				
			||||||
 | 
									FromTreePath: ctx.Repo.TreePath,
 | 
				
			||||||
 | 
									TreePath:     form.TreePath,
 | 
				
			||||||
 | 
									Content:      strings.ReplaceAll(form.Content, "\r", ""),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Signoff: form.Signoff,
 | 
				
			||||||
	}); err != nil {
 | 
						}); err != nil {
 | 
				
			||||||
		// This is where we handle all the errors thrown by files_service.CreateOrUpdateRepoFile
 | 
							// This is where we handle all the errors thrown by files_service.ChangeRepoFiles
 | 
				
			||||||
		if git.IsErrNotExist(err) {
 | 
							if git.IsErrNotExist(err) {
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
 | 
				
			||||||
		} else if git_model.IsErrLFSFileLocked(err) {
 | 
							} else if git_model.IsErrLFSFileLocked(err) {
 | 
				
			||||||
@@ -478,13 +487,18 @@ func DeleteFilePost(ctx *context.Context) {
 | 
				
			|||||||
		message += "\n\n" + form.CommitMessage
 | 
							message += "\n\n" + form.CommitMessage
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.DeleteRepoFileOptions{
 | 
						if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
		LastCommitID: form.LastCommit,
 | 
							LastCommitID: form.LastCommit,
 | 
				
			||||||
		OldBranch:    ctx.Repo.BranchName,
 | 
							OldBranch:    ctx.Repo.BranchName,
 | 
				
			||||||
		NewBranch:    branchName,
 | 
							NewBranch:    branchName,
 | 
				
			||||||
		TreePath:     ctx.Repo.TreePath,
 | 
							Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
		Message:      message,
 | 
								{
 | 
				
			||||||
		Signoff:      form.Signoff,
 | 
									Operation: "delete",
 | 
				
			||||||
 | 
									TreePath:  ctx.Repo.TreePath,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Message: message,
 | 
				
			||||||
 | 
							Signoff: form.Signoff,
 | 
				
			||||||
	}); err != nil {
 | 
						}); err != nil {
 | 
				
			||||||
		// This is where we handle all the errors thrown by repofiles.DeleteRepoFile
 | 
							// This is where we handle all the errors thrown by repofiles.DeleteRepoFile
 | 
				
			||||||
		if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
 | 
							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"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch string, treeNames []string) (*api.FilesResponse, error) {
 | 
				
			||||||
 | 
						files := []*api.ContentsResponse{}
 | 
				
			||||||
 | 
						for _, file := range treeNames {
 | 
				
			||||||
 | 
							fileContents, _ := GetContents(ctx, repo, file, branch, false) // ok if fails, then will be nil
 | 
				
			||||||
 | 
							files = append(files, fileContents)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
 | 
				
			||||||
 | 
						verification := GetPayloadCommitVerification(ctx, commit)
 | 
				
			||||||
 | 
						filesResponse := &api.FilesResponse{
 | 
				
			||||||
 | 
							Files:        files,
 | 
				
			||||||
 | 
							Commit:       fileCommitResponse,
 | 
				
			||||||
 | 
							Verification: verification,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return filesResponse, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetFileResponseFromCommit Constructs a FileResponse from a Commit object
 | 
					// GetFileResponseFromCommit Constructs a FileResponse from a Commit object
 | 
				
			||||||
func GetFileResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) {
 | 
					func GetFileResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) {
 | 
				
			||||||
	fileContents, _ := GetContents(ctx, repo, treeName, branch, false) // ok if fails, then will be nil
 | 
						fileContents, _ := GetContents(ctx, repo, treeName, branch, false) // ok if fails, then will be nil
 | 
				
			||||||
@@ -30,6 +46,20 @@ func GetFileResponseFromCommit(ctx context.Context, repo *repo_model.Repository,
 | 
				
			|||||||
	return fileResponse, nil
 | 
						return fileResponse, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// constructs a FileResponse with the file at the index from FilesResponse
 | 
				
			||||||
 | 
					func GetFileResponseFromFilesResponse(filesResponse *api.FilesResponse, index int) *api.FileResponse {
 | 
				
			||||||
 | 
						content := &api.ContentsResponse{}
 | 
				
			||||||
 | 
						if len(filesResponse.Files) > index {
 | 
				
			||||||
 | 
							content = filesResponse.Files[index]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fileResponse := &api.FileResponse{
 | 
				
			||||||
 | 
							Content:      content,
 | 
				
			||||||
 | 
							Commit:       filesResponse.Commit,
 | 
				
			||||||
 | 
							Verification: filesResponse.Verification,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return fileResponse
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
 | 
					// GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
 | 
				
			||||||
func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) {
 | 
					func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) {
 | 
				
			||||||
	if repo == nil {
 | 
						if repo == nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,23 +41,36 @@ type CommitDateOptions struct {
 | 
				
			|||||||
	Committer time.Time
 | 
						Committer time.Time
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UpdateRepoFileOptions holds the repository file update options
 | 
					type ChangeRepoFile struct {
 | 
				
			||||||
type UpdateRepoFileOptions 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
 | 
						LastCommitID string
 | 
				
			||||||
	OldBranch    string
 | 
						OldBranch    string
 | 
				
			||||||
	NewBranch    string
 | 
						NewBranch    string
 | 
				
			||||||
	TreePath     string
 | 
					 | 
				
			||||||
	FromTreePath string
 | 
					 | 
				
			||||||
	Message      string
 | 
						Message      string
 | 
				
			||||||
	Content      string
 | 
						Files        []*ChangeRepoFile
 | 
				
			||||||
	SHA          string
 | 
					 | 
				
			||||||
	IsNewFile    bool
 | 
					 | 
				
			||||||
	Author       *IdentityOptions
 | 
						Author       *IdentityOptions
 | 
				
			||||||
	Committer    *IdentityOptions
 | 
						Committer    *IdentityOptions
 | 
				
			||||||
	Dates        *CommitDateOptions
 | 
						Dates        *CommitDateOptions
 | 
				
			||||||
	Signoff      bool
 | 
						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) {
 | 
					func detectEncodingAndBOM(entry *git.TreeEntry, repo *repo_model.Repository) (string, bool) {
 | 
				
			||||||
	reader, err := entry.Blob().DataAsync()
 | 
						reader, err := entry.Blob().DataAsync()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -125,8 +138,8 @@ func detectEncodingAndBOM(entry *git.TreeEntry, repo *repo_model.Repository) (st
 | 
				
			|||||||
	return encoding, false
 | 
						return encoding, false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateOrUpdateRepoFile adds or updates a file in the given repository
 | 
					// ChangeRepoFiles adds, updates or removes multiple files in the given repository
 | 
				
			||||||
func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UpdateRepoFileOptions) (*structs.FileResponse, error) {
 | 
					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 no branch name is set, assume default branch
 | 
				
			||||||
	if opts.OldBranch == "" {
 | 
						if opts.OldBranch == "" {
 | 
				
			||||||
		opts.OldBranch = repo.DefaultBranch
 | 
							opts.OldBranch = repo.DefaultBranch
 | 
				
			||||||
@@ -146,6 +159,38 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
 | 
				
			|||||||
		return nil, err
 | 
							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.
 | 
						// 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.
 | 
						// 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 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) {
 | 
							if err != nil && !git.IsErrBranchNotExist(err) {
 | 
				
			||||||
			return nil, 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
 | 
							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)
 | 
						message := strings.TrimSpace(opts.Message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
 | 
						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()
 | 
						defer t.Close()
 | 
				
			||||||
	hasOldBranch := true
 | 
						hasOldBranch := true
 | 
				
			||||||
	if err := t.Clone(opts.OldBranch); err != nil {
 | 
						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 {
 | 
							if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -209,9 +239,29 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	encoding := "UTF-8"
 | 
						for _, file := range opts.Files {
 | 
				
			||||||
	bom := false
 | 
							if file.Operation == "delete" {
 | 
				
			||||||
	executable := false
 | 
								// 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 {
 | 
						if hasOldBranch {
 | 
				
			||||||
		// Get the commit of the original branch
 | 
							// Get the commit of the original branch
 | 
				
			||||||
@@ -232,176 +282,27 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if !opts.IsNewFile {
 | 
							for _, file := range opts.Files {
 | 
				
			||||||
			fromEntry, err := commit.GetTreeEntryByPath(fromTreePath)
 | 
								if err := handleCheckErrors(file, commit, opts, repo); err != nil {
 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return nil, err
 | 
									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
 | 
						contentStore := lfs.NewContentStore()
 | 
				
			||||||
		// sure no parts of the path are existing files or links except for the last
 | 
						for _, file := range opts.Files {
 | 
				
			||||||
		// item in the path which is the file name, and that shouldn't exist IF it is
 | 
							switch file.Operation {
 | 
				
			||||||
		// a new file OR is being moved to a new path.
 | 
							case "create", "update":
 | 
				
			||||||
		treePathParts := strings.Split(treePath, "/")
 | 
								if err := CreateOrUpdateFile(ctx, t, file, contentStore, repo.ID, hasOldBranch); err != nil {
 | 
				
			||||||
		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 nil, err
 | 
									return nil, err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if index < len(treePathParts)-1 {
 | 
							case "delete":
 | 
				
			||||||
				if !entry.IsDir() {
 | 
								// Remove the file from the index
 | 
				
			||||||
					return nil, models.ErrFilePathInvalid{
 | 
								if err := t.RemoveFilesFromIndex(file.TreePath); err != nil {
 | 
				
			||||||
						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 {
 | 
					 | 
				
			||||||
				return nil, err
 | 
									return nil, err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID}
 | 
							default:
 | 
				
			||||||
			content = pointer.StringContent()
 | 
								return nil, fmt.Errorf("Invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath)
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	// 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
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -422,27 +323,6 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
 | 
				
			|||||||
		return nil, err
 | 
							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
 | 
						// Then push this tree to NewBranch
 | 
				
			||||||
	if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
 | 
						if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
 | 
				
			||||||
		log.Error("%T %v", err, err)
 | 
							log.Error("%T %v", err, err)
 | 
				
			||||||
@@ -454,7 +334,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
 | 
				
			|||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	file, err := GetFileResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePath)
 | 
						filesReponse, err := GetFilesResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePaths)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							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")
 | 
							_ = 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
 | 
					// 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)
 | 
						protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if protectedBranch != nil {
 | 
						if protectedBranch != nil {
 | 
				
			||||||
		protectedBranch.Repo = repo
 | 
							protectedBranch.Repo = repo
 | 
				
			||||||
		isUnprotectedFile := false
 | 
							globUnprotected := protectedBranch.GetUnprotectedFilePatterns()
 | 
				
			||||||
		glob := protectedBranch.GetUnprotectedFilePatterns()
 | 
							globProtected := protectedBranch.GetProtectedFilePatterns()
 | 
				
			||||||
		if len(glob) != 0 {
 | 
							canUserPush := protectedBranch.CanUserPush(ctx, doer)
 | 
				
			||||||
			isUnprotectedFile = protectedBranch.IsUnprotectedFile(glob, treePath)
 | 
							for _, treePath := range treePaths {
 | 
				
			||||||
		}
 | 
								isUnprotectedFile := false
 | 
				
			||||||
		if !protectedBranch.CanUserPush(ctx, doer) && !isUnprotectedFile {
 | 
								if len(globUnprotected) != 0 {
 | 
				
			||||||
			return models.ErrUserCannotCommit{
 | 
									isUnprotectedFile = protectedBranch.IsUnprotectedFile(globUnprotected, treePath)
 | 
				
			||||||
				UserName: doer.LowerName,
 | 
								}
 | 
				
			||||||
 | 
								if !canUserPush && !isUnprotectedFile {
 | 
				
			||||||
 | 
									return models.ErrUserCannotCommit{
 | 
				
			||||||
 | 
										UserName: doer.LowerName,
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if protectedBranch.IsProtectedFile(globProtected, treePath) {
 | 
				
			||||||
 | 
									return models.ErrFilePathProtected{
 | 
				
			||||||
 | 
										Path: treePath,
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if protectedBranch.RequireSignedCommits {
 | 
							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
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										161
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										161
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							@@ -4063,6 +4063,57 @@
 | 
				
			|||||||
            "$ref": "#/responses/notFound"
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "post": {
 | 
				
			||||||
 | 
					        "consumes": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "produces": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "repository"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Create or update multiple files in a repository",
 | 
				
			||||||
 | 
					        "operationId": "repoChangeFiles",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "owner of the repo",
 | 
				
			||||||
 | 
					            "name": "owner",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "name of the repo",
 | 
				
			||||||
 | 
					            "name": "repo",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "body",
 | 
				
			||||||
 | 
					            "in": "body",
 | 
				
			||||||
 | 
					            "required": true,
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "$ref": "#/definitions/ChangeFilesOptions"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "201": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/FilesResponse"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "403": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/error"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "422": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/error"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "/repos/{owner}/{repo}/contents/{filepath}": {
 | 
					    "/repos/{owner}/{repo}/contents/{filepath}": {
 | 
				
			||||||
@@ -15891,6 +15942,90 @@
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
					      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "ChangeFileOperation": {
 | 
				
			||||||
 | 
					      "description": "ChangeFileOperation for creating, updating or deleting a file",
 | 
				
			||||||
 | 
					      "type": "object",
 | 
				
			||||||
 | 
					      "required": [
 | 
				
			||||||
 | 
					        "operation",
 | 
				
			||||||
 | 
					        "content"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "properties": {
 | 
				
			||||||
 | 
					        "content": {
 | 
				
			||||||
 | 
					          "description": "content must be base64 encoded",
 | 
				
			||||||
 | 
					          "type": "string",
 | 
				
			||||||
 | 
					          "x-go-name": "Content"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "from_path": {
 | 
				
			||||||
 | 
					          "description": "old path of the file to move",
 | 
				
			||||||
 | 
					          "type": "string",
 | 
				
			||||||
 | 
					          "x-go-name": "FromPath"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "operation": {
 | 
				
			||||||
 | 
					          "description": "indicates what to do with the file",
 | 
				
			||||||
 | 
					          "type": "string",
 | 
				
			||||||
 | 
					          "enum": [
 | 
				
			||||||
 | 
					            "create",
 | 
				
			||||||
 | 
					            "update",
 | 
				
			||||||
 | 
					            "delete"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "x-go-name": "Operation"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "path": {
 | 
				
			||||||
 | 
					          "description": "path to the existing or new file",
 | 
				
			||||||
 | 
					          "type": "string",
 | 
				
			||||||
 | 
					          "x-go-name": "Path"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "sha": {
 | 
				
			||||||
 | 
					          "description": "sha is the SHA for the file that already exists, required for update, delete",
 | 
				
			||||||
 | 
					          "type": "string",
 | 
				
			||||||
 | 
					          "x-go-name": "SHA"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "ChangeFilesOptions": {
 | 
				
			||||||
 | 
					      "description": "ChangeFilesOptions options for creating, updating or deleting multiple files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)",
 | 
				
			||||||
 | 
					      "type": "object",
 | 
				
			||||||
 | 
					      "properties": {
 | 
				
			||||||
 | 
					        "author": {
 | 
				
			||||||
 | 
					          "$ref": "#/definitions/Identity"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "branch": {
 | 
				
			||||||
 | 
					          "description": "branch (optional) to base this file from. if not given, the default branch is used",
 | 
				
			||||||
 | 
					          "type": "string",
 | 
				
			||||||
 | 
					          "x-go-name": "BranchName"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "committer": {
 | 
				
			||||||
 | 
					          "$ref": "#/definitions/Identity"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "dates": {
 | 
				
			||||||
 | 
					          "$ref": "#/definitions/CommitDateOptions"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "files": {
 | 
				
			||||||
 | 
					          "type": "array",
 | 
				
			||||||
 | 
					          "items": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/ChangeFileOperation"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "x-go-name": "Files"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "message": {
 | 
				
			||||||
 | 
					          "description": "message (optional) for the commit of this file. if not supplied, a default message will be used",
 | 
				
			||||||
 | 
					          "type": "string",
 | 
				
			||||||
 | 
					          "x-go-name": "Message"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "new_branch": {
 | 
				
			||||||
 | 
					          "description": "new_branch (optional) will make a new branch from `branch` before creating the file",
 | 
				
			||||||
 | 
					          "type": "string",
 | 
				
			||||||
 | 
					          "x-go-name": "NewBranchName"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "signoff": {
 | 
				
			||||||
 | 
					          "description": "Add a Signed-off-by trailer by the committer at the end of the commit log message.",
 | 
				
			||||||
 | 
					          "type": "boolean",
 | 
				
			||||||
 | 
					          "x-go-name": "Signoff"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "ChangedFile": {
 | 
					    "ChangedFile": {
 | 
				
			||||||
      "description": "ChangedFile store information about files affected by the pull request",
 | 
					      "description": "ChangedFile store information about files affected by the pull request",
 | 
				
			||||||
      "type": "object",
 | 
					      "type": "object",
 | 
				
			||||||
@@ -18326,6 +18461,26 @@
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
					      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "FilesResponse": {
 | 
				
			||||||
 | 
					      "description": "FilesResponse contains information about multiple files from a repo",
 | 
				
			||||||
 | 
					      "type": "object",
 | 
				
			||||||
 | 
					      "properties": {
 | 
				
			||||||
 | 
					        "commit": {
 | 
				
			||||||
 | 
					          "$ref": "#/definitions/FileCommitResponse"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "files": {
 | 
				
			||||||
 | 
					          "type": "array",
 | 
				
			||||||
 | 
					          "items": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/ContentsResponse"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "x-go-name": "Files"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "verification": {
 | 
				
			||||||
 | 
					          "$ref": "#/definitions/PayloadCommitVerification"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "GPGKey": {
 | 
					    "GPGKey": {
 | 
				
			||||||
      "description": "GPGKey a user GPG key to sign commit and tag in repository",
 | 
					      "description": "GPGKey a user GPG key to sign commit and tag in repository",
 | 
				
			||||||
      "type": "object",
 | 
					      "type": "object",
 | 
				
			||||||
@@ -21996,6 +22151,12 @@
 | 
				
			|||||||
        "$ref": "#/definitions/FileResponse"
 | 
					        "$ref": "#/definitions/FileResponse"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "FilesResponse": {
 | 
				
			||||||
 | 
					      "description": "FilesResponse",
 | 
				
			||||||
 | 
					      "schema": {
 | 
				
			||||||
 | 
					        "$ref": "#/definitions/FilesResponse"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "GPGKey": {
 | 
					    "GPGKey": {
 | 
				
			||||||
      "description": "GPGKey",
 | 
					      "description": "GPGKey",
 | 
				
			||||||
      "schema": {
 | 
					      "schema": {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,18 +11,22 @@ import (
 | 
				
			|||||||
	files_service "code.gitea.io/gitea/services/repository/files"
 | 
						files_service "code.gitea.io/gitea/services/repository/files"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FileResponse, error) {
 | 
					func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FilesResponse, error) {
 | 
				
			||||||
	opts := &files_service.UpdateRepoFileOptions{
 | 
						opts := &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
 | 
							Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Operation: "create",
 | 
				
			||||||
 | 
									TreePath:  treePath,
 | 
				
			||||||
 | 
									Content:   content,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		OldBranch: branchName,
 | 
							OldBranch: branchName,
 | 
				
			||||||
		TreePath:  treePath,
 | 
					 | 
				
			||||||
		Content:   content,
 | 
					 | 
				
			||||||
		IsNewFile: true,
 | 
					 | 
				
			||||||
		Author:    nil,
 | 
							Author:    nil,
 | 
				
			||||||
		Committer: nil,
 | 
							Committer: nil,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, user, opts)
 | 
						return files_service.ChangeRepoFiles(git.DefaultContext, repo, user, opts)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func createFile(user *user_model.User, repo *repo_model.Repository, treePath string) (*api.FileResponse, error) {
 | 
					func createFile(user *user_model.User, repo *repo_model.Repository, treePath string) (*api.FilesResponse, error) {
 | 
				
			||||||
	return createFileInBranch(user, repo, treePath, repo.DefaultBranch, "This is a NEW file")
 | 
						return createFileInBranch(user, repo, treePath, repo.DefaultBranch, "This is a NEW file")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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)
 | 
							assert.NotEmpty(t, baseRepo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// create a commit on new branch.
 | 
							// create a commit on new branch.
 | 
				
			||||||
		_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
 | 
							_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
			TreePath:  "important_file",
 | 
								Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										Operation: "create",
 | 
				
			||||||
 | 
										TreePath:  "important_file",
 | 
				
			||||||
 | 
										Content:   "Just a non-important file",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
			Message:   "Add a important file",
 | 
								Message:   "Add a important file",
 | 
				
			||||||
			Content:   "Just a non-important file",
 | 
					 | 
				
			||||||
			IsNewFile: true,
 | 
					 | 
				
			||||||
			OldBranch: "main",
 | 
								OldBranch: "main",
 | 
				
			||||||
			NewBranch: "important-secrets",
 | 
								NewBranch: "important-secrets",
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		assert.NoError(t, err)
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// create a commit on main branch.
 | 
							// create a commit on main branch.
 | 
				
			||||||
		_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
 | 
							_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
			TreePath:  "important_file",
 | 
								Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										Operation: "create",
 | 
				
			||||||
 | 
										TreePath:  "important_file",
 | 
				
			||||||
 | 
										Content:   "Not the same content :P",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
			Message:   "Add a important file",
 | 
								Message:   "Add a important file",
 | 
				
			||||||
			Content:   "Not the same content :P",
 | 
					 | 
				
			||||||
			IsNewFile: true,
 | 
					 | 
				
			||||||
			OldBranch: "main",
 | 
								OldBranch: "main",
 | 
				
			||||||
			NewBranch: "main",
 | 
								NewBranch: "main",
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -101,11 +101,15 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
 | 
				
			|||||||
	assert.NotEmpty(t, headRepo)
 | 
						assert.NotEmpty(t, headRepo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// create a commit on base Repo
 | 
						// create a commit on base Repo
 | 
				
			||||||
	_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, actor, &files_service.UpdateRepoFileOptions{
 | 
						_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, actor, &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
		TreePath:  "File_A",
 | 
							Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Operation: "create",
 | 
				
			||||||
 | 
									TreePath:  "File_A",
 | 
				
			||||||
 | 
									Content:   "File A",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		Message:   "Add File A",
 | 
							Message:   "Add File A",
 | 
				
			||||||
		Content:   "File A",
 | 
					 | 
				
			||||||
		IsNewFile: true,
 | 
					 | 
				
			||||||
		OldBranch: "master",
 | 
							OldBranch: "master",
 | 
				
			||||||
		NewBranch: "master",
 | 
							NewBranch: "master",
 | 
				
			||||||
		Author: &files_service.IdentityOptions{
 | 
							Author: &files_service.IdentityOptions{
 | 
				
			||||||
@@ -124,11 +128,15 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
 | 
				
			|||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// create a commit on head Repo
 | 
						// create a commit on head Repo
 | 
				
			||||||
	_, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, headRepo, actor, &files_service.UpdateRepoFileOptions{
 | 
						_, err = files_service.ChangeRepoFiles(git.DefaultContext, headRepo, actor, &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
		TreePath:  "File_B",
 | 
							Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Operation: "create",
 | 
				
			||||||
 | 
									TreePath:  "File_B",
 | 
				
			||||||
 | 
									Content:   "File B",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		Message:   "Add File on PR branch",
 | 
							Message:   "Add File on PR branch",
 | 
				
			||||||
		Content:   "File B",
 | 
					 | 
				
			||||||
		IsNewFile: true,
 | 
					 | 
				
			||||||
		OldBranch: "master",
 | 
							OldBranch: "master",
 | 
				
			||||||
		NewBranch: "newBranch",
 | 
							NewBranch: "newBranch",
 | 
				
			||||||
		Author: &files_service.IdentityOptions{
 | 
							Author: &files_service.IdentityOptions{
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						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/git"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	api "code.gitea.io/gitea/modules/structs"
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
@@ -19,33 +20,90 @@ import (
 | 
				
			|||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func getCreateRepoFileOptions(repo *repo_model.Repository) *files_service.UpdateRepoFileOptions {
 | 
					func getCreateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
 | 
				
			||||||
	return &files_service.UpdateRepoFileOptions{
 | 
						return &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
 | 
							Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Operation: "create",
 | 
				
			||||||
 | 
									TreePath:  "new/file.txt",
 | 
				
			||||||
 | 
									Content:   "This is a NEW file",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		OldBranch: repo.DefaultBranch,
 | 
							OldBranch: repo.DefaultBranch,
 | 
				
			||||||
		NewBranch: repo.DefaultBranch,
 | 
							NewBranch: repo.DefaultBranch,
 | 
				
			||||||
		TreePath:  "new/file.txt",
 | 
					 | 
				
			||||||
		Message:   "Creates new/file.txt",
 | 
							Message:   "Creates new/file.txt",
 | 
				
			||||||
		Content:   "This is a NEW file",
 | 
					 | 
				
			||||||
		IsNewFile: true,
 | 
					 | 
				
			||||||
		Author:    nil,
 | 
							Author:    nil,
 | 
				
			||||||
		Committer: nil,
 | 
							Committer: nil,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func getUpdateRepoFileOptions(repo *repo_model.Repository) *files_service.UpdateRepoFileOptions {
 | 
					func getUpdateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
 | 
				
			||||||
	return &files_service.UpdateRepoFileOptions{
 | 
						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,
 | 
							OldBranch: repo.DefaultBranch,
 | 
				
			||||||
		NewBranch: repo.DefaultBranch,
 | 
							NewBranch: repo.DefaultBranch,
 | 
				
			||||||
		TreePath:  "README.md",
 | 
					 | 
				
			||||||
		Message:   "Updates README.md",
 | 
							Message:   "Updates README.md",
 | 
				
			||||||
		SHA:       "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
 | 
					 | 
				
			||||||
		Content:   "This is UPDATED content for the README file",
 | 
					 | 
				
			||||||
		IsNewFile: false,
 | 
					 | 
				
			||||||
		Author:    nil,
 | 
							Author:    nil,
 | 
				
			||||||
		Committer: 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 {
 | 
					func getExpectedFileResponseForRepofilesCreate(commitID, lastCommitSHA string) *api.FileResponse {
 | 
				
			||||||
	treePath := "new/file.txt"
 | 
						treePath := "new/file.txt"
 | 
				
			||||||
	encoding := "base64"
 | 
						encoding := "base64"
 | 
				
			||||||
@@ -183,7 +241,7 @@ func getExpectedFileResponseForRepofilesUpdate(commitID, filename, lastCommitSHA
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
 | 
					func TestChangeRepoFilesForCreate(t *testing.T) {
 | 
				
			||||||
	// setup
 | 
						// setup
 | 
				
			||||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
						onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
				
			||||||
		ctx := test.MockContext(t, "user2/repo1")
 | 
							ctx := test.MockContext(t, "user2/repo1")
 | 
				
			||||||
@@ -196,10 +254,10 @@ func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		repo := ctx.Repo.Repository
 | 
							repo := ctx.Repo.Repository
 | 
				
			||||||
		doer := ctx.Doer
 | 
							doer := ctx.Doer
 | 
				
			||||||
		opts := getCreateRepoFileOptions(repo)
 | 
							opts := getCreateRepoFilesOptions(repo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// test
 | 
							// test
 | 
				
			||||||
		fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
 | 
							filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// asserts
 | 
							// asserts
 | 
				
			||||||
		assert.NoError(t, err)
 | 
							assert.NoError(t, err)
 | 
				
			||||||
@@ -211,16 +269,16 @@ func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
 | 
				
			|||||||
		expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID, lastCommit.ID.String())
 | 
							expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID, lastCommit.ID.String())
 | 
				
			||||||
		assert.NotNil(t, expectedFileResponse)
 | 
							assert.NotNil(t, expectedFileResponse)
 | 
				
			||||||
		if expectedFileResponse != nil {
 | 
							if expectedFileResponse != nil {
 | 
				
			||||||
			assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
 | 
								assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0])
 | 
				
			||||||
			assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
 | 
								assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA)
 | 
				
			||||||
			assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
 | 
								assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
 | 
				
			||||||
			assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
 | 
								assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email)
 | 
				
			||||||
			assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
 | 
								assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
 | 
					func TestChangeRepoFilesForUpdate(t *testing.T) {
 | 
				
			||||||
	// setup
 | 
						// setup
 | 
				
			||||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
						onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
				
			||||||
		ctx := test.MockContext(t, "user2/repo1")
 | 
							ctx := test.MockContext(t, "user2/repo1")
 | 
				
			||||||
@@ -233,10 +291,10 @@ func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		repo := ctx.Repo.Repository
 | 
							repo := ctx.Repo.Repository
 | 
				
			||||||
		doer := ctx.Doer
 | 
							doer := ctx.Doer
 | 
				
			||||||
		opts := getUpdateRepoFileOptions(repo)
 | 
							opts := getUpdateRepoFilesOptions(repo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// test
 | 
							// test
 | 
				
			||||||
		fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
 | 
							filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// asserts
 | 
							// asserts
 | 
				
			||||||
		assert.NoError(t, err)
 | 
							assert.NoError(t, err)
 | 
				
			||||||
@@ -244,17 +302,17 @@ func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
 | 
				
			|||||||
		defer gitRepo.Close()
 | 
							defer gitRepo.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
 | 
							commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
 | 
				
			||||||
		lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
 | 
							lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
 | 
				
			||||||
		expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
 | 
							expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String())
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
 | 
							assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0])
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
 | 
							assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA)
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
 | 
							assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
 | 
							assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email)
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
 | 
							assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
 | 
					func TestChangeRepoFilesForUpdateWithFileMove(t *testing.T) {
 | 
				
			||||||
	// setup
 | 
						// setup
 | 
				
			||||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
						onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
				
			||||||
		ctx := test.MockContext(t, "user2/repo1")
 | 
							ctx := test.MockContext(t, "user2/repo1")
 | 
				
			||||||
@@ -267,12 +325,12 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		repo := ctx.Repo.Repository
 | 
							repo := ctx.Repo.Repository
 | 
				
			||||||
		doer := ctx.Doer
 | 
							doer := ctx.Doer
 | 
				
			||||||
		opts := getUpdateRepoFileOptions(repo)
 | 
							opts := getUpdateRepoFilesOptions(repo)
 | 
				
			||||||
		opts.FromTreePath = "README.md"
 | 
							opts.Files[0].FromTreePath = "README.md"
 | 
				
			||||||
		opts.TreePath = "README_new.md" // new file name, README_new.md
 | 
							opts.Files[0].TreePath = "README_new.md" // new file name, README_new.md
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// test
 | 
							// test
 | 
				
			||||||
		fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
 | 
							filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// asserts
 | 
							// asserts
 | 
				
			||||||
		assert.NoError(t, err)
 | 
							assert.NoError(t, err)
 | 
				
			||||||
@@ -280,32 +338,32 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
 | 
				
			|||||||
		defer gitRepo.Close()
 | 
							defer gitRepo.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
 | 
							commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
 | 
				
			||||||
		lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
 | 
							lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
 | 
				
			||||||
		expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
 | 
							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
 | 
							// 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) {
 | 
							switch err.(type) {
 | 
				
			||||||
		case git.ErrNotExist:
 | 
							case git.ErrNotExist:
 | 
				
			||||||
			// correct, continue
 | 
								// correct, continue
 | 
				
			||||||
		default:
 | 
							default:
 | 
				
			||||||
			t.Fatalf("expected git.ErrNotExist, got:%v", err)
 | 
								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.NoError(t, err)
 | 
				
			||||||
		assert.Nil(t, fromEntry)  // Should no longer exist here
 | 
							assert.Nil(t, fromEntry)  // Should no longer exist here
 | 
				
			||||||
		assert.NotNil(t, toEntry) // Should exist here
 | 
							assert.NotNil(t, toEntry) // Should exist here
 | 
				
			||||||
		// assert SHA has remained the same but paths use the new file name
 | 
							// 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.SHA, filesResponse.Files[0].SHA)
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Content.Name, fileResponse.Content.Name)
 | 
							assert.EqualValues(t, expectedFileResponse.Content.Name, filesResponse.Files[0].Name)
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Content.Path, fileResponse.Content.Path)
 | 
							assert.EqualValues(t, expectedFileResponse.Content.Path, filesResponse.Files[0].Path)
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Content.URL, fileResponse.Content.URL)
 | 
							assert.EqualValues(t, expectedFileResponse.Content.URL, filesResponse.Files[0].URL)
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
 | 
							assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA)
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
 | 
							assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Test opts with branch names removed, should get same results as above test
 | 
					// Test opts with branch names removed, should get same results as above test
 | 
				
			||||||
func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
 | 
					func TestChangeRepoFilesWithoutBranchNames(t *testing.T) {
 | 
				
			||||||
	// setup
 | 
						// setup
 | 
				
			||||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
						onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
				
			||||||
		ctx := test.MockContext(t, "user2/repo1")
 | 
							ctx := test.MockContext(t, "user2/repo1")
 | 
				
			||||||
@@ -318,12 +376,12 @@ func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		repo := ctx.Repo.Repository
 | 
							repo := ctx.Repo.Repository
 | 
				
			||||||
		doer := ctx.Doer
 | 
							doer := ctx.Doer
 | 
				
			||||||
		opts := getUpdateRepoFileOptions(repo)
 | 
							opts := getUpdateRepoFilesOptions(repo)
 | 
				
			||||||
		opts.OldBranch = ""
 | 
							opts.OldBranch = ""
 | 
				
			||||||
		opts.NewBranch = ""
 | 
							opts.NewBranch = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// test
 | 
							// test
 | 
				
			||||||
		fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
 | 
							filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// asserts
 | 
							// asserts
 | 
				
			||||||
		assert.NoError(t, err)
 | 
							assert.NoError(t, err)
 | 
				
			||||||
@@ -331,13 +389,86 @@ func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
 | 
				
			|||||||
		defer gitRepo.Close()
 | 
							defer gitRepo.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		commit, _ := gitRepo.GetBranchCommit(repo.DefaultBranch)
 | 
							commit, _ := gitRepo.GetBranchCommit(repo.DefaultBranch)
 | 
				
			||||||
		lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
 | 
							lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
 | 
				
			||||||
		expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
 | 
							expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String())
 | 
				
			||||||
		assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
 | 
							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
 | 
						// setup
 | 
				
			||||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
						onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
				
			||||||
		ctx := test.MockContext(t, "user2/repo1")
 | 
							ctx := test.MockContext(t, "user2/repo1")
 | 
				
			||||||
@@ -352,63 +483,63 @@ func TestCreateOrUpdateRepoFileErrors(t *testing.T) {
 | 
				
			|||||||
		doer := ctx.Doer
 | 
							doer := ctx.Doer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("bad branch", func(t *testing.T) {
 | 
							t.Run("bad branch", func(t *testing.T) {
 | 
				
			||||||
			opts := getUpdateRepoFileOptions(repo)
 | 
								opts := getUpdateRepoFilesOptions(repo)
 | 
				
			||||||
			opts.OldBranch = "bad_branch"
 | 
								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.Error(t, err)
 | 
				
			||||||
			assert.Nil(t, fileResponse)
 | 
								assert.Nil(t, filesResponse)
 | 
				
			||||||
			expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
 | 
								expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
 | 
				
			||||||
			assert.EqualError(t, err, expectedError)
 | 
								assert.EqualError(t, err, expectedError)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("bad SHA", func(t *testing.T) {
 | 
							t.Run("bad SHA", func(t *testing.T) {
 | 
				
			||||||
			opts := getUpdateRepoFileOptions(repo)
 | 
								opts := getUpdateRepoFilesOptions(repo)
 | 
				
			||||||
			origSHA := opts.SHA
 | 
								origSHA := opts.Files[0].SHA
 | 
				
			||||||
			opts.SHA = "bad_sha"
 | 
								opts.Files[0].SHA = "bad_sha"
 | 
				
			||||||
			fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
 | 
								filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
 | 
				
			||||||
			assert.Nil(t, fileResponse)
 | 
								assert.Nil(t, filesResponse)
 | 
				
			||||||
			assert.Error(t, err)
 | 
								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)
 | 
								assert.EqualError(t, err, expectedError)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("new branch already exists", func(t *testing.T) {
 | 
							t.Run("new branch already exists", func(t *testing.T) {
 | 
				
			||||||
			opts := getUpdateRepoFileOptions(repo)
 | 
								opts := getUpdateRepoFilesOptions(repo)
 | 
				
			||||||
			opts.NewBranch = "develop"
 | 
								opts.NewBranch = "develop"
 | 
				
			||||||
			fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
 | 
								filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
 | 
				
			||||||
			assert.Nil(t, fileResponse)
 | 
								assert.Nil(t, filesResponse)
 | 
				
			||||||
			assert.Error(t, err)
 | 
								assert.Error(t, err)
 | 
				
			||||||
			expectedError := "branch already exists [name: " + opts.NewBranch + "]"
 | 
								expectedError := "branch already exists [name: " + opts.NewBranch + "]"
 | 
				
			||||||
			assert.EqualError(t, err, expectedError)
 | 
								assert.EqualError(t, err, expectedError)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("treePath is empty:", func(t *testing.T) {
 | 
							t.Run("treePath is empty:", func(t *testing.T) {
 | 
				
			||||||
			opts := getUpdateRepoFileOptions(repo)
 | 
								opts := getUpdateRepoFilesOptions(repo)
 | 
				
			||||||
			opts.TreePath = ""
 | 
								opts.Files[0].TreePath = ""
 | 
				
			||||||
			fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
 | 
								filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
 | 
				
			||||||
			assert.Nil(t, fileResponse)
 | 
								assert.Nil(t, filesResponse)
 | 
				
			||||||
			assert.Error(t, err)
 | 
								assert.Error(t, err)
 | 
				
			||||||
			expectedError := "path contains a malformed path component [path: ]"
 | 
								expectedError := "path contains a malformed path component [path: ]"
 | 
				
			||||||
			assert.EqualError(t, err, expectedError)
 | 
								assert.EqualError(t, err, expectedError)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("treePath is a git directory:", func(t *testing.T) {
 | 
							t.Run("treePath is a git directory:", func(t *testing.T) {
 | 
				
			||||||
			opts := getUpdateRepoFileOptions(repo)
 | 
								opts := getUpdateRepoFilesOptions(repo)
 | 
				
			||||||
			opts.TreePath = ".git"
 | 
								opts.Files[0].TreePath = ".git"
 | 
				
			||||||
			fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
 | 
								filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
 | 
				
			||||||
			assert.Nil(t, fileResponse)
 | 
								assert.Nil(t, filesResponse)
 | 
				
			||||||
			assert.Error(t, err)
 | 
								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)
 | 
								assert.EqualError(t, err, expectedError)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("create file that already exists", func(t *testing.T) {
 | 
							t.Run("create file that already exists", func(t *testing.T) {
 | 
				
			||||||
			opts := getCreateRepoFileOptions(repo)
 | 
								opts := getCreateRepoFilesOptions(repo)
 | 
				
			||||||
			opts.TreePath = "README.md" // already exists
 | 
								opts.Files[0].TreePath = "README.md" // already exists
 | 
				
			||||||
			fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
 | 
								fileResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
 | 
				
			||||||
			assert.Nil(t, fileResponse)
 | 
								assert.Nil(t, fileResponse)
 | 
				
			||||||
			assert.Error(t, err)
 | 
								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)
 | 
								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)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user