mirror of https://github.com/go-gitea/gitea.git
API endpoint for changing/creating/deleting multiple files (#24887)
This PR creates an API endpoint for creating/updating/deleting multiple files in one API call similar to the solution provided by [GitLab](https://docs.gitlab.com/ee/api/commits.html#create-a-commit-with-multiple-files-and-actions). To archive this, the CreateOrUpdateRepoFile and DeleteRepoFIle functions in files service are unified into one function supporting multiple files and actions. Resolves #14619
This commit is contained in:
parent
245f2c08db
commit
275d4b7e3f
|
@ -64,6 +64,35 @@ func (o *UpdateFileOptions) Branch() string {
|
||||||
return o.FileOptions.BranchName
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
Loading…
Reference in New Issue