blob: 8981b07c7e2de4f2b4d276dd0fe7b8245665e9ae [file] [log] [blame]
// 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.sql.Timestamp;
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 Timestamp 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(Timestamp 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)
throws NoSuchProjectException, IOException {
List<BatchUpdate> updates = new ArrayList<>(projects.size());
for (Project.NameKey project : projects) {
updates.add(getRepo(project).getUpdate().setNotify(notify).setRefLogMessage("merged"));
}
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();
}
}