Permalink
Browse files

Teach fetch to prune stale tracking branches

If the source branch for a local tracking branch has been removed
from the remote repository, users may also want (or need) to delete
it from the local repository.  A good example is when a branch named
"refs/heads/bar" switches from a file to a directory, and the local
path name in "refs/remotes/origin/bar" must also change.

Local deletes are done before updates, to handle this special case
of file to directory (or directory to file) conversions with as few
errors as possible.

Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
Signed-off-by: Robin Rosenberg <robin.rosenberg@dewire.com>
  • Loading branch information...
1 parent 1c6e458 commit 990f723523f24e7977d5374748b5a5cfa6448c79 @spearce spearce committed with robinrosenberg Feb 12, 2009
@@ -39,6 +39,7 @@
import org.kohsuke.args4j.Option;
import org.spearce.jgit.lib.Constants;
+import org.spearce.jgit.lib.ObjectId;
import org.spearce.jgit.lib.RefUpdate;
import org.spearce.jgit.transport.FetchResult;
import org.spearce.jgit.transport.TrackingRefUpdate;
@@ -75,9 +76,12 @@ private String longTypeOf(final TrackingRefUpdate u) {
final RefUpdate.Result r = u.getResult();
if (r == RefUpdate.Result.LOCK_FAILURE)
return "[lock fail]";
-
if (r == RefUpdate.Result.IO_FAILURE)
return "[i/o error]";
+ if (r == RefUpdate.Result.REJECTED)
+ return "[rejected]";
+ if (ObjectId.zeroId().equals(u.getNewObjectId()))
+ return "[deleted]";
if (r == RefUpdate.Result.NEW) {
if (u.getRemoteName().startsWith(Constants.R_HEADS))
@@ -99,8 +103,6 @@ else if (u.getLocalName().startsWith(Constants.R_TAGS))
return aOld + ".." + aNew;
}
- if (r == RefUpdate.Result.REJECTED)
- return "[rejected]";
if (r == RefUpdate.Result.NO_CHANGE)
return "[up to date]";
return "[" + r.name() + "]";
@@ -56,6 +56,12 @@ void nofsck(final boolean ignored) {
fsck = Boolean.FALSE;
}
+ @Option(name = "--prune", usage = "prune stale tracking refs")
+ private Boolean prune;
+
+ @Option(name = "--dry-run")
+ private boolean dryRun;
+
@Option(name = "--thin", usage = "fetch thin pack")
private Boolean thin;
@@ -75,6 +81,9 @@ protected void run() throws Exception {
final Transport tn = Transport.open(db, remote);
if (fsck != null)
tn.setCheckFetchedObjects(fsck.booleanValue());
+ if (prune != null)
+ tn.setRemoveDeletedRefs(prune.booleanValue());
+ tn.setDryRun(dryRun);
if (thin != null)
tn.setFetchThin(thin.booleanValue());
final FetchResult r;
@@ -59,6 +59,7 @@
import org.spearce.jgit.lib.ObjectId;
import org.spearce.jgit.lib.ProgressMonitor;
import org.spearce.jgit.lib.Ref;
+import org.spearce.jgit.lib.Repository;
import org.spearce.jgit.revwalk.ObjectWalk;
import org.spearce.jgit.revwalk.RevWalk;
@@ -152,6 +153,8 @@ else if (tagopt == TagOpt.FETCH_TAGS)
}
final RevWalk walk = new RevWalk(transport.local);
+ if (transport.isRemoveDeletedRefs())
+ deleteStaleTrackingRefs(result, walk);
for (TrackingRefUpdate u : localUpdates) {
try {
u.update(walk);
@@ -366,6 +369,51 @@ private TrackingRefUpdate createUpdate(final RefSpec spec,
return new TrackingRefUpdate(transport.local, spec, newId, "fetch");
}
+ private void deleteStaleTrackingRefs(final FetchResult result,
+ final RevWalk walk) throws TransportException {
+ final Repository db = transport.local;
+ for (final Ref ref : db.getAllRefs().values()) {
+ final String refname = ref.getName();
+ for (final RefSpec spec : toFetch) {
+ if (spec.matchDestination(refname)) {
+ final RefSpec s = spec.expandFromDestination(refname);
+ if (result.getAdvertisedRef(s.getSource()) == null) {
+ deleteTrackingRef(result, db, walk, s, ref);
+ }
+ }
+ }
+ }
+ }
+
+ private void deleteTrackingRef(final FetchResult result,
+ final Repository db, final RevWalk walk, final RefSpec spec,
+ final Ref localRef) throws TransportException {
+ final String name = localRef.getName();
+ try {
+ final TrackingRefUpdate u = new TrackingRefUpdate(db, name, spec
+ .getSource(), true, ObjectId.zeroId(), "deleted");
+ result.add(u);
+ if (transport.isDryRun()){
+ return;
+ }
+ u.delete(walk);
+ switch (u.getResult()) {
+ case NEW:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ case FORCED:
+ break;
+ default:
+ throw new TransportException(transport.getURI(),
+ "Cannot delete stale tracking ref " + name + ": "
+ + u.getResult().name());
+ }
+ } catch (IOException e) {
+ throw new TransportException(transport.getURI(),
+ "Cannot delete stale tracking ref " + name, e);
+ }
+ }
+
private static boolean isTag(final Ref r) {
return isTag(r.getName());
}
@@ -355,6 +355,9 @@ private static String findTrackingRefName(final String remoteName,
/** Should an incoming (fetch) transfer validate objects? */
private boolean checkFetchedObjects;
+ /** Should refs no longer on the source be pruned from the destination? */
+ private boolean removeDeletedRefs;
+
/**
* Create a new transport instance.
*
@@ -516,6 +519,30 @@ public void setPushThin(final boolean pushThin) {
}
/**
+ * @return true if destination refs should be removed if they no longer
+ * exist at the source repository.
+ */
+ public boolean isRemoveDeletedRefs() {
+ return removeDeletedRefs;
+ }
+
+ /**
+ * Set whether or not to remove refs which no longer exist in the source.
+ * <p>
+ * If true, refs at the destination repository (local for fetch, remote for
+ * push) are deleted if they no longer exist on the source side (remote for
+ * fetch, local for push).
+ * <p>
+ * False by default, as this may cause data to become unreachable, and
+ * eventually be deleted on the next GC.
+ *
+ * @param remove true to remove refs that no longer exist.
+ */
+ public void setRemoveDeletedRefs(final boolean remove) {
+ removeDeletedRefs = remove;
+ }
+
+ /**
* Apply provided remote configuration on this transport.
*
* @param cfg

0 comments on commit 990f723

Please sign in to comment.