Support to reparent all children of a parent project

This change adds a new option to the 'set-project-parent' command that
allows to reparent all child projects of one parent project to another
parent project. It is possible to specify certain children of the old parent
that should not be reparented.

This is e.g. useful if you have defined a set of default access rights on a
parent project P and now one of its child projects C should not inherit these
default access rights. The new functionality allows you to easily introduce a
new hierarchy level under P:
1. create new child project D under P which defines the default access rights
2. reparent all child projects of P to D, except C (and D)
3. remove the default access rights from P

Change-Id: I3c6d8407f357f601c8000e6bc5cbc696012ecd09
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin 2011-09-27 08:36:17 +02:00
parent 08dd4df414
commit 0438841998
2 changed files with 146 additions and 14 deletions

View File

@ -10,6 +10,8 @@ SYNOPSIS
[verse]
'ssh' -p <port> <host> 'gerrit set-project-parent'
[--parent <NAME>]
[--children-of <NAME>]
[--exclude <NAME>]
<NAME> ...
DESCRIPTION
@ -33,6 +35,19 @@ OPTIONS
Name of the parent to inherit through. If not specified,
the parent is set back to the default `All-Projects`.
--children-of::
Name of the parent project for which all child projects should be
reparented. If the new parent project or any project in its
parent line is a child of this parent project it is automatically
excluded from reparenting.
--exclude::
Name of a child project that should not be reparented. This
option can only be used if the option --children-of is set.
Multiple child projects can be excluded from reparenting by
specifying the --exclude option multiple times. Excluding a
project that is not a child project has no effect.
EXAMPLES
--------
Configure `kernel/omap` to inherit permissions from `kernel/common`:
@ -41,6 +56,13 @@ Configure `kernel/omap` to inherit permissions from `kernel/common`:
$ ssh -p 29418 review.example.com gerrit set-project-parent --parent kernel/common kernel/omap
====
Reparent all children of `myParent` to `myOtherParent`:
====
$ ssh -p 29418 review.example.com gerrit set-project-parent \
--children-of myParent --parent myOtherParent
====
SEE ALSO
--------

View File

