Initial revision Change-Id: I5b1426c64223da76527d954e8bbee6763667a1be
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c650a1d --- /dev/null +++ b/.gitignore
@@ -0,0 +1,4 @@ +/target +/.classpath +/.settings +/.project
diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..11069ed --- /dev/null +++ b/LICENSE
@@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +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.
diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d5a58af --- /dev/null +++ b/pom.xml
@@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +Copyright (C) 2013 The Android Open Source Project + +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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>com.googlesource.gerrit.plugins</groupId> + <artifactId>wip-plugin</artifactId> + <packaging>jar</packaging> + <version>1.0</version> + <properties> + <Gerrit-ApiType>plugin</Gerrit-ApiType> + <Gerrit-ApiVersion>2.8-SNAPSHOT</Gerrit-ApiVersion> + </properties> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>2.4</version> + <configuration> + <archive> + <manifestEntries> + <Gerrit-PluginName>wip</Gerrit-PluginName> + + <Gerrit-Module>com.googlesource.gerrit.plugins.wip.Module</Gerrit-Module> + <Gerrit-HttpModule>com.googlesource.gerrit.plugins.wip.HttpModule</Gerrit-HttpModule> + <Gerrit-SshModule>com.googlesource.gerrit.plugins.wip.SshModule</Gerrit-SshModule> + + <Implementation-Vendor>Gerrit Code Review</Implementation-Vendor> + <Implementation-URL>http://code.google.com/p/gerrit/</Implementation-URL> + + <Implementation-Title>Plugin ${project.artifactId}</Implementation-Title> + <Implementation-Version>${project.version}</Implementation-Version> + + <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType> + <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion> + </manifestEntries> + </archive> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>2.3.2</version> + <configuration> + <source>1.6</source> + <target>1.6</target> + <encoding>UTF-8</encoding> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.google.gerrit</groupId> + <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId> + <version>${Gerrit-ApiVersion}</version> + <scope>provided</scope> + </dependency> + </dependencies> + + <repositories> + <repository> + <id>gerrit-api-repository</id> + <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url> + </repository> + </repositories> +</project>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/wip/BaseAction.java b/src/main/java/com/googlesource/gerrit/plugins/wip/BaseAction.java new file mode 100644 index 0000000..47c5f38 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/wip/BaseAction.java
@@ -0,0 +1,115 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.googlesource.gerrit.plugins.wip; + +import java.util.Collections; + +import com.google.common.base.Strings; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.ChangeMessage; +import com.google.gerrit.reviewdb.client.Change.Id; +import com.google.gerrit.reviewdb.client.Change.Status; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gwtorm.server.AtomicUpdate; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +abstract class BaseAction { + static class Input { + String message; + } + + private final Provider<ReviewDb> dbProvider; + private final Provider<CurrentUser> userProvider; + + @Inject + BaseAction(Provider<ReviewDb> dbProvider, + Provider<CurrentUser> userProvider) { + this.dbProvider = dbProvider; + this.userProvider = userProvider; + } + + protected void changeStatus(Change change, Input input, final Status from, + final Status to) throws OrmException, + ResourceConflictException { + ReviewDb db = dbProvider.get(); + Id changeId = change.getId(); + db.changes().beginTransaction(changeId); + try { + change = db.changes().atomicUpdate( + changeId, + new AtomicUpdate<Change>() { + @Override + public Change update(Change change) { + if (change.getStatus() == from) { + change.setStatus(to); + ChangeUtil.updated(change); + return change; + } + return null; + } + }); + + if (change == null) { + throw new ResourceConflictException("change is " + + status(db.changes().get(changeId))); + } + + db.changeMessages().insert(Collections.singleton( + newMessage(input, change))); + new ApprovalsUtil(db).syncChangeStatus(change); + db.commit(); + } finally { + db.rollback(); + } + } + + private ChangeMessage newMessage(Input input, + Change change) throws OrmException { + StringBuilder msg = new StringBuilder( + "Change " + + change.getId().get() + + ": " + + ((change.getStatus() == Status.WORKINPROGRESS) + ? "Work In Progress" + : "Ready For Review")); + if (!Strings.nullToEmpty(input.message).trim().isEmpty()) { + msg.append("\n\n"); + msg.append(input.message.trim()); + } + + ChangeMessage message = new ChangeMessage( + new ChangeMessage.Key( + change.getId(), + ChangeUtil.messageUUID(dbProvider.get())), + ((IdentifiedUser)userProvider.get()).getAccountId(), + change.getLastUpdatedOn(), + change.currentPatchSetId()); + message.setMessage(msg.toString()); + return message; + } + + protected static String status(Change change) { + return change != null + ? change.getStatus().name().toLowerCase() + : "deleted"; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/wip/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/wip/HttpModule.java new file mode 100644 index 0000000..a3fea87 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/wip/HttpModule.java
@@ -0,0 +1,28 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.googlesource.gerrit.plugins.wip; + +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.webui.JavaScriptPlugin; +import com.google.gerrit.extensions.webui.WebUiPlugin; +import com.google.gerrit.httpd.plugins.HttpPluginModule; + +public class HttpModule extends HttpPluginModule { + @Override + protected void configureServlets() { + DynamicSet.bind(binder(), WebUiPlugin.class) + .toInstance(new JavaScriptPlugin("wip.js")); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/wip/Module.java b/src/main/java/com/googlesource/gerrit/plugins/wip/Module.java new file mode 100644 index 0000000..6a02eb4 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/wip/Module.java
@@ -0,0 +1,42 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.googlesource.gerrit.plugins.wip; + +import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND; +import static com.googlesource.gerrit.plugins.wip.WorkInProgressCapability.WORK_IN_PROGRESS; + +import com.google.gerrit.extensions.annotations.Exports; +import com.google.gerrit.extensions.config.CapabilityDefinition; +import com.google.gerrit.extensions.restapi.RestApiModule; +import com.google.inject.AbstractModule; + +public class Module extends AbstractModule { + + @Override + protected void configure() { + bind(CapabilityDefinition.class) + .annotatedWith(Exports.named(WORK_IN_PROGRESS)) + .to(WorkInProgressCapability.class); + install(new RestApiModule() { + @Override + protected void configure() { + post(REVISION_KIND, "set-wip") + .to(WorkInProgressAction.class); + post(REVISION_KIND, "set-ready") + .to(ReadyForReviewAction.class); + } + }); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/wip/ReadyForReviewAction.java b/src/main/java/com/googlesource/gerrit/plugins/wip/ReadyForReviewAction.java new file mode 100644 index 0000000..be68997 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/wip/ReadyForReviewAction.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.googlesource.gerrit.plugins.wip; + +import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Change.Status; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.change.RevisionResource; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +@RequiresCapability(WorkInProgressCapability.WORK_IN_PROGRESS) +class ReadyForReviewAction extends BaseAction implements + UiAction<RevisionResource>, + RestModifyView<RevisionResource, BaseAction.Input> { + + @Inject + ReadyForReviewAction(Provider<ReviewDb> dbProvider, + Provider<CurrentUser> userProvider) { + super(dbProvider, userProvider); + } + + @Override + public Object apply(RevisionResource rsrc, Input input) + throws ResourceConflictException, OrmException { + Change change = rsrc.getChange(); + if (change.getStatus() != Status.WORKINPROGRESS) { + throw new ResourceConflictException("change is " + status(change)); + } + + if (!change.currentPatchSetId().equals(rsrc.getPatchSet().getId())) { + throw new ResourceConflictException("not current patch set"); + } + + changeStatus(change, input, Status.WORKINPROGRESS, Status.NEW); + return Response.none(); + } + + @Override + public Description getDescription(RevisionResource rsrc) { + PatchSet.Id current = rsrc.getChange().currentPatchSetId(); + return new Description() + .setLabel("Ready") + .setTitle("Set Ready For Review") + .setVisible(rsrc.getChange().getStatus() == Status.WORKINPROGRESS + && rsrc.getPatchSet().getId().equals(current)); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/wip/SetCommand.java b/src/main/java/com/googlesource/gerrit/plugins/wip/SetCommand.java new file mode 100644 index 0000000..535c85a --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/wip/SetCommand.java
@@ -0,0 +1,214 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.googlesource.gerrit.plugins.wip; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.RevId; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.change.ChangeResource; +import com.google.gerrit.server.change.RevisionResource; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.sshd.CommandMetaData; +import com.google.gerrit.sshd.SshCommand; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.ResultSet; +import com.google.inject.Inject; +import com.google.inject.Provider; + +@RequiresCapability(WorkInProgressCapability.WORK_IN_PROGRESS) +@CommandMetaData(name = "set", description = "Mark the change as WIP or Ready") +public class SetCommand extends SshCommand { + private static final Logger log = LoggerFactory.getLogger(SetCommand.class); + + private final Set<PatchSet> patchSets = new HashSet<PatchSet>(); + + @Argument(index = 0, required = true, + multiValued = true, + metaVar = "{COMMIT | CHANGE,PATCHSET}", + usage = "list of commits or patch sets to review") + void addPatchSetId(String token) { + try { + patchSets.add(parsePatchSet(token)); + } catch (UnloggedFailure e) { + throw new IllegalArgumentException(e.getMessage(), e); + } catch (OrmException e) { + throw new IllegalArgumentException("database error", e); + } + } + + @Option(name = "--project", aliases = "-p", + usage = "project containing the specified patch set(s)") + private ProjectControl projectControl; + + @Option(name = "--message", aliases = "-m", + usage = "cover message on change(s)", metaVar = "MESSAGE") + private String changeComment; + + @Option(name = "--wip", aliases = "-w", + usage = "mark the specified change(s) as WIP") + private boolean wipChange; + + @Option(name = "--readyChange", aliases = "-r", + usage = "mark the specified change(s) as Ready for Review") + private boolean readyChange; + + @Inject + private ReviewDb db; + + @Inject + private ChangeControl.Factory changeControlFactory; + + @Inject + private Provider<WorkInProgressAction> wipProvider; + + @Inject + private Provider<ReadyForReviewAction> readyProvider; + + @Override + protected void run() throws UnloggedFailure { + if (wipChange && readyChange) { + throw error("wip and ready options are mutually exclusive"); + } + if (!wipChange && !readyChange) { + throw error("wip or ready option must be specified"); + } + + boolean ok = true; + for (PatchSet patchSet : patchSets) { + try { + mark(patchSet); + } catch (NoSuchChangeException e) { + ok = false; + writeError("no such change " + patchSet.getId().getParentKey().get()); + } catch (ResourceConflictException e) { + writeError("error: " + e.getMessage() + "\n"); + ok = false; + } catch (OrmException e) { + ok = false; + writeError("fatal: internal server error while approving " + + patchSet.getId() + "\n"); + log.error("internal error while approving " + patchSet.getId(), e); + } + } + + if (!ok) { + throw new UnloggedFailure(1, "one or more mark operations failed"); + } + } + + private void mark(PatchSet patchSet) throws NoSuchChangeException, + ResourceConflictException, OrmException { + RevisionResource rsrc = new RevisionResource( + new ChangeResource(changeControlFactory + .controlFor(patchSet.getId().getParentKey())), patchSet); + BaseAction.Input input = new BaseAction.Input(); + input.message = changeComment; + if (wipChange) { + wipProvider.get().apply(rsrc, input); + } else { + readyProvider.get().apply(rsrc, input); + } + } + + private PatchSet parsePatchSet(String patchIdentity) + throws UnloggedFailure, OrmException { + // By commit? + // + if (patchIdentity.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) { + RevId id = new RevId(patchIdentity); + ResultSet<PatchSet> patches; + if (id.isComplete()) { + patches = db.patchSets().byRevision(id); + } else { + patches = db.patchSets().byRevisionRange(id, id.max()); + } + + Set<PatchSet> matches = new HashSet<PatchSet>(); + for (PatchSet ps : patches) { + Change change = db.changes().get(ps.getId().getParentKey()); + if (inProject(change)) { + matches.add(ps); + } + } + + switch (matches.size()) { + case 1: + return matches.iterator().next(); + case 0: + throw error("\"" + patchIdentity + "\" no such patch set"); + default: + throw error("\"" + patchIdentity + "\" matches multiple patch sets"); + } + } + + // By older style change,patchset? + // + if (patchIdentity.matches("^[1-9][0-9]*,[1-9][0-9]*$")) { + PatchSet.Id patchSetId; + try { + patchSetId = PatchSet.Id.parse(patchIdentity); + } catch (IllegalArgumentException e) { + throw error("\"" + patchIdentity + "\" is not a valid patch set"); + } + PatchSet patchSet = db.patchSets().get(patchSetId); + if (patchSet == null) { + throw error("\"" + patchIdentity + "\" no such patch set"); + } + if (projectControl != null) { + Change change = db.changes().get(patchSetId.getParentKey()); + if (!inProject(change)) { + throw error("change " + change.getId() + " not in project " + + projectControl.getProject().getName()); + } + } + return patchSet; + } + + throw error("\"" + patchIdentity + "\" is not a valid patch set"); + } + + private boolean inProject(Change change) { + if (projectControl == null) { + // No --project option, so they want every project. + return true; + } + return projectControl.getProject().getNameKey().equals(change.getProject()); + } + + private void writeError(String msg) { + try { + err.write(msg.getBytes(ENC)); + } catch (IOException e) { + } + } + + private static UnloggedFailure error(String msg) { + return new UnloggedFailure(1, msg); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/wip/SshModule.java b/src/main/java/com/googlesource/gerrit/plugins/wip/SshModule.java new file mode 100644 index 0000000..7b21460 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/wip/SshModule.java
@@ -0,0 +1,24 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.googlesource.gerrit.plugins.wip; + +import com.google.gerrit.sshd.PluginCommandModule; + +public class SshModule extends PluginCommandModule { + @Override + protected void configureCommands() { + command(SetCommand.class); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/wip/WorkInProgressAction.java b/src/main/java/com/googlesource/gerrit/plugins/wip/WorkInProgressAction.java new file mode 100644 index 0000000..e5601f0 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/wip/WorkInProgressAction.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.googlesource.gerrit.plugins.wip; + +import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Change.Status; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.change.RevisionResource; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +@RequiresCapability(WorkInProgressCapability.WORK_IN_PROGRESS) +class WorkInProgressAction extends BaseAction implements + UiAction<RevisionResource>, + RestModifyView<RevisionResource, BaseAction.Input> { + + @Inject + WorkInProgressAction(Provider<ReviewDb> dbProvider, + Provider<CurrentUser> userProvider) { + super(dbProvider, userProvider); + } + + @Override + public Object apply(RevisionResource rsrc, Input input) + throws ResourceConflictException, OrmException { + Change change = rsrc.getChange(); + if (change.getStatus() != Status.NEW) { + throw new ResourceConflictException("change is " + status(change)); + } + + if (!change.currentPatchSetId().equals(rsrc.getPatchSet().getId())) { + throw new ResourceConflictException("not current patch set"); + } + + changeStatus(change, input, Status.NEW, Status.WORKINPROGRESS); + return Response.none(); + } + + @Override + public Description getDescription(RevisionResource rsrc) { + PatchSet.Id current = rsrc.getChange().currentPatchSetId(); + return new Description() + .setLabel("WIP") + .setTitle("Set Work In Progress") + .setVisible(rsrc.getChange().getStatus() == Status.NEW + && rsrc.getPatchSet().getId().equals(current)); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/wip/WorkInProgressCapability.java b/src/main/java/com/googlesource/gerrit/plugins/wip/WorkInProgressCapability.java new file mode 100644 index 0000000..d6b376a --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/wip/WorkInProgressCapability.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.googlesource.gerrit.plugins.wip; + +import com.google.gerrit.extensions.config.CapabilityDefinition; + +public class WorkInProgressCapability extends CapabilityDefinition { + static final String WORK_IN_PROGRESS = "workInProgress"; + + @Override + public String getDescription() { + return "Work In Progress"; + } +}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md new file mode 100644 index 0000000..a18fb93 --- /dev/null +++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,74 @@ +Gerrit Work in Progress plugin +============================== + +This plugin adds a new button that allows an authorized user to set a +change to Work In Progress, and a button to change from WIP back +to a "Ready For Review" state. + +Any change in the WIP state will not show up in anyone's Review +Requests. Pushing a new patchset will reset the change to Review +In Progress. + +In addition this plugin exposes this functionality as REST endpoints +and SSH command. + +It is intended to be used in combination with the new "Change Owners" group. The +plugin owned capability "Work In Progress" can be granted to that group, +so that only change owners can toggle the WIP state. + +Work In Progress Workflow: +-------------------------- + +Turn a change to WIP +-------------------- + + + +New comment message +------------------- + + + +Ready For Review button is shown for WIP change +----------------------------------------------- + + + +The change is shown with status "Work In Progress" on change list +----------------------------------------------------------------- + + + +Reviewers Dashboard filters the WIP changes +------------------------------------------- + + + +Mark it as Ready For Review +--------------------------- + + + +Comments are updated correspondingly +------------------------------------ + + + +Reviewers Dashboard shows that change again +------------------------------------------- + + + +Known limitations +----------------- + +Old change screen doesn't support JS API. So that the popup dialog is not shown +if "Work In Progress" and "Ready In Review" buttons are used and no comments +can be provided. New change screen should be used for best experience with +WIP Workflow plugin. + +Authorship +---------- + +[This plugin is based on previous work of David Shrewsbury](https://gerrit-review.googlesource.com/36091) +
diff --git a/src/main/resources/Documentation/cmd-set.md b/src/main/resources/Documentation/cmd-set.md new file mode 100644 index 0000000..a2f58eb --- /dev/null +++ b/src/main/resources/Documentation/cmd-set.md
@@ -0,0 +1,79 @@ +@PLUGIN@ set +============ + +NAME +---- +@PLUGIN@ set - Mark change as Work In Progres or Ready For Review + +SYNOPSIS +-------- +``` +ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ set + [--project <PROJECT> | -p <PROJECT>] + [--message <MESSAGE> | -m <MESSAGE>] + [--wip | --ready] + {COMMIT | CHANGEID,PATCHSET}... +``` + +DESCRIPTION +----------- +Mark the change as Work In Progress or Ready For Review. + +Patch sets should be specified as complete or abbreviated commit +SHA-1s. If the same commit is available in multiple projects the +--project option may be used to limit where Gerrit searches for +the change to only the contents of the specified project. + +For current backward compatibility with user tools patch sets may +also be specified in the legacy 'CHANGEID,PATCHSET' format, such as +'8242,2'. Support for this legacy format is planned to be removed +in a future edition of @PLUGIN@. Use of commit SHA-1s is strongly +encouraged. + +ACCESS +------ +Caller must be a member of a group that is granted the 'Work In Progress' +capability (provided by this plugin) or the 'Administrate Server' +capability. + +SCRIPTING +--------- +This command is intended to be used in scripts. + +OPTIONS +------- + +`--project` +: Name of the project the intended changes are contained + within. This option must be supplied before the commit + SHA-1 in order to take effect. + +`--message` +: Optional description why the state of the change is toggled. + +`--wip` +: Mark the change as Work In Progress + (option is mutually exclusive with --ready) + +`--ready` +: Mark the change as Ready For Review + (option is mutually exclusive with --wip) + +EXAMPLES +-------- +Mark the change as Work In Progress: + +``` + $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ set --wip -m '"Needs some refactorig"' c0ff33 +``` + +Mark the change as Ready For Review: + +``` + $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ set --ready -m '"Refactorig is done"' c0ff33 +``` + +SEE ALSO +-------- + +* [Access Control](../../../Documentation/access-control.html)
diff --git a/src/main/resources/Documentation/images/changes_are_shown.png b/src/main/resources/Documentation/images/changes_are_shown.png new file mode 100644 index 0000000..b41219c --- /dev/null +++ b/src/main/resources/Documentation/images/changes_are_shown.png Binary files differ
diff --git a/src/main/resources/Documentation/images/filtered_wip_changes.png b/src/main/resources/Documentation/images/filtered_wip_changes.png new file mode 100644 index 0000000..fb56935 --- /dev/null +++ b/src/main/resources/Documentation/images/filtered_wip_changes.png Binary files differ
diff --git a/src/main/resources/Documentation/images/mark_as_ready.png b/src/main/resources/Documentation/images/mark_as_ready.png new file mode 100644 index 0000000..6dce392 --- /dev/null +++ b/src/main/resources/Documentation/images/mark_as_ready.png Binary files differ
diff --git a/src/main/resources/Documentation/images/mark_as_wip.png b/src/main/resources/Documentation/images/mark_as_wip.png new file mode 100644 index 0000000..4fbbfc0 --- /dev/null +++ b/src/main/resources/Documentation/images/mark_as_wip.png Binary files differ
diff --git a/src/main/resources/Documentation/images/ready_for_review.png b/src/main/resources/Documentation/images/ready_for_review.png new file mode 100644 index 0000000..7591fa9 --- /dev/null +++ b/src/main/resources/Documentation/images/ready_for_review.png Binary files differ
diff --git a/src/main/resources/Documentation/images/updated_comments.png b/src/main/resources/Documentation/images/updated_comments.png new file mode 100644 index 0000000..e4b69b6 --- /dev/null +++ b/src/main/resources/Documentation/images/updated_comments.png Binary files differ
diff --git a/src/main/resources/Documentation/images/wip_comment.png b/src/main/resources/Documentation/images/wip_comment.png new file mode 100644 index 0000000..9d265a3 --- /dev/null +++ b/src/main/resources/Documentation/images/wip_comment.png Binary files differ
diff --git a/src/main/resources/Documentation/images/wip_on_change_list.png b/src/main/resources/Documentation/images/wip_on_change_list.png new file mode 100644 index 0000000..3a34822 --- /dev/null +++ b/src/main/resources/Documentation/images/wip_on_change_list.png Binary files differ
diff --git a/src/main/resources/Documentation/rest-api-changes.md b/src/main/resources/Documentation/rest-api-changes.md new file mode 100644 index 0000000..569a5bd --- /dev/null +++ b/src/main/resources/Documentation/rest-api-changes.md
@@ -0,0 +1,88 @@ +@PLUGIN@ - /changes/ REST API +============================= + +This page describes the changes related REST endpoints that are added +by the @PLUGIN@. + +Please also take note of the general information on the +[REST API](../../../Documentation/rest-api.html). + +<a id="revision-endpoints"> Revision Endpoints +---------------------------------------------- + +### <a id="set-wip"> Mark Revision as Work In Progress +_POST /changes/[\{change-id\}](../../../Documentation/rest-api-changes.html#change-id)/revisions/[\{revision-id\}](../../../Documentation/rest-api-changes.html#revision-id)/@PLUGIN@~set-wip_ + +Mark a revision as Work In Progress. + +Options for that endpoint can be specified in the request body as a +[Input](#options-input) entity. + +Caller must be a member of a group that is granted the 'Work In Progress' +capability (provided by this plugin) or be a member of the Administrators +group. + +#### Request + +``` + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/@PLUGIN@~set-wip HTTP/1.0 + Content-Type: application/json;charset=UTF-8 + + { + "message": "Refactoring needs to be done before we can proceed here." + } +``` + +#### Response + +``` + HTTP/1.1 204 No Content +``` + +### <a id="set-ready"> Mark Revision as Ready for Review +_POST /changes/[\{change-id\}](../../../Documentation/rest-api-changes.html#change-id)/revisions/[\{revision-id\}](../../../Documentation/rest-api-changes.html#revision-id)/@PLUGIN@~set-ready_ + +Mark a revision as Ready For Review. + +Options for that endpoint can be specified in the request body as a +[Input](#options-input) entity. + +Caller must be a member of a group that is granted the 'Work In Progress' +capability (provided by this plugin) or be a member of the Administrators +group. + +#### Request + +``` + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/@PLUGIN@~set-ready HTTP/1.0 + Content-Type: application/json;charset=UTF-8 + + { + "message": "Refactoring is done." + } +``` + +#### Response + +``` + HTTP/1.1 204 No Content +``` + + +<a id="json-entities">JSON Entities +----------------------------------- + +### <a id="options-info"></a> Input + +The `Input` entity can contain a message. + +* _message_ (optional): Description why the status of this change is toggled. + +SEE ALSO +-------- + +* [Changes related REST endpoints](../../../Documentation/rest-api-changes.html) + +GERRIT +------ +Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/main/resources/static/wip.js b/src/main/resources/static/wip.js new file mode 100644 index 0000000..8b701ba --- /dev/null +++ b/src/main/resources/static/wip.js
@@ -0,0 +1,50 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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. + +Gerrit.install(function(self) { + function onSetWip(c) { + var t = c.textarea(); + var b = c.button('WIP', {onclick: function() { + c.call( + {message: t.value}, + function(r) { + c.hide(); + c.refresh(); + }); + }}); + c.popup(c.div( + t, + c.br(), + b)); + t.focus(); + } + function onSetReady(c) { + var t = c.textarea(); + var b = c.button('Ready', {onclick: function() { + c.call( + {message: t.value}, + function(r) { + c.hide(); + c.refresh(); + }); + }}); + c.popup(c.div( + t, + c.br(), + b)); + t.focus(); + } + self.onAction('revision', 'set-ready', onSetReady); + self.onAction('revision', 'set-wip', onSetWip); + });