From cf23fdb59203624289e854eab233a13351131a21 Mon Sep 17 00:00:00 2001 From: yystopf Date: Tue, 5 Jul 2022 14:31:02 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9Acommit=20diff?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0api=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routers/api/v1/repo/commits.go | 27 +++++++++++++++++++++ templates/swagger/v1_json.tmpl | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 282798c54..102d08ace 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -539,7 +539,34 @@ func GetFileAllCommits(ctx *context.APIContext) { } // 获取 commit diff +// Diff get diffs by commit on a repository func Diff(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/diff repository repoGetDiffs + // --- + // summary: Get diffs by commit from a repository + // 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: sha + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // 200: + // description: success + // "404": + // "$ref": "#/responses/notFound" commitID := ctx.Params(":sha") diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 2755891bd..02666522b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3320,6 +3320,49 @@ } } }, + "/repos/{owner}/{repo}/commits/{sha}/diff": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get diffs by commit from a repository", + "operationId": "repoGetDiffs", + "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 + }, + { + "type": "string", + "description": "name of the repo", + "name": "sha", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "success" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/commits_slice": { "get": { "produces": [ -- 2.34.1 From a3b1439a520560cfbe911b746ac50940e2623284 Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 6 Jul 2022 14:10:12 +0800 Subject: [PATCH 02/10] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=8D=95=E6=96=87=E4=BB=B6blame=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/git/blame.go | 109 +++++++++++++++++++++++++++++++++ routers/api/v1/api.go | 1 + routers/api/v1/repo/blame.go | 106 ++++++++++++++++++++++++++++++++ templates/swagger/v1_json.tmpl | 50 +++++++++++++++ 4 files changed, 266 insertions(+) create mode 100644 routers/api/v1/repo/blame.go diff --git a/modules/git/blame.go b/modules/git/blame.go index fcbf18398..dfee86371 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -12,6 +12,7 @@ import ( "os" "os/exec" "regexp" + "time" "code.gitea.io/gitea/modules/process" ) @@ -22,6 +23,25 @@ type BlamePart struct { Lines []string } +type ApiBlameCommit struct { + ID string `json:"id"` + Author *Signature `json:"author"` + Commiter *Signature `json:"commiter"` + CommitMessage string `json:"commit_message"` + Parents []string `json:"parents"` + AuthoredTime time.Time `json:"authored_time"` + CommittedTime time.Time `json:"committed_time"` + CreatedTime time.Time `json:"created_time"` +} + +type ApiBlamePart struct { + Sha string `json:"-"` + Commit *ApiBlameCommit `json:"commit"` + CurrentNumber int `json:"current_number"` + EffectLine int `json:"effect_line"` + Lines []string `json:"lines"` +} + // BlameReader returns part of file blame one by one type BlameReader struct { cmd *exec.Cmd @@ -34,6 +54,95 @@ type BlameReader struct { var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") +func GetBlameCommit(repo *Repository, sha string) *ApiBlameCommit { + commit, err := repo.GetCommit(sha) + var apiParents []string + for i := 0; i < commit.ParentCount(); i++ { + sha, _ := commit.ParentID(i) + apiParents = append(apiParents, sha.String()) + } + + if err != nil { + return &ApiBlameCommit{} + } else { + return &ApiBlameCommit{ + ID: sha, + Author: commit.Author, + Commiter: commit.Committer, + CommitMessage: commit.CommitMessage, + Parents: apiParents, + AuthoredTime: commit.Author.When, + CommittedTime: commit.Committer.When, + CreatedTime: commit.Committer.When, + } + } +} + +func (r *BlameReader) NextApiPart(repo *Repository) (*ApiBlamePart, error) { + var blamePart *ApiBlamePart + + reader := r.reader + effectLine := 0 + + if r.lastSha != nil { + blamePart = &ApiBlamePart{*r.lastSha, GetBlameCommit(repo, *r.lastSha), 0, effectLine, make([]string, 0)} + } + + var line []byte + var isPrefix bool + var err error + + for err != io.EOF { + line, isPrefix, err = reader.ReadLine() + if err != nil && err != io.EOF { + return blamePart, err + } + + if len(line) == 0 { + // isPrefix will be false + continue + } + + lines := shaLineRegex.FindSubmatch(line) + if lines != nil { + sha1 := string(lines[1]) + + if blamePart == nil { + blamePart = &ApiBlamePart{sha1, GetBlameCommit(repo, sha1), 0, effectLine, make([]string, 0)} + } + + if blamePart.Sha != sha1 { + r.lastSha = &sha1 + // need to munch to end of line... + for isPrefix { + _, isPrefix, err = reader.ReadLine() + if err != nil && err != io.EOF { + return blamePart, err + } + } + return blamePart, nil + } + } else if line[0] == '\t' { + code := line[1:] + effectLine += 1 + blamePart.Lines = append(blamePart.Lines, string(code)) + } + blamePart.EffectLine = effectLine + + // need to munch to end of line... + for isPrefix { + _, isPrefix, err = reader.ReadLine() + if err != nil && err != io.EOF { + return blamePart, err + } + } + } + + r.lastSha = nil + + return blamePart, nil +} + // NextPart returns next part of blame (sequential code lines with the same commit) func (r *BlameReader) NextPart() (*BlamePart, error) { var blamePart *BlamePart diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 9da5c6618..121be8c81 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1045,6 +1045,7 @@ func Routes() *web.Route { m.Get("/issue_templates", context.ReferencesGitRepo(false), repo.GetIssueTemplates) m.Get("/languages", reqRepoReader(models.UnitTypeCode), repo.GetLanguages) m.Get("/diffs", context.RepoRef(), repo.GetRepoDiffs) + m.Get("/blame", context.RepoRef(), repo.GetRepoRefBlame) }, repoAssignment()) }) diff --git a/routers/api/v1/repo/blame.go b/routers/api/v1/repo/blame.go new file mode 100644 index 000000000..42cb55d60 --- /dev/null +++ b/routers/api/v1/repo/blame.go @@ -0,0 +1,106 @@ +package repo + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/git" +) + +func GetRepoRefBlame(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/blame repository repoGetRefBlame + // --- + // summary: Get blame from a repository by sha and filepath + // 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: sha + // in: query + // description: repo commit sha or branch + // type: string + // required: true + // - name: filepath + // in: query + // description: filepath in repository + // type: string + // required: true + // responses: + // 200: + // description: success + // "404": + // "$ref": "#/responses/notFound" + + if ctx.Repo.Repository.IsEmpty { + ctx.NotFound() + return + } + + var commit *git.Commit + if sha := ctx.QueryTrim("sha"); len(sha) > 0 { + var err error + commit, err = ctx.Repo.GitRepo.GetCommit(sha) + if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetCommit", err) + } + return + } + } + fmt.Println(commit) + + filepath := ctx.QueryTrim("filepath") + fmt.Println(filepath) + entry, err := commit.GetTreeEntryByPath(filepath) + if err != nil { + ctx.NotFoundOrServerError("commit.GetTreeEntryByPath", git.IsErrNotExist, err) + return + } + + blob := entry.Blob() + numLines, err := blob.GetBlobLineCount() + if err != nil { + ctx.NotFound("GetBlobLineCount", err) + return + } + blameReader, err := git.CreateBlameReader(ctx, models.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name), commit.ID.String(), filepath) + if err != nil { + ctx.NotFound("CreateBlameReader", err) + return + } + defer blameReader.Close() + + blameParts := make([]git.ApiBlamePart, 0) + currentNumber := 1 + for { + blamePart, err := blameReader.NextApiPart(ctx.Repo.GitRepo) + if err != nil { + ctx.NotFound("NextPart", err) + return + } + if blamePart == nil { + break + } + blamePart.CurrentNumber = currentNumber + blameParts = append(blameParts, *blamePart) + currentNumber += blamePart.EffectLine + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "num_lines": numLines, + "blame_parts": blameParts, + }) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 02666522b..9a0c9dc3c 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2424,6 +2424,56 @@ } } }, + "/repos/{owner}/{repo}/blame": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get blame from a repository by sha and filepath", + "operationId": "repoGetRefBlame", + "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 + }, + { + "type": "string", + "description": "repo commit sha or branch", + "name": "sha", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "filepath in repository", + "name": "filepath", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "success" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/branch_name_set": { "get": { "produces": [ -- 2.34.1 From 4a21b9f7acf7978f038721ddec3643c3b8678b2a Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 6 Jul 2022 14:49:31 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20blame=20=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E7=BB=93=E6=9E=84=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routers/api/v1/repo/blame.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/routers/api/v1/repo/blame.go b/routers/api/v1/repo/blame.go index 42cb55d60..562d1e501 100644 --- a/routers/api/v1/repo/blame.go +++ b/routers/api/v1/repo/blame.go @@ -9,6 +9,13 @@ import ( "code.gitea.io/gitea/modules/git" ) +type APIBlameResponse struct { + FileSize int64 `json:"file_size"` + FileName string `json:"file_name"` + NumberLines int `json:"num_lines"` + BlameParts []git.ApiBlamePart `json:"blame_parts"` +} + func GetRepoRefBlame(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/blame repository repoGetRefBlame // --- @@ -99,8 +106,10 @@ func GetRepoRefBlame(ctx *context.APIContext) { currentNumber += blamePart.EffectLine } - ctx.JSON(http.StatusOK, map[string]interface{}{ - "num_lines": numLines, - "blame_parts": blameParts, + ctx.JSON(http.StatusOK, APIBlameResponse{ + FileSize: blob.Size(), + FileName: blob.Name(), + NumberLines: numLines, + BlameParts: blameParts, }) } -- 2.34.1 From f2d50f4cb88bf5239c6710da6863e2774e9faf1a Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 6 Jul 2022 18:16:00 +0800 Subject: [PATCH 04/10] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E4=BF=AE=E6=94=B9=E6=96=87=E4=BB=B6api=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/structs/repo_file.go | 15 ++ routers/api/v1/api.go | 5 + routers/api/v1/repo/blame.go | 2 +- routers/api/v1/repo/commits.go | 2 +- routers/api/v1/repo/file.go | 129 ++++++++++++++++ routers/api/v1/swagger/options.go | 3 + templates/swagger/v1_json.tmpl | 235 ++++++++++++++++++++++++------ 7 files changed, 345 insertions(+), 46 deletions(-) diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index 3b40224e9..828254ce1 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -30,6 +30,11 @@ type CreateFileOptions struct { Content string `json:"content"` } +// BatchCreateFileOptions options for creating more files +type BatchCreateFileOptions struct { + BatchFiles []CreateFileOptions `json:"batch_files"` +} + // DeleteFileOptions options for deleting files (used for other File structs below) // 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 { @@ -39,6 +44,11 @@ type DeleteFileOptions struct { SHA string `json:"sha" binding:"Required"` } +// BatchDeleteFileOptions options for deleting files +type BatchDeleteFileOptions struct { + BatchFiles []DeleteFileOptions `json:"batch_files"` +} + // UpdateFileOptions options for updating 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 UpdateFileOptions struct { @@ -50,6 +60,11 @@ type UpdateFileOptions struct { FromPath string `json:"from_path" binding:"MaxSize(500)"` } +// BatchUpdateFileOptions options for updating more files. +type BatchUpdateFileOptions struct { + BatchFiles []UpdateFileOptions `json:"batch_files"` +} + // FileLinksResponse contains the links for a repo's file type FileLinksResponse struct { Self *string `json:"self"` diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 121be8c81..f91fb92d3 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1032,6 +1032,11 @@ func Routes() *web.Route { m.Put("", bind(api.UpdateFileOptions{}), repo.UpdateFile) m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile) }, reqRepoWriter(models.UnitTypeCode), reqToken()) + m.Group("/batch/*", func() { + m.Post("", bind(api.BatchCreateFileOptions{}), repo.BatchCreateFile) + m.Put("", bind(api.BatchUpdateFileOptions{}), repo.BatchUpdateFile) + m.Delete("", bind(api.BatchDeleteFileOptions{}), repo.BatchDeleteFile) + }, reqRepoWriter(models.UnitTypeCode), reqToken()) }, reqRepoReader(models.UnitTypeCode)) m.Get("/signing-key.gpg", misc.SigningKey) m.Group("/topics", func() { diff --git a/routers/api/v1/repo/blame.go b/routers/api/v1/repo/blame.go index 562d1e501..44159902d 100644 --- a/routers/api/v1/repo/blame.go +++ b/routers/api/v1/repo/blame.go @@ -19,7 +19,7 @@ type APIBlameResponse struct { func GetRepoRefBlame(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/blame repository repoGetRefBlame // --- - // summary: Get blame from a repository by sha and filepath + // summary: Get blame from a repository by sha and filepath*** // produces: // - application/json // parameters: diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 102d08ace..edfbbc321 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -541,7 +541,7 @@ func GetFileAllCommits(ctx *context.APIContext) { // 获取 commit diff // Diff get diffs by commit on a repository func Diff(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/diff repository repoGetDiffs + // swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/diff repository repoGetDiffs*** // --- // summary: Get diffs by commit from a repository // produces: diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 3cb41457d..89480355e 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -285,6 +285,49 @@ func CreateFile(ctx *context.APIContext) { } } +// BatchCreateFile handles API call for creating a file*** +func BatchCreateFile(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/contents/batch/{filepath} repository repoBatchCreateFile + // --- + // summary: Create some 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: filepath + // in: path + // description: path of the file to create + // type: string + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/BatchCreateFileOptions" + // responses: + // "201": + // "$ref": "#/responses/FileResponse" + // "403": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/error" + + ctx.JSON(http.StatusOK, map[string]string{}) +} + // UpdateFile handles API call for updating a file func UpdateFile(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile @@ -392,6 +435,49 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { ctx.Error(http.StatusInternalServerError, "UpdateFile", err) } +// BatchUpdateFile handles API call for updating a file +func BatchUpdateFile(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/contents/batch/{filepath} repository repoBatchUpdateFile + // --- + // summary: Update some files in a repository*** + // consumes: + // - application/json + // produces: + // - application/jsoncontent + // 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: filepath + // in: path + // description: path of the file to update + // type: string + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/BatchUpdateFileOptions" + // responses: + // "200": + // "$ref": "#/responses/FileResponse" + // "403": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/error" + + ctx.JSON(http.StatusOK, map[string]string{}) +} + // Called from both CreateFile or UpdateFile to handle both func createOrUpdateFile(ctx *context.APIContext, opts *repofiles.UpdateRepoFileOptions) (*api.FileResponse, error) { if !canWriteFiles(ctx.Repo) { @@ -515,6 +601,49 @@ func DeleteFile(ctx *context.APIContext) { } } +// BatchDeleteFile Delete some files in a repository +func BatchDeleteFile(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/contents/batch/{filepath} repository repoBatchDeleteFile + // --- + // summary: Delete some 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: filepath + // in: path + // description: path of the file to delete + // type: string + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/BatchDeleteFileOptions" + // responses: + // "200": + // "$ref": "#/responses/FileDeleteResponse" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/error" + + ctx.JSON(http.StatusOK, map[string]string{}) +} + // GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir func GetContents(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index a86b610f1..b63299dcb 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -119,6 +119,9 @@ type swaggerParameterBodies struct { // in:body CreateFileOptions api.CreateFileOptions + // in:body + BatchCreateFileOptions api.BatchCreateFileOptions + // in:body UpdateFileOptions api.UpdateFileOptions diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 9a0c9dc3c..38714f840 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2432,7 +2432,7 @@ "tags": [ "repository" ], - "summary": "Get blame from a repository by sha and filepath", + "summary": "Get blame from a repository by sha and filepath***", "operationId": "repoGetRefBlame", "parameters": [ { @@ -3370,49 +3370,6 @@ } } }, - "/repos/{owner}/{repo}/commits/{sha}/diff": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "repository" - ], - "summary": "Get diffs by commit from a repository", - "operationId": "repoGetDiffs", - "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 - }, - { - "type": "string", - "description": "name of the repo", - "name": "sha", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "success" - }, - "404": { - "$ref": "#/responses/notFound" - } - } - } - }, "/repos/{owner}/{repo}/commits_slice": { "get": { "produces": [ @@ -3512,6 +3469,182 @@ } } }, + "/repos/{owner}/{repo}/contents/batch/{filepath}": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/jsoncontent" + ], + "tags": [ + "repository" + ], + "summary": "Update some files in a repository***", + "operationId": "repoBatchUpdateFile", + "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 + }, + { + "type": "string", + "description": "path of the file to update", + "name": "filepath", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchUpdateFileOptions" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/FileResponse" + }, + "403": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/error" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Create some files in a repository***", + "operationId": "repoBatchCreateFile", + "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 + }, + { + "type": "string", + "description": "path of the file to create", + "name": "filepath", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchCreateFileOptions" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/FileResponse" + }, + "403": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/error" + } + } + }, + "delete": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Delete some files in a repository***", + "operationId": "repoBatchDeleteFile", + "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 + }, + { + "type": "string", + "description": "path of the file to delete", + "name": "filepath", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BatchDeleteFileOptions" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/FileDeleteResponse" + }, + "400": { + "$ref": "#/responses/error" + }, + "403": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/error" + } + } + } + }, "/repos/{owner}/{repo}/contents/{filepath}": { "get": { "produces": [ @@ -13507,6 +13640,20 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "BatchCreateFileOptions": { + "description": "BatchCreateFileOptions options for creating more files", + "type": "object", + "properties": { + "batch_files": { + "type": "array", + "items": { + "$ref": "#/definitions/CreateFileOptions" + }, + "x-go-name": "BatchFiles" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Branch": { "description": "Branch represents a repository branch", "type": "object", -- 2.34.1 From 93de827180bc00a38fb1aef0701386ac4b8074f8 Mon Sep 17 00:00:00 2001 From: yystopf Date: Tue, 12 Jul 2022 11:43:02 +0800 Subject: [PATCH 05/10] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=9B=B4=E6=94=B9=E6=96=87=E4=BB=B6=E7=BB=9F=E4=B8=80=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=BB=A5=E5=8F=8A=E7=BB=93=E6=9E=84=E4=BD=93=E5=AE=9A?= =?UTF-8?q?=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/repofiles/update.go | 43 +++++++ modules/structs/repo_file.go | 22 ++-- routers/api/v1/api.go | 6 +- routers/api/v1/repo/file.go | 167 +++++++++++------------- routers/api/v1/swagger/options.go | 2 +- templates/swagger/v1_json.tmpl | 205 +++++++++++------------------- 6 files changed, 204 insertions(+), 241 deletions(-) diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index 5b45479f3..f702e197a 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -25,6 +25,24 @@ import ( "golang.org/x/text/transform" ) +type FileActionType int + +const ( + ActionTypeCreate FileActionType = iota + 1 + ActionTypeUpdate + ActionTypeDelete +) + +var fileActionTypes = map[string]FileActionType{ + "create": ActionTypeCreate, + "update": ActionTypeUpdate, + "delete": ActionTypeDelete, +} + +func ToFileActionType(name string) FileActionType { + return fileActionTypes[name] +} + // IdentityOptions for a person's identity like an author or committer type IdentityOptions struct { Name string @@ -54,6 +72,25 @@ type UpdateRepoFileOptions struct { Signoff bool } +type BatchSingleFileOption struct { + Content string + TreePath string + FromTreePath string + ActionType FileActionType +} +type BatchUpdateFileOptions struct { + Files []BatchSingleFileOption + LastCommitID string + OldBranch string + NewBranch string + Message string + SHA string + Author *IdentityOptions + Commiter *IdentityOptions + Dates *CommitDateOptions + Signoff bool +} + func detectEncodingAndBOM(entry *git.TreeEntry, repo *models.Repository) (string, bool) { reader, err := entry.Blob().DataAsync() if err != nil { @@ -466,3 +503,9 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up } return file, nil } + +func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, opts *BatchUpdateFileOptions) ([]*structs.FileResponse, error) { + var responses []*structs.FileResponse + + return responses, nil +} diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index 828254ce1..09e033206 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -31,8 +31,16 @@ type CreateFileOptions struct { } // BatchCreateFileOptions options for creating more files -type BatchCreateFileOptions struct { - BatchFiles []CreateFileOptions `json:"batch_files"` +type BatchChangeFileOptions struct { + Header FileOptions `json:"header"` + Files []struct { + // enum: text,base64 + Encoding string `json:"encoding"` + FilePath string `json:"file_path"` + Content string `json:"content"` + // enum: create,update,delete + ActionType string `json:"action_type"` + } `json:"files"` } // DeleteFileOptions options for deleting files (used for other File structs below) @@ -44,11 +52,6 @@ type DeleteFileOptions struct { SHA string `json:"sha" binding:"Required"` } -// BatchDeleteFileOptions options for deleting files -type BatchDeleteFileOptions struct { - BatchFiles []DeleteFileOptions `json:"batch_files"` -} - // UpdateFileOptions options for updating 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 UpdateFileOptions struct { @@ -60,11 +63,6 @@ type UpdateFileOptions struct { FromPath string `json:"from_path" binding:"MaxSize(500)"` } -// BatchUpdateFileOptions options for updating more files. -type BatchUpdateFileOptions struct { - BatchFiles []UpdateFileOptions `json:"batch_files"` -} - // FileLinksResponse contains the links for a repo's file type FileLinksResponse struct { Self *string `json:"self"` diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f91fb92d3..3094d3e5a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1032,10 +1032,8 @@ func Routes() *web.Route { m.Put("", bind(api.UpdateFileOptions{}), repo.UpdateFile) m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile) }, reqRepoWriter(models.UnitTypeCode), reqToken()) - m.Group("/batch/*", func() { - m.Post("", bind(api.BatchCreateFileOptions{}), repo.BatchCreateFile) - m.Put("", bind(api.BatchUpdateFileOptions{}), repo.BatchUpdateFile) - m.Delete("", bind(api.BatchDeleteFileOptions{}), repo.BatchDeleteFile) + m.Group("/batch", func() { + m.Post("", bind(api.BatchChangeFileOptions{}), repo.BatchChangeFile) }, reqRepoWriter(models.UnitTypeCode), reqToken()) }, reqRepoReader(models.UnitTypeCode)) m.Get("/signing-key.gpg", misc.SigningKey) diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 89480355e..924769959 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -285,11 +285,11 @@ func CreateFile(ctx *context.APIContext) { } } -// BatchCreateFile handles API call for creating a file*** -func BatchCreateFile(ctx *context.APIContext) { - // swagger:operation POST /repos/{owner}/{repo}/contents/batch/{filepath} repository repoBatchCreateFile +// BatchChangeFile handles API call for change some files*** +func BatchChangeFile(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/contents/batch repository repoBatchChangeFile // --- - // summary: Create some files in a repository*** + // summary: Change some files in a repository*** // consumes: // - application/json // produces: @@ -305,16 +305,11 @@ func BatchCreateFile(ctx *context.APIContext) { // description: name of the repo // type: string // required: true - // - name: filepath - // in: path - // description: path of the file to create - // type: string - // required: true // - name: body // in: body // required: true // schema: - // "$ref": "#/definitions/BatchCreateFileOptions" + // "$ref": "#/definitions/BatchChangeFileOptions" // responses: // "201": // "$ref": "#/responses/FileResponse" @@ -324,7 +319,32 @@ func BatchCreateFile(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "422": // "$ref": "#/responses/error" + apiBatchOpts := web.GetForm(ctx).(*api.BatchChangeFileOptions) + fmt.Println(apiBatchOpts) + if ctx.Repo.Repository.IsEmpty { + ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty")) + } + + if apiBatchOpts.Header.BranchName == "" { + apiBatchOpts.Header.BranchName = ctx.Repo.Repository.DefaultBranch + } + + if apiBatchOpts.Header.Message == "" { + apiBatchOpts.Header.Message = time.Now().Format("RFC3339") + } + + if apiBatchOpts.Header.Dates.Author.IsZero() { + apiBatchOpts.Header.Dates.Author = time.Now() + } + + if apiBatchOpts.Header.Dates.Committer.IsZero() { + apiBatchOpts.Header.Dates.Committer = time.Now() + } + + if _, err := createOrUpdateOrDeleteFiles(ctx, apiBatchOpts); err != nil { + handleCreateOrUpdateFileError(ctx, err) + } ctx.JSON(http.StatusOK, map[string]string{}) } @@ -435,47 +455,51 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { ctx.Error(http.StatusInternalServerError, "UpdateFile", err) } -// BatchUpdateFile handles API call for updating a file -func BatchUpdateFile(ctx *context.APIContext) { - // swagger:operation PUT /repos/{owner}/{repo}/contents/batch/{filepath} repository repoBatchUpdateFile - // --- - // summary: Update some files in a repository*** - // consumes: - // - application/json - // produces: - // - application/jsoncontent - // 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: filepath - // in: path - // description: path of the file to update - // type: string - // required: true - // - name: body - // in: body - // required: true - // schema: - // "$ref": "#/definitions/BatchUpdateFileOptions" - // responses: - // "200": - // "$ref": "#/responses/FileResponse" - // "403": - // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" - // "422": - // "$ref": "#/responses/error" +func createOrUpdateOrDeleteFiles(ctx *context.APIContext, apiBatchOpts *api.BatchChangeFileOptions) ([]*api.FileResponse, error) { + if !canWriteFiles(ctx.Repo) { + return nil, models.ErrUserDoesNotHaveAccessToRepo{ + UserID: ctx.User.ID, + RepoName: ctx.Repo.Repository.LowerName, + } + } - ctx.JSON(http.StatusOK, map[string]string{}) + var files []repofiles.BatchSingleFileOption + for _, f := range apiBatchOpts.Files { + if f.Encoding == "base64" { + content, err := base64.StdEncoding.DecodeString(f.Content) + if err != nil { + return nil, err + } + f.Content = string(content) + } + files = append(files, repofiles.BatchSingleFileOption{ + Content: f.Content, + TreePath: f.FilePath, + ActionType: repofiles.ToFileActionType(f.ActionType), + }) + } + + opts := &repofiles.BatchUpdateFileOptions{ + Files: files, + Message: apiBatchOpts.Header.Message, + OldBranch: apiBatchOpts.Header.BranchName, + NewBranch: apiBatchOpts.Header.NewBranchName, + Commiter: &repofiles.IdentityOptions{ + Name: apiBatchOpts.Header.Committer.Name, + Email: apiBatchOpts.Header.Committer.Email, + }, + Author: &repofiles.IdentityOptions{ + Name: apiBatchOpts.Header.Author.Name, + Email: apiBatchOpts.Header.Author.Email, + }, + Dates: &repofiles.CommitDateOptions{ + Author: apiBatchOpts.Header.Dates.Author, + Committer: apiBatchOpts.Header.Dates.Committer, + }, + Signoff: apiBatchOpts.Header.Signoff, + } + + return repofiles.CreateOrUpdateOrDeleteRepofiles(ctx.Repo.Repository, ctx.User, opts) } // Called from both CreateFile or UpdateFile to handle both @@ -601,49 +625,6 @@ func DeleteFile(ctx *context.APIContext) { } } -// BatchDeleteFile Delete some files in a repository -func BatchDeleteFile(ctx *context.APIContext) { - // swagger:operation DELETE /repos/{owner}/{repo}/contents/batch/{filepath} repository repoBatchDeleteFile - // --- - // summary: Delete some 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: filepath - // in: path - // description: path of the file to delete - // type: string - // required: true - // - name: body - // in: body - // required: true - // schema: - // "$ref": "#/definitions/BatchDeleteFileOptions" - // responses: - // "200": - // "$ref": "#/responses/FileDeleteResponse" - // "400": - // "$ref": "#/responses/error" - // "403": - // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/error" - - ctx.JSON(http.StatusOK, map[string]string{}) -} - // GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir func GetContents(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index b63299dcb..bc052f946 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -120,7 +120,7 @@ type swaggerParameterBodies struct { CreateFileOptions api.CreateFileOptions // in:body - BatchCreateFileOptions api.BatchCreateFileOptions + BatchChangeFileOptions api.BatchChangeFileOptions // in:body UpdateFileOptions api.UpdateFileOptions diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 38714f840..4ca60f827 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3469,65 +3469,7 @@ } } }, - "/repos/{owner}/{repo}/contents/batch/{filepath}": { - "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/jsoncontent" - ], - "tags": [ - "repository" - ], - "summary": "Update some files in a repository***", - "operationId": "repoBatchUpdateFile", - "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 - }, - { - "type": "string", - "description": "path of the file to update", - "name": "filepath", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/BatchUpdateFileOptions" - } - } - ], - "responses": { - "200": { - "$ref": "#/responses/FileResponse" - }, - "403": { - "$ref": "#/responses/error" - }, - "404": { - "$ref": "#/responses/notFound" - }, - "422": { - "$ref": "#/responses/error" - } - } - }, + "/repos/{owner}/{repo}/contents/batch": { "post": { "consumes": [ "application/json" @@ -3538,8 +3480,8 @@ "tags": [ "repository" ], - "summary": "Create some files in a repository***", - "operationId": "repoBatchCreateFile", + "summary": "Change some files in a repository***", + "operationId": "repoBatchChangeFile", "parameters": [ { "type": "string", @@ -3555,19 +3497,12 @@ "in": "path", "required": true }, - { - "type": "string", - "description": "path of the file to create", - "name": "filepath", - "in": "path", - "required": true - }, { "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/BatchCreateFileOptions" + "$ref": "#/definitions/BatchChangeFileOptions" } } ], @@ -3585,64 +3520,6 @@ "$ref": "#/responses/error" } } - }, - "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "repository" - ], - "summary": "Delete some files in a repository***", - "operationId": "repoBatchDeleteFile", - "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 - }, - { - "type": "string", - "description": "path of the file to delete", - "name": "filepath", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/BatchDeleteFileOptions" - } - } - ], - "responses": { - "200": { - "$ref": "#/responses/FileDeleteResponse" - }, - "400": { - "$ref": "#/responses/error" - }, - "403": { - "$ref": "#/responses/error" - }, - "404": { - "$ref": "#/responses/error" - } - } } }, "/repos/{owner}/{repo}/contents/{filepath}": { @@ -13640,16 +13517,46 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, - "BatchCreateFileOptions": { + "BatchChangeFileOptions": { "description": "BatchCreateFileOptions options for creating more files", "type": "object", "properties": { - "batch_files": { + "files": { "type": "array", "items": { - "$ref": "#/definitions/CreateFileOptions" + "type": "object", + "properties": { + "action_type": { + "type": "string", + "enum": [ + "create", + "update", + "delete" + ], + "x-go-name": "ActionType" + }, + "content": { + "type": "string", + "x-go-name": "Content" + }, + "encoding": { + "type": "string", + "enum": [ + "text", + "base64" + ], + "x-go-name": "Encoding" + }, + "file_path": { + "type": "string", + "x-go-name": "FilePath" + } + } }, - "x-go-name": "BatchFiles" + "x-go-name": "Files" + }, + "header": { + "$ref": "#/definitions/FileOptions" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -16304,6 +16211,42 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "FileOptions": { + "description": "FileOptions options for all file APIs", + "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" + }, + "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" + }, "FileResponse": { "description": "FileResponse contains information about a repo's file", "type": "object", -- 2.34.1 From 63ef5921d1ae1a00222c08220f9fe3a061618614 Mon Sep 17 00:00:00 2001 From: yystopf Date: Tue, 12 Jul 2022 16:11:50 +0800 Subject: [PATCH 06/10] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=9B=B4=E6=94=B9=E6=96=87=E4=BB=B6=E5=87=BD=E6=95=B0=E5=AE=9A?= =?UTF-8?q?=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/repofiles/update.go | 329 +++++++++++++++++++++++++++++++++++- routers/api/v1/repo/file.go | 2 +- 2 files changed, 326 insertions(+), 5 deletions(-) diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index f702e197a..bb8d4c553 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -300,7 +300,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up } else if changed { return nil, models.ErrCommitIDDoesNotMatch{ GivenCommitID: opts.LastCommitID, - CurrentCommitID: opts.LastCommitID, + CurrentCommitID: commit.ID.String(), } } // The file wasn't modified, so we are good to delete it @@ -504,8 +504,329 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up return file, nil } -func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, opts *BatchUpdateFileOptions) ([]*structs.FileResponse, error) { - var responses []*structs.FileResponse +func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, opts *BatchUpdateFileOptions) (*structs.FileResponse, error) { - return responses, nil + if opts.OldBranch == "" { + opts.OldBranch = repo.DefaultBranch + } + + if opts.NewBranch == "" { + opts.NewBranch = opts.OldBranch + } + + // oldBranch must exist for this operation + if _, err := repo_module.GetBranch(repo, opts.OldBranch); err != nil { + return nil, err + } + + if opts.NewBranch != opts.OldBranch { + existingBranch, err := repo_module.GetBranch(repo, opts.NewBranch) + if existingBranch != nil { + return nil, models.ErrBranchAlreadyExists{ + BranchName: opts.NewBranch, + } + } + if err != nil && !git.IsErrBranchNotExist(err) { + return nil, err + } + } else { + protectedBranch, err := repo.GetBranchProtection(opts.OldBranch) + if err != nil { + return nil, err + } + if protectedBranch != nil { + if !protectedBranch.CanUserPush(doer.ID) { + return nil, models.ErrUserCannotCommit{ + UserName: doer.LowerName, + } + } + if protectedBranch.RequireSignedCommits { + _, _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) + if err != nil { + if !models.IsErrWontSign(err) { + return nil, err + } + return nil, models.ErrUserCannotCommit{ + UserName: doer.LowerName, + } + } + } + patterns := protectedBranch.GetProtectedFilePatterns() + for _, pat := range patterns { + for _, file := range opts.Files { + if pat.Match(strings.ToLower(file.TreePath)) { + return nil, models.ErrFilePathProtected{ + Path: file.TreePath, + } + } + } + } + } + } + + optTreePath := opts.Files[0].TreePath + optFromTreePath := opts.Files[0].FromTreePath + optActionType := opts.Files[0].ActionType + optContent := opts.Files[0].Content + + if optTreePath != "" && optFromTreePath == "" { + optFromTreePath = optTreePath + } + + treePath := CleanUploadFileName(optTreePath) + if treePath == "" { + return nil, models.ErrFilenameInvalid{ + Path: optTreePath, + } + } + + fromTreePath := CleanUploadFileName(optFromTreePath) + if fromTreePath == "" && optFromTreePath != "" { + return nil, models.ErrFilenameInvalid{ + Path: optFromTreePath, + } + } + + message := strings.TrimSpace(opts.Message) + author, commiter := GetAuthorAndCommitterUsers(opts.Author, opts.Commiter, doer) + + t, err := NewTemporaryUploadRepository(repo) + if err != nil { + log.Error("%v", 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 + } + + if opts.LastCommitID == "" { + opts.LastCommitID = commit.ID.String() + } else { + lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID) + if err != nil { + return nil, fmt.Errorf("ConvertToSHA1: Invalid last commit ID: %v", err) + } + opts.LastCommitID = lastCommitID.String() + } + + encoding := "UTF-8" + bom := false + executable := false + + switch optActionType { + case ActionTypeCreate: + case ActionTypeDelete: + default: + fromEntry, err := commit.GetTreeEntryByPath(fromTreePath) + if err != nil { + return nil, err + } + if opts.SHA != "" { + if opts.SHA != fromEntry.ID.String() { + return nil, models.ErrSHADoesNotMatch{ + Path: optTreePath, + GivenSHA: opts.SHA, + CurrentSHA: fromEntry.ID.String(), + } + } + } else if opts.LastCommitID != "" { + 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, + } + } + } + } else { + return nil, models.ErrSHAOrCommitIDNotProvided{} + } + encoding, bom = detectEncodingAndBOM(fromEntry, repo) + executable = fromEntry.IsExecutable() + } + + treePathParts := strings.Split(treePath, "/") + subTreePath := "" + for index, part := range treePathParts { + subTreePath = path.Join(subTreePath, part) + entry, err := commit.GetTreeEntryByPath(subTreePath) + if err != nil { + if git.IsErrNotExist(err) { + break + } + return nil, err + } + if index < len(treePathParts)-1 { + if !entry.IsDir() { + return nil, 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 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 || optActionType == ActionTypeCreate { + 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(optTreePath, optFromTreePath) + if err != nil { + return nil, fmt.Errorf("UpdateRepoFile: %v", err) + } + + if optActionType == ActionTypeCreate { + for _, file := range filesInIndex { + if file == optTreePath { + return nil, models.ErrRepoFileAlreadyExists{ + Path: optTreePath, + } + } + } + } + + // Remove the old path from the tree + if fromTreePath != treePath && len(filesInIndex) > 0 { + for _, file := range filesInIndex { + if file == fromTreePath { + if err := t.RemoveFilesFromIndex(optFromTreePath); err != nil { + return nil, err + } + } + } + } + + content := optContent + 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 { + log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", optTreePath, optFromTreePath, encoding, err) + result = content + } + content = result + } else { + log.Error("Unknown encoding: %s", encoding) + } + } + + optContent = content + var lfsMetaObject *models.LFSMetaObject + if setting.LFS.StartServer { + filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ + Attributes: []string{"filter"}, + Filenames: []string{treePath}, + }) + if err != nil { + return nil, err + } + + if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" { + pointer, err := lfs.GeneratePointer(strings.NewReader(optContent)) + if err != nil { + return nil, err + } + lfsMetaObject = &models.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID} + content = pointer.StringContent() + } + } + + objectHash, err := t.HashObject(strings.NewReader(content)) + if err != nil { + return nil, err + } + + 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 + } + } + + treeHash, err := t.WriteTree() + if err != nil { + return nil, err + } + + var commitHash string + if opts.Dates != nil { + commitHash, err = t.CommitTreeWithDate(author, commiter, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) + } else { + commitHash, err = t.CommitTree(author, commiter, treeHash, message, opts.Signoff) + } + + if err != nil { + return nil, err + } + + if lfsMetaObject != nil { + lfsMetaObject, err = models.NewLFSMetaObject(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(optContent)); err != nil { + if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { + return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err) + } + return nil, err + } + } + } + + if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { + log.Error("%T %v", err, err) + return nil, err + } + + commit, err = t.GetCommit(commitHash) + if err != nil { + return nil, err + } + + file, err := GetFileResponseFromCommit(repo, commit, opts.NewBranch, treePath) + if err != nil { + return nil, err + } + + return file, nil } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 924769959..f6f42bf9d 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -455,7 +455,7 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { ctx.Error(http.StatusInternalServerError, "UpdateFile", err) } -func createOrUpdateOrDeleteFiles(ctx *context.APIContext, apiBatchOpts *api.BatchChangeFileOptions) ([]*api.FileResponse, error) { +func createOrUpdateOrDeleteFiles(ctx *context.APIContext, apiBatchOpts *api.BatchChangeFileOptions) (*api.FileResponse, error) { if !canWriteFiles(ctx.Repo) { return nil, models.ErrUserDoesNotHaveAccessToRepo{ UserID: ctx.User.ID, -- 2.34.1 From 8e5f72bc41ea5048e303b3dbe17e111895cd8134 Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 13 Jul 2022 17:16:06 +0800 Subject: [PATCH 07/10] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/repofiles/update.go | 356 ++++++++++++++++++++++-------------- 1 file changed, 217 insertions(+), 139 deletions(-) diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index bb8d4c553..40efb0cad 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -618,28 +618,47 @@ func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, opts.LastCommitID = lastCommitID.String() } - encoding := "UTF-8" - bom := false - executable := false + var commitHash string + if optActionType == ActionTypeDelete { + // Get the files in the index + filesInIndex, err := t.LsFiles(optTreePath) + if err != nil { + return nil, fmt.Errorf("DeleteRepoFile: %v", err) + } + // Find the file we want to delete in the index + inFilelist := false + for _, file := range filesInIndex { + if file == optTreePath { + inFilelist = true + break + } + } + if !inFilelist { + return nil, models.ErrRepoFileDoesNotExist{ + Path: optTreePath, + } + } - switch optActionType { - case ActionTypeCreate: - case ActionTypeDelete: - default: - fromEntry, err := commit.GetTreeEntryByPath(fromTreePath) + // 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 opts.SHA != fromEntry.ID.String() { + // 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: optTreePath, + Path: treePath, GivenSHA: opts.SHA, - CurrentSHA: fromEntry.ID.String(), + 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 { @@ -648,168 +667,227 @@ func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, 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{} } - encoding, bom = detectEncodingAndBOM(fromEntry, repo) - executable = fromEntry.IsExecutable() - } - treePathParts := strings.Split(treePath, "/") - subTreePath := "" - for index, part := range treePathParts { - subTreePath = path.Join(subTreePath, part) - entry, err := commit.GetTreeEntryByPath(subTreePath) - if err != nil { - if git.IsErrNotExist(err) { - break - } + // Remove the file from the index + if err := t.RemoveFilesFromIndex(optTreePath); err != nil { return nil, err } - if index < len(treePathParts)-1 { - if !entry.IsDir() { + + // Now write the tree + treeHash, err := t.WriteTree() + if err != nil { + return nil, err + } + + // Now commit the tree + if opts.Dates != nil { + commitHash, err = t.CommitTreeWithDate(author, commiter, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) + } else { + commitHash, err = t.CommitTree(author, commiter, treeHash, message, opts.Signoff) + } + if err != nil { + return nil, err + } + } else { + encoding := "UTF-8" + bom := false + executable := false + + switch optActionType { + case ActionTypeCreate: + case ActionTypeDelete: + default: + fromEntry, err := commit.GetTreeEntryByPath(fromTreePath) + if err != nil { + return nil, err + } + if opts.SHA != "" { + if opts.SHA != fromEntry.ID.String() { + return nil, models.ErrSHADoesNotMatch{ + Path: optTreePath, + GivenSHA: opts.SHA, + CurrentSHA: fromEntry.ID.String(), + } + } + } else if opts.LastCommitID != "" { + 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, + } + } + } + } else { + return nil, models.ErrSHAOrCommitIDNotProvided{} + } + encoding, bom = detectEncodingAndBOM(fromEntry, repo) + executable = fromEntry.IsExecutable() + } + + treePathParts := strings.Split(treePath, "/") + subTreePath := "" + for index, part := range treePathParts { + subTreePath = path.Join(subTreePath, part) + entry, err := commit.GetTreeEntryByPath(subTreePath) + if err != nil { + if git.IsErrNotExist(err) { + break + } + return nil, err + } + if index < len(treePathParts)-1 { + if !entry.IsDir() { + return nil, 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 nil, models.ErrFilePathInvalid{ - Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", subTreePath), + Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath), Path: subTreePath, Name: part, - Type: git.EntryModeBlob, + Type: git.EntryModeSymlink, } - } - } 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 || optActionType == ActionTypeCreate { - 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(optTreePath, optFromTreePath) - if err != nil { - return nil, fmt.Errorf("UpdateRepoFile: %v", err) - } - - if optActionType == ActionTypeCreate { - for _, file := range filesInIndex { - if file == optTreePath { + } 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 || optActionType == ActionTypeCreate { return nil, models.ErrRepoFileAlreadyExists{ - Path: optTreePath, + Path: treePath, } } } - } + // Get the two paths (might be the same if not moving) from the index if they exist + filesInIndex, err := t.LsFiles(optTreePath, optFromTreePath) + if err != nil { + return nil, fmt.Errorf("UpdateRepoFile: %v", err) + } - // Remove the old path from the tree - if fromTreePath != treePath && len(filesInIndex) > 0 { - for _, file := range filesInIndex { - if file == fromTreePath { - if err := t.RemoveFilesFromIndex(optFromTreePath); err != nil { + if optActionType == ActionTypeCreate { + for _, file := range filesInIndex { + if file == optTreePath { + return nil, models.ErrRepoFileAlreadyExists{ + Path: optTreePath, + } + } + } + } + + // Remove the old path from the tree + if fromTreePath != treePath && len(filesInIndex) > 0 { + for _, file := range filesInIndex { + if file == fromTreePath { + if err := t.RemoveFilesFromIndex(optFromTreePath); err != nil { + return nil, err + } + } + } + } + + content := optContent + 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 { + log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", optTreePath, optFromTreePath, encoding, err) + result = content + } + content = result + } else { + log.Error("Unknown encoding: %s", encoding) + } + } + + optContent = content + var lfsMetaObject *models.LFSMetaObject + if setting.LFS.StartServer { + filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ + Attributes: []string{"filter"}, + Filenames: []string{treePath}, + }) + if err != nil { + return nil, err + } + + if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" { + pointer, err := lfs.GeneratePointer(strings.NewReader(optContent)) + if err != nil { return nil, err } + lfsMetaObject = &models.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID} + content = pointer.StringContent() } } - } - content := optContent - 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 { - log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", optTreePath, optFromTreePath, encoding, err) - result = content + objectHash, err := t.HashObject(strings.NewReader(content)) + if err != nil { + return nil, err + } + + if executable { + if err := t.AddObjectToIndex("100755", objectHash, treePath); err != nil { + return nil, err } - content = result } else { - log.Error("Unknown encoding: %s", encoding) + if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil { + return nil, err + } } - } - optContent = content - var lfsMetaObject *models.LFSMetaObject - if setting.LFS.StartServer { - filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ - Attributes: []string{"filter"}, - Filenames: []string{treePath}, - }) + treeHash, err := t.WriteTree() if err != nil { return nil, err } - if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" { - pointer, err := lfs.GeneratePointer(strings.NewReader(optContent)) + if opts.Dates != nil { + commitHash, err = t.CommitTreeWithDate(author, commiter, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) + } else { + commitHash, err = t.CommitTree(author, commiter, treeHash, message, opts.Signoff) + } + + if err != nil { + return nil, err + } + + if lfsMetaObject != nil { + lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject) if err != nil { return nil, err } - lfsMetaObject = &models.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID} - content = pointer.StringContent() - } - } - - objectHash, err := t.HashObject(strings.NewReader(content)) - if err != nil { - return nil, err - } - - 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 - } - } - - treeHash, err := t.WriteTree() - if err != nil { - return nil, err - } - - var commitHash string - if opts.Dates != nil { - commitHash, err = t.CommitTreeWithDate(author, commiter, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) - } else { - commitHash, err = t.CommitTree(author, commiter, treeHash, message, opts.Signoff) - } - - if err != nil { - return nil, err - } - - if lfsMetaObject != nil { - lfsMetaObject, err = models.NewLFSMetaObject(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(optContent)); err != nil { - if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { - return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, 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(optContent)); err != nil { + if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { + return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err) + } + return nil, err + } + } } } -- 2.34.1 From 445a5f7d77409a0fb2c2074dfcb79af0d79b8fa5 Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 13 Jul 2022 18:13:47 +0800 Subject: [PATCH 08/10] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/repofiles/update.go | 581 ++++++++++++++++++------------------ routers/api/v1/repo/file.go | 40 +-- 2 files changed, 315 insertions(+), 306 deletions(-) diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index 40efb0cad..859e57e76 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" + "github.com/gobwas/glob" stdcharset "golang.org/x/net/html/charset" "golang.org/x/text/transform" @@ -72,6 +73,12 @@ type UpdateRepoFileOptions struct { Signoff bool } +type ExchangeFileOption struct { + FileChan chan BatchSingleFileOption + StopChan chan bool + ErrChan chan error +} + type BatchSingleFileOption struct { Content string TreePath string @@ -504,8 +511,9 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up return file, nil } -func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, opts *BatchUpdateFileOptions) (*structs.FileResponse, error) { +func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, opts *BatchUpdateFileOptions, exchange *ExchangeFileOption) (*structs.FileResponse, error) { + var protectedPatterns []glob.Glob if opts.OldBranch == "" { opts.OldBranch = repo.DefaultBranch } @@ -551,39 +559,16 @@ func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, } } } - patterns := protectedBranch.GetProtectedFilePatterns() - for _, pat := range patterns { - for _, file := range opts.Files { - if pat.Match(strings.ToLower(file.TreePath)) { - return nil, models.ErrFilePathProtected{ - Path: file.TreePath, - } - } - } - } - } - } - - optTreePath := opts.Files[0].TreePath - optFromTreePath := opts.Files[0].FromTreePath - optActionType := opts.Files[0].ActionType - optContent := opts.Files[0].Content - - if optTreePath != "" && optFromTreePath == "" { - optFromTreePath = optTreePath - } - - treePath := CleanUploadFileName(optTreePath) - if treePath == "" { - return nil, models.ErrFilenameInvalid{ - Path: optTreePath, - } - } - - fromTreePath := CleanUploadFileName(optFromTreePath) - if fromTreePath == "" && optFromTreePath != "" { - return nil, models.ErrFilenameInvalid{ - Path: optFromTreePath, + protectedPatterns = protectedBranch.GetProtectedFilePatterns() + // for _, pat := range patterns { + // for _, file := range opts.Files { + // if pat.Match(strings.ToLower(file.TreePath)) { + // return nil, models.ErrFilePathProtected{ + // Path: file.TreePath, + // } + // } + // } + // } } } @@ -617,279 +602,297 @@ func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, } opts.LastCommitID = lastCommitID.String() } - var commitHash string - if optActionType == ActionTypeDelete { - // Get the files in the index - filesInIndex, err := t.LsFiles(optTreePath) - if err != nil { - return nil, fmt.Errorf("DeleteRepoFile: %v", err) - } - // Find the file we want to delete in the index - inFilelist := false - for _, file := range filesInIndex { - if file == optTreePath { - inFilelist = true - break - } - } - if !inFilelist { - return nil, models.ErrRepoFileDoesNotExist{ - Path: optTreePath, - } - } - // 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(), + for { + select { + case file := <-exchange.FileChan: + fmt.Println(protectedPatterns) + fmt.Println(file) + + optTreePath := file.TreePath + optFromTreePath := file.FromTreePath + optActionType := file.ActionType + optContent := file.Content + + if optTreePath != "" && optFromTreePath == "" { + optFromTreePath = optTreePath + } + + treePath := CleanUploadFileName(optTreePath) + if treePath == "" { + return nil, models.ErrFilenameInvalid{ + Path: optTreePath, } } - } 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, + + fromTreePath := CleanUploadFileName(optFromTreePath) + if fromTreePath == "" && optFromTreePath != "" { + return nil, models.ErrFilenameInvalid{ + Path: optFromTreePath, + } + } + + if optActionType == ActionTypeDelete { + // Get the files in the index + filesInIndex, err := t.LsFiles(optTreePath) + if err != nil { + return nil, fmt.Errorf("DeleteRepoFile: %v", err) + } + // Find the file we want to delete in the index + inFilelist := false + for _, file := range filesInIndex { + if file == optTreePath { + inFilelist = true + break } } - // 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(optTreePath); err != nil { - return nil, err - } - - // Now write the tree - treeHash, err := t.WriteTree() - if err != nil { - return nil, err - } - - // Now commit the tree - if opts.Dates != nil { - commitHash, err = t.CommitTreeWithDate(author, commiter, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) - } else { - commitHash, err = t.CommitTree(author, commiter, treeHash, message, opts.Signoff) - } - if err != nil { - return nil, err - } - } else { - encoding := "UTF-8" - bom := false - executable := false - - switch optActionType { - case ActionTypeCreate: - case ActionTypeDelete: - default: - fromEntry, err := commit.GetTreeEntryByPath(fromTreePath) - if err != nil { - return nil, err - } - if opts.SHA != "" { - if opts.SHA != fromEntry.ID.String() { - return nil, models.ErrSHADoesNotMatch{ - Path: optTreePath, - GivenSHA: opts.SHA, - CurrentSHA: fromEntry.ID.String(), - } - } - } else if opts.LastCommitID != "" { - 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, - } - } - } - } else { - return nil, models.ErrSHAOrCommitIDNotProvided{} - } - encoding, bom = detectEncodingAndBOM(fromEntry, repo) - executable = fromEntry.IsExecutable() - } - - treePathParts := strings.Split(treePath, "/") - subTreePath := "" - for index, part := range treePathParts { - subTreePath = path.Join(subTreePath, part) - entry, err := commit.GetTreeEntryByPath(subTreePath) - if err != nil { - if git.IsErrNotExist(err) { - break - } - return nil, err - } - if index < len(treePathParts)-1 { - if !entry.IsDir() { - return nil, 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 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 || optActionType == ActionTypeCreate { - 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(optTreePath, optFromTreePath) - if err != nil { - return nil, fmt.Errorf("UpdateRepoFile: %v", err) - } - - if optActionType == ActionTypeCreate { - for _, file := range filesInIndex { - if file == optTreePath { - return nil, models.ErrRepoFileAlreadyExists{ + if !inFilelist { + return nil, models.ErrRepoFileDoesNotExist{ Path: optTreePath, } } - } - } - // Remove the old path from the tree - if fromTreePath != treePath && len(filesInIndex) > 0 { - for _, file := range filesInIndex { - if file == fromTreePath { - if err := t.RemoveFilesFromIndex(optFromTreePath); err != nil { + // 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(optTreePath); err != nil { + return nil, err + } + + } else { + encoding := "UTF-8" + bom := false + executable := false + + if optActionType == ActionTypeUpdate { + fromEntry, err := commit.GetTreeEntryByPath(fromTreePath) + if err != nil { + return nil, err + } + if opts.SHA != "" { + if opts.SHA != fromEntry.ID.String() { + return nil, models.ErrSHADoesNotMatch{ + Path: optTreePath, + GivenSHA: opts.SHA, + CurrentSHA: fromEntry.ID.String(), + } + } + } else if opts.LastCommitID != "" { + 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, + } + } + } + } else { + return nil, models.ErrSHAOrCommitIDNotProvided{} + } + encoding, bom = detectEncodingAndBOM(fromEntry, repo) + executable = fromEntry.IsExecutable() + } + + treePathParts := strings.Split(treePath, "/") + subTreePath := "" + for index, part := range treePathParts { + subTreePath = path.Join(subTreePath, part) + entry, err := commit.GetTreeEntryByPath(subTreePath) + if err != nil { + if git.IsErrNotExist(err) { + break + } + return nil, err + } + if index < len(treePathParts)-1 { + if !entry.IsDir() { + return nil, 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 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 || optActionType == ActionTypeCreate { + 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(optTreePath, optFromTreePath) + if err != nil { + return nil, fmt.Errorf("UpdateRepoFile: %v", err) + } + + if optActionType == ActionTypeCreate { + for _, file := range filesInIndex { + if file == optTreePath { + return nil, models.ErrRepoFileAlreadyExists{ + Path: optTreePath, + } + } + } + } + + // Remove the old path from the tree + if fromTreePath != treePath && len(filesInIndex) > 0 { + for _, file := range filesInIndex { + if file == fromTreePath { + if err := t.RemoveFilesFromIndex(optFromTreePath); err != nil { + return nil, err + } + } + } + } + + content := optContent + 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 { + log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", optTreePath, optFromTreePath, encoding, err) + result = content + } + content = result + } else { + log.Error("Unknown encoding: %s", encoding) + } + } + + optContent = content + var lfsMetaObject *models.LFSMetaObject + if setting.LFS.StartServer { + filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ + Attributes: []string{"filter"}, + Filenames: []string{treePath}, + }) + if err != nil { + return nil, err + } + + if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" { + pointer, err := lfs.GeneratePointer(strings.NewReader(optContent)) + if err != nil { + return nil, err + } + lfsMetaObject = &models.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID} + content = pointer.StringContent() + } + } + + objectHash, err := t.HashObject(strings.NewReader(content)) + if err != nil { + return nil, err + } + + 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 } } - } - } - content := optContent - 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 { - log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", optTreePath, optFromTreePath, encoding, err) - result = content - } - content = result - } else { - log.Error("Unknown encoding: %s", encoding) - } - } - - optContent = content - var lfsMetaObject *models.LFSMetaObject - if setting.LFS.StartServer { - filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ - Attributes: []string{"filter"}, - Filenames: []string{treePath}, - }) - if err != nil { - return nil, err - } - - if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" { - pointer, err := lfs.GeneratePointer(strings.NewReader(optContent)) - if err != nil { - return nil, err - } - lfsMetaObject = &models.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID} - content = pointer.StringContent() - } - } - - objectHash, err := t.HashObject(strings.NewReader(content)) - if err != nil { - return nil, err - } - - 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 - } - } - - treeHash, err := t.WriteTree() - if err != nil { - return nil, err - } - - if opts.Dates != nil { - commitHash, err = t.CommitTreeWithDate(author, commiter, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) - } else { - commitHash, err = t.CommitTree(author, commiter, treeHash, message, opts.Signoff) - } - - if err != nil { - return nil, err - } - - if lfsMetaObject != nil { - lfsMetaObject, err = models.NewLFSMetaObject(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(optContent)); err != nil { - if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { - return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err) + if lfsMetaObject != nil { + lfsMetaObject, err = models.NewLFSMetaObject(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(optContent)); err != nil { + if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { + return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err) + } + return nil, err + } } - return nil, err } } + case err := <-exchange.ErrChan: + return nil, err + case _ = <-exchange.StopChan: + goto end } } +end: + // Now write the tree + treeHash, err := t.WriteTree() + if err != nil { + return nil, err + } + + // Now commit the tree + if opts.Dates != nil { + commitHash, err = t.CommitTreeWithDate(author, commiter, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) + } else { + commitHash, err = t.CommitTree(author, commiter, treeHash, message, opts.Signoff) + } + if err != nil { + return nil, err + } if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { log.Error("%T %v", err, err) @@ -901,7 +904,7 @@ func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, return nil, err } - file, err := GetFileResponseFromCommit(repo, commit, opts.NewBranch, treePath) + file, err := GetFileResponseFromCommit(repo, commit, opts.NewBranch, "") if err != nil { return nil, err } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index f6f42bf9d..5bf74a5f9 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -462,25 +462,31 @@ func createOrUpdateOrDeleteFiles(ctx *context.APIContext, apiBatchOpts *api.Batc RepoName: ctx.Repo.Repository.LowerName, } } - - var files []repofiles.BatchSingleFileOption - for _, f := range apiBatchOpts.Files { - if f.Encoding == "base64" { - content, err := base64.StdEncoding.DecodeString(f.Content) - if err != nil { - return nil, err - } - f.Content = string(content) - } - files = append(files, repofiles.BatchSingleFileOption{ - Content: f.Content, - TreePath: f.FilePath, - ActionType: repofiles.ToFileActionType(f.ActionType), - }) + fileChan := make(chan repofiles.BatchSingleFileOption) + stopChan := make(chan bool) + errChan := make(chan error) + exchangeOption := &repofiles.ExchangeFileOption{ + FileChan: fileChan, + StopChan: stopChan, + ErrChan: errChan, } + go func() { + for _, f := range apiBatchOpts.Files { + if f.Encoding == "base64" { + content, err := base64.StdEncoding.DecodeString(f.Content) + exchangeOption.ErrChan <- err + f.Content = string(content) + } + exchangeOption.FileChan <- repofiles.BatchSingleFileOption{ + Content: f.Content, + TreePath: f.FilePath, + ActionType: repofiles.ToFileActionType(f.ActionType), + } + } + exchangeOption.StopChan <- true + }() opts := &repofiles.BatchUpdateFileOptions{ - Files: files, Message: apiBatchOpts.Header.Message, OldBranch: apiBatchOpts.Header.BranchName, NewBranch: apiBatchOpts.Header.NewBranchName, @@ -499,7 +505,7 @@ func createOrUpdateOrDeleteFiles(ctx *context.APIContext, apiBatchOpts *api.Batc Signoff: apiBatchOpts.Header.Signoff, } - return repofiles.CreateOrUpdateOrDeleteRepofiles(ctx.Repo.Repository, ctx.User, opts) + return repofiles.CreateOrUpdateOrDeleteRepofiles(ctx.Repo.Repository, ctx.User, opts, exchangeOption) } // Called from both CreateFile or UpdateFile to handle both -- 2.34.1 From bfd5804dd07798eebd1d8d53021b58b827b68d37 Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 13 Jul 2022 18:18:23 +0800 Subject: [PATCH 09/10] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E4=BF=9D=E6=8A=A4?= =?UTF-8?q?=E5=88=86=E6=94=AFpatterns=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/repofiles/update.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index 859e57e76..4af0a1ab6 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -560,15 +560,6 @@ func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, } } protectedPatterns = protectedBranch.GetProtectedFilePatterns() - // for _, pat := range patterns { - // for _, file := range opts.Files { - // if pat.Match(strings.ToLower(file.TreePath)) { - // return nil, models.ErrFilePathProtected{ - // Path: file.TreePath, - // } - // } - // } - // } } } @@ -607,8 +598,13 @@ func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, for { select { case file := <-exchange.FileChan: - fmt.Println(protectedPatterns) - fmt.Println(file) + for _, pat := range protectedPatterns { + if pat.Match(strings.ToLower(file.TreePath)) { + return nil, models.ErrFilePathProtected{ + Path: file.TreePath, + } + } + } optTreePath := file.TreePath optFromTreePath := file.FromTreePath -- 2.34.1 From a7eebc5c598ca02df00c713a1a902cb61f931094 Mon Sep 17 00:00:00 2001 From: yystopf Date: Thu, 14 Jul 2022 14:23:29 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/repofiles/file.go | 15 +++++++++++++++ modules/repofiles/update.go | 7 +++++-- modules/structs/repo_file.go | 6 ++++++ routers/api/v1/repo/file.go | 10 ++++++---- templates/swagger/v1_json.tmpl | 2 +- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/modules/repofiles/file.go b/modules/repofiles/file.go index abd14b1db..7581552a4 100644 --- a/modules/repofiles/file.go +++ b/modules/repofiles/file.go @@ -15,6 +15,21 @@ import ( api "code.gitea.io/gitea/modules/structs" ) +func GetBatchFileResponseFromCommit(repo *models.Repository, commit *git.Commit, branch string, treeNames []string) (*api.BatchFileResponse, error) { + fileCommitResponse, _ := GetFileCommitResponse(repo, commit) + verification := GetPayloadCommitVerification(commit) + batchFileResponse := &api.BatchFileResponse{ + Commit: fileCommitResponse, + Verification: verification, + } + for _, treeName := range treeNames { + fileContent, _ := GetContents(repo, treeName, branch, false) + batchFileResponse.Contents = append(batchFileResponse.Contents, fileContent) + } + + return batchFileResponse, nil +} + // GetFileResponseFromCommit Constructs a FileResponse from a Commit object func GetFileResponseFromCommit(repo *models.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) { fileContents, _ := GetContents(repo, treeName, branch, false) // ok if fails, then will be nil diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index 4af0a1ab6..cbb348e65 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -511,7 +511,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up return file, nil } -func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, opts *BatchUpdateFileOptions, exchange *ExchangeFileOption) (*structs.FileResponse, error) { +func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, opts *BatchUpdateFileOptions, exchange *ExchangeFileOption) (*structs.BatchFileResponse, error) { var protectedPatterns []glob.Glob if opts.OldBranch == "" { @@ -594,6 +594,7 @@ func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, opts.LastCommitID = lastCommitID.String() } var commitHash string + var treeNames []string for { select { @@ -867,6 +868,8 @@ func CreateOrUpdateOrDeleteRepofiles(repo *models.Repository, doer *models.User, } } } + opts.Files = append(opts.Files, file) + treeNames = append(treeNames, file.TreePath) case err := <-exchange.ErrChan: return nil, err case _ = <-exchange.StopChan: @@ -900,7 +903,7 @@ end: return nil, err } - file, err := GetFileResponseFromCommit(repo, commit, opts.NewBranch, "") + file, err := GetBatchFileResponseFromCommit(repo, commit, opts.NewBranch, treeNames) if err != nil { return nil, err } diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index 09e033206..f6b7e6dcb 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -119,6 +119,12 @@ type FileResponse struct { Verification *PayloadCommitVerification `json:"verification"` } +type BatchFileResponse struct { + Contents []*ContentsResponse `json:"contents"` + Commit *FileCommitResponse `json:"commit"` + Verification *PayloadCommitVerification `json:"verification"` +} + // FileDeleteResponse contains information about a repo's file that was deleted type FileDeleteResponse struct { Content interface{} `json:"content"` // to be set to nil diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 5bf74a5f9..301ab5a0a 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -280,6 +280,7 @@ func CreateFile(ctx *context.APIContext) { if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil { handleCreateOrUpdateFileError(ctx, err) + return } else { ctx.JSON(http.StatusCreated, fileResponse) } @@ -312,7 +313,7 @@ func BatchChangeFile(ctx *context.APIContext) { // "$ref": "#/definitions/BatchChangeFileOptions" // responses: // "201": - // "$ref": "#/responses/FileResponse" + // "$ref": "#/responses/BatchFileResponse" // "403": // "$ref": "#/responses/error" // "404": @@ -342,10 +343,11 @@ func BatchChangeFile(ctx *context.APIContext) { apiBatchOpts.Header.Dates.Committer = time.Now() } - if _, err := createOrUpdateOrDeleteFiles(ctx, apiBatchOpts); err != nil { + if batchFileResponse, err := createOrUpdateOrDeleteFiles(ctx, apiBatchOpts); err != nil { handleCreateOrUpdateFileError(ctx, err) + } else { + ctx.JSON(http.StatusOK, batchFileResponse) } - ctx.JSON(http.StatusOK, map[string]string{}) } // UpdateFile handles API call for updating a file @@ -455,7 +457,7 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { ctx.Error(http.StatusInternalServerError, "UpdateFile", err) } -func createOrUpdateOrDeleteFiles(ctx *context.APIContext, apiBatchOpts *api.BatchChangeFileOptions) (*api.FileResponse, error) { +func createOrUpdateOrDeleteFiles(ctx *context.APIContext, apiBatchOpts *api.BatchChangeFileOptions) (*api.BatchFileResponse, error) { if !canWriteFiles(ctx.Repo) { return nil, models.ErrUserDoesNotHaveAccessToRepo{ UserID: ctx.User.ID, diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 4ca60f827..c4058a0a1 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3508,7 +3508,7 @@ ], "responses": { "201": { - "$ref": "#/responses/FileResponse" + "$ref": "#/responses/BatchFileResponse" }, "403": { "$ref": "#/responses/error" -- 2.34.1