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"` | 	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,7 +20,7 @@ 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", | ||||||
| @@ -34,8 +34,6 @@ func getDeleteFileOptions() *api.DeleteFileOptions { | |||||||
| 				Email: "janedoe@example.com", | 				Email: "janedoe@example.com", | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 			SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", |  | ||||||
| 		}, |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ 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", | ||||||
| @@ -42,8 +42,6 @@ func getUpdateFileOptions() *api.UpdateFileOptions { | |||||||
| 				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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user