Merge "Add new SSH command to rename groups"
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 64c42a0..a6296b4 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -57,6 +57,9 @@
 link:cmd-ls-projects.html[gerrit ls-projects]::
 	List projects visible to the caller.
 
+link:cmd-ls-projects.html[gerrit rename-group]::
+	Rename an account group.
+
 link:cmd-set-reviewers.html[gerrit set-reviewers]::
         Add or remove reviewers on a change.
 
diff --git a/Documentation/cmd-rename-group.txt b/Documentation/cmd-rename-group.txt
new file mode 100644
index 0000000..e810727
--- /dev/null
+++ b/Documentation/cmd-rename-group.txt
@@ -0,0 +1,46 @@
+gerrit rename-group
+===================
+
+NAME
+----
+gerrit rename-group - Rename an account group.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit rename-group'
+  <GROUP>
+  <NEWNAME>
+
+DESCRIPTION
+-----------
+Renames an account group.
+
+ACCESS
+------
+Caller must be a member of the group owning the group to be renamed
+or be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<GROUP>::
+	Required; name of the group to be renamed.
+
+<NEWNAME>::
+	Required; new name of the group.
+
+EXAMPLES
+--------
+Rename the group "MyGroup" to "MyCommitters".
+
+====
+	$ ssh -p 29418 user@review.example.com gerrit rename-group MyGroup MyCommitters
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
index d7cd195..2d7770e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
@@ -19,51 +19,25 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.account.GroupDetailFactory;
-import com.google.gerrit.server.git.RenameGroupOp;
-import com.google.gwtorm.client.OrmDuplicateKeyException;
+import com.google.gerrit.server.account.PerformRenameGroup;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.Collections;
-import java.util.Date;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-
 class RenameGroup extends Handler<GroupDetail> {
   interface Factory {
     RenameGroup create(AccountGroup.Id id, String newName);
   }
 
-  private final ReviewDb db;
-  private final GroupCache groupCache;
-  private final GroupControl.Factory groupControlFactory;
-  private final GroupDetailFactory.Factory groupDetailFactory;
-  private final RenameGroupOp.Factory renameGroupOpFactory;
-  private final IdentifiedUser currentUser;
+  private final PerformRenameGroup.Factory performRenameGroupFactory;
 
   private final AccountGroup.Id groupId;
   private final String newName;
 
   @Inject
-  RenameGroup(final ReviewDb db, final GroupCache groupCache,
-      final GroupControl.Factory groupControlFactory,
-      final GroupDetailFactory.Factory groupDetailFactory,
-      final RenameGroupOp.Factory renameGroupOpFactory,
-      final IdentifiedUser currentUser,
+  RenameGroup(final PerformRenameGroup.Factory performRenameGroupFactory,
       @Assisted final AccountGroup.Id groupId, @Assisted final String newName) {
-    this.db = db;
-    this.groupCache = groupCache;
-    this.groupControlFactory = groupControlFactory;
-    this.groupDetailFactory = groupDetailFactory;
-    this.renameGroupOpFactory = renameGroupOpFactory;
-    this.currentUser = currentUser;
+    this.performRenameGroupFactory = performRenameGroupFactory;
     this.groupId = groupId;
     this.newName = newName;
   }
@@ -71,46 +45,6 @@
   @Override
   public GroupDetail call() throws OrmException, NameAlreadyUsedException,
       NoSuchGroupException {
-    final GroupControl ctl = groupControlFactory.validateFor(groupId);
-    final AccountGroup group = db.accountGroups().get(groupId);
-    if (group == null || !ctl.isOwner()) {
-      throw new NoSuchGroupException(groupId);
-    }
-
-    final AccountGroup.NameKey old = group.getNameKey();
-    final AccountGroup.NameKey key = new AccountGroup.NameKey(newName);
-
-    try {
-      final AccountGroupName id = new AccountGroupName(key, groupId);
-      db.accountGroupNames().insert(Collections.singleton(id));
-    } catch (OrmDuplicateKeyException dupeErr) {
-      // If we are using this identity, don't report the exception.
-      //
-      AccountGroupName other = db.accountGroupNames().get(key);
-      if (other != null && other.getId().equals(groupId)) {
-        return groupDetailFactory.create(groupId).call();
-      }
-
-      // Otherwise, someone else has this identity.
-      //
-      throw new NameAlreadyUsedException();
-    }
-
-    group.setNameKey(key);
-    db.accountGroups().update(Collections.singleton(group));
-
-    AccountGroupName priorName = db.accountGroupNames().get(old);
-    if (priorName != null) {
-      db.accountGroupNames().delete(Collections.singleton(priorName));
-    }
-
-    groupCache.evict(group);
-    groupCache.evictAfterRename(old);
-    renameGroupOpFactory.create( //
-        currentUser.newCommitterIdent(new Date(), TimeZone.getDefault()), //
-        group.getGroupUUID(), //
-        old.get(), newName).start(0, TimeUnit.MILLISECONDS);
-
-    return groupDetailFactory.create(groupId).call();
+    return performRenameGroupFactory.create().renameGroup(groupId, newName);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
new file mode 100644
index 0000000..92f5fbf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2011 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.account;
+
+import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountGroupName;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.RenameGroupOp;
+import com.google.gwtorm.client.OrmDuplicateKeyException;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+public class PerformRenameGroup {
+
+  public interface Factory {
+    PerformRenameGroup create();
+  }
+
+  private final ReviewDb db;
+  private final GroupCache groupCache;
+  private final GroupControl.Factory groupControlFactory;
+  private final GroupDetailFactory.Factory groupDetailFactory;
+  private final RenameGroupOp.Factory renameGroupOpFactory;
+  private final IdentifiedUser currentUser;
+
+  @Inject
+  PerformRenameGroup(final ReviewDb db, final GroupCache groupCache,
+      final GroupControl.Factory groupControlFactory,
+      final GroupDetailFactory.Factory groupDetailFactory,
+      final RenameGroupOp.Factory renameGroupOpFactory,
+      final IdentifiedUser currentUser) {
+    this.db = db;
+    this.groupCache = groupCache;
+    this.groupControlFactory = groupControlFactory;
+    this.groupDetailFactory = groupDetailFactory;
+    this.renameGroupOpFactory = renameGroupOpFactory;
+    this.currentUser = currentUser;
+  }
+
+  public GroupDetail renameGroup(final String groupName,
+      final String newGroupName) throws OrmException, NameAlreadyUsedException,
+      NoSuchGroupException {
+    final AccountGroup.NameKey groupNameKey =
+        new AccountGroup.NameKey(groupName);
+    final AccountGroup group = groupCache.get(groupNameKey);
+    if (group == null) {
+      throw new NoSuchGroupException(groupNameKey);
+    }
+    return renameGroup(group.getId(), newGroupName);
+  }
+
+  public GroupDetail renameGroup(final AccountGroup.Id groupId,
+      final String newName) throws OrmException, NameAlreadyUsedException,
+      NoSuchGroupException {
+    final GroupControl ctl = groupControlFactory.validateFor(groupId);
+    final AccountGroup group = db.accountGroups().get(groupId);
+    if (group == null || !ctl.isOwner()) {
+      throw new NoSuchGroupException(groupId);
+    }
+
+    final AccountGroup.NameKey old = group.getNameKey();
+    final AccountGroup.NameKey key = new AccountGroup.NameKey(newName);
+
+    try {
+      final AccountGroupName id = new AccountGroupName(key, groupId);
+      db.accountGroupNames().insert(Collections.singleton(id));
+    } catch (OrmDuplicateKeyException dupeErr) {
+      // If we are using this identity, don't report the exception.
+      //
+      AccountGroupName other = db.accountGroupNames().get(key);
+      if (other != null && other.getId().equals(groupId)) {
+        return groupDetailFactory.create(groupId).call();
+      }
+
+      // Otherwise, someone else has this identity.
+      //
+      throw new NameAlreadyUsedException();
+    }
+
+    group.setNameKey(key);
+    db.accountGroups().update(Collections.singleton(group));
+
+    AccountGroupName priorName = db.accountGroupNames().get(old);
+    if (priorName != null) {
+      db.accountGroupNames().delete(Collections.singleton(priorName));
+    }
+
+    groupCache.evict(group);
+    groupCache.evictAfterRename(old);
+    renameGroupOpFactory.create( //
+        currentUser.newCommitterIdent(new Date(), TimeZone.getDefault()), //
+        group.getGroupUUID(), //
+        old.get(), newName).start(0, TimeUnit.MILLISECONDS);
+
+    return groupDetailFactory.create(groupId).call();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index 3ba9aab..42c2531 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.account.GroupDetailFactory;
 import com.google.gerrit.server.account.GroupMembersFactory;
 import com.google.gerrit.server.account.PerformCreateGroup;
+import com.google.gerrit.server.account.PerformRenameGroup;
 import com.google.gerrit.server.git.CreateCodeReviewNotes;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -89,6 +90,7 @@
     factory(MergeFailSender.Factory.class);
     factory(RegisterNewEmailSender.Factory.class);
     factory(PerformCreateGroup.Factory.class);
+    factory(PerformRenameGroup.Factory.class);
     factory(GroupDetailFactory.Factory.class);
     factory(GroupMembersFactory.Factory.class);
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index 6a69791..da04196 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
@@ -28,6 +28,7 @@
     command(gerrit, "approve").to(ReviewCommand.class);
     command(gerrit, "create-account").to(CreateAccountCommand.class);
     command(gerrit, "create-group").to(CreateGroupCommand.class);
+    command(gerrit, "rename-group").to(RenameGroupCommand.class);
     command(gerrit, "create-project").to(CreateProject.class);
     command(gerrit, "gsql").to(AdminQueryShell.class);
     command(gerrit, "set-reviewers").to(SetReviewersCommand.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
new file mode 100644
index 0000000..b6d5bf5
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2011 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.sshd.commands;
+
+import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.server.account.PerformRenameGroup;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+
+import java.io.IOException;
+
+public class RenameGroupCommand extends BaseCommand {
+
+  @Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of the group to be renamed")
+  private String groupName;
+
+  @Argument(index = 1, required = true, metaVar = "NEWNAME", usage = "new name of the group")
+  private String newGroupName;
+
+  @Inject
+  private PerformRenameGroup.Factory performRenameGroupFactory;
+
+  @Override
+  public void start(final Environment env) throws IOException {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        parseCommandLine();
+        try {
+          performRenameGroupFactory.create().renameGroup(groupName, newGroupName);
+        } catch (OrmException e) {
+          throw die(e);
+        } catch (NameAlreadyUsedException e) {
+          throw die(e);
+        } catch (NoSuchGroupException e) {
+          throw die(e);
+        }
+      }
+    });
+  }
+}