Skip to content

Commit 023c465

Browse files
committed
Initial implementation of ApplyDiffPatch
This code adds a simple endpoint to add diff patch application to Gitea
1 parent 2447ffc commit 023c465

File tree

9 files changed

+474
-0
lines changed

9 files changed

+474
-0
lines changed

modules/repofiles/patch.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2020 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 repofiles
6+
7+
import (
8+
"fmt"
9+
"strings"
10+
11+
"code.gitea.io/gitea/models"
12+
"code.gitea.io/gitea/modules/git"
13+
"code.gitea.io/gitea/modules/log"
14+
repo_module "code.gitea.io/gitea/modules/repository"
15+
"code.gitea.io/gitea/modules/structs"
16+
api "code.gitea.io/gitea/modules/structs"
17+
)
18+
19+
// ApplyDiffPatchOptions holds the repository diff patch update options
20+
type ApplyDiffPatchOptions struct {
21+
LastCommitID string
22+
OldBranch string
23+
NewBranch string
24+
Message string
25+
Content string
26+
SHA string
27+
Author *IdentityOptions
28+
Committer *IdentityOptions
29+
Dates *CommitDateOptions
30+
}
31+
32+
// ApplyDiffPatch applies a patch to the given repository
33+
func ApplyDiffPatch(repo *models.Repository, doer *models.User, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
34+
// If no branch name is set, assume master
35+
if opts.OldBranch == "" {
36+
opts.OldBranch = repo.DefaultBranch
37+
}
38+
if opts.NewBranch == "" {
39+
opts.NewBranch = opts.OldBranch
40+
}
41+
42+
// oldBranch must exist for this operation
43+
if _, err := repo_module.GetBranch(repo, opts.OldBranch); err != nil {
44+
return nil, err
45+
}
46+
47+
// A NewBranch can be specified for the patch to be applied to.
48+
// Check to make sure the branch does not already exist, otherwise we can't proceed.
49+
// If we aren't branching to a new branch, make sure user can commit to the given branch
50+
if opts.NewBranch != opts.OldBranch {
51+
existingBranch, err := repo_module.GetBranch(repo, opts.NewBranch)
52+
if existingBranch != nil {
53+
return nil, models.ErrBranchAlreadyExists{
54+
BranchName: opts.NewBranch,
55+
}
56+
}
57+
if err != nil && !git.IsErrBranchNotExist(err) {
58+
return nil, err
59+
}
60+
} else {
61+
protectedBranch, err := repo.GetBranchProtection(opts.OldBranch)
62+
if err != nil {
63+
return nil, err
64+
}
65+
if protectedBranch != nil && !protectedBranch.CanUserPush(doer.ID) {
66+
return nil, models.ErrUserCannotCommit{
67+
UserName: doer.LowerName,
68+
}
69+
}
70+
if protectedBranch != nil && protectedBranch.RequireSignedCommits {
71+
_, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch)
72+
if err != nil {
73+
if !models.IsErrWontSign(err) {
74+
return nil, err
75+
}
76+
return nil, models.ErrUserCannotCommit{
77+
UserName: doer.LowerName,
78+
}
79+
}
80+
}
81+
}
82+
83+
message := strings.TrimSpace(opts.Message)
84+
85+
author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
86+
87+
t, err := NewTemporaryUploadRepository(repo)
88+
if err != nil {
89+
log.Error("%v", err)
90+
}
91+
defer t.Close()
92+
if err := t.Clone(opts.OldBranch); err != nil {
93+
return nil, err
94+
}
95+
if err := t.SetDefaultIndex(); err != nil {
96+
return nil, err
97+
}
98+
99+
// Get the commit of the original branch
100+
commit, err := t.GetBranchCommit(opts.OldBranch)
101+
if err != nil {
102+
return nil, err // Couldn't get a commit for the branch
103+
}
104+
105+
// Assigned LastCommitID in opts if it hasn't been set
106+
if opts.LastCommitID == "" {
107+
opts.LastCommitID = commit.ID.String()
108+
} else {
109+
lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID)
110+
if err != nil {
111+
return nil, fmt.Errorf("ApplyPatch: Invalid last commit ID: %v", err)
112+
}
113+
opts.LastCommitID = lastCommitID.String()
114+
}
115+
116+
stdout := &strings.Builder{}
117+
stderr := &strings.Builder{}
118+
119+
err = git.NewCommand("apply", "--index", "--cached", "--ignore-whitespace", "--whitespace=fix").RunInDirFullPipeline(t.basePath, stdout, stderr, strings.NewReader(opts.Content))
120+
if err != nil {
121+
return nil, fmt.Errorf("Error: Stdout: %s\nStderr: %s\nErr: %v", stdout.String(), stderr.String(), err)
122+
}
123+
124+
// Now write the tree
125+
treeHash, err := t.WriteTree()
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
// Now commit the tree
131+
var commitHash string
132+
if opts.Dates != nil {
133+
commitHash, err = t.CommitTreeWithDate(author, committer, treeHash, message, opts.Dates.Author, opts.Dates.Committer)
134+
} else {
135+
commitHash, err = t.CommitTree(author, committer, treeHash, message)
136+
}
137+
if err != nil {
138+
return nil, err
139+
}
140+
141+
// Then push this tree to NewBranch
142+
if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
143+
return nil, err
144+
}
145+
146+
commit, err = t.GetCommit(commitHash)
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
152+
verification := GetPayloadCommitVerification(commit)
153+
fileResponse := &api.FileResponse{
154+
Commit: fileCommitResponse,
155+
Verification: verification,
156+
}
157+
158+
return fileResponse, nil
159+
}

