summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIsmo Haataja <ismo.haataja@digia.com>2013-04-30 15:14:54 +0300
committerIsmo Haataja <ismo.haataja@digia.com>2013-08-22 07:42:01 +0200
commit8a29078540e2d9afb8fe1ff381424df5979d8d81 (patch)
treeaee61f2446422712ebf4e54f368fd4a2be384ec4
parent42d1d7d1aabd0dcfd0a0439c57c4427e5790136e (diff)
Add new change state "deferred".v2.2.1-based
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. Task-number: QTQAINFRA-598 Change-Id: I57b4f716fb826c5f246d02463b1ed9c75c4d04ae Reviewed-by: Sergio Ahumada <sergio.ahumada@digia.com> Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@digia.com>
-rw-r--r--Documentation/cmd-review.txt18
-rw-r--r--Documentation/cmd-stream-events.txt13
-rw-r--r--Documentation/config-gerrit.txt5
-rw-r--r--Documentation/config-hooks.txt13
-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.txt32
-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/ChangeManageService.java4
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/CommonDetail.java9
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/TopicManageService.java4
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java18
-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/GerritCss.java2
-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/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/ChangeSetComplexDisclosurePanel.java19
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java19
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/TopicConstants.java6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/TopicConstants.properties6
-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.css25
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java5
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java8
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java1
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeferChange.java125
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/DeferTopic.java90
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicDetailFactory.java5
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicManageServiceImpl.java8
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/topic/TopicModule.java1
-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/AbstractEntity.java11
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java31
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java82
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/TopicUtil.java77
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeDeferredEvent.java24
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java9
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/DeferredSender.java46
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java18
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/TopicControl.java12
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java107
-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.java28
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,