From 7f9fb8000252ac57b1613539927e34c9cdb9ef9b Mon Sep 17 00:00:00 2001 From: Dan Wang Date: Fri, 3 Jun 2016 16:39:45 -0700 Subject: [PATCH] Push implementation of option strings Example usage: $ ./jgit push \ --push-option "Reviewer=j.doe@example.org" \ --push-option "" \ origin HEAD:refs/for/master Stefan Beller has also made an equivalent change to CGit: http://thread.gmane.org/gmane.comp.version-control.git/299872 Change-Id: I6797e50681054dce3bd179e80b731aef5e200d77 Signed-off-by: Dan Wang --- .../src/org/eclipse/jgit/pgm/Push.java | 4 + .../jgit/transport/PushOptionsTest.java | 363 ++++++++++++++++++ .../eclipse/jgit/internal/JGitText.properties | 1 + .../src/org/eclipse/jgit/api/PushCommand.java | 24 +- .../org/eclipse/jgit/internal/JGitText.java | 1 + .../org/eclipse/jgit/lib/BatchRefUpdate.java | 46 ++- .../transport/BasePackPushConnection.java | 46 ++- .../jgit/transport/BaseReceivePack.java | 75 ++++ .../jgit/transport/GitProtocolConstants.java | 7 + .../eclipse/jgit/transport/PushProcess.java | 15 + .../eclipse/jgit/transport/ReceivePack.java | 13 + .../org/eclipse/jgit/transport/Transport.java | 22 ++ 12 files changed, 611 insertions(+), 6 deletions(-) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushOptionsTest.java diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Push.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Push.java index 33ea1deb8..1a4b5525b 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Push.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Push.java @@ -108,6 +108,9 @@ class Push extends TextBuiltin { @Option(name = "--dry-run") private boolean dryRun; + @Option(name = "--push-option", aliases = { "-t" }) + private List pushOptions = new ArrayList<>(); + private boolean shownURI; @Override @@ -127,6 +130,7 @@ class Push extends TextBuiltin { push.setThin(thin); push.setAtomic(atomic); push.setTimeout(timeout); + push.setPushOptions(pushOptions); Iterable results = push.call(); for (PushResult result : results) { try (ObjectReader reader = db.newObjectReader()) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushOptionsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushOptionsTest.java new file mode 100644 index 000000000..1554f8430 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushOptionsTest.java @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2016, Google Inc. + * 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.transport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.PushCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.api.errors.TransportException; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class PushOptionsTest extends RepositoryTestCase { + private URIish uri; + private TestProtocol testProtocol; + private Object ctx = new Object(); + private InMemoryRepository server; + private InMemoryRepository client; + private ObjectId obj1; + private ObjectId obj2; + private BaseReceivePack baseReceivePack; + + @Before + public void setUp() throws Exception { + super.setUp(); + + server = newRepo("server"); + client = newRepo("client"); + + testProtocol = new TestProtocol<>(null, + new ReceivePackFactory() { + @Override + public ReceivePack create(Object req, Repository database) + throws ServiceNotEnabledException, + ServiceNotAuthorizedException { + ReceivePack receivePack = new ReceivePack(database); + receivePack.setAllowPushOptions(true); + receivePack.setAtomic(true); + baseReceivePack = receivePack; + return receivePack; + } + }); + + uri = testProtocol.register(ctx, server); + + try (ObjectInserter ins = client.newObjectInserter()) { + obj1 = ins.insert(Constants.OBJ_BLOB, Constants.encode("test")); + obj2 = ins.insert(Constants.OBJ_BLOB, Constants.encode("file")); + ins.flush(); + } + } + + @After + public void tearDown() { + baseReceivePack = null; + Transport.unregister(testProtocol); + } + + private static InMemoryRepository newRepo(String name) { + return new InMemoryRepository(new DfsRepositoryDescription(name)); + } + + private List commands(boolean atomicSafe) + throws IOException { + List cmds = new ArrayList<>(); + cmds.add(new RemoteRefUpdate(null, null, obj1, "refs/heads/one", + true /* force update */, null /* no local tracking ref */, + ObjectId.zeroId())); + cmds.add(new RemoteRefUpdate(null, null, obj2, "refs/heads/two", + true /* force update */, null /* no local tracking ref */, + atomicSafe ? ObjectId.zeroId() : obj1)); + return cmds; + } + + private void connectLocalToRemote(Git local, Git remote) + throws URISyntaxException, IOException { + StoredConfig config = local.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + remoteConfig.addURI(new URIish( + remote.getRepository().getDirectory().toURI().toURL())); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + } + + private RevCommit addCommit(Git git) + throws IOException, NoFilepatternException, GitAPIException { + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + return git.commit().setMessage("adding f").call(); + } + + @Test + public void testNonAtomicPushWithOptions() throws Exception { + PushResult r; + server.setPerformsAtomicTransactions(false); + List pushOptions = Arrays.asList("Hello", "World!"); + + try (Transport tn = testProtocol.open(uri, client, "server")) { + tn.setPushAtomic(false); + tn.setPushOptions(pushOptions); + + r = tn.push(NullProgressMonitor.INSTANCE, commands(false)); + } + + RemoteRefUpdate one = r.getRemoteUpdate("refs/heads/one"); + RemoteRefUpdate two = r.getRemoteUpdate("refs/heads/two"); + + assertSame(RemoteRefUpdate.Status.OK, one.getStatus()); + assertSame(RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED, + two.getStatus()); + assertEquals(pushOptions, baseReceivePack.getPushOptions()); + } + + @Test + public void testAtomicPushWithOptions() throws Exception { + PushResult r; + server.setPerformsAtomicTransactions(true); + List pushOptions = Arrays.asList("Hello", "World!"); + + try (Transport tn = testProtocol.open(uri, client, "server")) { + tn.setPushAtomic(true); + tn.setPushOptions(pushOptions); + + r = tn.push(NullProgressMonitor.INSTANCE, commands(true)); + } + + RemoteRefUpdate one = r.getRemoteUpdate("refs/heads/one"); + RemoteRefUpdate two = r.getRemoteUpdate("refs/heads/two"); + + assertSame(RemoteRefUpdate.Status.OK, one.getStatus()); + assertSame(RemoteRefUpdate.Status.OK, two.getStatus()); + assertEquals(pushOptions, baseReceivePack.getPushOptions()); + } + + @Test + public void testFailedAtomicPushWithOptions() throws Exception { + PushResult r; + server.setPerformsAtomicTransactions(true); + List pushOptions = Arrays.asList("Hello", "World!"); + + try (Transport tn = testProtocol.open(uri, client, "server")) { + tn.setPushAtomic(true); + tn.setPushOptions(pushOptions); + + r = tn.push(NullProgressMonitor.INSTANCE, commands(false)); + } + + RemoteRefUpdate one = r.getRemoteUpdate("refs/heads/one"); + RemoteRefUpdate two = r.getRemoteUpdate("refs/heads/two"); + + assertSame(RemoteRefUpdate.Status.REJECTED_OTHER_REASON, + one.getStatus()); + assertSame(RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED, + two.getStatus()); + assertEquals(new ArrayList(), baseReceivePack.getPushOptions()); + } + + @Test + public void testThinPushWithOptions() throws Exception { + PushResult r; + List pushOptions = Arrays.asList("Hello", "World!"); + + try (Transport tn = testProtocol.open(uri, client, "server")) { + tn.setPushThin(true); + tn.setPushOptions(pushOptions); + + r = tn.push(NullProgressMonitor.INSTANCE, commands(false)); + } + + RemoteRefUpdate one = r.getRemoteUpdate("refs/heads/one"); + RemoteRefUpdate two = r.getRemoteUpdate("refs/heads/two"); + + assertSame(RemoteRefUpdate.Status.OK, one.getStatus()); + assertSame(RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED, + two.getStatus()); + assertEquals(pushOptions, baseReceivePack.getPushOptions()); + } + + @Test + public void testPushWithoutOptions() throws Exception { + try (Git local = new Git(db); + Git remote = new Git(createBareRepository())) { + connectLocalToRemote(local, remote); + + final StoredConfig config2 = remote.getRepository().getConfig(); + config2.setBoolean("receive", null, "pushoptions", true); + config2.save(); + + RevCommit commit = addCommit(local); + + local.checkout().setName("not-pushed").setCreateBranch(true).call(); + local.checkout().setName("branchtopush").setCreateBranch(true).call(); + + assertNull(remote.getRepository().resolve("refs/heads/branchtopush")); + assertNull(remote.getRepository().resolve("refs/heads/not-pushed")); + assertNull(remote.getRepository().resolve("refs/heads/master")); + + PushCommand pushCommand = local.push().setRemote("test"); + pushCommand.call(); + + assertEquals(commit.getId(), + remote.getRepository().resolve("refs/heads/branchtopush")); + assertNull(remote.getRepository().resolve("refs/heads/not-pushed")); + assertNull(remote.getRepository().resolve("refs/heads/master")); + } + } + + @Test + public void testPushWithEmptyOptions() throws Exception { + try (Git local = new Git(db); + Git remote = new Git(createBareRepository())) { + connectLocalToRemote(local, remote); + + final StoredConfig config2 = remote.getRepository().getConfig(); + config2.setBoolean("receive", null, "pushoptions", true); + config2.save(); + + RevCommit commit = addCommit(local); + + local.checkout().setName("not-pushed").setCreateBranch(true).call(); + local.checkout().setName("branchtopush").setCreateBranch(true).call(); + assertNull(remote.getRepository().resolve("refs/heads/branchtopush")); + assertNull(remote.getRepository().resolve("refs/heads/not-pushed")); + assertNull(remote.getRepository().resolve("refs/heads/master")); + + List pushOptions = new ArrayList<>(); + PushCommand pushCommand = local.push().setRemote("test") + .setPushOptions(pushOptions); + pushCommand.call(); + + assertEquals(commit.getId(), + remote.getRepository().resolve("refs/heads/branchtopush")); + assertNull(remote.getRepository().resolve("refs/heads/not-pushed")); + assertNull(remote.getRepository().resolve("refs/heads/master")); + } + } + + @Test + public void testAdvertisedButUnusedPushOptions() throws Exception { + try (Git local = new Git(db); + Git remote = new Git(createBareRepository())) { + connectLocalToRemote(local, remote); + + final StoredConfig config2 = remote.getRepository().getConfig(); + config2.setBoolean("receive", null, "pushoptions", true); + config2.save(); + + RevCommit commit = addCommit(local); + + local.checkout().setName("not-pushed").setCreateBranch(true).call(); + local.checkout().setName("branchtopush").setCreateBranch(true).call(); + + assertNull(remote.getRepository().resolve("refs/heads/branchtopush")); + assertNull(remote.getRepository().resolve("refs/heads/not-pushed")); + assertNull(remote.getRepository().resolve("refs/heads/master")); + + PushCommand pushCommand = local.push().setRemote("test") + .setPushOptions(null); + pushCommand.call(); + + assertEquals(commit.getId(), + remote.getRepository().resolve("refs/heads/branchtopush")); + assertNull(remote.getRepository().resolve("refs/heads/not-pushed")); + assertNull(remote.getRepository().resolve("refs/heads/master")); + } + } + + @Test(expected = TransportException.class) + public void testPushOptionsNotSupported() throws Exception { + try (Git local = new Git(db); + Git remote = new Git(createBareRepository())) { + connectLocalToRemote(local, remote); + + final StoredConfig config2 = remote.getRepository().getConfig(); + config2.setBoolean("receive", null, "pushoptions", false); + config2.save(); + + addCommit(local); + + local.checkout().setName("not-pushed").setCreateBranch(true).call(); + local.checkout().setName("branchtopush").setCreateBranch(true).call(); + + assertNull(remote.getRepository().resolve("refs/heads/branchtopush")); + assertNull(remote.getRepository().resolve("refs/heads/not-pushed")); + assertNull(remote.getRepository().resolve("refs/heads/master")); + + List pushOptions = new ArrayList<>(); + PushCommand pushCommand = local.push().setRemote("test") + .setPushOptions(pushOptions); + pushCommand.call(); + + fail("should already have thrown TransportException"); + } + } +} \ No newline at end of file diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index e68bca032..ebe1befee 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -498,6 +498,7 @@ pushCertificateInvalidHeader=Push certificate has invalid header format pushCertificateInvalidSignature=Push certificate has invalid signature format pushIsNotSupportedForBundleTransport=Push is not supported for bundle transport pushNotPermitted=push not permitted +pushOptionsNotSupported=Push options not supported; received {0} rawLogMessageDoesNotParseAsLogEntry=Raw log message does not parse as log entry readingObjectsFromLocalRepositoryFailed=reading objects from local repository failed: {0} readTimedOut=Read timed out after {0} ms diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java index 0a49f7806..bd4521b51 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java @@ -96,6 +96,8 @@ public class PushCommand extends private OutputStream out; + private List pushOptions; + /** * @param repo */ @@ -149,6 +151,7 @@ public class PushCommand extends if (receivePack != null) transport.setOptionReceivePack(receivePack); transport.setDryRun(dryRun); + transport.setPushOptions(pushOptions); configure(transport); final Collection toPush = transport @@ -189,7 +192,6 @@ public class PushCommand extends } return pushResults; - } /** @@ -453,4 +455,24 @@ public class PushCommand extends this.out = out; return this; } + + /** + * @return the option strings associated with the push operation + * @since 4.5 + */ + public List getPushOptions() { + return pushOptions; + } + + /** + * Sets the option strings associated with the push operation. + * + * @param pushOptions + * @return {@code this} + * @since 4.5 + */ + public PushCommand setPushOptions(List pushOptions) { + this.pushOptions = pushOptions; + return this; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index b7ef0854c..313512f99 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -557,6 +557,7 @@ public class JGitText extends TranslationBundle { /***/ public String pushCertificateInvalidSignature; /***/ public String pushIsNotSupportedForBundleTransport; /***/ public String pushNotPermitted; + /***/ public String pushOptionsNotSupported; /***/ public String rawLogMessageDoesNotParseAsLogEntry; /***/ public String readingObjectsFromLocalRepositoryFailed; /***/ public String readTimedOut; 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 266ca7b06..8550ec3a3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java @@ -92,6 +92,9 @@ public class BatchRefUpdate { /** Whether updates should be atomic. */ private boolean atomic; + /** Push options associated with this update. */ + private List pushOptions; + /** * Initialize a new batch update. * @@ -300,6 +303,16 @@ public class BatchRefUpdate { return this; } + /** + * Gets the list of option strings associated with this update. + * + * @return pushOptions + * @since 4.5 + */ + public List getPushOptions() { + return pushOptions; + } + /** * Execute this batch update. *