modules/structs/repo_file.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ type UpdateFileOptions struct {
4848
FromPath string `json:"from_path" binding:"MaxSize(500)"`
4949
}
5050

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

options/locale/locale_en-US.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,9 @@ editor.add_tmpl = Add '<filename>'
795795
editor.add = Add '%s'
796796
editor.update = Update '%s'
797797
editor.delete = Delete '%s'
798+
editor.patch = Apply Patch
799+
editor.fail_to_apply_patch = Unable to apply patch '%s'
800+
editor.new_patch = New Patch
798801
editor.commit_message_desc = Add an optional extended description…
799802
editor.commit_directly_to_this_branch = Commit directly to the <strong class="branch-name">%s</strong> branch.
800803
editor.create_new_branch = Create a <strong>new branch</strong> for this commit and start a pull request.

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,7 @@ func RegisterRoutes(m *macaron.Macaron) {
838838
m.Get("/blobs/:sha", context.RepoRef(), repo.GetBlob)
839839
m.Get("/tags/:sha", context.RepoRef(), repo.GetTag)
840840
}, reqRepoReader(models.UnitTypeCode))
841+
m.Post("/diffpatch", reqRepoWriter(models.UnitTypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
841842
m.Group("/contents", func() {
842843
m.Get("", repo.GetContentsList)
843844
m.Get("/*", repo.GetContents)

routers/api/v1/repo/patch.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2020 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/repofiles"
14+
api "code.gitea.io/gitea/modules/structs"
15+
)
16+
17+
// ApplyDiffPatch handles API call for applying a patch
18+
func ApplyDiffPatch(ctx *context.APIContext, apiOpts api.ApplyDiffPatchFileOptions) {
19+
// swagger:operation POST /repos/{owner}/{repo}/diffpatch repository repoApplyDiffPatch
20+
// ---
21+
// summary: Apply diff patch to repository
22+
// consumes:
23+
// - application/json
24+
// produces:
25+
// - application/json
26+
// parameters:
27+
// - name: owner
28+
// in: path
29+
// description: owner of the repo
30+
// type: string
31+
// required: true
32+
// - name: repo
33+
// in: path
34+
// description: name of the repo
35+
// type: string
36+
// required: true
37+
// - name: body
38+
// in: body
39+
// required: true
40+
// schema:
41+
// "$ref": "#/definitions/UpdateFileOptions"
42+
// responses:
43+
// "200":
44+
// "$ref": "#/responses/FileResponse"
45+
opts := &repofiles.ApplyDiffPatchOptions{
46+
Content: apiOpts.Content,
47+
SHA: apiOpts.SHA,
48+
Message: apiOpts.Message,
49+
OldBranch: apiOpts.BranchName,
50+
NewBranch: apiOpts.NewBranchName,
51+
Committer: &repofiles.IdentityOptions{
52+
Name: apiOpts.Committer.Name,
53+
Email: apiOpts.Committer.Email,
54+
},
55+
Author: &repofiles.IdentityOptions{
56+
Name: apiOpts.Author.Name,
57+
Email: apiOpts.Author.Email,
58+
},
59+
Dates: &repofiles.CommitDateOptions{
60+
Author: apiOpts.Dates.Author,
61+
Committer: apiOpts.Dates.Committer,
62+
},
63+
}
64+
if opts.Dates.Author.IsZero() {
65+
opts.Dates.Author = time.Now()
66+
}
67+
if opts.Dates.Committer.IsZero() {
68+
opts.Dates.Committer = time.Now()
69+
}
70+
71+
if opts.Message == "" {
72+
opts.Message = "apply-patch"
73+
}
74+
75+
if !canWriteFiles(ctx.Repo) {
76+
ctx.Error(http.StatusInternalServerError, "ApplyPatch", models.ErrUserDoesNotHaveAccessToRepo{
77+
UserID: ctx.User.ID,
78+
RepoName: ctx.Repo.Repository.LowerName,
79+
})
80+
}
81+
82+
if fileResponse, err := repofiles.ApplyDiffPatch(ctx.Repo.Repository, ctx.User, opts); err != nil {
83+
ctx.Error(http.StatusInternalServerError, "ApplyPatch", err)
84+
} else {
85+
ctx.JSON(http.StatusCreated, fileResponse)
86+
}
87+
}

routers/repo/patch.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2020 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+
"strings"
9+
10+
"code.gitea.io/gitea/models"
11+
"code.gitea.io/gitea/modules/auth"
12+
"code.gitea.io/gitea/modules/base"
13+
"code.gitea.io/gitea/modules/context"
14+
"code.gitea.io/gitea/modules/repofiles"
15+
"code.gitea.io/gitea/modules/setting"
16+
)
17+
18+
const (
19+
tplPatchFile base.TplName = "repo/editor/patch"
20+
)
21+
22+
// NewDiffPatch render create patch page
23+
func NewDiffPatch(ctx *context.Context) {
24+
ctx.Data["RequireHighlightJS"] = true
25+
26+
canCommit := renderCommitRights(ctx)
27+
28+
ctx.Data["TreePath"] = "patch"
29+
30+
ctx.Data["commit_summary"] = ""
31+
ctx.Data["commit_message"] = ""
32+
if canCommit {
33+
ctx.Data["commit_choice"] = frmCommitChoiceDirect
34+
} else {
35+
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
36+
}
37+
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
38+
ctx.Data["last_commit"] = ctx.Repo.CommitID
39+
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
40+
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
41+
42+
ctx.HTML(200, tplPatchFile)
43+
}
44+
45+
// NewDiffPatchPost response for sending patch page
46+
func NewDiffPatchPost(ctx *context.Context, form auth.EditRepoFileForm) {
47+
canCommit := renderCommitRights(ctx)
48+
branchName := ctx.Repo.BranchName
49+
if form.CommitChoice == frmCommitChoiceNewBranch {
50+
branchName = form.NewBranchName
51+
}
52+
ctx.Data["RequireHighlightJS"] = true
53+
ctx.Data["TreePath"] = "patch"
54+
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + ctx.Repo.BranchName
55+
ctx.Data["FileContent"] = form.Content
56+
ctx.Data["commit_summary"] = form.CommitSummary
57+
ctx.Data["commit_message"] = form.CommitMessage
58+
ctx.Data["commit_choice"] = form.CommitChoice
59+
ctx.Data["new_branch_name"] = form.NewBranchName
60+
ctx.Data["last_commit"] = ctx.Repo.CommitID
61+
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
62+
63+
if ctx.HasError() {
64+
ctx.HTML(200, tplPatchFile)
65+
return
66+
}
67+
68+
// Cannot commit to a an existing branch if user doesn't have rights
69+
if branchName == ctx.Repo.BranchName && !canCommit {
70+
ctx.Data["Err_NewBranchName"] = true
71+
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
72+
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
73+
return
74+
}
75+
76+
// CommitSummary is optional in the web form, if empty, give it a default message based on add or update
77+
// `message` will be both the summary and message combined
78+
message := strings.TrimSpace(form.CommitSummary)
79+
if len(message) == 0 {
80+
message = ctx.Tr("repo.editor.patch")
81+
}
82+
83+
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
84+
if len(form.CommitMessage) > 0 {
85+
message += "\n\n" + form.CommitMessage
86+
}
87+
88+
if _, err := repofiles.ApplyDiffPatch(ctx.Repo.Repository, ctx.User, &repofiles.ApplyDiffPatchOptions{
89+
LastCommitID: form.LastCommit,
90+
OldBranch: ctx.Repo.BranchName,
91+
NewBranch: branchName,
92+
Message: message,
93+
Content: strings.Replace(form.Content, "\r", "", -1),
94+
}); err != nil {
95+
if models.IsErrBranchAlreadyExists(err) {
96+
// For when a user specifies a new branch that already exists
97+
ctx.Data["Err_NewBranchName"] = true
98+
if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
99+
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
100+
} else {
101+
ctx.Error(500, err.Error())
102+
}
103+
} else if models.IsErrCommitIDDoesNotMatch(err) {
104+
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
105+
} else {
106+
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
107+
}
108+
}
109+
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + ctx.Repo.BranchName + "..." + form.NewBranchName)
110+
}

0 commit comments

Comments
 (0)