How to clone master and checkout another branch in pygit2 without detached HEAD

630 views Asked by At

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)
2

There are 2 answers

1
torek On

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.

1
Eric Van Bezooijen On

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)