Support atomic push in JGit client

This should mirror the behavior of `git push --atomic` where the
client asks the server to apply all-or-nothing. Some JGit servers
already support this based on a custom DFS backend. InMemoryRepository
is extended to support atomic push for unit testing purposes.

Local disk server side support inside of JGit is a more complex animal
due to the excessive amount of file locking required to protect every
reference as a loose reference.

Change-Id: I15083fbe48447678e034afeffb4639572a32f50c
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 1879ef5..9098c12 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
@@ -82,6 +82,9 @@ class Push extends TextBuiltin {
 	@Option(name = "--all")
 	private boolean all;
 
+	@Option(name = "--atomic")
+	private boolean atomic;
+
 	@Option(name = "--tags")
 	private boolean tags;
 
@@ -122,6 +125,7 @@ protected void run() throws Exception {
 				push.setPushTags();
 			push.setRemote(remote);
 			push.setThin(thin);
+			push.setAtomic(atomic);
 			push.setTimeout(timeout);
 			Iterable<PushResult> results = push.call();
 			for (PushResult result : results) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
new file mode 100644
index 0000000..782e414
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2015, 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.assertSame;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+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.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 AtomicPushTest {
+	private URIish uri;
+	private TestProtocol<Object> testProtocol;
+	private Object ctx = new Object();
+	private InMemoryRepository server;
+	private InMemoryRepository client;
+	private ObjectId obj1;
+	private ObjectId obj2;
+
+	@Before
+	public void setUp() throws Exception {
+		server = newRepo("server");
+		client = newRepo("client");
+		testProtocol = new TestProtocol<>(
+				null,
+				new ReceivePackFactory<Object>() {
+					@Override
+					public ReceivePack create(Object req, Repository db)
+							throws ServiceNotEnabledException,
+							ServiceNotAuthorizedException {
+						return new ReceivePack(db);
+					}
+				});
+		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() {
+		Transport.unregister(testProtocol);
+	}
+
+	private static InMemoryRepository newRepo(String name) {
+		return new InMemoryRepository(new DfsRepositoryDescription(name));
+	}
+
+	@Test
+	public void pushNonAtomic() throws Exception {
+		PushResult r;
+		server.setPerformsAtomicTransactions(false);
+		Transport tn = testProtocol.open(uri, client, "server");
+		try {
+			tn.setPushAtomic(false);
+			r = tn.push(NullProgressMonitor.INSTANCE, commands());
+		} finally {
+			tn.close();
+		}
+
+		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());
+	}
+
+	@Test
+	public void pushAtomicClientGivesUpEarly() throws Exception {
+		PushResult r;
+		Transport tn = testProtocol.open(uri, client, "server");
+		try {
+			tn.setPushAtomic(true);
+			r = tn.push(NullProgressMonitor.INSTANCE, commands());
+		} finally {
+			tn.close();
+		}
+
+		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(JGitText.get().transactionAborted, one.getMessage());
+	}
+
+	@Test
+	public void pushAtomicDisabled() throws Exception {
+		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 */,
+				ObjectId.zeroId()));
+
+		server.setPerformsAtomicTransactions(false);
+		Transport tn = testProtocol.open(uri, client, "server");
+		try {
+			tn.setPushAtomic(true);
+			tn.push(NullProgressMonitor.INSTANCE, cmds);
+			fail("did not throw TransportException");
+		} catch (TransportException e) {
+			assertEquals(
+					uri + ": " + JGitText.get().atomicPushNotSupported,
+					e.getMessage());
+		} finally {
+			tn.close();
+		}
+	}
+
+	private List<RemoteRefUpdate> commands() 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 */,
+				obj1));
+		return cmds;
+	}
+}
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 d0e1c77..0e9b0b5 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -20,6 +20,7 @@
 atLeastOnePathIsRequired=At least one path is required.
 atLeastOnePatternIsRequired=At least one pattern is required.
 atLeastTwoFiltersNeeded=At least two filters needed.
+atomicPushNotSupported=Atomic push not supported.
 authenticationNotSupported=authentication not supported
 badBase64InputCharacterAt=Bad Base64 input character at {0} : {1} (decimal)
 badEntryDelimiter=Bad entry delimiter
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 227e322..f5b82bd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java
@@ -89,9 +89,8 @@ public class PushCommand extends
 	private String receivePack = RemoteConfig.DEFAULT_RECEIVE_PACK;
 
 	private boolean dryRun;
-
+	private boolean atomic;
 	private boolean force;
-
 	private boolean thin = Transport.DEFAULT_PUSH_THIN;
 
 	private OutputStream out;
