blob: d448bd2dce159e0fcb4856eda6bda592b18f213e [file] [log] [blame]
// Copyright (C) 2010 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.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.ListChildProjects;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
@CommandMetaData(name = "set-project-parent", description = "Change the project permissions are inherited from")
final class AdminSetParent extends SshCommand {
private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);
@Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "new parent project")
private ProjectControl newParent;
@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<>();
@Argument(index = 0, required = false, multiValued = true, metaVar = "NAME",
usage = "projects to modify")
private List<ProjectControl> children = new ArrayList<>();
@Inject
private ProjectCache projectCache;
@Inject
private MetaDataUpdate.User metaDataUpdateFactory;
@Inject
private AllProjectsName allProjectsName;
@Inject
private Provider<ListChildProjects> listChildProjects;
private Project.NameKey newParentKey = null;
@Override
protected void run() 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<>();
grandParents.add(allProjectsName);
if (newParent != null) {
newParentKey = newParent.getProject().getNameKey();
// Catalog all grandparents of the "parent", we want to
// catch a cycle in the parent pointers before it occurs.
//
Project.NameKey gp = newParent.getProject().getParent();
while (gp != null && grandParents.add(gp)) {
final ProjectState s = projectCache.get(gp);
if (s != null) {
gp = s.getProject().getParent();
} else {
break;
}
}
}
final List<Project.NameKey> childProjects = Lists.newArrayList();
for (final ProjectControl pc : children) {
childProjects.add(pc.getProject().getNameKey());
}
if (oldParent != null) {
childProjects.addAll(getChildrenForReparenting(oldParent));
}
for (final Project.NameKey nameKey : childProjects) {
final String name = nameKey.get();
if (allProjectsName.equals(nameKey)) {
// Don't allow the wild card project to have a parent.
//
err.append("error: Cannot set parent of '").append(name).append("'\n");
continue;
}
if (grandParents.contains(nameKey) || nameKey.equals(newParentKey)) {
// Try to avoid creating a cycle in the parent pointers.
//
err.append("error: Cycle exists between '")
.append(name)
.append("' and '")
.append(newParentKey != null ? newParentKey.get() : allProjectsName.get())
.append("'\n");
continue;
}
try {
MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
try {
ProjectConfig config = ProjectConfig.read(md);
config.getProject().setParentName(newParentKey);
md.setMessage("Inherit access from "
+ (newParentKey != null ? newParentKey.get() : allProjectsName.get()) + "\n");
config.commit(md);
} finally {
md.close();
}
} catch (RepositoryNotFoundException notFound) {
err.append("error: Project ").append(name).append(" not found\n");
} catch (IOException e) {
final String msg = "Cannot update project " + name;
log.error(msg, e);
err.append("error: ").append(msg).append("\n");
} catch (ConfigInvalidException e) {
final String msg = "Cannot update project " + name;
log.error(msg, e);
err.append("error: ").append(msg).append("\n");
}
projectCache.evict(nameKey);
}
if (err.length() > 0) {
while (err.charAt(err.length() - 1) == '\n') {
err.setLength(err.length() - 1);
}
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.NameKey> getChildrenForReparenting(final ProjectControl parent) {
final List<Project.NameKey> childProjects = Lists.newArrayList();
final List<Project.NameKey> excluded =
new ArrayList<>(excludedChildren.size());
for (final ProjectControl excludedChild : excludedChildren) {
excluded.add(excludedChild.getProject().getNameKey());
}
final List<Project.NameKey> automaticallyExcluded =
new ArrayList<>(excludedChildren.size());
if (newParentKey != null) {
automaticallyExcluded.addAll(getAllParents(newParentKey));
}
for (final ProjectInfo child : listChildProjects.get().apply(
new ProjectResource(parent))) {
final Project.NameKey childName = new Project.NameKey(child.name);
if (!excluded.contains(childName)) {
if (!automaticallyExcluded.contains(childName)) {
childProjects.add(childName);
} 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) {
ProjectState ps = projectCache.get(projectName);
return ImmutableSet.copyOf(Iterables.transform(
ps != null ? ps.parents() : Collections.<ProjectState> emptySet(),
new Function<ProjectState, Project.NameKey> () {
@Override
public Project.NameKey apply(ProjectState in) {
return in.getProject().getNameKey();
}
}));
}
}