Line data Source code
1 : // Copyright (C) 2010 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License 14 : 15 : package com.google.gerrit.sshd.commands; 16 : 17 : import static com.google.gerrit.server.project.ProjectCache.illegalState; 18 : import static java.util.stream.Collectors.toList; 19 : 20 : import com.google.gerrit.entities.Project; 21 : import com.google.gerrit.extensions.api.projects.ParentInput; 22 : import com.google.gerrit.extensions.common.ProjectInfo; 23 : import com.google.gerrit.extensions.restapi.AuthException; 24 : import com.google.gerrit.extensions.restapi.BadRequestException; 25 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 26 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 27 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException; 28 : import com.google.gerrit.server.permissions.PermissionBackendException; 29 : import com.google.gerrit.server.project.ProjectCache; 30 : import com.google.gerrit.server.project.ProjectResource; 31 : import com.google.gerrit.server.project.ProjectState; 32 : import com.google.gerrit.server.restapi.project.ListChildProjects; 33 : import com.google.gerrit.server.restapi.project.SetParent; 34 : import com.google.gerrit.sshd.CommandMetaData; 35 : import com.google.gerrit.sshd.SshCommand; 36 : import com.google.inject.Inject; 37 : import java.io.IOException; 38 : import java.util.ArrayList; 39 : import java.util.Collections; 40 : import java.util.List; 41 : import java.util.Optional; 42 : import java.util.Set; 43 : import org.kohsuke.args4j.Argument; 44 : import org.kohsuke.args4j.Option; 45 : 46 : @CommandMetaData( 47 : name = "set-project-parent", 48 : description = "Change the project permissions are inherited from") 49 1 : final class SetParentCommand extends SshCommand { 50 : @Option( 51 : name = "--parent", 52 : aliases = {"-p"}, 53 : metaVar = "NAME", 54 : usage = "new parent project") 55 : private ProjectState newParent; 56 : 57 : @Option( 58 : name = "--children-of", 59 : metaVar = "NAME", 60 : usage = "parent project for which the child projects should be reparented") 61 : private ProjectState oldParent; 62 : 63 1 : @Option( 64 : name = "--exclude", 65 : metaVar = "NAME", 66 : usage = "child project of old parent project which should not be reparented") 67 : private List<ProjectState> excludedChildren = new ArrayList<>(); 68 : 69 1 : @Argument( 70 : index = 0, 71 : required = false, 72 : multiValued = true, 73 : metaVar = "NAME", 74 : usage = "projects to modify") 75 : private List<ProjectState> children = new ArrayList<>(); 76 : 77 : @Inject private ProjectCache projectCache; 78 : 79 : @Inject private ListChildProjects listChildProjects; 80 : 81 : @Inject private SetParent setParent; 82 : 83 : private Project.NameKey newParentKey; 84 : 85 : private static ParentInput parentInput(String parent) { 86 0 : ParentInput input = new ParentInput(); 87 0 : input.parent = parent; 88 0 : return input; 89 : } 90 : 91 : @Override 92 : protected void run() throws Failure { 93 0 : enableGracefulStop(); 94 0 : if (oldParent == null && children.isEmpty()) { 95 0 : throw die( 96 : "child projects have to be specified as " 97 : + "arguments or the --children-of option has to be set"); 98 : } 99 0 : if (oldParent == null && !excludedChildren.isEmpty()) { 100 0 : throw die("--exclude can only be used together with --children-of"); 101 : } 102 : 103 0 : final StringBuilder err = new StringBuilder(); 104 : 105 0 : if (newParent != null) { 106 0 : newParentKey = newParent.getProject().getNameKey(); 107 : } 108 : 109 0 : final List<Project.NameKey> childProjects = 110 0 : children.stream().map(ProjectState::getNameKey).collect(toList()); 111 0 : if (oldParent != null) { 112 : try { 113 0 : childProjects.addAll(getChildrenForReparenting(oldParent)); 114 0 : } catch (PermissionBackendException e) { 115 0 : throw new Failure(1, "permissions unavailable", e); 116 0 : } catch (Exception e) { 117 0 : throw new Failure(1, "failure in request", e); 118 0 : } 119 : } 120 : 121 0 : for (Project.NameKey nameKey : childProjects) { 122 0 : final String name = nameKey.get(); 123 0 : ProjectState project = projectCache.get(nameKey).orElseThrow(illegalState(nameKey)); 124 : try { 125 0 : setParent.apply(new ProjectResource(project, user), parentInput(newParentKey.get())); 126 0 : } catch (AuthException e) { 127 0 : err.append("error: insuffient access rights to change parent of '") 128 0 : .append(name) 129 0 : .append("'\n"); 130 0 : } catch (ResourceConflictException | ResourceNotFoundException | BadRequestException e) { 131 0 : err.append("error: ").append(e.getMessage()).append("'\n"); 132 0 : } catch (UnprocessableEntityException | IOException e) { 133 0 : throw new Failure(1, "failure in request", e); 134 0 : } catch (PermissionBackendException e) { 135 0 : throw new Failure(1, "permissions unavailable", e); 136 0 : } 137 0 : } 138 : 139 0 : if (err.length() > 0) { 140 0 : while (err.charAt(err.length() - 1) == '\n') { 141 0 : err.setLength(err.length() - 1); 142 : } 143 0 : throw die(err.toString()); 144 : } 145 0 : } 146 : 147 : /** 148 : * Returns the children of the specified parent project that should be reparented. The returned 149 : * list of child projects does not contain projects that were specified to be excluded from 150 : * reparenting. 151 : */ 152 : private List<Project.NameKey> getChildrenForReparenting(ProjectState parent) throws Exception { 153 0 : final List<Project.NameKey> childProjects = new ArrayList<>(); 154 0 : final List<Project.NameKey> excluded = new ArrayList<>(excludedChildren.size()); 155 0 : for (ProjectState excludedChild : excludedChildren) { 156 0 : excluded.add(excludedChild.getProject().getNameKey()); 157 0 : } 158 0 : final List<Project.NameKey> automaticallyExcluded = new ArrayList<>(excludedChildren.size()); 159 0 : if (newParentKey != null) { 160 0 : automaticallyExcluded.addAll(getAllParents(newParentKey)); 161 : } 162 0 : for (ProjectInfo child : listChildProjects.apply(new ProjectResource(parent, user)).value()) { 163 0 : final Project.NameKey childName = Project.nameKey(child.name); 164 0 : if (!excluded.contains(childName)) { 165 0 : if (!automaticallyExcluded.contains(childName)) { 166 0 : childProjects.add(childName); 167 : } else { 168 0 : stdout.println( 169 : "Automatically excluded '" 170 : + childName 171 : + "' " 172 : + "from reparenting because it is in the parent " 173 : + "line of the new parent '" 174 : + newParentKey 175 : + "'."); 176 : } 177 : } 178 0 : } 179 0 : return childProjects; 180 : } 181 : 182 : private Set<Project.NameKey> getAllParents(Project.NameKey projectName) { 183 0 : Optional<ProjectState> ps = projectCache.get(projectName); 184 0 : if (!ps.isPresent()) { 185 0 : return Collections.emptySet(); 186 : } 187 0 : return ps.get().parents().transform(ProjectState::getNameKey).toSet(); 188 : } 189 : }