Skip to content

Commit

Permalink
Add apply-patch, basic revert and cherry-pick functionality (go-gitea…
Browse files Browse the repository at this point in the history
…#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 go-gitea#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 go-gitea#3880 
Fix go-gitea#17986 

Signed-off-by: Andrew Thornton <art27@cantab.net>
  • Loading branch information
zeripath authored and Stelios Malathouras committed Mar 28, 2022
1 parent cdf0e4a commit f1f5e25
Show file tree
Hide file tree
Showing 23 changed files with 1,211 additions and 57 deletions.
18 changes: 16 additions & 2 deletions modules/git/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ func GetRawDiff(ctx context.Context, repoPath, commitID string, diffType RawDiff
return GetRawDiffForFile(ctx, repoPath, "", commitID, diffType, "", writer)
}

// GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer.
func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error {
stderr := new(bytes.Buffer)
cmd := NewCommand(ctx, "show", "--pretty=format:revert %H%n", "-R", commitID)
if err := cmd.RunWithContext(&RunContext{
Timeout: -1,
Dir: repoPath,
Stdout: writer,
Stderr: stderr,
}); err != nil {
return fmt.Errorf("Run: %v - %s", err, stderr)
}
return nil
}

// GetRawDiffForFile dumps diff results of file in given commit ID to io.Writer.
func GetRawDiffForFile(ctx context.Context, repoPath, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
repo, closer, err := RepositoryFromContextOrOpen(ctx, repoPath)
Expand Down Expand Up @@ -221,8 +236,7 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
}
}
}
err := scanner.Err()
if err != nil {
if err := scanner.Err(); err != nil {
return "", err
}

Expand Down
8 changes: 8 additions & 0 deletions modules/structs/repo_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ type UpdateFileOptions struct {
FromPath string `json:"from_path" binding:"MaxSize(500)"`
}

// ApplyDiffPatchFileOptions options for applying a diff patch
// 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 ApplyDiffPatchFileOptions struct {
DeleteFileOptions
// required: true
Content string `json:"content"`
}

// FileLinksResponse contains the links for a repo's file
type FileLinksResponse struct {
Self *string `json:"self"`
Expand Down
19 changes: 19 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,10 @@ editor.add_tmpl = Add '<filename>'
editor.add = Add '%s'
editor.update = Update '%s'
editor.delete = Delete '%s'
editor.patch = Apply Patch
editor.patching = Patching:
editor.fail_to_apply_patch = Unable to apply patch '%s'
editor.new_patch = New Patch
editor.commit_message_desc = Add an optional extended description…
editor.signoff_desc = Add a Signed-off-by trailer by the committer at the end of the commit log message.
editor.commit_directly_to_this_branch = Commit directly to the <strong class="branch-name">%s</strong> branch.
Expand Down Expand Up @@ -1110,6 +1114,8 @@ editor.cannot_commit_to_protected_branch = Cannot commit to protected branch '%s
editor.no_commit_to_branch = Unable to commit directly to branch because:
editor.user_no_push_to_branch = User cannot push to branch
editor.require_signed_commit = Branch requires a signed commit
editor.cherry_pick = Cherry-pick %s onto:
editor.revert = Revert %s onto:
commits.desc = Browse source code change history.
commits.commits = Commits
Expand All @@ -1130,6 +1136,14 @@ commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does n
commits.gpg_key_id = GPG Key ID
commits.ssh_key_fingerprint = SSH Key Fingerprint
commit.actions = Actions
commit.revert = Revert
commit.revert-header = Revert: %s
commit.revert-content = Select branch to revert onto:
commit.cherry-pick = Cherry-pick
commit.cherry-pick-header = Cherry-pick: %s
commit.cherry-pick-content = Select branch to cherry-pick onto:
ext_issues = Access to External Issues
ext_issues.desc = Link to an external issue tracker.
Expand Down Expand Up @@ -2215,11 +2229,16 @@ branch.included_desc = This branch is part of the default branch
branch.included = Included
branch.create_new_branch = Create branch from branch:
branch.confirm_create_branch = Create branch
branch.create_branch_operation = Create branch
branch.new_branch = Create new branch
branch.new_branch_from = Create new branch from '%s'
branch.renamed = Branch %s was renamed to %s.

tag.create_tag = Create tag <strong>%s</strong>
tag.create_tag_operation = Create tag
tag.confirm_create_tag = Create tag
tag.create_tag_from = Create new tag from '%s'

tag.create_success = Tag '%s' has been created.

topic.manage_topics = Manage Topics
Expand Down
1 change: 1 addition & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
107 changes: 107 additions & 0 deletions routers/api/v1/repo/patch.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit f1f5e25

Please sign in to comment.