blob: da1b0563426e626a27176841acd77a3e75df1b98 [file] [log] [blame]
/*
* Copyright (C) 2019, Google Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.storage.reftable;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.ReflogEntry;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.SymbolicRef;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;
import static org.eclipse.jgit.lib.Ref.Storage.NEW;
import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
/**
* {@link org.eclipse.jgit.lib.BatchRefUpdate} for Reftable based RefDatabase.
*/
public abstract class ReftableBatchRefUpdate extends BatchRefUpdate {
private final Lock lock;
private final ReftableDatabase refDb;
private final Repository repository;
/**
* Initialize.
*
* @param refDb
* The RefDatabase
* @param reftableDb
* The ReftableDatabase
* @param lock
* A lock protecting the refdatabase's state
* @param repository
* The repository on which this update will run
*/
protected ReftableBatchRefUpdate(RefDatabase refDb,
ReftableDatabase reftableDb, Lock lock, Repository repository) {
super(refDb);
this.refDb = reftableDb;
this.lock = lock;
this.repository = repository;
}
@Override
public void execute(RevWalk rw, ProgressMonitor pm, List<String> options) {
List<ReceiveCommand> pending = getPending();
if (pending.isEmpty()) {
return;
}
if (options != null) {
setPushOptions(options);
}
try {
if (!checkObjectExistence(rw, pending)) {
return;
}
// if we are here, checkObjectExistence might have flagged some problems
// but the transaction is not atomic, so we should proceed with the other
// pending commands.
pending = getPending();
if (!checkNonFastForwards(rw, pending)) {
return;
}
pending = getPending();
lock.lock();
try {
if (!checkExpected(pending)) {
return;
}
pending = getPending();
if (!checkConflicting(pending)) {
return;
}
pending = getPending();
if (!blockUntilTimestamps(MAX_WAIT)) {
return;
}
List<Ref> newRefs = toNewRefs(rw, pending);
applyUpdates(newRefs, pending);
for (ReceiveCommand cmd : pending) {
if (cmd.getResult() == NOT_ATTEMPTED) {
// XXX this is a bug in DFS ?
cmd.setResult(OK);
}
}
} finally {
lock.unlock();
}
} catch (IOException e) {
pending.get(0).setResult(LOCK_FAILURE, "io error"); //$NON-NLS-1$
ReceiveCommand.abort(pending);
}
}
/**
* Implements the storage-specific part of the update.
*
* @param newRefs
* the new refs to create
* @param pending
* the pending receive commands to be executed
* @throws IOException
* if any of the writes fail.
*/
protected abstract void applyUpdates(List<Ref> newRefs,
List<ReceiveCommand> pending) throws IOException;
private List<ReceiveCommand> getPending() {
return ReceiveCommand.filter(getCommands(), NOT_ATTEMPTED);
}
private boolean checkObjectExistence(RevWalk rw,
List<ReceiveCommand> pending) throws IOException {
for (ReceiveCommand cmd : pending) {
try {
if (!cmd.getNewId().equals(ObjectId.zeroId())) {
rw.parseAny(cmd.getNewId());
}
} catch (MissingObjectException e) {
// ReceiveCommand#setResult(Result) converts REJECTED to
// REJECTED_NONFASTFORWARD, even though that result is also
// used for a missing object. Eagerly handle this case so we
// can set the right result.
cmd.setResult(REJECTED_MISSING_OBJECT);
if (isAtomic()) {
ReceiveCommand.abort(pending);
return false;
}
}
}
return true;
}
private boolean checkNonFastForwards(RevWalk rw,
List<ReceiveCommand> pending) throws IOException {
if (isAllowNonFastForwards()) {
return true;
}
for (ReceiveCommand cmd : pending) {
cmd.updateType(rw);
if (cmd.getType() == UPDATE_NONFASTFORWARD) {
cmd.setResult(REJECTED_NONFASTFORWARD);
if (isAtomic()) {
ReceiveCommand.abort(pending);
return false;
}
}
}
return true;
}
private boolean checkConflicting(List<ReceiveCommand> pending)
throws IOException {
TreeSet<String> added = new TreeSet<>();
Set<String> deleted =
pending.stream()
.filter(cmd -> cmd.getType() == DELETE)
.map(c -> c.getRefName())
.collect(Collectors.toSet());
boolean ok = true;
for (ReceiveCommand cmd : pending) {
if (cmd.getType() == DELETE) {
continue;
}
String name = cmd.getRefName();
if (refDb.isNameConflicting(name, added, deleted)) {
if (isAtomic()) {
cmd.setResult(
ReceiveCommand.Result.REJECTED_OTHER_REASON, JGitText.get().transactionAborted);
} else {
cmd.setResult(LOCK_FAILURE);
}
ok = false;
}
added.add(name);
}
if (isAtomic()) {
if (!ok) {
pending.stream()
.filter(cmd -> cmd.getResult() == NOT_ATTEMPTED)
.forEach(cmd -> cmd.setResult(LOCK_FAILURE));
}
return ok;
}
for (ReceiveCommand cmd : pending) {
if (cmd.getResult() == NOT_ATTEMPTED) {
return true;
}
}
return false;
}
private boolean checkExpected(List<ReceiveCommand> pending)
throws IOException {
for (ReceiveCommand cmd : pending) {
if (!matchOld(cmd, refDb.exactRef(cmd.getRefName()))) {
cmd.setResult(LOCK_FAILURE);
if (isAtomic()) {
ReceiveCommand.abort(pending);
return false;
}
}
}
return true;
}
private static boolean matchOld(ReceiveCommand cmd, @Nullable Ref ref) {
if (ref == null) {
return AnyObjectId.isEqual(ObjectId.zeroId(), cmd.getOldId())
&& cmd.getOldSymref() == null;
} else if (ref.isSymbolic()) {
return ref.getTarget().getName().equals(cmd.getOldSymref());
}
ObjectId id = ref.getObjectId();
if (id == null) {
id = ObjectId.zeroId();
}
return cmd.getOldId().equals(id);
}
/**
* Writes the refs to the writer, and calls finish.
*
* @param writer
* the writer on which we should write.
* @param newRefs
* the ref data to write..
* @param pending
* the log data to write.
* @throws IOException
* in case of problems.
*/
protected void write(ReftableWriter writer, List<Ref> newRefs,
List<ReceiveCommand> pending) throws IOException {
long updateIndex = refDb.nextUpdateIndex();
writer.setMinUpdateIndex(updateIndex).setMaxUpdateIndex(updateIndex)
.begin().sortAndWriteRefs(newRefs);
if (!isRefLogDisabled()) {
writeLog(writer, updateIndex, pending);
}
}
private void writeLog(ReftableWriter writer, long updateIndex,
List<ReceiveCommand> pending) throws IOException {
Map<String, ReceiveCommand> cmds = new HashMap<>();
List<String> byName = new ArrayList<>(pending.size());
for (ReceiveCommand cmd : pending) {
cmds.put(cmd.getRefName(), cmd);
byName.add(cmd.getRefName());
}
Collections.sort(byName);
PersonIdent ident = getRefLogIdent();
if (ident == null) {
ident = new PersonIdent(repository);
}
for (String name : byName) {
ReceiveCommand cmd = cmds.get(name);
if (isRefLogDisabled(cmd)) {
continue;
}
String msg = getRefLogMessage(cmd);
if (isRefLogIncludingResult(cmd)) {
String strResult = toResultString(cmd);
if (strResult != null) {
msg = msg.isEmpty() ? strResult : msg + ": " + strResult; //$NON-NLS-1$
}
}
writer.writeLog(name, updateIndex, ident, cmd.getOldId(),
cmd.getNewId(), msg);
}
}
private String toResultString(ReceiveCommand cmd) {
switch (cmd.getType()) {
case CREATE:
return ReflogEntry.PREFIX_CREATED;
case UPDATE:
// Match the behavior of a single RefUpdate. In that case, setting
// the force bit completely bypasses the potentially expensive
// isMergedInto check, by design, so the reflog message may be
// inaccurate.
//
// Similarly, this class bypasses the isMergedInto checks when the
// force bit is set, meaning we can't actually distinguish between
// UPDATE and UPDATE_NONFASTFORWARD when isAllowNonFastForwards()
// returns true.
return isAllowNonFastForwards() ? ReflogEntry.PREFIX_FORCED_UPDATE
: ReflogEntry.PREFIX_FAST_FORWARD;
case UPDATE_NONFASTFORWARD:
return ReflogEntry.PREFIX_FORCED_UPDATE;
default:
return null;
}
}
// Extracts and peels the refs out of the ReceiveCommands
private static List<Ref> toNewRefs(RevWalk rw, List<ReceiveCommand> pending)
throws IOException {
List<Ref> refs = new ArrayList<>(pending.size());
for (ReceiveCommand cmd : pending) {
if (cmd.getResult() != NOT_ATTEMPTED) {
continue;
}
String name = cmd.getRefName();
ObjectId newId = cmd.getNewId();
String newSymref = cmd.getNewSymref();
if (AnyObjectId.isEqual(ObjectId.zeroId(), newId)
&& newSymref == null) {
refs.add(new ObjectIdRef.Unpeeled(NEW, name, null));
continue;
} else if (newSymref != null) {
refs.add(new SymbolicRef(name,
new ObjectIdRef.Unpeeled(NEW, newSymref, null)));
continue;
}
RevObject obj = rw.parseAny(newId);
RevObject peel = null;
if (obj instanceof RevTag) {
peel = rw.peel(obj);
}
if (peel != null) {
refs.add(new ObjectIdRef.PeeledTag(PACKED, name, newId,
peel.copy()));
} else {
refs.add(new ObjectIdRef.PeeledNonTag(PACKED, name, newId));
}
}
return refs;
}
}