Add support for branching operation

Change-Id: Ic129697f86646c6e965d019eaee1423cc8136d71
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/BranchManifest.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/BranchManifest.java
new file mode 100644
index 0000000..68e772d
--- /dev/null
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/BranchManifest.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2015 Advanced Micro Devices, Inc. All rights reserved.
+//
+// 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.amd.gerrit.plugins.manifestsubscription;
+
+import com.amd.gerrit.plugins.manifestsubscription.manifest.Manifest;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
+
+import javax.xml.bind.JAXBException;
+import java.io.IOException;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "branch", description = "branch all projects described in a manifest on the server")
+public class BranchManifest extends SshCommand {
+
+  @Inject
+  private GitRepositoryManager gitRepoManager;
+
+  @Option(name = "-r", aliases = {"--manifest-repo"},
+          usage = "", required = true)
+  private String manifestRepo;
+
+  @Option(name = "-c", aliases = {"--manifest-commit-ish"},
+          usage = "", required = true)
+  private String manifestCommitish;
+
+  @Option(name = "-p", aliases = {"--manifest-path"},
+          usage = "", required = true)
+  private String manifestPath;
+
+  @Option(name = "-b", aliases = {"--new-branch"},
+      usage = "", required = true)
+  private String newBranch;
+
+  @Override
+  protected void run() {
+    stdout.println("Branching manifest:");
+    stdout.println(manifestRepo);
+    stdout.println(manifestCommitish);
+    stdout.println(manifestPath);
+
+
+    Project.NameKey p = new Project.NameKey(manifestRepo);
+    try {
+      Repository repo = gitRepoManager.openRepository(p);
+      ObjectId commitId = repo.resolve(manifestCommitish);
+      VersionedManifests vManifests = new VersionedManifests(manifestCommitish);
+      vManifests.load(repo, commitId);
+      CanonicalManifest manifests = new CanonicalManifest(vManifests);
+
+      Manifest manifest = manifests.getCanonicalManifest(manifestPath);
+
+      stdout.println("");
+      stdout.println("Branch '" + newBranch +
+          "' will be created for the following projects:");
+      for (com.amd.gerrit.plugins.manifestsubscription.manifest.Project proj :
+            manifest.getProject()) {
+        stdout.print(proj.getRevision());
+        stdout.print("\t");
+        stdout.println(proj.getName());
+      }
+
+      VersionedManifests.branchManifest(gitRepoManager, manifest, newBranch);
+
+    } catch (IOException | ConfigInvalidException | ManifestReadException |
+        JAXBException | GitAPIException e) {
+      e.printStackTrace(stderr);
+    }
+  }
+}
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestOp.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestOp.java
index d6fbe5d..dd3ba05 100644
--- a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestOp.java
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestOp.java
@@ -2,8 +2,16 @@
 
 import com.amd.gerrit.plugins.manifestsubscription.manifest.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.InvalidRefNameException;
+import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
+import org.eclipse.jgit.api.errors.RefNotFoundException;
+
+import java.io.IOException;
 
 public interface ManifestOp {
   boolean apply(Project project, String hash, String name,
-                GitRepositoryManager gitRepoManager);
+                GitRepositoryManager gitRepoManager) throws
+      RefAlreadyExistsException, InvalidRefNameException,
+      RefNotFoundException, GitAPIException, IOException;
 }
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestSubscription.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestSubscription.java
index 1dbf190..9899c9c 100644
--- a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestSubscription.java
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/ManifestSubscription.java
@@ -22,12 +22,12 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -281,7 +281,7 @@
             updateManifest(store, STORE_BRANCH_PREFIX + bp,
                            manifest, projectName);
 
-          } catch (ManifestReadException e) {
+          } catch (ManifestReadException | GitAPIException e) {
             e.printStackTrace();
           }
 
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/SshModule.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/SshModule.java
index b0940eb..c3363cb 100644
--- a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/SshModule.java
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/SshModule.java
@@ -20,5 +20,6 @@
   @Override
   protected void configureCommands() {
     command(ShowSubscription.class);
+    command(BranchManifest.class);
   }
 }
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/VersionedManifests.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/VersionedManifests.java
index d8e571d..91ffa7c 100644
--- a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/VersionedManifests.java
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/VersionedManifests.java
@@ -21,6 +21,11 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.VersionedMetaData;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.InvalidRefNameException;
+import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
+import org.eclipse.jgit.api.errors.RefNotFoundException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -202,6 +207,40 @@
     saveFile(path, output.toByteArray());
   }
 
+  static void branchManifest(GitRepositoryManager gitRepoManager,
+                             Manifest manifest,
+                             final String branch)
+                                          throws GitAPIException, IOException {
+    Table<String, String, String> lookup = HashBasedTable.create();
+    String defaultRef = null;
+
+    if (manifest.getDefault() != null) {
+      defaultRef = manifest.getDefault().getRevision();
+    }
+
+    ManifestOp op = new ManifestOp() {
+      @Override
+      public boolean apply(com.amd.gerrit.plugins.manifestsubscription.manifest.Project project,
+                           String hash, String name,
+                           GitRepositoryManager gitRepoManager) throws
+                                RefAlreadyExistsException,
+                                InvalidRefNameException, RefNotFoundException,
+                                GitAPIException, IOException {
+        Project.NameKey p = new Project.NameKey(project.getName());
+        Repository db = gitRepoManager.openRepository(p);
+        Git git = new Git(db);
+
+        git.branchCreate().setName(branch).setStartPoint(hash).call();
+
+        git.close();
+        //db.close(); //closed by git.close()
+        return true;
+      }
+    };
+
+    traverseManifestAndApplyOp(gitRepoManager, manifest.getProject(), defaultRef, op, lookup);
+  }
+
   /**
    * Pass in a {@link com.google.common.collect.Table} if you want to reuse
    * the lookup cache
@@ -211,7 +250,9 @@
    * @param lookup
    */
   static void affixManifest(GitRepositoryManager gitRepoManager,
-                            Manifest manifest, Table<String, String, String> lookup) {
+                            Manifest manifest,
+                            Table<String, String, String> lookup)
+                                          throws GitAPIException, IOException {
     if (lookup == null) {
       // project, branch, hash
       lookup = HashBasedTable.create();
@@ -242,7 +283,7 @@
       List<com.amd.gerrit.plugins.manifestsubscription.manifest.Project> projects,
       String defaultRef,
       ManifestOp op,
-      Table<String, String, String> lookup) {
+      Table<String, String, String> lookup) throws GitAPIException, IOException {
 
     String ref;
     String hash;
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index a5adbd3..22bd289 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -55,6 +55,7 @@
 Manifest represent raw XML
 CanonicalManifest resolve <include> and <remove-project>
 
+* TODO: possibly separate out manifest op into a separate plugin and make VesionedManifest as a libary (manifest-base or something)
 * TODO: generate snapshot manifest for all commit (Currently only generated on a per push/ref-updated basis)
   * Make it configurable, possibly per project, using plugin config or via special tag in manifest
 * TODO: monitor all manifest branch if no branch is specified
diff --git a/src/main/resources/Documentation/cmd-branch.md b/src/main/resources/Documentation/cmd-branch.md
new file mode 100644
index 0000000..8cd65a3
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-branch.md
@@ -0,0 +1,47 @@
+@PLUGIN@ branch
+==============
+
+NAME
+----
+@PLUGIN@ branch - Branch all projects on the server described by a manifest
+on the server.
+
+SYNOPSIS
+--------
+```
+ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ branch
+  {-r/--manifest-repo <manifest repo>}
+  {-c/--manifest-commit-ish <manifest commit-ish>}
+  {-p/--manifest-path <manifest path>}
+  {-b/--new-branch <new branch name>}
+  [--help]
+```
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group
+
+OPTIONS
+-------
+
+`-r/--manifest-repo <manifest repo>`
+`-c/--manifest-commit-ish <manifest commit-ish>`
+`-p/--manifest-path <manifest path>`
+: The manifest the branching operation is based on
+
+`-b/--new-branch <new branch name>`
+: The name of the branch that will be created on all projects in the manifest specified above
+
+EXAMPLES
+--------
+Branch all projects on the server described in the `default.xml` manifest in commit 5f51acb585b6a of repo `demo/build_manifest` to branch `releases/1.0.0`
+```
+ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ branch -r demo/build_manifest -c 5f51acb585b6a -p default.xml -b releases/1.0.0
+
+```
+
+Branch all projects on the server described in the `default.xml` manifest in commit v0.9-15-g5f51acb of repo `demo/build_manifest` to branch `releases/1.0.0`
+```
+ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ branch -r demo/build_manifest -c v0.9-15-g5f51acb -p default.xml -b releases/1.0.0
+
+```