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
This commit is contained in:
parent
f109af47d0
commit
3d8e6b1e16
|
@ -82,6 +82,9 @@ class Push extends TextBuiltin {
|
||||||
@Option(name = "--all")
|
@Option(name = "--all")
|
||||||
private boolean all;
|
private boolean all;
|
||||||
|
|
||||||
|
@Option(name = "--atomic")
|
||||||
|
private boolean atomic;
|
||||||
|
|
||||||
@Option(name = "--tags")
|
@Option(name = "--tags")
|
||||||
private boolean tags;
|
private boolean tags;
|
||||||
|
|
||||||
|
@ -122,6 +125,7 @@ protected void run() throws Exception {
|
||||||
push.setPushTags();
|
push.setPushTags();
|
||||||
push.setRemote(remote);
|
push.setRemote(remote);
|
||||||
push.setThin(thin);
|
push.setThin(thin);
|
||||||
|
push.setAtomic(atomic);
|
||||||
push.setTimeout(timeout);
|
push.setTimeout(timeout);
|
||||||
Iterable<PushResult> results = push.call();
|
Iterable<PushResult> results = push.call();
|
||||||
for (PushResult result : results) {
|
for (PushResult result : results) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ argumentIsNotAValidCommentString=Invalid comment: {0}
|
||||||
atLeastOnePathIsRequired=At least one path is required.
|
atLeastOnePathIsRequired=At least one path is required.
|
||||||
atLeastOnePatternIsRequired=At least one pattern is required.
|
atLeastOnePatternIsRequired=At least one pattern is required.
|
||||||
atLeastTwoFiltersNeeded=At least two filters needed.
|
atLeastTwoFiltersNeeded=At least two filters needed.
|
||||||
|
atomicPushNotSupported=Atomic push not supported.
|
||||||
authenticationNotSupported=authentication not supported
|
authenticationNotSupported=authentication not supported
|
||||||
badBase64InputCharacterAt=Bad Base64 input character at {0} : {1} (decimal)
|
badBase64InputCharacterAt=Bad Base64 input character at {0} : {1} (decimal)
|
||||||
badEntryDelimiter=Bad entry delimiter
|
badEntryDelimiter=Bad entry delimiter
|
||||||
|
|
|
@ -89,9 +89,8 @@ public class PushCommand extends
|
||||||
private String receivePack = RemoteConfig.DEFAULT_RECEIVE_PACK;
|
private String receivePack = RemoteConfig.DEFAULT_RECEIVE_PACK;
|
||||||
|
|
||||||
private boolean dryRun;
|
private boolean dryRun;
|
||||||
|
private boolean atomic;
|
||||||
private boolean force;
|
private boolean force;
|
||||||
|
|
||||||
private boolean thin = Transport.DEFAULT_PUSH_THIN;
|
private boolean thin = Transport.DEFAULT_PUSH_THIN;
|
||||||
|
|
||||||
private OutputStream out;
|
private OutputStream out;
|
||||||
|
@ -145,6 +144,7 @@ public Iterable<PushResult> call() throws GitAPIException,
|
||||||
transports = Transport.openAll(repo, remote, Transport.Operation.PUSH);
|
transports = Transport.openAll(repo, remote, Transport.Operation.PUSH);
|
||||||
for (final Transport transport : transports) {
|
for (final Transport transport : transports) {
|
||||||
transport.setPushThin(thin);
|
transport.setPushThin(thin);
|
||||||
|
transport.setPushAtomic(atomic);
|
||||||
if (receivePack != null)
|
if (receivePack != null)
|
||||||
transport.setOptionReceivePack(receivePack);
|
transport.setOptionReceivePack(receivePack);
|
||||||
transport.setDryRun(dryRun);
|
transport.setDryRun(dryRun);
|
||||||
|
@ -396,6 +396,29 @@ public PushCommand setThin(boolean thin) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
* @return the force preference for push operation
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -79,6 +79,7 @@ public static JGitText get() {
|
||||||
/***/ public String atLeastOnePathIsRequired;
|
/***/ public String atLeastOnePathIsRequired;
|
||||||
/***/ public String atLeastOnePatternIsRequired;
|
/***/ public String atLeastOnePatternIsRequired;
|
||||||
/***/ public String atLeastTwoFiltersNeeded;
|
/***/ public String atLeastTwoFiltersNeeded;
|
||||||
|
/***/ public String atomicPushNotSupported;
|
||||||
/***/ public String authenticationNotSupported;
|
/***/ public String authenticationNotSupported;
|
||||||
/***/ public String badBase64InputCharacterAt;
|
/***/ public String badBase64InputCharacterAt;
|
||||||
/***/ public String badEntryDelimiter;
|
/***/ public String badEntryDelimiter;
|
||||||
|
|
|
@ -13,14 +13,22 @@
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
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.internal.storage.pack.PackExt;
|
||||||
|
import org.eclipse.jgit.lib.BatchRefUpdate;
|
||||||
import org.eclipse.jgit.lib.ObjectId;
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
import org.eclipse.jgit.lib.ObjectIdRef;
|
import org.eclipse.jgit.lib.ObjectIdRef;
|
||||||
|
import org.eclipse.jgit.lib.ProgressMonitor;
|
||||||
import org.eclipse.jgit.lib.Ref;
|
import org.eclipse.jgit.lib.Ref;
|
||||||
import org.eclipse.jgit.lib.Ref.Storage;
|
import org.eclipse.jgit.lib.Ref.Storage;
|
||||||
import org.eclipse.jgit.lib.SymbolicRef;
|
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.revwalk.RevWalk;
|
||||||
|
import org.eclipse.jgit.transport.ReceiveCommand;
|
||||||
import org.eclipse.jgit.util.RefList;
|
import org.eclipse.jgit.util.RefList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,8 +54,8 @@ public InMemoryRepository build() throws IOException {
|
||||||
static final AtomicInteger packId = new AtomicInteger();
|
static final AtomicInteger packId = new AtomicInteger();
|
||||||
|
|
||||||
private final DfsObjDatabase objdb;
|
private final DfsObjDatabase objdb;
|
||||||
|
|
||||||
private final DfsRefDatabase refdb;
|
private final DfsRefDatabase refdb;
|
||||||
|
private boolean performsAtomicTransactions = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a new in-memory repository.
|
* Initialize a new in-memory repository.
|
||||||
|
@ -76,6 +84,17 @@ public DfsRefDatabase getRefDatabase() {
|
||||||
return refdb;
|
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 class MemObjDatabase extends DfsObjDatabase {
|
||||||
private List<DfsPackDescription> packs = new ArrayList<DfsPackDescription>();
|
private List<DfsPackDescription> packs = new ArrayList<DfsPackDescription>();
|
||||||
|
|
||||||
|
@ -235,28 +254,131 @@ public void setReadAheadBytes(int b) {
|
||||||
|
|
||||||
private class MemRefDatabase extends DfsRefDatabase {
|
private class MemRefDatabase extends DfsRefDatabase {
|
||||||
private final ConcurrentMap<String, Ref> refs = new ConcurrentHashMap<String, Ref>();
|
private final ConcurrentMap<String, Ref> refs = new ConcurrentHashMap<String, Ref>();
|
||||||
|
private final ReadWriteLock lock = new ReentrantReadWriteLock(true /* fair */);
|
||||||
|
|
||||||
MemRefDatabase() {
|
MemRefDatabase() {
|
||||||
super(InMemoryRepository.this);
|
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
|
@Override
|
||||||
protected RefCache scanAllRefs() throws IOException {
|
protected RefCache scanAllRefs() throws IOException {
|
||||||
RefList.Builder<Ref> ids = new RefList.Builder<Ref>();
|
RefList.Builder<Ref> ids = new RefList.Builder<Ref>();
|
||||||
RefList.Builder<Ref> sym = new RefList.Builder<Ref>();
|
RefList.Builder<Ref> sym = new RefList.Builder<Ref>();
|
||||||
|
try {
|
||||||
|
lock.readLock().lock();
|
||||||
for (Ref ref : refs.values()) {
|
for (Ref ref : refs.values()) {
|
||||||
if (ref.isSymbolic())
|
if (ref.isSymbolic())
|
||||||
sym.add(ref);
|
sym.add(ref);
|
||||||
ids.add(ref);
|
ids.add(ref);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
lock.readLock().unlock();
|
||||||
|
}
|
||||||
ids.sort();
|
ids.sort();
|
||||||
sym.sort();
|
sym.sort();
|
||||||
return new RefCache(ids.toRefList(), sym.toRefList());
|
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
|
@Override
|
||||||
protected boolean compareAndPut(Ref oldRef, Ref newRef)
|
protected boolean compareAndPut(Ref oldRef, Ref newRef)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
try {
|
||||||
|
lock.writeLock().lock();
|
||||||
ObjectId id = newRef.getObjectId();
|
ObjectId id = newRef.getObjectId();
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
try (RevWalk rw = new RevWalk(getRepository())) {
|
try (RevWalk rw = new RevWalk(getRepository())) {
|
||||||
|
@ -269,7 +391,6 @@ protected boolean compareAndPut(Ref oldRef, Ref newRef)
|
||||||
if (oldRef == null)
|
if (oldRef == null)
|
||||||
return refs.putIfAbsent(name, newRef) == null;
|
return refs.putIfAbsent(name, newRef) == null;
|
||||||
|
|
||||||
synchronized (refs) {
|
|
||||||
Ref cur = refs.get(name);
|
Ref cur = refs.get(name);
|
||||||
Ref toCompare = cur;
|
Ref toCompare = cur;
|
||||||
if (toCompare != null) {
|
if (toCompare != null) {
|
||||||
|
@ -294,22 +415,29 @@ protected boolean compareAndPut(Ref oldRef, Ref newRef)
|
||||||
if (eq(toCompare, oldRef))
|
if (eq(toCompare, oldRef))
|
||||||
return refs.replace(name, cur, newRef);
|
return refs.replace(name, cur, newRef);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (oldRef.getStorage() == Storage.NEW)
|
if (oldRef.getStorage() == Storage.NEW)
|
||||||
return refs.putIfAbsent(name, newRef) == null;
|
return refs.putIfAbsent(name, newRef) == null;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
} finally {
|
||||||
|
lock.writeLock().unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean compareAndRemove(Ref oldRef) throws IOException {
|
protected boolean compareAndRemove(Ref oldRef) throws IOException {
|
||||||
|
try {
|
||||||
|
lock.writeLock().lock();
|
||||||
String name = oldRef.getName();
|
String name = oldRef.getName();
|
||||||
Ref cur = refs.get(name);
|
Ref cur = refs.get(name);
|
||||||
if (cur != null && eq(cur, oldRef))
|
if (cur != null && eq(cur, oldRef))
|
||||||
return refs.remove(name, cur);
|
return refs.remove(name, cur);
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
|
} finally {
|
||||||
|
lock.writeLock().unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean eq(Ref a, Ref b) {
|
private boolean eq(Ref a, Ref b) {
|
||||||
|
|
|
@ -44,6 +44,8 @@
|
||||||
|
|
||||||
package org.eclipse.jgit.transport;
|
package org.eclipse.jgit.transport;
|
||||||
|
|
||||||
|
import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_ATOMIC;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.text.MessageFormat;
|
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;
|
public static final String CAPABILITY_SIDE_BAND_64K = GitProtocolConstants.CAPABILITY_SIDE_BAND_64K;
|
||||||
|
|
||||||
private final boolean thinPack;
|
private final boolean thinPack;
|
||||||
|
private final boolean atomic;
|
||||||
|
|
||||||
|
private boolean capableAtomic;
|
||||||
private boolean capableDeleteRefs;
|
private boolean capableDeleteRefs;
|
||||||
|
|
||||||
private boolean capableReport;
|
private boolean capableReport;
|
||||||
|
|
||||||
private boolean capableSideBand;
|
private boolean capableSideBand;
|
||||||
|
|
||||||
private boolean capableOfsDelta;
|
private boolean capableOfsDelta;
|
||||||
|
|
||||||
private boolean sentCommand;
|
private boolean sentCommand;
|
||||||
|
|
||||||
private boolean writePack;
|
private boolean writePack;
|
||||||
|
|
||||||
/** Time in milliseconds spent transferring the pack data. */
|
/** Time in milliseconds spent transferring the pack data. */
|
||||||
|
@ -135,6 +135,7 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen
|
||||||
public BasePackPushConnection(final PackTransport packTransport) {
|
public BasePackPushConnection(final PackTransport packTransport) {
|
||||||
super(packTransport);
|
super(packTransport);
|
||||||
thinPack = transport.isPushThin();
|
thinPack = transport.isPushThin();
|
||||||
|
atomic = transport.isPushAtomic();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void push(final ProgressMonitor monitor,
|
public void push(final ProgressMonitor monitor,
|
||||||
|
@ -224,6 +225,11 @@ protected void doPush(final ProgressMonitor monitor,
|
||||||
private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
|
private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
|
||||||
final ProgressMonitor monitor, OutputStream outputStream) throws IOException {
|
final ProgressMonitor monitor, OutputStream outputStream) throws IOException {
|
||||||
final String capabilities = enableCapabilities(monitor, outputStream);
|
final String capabilities = enableCapabilities(monitor, outputStream);
|
||||||
|
if (atomic && !capableAtomic) {
|
||||||
|
throw new TransportException(uri,
|
||||||
|
JGitText.get().atomicPushNotSupported);
|
||||||
|
}
|
||||||
|
|
||||||
for (final RemoteRefUpdate rru : refUpdates) {
|
for (final RemoteRefUpdate rru : refUpdates) {
|
||||||
if (!capableDeleteRefs && rru.isDelete()) {
|
if (!capableDeleteRefs && rru.isDelete()) {
|
||||||
rru.setStatus(Status.REJECTED_NODELETE);
|
rru.setStatus(Status.REJECTED_NODELETE);
|
||||||
|
@ -259,6 +265,8 @@ private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
|
||||||
private String enableCapabilities(final ProgressMonitor monitor,
|
private String enableCapabilities(final ProgressMonitor monitor,
|
||||||
OutputStream outputStream) {
|
OutputStream outputStream) {
|
||||||
final StringBuilder line = new StringBuilder();
|
final StringBuilder line = new StringBuilder();
|
||||||
|
if (atomic)
|
||||||
|
capableAtomic = wantCapability(line, CAPABILITY_ATOMIC);
|
||||||
capableReport = wantCapability(line, CAPABILITY_REPORT_STATUS);
|
capableReport = wantCapability(line, CAPABILITY_REPORT_STATUS);
|
||||||
capableDeleteRefs = wantCapability(line, CAPABILITY_DELETE_REFS);
|
capableDeleteRefs = wantCapability(line, CAPABILITY_DELETE_REFS);
|
||||||
capableOfsDelta = wantCapability(line, CAPABILITY_OFS_DELTA);
|
capableOfsDelta = wantCapability(line, CAPABILITY_OFS_DELTA);
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -183,6 +184,7 @@ else if (!preprocessed.isEmpty())
|
||||||
|
|
||||||
private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
|
private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
|
||||||
throws TransportException {
|
throws TransportException {
|
||||||
|
boolean atomic = transport.isPushAtomic();
|
||||||
final Map<String, RemoteRefUpdate> result = new HashMap<String, RemoteRefUpdate>();
|
final Map<String, RemoteRefUpdate> result = new HashMap<String, RemoteRefUpdate>();
|
||||||
for (final RemoteRefUpdate rru : toPush.values()) {
|
for (final RemoteRefUpdate rru : toPush.values()) {
|
||||||
final Ref advertisedRef = connection.getRef(rru.getRemoteName());
|
final Ref advertisedRef = connection.getRef(rru.getRemoteName());
|
||||||
|
@ -205,6 +207,9 @@ private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
|
||||||
if (rru.isExpectingOldObjectId()
|
if (rru.isExpectingOldObjectId()
|
||||||
&& !rru.getExpectedOldObjectId().equals(advertisedOld)) {
|
&& !rru.getExpectedOldObjectId().equals(advertisedOld)) {
|
||||||
rru.setStatus(Status.REJECTED_REMOTE_CHANGED);
|
rru.setStatus(Status.REJECTED_REMOTE_CHANGED);
|
||||||
|
if (atomic) {
|
||||||
|
return rejectAll();
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,14 +241,28 @@ private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
|
||||||
JGitText.get().readingObjectsFromLocalRepositoryFailed, x.getMessage()), x);
|
JGitText.get().readingObjectsFromLocalRepositoryFailed, x.getMessage()), x);
|
||||||
}
|
}
|
||||||
rru.setFastForward(fastForward);
|
rru.setFastForward(fastForward);
|
||||||
if (!fastForward && !rru.isForceUpdate())
|
if (!fastForward && !rru.isForceUpdate()) {
|
||||||
rru.setStatus(Status.REJECTED_NONFASTFORWARD);
|
rru.setStatus(Status.REJECTED_NONFASTFORWARD);
|
||||||
else
|
if (atomic) {
|
||||||
|
return rejectAll();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
result.put(rru.getRemoteName(), rru);
|
result.put(rru.getRemoteName(), rru);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result;
|
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() {
|
private void modifyUpdatesForDryRun() {
|
||||||
for (final RemoteRefUpdate rru : toPush.values())
|
for (final RemoteRefUpdate rru : toPush.values())
|
||||||
if (rru.getStatus() == Status.NOT_ATTEMPTED)
|
if (rru.getStatus() == Status.NOT_ATTEMPTED)
|
||||||
|
|
|
@ -752,6 +752,9 @@ private static String findTrackingRefName(final String remoteName,
|
||||||
/** Should push produce thin-pack when sending objects to remote repository. */
|
/** Should push produce thin-pack when sending objects to remote repository. */
|
||||||
private boolean pushThin = DEFAULT_PUSH_THIN;
|
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. */
|
/** Should push just check for operation result, not really push. */
|
||||||
private boolean dryRun;
|
private boolean dryRun;
|
||||||
|
|
||||||
|
@ -969,6 +972,31 @@ public void setPushThin(final boolean pushThin) {
|
||||||
this.pushThin = pushThin;
|
this.pushThin = 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
|
* @return true if destination refs should be removed if they no longer
|
||||||
* exist at the source repository.
|
* exist at the source repository.
|
||||||
|
|
Loading…
Reference in New Issue