diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java index 1a1f5b487..6f7aa63ed 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java @@ -10,6 +10,7 @@ package org.eclipse.jgit.api; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -392,28 +393,64 @@ public void testPushDefaultMatching() throws Exception { git.add().addFilepattern("f").call(); RevCommit commit = git.commit().setMessage("adding f").call(); - git.checkout().setName("also-pushed").setCreateBranch(true).call(); + git.checkout().setName("not-pushed").setCreateBranch(true).call(); git.checkout().setName("branchtopush").setCreateBranch(true).call(); assertEquals(null, git2.getRepository().resolve("refs/heads/branchtopush")); assertEquals(null, - git2.getRepository().resolve("refs/heads/also-pushed")); + git2.getRepository().resolve("refs/heads/not-pushed")); assertEquals(null, git2.getRepository().resolve("refs/heads/master")); - git.push().setRemote("test").setPushDefault(PushDefault.MATCHING) + // push master and branchtopush + git.push().setRemote("test").setRefSpecs( + new RefSpec("refs/heads/master:refs/heads/master"), + new RefSpec( + "refs/heads/branchtopush:refs/heads/branchtopush")) .call(); - assertEquals(commit.getId(), - git2.getRepository().resolve("refs/heads/branchtopush")); - assertEquals(commit.getId(), - git2.getRepository().resolve("refs/heads/also-pushed")); assertEquals(commit.getId(), git2.getRepository().resolve("refs/heads/master")); - assertEquals(commit.getId(), git.getRepository() - .resolve("refs/remotes/origin/branchtopush")); - assertEquals(commit.getId(), git.getRepository() - .resolve("refs/remotes/origin/also-pushed")); assertEquals(commit.getId(), + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + // Create two different commits on these two branches + writeTrashFile("b", "on branchtopush"); + git.add().addFilepattern("b").call(); + RevCommit bCommit = git.commit().setMessage("on branchtopush") + .call(); + git.checkout().setName("master").call(); + writeTrashFile("m", "on master"); + git.add().addFilepattern("m").call(); + RevCommit mCommit = git.commit().setMessage("on master").call(); + // Now push with mode "matching": should push both branches. + Iterable result = git.push().setRemote("test") + .setPushDefault(PushDefault.MATCHING) + .call(); + int n = 0; + for (PushResult r : result) { + n++; + assertEquals(1, n); + assertEquals(2, r.getRemoteUpdates().size()); + for (RemoteRefUpdate update : r.getRemoteUpdates()) { + assertFalse(update.isMatching()); + assertTrue(update.getSrcRef() + .equals("refs/heads/branchtopush") + || update.getSrcRef().equals("refs/heads/master")); + assertEquals(RemoteRefUpdate.Status.OK, update.getStatus()); + } + } + assertEquals(bCommit.getId(), + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(mCommit.getId(), + git2.getRepository().resolve("refs/heads/master")); + assertEquals(bCommit.getId(), git.getRepository() + .resolve("refs/remotes/origin/branchtopush")); + assertEquals(null, git.getRepository() + .resolve("refs/remotes/origin/not-pushed")); + assertEquals(mCommit.getId(), git.getRepository().resolve("refs/remotes/origin/master")); } } 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 5569bca23..b56308cb7 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 @@ -466,4 +466,18 @@ public void onlyWildCard() { assertTrue(a.matchSource("refs/heads/master")); assertNull(a.getDestination()); } + + @Test + public void matching() { + RefSpec a = new RefSpec(":"); + assertTrue(a.isMatching()); + assertFalse(a.isForceUpdate()); + } + + @Test + public void matchingForced() { + RefSpec a = new RefSpec("+:"); + assertTrue(a.isMatching()); + assertTrue(a.isForceUpdate()); + } } 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 4f57f3512..08353dfdf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java @@ -230,7 +230,7 @@ private void determineDefaultRefSpecs(Config config) refSpecs.add(new RefSpec(getCurrentBranch())); break; case MATCHING: - setPushAll(); + refSpecs.add(new RefSpec(":")); //$NON-NLS-1$ break; case NOTHING: throw new InvalidRefNameException( 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 c4f276988..c76cee698 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java @@ -26,6 +26,7 @@ import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.hooks.PrePushHook; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; @@ -141,9 +142,13 @@ PushResult execute(ProgressMonitor monitor) res.setAdvertisedRefs(transport.getURI(), connection .getRefsMap()); res.peerUserAgent = connection.getPeerUserAgent(); - res.setRemoteUpdates(toPush); monitor.endTask(); + Map expanded = expandMatching(); + toPush.clear(); + toPush.putAll(expanded); + + res.setRemoteUpdates(toPush); final Map preprocessed = prepareRemoteUpdates(); List willBeAttempted = preprocessed.values() .stream().filter(u -> { @@ -237,25 +242,8 @@ private Map prepareRemoteUpdates() continue; } - // check for fast-forward: - // - both old and new ref must point to commits, AND - // - both of them must be known for us, exist in repository, AND - // - old commit must be ancestor of new commit - boolean fastForward = true; - try { - RevObject oldRev = walker.parseAny(advertisedOld); - final RevObject newRev = walker.parseAny(rru.getNewObjectId()); - if (!(oldRev instanceof RevCommit) - || !(newRev instanceof RevCommit) - || !walker.isMergedInto((RevCommit) oldRev, - (RevCommit) newRev)) - fastForward = false; - } catch (MissingObjectException x) { - fastForward = false; - } catch (Exception x) { - throw new TransportException(transport.getURI(), MessageFormat.format( - JGitText.get().readingObjectsFromLocalRepositoryFailed, x.getMessage()), x); - } + boolean fastForward = isFastForward(advertisedOld, + rru.getNewObjectId()); rru.setFastForward(fastForward); if (!fastForward && !rru.isForceUpdate()) { rru.setStatus(Status.REJECTED_NONFASTFORWARD); @@ -269,6 +257,134 @@ private Map prepareRemoteUpdates() return result; } + /** + * Determines whether an update from {@code oldOid} to {@code newOid} is a + * fast-forward update: + *
    + *
  • both old and new must be commits, AND
  • + *
  • both of them must be known to us and exist in the repository, + * AND
  • + *
  • the old commit must be an ancestor of the new commit.
  • + *
