Skip to content

feat: add Remote::default_branch() to learn the default branch of a remote without connecting to it - #3037

Open
Amey Pawar (ameyypawar) wants to merge 1 commit into
GitoxideLabs:mainfrom
ameyypawar:gix-remote-default-branch
Open

Amey Pawar (ameyypawar) wants to merge 1 commit into
GitoxideLabs:mainfrom
ameyypawar:gix-remote-default-branch

Conversation

@ameyypawar

Copy link
Copy Markdown
Contributor

Created by Claude Code on behalf of Amey, who reviewed it before submitting. Everything below this line is the agent's writing, not his.


Summary

  • Add Remote::default_branch(), as proposed in Add a Remote::default_branch() function #2792 (originally Add a Remote::default_branch() function #755). It returns the default branch of a remote, like refs/heads/main, as recorded in refs/remotes/<name>/HEAD by git clone, git remote set-head and, since Git 2.48, git fetch. It doesn't connect to the remote, which makes it the offline variant mentioned in Add a Remote::default_branch() function #755.

  • The target of refs/remotes/<name>/HEAD, like refs/remotes/origin/main, is mapped back to the branch on the remote with the fetch refspecs of the remote, so refspecs that rename branches work too.

  • None is returned in these cases:

    • the remote has no name
    • refs/remotes/<name>/HEAD is missing or not symbolic
    • no fetch refspec produces its target

    It's an error if more than one remote reference maps to it.

  • It returns the name on the remote, like git2::Remote::default_branch(), which takes it from the HEAD advertised by a connected remote (git_remote__default_branch()). It carries a git2 alias. The remote-tracking branch, which git symbolic-ref refs/remotes/origin/HEAD prints, remains available as the target of refs/remotes/<name>/HEAD.

Git baseline

Git 2.52.0 was run on the new remote-default-branch fixture. The fixture is a git clone of base plus remotes that change refs/remotes/<name>/HEAD or their fetch refspecs, and none of them is ever fetched. git branch --track probe refs/remotes/<name>/HEAD shows Git's own reverse mapping in branch.probe.merge, which comes from remote_find_tracking() → refspec_find_match().

remote refs/remotes/<name>/HEAD → fetch refspecs branch.probe.merge default_branch()
origin refs/remotes/origin/main +refs/heads/*:refs/remotes/origin/* refs/heads/main refs/heads/main
other-head refs/remotes/other-head/a +refs/heads/*:refs/remotes/other-head/* refs/heads/a refs/heads/a
renamed refs/remotes/renamed/default +refs/heads/main:refs/remotes/renamed/default refs/heads/main refs/heads/main
team/origin refs/remotes/team/origin/main +refs/heads/*:refs/remotes/team/origin/* refs/heads/main refs/heads/main
unmapped refs/remotes/unmapped/main +refs/heads/a:refs/remotes/unmapped/a not a branch None
ambiguous refs/remotes/ambiguous/main +refs/heads/*:refs/remotes/ambiguous/*
+refs/tags/*:refs/remotes/ambiguous/*
refs/heads/main error

The fixture has three more remotes:

  • dangling points to a deleted remote-tracking branch. git branch refuses it, while default_branch() still maps the name to refs/heads/main, much like git symbolic-ref still prints dangling targets.
  • detached, whose HEAD isn't symbolic, returns None.
  • no-head returns None.

For origin, the test also checks the result against git ls-remote --symref origin HEAD, which prints ref: refs/heads/main HEAD.

The ambiguous row deviates from Git on purpose. Git uses the first matching refspec in configuration order, but Repository::try_find_remote() sorts and deduplicates refspecs, so that order is gone. With +refs/tags/*:… configured before +refs/heads/*:…, Git answers refs/tags/main, while a first-match version of this method answered refs/heads/main. So ambiguity is an error, as in upstream_branch_and_remote_for_tracking_branch().

Validation

  • cargo test -p gix --tests passes for every target, with 490 tests in tests/gix including the 3 new ones.
  • GIX_TEST_FIXTURE_HASH=sha256 cargo test -p gix --test gix passes all 490 tests.
  • cargo test -p gix --doc -- remote passes all 6 doctests. They share make_remote_repos.sh, whose generated archive is git-ignored.
  • cargo fmt --all -- --check, cargo clippy -p gix --all-targets (no findings in the changed files), cargo check -p gix --no-default-features --features sha1, and RUSTDOCFLAGS="-D warnings" cargo doc -p gix --no-deps --features blocking-network-client all pass.
  • Each of these 8 mutations of default_branch() makes a new test fail:
    1. returning the remote-tracking branch unmapped
    2. stripping refs/remotes/<name>/ instead of using the refspecs
    3. accepting an ambiguous mapping
    4. requiring the remote-tracking branch to exist
    5. mapping a non-symbolic HEAD by its own name
    6. falling back to origin for anonymous remotes
    7. failing on names that can't be part of a reference name
    8. handling only remote::Name::Symbol, even though names with a / are classified as remote::Name::Url

Not addressed here

  • gix clones store refs/remotes/<remote>/HEAD as a direct reference, while git clone makes it symbolic. gix/tests/gix/clone.rs asserts that "remote HEAD is stored as the peeled object id advertised by the remote". So default_branch() returns None in repositories cloned by gix, as its docs note. That stays true until clone writes the symbolic reference like git clone does.
  • gix doesn't create or update refs/remotes/<name>/HEAD on fetch. git fetch has done that since Git 2.48, controlled by remote.<name>.followRemoteHEAD.
  • Remote::refspecs() documents "order of occurrence in the configuration", and branch_remote_tracking_ref_name() documents first-match in configuration order. But try_find_remote() sorts the refspecs.
  • MatchGroup::match_rhs() drops only the mappings that a negative refspec excludes, while Git's refspec_find_negative_match() fails the whole lookup. This already applies to upstream_branch_and_remote_for_tracking_branch().
  • auto_hidden_revisions() in gix-tix derives the same thing with upstream_branch_and_remote_for_tracking_branch() and could use this method instead.

Happy to follow up on any of these separately.

Comment thread gix/src/remote/access.rs Fixed
Comment thread gix/src/remote/access.rs Fixed
… remote without connecting to it.

`git clone` and `git remote set-head` record the default branch of a remote in the symbolic reference
`refs/remotes/<name>/HEAD`, which points to the remote-tracking branch of that branch, like
`refs/remotes/origin/main`. Since Git 2.48, `git fetch` also creates it if it's missing.

`Remote::default_branch()` maps the target of that reference back to the branch on the remote with the
fetch refspecs of the remote, and returns names like `refs/heads/main`. That's the same kind of name that
`git2::Remote::default_branch()` returns, but `git2` asks the connected remote for its `HEAD`, while this
works offline with what was recorded locally.

It's an error if the fetch refspecs map more than one remote reference to the remote-tracking branch.
Git would use the first matching refspec in configuration order, but the refspecs of a `Remote` are sorted
when they are read from the configuration, so that order isn't known anymore.

(see GitoxideLabs#2792)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants