-
Notifications
You must be signed in to change notification settings - Fork 81
Open
Description
Version
6.8.0
Operating System
MacOS
Bug description
When using multiple wildcard refspecs that map to the same destination pattern with setRemoveDeletedRefs(true), JGit incorrectly prunes refs that are:
- Advertised by the remote server
- Already up-to-date (local SHA == remote SHA)
- Matching the wildcard destination pattern
This causes refs to alternate between created and deleted on subsequent fetches.
Actual behavior
Run 1:
- Remote advertises: refs/merge-requests/42/head → abc123
- RefSpec 2 matches and creates: refs/remotes/merge-requests/42/head → abc123
- TrackingRefUpdate status: NEW ✓
Run 2 (unchanged remote):
- Remote advertises: refs/merge-requests/42/head → abc123 (same SHA)
- RefSpec 1: Match, but up-to-date (not added to TrackingRefUpdates)
- RefSpec 2: No match (checks refs/merge-requests/*/from)
- Prune logic: "Local ref refs/remotes/merge-requests/42/head wasn't updated by ANY refspec → must be deleted!"
- TrackingRefUpdate status: FORCED to 00000000... ✗
- Result: ALL REFS DELETED ✗
Run 3:
- Same as Run 1 - refs recreated
- Pattern repeats: create → delete → create → delete...
Expected behavior
When fetching with multiple wildcard refspecs like +refs/merge-requests/*/head:refs/remotes/merge-requests/*/head and +refs/merge-requests/*/from:refs/remotes/merge-requests/*/head:
Run 1:
- Remote advertises: refs/merge-requests/42/head → abc123
- RefSpec matches and creates: refs/remotes/merge-requests/42/head → abc123
- Result: Ref created ✓
Run 2 (unchanged remote):
- Remote advertises: refs/merge-requests/42/head → abc123 (same SHA)
- Local has: refs/remotes/merge-requests/42/head → abc123 (up-to-date)
- Result: No changes, ref remains ✓
Run 3:
- Result: Still stable ✓
Relevant log output
Other information
Impact
Affects any application using JGit with:
- Multiple wildcard refspecs mapping to the same destination
- Pruning enabled
- Real-world use case: Cross-platform merge request/pull request support (GitHub + GitLab + others)
Minimal reproduction example:
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.TrackingRefUpdate;
public class JGitWildcardPruneBug {
public static void main(String[] args) throws Exception {
String remoteUrl = "https://gitlab.com/gitlab-org/gitlab-runner.git";
try (TempDirectory tempDir = new TempDirectory("jgit-bug-test")) {
try (Git git = Git.cloneRepository().setURI(remoteUrl).setDirectory(tempDir.getPath().toFile())
.setCloneAllBranches(false).call()) {
// Add MULTIPLE refspecs that overlap in destination
RefSpec wildcardRefSpec1 = new RefSpec("+refs/merge-requests/*/head:refs/merge-requests/*/head");
RefSpec wildcardRefSpec2 = new RefSpec("+refs/merge-requests/*/from:refs/merge-requests/*/head");
for (int run = 1; run <= 3; run++) {
System.out.println("=== RUN " + run + " ===");
FetchResult result1 = git.fetch().setRefSpecs(Arrays.asList(wildcardRefSpec1, wildcardRefSpec2))
.setRemoveDeletedRefs(true).call();
printFetchResult(result1);
printLocalMergeRequestRefs(git, "AFTER RUN " + run);
System.out.println();
}
}
}
}
private static void printFetchResult(FetchResult result) {
Collection<TrackingRefUpdate> updates = result.getTrackingRefUpdates();
long mrUpdates = updates.stream().filter(u -> u.getLocalName().contains("merge-requests")).count();
if (mrUpdates > 0) {
System.out.println(" - MR ref updates: " + mrUpdates);
updates.stream().filter(u -> u.getLocalName().contains("merge-requests")).limit(3).forEach(update -> {
System.out.printf(" %s: %s -> %s (%s)%n", update.getLocalName(),
update.getOldObjectId().abbreviate(7).name(), update.getNewObjectId().abbreviate(7).name(),
update.getResult());
});
if (mrUpdates > 3) {
System.out.println(" ... (" + (mrUpdates - 3) + " more)");
}
} else {
System.out.println(" - No MR ref updates");
}
}
private static void printLocalMergeRequestRefs(Git git, String label) throws Exception {
Collection<Ref> refs = git.getRepository().getRefDatabase().getRefs();
long mrCount = refs.stream().filter(ref -> ref.getName().contains("merge-requests")).count();
System.out.println(label + " - Local MR refs: " + mrCount);
if (mrCount == 0) {
System.out.println("ALL MERGE REQUEST REFS WERE DELETED!");
}
}
static class TempDirectory implements AutoCloseable {
private final Path path;
public TempDirectory(String prefix) throws IOException {
this.path = Files.createTempDirectory(prefix);
}
public Path getPath() {
return path;
}
@Override
public void close() throws IOException {
if (Files.exists(path)) {
Files.walk(path).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
}
}
}
}
Expected Output
=== RUN 1 ===
- MR ref updates: 1720
refs/merge-requests/1004/head: 0000000 -> f4a3dad (NEW)
refs/merge-requests/1005/head: 0000000 -> 5ac68e9 (NEW)
refs/merge-requests/1006/head: 0000000 -> 88ff5e7 (NEW)
... (1717 more)
AFTER RUN 1 - Local MR refs: 1720
=== RUN 2 ===
- No MR ref updates
AFTER RUN 2 - Local MR refs: 1720
=== RUN 3 ===
- No MR ref updates
AFTER RUN 3 - Local MR refs: 1720
Actual Output
=== RUN 1 ===
- MR ref updates: 1720
refs/merge-requests/1004/head: 0000000 -> f4a3dad (NEW)
refs/merge-requests/1005/head: 0000000 -> 5ac68e9 (NEW)
refs/merge-requests/1006/head: 0000000 -> 88ff5e7 (NEW)
... (1717 more)
AFTER RUN 1 - Local MR refs: 1720
=== RUN 2 ===
- MR ref updates: 1720
refs/merge-requests/1004/head: f4a3dad -> 0000000 (FORCED)
refs/merge-requests/1005/head: 5ac68e9 -> 0000000 (FORCED)
refs/merge-requests/1006/head: 88ff5e7 -> 0000000 (FORCED)
... (1717 more)
AFTER RUN 2 - Local MR refs: 0
ALL MERGE REQUEST REFS WERE DELETED!
=== RUN 3 ===
- MR ref updates: 1720
refs/merge-requests/1004/head: 0000000 -> f4a3dad (NEW)
refs/merge-requests/1005/head: 0000000 -> 5ac68e9 (NEW)
refs/merge-requests/1006/head: 0000000 -> 88ff5e7 (NEW)
... (1717 more)
AFTER RUN 3 - Local MR refs: 1720
Metadata
Metadata
Assignees
Labels
No labels