mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-27 05:55:21 +08:00
Make "update file" API can create a new file when SHA is not set (#35738)
Fix #19008, use GitHub's behavior (empty SHA to create a new file)
This commit is contained in:
parent
397d666432
commit
9a73a1fb83
@ -24,13 +24,6 @@ type FileOptions struct {
|
|||||||
Signoff bool `json:"signoff"`
|
Signoff bool `json:"signoff"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileOptionsWithSHA struct {
|
|
||||||
FileOptions
|
|
||||||
// the blob ID (SHA) for the file that already exists, it is required for changing existing files
|
|
||||||
// required: true
|
|
||||||
SHA string `json:"sha" binding:"Required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FileOptions) GetFileOptions() *FileOptions {
|
func (f *FileOptions) GetFileOptions() *FileOptions {
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
@ -41,7 +34,7 @@ type FileOptionsInterface interface {
|
|||||||
|
|
||||||
var _ FileOptionsInterface = (*FileOptions)(nil)
|
var _ FileOptionsInterface = (*FileOptions)(nil)
|
||||||
|
|
||||||
// CreateFileOptions options for creating files
|
// CreateFileOptions options for creating a file
|
||||||
// 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)
|
// 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 CreateFileOptions struct {
|
type CreateFileOptions struct {
|
||||||
FileOptions
|
FileOptions
|
||||||
@ -50,16 +43,21 @@ type CreateFileOptions struct {
|
|||||||
ContentBase64 string `json:"content"`
|
ContentBase64 string `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteFileOptions options for deleting files (used for other File structs below)
|
// DeleteFileOptions options for deleting a file
|
||||||
// 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)
|
// 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 DeleteFileOptions struct {
|
type DeleteFileOptions struct {
|
||||||
FileOptionsWithSHA
|
FileOptions
|
||||||
|
// the blob ID (SHA) for the file to delete
|
||||||
|
// required: true
|
||||||
|
SHA string `json:"sha" binding:"Required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateFileOptions options for updating files
|
// UpdateFileOptions options for updating or creating a file
|
||||||
// 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)
|
// 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 UpdateFileOptions struct {
|
type UpdateFileOptions struct {
|
||||||
FileOptionsWithSHA
|
FileOptions
|
||||||
|
// the blob ID (SHA) for the file that already exists to update, or leave it empty to create a new file
|
||||||
|
SHA string `json:"sha"`
|
||||||
// content must be base64 encoded
|
// content must be base64 encoded
|
||||||
// required: true
|
// required: true
|
||||||
ContentBase64 string `json:"content"`
|
ContentBase64 string `json:"content"`
|
||||||
|
|||||||
@ -525,7 +525,7 @@ func CreateFile(ctx *context.APIContext) {
|
|||||||
func UpdateFile(ctx *context.APIContext) {
|
func UpdateFile(ctx *context.APIContext) {
|
||||||
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
|
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
|
||||||
// ---
|
// ---
|
||||||
// summary: Update a file in a repository
|
// summary: Update a file in a repository if SHA is set, or create the file if SHA is not set
|
||||||
// consumes:
|
// consumes:
|
||||||
// - application/json
|
// - application/json
|
||||||
// produces:
|
// produces:
|
||||||
@ -554,6 +554,8 @@ func UpdateFile(ctx *context.APIContext) {
|
|||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/FileResponse"
|
// "$ref": "#/responses/FileResponse"
|
||||||
|
// "201":
|
||||||
|
// "$ref": "#/responses/FileResponse"
|
||||||
// "403":
|
// "403":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "404":
|
// "404":
|
||||||
@ -572,8 +574,9 @@ func UpdateFile(ctx *context.APIContext) {
|
|||||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
willCreate := apiOpts.SHA == ""
|
||||||
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
|
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
|
||||||
Operation: "update",
|
Operation: util.Iif(willCreate, "create", "update"),
|
||||||
ContentReader: contentReader,
|
ContentReader: contentReader,
|
||||||
SHA: apiOpts.SHA,
|
SHA: apiOpts.SHA,
|
||||||
FromTreePath: apiOpts.FromPath,
|
FromTreePath: apiOpts.FromPath,
|
||||||
@ -587,7 +590,7 @@ func UpdateFile(ctx *context.APIContext) {
|
|||||||
handleChangeRepoFilesError(ctx, err)
|
handleChangeRepoFilesError(ctx, err)
|
||||||
} else {
|
} else {
|
||||||
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||||||
ctx.JSON(http.StatusOK, fileResponse)
|
ctx.JSON(util.Iif(willCreate, http.StatusCreated, http.StatusOK), fileResponse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
templates/swagger/v1_json.tmpl
generated
16
templates/swagger/v1_json.tmpl
generated
@ -7634,7 +7634,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"repository"
|
"repository"
|
||||||
],
|
],
|
||||||
"summary": "Update a file in a repository",
|
"summary": "Update a file in a repository if SHA is set, or create the file if SHA is not set",
|
||||||
"operationId": "repoUpdateFile",
|
"operationId": "repoUpdateFile",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -7671,6 +7671,9 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/FileResponse"
|
"$ref": "#/responses/FileResponse"
|
||||||
},
|
},
|
||||||
|
"201": {
|
||||||
|
"$ref": "#/responses/FileResponse"
|
||||||
|
},
|
||||||
"403": {
|
"403": {
|
||||||
"$ref": "#/responses/error"
|
"$ref": "#/responses/error"
|
||||||
},
|
},
|
||||||
@ -22886,7 +22889,7 @@
|
|||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
"CreateFileOptions": {
|
"CreateFileOptions": {
|
||||||
"description": "CreateFileOptions options for creating 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)",
|
"description": "CreateFileOptions options for creating a file\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",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"content"
|
"content"
|
||||||
@ -23904,7 +23907,7 @@
|
|||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
"DeleteFileOptions": {
|
"DeleteFileOptions": {
|
||||||
"description": "DeleteFileOptions options for deleting files (used for other File structs below)\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)",
|
"description": "DeleteFileOptions options for deleting a file\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",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"sha"
|
"sha"
|
||||||
@ -23940,7 +23943,7 @@
|
|||||||
"x-go-name": "NewBranchName"
|
"x-go-name": "NewBranchName"
|
||||||
},
|
},
|
||||||
"sha": {
|
"sha": {
|
||||||
"description": "the blob ID (SHA) for the file that already exists, it is required for changing existing files",
|
"description": "the blob ID (SHA) for the file to delete",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "SHA"
|
"x-go-name": "SHA"
|
||||||
},
|
},
|
||||||
@ -28700,10 +28703,9 @@
|
|||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
"UpdateFileOptions": {
|
"UpdateFileOptions": {
|
||||||
"description": "UpdateFileOptions options for updating 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)",
|
"description": "UpdateFileOptions options for updating or creating a file\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",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"sha",
|
|
||||||
"content"
|
"content"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -28747,7 +28749,7 @@
|
|||||||
"x-go-name": "NewBranchName"
|
"x-go-name": "NewBranchName"
|
||||||
},
|
},
|
||||||
"sha": {
|
"sha": {
|
||||||
"description": "the blob ID (SHA) for the file that already exists, it is required for changing existing files",
|
"description": "the blob ID (SHA) for the file that already exists to update, or leave it empty to create a new file",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "SHA"
|
"x-go-name": "SHA"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,21 +20,19 @@ import (
|
|||||||
|
|
||||||
func getDeleteFileOptions() *api.DeleteFileOptions {
|
func getDeleteFileOptions() *api.DeleteFileOptions {
|
||||||
return &api.DeleteFileOptions{
|
return &api.DeleteFileOptions{
|
||||||
FileOptionsWithSHA: api.FileOptionsWithSHA{
|
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
||||||
FileOptions: api.FileOptions{
|
FileOptions: api.FileOptions{
|
||||||
BranchName: "master",
|
BranchName: "master",
|
||||||
NewBranchName: "master",
|
NewBranchName: "master",
|
||||||
Message: "Removing the file new/file.txt",
|
Message: "Removing the file new/file.txt",
|
||||||
Author: api.Identity{
|
Author: api.Identity{
|
||||||
Name: "John Doe",
|
Name: "John Doe",
|
||||||
Email: "johndoe@example.com",
|
Email: "johndoe@example.com",
|
||||||
},
|
},
|
||||||
Committer: api.Identity{
|
Committer: api.Identity{
|
||||||
Name: "Jane Doe",
|
Name: "Jane Doe",
|
||||||
Email: "janedoe@example.com",
|
Email: "janedoe@example.com",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,21 +28,19 @@ func getUpdateFileOptions() *api.UpdateFileOptions {
|
|||||||
content := "This is updated text"
|
content := "This is updated text"
|
||||||
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
|
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
|
||||||
return &api.UpdateFileOptions{
|
return &api.UpdateFileOptions{
|
||||||
FileOptionsWithSHA: api.FileOptionsWithSHA{
|
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
||||||
FileOptions: api.FileOptions{
|
FileOptions: api.FileOptions{
|
||||||
BranchName: "master",
|
BranchName: "master",
|
||||||
NewBranchName: "master",
|
NewBranchName: "master",
|
||||||
Message: "My update of new/file.txt",
|
Message: "My update of new/file.txt",
|
||||||
Author: api.Identity{
|
Author: api.Identity{
|
||||||
Name: "John Doe",
|
Name: "John Doe",
|
||||||
Email: "johndoe@example.com",
|
Email: "johndoe@example.com",
|
||||||
},
|
},
|
||||||
Committer: api.Identity{
|
Committer: api.Identity{
|
||||||
Name: "Anne Doe",
|
Name: "Anne Doe",
|
||||||
Email: "annedoe@example.com",
|
Email: "annedoe@example.com",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
|
||||||
},
|
},
|
||||||
ContentBase64: contentEncoded,
|
ContentBase64: contentEncoded,
|
||||||
}
|
}
|
||||||
@ -180,6 +178,15 @@ func TestAPIUpdateFile(t *testing.T) {
|
|||||||
assert.Equal(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
|
assert.Equal(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
|
||||||
assert.Equal(t, updateFileOptions.Message+"\n", fileResponse.Commit.Message)
|
assert.Equal(t, updateFileOptions.Message+"\n", fileResponse.Commit.Message)
|
||||||
|
|
||||||
|
// Test updating a file without SHA (should create the file)
|
||||||
|
updateFileOptions = getUpdateFileOptions()
|
||||||
|
updateFileOptions.SHA = ""
|
||||||
|
req = NewRequestWithJSON(t, "PUT", "/api/v1/repos/user2/repo1/contents/update-create.txt", &updateFileOptions).AddTokenAuth(token2)
|
||||||
|
resp = MakeRequest(t, req, http.StatusCreated)
|
||||||
|
DecodeJSON(t, resp, &fileResponse)
|
||||||
|
assert.Equal(t, "08bd14b2e2852529157324de9c226b3364e76136", fileResponse.Content.SHA)
|
||||||
|
assert.Equal(t, setting.AppURL+"user2/repo1/raw/branch/master/update-create.txt", *fileResponse.Content.DownloadURL)
|
||||||
|
|
||||||
// Test updating a file and renaming it
|
// Test updating a file and renaming it
|
||||||
updateFileOptions = getUpdateFileOptions()
|
updateFileOptions = getUpdateFileOptions()
|
||||||
updateFileOptions.BranchName = repo1.DefaultBranch
|
updateFileOptions.BranchName = repo1.DefaultBranch
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user