diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java index c82a0cde8..1b13d0ba2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java @@ -181,6 +181,13 @@ public abstract class BasePackFetchConnection extends BasePackConnection */ public static final String OPTION_NO_DONE = "no-done"; //$NON-NLS-1$ + /** + * The client supports fetching objects at the tip of any ref, even if not + * advertised. + * @since 3.1 + */ + public static final String OPTION_ALLOW_TIP_SHA1_IN_WANT = "allow-tip-sha1-in-want"; //$NON-NLS-1$ + static enum MultiAck { OFF, CONTINUE, DETAILED; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java index c3e39868d..1286718de 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java @@ -43,11 +43,17 @@ package org.eclipse.jgit.transport; +import java.util.HashMap; +import java.util.Map; + import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config.SectionParser; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; /** - * The standard "transfer", "fetch" and "receive" configuration parameters. + * The standard "transfer", "fetch", "receive", and "uploadpack" configuration + * parameters. */ public class TransferConfig { /** Key for {@link Config#get(SectionParser)}. */ @@ -58,9 +64,18 @@ public class TransferConfig { }; private final boolean fsckObjects; + private final boolean allowTipSha1InWant; + private final String[] hideRefs; + + TransferConfig(final Repository db) { + this(db.getConfig()); + } private TransferConfig(final Config rc) { fsckObjects = rc.getBoolean("receive", "fsckobjects", false); //$NON-NLS-1$ //$NON-NLS-2$ + allowTipSha1InWant = rc.getBoolean( + "uploadpack", "allowtipsha1inwant", false); //$NON-NLS-1$ //$NON-NLS-2$ + hideRefs = rc.getStringList("uploadpack", null, "hiderefs"); //$NON-NLS-1$ //$NON-NLS-2$ } /** @@ -69,4 +84,41 @@ public class TransferConfig { public boolean isFsckObjects() { return fsckObjects; } + + /** + * @return allow clients to request non-advertised tip SHA-1s? + */ + public boolean isAllowTipSha1InWant() { + return allowTipSha1InWant; + } + + /** + * @return {@link RefFilter} respecting configured hidden refs. + */ + public RefFilter getRefFilter() { + if (hideRefs.length == 0) + return RefFilter.DEFAULT; + + return new RefFilter() { + public Map filter(Map refs) { + Map result = new HashMap(); + for (Map.Entry e : refs.entrySet()) { + boolean add = true; + for (String hide : hideRefs) { + if (e.getKey().equals(hide) || prefixMatch(hide, e.getKey())) { + add = false; + break; + } + } + if (add) + result.put(e.getKey(), e.getValue()); + } + return result; + } + + private boolean prefixMatch(String p, String s) { + return p.charAt(p.length() - 1) == '/' && s.startsWith(p); + } + }; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java index e5e04d014..0d23cf792 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -49,6 +49,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -89,6 +90,8 @@ import org.eclipse.jgit.util.io.TimeoutOutputStream; * Implements the server side of a fetch connection, transmitting objects. */ public class UploadPack { + static final String OPTION_ALLOW_TIP_SHA1_IN_WANT = BasePackFetchConnection.OPTION_ALLOW_TIP_SHA1_IN_WANT; + static final String OPTION_INCLUDE_TAG = BasePackFetchConnection.OPTION_INCLUDE_TAG; static final String OPTION_MULTI_ACK = BasePackFetchConnection.OPTION_MULTI_ACK; @@ -113,12 +116,55 @@ public class UploadPack { public static enum RequestPolicy { /** Client may only ask for objects the server advertised a reference for. */ ADVERTISED, - /** Client may ask for any commit reachable from a reference. */ + + /** + * Client may ask for any commit reachable from a reference advertised by + * the server. + */ REACHABLE_COMMIT, + + /** + * Client may ask for objects that are the tip of any reference, even if not + * advertised. + *

