diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java index 6479d157e..0884d7223 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java @@ -95,6 +95,53 @@ public void testForcedFetch() throws Exception { res.getTrackingRefUpdate("refs/heads/master").getResult()); } + @Test + public void testFetchSimpleNegativeRefSpec() throws Exception { + remoteGit.commit().setMessage("commit").call(); + + FetchResult res = git.fetch().setRemote("test") + .setRefSpecs("refs/heads/master:refs/heads/test", + "^:refs/heads/test") + .call(); + assertNull(res.getTrackingRefUpdate("refs/heads/test")); + + res = git.fetch().setRemote("test") + .setRefSpecs("refs/heads/master:refs/heads/test", + "^refs/heads/master") + .call(); + assertNull(res.getTrackingRefUpdate("refs/heads/test")); + } + + @Test + public void negativeRefSpecFilterBySource() throws Exception { + remoteGit.commit().setMessage("commit").call(); + remoteGit.branchCreate().setName("test").call(); + remoteGit.commit().setMessage("commit1").call(); + remoteGit.branchCreate().setName("dev").call(); + + FetchResult res = git.fetch().setRemote("test") + .setRefSpecs("refs/*:refs/origins/*", "^refs/*/test") + .call(); + assertNotNull(res.getTrackingRefUpdate("refs/origins/heads/master")); + assertNull(res.getTrackingRefUpdate("refs/origins/heads/test")); + assertNotNull(res.getTrackingRefUpdate("refs/origins/heads/dev")); + } + + @Test + public void negativeRefSpecFilterByDestination() throws Exception { + remoteGit.commit().setMessage("commit").call(); + remoteGit.branchCreate().setName("meta").call(); + remoteGit.commit().setMessage("commit1").call(); + remoteGit.branchCreate().setName("data").call(); + + FetchResult res = git.fetch().setRemote("test") + .setRefSpecs("refs/*:refs/secret/*", "^:refs/secret/*/meta") + .call(); + assertNotNull(res.getTrackingRefUpdate("refs/secret/heads/master")); + assertNull(res.getTrackingRefUpdate("refs/secret/heads/meta")); + assertNotNull(res.getTrackingRefUpdate("refs/secret/heads/data")); + } + @Test public void fetchAddsBranches() throws Exception { final String branch1 = "b1"; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java index b56308cb7..ef0817adb 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java @@ -443,6 +443,26 @@ public void invalidSetDestination() { a.setDestination("refs/remotes/origin/*/*"); } + @Test(expected = IllegalArgumentException.class) + public void invalidNegativeAndForce() { + assertNotNull(new RefSpec("^+refs/heads/master")); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidForceAndNegative() { + assertNotNull(new RefSpec("+^refs/heads/master")); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidNegativeNoSrcDest() { + assertNotNull(new RefSpec("^")); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidNegativeBothSrcDest() { + assertNotNull(new RefSpec("^refs/heads/*:refs/heads/*")); + } + @Test public void sourceOnlywithWildcard() { RefSpec a = new RefSpec("refs/heads/*", @@ -480,4 +500,32 @@ public void matchingForced() { assertTrue(a.isMatching()); assertTrue(a.isForceUpdate()); } + + @Test + public void negativeRefSpecWithDest() { + RefSpec a = new RefSpec("^:refs/readonly/*"); + assertTrue(a.isNegative()); + assertNull(a.getSource()); + assertEquals(a.getDestination(), "refs/readonly/*"); + } + + // Because of some of the API's existing behavior, without a colon at the + // end of the refspec, dest will be null. + @Test + public void negativeRefSpecWithSrcAndNullDest() { + RefSpec a = new RefSpec("^refs/testdata/*"); + assertTrue(a.isNegative()); + assertNull(a.getDestination()); + assertEquals(a.getSource(), "refs/testdata/*"); + } + + // Because of some of the API's existing behavior, with a colon at the end + // of the refspec, dest will be empty. + @Test + public void negativeRefSpecWithSrcAndEmptyDest() { + RefSpec a = new RefSpec("^refs/testdata/*:"); + assertTrue(a.isNegative()); + assertTrue(a.getDestination().isEmpty()); + assertEquals(a.getSource(), "refs/testdata/*"); + } } 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 e6f4e65e7..9f264fca3 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -393,6 +393,7 @@ invalidLineInConfigFileWithParam=Invalid line in config file: {0} invalidModeFor=Invalid mode {0} for {1} {2} in {3}. invalidModeForPath=Invalid mode {0} for path {1} invalidNameContainsDotDot=Invalid name (contains ".."): {0} +invalidNegativeAndForce= RefSpec can't be negative and forceful. invalidObject=Invalid {0} {1}: {2} invalidOldIdSent=invalid old id sent invalidPacketLineHeader=Invalid packet line header: {0} 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 16b3f372e..b81e605c1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -421,6 +421,7 @@ public static JGitText get() { /***/ public String invalidModeFor; /***/ public String invalidModeForPath; /***/ public String invalidNameContainsDotDot; + /***/ public String invalidNegativeAndForce; /***/ public String invalidObject; /***/ public String invalidOldIdSent; /***/ public String invalidPacketLineHeader; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java index 7d7b3ee0a..87e547603 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.NotSupportedException; @@ -56,6 +57,12 @@ class FetchProcess { /** List of things we want to fetch from the remote repository. */ private final Collection toFetch; + /** + * List of things we don't want to fetch from the remote repository or to + * the local repository. + */ + private final Collection negativeRefSpecs; + /** Set of refs we will actually wind up asking to obtain. */ private final HashMap askFor = new HashMap<>(); @@ -74,9 +81,12 @@ class FetchProcess { private Map localRefs; - FetchProcess(Transport t, Collection f) { + FetchProcess(Transport t, Collection refSpecs) { transport = t; - toFetch = f; + toFetch = refSpecs.stream().filter(refSpec -> !refSpec.isNegative()) + .collect(Collectors.toList()); + negativeRefSpecs = refSpecs.stream().filter(RefSpec::isNegative) + .collect(Collectors.toList()); } void execute(ProgressMonitor monitor, FetchResult result, @@ -389,8 +399,13 @@ private boolean askForIsComplete() throws TransportException { private void expandWildcard(RefSpec spec, Set matched) throws TransportException { for (Ref src : conn.getRefs()) { - if (spec.matchSource(src) && matched.add(src)) - want(src, spec.expandFromSource(src)); + if (spec.matchSource(src)) { + RefSpec expandedRefSpec = spec.expandFromSource(src); + if (!matchNegativeRefSpec(expandedRefSpec) + && matched.add(src)) { + want(src, expandedRefSpec); + } + } } } @@ -406,11 +421,27 @@ private void expandSingle(RefSpec spec, Set matched) if (src == null) { throw new TransportException(MessageFormat.format(JGitText.get().remoteDoesNotHaveSpec, want)); } - if (matched.add(src)) { + if (!matchNegativeRefSpec(spec) && matched.add(src)) { want(src, spec); } } + private boolean matchNegativeRefSpec(RefSpec spec) { + for (RefSpec negativeRefSpec : negativeRefSpecs) { + if (negativeRefSpec.getSource() != null && spec.getSource() != null + && negativeRefSpec.matchSource(spec.getSource())) { + return true; + } + + if (negativeRefSpec.getDestination() != null + && spec.getDestination() != null && negativeRefSpec + .matchDestination(spec.getDestination())) { + return true; + } + } + return false; + } + private boolean localHasObject(ObjectId id) throws TransportException { try { return transport.local.getObjectDatabase().has(id); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java index 56d0036a2..b0363147c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java @@ -53,6 +53,9 @@ public static boolean isWildcard(String s) { /** Is this the special ":" RefSpec? */ private boolean matching; + /** Is this a negative refspec. */ + private boolean negative; + /** * How strict to be about wildcards. * @@ -96,12 +99,23 @@ public RefSpec() { wildcard = false; srcName = Constants.HEAD; dstName = null; + negative =false; allowMismatchedWildcards = WildcardMode.REQUIRE_MATCH; } /** * Parse a ref specification for use during transport operations. *

+ * {@link RefSpec}s can be regular or negative, regular RefSpecs indicate + * what to include in transport operations while negative RefSpecs indicate + * what to exclude in fetch. + *

+ * Negative {@link RefSpec}s can't be force, must have only source or + * destination. Wildcard patterns are also supported in negative RefSpecs + * but they can not go with {@code WildcardMode.REQUIRE_MATCH} because they + * are natually one to many mappings. + * + *

* Specifications are typically one of the following forms: *

    *
  • refs/heads/master
  • @@ -121,6 +135,12 @@ public RefSpec() { *
  • refs/heads/*:refs/heads/master
  • *
* + * Negative specifications are usually like: + *
    + *
  • ^:refs/heads/master
  • + *
  • ^refs/heads/*
  • + *
+ * * @param spec * string describing the specification. * @param mode @@ -133,11 +153,22 @@ public RefSpec() { public RefSpec(String spec, WildcardMode mode) { this.allowMismatchedWildcards = mode; String s = spec; + + if (s.startsWith("^+") || s.startsWith("+^")) { + throw new IllegalArgumentException( + JGitText.get().invalidNegativeAndForce); + } + if (s.startsWith("+")) { //$NON-NLS-1$ force = true; s = s.substring(1); } + if(s.startsWith("^")) { + negative = true; + s = s.substring(1); + } + boolean matchPushSpec = false; final int c = s.lastIndexOf(':'); if (c == 0) { @@ -181,6 +212,21 @@ public RefSpec(String spec, WildcardMode mode) { } srcName = checkValid(s); } + + // Negative refspecs must only have dstName or srcName. + if (isNegative()) { + if (isNullOrEmpty(srcName) && isNullOrEmpty(dstName)) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().invalidRefSpec, spec)); + } + if (!isNullOrEmpty(srcName) && !isNullOrEmpty(dstName)) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().invalidRefSpec, spec)); + } + if(wildcard && mode == WildcardMode.REQUIRE_MATCH) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().invalidRefSpec, spec));} + } matching = matchPushSpec; } @@ -205,13 +251,15 @@ public RefSpec(String spec, WildcardMode mode) { * the specification is invalid. */ public RefSpec(String spec) { - this(spec, WildcardMode.REQUIRE_MATCH); + this(spec, spec.startsWith("^") ? WildcardMode.ALLOW_MISMATCH + : WildcardMode.REQUIRE_MATCH); } private RefSpec(RefSpec p) { matching = false; force = p.isForceUpdate(); wildcard = p.isWildcard(); + negative = p.isNegative(); srcName = p.getSource(); dstName = p.getDestination(); allowMismatchedWildcards = p.allowMismatchedWildcards; @@ -246,6 +294,10 @@ public boolean isForceUpdate() { */ public RefSpec setForceUpdate(boolean forceUpdate) { final RefSpec r = new RefSpec(this); + if (forceUpdate && isNegative()) { + throw new IllegalArgumentException( + JGitText.get().invalidNegativeAndForce); + } r.matching = matching; r.force = forceUpdate; return r; @@ -264,6 +316,15 @@ public boolean isWildcard() { return wildcard; } + /** + * Check if this specification is a negative one. + * + * @return true if this specification is negative. + */ + public boolean isNegative() { + return negative; + } + /** * Get the source ref description. *

@@ -435,6 +496,10 @@ private RefSpec expandFromSourceImp(String name) { return this; } + private static boolean isNullOrEmpty(String refName) { + return refName == null || refName.isEmpty(); + } + /** * Expand this specification to exactly match a ref. *

@@ -570,6 +635,9 @@ public boolean equals(Object obj) { if (isForceUpdate() != b.isForceUpdate()) { return false; } + if(isNegative() != b.isNegative()) { + return false; + } if (isMatching()) { return b.isMatching(); } else if (b.isMatching()) { @@ -587,6 +655,9 @@ public String toString() { if (isForceUpdate()) { r.append('+'); } + if(isNegative()) { + r.append('^'); + } if (isMatching()) { r.append(':'); } else { 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 0eab4434e..3222d6330 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -1230,7 +1230,9 @@ public void setPushOptions(List pushOptions) { * @param toFetch * specification of refs to fetch locally. May be null or the * empty collection to use the specifications from the - * RemoteConfig. Source for each RefSpec can't be null. + * RemoteConfig. May contains regular and negative + * {@link RefSpec}s. Source for each regular RefSpec can't + * be null. * @return information describing the tracking refs updated. * @throws org.eclipse.jgit.errors.NotSupportedException * this transport implementation does not support fetching @@ -1264,7 +1266,9 @@ public FetchResult fetch(final ProgressMonitor monitor, * @param toFetch * specification of refs to fetch locally. May be null or the * empty collection to use the specifications from the - * RemoteConfig. Source for each RefSpec can't be null. + * RemoteConfig. May contains regular and negative + * {@link RefSpec}s. Source for each regular RefSpec can't + * be null. * @param branch * the initial branch to check out when cloning the repository. * Can be specified as ref name (refs/heads/master),