Fetch: Introduce negative refspecs.

Implement negative refspecs in JGit fetch, following C Git. Git
supports negative refspecs in source only while this change supports
them in both source and destination.

If one branch is equal to any branch or matches any pattern in the
negative refspecs collection, the branch will not be fetched even if
it's in the toFetch collection.

With this feature, users can express more complex patterns during fetch.

Change-Id: Iaa1cd4de5c08c273e198b72e12e3dadae7be709f
Sign-off-by: Yunjie Li<yunjieli@google.com>
This commit is contained in:
yunjieli 2022-03-28 14:47:02 -07:00
parent 68a0725896
commit eca101fc05
7 changed files with 211 additions and 8 deletions

View File

@ -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";

View File

@ -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/*");
}
}

View File

@ -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}

View File

@ -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;

View File

@ -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<RefSpec> toFetch;
/**
* List of things we don't want to fetch from the remote repository or to
* the local repository.
*/
private final Collection<RefSpec> negativeRefSpecs;
/** Set of refs we will actually wind up asking to obtain. */
private final HashMap<ObjectId, Ref> askFor = new HashMap<>();
@ -74,9 +81,12 @@ class FetchProcess {
private Map<String, Ref> localRefs;
FetchProcess(Transport t, Collection<RefSpec> f) {
FetchProcess(Transport t, Collection<RefSpec> 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<Ref> 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<Ref> 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);

View File

@ -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.
* <p>
* {@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.
* <p>
* 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.
*
* <p>
* Specifications are typically one of the following forms:
* <ul>
* <li><code>refs/heads/master</code></li>
@ -121,6 +135,12 @@ public RefSpec() {
* <li><code>refs/heads/*:refs/heads/master</code></li>
* </ul>
*
* Negative specifications are usually like:
* <ul>
* <li><code>^:refs/heads/master</code></li>
* <li><code>^refs/heads/*</code></li>
* </ul>
*
* @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.
* <p>
@ -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.
* <p>
@ -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 {

View File

@ -1230,7 +1230,9 @@ public void setPushOptions(List<String> 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 (<code>refs/heads/master</code>),