| // 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 static com.google.gerrit.server.project.ProjectCache.illegalState; |
| import static java.util.stream.Collectors.toList; |
| |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.extensions.api.projects.ParentInput; |
| import com.google.gerrit.extensions.common.ProjectInfo; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.ResourceNotFoundException; |
| import com.google.gerrit.extensions.restapi.UnprocessableEntityException; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.ProjectResource; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gerrit.server.restapi.project.ListChildProjects; |
| import com.google.gerrit.server.restapi.project.SetParent; |
| import com.google.gerrit.sshd.CommandMetaData; |
| import com.google.gerrit.sshd.SshCommand; |
| import com.google.inject.Inject; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.Set; |
| import org.kohsuke.args4j.Argument; |
| import org.kohsuke.args4j.Option; |
| |
| @CommandMetaData( |
| name = "set-project-parent", |
| description = "Change the project permissions are inherited from") |
| final class SetParentCommand extends SshCommand { |
| @Option( |
| name = "--parent", |
| aliases = {"-p"}, |
| metaVar = "NAME", |
| usage = "new parent project") |
| private ProjectState newParent; |
| |
| @Option( |
| name = "--children-of", |
| metaVar = "NAME", |
| usage = "parent project for which the child projects should be reparented") |
| private ProjectState oldParent; |
| |
| @Option( |
| name = "--exclude", |
| metaVar = "NAME", |
| usage = "child project of old parent project which should not be reparented") |
| private List<ProjectState> excludedChildren = new ArrayList<>(); |
| |
| @Argument( |
| index = 0, |
| required = false, |
| multiValued = true, |
| metaVar = "NAME", |
| usage = "projects to modify") |
| private List<ProjectState> children = new ArrayList<>(); |
| |
| @Inject private ProjectCache projectCache; |
| |
| @Inject private ListChildProjects listChildProjects; |
| |
| @Inject private SetParent setParent; |
| |
| private Project.NameKey newParentKey; |
| |
| private static ParentInput parentInput(String parent) { |
| ParentInput input = new ParentInput(); |
| input.parent = parent; |
| return input; |
| } |
| |
| @Override |
| protected void run() throws Failure { |
| enableGracefulStop(); |
| if (oldParent == null && children.isEmpty()) { |
| throw die( |
| "child projects have to be specified as " |
| + "arguments or the --children-of option has to be set"); |
| } |
| if (oldParent == null && !excludedChildren.isEmpty()) { |
| throw die("--exclude can only be used together with --children-of"); |
| } |
| |
| final StringBuilder err = new StringBuilder(); |
| |
| if (newParent != null) { |
| newParentKey = newParent.getProject().getNameKey(); |
| } |
| |
| final List<Project.NameKey> childProjects = |
| children.stream().map(ProjectState::getNameKey).collect(toList()); |
| if (oldParent != null) { |
| try { |
| childProjects.addAll(getChildrenForReparenting(oldParent)); |
| } catch (PermissionBackendException e) { |
| throw new Failure(1, "permissions unavailable", e); |
| } catch (Exception e) { |
| throw new Failure(1, "failure in request", e); |
| } |
| } |
| |
| for (Project.NameKey nameKey : childProjects) { |
| final String name = nameKey.get(); |
| ProjectState project = projectCache.get(nameKey).orElseThrow(illegalState(nameKey)); |
| try { |
| setParent.apply(new ProjectResource(project, user), parentInput(newParentKey.get())); |
| } catch (AuthException e) { |
| err.append("error: insuffient access rights to change parent of '") |
| .append(name) |
| .append("'\n"); |
| } catch (ResourceConflictException | ResourceNotFoundException | BadRequestException e) { |
| err.append("error: ").append(e.getMessage()).append("'\n"); |
| } catch (UnprocessableEntityException | IOException e) { |
| throw new Failure(1, "failure in request", e); |
| } catch (PermissionBackendException e) { |
| throw new Failure(1, "permissions unavailable", e); |
| } |
| } |
| |
| if (err.length() > 0) { |
| while (err.charAt(err.length() - 1) == '\n') { |
| err.setLength(err.length() - 1); |
| } |
| throw die(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(ProjectState parent) throws Exception { |
| final List<Project.NameKey> childProjects = new ArrayList<>(); |
| final List<Project.NameKey> excluded = new ArrayList<>(excludedChildren.size()); |
| for (ProjectState excludedChild : excludedChildren) { |
| excluded.add(excludedChild.getProject().getNameKey()); |
| } |
| final List<Project.NameKey> automaticallyExcluded = new ArrayList<>(excludedChildren.size()); |
| if (newParentKey != null) { |
| automaticallyExcluded.addAll(getAllParents(newParentKey)); |
| } |
| for (ProjectInfo child : listChildProjects.apply(new ProjectResource(parent, user)).value()) { |
| final Project.NameKey childName = 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(Project.NameKey projectName) { |
| Optional<ProjectState> ps = projectCache.get(projectName); |
| if (!ps.isPresent()) { |
| return Collections.emptySet(); |
| } |
| return ps.get().parents().transform(ProjectState::getNameKey).toSet(); |
| } |
| } |