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: I15083fbe48447678e034afeffb4639572a32f50ctags/v4.2.0.201601211800-r
@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; | ||||
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) { |
/* | |||||
* 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; | |||||
} | |||||
} |
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 |
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; | ||||
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); | ||||
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 | ||||
*/ | */ |
/***/ 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; |
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; | ||||
/** | /** | ||||
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. | ||||
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>(); | ||||
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>(); | ||||
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(); | 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 { | ||||
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 cur = refs.get(name); | ||||
Ref toCompare = cur; | Ref toCompare = cur; | ||||
if (toCompare != null) { | if (toCompare != null) { | ||||
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) | |||||
return refs.putIfAbsent(name, newRef) == null; | |||||
if (oldRef.getStorage() == Storage.NEW) | |||||
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 { | ||||
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) { | private boolean eq(Ref a, Ref b) { |
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; | ||||
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. */ | ||||
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, | ||||
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); | ||||
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); |
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; | ||||
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()); | ||||
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; | ||||
} | } | ||||
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) |
/** 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; | ||||
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. |