| // Copyright (C) 2018 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.project; |
| |
| import static com.google.gerrit.server.project.ProjectCache.illegalState; |
| import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.INIT_REPO; |
| |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Strings; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.AccessSection; |
| import com.google.gerrit.entities.AccountGroup; |
| import com.google.gerrit.entities.BooleanProjectConfig; |
| import com.google.gerrit.entities.GroupDescription; |
| import com.google.gerrit.entities.GroupReference; |
| import com.google.gerrit.entities.Permission; |
| import com.google.gerrit.entities.PermissionRule; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.extensions.events.NewProjectCreatedListener; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.git.LockFailureException; |
| 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.GerritInstanceId; |
| 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.GitRepositoryManager.Status; |
| import com.google.gerrit.server.git.RepositoryExistsException; |
| import com.google.gerrit.server.git.meta.MetaDataUpdate; |
| import com.google.gerrit.server.plugincontext.PluginSetContext; |
| import com.google.gerrit.server.update.context.RefUpdateContext; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import java.io.IOException; |
| import java.util.List; |
| 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; |
| |
| /** |
| * Business logic for creating projects. |
| * |
| * <p>This creates the repository, the underlying configuration in {@code refs/meta/config} and |
| * initializes a first commit if necessary. |
| */ |
| public class ProjectCreator { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final GitRepositoryManager repoManager; |
| private final PluginSetContext<NewProjectCreatedListener> createdListeners; |
| private final ProjectCache projectCache; |
| private final GroupBackend groupBackend; |
| private final MetaDataUpdate.User metaDataUpdateFactory; |
| private final GitReferenceUpdated referenceUpdated; |
| private final RepositoryConfig repositoryCfg; |
| private final Provider<PersonIdent> serverIdent; |
| private final Provider<IdentifiedUser> identifiedUser; |
| private final ProjectConfig.Factory projectConfigFactory; |
| private final String gerritInstanceId; |
| |
| @Inject |
| ProjectCreator( |
| GitRepositoryManager repoManager, |
| PluginSetContext<NewProjectCreatedListener> createdListeners, |
| ProjectCache projectCache, |
| GroupBackend groupBackend, |
| MetaDataUpdate.User metaDataUpdateFactory, |
| GitReferenceUpdated referenceUpdated, |
| RepositoryConfig repositoryCfg, |
| @GerritPersonIdent Provider<PersonIdent> serverIdent, |
| @Nullable @GerritInstanceId String gerritInstanceId, |
| Provider<IdentifiedUser> identifiedUser, |
| ProjectConfig.Factory projectConfigFactory) { |
| this.repoManager = repoManager; |
| this.createdListeners = createdListeners; |
| this.projectCache = projectCache; |
| this.groupBackend = groupBackend; |
| this.metaDataUpdateFactory = metaDataUpdateFactory; |
| this.referenceUpdated = referenceUpdated; |
| this.repositoryCfg = repositoryCfg; |
| this.serverIdent = serverIdent; |
| this.gerritInstanceId = gerritInstanceId; |
| this.identifiedUser = identifiedUser; |
| this.projectConfigFactory = projectConfigFactory; |
| } |
| |
| @CanIgnoreReturnValue |
| public ProjectState createProject(CreateProjectArgs args) |
| throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException { |
| try (RefUpdateContext ctx = RefUpdateContext.open(INIT_REPO)) { |
| final Project.NameKey nameKey = args.getProject(); |
| try { |
| final String head = args.permissionsOnly ? RefNames.REFS_CONFIG : args.branch.get(0); |
| Status status = repoManager.getRepositoryStatus(nameKey); |
| if (!status.equals(Status.NON_EXISTENT)) { |
| throw new RepositoryExistsException(nameKey, "Repository status: " + status); |
| } |
| try (Repository repo = repoManager.createRepository(nameKey)) { |
| projectCache.evict(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).orElseThrow(illegalState(nameKey)); |
| } |
| } catch (RepositoryExistsException e) { |
| throw new ResourceConflictException( |
| "Cannot create " |
| + nameKey.get() |
| + " because the name is already occupied by another project.", |
| e); |
| } catch (RepositoryNotFoundException badName) { |
| throw new BadRequestException("invalid project name: " + nameKey, badName); |
| } |
| } |
| } |
| |
| private void createProjectConfig(CreateProjectArgs args) |
| throws IOException, ConfigInvalidException { |
| try (MetaDataUpdate md = metaDataUpdateFactory.create(args.getProject())) { |
| ProjectConfig config = projectConfigFactory.read(md); |
| |
| config.updateProject( |
| newProject -> { |
| newProject.setDescription(Strings.nullToEmpty(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.setParent(args.newParent); |
| } |
| }); |
| |
| if (!args.ownerIds.isEmpty()) { |
| config.upsertAccessSection( |
| AccessSection.ALL, |
| all -> { |
| for (AccountGroup.UUID ownerId : args.ownerIds) { |
| GroupDescription.Basic g = groupBackend.get(ownerId); |
| if (g != null) { |
| GroupReference group = config.resolve(GroupReference.forGroup(g)); |
| all.upsertPermission(Permission.OWNER).add(PermissionRule.builder(group)); |
| } |
| } |
| }); |
| } |
| |
| md.setMessage("Created project\n"); |
| config.commit(md); |
| md.getRepository().setGitwebDescription(args.projectDescription); |
| } |
| projectCache.onCreateProject(args.getProject()); |
| } |
| |
| 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.get()); |
| 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 LOCK_FAILURE: |
| throw new LockFailureException(String.format("Failed to create ref \"%s\"", ref), ru); |
| case FAST_FORWARD: |
| case FORCED: |
| case IO_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; |
| } |
| |
| ProjectCreator.Event event = new ProjectCreator.Event(name, head, gerritInstanceId); |
| createdListeners.runEach(l -> l.onNewProjectCreated(event)); |
| } |
| |
| static class Event extends AbstractNoNotifyEvent implements NewProjectCreatedListener.Event { |
| private final Project.NameKey name; |
| private final String head; |
| private final String gerritInstanceId; |
| |
| Event(Project.NameKey name, String head, @Nullable String gerritInstanceId) { |
| this.name = name; |
| this.head = head; |
| this.gerritInstanceId = gerritInstanceId; |
| } |
| |
| @Override |
| public String getProjectName() { |
| return name.get(); |
| } |
| |
| @Override |
| public String getHeadName() { |
| return head; |
| } |
| |
| @Override |
| public String getInstanceId() { |
| return gerritInstanceId; |
| } |
| } |
| } |