aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java')
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java319
1 files changed, 241 insertions, 78 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
index 9721ee9eb0..8f90f326d3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
@@ -1,44 +1,11 @@
/*
- * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
- * and other copyright owners as documented in the project's IP log.
+ * Copyright (C) 2008, 2022 Marek Zawirski <marek.zawirski@gmail.com> 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.transport;
@@ -47,13 +14,19 @@ import java.io.IOException;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.Collection;
-import java.util.HashMap;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.api.errors.AbortedByHookException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.NotSupportedException;
import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.hooks.PrePushHook;
import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
@@ -86,6 +59,11 @@ class PushProcess {
/** an outputstream to write messages to */
private final OutputStream out;
+ /** A list of option strings associated with this push */
+ private List<String> pushOptions;
+
+ private final PrePushHook prePush;
+
/**
* Create process for specified transport and refs updates specification.
*
@@ -94,12 +72,15 @@ class PushProcess {
* connection.
* @param toPush
* specification of refs updates (and local tracking branches).
- *
+ * @param prePush
+ * {@link PrePushHook} to run after the remote advertisement has
+ * been gotten
* @throws TransportException
+ * if a protocol error occurred during push/fetch
*/
- PushProcess(final Transport transport,
- final Collection<RemoteRefUpdate> toPush) throws TransportException {
- this(transport, toPush, null);
+ PushProcess(Transport transport, Collection<RemoteRefUpdate> toPush,
+ PrePushHook prePush) throws TransportException {
+ this(transport, toPush, prePush, null);
}
/**
@@ -110,18 +91,23 @@ class PushProcess {
* connection.
* @param toPush
* specification of refs updates (and local tracking branches).
+ * @param prePush
+ * {@link PrePushHook} to run after the remote advertisement has
+ * been gotten
* @param out
* OutputStream to write messages to
* @throws TransportException
+ * if a protocol error occurred during push/fetch
*/
- PushProcess(final Transport transport,
- final Collection<RemoteRefUpdate> toPush, OutputStream out)
- throws TransportException {
+ PushProcess(Transport transport, Collection<RemoteRefUpdate> toPush,
+ PrePushHook prePush, OutputStream out) throws TransportException {
this.walker = new RevWalk(transport.local);
this.transport = transport;
- this.toPush = new HashMap<String, RemoteRefUpdate>();
+ this.toPush = new LinkedHashMap<>();
+ this.prePush = prePush;
this.out = out;
- for (final RemoteRefUpdate rru : toPush) {
+ this.pushOptions = transport.getPushOptions();
+ for (RemoteRefUpdate rru : toPush) {
if (this.toPush.put(rru.getRemoteName(), rru) != null)
throw new TransportException(MessageFormat.format(
JGitText.get().duplicateRemoteRefUpdateIsIllegal, rru.getRemoteName()));
@@ -144,7 +130,7 @@ class PushProcess {
* when some error occurred during operation, like I/O, protocol
* error, or local database consistency error.
*/
- PushResult execute(final ProgressMonitor monitor)
+ PushResult execute(ProgressMonitor monitor)
throws NotSupportedException, TransportException {
try {
monitor.beginTask(PROGRESS_OPENING_CONNECTION,
@@ -156,10 +142,39 @@ class PushProcess {
res.setAdvertisedRefs(transport.getURI(), connection
.getRefsMap());
res.peerUserAgent = connection.getPeerUserAgent();
- res.setRemoteUpdates(toPush);
monitor.endTask();
+ Map<String, RemoteRefUpdate> expanded = expandMatching();
+ toPush.clear();
+ toPush.putAll(expanded);
+
+ res.setRemoteUpdates(toPush);
final Map<String, RemoteRefUpdate> preprocessed = prepareRemoteUpdates();
+ List<RemoteRefUpdate> willBeAttempted = preprocessed.values()
+ .stream().filter(u -> {
+ switch (u.getStatus()) {
+ case NON_EXISTING:
+ case REJECTED_NODELETE:
+ case REJECTED_NONFASTFORWARD:
+ case REJECTED_OTHER_REASON:
+ case REJECTED_REMOTE_CHANGED:
+ case UP_TO_DATE:
+ return false;
+ default:
+ return true;
+ }
+ }).collect(Collectors.toList());
+ if (!willBeAttempted.isEmpty()) {
+ if (prePush != null) {
+ try {
+ prePush.setRefs(willBeAttempted);
+ prePush.setDryRun(transport.isDryRun());
+ prePush.call();
+ } catch (AbortedByHookException | IOException e) {
+ throw new TransportException(e.getMessage(), e);
+ }
+ }
+ }
if (transport.isDryRun())
modifyUpdatesForDryRun();
else if (!preprocessed.isEmpty())
@@ -170,7 +185,7 @@ class PushProcess {
}
if (!transport.isDryRun())
updateTrackingRefs();
- for (final RemoteRefUpdate rru : toPush.values()) {
+ for (RemoteRefUpdate rru : toPush.values()) {
final TrackingRefUpdate tru = rru.getTrackingRefUpdate();
if (tru != null)
res.add(tru);
@@ -183,11 +198,17 @@ class PushProcess {
private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
throws TransportException {
- final Map<String, RemoteRefUpdate> result = new HashMap<String, RemoteRefUpdate>();
- for (final RemoteRefUpdate rru : toPush.values()) {
+ boolean atomic = transport.isPushAtomic();
+ final Map<String, RemoteRefUpdate> result = new LinkedHashMap<>();
+ for (RemoteRefUpdate rru : toPush.values()) {
final Ref advertisedRef = connection.getRef(rru.getRemoteName());
- final ObjectId advertisedOld = (advertisedRef == null ? ObjectId
- .zeroId() : advertisedRef.getObjectId());
+ ObjectId advertisedOld = null;
+ if (advertisedRef != null) {
+ advertisedOld = advertisedRef.getObjectId();
+ }
+ if (advertisedOld == null) {
+ advertisedOld = ObjectId.zeroId();
+ }
if (rru.getNewObjectId().equals(advertisedOld)) {
if (rru.isDelete()) {
@@ -205,8 +226,14 @@ class PushProcess {
if (rru.isExpectingOldObjectId()
&& !rru.getExpectedOldObjectId().equals(advertisedOld)) {
rru.setStatus(Status.REJECTED_REMOTE_CHANGED);
+ if (atomic) {
+ return rejectAll();
+ }
continue;
}
+ if (!rru.isExpectingOldObjectId()) {
+ rru.setExpectedOldObjectId(advertisedOld);
+ }
// create ref (hasn't existed on remote side) and delete ref
// are always fast-forward commands, feasible at this level
@@ -216,42 +243,168 @@ class PushProcess {
continue;
}
- // check for fast-forward:
- // - both old and new ref must point to commits, AND
- // - both of them must be known for us, exist in repository, AND
- // - old commit must be ancestor of new commit
- boolean fastForward = true;
- try {
- RevObject oldRev = walker.parseAny(advertisedOld);
- final RevObject newRev = walker.parseAny(rru.getNewObjectId());
- if (!(oldRev instanceof RevCommit)
- || !(newRev instanceof RevCommit)
- || !walker.isMergedInto((RevCommit) oldRev,
- (RevCommit) newRev))
- fastForward = false;
- } catch (MissingObjectException x) {
- fastForward = false;
- } catch (Exception x) {
- throw new TransportException(transport.getURI(), MessageFormat.format(
- JGitText.get().readingObjectsFromLocalRepositoryFailed, x.getMessage()), x);
- }
+ boolean fastForward = isFastForward(advertisedOld,
+ rru.getNewObjectId());
rru.setFastForward(fastForward);
- if (!fastForward && !rru.isForceUpdate())
+ if (!fastForward && !rru.isForceUpdate()) {
rru.setStatus(Status.REJECTED_NONFASTFORWARD);
- else
+ if (atomic) {
+ return rejectAll();
+ }
+ } else {
result.put(rru.getRemoteName(), rru);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Determines whether an update from {@code oldOid} to {@code newOid} is a
+ * fast-forward update:
+ * <ul>
+ * <li>both old and new must be commits, AND</li>
+ * <li>both of them must be known to us and exist in the repository,
+ * AND</li>
+ * <li>the old commit must be an ancestor of the new commit.</li>
+ * </ul>
+ *
+ * @param oldOid
+ * {@link ObjectId} of the old commit
+ * @param newOid
+ * {@link ObjectId} of the new commit
+ * @return {@code true} if the update fast-forwards, {@code false} otherwise
+ * @throws TransportException
+ * if a protocol error occurred during push/fetch
+ */
+ private boolean isFastForward(ObjectId oldOid, ObjectId newOid)
+ throws TransportException {
+ try {
+ RevObject oldRev = walker.parseAny(oldOid);
+ RevObject newRev = walker.parseAny(newOid);
+ if (!(oldRev instanceof RevCommit) || !(newRev instanceof RevCommit)
+ || !walker.isMergedInto((RevCommit) oldRev,
+ (RevCommit) newRev)) {
+ return false;
+ }
+ } catch (MissingObjectException x) {
+ return false;
+ } catch (Exception x) {
+ throw new TransportException(transport.getURI(),
+ MessageFormat.format(JGitText
+ .get().readingObjectsFromLocalRepositoryFailed,
+ x.getMessage()),
+ x);
+ }
+ return true;
+ }
+
+ /**
+ * Expands all placeholder {@link RemoteRefUpdate}s for "matching"
+ * {@link RefSpec}s ":" in {@link #toPush} and returns the resulting map in
+ * which the placeholders have been replaced by their expansion.
+ *
+ * @return a new map of {@link RemoteRefUpdate}s keyed by remote name
+ * @throws TransportException
+ * if the expansion results in duplicate updates
+ */
+ private Map<String, RemoteRefUpdate> expandMatching()
+ throws TransportException {
+ Map<String, RemoteRefUpdate> result = new LinkedHashMap<>();
+ boolean hadMatch = false;
+ for (RemoteRefUpdate update : toPush.values()) {
+ if (update.isMatching()) {
+ if (hadMatch) {
+ throw new TransportException(MessageFormat.format(
+ JGitText.get().duplicateRemoteRefUpdateIsIllegal,
+ ":")); //$NON-NLS-1$
+ }
+ expandMatching(result, update);
+ hadMatch = true;
+ } else if (result.put(update.getRemoteName(), update) != null) {
+ throw new TransportException(MessageFormat.format(
+ JGitText.get().duplicateRemoteRefUpdateIsIllegal,
+ update.getRemoteName()));
+ }
}
return result;
}
+ /**
+ * Expands the placeholder {@link RemoteRefUpdate} {@code match} for a
+ * "matching" {@link RefSpec} ":" or "+:" and puts the expansion into the
+ * given map {@code updates}.
+ *
+ * @param updates
+ * map to put the expansion in
+ * @param match
+ * the placeholder {@link RemoteRefUpdate} to expand
+ *
+ * @throws TransportException
+ * if the expansion results in duplicate updates, or the local
+ * branches cannot be determined
+ */
+ private void expandMatching(Map<String, RemoteRefUpdate> updates,
+ RemoteRefUpdate match) throws TransportException {
+ try {
+ Map<String, Ref> advertisement = connection.getRefsMap();
+ Collection<RefSpec> fetchSpecs = match.getFetchSpecs();
+ boolean forceUpdate = match.isForceUpdate();
+ for (Ref local : transport.local.getRefDatabase()
+ .getRefsByPrefix(Constants.R_HEADS)) {
+ if (local.isSymbolic()) {
+ continue;
+ }
+ String name = local.getName();
+ Ref advertised = advertisement.get(name);
+ if (advertised == null || advertised.isSymbolic()) {
+ continue;
+ }
+ ObjectId oldOid = advertised.getObjectId();
+ if (oldOid == null || ObjectId.zeroId().equals(oldOid)) {
+ continue;
+ }
+ ObjectId newOid = local.getObjectId();
+ if (newOid == null || ObjectId.zeroId().equals(newOid)) {
+ continue;
+ }
+
+ RemoteRefUpdate rru = new RemoteRefUpdate(transport.local, name,
+ newOid, name, forceUpdate,
+ Transport.findTrackingRefName(name, fetchSpecs),
+ oldOid);
+ if (updates.put(rru.getRemoteName(), rru) != null) {
+ throw new TransportException(MessageFormat.format(
+ JGitText.get().duplicateRemoteRefUpdateIsIllegal,
+ rru.getRemoteName()));
+ }
+ }
+ } catch (IOException x) {
+ throw new TransportException(transport.getURI(),
+ MessageFormat.format(JGitText
+ .get().readingObjectsFromLocalRepositoryFailed,
+ x.getMessage()),
+ x);
+ }
+ }
+
+ private Map<String, RemoteRefUpdate> rejectAll() {
+ for (RemoteRefUpdate rru : toPush.values()) {
+ if (rru.getStatus() == Status.NOT_ATTEMPTED) {
+ rru.setStatus(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
+ rru.setMessage(JGitText.get().transactionAborted);
+ }
+ }
+ return Collections.emptyMap();
+ }
+
private void modifyUpdatesForDryRun() {
- for (final RemoteRefUpdate rru : toPush.values())
+ for (RemoteRefUpdate rru : toPush.values())
if (rru.getStatus() == Status.NOT_ATTEMPTED)
rru.setStatus(Status.OK);
}
private void updateTrackingRefs() {
- for (final RemoteRefUpdate rru : toPush.values()) {
+ for (RemoteRefUpdate rru : toPush.values()) {
final Status status = rru.getStatus();
if (rru.hasTrackingRefUpdate()
&& (status == Status.UP_TO_DATE || status == Status.OK)) {
@@ -267,4 +420,14 @@ class PushProcess {
}
}
}
+
+ /**
+ * Gets the list of option strings associated with this push.
+ *
+ * @return pushOptions
+ * @since 4.5
+ */
+ public List<String> getPushOptions() {
+ return pushOptions;
+ }
}