/* * Copyright (C) 2008-2012, Google Inc. * Copyright (C) 2008, Shawn O. Pearce * 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 java.io.IOException; import java.text.MessageFormat; 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 org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; /** * 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 { 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; /** * 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(); } /** * @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; } /** @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(final 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. */ public String getRefLogMessage() { return refLogMessage; } /** @return {@code true} if the ref log message should show the result. */ public boolean isRefLogIncludingResult() { return refLogIncludeResult; } /** * Set the message to include in the reflog. * * @param msg * the message to describe this change. It may be null if * appendStatus is null in order not to append to the reflog * @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. * * @return {@code this}. */ public BatchRefUpdate disableRefLog() { refLogMessage = null; refLogIncludeResult = false; return this; } /** @return true if log has been disabled by {@link #disableRefLog()}. */ public boolean isRefLogDisabled() { return refLogMessage == null; } /** @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; } /** * Execute this batch update. *

* The default implementation of this method performs a sequential reference * update over each reference. * * @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 * 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 { monitor.beginTask(JGitText.get().updatingReferences, commands.size()); List commands2 = new ArrayList( commands.size()); List namesToCheck = 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) { cmd.updateType(walk); switch (cmd.getType()) { case CREATE: namesToCheck.add(cmd.getRefName()); 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 takenNames = new HashSet(refdb.getRefs( RefDatabase.ALL).keySet()); Collection takenPrefixes = getTakenPrefixes(takenNames); // Now to the update that may require more room in the name space for (ReceiveCommand cmd : commands2) { try { monitor.update(1); 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: monitor.update(1); 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)); } } } catch (IOException err) { cmd.setResult(REJECTED_OTHER_REASON, MessageFormat.format( JGitText.get().lockError, err.getMessage())); } } } monitor.endTask(); } private static Collection getTakenPrefixes( final Collection names) { Collection ref = new HashSet(); for (String name : names) ref.addAll(getPrefixes(name)); return ref; } private static void addRefToPrefixes(Collection prefixes, String name) { for (String prefix : getPrefixes(name)) { prefixes.add(prefix); } } static Collection getPrefixes(String s) { Collection ret = new HashSet(); int p1 = s.indexOf('/'); while (p1 > 0) { ret.add(s.substring(0, p1)); p1 = s.indexOf('/', p1 + 1); } return ret; } /** * 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 * 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()) ru.disableRefLog(); else { ru.setRefLogIdent(refLogIdent); ru.setRefLogMessage(refLogMessage, refLogIncludeResult); } 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; } } }