Added support for ref-update events

Whenever a ref is updated via either a direct push to a branch or a
Gerrit change submission, Gerrit will now send a new "ref-updated"
event to the event stream.  This can be used by external agents for
things like automatic submodule ref updating, replication, build bot
hooks, etc.

Change-Id: Ice9d65db8fd662d53df53ff6b61d811815c9f2f6
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index aae908a..3b348da 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -102,6 +102,15 @@
 
 comment:: Comment text author had written
 
+Ref Updated
+^^^^^^^^^^^
+type:: "ref-updated"
+
+submitter:: link:json.html#account[account attribute]
+
+refUpdate:: link:json.html#refupdate[refupdate attribute]
+
+
 SEE ALSO
 --------
 
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index e271ba8..fd2ae82 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -66,6 +66,15 @@
   change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --restorer <restorer> --reason <reason>
 ====
 
+ref-updated
+~~~~~~~~~~~
+
+Called whenever a ref has been updated.
+
+====
+  ref-updated --oldrev <old rev> --newrev <new rev> --refname <ref name> --project <project name> --submitter <submitter>
+====
+
 
 Configuration Settings
 ----------------------
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 1c9a808..99b158da 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -107,6 +107,19 @@
 
 by:: Reviewer of the patch set in <<account,account attribute>>.
 
+[[refupdate]]
+refupdate
+--------
+Information about a ref that was updated.
+
+oldRev:: The old value of the ref, prior to the update.
+
+newRev:: The new value the ref was updated to.
+
+project:: Project path in Gerrit
+
+refName:: Ref name within project.
+
 SEE ALSO
 --------
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
index 0472dd4..202c949 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.InvalidRevisionException;
@@ -58,6 +59,7 @@
   private final IdentifiedUser identifiedUser;
   private final GitRepositoryManager repoManager;
   private final ReplicationQueue replication;
+  private final ChangeHookRunner hooks;
 
   private final Project.NameKey projectName;
   private final String branchName;
@@ -69,6 +71,7 @@
       final IdentifiedUser identifiedUser,
       final GitRepositoryManager repoManager,
       final ReplicationQueue replication,
+      final ChangeHookRunner hooks,
 
       @Assisted Project.NameKey projectName,
       @Assisted("branchName") String branchName,
@@ -78,6 +81,7 @@
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
     this.replication = replication;
+    this.hooks = hooks;
 
     this.projectName = projectName;
     this.branchName = branchName;
@@ -136,6 +140,7 @@
           case NEW:
           case NO_CHANGE:
             replication.scheduleUpdate(name.getParentKey(), refname);
