Push implementation of option strings

Example usage:
$ ./jgit push \
  --push-option "Reviewer=j.doe@example.org" \
  --push-option "<arbitrary string>" \
  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 <dwwang@google.com>
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 33ea1de..1a4b552 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 @@ void nothin(@SuppressWarnings("unused") final boolean ignored) {
 	@Option(name = "--dry-run")
 	private boolean dryRun;
 
+	@Option(name = "--push-option", aliases = { "-t" })
+	private List<String> pushOptions = new ArrayList<>();
+
 	private boolean shownURI;
 
 	@Override
@@ -127,6 +130,7 @@ protected void run() throws Exception {
 			push.setThin(thin);
 			push.setAtomic(atomic);
 			push.setTimeout(timeout);
+			push.setPushOptions(pushOptions);
 			Iterable<PushResult> 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 0000000..1554f84
--- /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<Object> 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<Object>() {
+					@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<RemoteRefUpdate> commands(boolean atomicSafe)
+			throws IOException {
+		List<RemoteRefUpdate> 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<String> 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<String> 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<String> 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<String>(), baseReceivePack.getPushOptions());
+	}
+
+	@Test
+	public void testThinPushWithOptions() throws Exception {
+		PushResult r;
+		List<String> 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<String> 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<String> 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 e68bca0..ebe1bef 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 @@
 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 0a49f78..bd4521b 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<String> pushOptions;
+
 	/**
 	 * @param repo
 	 */
@@ -149,6 +151,7 @@ public Iterable<PushResult> call() throws GitAPIException,
 				if (receivePack != null)
 					transport.setOptionReceivePack(receivePack);
 				transport.setDryRun(dryRun);
+				transport.setPushOptions(pushOptions);
 				configure(transport);
 
 				final Collection<RemoteRefUpdate> toPush = transport
@@ -189,7 +192,6 @@ public Iterable<PushResult> call() throws GitAPIException,
 		}
 
 		return pushResults;
-
 	}
 
 	/**
@@ -453,4 +455,24 @@ public PushCommand setOutputStream(OutputStream out) {
 		this.out = out;
 		return this;
 	}
+
+	/**
+	 * @return the option strings associated with the push operation
+	 * @since 4.5
+	 */
+	public List<String> getPushOptions() {
+		return pushOptions;
+	}
+
+	/**
+	 * Sets the option strings associated with the push operation.
+	 *
+	 * @param pushOptions
+	 * @return {@code this}
+	 * @since 4.5
+	 */
+	public PushCommand setPushOptions(List<String> 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 b7ef085..313512f 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 static JGitText get() {
 	/***/ 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 266ca7b..8550ec3 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<String> pushOptions;
+
 	/**
 	 * Initialize a new batch update.
 	 *
@@ -301,27 +304,40 @@ public BatchRefUpdate addCommand(Collection<ReceiveCommand> cmd) {
 	}
 
 	/**
+	 * Gets the list of option strings associated with this update.
+	 *
+	 * @return pushOptions
+	 * @since 4.5
+	 */
+	public List<String> getPushOptions() {
+		return pushOptions;
+	}
+
+	/**
 	 * Execute this batch update.
 	 * <p>
 	 * The default implementation of this method performs a sequential reference
 	 * update over each reference.
 	 * <p>
 	 * Implementations must respect the atomicity requirements of the underlying
-	 * database as described in {@link #setAtomic(boolean)} and {@link
-	 * 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<String> options) throws IOException {
 
 		if (atomic && !refdb.performsAtomicTransactions()) {
 			for (ReceiveCommand c : commands) {
@@ -333,6 +349,10 @@ public void execute(RevWalk walk, ProgressMonitor monitor)
 			return;
 		}
 
+		if (options != null) {
+			pushOptions = options;
+		}
+
 		monitor.beginTask(JGitText.get().updatingReferences, commands.size());
 		List<ReceiveCommand> commands2 = new ArrayList<ReceiveCommand>(
 				commands.size());
@@ -412,6 +432,24 @@ public void execute(RevWalk walk, ProgressMonitor monitor)
 		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<String> getTakenPrefixes(
 			final Collection<String> names) {
 		Collection<String> ref = new HashSet<String>();
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 0cbbdc7..86cc484 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.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<String> 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 BasePackPushConnection(final PackTransport packTransport) {
 		super(packTransport);
 		thinPack = transport.isPushThin();
 		atomic = transport.isPushAtomic();
+		pushOptions = transport.getPushOptions();
 	}
 
 	public void push(final ProgressMonitor monitor,
@@ -197,6 +209,9 @@ protected void doPush(final ProgressMonitor monitor,
 			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 @@ private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
 					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 @@ private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
 		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 @@ private String enableCapabilities(final ProgressMonitor monitor,
 		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 @@ private void readStatusReport(final Map<String, RemoteRefUpdate> refUpdates)
 			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 @@ private String readStringLongTimeout() throws IOException {
 		}
 	}
 
+	/**
+	 * Gets the list of option strings associated with this push.
+	 *
+	 * @return pushOptions
+	 * @since 4.5
+	 */
+	public List<String> 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 aae4bd9..b9923b9 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_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 Set<String> getCapabilities() {
 	/** 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 Set<String> getCapabilities() {
 
 	private boolean quiet;
 
+	/**
+	 * A list of option strings associated with a push.
+	 * @since 4.5
+	 */
+	protected List<String> 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 @@ protected BaseReceivePack(final Repository into) {
 		allowBranchDeletes = rc.allowDeletes;
 		allowNonFastForwards = rc.allowNonFastForwards;
 		allowOfsDelta = rc.allowOfsDelta;
+		allowPushOptions = rc.allowPushOptions;
 		advertiseRefsHook = AdvertiseRefsHook.DEFAULT;
 		refFilter = RefFilter.DEFAULT;
 		advertisedHaves = new HashSet<ObjectId>();
@@ -330,6 +347,8 @@ public ReceiveConfig parse(final Config cfg) {
 		final boolean allowDeletes;
 		final boolean allowNonFastForwards;
 		final boolean allowOfsDelta;
+		final boolean allowPushOptions;
+
 		final SignedPushConfig signedPush;
 
 		ReceiveConfig(final Config config) {
@@ -339,6 +358,8 @@ public ReceiveConfig parse(final Config cfg) {
 					"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);
 		}
 	}
@@ -788,6 +809,25 @@ public void setAllowQuiet(boolean 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.
 	 *
 	 * @return true if the client has requested the server to be less verbose.
@@ -805,6 +845,24 @@ public boolean isQuiet() throws RequestNotYetReadException {
 	}
 
 	/**
+	 * 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<String> getPushOptions() throws RequestNotYetReadException {
+		if (enabledCapabilities == null) {
+			throw new RequestNotYetReadException();
+		}
+		return Collections.unmodifiableList(pushOptions);
+	}
+
+	/**
 	 * Set the configuration for push certificate verification.
 	 *
 	 * @param cfg
@@ -1076,6 +1134,10 @@ public void sendAdvertisedRefs(final RefAdvertiser adv)
 			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 @@ static ReceiveCommand parseCommand(String line) throws PackProtocolException {
 	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;
 
@@ -1205,6 +1269,17 @@ protected void enableCapabilities() {
 	}
 
 	/**
+	 * 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.
 	 *
 	 * @param name
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 efde062..2031147 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 5cea882..5590c2d 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.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<String> pushOptions;
+
 	/**
 	 * Create process for specified transport and refs updates specification.
 	 *
@@ -122,6 +126,7 @@ class PushProcess {
 		this.transport = transport;
 		this.toPush = new HashMap<String, RemoteRefUpdate>();
 		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 @@ private void updateTrackingRefs() {
 			}
 		}
 	}
+
+	/**
+	 * Gets the list of option strings associated with this push.
+	 *
+	 * @return pushOptions
+	 * @since 4.5
+	 */
+	public List<String> 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 2477806..d16b723 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 @@ protected void enableCapabilities() {
 		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 @@ private void service() throws IOException {
 			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 862b3bd..bc4843a 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 @@ private static String findTrackingRefName(final String remoteName,
 	/** Assists with authentication the connection. */
 	private CredentialsProvider credentialsProvider;
 
+	/** The option strings associated with the push operation. */
+	private List<String> pushOptions;
+
 	private PrintStream hookOutRedirect;
 
 	private PrePushHook prePush;
@@ -1121,6 +1124,25 @@ public CredentialsProvider getCredentialsProvider() {
 	}
 
 	/**
+	 * @return the option strings associated with the push operation
+	 * @since 4.5
+	 */
+	public List<String> 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<String> pushOptions) {
+		this.pushOptions = pushOptions;
+	}
+
+	/**
 	 * Fetch objects and refs from the remote repository to the local one.
 	 * <p>
 	 * This is a utility function providing standard fetch behavior. Local