Add apply-patch, basic revert and cherry-pick functionality (#17902)
This code adds a simple endpoint to apply patches to repositories and branches on gitea. This is then used along with the conflicting checking code in #18004 to provide a basic implementation of cherry-pick revert. Now because the buttons necessary for cherry-pick and revert have required us to create a dropdown next to the Browse Source button I've also implemented Create Branch and Create Tag operations. Fix #3880 Fix #17986 Signed-off-by: Andrew Thornton <art27@cantab.net>tokarchuk/v1.17
parent
439ad34c71
commit
eb748f5f3c
@ -0,0 +1,107 @@ |
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repo |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"time" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
api "code.gitea.io/gitea/modules/structs" |
||||||
|
"code.gitea.io/gitea/modules/web" |
||||||
|
"code.gitea.io/gitea/services/repository/files" |
||||||
|
) |
||||||
|
|
||||||
|
// ApplyDiffPatch handles API call for applying a patch
|
||||||
|
func ApplyDiffPatch(ctx *context.APIContext) { |
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/diffpatch repository repoApplyDiffPatch
|
||||||
|
// ---
|
||||||
|
// summary: Apply diff patch to 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: body
|
||||||
|
// in: body
|
||||||
|
// required: true
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/UpdateFileOptions"
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/FileResponse"
|
||||||
|
apiOpts := web.GetForm(ctx).(*api.ApplyDiffPatchFileOptions) |
||||||
|
|
||||||
|
opts := &files.ApplyDiffPatchOptions{ |
||||||
|
Content: apiOpts.Content, |
||||||
|
SHA: apiOpts.SHA, |
||||||
|
Message: apiOpts.Message, |
||||||
|
OldBranch: apiOpts.BranchName, |
||||||
|
NewBranch: apiOpts.NewBranchName, |
||||||
|
Committer: &files.IdentityOptions{ |
||||||
|
Name: apiOpts.Committer.Name, |
||||||
|
Email: apiOpts.Committer.Email, |
||||||
|
}, |
||||||
|
Author: &files.IdentityOptions{ |
||||||
|
Name: apiOpts.Author.Name, |
||||||
|
Email: apiOpts.Author.Email, |
||||||
|
}, |
||||||
|
Dates: &files.CommitDateOptions{ |
||||||
|
Author: apiOpts.Dates.Author, |
||||||
|
Committer: apiOpts.Dates.Committer, |
||||||
|
}, |
||||||
|
Signoff: apiOpts.Signoff, |
||||||
|
} |
||||||
|
if opts.Dates.Author.IsZero() { |
||||||
|
opts.Dates.Author = time.Now() |
||||||
|
} |
||||||
|
if opts.Dates.Committer.IsZero() { |
||||||
|
opts.Dates.Committer = time.Now() |
||||||
|
} |
||||||
|
|
||||||
|
if opts.Message == "" { |
||||||
|
opts.Message = "apply-patch" |
||||||
|
} |
||||||
|
|
||||||
|
if !canWriteFiles(ctx.Repo) { |
||||||
|
ctx.Error(http.StatusInternalServerError, "ApplyPatch", models.ErrUserDoesNotHaveAccessToRepo{ |
||||||
|
UserID: ctx.User.ID, |
||||||
|
RepoName: ctx.Repo.Repository.LowerName, |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.User, opts) |
||||||
|
if err != nil { |
||||||
|
if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) { |
||||||
|
ctx.Error(http.StatusForbidden, "Access", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if models.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || |
||||||
|
models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) { |
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if models.IsErrBranchDoesNotExist(err) || git.IsErrBranchNotExist(err) { |
||||||
|
ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.Error(http.StatusInternalServerError, "ApplyPatch", err) |
||||||
|
} else { |
||||||
|
ctx.JSON(http.StatusCreated, fileResponse) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,189 @@ |
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repo |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"errors" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/models/unit" |
||||||
|
"code.gitea.io/gitea/modules/base" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/util" |
||||||
|
"code.gitea.io/gitea/modules/web" |
||||||
|
"code.gitea.io/gitea/services/forms" |
||||||
|
"code.gitea.io/gitea/services/repository/files" |
||||||
|
) |
||||||
|
|
||||||
|
var tplCherryPick base.TplName = "repo/editor/cherry_pick" |
||||||
|
|
||||||
|
// CherryPick handles cherrypick GETs
|
||||||
|
func CherryPick(ctx *context.Context) { |
||||||
|
ctx.Data["SHA"] = ctx.Params(":sha") |
||||||
|
cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.Params(":sha")) |
||||||
|
if err != nil { |
||||||
|
if git.IsErrNotExist(err) { |
||||||
|
ctx.NotFound("Missing Commit", err) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.ServerError("GetCommit", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if ctx.FormString("cherry-pick-type") == "revert" { |
||||||
|
ctx.Data["CherryPickType"] = "revert" |
||||||
|
ctx.Data["commit_summary"] = "revert " + ctx.Params(":sha") |
||||||
|
ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() |
||||||
|
} else { |
||||||
|
ctx.Data["CherryPickType"] = "cherry-pick" |
||||||
|
splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) |
||||||
|
ctx.Data["commit_summary"] = splits[0] |
||||||
|
ctx.Data["commit_message"] = splits[1] |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Data["RequireHighlightJS"] = true |
||||||
|
|
||||||
|
canCommit := renderCommitRights(ctx) |
||||||
|
ctx.Data["TreePath"] = "patch" |
||||||
|
|
||||||
|
if canCommit { |
||||||
|
ctx.Data["commit_choice"] = frmCommitChoiceDirect |
||||||
|
} else { |
||||||
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch |
||||||
|
} |
||||||
|
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) |
||||||
|
ctx.Data["last_commit"] = ctx.Repo.CommitID |
||||||
|
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") |
||||||
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() |
||||||
|
|
||||||
|
ctx.HTML(200, tplCherryPick) |
||||||
|
} |
||||||
|
|
||||||
|
// CherryPickPost handles cherrypick POSTs
|
||||||
|
func CherryPickPost(ctx *context.Context) { |
||||||
|
form := web.GetForm(ctx).(*forms.CherryPickForm) |
||||||
|
|
||||||
|
sha := ctx.Params(":sha") |
||||||
|
ctx.Data["SHA"] = sha |
||||||
|
if form.Revert { |
||||||
|
ctx.Data["CherryPickType"] = "revert" |
||||||
|
} else { |
||||||
|
ctx.Data["CherryPickType"] = "cherry-pick" |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Data["RequireHighlightJS"] = true |
||||||
|
canCommit := renderCommitRights(ctx) |
||||||
|
branchName := ctx.Repo.BranchName |
||||||
|
if form.CommitChoice == frmCommitChoiceNewBranch { |
||||||
|
branchName = form.NewBranchName |
||||||
|
} |
||||||
|
ctx.Data["commit_summary"] = form.CommitSummary |
||||||
|
ctx.Data["commit_message"] = form.CommitMessage |
||||||
|
ctx.Data["commit_choice"] = form.CommitChoice |
||||||
|
ctx.Data["new_branch_name"] = form.NewBranchName |
||||||
|
ctx.Data["last_commit"] = ctx.Repo.CommitID |
||||||
|
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") |
||||||
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() |
||||||
|
|
||||||
|
if ctx.HasError() { |
||||||
|
ctx.HTML(200, tplCherryPick) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Cannot commit to a an existing branch if user doesn't have rights
|
||||||
|
if branchName == ctx.Repo.BranchName && !canCommit { |
||||||
|
ctx.Data["Err_NewBranchName"] = true |
||||||
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch |
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplCherryPick, &form) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
message := strings.TrimSpace(form.CommitSummary) |
||||||
|
if message == "" { |
||||||
|
if form.Revert { |
||||||
|
message = ctx.Tr("repo.commit.revert-header", sha) |
||||||
|
} else { |
||||||
|
message = ctx.Tr("repo.commit.cherry-pick-header", sha) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
form.CommitMessage = strings.TrimSpace(form.CommitMessage) |
||||||
|
if len(form.CommitMessage) > 0 { |
||||||
|
message += "\n\n" + form.CommitMessage |
||||||
|
} |
||||||
|
|
||||||
|
opts := &files.ApplyDiffPatchOptions{ |
||||||
|
LastCommitID: form.LastCommit, |
||||||
|
OldBranch: ctx.Repo.BranchName, |
||||||
|
NewBranch: branchName, |
||||||
|
Message: message, |
||||||
|
} |
||||||
|
|
||||||
|
// First lets try the simple plain read-tree -m approach
|
||||||
|
opts.Content = sha |
||||||
|
if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.User, form.Revert, opts); err != nil { |
||||||
|
if models.IsErrBranchAlreadyExists(err) { |
||||||
|
// User has specified a branch that already exists
|
||||||
|
branchErr := err.(models.ErrBranchAlreadyExists) |
||||||
|
ctx.Data["Err_NewBranchName"] = true |
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) |
||||||
|
return |
||||||
|
} else if models.IsErrCommitIDDoesNotMatch(err) { |
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) |
||||||
|
return |
||||||
|
} |
||||||
|
// Drop through to the apply technique
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{} |
||||||
|
if form.Revert { |
||||||
|
if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil { |
||||||
|
if git.IsErrNotExist(err) { |
||||||
|
ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.Params(":sha")+" does not exist.")) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.ServerError("GetRawDiff", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} else { |
||||||
|
if err := git.GetRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, git.RawDiffType("patch"), buf); err != nil { |
||||||
|
if git.IsErrNotExist(err) { |
||||||
|
ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.Params(":sha")+" does not exist.")) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.ServerError("GetRawDiff", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
opts.Content = buf.String() |
||||||
|
ctx.Data["FileContent"] = opts.Content |
||||||
|
|
||||||
|
if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.User, opts); err != nil { |
||||||
|
if models.IsErrBranchAlreadyExists(err) { |
||||||
|
// User has specified a branch that already exists
|
||||||
|
branchErr := err.(models.ErrBranchAlreadyExists) |
||||||
|
ctx.Data["Err_NewBranchName"] = true |
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) |
||||||
|
return |
||||||
|
} else if models.IsErrCommitIDDoesNotMatch(err) { |
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) |
||||||
|
return |
||||||
|
} else { |
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(unit.TypePullRequests) { |
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) |
||||||
|
} else { |
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,120 @@ |
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repo |
||||||
|
|
||||||
|
import ( |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/models/unit" |
||||||
|
"code.gitea.io/gitea/modules/base" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/util" |
||||||
|
"code.gitea.io/gitea/modules/web" |
||||||
|
"code.gitea.io/gitea/services/forms" |
||||||
|
"code.gitea.io/gitea/services/repository/files" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
tplPatchFile base.TplName = "repo/editor/patch" |
||||||
|
) |
||||||
|
|
||||||
|
// NewDiffPatch render create patch page
|
||||||
|
func NewDiffPatch(ctx *context.Context) { |
||||||
|
ctx.Data["RequireHighlightJS"] = true |
||||||
|
|
||||||
|
canCommit := renderCommitRights(ctx) |
||||||
|
|
||||||
|
ctx.Data["TreePath"] = "patch" |
||||||
|
|
||||||
|
ctx.Data["commit_summary"] = "" |
||||||
|
ctx.Data["commit_message"] = "" |
||||||
|
if canCommit { |
||||||
|
ctx.Data["commit_choice"] = frmCommitChoiceDirect |
||||||
|
} else { |
||||||
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch |
||||||
|
} |
||||||
|
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) |
||||||
|
ctx.Data["last_commit"] = ctx.Repo.CommitID |
||||||
|
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") |
||||||
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() |
||||||
|
|
||||||
|
ctx.HTML(200, tplPatchFile) |
||||||
|
} |
||||||
|
|
||||||
|
// NewDiffPatchPost response for sending patch page
|
||||||
|
func NewDiffPatchPost(ctx *context.Context) { |
||||||
|
form := web.GetForm(ctx).(*forms.EditRepoFileForm) |
||||||
|
|
||||||
|
canCommit := renderCommitRights(ctx) |
||||||
|
branchName := ctx.Repo.BranchName |
||||||
|
if form.CommitChoice == frmCommitChoiceNewBranch { |
||||||
|
branchName = form.NewBranchName |
||||||
|
} |
||||||
|
ctx.Data["RequireHighlightJS"] = true |
||||||
|
ctx.Data["TreePath"] = "patch" |
||||||
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() |
||||||
|
ctx.Data["FileContent"] = form.Content |
||||||
|
ctx.Data["commit_summary"] = form.CommitSummary |
||||||
|
ctx.Data["commit_message"] = form.CommitMessage |
||||||
|
ctx.Data["commit_choice"] = form.CommitChoice |
||||||
|
ctx.Data["new_branch_name"] = form.NewBranchName |
||||||
|
ctx.Data["last_commit"] = ctx.Repo.CommitID |
||||||
|
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") |
||||||
|
|
||||||
|
if ctx.HasError() { |
||||||
|
ctx.HTML(200, tplPatchFile) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Cannot commit to a an existing branch if user doesn't have rights
|
||||||
|
if branchName == ctx.Repo.BranchName && !canCommit { |
||||||
|
ctx.Data["Err_NewBranchName"] = true |
||||||
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch |
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// CommitSummary is optional in the web form, if empty, give it a default message based on add or update
|
||||||
|
// `message` will be both the summary and message combined
|
||||||
|
message := strings.TrimSpace(form.CommitSummary) |
||||||
|
if len(message) == 0 { |
||||||
|
message = ctx.Tr("repo.editor.patch") |
||||||
|
} |
||||||
|
|
||||||
|
form.CommitMessage = strings.TrimSpace(form.CommitMessage) |
||||||
|
if len(form.CommitMessage) > 0 { |
||||||
|
message += "\n\n" + form.CommitMessage |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.User, &files.ApplyDiffPatchOptions{ |
||||||
|
LastCommitID: form.LastCommit, |
||||||
|
OldBranch: ctx.Repo.BranchName, |
||||||
|
NewBranch: branchName, |
||||||
|
Message: message, |
||||||
|
Content: strings.ReplaceAll(form.Content, "\r", ""), |
||||||
|
}); err != nil { |
||||||
|
if models.IsErrBranchAlreadyExists(err) { |
||||||
|
// User has specified a branch that already exists
|
||||||
|
branchErr := err.(models.ErrBranchAlreadyExists) |
||||||
|
ctx.Data["Err_NewBranchName"] = true |
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) |
||||||
|
return |
||||||
|
} else if models.IsErrCommitIDDoesNotMatch(err) { |
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) |
||||||
|
return |
||||||
|
} else { |
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(unit.TypePullRequests) { |
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) |
||||||
|
} else { |
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath)) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,126 @@ |
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package files |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
repo_model "code.gitea.io/gitea/models/repo" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"code.gitea.io/gitea/modules/structs" |
||||||
|
"code.gitea.io/gitea/services/pull" |
||||||
|
) |
||||||
|
|
||||||
|
// CherryPick cherrypicks or reverts a commit to the given repository
|
||||||
|
func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, revert bool, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) { |
||||||
|
if err := opts.Validate(ctx, repo, doer); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
message := strings.TrimSpace(opts.Message) |
||||||
|
|
||||||
|
author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) |
||||||
|
|
||||||
|
t, err := NewTemporaryUploadRepository(ctx, 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 // Couldn't get a commit for the branch
|
||||||
|
} |
||||||
|
|
||||||
|
// Assigned LastCommitID in opts if it hasn't been set
|
||||||
|
if opts.LastCommitID == "" { |
||||||
|
opts.LastCommitID = commit.ID.String() |
||||||
|
} else { |
||||||
|
lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("CherryPick: Invalid last commit ID: %v", err) |
||||||
|
} |
||||||
|
opts.LastCommitID = lastCommitID.String() |
||||||
|
if commit.ID.String() != opts.LastCommitID { |
||||||
|
return nil, models.ErrCommitIDDoesNotMatch{ |
||||||
|
GivenCommitID: opts.LastCommitID, |
||||||
|
CurrentCommitID: opts.LastCommitID, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
commit, err = t.GetCommit(strings.TrimSpace(opts.Content)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
parent, err := commit.ParentID(0) |
||||||
|
if err != nil { |
||||||
|
parent = git.MustIDFromString(git.EmptyTreeSHA) |
||||||
|
} |
||||||
|
|
||||||
|
base, right := parent.String(), commit.ID.String() |
||||||
|
|
||||||
|
if revert { |
||||||
|
right, base = base, right |
||||||
|
} |
||||||
|
|
||||||
|
description := fmt.Sprintf("CherryPick %s onto %s", right, opts.OldBranch) |
||||||
|
conflict, _, err := pull.AttemptThreeWayMerge(ctx, |
||||||
|
t.basePath, t.gitRepo, base, opts.LastCommitID, right, description) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("failed to three-way merge %s onto %s: %v", right, opts.OldBranch, err) |
||||||
|
} |
||||||
|
|
||||||
|
if conflict { |
||||||
|
return nil, fmt.Errorf("failed to merge due to conflicts") |
||||||
|
} |
||||||
|
|
||||||
|
treeHash, err := t.WriteTree() |
||||||
|
if err != nil { |
||||||
|
// likely non-sensical tree due to merge conflicts...
|
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Now commit the tree
|
||||||
|
var commitHash string |
||||||
|
if opts.Dates != nil { |
||||||
|
commitHash, err = t.CommitTreeWithDate(author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) |
||||||
|
} else { |
||||||
|
commitHash, err = t.CommitTree(author, committer, treeHash, message, opts.Signoff) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Then push this tree to NewBranch
|
||||||
|
if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
commit, err = t.GetCommit(commitHash) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
|
||||||
|
verification := GetPayloadCommitVerification(commit) |
||||||
|
fileResponse := &structs.FileResponse{ |
||||||
|
Commit: fileCommitResponse, |
||||||
|
Verification: verification, |
||||||
|
} |
||||||
|
|
||||||
|
return fileResponse, nil |
||||||
|
} |
@ -0,0 +1,193 @@ |
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package files |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
repo_model "code.gitea.io/gitea/models/repo" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"code.gitea.io/gitea/modules/structs" |
||||||
|
asymkey_service "code.gitea.io/gitea/services/asymkey" |
||||||
|
) |
||||||
|
|
||||||
|
// ApplyDiffPatchOptions holds the repository diff patch update options
|
||||||
|
type ApplyDiffPatchOptions struct { |
||||||
|
LastCommitID string |
||||||
|
OldBranch string |
||||||
|
NewBranch string |
||||||
|
Message string |
||||||
|
Content string |
||||||
|
SHA string |
||||||
|
Author *IdentityOptions |
||||||
|
Committer *IdentityOptions |
||||||
|
Dates *CommitDateOptions |
||||||
|
Signoff bool |
||||||
|
} |
||||||
|
|
||||||
|
// Validate validates the provided options
|
||||||
|
func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error { |
||||||
|
// If no branch name is set, assume master
|
||||||
|
if opts.OldBranch == "" { |
||||||
|
opts.OldBranch = repo.DefaultBranch |
||||||
|
} |
||||||
|
if opts.NewBranch == "" { |
||||||
|
opts.NewBranch = opts.OldBranch |
||||||
|
} |
||||||
|
|
||||||
|
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath()) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer closer.Close() |
||||||
|
|
||||||
|
// oldBranch must exist for this operation
|
||||||
|
if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
// A NewBranch can be specified for the patch to be applied to.
|
||||||
|
// Check to make sure the branch does not already exist, otherwise we can't proceed.
|
||||||
|
// If we aren't branching to a new branch, make sure user can commit to the given branch
|
||||||
|
if opts.NewBranch != opts.OldBranch { |
||||||
|
existingBranch, err := gitRepo.GetBranch(opts.NewBranch) |
||||||
|
if existingBranch != nil { |
||||||
|
return models.ErrBranchAlreadyExists{ |
||||||
|
BranchName: opts.NewBranch, |
||||||
|
} |
||||||
|
} |
||||||
|
if err != nil && !git.IsErrBranchNotExist(err) { |
||||||
|
return err |
||||||
|
} |
||||||
|
} else { |
||||||
|
protectedBranch, err := models.GetProtectedBranchBy(repo.ID, opts.OldBranch) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if protectedBranch != nil && !protectedBranch.CanUserPush(doer.ID) { |
||||||
|
return models.ErrUserCannotCommit{ |
||||||
|
UserName: doer.LowerName, |
||||||
|
} |
||||||
|
} |
||||||
|
if protectedBranch != nil && protectedBranch.RequireSignedCommits { |
||||||
|
_, _, _, err := asymkey_service.SignCRUDAction(ctx, repo.RepoPath(), doer, repo.RepoPath(), opts.OldBranch) |
||||||
|
if err != nil { |
||||||
|
if !asymkey_service.IsErrWontSign(err) { |
||||||
|
return err |
||||||
|
} |
||||||
|
return models.ErrUserCannotCommit{ |
||||||
|
UserName: doer.LowerName, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ApplyDiffPatch applies a patch to the given repository
|
||||||
|
func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) { |
||||||
|
if err := opts.Validate(ctx, repo, doer); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
message := strings.TrimSpace(opts.Message) |
||||||
|
|
||||||
|
author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) |
||||||
|
|
||||||
|
t, err := NewTemporaryUploadRepository(ctx, 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 // Couldn't get a commit for the branch
|
||||||
|
} |
||||||
|
|
||||||
|
// Assigned LastCommitID in opts if it hasn't been set
|
||||||
|
if opts.LastCommitID == "" { |
||||||
|
opts.LastCommitID = commit.ID.String() |
||||||
|
} else { |
||||||
|
lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("ApplyPatch: Invalid last commit ID: %v", err) |
||||||
|
} |
||||||
|
opts.LastCommitID = lastCommitID.String() |
||||||
|
if commit.ID.String() != opts.LastCommitID { |
||||||
|
return nil, models.ErrCommitIDDoesNotMatch{ |
||||||
|
GivenCommitID: opts.LastCommitID, |
||||||
|
CurrentCommitID: opts.LastCommitID, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
stdout := &strings.Builder{} |
||||||
|
stderr := &strings.Builder{} |
||||||
|
|
||||||
|
args := []string{"apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary"} |
||||||
|
|
||||||
|
if git.CheckGitVersionAtLeast("2.32") == nil { |
||||||
|
args = append(args, "-3") |
||||||
|
} |
||||||
|
|
||||||
|
cmd := git.NewCommand(ctx, args...) |
||||||
|
if err := cmd.RunWithContext(&git.RunContext{ |
||||||
|
Timeout: -1, |
||||||
|
Dir: t.basePath, |
||||||
|
Stdout: stdout, |
||||||
|
Stderr: stderr, |
||||||
|
Stdin: strings.NewReader(opts.Content), |
||||||
|
}); err != nil { |
||||||
|
return nil, fmt.Errorf("Error: Stdout: %s\nStderr: %s\nErr: %v", stdout.String(), stderr.String(), err) |
||||||
|
} |
||||||
|
|
||||||
|
// Now write the tree
|
||||||
|
treeHash, err := t.WriteTree() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Now commit the tree
|
||||||
|
var commitHash string |
||||||
|
if opts.Dates != nil { |
||||||
|
commitHash, err = t.CommitTreeWithDate(author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) |
||||||
|
} else { |
||||||
|
commitHash, err = t.CommitTree(author, committer, treeHash, message, opts.Signoff) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Then push this tree to NewBranch
|
||||||
|
if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
commit, err = t.GetCommit(commitHash) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
|
||||||
|
verification := GetPayloadCommitVerification(commit) |
||||||
|
fileResponse := &structs.FileResponse{ |
||||||
|
Commit: fileCommitResponse, |
||||||
|
Verification: verification, |
||||||
|
} |
||||||
|
|
||||||
|
return fileResponse, nil |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
<div class="page-content repository file editor edit"> |
||||||
|
{{template "repo/header" .}} |
||||||
|
<div class="ui container"> |
||||||
|
{{template "base/alert" .}} |
||||||
|
<form class="ui edit form" method="post" action="{{.RepoLink}}/_cherrypick/{{.SHA}}/{{.BranchName | PathEscapeSegments}}"> |
||||||
|
{{.CsrfTokenHtml}} |
||||||
|
<input type="hidden" name="last_commit" value="{{.last_commit}}"> |
||||||
|
<input type="hidden" name="page_has_posted" value="true"> |
||||||
|
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}"> |
||||||
|
<div class="ui secondary menu"> |
||||||
|
<div class="fitted item treepath"> |
||||||
|
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> |
||||||
|
{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}} |
||||||
|
{{$shalink := printf "<a class=\"ui blue sha label\" href=\"%s\">%s</a>" (Escape $shaurl) (ShortSha .SHA)}} |
||||||
|
{{if eq .CherryPickType "revert"}} |
||||||
|
{{.i18n.Tr "repo.editor.revert" $shalink | Str2html}} |
||||||
|
{{else}} |
||||||
|
{{.i18n.Tr "repo.editor.cherry_pick" $shalink | Str2html}} |
||||||
|
{{end}} |
||||||
|
<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a> |
||||||
|
<div class="divider">:</div> |
||||||
|
<a class="section" href="{{$.BranchLink}}">{{.BranchName}}</a> |
||||||
|
<span>{{.i18n.Tr "repo.editor.or"}} <a href="{{$shaurl}}">{{.i18n.Tr "repo.editor.cancel_lower"}}</a></span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "repo/editor/commit_form" .}} |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,59 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
<div class="page-content repository file editor edit"> |
||||||
|
{{template "repo/header" .}} |
||||||
|
<div class="ui container"> |
||||||
|
{{template "base/alert" .}} |
||||||
|
<form class="ui edit form" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"> |
||||||
|
{{.CsrfTokenHtml}} |
||||||
|
<input type="hidden" name="last_commit" value="{{.last_commit}}"> |
||||||
|
<input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> |
||||||
|
<div class="ui secondary menu"> |
||||||
|
<div class="fitted item treepath"> |
||||||
|
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> |
||||||
|
{{.i18n.Tr "repo.editor.patching"}} |
||||||
|
<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a> |
||||||
|
<div class="divider">:</div> |
||||||
|
<a class="section" href="{{$.BranchLink}}">{{.BranchName}}</a> |
||||||
|
<span>{{.i18n.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}">{{.i18n.Tr "repo.editor.cancel_lower"}}</a></span> |
||||||
|
<input type="hidden" id="tree_path" name="tree_path" value="patch" required> |
||||||
|
<input id="file-name" type="hidden" value="diff.patch"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="field"> |
||||||
|
<div class="ui top attached tabular menu" data-write="write"> |
||||||
|
<a class="active item" data-tab="write">{{svg "octicon-code" 16 "mr-2"}}{{.i18n.Tr "repo.editor.new_patch"}}</a> |
||||||
|
</div> |
||||||
|
<div class="ui bottom attached active tab segment" data-tab="write"> |
||||||
|
<textarea id="edit_area" name="content" class="hide" data-id="repo-{{.Repository.Name}}-patch" |
||||||
|
data-context="{{.RepoLink}}" |
||||||
|
data-line-wrap-extensions="{{.LineWrapExtensions}}"> |
||||||
|
{{.FileContent}}</textarea> |
||||||
|
<div class="editor-loading is-loading"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "repo/editor/commit_form" .}} |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="ui small basic modal" id="edit-empty-content-modal"> |
||||||
|
<div class="ui icon header"> |
||||||
|
<i class="file icon"></i> |
||||||
|
{{.i18n.Tr "repo.editor.commit_empty_file_header"}} |
||||||
|
</div> |
||||||
|
<div class="center content"> |
||||||
|
<p>{{.i18n.Tr "repo.editor.commit_empty_file_text"}}</p> |
||||||
|
</div> |
||||||
|
<div class="actions"> |
||||||
|
<div class="ui red basic cancel inverted button"> |
||||||
|
<i class="remove icon"></i> |
||||||
|
{{.i18n.Tr "repo.editor.cancel"}} |
||||||
|
</div> |
||||||
|
<div class="ui green basic ok inverted button"> |
||||||
|
<i class="save icon"></i> |
||||||
|
{{.i18n.Tr "repo.editor.commit_changes"}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -1,9 +1,18 @@ |
|||||||
import $ from 'jquery'; |
import $ from 'jquery'; |
||||||
|
|
||||||
export function initRepoBranchButton() { |
export function initRepoBranchButton() { |
||||||
$('.show-create-branch-modal.button').on('click', function () { |
$('.show-create-branch-modal').on('click', function () { |
||||||
$('#create-branch-form')[0].action = $('#create-branch-form').data('base-action') + $(this).data('branch-from-urlcomponent'); |
let modalFormName = $(this).attr('data-modal-form'); |
||||||
$('#modal-create-branch-from-span').text($(this).data('branch-from')); |
if (!modalFormName) { |
||||||
$($(this).data('modal')).modal('show'); |
modalFormName = '#create-branch-form'; |
||||||
|
} |
||||||
|
$(modalFormName)[0].action = $(modalFormName).attr('data-base-action') + $(this).attr('data-branch-from-urlcomponent'); |
||||||
|
let fromSpanName = $(this).attr('data-modal-from-span'); |
||||||
|
if (!fromSpanName) { |
||||||
|
fromSpanName = '#modal-create-branch-from-span'; |
||||||
|
} |
||||||
|
|
||||||
|
$(fromSpanName).text($(this).attr('data-branch-from')); |
||||||
|
$($(this).attr('data-modal')).modal('show'); |
||||||
}); |
}); |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue