]> source.dussan.org Git - gitea.git/commitdiff
Add apply-patch, basic revert and cherry-pick functionality (#17902)
authorzeripath <art27@cantab.net>
Wed, 9 Feb 2022 20:28:55 +0000 (20:28 +0000)
committerGitHub <noreply@github.com>
Wed, 9 Feb 2022 20:28:55 +0000 (20:28 +0000)
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>
23 files changed:
modules/git/diff.go
modules/structs/repo_file.go
options/locale/locale_en-US.ini
routers/api/v1/api.go
routers/api/v1/repo/patch.go [new file with mode: 0644]
routers/web/repo/cherry_pick.go [new file with mode: 0644]
routers/web/repo/patch.go [new file with mode: 0644]
routers/web/web.go
services/forms/repo_form.go
services/pull/patch.go
services/repository/files/cherry_pick.go [new file with mode: 0644]
services/repository/files/patch.go [new file with mode: 0644]
templates/repo/branch_dropdown.tmpl
templates/repo/commit_page.tmpl
templates/repo/editor/cherry_pick.tmpl [new file with mode: 0644]
templates/repo/editor/patch.tmpl [new file with mode: 0644]
templates/repo/home.tmpl
templates/swagger/v1_json.tmpl
web_src/js/components/RepoBranchTagDropdown.js
web_src/js/features/common-global.js
web_src/js/features/repo-branch.js
web_src/js/features/repo-legacy.js
web_src/less/_repository.less

index d71b0b247185c1e8d03227c2021b263f60567191..2d85db475396fa890adab89f634825d96dcaef1e 100644 (file)
@@ -32,6 +32,21 @@ func GetRawDiff(ctx context.Context, repoPath, commitID string, diffType RawDiff
        return GetRawDiffForFile(ctx, repoPath, "", commitID, diffType, "", writer)
 }
 
+// GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer.
+func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error {
+       stderr := new(bytes.Buffer)
+       cmd := NewCommand(ctx, "show", "--pretty=format:revert %H%n", "-R", commitID)
+       if err := cmd.RunWithContext(&RunContext{
+               Timeout: -1,
+               Dir:     repoPath,
+               Stdout:  writer,
+               Stderr:  stderr,
+       }); err != nil {
+               return fmt.Errorf("Run: %v - %s", err, stderr)
+       }
+       return nil
+}
+
 // GetRawDiffForFile dumps diff results of file in given commit ID to io.Writer.
 func GetRawDiffForFile(ctx context.Context, repoPath, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
        repo, closer, err := RepositoryFromContextOrOpen(ctx, repoPath)
@@ -221,8 +236,7 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
                        }
                }
        }
