diff options
51 files changed, 949 insertions, 44 deletions
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt index 5645f5137f..a496a657eb 100644 --- a/Documentation/cmd-review.txt +++ b/Documentation/cmd-review.txt @@ -8,8 +8,8 @@ gerrit review - Verify, approve and/or submit one or more patch sets SYNOPSIS -------- [verse] -'ssh' -p <port> <host> 'gerrit approve' [\--project <PROJECT>] [\--message <MESSAGE>] [\--verified <N>] [\--code-review <N>] [\--abandon] [\--restore] [\--submit] {COMMIT | CHANGEID,PATCHSET}... -'ssh' -p <port> <host> 'gerrit review' [\--project <PROJECT>] [\--message <MESSAGE>] [\--verified <N>] [\--code-review <N>] [\--abandon] [\--restore] [\--submit] {COMMIT | CHANGEID,PATCHSET}... +'ssh' -p <port> <host> 'gerrit approve' [\--project <PROJECT>] [\--message <MESSAGE>] [\--verified <N>] [\--code-review <N>] [\--defer] [\--abandon] [\--restore] [\--submit] {COMMIT | CHANGEID,PATCHSET}... +'ssh' -p <port> <host> 'gerrit review' [\--project <PROJECT>] [\--message <MESSAGE>] [\--verified <N>] [\--code-review <N>] [\--defer] [\--abandon] [\--restore] [\--submit] {COMMIT | CHANGEID,PATCHSET}... DESCRIPTION ----------- @@ -54,18 +54,24 @@ OPTIONS differs per site, check the output of \--help, or contact your site administrator for further details. +\--defer:: + Defer the specified patch set(s). + (option is mutually exclusive with --abandon, --submit and + --restore) + \--abandon:: Abandon the specified patch set(s). - (option is mutually exclusive with --submit and --restore) + (option is mutually exclusive with --defer, --submit and + --restore) \--restore:: - Restore the specified abandonned patch set(s). - (option is mutually exclusive with --abandon) + Restore the specified deferred or abandonned patch set(s). + (option is mutually exclusive with --defer and --abandon) \--submit:: -s:: Submit the specified patch set(s) for merging. - (option is mutually exclusive with --abandon) + (option is mutually exclusive with --defer and --abandon) ACCESS ------ diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt index fb54f67edb..b467b6789c 100644 --- a/Documentation/cmd-stream-events.txt +++ b/Documentation/cmd-stream-events.txt @@ -43,7 +43,8 @@ SCHEMA The JSON messages consist of nested objects referencing the *change*, *patchset*, *account* involved, and other attributes as appropriate. The currently supported message types are *patchset-created*, -*comment-added*, *change-merged*, and *change-abandoned*. +*comment-added*, *change-merged*, *change-deferred* and +*change-abandoned*. Note that any field may be missing in the JSON messages, so consumers of this JSON stream should deal with that appropriately. @@ -60,6 +61,16 @@ patchset:: link:json.html#patchset[patchset attribute] uploader:: link:json.html#account[account attribute] +Change Deferred +^^^^^^^^^^^^^^^ +type:: "change-deferred" + +change:: link:json.html#change[change attribute] + +patchset:: link:json.html#patchset[patchset attribute] + +deferrer:: link:json.html#account[account attribute] + Change Abandoned ^^^^^^^^^^^^^^^^ type:: "change-abandoned" diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index cec2783976..85d243d42f 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -1013,6 +1013,11 @@ Optional filename for the change merged hook, if not specified then Optional filename for the change abandoned hook, if not specified then `change-abandoned` will be used. +[[hooks.changeDeferredHook]]hooks.changeDeferredHook:: ++ +Optional filename for the change deferred hook, if not specified then +`change-deferred` will be used. + [[http]]Section http ~~~~~~~~~~~~~~~~~~~~ diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt index fd2ae82b93..1a2db1d7e7 100644 --- a/Documentation/config-hooks.txt +++ b/Documentation/config-hooks.txt @@ -57,6 +57,15 @@ Called whenever a change has been abandoned. change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --abandoner <abandoner> --reason <reason> ==== +change-deferred +~~~~~~~~~~~~~~~~ + +Called whenever a change has been deferred. + +==== + change-deferred --change <change id> --change-url <change url> --project <project name> --branch <branch> --deferrer <deferrer> --reason <reason> +==== + change-restored ~~~~~~~~~~~~~~~~ @@ -84,8 +93,8 @@ filenames it looks for by adding a [hooks] section to gerrit.config. Gerrit will use the value of hooks.path for the hooks directory, and the values of hooks.patchsetCreatedHook, hooks.commentAddedHook, -hooks.changeMergedHook and hooks.changeAbandonedHook for the -filenames for the hooks. +hooks.changeMergedHook, hooks.changeAbandonedHook and +hooks.changeDeferredHook for the filenames for the hooks. Missing Change URLs ------------------- diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt index 168bbfe169..2ff59ad446 100644 --- a/Documentation/config-mail.txt +++ b/Documentation/config-mail.txt @@ -50,6 +50,13 @@ Comment.vm The `Comment.vm` template will determine the contents of the email related to a user submitting comments on changes. It is a `ChangeEmail`: see +Deferred.vm +~~~~~~~~~~~~ + +The `Deferred.vm` template will determine the contents of the email related +to a change being deferred. It is a `ChangeEmail`: see `ChangeSubject.vm` and +`ChangeFooter.vm`. + Merged.vm ~~~~~~~~~ diff --git a/Documentation/error-change-closed.txt b/Documentation/error-change-closed.txt index 7170a65917..97e77084a2 100644 --- a/Documentation/error-change-closed.txt +++ b/Documentation/error-change-closed.txt @@ -7,7 +7,7 @@ that is already closed. This error occurs if you are trying to push a commit that contains the Change-Id of a closed change in its commit message. A change can be closed either because it was already submitted and merged or -because it was abandoned. +because it was deferred or abandoned. If the change for which you wanted to upload a new patch set was already submitted and merged you may want to push your commit as a @@ -18,11 +18,12 @@ recommendable to do a link:http://www.kernel.org/pub/software/scm/git/docs/git-r change. Pushing again should now create a new change in Gerrit. If the change for which you wanted to upload a new patch set was -abandoned and your new changes overcome the reasons for abandoning -this change you may want to restore the change in the Gerrit WebUI -(browse the abandoned change in the Gerrit WebUI and click on the -'Restore Change' button). Afterwards the push should succeed and a -new patch set for this change will be created. +deferred or abandoned and your new changes overcome the reasons for +deferring/abandoning this change you may want to restore the change +in the Gerrit WebUI (browse the deferred/abandoned change in the +Gerrit WebUI and click on the 'Restore Change' button). Afterwards +the push should succeed and a new patch set for this change will be +created. GERRIT diff --git a/Documentation/error-no-new-changes.txt b/Documentation/error-no-new-changes.txt index 347c080f23..0c3557f02b 100644 --- a/Documentation/error-no-new-changes.txt +++ b/Documentation/error-no-new-changes.txt @@ -26,8 +26,8 @@ Please note that each commit can really be pushed only once. This means: . you cannot push a commit again even if the change for which the - commit was pushed before was abandoned (but you may restore the - abandoned change) + commit was pushed before was deferred or abandoned (but you may + restore the deferred/abandoned change) . you cannot reset a change to an old patch set by pushing the old commit for this change again . if a commit was pushed to one branch you cannot push this commit diff --git a/Documentation/json.txt b/Documentation/json.txt index 99b158daae..6d7e663906 100644 --- a/Documentation/json.txt +++ b/Documentation/json.txt @@ -46,6 +46,8 @@ status:: Current state of this change. ABANDONED;; Change was abandoned by its owner or administrator. + DEFERRED;; Change was deferred by its owner or administrator. + trackingIds:: Issue tracking system links in <<trackingid,trackingid attribute>>, scraped out of the commit message based on the server's diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt index ba031dbb9a..ff5e70fbd0 100644 --- a/Documentation/user-search.txt +++ b/Documentation/user-search.txt @@ -11,16 +11,18 @@ matches the search, the change will be presented instead of a list. [options="header"] -|================================================= -|Description | Default Query -|All > Open | status:open '(or is:open)' -|All > Merged | status:merged -|All > Abandoned | status:abandoned -|My > Dafts | has:draft -|My > Watched Changes | status:open is:watched -|My > Starred Changes | is:starred -|Open changes in Foo | status:open project:Foo -|================================================= +|======================================================== +|Description | Default Query +|All > Open | status:open '(or is:open)' +|All > Merged | status:merged +|All > Deferred | status:deferred +|All > Abandoned | status:abandoned +|My > Dafts | has:draft +|My > Deferred Changes | owner:'username' status:deferred +|My > Watched Changes | status:open is:watched +|My > Starred Changes | is:starred +|Open changes in Foo | status:open project:Foo +|======================================================== Basic Change Search ------------------- @@ -222,9 +224,9 @@ True if the change is other open or submitted, merge pending. is:closed:: + -True if the change is either merged or abandoned. +True if the change is either merged, deferred or abandoned. -is:submitted, is:merged, is:abandoned:: +is:submitted, is:merged, is:deferred, is:abandoned:: + Same as <<status,status:'STATE'>>. @@ -245,7 +247,7 @@ Change has been submitted, but is waiting for a dependency. status:closed:: + -True if the change is either 'merged' or 'abandoned'. +True if the change is either 'merged', 'deferred' or 'abandoned'. status:merged:: + @@ -255,6 +257,10 @@ status:abandoned:: + Change has been abandoned by the change owner, or administrator. +status:deferred:: ++ +Change has been deferred by the change owner, or administrator. + Boolean Operators ----------------- diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java index 090dca489f..18c814834e 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java @@ -80,6 +80,9 @@ public class PageLinks { case ABANDONED: return "status:abandoned " + op("project", proj.get()); + case DEFERRED: + return "status:deferred " + op("project", proj.get()); + case MERGED: return "status:merged " + op("project", proj.get()); diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java index 41fc53866f..f0e9340f94 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java @@ -32,6 +32,10 @@ public interface ChangeManageService extends RemoteJsonService { AsyncCallback<ChangeDetail> callback); @SignInRequired + void deferChange(PatchSet.Id patchSetId, String message, + AsyncCallback<ChangeDetail> callback); + + @SignInRequired void revertChange(PatchSet.Id patchSetId, String message, AsyncCallback<ChangeDetail> callback); diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/CommonDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/CommonDetail.java index 6c3d438925..66729bbb4d 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/CommonDetail.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/CommonDetail.java @@ -25,6 +25,7 @@ public abstract class CommonDetail { protected AccountInfoCache accounts; protected boolean allowsAnonymous; protected boolean canAbandon; + protected boolean canDefer; protected boolean canRestore; protected boolean canRevert; protected boolean starred; @@ -59,6 +60,14 @@ public abstract class CommonDetail { canAbandon = a; } + public boolean canDefer() { + return canDefer; + } + + public void setCanDefer(final boolean a) { + canDefer = a; + } + public boolean canRestore() { return canRestore; } diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/TopicManageService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/TopicManageService.java index 08b2b54a0d..aac6abc52f 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/TopicManageService.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/TopicManageService.java @@ -32,6 +32,10 @@ public interface TopicManageService extends RemoteJsonService { AsyncCallback<TopicDetail> callback); @SignInRequired + void deferTopic(ChangeSet.Id changeSetId, String message, + AsyncCallback<TopicDetail> callback); + + @SignInRequired void revertTopic(ChangeSet.Id changeSetId, String message, AsyncCallback<TopicDetail> callback); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java index 2677084bef..10f509fcc3 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java @@ -202,6 +202,9 @@ public class Dispatcher { } else if ("mine,drafts".equals(token)) { return QueryScreen.forQuery("has:draft"); + } else if ("mine,deferred".equals(token)) { + return QueryScreen.forQuery("owner:" + Gerrit.getUserAccount().getUserName() + " status:deferred"); + } else { String p = "mine,watched,"; if (token.startsWith(p)) { @@ -215,6 +218,11 @@ public class Dispatcher { private static Screen all(final String token) { String p; + p = "all,deferred,"; + if (token.startsWith(p)) { + return QueryScreen.forQuery("status:deferred", skip(p, token)); + } + p = "all,abandoned,"; if (token.startsWith(p)) { return QueryScreen.forQuery("status:abandoned", skip(p, token)); @@ -266,6 +274,16 @@ public class Dispatcher { s.substring(c + 1)); } + p = "project,deferred,"; + if (token.startsWith(p)) { + final String s = skip(p, token); + final int c = s.indexOf(','); + Project.NameKey proj = Project.NameKey.parse(s.substring(0, c)); + return QueryScreen.forQuery( // + "status:deferred " + op("project", proj.get()), // + s.substring(c + 1)); + } + return new NotFoundScreen(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java index 715d7d9917..288d7a38eb 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java @@ -457,6 +457,7 @@ public class Gerrit implements EntryPoint { addLink(m, C.menuAllStaged(), PageLinks.toChangeQuery("status:staged")); addLink(m, C.menuAllIntegrating(), PageLinks.toChangeQuery("status:integrating")); addLink(m, C.menuAllMerged(), PageLinks.toChangeQuery("status:merged")); + addLink(m, C.menuAllDeferred(), PageLinks.toChangeQuery("status:deferred")); addLink(m, C.menuAllAbandoned(), PageLinks.toChangeQuery("status:abandoned")); menuLeft.add(m, C.menuAll()); @@ -464,6 +465,8 @@ public class Gerrit implements EntryPoint { m = new LinkMenuBar(); addLink(m, C.menuMyChanges(), PageLinks.MINE); addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("has:draft")); + addLink(m, C.menuMyDeferredChanges(), PageLinks.toChangeQuery( + "owner:" + Gerrit.getUserAccount().getUserName() + " status:deferred")); addLink(m, C.menuMyWatchedChanges(), PageLinks.toChangeQuery("is:watched status:open")); addLink(m, C.menuMyStarredChanges(), PageLinks.toChangeQuery("is:starred")); menuLeft.add(m, C.menuMine()); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java index 58737d933f..1512742284 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java @@ -57,10 +57,12 @@ public interface GerritConstants extends Constants { String menuAllIntegrating(); String menuAllMerged(); String menuAllAbandoned(); + String menuAllDeferred(); String menuMine(); String menuMyChanges(); String menuMyDrafts(); + String menuMyDeferredChanges(); String menuMyWatchedChanges(); String menuMyStarredChanges(); @@ -90,6 +92,7 @@ public interface GerritConstants extends Constants { String jumpAllOpen(); String jumpAllMerged(); String jumpAllAbandoned(); + String jumpAllDeferred(); String jumpMine(); String jumpMineDrafts(); String jumpMineWatched(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties index 186fe2570c..17f678ba14 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties @@ -39,12 +39,14 @@ menuAllStaged = Staged menuAllIntegrating = Integrating menuAllMerged = Merged menuAllAbandoned = Abandoned +menuAllDeferred = Deferred menuMine = My menuMyChanges = Changes menuMyDrafts = Drafts menuMyStarredChanges = Starred Changes menuMyWatchedChanges = Watched Changes +menuMyDeferredChanges = Deferred Changes menuAdmin = Admin menuPeople = People @@ -72,6 +74,7 @@ sectionJumping = Jumping jumpAllOpen = Go to all open changes jumpAllMerged = Go to all merged changes jumpAllAbandoned = Go to all abandoned changes +jumpAllDeferred = Go to all deferred changes jumpMine = Go to my dashboard jumpMineWatched = Go to watched changes jumpMineDrafts = Go to drafts diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java index 2370f0f066..b41ac83fcb 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java @@ -21,6 +21,8 @@ public interface GerritCss extends CssResource { String greenCheckClass(); String abandonChangeDialog(); String abandonMessage(); + String deferChangeDialog(); + String deferMessage(); String revertChangeDialog(); String revertMessage(); String accountContactOnFile(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java index 873045dd44..15e019d69a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java @@ -44,6 +44,12 @@ class JumpKeys { Gerrit.display(PageLinks.toChangeQuery("status:abandoned")); } }); + jumps.add(new KeyCommand(0, 'f', Gerrit.C.jumpAllDeferred()) { + @Override + public void onKeyPress(final KeyPressEvent event) { + Gerrit.display(PageLinks.toChangeQuery("status:deferred")); + } + }); if (Gerrit.isSignedIn()) { jumps.add(new KeyCommand(0, 'i', Gerrit.C.jumpMine()) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java index a3e3d6a5f0..f3e7a88d73 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java @@ -22,6 +22,7 @@ public interface ChangeConstants extends Constants { String statusLongSubmitted(); String statusLongMerged(); String statusLongAbandoned(); + String statusLongDeferred(); String changesRecentlyClosed(); @@ -30,6 +31,7 @@ public interface ChangeConstants extends Constants { String draftsHeading(); String allOpenChanges(); String allAbandonedChanges(); + String allDeferredChanges(); String allMergedChanges(); String changeTableColumnID(); @@ -120,6 +122,12 @@ public interface ChangeConstants extends Constants { String oldVersionHistory(); String baseDiffItem(); + String buttonDeferChangeBegin(); + String buttonDeferChangeSend(); + String buttonDeferChangeCancel(); + String headingDeferMessage(); + String deferChangeTitle(); + String buttonReview(); String buttonPublishCommentsSend(); String buttonPublishSubmitSend(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties index fea3723d2b..65bb065dbb 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties @@ -2,6 +2,7 @@ statusLongNew = Review in Progress statusLongSubmitted = Submitted, Merge Pending statusLongMerged = Merged statusLongAbandoned = Abandoned +statusLongDeferred = Deferred starredHeading = Starred Changes watchedHeading = Open Changes of Watched Projects @@ -9,6 +10,7 @@ draftsHeading = Changes with unpublished drafts changesRecentlyClosed = Recently closed allOpenChanges = All open changes allAbandonedChanges = All abandoned changes +allDeferredChanges = All deferred changes allMergedChanges = All merged changes changeTableColumnID = ID @@ -90,6 +92,12 @@ abandonChangeTitle = Code Review - Abandon Change oldVersionHistory = Old Version History: baseDiffItem = Base +buttonDeferChangeBegin = Defer Change +buttonDeferChangeSend = Defer Change +buttonDeferChangeCancel = Cancel +headingDeferMessage = Defer Message: +deferChangeTitle = Code Review - Defer Change + buttonRevertChangeBegin = Revert Change buttonRevertChangeSend = Revert Change buttonRevertChangeCancel = Cancel diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java index b01fcdb457..782b63112d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java @@ -24,6 +24,7 @@ public interface ChangeMessages extends Messages { String changesOpenInProject(String string); String changesMergedInProject(String string); String changesAbandonedInProject(String string); + String changesDeferredInProject(String string); String revertChangeDefaultMessage(String commitMsg, String commitId); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties index 9ae7ec7dff..0f318d024c 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties @@ -4,6 +4,7 @@ changesReviewableBy = Review Requests for {0} changesOpenInProject = Open Changes In {0} changesMergedInProject = Merged Changes In {0} changesAbandonedInProject = Abandoned Changes In {0} +changesDeferredInProject = Deferred Changes In {0} revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeSetComplexDisclosurePanel.java index 1ff53bf0bf..2056919600 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeSetComplexDisclosurePanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeSetComplexDisclosurePanel.java @@ -243,6 +243,25 @@ class ChangeSetComplexDisclosurePanel extends CommonComplexDisclosurePanel { actionsPanel.add(b); } + if (topicDetail.canDefer()) { + final Button b = new Button(Util.TC.buttonDeferTopicBegin()); + b.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + b.setEnabled(false); + new CommentedChangeActionDialog<TopicDetail>(changeSet.getId(), createCommentedCallback(b), + Util.TC.deferTopicTitle(), Util.TC.headingDeferMessage(), + Util.TC.buttonDeferTopicSend(), Util.TC.buttonDeferTopicCancel(), + Gerrit.RESOURCES.css().deferChangeDialog(), Gerrit.RESOURCES.css().deferMessage()) { + public void onSend() { + Util.T_MANAGE_SVC.deferTopic(getChangeSetId() , getMessageText(), createCallback()); + } + }.center(); + } + }); + actionsPanel.add(b); + } + if (topicDetail.canAbandon()) { final Button b = new Button(Util.TC.buttonAbandonTopicBegin()); b.addClickHandler(new ClickHandler() { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java index 2c044fa0f1..8fb10600eb 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java @@ -283,6 +283,25 @@ class PatchSetComplexDisclosurePanel extends CommonComplexDisclosurePanel { actionsPanel.add(b); } + if (changeDetail.canDefer()) { + final Button b = new Button(Util.C.buttonDeferChangeBegin()); + b.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + b.setEnabled(false); + new CommentedChangeActionDialog<ChangeDetail>(patchSet.getId(), createCommentedCallback(b), + Util.C.deferChangeTitle(), Util.C.headingDeferMessage(), + Util.C.buttonDeferChangeSend(), Util.C.buttonDeferChangeCancel(), + Gerrit.RESOURCES.css().deferChangeDialog(), Gerrit.RESOURCES.css().deferMessage()) { + public void onSend() { + Util.MANAGE_SVC.deferChange(getPatchSetId() , getMessageText(), createCallback()); + } + }.center(); + } + }); + actionsPanel.add(b); + } + if (changeDetail.canAbandon()) { final Button b = new Button(Util.C.buttonAbandonChangeBegin()); b.addClickHandler(new ClickHandler() { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/TopicConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/TopicConstants.java index 0521b0bbaa..6846785684 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/TopicConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/TopicConstants.java @@ -36,6 +36,12 @@ public interface TopicConstants extends Constants { String headingAbandonMessage(); String abandonTopicTitle(); + String buttonDeferTopicBegin(); + String buttonDeferTopicSend(); + String buttonDeferTopicCancel(); + String headingDeferMessage(); + String deferTopicTitle(); + String buttonRestoreTopicBegin(); String restoreTopicTitle(); String buttonRestoreTopicCancel(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/TopicConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/TopicConstants.properties index 91a374d4ad..851a6024c5 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/TopicConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/TopicConstants.properties @@ -10,6 +10,12 @@ buttonAbandonTopicCancel = Cancel headingAbandonMessage = Abandon Message: abandonTopicTitle = Code Review - Abandon Topic +buttonDeferTopicBegin = Defer Topic +buttonDeferTopicSend = Defer Topic +buttonDeferTopicCancel = Cancel +headingDeferMessage = Defer Message: +deferTopicTitle = Code Review - Defer Topic + buttonRevertTopicBegin = Revert Topic buttonRevertTopicSend = Revert Topic buttonRevertTopicCancel = Cancel diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java index 296138f168..84a05d9247 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java @@ -68,6 +68,8 @@ public class Util { return C.statusLongMerged(); case ABANDONED: return C.statusLongAbandoned(); + case DEFERRED: + return C.statusLongDeferred(); default: return status.name(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css index bcc7340b39..af45c3e7fa 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css @@ -1284,6 +1284,31 @@ a:hover.downloadLink { font-size: small; } +/** DeferChangeDialog **/ + +.deferChangeDialog .gwt-DisclosurePanel .header td { + font-weight: bold; + white-space: nowrap; +} + +.deferChangeDialog .smallHeading { + font-size: small; + font-weight: bold; + white-space: nowrap; +} +.deferChangeDialog .deferMessage { + margin-left: 10px; + background: trimColor; + padding: 5px 5px 5px 5px; +} +.deferChangeDialog .deferMessage textarea { + font-size: small; +} +.deferChangeDialog .gwt-Hyperlink { + white-space: nowrap; + font-size: small; +} + /** RevertChangeDialog **/ .revertChangeDialog .gwt-DisclosurePanel .header td { diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java index 9e6246513b..312f9e6685 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java @@ -104,8 +104,9 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> { detail.setChange(change); detail.setAllowsAnonymous(control.forAnonymousUser().isVisible()); - detail.setCanAbandon(change.getStatus().isOpen() && control.canAbandon()); - detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED && control.canRestore()); + detail.setCanAbandon((change.getStatus() == Change.Status.DEFERRED || change.getStatus().isOpen()) && control.canAbandon()); + detail.setCanDefer((change.getStatus() == Change.Status.ABANDONED || change.getStatus().isOpen()) && control.canDefer()); + detail.setCanRestore((change.getStatus() == Change.Status.ABANDONED || change.getStatus() == Change.Status.DEFERRED) && control.canRestore()); detail.setCanSubmit(canSubmitResult == CanSubmitResult.OK); detail.setStarred(control.getCurrentUser().getStarredChanges().contains( changeId)); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java index a8be03d888..121d478b82 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java @@ -24,6 +24,7 @@ import com.google.inject.Inject; class ChangeManageServiceImpl implements ChangeManageService { private final SubmitAction.Factory submitAction; private final AbandonChange.Factory abandonChangeFactory; + private final DeferChange.Factory deferChangeFactory; private final RestoreChange.Factory restoreChangeFactory; private final RevertChange.Factory revertChangeFactory; private final StagingAction.Factory stagingActionFactory; @@ -32,12 +33,14 @@ class ChangeManageServiceImpl implements ChangeManageService { @Inject ChangeManageServiceImpl(final SubmitAction.Factory patchSetAction, final AbandonChange.Factory abandonChangeFactory, + final DeferChange.Factory deferChangeFactory, final RestoreChange.Factory restoreChangeFactory, final RevertChange.Factory revertChangeFactory, final StagingAction.Factory stagingActionFactory, final UnstageChange.Factory unstageChangeFactory) { this.submitAction = patchSetAction; this.abandonChangeFactory = abandonChangeFactory; + this.deferChangeFactory = deferChangeFactory; this.restoreChangeFactory = restoreChangeFactory; this.revertChangeFactory = revertChangeFactory; this.stagingActionFactory = stagingActionFactory; @@ -54,6 +57,11 @@ class ChangeManageServiceImpl implements ChangeManageService { abandonChangeFactory.create(patchSetId, message).to(callback); } + public void deferChange(final PatchSet.Id patchSetId, final String message, + final AsyncCallback<ChangeDetail> callback) { + deferChangeFactory.create(patchSetId, message).to(callback); + } + public void revertChange(final PatchSet.Id patchSetId, final String message, final AsyncCallback<ChangeDetail> callback) { revertChangeFactory.create(patchSetId, message).to(callback); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java index 8797ed88f4..7aae7a8d74 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java @@ -30,6 +30,7 @@ public class ChangeModule extends RpcServletModule { @Override protected void configure() { factory(AbandonChange.Factory.class); + factory(DeferChange.Factory.class); factory(RestoreChange.Factory.class); factory(RevertChange.Factory.class); factory(ChangeDetailFactory.Factory.class); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeferChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeferChange.java new file mode 100644 index 0000000000..9aadf05f00 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeferChange.java @@ -0,0 +1,125 @@ +// Copyright (C) 2009 The Android Open Source Project, +// Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd.rpc.changedetail; + +import com.google.gerrit.common.ChangeHookRunner; +import com.google.gerrit.common.data.ChangeDetail; +import com.google.gerrit.common.errors.NoSuchEntityException; +import com.google.gerrit.httpd.rpc.Handler; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.PatchSet; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.MergeOp; +import com.google.gerrit.server.git.MergeQueue; +import com.google.gerrit.server.mail.DeferredSender; +import com.google.gerrit.server.mail.EmailException; +import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.project.InvalidChangeOperationException; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.project.NoSuchRefException; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +import org.eclipse.jgit.lib.Repository; + +import java.io.IOException; +import javax.annotation.Nullable; + +class DeferChange extends Handler<ChangeDetail> { + interface Factory { + DeferChange create(PatchSet.Id patchSetId, String message); + } + + private final ChangeControl.Factory changeControlFactory; + private final ReviewDb db; + private final IdentifiedUser currentUser; + private final DeferredSender.Factory deferredSenderFactory; + private final ChangeDetailFactory.Factory changeDetailFactory; + + private final PatchSet.Id patchSetId; + @Nullable + private final String message; + + private final ChangeHookRunner hooks; + private final MergeQueue merger; + private final MergeOp.Factory opFactory; + private final GitRepositoryManager gitManager; + + @Inject + DeferChange(final ChangeControl.Factory changeControlFactory, + final ReviewDb db, final IdentifiedUser currentUser, + final DeferredSender.Factory deferredSenderFactory, + final ChangeDetailFactory.Factory changeDetailFactory, + @Assisted final PatchSet.Id patchSetId, + @Assisted @Nullable final String message, final ChangeHookRunner hooks, + MergeQueue merger, MergeOp.Factory opFactory, + GitRepositoryManager gitManager) { + this.changeControlFactory = changeControlFactory; + this.db = db; + this.currentUser = currentUser; + this.deferredSenderFactory = deferredSenderFactory; + this.changeDetailFactory = changeDetailFactory; + + this.patchSetId = patchSetId; + this.message = message; + this.hooks = hooks; + this.merger = merger; + this.opFactory = opFactory; + this.gitManager = gitManager; + } + + @Override + public ChangeDetail call() throws NoSuchChangeException, OrmException, + EmailException, NoSuchEntityException, InvalidChangeOperationException, + PatchSetInfoNotAvailableException { + final Change.Id changeId = patchSetId.getParentKey(); + final ChangeControl control = changeControlFactory.validateFor(changeId); + if (!control.canDefer()) { + throw new NoSuchChangeException(changeId); + } + + ChangeUtil.defer(patchSetId, currentUser, message, db, + deferredSenderFactory, hooks); + + final Change change = db.changes().get(changeId); + final boolean staged = change.getStatus() == Change.Status.STAGED; + + // If the change was staged, the staging branch needs to be updated. + if (staged) { + Repository git = null; + try { + git = gitManager.openRepository(change.getProject()); + ChangeUtil.rebuildStaging(change.getDest(), currentUser, db, git, + opFactory, merger, hooks); + } catch (IOException e) { + // Failed to open git repository. + } catch (NoSuchRefException e) { + // Invalid change destination branch. + } finally { + if (git != null) { + git.close(); + } + } + } + + return changeDetailFactory.create(changeId).call(); + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/DeferTopic.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/DeferTopic.java new file mode 100644 index 0000000000..33e0f69c27 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/DeferTopic.java @@ -0,0 +1,90 @@ +// Copyright (C) 2011 The Android Open Source Project, +// Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd.rpc.topic; + +import com.google.gerrit.common.ChangeHookRunner; +import com.google.gerrit.common.data.TopicDetail; +import com.google.gerrit.common.errors.NoSuchEntityException; +import com.google.gerrit.httpd.rpc.Handler; +import com.google.gerrit.reviewdb.ChangeSet; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.reviewdb.Topic; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.TopicUtil; +import com.google.gerrit.server.mail.DeferredSender; +import com.google.gerrit.server.mail.EmailException; +import com.google.gerrit.server.project.InvalidChangeOperationException; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.project.NoSuchTopicException; +import com.google.gerrit.server.project.TopicControl; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +import javax.annotation.Nullable; + +class DeferTopic extends Handler<TopicDetail> { + interface Factory { + DeferTopic create(ChangeSet.Id changeSetId, String message); + } + + private final TopicControl.Factory topicControlFactory; + private final ReviewDb db; + private final IdentifiedUser currentUser; + private final DeferredSender.Factory deferredSenderFactory; + private final TopicDetailFactory.Factory topicDetailFactory; + + private final ChangeSet.Id changeSetId; + @Nullable + private final String message; + + private final ChangeHookRunner hooks; + + @Inject + DeferTopic(final TopicControl.Factory topicControlFactory, + final ReviewDb db, final IdentifiedUser currentUser, + final DeferredSender.Factory deferredSenderFactory, + final TopicDetailFactory.Factory topicDetailFactory, + @Assisted final ChangeSet.Id changeSetId, + @Assisted @Nullable final String message, final ChangeHookRunner hooks) { + this.db = db; + this.currentUser = currentUser; + this.deferredSenderFactory = deferredSenderFactory; + this.topicControlFactory = topicControlFactory; + this.topicDetailFactory = topicDetailFactory; + + this.changeSetId = changeSetId; + this.message = message; + this.hooks = hooks; + } + + @Override + public TopicDetail call() throws NoSuchTopicException, NoSuchChangeException, + OrmException, EmailException, NoSuchEntityException, + ChangeSetInfoNotAvailableException, InvalidChangeOperationException { + + final Topic.Id topicId = changeSetId.getParentKey(); + final TopicControl topicControl = topicControlFactory.validateFor(topicId); + if (!topicControl.canDefer()) { + throw new NoSuchTopicException(topicId); + } + + TopicUtil.defer(changeSetId, currentUser, message, db, + deferredSenderFactory, hooks); + + return topicDetailFactory.create(topicId).call(); + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicDetailFactory.java index 931905cf71..35f85f48c1 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicDetailFactory.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicDetailFactory.java @@ -113,8 +113,9 @@ public class TopicDetailFactory extends Handler<TopicDetail> { detail.setTopic(topic); detail.setAllowsAnonymous(control.forUser(anonymousUser).isVisible()); - detail.setCanAbandon(topic.getStatus().isOpen() && control.canAbandon()); - detail.setCanRestore(topic.getStatus() == AbstractEntity.Status.ABANDONED && control.canRestore()); + detail.setCanAbandon((topic.getStatus() == AbstractEntity.Status.DEFERRED || topic.getStatus().isOpen()) && control.canAbandon()); + detail.setCanDefer((topic.getStatus() == AbstractEntity.Status.ABANDONED || topic.getStatus().isOpen()) && control.canDefer()); + detail.setCanRestore((topic.getStatus() == AbstractEntity.Status.ABANDONED || topic.getStatus() == AbstractEntity.Status.DEFERRED) && control.canRestore()); detail.setStarred(control.getCurrentUser().getStarredChanges().contains( topicId)); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicManageServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicManageServiceImpl.java index d628def421..7dd40ceaf9 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicManageServiceImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicManageServiceImpl.java @@ -24,6 +24,7 @@ import com.google.inject.Inject; class TopicManageServiceImpl implements TopicManageService { private final SubmitAction.Factory submitAction; private final AbandonTopic.Factory abandonTopicFactory; + private final DeferTopic.Factory deferTopicFactory; private final RestoreTopic.Factory restoreTopicFactory; private final RevertTopic.Factory revertTopicFactory; private final StagingAction.Factory stagingActionFactory; @@ -32,12 +33,14 @@ class TopicManageServiceImpl implements TopicManageService { @Inject TopicManageServiceImpl(final SubmitAction.Factory changeSetAction, final AbandonTopic.Factory abandonTopicFactory, + final DeferTopic.Factory deferTopicFactory, final RestoreTopic.Factory restoreTopicFactory, final RevertTopic.Factory revertTopicFactory, final StagingAction.Factory stagingActionFactory, final UnstageAction.Factory unstageActionFactory) { this.submitAction = changeSetAction; this.abandonTopicFactory = abandonTopicFactory; + this.deferTopicFactory = deferTopicFactory; this.restoreTopicFactory = restoreTopicFactory; this.revertTopicFactory = revertTopicFactory; this.stagingActionFactory = stagingActionFactory; @@ -54,6 +57,11 @@ class TopicManageServiceImpl implements TopicManageService { abandonTopicFactory.create(csid, message).to(cb); } + public void deferTopic(final ChangeSet.Id csid, final String message, + final AsyncCallback<TopicDetail> cb) { + deferTopicFactory.create(csid, message).to(cb); + } + public void revertTopic(final ChangeSet.Id csid, final String message, final AsyncCallback<TopicDetail> cb) { revertTopicFactory.create(csid, message).to(cb); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicModule.java index 28e495ea09..361a622f91 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicModule.java @@ -30,6 +30,7 @@ public class TopicModule extends RpcServletModule { @Override protected void configure() { factory(AbandonTopic.Factory.class); + factory(DeferTopic.Factory.class); factory(RestoreTopic.Factory.class); factory(RevertTopic.Factory.class); factory(AddTopicReviewer.Factory.class); diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java index dae08934ad..eed007bb86 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java @@ -86,6 +86,7 @@ public class SitePathInitializer { chmod(0755, site.gerrit_sh); extractMailExample("Abandoned.vm"); + extractMailExample("Deferred.vm"); extractMailExample("ChangeFooter.vm"); extractMailExample("ChangeSubject.vm"); extractMailExample("Comment.vm"); diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractEntity.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractEntity.java index c095890f9c..86e1dc3440 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractEntity.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractEntity.java @@ -200,7 +200,16 @@ public abstract class AbstractEntity { * a replacement patch/change set, and it cannot be merged. Draft comments however * may be published, permitting reviewers to send constructive feedback. */ - ABANDONED('A'); + ABANDONED('A'), + /** + * Change/topic is closed, but was not submitted to its destination branch. + * + * <p> + * Once a change/topic has been deferred, it cannot be further modified by adding + * a replacement patch/change set, and it cannot be merged. Draft comments however + * may be published, permitting reviewers to send constructive feedback. + */ + DEFERRED('D'); private final char code; private final boolean closed; diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java index 4f99a17550..3fa53e53ce 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java +++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java @@ -31,6 +31,7 @@ import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.events.ApprovalAttribute; import com.google.gerrit.server.events.ChangeAbandonedEvent; +import com.google.gerrit.server.events.ChangeDeferredEvent; import com.google.gerrit.server.events.ChangeEvent; import com.google.gerrit.server.events.ChangeMergedEvent; import com.google.gerrit.server.events.ChangeRestoreEvent; @@ -98,6 +99,9 @@ public class ChangeHookRunner { /** Filename of the change abandoned hook. */ private final File changeAbandonedHook; + /** Filename of the change deferred hook. */ + private final File changeDeferredHook; + /** Filename of the change abandoned hook. */ private final File changeRestoredHook; @@ -148,6 +152,7 @@ public class ChangeHookRunner { commentAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "commentAddedHook", "comment-added")).getPath()); changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath()); changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath()); + changeDeferredHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeDeferredHook", "change-deferred")).getPath()); changeRestoredHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeRestoredHook", "change-restored")).getPath()); refUpdatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdatedHook", "ref-updated")).getPath()); } @@ -328,6 +333,32 @@ public class ChangeHookRunner { } /** + * Fire the Change Deferred Hook. + * + * @param change The change itself. + * @param account The gerrit user who deferred the change. + * @param reason Reason for deferring the change. + */ + public void doChangeDeferredHook(final Change change, final Account account, final String reason) { + final ChangeDeferredEvent event = new ChangeDeferredEvent(); + + event.change = eventFactory.asChangeAttribute(change); + event.deferrer = eventFactory.asAccountAttribute(account); + event.reason = reason; + fireEvent(change, event); + + final List<String> args = new ArrayList<String>(); + addArg(args, "--change", event.change.id); + addArg(args, "--change-url", event.change.url); + addArg(args, "--project", event.change.project); + addArg(args, "--branch", event.change.branch); + addArg(args, "--deferrer", getDisplayName(account)); + addArg(args, "--reason", reason == null ? "" : reason); + + runHook(openRepository(change), changeDeferredHook, args); + } + + /** * Fire the Change Restored Hook. * * @param change The change itself. diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java index cd837aa752..53eb9ab5c0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java @@ -41,6 +41,7 @@ import com.google.gerrit.server.git.MergeQueue; import com.google.gerrit.server.git.ReplicationQueue; import com.google.gerrit.server.git.StagingUtil; import com.google.gerrit.server.mail.AbandonedSender; +import com.google.gerrit.server.mail.DeferredSender; import com.google.gerrit.server.mail.EmailException; import com.google.gerrit.server.mail.RestoredSender; import com.google.gerrit.server.mail.RevertedSender; @@ -260,7 +261,8 @@ public class ChangeUtil { new AtomicUpdate<Change>() { @Override public Change update(Change change) { - if (change.getStatus().isOpen() + if ((change.getStatus().isOpen() + || change.getStatus() == Change.Status.DEFERRED) && change.currentPatchSetId().equals(patchSetId)) { change.setStatus(Change.Status.ABANDONED); ChangeUtil.updated(change); @@ -298,6 +300,79 @@ public class ChangeUtil { hooks.doChangeAbandonedHook(updatedChange, user.getAccount(), message); } + public static void defer(final PatchSet.Id patchSetId, + final IdentifiedUser user, final String message, final ReviewDb db, + final DeferredSender.Factory senderFactory, + final ChangeHookRunner hooks) throws NoSuchChangeException, + InvalidChangeOperationException, EmailException, OrmException { + defer(patchSetId, user, message, db, senderFactory, hooks, true); + } + + public static void defer(final PatchSet.Id patchSetId, + final IdentifiedUser user, final String message, final ReviewDb db, + final DeferredSender.Factory senderFactory, + final ChangeHookRunner hooks, final boolean sendMail) throws NoSuchChangeException, + InvalidChangeOperationException, EmailException, OrmException { + final Change.Id changeId = patchSetId.getParentKey(); + final PatchSet patch = db.patchSets().get(patchSetId); + if (patch == null) { + throw new NoSuchChangeException(changeId); + } + + final ChangeMessage cmsg = + new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil + .messageUUID(db)), user.getAccountId()); + final StringBuilder msgBuf = + new StringBuilder("Patch Set " + patchSetId.get() + ": Deferred"); + if (message != null && message.length() > 0) { + msgBuf.append("\n\n"); + msgBuf.append(message); + } + cmsg.setMessage(msgBuf.toString()); + + final Change updatedChange = db.changes().atomicUpdate(changeId, + new AtomicUpdate<Change>() { + @Override + public Change update(Change change) { + if ((change.getStatus().isOpen() + || change.getStatus() == Change.Status.ABANDONED) + && change.currentPatchSetId().equals(patchSetId)) { + change.setStatus(Change.Status.DEFERRED); + ChangeUtil.updated(change); + return change; + } else { + return null; + } + } + }); + + if (updatedChange == null) { + throw new InvalidChangeOperationException( + "Change is no longer open or patchset is not latest"); + } + + db.changeMessages().insert(Collections.singleton(cmsg)); + + final List<PatchSetApproval> approvals = + db.patchSetApprovals().byChange(changeId).toList(); + for (PatchSetApproval a : approvals) { + a.cache(updatedChange); + } + db.patchSetApprovals().update(approvals); + + if (senderFactory != null) { + // Email the reviewers + final DeferredSender cm = senderFactory.create(updatedChange); + cm.setFrom(user.getAccountId()); + cm.setChangeMessage(cmsg); + cm.send(); + } else { + log.error("Cannot send email when deferring a change."); + } + + hooks.doChangeDeferredHook(updatedChange, user.getAccount(), message); + } + public static void revert(final PatchSet.Id patchSetId, final IdentifiedUser user, final String message, final ReviewDb db, final RevertedSender.Factory revertedSenderFactory, @@ -460,7 +535,8 @@ public class ChangeUtil { new AtomicUpdate<Change>() { @Override public Change update(Change change) { - if (change.getStatus() == Change.Status.ABANDONED + if ((change.getStatus() == Change.Status.ABANDONED || + change.getStatus() == Change.Status.DEFERRED) && change.currentPatchSetId().equals(patchSetId)) { change.setStatus(Change.Status.NEW); ChangeUtil.updated(change); @@ -473,7 +549,7 @@ public class ChangeUtil { if (updatedChange == null) { throw new InvalidChangeOperationException( - "Change is not abandoned or patchset is not latest"); + "Change is not abandoned/deferred or patchset is not latest"); } db.changeMessages().insert(Collections.singleton(cmsg)); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/TopicUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/TopicUtil.java index 4e1b6360d1..deb67d826b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/TopicUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/TopicUtil.java @@ -45,6 +45,7 @@ import com.google.gerrit.server.project.NoSuchRefException; import com.google.gerrit.server.project.NoSuchTopicException; import com.google.gerrit.server.project.TopicControl; import com.google.gerrit.server.mail.AbandonedSender; +import com.google.gerrit.server.mail.DeferredSender; import com.google.gerrit.server.mail.AddReviewerSender; import com.google.gerrit.server.mail.EmailException; import com.google.gerrit.server.mail.RestoredSender; @@ -287,6 +288,79 @@ public static ChangeSetApproval createStagingApproval( } } + public static void defer(final ChangeSet.Id changeSetId, + final IdentifiedUser user, final String message, final ReviewDb db, + final DeferredSender.Factory deferredSenderFactory, + final ChangeHookRunner hooks) throws NoSuchTopicException, + NoSuchChangeException, InvalidChangeOperationException, + EmailException, OrmException { + final Topic.Id topicId = changeSetId.getParentKey(); + final ChangeSet changeSet = db.changeSets().get(changeSetId); + if (changeSet == null) { + throw new NoSuchTopicException(topicId); + } + + final TopicMessage tmsg = + new TopicMessage(new TopicMessage.Key(topicId, ChangeUtil + .messageUUID(db)), user.getAccountId()); + final StringBuilder msgBuf = + new StringBuilder("Change Set " + changeSetId.get() + ": Deferred"); + if (message != null && message.length() > 0) { + msgBuf.append("\n\n"); + msgBuf.append(message); + } + tmsg.setMessage(msgBuf.toString()); + + final Topic updatedTopic = db.topics().atomicUpdate(topicId, + new AtomicUpdate<Topic>() { + @Override + public Topic update(Topic topic) { + if (topic.getStatus().isOpen() + && topic.currentChangeSetId().equals(changeSetId)) { + topic.setStatus(Change.Status.DEFERRED); + TopicUtil.updated(topic); + return topic; + } else { + return null; + } + } + }); + + Change lastChange = null; + if (updatedTopic == null) { + throw new InvalidChangeOperationException( + "Topic is no longer open or changeset is not latest"); + } else { + // Defer the changes belonging to the Topic + // + List<Change> toDefer = db.changes().byTopicOpenAll(topicId).toList(); + for (Change c : toDefer) { + ChangeUtil.defer(c.currentPatchSetId(), user, message, db, + deferredSenderFactory, hooks, false); + } + lastChange = toDefer.get(toDefer.size() - 1); + } + + db.topicMessages().insert(Collections.singleton(tmsg)); + + final List<ChangeSetApproval> approvals = + db.changeSetApprovals().byTopic(topicId).toList(); + for (ChangeSetApproval a : approvals) { + a.cache(updatedTopic); + } + db.changeSetApprovals().update(approvals); + + // Email the reviewers + // TODO Topic support + // Meanwhile, sending mails in "behalf" of the last change of the topic + if (lastChange != null) { + final DeferredSender cm = deferredSenderFactory.create(lastChange); + cm.setFrom(user.getAccountId()); + cm.setTopicMessage(tmsg); + cm.send(); + } + } + public static void revert(final ChangeSet.Id changeSetId, final IdentifiedUser user, final String message, final ReviewDb db, final RevertedSender.Factory revertedSenderFactory, @@ -560,10 +634,11 @@ public static ChangeSetApproval createStagingApproval( final AbstractEntity.Status tStatus = t.getStatus(); if (t.getTopic().equals(topicName)) { if (tStatus.equals(AbstractEntity.Status.ABANDONED) || + tStatus.equals(AbstractEntity.Status.DEFERRED) || tStatus.equals(AbstractEntity.Status.MERGED)) continue; // If we don't have a mess in our DB, we must have only // one topic with the same String in a different status than - // MERGED or ABANDONED + // MERGED, ABANDONED or DEFERRED // else return t; } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java index b3edf0dd4e..8c79a46a0d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java @@ -30,6 +30,7 @@ import com.google.gerrit.server.git.ReceiveCommits; import com.google.gerrit.server.git.StagingMergeDelegate; import com.google.gerrit.server.git.SubmitMergeDelegate; import com.google.gerrit.server.mail.AbandonedSender; +import com.google.gerrit.server.mail.DeferredSender; import com.google.gerrit.server.mail.AddReviewerSender; import com.google.gerrit.server.mail.BuildApprovedSender; import com.google.gerrit.server.mail.BuildRejectedSender; @@ -81,6 +82,7 @@ public class GerritRequestModule extends FactoryModule { factory(PublishTopicComments.Factory.class); factory(ReplacePatchSetSender.Factory.class); factory(AbandonedSender.Factory.class); + factory(DeferredSender.Factory.class); factory(RevertedSender.Factory.class); factory(RestoredSender.Factory.class); factory(CommentSender.Factory.class); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeDeferredEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeDeferredEvent.java new file mode 100644 index 0000000000..8edec816d1 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeDeferredEvent.java @@ -0,0 +1,24 @@ +// Copyright (C) 2010 The Android Open Source Project, +// Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.events; + +public class ChangeDeferredEvent extends ChangeEvent { + public final String type = "change-deferred"; + public ChangeAttribute change; + public PatchSetAttribute patchSet; + public AccountAttribute deferrer; + public String reason; +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java index 893cca64dc..b5fc953486 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java @@ -955,6 +955,11 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { reject(cmd, "change " + change.getId() + " closed"); return false; } + if (change.getStatus().equals(AbstractEntity.Status.DEFERRED) + && topic == null) { + reject(cmd, "change " + change.getId() + " closed"); + return false; + } Topic.Id topicId = topic != null ? topic.getId() : null; ChangeSet.Id csId = topic != null ? topic.currentChangeSetId() : null; @@ -986,6 +991,10 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { reject(cmd, "change " + change.getId() + " closed"); return false; } + if (change.getStatus().equals(AbstractEntity.Status.DEFERRED) && !topic) { + reject(cmd, "change " + change.getId() + " closed"); + return false; + } if (toReplace.containsKey(change.getId())) { reject(cmd, "duplicate request"); return false; diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeferredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeferredSender.java new file mode 100644 index 0000000000..04a4a4520b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeferredSender.java @@ -0,0 +1,46 @@ +// Copyright (C) 2009 The Android Open Source Project, +// Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail; + +import com.google.gerrit.reviewdb.Change; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +/** Send notice about a change being deferred by its owner. */ +public class DeferredSender extends ReplyToChangeSender { + public static interface Factory { + DeferredSender create(Change change); + } + + @Inject + public DeferredSender(EmailArguments ea, @Assisted Change c) { + super(ea, c, "defer"); + } + + @Override + protected void init() throws EmailException { + super.init(); + + ccAllApprovals(); + bccStarredBy(); + bccWatchesNotifyAllComments(); + } + + @Override + protected void formatChange() throws EmailException { + appendText(velocifyFile("Deferred.vm")); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java index c9652fef87..ce9023df04 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java @@ -166,9 +166,25 @@ public class ChangeControl { && (change.getStatus() != Change.Status.INTEGRATING); } + /** Can this user defer this change? */ + public boolean canDefer() { + if (change.getTopicId() != null) return false; + boolean userCan = isOwner() // owner (aka creator) of the change can defer + || getRefControl().isOwner() // branch owner can defer + || getProjectControl().isOwner() // project owner can defer + || getCurrentUser().isAdministrator() // site administers are god + ; + + // Cannot defer changes that are already processed by the continuous + // integration system. + return userCan + && (change.getStatus() != Change.Status.INTEGRATING); + } + /** Can this user restore this change? */ public boolean canRestore() { - return canAbandon(); // Anyone who can abandon the change can restore it back + // Anyone who can abandon or defer the change can restore it back + return canAbandon() || canDefer(); } /** All value ranges of any allowed label permission. */ diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/TopicControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/TopicControl.java index d9cf3a964f..ba55b44af9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/TopicControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/TopicControl.java @@ -152,9 +152,19 @@ public class TopicControl { ; } + /** Can this user defer this topic? */ + public boolean canDefer() { + return isOwner() // owner (aka creator) of the change can defer + || getRefControl().isOwner() // branch owner can defer + || getProjectControl().isOwner() // project owner can defer + || getCurrentUser().isAdministrator() // site administers are god + ; + } + /** Can this user restore this topic? */ public boolean canRestore() { - return canAbandon(); // Anyone who can abandon the change can restore it back + // Anyone who can abandon or defer the change can restore it back + return canAbandon() || canDefer(); } /** All value ranges of any allowed label permission. */ diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java index b31bf655de..3c6df14a0b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java @@ -76,7 +76,8 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> { @Rewrite("-status:merged") public Predicate<ChangeData> r00_notMerged() { return or(ChangeStatusPredicate.open(dbProvider), - new ChangeStatusPredicate(dbProvider, Change.Status.ABANDONED)); + new ChangeStatusPredicate(dbProvider, Change.Status.ABANDONED), + new ChangeStatusPredicate(dbProvider, Change.Status.DEFERRED)); } @SuppressWarnings("unchecked") @@ -89,6 +90,14 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> { @SuppressWarnings("unchecked") @NoCostComputation + @Rewrite("-status:deferred") + public Predicate<ChangeData> r00_notDeferred() { + return or(ChangeStatusPredicate.open(dbProvider), + new ChangeStatusPredicate(dbProvider, Change.Status.MERGED)); + } + + @SuppressWarnings("unchecked") + @NoCostComputation @Rewrite("sortkey_before:z A=(age:*)") public Predicate<ChangeData> r00_ageToSortKey(@Named("A") AgePredicate a) { String cut = ChangeUtil.sortKey(a.getCut(), Integer.MAX_VALUE); @@ -248,6 +257,50 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> { }; } + @Rewrite("status:deferred P=(project:*) S=(sortkey_after:*) L=(limit:*)") + public Predicate<ChangeData> r10_byProjectDeferredPrev( + @Named("P") final ProjectPredicate p, + @Named("S") final SortKeyPredicate.After s, + @Named("L") final IntPredicate<ChangeData> l) { + return new PaginatedSource(40000, s.getValue(), l.intValue()) { + @Override + ResultSet<Change> scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.byProjectClosedPrev(Change.Status.DEFERRED.getCode(), // + p.getValueKey(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.DEFERRED + && p.match(cd) // + && s.match(cd); + } + }; + } + + @Rewrite("status:deferred P=(project:*) S=(sortkey_before:*) L=(limit:*)") + public Predicate<ChangeData> r10_byProjectDeferredNext( + @Named("P") final ProjectPredicate p, + @Named("S") final SortKeyPredicate.Before s, + @Named("L") final IntPredicate<ChangeData> l) { + return new PaginatedSource(40000, s.getValue(), l.intValue()) { + @Override + ResultSet<Change> scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.byProjectClosedNext(Change.Status.DEFERRED.getCode(), // + p.getValueKey(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.DEFERRED + && p.match(cd) // + && s.match(cd); + } + }; + } + @Rewrite("status:open S=(sortkey_after:*) L=(limit:*)") public Predicate<ChangeData> r20_byOpenPrev( @Named("S") final SortKeyPredicate.After s, @@ -381,11 +434,59 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> { } @SuppressWarnings("unchecked") + @Rewrite("status:deferred S=(sortkey_after:*) L=(limit:*)") + public Predicate<ChangeData> r20_byDeferredPrev( + @Named("S") final SortKeyPredicate.After s, + @Named("L") final IntPredicate<ChangeData> l) { + return new PaginatedSource(50000, s.getValue(), l.intValue()) { + { + init("r20_byDeferredPrev", s, l); + } + + @Override + ResultSet<Change> scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.allClosedPrev(Change.Status.DEFERRED.getCode(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.DEFERRED + && s.match(cd); + } + }; + } + + @SuppressWarnings("unchecked") + @Rewrite("status:deferred S=(sortkey_before:*) L=(limit:*)") + public Predicate<ChangeData> r20_byDeferredNext( + @Named("S") final SortKeyPredicate.Before s, + @Named("L") final IntPredicate<ChangeData> l) { + return new PaginatedSource(50000, s.getValue(), l.intValue()) { + { + init("r20_byDeferredNext", s, l); + } + + @Override + ResultSet<Change> scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.allClosedNext(Change.Status.DEFERRED.getCode(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.DEFERRED + && s.match(cd); + } + }; + } + + @SuppressWarnings("unchecked") @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)") public Predicate<ChangeData> r20_byClosedPrev( @Named("S") final SortKeyPredicate.After s, @Named("L") final IntPredicate<ChangeData> l) { - return or(r20_byMergedPrev(s, l), r20_byAbandonedPrev(s, l)); + return or(r20_byMergedPrev(s, l), r20_byAbandonedPrev(s, l), r20_byDeferredPrev(s, l)); } @SuppressWarnings("unchecked") @@ -393,7 +494,7 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> { public Predicate<ChangeData> r20_byClosedNext( @Named("S") final SortKeyPredicate.Before s, @Named("L") final IntPredicate<ChangeData> l) { - return or(r20_byMergedNext(s, l), r20_byAbandonedNext(s, l)); + return or(r20_byMergedNext(s, l), r20_byAbandonedNext(s, l), r20_byDeferredNext(s, l)); } @SuppressWarnings("unchecked") diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Deferred.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Deferred.vm new file mode 100644 index 0000000000..25447313f3 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Deferred.vm @@ -0,0 +1,45 @@ +## Copyright (C) 2010 The Android Open Source Project, +## Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## +## +## Template Type: +## ------------- +## This is a velocity mail template, see: http://velocity.apache.org and the +## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. +## +## Template File Names and extensions: +## ---------------------------------- +## Gerrit will use templates ending in ".vm" but will ignore templates ending +## in ".vm.example". If a .vm template does not exist, the default internal +## gerrit template which is the same as the .vm.example will be used. If you +## want to override the default template, copy the .vm.example file to a .vm +## file and edit it appropriately. +## +## This Template: +## -------------- +## The Deferred.vm template will determine the contents of the email related +## to a change being deferred. It is a ChangeEmail: see ChangeSubject.vm and +## ChangeFooter.vm. +## +$fromName has deferred this change. + +Change subject: $change.subject +...................................................................... + + +#if ($coverLetter) +$coverLetter + +#end diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java index da6b4cf146..f7506ca580 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java @@ -34,6 +34,7 @@ import com.google.gerrit.server.git.MergeOp; import com.google.gerrit.server.git.MergeQueue; import com.google.gerrit.server.git.StagingUtil; import com.google.gerrit.server.mail.AbandonedSender; +import com.google.gerrit.server.mail.DeferredSender; import com.google.gerrit.server.mail.EmailException; import com.google.gerrit.server.mail.RestoredSender; import com.google.gerrit.server.patch.PublishComments; @@ -101,6 +102,9 @@ public class ReviewCommand extends BaseCommand { @Option(name = "--abandon", usage = "abandon the patch set") private boolean abandonChange; + @Option(name = "--defer", usage = "defer the patch set") + private boolean deferChange; + @Option(name = "--restore", usage = "restore an abandoned the patch set") private boolean restoreChange; @@ -132,6 +136,9 @@ public class ReviewCommand extends BaseCommand { private AbandonedSender.Factory abandonedSenderFactory; @Inject + private DeferredSender.Factory deferredSenderFactory; + + @Inject private RestoredSender.Factory restoredSenderFactory; @Inject @@ -173,6 +180,18 @@ public class ReviewCommand extends BaseCommand { if (submitChange) { throw error("abandon and submit actions are mutually exclusive"); } + if (deferChange) { + throw error("abandon and defer actions are mutually exclusive"); + } + } + + if (deferChange) { + if (restoreChange) { + throw error("defer and restore actions are mutually exclusive"); + } + if (submitChange) { + throw error("defer and submit actions are mutually exclusive"); + } } boolean ok = true; @@ -309,6 +328,15 @@ public class ReviewCommand extends BaseCommand { } } + if (deferChange) { + if (changeControl.canDefer()) { + ChangeUtil.defer(patchSetId, currentUser, changeComment, db, + deferredSenderFactory, hooks); + } else { + throw error("Not permitted to defer change"); + } + } + if (restoreChange) { if (changeControl.canRestore()) { ChangeUtil.restore(patchSetId, currentUser, changeComment, db, |