BatchRefUpdate.java
/*
* Copyright (C) 2008-2012, Google Inc.
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.lib;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
import static java.util.stream.Collectors.toCollection;
import java.io.IOException;
import java.text.MessageFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.TimeoutException;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushCertificate;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.util.time.ProposedTimestamp;
/**
* Batch of reference updates to be applied to a repository.
* <p>
* The batch update is primarily useful in the transport code, where a client or
* server is making changes to more than one reference at a time.
*/
public class BatchRefUpdate {
/**
* Maximum delay the calling thread will tolerate while waiting for a
* {@code MonotonicClock} to resolve associated {@link ProposedTimestamp}s.
* <p>
* A default of 5 seconds was chosen by guessing. A common assumption is
* clock skew between machines on the same LAN using an NTP server also on
* the same LAN should be under 5 seconds. 5 seconds is also not that long
* for a large `git push` operation to complete.
*
* @since 4.9
*/
protected static final Duration MAX_WAIT = Duration.ofSeconds(5);
private final RefDatabase refdb;
/** Commands to apply during this batch. */
private final List<ReceiveCommand> commands;
/** Does the caller permit a forced update on a reference? */
private boolean allowNonFastForwards;
/** Identity to record action as within the reflog. */
private PersonIdent refLogIdent;
/** Message the caller wants included in the reflog. */
private String refLogMessage;
/** Should the result value be appended to {@link #refLogMessage}. */
private boolean refLogIncludeResult;
/**
* Should reflogs be written even if the configured default for this ref is
* not to write it.
*/
private boolean forceRefLog;
/** Push certificate associated with this update. */
private PushCertificate pushCert;
/** Whether updates should be atomic. */
private boolean atomic;
/** Push options associated with this update. */
private List<String> pushOptions;
/** Associated timestamps that should be blocked on before update. */
private List<ProposedTimestamp> timestamps;
/**
* Initialize a new batch update.
*
* @param refdb
* the reference database of the repository to be updated.
*/
protected BatchRefUpdate(RefDatabase refdb) {
this.refdb = refdb;
this.commands = new ArrayList<>();
this.atomic = refdb.performsAtomicTransactions();
}
/**
* Whether the batch update will permit a non-fast-forward update to an
* existing reference.
*
* @return true if the batch update will permit a non-fast-forward update to
* an existing reference.
*/
public boolean isAllowNonFastForwards() {
return allowNonFastForwards;
}
/**
* Set if this update wants to permit a forced update.
*
* @param allow
* true if this update batch should ignore merge tests.
* @return {@code this}.
*/
public BatchRefUpdate setAllowNonFastForwards(boolean allow) {
allowNonFastForwards = allow;
return this;
}
/**
* Get identity of the user making the change in the reflog.
*
* @return identity of the user making the change in the reflog.
*/
public PersonIdent getRefLogIdent() {
return refLogIdent;
}
/**
* Set the identity of the user appearing in the reflog.
* <p>
* The timestamp portion of the identity is ignored. A new identity with the
* current timestamp will be created automatically when the update occurs
* and the log record is written.
*
* @param pi
* identity of the user. If null the identity will be
* automatically determined based on the repository
* configuration.
* @return {@code this}.
*/
public BatchRefUpdate setRefLogIdent(PersonIdent pi) {
refLogIdent = pi;
return this;
}
/**
* Get the message to include in the reflog.
*
* @return message the caller wants to include in the reflog; null if the
* update should not be logged.
*/
@Nullable
public String getRefLogMessage() {
return refLogMessage;
}
/**
* Check whether the reflog message should include the result of the update,
* such as fast-forward or force-update.
* <p>
* Describes the default for commands in this batch that do not override it
* with
* {@link org.eclipse.jgit.transport.ReceiveCommand#setRefLogMessage(String, boolean)}.
*
* @return true if the message should include the result.
*/
public boolean isRefLogIncludingResult() {
return refLogIncludeResult;
}
/**
* Set the message to include in the reflog.
* <p>
* Repository implementations may limit which reflogs are written by
* default, based on the project configuration. If a repo is not configured
* to write logs for this ref by default, setting the message alone may have
* no effect. To indicate that the repo should write logs for this update in
* spite of configured defaults, use {@link #setForceRefLog(boolean)}.
* <p>
* Describes the default for commands in this batch that do not override it
* with
* {@link org.eclipse.jgit.transport.ReceiveCommand#setRefLogMessage(String, boolean)}.
*
* @param msg
* the message to describe this change. If null and appendStatus
* is false, the reflog will not be updated.
* @param appendStatus
* true if the status of the ref change (fast-forward or
* forced-update) should be appended to the user supplied
* message.
* @return {@code this}.
*/
public BatchRefUpdate setRefLogMessage(String msg, boolean appendStatus) {
if (msg == null && !appendStatus)
disableRefLog();
else if (msg == null && appendStatus) {
refLogMessage = ""; //$NON-NLS-1$
refLogIncludeResult = true;
} else {
refLogMessage = msg;
refLogIncludeResult = appendStatus;
}
return this;
}
/**
* Don't record this update in the ref's associated reflog.
* <p>
* Equivalent to {@code setRefLogMessage(null, false)}.
*
* @return {@code this}.
*/
public BatchRefUpdate disableRefLog() {
refLogMessage = null;
refLogIncludeResult = false;
return this;
}
/**
* Force writing a reflog for the updated ref.
*
* @param force whether to force.
* @return {@code this}
* @since 4.9
*/
public BatchRefUpdate setForceRefLog(boolean force) {
forceRefLog = force;
return this;
}
/**
* Check whether log has been disabled by {@link #disableRefLog()}.
*
* @return true if disabled.
*/
public boolean isRefLogDisabled() {
return refLogMessage == null;
}
/**
* Check whether the reflog should be written regardless of repo defaults.
*
* @return whether force writing is enabled.
* @since 4.9
*/
protected boolean isForceRefLog() {
return forceRefLog;
}
/**
* Request that all updates in this batch be performed atomically.
* <p>
* When atomic updates are used, either all commands apply successfully, or
* none do. Commands that might have otherwise succeeded are rejected with
* {@code REJECTED_OTHER_REASON}.
* <p>
* This method only works if the underlying ref database supports atomic
* transactions, i.e.
* {@link org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions()}
* returns true. Calling this method with true if the underlying ref
* database does not support atomic transactions will cause all commands to
* fail with {@code
* REJECTED_OTHER_REASON}.
*
* @param atomic
* whether updates should be atomic.
* @return {@code this}
* @since 4.4
*/
public BatchRefUpdate setAtomic(boolean atomic) {
this.atomic = atomic;
return this;
}
/**
* Whether updates should be atomic.
*
* @return atomic whether updates should be atomic.
* @since 4.4
*/
public boolean isAtomic() {
return atomic;
}
/**
* Set a push certificate associated with this update.
* <p>
* This usually includes commands to update the refs in this batch, but is not
* required to.
*
* @param cert
* push certificate, may be null.
* @since 4.1
*/
public void setPushCertificate(PushCertificate cert) {
pushCert = cert;
}
/**
* Set the push certificate associated with this update.
* <p>
* This usually includes commands to update the refs in this batch, but is not
* required to.
*
* @return push certificate, may be null.
* @since 4.1
*/
protected PushCertificate getPushCertificate() {
return pushCert;
}
/**
* Get commands this update will process.
*
* @return commands this update will process.
*/
public List<ReceiveCommand> getCommands() {
return Collections.unmodifiableList(commands);
}
/**
* Add a single command to this batch update.
*
* @param cmd
* the command to add, must not be null.
* @return {@code this}.
*/
public BatchRefUpdate addCommand(ReceiveCommand cmd) {
commands.add(cmd);
return this;
}
/**
* Add commands to this batch update.
*
* @param cmd
* the commands to add, must not be null.
* @return {@code this}.
*/
public BatchRefUpdate addCommand(ReceiveCommand... cmd) {
return addCommand(Arrays.asList(cmd));
}
/**
* Add commands to this batch update.
*
* @param cmd
* the commands to add, must not be null.
* @return {@code this}.
*/
public BatchRefUpdate addCommand(Collection<ReceiveCommand> cmd) {
commands.addAll(cmd);
return this;
}
/**
* Gets the list of option strings associated with this update.
*
* @return push options that were passed to {@link #execute}; prior to calling
* {@link #execute}, always returns null.
* @since 4.5
*/
@Nullable
public List<String> getPushOptions() {
return pushOptions;
}
/**
* Set push options associated with this update.
* <p>
* Implementations must call this at the top of {@link #execute(RevWalk,
* ProgressMonitor, List)}.
*
* @param options options passed to {@code execute}.
* @since 4.9
*/
protected void setPushOptions(List<String> options) {
pushOptions = options;
}
/**
* Get list of timestamps the batch must wait for.
*
* @return list of timestamps the batch must wait for.
* @since 4.6
*/
public List<ProposedTimestamp> getProposedTimestamps() {
if (timestamps != null) {
return Collections.unmodifiableList(timestamps);
}
return Collections.emptyList();
}
/**
* Request the batch to wait for the affected timestamps to resolve.
*
* @param ts
* a {@link org.eclipse.jgit.util.time.ProposedTimestamp} object.
* @return {@code this}.
* @since 4.6
*/
public BatchRefUpdate addProposedTimestamp(ProposedTimestamp ts) {
if (timestamps == null) {
timestamps = new ArrayList<>(4);
}
timestamps.add(ts);
return this;
}
/**
* Execute this batch update.
* <p>
* The default implementation of this method performs a sequential reference
* update over each reference.
* <p>
* Implementations must respect the atomicity requirements of the underlying
* database as described in {@link #setAtomic(boolean)} and
* {@link org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions()}.
*
* @param walk
* a RevWalk to parse tags in case the storage system wants to
* store them pre-peeled, a common performance optimization.
* @param monitor
* progress monitor to receive update status on.
* @param options
* a list of option strings; set null to execute without
* @throws java.io.IOException
* the database is unable to accept the update. Individual
* command status must be tested to determine if there is a
* partial failure, or a total failure.
* @since 4.5
*/
public void execute(RevWalk walk, ProgressMonitor monitor,
List<String> options) throws IOException {
if (atomic && !refdb.performsAtomicTransactions()) {
for (ReceiveCommand c : commands) {
if (c.getResult() == NOT_ATTEMPTED) {
c.setResult(REJECTED_OTHER_REASON,
JGitText.get().atomicRefUpdatesNotSupported);
}
}
return;
}
if (!blockUntilTimestamps(MAX_WAIT)) {
return;
}
if (options != null) {
setPushOptions(options);
}
monitor.beginTask(JGitText.get().updatingReferences, commands.size());
List<ReceiveCommand> commands2 = new ArrayList<>(
commands.size());
// First delete refs. This may free the name space for some of the
// updates.
for (ReceiveCommand cmd : commands) {
try {
if (cmd.getResult() == NOT_ATTEMPTED) {
if (isMissing(walk, cmd.getOldId())
|| isMissing(walk, cmd.getNewId())) {
cmd.setResult(ReceiveCommand.Result.REJECTED_MISSING_OBJECT);
continue;
}
cmd.updateType(walk);
switch (cmd.getType()) {
case CREATE:
commands2.add(cmd);
break;
case UPDATE:
case UPDATE_NONFASTFORWARD:
commands2.add(cmd);
break;
case DELETE:
RefUpdate rud = newUpdate(cmd);
monitor.update(1);
cmd.setResult(rud.delete(walk));
}
}
} catch (IOException err) {
cmd.setResult(
REJECTED_OTHER_REASON,
MessageFormat.format(JGitText.get().lockError,
err.getMessage()));
}
}
if (!commands2.isEmpty()) {
// What part of the name space is already taken
Collection<String> takenNames = refdb.getRefs().stream()
.map(Ref::getName)
.collect(toCollection(HashSet::new));
Collection<String> takenPrefixes = getTakenPrefixes(takenNames);
// Now to the update that may require more room in the name space
for (ReceiveCommand cmd : commands2) {
try {
if (cmd.getResult() == NOT_ATTEMPTED) {
cmd.updateType(walk);
RefUpdate ru = newUpdate(cmd);
SWITCH: switch (cmd.getType()) {
case DELETE:
// Performed in the first phase
break;
case UPDATE:
case UPDATE_NONFASTFORWARD:
RefUpdate ruu = newUpdate(cmd);
cmd.setResult(ruu.update(walk));
break;
case CREATE:
for (String prefix : getPrefixes(cmd.getRefName())) {
if (takenNames.contains(prefix)) {
cmd.setResult(Result.LOCK_FAILURE);
break SWITCH;
}
}
if (takenPrefixes.contains(cmd.getRefName())) {
cmd.setResult(Result.LOCK_FAILURE);
break SWITCH;
}
ru.setCheckConflicting(false);
takenPrefixes.addAll(getPrefixes(cmd.getRefName()));
takenNames.add(cmd.getRefName());
cmd.setResult(ru.update(walk));
}
}
} catch (IOException err) {
cmd.setResult(REJECTED_OTHER_REASON, MessageFormat.format(
JGitText.get().lockError, err.getMessage()));
} finally {
monitor.update(1);
}
}
}
monitor.endTask();
}
private static boolean isMissing(RevWalk walk, ObjectId id)
throws IOException {
if (id.equals(ObjectId.zeroId())) {
return false; // Explicit add or delete is not missing.
}
try {
walk.parseAny(id);
return false;
} catch (MissingObjectException e) {
return true;
}
}
/**
* Wait for timestamps to be in the past, aborting commands on timeout.
*
* @param maxWait
* maximum amount of time to wait for timestamps to resolve.
* @return true if timestamps were successfully waited for; false if
* commands were aborted.
* @since 4.6
*/
protected boolean blockUntilTimestamps(Duration maxWait) {
if (timestamps == null) {
return true;
}
try {
ProposedTimestamp.blockUntil(timestamps, maxWait);
return true;
} catch (TimeoutException | InterruptedException e) {
String msg = JGitText.get().timeIsUncertain;
for (ReceiveCommand c : commands) {
if (c.getResult() == NOT_ATTEMPTED) {
c.setResult(REJECTED_OTHER_REASON, msg);
}
}
return false;
}
}
/**
* Execute this batch update without option strings.
*
* @param walk
* a RevWalk to parse tags in case the storage system wants to
* store them pre-peeled, a common performance optimization.
* @param monitor
* progress monitor to receive update status on.
* @throws java.io.IOException
* the database is unable to accept the update. Individual
* command status must be tested to determine if there is a
* partial failure, or a total failure.
*/
public void execute(RevWalk walk, ProgressMonitor monitor)
throws IOException {
execute(walk, monitor, null);
}
private static Collection<String> getTakenPrefixes(Collection<String> names) {
Collection<String> ref = new HashSet<>();
for (String name : names) {
addPrefixesTo(name, ref);
}
return ref;
}
/**
* Get all path prefixes of a ref name.
*
* @param name
* ref name.
* @return path prefixes of the ref name. For {@code refs/heads/foo}, returns
* {@code refs} and {@code refs/heads}.
* @since 4.9
*/
protected static Collection<String> getPrefixes(String name) {
Collection<String> ret = new HashSet<>();
addPrefixesTo(name, ret);
return ret;
}
/**
* Add prefixes of a ref name to an existing collection.
*
* @param name
* ref name.
* @param out
* path prefixes of the ref name. For {@code refs/heads/foo},
* returns {@code refs} and {@code refs/heads}.
* @since 4.9
*/
protected static void addPrefixesTo(String name, Collection<String> out) {
int p1 = name.indexOf('/');
while (p1 > 0) {
out.add(name.substring(0, p1));
p1 = name.indexOf('/', p1 + 1);
}
}
/**
* Create a new RefUpdate copying the batch settings.
*
* @param cmd
* specific command the update should be created to copy.
* @return a single reference update command.
* @throws java.io.IOException
* the reference database cannot make a new update object for
* the given reference.
*/
protected RefUpdate newUpdate(ReceiveCommand cmd) throws IOException {
RefUpdate ru = refdb.newUpdate(cmd.getRefName(), false);
if (isRefLogDisabled(cmd)) {
ru.disableRefLog();
} else {
ru.setRefLogIdent(refLogIdent);
ru.setRefLogMessage(getRefLogMessage(cmd), isRefLogIncludingResult(cmd));
ru.setForceRefLog(isForceRefLog(cmd));
}
ru.setPushCertificate(pushCert);
switch (cmd.getType()) {
case DELETE:
if (!ObjectId.zeroId().equals(cmd.getOldId()))
ru.setExpectedOldObjectId(cmd.getOldId());
ru.setForceUpdate(true);
return ru;
case CREATE:
case UPDATE:
case UPDATE_NONFASTFORWARD:
default:
ru.setForceUpdate(isAllowNonFastForwards());
ru.setExpectedOldObjectId(cmd.getOldId());
ru.setNewObjectId(cmd.getNewId());
return ru;
}
}
/**
* Check whether reflog is disabled for a command.
*
* @param cmd
* specific command.
* @return whether the reflog is disabled, taking into account the state from
* this instance as well as overrides in the given command.
* @since 4.9
*/
protected boolean isRefLogDisabled(ReceiveCommand cmd) {
return cmd.hasCustomRefLog() ? cmd.isRefLogDisabled() : isRefLogDisabled();
}
/**
* Get reflog message for a command.
*
* @param cmd
* specific command.
* @return reflog message, taking into account the state from this instance as
* well as overrides in the given command.
* @since 4.9
*/
protected String getRefLogMessage(ReceiveCommand cmd) {
return cmd.hasCustomRefLog() ? cmd.getRefLogMessage() : getRefLogMessage();
}
/**
* Check whether the reflog message for a command should include the result.
*
* @param cmd
* specific command.
* @return whether the reflog message should show the result, taking into
* account the state from this instance as well as overrides in the
* given command.
* @since 4.9
*/
protected boolean isRefLogIncludingResult(ReceiveCommand cmd) {
return cmd.hasCustomRefLog()
? cmd.isRefLogIncludingResult() : isRefLogIncludingResult();
}
/**
* Check whether the reflog for a command should be written regardless of repo
* defaults.
*
* @param cmd
* specific command.
* @return whether force writing is enabled.
* @since 4.9
*/
protected boolean isForceRefLog(ReceiveCommand cmd) {
Boolean isForceRefLog = cmd.isForceRefLog();
return isForceRefLog != null ? isForceRefLog.booleanValue()
: isForceRefLog();
}
/** {@inheritDoc} */
@Override
public String toString() {
StringBuilder r = new StringBuilder();
r.append(getClass().getSimpleName()).append('[');
if (commands.isEmpty())
return r.append(']').toString();
r.append('\n');
for (ReceiveCommand cmd : commands) {
r.append(" "); //$NON-NLS-1$
r.append(cmd);
r.append(" (").append(cmd.getResult()); //$NON-NLS-1$
if (cmd.getMessage() != null) {
r.append(": ").append(cmd.getMessage()); //$NON-NLS-1$
}
r.append(")\n"); //$NON-NLS-1$
}
return r.append(']').toString();
}
}