/* * Copyright (C) 2008-2012, Google Inc. * Copyright (C) 2008, Shawn O. Pearce 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.lib; import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; 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.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. *

* 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. *

* 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 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 pushOptions; /** Associated timestamps that should be blocked on before update. */ private List 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. *

* 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. *

* 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. *

* 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)}. *

* 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. *

* 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. *

* 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}. *

* 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. *

* 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. *

* 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 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 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 getPushOptions() { return pushOptions; } /** * Set push options associated with this update. *

* 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 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 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. *

* The default implementation of this method performs a sequential reference * update over each reference. *

* 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 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 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()) { // Perform updates 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 (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: cmd.setResult(ru.update(walk)); break; } } } 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); } /** * 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 getPrefixes(String name) { Collection 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 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(); } }