mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 11:28:24 +00: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:
		| @@ -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"` | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										16
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @@ -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" | ||||
|         }, | ||||
|   | ||||
| @@ -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", | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user