mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 09:31:53 +01:00 
			
		
		
		
	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) | ||||
| } | ||||
| 
 | ||||
| // 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 | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -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"` | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
							
								
								
									
										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). | ||||
| 					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() { | ||||
|  | ||||
| @ -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) | ||||
| } | ||||
| 
 | ||||
| //  ____ ___        .__                    .___ | ||||
| // |    |   \______ |  |   _________     __| _/ | ||||
| // |    |   /\____ \|  |  /  _ \__  \   / __ | | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
							
								
								
									
										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}} | ||||
| {{$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"> | ||||
| @ -42,11 +56,13 @@ | ||||
| 									{{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> | ||||
|  | ||||
| @ -18,14 +18,123 @@ | ||||
| 			{{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> | ||||
|  | ||||
							
								
								
									
										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"}} | ||||
| 							</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"> | ||||
|  | ||||
| @ -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}": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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'); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| @ -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'); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| @ -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'); | ||||
|   } | ||||
|  | ||||
| @ -2551,12 +2551,6 @@ | ||||
|   padding-top: 15px; | ||||
| } | ||||
| 
 | ||||
| .browse-button { | ||||
|   position: absolute; | ||||
|   right: 1rem; | ||||
|   top: .75rem; | ||||
| } | ||||
| 
 | ||||
| .commit-header-row { | ||||
|   min-height: 50px !important; | ||||
|   padding-top: 0 !important; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user