summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIsmo Haataja <ismo.haataja@digia.com>2013-04-30 15:14:54 +0300
committerOswald Buddenhagen <oswald.buddenhagen@qt.io>2017-04-27 17:57:36 +0200
commit92ae224f7f21e543397c7e880a9ea3f6536cb68c (patch)
tree57c0bc70dd4579de9667c801c5cf3f4e7443a3c8
parent2e4ccb61957e8c485091503fd4e944bd9f5002a4 (diff)
Add new change state "deferred".
Support for new change state "deferred" to be able to discern "really dead" from "postponed" changes. This is equal to "abandoned" for all practical purposes except presentation and filtering. State transitions are possible between "review in progress", "abandoned" and "deferred". And menu bar has new "All/deferred" and "My/Deferred Changes" default filters. ============= initial feedback from dave borowitz was slightly negative, the alternative suggestion being to leave the state machine alone and instead use a custom label to mark the deferred changes. in principle i agree, as long as the visible workflow is not affected. otoh, one can do a reductio ad absurdum on the argument ... e.g., why are MERGED and ABANDONED separate states? technically, they are both just CLOSED, with additional label "integrated". ============= Task-number: QTQAINFRA-598 Change-Id: Ib1801f35eabf6b3b32bf41b0f3b5b496fbcda72a Reviewed-by: Ismo Haataja <ismo.haataja@digia.com>
-rw-r--r--Documentation/cmd-review.txt20
-rw-r--r--Documentation/cmd-stream-events.txt14
-rw-r--r--Documentation/config-gerrit.txt5
-rw-r--r--Documentation/config-hooks.txt11
-rw-r--r--Documentation/config-mail.txt7
-rw-r--r--Documentation/error-change-closed.txt13
-rw-r--r--Documentation/error-no-new-changes.txt4
-rw-r--r--Documentation/json.txt2
-rw-r--r--Documentation/user-search.txt33
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java3
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java9
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java2
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java9
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java17
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java3
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java3
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties3
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties1
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java5
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java9
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java8
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties8
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java1
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties1
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java4
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java40
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css2
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java8
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java13
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java15
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java34
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java12
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/Defer.java149
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java1
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java7
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeDeferredEvent.java28
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java1
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/DeferredSender.java50
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java1
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java16
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java5
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java108
-rw-r--r--gerrit-server/src/main/resources/com/google/gerrit/server/mail/Deferred.vm45
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java48
52 files changed, 756 insertions, 51 deletions
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 65c21db838..b8b4b0cc90 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -13,7 +13,7 @@ SYNOPSIS
[--message <MESSAGE> | -m <MESSAGE>]
[--force-message]
[--submit | -s]
- [--abandon | --restore]
+ [--abandon | --defer | --restore]
[--publish]
[--delete]
[--verified <N>] [--code-review <N>]
@@ -74,24 +74,30 @@ fails because the user is not permitted to change the label.
--abandon::
Abandon the specified change(s).
- (option is mutually exclusive with --submit and --restore)
+ (option is mutually exclusive with --defer, --submit and
+ --restore)
+
+--defer::
+ Defer the specified patch set(s).
+ (option is mutually exclusive with --abandon, --submit and
+ --restore)
--restore::
- Restore the specified abandoned change(s).
- (option is mutually exclusive with --abandon)
+ Restore the specified deferred or abandoned change(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)
--publish::
Publish the specified draft patch set(s).
- (option is mutually exclusive with --submit, --restore, --abandon, and --delete)
+ (option is mutually exclusive with --submit, --restore, --defer, --abandon, and --publish)
--delete::
Delete the specified draft patch set(s).
- (option is mutually exclusive with --submit, --restore, --abandon, and --publish)
+ (option is mutually exclusive with --submit, --restore, --defer, --abandon, and --publish)
--code-review::
--verified::
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 6da0ef0242..0a4c209e49 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -45,7 +45,7 @@ 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*,
-*draft-published*, *change-abandoned*, *change-restored*,
+*draft-published*, *change-deferred*, *change-abandoned*, *change-restored*,
*change-merged*, *merge-failed*, *comment-added*, *ref-updated* and
*reviewer-added*.
@@ -74,6 +74,18 @@ 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]
+
+reason:: Reason for deferring the change.
+
Change Abandoned
^^^^^^^^^^^^^^^^
type:: "change-abandoned"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index c136aa0e33..a79151acac 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1414,6 +1414,11 @@ Optional filename for the merge failed 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.
+
[[hooks.changeRestoredHook]]hooks.changeRestoredHook::
+
Optional filename for the change restored hook, if not specified then
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 1236077d4b..a6b7ec69a5 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -93,6 +93,15 @@ Called whenever a change has been abandoned.
change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --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
~~~~~~~~~~~~~~~
@@ -140,7 +149,7 @@ Gerrit will use the value of hooks.path for the hooks directory.
For the hook filenames, Gerrit will use the values of hooks.patchsetCreatedHook,
hooks.draftPublishedHook, hooks.commentAddedHook, hooks.changeMergedHook,
-hooks.changeAbandonedHook, hooks.changeRestoredHook, hooks.refUpdatedHook,
+hooks.changeAbandonedHook, hooks.changeDeferredHook, hooks.changeRestoredHook,
hooks.refUpdateHook, hooks.reviewerAddedHook and hooks.claSignedHook.
Missing Change URLs
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 8de8e59d82..893bf880b2 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -65,6 +65,13 @@ The `CommitMessageEdited.vm` template will determine the contents of the email
related to a user editing the commit message through the Gerrit UI. It is a
`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+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 3244fb35e2..bb71ca4aa6 100644
--- a/Documentation/error-change-closed.txt
+++ b/Documentation/error-change-closed.txt
@@ -10,7 +10,7 @@ When Pushing a Commit
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
@@ -21,11 +21,12 @@ recommended to do a link:http://www.kernel.org/pub/software/scm/git/docs/git-reb
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.
When Submitting a Review Label
------------------------------
diff --git a/Documentation/error-no-new-changes.txt b/Documentation/error-no-new-changes.txt
index 8e409efa55..570e6d5142 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 3893e3f534..01e839d511 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -54,6 +54,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.
+
comments:: All inline/file comments for this change in <<message,message attributes>>.
trackingIds:: Issue tracking system links in
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 781118f5df..30470e5d43 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -11,17 +11,19 @@ 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 > Drafts | is:draft
-|My > Watched Changes | status:open is:watched
-|My > Starred Changes | is:starred
-|My > Draft Comments | has:draft
-|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 > Drafts | is:draft
+|My > Deferred Changes | owner:'username' status:deferred
+|My > Watched Changes | status:open is:watched
+|My > Starred Changes | is:starred
+|My > Draft Comments | has:draft
+|Open changes in Foo | status:open project:Foo
+|========================================================
Basic Change Search
@@ -240,9 +242,9 @@ True if the change is a draft.
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'>>.
@@ -263,7 +265,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::
+
@@ -273,6 +275,9 @@ status:abandoned::
+
Change has been abandoned.
+status:deferred::
++
+Change has been deferred.
Argument Quoting
----------------
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 beab1d9d45..827992fc64 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
@@ -1,4 +1,5 @@
// Copyright (C) 2008 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -106,6 +107,8 @@ public class PageLinks {
switch (status) {
case ABANDONED:
return "status:abandoned";
+ case DEFERRED:
+ return "status:deferred";
case MERGED:
return "status:merged";
case NEW:
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index fce27a0ffb..ee39c706e0 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -28,6 +28,7 @@ public class ChangeDetail {
protected AccountInfoCache accounts;
protected boolean allowsAnonymous;
protected boolean canAbandon;
+ protected boolean canDefer;
protected boolean canEditCommitMessage;
protected boolean canPublish;
protected boolean canRebase;
@@ -79,6 +80,14 @@ public class ChangeDetail {
canAbandon = a;
}
+ public boolean canDefer() {
+ return canDefer;
+ }
+
+ public void setCanDefer(final boolean a) {
+ canDefer = a;
+ }
+
public boolean canEditCommitMessage() {
return canEditCommitMessage;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index 989bd85a99..ec1721c3f6 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -23,6 +23,7 @@ import java.util.List;
/** A single permission within an {@link AccessSection} of a project. */
public class Permission implements Comparable<Permission> {
public static final String ABANDON = "abandon";
+ public static final String DEFER = "defer";
public static final String CREATE = "create";
public static final String DELETE_DRAFTS = "deleteDrafts";
public static final String EDIT_TOPIC_NAME = "editTopicName";
@@ -51,6 +52,7 @@ public class Permission implements Comparable<Permission> {
NAMES_LC.add(OWNER.toLowerCase());
NAMES_LC.add(READ.toLowerCase());
NAMES_LC.add(ABANDON.toLowerCase());
+ NAMES_LC.add(DEFER.toLowerCase());
NAMES_LC.add(CREATE.toLowerCase());
NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
NAMES_LC.add(FORGE_COMMITTER.toLowerCase());
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
index 1c87c71e84..295d4a8da5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
@@ -1,4 +1,5 @@
// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -85,7 +86,13 @@ public class ReviewResult {
DEST_BRANCH_NOT_FOUND,
/** Not permitted to edit the topic name */
- EDIT_TOPIC_NAME_NOT_PERMITTED
+ EDIT_TOPIC_NAME_NOT_PERMITTED,
+
+ /** Not permitted to defer this change. */
+ DEFER_NOT_PERMITTED,
+
+ /** Review operation invalid because change is not deferred. */
+ CHANGE_NOT_DEFERRED
}
protected Type type;
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 35adf4ba8b..d1fd94ef72 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
@@ -263,6 +263,10 @@ public class Dispatcher {
return PageLinks.toChangeQuery("is:starred");
}
+ if (matchExact("mine,deferred", token)) {
+ return PageLinks.toChangeQuery("owner:" + Gerrit.getUserAccount().getUserName() + " status:deferred");
+ }
+
if (matchExact("mine,drafts", token)) {
return PageLinks.toChangeQuery("is:draft");
}
@@ -275,6 +279,10 @@ public class Dispatcher {
return PageLinks.toChangeQuery("is:watched status:open", skip(token));
}
+ if (matchPrefix("all,deferred", token)) {
+ return PageLinks.toChangeQuery("status:deferred", skip(token));
+ }
+
return null;
}
@@ -322,6 +330,15 @@ public class Dispatcher {
s.substring(c + 1));
}
+ if (matchPrefix("project,deferred,", token)) {
+ final String s = skip(token);
+ final int c = s.indexOf(',');
+ Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
+ return PageLinks.toChangeQuery( //
+ "status:deferred " + op("project", proj.get()), //
+ s.substring(c + 1));
+ }
+
return null;
}
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 26a58b7b41..9558ea7002 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
@@ -630,6 +630,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());
@@ -638,6 +639,8 @@ public class Gerrit implements EntryPoint {
addLink(m, C.menuMyChanges(), PageLinks.MINE);
addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("is:draft"));
addLink(m, C.menuMyDraftComments(), 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 2b16e86d7a..6134b9b46a 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();
String menuMyDraftComments();
@@ -107,6 +109,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 5cb3635e7a..611b357ca9 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,6 +39,7 @@ menuAllStaged = Staged
menuAllIntegrating = Integrating
menuAllMerged = Merged
menuAllAbandoned = Abandoned
+menuAllDeferred = Deferred
menuMine = My
menuMyChanges = Changes
@@ -46,6 +47,7 @@ menuMyDrafts = Drafts
menuMyStarredChanges = Starred Changes
menuMyWatchedChanges = Watched Changes
menuMyDraftComments = Draft Comments
+menuMyDeferredChanges = Deferred Changes
menuDiff = Differences
menuDiffCommit = Commit Message
@@ -89,6 +91,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/JumpKeys.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
index a41ff02756..022b349349 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/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 054b3fc5f2..729ec1d0d2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -105,6 +105,7 @@ public class SearchSuggestOracle extends HighlightSuggestOracle {
suggestions.add("is:submitted");
suggestions.add("is:merged");
suggestions.add("is:abandoned");
+ suggestions.add("is:deferred");
suggestions.add("is:integrating");
suggestions.add("is:staged");
@@ -115,6 +116,7 @@ public class SearchSuggestOracle extends HighlightSuggestOracle {
suggestions.add("status:closed");
suggestions.add("status:merged");
suggestions.add("status:abandoned");
+ suggestions.add("status:deferred");
suggestions.add("status:integrating");
suggestions.add("status:staged");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 917c078d60..f1d12b3609 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -1,4 +1,5 @@
// Copyright (C) 2008 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -103,6 +104,7 @@ public interface AccountConstants extends Constants {
String watchedProjectColumnAllComments();
String watchedProjectColumnSubmittedChanges();
String watchedProjectColumnAbandonedChanges();
+ String watchedProjectColumnDeferredChanges();
String contactFieldFullName();
String contactFieldEmail();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 7175b6a09b..ce05058541 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -101,6 +101,7 @@ watchedProjectColumnNewPatchSets = New Patch Sets
watchedProjectColumnAllComments = All Comments
watchedProjectColumnSubmittedChanges = Submitted Changes
watchedProjectColumnAbandonedChanges = Abandoned Changes
+watchedProjectColumnDeferredChanges = Deferred Changes
contactFieldFullName = Full Name
contactFieldEmail = Preferred Email
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
index e2286478dc..b8c94323e6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
@@ -1,4 +1,5 @@
// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -57,11 +58,13 @@ public class MyWatchesTable extends FancyFlexTable<AccountProjectWatchInfo> {
table.setText(1, 2, Util.C.watchedProjectColumnAllComments());
table.setText(1, 3, Util.C.watchedProjectColumnSubmittedChanges());
table.setText(1, 4, Util.C.watchedProjectColumnAbandonedChanges());
+ table.setText(1, 5, Util.C.watchedProjectColumnDeferredChanges());
fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().dataHeader());
fmt.addStyleName(1, 1, Gerrit.RESOURCES.css().dataHeader());
fmt.addStyleName(1, 2, Gerrit.RESOURCES.css().dataHeader());
fmt.addStyleName(1, 3, Gerrit.RESOURCES.css().dataHeader());
fmt.addStyleName(1, 4, Gerrit.RESOURCES.css().dataHeader());
+ fmt.addStyleName(1, 5, Gerrit.RESOURCES.css().dataHeader());
}
public void deleteChecked() {
@@ -143,6 +146,7 @@ public class MyWatchesTable extends FancyFlexTable<AccountProjectWatchInfo> {
addNotifyButton(AccountProjectWatch.NotifyType.ALL_COMMENTS, info, row, 5);
addNotifyButton(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES, info, row, 6);
addNotifyButton(AccountProjectWatch.NotifyType.ABANDONED_CHANGES, info, row, 7);
+ addNotifyButton(AccountProjectWatch.NotifyType.DEFERRED_CHANGES, info, row, 8);
final FlexCellFormatter fmt = table.getFlexCellFormatter();
fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
@@ -152,6 +156,7 @@ public class MyWatchesTable extends FancyFlexTable<AccountProjectWatchInfo> {
fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell());
fmt.addStyleName(row, 6, Gerrit.RESOURCES.css().dataCell());
fmt.addStyleName(row, 7, Gerrit.RESOURCES.css().dataCell());
+ fmt.addStyleName(row, 8, Gerrit.RESOURCES.css().dataCell());
setRowItem(row, info);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index e56642bad2..6666d23026 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -33,7 +33,14 @@ public class ChangeApi {
call(id, "abandon").post(input, cb);
}
- /** Restore a previously abandoned change to be open again. */
+ /** Defer the change, ending its review. */
+ public static void defer(int id, String msg, AsyncCallback<ChangeInfo> cb) {
+ Input input = Input.create();
+ input.message(emptyToNull(msg));
+ call(id, "defer").post(input, cb);
+ }
+
+ /** Restore a previously abandoned or deferred change to be open again. */
public static void restore(int id, String msg, AsyncCallback<ChangeInfo> cb) {
Input input = Input.create();
input.message(emptyToNull(msg));
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 63785805d4..e1f4454cac 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
@@ -23,6 +23,7 @@ public interface ChangeConstants extends Constants {
String statusLongMerged();
String statusLongAbandoned();
String statusLongDraft();
+ String statusLongDeferred();
String myDashboardTitle();
String unknownDashboardTitle();
@@ -35,6 +36,7 @@ public interface ChangeConstants extends Constants {
String draftsHeading();
String allOpenChanges();
String allAbandonedChanges();
+ String allDeferredChanges();
String allMergedChanges();
String changeTableColumnSubject();
@@ -146,6 +148,12 @@ public interface ChangeConstants extends Constants {
String baseDiffItem();
String autoMerge();
+ String buttonDeferChangeBegin();
+ String buttonDeferChangeSend();
+
+ 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 2f4120250d..a397869899 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
@@ -3,6 +3,7 @@ statusLongSubmitted = Submitted, Merge Pending
statusLongMerged = Merged
statusLongAbandoned = Abandoned
statusLongDraft = Draft
+statusLongDeferred = Deferred
starredHeading = Starred Changes
watchedHeading = Open Changes of Watched Projects
@@ -14,6 +15,7 @@ outgoingReviews = Outgoing reviews
recentlyClosed = Recently closed
allOpenChanges = All open changes
allAbandonedChanges = All abandoned changes
+allDeferredChanges = All deferred changes
allMergedChanges = All merged changes
changeTableColumnSubject = Subject
@@ -113,6 +115,12 @@ autoMerge = Auto Merge
buttonRebaseChange = Rebase Change
+buttonDeferChangeBegin = Defer Change
+buttonDeferChangeSend = Defer Change
+
+headingDeferMessage = Defer Message:
+deferChangeTitle = Code Review - Defer Change
+
buttonRevertChangeBegin = Revert Change
buttonRevertChangeSend = Revert Change
headingRevertMessage = Revert Commit Message:
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 7e201da5f0..7f5cc8c778 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
@@ -22,6 +22,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 15d0b53878..1fa14e5a66 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 @@ accountDashboardTitle = Code Review Dashboard 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/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index c69f915ea3..f113c73fdf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -1,4 +1,5 @@
// Copyright (C) 2008 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -193,7 +194,8 @@ public class ChangeScreen extends Screen
dependsOn.setChangeRowFormatter(new ChangeTable.ChangeRowFormatter() {
@Override
public String getRowStyle(ChangeInfo c) {
- if (! c.isLatest() || Change.Status.ABANDONED.equals(c.getStatus())) {
+ if (! c.isLatest() || Change.Status.ABANDONED.equals(c.getStatus())
+ || Change.Status.DEFERRED.equals(c.getStatus())) {
return Gerrit.RESOURCES.css().outdated();
}
return null;
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 bdea2ec1be..ec737ea901 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
@@ -474,6 +474,46 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
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 ActionDialog(b, false, Util.C.deferChangeTitle(),
+ Util.C.headingDeferMessage()) {
+ {
+ sendButton.setText(Util.C.buttonDeferChangeSend());
+ }
+
+ @Override
+ public void onSend() {
+ // TODO: once the other users of ActionDialog have converted to
+ // REST APIs, we can use createCallback() rather than providing
+ // them directly.
+ ChangeApi.defer(changeDetail.getChange().getChangeId(),
+ getMessageText(), new GerritCallback<ChangeInfo>() {
+ @Override
+ public void onSuccess(ChangeInfo result) {
+ sent = true;
+ Gerrit.display(PageLinks.toChange(new Change.Id(result
+ ._number())));
+ hide();
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ enableButtons(true);
+ super.onFailure(caught);
+ }
+ });
+ }
+ }.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/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
index 590ad876a3..96593166f1 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
@@ -60,6 +60,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 b2880d022c..678698f502 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
@@ -1512,6 +1512,8 @@ a:hover.downloadLink {
}
+
+
/** AccountGroupInfoScreen **/
.groupUUIDPanel {
margin-bottom: 10px;
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 d3c1f69246..8a4efd6a22 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
@@ -127,10 +127,14 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
detail.setAllowsAnonymous(control.forUser(anonymousUser).isVisible(db));
detail.setCanAbandon(change.getStatus() != Change.Status.DRAFT
- && change.getStatus().isOpen()
+ && (change.getStatus() == Change.Status.DEFERRED || change.getStatus().isOpen())
&& control.canAbandon());
+ detail.setCanDefer(change.getStatus() != Change.Status.DRAFT
+ && (change.getStatus() == Change.Status.ABANDONED || change.getStatus().isOpen())
+ && control.canDefer());
detail.setCanPublish(control.canPublish(db));
- detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED
+ detail.setCanRestore(
+ (change.getStatus() == Change.Status.ABANDONED || change.getStatus() == Change.Status.DEFERRED)
&& control.canRestore()
&& ProjectUtil.branchExists(repoManager, change.getDest()));
detail.setCanDeleteDraft(control.canDeleteDraft(db));
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 bf0af7f59a..bb33a7becc 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(0700, site.tmp_dir);
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/client/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
index c7f52b0b76..f7ba27fef9 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
@@ -23,7 +23,7 @@ public final class AccountProjectWatch {
public enum NotifyType {
NEW_CHANGES, NEW_PATCHSETS, ALL_COMMENTS, SUBMITTED_CHANGES,
- ABANDONED_CHANGES, ALL
+ ABANDONED_CHANGES, DEFERRED_CHANGES, ALL
}
public static final String FILTER_ALL = "*";
@@ -116,6 +116,9 @@ public final class AccountProjectWatch {
@Column(id = 6)
protected boolean notifyAbandonedChanges;
+ @Column(id = 7)
+ protected boolean notifyDeferredChanges;
+
protected AccountProjectWatch() {
}
@@ -156,6 +159,9 @@ public final class AccountProjectWatch {
case ABANDONED_CHANGES:
return notifyAbandonedChanges;
+ case DEFERRED_CHANGES:
+ return notifyDeferredChanges;
+
case ALL:
break;
}
@@ -184,12 +190,17 @@ public final class AccountProjectWatch {
notifyAbandonedChanges = v;
break;
+ case DEFERRED_CHANGES:
+ notifyDeferredChanges = v;
+ break;
+
case ALL:
notifyNewChanges = v;
notifyNewPatchSets = v;
notifyAllComments = v;
notifySubmittedChanges = v;
notifyAbandonedChanges = v;
+ notifyDeferredChanges = v;
break;
}
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index cf63c9f0c5..98fbf37cfe 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -219,13 +219,14 @@ public final class Change {
* <p>
* This is the default state assigned to a change when it is first created
* in the database. A change stays in the NEW state throughout its review
- * cycle, until the change is submitted or abandoned.
+ * cycle, until the change is submitted, abandoned or deferred.
*
* <p>
* Changes in the NEW state can be moved to:
* <ul>
* <li>{@link #SUBMITTED} - when the Submit Patch Set action is used;
* <li>{@link #ABANDONED} - when the Abandon action is used.
+ * <li>{@link #DEFERRED} - when the Defer action is used.
* </ul>
*/
NEW(STATUS_NEW),
@@ -309,7 +310,17 @@ public final class Change {
* a replacement patch set, and it cannot be merged. Draft comments however
* may be published, permitting reviewers to send constructive feedback.
*/
- ABANDONED('A');
+ ABANDONED('A'),
+
+ /**
+ * Change is closed, but was not submitted to its destination branch.
+ *
+ * <p>
+ * Once a change 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 2d546014e9..f3765ed755 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
@@ -1,4 +1,5 @@
// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -34,6 +35,7 @@ import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.data.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.ChangeRestoredEvent;
@@ -177,6 +179,9 @@ public class ChangeHookRunner implements ChangeHooks, LifecycleListener {
/** Filename of the change abandoned hook. */
private final File changeAbandonedHook;
+ /** Filename of the change deferred hook. */
+ private final File changeDeferredHook;
+
/** Filename of the change restored hook. */
private final File changeRestoredHook;
@@ -251,6 +256,7 @@ public class ChangeHookRunner implements ChangeHooks, LifecycleListener {
changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath());
mergeFailedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "mergeFailed", "merge-failed")).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());
reviewerAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "reviewerAddedHook", "reviewer-added")).getPath());
@@ -489,6 +495,34 @@ public class ChangeHookRunner implements ChangeHooks, LifecycleListener {
runHook(change.getProject(), changeAbandonedHook, args);
}
+ /**
+ * 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 ReviewDb db) throws OrmException {
+ final ChangeDeferredEvent event = new ChangeDeferredEvent();
+
+ event.change = eventFactory.asChangeAttribute(change);
+ event.deferrer = eventFactory.asAccountAttribute(account);
+ event.reason = reason;
+ fireEvent(change, event, db);
+
+ 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, "--topic", event.change.topic);
+ addArg(args, "--deferrer", getDisplayName(account));
+ addArg(args, "--reason", reason == null ? "" : reason);
+
+ runHook(change.getProject(), changeDeferredHook, args);
+ }
+
public void doChangeRestoredHook(final Change change, final Account account,
final String reason, final ReviewDb db) throws OrmException {
final ChangeRestoredEvent event = new ChangeRestoredEvent();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index 48a52a02f5..f05c10b91d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -1,4 +1,5 @@
// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -106,6 +107,17 @@ public interface ChangeHooks {
String reason, ReviewDb db) throws OrmException;
/**
+ * 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.
+ * @throws OrmException
+ */
+ public void doChangeDeferredHook(Change change, Account account,
+ String reason, ReviewDb db) throws OrmException;
+
+ /**
* Fire the Change Restored Hook.
*
* @param change The change itself.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index 6011ab087a..01df4d006e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -1,4 +1,5 @@
// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -41,6 +42,11 @@ public final class DisabledChangeHooks implements ChangeHooks {
}
@Override
+ public void doChangeDeferredHook(Change change, Account account,
+ String reason, ReviewDb db) {
+ }
+
+ @Override
public void doChangeMergedHook(Change change, Account account,
PatchSet patchSet, ReviewDb db) {
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index adb8501486..596a727e62 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -86,7 +86,7 @@ public class Abandon implements RestModifyView<ChangeResource, Input> {
Change change = req.getChange();
if (!control.canAbandon()) {
throw new AuthException("abandon not permitted");
- } else if (!change.getStatus().isOpen()) {
+ } else if (!change.getStatus().isOpen() && change.getStatus() != Change.Status.DEFERRED) {
throw new ResourceConflictException("change is " + status(change));
}
@@ -101,7 +101,7 @@ public class Abandon implements RestModifyView<ChangeResource, Input> {
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
- if (change.getStatus().isOpen()) {
+ if (change.getStatus().isOpen() || change.getStatus() == Change.Status.DEFERRED) {
change.setStatus(Change.Status.ABANDONED);
ChangeUtil.updated(change);
return change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Defer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Defer.java
new file mode 100644
index 0000000000..833038b3e7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Defer.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 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.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.Defer.Input;
+import com.google.gerrit.server.mail.DeferredSender;
+import com.google.gerrit.server.mail.ReplyToChangeSender;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+
+public class Defer implements RestModifyView<ChangeResource, Input> {
+ private static final Logger log = LoggerFactory.getLogger(Defer.class);
+
+ private final ChangeHooks hooks;
+ private final DeferredSender.Factory deferredSenderFactory;
+ private final Provider<ReviewDb> dbProvider;
+ private final ChangeJson json;
+
+ public static class Input {
+ @DefaultInput
+ public String message;
+ }
+
+ @Inject
+ Defer(ChangeHooks hooks,
+ DeferredSender.Factory deferredSenderFactory,
+ Provider<ReviewDb> dbProvider,
+ ChangeJson json) {
+ this.hooks = hooks;
+ this.deferredSenderFactory = deferredSenderFactory;
+ this.dbProvider = dbProvider;
+ this.json = json;
+ }
+
+ @Override
+ public Object apply(ChangeResource req, Input input)
+ throws BadRequestException, AuthException,
+ ResourceConflictException, Exception {
+ ChangeControl control = req.getControl();
+ IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
+ Change change = req.getChange();
+ if (!control.canDefer()) {
+ throw new AuthException("defer not permitted");
+ } else if (!change.getStatus().isOpen() && change.getStatus() != Change.Status.ABANDONED) {
+ throw new ResourceConflictException("change is " + status(change));
+ }
+
+ ChangeMessage message;
+ ReviewDb db = dbProvider.get();
+ db.changes().beginTransaction(change.getId());
+ try {
+ change = db.changes().atomicUpdate(
+ change.getId(),
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus().isOpen() || change.getStatus() == Change.Status.ABANDONED) {
+ change.setStatus(Change.Status.DEFERRED);
+ ChangeUtil.updated(change);
+ return change;
+ }
+ return null;
+ }
+ });
+ if (change == null) {
+ throw new ResourceConflictException("change is "
+ + status(db.changes().get(req.getChange().getId())));
+ }
+ message = newMessage(input, caller, change);
+ db.changeMessages().insert(Collections.singleton(message));
+ new ApprovalsUtil(db).syncChangeStatus(change);
+ db.commit();
+ } finally {
+ db.rollback();
+ }
+
+ try {
+ ReplyToChangeSender cm = deferredSenderFactory.create(change);
+ cm.setFrom(caller.getAccountId());
+ cm.setChangeMessage(message);
+ cm.send();
+ } catch (Exception e) {
+ log.error("Cannot email update for change " + change.getChangeId(), e);
+ }
+ hooks.doChangeDeferredHook(change,
+ caller.getAccount(),
+ Strings.emptyToNull(input.message),
+ db);
+ return json.format(change);
+ }
+
+ private ChangeMessage newMessage(Input input, IdentifiedUser caller,
+ Change change) throws OrmException {
+ StringBuilder msg = new StringBuilder();
+ msg.append("Deferred");
+ if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
+ msg.append("\n\n");
+ msg.append(input.message.trim());
+ }
+
+ ChangeMessage message = new ChangeMessage(
+ new ChangeMessage.Key(
+ change.getId(),
+ ChangeUtil.messageUUID(dbProvider.get())),
+ caller.getAccountId(),
+ change.getLastUpdatedOn(),
+ change.currentPatchSetId());
+ message.setMessage(msg.toString());
+ return message;
+ }
+
+ private static String status(Change change) {
+ return change != null ? change.getStatus().name().toLowerCase() : "deleted";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 22e0863544..ff0d97f86d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -51,6 +51,7 @@ public class Module extends RestApiModule {
put(CHANGE_KIND, "topic").to(PutTopic.class);
delete(CHANGE_KIND, "topic").to(PutTopic.class);
post(CHANGE_KIND, "abandon").to(Abandon.class);
+ post(CHANGE_KIND, "defer").to(Defer.class);
post(CHANGE_KIND, "restore").to(Restore.class);
post(CHANGE_KIND, "revert").to(Revert.class);
post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index afb58f9f99..57f6b6670d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -1,4 +1,5 @@
// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -73,7 +74,8 @@ public class Restore implements RestModifyView<ChangeResource, Input> {
Change change = req.getChange();
if (!control.canRestore()) {
throw new AuthException("restore not permitted");
- } else if (change.getStatus() != Status.ABANDONED) {
+ } else if (change.getStatus() != Status.ABANDONED
+ && change.getStatus() != Status.DEFERRED) {
throw new ResourceConflictException("change is " + status(change));
}
@@ -86,7 +88,8 @@ public class Restore implements RestModifyView<ChangeResource, Input> {
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.setStatus(Change.Status.NEW);
ChangeUtil.updated(change);
return change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index d7bf5a36b5..e7d146ae2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -1,4 +1,5 @@
// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -252,6 +253,11 @@ public class RebaseChange {
+ depChange.getKey().toString());
}
+ if (depChange.getStatus() == Status.DEFERRED) {
+ throw new IOException("Cannot rebase a change with an deferred parent: "
+ + depChange.getKey().toString());
+ }
+
if (depChange.getStatus().isOpen()) {
if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
throw new IOException(
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..83ba77f6b6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeDeferredEvent.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2010 The Android Open Source Project,
+// Copyright (C) 2014 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;
+
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
+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 954036ac50..fd63c118c6 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
@@ -2224,6 +2224,7 @@ public class ReceiveCommits {
if (change.getStatus() == Change.Status.MERGED ||
change.getStatus() == Change.Status.ABANDONED ||
+ change.getStatus() == Change.Status.DEFERRED ||
!change.getDest().get().equals(refName)) {
// If it's already merged or the commit is not aimed for
// this change's destination, don't make further updates.
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..dbb39f1f85
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeferredSender.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2009 The Android Open Source Project,
+// Copyright (C) 2014 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.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.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 extends
+ ReplyToChangeSender.Factory<DeferredSender> {
+ 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();
+ includeWatchers(NotifyType.DEFERRED_CHANGES);
+ includeWatchers(NotifyType.ALL_COMMENTS);
+ }
+
+ @Override
+ protected void formatChange() throws EmailException {
+ appendText(velocifyFile("Deferred.vm"));
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
index d678136335..ef09d50761 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
@@ -21,6 +21,7 @@ public class EmailModule extends FactoryModule {
@Override
protected void configure() {
factory(AbandonedSender.Factory.class);
+ factory(DeferredSender.Factory.class);
factory(CommentSender.Factory.class);
factory(RevertedSender.Factory.class);
factory(RestoredSender.Factory.class);
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 9f20a711f1..ab6a6f0b39 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
@@ -212,9 +212,23 @@ public class ChangeControl {
|| getRefControl().canRebase();
}
+ /** Can this user defer this change? */
+ public boolean canDefer() {
+ boolean userCan = isOwner() // owner (aka creator) of the change can defer
+ || getRefControl().isOwner() // branch owner can defer
+ || getProjectControl().isOwner() // project owner can defer
+ || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+ || getRefControl().canDefer() // user can defer a specific ref
+ ;
+
+ // Cannot defer changes that are being 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
+ return (canAbandon() || canDefer()) // Anyone who can abandon or defer the change can restore it back
&& getRefControl().canUpload(); // as long as you can upload too
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 0ebec81c68..320df703ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -350,6 +350,11 @@ public class RefControl {
return canPerform(Permission.ABANDON);
}
+ /** @return true if this user can defer a change for this ref */
+ public boolean canDefer() {
+ return canPerform(Permission.DEFER);
+ }
+
/** @return true if this user can remove a reviewer for a change. */
public boolean canRemoveReviewer() {
return canPerform(Permission.REMOVE_REVIEWER);
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 e6251bc1a6..985eede34b 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
@@ -1,4 +1,5 @@
// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2014 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.
@@ -77,7 +78,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")
@@ -90,6 +92,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);
@@ -318,6 +328,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,
@@ -451,11 +505,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")
@@ -463,7 +565,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..c5eeea83a0
--- /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) 2014 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 b48885fccc..a26dfd7259 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
@@ -32,6 +32,7 @@ import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.Abandon;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.Defer;
import com.google.gerrit.server.change.PostReview;
import com.google.gerrit.server.change.Restore;
import com.google.gerrit.server.change.RevisionResource;
@@ -101,7 +102,10 @@ public class ReviewCommand extends SshCommand {
@Option(name = "--abandon", usage = "abandon the specified change(s)")
private boolean abandonChange;
- @Option(name = "--restore", usage = "restore the specified abandoned change(s)")
+ @Option(name = "--defer", usage = "defer the specified change(s)")
+ private boolean deferChange;
+
+ @Option(name = "--restore", usage = "restore the specified abandoned or deferred change(s)")
private boolean restoreChange;
@Option(name = "--submit", aliases = "-s", usage = "submit the specified patch set(s)")
@@ -155,6 +159,9 @@ public class ReviewCommand extends SshCommand {
private Provider<Abandon> abandonProvider;
@Inject
+ private Provider<Defer> deferProvider;
+
+ @Inject
private Provider<PostReview> reviewProvider;
@Inject
@@ -181,6 +188,9 @@ public class ReviewCommand extends SshCommand {
if (submitChange) {
throw error("abandon and submit actions are mutually exclusive");
}
+ if (deferChange) {
+ throw error("abandon and defer actions are mutually exclusive");
+ }
if (publishPatchSet) {
throw error("abandon and publish actions are mutually exclusive");
}
@@ -191,6 +201,20 @@ public class ReviewCommand extends SshCommand {
throw error("abandon and stage 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");
+ }
+ if (publishPatchSet) {
+ throw error("defer and publish actions are mutually exclusive");
+ }
+ if (deleteDraftPatchSet) {
+ throw error("defer and delete actions are mutually exclusive");
+ }
+ }
if (publishPatchSet) {
if (restoreChange) {
throw error("publish and restore actions are mutually exclusive");
@@ -261,9 +285,9 @@ public class ReviewCommand extends SshCommand {
review.labels.putAll(customLabels);
// If review labels are being applied, the comment will be included
- // on the review note. We don't need to add it again on the abandon
- // or restore comment.
- if (!review.labels.isEmpty() && (abandonChange || restoreChange)) {
+ // on the review note. We don't need to add it again on the abandon,
+ // defer or restore comment.
+ if (!review.labels.isEmpty() && (abandonChange || deferChange || restoreChange)) {
changeComment = null;
}
@@ -284,6 +308,18 @@ public class ReviewCommand extends SshCommand {
} catch (ResourceConflictException e) {
writeError("error: " + parseError(Type.CHANGE_IS_CLOSED) + "\n");
}
+ } else if (deferChange) {
+ final Defer defer = deferProvider.get();
+ final Defer.Input input = new Defer.Input();
+ input.message = changeComment;
+ applyReview(ctl, patchSet, review);
+ try {
+ defer.apply(new ChangeResource(ctl), input);
+ } catch (AuthException e) {
+ writeError("error: " + parseError(Type.DEFER_NOT_PERMITTED) + "\n");
+ } catch (ResourceConflictException e) {
+ writeError("error: " + parseError(Type.CHANGE_IS_CLOSED) + "\n");
+ }
} else if (restoreChange) {
final Restore restore = restoreProvider.get();
final Restore.Input input = new Restore.Input();
@@ -354,6 +390,8 @@ public class ReviewCommand extends SshCommand {
switch (type) {
case ABANDON_NOT_PERMITTED:
return "not permitted to abandon change";
+ case DEFER_NOT_PERMITTED:
+ return "not permitted to defer change";
case RESTORE_NOT_PERMITTED:
return "not permitted to restore change";
case SUBMIT_NOT_PERMITTED:
@@ -364,6 +402,8 @@ public class ReviewCommand extends SshCommand {
return "change is closed";
case CHANGE_NOT_ABANDONED:
return "change is not abandoned";
+ case CHANGE_NOT_DEFERRED:
+ return "change is not deferred";
case PUBLISH_NOT_PERMITTED:
return "not permitted to publish change";
case DELETE_NOT_PERMITTED: