Batch update project.config on group renames

When a group is renamed through the web UI, each project.config
(and corresponding groups file) is updated with the new name if
there is a permission rule referencing the group.  This ensures
authorship of the relevant modification is charged back to the
user that modified the group name.

Change-Id: Ic44c46553b34548e792269e6ce835ecffea42db7
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2011-01-11 16:51:37 -08:00
parent 71bebadf9f
commit 4b5191e689
4 changed files with 174 additions and 0 deletions

View File

@@ -21,14 +21,19 @@ 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.git.RenameGroupOp;
import com.google.gwtorm.client.OrmDuplicateKeyException;
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 {
@@ -39,6 +44,8 @@ class RenameGroup extends Handler<GroupDetail> {
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 AccountGroup.Id groupId;
private final String newName;
@@ -47,11 +54,15 @@ class RenameGroup extends Handler<GroupDetail> {
RenameGroup(final ReviewDb db, final GroupCache groupCache,
final GroupControl.Factory groupControlFactory,
final GroupDetailFactory.Factory groupDetailFactory,
final RenameGroupOp.Factory renameGroupOpFactory,
final IdentifiedUser currentUser,
@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.groupId = groupId;
this.newName = newName;
}
@@ -94,6 +105,10 @@ class RenameGroup extends Handler<GroupDetail> {
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();
}

View File

@@ -20,6 +20,7 @@ import com.google.gerrit.server.config.FactoryModule;
public class GitModule extends FactoryModule {
@Override
protected void configure() {
factory(RenameGroupOp.Factory.class);
factory(MetaDataUpdate.InternalFactory.class);
bind(MetaDataUpdate.Server.class);
}

View File

@@ -130,6 +130,11 @@ public class ProjectConfig extends VersionedMetaData {
return group;
}
/** @return the group reference, if the group is used by at least one rule. */
public GroupReference getGroup(AccountGroup.UUID uuid) {
return groupsByUUID.get(uuid);
}
/**
* Check all GroupReferences use current group name, repairing stale ones.
*

View File

@@ -0,0 +1,153 @@
// 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.git;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.Project.NameKey;
import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.PersonIdent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class RenameGroupOp extends DefaultQueueOp {
public interface Factory {
RenameGroupOp create(@Assisted("author") PersonIdent author,
@Assisted AccountGroup.UUID uuid, @Assisted("oldName") String oldName,
@Assisted("newName") String newName);
}
private static final int MAX_TRIES = 10;
private static final Logger log =
LoggerFactory.getLogger(RenameGroupOp.class);
private final ProjectCache projectCache;
private final MetaDataUpdate.Server metaDataUpdateFactory;
private final PersonIdent author;
private final AccountGroup.UUID uuid;
private final String oldName;
private final String newName;
private final List<Project.NameKey> retryOn;
private boolean tryingAgain;
@Inject
public RenameGroupOp(WorkQueue workQueue, ProjectCache projectCache,
MetaDataUpdate.Server metaDataUpdateFactory,
@Assisted("author") PersonIdent author, @Assisted AccountGroup.UUID uuid,
@Assisted("oldName") String oldName, @Assisted("newName") String newName) {
super(workQueue);
this.projectCache = projectCache;
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.author = author;
this.uuid = uuid;
this.oldName = oldName;
this.newName = newName;
this.retryOn = new ArrayList<Project.NameKey>();
}
@Override
public void run() {
Iterable<NameKey> names = tryingAgain ? retryOn : projectCache.all();
for (Project.NameKey projectName : names) {
ProjectConfig config = projectCache.get(projectName).getConfig();
GroupReference ref = config.getGroup(uuid);
if (ref == null || newName.equals(ref.getName())) {
continue;
}
try {
MetaDataUpdate md = metaDataUpdateFactory.create(projectName);
try {
rename(md);
} finally {
md.close();
}
} catch (RepositoryNotFoundException noProject) {
continue;
} catch (ConfigInvalidException err) {
log.error("Cannot rename group " + oldName + " in " + projectName, err);
} catch (IOException err) {
log.error("Cannot rename group " + oldName + " in " + projectName, err);
}
}
// If one or more projects did not update, wait 5 minutes
// and give it another attempt.
if (!retryOn.isEmpty() && !tryingAgain) {
tryingAgain = true;
start(5, TimeUnit.MINUTES);
}
}
private void rename(MetaDataUpdate md) throws IOException,
ConfigInvalidException {
boolean success = false;
for (int attempts = 0; !success && attempts < MAX_TRIES; attempts++) {
ProjectConfig config = ProjectConfig.read(md);
// The group isn't referenced, or its name has been fixed already.
//
GroupReference ref = config.getGroup(uuid);
if (ref == null || newName.equals(ref.getName())) {
projectCache.evict(config.getProject());
return;
}
ref.setName(newName);
md.getCommitBuilder().setAuthor(author);
md.setMessage("Rename group " + oldName + " to " + newName + "\n");
if (config.commit(md)) {
projectCache.evict(config.getProject());
success = true;
} else {
try {
Thread.sleep(25 /* milliseconds */);
} catch (InterruptedException wakeUp) {
continue;
}
}
}
if (!success) {
if (tryingAgain) {
log.warn("Could not rename group " + oldName + " to " + newName
+ " in " + md.getProjectName().get());
} else {
retryOn.add(md.getProjectName());
}
}
}
@Override
public String toString() {
return "Rename Group " + oldName;
}
}