Added cla-signed hook

The cla-signed hook is fired every time a user accepts a contributor
license agreement.

Change-Id: Ia907b879e84517fe16ed898e6eaf6cff1d80bbdf
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index fd2ae82..963b628 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -75,6 +75,15 @@
   ref-updated --oldrev <old rev> --newrev <new rev> --refname <ref name> --project <project name> --submitter <submitter>
 ====
 
+cla-signed
+~~~~~~~~~~~
+
+Called whenever a user signs a contributor license agreement
+
+====
+  cla-signed --submitter <submitter> --user-id <user_id> --cla-id <cla_id>
+====
+
 
 Configuration Settings
 ----------------------
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index 173ca60..f69a0d6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.common.data.AccountSecurity;
 import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.errors.ContactInformationStoreException;
@@ -32,6 +33,7 @@
 import com.google.gerrit.reviewdb.ContactInformation;
 import com.google.gerrit.reviewdb.ContributorAgreement;
 import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountByEmailCache;
@@ -89,6 +91,8 @@
   private final MyGroupsFactory.Factory myGroupsFactory;
   private final GroupDetailFactory.Factory groupDetailFactory;
 
+  private final ChangeHookRunner hooks;
+
   @Inject
   AccountSecurityImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser, final ContactStore cs,
@@ -102,7 +106,8 @@
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
       final ExternalIdDetailFactory.Factory externalIdDetailFactory,
       final MyGroupsFactory.Factory myGroupsFactory,
-      final GroupDetailFactory.Factory groupDetailFactory) {
+      final GroupDetailFactory.Factory groupDetailFactory,
+      final ChangeHookRunner hooks) {
     super(schema, currentUser);
     contactStore = cs;
     authConfig = ac;
@@ -123,6 +128,7 @@
     this.externalIdDetailFactory = externalIdDetailFactory;
     this.myGroupsFactory = myGroupsFactory;
     this.groupDetailFactory = groupDetailFactory;
+    this.hooks = hooks;
   }
 
   public void mySshKeys(final AsyncCallback<List<AccountSshKey>> callback) {
@@ -279,6 +285,8 @@
                 .getAccountId(), id));
         if (cla.isAutoVerify()) {
           a.review(AccountAgreement.Status.VERIFIED, null);
+
+          hooks.doClaSignupHook(user.get().getAccount(), cla);
         }
         db.accountAgreements().insert(Collections.singleton(a));
         return VoidResult.INSTANCE;
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 8205946..0a7337a 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
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ContributorAgreement;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.IdentifiedUser;
@@ -103,6 +104,9 @@
     /** Filename of the ref updated hook. */
     private final File refUpdatedHook;
 
+    /** Filename of the cla signed hook. */
+    private final File claSignedHook;
+
     /** Repository Manager. */
     private final GitRepositoryManager repoManager;
 
@@ -149,6 +153,7 @@
         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());
+        claSignedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "claSignedHook", "cla-signed")).getPath());
     }
 
     public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
@@ -390,6 +395,17 @@
       runHook(openRepository(refName.getParentKey()), refUpdatedHook, args);
     }
 
+    public void doClaSignupHook(Account account, ContributorAgreement cla) {
+      if (account != null) {
+        final List<String> args = new ArrayList<String>();
+        addArg(args, "--submitter", getDisplayName(account));
+        addArg(args, "--user-id", account.getId().toString());
+        addArg(args, "--cla-id", cla.getId().toString());
+
+        runHook(claSignedHook, args);
+      }
+    }
+
     private void fireEvent(final Change change, final ChangeEvent event) {
       for (ChangeListenerHolder holder : listeners.values()) {
           if (isVisibleTo(change, holder.user)) {
@@ -477,6 +493,12 @@
     }
   }
 
+  private synchronized void runHook(File hook, List<String> args) {
+    if (hook.exists()) {
+      hookQueue.execute(new HookTask(null, hook, args));
+    }
+  }
+
   private final class HookTask implements Runnable {
     private final Repository repo;
     private final File hook;
@@ -497,10 +519,12 @@
 
         final ProcessBuilder pb = new ProcessBuilder(argv);
         pb.redirectErrorStream(true);
-        pb.directory(repo.getDirectory());
+        if (repo != null) {
+          pb.directory(repo.getDirectory());
 
-        final Map<String, String> env = pb.environment();
-        env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
+          final Map<String, String> env = pb.environment();
+          env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
+        }
 
         Process ps = pb.start();
         ps.getOutputStream().close();
@@ -522,7 +546,9 @@
       } catch (Throwable err) {
         log.error("Error running hook " + hook.getAbsolutePath(), err);
       } finally {
-        repo.close();
+        if (repo != null) {
+          repo.close();
+        }
       }
     }