+ * This may happen, for example, when a custom {@link RefFilter} is set. + */ + TIP, + + /** + * Client may ask for any commit reachable from any reference, even if that + * reference wasn't advertised. + */ + REACHABLE_COMMIT_TIP, + /** Client may ask for any SHA-1 in the repository. */ ANY; } + /** + * Validator for client requests. + * + * @since 3.1 + */ + public interface RequestValidator { + /** + * Check a list of client wants against the request policy. + * + * @param up + * {@link UploadPack} instance. + * @param wants + * objects the client requested that were not advertised. + * + * @throws PackProtocolException + * if one or more wants is not valid. + * @throws IOException + * if a low-level exception occurred. + * @since 3.1 + */ + void checkWants(UploadPack up, List wants) + throws PackProtocolException, IOException; + } + /** Data in the first line of a request, the line itself plus options. */ public static class FirstLine { private final String line; @@ -166,6 +212,9 @@ public class UploadPack { /** Configuration to pass into the PackWriter. */ private PackConfig packConfig; + /** Configuration for various transfer options. */ + private TransferConfig transferConfig; + /** Timeout in seconds to wait for client interaction. */ private int timeout; @@ -253,7 +302,7 @@ public class UploadPack { private final RevFlagSet SAVE; - private RequestPolicy requestPolicy = RequestPolicy.ADVERTISED; + private RequestValidator requestValidator = new AdvertisedRequestValidator(); private MultiAck multiAck = MultiAck.OFF; @@ -285,6 +334,8 @@ public class UploadPack { SAVE.add(PEER_HAS); SAVE.add(COMMON); SAVE.add(SATISFIED); + + setTransferConfig(null); } /** @return the repository this upload is reading from. */ @@ -324,7 +375,10 @@ public class UploadPack { refs = allRefs; else refs = db.getAllRefs(); - refs = refFilter.filter(refs); + if (refFilter == RefFilter.DEFAULT) + refs = transferConfig.getRefFilter().filter(refs); + else + refs = refFilter.filter(refs); } /** @return timeout (in seconds) before aborting an IO operation. */ @@ -363,13 +417,24 @@ public class UploadPack { */ public void setBiDirectionalPipe(final boolean twoWay) { biDirectionalPipe = twoWay; - if (!biDirectionalPipe && requestPolicy == RequestPolicy.ADVERTISED) - requestPolicy = RequestPolicy.REACHABLE_COMMIT; } - /** @return policy used by the service to validate client requests. */ + /** + * @return policy used by the service to validate client requests, or null for + * a custom request validator. + */ public RequestPolicy getRequestPolicy() { - return requestPolicy; + if (requestValidator instanceof AdvertisedRequestValidator) + return RequestPolicy.ADVERTISED; + if (requestValidator instanceof ReachableCommitRequestValidator) + return RequestPolicy.REACHABLE_COMMIT; + if (requestValidator instanceof TipRequestValidator) + return RequestPolicy.TIP; + if (requestValidator instanceof ReachableCommitTipRequestValidator) + return RequestPolicy.REACHABLE_COMMIT_TIP; + if (requestValidator instanceof AnyRequestValidator) + return RequestPolicy.ANY; + return null; } /** @@ -378,11 +443,40 @@ public class UploadPack { * By default the policy is {@link RequestPolicy#ADVERTISED}, * which is the Git default requiring clients to only ask for an * object that a reference directly points to. This may be relaxed - * to {@link RequestPolicy#REACHABLE_COMMIT} when callers - * have {@link #setBiDirectionalPipe(boolean)} set to false. + * to {@link RequestPolicy#REACHABLE_COMMIT} or + * {@link RequestPolicy#REACHABLE_COMMIT_TIP} when callers have + * {@link #setBiDirectionalPipe(boolean)} set to false. + * Overrides any policy specified in a {@link TransferConfig}. */ public void setRequestPolicy(RequestPolicy policy) { - requestPolicy = policy != null ? policy : RequestPolicy.ADVERTISED; + switch (policy) { + case ADVERTISED: + default: + requestValidator = new AdvertisedRequestValidator(); + break; + case REACHABLE_COMMIT: + requestValidator = new ReachableCommitRequestValidator(); + break; + case TIP: + requestValidator = new TipRequestValidator(); + break; + case REACHABLE_COMMIT_TIP: + requestValidator = new ReachableCommitTipRequestValidator(); + break; + case ANY: + requestValidator = new AnyRequestValidator(); + break; + } + } + + /** + * @param validator + * custom validator for client want list. + * @since 3.1 + */ + public void setRequestValidator(RequestValidator validator) { + requestValidator = validator != null ? validator + : new AdvertisedRequestValidator(); } /** @return the hook used while advertising the refs to the client */ @@ -417,7 +511,8 @@ public class UploadPack { *

* Only refs allowed by this filter will be sent to the client. * The filter is run against the refs specified by the - * {@link AdvertiseRefsHook} (if applicable). + * {@link AdvertiseRefsHook} (if applicable). If null or not set, uses the + * filter implied by the {@link TransferConfig}. * * @param refFilter * the filter; may be null to show all refs. @@ -452,6 +547,17 @@ public class UploadPack { this.packConfig = pc; } + /** + * @param tc + * configuration controlling transfer options. If null the source + * repository's settings will be used. + */ + public void setTransferConfig(TransferConfig tc) { + this.transferConfig = tc != null ? tc : new TransferConfig(db); + setRequestPolicy(transferConfig.isAllowTipSha1InWant() + ? RequestPolicy.TIP : RequestPolicy.ADVERTISED); + } + /** @return the configured logger. */ public UploadPackLogger getLogger() { return logger; @@ -558,15 +664,10 @@ public class UploadPack { private void service() throws IOException { if (biDirectionalPipe) sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut)); - else if (requestPolicy == RequestPolicy.ANY) + else if (requestValidator instanceof AnyRequestValidator) advertised = Collections.emptySet(); - else { - advertised = new HashSet(); - for (Ref ref : getAdvertisedOrDefaultRefs().values()) { - if (ref.getObjectId() != null) - advertised.add(ref.getObjectId()); - } - } + else + advertised = refIdSet(getAdvertisedOrDefaultRefs().values()); boolean sendPack; try { @@ -618,6 +719,15 @@ public class UploadPack { sendPack(); } + private static Set refIdSet(Collection refs) { + Set ids = new HashSet(refs.size()); + for (Ref ref : refs) { + if (ref.getObjectId() != null) + ids.add(ref.getObjectId()); + } + return ids; + } + private void reportErrorDuringNegotiate(String msg) { try { pckOut.writeString("ERR " + msg + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ @@ -693,6 +803,11 @@ public class UploadPack { adv.advertiseCapability(OPTION_SHALLOW); if (!biDirectionalPipe) adv.advertiseCapability(OPTION_NO_DONE); + RequestPolicy policy = getRequestPolicy(); + if (policy == RequestPolicy.TIP + || policy == RequestPolicy.REACHABLE_COMMIT_TIP + || policy == null) + adv.advertiseCapability(OPTION_ALLOW_TIP_SHA1_IN_WANT); adv.setDerefTags(true); advertised = adv.send(getAdvertisedOrDefaultRefs()); adv.end(); @@ -921,27 +1036,13 @@ public class UploadPack { private void parseWants() throws IOException { AsyncRevObjectQueue q = walk.parseAny(wantIds, true); try { - List checkReachable = null; + List notAdvertisedWants = null; RevObject obj; while ((obj = q.next()) != null) { if (!advertised.contains(obj)) { - switch (requestPolicy) { - case ADVERTISED: - default: - throw new PackProtocolException(MessageFormat.format( - JGitText.get().wantNotValid, obj)); - case REACHABLE_COMMIT: - if (!(obj instanceof RevCommit)) { - throw new PackProtocolException(MessageFormat.format( - JGitText.get().wantNotValid, obj)); - } - if (checkReachable == null) - checkReachable = new ArrayList(); - checkReachable.add((RevCommit) obj); - break; - case ANY: - break; - } + if (notAdvertisedWants == null) + notAdvertisedWants = new ArrayList(); + notAdvertisedWants.add(obj); } want(obj); @@ -953,8 +1054,8 @@ public class UploadPack { want(obj); } } - if (checkReachable != null) - checkNotAdvertisedWants(checkReachable); + if (notAdvertisedWants != null) + requestValidator.checkWants(this, notAdvertisedWants); wantIds.clear(); } catch (MissingObjectException notFound) { ObjectId id = notFound.getObjectId(); @@ -972,17 +1073,101 @@ public class UploadPack { } } - private void checkNotAdvertisedWants(List notAdvertisedWants) + /** + * Validator corresponding to {@link RequestPolicy#ADVERTISED}. + * + * @since 3.1 + */ + public static final class AdvertisedRequestValidator + implements RequestValidator { + public void checkWants(UploadPack up, List wants) + throws PackProtocolException, IOException { + if (!up.isBiDirectionalPipe()) + new ReachableCommitRequestValidator().checkWants(up, wants); + else if (!wants.isEmpty()) + throw new PackProtocolException(MessageFormat.format( + JGitText.get().wantNotValid, wants.iterator().next().name())); + } + } + + /** + * Validator corresponding to {@link RequestPolicy#REACHABLE_COMMIT}. + * + * @since 3.1 + */ + public static final class ReachableCommitRequestValidator + implements RequestValidator { + public void checkWants(UploadPack up, List wants) + throws PackProtocolException, IOException { + checkNotAdvertisedWants(up.getRevWalk(), wants, + refIdSet(up.getAdvertisedRefs().values())); + } + } + + /** + * Validator corresponding to {@link RequestPolicy#TIP}. + * + * @since 3.1 + */ + public static final class TipRequestValidator implements RequestValidator { + public void checkWants(UploadPack up, List wants) + throws PackProtocolException, IOException { + if (!up.isBiDirectionalPipe()) + new ReachableCommitTipRequestValidator().checkWants(up, wants); + else if (!wants.isEmpty()) { + Set refIds = + refIdSet(up.getRepository().getAllRefs().values()); + for (RevObject obj : wants) { + if (!refIds.contains(obj)) + throw new PackProtocolException(MessageFormat.format( + JGitText.get().wantNotValid, obj.name())); + } + } + } + } + + /** + * Validator corresponding to {@link RequestPolicy#REACHABLE_COMMIT_TIP}. + * + * @since 3.1 + */ + public static final class ReachableCommitTipRequestValidator + implements RequestValidator { + public void checkWants(UploadPack up, List wants) + throws PackProtocolException, IOException { + checkNotAdvertisedWants(up.getRevWalk(), wants, + refIdSet(up.getRepository().getAllRefs().values())); + } + } + + /** + * Validator corresponding to {@link RequestPolicy#ANY}. + * + * @since 3.1 + */ + public static final class AnyRequestValidator implements RequestValidator { + public void checkWants(UploadPack up, List wants) + throws PackProtocolException, IOException { + // All requests are valid. + } + } + + private static void checkNotAdvertisedWants(RevWalk walk, + List notAdvertisedWants, Set reachableFrom) throws MissingObjectException, IncorrectObjectTypeException, IOException { - // Walk the requested commits back to the advertised commits. - // If any commit exists, a branch was deleted or rewound and - // the repository owner no longer exports that requested item. - // If the requested commit is merged into an advertised branch - // it will be marked UNINTERESTING and no commits return. - - for (RevCommit c : notAdvertisedWants) - walk.markStart(c); - for (ObjectId id : advertised) { + // Walk the requested commits back to the provided set of commits. If any + // commit exists, a branch was deleted or rewound and the repository owner + // no longer exports that requested item. If the requested commit is merged + // into an advertised branch it will be marked UNINTERESTING and no commits + // return. + + for (RevObject obj : notAdvertisedWants) { + if (!(obj instanceof RevCommit)) + throw new PackProtocolException(MessageFormat.format( + JGitText.get().wantNotValid, obj.name())); + walk.markStart((RevCommit) obj); + } + for (ObjectId id : reachableFrom) { try { walk.markUninteresting(walk.parseCommit(id)); } catch (IncorrectObjectTypeException notCommit) {