@@ -307,21 +320,24 @@ public class BatchRefUpdate { * update over each reference. *

* Implementations must respect the atomicity requirements of the underlying - * database as described in {@link #setAtomic(boolean)} and {@link - * RefDatabase#performsAtomicTransactions()}. + * database as described in {@link #setAtomic(boolean)} and + * {@link 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 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 options) throws IOException { if (atomic && !refdb.performsAtomicTransactions()) { for (ReceiveCommand c : commands) { @@ -333,6 +349,10 @@ public class BatchRefUpdate { return; } + if (options != null) { + pushOptions = options; + } + monitor.beginTask(JGitText.get().updatingReferences, commands.size()); List commands2 = new ArrayList( commands.size()); @@ -412,6 +432,24 @@ public class BatchRefUpdate { monitor.endTask(); } + /** + * 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 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); + } + private static Collection getTakenPrefixes( final Collection names) { Collection ref = new HashSet(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java index 0cbbdc77e..86cc484e3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java @@ -52,6 +52,7 @@ import java.io.OutputStream; import java.text.MessageFormat; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -113,14 +114,24 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen */ public static final String CAPABILITY_SIDE_BAND_64K = GitProtocolConstants.CAPABILITY_SIDE_BAND_64K; + /** + * The server supports the receiving of push options. + * @since 4.5 + */ + public static final String CAPABILITY_PUSH_OPTIONS = GitProtocolConstants.CAPABILITY_PUSH_OPTIONS; + private final boolean thinPack; private final boolean atomic; + /** A list of option strings associated with this push. */ + private List pushOptions; + private boolean capableAtomic; private boolean capableDeleteRefs; private boolean capableReport; private boolean capableSideBand; private boolean capableOfsDelta; + private boolean capablePushOptions; private boolean sentCommand; private boolean writePack; @@ -138,6 +149,7 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen super(packTransport); thinPack = transport.isPushThin(); atomic = transport.isPushAtomic(); + pushOptions = transport.getPushOptions(); } public void push(final ProgressMonitor monitor, @@ -197,6 +209,9 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen OutputStream outputStream) throws TransportException { try { writeCommands(refUpdates.values(), monitor, outputStream); + + if (pushOptions != null && capablePushOptions) + transmitOptions(); if (writePack) writePack(refUpdates, monitor); if (sentCommand) { @@ -232,6 +247,12 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen JGitText.get().atomicPushNotSupported); } + if (pushOptions != null && !capablePushOptions) { + throw new TransportException(uri, + MessageFormat.format(JGitText.get().pushOptionsNotSupported, + pushOptions.toString())); + } + for (final RemoteRefUpdate rru : refUpdates) { if (!capableDeleteRefs && rru.isDelete()) { rru.setStatus(Status.REJECTED_NODELETE); @@ -269,6 +290,14 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen outNeedsEnd = false; } + private void transmitOptions() throws IOException { + for (final String pushOption : pushOptions) { + pckOut.writeString(pushOption); + } + + pckOut.end(); + } + private String enableCapabilities(final ProgressMonitor monitor, OutputStream outputStream) { final StringBuilder line = new StringBuilder(); @@ -278,6 +307,10 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen capableDeleteRefs = wantCapability(line, CAPABILITY_DELETE_REFS); capableOfsDelta = wantCapability(line, CAPABILITY_OFS_DELTA); + if (pushOptions != null) { + capablePushOptions = wantCapability(line, CAPABILITY_PUSH_OPTIONS); + } + capableSideBand = wantCapability(line, CAPABILITY_SIDE_BAND_64K); if (capableSideBand) { in = new SideBandInputStream(in, monitor, getMessageWriter(), @@ -333,7 +366,8 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen throws IOException { final String unpackLine = readStringLongTimeout(); if (!unpackLine.startsWith("unpack ")) //$NON-NLS-1$ - throw new PackProtocolException(uri, MessageFormat.format(JGitText.get().unexpectedReportLine, unpackLine)); + throw new PackProtocolException(uri, MessageFormat + .format(JGitText.get().unexpectedReportLine, unpackLine)); final String unpackStatus = unpackLine.substring("unpack ".length()); //$NON-NLS-1$ if (unpackStatus.startsWith("error Pack exceeds the limit of")) {//$NON-NLS-1$ throw new TooLargePackException(uri, @@ -404,6 +438,16 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen } } + /** + * Gets the list of option strings associated with this push. + * + * @return pushOptions + * @since 4.5 + */ + public List getPushOptions() { + return pushOptions; + } + private static class CheckingSideBandOutputStream extends OutputStream { private final InputStream in; private final OutputStream out; 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 aae4bd9c3..b9923b95e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -48,6 +48,7 @@ import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_DELETE_ import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_OFS_DELTA; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_QUIET; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_REPORT_STATUS; +import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_OPTIONS; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SIDE_BAND_64K; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; import static org.eclipse.jgit.transport.SideBandOutputStream.CH_DATA; @@ -178,6 +179,9 @@ public abstract class BaseReceivePack { /** Should an incoming transfer permit non-fast-forward requests? */ private boolean allowNonFastForwards; + /** Should an incoming transfer permit push options? **/ + private boolean allowPushOptions; + /** * Should the requested ref updates be performed as a single atomic * transaction? @@ -247,6 +251,18 @@ public abstract class BaseReceivePack { private boolean quiet; + /** + * A list of option strings associated with a push. + * @since 4.5 + */ + protected List pushOptions; + + /** + * Whether the client intends to use push options. + * @since 4.5 + */ + protected boolean usePushOptions; + /** Lock around the received pack file, while updating refs. */ private PackLock packLock; @@ -311,6 +327,7 @@ public abstract class BaseReceivePack { allowBranchDeletes = rc.allowDeletes; allowNonFastForwards = rc.allowNonFastForwards; allowOfsDelta = rc.allowOfsDelta; + allowPushOptions = rc.allowPushOptions; advertiseRefsHook = AdvertiseRefsHook.DEFAULT; refFilter = RefFilter.DEFAULT; advertisedHaves = new HashSet(); @@ -330,6 +347,8 @@ public abstract class BaseReceivePack { final boolean allowDeletes; final boolean allowNonFastForwards; final boolean allowOfsDelta; + final boolean allowPushOptions; + final SignedPushConfig signedPush; ReceiveConfig(final Config config) { @@ -339,6 +358,8 @@ public abstract class BaseReceivePack { "denynonfastforwards", false); //$NON-NLS-1$ allowOfsDelta = config.getBoolean("repack", "usedeltabaseoffset", //$NON-NLS-1$ //$NON-NLS-2$ true); + allowPushOptions = config.getBoolean("receive", "pushoptions", //$NON-NLS-1$ //$NON-NLS-2$ + false); signedPush = SignedPushConfig.KEY.parse(config); } } @@ -787,6 +808,25 @@ public abstract class BaseReceivePack { allowQuiet = allow; } + /** + * @return true if the server supports the receiving of push options. + * @since 4.5 + */ + public boolean isAllowPushOptions() { + return allowPushOptions; + } + + /** + * Configure if the server supports the receiving of push options. + * + * @param allow + * true to permit option strings. + * @since 4.5 + */ + public void setAllowPushOptions(boolean allow) { + allowPushOptions = allow; + } + /** * True if the client wants less verbose output. * @@ -804,6 +844,24 @@ public abstract class BaseReceivePack { return quiet; } + /** + * Gets the list of string options associated with this push. + * + * @return pushOptions + * @throws RequestNotYetReadException + * if the client's request has not yet been read from the wire, + * so we do not know if they expect push options. Note that the + * client may have already written the request, it just has not + * been read. + * @since 4.5 + */ + public List getPushOptions() throws RequestNotYetReadException { + if (enabledCapabilities == null) { + throw new RequestNotYetReadException(); + } + return Collections.unmodifiableList(pushOptions); + } + /** * Set the configuration for push certificate verification. * @@ -1076,6 +1134,10 @@ public abstract class BaseReceivePack { adv.advertiseCapability(CAPABILITY_ATOMIC); if (allowOfsDelta) adv.advertiseCapability(CAPABILITY_OFS_DELTA); + if (allowPushOptions) { + adv.advertiseCapability(CAPABILITY_PUSH_OPTIONS); + pushOptions = new ArrayList<>(); + } adv.advertiseCapability(OPTION_AGENT, UserAgent.get()); adv.send(getAdvertisedOrDefaultRefs()); for (ObjectId obj : advertisedHaves) @@ -1192,6 +1254,8 @@ public abstract class BaseReceivePack { protected void enableCapabilities() { sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K); quiet = allowQuiet && isCapabilityEnabled(CAPABILITY_QUIET); + usePushOptions = allowPushOptions + && isCapabilityEnabled(CAPABILITY_PUSH_OPTIONS); if (sideBand) { OutputStream out = rawOut; @@ -1204,6 +1268,17 @@ public abstract class BaseReceivePack { } } + /** + * Sets the client's intention regarding push options. + * + * @param usePushOptions + * whether the client intends to use push options + * @since 4.5 + */ + public void setUsePushOptions(boolean usePushOptions) { + this.usePushOptions = usePushOptions; + } + /** * Check if the peer requested a capability. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java index efde06262..203114782 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java @@ -208,6 +208,13 @@ public class GitProtocolConstants { */ public static final String OPTION_AGENT = "agent"; //$NON-NLS-1$ + /** + * The server supports the receiving of push options. + * + * @since 4.5 + */ + public static final String CAPABILITY_PUSH_OPTIONS = "push-options"; //$NON-NLS-1$ + static enum MultiAck { OFF, CONTINUE, DETAILED; } 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 5cea88215..5590c2d25 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java @@ -49,6 +49,7 @@ import java.text.MessageFormat; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.eclipse.jgit.errors.MissingObjectException; @@ -87,6 +88,9 @@ class PushProcess { /** an outputstream to write messages to */ private final OutputStream out; + /** A list of option strings associated with this push */ + private List pushOptions; + /** * Create process for specified transport and refs updates specification. * @@ -122,6 +126,7 @@ class PushProcess { this.transport = transport; this.toPush = new HashMap(); this.out = out; + this.pushOptions = transport.getPushOptions(); for (final RemoteRefUpdate rru : toPush) { if (this.toPush.put(rru.getRemoteName(), rru) != null) throw new TransportException(MessageFormat.format( @@ -294,4 +299,14 @@ class PushProcess { } } } + + /** + * Gets the list of option strings associated with this push. + * + * @return pushOptions + * @since 4.5 + */ + public List getPushOptions() { + return pushOptions; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java index 2477806bd..d16b723ff 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java @@ -174,6 +174,15 @@ public class ReceivePack extends BaseReceivePack { super.enableCapabilities(); } + private void readPushOptions() throws IOException { + String pushOption = pckIn.readString(); + + while (pushOption != PacketLineIn.END) { + pushOptions.add(pushOption); + pushOption = pckIn.readString(); + } + } + private void service() throws IOException { if (isBiDirectionalPipe()) { sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut)); @@ -184,6 +193,10 @@ public class ReceivePack extends BaseReceivePack { return; recvCommands(); if (hasCommands()) { + if (usePushOptions) { + readPushOptions(); + } + Throwable unpackError = null; if (needPack()) { try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java index 862b3bdeb..bc4843a8a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -773,6 +773,9 @@ public abstract class Transport implements AutoCloseable { /** Assists with authentication the connection. */ private CredentialsProvider credentialsProvider; + /** The option strings associated with the push operation. */ + private List pushOptions; + private PrintStream hookOutRedirect; private PrePushHook prePush; @@ -1120,6 +1123,25 @@ public abstract class Transport implements AutoCloseable { return credentialsProvider; } + /** + * @return the option strings associated with the push operation + * @since 4.5 + */ + public List getPushOptions() { + return pushOptions; + } + + /** + * Sets the option strings associated with the push operation. + * + * @param pushOptions + * null if push options are unsupported + * @since 4.5 + */ + public void setPushOptions(final List pushOptions) { + this.pushOptions = pushOptions; + } + /** * Fetch objects and refs from the remote repository to the local one. *