Merge branch 'stable-2.14' into stable-2.15

* stable-2.14:
  Add submit hook
  Module: Bind listeners in alphabetical order
  Annotate RefUpdate and CommitReceived as Singleton
  Reword documentation of ref-update and commit-received hooks
  Improve hooks documentation structure

Change-Id: I5f5eed398e182c8542b4b53722d942fb2b6bde5a
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/CommitReceived.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/CommitReceived.java
index 73b5ff5..b03a77b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/CommitReceived.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/CommitReceived.java
@@ -20,10 +20,12 @@
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import java.util.Collections;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
 
+@Singleton
 public class CommitReceived implements CommitValidationListener {
   private final Hook hook;
   private final HookFactory hookFactory;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/Module.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/Module.java
index df16d15..aeea41e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/Module.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.events.TopicEditedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.MergeValidationListener;
 import com.google.gerrit.server.git.validators.RefOperationValidationListener;
 import com.google.inject.Scopes;
 import com.google.inject.internal.UniqueAnnotations;
@@ -49,15 +50,15 @@
     DynamicSet.bind(binder(), ChangeMergedListener.class).to(ChangeMerged.class);
     DynamicSet.bind(binder(), ChangeRestoredListener.class).to(ChangeRestored.class);
     DynamicSet.bind(binder(), CommentAddedListener.class).to(CommentAdded.class);
+    DynamicSet.bind(binder(), CommitValidationListener.class).to(CommitReceived.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(GitReferenceUpdated.class);
     DynamicSet.bind(binder(), HashtagsEditedListener.class).to(HashtagsEdited.class);
+    DynamicSet.bind(binder(), MergeValidationListener.class).to(Submit.class);
     DynamicSet.bind(binder(), NewProjectCreatedListener.class).to(NewProjectCreated.class);
     DynamicSet.bind(binder(), RefOperationValidationListener.class).to(RefUpdate.class);
     DynamicSet.bind(binder(), ReviewerAddedListener.class).to(ReviewerAdded.class);
     DynamicSet.bind(binder(), ReviewerDeletedListener.class).to(ReviewerDeleted.class);
     DynamicSet.bind(binder(), RevisionCreatedListener.class).to(RevisionCreated.class);
     DynamicSet.bind(binder(), TopicEditedListener.class).to(TopicEdited.class);
-
-    DynamicSet.bind(binder(), CommitValidationListener.class).to(CommitReceived.class);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/RefUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/RefUpdate.java
index 5840874..7eedfa5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/RefUpdate.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/RefUpdate.java
@@ -21,10 +21,12 @@
 import com.google.gerrit.server.git.validators.ValidationMessage;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import java.util.Collections;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
 
+@Singleton
 public class RefUpdate implements RefOperationValidationListener {
   private final Hook hook;
   private final HookFactory hookFactory;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/Submit.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/Submit.java
new file mode 100644
index 0000000..8e2f1e0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/Submit.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2018 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.hooks;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.validators.MergeValidationException;
+import com.google.gerrit.server.git.validators.MergeValidationListener;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Repository;
+
+@Singleton
+public class Submit implements MergeValidationListener {
+  private final Hook hook;
+  private final HookFactory hookFactory;
+
+  @Inject
+  Submit(HookFactory hookFactory) {
+    this.hook = hookFactory.createSync("submitHook", "submit");
+    this.hookFactory = hookFactory;
+  }
+
+  @Override
+  public void onPreMerge(
+      Repository repo,
+      CodeReviewCommit commit,
+      ProjectState destProject,
+      Branch.NameKey destBranch,
+      PatchSet.Id patchSetId,
+      IdentifiedUser caller)
+      throws MergeValidationException {
+    String projectName = destProject.getProject().getName();
+
+    HookArgs args = hookFactory.createArgs();
+    args.add("--project", projectName);
+    args.add("--branch", destBranch.get());
+    args.add("--submitter", caller.getNameEmail());
+    args.add("--submitter-username", caller.getUserName());
+    args.add("--patchset", patchSetId.get());
+    args.add("--commit", commit.getId().name());
+
+    HookResult result = hook.execute(projectName, args);
+    if (result != null && result.getExitValue() != 0) {
+      throw new MergeValidationException(result.toString());
+    }
+  }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index e3aa1b9..7a1cbd2 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -78,6 +78,9 @@
 hooks.reviewerDeletedHook
 :	Filename for the reviewer update hook. If not set, defaults to `reviewer-deleted`.
 
+hooks.submitHook
+:	Filename for the submit hook. If not set, defaults to `submit`.
+
 hooks.topicChangedHook
 :	Filename for the topic changed hook. If not set, defaults to `topic-changed`.
 
diff --git a/src/main/resources/Documentation/hooks.md b/src/main/resources/Documentation/hooks.md
index 09a7c25..fd92b63 100644
--- a/src/main/resources/Documentation/hooks.md
+++ b/src/main/resources/Documentation/hooks.md
@@ -1,41 +1,56 @@
-Supported Hooks
-===============
+# Supported Hooks
 
-ref-update
-----------
+[TOC]
 
-This is called when a ref update request is received by Gerrit. It allows a
-request to be rejected before it is committed to the Gerrit repository. If
-the script exits with non-zero return code the update will be rejected. Any
-output from the script will be returned to the user, regardless of the return
-code.
+## Synchronous Hooks
 
-This hook is called synchronously so it is recommended that it not block. A
-default timeout on the hook is set to 30 seconds to avoid "runaway" hooks using
-up server threads.  See [`hooks.syncHookTimeout`][1] for configuration details.
+These hooks are invoked synchronously so it is recommended that they not block.
+
+A default timeout on the hook is set to 30 seconds to avoid "runaway" hooks using
+up server threads.  The timeout can be changed by setting [`hooks.syncHookTimeout`][1].
+
+### ref-update
+
+This is called when a ref update request (direct push, non-fastforward update, or
+ref deletion) is received by Gerrit. It allows a request to be rejected before it
+is committed to the Gerrit repository.
+
+If the hook exits with non-zero return code the update will be rejected. Any output
+from the hook will be returned to the user, regardless of the return code.
 
 ```
   ref-update --project <project name> --refname <refname> --uploader <uploader> --uploader-username <username> --oldrev <sha1> --newrev <sha1>
 ```
 
-commit-received
----------------
+### commit-received
 
-This is called when a push request is received by Gerrit. It allows a push to be
-rejected before it is committed to the Gerrit repository. If the script exits
-with non-zero return code the push will be rejected. Any output from the script
-will be returned to the user, regardless of the return code.
+This is called when a commit is received for review by Gerrit. It allows a push to
+be rejected before the review is created.
 
-This hook is called synchronously so it is recommended that it not block. A
-default timeout on the hook is set to 30 seconds to avoid "runaway" hooks using
-up server threads.  See [`hooks.syncHookTimeout`][1] for configuration details.
+If the hook exits with non-zero return code the push will be rejected. Any output
+from the hook will be returned to the user, regardless of the return code.
 
 ```
   commit-received --project <project name> --refname <refname> --uploader <uploader> --uploader-username <username> --oldrev <sha1> --newrev <sha1> --cmdref <refname>
 ```
 
-patchset-created
-----------------
+### submit
+
+This is called when a user attempts to submit a change. It allows the submit to
+be rejected.
+
+If the hook exits with non-zero return code the submit will be rejected and the
+ouput from the hook will be returned to the user.
+
+```
+  submit --project <project name> --branch <branch> --submitter <submitter> --patchset <patchset id> --commit <sha1>
+```
+
+## Asynchronous Hooks
+
+These hooks are invoked asynchronously on a background thread.
+
+### patchset-created
 
 Called whenever a patchset is created (this includes new changes).
 
@@ -46,8 +61,7 @@
 The `--kind` parameter represents the kind of change uploaded. See documentation
 of [`patchSet`][2] for details.
 
-comment-added
--------------
+### comment-added
 
 Called whenever a comment is added to a change.
 
@@ -55,8 +69,7 @@
   comment-added --change <change id> --change-url <change url> --change-owner <change owner> --change-owner-username <username> --project <project name> --branch <branch> --topic <topic> --author <comment author> --author-username <username> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> --<approval category id>-oldValue <score> ...]
 ```
 
-change-merged
--------------
+### change-merged
 
 Called whenever a change has been merged.
 
@@ -64,8 +77,7 @@
   change-merged --change <change id> --change-url <change url> --change-owner <change owner> --change-owner-username <username> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --submitter-username <username> --commit <sha1> --newrev <sha1>
 ```
 
-change-abandoned
-----------------
+### change-abandoned
 
 Called whenever a change has been abandoned.
 
@@ -73,8 +85,7 @@
   change-abandoned --change <change id> --change-url <change url> --change-owner <change owner> --change-owner-username <username> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --abandoner-username <username> --commit <sha1> --reason <reason>
 ```
 
-change-restored
----------------
+### change-restored
 
 Called whenever a change has been restored.
 
@@ -82,8 +93,7 @@
   change-restored --change <change id> --change-url <change url> --change-owner <change owner> --change-owner-username <username> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --restorer-username <username> --commit <sha1> --reason <reason>
 ```
 
-ref-updated
------------
+### ref-updated
 
 Called whenever a ref has been updated.
 
@@ -91,8 +101,7 @@
   ref-updated --oldrev <old rev> --newrev <new rev> --refname <ref name> --project <project name> --submitter <submitter> --submitter-username <username>
 ```
 
-project-created
----------------
+### project-created
 
 Called whenever a project has been created.
 
@@ -100,8 +109,7 @@
   project-created --project <project name> --head <head name>
 ```
 
-reviewer-added
---------------
+### reviewer-added
 
 Called whenever a reviewer is added to a change.
 
@@ -109,8 +117,7 @@
   reviewer-added --change <change id> --change-url <change url> --change-owner <change owner> --change-owner-username <username> --project <project name> --branch <branch> --reviewer <reviewer> --reviewer-username <username>
 ```
 
-reviewer-deleted
-----------------
+### reviewer-deleted
 
 Called whenever a reviewer (with a vote) is removed from a change.
 
@@ -118,8 +125,7 @@
   reviewer-deleted --change <change id> --change-url <change url> --change-owner <change owner> --change-owner-username <username> --project <project name> --branch <branch> --reviewer <reviewer> [--<approval category id> <score> --<approval category id> <score> ...]
 ```
 
-topic-changed
--------------
+### topic-changed
 
 Called whenever a change's topic is changed from the Web UI or via the REST API.
 
@@ -127,8 +133,7 @@
   topic-changed --change <change id> --change-owner <change owner> --change-owner-username <username> --project <project name> --branch <branch> --changer <changer> --changer-username <username> --old-topic <old topic> --new-topic <new topic>
 ```
 
-hashtags-changed
-----------------
+### hashtags-changed
 
 Called whenever hashtags are added to or removed from a change from the Web UI
 or via the REST API.
@@ -147,8 +152,7 @@
 hashtag remaining on the change after the add or remove operation has
 been performed.
 
-cla-signed
-----------
+### cla-signed
 
 Called whenever a user signs a contributor license agreement.