diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
index 4ba446edbe38f4b9fdf38f3a87a0b41645fe7891..f85c22ddb5e6cd589f0ee13cf9a9ec1b74b8ee7e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
@@ -76,6 +76,7 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
+import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser;
 import org.eclipse.jgit.util.io.InterruptTimer;
 import org.eclipse.jgit.util.io.TimeoutInputStream;
 import org.eclipse.jgit.util.io.TimeoutOutputStream;
@@ -519,7 +520,7 @@ public void println() {
 
 	private void service() throws IOException {
 		if (biDirectionalPipe)
-			sendAdvertisedRefs();
+			sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut));
 		else
 			refs = db.getAllRefs();
 		recvCommands();
@@ -574,9 +575,17 @@ private void unlockPack() {
 		}
 	}
 
-	private void sendAdvertisedRefs() throws IOException {
+	/**
+	 * Generate an advertisement of available refs and capabilities.
+	 *
+	 * @param adv
+	 *            the advertisement formatter.
+	 * @throws IOException
+	 *             the formatter failed to write an advertisement.
+	 */
+	public void sendAdvertisedRefs(final RefAdvertiser adv) throws IOException {
 		final RevFlag advertised = walk.newFlag("ADVERTISED");
-		final RefAdvertiser adv = new RefAdvertiser(pckOut, walk, advertised);
+		adv.init(walk, advertised);
 		adv.advertiseCapability(CAPABILITY_DELETE_REFS);
 		adv.advertiseCapability(CAPABILITY_REPORT_STATUS);
 		if (allowOfsDelta)
@@ -589,7 +598,7 @@ private void sendAdvertisedRefs() throws IOException {
 		adv.includeAdditionalHaves();
 		if (adv.isEmpty())
 			adv.advertiseId(ObjectId.zeroId(), "capabilities^{}");
-		pckOut.end();
+		adv.end();
 	}
 
 	private void recvCommands() throws IOException {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
index 6a6053d9412bbc82ec8a8ceb7a2b22b59f416707..2a06ed889e36535e1c13ccfdb1a639a08c6f37ee 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
@@ -61,12 +61,28 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 /** Support for the start of {@link UploadPack} and {@link ReceivePack}. */
-class RefAdvertiser {
-	private final PacketLineOut pckOut;
+public abstract class RefAdvertiser {
+	static class PacketLineOutRefAdvertiser extends RefAdvertiser {
+		private final PacketLineOut pckOut;
 
-	private final RevWalk walk;
+		PacketLineOutRefAdvertiser(PacketLineOut out) {
+			pckOut = out;
+		}
+
+		@Override
+		protected void writeOne(final CharSequence line) throws IOException {
+			pckOut.writeString(line.toString());
+		}
 
-	private final RevFlag ADVERTISED;
+		@Override
+		protected void end() throws IOException {
+			pckOut.end();
+		}
+	}
+
+	private RevWalk walk;
+
+	private RevFlag ADVERTISED;
 
 	private final StringBuilder tmpLine = new StringBuilder(100);
 
@@ -78,22 +94,72 @@ class RefAdvertiser {
 
 	private boolean first = true;
 
-	RefAdvertiser(final PacketLineOut out, final RevWalk protoWalk,
-			final RevFlag advertisedFlag) {
-		pckOut = out;
+	/**
+	 * Initialize a new advertisement formatter.
+	 *
+	 * @param protoWalk
+	 *            the RevWalk used to parse objects that are advertised.
+	 * @param advertisedFlag
+	 *            flag marked on any advertised objects parsed out of the
+	 *            {@code protoWalk}'s object pool, permitting the caller to
+	 *            later quickly determine if an object was advertised (or not).
+	 */
+	public void init(final RevWalk protoWalk, final RevFlag advertisedFlag) {
 		walk = protoWalk;
 		ADVERTISED = advertisedFlag;
 	}
 
-	void setDerefTags(final boolean deref) {
+	/**
+	 * Toggle tag peeling.
+	 * <p>
+	 * <p>
+	 * This method must be invoked prior to any of the following:
+	 * <ul>
+	 * <li>{@link #send(Collection)}
+	 * <li>{@link #advertiseHave(AnyObjectId)}
+	 * <li>{@link #includeAdditionalHaves()}
+	 * </ul>
+	 *
+	 * @param deref
+	 *            true to show the dereferenced value of a tag as the special
+	 *            ref <code>$tag^{}</code> ; false to omit it from the output.
+	 */
+	public void setDerefTags(final boolean deref) {
 		derefTags = deref;
 	}
 
-	void advertiseCapability(String name) {
+	/**
+	 * Add one protocol capability to the initial advertisement.
+	 * <p>
+	 * This method must be invoked prior to any of the following:
+	 * <ul>
+	 * <li>{@link #send(Collection)}
+	 * <li>{@link #advertiseHave(AnyObjectId)}
+	 * <li>{@link #includeAdditionalHaves()}
+	 * </ul>
+	 *
+	 * @param name
+	 *            the name of a single protocol capability supported by the
+	 *            caller. The set of capabilities are sent to the client in the
+	 *            advertisement, allowing the client to later selectively enable
+	 *            features it recognizes.
+	 */
+	public void advertiseCapability(String name) {
 		capablities.add(name);
 	}
 
-	void send(final Collection<Ref> refs) throws IOException {
+	/**
+	 * Format an advertisement for the supplied refs.
+	 *
+	 * @param refs
+	 *            zero or more refs to format for the client. The collection is
+	 *            copied and sorted before display and therefore may appear in
+	 *            any order.
+	 * @throws IOException
+	 *             the underlying output stream failed to write out an
+	 *             advertisement record.
+	 */
+	public void send(final Collection<Ref> refs) throws IOException {
 		for (final Ref r : RefComparator.sort(refs)) {
 			final RevObject obj = parseAnyOrNull(r.getObjectId());
 			if (obj != null) {
@@ -104,7 +170,21 @@ void send(final Collection<Ref> refs) throws IOException {
 		}
 	}
 
-	void advertiseHave(AnyObjectId id) throws IOException {
+	/**
+	 * Advertise one object is available using the magic {@code .have}.
+	 * <p>
+	 * The magic {@code .have} advertisement is not available for fetching by a
+	 * client, but can be used by a client when considering a delta base
+	 * candidate before transferring data in a push. Within the record created
+	 * by this method the ref name is simply the invalid string {@code .have}.
+	 *
+	 * @param id
+	 *            identity of the object that is assumed to exist.
+	 * @throws IOException
+	 *             the underlying output stream failed to write out an
+	 *             advertisement record.
+	 */
+	public void advertiseHave(AnyObjectId id) throws IOException {
 		RevObject obj = parseAnyOrNull(id);
 		if (obj != null) {
 			advertiseAnyOnce(obj, ".have");
@@ -113,7 +193,14 @@ void advertiseHave(AnyObjectId id) throws IOException {
 		}
 	}
 
-	void includeAdditionalHaves() throws IOException {
+	/**
+	 * Include references of alternate repositories as {@code .have} lines.
+	 *
+	 * @throws IOException
+	 *             the underlying output stream failed to write out an
+	 *             advertisement record.
+	 */
+	public void includeAdditionalHaves() throws IOException {
 		additionalHaves(walk.getRepository().getObjectDatabase());
 	}
 
@@ -129,7 +216,8 @@ private void additionalHaves(final Repository alt) throws IOException {
 			advertiseHave(r.getObjectId());
 	}
 
-	boolean isEmpty() {
+	/** @return true if no advertisements have been sent yet. */
+	public boolean isEmpty() {
 		return first;
 	}
 
@@ -172,7 +260,22 @@ private void advertiseTag(final RevTag tag, final String refName)
 		advertiseAny(tag.getObject(), refName);
 	}
 
-	void advertiseId(final AnyObjectId id, final String refName)
+	/**
+	 * Advertise one object under a specific name.
+	 * <p>
+	 * If the advertised object is a tag, this method does not advertise the
+	 * peeled version of it.
+	 *
+	 * @param id
+	 *            the object to advertise.
+	 * @param refName
+	 *            name of the reference to advertise the object as, can be any
+	 *            string not including the NUL byte.
+	 * @throws IOException
+	 *             the underlying output stream failed to write out an
+	 *             advertisement record.
+	 */
+	public void advertiseId(final AnyObjectId id, final String refName)
 			throws IOException {
 		tmpLine.setLength(0);
 		id.copyTo(tmpId, tmpLine);
@@ -190,6 +293,27 @@ void advertiseId(final AnyObjectId id, final String refName)
 			}
 		}
 		tmpLine.append('\n');
-		pckOut.writeString(tmpLine.toString());
+		writeOne(tmpLine);
 	}
+
+	/**
+	 * Write a single advertisement line.
+	 *
+	 * @param line
+	 *            the advertisement line to be written. The line always ends
+	 *            with LF. Never null or the empty string.
+	 * @throws IOException
+	 *             the underlying output stream failed to write out an
+	 *             advertisement record.
+	 */
+	protected abstract void writeOne(CharSequence line) throws IOException;
+
+	/**
+	 * Mark the end of the advertisements.
+	 *
+	 * @throws IOException
+	 *             the underlying output stream failed to write out an
+	 *             advertisement record.
+	 */
+	protected abstract void end() throws IOException;
 }
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 fe2afbe9b752b0c51ab085f41c7fdea640ebff52..6b81bc4925646c9d1fc8cae2f84ea14982686ad8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
@@ -70,6 +70,7 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser;
 import org.eclipse.jgit.util.io.InterruptTimer;
 import org.eclipse.jgit.util.io.TimeoutInputStream;
 import org.eclipse.jgit.util.io.TimeoutOutputStream;
@@ -282,7 +283,7 @@ public void upload(final InputStream input, final OutputStream output,
 
 	private void service() throws IOException {
 		if (biDirectionalPipe)
-			sendAdvertisedRefs();
+			sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut));
 		else {
 			refs = db.getAllRefs();
 			for (Ref r : refs.values()) {
@@ -309,8 +310,16 @@ else if (options.contains(OPTION_MULTI_ACK))
 			sendPack();
 	}
 
-	private void sendAdvertisedRefs() throws IOException {
-		final RefAdvertiser adv = new RefAdvertiser(pckOut, walk, ADVERTISED);
+	/**
+	 * Generate an advertisement of available refs and capabilities.
+	 *
+	 * @param adv
+	 *            the advertisement formatter.
+	 * @throws IOException
+	 *             the formatter failed to write an advertisement.
+	 */
+	public void sendAdvertisedRefs(final RefAdvertiser adv) throws IOException {
+		adv.init(walk, ADVERTISED);
 		adv.advertiseCapability(OPTION_INCLUDE_TAG);
 		adv.advertiseCapability(OPTION_MULTI_ACK_DETAILED);
 		adv.advertiseCapability(OPTION_MULTI_ACK);
@@ -322,7 +331,7 @@ private void sendAdvertisedRefs() throws IOException {
 		adv.setDerefTags(true);
 		refs = db.getAllRefs();
 		adv.send(refs.values());
-		pckOut.end();
+		adv.end();
 	}
 
 	private void recvWants() throws IOException {