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>
This commit is contained in:
zeripath 2022-02-09 20:28:55 +00:00 committed by GitHub
parent 439ad34c71
commit eb748f5f3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1211 additions and 57 deletions

View file

@ -975,6 +975,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
m.Get("/tags/{sha}", context.RepoRefForAPI, repo.GetAnnotatedTag)
m.Get("/notes/{sha}", repo.GetNote)
}, reqRepoReader(unit.TypeCode))
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
m.Group("/contents", func() {
m.Get("", repo.GetContentsList)
m.Get("/*", repo.GetContents)

View file

@ -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)
}
}

View file

@ -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))
}
}

120
routers/web/repo/patch.go Normal file
View file

@ -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))
}
}

View file

@ -808,6 +808,10 @@ func RegisterRoutes(m *web.Route) {
m.Combo("/_upload/*", repo.MustBeAbleToUpload).
Get(repo.UploadFile).
Post(bindIgnErr(forms.UploadRepoFileForm{}), repo.UploadFilePost)
m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch).
Post(bindIgnErr(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
m.Combo("/_cherrypick/{sha:([a-f0-9]{7,40})}/*").Get(repo.CherryPick).
Post(bindIgnErr(forms.CherryPickForm{}), repo.CherryPickPost)
}, context.RepoRefByType(context.RepoRefBranch), repo.MustBeEditable)
m.Group("", func() {
m.Post("/upload-file", repo.UploadFileToServer)
@ -1029,6 +1033,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("", func() {
m.Get("/graph", repo.Graph)
m.Get("/commit/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
m.Get("/cherry-pick/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
m.Group("/src", func() {