+ * + * @param oldOid + * {@link ObjectId} of the old commit + * @param newOid + * {@link ObjectId} of the new commit + * @return {@code true} if the update fast-forwards, {@code false} otherwise + * @throws TransportException + */ + private boolean isFastForward(ObjectId oldOid, ObjectId newOid) + throws TransportException { + try { + RevObject oldRev = walker.parseAny(oldOid); + RevObject newRev = walker.parseAny(newOid); + if (!(oldRev instanceof RevCommit) || !(newRev instanceof RevCommit) + || !walker.isMergedInto((RevCommit) oldRev, + (RevCommit) newRev)) { + return false; + } + } catch (MissingObjectException x) { + return false; + } catch (Exception x) { + throw new TransportException(transport.getURI(), + MessageFormat.format(JGitText + .get().readingObjectsFromLocalRepositoryFailed, + x.getMessage()), + x); + } + return true; + } + + /** + * Expands all placeholder {@link RemoteRefUpdate}s for "matching" + * {@link RefSpec}s ":" in {@link #toPush} and returns the resulting map in + * which the placeholders have been replaced by their expansion. + * + * @return a new map of {@link RemoteRefUpdate}s keyed by remote name + * @throws TransportException + * if the expansion results in duplicate updates + */ + private Map expandMatching() + throws TransportException { + Map result = new LinkedHashMap<>(); + boolean hadMatch = false; + for (RemoteRefUpdate update : toPush.values()) { + if (update.isMatching()) { + if (hadMatch) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + ":")); //$NON-NLS-1$ + } + expandMatching(result, update); + hadMatch = true; + } else if (result.put(update.getRemoteName(), update) != null) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + update.getRemoteName())); + } + } + return result; + } + + /** + * Expands the placeholder {@link RemoteRefUpdate} {@code match} for a + * "matching" {@link RefSpec} ":" or "+:" and puts the expansion into the + * given map {@code updates}. + * + * @param updates + * map to put the expansion in + * @param match + * the placeholder {@link RemoteRefUpdate} to expand + * + * @throws TransportException + * if the expansion results in duplicate updates, or the local + * branches cannot be determined + */ + private void expandMatching(Map updates, + RemoteRefUpdate match) throws TransportException { + try { + Map advertisement = connection.getRefsMap(); + Collection fetchSpecs = match.getFetchSpecs(); + boolean forceUpdate = match.isForceUpdate(); + for (Ref local : transport.local.getRefDatabase() + .getRefsByPrefix(Constants.R_HEADS)) { + if (local.isSymbolic()) { + continue; + } + String name = local.getName(); + Ref advertised = advertisement.get(name); + if (advertised == null || advertised.isSymbolic()) { + continue; + } + ObjectId oldOid = advertised.getObjectId(); + if (oldOid == null || ObjectId.zeroId().equals(oldOid)) { + continue; + } + ObjectId newOid = local.getObjectId(); + if (newOid == null || ObjectId.zeroId().equals(newOid)) { + continue; + } + + RemoteRefUpdate rru = new RemoteRefUpdate(transport.local, name, + newOid, name, forceUpdate, + Transport.findTrackingRefName(name, fetchSpecs), + oldOid); + if (updates.put(rru.getRemoteName(), rru) != null) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + rru.getRemoteName())); + } + } + } catch (IOException x) { + throw new TransportException(transport.getURI(), + MessageFormat.format(JGitText + .get().readingObjectsFromLocalRepositoryFailed, + x.getMessage()), + x); + } + } + private Map rejectAll() { for (RemoteRefUpdate rru : toPush.values()) { if (rru.getStatus() == Status.NOT_ATTEMPTED) { 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 ac357afda..51fda84f3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java @@ -12,11 +12,11 @@ import java.io.Serializable; import java.text.MessageFormat; +import java.util.Objects; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.util.References; /** * Describes how refs in one repository copy into another repository. @@ -50,6 +50,9 @@ public static boolean isWildcard(String s) { /** Is this specification actually a wildcard match? */ private boolean wildcard; + /** Is this the special ":" RefSpec? */ + private boolean matching; + /** * How strict to be about wildcards. * @@ -71,6 +74,7 @@ public enum WildcardMode { */ ALLOW_MISMATCH } + /** Whether a wildcard is allowed on one side but not the other. */ private WildcardMode allowMismatchedWildcards; @@ -87,6 +91,7 @@ public enum WildcardMode { * applications, as at least one field must be set to match a source name. */ public RefSpec() { + matching = false; force = false; wildcard = false; srcName = Constants.HEAD; @@ -133,17 +138,25 @@ public RefSpec(String spec, WildcardMode mode) { s = s.substring(1); } + boolean matchPushSpec = false; final int c = s.lastIndexOf(':'); if (c == 0) { s = s.substring(1); - if (isWildcard(s)) { + if (s.isEmpty()) { + matchPushSpec = true; wildcard = true; - if (mode == WildcardMode.REQUIRE_MATCH) { - throw new IllegalArgumentException(MessageFormat - .format(JGitText.get().invalidWildcards, spec)); + srcName = Constants.R_HEADS + '*'; + dstName = srcName; + } else { + if (isWildcard(s)) { + wildcard = true; + if (mode == WildcardMode.REQUIRE_MATCH) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().invalidWildcards, spec)); + } } + dstName = checkValid(s); } - dstName = checkValid(s); } else if (c > 0) { String src = s.substring(0, c); String dst = s.substring(c + 1); @@ -168,6 +181,7 @@ public RefSpec(String spec, WildcardMode mode) { } srcName = checkValid(s); } + matching = matchPushSpec; } /** @@ -195,6 +209,7 @@ public RefSpec(String spec) { } private RefSpec(RefSpec p) { + matching = false; force = p.isForceUpdate(); wildcard = p.isWildcard(); srcName = p.getSource(); @@ -202,6 +217,17 @@ private RefSpec(RefSpec p) { allowMismatchedWildcards = p.allowMismatchedWildcards; } + /** + * Tells whether this {@link RefSpec} is the special "matching" RefSpec ":" + * for pushing. + * + * @return whether this is a "matching" RefSpec + * @since 6.1 + */ + public boolean isMatching() { + return matching; + } + /** * Check if this specification wants to forcefully update the destination. * @@ -220,6 +246,7 @@ public boolean isForceUpdate() { */ public RefSpec setForceUpdate(boolean forceUpdate) { final RefSpec r = new RefSpec(this); + r.matching = matching; r.force = forceUpdate; return r; } @@ -541,37 +568,36 @@ public boolean equals(Object obj) { if (!(obj instanceof RefSpec)) return false; final RefSpec b = (RefSpec) obj; - if (isForceUpdate() != b.isForceUpdate()) + if (isForceUpdate() != b.isForceUpdate()) { return false; - if (isWildcard() != b.isWildcard()) - return false; - if (!eq(getSource(), b.getSource())) - return false; - if (!eq(getDestination(), b.getDestination())) - return false; - return true; - } - - private static boolean eq(String a, String b) { - if (References.isSameObject(a, b)) { - return true; } - if (a == null || b == null) + if (isMatching()) { + return b.isMatching(); + } else if (b.isMatching()) { return false; - return a.equals(b); + } + return isWildcard() == b.isWildcard() + && Objects.equals(getSource(), b.getSource()) + && Objects.equals(getDestination(), b.getDestination()); } /** {@inheritDoc} */ @Override public String toString() { final StringBuilder r = new StringBuilder(); - if (isForceUpdate()) + if (isForceUpdate()) { r.append('+'); - if (getSource() != null) - r.append(getSource()); - if (getDestination() != null) { + } + if (isMatching()) { r.append(':'); - r.append(getDestination()); + } else { + if (getSource() != null) { + r.append(getSource()); + } + if (getDestination() != null) { + r.append(':'); + r.append(getDestination()); + } } return r.toString(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java index 43eaac792..b90ae813e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java @@ -12,7 +12,9 @@ import java.io.IOException; import java.text.MessageFormat; +import java.util.Collection; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -115,6 +117,12 @@ public enum Status { private RefUpdate localUpdate; + /** + * If set, the RemoteRefUpdate is a placeholder for the "matching" RefSpec + * to be expanded after the advertisements have been received in a push. + */ + private Collection fetchSpecs; + /** * Construct remote ref update request by providing an update specification. * Object is created with default @@ -259,24 +267,38 @@ public RemoteRefUpdate(final Repository localDb, final String srcRef, final ObjectId srcId, final String remoteName, final boolean forceUpdate, final String localName, final ObjectId expectedOldObjectId) throws IOException { - if (remoteName == null) - throw new IllegalArgumentException(JGitText.get().remoteNameCannotBeNull); - if (srcId == null && srcRef != null) - throw new IOException(MessageFormat.format( - JGitText.get().sourceRefDoesntResolveToAnyObject, srcRef)); + this(localDb, srcRef, srcId, remoteName, forceUpdate, localName, null, + expectedOldObjectId); + } - if (srcRef != null) + private RemoteRefUpdate(Repository localDb, String srcRef, ObjectId srcId, + String remoteName, boolean forceUpdate, String localName, + Collection fetchSpecs, ObjectId expectedOldObjectId) + throws IOException { + if (fetchSpecs == null) { + if (remoteName == null) { + throw new IllegalArgumentException( + JGitText.get().remoteNameCannotBeNull); + } + if (srcId == null && srcRef != null) { + throw new IOException(MessageFormat.format( + JGitText.get().sourceRefDoesntResolveToAnyObject, + srcRef)); + } + } + if (srcRef != null) { this.srcRef = srcRef; - else if (srcId != null && !srcId.equals(ObjectId.zeroId())) + } else if (srcId != null && !srcId.equals(ObjectId.zeroId())) { this.srcRef = srcId.name(); - else + } else { this.srcRef = null; - - if (srcId != null) + } + if (srcId != null) { this.newObjectId = srcId; - else + } else { this.newObjectId = ObjectId.zeroId(); - + } + this.fetchSpecs = fetchSpecs; this.remoteName = remoteName; this.forceUpdate = forceUpdate; if (localName != null && localDb != null) { @@ -292,8 +314,9 @@ else if (srcId != null && !srcId.equals(ObjectId.zeroId())) ? localUpdate.getOldObjectId() : ObjectId.zeroId(), newObjectId); - } else + } else { trackingRefUpdate = null; + } this.localDb = localDb; this.expectedOldObjectId = expectedOldObjectId; this.status = Status.NOT_ATTEMPTED; @@ -318,9 +341,55 @@ else if (srcId != null && !srcId.equals(ObjectId.zeroId())) */ public RemoteRefUpdate(final RemoteRefUpdate base, final ObjectId newExpectedOldObjectId) throws IOException { - this(base.localDb, base.srcRef, base.remoteName, base.forceUpdate, + this(base.localDb, base.srcRef, base.newObjectId, base.remoteName, + base.forceUpdate, (base.trackingRefUpdate == null ? null : base.trackingRefUpdate - .getLocalName()), newExpectedOldObjectId); + .getLocalName()), + base.fetchSpecs, newExpectedOldObjectId); + } + + /** + * Creates a "placeholder" update for the "matching" RefSpec ":". + * + * @param localDb + * local repository to push from + * @param forceUpdate + * whether non-fast-forward updates shall be allowed + * @param fetchSpecs + * The fetch {@link RefSpec}s to use when this placeholder is + * expanded to determine remote tracking branch updates + */ + RemoteRefUpdate(Repository localDb, boolean forceUpdate, + @NonNull Collection fetchSpecs) { + this.localDb = localDb; + this.forceUpdate = forceUpdate; + this.fetchSpecs = fetchSpecs; + this.trackingRefUpdate = null; + this.srcRef = null; + this.remoteName = null; + this.newObjectId = null; + this.status = Status.NOT_ATTEMPTED; + } + + /** + * Tells whether this {@link RemoteRefUpdate} is a placeholder for a + * "matching" {@link RefSpec}. + * + * @return {@code true} if this is a placeholder, {@code false} otherwise + * @since 6.1 + */ + public boolean isMatching() { + return fetchSpecs != null; + } + + /** + * Retrieves the fetch {@link RefSpec}s of this {@link RemoteRefUpdate}. + * + * @return the fetch {@link RefSpec}s, or {@code null} if + * {@code this.}{@link #isMatching()} {@code == false} + */ + Collection getFetchSpecs() { + return fetchSpecs; } /** 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 1245f78a6..0eab4434e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -589,6 +589,11 @@ public static Collection findRemoteRefUpdatesFor( final Collection procRefs = expandPushWildcardsFor(db, specs); for (RefSpec spec : procRefs) { + if (spec.isMatching()) { + result.add(new RemoteRefUpdate(db, spec.isForceUpdate(), + fetchSpecs)); + continue; + } String srcSpec = spec.getSource(); final Ref srcRef = db.findRef(srcSpec); if (srcRef != null) @@ -659,7 +664,7 @@ private static Collection expandPushWildcardsFor( List localRefs = null; for (RefSpec spec : specs) { - if (spec.isWildcard()) { + if (!spec.isMatching() && spec.isWildcard()) { if (localRefs == null) { localRefs = db.getRefDatabase().getRefs(); } @@ -675,7 +680,7 @@ private static Collection expandPushWildcardsFor( return procRefs; } - private static String findTrackingRefName(final String remoteName, + static String findTrackingRefName(final String remoteName, final Collection fetchSpecs) { // try to find matching tracking refs for (RefSpec fetchSpec : fetchSpecs) {