summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Pearce <spearce@spearce.org>2015-12-02 21:47:58 -0800
committerShawn Pearce <spearce@spearce.org>2015-12-02 22:06:06 -0800
commit3d8e6b1e16092701c31463092e945b8f00886bb7 (patch)
treed02684d25c4b68bf331ea3c7405df2ad4cac5b86
parentf109af47d07ecc20767f245a384ebd1dc201b6af (diff)
downloadjgit-3d8e6b1e16092701c31463092e945b8f00886bb7.tar.gz
jgit-3d8e6b1e16092701c31463092e945b8f00886bb7.zip
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
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Push.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java200
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java27
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java180
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java16
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java23
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java28
9 files changed, 446 insertions, 34 deletions
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 1879ef51ff..9098c1263d 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 @@ class Push extends TextBuiltin {
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 0000000000..782e414b62
--- /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 d0e1c779e4..0e9b0b59e6 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 @@ argumentIsNotAValidCommentString=Invalid comment: {0}
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 227e32236d..f5b82bdd7d 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 class PushCommand extends
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 class PushCommand extends
}
/**
+ * @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 f6fd8a396a..796eaaebf5 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 class JGitText extends TranslationBundle {
/***/ 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 832e4fb6a8..1c664b4097 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.Objects;
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 class InMemoryRepository extends DfsRepository {
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 class InMemoryRepository extends DfsRepository {
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 class InMemoryRepository extends DfsRepository {
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 @@ public class InMemoryRepository extends DfsRepository {
if (eq(toCompare, oldRef))
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
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 24fb3be64c..f7bac6d060 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 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen
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 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen
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 9721ee9eb0..b557812ad1 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.IOException;
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 @@ class PushProcess {
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 @@ class PushProcess {
if (rru.isExpectingOldObjectId()
&& !rru.getExpectedOldObjectId().equals(advertisedOld)) {
rru.setStatus(Status.REJECTED_REMOTE_CHANGED);
+ if (atomic) {
+ return rejectAll();
+ }
continue;
}
@@ -236,14 +241,28 @@ class PushProcess {
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 cc7db47df5..6af153cbc9 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 @@ public abstract class Transport {
/** 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 abstract class Transport {
}
/**
+ * 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.
*/
tion value='bugfix/noid/compatibility-with-30'>bugfix/noid/compatibility-with-30 Nextcloud server, a safe home for all your data: https://github.com/nextcloud/serverwww-data
summaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/l10n/sq.js
blob: bf9a63aa3d0e8a925fc45d4c699e884d21da2859 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
OC.L10N.register(
    "files_sharing",
    {
    "Shared with you" : "Të ndara me ju",
    "Nothing shared with you yet" : "Ende pa ndarë gjë me ju",
    "Files and folders others share with you will show up here" : "Këtu do të shfaqen kartelat dhe dosjet që të jerët ndajnë me ju",
    "Shared with others" : "Të ndara me të tjerët",
    "Nothing shared yet" : "Ende pa ndarë gjë",
    "Files and folders you share will show up here" : "Këtu do të shfaqen kartelat dhe dosjet që ndani me të tjerët",
    "Shared by link" : "Të ndara me lidhje",
    "No shared links" : "Pa lidhje ndarjesh",
    "Files and folders you share by link will show up here" : "Këtu do të shfaqen kartelat dhe dosjet që ndani përmes lidhjesh",
    "Deleted shares" : "Fshi shpërndarjet",
    "Shares" : "Shpërndarje",
    "Restore" : "Rikthe",
    "error" : "gabim",
    "Download" : "Shkarko",
    "Delete" : "Delete",
    "You can upload into this folder" : "Mund të ngarkoni te kjo dosje",
    "Terms of service" : "Termat e shërbimit",
    "No compatible server found at {remote}" : "Asnjë server i pajtueshëm nuk është gjetur tek { }",
    "Invalid server URL" : "Server i pavlefshëm URL",
    "Failed to add the public link to your Nextcloud" : "Nuk arritët të shtoni ndërlidhjen publike tek Nextcloud juaj",
    "No expiration date set" : "Nuk është vendosur data e përfundimit",
    "Shared by" : "Ndarë nga",
    "File shares" : "Ndarja e skedarëve",
    "Downloaded via public link" : "Shkarkuar nga një link publik",
    "Downloaded by {email}" : "Shkarkuar nga {email}",
    "{file} downloaded via public link" : "{file} shkarkuar përmes një lidhjeje publike",
    "{email} downloaded {file}" : "{email} shkarkoi {file}",
    "Shared with group {group}" : "U nda me grupin {group}",
    "Removed share for group {group}" : "Hoqi ndarjen për grupin {group}",
    "{actor} shared with group {group}" : "{actor} u nda me grupin {group}",
    "{actor} removed share for group {group}" : "{actor} hoqi ndarjen për grupin {group}",
    "You shared {file} with group {group}" : "Ndatë {file} me grupin {group}",
    "You removed group {group} from {file}" : "Hoqët grupin {group} nga {file}",
    "{actor} shared {file} with group {group}" : "{actor} ndau {file} me grupin {group}",
    "{actor} removed group {group} from {file}" : "{actor} hoqi grupin {group} nga {file}",
    "Shared as public link" : "U nda si një lidhje publike",
    "Removed public link" : "Hoqi lidhje publike",
    "Public link expired" : "Lidhja publike skadoi",
    "{actor} shared as public link" : "{actor} u nda si një lidhje publike",
    "{actor} removed public link" : "{actor} hoqi lidhje publike",
    "Public link of {actor} expired" : "Lidhja publike e {actor} skadoi",
    "You shared {file} as public link" : "Ndatë {file} si një lidhje publike",
    "You removed public link for {file}" : "Hoqët lidhje publike për {file}",
    "Public link expired for {file}" : "Lidhja publike skadoi për {file}",
    "{actor} shared {file} as public link" : "{actor} u nda {file} si një lidhje publike",
    "{actor} removed public link for {file}" : "{actor} hoqi lidhje publike për {file}",
    "Public link of {actor} for {file} expired" : "Lidhja publike e {actor} për {file} ka skaduar",
    "{user} accepted the remote share" : "{user} pranoi ndarjen e largët",
    "{user} declined the remote share" : "{user} hodhi tej ndarjen e largët",
    "You received a new remote share {file} from {user}" : "Morët një ndarje të largët {file} nga {user}",
    "{user} accepted the remote share of {file}" : "{user} pranoi ndarjen e largët të {file}",
    "{user} declined the remote share of {file}" : "{user} hodhi tej ndarjen e largët të {file}",
    "{user} unshared {file} from you" : "{user} nuk ndau {file} nga ju",
    "Shared with {user}" : "Ndarë me {user}",
    "Removed share for {user}" : "Hoqi ndarjen për {user}",
    "{actor} shared with {user}" : "{aktori} ndau me {përdoruesin}",
    "{actor} removed share for {user}" : "{actor} hoqi ndarjen për {user}",
    "Shared by {actor}" : "U nda nga {actor}",
    "{actor} removed share" : "{actor} hoqi ndarjen",
    "You shared {file} with {user}" : "Ndatë {file} me {user}",
    "You removed {user} from {file}" : "Hoqët {user} nga {file}",
    "{actor} shared {file} with {user}" : "{aktori} ndau {skedarë} me {përdoruesin}",
    "{actor} removed {user} from {file}" : "{actor} hoqi {user} nga {file}",
    "{actor} shared {file} with you" : "{actor} ndau {skedarë} me ju",
    "A file or folder shared by mail or by public link was <strong>downloaded</strong>" : "Një skedar ose dosje e ndarë nga posta ose lidhja publike ishte <strong> shkarkuar</strong>",
    "A file or folder was shared from <strong>another server</strong>" : "Një kartelë ose dosje u nda prej një <strong>shërbyesi tjetër</strong>",
    "A file or folder has been <strong>shared</strong>" : "U <strong>nda me të tjerë</strong> një kartelë ose dosje",
    "Wrong share ID, share doesn't exist" : "ID e gabuar ndarjeje, ndarja s’ekziston",
    "Could not delete share" : "Ndarja s’u fshi dot",
    "Please specify a file or folder path" : "Ju lutemi, tregoni një shteg kartele ose dosjeje",
    "Wrong path, file/folder doesn't exist" : "Shteg i gabuar, kratela/dosja s’ekziston",
    "Could not create share" : "Ndarja nuk u krijua dot",
    "invalid permissions" : "leje e pavlefshme",
    "Please specify a valid user" : "Ju lutemi, tregoni një përdorues të vlefshëm",
    "Group sharing is disabled by the administrator" : "Ndarja në grup është çaktivizuar nga përgjegjësi",
    "Please specify a valid group" : "Ju lutemi, tregoni një grup të vlefshëm",
    "Public link sharing is disabled by the administrator" : "Ndarja e lidhjeve publike është çaktivizuar nga përgjegjësi",
    "Public upload disabled by the administrator" : "Ngarkimi publik është çaktivizuar nga përgjegjësi",
    "Public upload is only possible for publicly shared folders" : "Ngarkimi publik është i mundshëm vetëm për dosje të ndara publikisht",
    "Invalid date, date format must be YYYY-MM-DD" : "Datë e pavlefshme, formati i datës duhet të jetë VVVV-MM-DD",
    "You cannot share to a Circle if the app is not enabled" : "Nuk mund të shpërndani në një rreth nëse aplikacioni nuk është i aktivizuar",
    "Please specify a valid circle" : "Ju lutem specifikoni një rreth i vlefshëm",
    "Unknown share type" : "Lloj i panjohur ndarjesh",
    "Not a directory" : "S’është drejtori",
    "Could not lock path" : "S’u kyç dot shtegu",
    "Wrong or no update parameter given" : "Ose u dha parametër i gabuar përditësimesh, pse s’u dha fare ",
    "Can't change permissions for public share links" : "S’mund të ndryshohen lejet për lidhje ndarjesh publike",
    "shared by %s" : "ndarë nga %s",
    "Direct link" : "Lidhje e drejtpërdrejtë",
    "Add to your Nextcloud" : "Shtojeni tek Nextcloud-i juaj",
    "Share API is disabled" : "API i ndarjeve është çaktivizuar",
    "File sharing" : "Shpërndarja e skedarëve",
    "Accept" : "Prano",
    "Sharing" : "Ndarje",
    "Allow editing" : "Lejo redaktimin",
    "Allow resharing" : "Lejo rindarje",
    "Set expiration date" : "Caktoni datë skadimi",
    "Note to recipient" : "Shënim për marrësin",
    "Unshare" : "Hiqe ndarjen",
    "group" : "grup",
    "conversation" : "bisedë",
    "remote" : "i largët",
    "guest" : "vizitor",
    "Link copied" : "Linku u kopjua",
    "Copy to clipboard" : "Kopjo në dërrasë ",
    "Only works for users with access to this folder" : "Punon vetëm për përdoruesit që kanë qasje në këtë dosje",
    "Password protection" : "Password protection",
    "Enter a password" : "Fusni një fjalëkalim",
    "Cancel" : "Anullo",
    "Read only" : "Vetëm i lexueshëm",
    "Allow upload and editing" : "Lejo ngarkim dhe editim",
    "File drop (upload only)" : "Lësho skedar (vetëm ngarkim)",
    "Hide download" : "Fshih shkarkimin",
    "Password protect" : "Mbroje me fjalëkalim",
    "Share link" : "Share link",
    "Resharing is not allowed" : "Nuk lejohet rishperndarja",
    "Shared" : "Ndarë",
    "Share" : "shpërndaj",
    "Shared with" : "Ndarë me",
    "Shared with you and the group {group} by {owner}" : "Ndarë me ju dhe me grupin {group} nga {owner}",
    "Shared with you by {owner}" : "Ndarë me ju nga {owner}",
    "No entries found in this folder" : "S’u gjetën zëra në këtë dosje",
    "Name" : "Emër",
    "Share time" : "Kohë ndarjeje",
    "Expiration date" : "Datë skadimi",
    "Sorry, this link doesn’t seem to work anymore." : "Na ndjeni, kjo lidhje duket se nuk funksionon më.",
    "Reasons might be:" : "Arsyet mund të jenë:",
    "the item was removed" : "objekti është hequr",
    "the link expired" : "lidhja ka skaduar",
    "sharing is disabled" : "ndarjet janë çaktivizuar",
    "For more info, please ask the person who sent this link." : "Për më shumë të dhëna, ju lutemi, pyetni personin që ju dërgoi këtë lidhje.",
    "Download %s" : "Shkarko %s",
    "Upload files to %s" : "Ngrako skedarët tek %s",
    "Note" : "Shënim",
    "Select or drop files" : "Përzgjidh ose hiq skedarët",
    "Uploaded files:" : "Skedarët e ngarkuar:",
    "could not delete share" : "ndarja s’u fshi dot",
    "Uploading files…" : "Skedarët po ngarkohen..."
},
"nplurals=2; plural=(n != 1);");