blob: bf8421fc2f2cb78d899c08ac7eb55bd1252a1d7d [file] [log] [blame]
// Copyright (C) 2019 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.notedb;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ListMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.InsertedObject;
import com.google.gerrit.server.update.ChainedReceiveCommands;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
/**
* Wrapper around {@link Repository} that keeps track of related {@link ObjectInserter}s and other
* objects that are jointly closed when invoking {@link #close}.
*/
class OpenRepo implements AutoCloseable {
final Repository repo;
final RevWalk rw;
final ChainedReceiveCommands cmds;
final ObjectInserter tempIns;
private final InMemoryInserter inMemIns;
@Nullable private final ObjectInserter finalIns;
private final boolean close;
/** Returns a {@link OpenRepo} wrapping around an open {@link Repository}. */
static OpenRepo open(GitRepositoryManager repoManager, Project.NameKey project)
throws IOException {
Repository repo = repoManager.openRepository(project); // Closed by OpenRepo#close.
ObjectInserter ins = repo.newObjectInserter(); // Closed by OpenRepo#close.
ObjectReader reader = ins.newReader(); // Not closed by OpenRepo#close.
try (RevWalk rw = new RevWalk(reader)) { // Doesn't escape OpenRepo constructor.
return new OpenRepo(repo, rw, ins, new ChainedReceiveCommands(repo), true) {
@Override
public void close() {
reader.close();
super.close();
}
};
}
}
OpenRepo(
Repository repo,
RevWalk rw,
@Nullable ObjectInserter ins,
ChainedReceiveCommands cmds,
boolean close) {
ObjectReader reader = rw.getObjectReader();
checkArgument(
ins == null || reader.getCreatedFromInserter() == ins,
"expected reader to be created from %s, but was %s",
ins,
reader.getCreatedFromInserter());
this.repo = requireNonNull(repo);
this.inMemIns = new InMemoryInserter(rw.getObjectReader());
this.tempIns = inMemIns;
this.rw = new RevWalk(tempIns.newReader());
this.finalIns = ins;
this.cmds = requireNonNull(cmds);
this.close = close;
}
@Override
public void close() {
rw.getObjectReader().close();
rw.close();
if (close) {
if (finalIns != null) {
finalIns.close();
}
repo.close();
}
}
void flush() throws IOException {
flushToFinalInserter();
finalIns.flush();
}
void flushToFinalInserter() throws IOException {
checkState(finalIns != null);
for (InsertedObject obj : inMemIns.getInsertedObjects()) {
finalIns.insert(obj.type(), obj.data().toByteArray());
}
inMemIns.clear();
}
private static <U extends AbstractChangeUpdate> boolean allowWrite(
Collection<U> updates, ObjectId old) {
if (!old.equals(ObjectId.zeroId())) {
return true;
}
return updates.iterator().next().allowWriteToNewRef();
}
<U extends AbstractChangeUpdate> void addUpdatesNoLimits(ListMultimap<String, U> all)
throws IOException {
addUpdates(
all, Optional.empty() /* unlimited updates */, Optional.empty() /* unlimited patch sets */);
}
<U extends AbstractChangeUpdate> void addUpdates(
ListMultimap<String, U> all, Optional<Integer> maxUpdates, Optional<Integer> maxPatchSets)
throws IOException {
for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) {
String refName = e.getKey();
Collection<U> updates = e.getValue();
ObjectId old = cmds.get(refName).orElse(ObjectId.zeroId());
// Only actually write to the ref if one of the updates explicitly allows
// us to do so, i.e. it is known to represent a new change. This avoids
// writing partial change meta if the change hasn't been backfilled yet.
if (!allowWrite(updates, old)) {
continue;
}
int updateCount = 0;
U first = updates.iterator().next();
if (maxUpdates.isPresent()) {
checkState(first.getNotes() != null, "expected ChangeNotes on %s", first);
updateCount = first.getNotes().getUpdateCount();
}
ObjectId curr = old;
for (U update : updates) {
if (maxPatchSets.isPresent() && update.psId != null) {
// Patch set IDs are assigned consecutively. Patch sets may have been deleted, but the ID
// is still a good estimate and an upper bound.
if (update.psId.get() > maxPatchSets.get()) {
throw new LimitExceededException(
String.format(
"Change %d may not exceed %d patch sets. To continue working on this change, "
+ "recreate it with a new Change-Id, then abandon this one.",
update.getId().get(), maxPatchSets.get()));
}
}
if (update.isRootOnly() && !old.equals(ObjectId.zeroId())) {
throw new StorageException("Given ChangeUpdate is only allowed on initial commit");
}
ObjectId next = update.apply(rw, tempIns, curr);
if (next == null) {
continue;
}
if (maxUpdates.isPresent()
&& !Objects.equals(next, curr)
&& ++updateCount > maxUpdates.get()
&& !update.bypassMaxUpdates()) {
throw new LimitExceededException(
String.format(
"Change %s may not exceed %d updates. It may still be abandoned, submitted and"
+ " you can add/remove reviewers to/from the attention-set. To continue"
+ " working on this change, recreate it with a new Change-Id, then abandon"
+ " this one.",
update.getId(), maxUpdates.get()));
}
curr = next;
}
if (!old.equals(curr)) {
cmds.add(new ReceiveCommand(old, curr, refName));
}
}
}
}