in base repo is consistent with the head commit of head branch in the head repo
+ // get head commit of PR
+ prHeadRef := pull.GetGitRefName()
+ if err := pull.LoadBaseRepo(ctx); err != nil {
+ ctx.ServerError("Unable to load base repo", err)
+ return
+ }
+ prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef)
+ if err != nil {
+ ctx.ServerError("Get head commit Id of pr fail", err)
+ return
+ }
+
+ // get head commit of branch in the head repo
+ if err := pull.LoadHeadRepo(ctx); err != nil {
+ ctx.ServerError("Unable to load head repo", err)
+ return
+ }
+ if ok := git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.BaseBranch); !ok {
+ // todo localize
+ ctx.JSONError("The origin branch is delete, cannot reopen.")
+ return
+ }
+ headBranchRef := pull.GetGitHeadBranchRefName()
+ headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef)
+ if err != nil {
+ ctx.ServerError("Get head commit Id of head branch fail", err)
+ return
+ }
- log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
+ err = pull.LoadIssue(ctx)
+ if err != nil {
+ ctx.ServerError("load the issue of pull request error", err)
+ return
+ }
+
+ if prHeadCommitID != headBranchCommitID {
+ // force push to base repo
+ err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
+ Remote: pull.BaseRepo.RepoPath(),
+ Branch: pull.HeadBranch + ":" + prHeadRef,
+ Force: true,
+ Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
+ })
+ if err != nil {
+ ctx.ServerError("force push error", err)
+ return
}
}
}
- // Redirect to comment hashtag if there is any actual content.
- typeName := "issues"
- if issue.IsPull {
- typeName = "pulls"
- }
- if comment != nil {
- ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
+ if pr != nil {
+ ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
} else {
- ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
+ issue.IsClosed = form.Status == "close"
+ issue.ClosedStatus = issues_model.IssueClosedStatus(0)
+ if issue.IsClosed {
+ issue.ClosedStatus = form.ClosedStatus
+ }
+ if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, ""); err != nil {
+ log.Error("ChangeStatus: %v", err)
+
+ if issues_model.IsErrDependenciesLeft(err) {
+ if issue.IsPull {
+ ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
+ } else {
+ ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
+ }
+ return
+ }
+ } else {
+ if err := stopTimerIfAvailable(ctx.Doer, issue); err != nil {
+ ctx.ServerError("CreateOrStopIssueStopwatch", err)
+ return
+ }
+
+ log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
+ }
}
- }()
- // Fix #321: Allow empty comments, as long as we have attachments.
- if len(form.Content) == 0 && len(attachments) == 0 {
- return
}
- comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
- if err != nil {
- ctx.ServerError("CreateIssueComment", err)
- return
+ // Redirect to comment hashtag if there is any actual content.
+ typeName := "issues"
+ if issue.IsPull {
+ typeName = "pulls"
+ }
+ if comment != nil {
+ ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
+ } else {
+ ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
}
-
- log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
}
// UpdateCommentContent change comment of issue's content
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index a26a2d89c5d65..62f605818dc80 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -455,9 +455,10 @@ func (f *CreateIssueForm) Validate(req *http.Request, errs binding.Errors) bindi
// CreateCommentForm form for creating comment
type CreateCommentForm struct {
- Content string
- Status string `binding:"OmitEmpty;In(reopen,close)"`
- Files []string
+ Content string
+ Status string `binding:"OmitEmpty;In(reopen,close)"`
+ Files []string
+ ClosedStatus issues_model.IssueClosedStatus
}
// Validate validates the fields
diff --git a/services/issue/commit.go b/services/issue/commit.go
index e493a032114af..322f4ec6c31d5 100644
--- a/services/issue/commit.go
+++ b/services/issue/commit.go
@@ -193,7 +193,8 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
}
if close != refIssue.IsClosed {
refIssue.Repo = refRepo
- if err := ChangeStatus(ctx, refIssue, doer, c.Sha1, close); err != nil {
+ refIssue.IsClosed = close
+ if err := ChangeStatus(ctx, refIssue, doer, c.Sha1); err != nil {
return err
}
}
diff --git a/services/issue/status.go b/services/issue/status.go
index 3718a5048f93f..15bc19370b263 100644
--- a/services/issue/status.go
+++ b/services/issue/status.go
@@ -13,10 +13,10 @@ import (
)
// ChangeStatus changes issue status to open or closed.
-func ChangeStatus(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string, closed bool) error {
- comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer, closed)
+func ChangeStatus(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string) error {
+ comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer)
if err != nil {
- if issues_model.IsErrDependenciesLeft(err) && closed {
+ if issues_model.IsErrDependenciesLeft(err) && issue.IsClosed {
if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {
log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err)
}
@@ -24,13 +24,13 @@ func ChangeStatus(ctx context.Context, issue *issues_model.Issue, doer *user_mod
return err
}
- if closed {
+ if issue.IsClosed {
if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {
return err
}
}
- notification.NotifyIssueChangeStatus(ctx, doer, commitID, issue, comment, closed)
+ notification.NotifyIssueChangeStatus(ctx, doer, commitID, issue, comment, issue.IsClosed)
return nil
}
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 7051fd9eda24d..a22c114cfee6c 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -230,7 +230,8 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
}
close := ref.RefAction == references.XRefActionCloses
if close != ref.Issue.IsClosed {
- if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID, close); err != nil {
+ ref.Issue.IsClosed = close
+ if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID); err != nil {
// Allow ErrDependenciesLeft
if !issues_model.IsErrDependenciesLeft(err) {
return err
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 0b6194b1436d8..e9022488175a0 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -562,7 +562,8 @@ func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64,
var errs errlist
for _, pr := range prs {
- if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
+ pr.Issue.IsClosed = true
+ if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
errs = append(errs, err)
}
}
@@ -596,7 +597,8 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
if pr.BaseRepoID == repo.ID {
continue
}
- if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) {
+ pr.Issue.IsClosed = true
+ if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrPullWasClosed(err) {
errs = append(errs, err)
}
}
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
index 422a4f0461108..0974fcbbd044a 100644
--- a/templates/mail/issue/default.tmpl
+++ b/templates/mail/issue/default.tmpl
@@ -36,7 +36,15 @@
{{end}}
{{if eq .ActionName "close"}}
- {{.locale.Tr "mail.issue.action.close" (Escape .Doer.Name) .Issue.Index | Str2html}}
+ {{$closeTrans := "mail.issue.action.close"}}
+ {{if eq .Issue.ClosedStatus 1}}
+ {{$closeTrans = "mail.issue.action.close_as_archived"}}
+ {{else if eq .Issue.ClosedStatus 2}}
+ {{$closeTrans = "mail.issue.action.close_as_resolved"}}
+ {{else if eq .Issue.ClosedStatus 3}}
+ {{$closeTrans = "mail.issue.action.close_as_stale"}}
+ {{end}}
+ {{.locale.Tr $closeTrans (Escape .Doer.Name) .Issue.Index | Str2html}}
{{else if eq .ActionName "reopen"}}
{{.locale.Tr "mail.issue.action.reopen" (Escape .Doer.Name) .Issue.Index | Str2html}}
{{else if eq .ActionName "merge"}}
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index f6572d49653a4..51d325a2bf624 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -102,21 +102,42 @@
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 8cf5332bafc48..8388605082ce1 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -17954,6 +17954,10 @@
"type": "string",
"x-go-name": "Body"
},
+ "closed_status": {
+ "type": "string",
+ "x-go-name": "ClosedStatus"
+ },
"due_date": {
"type": "string",
"format": "date-time",
diff --git a/web_src/css/base.css b/web_src/css/base.css
index d44f949318a73..23488760cd587 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1302,10 +1302,6 @@ img.ui.avatar,
margin-right: 6px;
}
-.ui.status.buttons .svg {
- margin-right: 4px;
-}
-
.ui.inline.delete-button {
padding: 8px 15px;
font-weight: var(--font-weight-normal);
diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css
index 373b815d5c936..aca59455301d0 100644
--- a/web_src/css/modules/button.css
+++ b/web_src/css/modules/button.css
@@ -54,6 +54,10 @@ a.btn:hover {
/* other button styles */
+.ui.buttons {
+ vertical-align: middle;
+}
+
.ui.buttons .button:first-child {
border-left: 1px solid var(--color-light-border);
}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 34fa2a0052fc7..33b90f7788c07 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -983,16 +983,6 @@
border-radius: var(--border-radius);
}
-@media (max-width: 767.98px) {
- .repository.view.issue .comment-list .comment .content .form .button {
- width: 100%;
- margin: 0;
- }
- .repository.view.issue .comment-list .comment .content .form .button:not(:last-child) {
- margin-bottom: 1rem;
- }
-}
-
.repository.view.issue .comment-list .comment .merge-section {
background-color: var(--color-box-body);
}
@@ -1062,10 +1052,6 @@
clear: none;
}
-.repository.view.issue .comment-list .comment .ui.form .field.footer {
- overflow: hidden;
-}
-
.repository.view.issue .comment-list .comment .ui.form .field .tab.markup {
min-height: 5rem;
}
@@ -1074,11 +1060,6 @@
margin-top: 10px;
}
-.repository.view.issue .comment-list .code-comment {
- border: 1px solid transparent;
- margin: 0;
-}
-
/* fix fomantic's border-radius via :first-child with hidden elements */
.collapsible-comment-box:has(.gt-hidden) {
border-radius: var(--border-radius) !important;
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 194ffca57c4a5..f55268f5aacd9 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -633,15 +633,42 @@ export function initRepoIssueBranchSelect() {
export function initSingleCommentEditor($commentForm) {
// pages:
// * normal new issue/pr page, no status-button
- // * issue/pr view page, with comment form, has status-button
- const opts = {};
- const $statusButton = $('#status-button');
- if ($statusButton.length) {
- opts.onContentChanged = (editor) => {
- $statusButton.text($statusButton.attr(editor.value().trim() ? 'data-status-and-comment' : 'data-status'));
- };
+ // * issue/pr view page, with comment form, has status button, and/or status dropdown
+ const $statusButton = $commentForm.find('.status-button');
+ const $statusDropdown = $commentForm.find('.status-dropdown');
+ let comboMarkdownEditor;
+
+ // update the status-button's text according to the editor's content
+ const updateStatusButtonText = () => {
+ if (!comboMarkdownEditor || !$statusButton.length) return;
+ $statusButton.text($statusButton.attr(comboMarkdownEditor.value().trim() ? 'data-status-and-comment' : 'data-status'));
+ };
+
+ // update the status-button's text and value according to the selected dropdown's value (close status)
+ const updateStatusButtonByCloseStatus = (val) => {
+ if (!$statusButton.length) return;
+ const $item = $statusDropdown.dropdown('get item');
+ $statusButton.attr('data-status', $item.attr('data-status'));
+ $statusButton.attr('data-status-and-comment', $item.attr('data-status-and-comment'));
+ $statusButton.value = val === '-1' ? 'reopen' : 'close';
+ updateStatusButtonText();
+ };
+
+ if ($statusDropdown.length) {
+ $statusDropdown.dropdown('setting', {
+ selectOnKeydown: false,
+ allowReselection: true,
+ onChange: updateStatusButtonByCloseStatus,
+ });
+ const selectedValue = $statusDropdown.find('input[type=hidden]').val();
+ $statusDropdown.dropdown('set selected', selectedValue);
+ updateStatusButtonByCloseStatus(selectedValue);
}
- initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts);
+
+ const editorOpts = {onContentChanged: updateStatusButtonText};
+ initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), editorOpts).then((editor) => {
+ comboMarkdownEditor = editor;
+ });
}
export function initIssueTemplateCommentEditors($commentForm) {