-       err := scanner.Err()
-       if err != nil {
+       if err := scanner.Err(); err != nil {
                return "", err
        }
 
index 71733c90e704518f78ac201004fc9c8afcbab21a..e2947bf7ac7b047e418fc2ba353613793ef11ec4 100644 (file)
@@ -50,6 +50,14 @@ type UpdateFileOptions struct {
        FromPath string `json:"from_path" binding:"MaxSize(500)"`
 }
 
+// ApplyDiffPatchFileOptions options for applying a diff patch
+// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
+type ApplyDiffPatchFileOptions struct {
+       DeleteFileOptions
+       // required: true
+       Content string `json:"content"`
+}
+
 // FileLinksResponse contains the links for a repo's file
 type FileLinksResponse struct {
        Self    *string `json:"self"`
index e91016bdc05069a4ed4882c2ecdae1903975d567..7b3671f90aea3fa7b98bdf36bc648af822f59e27 100644 (file)
@@ -1075,6 +1075,10 @@ editor.add_tmpl = Add '<filename>'
 editor.add = Add '%s'
 editor.update = Update '%s'
 editor.delete = Delete '%s'
+editor.patch = Apply Patch
+editor.patching = Patching:
+editor.fail_to_apply_patch = Unable to apply patch '%s'
+editor.new_patch = New Patch
 editor.commit_message_desc = Add an optional extended description…
 editor.signoff_desc = Add a Signed-off-by trailer by the committer at the end of the commit log message.
 editor.commit_directly_to_this_branch = Commit directly to the <strong class="branch-name">%s</strong> branch.
@@ -1110,6 +1114,8 @@ editor.cannot_commit_to_protected_branch = Cannot commit to protected branch '%s
 editor.no_commit_to_branch = Unable to commit directly to branch because:
 editor.user_no_push_to_branch = User cannot push to branch
 editor.require_signed_commit = Branch requires a signed commit
+editor.cherry_pick = Cherry-pick %s onto:
+editor.revert = Revert %s onto:
 
 commits.desc = Browse source code change history.
 commits.commits = Commits
@@ -1130,6 +1136,14 @@ commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does n
 commits.gpg_key_id = GPG Key ID
 commits.ssh_key_fingerprint = SSH Key Fingerprint
 
+commit.actions = Actions
+commit.revert = Revert
+commit.revert-header = Revert: %s
+commit.revert-content = Select branch to revert onto:
+commit.cherry-pick = Cherry-pick
+commit.cherry-pick-header = Cherry-pick: %s
+commit.cherry-pick-content = Select branch to cherry-pick onto:
+
 ext_issues = Access to External Issues
 ext_issues.desc = Link to an external issue tracker.
 
@@ -2215,11 +2229,16 @@ branch.included_desc = This branch is part of the default branch
 branch.included = Included
 branch.create_new_branch = Create branch from branch:
 branch.confirm_create_branch = Create branch
+branch.create_branch_operation = Create branch
 branch.new_branch = Create new branch
 branch.new_branch_from = Create new branch from '%s'
 branch.renamed = Branch %s was renamed to %s.
 
 tag.create_tag = Create tag <strong>%s</strong>
+tag.create_tag_operation = Create tag
+tag.confirm_create_tag = Create tag
+tag.create_tag_from = Create new tag from '%s'
+
 tag.create_success = Tag '%s' has been created.
 
 topic.manage_topics = Manage Topics
index df00a852f22075a1239bc5dec625f41d55d7b332..6d8ab8ce98fd9765b8c54a88027495c5d32af1b1 100644 (file)
@@ -975,6 +975,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
                                        m.Get("/tags/{sha}", context.RepoRefForAPI, repo.GetAnnotatedTag)
                                        m.Get("/notes/{sha}", repo.GetNote)
                                }, reqRepoReader(unit.TypeCode))
+                               m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
                                m.Group("/contents", func() {
                                        m.Get("", repo.GetContentsList)
                                        m.Get("/*", repo.GetContents)
diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go
new file mode 100644 (file)
index 0000000..64a7a32
--- /dev/null
@@ -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)
+       }
+}
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
new file mode 100644 (file)
index 0000000..1b0d845
--- /dev/null
@@ -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))
+       }
+}
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
new file mode 100644 (file)
index 0000000..b00065a
--- /dev/null
@@ -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))
+       }
+}
index 60a379aef8580a391ab55845ae87a6fe5610adbc..52eca9a0a6d9f3940fdd6289dbca17c9f2922faa 100644 (file)
@@ -808,6 +808,10 @@ func RegisterRoutes(m *web.Route) {
                                m.Combo("/_upload/*", repo.MustBeAbleToUpload).
                                        Get(repo.UploadFile).
                                        Post(bindIgnErr(forms.UploadRepoFileForm{}), repo.UploadFilePost)
+                               m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch).
+                                       Post(bindIgnErr(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
+                               m.Combo("/_cherrypick/{sha:([a-f0-9]{7,40})}/*").Get(repo.CherryPick).
+                                       Post(bindIgnErr(forms.CherryPickForm{}), repo.CherryPickPost)
                        }, context.RepoRefByType(context.RepoRefBranch), repo.MustBeEditable)
                        m.Group("", func() {
                                m.Post("/upload-file", repo.UploadFileToServer)
@@ -1029,6 +1033,7 @@ func RegisterRoutes(m *web.Route) {
                m.Group("", func() {
                        m.Get("/graph", repo.Graph)
                        m.Get("/commit/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
+                       m.Get("/cherry-pick/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
                }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
 
                m.Group("/src", func() {
index b32bd3cafd9d30b058e6dfc60b7a62fb9d2b456a..da709ef800240b32a86498172aa0c1423a12fd19 100644 (file)
@@ -754,6 +754,30 @@ func (f *EditPreviewDiffForm) Validate(req *http.Request, errs binding.Errors) b
        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)
+}
+
 //  ____ ___        .__                    .___
 // |    |   \______ |  |   _________     __| _/
 // |    |   /\____ \|  |  /  _ \__  \   / __ |
index 731c9d5717559706bf1921e782487002b147e432..a2c8345326f0bf88f38cc318524cd715363347fa 100644 (file)
@@ -87,7 +87,7 @@ func TestPatch(pr *models.PullRequest) error {
        pr.MergeBase = strings.TrimSpace(pr.MergeBase)
 
        // 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
        }
 
@@ -217,19 +217,20 @@ func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, g
        return nil
 }
 
-func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) {
-       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))
-       defer finished()
+// 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
+func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repository, base, ours, theirs, description string) (bool, []string, error) {
+       ctx, cancel := context.WithCancel(ctx)
+       defer cancel()
 
        // 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)
-               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
        unmerged := make(chan *unmergedFile)
-       go unmergedFiles(ctx, tmpBasePath, unmerged)
+       go unmergedFiles(ctx, gitPath, unmerged)
 
        defer func() {
                cancel()
@@ -239,8 +240,8 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
        }()
 
        numberOfConflicts := 0
-       pr.ConflictedFiles = make([]string, 0, 5)
        conflict := false
+       conflictedFiles := make([]string, 0, 5)
 
        for file := range unmerged {
                if file == nil {
@@ -248,23 +249,33 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
                }
                if file.err != nil {
                        cancel()
-                       return false, file.err
+                       return false, nil, file.err
                }
 
                // 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 {
-                               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
                                if numberOfConflicts < 10 {
-                                       pr.ConflictedFiles = append(pr.ConflictedFiles, conflictErr.filename)
+                                       conflictedFiles = append(conflictedFiles, conflictErr.filename)
                                }
                                numberOfConflicts++
                                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 {
                treeHash, err := git.NewCommand(ctx, "write-tree").RunInDir(tmpBasePath)
diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go
new file mode 100644 (file)
index 0000000..dc932b3
--- /dev/null
@@ -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
+}
diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go
new file mode 100644 (file)
index 0000000..09a8b3e
--- /dev/null
@@ -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
+}
index a6120483b5a23f3faf592087d5802f36fc1e99d9..0e263e80756a70602d6d1bd4aa04da869268f0bf 100644 (file)
@@ -1,37 +1,51 @@
 {{$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}}
 <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">
                        <span class="text">
                                {{if $release}}
                                        {{.root.i18n.Tr "repo.release.compare"}}
                                {{else}}
-                                       {{if .root.IsViewTag}}{{svg "octicon-tag"}}{{else}}{{svg "octicon-git-branch"}}{{end}}
-                                       {{if .root.IsViewBranch}}{{.root.i18n.Tr "repo.branch"}}{{else if .root.IsViewTag}}{{.root.i18n.Tr "repo.tag"}}{{else}}{{.root.i18n.Tr "repo.tree"}}{{end}}:
-                                       <strong>{{if .root.IsViewBranch}}{{.root.BranchName}}{{else if .root.IsViewTag}}{{.root.TagName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong>
+                                       <span :class="{visible: isViewTag}" v-if="isViewTag" v-cloak>{{svg "octicon-tag"}} {{.root.i18n.Tr "repo.tag"}}:</span>
+                                       <span :class="{visible: isViewBranch}" v-if="isViewBranch" v-cloak>{{svg "octicon-git-branch"}} {{.root.i18n.Tr "repo.branch"}}:</span>
+                                       <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}}
                        </span>
                        {{svg "octicon-triangle-down" 14 "dropdown icon"}}
                </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}}
                                {{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}}
-                       {{range .root.Tags}}
-                               {{if $release}}
-                                       <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>
-                               {{else}}
-                                       <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>
+                       {{if (not .noTag)}}
+                               {{range .root.Tags}}
+                                       {{if $release}}
+                                               <div class="item tag {{if eq $release.TagName .}}selected{{end}}" data-url="{{PathEscapeSegments .}}">{{.}}</div>
+                                       {{else}}
+                                               <div class="item tag {{if eq $defaultBranch .}}selected{{end}}" data-url="{{PathEscapeSegments .}}">{{.}}</div>
+                                       {{end}}
                                {{end}}
                        {{end}}
                </div>
                <div class="menu transition" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
                        <div class="ui icon search input">
                                <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>
                        {{if $showBranchesInDropdown}}
                                <div class="header branch-tag-choice">
                                                                        {{svg "octicon-git-branch" 16 "mr-2"}}{{.root.i18n.Tr "repo.branches"}}
                                                                </span>
                                                        </a>
-                                                       <a class="reference column" href="#" @click="createTag = true; mode = 'tags'; focusSearchField()">
-                                                               <span class="text" :class="{black: mode == 'tags'}">
-                                                                       {{svg "octicon-tag" 16 "mr-2"}}{{.root.i18n.Tr "repo.tags"}}
-                                                               </span>
-                                                       </a>
+                                                       {{if not .noTag}}
+                                                               <a class="reference column" href="#" @click="createTag = true; mode = 'tags'; focusSearchField()">
+                                                                       <span class="text" :class="{black: mode == 'tags'}">
+                                                                               {{svg "octicon-tag" 16 "mr-2"}}{{.root.i18n.Tr "repo.tags"}}
+                                                                       </span>
+                                                               </a>
+                                                       {{end}}
                                                </div>
                                        </div>
                                </div>
index 331c439c0218fe8d2720eb449a51162139ca4eaa..370cafa2e194c6665934f9c76e7b9f3a5c1d0fd9 100644 (file)
                        {{end}}
                {{end}}
                <div class="ui top attached header clearing segment pr {{$class}}">
-                       {{if not $.PageIsWiki}}
-                       <a class="ui blue tiny button browse-button" href="{{.SourcePath}}">
-                               {{.i18n.Tr "repo.diff.browse_source"}}
-                       </a>
-                       {{end}}
-                       <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>
+                       <div class="df mb-4">
+                               <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>
+                               {{if not $.PageIsWiki}}
+                                       <div class="ui">
+                                               <a class="ui blue tiny button" href="{{.SourcePath}}">
+                                                       {{.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}}
-                               <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}}
                        {{if .BranchName}}
                                <span class="text grey mr-3">{{svg "octicon-git-branch" 16 "mr-2"}}{{.BranchName}}</span>
diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl
new file mode 100644 (file)
index 0000000..9cdda2e
--- /dev/null
@@ -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" .}}
diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl
new file mode 100644 (file)
index 0000000..cecda90
--- /dev/null
@@ -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" .}}
index 6d525c24da46f5f982e6daae232c96862a984185..30f1471c16d5c1a7e9d8f2c2b1cfec5ce19f588b 100644 (file)
                                                                {{.i18n.Tr "repo.editor.upload_file"}}
                                                        </a>
                                                {{end}}
+                                               {{if .CanAddFile}}
+                                                       <a href="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}" class="ui button">
+                                                               {{.i18n.Tr "repo.editor.patch"}}
+                                                       </a>
+                                               {{end}}
                                        {{end}}
                                        {{if and (ne $n 0) (not .IsViewFile) (not .IsBlame) }}
                                                <a href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}" class="ui button">
index 497636e78140a9bd7401c3f1d93b2cdb7dbe5874..0b0b83ebbc16f7f55098253778e002baae07c8f1 100644 (file)
         }
       }
     },
+    "/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}": {
       "get": {
         "produces": [
index e72563ff29a5474500c8cca436262d45ae13e9b0..d55fa91b924275d6b8cf3e47cf6a2417551f55bf 100644 (file)
@@ -10,11 +10,22 @@ export function initRepoBranchTagDropdown(selector) {
       items: [],
       mode: $data.data('mode'),
       searchTerm: '',
+      refName: '',
       noResults: '',
       canCreateBranch: false,
       menuVisible: 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.items.push({
@@ -64,6 +75,26 @@ export function initRepoBranchTagDropdown(selector) {
       beforeMount() {
         this.noResults = this.$el.getAttribute('data-no-results');
         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) => {
           if (this.$el.contains(event.target)) return;
@@ -80,7 +111,32 @@ export function initRepoBranchTagDropdown(selector) {
             prev.selected = false;
           }
           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() {
           if (!this.showCreateNewBranch) return;
index 84b67a68e1d03b1a2925bcfb9c4131285039398a..45bb96c26e2d38fed2a5e4008d5b266046c8dd5d 100644 (file)
@@ -313,9 +313,22 @@ export function initGlobalButtons() {
     alert('Nothing to hide');
   });
 
-  $('.show-modal.button').on('click', function () {
-    $($(this).data('modal')).modal('show');
-    const colorPickers = $($(this).data('modal')).find('.color-picker');
+  $('.show-modal').on('click', function () {
+    const modalDiv = $($(this).attr('data-modal'));
+    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) {
       initCompColorPicker();
     }
@@ -323,10 +336,10 @@ export function initGlobalButtons() {
 
   $('.delete-post.button').on('click', function () {
     const $this = $(this);
-    $.post($this.data('request-url'), {
+    $.post($this.attr('data-request-url'), {
       _csrf: csrfToken
     }).done(() => {
-      window.location.href = $this.data('done-url');
+      window.location.href = $this.attr('data-done-url');
     });
   });
 }
index bc713a73970d8a80ecd03053727a8c24b2b435d6..946f7f90a447a759cce78d9e68a81155a9206716 100644 (file)
@@ -1,9 +1,18 @@
 import $ from 'jquery';
 
 export function initRepoBranchButton() {
-  $('.show-create-branch-modal.button').on('click', function () {
-    $('#create-branch-form')[0].action = $('#create-branch-form').data('base-action') + $(this).data('branch-from-urlcomponent');
-    $('#modal-create-branch-from-span').text($(this).data('branch-from'));
-    $($(this).data('modal')).modal('show');
+  $('.show-create-branch-modal').on('click', function () {
+    let modalFormName = $(this).attr('data-modal-form');
+    if (!modalFormName) {
+      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');
   });
 }
index d92a0539d0bcb4003e6e9f13b9af0961722b6576..d51dfe185c54712df2b9074f6f2920e95d9b8e79 100644 (file)
@@ -436,7 +436,7 @@ export function initRepository() {
   });
 
   // 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) {
     initRepoBranchTagDropdown('.choose.reference .dropdown');
   }
index 6cf70abdf77232ce2218d92316d2a847999e4f7d..8b912ce90d861d351d219c8b1128115bc8cce2eb 100644 (file)
   padding-top: 15px;
 }
 
-.browse-button {
-  position: absolute;
-  right: 1rem;
-  top: .75rem;
-}
-
 .commit-header-row {
   min-height: 50px !important;
   padding-top: 0 !important;