diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java')
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java | 535 |
1 files changed, 430 insertions, 105 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java index 73850b7889..f9952501c3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java @@ -1,45 +1,12 @@ /* * 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. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others * - * 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 + * 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. * - * 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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.lib; @@ -49,17 +16,22 @@ import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_RE 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. @@ -68,6 +40,19 @@ import org.eclipse.jgit.transport.ReceiveCommand; * 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. */ @@ -86,6 +71,24 @@ public class BatchRefUpdate { 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 @@ -93,10 +96,14 @@ public class BatchRefUpdate { */ protected BatchRefUpdate(RefDatabase refdb) { this.refdb = refdb; - this.commands = new ArrayList<ReceiveCommand>(); + 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. */ @@ -116,7 +123,11 @@ public class BatchRefUpdate { return this; } - /** @return identity of the user making the change in the reflog. */ + /** + * 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; } @@ -134,7 +145,7 @@ public class BatchRefUpdate { * configuration. * @return {@code this}. */ - public BatchRefUpdate setRefLogIdent(final PersonIdent pi) { + public BatchRefUpdate setRefLogIdent(PersonIdent pi) { refLogIdent = pi; return this; } @@ -145,21 +156,41 @@ public class BatchRefUpdate { * @return message the caller wants to include in the reflog; null if the * update should not be logged. */ + @Nullable public String getRefLogMessage() { return refLogMessage; } - /** @return {@code true} if the ref log message should show the result. */ + /** + * 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. It may be null if - * appendStatus is null in order not to append to the reflog + * 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 @@ -181,6 +212,8 @@ public class BatchRefUpdate { /** * Don't record this update in the ref's associated reflog. + * <p> + * Equivalent to {@code setRefLogMessage(null, false)}. * * @return {@code this}. */ @@ -190,12 +223,104 @@ public class BatchRefUpdate { return this; } - /** @return true if log has been disabled by {@link #disableRefLog()}. */ + /** + * 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; } - /** @return commands this update will process. */ + /** + * 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); } @@ -236,36 +361,118 @@ public class BatchRefUpdate { } /** + * 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. - * @throws IOException + * @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) - throws IOException { + 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<ReceiveCommand>( + List<ReceiveCommand> commands2 = new ArrayList<>( commands.size()); - List<String> namesToCheck = new ArrayList<String>(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: - namesToCheck.add(cmd.getRefName()); commands2.add(cmd); break; case UPDATE: @@ -286,41 +493,21 @@ public class BatchRefUpdate { } } if (!commands2.isEmpty()) { - // What part of the name space is already taken - Collection<String> takenNames = new HashSet<String>(refdb.getRefs( - RefDatabase.ALL).keySet()); - Collection<String> takenPrefixes = getTakenPrefixes(takenNames); - - // Now to the update that may require more room in the name space + // 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: 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); - addRefToPrefixes(takenPrefixes, cmd.getRefName()); - takenNames.add(cmd.getRefName()); - cmd.setResult(ru.update(walk)); + switch (cmd.getType()) { + case DELETE: + // Performed in the first phase + break; + case UPDATE: + case UPDATE_NONFASTFORWARD: + case CREATE: + RefUpdate ru = newUpdate(cmd); + cmd.setResult(ru.update(walk)); + break; } } } catch (IOException err) { @@ -334,49 +521,127 @@ public class BatchRefUpdate { monitor.endTask(); } - private static Collection<String> getTakenPrefixes( - final Collection<String> names) { - Collection<String> ref = new HashSet<String>(); - for (String name : names) - ref.addAll(getPrefixes(name)); - return ref; + /** + * Get the ref database associated with this update. + * + * @return the ref database. + * @since 6.6 + */ + protected RefDatabase getRefDatabase() { + return refdb; } - private static void addRefToPrefixes(Collection<String> prefixes, - String name) { - for (String prefix : getPrefixes(name)) { - prefixes.add(prefix); + 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; } } - static Collection<String> getPrefixes(String s) { - Collection<String> ret = new HashSet<String>(); - int p1 = s.indexOf('/'); - while (p1 > 0) { - ret.add(s.substring(0, p1)); - p1 = s.indexOf('/', p1 + 1); + /** + * 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<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 IOException + * @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()) + if (isRefLogDisabled(cmd)) { ru.disableRefLog(); - else { + } else { ru.setRefLogIdent(refLogIdent); - ru.setRefLogMessage(refLogMessage, refLogIncludeResult); + ru.setRefLogMessage(getRefLogMessage(cmd), isRefLogIncludingResult(cmd)); + ru.setForceRefLog(isForceRefLog(cmd)); } + ru.setPushCertificate(pushCert); switch (cmd.getType()) { case DELETE: if (!ObjectId.zeroId().equals(cmd.getOldId())) @@ -395,6 +660,62 @@ public class BatchRefUpdate { } } + /** + * 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(); + } + @Override public String toString() { StringBuilder r = new StringBuilder(); @@ -406,7 +727,11 @@ public class BatchRefUpdate { for (ReceiveCommand cmd : commands) { r.append(" "); //$NON-NLS-1$ r.append(cmd); - r.append(" (").append(cmd.getResult()).append(")\n"); //$NON-NLS-1$ //$NON-NLS-2$ + 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(); } |