+            hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
             break;
           default: {
             final String msg =
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
index e8c8904..fffc126 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -46,6 +48,8 @@
   private final ProjectControl.Factory projectControlFactory;
   private final GitRepositoryManager repoManager;
   private final ReplicationQueue replication;
+  private final IdentifiedUser identifiedUser;
+  private final ChangeHookRunner hooks;
 
   private final Project.NameKey projectName;
   private final Set<Branch.NameKey> toRemove;
@@ -54,11 +58,15 @@
   DeleteBranches(final ProjectControl.Factory projectControlFactory,
       final GitRepositoryManager repoManager,
       final ReplicationQueue replication,
+      final IdentifiedUser identifiedUser,
+      final ChangeHookRunner hooks,
 
       @Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) {
     this.projectControlFactory = projectControlFactory;
     this.repoManager = repoManager;
     this.replication = replication;
+    this.identifiedUser = identifiedUser;
+    this.hooks = hooks;
 
     this.projectName = name;
     this.toRemove = toRemove;
@@ -85,8 +93,9 @@
       for (final Branch.NameKey branchKey : toRemove) {
         final String refname = branchKey.get();
         final RefUpdate.Result result;
+        final RefUpdate u;
         try {
-          final RefUpdate u = r.updateRef(refname);
+          u = r.updateRef(refname);
           u.setForceUpdate(true);
           result = u.delete();
         } catch (IOException e) {
@@ -101,6 +110,7 @@
           case FORCED:
             deleted.add(branchKey);
             replication.scheduleUpdate(projectName, refname);
+            hooks.doRefUpdatedHook(branchKey, u, identifiedUser.getAccount());
             break;
 
           case REJECTED_CURRENT_BRANCH:
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 d29bc23..c7c51b6 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Project;
@@ -35,6 +36,7 @@
 import com.google.gerrit.server.events.CommentAddedEvent;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.project.ProjectCache;
@@ -45,6 +47,8 @@
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -96,6 +100,9 @@
     /** Filename of the change abandoned hook. */
     private final File changeRestoredHook;
 
+    /** Filename of the ref updated hook. */
+    private final File refUpdatedHook;
+
     /** Repository Manager. */
     private final GitRepositoryManager repoManager;
 
@@ -141,6 +148,7 @@
         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());
         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());
     }
 
     public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
@@ -172,7 +180,16 @@
      * @return Repository or null.
      */
     private Repository openRepository(final Change change) {
-        Project.NameKey name = change.getProject();
+        return openRepository(change.getProject());
+    }
+
+    /**
+     * Get the Repository for the given project name, or null on error.
+     *
+     * @param name Project to get repo for,
+     * @return Repository or null.
+     */
+    private Repository openRepository(final Project.NameKey name) {
         try {
             return repoManager.openRepository(name.get());
         } catch (RepositoryNotFoundException err) {
@@ -335,6 +352,44 @@
         runHook(openRepository(change), changeRestoredHook, args);
     }
 
+    /**
+     * Fire the Ref Updated Hook
+     * @param project The project the ref update occured on
+     * @param refUpdate An actual RefUpdate object
+     * @param account The gerrit user who moved the ref
+     */
+    public void doRefUpdatedHook(final Branch.NameKey refName, final RefUpdate refUpdate, final Account account) {
+      doRefUpdatedHook(refName, refUpdate.getOldObjectId(), refUpdate.getNewObjectId(), account);
+    }
+
+    /**
+     * Fire the Ref Updated Hook
+     * @param refName The Branch.NameKey of the ref that was updated
+     * @param oldId The ref's old id
+     * @param newId The ref's new id
+     * @param account The gerrit user who moved the ref
+     */
+    public void doRefUpdatedHook(final Branch.NameKey refName, final ObjectId oldId, final ObjectId newId, final Account account) {
+      final RefUpdatedEvent event = new RefUpdatedEvent();
+
+      if (account != null) {
+        event.submitter = eventFactory.asAccountAttribute(account);
+      }
+      event.refUpdate = eventFactory.asRefUpdateAttribute(oldId, newId, refName);
+      fireEvent(refName, event);
+
+      final List<String> args = new ArrayList<String>();
+      addArg(args, "--oldrev", event.refUpdate.oldRev);
+      addArg(args, "--newrev", event.refUpdate.newRev);
+      addArg(args, "--refname", event.refUpdate.refName);
+      addArg(args, "--project", event.refUpdate.project);
+      if (account != null) {
+        addArg(args, "--submitter", getDisplayName(account));
+      }
+
+      runHook(openRepository(refName.getParentKey()), refUpdatedHook, args);
+    }
+
     private void fireEvent(final Change change, final ChangeEvent event) {
       for (ChangeListenerHolder holder : listeners.values()) {
           if (isVisibleTo(change, holder.user)) {
@@ -343,6 +398,14 @@
       }
     }
 
+    private void fireEvent(Branch.NameKey branchName, final ChangeEvent event) {
+      for (ChangeListenerHolder holder : listeners.values()) {
+          if (isVisibleTo(branchName, holder.user)) {
+              holder.listener.onChangeEvent(event);
+          }
+      }
+    }
+
     private boolean isVisibleTo(Change change, IdentifiedUser user) {
         final ProjectState pe = projectCache.get(change.getProject());
         if (pe == null) {
@@ -352,6 +415,15 @@
         return pc.controlFor(change).isVisible();
     }
 
+    private boolean isVisibleTo(Branch.NameKey branchName, IdentifiedUser user) {
+        final ProjectState pe = projectCache.get(branchName.getParentKey());
+        if (pe == null) {
+          return false;
+        }
+        final ProjectControl pc = pe.controlFor(user);
+        return pc.controlForRef(branchName).isVisible();
+    }
+
     /**
      * Create an ApprovalAttribute for the given approval suitable for serialization to JSON.
      * @param approval
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 447b098..573e6bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
@@ -28,6 +29,8 @@
 import com.google.inject.Singleton;
 import com.google.inject.internal.Nullable;
 
+import org.eclipse.jgit.lib.ObjectId;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Map;
@@ -68,6 +71,23 @@
   }
 
   /**
+   * Create a RefUpdateAttribute for the given old ObjectId, new ObjectId, and
+   * branch that is suitable for serialization to JSON.
+   *
+   * @param refUpdate
+   * @param refName
+   * @return object suitable for serialization to JSON
+   */
+  public RefUpdateAttribute asRefUpdateAttribute(final ObjectId oldId, final ObjectId newId, final Branch.NameKey refName) {
+    RefUpdateAttribute ru = new RefUpdateAttribute();
+    ru.newRev = newId != null ? newId.getName() : ObjectId.zeroId().getName();
+    ru.oldRev = oldId != null ? oldId.getName() : ObjectId.zeroId().getName();
+    ru.project = refName.getParentKey().get();
+    ru.refName = refName.getShortName();
+    return ru;
+  }
+
+  /**
    * Extend the existing ChangeAttribute with additional fields.
    *
    * @param a
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java
new file mode 100644
index 0000000..e4d715a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2010 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 RefUpdateAttribute {
+  public String oldRev;
+  public String newRev;
+  public String refName;
+  public String project;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
new file mode 100644
index 0000000..f90bc81
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2010 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 RefUpdatedEvent extends ChangeEvent {
+  public final String type = "ref-updated";
+  public AccountAttribute submitter;
+  public RefUpdateAttribute refUpdate;
+}
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 f699d0d..2efb1f4 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
@@ -881,6 +881,13 @@
           case FAST_FORWARD:
             replication.scheduleUpdate(destBranch.getParentKey(), branchUpdate
                 .getName());
+
+            Account account = null;
+            final PatchSetApproval submitter = getSubmitter(mergeTip.patchsetId);
+            if (submitter != null) {
+              account = accountCache.get(submitter.getAccountId()).getAccount();
+            }
+            hooks.doRefUpdatedHook(destBranch, branchUpdate, account);
             break;
 
           default:
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 bddb89f..8b6f397 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
@@ -302,6 +302,8 @@
           // Change refs are scheduled when they are created.
           //
           replication.scheduleUpdate(project.getNameKey(), c.getRefName());
+          Branch.NameKey destBranch = new Branch.NameKey(project.getNameKey(), c.getRefName());
+          hooks.doRefUpdatedHook(destBranch, c.getOldId(), c.getNewId(), currentUser.getAccount());
         }
       }
     }