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:
parent
439ad34c71
commit
eb748f5f3c
|
@ -32,6 +32,21 @@ func GetRawDiff(ctx context.Context, repoPath, commitID string, diffType RawDiff
|
||||||
return GetRawDiffForFile(ctx, repoPath, "", commitID, diffType, "", writer)
|
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.
|
// 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 {
|
func GetRawDiffForFile(ctx context.Context, repoPath, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
|
||||||
repo, closer, err := RepositoryFromContextOrOpen(ctx, repoPath)
|
repo, closer, err := RepositoryFromContextOrOpen(ctx, repoPath)
|
||||||
|
@ -221,8 +236,7 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := scanner.Err()
|
if err := scanner.Err(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,14 @@ type UpdateFileOptions struct {
|
||||||
FromPath string `json:"from_path" binding:"MaxSize(500)"`
|
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
|
// FileLinksResponse contains the links for a repo's file
|
||||||
type FileLinksResponse struct {
|
type FileLinksResponse struct {
|
||||||
Self *string `json:"self"`
|
Self *string `json:"self"`
|
||||||
|
|
|
@ -1075,6 +1075,10 @@ editor.add_tmpl = Add '<filename>'
|
||||||
editor.add = Add '%s'
|
editor.add = Add '%s'
|
||||||
editor.update = Update '%s'
|
editor.update = Update '%s'
|
||||||
editor.delete = Delete '%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.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.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.
|
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
|
||||||
editor.no_commit_to_branch = Unable to commit directly to branch because:
|
editor.no_commit_to_branch = Unable to commit directly to branch because:
|
||||||
editor.user_no_push_to_branch = User cannot push to branch
|
editor.user_no_push_to_branch = User cannot push to branch
|
||||||
editor.require_signed_commit = Branch requires a signed commit
|
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.desc = Browse source code change history.
|
||||||
commits.commits = Commits
|
commits.commits = Commits
|
||||||
|
@ -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.gpg_key_id = GPG Key ID
|
||||||
commits.ssh_key_fingerprint = SSH Key Fingerprint
|
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 = Access to External Issues
|
||||||
ext_issues.desc = Link to an external issue tracker.
|
ext_issues.desc = Link to an external issue tracker.
|
||||||
|
|
||||||
|
@ -2215,11 +2229,16 @@ branch.included_desc = This branch is part of the default branch
|
||||||
branch.included = Included
|
branch.included = Included
|
||||||
branch.create_new_branch = Create branch from branch:
|
branch.create_new_branch = Create branch from branch:
|
||||||
branch.confirm_create_branch = Create branch
|
branch.confirm_create_branch = Create branch
|
||||||
|
branch.create_branch_operation = Create branch
|
||||||
branch.new_branch = Create new branch
|
branch.new_branch = Create new branch
|
||||||
branch.new_branch_from = Create new branch from '%s'
|
branch.new_branch_from = Create new branch from '%s'
|
||||||
branch.renamed = Branch %s was renamed to %s.
|
branch.renamed = Branch %s was renamed to %s.
|
||||||
|
|
||||||
tag.create_tag = Create tag <strong>%s</strong>
|
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.
|
tag.create_success = Tag '%s' has been created.
|
||||||
|
|
||||||
topic.manage_topics = Manage Topics
|
topic.manage_topics = Manage Topics
|
||||||
|
|
|
@ -975,6 +975,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||||
m.Get("/tags/{sha}", context.RepoRefForAPI, repo.GetAnnotatedTag)
|
m.Get("/tags/{sha}", context.RepoRefForAPI, repo.GetAnnotatedTag)
|
||||||
m.Get("/notes/{sha}", repo.GetNote)
|
m.Get("/notes/{sha}", repo.GetNote)
|
||||||
}, reqRepoReader(unit.TypeCode))
|
}, reqRepoReader(unit.TypeCode))
|
||||||
|
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
|
||||||
m.Group("/contents", func() {
|
m.Group("/contents", func() {
|
||||||
m.Get("", repo.GetContentsList)
|
m.Get("", repo.GetContentsList)
|
||||||
m.Get("/*", repo.GetContents)
|
m.Get("/*", repo.GetContents)
|
||||||
|
|
107
routers/api/v1/repo/patch.go
Normal file
107
routers/api/v1/repo/patch.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
189
routers/web/repo/cherry_pick.go
Normal file
189
routers/web/repo/cherry_pick.go
Normal 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
120
routers/web/repo/patch.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -808,6 +808,10 @@ func RegisterRoutes(m *web.Route) {
|
||||||
m.Combo("/_upload/*", repo.MustBeAbleToUpload).
|
m.Combo("/_upload/*", repo.MustBeAbleToUpload).
|
||||||
Get(repo.UploadFile).
|
Get(repo.UploadFile).
|
||||||
Post(bindIgnErr(forms.UploadRepoFileForm{}), repo.UploadFilePost)
|
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)
|
}, context.RepoRefByType(context.RepoRefBranch), repo.MustBeEditable)
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Post("/upload-file", repo.UploadFileToServer)
|
m.Post("/upload-file", repo.UploadFileToServer)
|
||||||
|
@ -1029,6 +1033,7 @@ func RegisterRoutes(m *web.Route) {
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Get("/graph", repo.Graph)
|
m.Get("/graph", repo.Graph)
|
||||||
m.Get("/commit/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
|
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)
|
}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
|
||||||
|
|
||||||
m.Group("/src", func() {
|
m.Group("/src", func() {
|
||||||
|
|
|
@ -754,6 +754,30 @@ func (f *EditPreviewDiffForm) Validate(req *http.Request, errs binding.Errors) b
|
||||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _________ .__ __________.__ __
|
||||||
|
// \_ ___ \| |__ __________________ ___.__. \______ \__| ____ | | __
|
||||||
|
// / \ \/| | \_/ __ \_ __ \_ __ < | | | ___/ |/ ___\| |/ /
|
||||||
|
// \ \___| Y \ ___/| | \/| | \/\___ | | | | \ \___| <
|
||||||
|
// \______ /___| /\___ >__| |__| / ____| |____| |__|\___ >__|_ \
|
||||||
|
// \/ \/ \/ \/ \/ \/
|
||||||
|
|
||||||
|
// CherryPickForm form for changing repository file
|
||||||
|
type CherryPickForm struct {
|
||||||
|
CommitSummary string `binding:"MaxSize(100)"`
|
||||||
|
CommitMessage string
|
||||||
|
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||||
|
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||||
|
LastCommit string
|
||||||
|
Revert bool
|
||||||
|
Signoff bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the fields
|
||||||
|
func (f *CherryPickForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||||
|
ctx := context.GetContext(req)
|
||||||
|
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
|
}
|
||||||
|
|
||||||
// ____ ___ .__ .___
|
// ____ ___ .__ .___
|
||||||
// | | \______ | | _________ __| _/
|
// | | \______ | | _________ __| _/
|
||||||
// | | /\____ \| | / _ \__ \ / __ |
|
// | | /\____ \| | / _ \__ \ / __ |
|
||||||
|
|
|
@ -87,7 +87,7 @@ func TestPatch(pr *models.PullRequest) error {
|
||||||
pr.MergeBase = strings.TrimSpace(pr.MergeBase)
|
pr.MergeBase = strings.TrimSpace(pr.MergeBase)
|
||||||
|
|
||||||
// 2. Check for conflicts
|
// 2. Check for conflicts
|
||||||
if conflicts, err := checkConflicts(pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == models.PullRequestStatusEmpty {
|
if conflicts, err := checkConflicts(ctx, pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == models.PullRequestStatusEmpty {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,19 +217,20 @@ func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, g
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) {
|
// AttemptThreeWayMerge will attempt to three way merge using git read-tree and then follow the git merge-one-file algorithm to attempt to resolve basic conflicts
|
||||||
ctx, cancel, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("checkConflicts: pr[%d] %s/%s#%d", pr.ID, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Index))
|
func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repository, base, ours, theirs, description string) (bool, []string, error) {
|
||||||
defer finished()
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
// First we use read-tree to do a simple three-way merge
|
// First we use read-tree to do a simple three-way merge
|
||||||
if _, err := git.NewCommand(ctx, "read-tree", "-m", pr.MergeBase, "base", "tracking").RunInDir(tmpBasePath); err != nil {
|
if _, err := git.NewCommand(ctx, "read-tree", "-m", base, ours, theirs).RunInDir(gitPath); err != nil {
|
||||||
log.Error("Unable to run read-tree -m! Error: %v", err)
|
log.Error("Unable to run read-tree -m! Error: %v", err)
|
||||||
return false, fmt.Errorf("unable to run read-tree -m! Error: %v", err)
|
return false, nil, fmt.Errorf("unable to run read-tree -m! Error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then we use git ls-files -u to list the unmerged files and collate the triples in unmergedfiles
|
// Then we use git ls-files -u to list the unmerged files and collate the triples in unmergedfiles
|
||||||
unmerged := make(chan *unmergedFile)
|
unmerged := make(chan *unmergedFile)
|
||||||
go unmergedFiles(ctx, tmpBasePath, unmerged)
|
go unmergedFiles(ctx, gitPath, unmerged)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
cancel()
|
cancel()
|
||||||
|
@ -239,8 +240,8 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
|
||||||
}()
|
}()
|
||||||
|
|
||||||
numberOfConflicts := 0
|
numberOfConflicts := 0
|
||||||
pr.ConflictedFiles = make([]string, 0, 5)
|
|
||||||
conflict := false
|
conflict := false
|
||||||
|
conflictedFiles := make([]string, 0, 5)
|
||||||
|
|
||||||
for file := range unmerged {
|
for file := range unmerged {
|
||||||
if file == nil {
|
if file == nil {
|
||||||
|
@ -248,23 +249,33 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
|
||||||
}
|
}
|
||||||
if file.err != nil {
|
if file.err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return false, file.err
|
return false, nil, file.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// OK now we have the unmerged file triplet attempt to merge it
|
// OK now we have the unmerged file triplet attempt to merge it
|
||||||
if err := attemptMerge(ctx, file, tmpBasePath, gitRepo); err != nil {
|
if err := attemptMerge(ctx, file, gitPath, gitRepo); err != nil {
|
||||||
if conflictErr, ok := err.(*errMergeConflict); ok {
|
if conflictErr, ok := err.(*errMergeConflict); ok {
|
||||||
log.Trace("Conflict: %s in PR[%d] %s/%s#%d", conflictErr.filename, pr.ID, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Index)
|
log.Trace("Conflict: %s in %s", conflictErr.filename, description)
|
||||||
conflict = true
|
conflict = true
|
||||||
if numberOfConflicts < 10 {
|
if numberOfConflicts < 10 {
|
||||||
pr.ConflictedFiles = append(pr.ConflictedFiles, conflictErr.filename)
|
conflictedFiles = append(conflictedFiles, conflictErr.filename)
|
||||||
}
|
}
|
||||||
numberOfConflicts++
|
numberOfConflicts++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return false, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return conflict, conflictedFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) {
|
||||||
|
description := fmt.Sprintf("PR[%d] %s/%s#%d", pr.ID, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Index)
|
||||||
|
conflict, _, err := AttemptThreeWayMerge(ctx,
|
||||||
|
tmpBasePath, gitRepo, pr.MergeBase, "base", "tracking", description)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
if !conflict {
|
if !conflict {
|
||||||
treeHash, err := git.NewCommand(ctx, "write-tree").RunInDir(tmpBasePath)
|
treeHash, err := git.NewCommand(ctx, "write-tree").RunInDir(tmpBasePath)
|
||||||
|
|
126
services/repository/files/cherry_pick.go
Normal file
126
services/repository/files/cherry_pick.go
Normal file
|
@ -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
|
||||||
|
}
|
193
services/repository/files/patch.go
Normal file
193
services/repository/files/patch.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -1,37 +1,51 @@
|
||||||
{{$release := .release}}
|
{{$release := .release}}
|
||||||
|
{{$defaultBranch := $.root.BranchName}}{{if and .root.IsViewTag (not .noTag)}}{{$defaultBranch = .root.TagName}}{{end}}{{if eq $defaultBranch ""}}{{$defaultBranch = $.root.Repository.DefaultBranch}}{{end}}
|
||||||
{{$showBranchesInDropdown := not .root.HideBranchesInDropdown}}
|
{{$showBranchesInDropdown := not .root.HideBranchesInDropdown}}
|
||||||
<div class="fitted item choose reference{{if not $release}} mr-1{{end}}">
|
<div class="fitted item choose reference{{if not $release}} mr-1{{end}}">
|
||||||
<div class="ui floating filter dropdown custom" data-can-create-branch="{{.root.CanCreateBranch}}" data-no-results="{{.root.i18n.Tr "repo.pulls.no_results"}}">
|
<div class="ui floating filter dropdown custom"
|
||||||
|
data-branch-form="{{if $.branchForm}}{{$.branchForm}}{{end}}"
|
||||||
|
data-can-create-branch="{{if .canCreateBranch}}{{.canCreateBranch}}{{else}}{{.root.CanCreateBranch}}{{end}}"
|
||||||
|
data-no-results="{{.root.i18n.Tr "repo.pulls.no_results"}}"
|
||||||
|
data-set-action="{{.setAction}}" data-submit-form="{{.submitForm}}"
|
||||||
|
data-view-type="{{if and .root.IsViewTag (not .noTag)}}tag{{else if .root.IsViewBranch}}branch{{else}}tree{{end}}"
|
||||||
|
data-ref-name="{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}"
|
||||||
|
data-branch-url-prefix="{{if .branchURLPrefix}}{{.branchURLPrefix}}{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/branch/{{end}}"
|
||||||
|
data-branch-url-suffix="{{if .branchURLSuffix}}{{.branchURLSuffix}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}"
|
||||||
|
data-tag-url-prefix="{{if .tagURLPrefix}}{{.tagURLPrefix}}{{else if $release}}{{$.root.RepoLink}}/compare/{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/tag/{{end}}"
|
||||||
|
data-tag-url-suffix="{{if .tagURLSuffix}}{{.tagURLSuffix}}{{else if $release}}...{{if $release.IsDraft}}{{PathEscapeSegments $release.Target}}{{else}}{{if $release.TagName}}{{PathEscapeSegments $release.TagName}}{{else}}{{PathEscapeSegments $release.Sha1}}{{end}}{{end}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}">
|
||||||
<div class="ui basic small compact button" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
|
<div class="ui basic small compact button" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
{{if $release}}
|
{{if $release}}
|
||||||
{{.root.i18n.Tr "repo.release.compare"}}
|
{{.root.i18n.Tr "repo.release.compare"}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if .root.IsViewTag}}{{svg "octicon-tag"}}{{else}}{{svg "octicon-git-branch"}}{{end}}
|
<span :class="{visible: isViewTag}" v-if="isViewTag" v-cloak>{{svg "octicon-tag"}} {{.root.i18n.Tr "repo.tag"}}:</span>
|
||||||
{{if .root.IsViewBranch}}{{.root.i18n.Tr "repo.branch"}}{{else if .root.IsViewTag}}{{.root.i18n.Tr "repo.tag"}}{{else}}{{.root.i18n.Tr "repo.tree"}}{{end}}:
|
<span :class="{visible: isViewBranch}" v-if="isViewBranch" v-cloak>{{svg "octicon-git-branch"}} {{.root.i18n.Tr "repo.branch"}}:</span>
|
||||||
<strong>{{if .root.IsViewBranch}}{{.root.BranchName}}{{else if .root.IsViewTag}}{{.root.TagName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong>
|
<span :class="{visible: isViewTree}" v-if="isViewTree" v-cloak>{{svg "octicon-git-branch"}} {{.root.i18n.Tr "repo.tree"}}:</span>
|
||||||
|
<strong ref="dropdownRefName">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong>
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
</div>
|
</div>
|
||||||
<div class="data" style="display: none" data-mode="{{if .root.IsViewTag}}tags{{else}}branches{{end}}">
|
<div class="data" style="display: none" data-mode="{{if or .root.IsViewTag .isTag}}tags{{else}}branches{{end}}">
|
||||||
{{if $showBranchesInDropdown}}
|
{{if $showBranchesInDropdown}}
|
||||||
{{range .root.Branches}}
|
{{range .root.Branches}}
|
||||||
<div class="item branch {{if eq $.root.BranchName .}}selected{{end}}" data-url="{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/branch/{{PathEscapeSegments .}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}">{{.}}</div>
|
<div class="item branch {{if eq $defaultBranch .}}selected{{end}}" data-url="{{PathEscapeSegments .}}">{{.}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{range .root.Tags}}
|
{{if (not .noTag)}}
|
||||||
{{if $release}}
|
{{range .root.Tags}}
|
||||||
<div class="item tag {{if eq $release.TagName .}}selected{{end}}" data-url="{{$.root.RepoLink}}/compare/{{PathEscapeSegments .}}...{{if $release.IsDraft}}{{PathEscapeSegments $release.Target}}{{else}}{{if $release.TagName}}{{PathEscapeSegments $release.TagName}}{{else}}{{PathEscapeSegments $release.Sha1}}{{end}}{{end}}">{{.}}</div>
|
{{if $release}}
|
||||||
{{else}}
|
<div class="item tag {{if eq $release.TagName .}}selected{{end}}" data-url="{{PathEscapeSegments .}}">{{.}}</div>
|
||||||
<div class="item tag {{if eq $.root.BranchName .}}selected{{end}}" data-url="{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/tag/{{PathEscapeSegments .}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}">{{.}}</div>
|
{{else}}
|
||||||
|
<div class="item tag {{if eq $defaultBranch .}}selected{{end}}" data-url="{{PathEscapeSegments .}}">{{.}}</div>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="menu transition" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
|
<div class="menu transition" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
|
||||||
<div class="ui icon search input">
|
<div class="ui icon search input">
|
||||||
<i class="icon df ac jc m-0">{{svg "octicon-filter" 16}}</i>
|
<i class="icon df ac jc m-0">{{svg "octicon-filter" 16}}</i>
|
||||||
<input name="search" ref="searchField" autocomplete="off" v-model="searchTerm" @keydown="keydown($event)" placeholder="{{if $showBranchesInDropdown}}{{.root.i18n.Tr "repo.filter_branch_and_tag"}}{{else}}{{.root.i18n.Tr "repo.find_tag"}}{{end}}...">
|
<input name="search" ref="searchField" autocomplete="off" v-model="searchTerm" @keydown="keydown($event)" placeholder="{{if $.noTag}}{{.root.i18n.Tr "repo.filter_branch"}}{{else if $showBranchesInDropdown}}{{.root.i18n.Tr "repo.filter_branch_and_tag"}}{{else}}{{.root.i18n.Tr "repo.find_tag"}}{{end}}...">
|
||||||
</div>
|
</div>
|
||||||
{{if $showBranchesInDropdown}}
|
{{if $showBranchesInDropdown}}
|
||||||
<div class="header branch-tag-choice">
|
<div class="header branch-tag-choice">
|
||||||
|
@ -42,11 +56,13 @@
|
||||||
{{svg "octicon-git-branch" 16 "mr-2"}}{{.root.i18n.Tr "repo.branches"}}
|
{{svg "octicon-git-branch" 16 "mr-2"}}{{.root.i18n.Tr "repo.branches"}}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="reference column" href="#" @click="createTag = true; mode = 'tags'; focusSearchField()">
|
{{if not .noTag}}
|
||||||
<span class="text" :class="{black: mode == 'tags'}">
|
<a class="reference column" href="#" @click="createTag = true; mode = 'tags'; focusSearchField()">
|
||||||
{{svg "octicon-tag" 16 "mr-2"}}{{.root.i18n.Tr "repo.tags"}}
|
<span class="text" :class="{black: mode == 'tags'}">
|
||||||
</span>
|
{{svg "octicon-tag" 16 "mr-2"}}{{.root.i18n.Tr "repo.tags"}}
|
||||||
</a>
|
</span>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,14 +18,123 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="ui top attached header clearing segment pr {{$class}}">
|
<div class="ui top attached header clearing segment pr {{$class}}">
|
||||||
{{if not $.PageIsWiki}}
|
<div class="df mb-4">
|
||||||
<a class="ui blue tiny button browse-button" href="{{.SourcePath}}">
|
<h3 class="mb-0 f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses "root" $}}</h3>
|
||||||
{{.i18n.Tr "repo.diff.browse_source"}}
|
{{if not $.PageIsWiki}}
|
||||||
</a>
|
<div class="ui">
|
||||||
{{end}}
|
<a class="ui blue tiny button" href="{{.SourcePath}}">
|
||||||
<h3 class="mt-0"><span class="message-wrapper"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</span></span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses "root" $}}</h3>
|
{{.i18n.Tr "repo.diff.browse_source"}}
|
||||||
|
</a>
|
||||||
|
{{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}}{{- /* */ -}}
|
||||||
|
<div class="ui blue tiny floating dropdown icon button">{{.i18n.Tr "repo.commit.actions"}}
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}<span class="sr-mobile-only">{{.i18n.Tr "repo.commit.actions"}}</span>
|
||||||
|
<div class="menu">
|
||||||
|
<div class="ui header">{{.i18n.Tr "repo.commit.actions"}}</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="item show-create-branch-modal"
|
||||||
|
data-content="{{$.i18n.Tr "repo.branch.new_branch_from" (.CommitID)}}"
|
||||||
|
data-branch-from="{{ShortSha .CommitID}}"
|
||||||
|
data-branch-from-urlcomponent="{{.CommitID}}"
|
||||||
|
data-modal="#create-branch-modal">
|
||||||
|
{{.i18n.Tr "repo.branch.create_branch_operation"}}
|
||||||
|
</div>
|
||||||
|
<div class="item show-create-branch-modal"
|
||||||
|
data-content="{{$.i18n.Tr "repo.branch.new_branch_from" (.CommitID)}}"
|
||||||
|
data-branch-from="{{ShortSha .CommitID}}"
|
||||||
|
data-branch-from-urlcomponent="{{.CommitID}}"
|
||||||
|
data-modal="#create-tag-modal"
|
||||||
|
data-modal-from-span="#modal-create-tag-from-span"
|
||||||
|
data-modal-form="#create-tag-form">
|
||||||
|
{{.i18n.Tr "repo.tag.create_tag_operation"}}
|
||||||
|
</div>
|
||||||
|
<div class="item show-modal revert-button"
|
||||||
|
data-modal="#cherry-pick-modal"
|
||||||
|
data-modal-cherry-pick-type="revert"
|
||||||
|
data-modal-cherry-pick-header="{{$.i18n.Tr "repo.commit.revert-header" (ShortSha .CommitID)}}"
|
||||||
|
data-modal-cherry-pick-content="{{$.i18n.Tr "repo.commit.revert-content"}}"
|
||||||
|
data-modal-cherry-pick-submit="{{.i18n.Tr "repo.commit.revert"}}">{{.i18n.Tr "repo.commit.revert"}}</a></div>
|
||||||
|
<div class="item cherry-pick-button show-modal"
|
||||||
|
data-modal="#cherry-pick-modal"
|
||||||
|
data-modal-cherry-pick-type="cherry-pick"
|
||||||
|
data-modal-cherry-pick-header="{{$.i18n.Tr "repo.commit.cherry-pick-header" (ShortSha .CommitID)}}"
|
||||||
|
data-modal-cherry-pick-content="{{$.i18n.Tr "repo.commit.cherry-pick-content"}}"
|
||||||
|
data-modal-cherry-pick-submit="{{.i18n.Tr "repo.commit.cherry-pick"}}">{{.i18n.Tr "repo.commit.cherry-pick"}}</a></div>
|
||||||
|
<div class="ui basic modal" id="cherry-pick-modal">
|
||||||
|
<div class="ui icon header">
|
||||||
|
<span id="cherry-pick-header"></span>
|
||||||
|
</div>
|
||||||
|
<div class="content center">
|
||||||
|
<p id="cherry-pick-content" class="branch-dropdown"></p>
|
||||||
|
{{template "repo/branch_dropdown" dict "root" .
|
||||||
|
"noTag" "true" "canCreateBranch" "false"
|
||||||
|
"branchForm" "branch-dropdown-form"
|
||||||
|
"branchURLPrefix" (printf "%s/_cherrypick/%s/" $.RepoLink .CommitID) "branchURLSuffix" ""
|
||||||
|
"setAction" "true" "submitForm" "true"}}
|
||||||
|
<form method="GET" action="{{$.RepoLink}}/_cherrypick/{{.CommitID}}/{{if $.BranchName}}{{PathEscapeSegments $.BranchName}}{{else}}{{PathEscapeSegments $.Repository.DefaultBranch}}{{end}}" id="branch-dropdown-form">
|
||||||
|
<input type="hidden" name="ref" value="{{if $.BranchName}}{{$.BranchName}}{{else}}{{$.Repository.DefaultBranch}}{{end}}">
|
||||||
|
<input type="hidden" name="refType" value="branch">
|
||||||
|
<input type="hidden" id="cherry-pick-type" name="cherry-pick-type"><br/>
|
||||||
|
<button type="submit" id="cherry-pick-submit" class="ui green button"></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui small modal" id="create-branch-modal">
|
||||||
|
<div class="header">
|
||||||
|
{{.i18n.Tr "repo.branch.new_branch"}}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<form class="ui form" id="create-branch-form" action="" data-base-action="{{.RepoLink}}/branches/_new/commit/" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="field">
|
||||||
|
<label>
|
||||||
|
{{.i18n.Tr "repo.branch.new_branch_from" "<span class=\"text\" id=\"modal-create-branch-from-span\"></span>" | Safe }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="required field">
|
||||||
|
<label for="new_branch_name">{{.i18n.Tr "repo.branch.name"}}</label>
|
||||||
|
<input id="new_branch_name" name="new_branch_name" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text right actions">
|
||||||
|
<div class="ui cancel button">{{.i18n.Tr "settings.cancel"}}</div>
|
||||||
|
<button class="ui green button">{{.i18n.Tr "repo.branch.confirm_create_branch"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui small modal" id="create-tag-modal">
|
||||||
|
<div class="header">
|
||||||
|
{{.i18n.Tr "repo.tag.create_tag_operation"}}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<form class="ui form" id="create-tag-form" action="" data-base-action="{{.RepoLink}}/branches/_new/commit/" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" name="create_tag" value="true">
|
||||||
|
<div class="field">
|
||||||
|
<label>
|
||||||
|
{{.i18n.Tr "repo.tag.create_tag_from" "<span class=\"text\" id=\"modal-create-tag-from-span\"></span>" | Safe }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="required field">
|
||||||
|
<label for="new_branch_name">{{.i18n.Tr "repo.release.tag_name"}}</label>
|
||||||
|
<input id="new_branch_name" name="new_branch_name" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text right actions">
|
||||||
|
<div class="ui cancel button">{{.i18n.Tr "settings.cancel"}}</div>
|
||||||
|
<button class="ui green button">{{.i18n.Tr "repo.tag.confirm_create_tag"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
{{if IsMultilineCommitMessage .Commit.Message}}
|
{{if IsMultilineCommitMessage .Commit.Message}}
|
||||||
<pre class="commit-body">{{RenderCommitBody $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
|
<pre class="commit-body mt-0">{{RenderCommitBody $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .BranchName}}
|
{{if .BranchName}}
|
||||||
<span class="text grey mr-3">{{svg "octicon-git-branch" 16 "mr-2"}}{{.BranchName}}</span>
|
<span class="text grey mr-3">{{svg "octicon-git-branch" 16 "mr-2"}}{{.BranchName}}</span>
|
||||||
|
|
32
templates/repo/editor/cherry_pick.tmpl
Normal file
32
templates/repo/editor/cherry_pick.tmpl
Normal file
|
@ -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" .}}
|
59
templates/repo/editor/patch.tmpl
Normal file
59
templates/repo/editor/patch.tmpl
Normal file
|
@ -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" .}}
|
|
@ -89,6 +89,11 @@
|
||||||
{{.i18n.Tr "repo.editor.upload_file"}}
|
{{.i18n.Tr "repo.editor.upload_file"}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .CanAddFile}}
|
||||||
|
<a href="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}" class="ui button">
|
||||||
|
{{.i18n.Tr "repo.editor.patch"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if and (ne $n 0) (not .IsViewFile) (not .IsBlame) }}
|
{{if and (ne $n 0) (not .IsViewFile) (not .IsBlame) }}
|
||||||
<a href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}" class="ui button">
|
<a href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}" class="ui button">
|
||||||
|
|
|
@ -3383,6 +3383,50 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/diffpatch": {
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Apply diff patch to repository",
|
||||||
|
"operationId": "repoApplyDiffPatch",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/UpdateFileOptions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/FileResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/editorconfig/{filepath}": {
|
"/repos/{owner}/{repo}/editorconfig/{filepath}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
|
|
@ -10,11 +10,22 @@ export function initRepoBranchTagDropdown(selector) {
|
||||||
items: [],
|
items: [],
|
||||||
mode: $data.data('mode'),
|
mode: $data.data('mode'),
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
refName: '',
|
||||||
noResults: '',
|
noResults: '',
|
||||||
canCreateBranch: false,
|
canCreateBranch: false,
|
||||||
menuVisible: false,
|
menuVisible: false,
|
||||||
createTag: false,
|
createTag: false,
|
||||||
active: 0
|
isViewTag: false,
|
||||||
|
isViewBranch: false,
|
||||||
|
isViewTree: false,
|
||||||
|
active: 0,
|
||||||
|
branchForm: '',
|
||||||
|
branchURLPrefix: '',
|
||||||
|
branchURLSuffix: '',
|
||||||
|
tagURLPrefix: '',
|
||||||
|
tagURLSuffix: '',
|
||||||
|
setAction: false,
|
||||||
|
submitForm: false,
|
||||||
};
|
};
|
||||||
$data.find('.item').each(function () {
|
$data.find('.item').each(function () {
|
||||||
data.items.push({
|
data.items.push({
|
||||||
|
@ -64,6 +75,26 @@ export function initRepoBranchTagDropdown(selector) {
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.noResults = this.$el.getAttribute('data-no-results');
|
this.noResults = this.$el.getAttribute('data-no-results');
|
||||||
this.canCreateBranch = this.$el.getAttribute('data-can-create-branch') === 'true';
|
this.canCreateBranch = this.$el.getAttribute('data-can-create-branch') === 'true';
|
||||||
|
this.branchForm = this.$el.getAttribute('data-branch-form');
|
||||||
|
switch (this.$el.getAttribute('data-view-type')) {
|
||||||
|
case 'tree':
|
||||||
|
this.isViewTree = true;
|
||||||
|
break;
|
||||||
|
case 'tag':
|
||||||
|
this.isViewTag = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.isViewBranch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.refName = this.$el.getAttribute('data-ref-name');
|
||||||
|
this.branchURLPrefix = this.$el.getAttribute('data-branch-url-prefix');
|
||||||
|
this.branchURLSuffix = this.$el.getAttribute('data-branch-url-suffix');
|
||||||
|
this.tagURLPrefix = this.$el.getAttribute('data-tag-url-prefix');
|
||||||
|
this.tagURLSuffix = this.$el.getAttribute('data-tag-url-suffix');
|
||||||
|
this.setAction = this.$el.getAttribute('data-set-action') === 'true';
|
||||||
|
this.submitForm = this.$el.getAttribute('data-submit-form') === 'true';
|
||||||
|
|
||||||
|
|
||||||
document.body.addEventListener('click', (event) => {
|
document.body.addEventListener('click', (event) => {
|
||||||
if (this.$el.contains(event.target)) return;
|
if (this.$el.contains(event.target)) return;
|
||||||
|
@ -80,7 +111,32 @@ export function initRepoBranchTagDropdown(selector) {
|
||||||
prev.selected = false;
|
prev.selected = false;
|
||||||
}
|
}
|
||||||
item.selected = true;
|
item.selected = true;
|
||||||
window.location.href = item.url;
|
const url = (item.tag) ? this.tagURLPrefix + item.url + this.tagURLSuffix : this.branchURLPrefix + item.url + this.branchURLSuffix;
|
||||||
|
if (this.branchForm === '') {
|
||||||
|
window.location.href = url;
|
||||||
|
} else {
|
||||||
|
this.isViewTree = false;
|
||||||
|
this.isViewTag = false;
|
||||||
|
this.isViewBranch = false;
|
||||||
|
this.$refs.dropdownRefName.textContent = item.name;
|
||||||
|
if (this.setAction) {
|
||||||
|
$(`#${this.branchForm}`).attr('action', url);
|
||||||
|
} else {
|
||||||
|
$(`#${this.branchForm} input[name="refURL"]`).val(url);
|
||||||
|
}
|
||||||
|
$(`#${this.branchForm} input[name="ref"]`).val(item.name);
|
||||||
|
if (item.tag) {
|
||||||
|
this.isViewTag = true;
|
||||||
|
$(`#${this.branchForm} input[name="refType"]`).val('tag');
|
||||||
|
} else {
|
||||||
|
this.isViewBranch = true;
|
||||||
|
$(`#${this.branchForm} input[name="refType"]`).val('branch');
|
||||||
|
}
|
||||||
|
if (this.submitForm) {
|
||||||
|
$(`#${this.branchForm}`).trigger('submit');
|
||||||
|
}
|
||||||
|
Vue.set(this, 'menuVisible', false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
createNewBranch() {
|
createNewBranch() {
|
||||||
if (!this.showCreateNewBranch) return;
|
if (!this.showCreateNewBranch) return;
|
||||||
|
|
|
@ -313,9 +313,22 @@ export function initGlobalButtons() {
|
||||||
alert('Nothing to hide');
|
alert('Nothing to hide');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.show-modal.button').on('click', function () {
|
$('.show-modal').on('click', function () {
|
||||||
$($(this).data('modal')).modal('show');
|
const modalDiv = $($(this).attr('data-modal'));
|
||||||
const colorPickers = $($(this).data('modal')).find('.color-picker');
|
for (const attrib of this.attributes) {
|
||||||
|
if (!attrib.name.startsWith('data-modal-')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const id = attrib.name.substring(11);
|
||||||
|
const target = modalDiv.find(`#${id}`);
|
||||||
|
if (target.is('input')) {
|
||||||
|
target.val(attrib.value);
|
||||||
|
} else {
|
||||||
|
target.text(attrib.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modalDiv.modal('show');
|
||||||
|
const colorPickers = $($(this).attr('data-modal')).find('.color-picker');
|
||||||
if (colorPickers.length > 0) {
|
if (colorPickers.length > 0) {
|
||||||
initCompColorPicker();
|
initCompColorPicker();
|
||||||
}
|
}
|
||||||
|
@ -323,10 +336,10 @@ export function initGlobalButtons() {
|
||||||
|
|
||||||
$('.delete-post.button').on('click', function () {
|
$('.delete-post.button').on('click', function () {
|
||||||
const $this = $(this);
|
const $this = $(this);
|
||||||
$.post($this.data('request-url'), {
|
$.post($this.attr('data-request-url'), {
|
||||||
_csrf: csrfToken
|
_csrf: csrfToken
|
||||||
}).done(() => {
|
}).done(() => {
|
||||||
window.location.href = $this.data('done-url');
|
window.location.href = $this.attr('data-done-url');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -436,7 +436,7 @@ export function initRepository() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// File list and commits
|
// File list and commits
|
||||||
if ($('.repository.file.list').length > 0 ||
|
if ($('.repository.file.list').length > 0 || $('.branch-dropdown').length > 0 ||
|
||||||
$('.repository.commits').length > 0 || $('.repository.release').length > 0) {
|
$('.repository.commits').length > 0 || $('.repository.release').length > 0) {
|
||||||
initRepoBranchTagDropdown('.choose.reference .dropdown');
|
initRepoBranchTagDropdown('.choose.reference .dropdown');
|
||||||
}
|
}
|
||||||
|
|
|
@ -2551,12 +2551,6 @@
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.browse-button {
|
|
||||||
position: absolute;
|
|
||||||
right: 1rem;
|
|
||||||
top: .75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-header-row {
|
.commit-header-row {
|
||||||
min-height: 50px !important;
|
min-height: 50px !important;
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
|
|
Loading…
Reference in a new issue