Change/remove a branch of an open issue (#9080)
* Add field with isIssueWriter to front end * Make branch field editable * Switch frontend to form and POST from javascript * Add /issue/id/ref endpoint to routes * Use UpdateIssueTitle model to change ref in backend * Removed crossreference check and adding comments on branch change * Use ref returned from POST to update the field * Prevent calling loadRepo from models/ * Branch/tag refreshed without page reload * Remove filter for empty branch name * Add clear option to tag list as well * Delete button translation and coloring * Fix for not showing selected branch name in new issue * Check that branch is not being changed on a PR * Change logic * Notification when changing issue ref * Fix for renamed permission parameter * Fix for failing build * Apply suggestions from code review Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Gitea <gitea@fake.local> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
		
							parent
							
								
									0ed8d268ad
								
							
						
					
					
						commit
						e204398754
					
				
					 11 changed files with 107 additions and 9 deletions
				
			
		|  | @ -709,6 +709,22 @@ func (issue *Issue) ChangeTitle(doer *User, oldTitle string) (err error) { | ||||||
| 	return sess.Commit() | 	return sess.Commit() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // ChangeRef changes the branch of this issue, as the given user.
 | ||||||
|  | func (issue *Issue) ChangeRef(doer *User, oldRef string) (err error) { | ||||||
|  | 	sess := x.NewSession() | ||||||
|  | 	defer sess.Close() | ||||||
|  | 
 | ||||||
|  | 	if err = sess.Begin(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err = updateIssueCols(sess, issue, "ref"); err != nil { | ||||||
|  | 		return fmt.Errorf("updateIssueCols: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return sess.Commit() | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // AddDeletePRBranchComment adds delete branch comment for pull request issue
 | // AddDeletePRBranchComment adds delete branch comment for pull request issue
 | ||||||
| func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branchName string) error { | func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branchName string) error { | ||||||
| 	issue, err := getIssueByID(x, issueID) | 	issue, err := getIssueByID(x, issueID) | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ type Notifier interface { | ||||||
| 	NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) | 	NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) | ||||||
| 	NotifyIssueClearLabels(doer *models.User, issue *models.Issue) | 	NotifyIssueClearLabels(doer *models.User, issue *models.Issue) | ||||||
| 	NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) | 	NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) | ||||||
|  | 	NotifyIssueChangeRef(doer *models.User, issue *models.Issue, oldRef string) | ||||||
| 	NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, | 	NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, | ||||||
| 		addedLabels []*models.Label, removedLabels []*models.Label) | 		addedLabels []*models.Label, removedLabels []*models.Label) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -102,6 +102,10 @@ func (*NullNotifier) NotifyIssueClearLabels(doer *models.User, issue *models.Iss | ||||||
| func (*NullNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) { | func (*NullNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // NotifyIssueChangeRef places a place holder function
 | ||||||
|  | func (*NullNotifier) NotifyIssueChangeRef(doer *models.User, issue *models.Issue, oldTitle string) { | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // NotifyIssueChangeLabels places a place holder function
 | // NotifyIssueChangeLabels places a place holder function
 | ||||||
| func (*NullNotifier) NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, | func (*NullNotifier) NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, | ||||||
| 	addedLabels []*models.Label, removedLabels []*models.Label) { | 	addedLabels []*models.Label, removedLabels []*models.Label) { | ||||||
|  |  | ||||||
|  | @ -148,3 +148,7 @@ func (r *indexerNotifier) NotifyIssueChangeContent(doer *models.User, issue *mod | ||||||
| func (r *indexerNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) { | func (r *indexerNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) { | ||||||
| 	issue_indexer.UpdateIssueIndexer(issue) | 	issue_indexer.UpdateIssueIndexer(issue) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (r *indexerNotifier) NotifyIssueChangeRef(doer *models.User, issue *models.Issue, oldRef string) { | ||||||
|  | 	issue_indexer.UpdateIssueIndexer(issue) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -178,6 +178,13 @@ func NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle str | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // NotifyIssueChangeRef notifies change reference to notifiers
 | ||||||
|  | func NotifyIssueChangeRef(doer *models.User, issue *models.Issue, oldRef string) { | ||||||
|  | 	for _, notifier := range notifiers { | ||||||
|  | 		notifier.NotifyIssueChangeRef(doer, issue, oldRef) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // NotifyIssueChangeLabels notifies change labels to notifiers
 | // NotifyIssueChangeLabels notifies change labels to notifiers
 | ||||||
| func NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, | func NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, | ||||||
| 	addedLabels []*models.Label, removedLabels []*models.Label) { | 	addedLabels []*models.Label, removedLabels []*models.Label) { | ||||||
|  |  | ||||||
|  | @ -760,6 +760,7 @@ code = Code | ||||||
| code.desc = Access source code, files, commits and branches. | code.desc = Access source code, files, commits and branches. | ||||||
| branch = Branch | branch = Branch | ||||||
| tree = Tree | tree = Tree | ||||||
|  | clear_ref = `Clear current reference` | ||||||
| filter_branch_and_tag = Filter branch or tag | filter_branch_and_tag = Filter branch or tag | ||||||
| branches = Branches | branches = Branches | ||||||
| tags = Tags | tags = Tags | ||||||
|  |  | ||||||
|  | @ -1244,7 +1244,7 @@ func ViewIssue(ctx *context.Context) { | ||||||
| 	ctx.Data["Participants"] = participants | 	ctx.Data["Participants"] = participants | ||||||
| 	ctx.Data["NumParticipants"] = len(participants) | 	ctx.Data["NumParticipants"] = len(participants) | ||||||
| 	ctx.Data["Issue"] = issue | 	ctx.Data["Issue"] = issue | ||||||
| 	ctx.Data["ReadOnly"] = true | 	ctx.Data["ReadOnly"] = false | ||||||
| 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string) | 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string) | ||||||
| 	ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID) | 	ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID) | ||||||
| 	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) | 	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) | ||||||
|  | @ -1344,6 +1344,30 @@ func UpdateIssueTitle(ctx *context.Context) { | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // UpdateIssueRef change issue's ref (branch)
 | ||||||
|  | func UpdateIssueRef(ctx *context.Context) { | ||||||
|  | 	issue := GetActionIssue(ctx) | ||||||
|  | 	if ctx.Written() { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull { | ||||||
|  | 		ctx.Error(403) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ref := ctx.QueryTrim("ref") | ||||||
|  | 
 | ||||||
|  | 	if err := issue_service.ChangeIssueRef(issue, ctx.User, ref); err != nil { | ||||||
|  | 		ctx.ServerError("ChangeRef", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx.JSON(200, map[string]interface{}{ | ||||||
|  | 		"ref": ref, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // UpdateIssueContent change issue's content
 | // UpdateIssueContent change issue's content
 | ||||||
| func UpdateIssueContent(ctx *context.Context) { | func UpdateIssueContent(ctx *context.Context) { | ||||||
| 	issue := GetActionIssue(ctx) | 	issue := GetActionIssue(ctx) | ||||||
|  |  | ||||||
|  | @ -733,6 +733,7 @@ func RegisterRoutes(m *macaron.Macaron) { | ||||||
| 				m.Post("/title", repo.UpdateIssueTitle) | 				m.Post("/title", repo.UpdateIssueTitle) | ||||||
| 				m.Post("/content", repo.UpdateIssueContent) | 				m.Post("/content", repo.UpdateIssueContent) | ||||||
| 				m.Post("/watch", repo.IssueWatch) | 				m.Post("/watch", repo.IssueWatch) | ||||||
|  | 				m.Post("/ref", repo.UpdateIssueRef) | ||||||
| 				m.Group("/dependency", func() { | 				m.Group("/dependency", func() { | ||||||
| 					m.Post("/add", repo.AddDependency) | 					m.Post("/add", repo.AddDependency) | ||||||
| 					m.Post("/delete", repo.RemoveDependency) | 					m.Post("/delete", repo.RemoveDependency) | ||||||
|  |  | ||||||
|  | @ -42,6 +42,20 @@ func ChangeTitle(issue *models.Issue, doer *models.User, title string) (err erro | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // ChangeIssueRef changes the branch of this issue, as the given user.
 | ||||||
|  | func ChangeIssueRef(issue *models.Issue, doer *models.User, ref string) error { | ||||||
|  | 	oldRef := issue.Ref | ||||||
|  | 	issue.Ref = ref | ||||||
|  | 
 | ||||||
|  | 	if err := issue.ChangeRef(doer, oldRef); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	notification.NotifyIssueChangeRef(doer, issue, oldRef) | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s)
 | // UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s)
 | ||||||
| // Deleting is done the GitHub way (quote from their api documentation):
 | // Deleting is done the GitHub way (quote from their api documentation):
 | ||||||
| // https://developer.github.com/v3/issues/#edit-an-issue
 | // https://developer.github.com/v3/issues/#edit-an-issue
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,10 @@ | ||||||
| {{if and (not .Issue.IsPull) (not .PageIsComparePull)}} | {{if and (not .Issue.IsPull) (not .PageIsComparePull)}} | ||||||
| <input id="ref_selector" name="ref" type="hidden" value="{{.Issue.Ref}}"> | <input id="ref_selector" name="ref" type="hidden" value="{{.Issue.Ref}}"> | ||||||
|  | <input id="editing_mode" name="edit_mode" type="hidden" value="{{(or .IsIssueWriter .HasIssuesOrPullsWritePermission)}}"> | ||||||
|  | <form method="POST" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form"> | ||||||
|  | 	{{$.CsrfTokenHtml}} | ||||||
|  | </form> | ||||||
|  | 
 | ||||||
| <div class="ui {{if .ReadOnly}}disabled{{end}} floating filter select-branch dropdown" data-no-results="{{.i18n.Tr "repo.pulls.no_results"}}"> | <div class="ui {{if .ReadOnly}}disabled{{end}} floating filter select-branch dropdown" data-no-results="{{.i18n.Tr "repo.pulls.no_results"}}"> | ||||||
| 	<div class="ui basic small button"> | 	<div class="ui basic small button"> | ||||||
| 		<span class="text branch-name">{{if .Issue.Ref}}{{$.RefEndName}}{{else}}{{.i18n.Tr "repo.issues.no_ref"}}{{end}}</span> | 		<span class="text branch-name">{{if .Issue.Ref}}{{$.RefEndName}}{{else}}{{.i18n.Tr "repo.issues.no_ref"}}{{end}}</span> | ||||||
|  | @ -27,14 +32,20 @@ | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div id="branch-list" class="scrolling menu reference-list-menu"> | 		<div id="branch-list" class="scrolling menu reference-list-menu"> | ||||||
| 		{{range .Branches}} | 			{{if .Issue.Ref}} | ||||||
| 			<div class="item" data-id="refs/heads/{{.}}" data-name="{{.}}" data-id-selector="#ref_selector">{{.}}</div> | 				<div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{$.i18n.Tr "repo.clear_ref"}}</a></strong></div> | ||||||
| 		{{end}} | 			{{end}}	 | ||||||
|  | 			{{range .Branches}} | ||||||
|  | 				<div class="item" data-id="refs/heads/{{.}}" data-name="{{.}}" data-id-selector="#ref_selector">{{.}}</div> | ||||||
|  | 			{{end}} | ||||||
| 		</div> | 		</div> | ||||||
| 		<div id="tag-list" class="scrolling menu reference-list-menu" style="display: none"> | 		<div id="tag-list" class="scrolling menu reference-list-menu" style="display: none"> | ||||||
| 		{{range .Tags}} | 			{{if .Issue.Ref}} | ||||||
| 			<div class="item" data-id="refs/tags/{{.}}" data-name="tags/{{.}}" data-id-selector="#ref_selector">{{.}}</div> | 				<div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{.i18n.Tr "repo.clear_ref"}}</a></strong></div> | ||||||
| 		{{end}} | 			{{end}}	 | ||||||
|  | 			{{range .Tags}} | ||||||
|  | 				<div class="item" data-id="refs/tags/{{.}}" data-name="tags/{{.}}" data-id-selector="#ref_selector">{{.}}</div> | ||||||
|  | 			{{end}} | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | @ -112,8 +112,23 @@ function initBranchSelector() { | ||||||
|   const $selectBranch = $('.ui.select-branch'); |   const $selectBranch = $('.ui.select-branch'); | ||||||
|   const $branchMenu = $selectBranch.find('.reference-list-menu'); |   const $branchMenu = $selectBranch.find('.reference-list-menu'); | ||||||
|   $branchMenu.find('.item:not(.no-select)').click(function () { |   $branchMenu.find('.item:not(.no-select)').click(function () { | ||||||
|     $($(this).data('id-selector')).val($(this).data('id')); |     const selectedValue = $(this).data('id'); | ||||||
|     $selectBranch.find('.ui .branch-name').text($(this).data('name')); |     const editMode = $('#editing_mode').val(); | ||||||
|  |     $($(this).data('id-selector')).val(selectedValue); | ||||||
|  | 
 | ||||||
|  |     if (editMode === 'true') { | ||||||
|  |       const form = $('#update_issueref_form'); | ||||||
|  | 
 | ||||||
|  |       $.post(form.attr('action'), { | ||||||
|  |         _csrf: csrf, | ||||||
|  |         ref: selectedValue | ||||||
|  |       }, | ||||||
|  |       () => { | ||||||
|  |         window.location.reload(); | ||||||
|  |       }); | ||||||
|  |     } else if (editMode === '') { | ||||||
|  |       $selectBranch.find('.ui .branch-name').text(selectedValue); | ||||||
|  |     } | ||||||
|   }); |   }); | ||||||
|   $selectBranch.find('.reference.column').on('click', function () { |   $selectBranch.find('.reference.column').on('click', function () { | ||||||
|     $selectBranch.find('.scrolling.reference-list-menu').css('display', 'none'); |     $selectBranch.find('.scrolling.reference-list-menu').css('display', 'none'); | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue