0

This works fine for me using pygit2:

  1. Clone master branch of a repo
  2. Create a branch
  3. Make some changes
  4. Push branch to origin

This is failing for me:

  1. Clone master branch of a repo
  2. Checkout an existing branch

I always end up with detached HEAD. Is there some trick to doing the checkout without getting a detached HEAD? I've spent well over a day on this now and I know there must be a way of doing this properly but every example I see online does what I do below ...

Here is my stripped down test case which fails every time for every branch I tried:

repo = pygit2.clone_repository(url,dir,bare=False,checkout_branch="master",callbacks=RemoteCallbacks())
checkout_branch = repo.branches["origin/{0}".format(branch))
ref = repo.lookup_reference(checkout_branch.name)
repo.checkout(ref)
  • 1
    Suppose the branch name is `foo`. `git checkout foo` works as you expect. However, `git checkout origin/foo` or `git checkout refs/heads/foo` would check out a detached HEAD. So check the value of `ref` or its variable referring to the actual branch name. – ElpieKay Jul 19 '21 at 06:23

2 Answers2

0

While pygit2 may throw its own extra wrinkles into the mix, start with this fact: When you clone a repository, you first get all of the other Git's commits and none of its branches. Then, just before the command-line git clone command returns a prompt to you, it creates one branch for you, based on the -b argument you supplied. If you didn't supply a -b argument, it—your own Git—asks the other Git what branch name they recommend, and creates a branch from that name.

The result is that if they, in their Git, have branches branch1, branch2, branch3, main, and xyzzy, and you specify -b main or don't specify anything and they recommend their main, your Git now has exactly one branch in your repository—your clone of their repository—and that's the branch named main. Your Git created this as your main; it's not their main. Their branch names are theirs.

If you want to have branches named fred, barney, betty, and wilma instead of their names, you can do that. To prevent your git clone from creating any branch at all you can add the -n option to your git clone command: now you get all their commits, and no branch at all and now you can choose a name that doesn't match any of their names.

But: what happens to their branch names? The answer is: your Git takes their branch names, such as branch1 and branch2 and main and so on, and changes those into remote-tracking names. The remote-tracking names your Git uses here are origin/branch1, origin/branch2, origin/main, and so on.

These remote-tracking names match their branch names, with the obvious substitution. But they're not branch names. They're remote-tracking names. What's the difference? It's that detached HEAD that you're seeing. Using a remote-tracking name means I don't intend to make any new commits. Using a branch name means I do intend to make new commits. If you intend to make new commits, you should use a branch name, not a remote-tracking name.

That, in general, may mean that you have to ask your Git to create a new branch name in your own local repository. How do you do that? Well, it all gets a bit complicated in complicated setups, but we start with this: A request to check out branch name X, for any X, can't proceed if there is no branch named X ... so if your Git does not have an X yet, your Git first checks to see if your have origin/X, and if so, your Git will create your X from your origin/X. Git calls this "DWIM mode", for "Do What I Mean (not what I say)". The git checkout command has a new flag, --no-guess, to disable this kind of guessing about what you meant. Using:

git checkout --no-guess branch1

won't look to see if you have an origin/branch1 before complaining that there is no branch1 to check out. The default is to check first: there's no branch1? Check for origin/branch1, if so, create branch1 using origin/branch1, the same way git clone creates your main from your origin/main that you got from their Git's main.

This is all a bit roundabout, but it tends to work out pretty well for most users, most of the time. As long as they don't run git checkout origin/branch1, that is.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Thank you for that detailed bit of explanation but it doesn't tell me how to make pygit2 do the right thing. However, after spending a few more hours I think I have a working solution which I will post here for the benefit of others. – Eric Van Bezooijen Jul 19 '21 at 20:55
0

I think I have resolved this issue by adding the following bit before I run a checkout. This assumes you want to checkout a branch that already exists remotely:

if not checkout_branch in repo.branches.local:
    remote_branch = "origin/" + checkout_branch
    if not remote_branch in repo.branches.remote:
        # handle a fatal error here
        pass
    (commit, reference) = repo.resolve_refish(remote_branch)
    repo.create_reference("refs/heads/" + checkout_branch,commit.hex)
  • That seems a likely way to implement the same kind of trick Git uses in its DWIM / guess mode. (Note that Git actually searches *all* remotes; if it gets exactly 1 match, that's the correct one; if it gets none, or 2+, there is a problem. Recently Git added the notion of a "preferred" remote for DWIM as well, so that if there's a match on the preferred remote, that's the one to use. Whether you want to do all of this, or whether pygit2 might already have its own code to do all of this, I have no idea.) – torek Jul 20 '21 at 03:35