Skip to content

Commit eb748f5

Browse files
authored
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>
1 parent 439ad34 commit eb748f5

File tree

23 files changed

+1211
-57
lines changed

23 files changed

+1211
-57
lines changed

modules/git/diff.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ func GetRawDiff(ctx context.Context, repoPath, commitID string, diffType RawDiff
3232
return GetRawDiffForFile(ctx, repoPath, "", commitID, diffType, "", writer)
3333
}
3434

35+
// GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer.
36+
func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error {
37+
stderr := new(bytes.Buffer)
38+
cmd := NewCommand(ctx, "show", "--pretty=format:revert %H%n", "-R", commitID)
39+
if err := cmd.RunWithContext(&RunContext{
40+
Timeout: -1,
41+
Dir: repoPath,
42+
Stdout: writer,
43+
Stderr: stderr,
44+
}); err != nil {
45+
return fmt.Errorf("Run: %v - %s", err, stderr)
46+
}
47+
return nil
48+
}
49+
3550
// GetRawDiffForFile dumps diff results of file in given commit ID to io.Writer.
3651
func GetRawDiffForFile(ctx context.Context, repoPath, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
3752
repo, closer, err := RepositoryFromContextOrOpen(ctx, repoPath)
@@ -221,8 +236,7 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
221236
}
222237
}
223238
}
224-
err := scanner.Err()
225-
if err != nil {
239+
if err := scanner.Err(); err != nil {
226240
return "", err
227241
}
228242

modules/structs/repo_file.go

+8
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ type UpdateFileOptions struct {
5050
FromPath string `json:"from_path" binding:"MaxSize(500)"`
5151
}
5252

53+
// ApplyDiffPatchFileOptions options for applying a diff patch
54+
// 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)
55+
type ApplyDiffPatchFileOptions struct {
56+
DeleteFileOptions
57+
// required: true
58+
Content string `json:"content"`
59+
}
60+
5361
// FileLinksResponse contains the links for a repo's file
5462
type FileLinksResponse struct {
5563
Self *string `json:"self"`

options/locale/locale_en-US.ini

+19
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,10 @@ editor.add_tmpl = Add '<filename>'
10751075
editor.add = Add '%s'
10761076
editor.update = Update '%s'
10771077
editor.delete = Delete '%s'
1078+
editor.patch = Apply Patch
1079+
editor.patching = Patching:
1080+
editor.fail_to_apply_patch = Unable to apply patch '%s'
1081+
editor.new_patch = New Patch
10781082
editor.commit_message_desc = Add an optional extended description…
10791083
editor.signoff_desc = Add a Signed-off-by trailer by the committer at the end of the commit log message.
10801084
editor.commit_directly_to_this_branch = Commit directly to the <strong class="branch-name">%s</strong> branch.
@@ -1110,6 +1114,8 @@ editor.cannot_commit_to_protected_branch = Cannot commit to protected branch '%s
11101114
editor.no_commit_to_branch = Unable to commit directly to branch because:
11111115
editor.user_no_push_to_branch = User cannot push to branch
11121116
editor.require_signed_commit = Branch requires a signed commit
1117+
editor.cherry_pick = Cherry-pick %s onto:
1118+
editor.revert = Revert %s onto:
11131119
11141120
commits.desc = Browse source code change history.
11151121
commits.commits = Commits
@@ -1130,6 +1136,14 @@ commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does n
11301136
commits.gpg_key_id = GPG Key ID
11311137
commits.ssh_key_fingerprint = SSH Key Fingerprint
11321138
1139+
commit.actions = Actions
1140+
commit.revert = Revert
1141+
commit.revert-header = Revert: %s
1142+
commit.revert-content = Select branch to revert onto:
1143+
commit.cherry-pick = Cherry-pick
1144+
commit.cherry-pick-header = Cherry-pick: %s
1145+
commit.cherry-pick-content = Select branch to cherry-pick onto:
1146+
11331147
ext_issues = Access to External Issues
11341148
ext_issues.desc = Link to an external issue tracker.
11351149
@@ -2215,11 +2229,16 @@ branch.included_desc = This branch is part of the default branch
22152229
branch.included = Included
22162230
branch.create_new_branch = Create branch from branch:
22172231
branch.confirm_create_branch = Create branch
2232+
branch.create_branch_operation = Create branch
22182233
branch.new_branch = Create new branch
22192234
branch.new_branch_from = Create new branch from '%s'
22202235
branch.renamed = Branch %s was renamed to %s.
22212236

22222237
tag.create_tag = Create tag <strong>%s</strong>
2238+
tag.create_tag_operation = Create tag
2239+
tag.confirm_create_tag = Create tag
2240+
tag.create_tag_from = Create new tag from '%s'
2241+
22232242
tag.create_success = Tag '%s' has been created.
22242243

22252244
topic.manage_topics = Manage Topics

routers/api/v1/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
975975
m.Get("/tags/{sha}", context.RepoRefForAPI, repo.GetAnnotatedTag)
976976
m.Get("/notes/{sha}", repo.GetNote)
977977
}, reqRepoReader(unit.TypeCode))
978+
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
978979
m.Group("/contents", func() {
979980
m.Get("", repo.GetContentsList)
980981
m.Get("/*", repo.GetContents)

routers/api/v1/repo/patch.go

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package repo
6+
7+
import (
8+
"net/http"
9+
"time"
10+
11+
"code.gitea.io/gitea/models"
12+
"code.gitea.io/gitea/modules/context"
13+
"code.gitea.io/gitea/modules/git"
14+
api "code.gitea.io/gitea/modules/structs"
15+
"code.gitea.io/gitea/modules/web"
16+
"code.gitea.io/gitea/services/repository/files"
17+
)
18+
19+
// ApplyDiffPatch handles API call for applying a patch
20+
func ApplyDiffPatch(ctx *context.APIContext) {
21+
// swagger:operation POST /repos/{owner}/{repo}/diffpatch repository repoApplyDiffPatch
22+
// ---
23+
// summary: Apply diff patch to repository
24+
// consumes:
25+
// - application/json
26+
// produces:
27+
// - application/json
28+
// parameters:
29+
// - name: owner
30+
// in: path
31+
// description: owner of the repo
32+
// type: string
33+
// required: true
34+
// - name: repo
35+
// in: path
36+
// description: name of the repo
37+
// type: string
38+
// required: true
39+
// - name: body
40+
// in: body
41+
// required: true
42+
// schema:
43+
// "$ref": "#/definitions/UpdateFileOptions"
44+
// responses:
45+
// "200":
46+
// "$ref": "#/responses/FileResponse"
47+
apiOpts := web.GetForm(ctx).(*api.ApplyDiffPatchFileOptions)
48+
49+
opts := &files.ApplyDiffPatchOptions{
50+
Content: apiOpts.Content,
51+
SHA: apiOpts.SHA,
52+
Message: apiOpts.Message,
53+
OldBranch: apiOpts.BranchName,
54+
NewBranch: apiOpts.NewBranchName,
55+
Committer: &files.IdentityOptions{
56+
Name: apiOpts.Committer.Name,
57+
Email: apiOpts.Committer.Email,
58+
},
59+
Author: &files.IdentityOptions{
60+
Name: apiOpts.Author.Name,
61+
Email: apiOpts.Author.Email,
62+
},
63+
Dates: &files.CommitDateOptions{
64+
Author: apiOpts.Dates.Author,
65+
Committer: apiOpts.Dates.Committer,
66+
},
67+
Signoff: apiOpts.Signoff,
68+
}
69+
if opts.Dates.Author.IsZero() {
70+
opts.Dates.Author = time.Now()
71+
}
72+
if opts.Dates.Committer.IsZero() {
73+
opts.Dates.Committer = time.Now()
74+
}
75+
76+
if opts.Message == "" {
77+
opts.Message = "apply-patch"
78+
}
79+
80+
if !canWriteFiles(ctx.Repo) {
81+
ctx.Error(http.StatusInternalServerError, "ApplyPatch", models.ErrUserDoesNotHaveAccessToRepo{
82+
UserID: ctx.User.ID,
83+
RepoName: ctx.Repo.Repository.LowerName,
84+
})
85+
return
86+
}
87+
88+
fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.User, opts)
89+
if err != nil {
90+
if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) {
91+
ctx.Error(http.StatusForbidden, "Access", err)
92+
return
93+
}
94+
if models.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) ||
95+
models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) {
96+
ctx.Error(http.StatusUnprocessableEntity, "Invalid", err)
97+
return
98+
}
99+
if models.IsErrBranchDoesNotExist(err) || git.IsErrBranchNotExist(err) {
100+
ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err)
101+
return
102+
}
103+
ctx.Error(http.StatusInternalServerError, "ApplyPatch", err)
104+
} else {
105+
ctx.JSON(http.StatusCreated, fileResponse)
106+
}
107+
}

0 commit comments

Comments
 (0)