Send event and execute hooks when topic is changed

When the topic is changed via the web UI or the REST API, send
an event to the event stream and execute a hook.

Bug: Issue 1992
Change-Id: Iba55469d50762478478eb07d9e2d8b0222ee3fd5
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index d5c7f9c..512c801 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -152,6 +152,15 @@
 
 reviewer:: link:json.html#account[account attribute]
 
+Topic Changed
+^^^^^^^^^^^^^
+type:: "topic-changed"
+
+change:: link:json.html#change[change attribute]
+
+changer:: link:json.html#account[account attribute]
+
+oldTopic:: Topic name before it was changed.
 
 SEE ALSO
 --------
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 8864054..5c9bf6a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1441,6 +1441,11 @@
 Optional filename for the reviewer added hook, if not specified then
 `reviewer-added` will be used.
 
+[[hooks.topicChangedHook]]hooks.topicChangedHook::
++
+Optional filename for the topic changed hook, if not specified then
+`topic-changed` will be used.
+
 [[hooks.claSignedHook]]hooks.claSignedHook::
 +
 Optional filename for the CLA signed hook, if not specified then
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 4b867e6..e232c0c 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -120,6 +120,15 @@
   reviewer-added --change <change id> --change-url <change url> --project <project name> --branch <branch> --reviewer <reviewer>
 ====
 
+topic-changed
+~~~~~~~~~~~~~
+
+Called whenever a change's topic is changed from the Web UI or via the REST API.
+
+====
+  topic-changed --change <change id> --changer <changer> --old-topic <old topic> --new-topic <new topic>
+====
+
 cla-signed
 ~~~~~~~~~~
 
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 92c178d..66a6ae8 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
@@ -44,6 +44,7 @@
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.events.ReviewerAddedEvent;
+import com.google.gerrit.server.events.TopicChangedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.project.ProjectCache;
@@ -186,6 +187,9 @@
     /** Filename of the reviewer added hook. */
     private final File reviewerAddedHook;
 
+    /** Filename of the topic changed hook. */
+    private final File topicChangedHook;
+
     /** Filename of the cla signed hook. */
     private final File claSignedHook;
 
@@ -254,6 +258,7 @@
         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());
         reviewerAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "reviewerAddedHook", "reviewer-added")).getPath());
+        topicChangedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "topicChangedHook", "topic-changed")).getPath());
         claSignedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "claSignedHook", "cla-signed")).getPath());
         refUpdateHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdateHook", "ref-update")).getPath());
         syncHookTimeout = config.getInt("hooks", "syncHookTimeout", 30);
@@ -560,6 +565,25 @@
       runHook(change.getProject(), reviewerAddedHook, args);
     }
 
+    public void doTopicChangedHook(final Change change, final Account account,
+        final String oldTopic, final ReviewDb db)
+            throws OrmException {
+      final TopicChangedEvent event = new TopicChangedEvent();
+
+      event.change = eventFactory.asChangeAttribute(change);
+      event.changer = eventFactory.asAccountAttribute(account);
+      event.oldTopic = oldTopic;
+      fireEvent(change, event, db);
+
+      final List<String> args = new ArrayList<String>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--changer", getDisplayName(account));
+      addArg(args, "--old-topic", oldTopic);
+      addArg(args, "--new-topic", event.change.topic);
+
+      runHook(change.getProject(), topicChangedHook, args);
+    }
+
     public void doClaSignupHook(Account account, ContributorAgreement cla) {
       if (account != null) {
         final List<String> args = new ArrayList<String>();
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 ccff39a..28c64a9 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
@@ -147,6 +147,16 @@
   public void doReviewerAddedHook(Change change, Account account,
       PatchSet patchSet, ReviewDb db) throws OrmException;
 
+  /**
+   * Fire the Topic Changed Hook
+   *
+   * @param change The change itself.
+   * @param account The gerrit user who changed the topic.
+   * @param oldTopic The old topic name.
+   */
+  public void doTopicChangedHook(Change change, Account account,
+      String oldTopic, ReviewDb db) throws OrmException;
+
   public void doClaSignupHook(Account account, ContributorAgreement cla);
 
   /**
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 db87595..021d1d2 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
@@ -91,6 +91,11 @@
   }
 
   @Override
+  public void doTopicChangedHook(Change change, Account account, String oldTopic,
+      ReviewDb db) {
+  }
+
+  @Override
   public void removeChangeListener(ChangeListener listener) {
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index f46aa9e..346d53c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -38,6 +39,7 @@
 class PutTopic implements RestModifyView<ChangeResource, Input> {
   private final Provider<ReviewDb> dbProvider;
   private final ChangeIndexer indexer;
+  private final ChangeHooks hooks;
 
   static class Input {
     @DefaultInput
@@ -46,9 +48,11 @@
   }
 
   @Inject
-  PutTopic(Provider<ReviewDb> dbProvider, ChangeIndexer indexer) {
+  PutTopic(Provider<ReviewDb> dbProvider, ChangeIndexer indexer,
+      ChangeHooks hooks) {
     this.dbProvider = dbProvider;
     this.indexer = indexer;
+    this.hooks = hooks;
   }
 
   @Override
@@ -80,9 +84,10 @@
             oldTopicName, newTopicName);
       }
 
+      IdentifiedUser currentUser = ((IdentifiedUser) control.getCurrentUser());
       ChangeMessage cmsg = new ChangeMessage(
           new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
-          ((IdentifiedUser) control.getCurrentUser()).getAccountId(),
+          currentUser.getAccountId(),
           change.currentPatchSetId());
       StringBuilder msgBuf = new StringBuilder(summary);
       if (!Strings.isNullOrEmpty(input.message)) {
@@ -101,6 +106,8 @@
         });
       db.changeMessages().insert(Collections.singleton(cmsg));
       indexer.index(change);
+      hooks.doTopicChangedHook(change, currentUser.getAccount(),
+          oldTopicName, db);
     }
     return Strings.isNullOrEmpty(newTopicName)
         ? Response.none()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
new file mode 100644
index 0000000..e725eac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
@@ -0,0 +1,25 @@
+// 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.google.gerrit.server.events;
+
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+
+public class TopicChangedEvent extends ChangeEvent {
+  public final String type = "topic-changed";
+  public ChangeAttribute change;
+  public AccountAttribute changer;
+  public String oldTopic;
+}
\ No newline at end of file