From 04fa307a70f8bc4f47a95fb18f74b0c9763d059b Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Tue, 22 May 2012 15:59:53 -0700 Subject: [PATCH 1/4] Reject non-fast-forwards earlier in BaseReceivePack If BaseReceivePack has setAllowNonFastForwards(false) configured (such as by receive.denynonfastforwards), automatically reject any command that attempts a non-fast-forward update before it goes further in processing. This matches with other checks in validateCommands(), such as the early failure of delete attempts when isAllowDeletes() is false. Change-Id: I3bb28e4dd6d17cb31ede09eb84ceb67cdb17ea5d --- .../org/eclipse/jgit/transport/BaseReceivePack.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java index c0d44b1db..a0b1c58c6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -1100,6 +1100,12 @@ public abstract class BaseReceivePack { } else { cmd.setType(ReceiveCommand.Type.UPDATE_NONFASTFORWARD); } + + if (cmd.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD + && !isAllowNonFastForwards()) { + cmd.setResult(Result.REJECTED_NONFASTFORWARD); + continue; + } } if (!cmd.getRefName().startsWith(Constants.R_REFS) @@ -1123,8 +1129,10 @@ public abstract class BaseReceivePack { /** Execute commands to update references. */ protected void executeCommands() { - List toApply = ReceiveCommand.filter(commands, - Result.NOT_ATTEMPTED); + List toApply = filterCommands(Result.NOT_ATTEMPTED); + if (toApply.isEmpty()) + return; + ProgressMonitor updating = NullProgressMonitor.INSTANCE; if (sideBand) { SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut); From d8d649a43eb6b72f97979112066378748d0cabaf Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Tue, 22 May 2012 16:22:41 -0700 Subject: [PATCH 2/4] Expose ReceiveCommand.updateType to check for UPDATE_NONFASTFORWARD When a command's type is UPDATE, JGit might not yet be sure if it is a fast-forward or not. Expose a utility method to compute the exact type by performing the merge base test, allowing the type to be switched to UPDATE_NONFASTFORWARD if old ObjectId is not contained in new ObjectId. BaseReceivePack already does this test when validating the incoming command list, so provide a package level backdoor to set the type and avoid needing to redo the merge test later. Change-Id: If5a6fcc50dc4d6f96e9bb0bb7bba15ebe8b86377 --- .../jgit/transport/BaseReceivePack.java | 10 ++--- .../jgit/transport/ReceiveCommand.java | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java index a0b1c58c6..9d72ef868 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -1086,11 +1086,11 @@ public abstract class BaseReceivePack { if (oldObj instanceof RevCommit && newObj instanceof RevCommit) { try { - if (!walk.isMergedInto((RevCommit) oldObj, - (RevCommit) newObj)) { - cmd - .setType(ReceiveCommand.Type.UPDATE_NONFASTFORWARD); - } + if (walk.isMergedInto((RevCommit) oldObj, + (RevCommit) newObj)) + cmd.setTypeFastForwardUpdate(); + else + cmd.setType(ReceiveCommand.Type.UPDATE_NONFASTFORWARD); } catch (MissingObjectException e) { cmd.setResult(Result.REJECTED_MISSING_OBJECT, e .getMessage()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java index 96814c85a..26bbcdcbc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java @@ -49,9 +49,13 @@ import java.util.ArrayList; import java.util.List; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevWalk; /** * A command being processed by {@link BaseReceivePack}. @@ -157,6 +161,8 @@ public class ReceiveCommand { private String message; + private boolean typeIsCorrect; + /** * Create a new command for {@link BaseReceivePack}. * @@ -264,6 +270,36 @@ public class ReceiveCommand { message = m; } + /** + * Update the type of this command by checking for fast-forward. + *

+ * If the command's current type is UPDATE, a merge test will be performed + * using the supplied RevWalk to determine if {@link #getOldId()} is fully + * merged into {@link #getNewId()}. If some commits are not merged the + * update type is changed to {@link Type#UPDATE_NONFASTFORWARD}. + * + * @param walk + * an instance to perform the merge test with. The caller must + * allocate and release this object. + * @throws IOException + * either oldId or newId is not accessible in the repository + * used by the RevWalk. This usually indicates data corruption, + * and the command cannot be processed. + */ + public void updateType(RevWalk walk) throws IOException { + if (typeIsCorrect) + return; + if (type == Type.UPDATE && !AnyObjectId.equals(oldId, newId)) { + RevObject o = walk.parseAny(oldId); + RevObject n = walk.parseAny(newId); + if (!(o instanceof RevCommit) + || !(n instanceof RevCommit) + || !walk.isMergedInto((RevCommit) o, (RevCommit) n)) + setType(Type.UPDATE_NONFASTFORWARD); + } + typeIsCorrect = true; + } + /** * Execute this command during a receive-pack session. *

@@ -314,6 +350,11 @@ public class ReceiveCommand { type = t; } + void setTypeFastForwardUpdate() { + type = Type.UPDATE; + typeIsCorrect = true; + } + private void setResult(final RefUpdate.Result r) { switch (r) { case NOT_ATTEMPTED: From 17be66acdbe662b42a263be77c435945902df968 Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Tue, 22 May 2012 16:45:06 -0700 Subject: [PATCH 3/4] Batch reference updates together for storage clone, fetch and push can all update multiple references in a single command invocation. Rather than performing sequential iteration of each reference change inside of the application code, push this down into the reference database where the implementation can take advantage of the batch size and optimize itself. For the local filesystem implementation the obvious optimization is to write a packed-refs file when the repository is completely empty. The initial clone, fetch or push into the destination may have hundreds of new references. Writing all of these as loose files is not efficient. This optimization is not implemented in this commit and is left as an exercise for the reader to supply in a future commit to JGit. To make the API changes simple, define the BatchRefUpdate type and implementation using the existing sequential behavior. Change-Id: I8e1674f091e05e24e3ff56ccbc687a6d18a6a61e --- .../org/eclipse/jgit/lib/BatchRefUpdate.java | 317 ++++++++++++++++++ .../src/org/eclipse/jgit/lib/RefDatabase.java | 11 + .../jgit/transport/BaseReceivePack.java | 19 +- .../jgit/transport/ReceiveCommand.java | 16 +- 4 files changed, 355 insertions(+), 8 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java new file mode 100644 index 000000000..e198e07e4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java @@ -0,0 +1,317 @@ +/* + * 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.List; + +import org.eclipse.jgit.internal.JGitText; +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 = ""; + 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 update + * 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 update) + throws IOException { + update.beginTask(JGitText.get().updatingReferences, commands.size()); + for (ReceiveCommand cmd : commands) { + try { + update.update(1); + + if (cmd.getResult() == NOT_ATTEMPTED) { + cmd.updateType(walk); + RefUpdate ru = newUpdate(cmd); + switch (cmd.getType()) { + case DELETE: + cmd.setResult(ru.delete(walk)); + continue; + + case CREATE: + case UPDATE: + case UPDATE_NONFASTFORWARD: + cmd.setResult(ru.update(walk)); + continue; + } + } + } catch (IOException err) { + cmd.setResult(REJECTED_OTHER_REASON, MessageFormat.format( + JGitText.get().lockError, err.getMessage())); + } + } + update.endTask(); + } + + /** + * 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; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java index 33c362305..ed2af4a00 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java @@ -146,6 +146,17 @@ public abstract class RefDatabase { public abstract RefRename newRename(String fromName, String toName) throws IOException; + /** + * Create a new batch update to attempt on this database. + *

+ * The default implementation performs a sequential update of each command. + * + * @return a new batch update object. + */ + public BatchRefUpdate newBatchUpdate() { + return new BatchRefUpdate(this); + } + /** * Read a single reference. *

diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java index 9d72ef868..e375221ee 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -67,6 +67,7 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config.SectionParser; import org.eclipse.jgit.lib.Constants; @@ -1139,12 +1140,20 @@ public abstract class BaseReceivePack { pm.setDelayStart(250, TimeUnit.MILLISECONDS); updating = pm; } - updating.beginTask(JGitText.get().updatingReferences, toApply.size()); - for (ReceiveCommand cmd : toApply) { - updating.update(1); - cmd.execute(this); + + BatchRefUpdate batch = db.getRefDatabase().newBatchUpdate(); + batch.setAllowNonFastForwards(isAllowNonFastForwards()); + batch.setRefLogIdent(getRefLogIdent()); + batch.setRefLogMessage("push", true); + batch.addCommand(toApply); + try { + batch.execute(walk, updating); + } catch (IOException err) { + for (ReceiveCommand cmd : toApply) { + if (cmd.getResult() == Result.NOT_ATTEMPTED) + cmd.reject(err); + } } - updating.endTask(); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java index 26bbcdcbc..4c7ffecce 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java @@ -337,8 +337,7 @@ public class ReceiveCommand { break; } } catch (IOException err) { - setResult(Result.REJECTED_OTHER_REASON, MessageFormat.format( - JGitText.get().lockError, err.getMessage())); + reject(err); } } @@ -355,7 +354,13 @@ public class ReceiveCommand { typeIsCorrect = true; } - private void setResult(final RefUpdate.Result r) { + /** + * Set the result of this command. + * + * @param r + * the new result code for this command. + */ + public void setResult(RefUpdate.Result r) { switch (r) { case NOT_ATTEMPTED: setResult(Result.NOT_ATTEMPTED); @@ -387,6 +392,11 @@ public class ReceiveCommand { } } + void reject(IOException err) { + setResult(Result.REJECTED_OTHER_REASON, MessageFormat.format( + JGitText.get().lockError, err.getMessage())); + } + @Override public String toString() { return getType().name() + ": " + getOldId().name() + " " From 3da13473f7a8baaac6ef1cf965062249c1de141f Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Tue, 22 May 2012 20:13:11 -0700 Subject: [PATCH 4/4] Use BatchRefUpdate for tracking refs in FetchProcess If there are a lot of references to modify, using BatchRefUpdate can save time if the underlying storage is able to combine these updates together. This should speed up initial clone or fetch into an empty repository, as some projects can have hundreds of release tags, or hundreds of branch heads. Change-Id: Iee9af8d5fa19080077d88357c18853540936e940 --- .../eclipse/jgit/transport/TransportTest.java | 3 +- .../eclipse/jgit/transport/FetchProcess.java | 149 +++++++++++------- .../jgit/transport/RemoteRefUpdate.java | 25 ++- .../jgit/transport/TrackingRefUpdate.java | 115 ++++++++++---- 4 files changed, 195 insertions(+), 97 deletions(-) diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java index 07ef39dc7..9899d14d8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java @@ -56,6 +56,7 @@ import java.util.Collections; import java.util.List; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.SampleDataRepositoryTestCase; import org.junit.After; import org.junit.Before; @@ -209,7 +210,7 @@ public class TransportTest extends SampleDataRepositoryTestCase { assertEquals("refs/remotes/test/a", tru.getLocalName()); assertEquals("refs/heads/a", tru.getRemoteName()); assertEquals(db.resolve("refs/heads/a"), tru.getNewObjectId()); - assertNull(tru.getOldObjectId()); + assertEquals(ObjectId.zeroId(), tru.getOldObjectId()); } @Test diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java index 27abf789d..66d0c62dd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java @@ -44,6 +44,11 @@ package org.eclipse.jgit.transport; +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_NONFASTFORWARD; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD; + import java.io.File; import java.io.IOException; import java.io.OutputStreamWriter; @@ -63,12 +68,13 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.BatchingProgressMonitor; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.revwalk.ObjectWalk; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.LockFile; @@ -97,6 +103,8 @@ class FetchProcess { private FetchConnection conn; + private Map localRefs; + FetchProcess(final Transport t, final Collection f) { transport = t; toFetch = f; @@ -108,6 +116,7 @@ class FetchProcess { localUpdates.clear(); fetchHeadUpdates.clear(); packLocks.clear(); + localRefs = null; try { executeImp(monitor, result); @@ -183,27 +192,40 @@ class FetchProcess { closeConnection(result); } + BatchRefUpdate batch = transport.local.getRefDatabase() + .newBatchUpdate() + .setAllowNonFastForwards(true) + .setRefLogMessage("fetch", true); final RevWalk walk = new RevWalk(transport.local); try { if (monitor instanceof BatchingProgressMonitor) { ((BatchingProgressMonitor) monitor).setDelayStart( 250, TimeUnit.MILLISECONDS); } - monitor.beginTask(JGitText.get().updatingReferences, localUpdates.size()); if (transport.isRemoveDeletedRefs()) - deleteStaleTrackingRefs(result, walk); + deleteStaleTrackingRefs(result, batch); for (TrackingRefUpdate u : localUpdates) { - try { - monitor.update(1); - u.update(walk); - result.add(u); - } catch (IOException err) { - throw new TransportException(MessageFormat.format(JGitText - .get().failureUpdatingTrackingRef, - u.getLocalName(), err.getMessage()), err); - } + result.add(u); + batch.addCommand(u.asReceiveCommand()); } - monitor.endTask(); + for (ReceiveCommand cmd : batch.getCommands()) { + cmd.updateType(walk); + if (cmd.getType() == UPDATE_NONFASTFORWARD + && cmd instanceof TrackingRefUpdate.Command + && !((TrackingRefUpdate.Command) cmd).canForceUpdate()) + cmd.setResult(REJECTED_NONFASTFORWARD); + } + if (transport.isDryRun()) { + for (ReceiveCommand cmd : batch.getCommands()) { + if (cmd.getResult() == NOT_ATTEMPTED) + cmd.setResult(OK); + } + } else + batch.execute(walk, monitor); + } catch (IOException err) { + throw new TransportException(MessageFormat.format( + JGitText.get().failureUpdatingTrackingRef, + getFirstFailedRefName(batch), err.getMessage()), err); } finally { walk.release(); } @@ -320,7 +342,7 @@ class FetchProcess { try { for (final ObjectId want : askFor.keySet()) ow.markStart(ow.parseAny(want)); - for (final Ref ref : transport.local.getAllRefs().values()) + for (final Ref ref : localRefs().values()) ow.markUninteresting(ow.parseAny(ref.getObjectId())); ow.checkConnectivity(); } finally { @@ -354,7 +376,7 @@ class FetchProcess { private Collection expandAutoFollowTags() throws TransportException { final Collection additionalTags = new ArrayList(); - final Map haveRefs = transport.local.getAllRefs(); + final Map haveRefs = localRefs(); for (final Ref r : conn.getRefs()) { if (!isTag(r)) continue; @@ -385,7 +407,7 @@ class FetchProcess { } private void expandFetchTags() throws TransportException { - final Map haveRefs = transport.local.getAllRefs(); + final Map haveRefs = localRefs(); for (final Ref r : conn.getRefs()) { if (!isTag(r)) continue; @@ -404,17 +426,10 @@ class FetchProcess { throws TransportException { final ObjectId newId = src.getObjectId(); if (spec.getDestination() != null) { - try { - final TrackingRefUpdate tru = createUpdate(spec, newId); - if (newId.equals(tru.getOldObjectId())) - return; - localUpdates.add(tru); - } catch (IOException err) { - // Bad symbolic ref? That is the most likely cause. - // - throw new TransportException( MessageFormat.format( - JGitText.get().cannotResolveLocalTrackingRefForUpdating, spec.getDestination()), err); - } + final TrackingRefUpdate tru = createUpdate(spec, newId); + if (newId.equals(tru.getOldObjectId())) + return; + localUpdates.add(tru); } askFor.put(newId, src); @@ -427,21 +442,41 @@ class FetchProcess { fetchHeadUpdates.add(fhr); } - private TrackingRefUpdate createUpdate(final RefSpec spec, - final ObjectId newId) throws IOException { - return new TrackingRefUpdate(transport.local, spec, newId, "fetch"); + private TrackingRefUpdate createUpdate(RefSpec spec, ObjectId newId) + throws TransportException { + Ref ref = localRefs().get(spec.getDestination()); + ObjectId oldId = ref != null && ref.getObjectId() != null + ? ref.getObjectId() + : ObjectId.zeroId(); + return new TrackingRefUpdate( + spec.isForceUpdate(), + spec.getSource(), + spec.getDestination(), + oldId, + newId); + } + + private Map localRefs() throws TransportException { + if (localRefs == null) { + try { + localRefs = transport.local.getRefDatabase() + .getRefs(RefDatabase.ALL); + } catch (IOException err) { + throw new TransportException(JGitText.get().cannotListRefs, err); + } + } + return localRefs; } - private void deleteStaleTrackingRefs(final FetchResult result, - final RevWalk walk) throws TransportException { - final Repository db = transport.local; - for (final Ref ref : db.getAllRefs().values()) { + private void deleteStaleTrackingRefs(FetchResult result, + BatchRefUpdate batch) throws IOException { + for (final Ref ref : localRefs().values()) { final String refname = ref.getName(); for (final RefSpec spec : toFetch) { if (spec.matchDestination(refname)) { final RefSpec s = spec.expandFromDestination(refname); if (result.getAdvertisedRef(s.getSource()) == null) { - deleteTrackingRef(result, db, walk, s, ref); + deleteTrackingRef(result, batch, s, ref); } } } @@ -449,31 +484,17 @@ class FetchProcess { } private void deleteTrackingRef(final FetchResult result, - final Repository db, final RevWalk walk, final RefSpec spec, - final Ref localRef) throws TransportException { - final String name = localRef.getName(); - try { - final TrackingRefUpdate u = new TrackingRefUpdate(db, name, spec - .getSource(), true, ObjectId.zeroId(), "deleted"); - result.add(u); - if (transport.isDryRun()){ - return; - } - u.delete(walk); - switch (u.getResult()) { - case NEW: - case NO_CHANGE: - case FAST_FORWARD: - case FORCED: - break; - default: - throw new TransportException(transport.getURI(), MessageFormat.format( - JGitText.get().cannotDeleteStaleTrackingRef2, name, u.getResult().name())); - } - } catch (IOException e) { - throw new TransportException(transport.getURI(), MessageFormat.format( - JGitText.get().cannotDeleteStaleTrackingRef, name), e); - } + final BatchRefUpdate batch, final RefSpec spec, final Ref localRef) { + if (localRef.getObjectId() == null) + return; + TrackingRefUpdate update = new TrackingRefUpdate( + true, + spec.getSource(), + localRef.getName(), + localRef.getObjectId(), + ObjectId.zeroId()); + result.add(update); + batch.addCommand(update.asReceiveCommand()); } private static boolean isTag(final Ref r) { @@ -483,4 +504,12 @@ class FetchProcess { private static boolean isTag(final String name) { return name.startsWith(Constants.R_TAGS); } + + private static String getFirstFailedRefName(BatchRefUpdate batch) { + for (ReceiveCommand cmd : batch.getCommands()) { + if (cmd.getResult() != ReceiveCommand.Result.OK) + return cmd.getRefName(); + } + return ""; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java index 3aaf7bb4e..421830202 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java @@ -49,6 +49,7 @@ import java.text.MessageFormat; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevWalk; @@ -144,6 +145,8 @@ public class RemoteRefUpdate { private final Repository localDb; + private RefUpdate localUpdate; + /** * Construct remote ref update request by providing an update specification. * Object is created with default {@link Status#NOT_ATTEMPTED} status and no @@ -299,10 +302,20 @@ public class RemoteRefUpdate { this.remoteName = remoteName; this.forceUpdate = forceUpdate; - if (localName != null && localDb != null) - trackingRefUpdate = new TrackingRefUpdate(localDb, localName, - remoteName, true, newObjectId, "push"); - else + if (localName != null && localDb != null) { + localUpdate = localDb.updateRef(localName); + localUpdate.setForceUpdate(true); + localUpdate.setRefLogMessage("push", true); + localUpdate.setNewObjectId(newObjectId); + trackingRefUpdate = new TrackingRefUpdate( + true, + remoteName, + localName, + localUpdate.getOldObjectId() != null + ? localUpdate.getOldObjectId() + : ObjectId.zeroId(), + newObjectId); + } else trackingRefUpdate = null; this.localDb = localDb; this.expectedOldObjectId = expectedOldObjectId; @@ -449,9 +462,9 @@ public class RemoteRefUpdate { */ protected void updateTrackingRef(final RevWalk walk) throws IOException { if (isDelete()) - trackingRefUpdate.delete(walk); + trackingRefUpdate.setResult(localUpdate.delete(walk)); else - trackingRefUpdate.update(walk); + trackingRefUpdate.setResult(localUpdate.update(walk)); } @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java index 03ecd81e0..3344c3f6a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java @@ -45,35 +45,31 @@ package org.eclipse.jgit.transport; -import java.io.IOException; - import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.RefUpdate; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.lib.RefUpdate.Result; -import org.eclipse.jgit.revwalk.RevWalk; /** Update of a locally stored tracking branch. */ public class TrackingRefUpdate { private final String remoteName; + private final String localName; + private boolean forceUpdate; + private ObjectId oldObjectId; + private ObjectId newObjectId; - private final RefUpdate update; - - TrackingRefUpdate(final Repository db, final RefSpec spec, - final AnyObjectId nv, final String msg) throws IOException { - this(db, spec.getDestination(), spec.getSource(), spec.isForceUpdate(), - nv, msg); - } + private RefUpdate.Result result; - TrackingRefUpdate(final Repository db, final String localName, - final String remoteName, final boolean forceUpdate, - final AnyObjectId nv, final String msg) throws IOException { + TrackingRefUpdate( + boolean canForceUpdate, + String remoteName, + String localName, + AnyObjectId oldValue, + AnyObjectId newValue) { this.remoteName = remoteName; - update = db.updateRef(localName); - update.setForceUpdate(forceUpdate); - update.setNewObjectId(nv); - update.setRefLogMessage(msg, true); + this.localName = localName; + this.forceUpdate = canForceUpdate; + this.oldObjectId = oldValue.copy(); + this.newObjectId = newValue.copy(); } /** @@ -95,7 +91,7 @@ public class TrackingRefUpdate { * @return the name used within this local repository. */ public String getLocalName() { - return update.getName(); + return localName; } /** @@ -104,7 +100,7 @@ public class TrackingRefUpdate { * @return new value. Null if the caller has not configured it. */ public ObjectId getNewObjectId() { - return update.getNewObjectId(); + return newObjectId; } /** @@ -115,11 +111,10 @@ public class TrackingRefUpdate { * value may change if someone else modified the ref between the time we * last read it and when the ref was locked for update. * - * @return the value of the ref prior to the update being attempted; null if - * the updated has not been attempted yet. + * @return the value of the ref prior to the update being attempted. */ public ObjectId getOldObjectId() { - return update.getOldObjectId(); + return oldObjectId; } /** @@ -127,15 +122,75 @@ public class TrackingRefUpdate { * * @return the status of the update. */ - public Result getResult() { - return update.getResult(); + public RefUpdate.Result getResult() { + return result; } - void update(final RevWalk walk) throws IOException { - update.update(walk); + void setResult(RefUpdate.Result result) { + this.result = result; } - void delete(final RevWalk walk) throws IOException { - update.delete(walk); + ReceiveCommand asReceiveCommand() { + return new Command(); + } + + final class Command extends ReceiveCommand { + private Command() { + super(oldObjectId, newObjectId, localName); + } + + boolean canForceUpdate() { + return forceUpdate; + } + + @Override + public void setResult(RefUpdate.Result status) { + result = status; + super.setResult(status); + } + + @Override + public void setResult(ReceiveCommand.Result status) { + result = decode(status); + super.setResult(status); + } + + @Override + public void setResult(ReceiveCommand.Result status, String msg) { + result = decode(status); + super.setResult(status, msg); + } + + private RefUpdate.Result decode(ReceiveCommand.Result status) { + switch (status) { + case OK: + if (AnyObjectId.equals(oldObjectId, newObjectId)) + return RefUpdate.Result.NO_CHANGE; + switch (getType()) { + case CREATE: + return RefUpdate.Result.NEW; + case UPDATE: + return RefUpdate.Result.FAST_FORWARD; + case DELETE: + case UPDATE_NONFASTFORWARD: + default: + return RefUpdate.Result.FORCED; + } + + case REJECTED_NOCREATE: + case REJECTED_NODELETE: + case REJECTED_NONFASTFORWARD: + return RefUpdate.Result.REJECTED; + case REJECTED_CURRENT_BRANCH: + return RefUpdate.Result.REJECTED_CURRENT_BRANCH; + case REJECTED_MISSING_OBJECT: + return RefUpdate.Result.IO_FAILURE; + case LOCK_FAILURE: + case NOT_ATTEMPTED: + case REJECTED_OTHER_REASON: + default: + return RefUpdate.Result.LOCK_FAILURE; + } + } } }