Negative fetch refspecs only affect negotiation: excluded refs are still updated, written to FETCH_HEAD, and pruned · Issue #7435 · libgit2/libgit2 · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Negative fetch refspecs only affect negotiation: excluded refs are still updated, written to FETCH_HEAD, and pruned #7435
A negative refspec (^refs/heads/private) in a fetch is honoured when libgit2 picks the refs to want (filter_wants → git_remote__matching_refspec, the only place negatives are checked). It is not honoured after the download:
Ref updates and FETCH_HEAD.update_one_tip matches every advertised ref against every active refspec with plain git_refspec_src_matches(). The positive wildcard therefore still creates or updates the remote-tracking ref of an excluded branch whenever that branch's objects are present, which they are whenever it shares history with a fetched branch. The negative refspec itself has no destination, so it is treated as "store in FETCH_HEAD" and adds the excluded ref to FETCH_HEAD. git_remote_write_fetchhead matches the same way.
Pruning.git_remote_prune decides a remote-tracking ref is stale without consulting negative refspecs. So once the remote deletes an excluded branch, its remote-tracking ref is pruned. git leaves it alone: refspec_find_all_matches finds no match for a ref a negative refspec excludes, so get_stale_heads doesn't consider it.
git applies negative refspecs to everything it fetches (apply_negative_refspecs in get_ref_map), except tag auto-following.
Reproduction steps
A C program that fetches origin the way git fetch [--prune] does, with git_remote_fetch(remote, NULL, &opts, NULL):
fetchrepro.c
/* * Fetch from "origin" as `git fetch [--prune] origin [<refspec>...]` * would: git_remote_fetch() with the given refspecs, or with NULL (the * remote's configured refspecs) when there are none. * * usage: fetchrepro <repository> [--prune] [<refspec>...] */#include<stdio.h>#include<string.h>#include<git2.h>staticintcheck(interror, constchar*what)
{
if (error<0) {
constgit_error*e=git_error_last();
fprintf(stderr, "%s: %s\n", what, e ? e->message : "unknown error");
return1;
}
return0;
}
intmain(intargc, char**argv)
{
git_repository*repo=NULL;
git_remote*remote=NULL;
git_fetch_optionsopts=GIT_FETCH_OPTIONS_INIT;
git_strarrayrefspecs= { NULL, 0 };
intfirst=2, failed;
if (argc<2) {
fprintf(stderr, "usage: %s <repository> [--prune] [<refspec>...]\n", argv[0]);
return2;
}
if (argc>2&& !strcmp(argv[2], "--prune")) {
opts.prune=GIT_FETCH_PRUNE;
first=3;
}
refspecs.strings=argv+first;
refspecs.count= (size_t)(argc-first);
git_libgit2_init();
failed=check(git_repository_open(&repo, argv[1]), "open") ||check(git_remote_lookup(&remote, repo, "origin"), "lookup") ||check(git_remote_fetch(remote, refspecs.count ? &refspecs : NULL, &opts, NULL), "fetch");
git_remote_free(remote);
git_repository_free(repo);
git_libgit2_shutdown();
returnfailed;
}
A script that runs the same fetches with git and with fetchrepro on identical clones:
As with git 2.54, origin/private is neither created by the fetch nor removed by the prune, and FETCH_HEAD doesn't list it.
Actual behavior
### 1. fetch with remote.origin.fetch = +refs/heads/*:refs/remotes/origin/*, ^refs/heads/private
--- git: remote-tracking refs
refs/remotes/origin/HEAD
refs/remotes/origin/gone
refs/remotes/origin/main
--- git: FETCH_HEAD
branch 'main' of src
not-for-merge branch 'gone' of src
--- libgit2: remote-tracking refs
refs/remotes/origin/HEAD
refs/remotes/origin/gone
refs/remotes/origin/main
refs/remotes/origin/private
--- libgit2: FETCH_HEAD
branch 'main' of src
not-for-merge branch 'gone' of src
not-for-merge branch 'private' of src
### 2. fetch --prune after the remote deletes "private" and "gone"
--- git-prune: remote-tracking refs
refs/remotes/origin/HEAD
refs/remotes/origin/main
refs/remotes/origin/private
--- libgit2-prune: remote-tracking refs
refs/remotes/origin/HEAD
refs/remotes/origin/main
In libgit2's clone, the fetch creates refs/remotes/origin/private and lists branch 'private' in FETCH_HEAD. fetch --prune then deletes refs/remotes/origin/private.
Version of libgit2 (release number or SHA1)
v1.9.4. The code involved is unchanged in v1.9.7 and on main (42a89d7), which the links above point to.
Operating system(s) tested
macOS 27 (arm64)
For reference, GitKit, which packages libgit2 for SwiftPM, works around this by compiling remote.c through a shim (Cocoanetics/GitKit#45). The shim routes remote.c's git_refspec_src_matches() calls through a matcher that never lets a negative refspec select a ref, and that drops a ref excluded by a negative refspec of the same set (the fetch's refspecs, or the configured ones for opportunistic updates). The tag auto-follow probe is left alone. In git_remote_prune, the shim counts an excluded remote ref as still present. I'm happy to turn that into a proper patch if that direction suits you.
Activity
Matching push compares advertised names as bytes; link libgit2 issues
Fetch and push a set of refspecs at once, as git applies them (#45)
Report pushes git refuses, and fetches, the way git does (#46)