From 04388419981597c17a1fe7c8a6e4e1975d989b94 Mon Sep 17 00:00:00 2001 From: Edwin Kempin Date: Tue, 27 Sep 2011 08:36:17 +0200 Subject: [PATCH] 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 --- Documentation/cmd-set-project-parent.txt | 22 +++ .../gerrit/sshd/commands/AdminSetParent.java | 138 ++++++++++++++++-- 2 files changed, 146 insertions(+), 14 deletions(-) diff --git a/Documentation/cmd-set-project-parent.txt b/Documentation/cmd-set-project-parent.txt index 9dd11d7d3c..1e7e6c5607 100644 --- a/Documentation/cmd-set-project-parent.txt +++ b/Documentation/cmd-set-project-parent.txt @@ -10,6 +10,8 @@ SYNOPSIS [verse] 'ssh' -p 'gerrit set-project-parent' [--parent ] + [--children-of ] + [--exclude ] ... 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 -------- diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java index 85b66143ce..91469f7658 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java @@ -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 excludedChildren = new ArrayList(); + + @Argument(index = 0, required = false, multiValued = true, metaVar = "NAME", + usage = "projects to modify") private List children = new ArrayList(); @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 grandParents = new HashSet(); - 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 childProjects = new ArrayList(); 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 getChildrenForReparenting(final ProjectControl parent) { + final List childProjects = new ArrayList(); + final List excluded = + new ArrayList(excludedChildren.size()); + for (final ProjectControl excludedChild : excludedChildren) { + excluded.add(excludedChild.getProject().getNameKey()); + } + final List automaticallyExcluded = + new ArrayList(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 getAllParents(final Project.NameKey projectName) { + final Set parents = new HashSet(); + 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 getChildren(final Project.NameKey parentName) { + final List childProjects = new ArrayList(); + 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, null 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; + } }