Add RequestPolicy.TIP to allow fetching non-advertised ref tips

Users of UploadPack may set a custom RefFilter or AdvertisedRefsHook
that limits which refs are advertised, but clients may learn of a
SHA-1 that the server should have as a ref tip through some
alternative means. Support serving such objects from the server side
with a new RequestPolicy.

As with ADVERTISED, we need a special relaxed RequestPolicy to allow
commits reachable from the set of valid tips for unidirectional
connections.

Change-Id: I0d0cc4f8ee04d265e5be8221b9384afb1b374315
This commit is contained in:
Dave Borowitz 2013-06-18 14:58:44 -07:00
parent 21b3a16ab7
commit fee679b587
1 changed files with 73 additions and 21 deletions

View File

@ -49,6 +49,7 @@
import java.io.OutputStream; import java.io.OutputStream;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -113,8 +114,27 @@ public class UploadPack {
public static enum RequestPolicy { public static enum RequestPolicy {
/** Client may only ask for objects the server advertised a reference for. */ /** Client may only ask for objects the server advertised a reference for. */
ADVERTISED, 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, REACHABLE_COMMIT,
/**
* Client may ask for objects that are the tip of some reference, even if
* that reference wasn't advertised.
* <p>
* 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. */ /** Client may ask for any SHA-1 in the repository. */
ANY; ANY;
} }
@ -363,8 +383,12 @@ public boolean isBiDirectionalPipe() {
*/ */
public void setBiDirectionalPipe(final boolean twoWay) { public void setBiDirectionalPipe(final boolean twoWay) {
biDirectionalPipe = twoWay; biDirectionalPipe = twoWay;
if (!biDirectionalPipe && requestPolicy == RequestPolicy.ADVERTISED) if (!biDirectionalPipe) {
requestPolicy = RequestPolicy.REACHABLE_COMMIT; if (requestPolicy == RequestPolicy.ADVERTISED)
requestPolicy = RequestPolicy.REACHABLE_COMMIT;
else if (requestPolicy == RequestPolicy.TIP)
requestPolicy = RequestPolicy.REACHABLE_COMMIT_TIP;
}
} }
/** @return policy used by the service to validate client requests. */ /** @return policy used by the service to validate client requests. */
@ -378,8 +402,9 @@ public RequestPolicy getRequestPolicy() {
* By default the policy is {@link RequestPolicy#ADVERTISED}, * By default the policy is {@link RequestPolicy#ADVERTISED},
* which is the Git default requiring clients to only ask for an * which is the Git default requiring clients to only ask for an
* object that a reference directly points to. This may be relaxed * object that a reference directly points to. This may be relaxed
* to {@link RequestPolicy#REACHABLE_COMMIT} when callers * to {@link RequestPolicy#REACHABLE_COMMIT} or
* have {@link #setBiDirectionalPipe(boolean)} set to false. * {@link RequestPolicy#REACHABLE_COMMIT_TIP} when callers have
* {@link #setBiDirectionalPipe(boolean)} set to false.
*/ */
public void setRequestPolicy(RequestPolicy policy) { public void setRequestPolicy(RequestPolicy policy) {
requestPolicy = policy != null ? policy : RequestPolicy.ADVERTISED; requestPolicy = policy != null ? policy : RequestPolicy.ADVERTISED;
@ -560,13 +585,8 @@ private void service() throws IOException {
sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut)); sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut));
else if (requestPolicy == RequestPolicy.ANY) else if (requestPolicy == RequestPolicy.ANY)
advertised = Collections.emptySet(); advertised = Collections.emptySet();
else { else
advertised = new HashSet<ObjectId>(); advertised = refIdSet(getAdvertisedOrDefaultRefs().values());
for (Ref ref : getAdvertisedOrDefaultRefs().values()) {
if (ref.getObjectId() != null)
advertised.add(ref.getObjectId());
}
}
boolean sendPack; boolean sendPack;
try { try {
@ -618,6 +638,15 @@ else if (requestPolicy == RequestPolicy.ANY)
sendPack(); sendPack();
} }
private static Set<ObjectId> refIdSet(Collection<Ref> refs) {
Set<ObjectId> ids = new HashSet<ObjectId>(refs.size());
for (Ref ref : refs) {
if (ref.getObjectId() != null)
ids.add(ref.getObjectId());
}
return ids;
}
private void reportErrorDuringNegotiate(String msg) { private void reportErrorDuringNegotiate(String msg) {
try { try {
pckOut.writeString("ERR " + msg + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ pckOut.writeString("ERR " + msg + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
@ -922,6 +951,8 @@ private void parseWants() throws IOException {
AsyncRevObjectQueue q = walk.parseAny(wantIds, true); AsyncRevObjectQueue q = walk.parseAny(wantIds, true);
try { try {
List<RevCommit> checkReachable = null; List<RevCommit> checkReachable = null;
Set<ObjectId> reachableFrom = null;
Set<ObjectId> tips = null;
RevObject obj; RevObject obj;
while ((obj = q.next()) != null) { while ((obj = q.next()) != null) {
if (!advertised.contains(obj)) { if (!advertised.contains(obj)) {
@ -935,8 +966,28 @@ private void parseWants() throws IOException {
throw new PackProtocolException(MessageFormat.format( throw new PackProtocolException(MessageFormat.format(
JGitText.get().wantNotValid, obj)); JGitText.get().wantNotValid, obj));
} }
if (checkReachable == null) if (checkReachable == null) {
checkReachable = new ArrayList<RevCommit>(); checkReachable = new ArrayList<RevCommit>();
reachableFrom = advertised;
}
checkReachable.add((RevCommit) obj);
break;
case TIP:
if (tips == null)
tips = refIdSet(db.getAllRefs().values());
if (!tips.contains(obj))
throw new PackProtocolException(MessageFormat.format(
JGitText.get().wantNotValid, obj));
break;
case REACHABLE_COMMIT_TIP:
if (!(obj instanceof RevCommit)) {
throw new PackProtocolException(MessageFormat.format(
JGitText.get().wantNotValid, obj));
}
if (checkReachable == null) {
checkReachable = new ArrayList<RevCommit>();
reachableFrom = refIdSet(db.getAllRefs().values());
}
checkReachable.add((RevCommit) obj); checkReachable.add((RevCommit) obj);
break; break;
case ANY: case ANY:
@ -954,7 +1005,7 @@ private void parseWants() throws IOException {
} }
} }
if (checkReachable != null) if (checkReachable != null)
checkNotAdvertisedWants(checkReachable); checkNotAdvertisedWants(checkReachable, reachableFrom);
wantIds.clear(); wantIds.clear();
} catch (MissingObjectException notFound) { } catch (MissingObjectException notFound) {
ObjectId id = notFound.getObjectId(); ObjectId id = notFound.getObjectId();
@ -972,17 +1023,18 @@ private void want(RevObject obj) {
} }
} }
private void checkNotAdvertisedWants(List<RevCommit> notAdvertisedWants) private void checkNotAdvertisedWants(List<RevCommit> notAdvertisedWants,
Set<ObjectId> reachableFrom)
throws MissingObjectException, IncorrectObjectTypeException, IOException { throws MissingObjectException, IncorrectObjectTypeException, IOException {
// Walk the requested commits back to the advertised commits. // Walk the requested commits back to the provided set of commits. If any
// If any commit exists, a branch was deleted or rewound and // commit exists, a branch was deleted or rewound and the repository owner
// the repository owner no longer exports that requested item. // no longer exports that requested item. If the requested commit is merged
// If the requested commit is merged into an advertised branch // into an advertised branch it will be marked UNINTERESTING and no commits
// it will be marked UNINTERESTING and no commits return. // return.
for (RevCommit c : notAdvertisedWants) for (RevCommit c : notAdvertisedWants)
walk.markStart(c); walk.markStart(c);
for (ObjectId id : advertised) { for (ObjectId id : reachableFrom) {
try { try {
walk.markUninteresting(walk.parseCommit(id)); walk.markUninteresting(walk.parseCommit(id));
} catch (IncorrectObjectTypeException notCommit) { } catch (IncorrectObjectTypeException notCommit) {