Merge "Send event to stream and execute hook when merge fails"
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 03aec40..ce23da6 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -44,7 +44,8 @@
 *patchSet*, *account* involved, and other attributes as appropriate.
 The currently supported message types are *patchset-created*,
 *draft-published*, *change-abandoned*, *change-restored*,
-*change-merged*, *comment-added*, *ref-updated* and *reviewer-added*.
+*change-merged*, *merge-failed*, *comment-added*, *ref-updated* and
+*reviewer-added*.
 
 Note that any field may be missing in the JSON messages, so consumers of
 this JSON stream should deal with that appropriately.
@@ -105,6 +106,18 @@
 
 submitter:: link:json.html#account[account attribute]
 
+Merge Failed
+^^^^^^^^^^^^
+type:: "merge-failed"
+
+change:: link:json.html#change[change attribute]
+
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
+submitter:: link:json.html#account[account attribute]
+
+reason:: Reason that the merge failed.
+
 Comment Added
 ^^^^^^^^^^^^^
 type:: "comment-added"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index d4259a9..12aa1a8 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1275,6 +1275,11 @@
 Optional filename for the change merged hook, if not specified then
 `change-merged` will be used.
 
+[[hooks.mergeFailedHook]]hooks.mergeFailedHook::
++
+Optional filename for the merge failed hook, if not specified then
+`merge-failed` will be used.
+
 [[hooks.changeAbandonedHook]]hooks.changeAbandonedHook::
 +
 Optional filename for the change abandoned hook, if not specified then
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 0783696..dfac6d1 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -57,6 +57,15 @@
   change-merged --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1>
 ====
 
+merge-failed
+~~~~~~~~~~~~
+
+Called whenever a change has failed to merge.
+
+====
+  merge-failed --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1> --reason <reason>
+====
+
 change-abandoned
 ~~~~~~~~~~~~~~~~
 
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 7299d07..177ec65 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
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.events.CommentAddedEvent;
 import com.google.gerrit.server.events.DraftPublishedEvent;
 import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.events.MergeFailedEvent;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.events.ReviewerAddedEvent;
@@ -109,6 +110,9 @@
     /** Filename of the change merged hook. */
     private final File changeMergedHook;
 
+    /** Filename of the merge failed hook. */
+    private final File mergeFailedHook;
+
     /** Filename of the change abandoned hook. */
     private final File changeAbandonedHook;
 
@@ -174,6 +178,7 @@
         draftPublishedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "draftPublishedHook", "draft-published")).getPath());
         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());
+        mergeFailedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "mergeFailed", "merge-failed")).getPath());
         changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath());
         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());
@@ -329,6 +334,30 @@
         runHook(change.getProject(), changeMergedHook, args);
     }
 
+    public void doMergeFailedHook(final Change change, final Account account,
+          final PatchSet patchSet, final String reason,
+          final ReviewDb db) throws OrmException {
+        final MergeFailedEvent event = new MergeFailedEvent();
+
+        event.change = eventFactory.asChangeAttribute(change);
+        event.submitter = eventFactory.asAccountAttribute(account);
+        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+        event.reason = reason;
+        fireEvent(change, event, db);
+
+        final List<String> args = new ArrayList<String>();
+        addArg(args, "--change", event.change.id);
+        addArg(args, "--change-url", event.change.url);
+        addArg(args, "--project", event.change.project);
+        addArg(args, "--branch", event.change.branch);
+        addArg(args, "--topic", event.change.topic);
+        addArg(args, "--submitter", getDisplayName(account));
+        addArg(args, "--commit", event.patchSet.revision);
+        addArg(args, "--reason",  reason == null ? "" : reason);
+
+        runHook(change.getProject(), mergeFailedHook, args);
+    }
+
     public void doChangeAbandonedHook(final Change change, final Account account,
           final String reason, final ReviewDb db) throws OrmException {
         final ChangeAbandonedEvent event = new ChangeAbandonedEvent();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index 028afe9..0d8dfb8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -83,6 +83,18 @@
       PatchSet patchSet, ReviewDb db) throws OrmException;
 
   /**
+   * Fire the Merge Failed Hook.
+   *
+   * @param change The change itself.
+   * @param account The gerrit user who attempted to submit the change.
+   * @param patchSet The patchset that failed to merge.
+   * @param reason The reason that the change failed to merge.
+   * @throws OrmException
+   */
+  public void doMergeFailedHook(Change change, Account account,
+      PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
+
+  /**
    * Fire the Change Abandoned Hook.
    *
    * @param change The change itself.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index 8794901..57ccba7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -46,6 +46,11 @@
   }
 
   @Override
+  public void doMergeFailedHook(Change change, Account account,
+      PatchSet patchSet, String reason, ReviewDb db) {
+  }
+
+  @Override
   public void doChangeRestoredHook(Change change, Account account,
       String reason, ReviewDb db) {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
new file mode 100644
index 0000000..e6ff525
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2012 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.google.gerrit.server.events;
+
+public class MergeFailedEvent extends ChangeEvent {
+    public final String type = "merge-failed";
+    public ChangeAttribute change;
+    public PatchSetAttribute patchSet;
+    public AccountAttribute submitter;
+    public String reason;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index c80b3e6..e7298b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -1053,17 +1053,23 @@
       }
     }
 
+    PatchSetApproval submitter = null;
+    try {
+      submitter = getSubmitter(db, c.currentPatchSetId());
+    } catch (Exception e) {
+      log.error("Cannot get submitter", e);
+    }
+
+    final PatchSetApproval from = submitter;
     workQueue.getDefaultQueue()
         .submit(requestScopePropagator.wrap(new Runnable() {
       @Override
       public void run() {
         PatchSet patchSet;
-        PatchSetApproval submitter;
         try {
           ReviewDb reviewDb = schemaFactory.open();
           try {
             patchSet = reviewDb.patchSets().get(c.currentPatchSetId());
-            submitter = getSubmitter(reviewDb, c.currentPatchSetId());
           } finally {
             reviewDb.close();
           }
@@ -1074,8 +1080,8 @@
 
         try {
           final MergeFailSender cm = mergeFailSenderFactory.create(c);
-          if (submitter != null) {
-            cm.setFrom(submitter.getAccountId());
+          if (from != null) {
+            cm.setFrom(from.getAccountId());
           }
           cm.setPatchSet(patchSet);
           cm.setChangeMessage(msg);
@@ -1090,5 +1096,15 @@
         return "send-email merge-failed";
       }
     }));
+
+    if (submitter != null) {
+      try {
+        hooks.doMergeFailedHook(c,
+            accountCache.get(submitter.getAccountId()).getAccount(),
+            db.patchSets().get(c.currentPatchSetId()), msg.getMessage(), db);
+      } catch (OrmException ex) {
+        log.error("Cannot run hook for merge failed " + c.getId(), ex);
+      }
+    }
   }
 }