@@ -145,6 +144,7 @@ public Iterable<PushResult> call() throws GitAPIException,
 			transports = Transport.openAll(repo, remote, Transport.Operation.PUSH);
 			for (final Transport transport : transports) {
 				transport.setPushThin(thin);
+				transport.setPushAtomic(atomic);
 				if (receivePack != null)
 					transport.setOptionReceivePack(receivePack);
 				transport.setDryRun(dryRun);
@@ -397,6 +397,29 @@ public PushCommand setThin(boolean thin) {
 	}
 
 	/**
+	 * @return true if all-or-nothing behavior is requested.
+	 * @since 4.2
+	 */
+	public boolean isAtomic() {
+		return atomic;
+	}
+
+	/**
+	 * Requests atomic push (all references updated, or no updates).
+	 *
+	 * Default setting is false.
+	 *
+	 * @param atomic
+	 * @return {@code this}
+	 * @since 4.2
+	 */
+	public PushCommand setAtomic(boolean atomic) {
+		checkCallable();
+		this.atomic = atomic;
+		return this;
+	}
+
+	/**
 	 * @return the force preference for push operation
 	 */
 	public boolean isForce() {
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 f6fd8a3..796eaae 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -79,6 +79,7 @@ public static JGitText get() {
 	/***/ public String atLeastOnePathIsRequired;
 	/***/ public String atLeastOnePatternIsRequired;
 	/***/ public String atLeastTwoFiltersNeeded;
+	/***/ public String atomicPushNotSupported;
 	/***/ public String authenticationNotSupported;
 	/***/ public String badBase64InputCharacterAt;
 	/***/ public String badEntryDelimiter;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
index 832e4fb..1c664b4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
@@ -13,14 +13,22 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Ref.Storage;
 import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.RefList;
 
 /**
@@ -46,8 +54,8 @@ public InMemoryRepository build() throws IOException {
 	static final AtomicInteger packId = new AtomicInteger();
 
 	private final DfsObjDatabase objdb;
-
 	private final DfsRefDatabase refdb;
+	private boolean performsAtomicTransactions = true;
 
 	/**
 	 * Initialize a new in-memory repository.
@@ -76,6 +84,17 @@ public DfsRefDatabase getRefDatabase() {
 		return refdb;
 	}
 
+	/**
+	 * Enable (or disable) the atomic reference transaction support.
+	 * <p>
+	 * Useful for testing atomic support enabled or disabled.
+	 *
+	 * @param atomic
+	 */
+	public void setPerformsAtomicTransactions(boolean atomic) {
+		performsAtomicTransactions = atomic;
+	}
+
 	private class MemObjDatabase extends DfsObjDatabase {
 		private List<DfsPackDescription> packs = new ArrayList<DfsPackDescription>();
 
@@ -235,41 +254,143 @@ public void setReadAheadBytes(int b) {
 
 	private class MemRefDatabase extends DfsRefDatabase {
 		private final ConcurrentMap<String, Ref> refs = new ConcurrentHashMap<String, Ref>();
+		private final ReadWriteLock lock = new ReentrantReadWriteLock(true /* fair */);
 
 		MemRefDatabase() {
 			super(InMemoryRepository.this);
 		}
 
 		@Override
+		public boolean performsAtomicTransactions() {
+			return performsAtomicTransactions;
+		}
+
+		@Override
+		public BatchRefUpdate newBatchUpdate() {
+			return new BatchRefUpdate(this) {
+				@Override
+				public void execute(RevWalk walk, ProgressMonitor monitor)
+						throws IOException {
+					if (performsAtomicTransactions()) {
+						try {
+							lock.writeLock().lock();
+							batch(walk, getCommands());
+						} finally {
+							lock.writeLock().unlock();
+						}
+					} else {
+						super.execute(walk, monitor);
+					}
+				}
+			};
+		}
+
+		@Override
 		protected RefCache scanAllRefs() throws IOException {
 			RefList.Builder<Ref> ids = new RefList.Builder<Ref>();
 			RefList.Builder<Ref> sym = new RefList.Builder<Ref>();
-			for (Ref ref : refs.values()) {
-				if (ref.isSymbolic())
-					sym.add(ref);
-				ids.add(ref);
+			try {
+				lock.readLock().lock();
+				for (Ref ref : refs.values()) {
+					if (ref.isSymbolic())
+						sym.add(ref);
+					ids.add(ref);
+				}
+			} finally {
+				lock.readLock().unlock();
 			}
 			ids.sort();
 			sym.sort();
 			return new RefCache(ids.toRefList(), sym.toRefList());
 		}
 
+		private void batch(RevWalk walk, List<ReceiveCommand> cmds) {
+			// Validate that the target exists in a new RevWalk, as the RevWalk
+			// from the RefUpdate might be reading back unflushed objects.
+			Map<ObjectId, ObjectId> peeled = new HashMap<>();
+			try (RevWalk rw = new RevWalk(getRepository())) {
+				for (ReceiveCommand c : cmds) {
+					if (!ObjectId.zeroId().equals(c.getNewId())) {
+						try {
+							RevObject o = rw.parseAny(c.getNewId());
+							if (o instanceof RevTag) {
+								peeled.put(o, rw.peel(o).copy());
+							}
+						} catch (IOException e) {
+							c.setResult(ReceiveCommand.Result.REJECTED_MISSING_OBJECT);
+							reject(cmds);
+							return;
+						}
+					}
+				}
+			}
+
+			// Check all references conform to expected old value.
+			for (ReceiveCommand c : cmds) {
+				Ref r = refs.get(c.getRefName());
+				if (r == null) {
+					if (c.getType() != ReceiveCommand.Type.CREATE) {
+						c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
+						reject(cmds);
+						return;
+					}
+				} else if (r.isSymbolic() || r.getObjectId() == null
+						|| !r.getObjectId().equals(c.getOldId())) {
+					c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
+					reject(cmds);
+					return;
+				}
+			}
+
+			// Write references.
+			for (ReceiveCommand c : cmds) {
+				if (c.getType() == ReceiveCommand.Type.DELETE) {
+					refs.remove(c.getRefName());
+					c.setResult(ReceiveCommand.Result.OK);
+					continue;
+				}
+
+				ObjectId p = peeled.get(c.getNewId());
+				Ref r;
+				if (p != null) {
+					r = new ObjectIdRef.PeeledTag(Storage.PACKED,
+							c.getRefName(), c.getNewId(), p);
+				} else {
+					r = new ObjectIdRef.PeeledNonTag(Storage.PACKED,
+							c.getRefName(), c.getNewId());
+				}
+				refs.put(r.getName(), r);
+				c.setResult(ReceiveCommand.Result.OK);
+			}
+			clearCache();
+		}
+
+		private void reject(List<ReceiveCommand> cmds) {
+			for (ReceiveCommand c : cmds) {
+				if (c.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
+					c.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON,
+							JGitText.get().transactionAborted);
+				}
+			}
+		}
+
 		@Override
 		protected boolean compareAndPut(Ref oldRef, Ref newRef)
 				throws IOException {
-			ObjectId id = newRef.getObjectId();
-			if (id != null) {
-				try (RevWalk rw = new RevWalk(getRepository())) {
-					// Validate that the target exists in a new RevWalk, as the RevWalk
-					// from the RefUpdate might be reading back unflushed objects.
-					rw.parseAny(id);
+			try {
+				lock.writeLock().lock();
+				ObjectId id = newRef.getObjectId();
+				if (id != null) {
+					try (RevWalk rw = new RevWalk(getRepository())) {
+						// Validate that the target exists in a new RevWalk, as the RevWalk
+						// from the RefUpdate might be reading back unflushed objects.
+						rw.parseAny(id);
+					}
 				}
-			}
-			String name = newRef.getName();
-			if (oldRef == null)
-				return refs.putIfAbsent(name, newRef) == null;
+				String name = newRef.getName();
+				if (oldRef == null)
+					return refs.putIfAbsent(name, newRef) == null;
 
-			synchronized (refs) {
 				Ref cur = refs.get(name);
 				Ref toCompare = cur;
 				if (toCompare != null) {
@@ -294,22 +415,29 @@ protected boolean compareAndPut(Ref oldRef, Ref newRef)
 					if (eq(toCompare, oldRef))
 						return refs.replace(name, cur, newRef);
 				}
+
+				if (oldRef.getStorage() == Storage.NEW)
+					return refs.putIfAbsent(name, newRef) == null;
+
+				return false;
+			} finally {
+				lock.writeLock().unlock();
 			}
-
-			if (oldRef.getStorage() == Storage.NEW)
-				return refs.putIfAbsent(name, newRef) == null;
-
-			return false;
 		}
 
 		@Override
 		protected boolean compareAndRemove(Ref oldRef) throws IOException {
-			String name = oldRef.getName();
-			Ref cur = refs.get(name);
-			if (cur != null && eq(cur, oldRef))
-				return refs.remove(name, cur);
-			else
-				return false;
+			try {
+				lock.writeLock().lock();
+				String name = oldRef.getName();
+				Ref cur = refs.get(name);
+				if (cur != null && eq(cur, oldRef))
+					return refs.remove(name, cur);
+				else
+					return false;
+			} finally {
+				lock.writeLock().unlock();
+			}
 		}
 
 		private boolean eq(Ref a, Ref b) {
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 24fb3be..f7bac6d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
@@ -44,6 +44,8 @@
 
 package org.eclipse.jgit.transport;
 
+import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_ATOMIC;
+
 import java.io.IOException;
 import java.io.OutputStream;
 import java.text.MessageFormat;
@@ -110,17 +112,15 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen
 	public static final String CAPABILITY_SIDE_BAND_64K = GitProtocolConstants.CAPABILITY_SIDE_BAND_64K;
 
 	private final boolean thinPack;
+	private final boolean atomic;
 
+	private boolean capableAtomic;
 	private boolean capableDeleteRefs;
-
 	private boolean capableReport;
-
 	private boolean capableSideBand;
-
 	private boolean capableOfsDelta;
 
 	private boolean sentCommand;
-
 	private boolean writePack;
 
 	/** Time in milliseconds spent transferring the pack data. */
@@ -135,6 +135,7 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen
 	public BasePackPushConnection(final PackTransport packTransport) {
 		super(packTransport);
 		thinPack = transport.isPushThin();
+		atomic = transport.isPushAtomic();
 	}
 
 	public void push(final ProgressMonitor monitor,
@@ -224,6 +225,11 @@ protected void doPush(final ProgressMonitor monitor,
 	private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
 			final ProgressMonitor monitor, OutputStream outputStream) throws IOException {
 		final String capabilities = enableCapabilities(monitor, outputStream);
+		if (atomic && !capableAtomic) {
+			throw new TransportException(uri,
+					JGitText.get().atomicPushNotSupported);
+		}
+
 		for (final RemoteRefUpdate rru : refUpdates) {
 			if (!capableDeleteRefs && rru.isDelete()) {
 				rru.setStatus(Status.REJECTED_NODELETE);
@@ -259,6 +265,8 @@ private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
 	private String enableCapabilities(final ProgressMonitor monitor,
 			OutputStream outputStream) {
 		final StringBuilder line = new StringBuilder();
+		if (atomic)
+			capableAtomic = wantCapability(line, CAPABILITY_ATOMIC);
 		capableReport = wantCapability(line, CAPABILITY_REPORT_STATUS);
 		capableDeleteRefs = wantCapability(line, CAPABILITY_DELETE_REFS);
 		capableOfsDelta = wantCapability(line, CAPABILITY_OFS_DELTA);
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 9721ee9..b557812 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
@@ -47,6 +47,7 @@
 import java.io.OutputStream;
 import java.text.MessageFormat;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -183,6 +184,7 @@ else if (!preprocessed.isEmpty())
 
 	private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
 			throws TransportException {
+		boolean atomic = transport.isPushAtomic();
 		final Map<String, RemoteRefUpdate> result = new HashMap<String, RemoteRefUpdate>();
 		for (final RemoteRefUpdate rru : toPush.values()) {
 			final Ref advertisedRef = connection.getRef(rru.getRemoteName());
@@ -205,6 +207,9 @@ private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
 			if (rru.isExpectingOldObjectId()
 					&& !rru.getExpectedOldObjectId().equals(advertisedOld)) {
 				rru.setStatus(Status.REJECTED_REMOTE_CHANGED);
+				if (atomic) {
+					return rejectAll();
+				}
 				continue;
 			}
 
@@ -236,14 +241,28 @@ private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
 						JGitText.get().readingObjectsFromLocalRepositoryFailed, x.getMessage()), x);
 			}
 			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;
 	}
 
+	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())
 			if (rru.getStatus() == Status.NOT_ATTEMPTED)
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 cc7db47..6af153c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
@@ -752,6 +752,9 @@ private static String findTrackingRefName(final String remoteName,
 	/** Should push produce thin-pack when sending objects to remote repository. */
 	private boolean pushThin = DEFAULT_PUSH_THIN;
 
+	/** Should push be all-or-nothing atomic behavior? */
+	private boolean pushAtomic;
+
 	/** Should push just check for operation result, not really push. */
 	private boolean dryRun;
 
@@ -970,6 +973,31 @@ public void setPushThin(final boolean pushThin) {
 	}
 
 	/**
+	 * Default setting is false.
+	 *
+	 * @return true if push requires all-or-nothing atomic behavior.
+	 * @since 4.2
+	 */
+	public boolean isPushAtomic() {
+		return pushAtomic;
+	}
+
+	/**
+	 * Request atomic push (all references succeed, or none do).
+	 * <p>
+	 * Server must also support atomic push. If the server does not support the
+	 * feature the push will abort without making changes.
+	 *
+	 * @param atomic
+	 *            true when push should be an all-or-nothing operation.
+	 * @see PackTransport
+	 * @since 4.2
+	 */
+	public void setPushAtomic(final boolean atomic) {
+		this.pushAtomic = atomic;
+	}
+
+	/**
 	 * @return true if destination refs should be removed if they no longer
 	 *         exist at the source repository.
 	 */