diff options
Diffstat (limited to 'java/com/google/gerrit/server/restapi/project/CreateProject.java')
-rw-r--r-- | java/com/google/gerrit/server/restapi/project/CreateProject.java | 424 |
1 files changed, 424 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java new file mode 100644 index 0000000000..b92fba8fe4 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java @@ -0,0 +1,424 @@ +// Copyright (C) 2013 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.server.restapi.project; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.common.ProjectUtil; +import com.google.gerrit.common.data.AccessSection; +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.common.data.GroupDescription; +import com.google.gerrit.common.data.GroupReference; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.common.data.PermissionRule; +import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.extensions.api.projects.ConfigInput; +import com.google.gerrit.extensions.api.projects.ProjectInput; +import com.google.gerrit.extensions.client.InheritableBoolean; +import com.google.gerrit.extensions.client.SubmitType; +import com.google.gerrit.extensions.common.ProjectInfo; +import com.google.gerrit.extensions.events.NewProjectCreatedListener; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestCollectionCreateView; +import com.google.gerrit.extensions.restapi.TopLevelResource; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.BooleanProjectConfig; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.GroupBackend; +import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.config.ProjectOwnerGroupsProvider; +import com.google.gerrit.server.config.RepositoryConfig; +import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.RepositoryCaseMismatchException; +import com.google.gerrit.server.git.meta.MetaDataUpdate; +import com.google.gerrit.server.group.GroupResolver; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.plugincontext.PluginItemContext; +import com.google.gerrit.server.plugincontext.PluginSetContext; +import com.google.gerrit.server.project.CreateProjectArgs; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectConfig; +import com.google.gerrit.server.project.ProjectJson; +import com.google.gerrit.server.project.ProjectNameLockManager; +import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.project.ProjectState; +import com.google.gerrit.server.validators.ProjectCreationValidationListener; +import com.google.gerrit.server.validators.ValidationException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.locks.Lock; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceiveCommand; + +@RequiresCapability(GlobalCapability.CREATE_PROJECT) +@Singleton +public class CreateProject + implements RestCollectionCreateView<TopLevelResource, ProjectResource, ProjectInput> { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final Provider<ProjectsCollection> projectsCollection; + private final Provider<GroupResolver> groupResolver; + private final PluginSetContext<ProjectCreationValidationListener> + projectCreationValidationListeners; + private final ProjectJson json; + private final GitRepositoryManager repoManager; + private final PluginSetContext<NewProjectCreatedListener> createdListeners; + private final ProjectCache projectCache; + private final GroupBackend groupBackend; + private final ProjectOwnerGroupsProvider.Factory projectOwnerGroups; + private final MetaDataUpdate.User metaDataUpdateFactory; + private final GitReferenceUpdated referenceUpdated; + private final RepositoryConfig repositoryCfg; + private final PersonIdent serverIdent; + private final Provider<IdentifiedUser> identifiedUser; + private final Provider<PutConfig> putConfig; + private final AllProjectsName allProjects; + private final AllUsersName allUsers; + private final PluginItemContext<ProjectNameLockManager> lockManager; + + @Inject + CreateProject( + Provider<ProjectsCollection> projectsCollection, + Provider<GroupResolver> groupResolver, + ProjectJson json, + PluginSetContext<ProjectCreationValidationListener> projectCreationValidationListeners, + GitRepositoryManager repoManager, + PluginSetContext<NewProjectCreatedListener> createdListeners, + ProjectCache projectCache, + GroupBackend groupBackend, + ProjectOwnerGroupsProvider.Factory projectOwnerGroups, + MetaDataUpdate.User metaDataUpdateFactory, + GitReferenceUpdated referenceUpdated, + RepositoryConfig repositoryCfg, + @GerritPersonIdent PersonIdent serverIdent, + Provider<IdentifiedUser> identifiedUser, + Provider<PutConfig> putConfig, + AllProjectsName allProjects, + AllUsersName allUsers, + PluginItemContext<ProjectNameLockManager> lockManager) { + this.projectsCollection = projectsCollection; + this.groupResolver = groupResolver; + this.projectCreationValidationListeners = projectCreationValidationListeners; + this.json = json; + this.repoManager = repoManager; + this.createdListeners = createdListeners; + this.projectCache = projectCache; + this.groupBackend = groupBackend; + this.projectOwnerGroups = projectOwnerGroups; + this.metaDataUpdateFactory = metaDataUpdateFactory; + this.referenceUpdated = referenceUpdated; + this.repositoryCfg = repositoryCfg; + this.serverIdent = serverIdent; + this.identifiedUser = identifiedUser; + this.putConfig = putConfig; + this.allProjects = allProjects; + this.allUsers = allUsers; + this.lockManager = lockManager; + } + + @Override + public Response<ProjectInfo> apply(TopLevelResource resource, IdString id, ProjectInput input) + throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException { + String name = id.get(); + if (input == null) { + input = new ProjectInput(); + } + if (input.name != null && !name.equals(input.name)) { + throw new BadRequestException("name must match URL"); + } + + CreateProjectArgs args = new CreateProjectArgs(); + args.setProjectName(ProjectUtil.sanitizeProjectName(name)); + + String parentName = + MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get()); + args.newParent = projectsCollection.get().parse(parentName, false).getNameKey(); + if (args.newParent.equals(allUsers)) { + throw new ResourceConflictException( + String.format("Cannot inherit from '%s' project", allUsers.get())); + } + args.createEmptyCommit = input.createEmptyCommit; + args.permissionsOnly = input.permissionsOnly; + args.projectDescription = Strings.emptyToNull(input.description); + args.submitType = input.submitType; + args.branch = normalizeBranchNames(input.branches); + if (input.owners == null || input.owners.isEmpty()) { + args.ownerIds = new ArrayList<>(projectOwnerGroups.create(args.getProject()).get()); + } else { + args.ownerIds = Lists.newArrayListWithCapacity(input.owners.size()); + for (String owner : input.owners) { + args.ownerIds.add(groupResolver.get().parse(owner).getGroupUUID()); + } + } + args.contributorAgreements = + MoreObjects.firstNonNull(input.useContributorAgreements, InheritableBoolean.INHERIT); + args.signedOffBy = MoreObjects.firstNonNull(input.useSignedOffBy, InheritableBoolean.INHERIT); + args.contentMerge = + input.submitType == SubmitType.FAST_FORWARD_ONLY + ? InheritableBoolean.FALSE + : MoreObjects.firstNonNull(input.useContentMerge, InheritableBoolean.INHERIT); + args.newChangeForAllNotInTarget = + MoreObjects.firstNonNull( + input.createNewChangeForAllNotInTarget, InheritableBoolean.INHERIT); + args.changeIdRequired = + MoreObjects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT); + args.rejectEmptyCommit = + MoreObjects.firstNonNull(input.rejectEmptyCommit, InheritableBoolean.INHERIT); + args.enableSignedPush = + MoreObjects.firstNonNull(input.enableSignedPush, InheritableBoolean.INHERIT); + args.requireSignedPush = + MoreObjects.firstNonNull(input.requireSignedPush, InheritableBoolean.INHERIT); + try { + args.maxObjectSizeLimit = ProjectConfig.validMaxObjectSizeLimit(input.maxObjectSizeLimit); + } catch (ConfigInvalidException e) { + throw new BadRequestException(e.getMessage()); + } + + Lock nameLock = lockManager.call(lockManager -> lockManager.getLock(args.getProject())); + nameLock.lock(); + try { + try { + projectCreationValidationListeners.runEach( + l -> l.validateNewProject(args), ValidationException.class); + } catch (ValidationException e) { + throw new ResourceConflictException(e.getMessage(), e); + } + + ProjectState projectState = createProject(args); + requireNonNull( + projectState, + () -> String.format("failed to create project %s", args.getProject().get())); + + if (input.pluginConfigValues != null) { + ConfigInput in = new ConfigInput(); + in.pluginConfigValues = input.pluginConfigValues; + putConfig.get().apply(projectState, in); + } + return Response.created(json.format(projectState)); + } finally { + nameLock.unlock(); + } + } + + private ProjectState createProject(CreateProjectArgs args) + throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException { + final Project.NameKey nameKey = args.getProject(); + try { + final String head = args.permissionsOnly ? RefNames.REFS_CONFIG : args.branch.get(0); + try (Repository repo = repoManager.openRepository(nameKey)) { + if (repo.getObjectDatabase().exists()) { + throw new ResourceConflictException("project \"" + nameKey + "\" exists"); + } + } catch (RepositoryNotFoundException e) { + // It does not exist, safe to ignore. + } + try (Repository repo = repoManager.createRepository(nameKey)) { + RefUpdate u = repo.updateRef(Constants.HEAD); + u.disableRefLog(); + u.link(head); + + createProjectConfig(args); + + if (!args.permissionsOnly && args.createEmptyCommit) { + createEmptyCommits(repo, nameKey, args.branch); + } + + fire(nameKey, head); + + return projectCache.get(nameKey); + } + } catch (RepositoryCaseMismatchException e) { + throw new ResourceConflictException( + "Cannot create " + + nameKey.get() + + " because the name is already occupied by another project." + + " The other project has the same name, only spelled in a" + + " different case."); + } catch (RepositoryNotFoundException badName) { + throw new BadRequestException("invalid project name: " + nameKey); + } catch (ConfigInvalidException e) { + String msg = "Cannot create " + nameKey; + logger.atSevere().withCause(e).log(msg); + throw e; + } + } + + private void createProjectConfig(CreateProjectArgs args) + throws IOException, ConfigInvalidException { + try (MetaDataUpdate md = metaDataUpdateFactory.create(args.getProject())) { + ProjectConfig config = ProjectConfig.read(md); + + Project newProject = config.getProject(); + newProject.setDescription(args.projectDescription); + newProject.setSubmitType( + MoreObjects.firstNonNull( + args.submitType, repositoryCfg.getDefaultSubmitType(args.getProject()))); + newProject.setBooleanConfig( + BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, args.contributorAgreements); + newProject.setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, args.signedOffBy); + newProject.setBooleanConfig(BooleanProjectConfig.USE_CONTENT_MERGE, args.contentMerge); + newProject.setBooleanConfig( + BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, + args.newChangeForAllNotInTarget); + newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, args.changeIdRequired); + newProject.setBooleanConfig(BooleanProjectConfig.REJECT_EMPTY_COMMIT, args.rejectEmptyCommit); + newProject.setMaxObjectSizeLimit(args.maxObjectSizeLimit); + newProject.setBooleanConfig(BooleanProjectConfig.ENABLE_SIGNED_PUSH, args.enableSignedPush); + newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_SIGNED_PUSH, args.requireSignedPush); + if (args.newParent != null) { + newProject.setParentName(args.newParent); + } + + if (!args.ownerIds.isEmpty()) { + AccessSection all = config.getAccessSection(AccessSection.ALL, true); + for (AccountGroup.UUID ownerId : args.ownerIds) { + GroupDescription.Basic g = groupBackend.get(ownerId); + if (g != null) { + GroupReference group = config.resolve(GroupReference.forGroup(g)); + all.getPermission(Permission.OWNER, true).add(new PermissionRule(group)); + } + } + } + + md.setMessage("Created project\n"); + config.commit(md); + md.getRepository().setGitwebDescription(args.projectDescription); + } + projectCache.onCreateProject(args.getProject()); + } + + private List<String> normalizeBranchNames(List<String> branches) throws BadRequestException { + if (branches == null || branches.isEmpty()) { + return Collections.singletonList(Constants.R_HEADS + Constants.MASTER); + } + + List<String> normalizedBranches = new ArrayList<>(); + for (String branch : branches) { + while (branch.startsWith("/")) { + branch = branch.substring(1); + } + branch = RefNames.fullName(branch); + if (!Repository.isValidRefName(branch)) { + throw new BadRequestException(String.format("Branch \"%s\" is not a valid name.", branch)); + } + if (!normalizedBranches.contains(branch)) { + normalizedBranches.add(branch); + } + } + return normalizedBranches; + } + + private void createEmptyCommits(Repository repo, Project.NameKey project, List<String> refs) + throws IOException { + try (ObjectInserter oi = repo.newObjectInserter()) { + CommitBuilder cb = new CommitBuilder(); + cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {})); + cb.setAuthor(metaDataUpdateFactory.getUserPersonIdent()); + cb.setCommitter(serverIdent); + cb.setMessage("Initial empty repository\n"); + + ObjectId id = oi.insert(cb); + oi.flush(); + + for (String ref : refs) { + RefUpdate ru = repo.updateRef(ref); + ru.setNewObjectId(id); + Result result = ru.update(); + switch (result) { + case NEW: + referenceUpdated.fire( + project, ru, ReceiveCommand.Type.CREATE, identifiedUser.get().state()); + break; + case FAST_FORWARD: + case FORCED: + case IO_FAILURE: + case LOCK_FAILURE: + case NOT_ATTEMPTED: + case NO_CHANGE: + case REJECTED: + case REJECTED_CURRENT_BRANCH: + case RENAMED: + case REJECTED_MISSING_OBJECT: + case REJECTED_OTHER_REASON: + default: + { + throw new IOException( + String.format("Failed to create ref \"%s\": %s", ref, result.name())); + } + } + } + } catch (IOException e) { + logger.atSevere().withCause(e).log("Cannot create empty commit for %s", project.get()); + throw e; + } + } + + private void fire(Project.NameKey name, String head) { + if (createdListeners.isEmpty()) { + return; + } + + Event event = new Event(name, head); + createdListeners.runEach(l -> l.onNewProjectCreated(event)); + } + + static class Event extends AbstractNoNotifyEvent implements NewProjectCreatedListener.Event { + private final Project.NameKey name; + private final String head; + + Event(Project.NameKey name, String head) { + this.name = name; + this.head = head; + } + + @Override + public String getProjectName() { + return name.get(); + } + + @Override + public String getHeadName() { + return head; + } + } +} |