summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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,