@ -32,6 +32,7 @@ import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -42,7 +43,16 @@ final class AdminSetParent extends BaseCommand {
@Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "new parent project")
private ProjectControl newParent;
@Argument(index = 0, required = true, multiValued = true, metaVar = "NAME", usage = "projects to modify")
@Option(name = "--children-of", metaVar = "NAME",
usage = "parent project for which the child projects should be reparented")
private ProjectControl oldParent;
@Option(name = "--exclude", metaVar = "NAME",
usage = "child project of old parent project which should not be reparented")
private List<ProjectControl> excludedChildren = new ArrayList<ProjectControl>();
@Argument(index = 0, required = false, multiValued = true, metaVar = "NAME",
usage = "projects to modify")
private List<ProjectControl> children = new ArrayList<ProjectControl>();
@Inject
@ -54,21 +64,37 @@ final class AdminSetParent extends BaseCommand {
@Inject
private AllProjectsName allProjectsName;
private PrintWriter stdout;
private Project.NameKey newParentKey = null;
@Override
public void start(final Environment env) {
startThread(new CommandRunnable() {
@Override
public void run() throws Exception {
parseCommandLine();
updateParents();
stdout = toPrintWriter(out);
try {
parseCommandLine();
updateParents();
} finally {
stdout.flush();
}
}
});
}
private void updateParents() throws Failure {
if (oldParent == null && children.isEmpty()) {
throw new UnloggedFailure(1, "fatal: child projects have to be specified as " +
"arguments or the --children-of option has to be set");
}
if (oldParent == null && !excludedChildren.isEmpty()) {
throw new UnloggedFailure(1, "fatal: --exclude can only be used together " +
"with --children-of");
}
final StringBuilder err = new StringBuilder();
final Set<Project.NameKey> grandParents = new HashSet<Project.NameKey>();
Project.NameKey newParentKey;
grandParents.add(allProjectsName);
@ -87,24 +113,28 @@ final class AdminSetParent extends BaseCommand {
break;
}
}
} else {
// If no parent was selected, set to NULL to use the default.
//
newParentKey = null;
}
final List<Project> childProjects = new ArrayList<Project>();
for (final ProjectControl pc : children) {
final Project.NameKey key = pc.getProject().getNameKey();
final String name = pc.getProject().getName();
childProjects.add(pc.getProject());
}
if (oldParent != null) {
childProjects.addAll(getChildrenForReparenting(oldParent));
}
if (allProjectsName.equals(key)) {
for (final Project project : childProjects) {
final String name = project.getName();
final Project.NameKey nameKey = project.getNameKey();
if (allProjectsName.equals(nameKey)) {
// Don't allow the wild card project to have a parent.
//
err.append("error: Cannot set parent of '" + name + "'\n");
continue;
}
if (grandParents.contains(key) || key.equals(newParentKey)) {
if (grandParents.contains(nameKey) || nameKey.equals(newParentKey)) {
// Try to avoid creating a cycle in the parent pointers.
//
err.append("error: Cycle exists between '" + name + "' and '"
@ -114,7 +144,7 @@ final class AdminSetParent extends BaseCommand {
}
try {
MetaDataUpdate md = metaDataUpdateFactory.create(key);
MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
try {
ProjectConfig config = ProjectConfig.read(md);
config.getProject().setParentName(newParentKey);
@ -134,7 +164,7 @@ final class AdminSetParent extends BaseCommand {
throw new Failure(1, "Cannot update project " + name, e);
}
projectCache.evict(pc.getProject());
projectCache.evict(project);
}
if (err.length() > 0) {
@ -144,4 +174,84 @@ final class AdminSetParent extends BaseCommand {
throw new UnloggedFailure(1, err.toString());
}
}
/**
* Returns the children of the specified parent project that should be
* reparented. The returned list of child projects does not contain projects
* that were specified to be excluded from reparenting.
*/
private List<Project> getChildrenForReparenting(final ProjectControl parent) {
final List<Project> childProjects = new ArrayList<Project>();
final List<Project.NameKey> excluded =
new ArrayList<Project.NameKey>(excludedChildren.size());
for (final ProjectControl excludedChild : excludedChildren) {
excluded.add(excludedChild.getProject().getNameKey());
}
final List<Project.NameKey> automaticallyExcluded =
new ArrayList<Project.NameKey>(excludedChildren.size());
if (newParentKey != null) {
automaticallyExcluded.addAll(getAllParents(newParentKey));
}
for (final Project child : getChildren(parent.getProject().getNameKey())) {
final Project.NameKey childName = child.getNameKey();
if (!excluded.contains(childName)) {
if (!automaticallyExcluded.contains(childName)) {
childProjects.add(child);
} else {
stdout.println("Automatically excluded '" + childName + "' " +
"from reparenting because it is in the parent " +
"line of the new parent '" + newParentKey + "'.");
}
}
}
return childProjects;
}
private Set<Project.NameKey> getAllParents(final Project.NameKey projectName) {
final Set<Project.NameKey> parents = new HashSet<Project.NameKey>();
Project.NameKey p = projectName;
while (p != null && parents.add(p)) {
final ProjectState e = projectCache.get(p);
if (e == null) {
// If we can't get it from the cache, pretend it's not present.
break;
}
p = getParentName(e.getProject());
}
return parents;
}
private List<Project> getChildren(final Project.NameKey parentName) {
final List<Project> childProjects = new ArrayList<Project>();
for (final Project.NameKey projectName : projectCache.all()) {
final ProjectState e = projectCache.get(projectName);
if (e == null) {
// If we can't get it from the cache, pretend it's not present.
continue;
}
if (parentName.equals(getParentName(e.getProject()))) {
childProjects.add(e.getProject());
}
}
return childProjects;
}
/**
* Returns the project parent name.
*
* @return Project parent name, <code>null</code> for the 'All-Projects' root
* project
*/
private Project.NameKey getParentName(final Project project) {
if (project.getParent() != null) {
return project.getParent();
}
if (project.getNameKey().equals(allProjectsName)) {
return null;
}
return allProjectsName;
}
}