diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index 4729bde491..59665062b7 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -24,13 +24,6 @@ type FileOptions struct { 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 { return f } @@ -41,7 +34,7 @@ type FileOptionsInterface interface { 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) type CreateFileOptions struct { FileOptions @@ -50,16 +43,21 @@ type CreateFileOptions struct { 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) 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) 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 // required: true ContentBase64 string `json:"content"` diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 2b6348c2fb..ba98263819 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -525,7 +525,7 @@ func CreateFile(ctx *context.APIContext) { func UpdateFile(ctx *context.APIContext) { // 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: // - application/json // produces: @@ -554,6 +554,8 @@ func UpdateFile(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/FileResponse" + // "201": + // "$ref": "#/responses/FileResponse" // "403": // "$ref": "#/responses/error" // "404": @@ -572,8 +574,9 @@ func UpdateFile(ctx *context.APIContext) { ctx.APIError(http.StatusUnprocessableEntity, err) return } + willCreate := apiOpts.SHA == "" opts.Files = append(opts.Files, &files_service.ChangeRepoFile{ - Operation: "update", + Operation: util.Iif(willCreate, "create", "update"), ContentReader: contentReader, SHA: apiOpts.SHA, FromTreePath: apiOpts.FromPath, @@ -587,7 +590,7 @@ func UpdateFile(ctx *context.APIContext) { handleChangeRepoFilesError(ctx, err) } else { fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) - ctx.JSON(http.StatusOK, fileResponse) + ctx.JSON(util.Iif(willCreate, http.StatusCreated, http.StatusOK), fileResponse) } } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 966aff12f8..0cefa6795f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7634,7 +7634,7 @@ "tags": [ "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", "parameters": [ { @@ -7671,6 +7671,9 @@ "200": { "$ref": "#/responses/FileResponse" }, + "201": { + "$ref": "#/responses/FileResponse" + }, "403": { "$ref": "#/responses/error" }, @@ -22886,7 +22889,7 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "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", "required": [ "content" @@ -23904,7 +23907,7 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "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", "required": [ "sha" @@ -23940,7 +23943,7 @@ "x-go-name": "NewBranchName" }, "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", "x-go-name": "SHA" }, @@ -28700,10 +28703,9 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "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", "required": [ - "sha", "content" ], "properties": { @@ -28747,7 +28749,7 @@ "x-go-name": "NewBranchName" }, "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", "x-go-name": "SHA" }, diff --git a/tests/integration/api_repo_file_delete_test.go b/tests/integration/api_repo_file_delete_test.go index 9dd47f93e6..59e2131618 100644 --- a/tests/integration/api_repo_file_delete_test.go +++ b/tests/integration/api_repo_file_delete_test.go @@ -20,21 +20,19 @@ import ( func getDeleteFileOptions() *api.DeleteFileOptions { return &api.DeleteFileOptions{ - FileOptionsWithSHA: api.FileOptionsWithSHA{ - FileOptions: api.FileOptions{ - BranchName: "master", - NewBranchName: "master", - Message: "Removing the file new/file.txt", - Author: api.Identity{ - Name: "John Doe", - Email: "johndoe@example.com", - }, - Committer: api.Identity{ - Name: "Jane Doe", - Email: "janedoe@example.com", - }, + SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", + FileOptions: api.FileOptions{ + BranchName: "master", + NewBranchName: "master", + Message: "Removing the file new/file.txt", + Author: api.Identity{ + Name: "John Doe", + Email: "johndoe@example.com", + }, + Committer: api.Identity{ + Name: "Jane Doe", + Email: "janedoe@example.com", }, - SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", }, } } diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go index 9a56711da6..6e6aae389f 100644 --- a/tests/integration/api_repo_file_update_test.go +++ b/tests/integration/api_repo_file_update_test.go @@ -28,21 +28,19 @@ func getUpdateFileOptions() *api.UpdateFileOptions { content := "This is updated text" contentEncoded := base64.StdEncoding.EncodeToString([]byte(content)) return &api.UpdateFileOptions{ - FileOptionsWithSHA: api.FileOptionsWithSHA{ - FileOptions: api.FileOptions{ - BranchName: "master", - NewBranchName: "master", - Message: "My update of new/file.txt", - Author: api.Identity{ - Name: "John Doe", - Email: "johndoe@example.com", - }, - Committer: api.Identity{ - Name: "Anne Doe", - Email: "annedoe@example.com", - }, + SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", + FileOptions: api.FileOptions{ + BranchName: "master", + NewBranchName: "master", + Message: "My update of new/file.txt", + Author: api.Identity{ + Name: "John Doe", + Email: "johndoe@example.com", + }, + Committer: api.Identity{ + Name: "Anne Doe", + Email: "annedoe@example.com", }, - SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", }, ContentBase64: contentEncoded, } @@ -180,6 +178,15 @@ func TestAPIUpdateFile(t *testing.T) { assert.Equal(t, expectedDownloadURL, *fileResponse.Content.DownloadURL) 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 updateFileOptions = getUpdateFileOptions() updateFileOptions.BranchName = repo1.DefaultBranch