Page MenuHomePhabricator

Hosted repos could set default branch in HEAD to help new clones
Closed, ResolvedPublic

Description

Our organization does not use "master" as the default branch. We host projects under Diffusion and set the default branch as appropriate. This works for the web interface and it's all great.

When a user clones the repo the following warning is generated

warning: remote HEAD refers to nonexistent ref, unable to checkout.

This requires the user to issue a checkout command like git checkout correct-branch before their clone is usable. Ultimately this isn't a big deal, but it would be nice if setting the default branch in Diffusion for a hosted repo would also set the bare repo's HEAD to point to that branch.

Having access to the bare repo I can fix it with git symbolic-ref HEAD refs/heads/correct-branch as in http://stackoverflow.com/a/3302018/859353

Event Timeline

jacobwalker0814 raised the priority of this task from to Needs Triage.
jacobwalker0814 updated the task description. (Show Details)
jacobwalker0814 added a project: Diffusion.
jacobwalker0814 added a subscriber: jacobwalker0814.

I can confirm this issue.

No matter if master exists or not, changing the phabricator repository setting will not change the bare git repositorys HEAD.
This also means that once a master branch has been pushed, it can not be deleted by force pushing, as git itself will prevent removal of a branch that HEAD points to.

+1 the problem is still here. There's no way for a end user to make it right without the need for an admin to go run a command on the server, which is not very good.

@aubort, please use the community forum (see: Discourse) if you'd like to discuss this issue.

If you'd like to understand upstream timelines and priorities, see Planning.

See also PHI1011. I think the fix here is likely easy, but I'd like more clarity on:

  1. What Git commands cause HEAD to be written as a side effect? First branch pushed? git clone of an observed repository? Anything else?
  2. Does modern git actually prevent you from deleting the ref that HEAD points at?
  3. What happens if we point HEAD at a nonsense ref?
  4. What happens if we git symbolic-ref --delete HEAD?

As long as there are no weird surprises in the answer to (1), the fix here is probably git symbolic-ref HEAD <default branch> during the normal daemon update process, with some possible checks and guardrails based on the answers to (3) and (4).

If the answer to (2) is "yes", we should detect that a commit deletes the HEAD ref and remove HEAD before accepting the commit. This may be quite difficult if hooks don't get to run before the HEAD check runs.

If the answer to (2) is "yes", we should detect that a commit deletes the HEAD ref and remove HEAD before accepting the commit. This may be quite difficult if hooks don't get to run before the HEAD check runs.

A [bare] repositorys HEAD never points to a commit hash but to a branch ref instead. So I don’t see why that logic would be necessary. Pushing to the referenced branch never changes the ref to the branch itself. Only branch deletion does/can.

Concerning 2.: I can't guarantee anything or give version numbers, but AFAIR git has always prevented deleting the default branch [on the remote repository].

Creating a bare repository with git creates HEAD pointing to refs/heads/master with no such branch existing.

Cloning gives a notice that you seem to have cloned an empty repository.

Pushing a different default branch does not automatically change HEAD to point to that branch. It stays pointing to the non-existent master branch.

Cloning the repository in that state clones with master as the default branch, as empty, no commits in master, and displays a warning “warning: remote HEAD refers to nonexistent ref, unable to checkout.”.

Manually removing the HEAD file makes the remote an invalid git repository. Cloning yields a fatal error:

fatal: 'D:\test\re' does not appear to be a git repository
fatal: Could not read from remote repository.

Tested with git version 2.19.1.windows.1 (and TortoiseGit 2.7.0.0).

So I don’t see why that logic would be necessary. ... Pushing to the referenced branch never changes the ref to the branch itself. Only branch deletion does/can.

Specifically, I want to make sure this still works, i.e. completely deleting the branch master in the remote:

$ git push origin :master

...when the origin is hosted by Phabricator. This is a valid Git operation and should be permitted, even if it isn't especially common. (The "Allow Dangerous Changes" stuff already provides guard rails for users doing this by accident.)

Creating a bare repository with git creates HEAD pointing to refs/heads/master with no such branch existing.

In PHI1011, a hosted instance had a bare repository with HEAD pointed at refs/heads/something-else although we've never set HEAD explicitly as far as I know. This leads me to believe that creating a bare repository with git clone --bare ... does not point HEAD at master in all cases. You're presumably right for "creating a bare repository [with git init]", but Phabricator users can create a repository by "Observing" an existing remote (so the bare repository is created with git clone --bare ...), then switching the repository to "Hosted". We need to anticipate/handle this case.

Empirically, on modern git:

What happens if we point HEAD at a nonsense ref?

  • You can git symbolic-ref HEAD refs/xyz where refs/xyz does not actually exist, the argument just needs to start with refs/xyz.
  • An invalid HEAD doesn't cause any problems with git ls-remote. It doesn't cause any different/worse problems with git clone than the original warning. So this seems "mostly fine".

Does modern git actually prevent you from deleting the ref that HEAD points at?

Git treats attempts to delete the ref pointed at HEAD as an error, which is a problem:

remote: error: By default, deleting the current branch is denied, because the next
remote: 'git clone' won't result in any file checked out, causing confusion.
remote: 
remote: You can set 'receive.denyDeleteCurrent' configuration variable to
remote: 'warn' or 'ignore' in the remote repository to allow deleting the
remote: current branch, with or without a warning message.
remote: 
remote: To squelch this message, you can set it to 'refuse'.
remote: error: refusing to delete the current branch: refs/tags/quack2

We can fix this by setting receive.denyDeleteCurrent to ignore.

What happens if we git symbolic-ref --delete HEAD?

This is forbidden.

$ git symbolic-ref --delete HEAD
fatal: deleting 'HEAD' is not allowed

This leads me to believe that creating a bare repository with git clone --bare ... does not point HEAD at master in all cases.

  • Doing a --bare clone of a repository with HEAD pointed at a valid refs/heads/X branch leaves HEAD pointed at the same branch in the clone. So this is a plausible explanation for cases where we have seen HEAD pointed somewhere other than refs/heads/master even though we never run git symbolic-ref HEAD ....
  • Doing a --bare clone of a repository with an invalid HEAD points HEAD at refs/heads/master in the clone.
  • Doing a --bare clone of a repository with a HEAD pointed at a tag creates a clone with "no" HEAD!?
git symbolic-ref HEAD
fatal: ref HEAD is not a symbolic ref
$ git clone <no-head-repository> tmp
Cloning into 'tmp'...
done.
Note: checking out 'db7dc993d4fb0dd9a6b37e32e7bc7e18c7122105'.

Weird. It still has a HEAD file pointed at the tagged commit. Anyway, not important for us and this still basically seems to work fine.