| // Copyright (C) 2016 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.submit; |
| |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.gerrit.server.project.ProjectCache.noSuchProject; |
| import static java.util.Objects.requireNonNull; |
| |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Maps; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.change.NotifyResolver; |
| import com.google.gerrit.server.git.CodeReviewCommit; |
| import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.git.MergeTip; |
| import com.google.gerrit.server.git.validators.OnSubmitValidators; |
| import com.google.gerrit.server.project.NoSuchProjectException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gerrit.server.update.BatchUpdate; |
| import com.google.inject.Inject; |
| import java.io.IOException; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.ObjectReader; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevFlag; |
| import org.eclipse.jgit.revwalk.RevSort; |
| |
| /** |
| * This is a helper class for MergeOp and not intended for general use. |
| * |
| * <p>Some database backends require to open a repository just once within a transaction of a |
| * submission, this caches open repositories to satisfy that requirement. |
| */ |
| public class MergeOpRepoManager implements AutoCloseable { |
| public class OpenRepo { |
| final Repository repo; |
| final CodeReviewRevWalk rw; |
| final RevFlag canMergeFlag; |
| final ObjectInserter ins; |
| |
| final ProjectState project; |
| BatchUpdate update; |
| |
| private final ObjectReader reader; |
| private final Map<BranchNameKey, OpenBranch> branches; |
| |
| private OpenRepo(Repository repo, ProjectState project) { |
| this.repo = repo; |
| this.project = project; |
| ins = repo.newObjectInserter(); |
| reader = ins.newReader(); |
| rw = CodeReviewCommit.newRevWalk(reader); |
| rw.sort(RevSort.TOPO); |
| rw.sort(RevSort.COMMIT_TIME_DESC, true); |
| rw.setRetainBody(false); |
| canMergeFlag = rw.newFlag("CAN_MERGE"); |
| rw.retainOnReset(canMergeFlag); |
| |
| branches = Maps.newHashMapWithExpectedSize(1); |
| } |
| |
| OpenBranch getBranch(BranchNameKey branch) throws IntegrationConflictException { |
| OpenBranch ob = branches.get(branch); |
| if (ob == null) { |
| ob = new OpenBranch(this, branch); |
| branches.put(branch, ob); |
| } |
| return ob; |
| } |
| |
| public Repository getRepo() { |
| return repo; |
| } |
| |
| Project.NameKey getProjectName() { |
| return project.getNameKey(); |
| } |
| |
| public CodeReviewRevWalk getCodeReviewRevWalk() { |
| return rw; |
| } |
| |
| public BatchUpdate getUpdate() { |
| checkState(caller != null, "call setContext before getUpdate"); |
| if (update == null) { |
| update = |
| batchUpdateFactory |
| .create(getProjectName(), caller, ts) |
| .setRepository(repo, rw, ins) |
| .setNotify(notify) |
| .setOnSubmitValidators(onSubmitValidatorsFactory.create()); |
| } |
| return update; |
| } |
| |
| // We want to reuse the open repo BUT not the BatchUpdate (because they are already executed) |
| public void resetExecutedUpdates() { |
| if (update != null && update.isExecuted()) { |
| update.close(); |
| update = null; |
| } |
| } |
| |
| private void close() { |
| if (update != null) { |
| update.close(); |
| } |
| rw.close(); |
| reader.close(); |
| ins.close(); |
| repo.close(); |
| } |
| } |
| |
| public static class OpenBranch { |
| final CodeReviewCommit oldTip; |
| MergeTip mergeTip; |
| |
| OpenBranch(OpenRepo or, BranchNameKey name) throws IntegrationConflictException { |
| try { |
| Ref ref = or.getRepo().exactRef(name.branch()); |
| if (ref != null) { |
| oldTip = or.rw.parseCommit(ref.getObjectId()); |
| } else if (Objects.equals(or.repo.getFullBranch(), name.branch()) |
| || Objects.equals(RefNames.REFS_CONFIG, name.branch())) { |
| oldTip = null; |
| } else { |
| throw new IntegrationConflictException( |
| "The destination branch " + name + " does not exist anymore."); |
| } |
| } catch (IOException e) { |
| throw new StorageException("Cannot open branch " + name, e); |
| } |
| } |
| } |
| |
| private final Map<Project.NameKey, OpenRepo> openRepos; |
| private final BatchUpdate.Factory batchUpdateFactory; |
| private final OnSubmitValidators.Factory onSubmitValidatorsFactory; |
| private final GitRepositoryManager repoManager; |
| private final ProjectCache projectCache; |
| |
| private Instant ts; |
| private IdentifiedUser caller; |
| private NotifyResolver.Result notify; |
| |
| @Inject |
| MergeOpRepoManager( |
| GitRepositoryManager repoManager, |
| ProjectCache projectCache, |
| BatchUpdate.Factory batchUpdateFactory, |
| OnSubmitValidators.Factory onSubmitValidatorsFactory) { |
| this.repoManager = repoManager; |
| this.projectCache = projectCache; |
| this.batchUpdateFactory = batchUpdateFactory; |
| this.onSubmitValidatorsFactory = onSubmitValidatorsFactory; |
| |
| openRepos = new HashMap<>(); |
| } |
| |
| public void setContext(Instant ts, IdentifiedUser caller, NotifyResolver.Result notify) { |
| this.ts = requireNonNull(ts); |
| this.caller = requireNonNull(caller); |
| this.notify = requireNonNull(notify); |
| } |
| |
| public OpenRepo getRepo(Project.NameKey project) throws NoSuchProjectException, IOException { |
| if (openRepos.containsKey(project)) { |
| return openRepos.get(project); |
| } |
| |
| ProjectState projectState = projectCache.get(project).orElseThrow(noSuchProject(project)); |
| try { |
| OpenRepo or = new OpenRepo(repoManager.openRepository(project), projectState); |
| openRepos.put(project, or); |
| return or; |
| } catch (RepositoryNotFoundException e) { |
| throw new NoSuchProjectException(project, e); |
| } |
| } |
| |
| public List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects, String refLogMessage) |
| throws NoSuchProjectException, IOException { |
| requireNonNull(refLogMessage, "refLogMessage"); |
| List<BatchUpdate> updates = new ArrayList<>(projects.size()); |
| for (Project.NameKey project : projects) { |
| updates.add(getRepo(project).getUpdate().setNotify(notify).setRefLogMessage(refLogMessage)); |
| } |
| return updates; |
| } |
| |
| public void resetUpdates(ImmutableSet<Project.NameKey> projects) |
| throws NoSuchProjectException, IOException { |
| for (Project.NameKey project : projects) { |
| getRepo(project).resetExecutedUpdates(); |
| } |
| } |
| |
| @Override |
| public void close() { |
| for (OpenRepo repo : openRepos.values()) { |
| repo.close(); |
| } |
| openRepos.clear(); |
| } |
| } |