I'm writing some helper code in Python to automate the creation of feature and hotfix branches. I'm currently using GitPython to call the git binary directly, using commands like local_repo.git.checkout(branch_name)
. This works, but there's a new possibility that my code will have to run where the git binary isn't available. (But security are fine with lots of Python modules, go figure!)
Therefore I'd like to perform a checkout using pygit2
, check that my code won't mess up any repos, and understand a bit more about the internals of git
. The various examples and questions I found on stackoverflow and around the web didn't quite seem to solve this, or were doing slightly different things, so I wanted to share this in case other people need it in the future. Plus find out if I am actually doing this correctly.
The test repo I am using is here. It has a main
branch as the default, with one commit 658acf5334f4eefab270080945510d7090bda25e. Then using that as the parent branch, I created dev
which has commit 99c1e3df1a25a87cb055d6ee63d2a577b20f065e.
After running git clone https://github.com/aclarknexient/pygit2test
, the output of git show-ref
is as follows:
658acf5334f4eefab270080945510d7090bda25e refs/heads/main
658acf5334f4eefab270080945510d7090bda25e refs/remotes/origin/HEAD
99c1e3df1a25a87cb055d6ee63d2a577b20f065e refs/remotes/origin/dev
658acf5334f4eefab270080945510d7090bda25e refs/remotes/origin/main
Question 1: is git show-ref
a good way to track what's going on regarding local and remote branches?
So I'd like to checkout dev
. I still haven't been able to find a good step-by-step guide to what happens during git checkout
, so this code is based on what I've been able to figure out. It seems to work, but I'm very reluctant to trust something when I don't understand the underlying steps involved!
from tqdm import tqdm
import pygit2
import os
# https://stackoverflow.com/a/65576165/2988388
class ProgressCallback(pygit2.RemoteCallbacks):
def __init__(self):
super().__init__()
self.pbar = tqdm()
def transfer_progress(self, stats):
self.pbar.total = stats.total_objects
self.pbar.n = stats.indexed_objects
self.pbar.refresh()
# https://stackoverflow.com/a/65588864/2988388
def credentials(self, url, username_from_url, allowed_types):
return pygit2.UserPass(
username="x-oauth-basic", password=os.environ.get("GITHUB_TOKEN")
)
def checkout_notify_flags(self) -> int:
return pygit2.GIT_CHECKOUT_NOTIFY_CONFLICT | pygit2.GIT_CHECKOUT_NOTIFY_UPDATED
# based upon https://github.com/MichaelBoselowitz/pygit2-examples/blob/master/examples.py
def dostuff(local_repo_path: str, branch_name: str):
local_repo = pygit2.Repository(local_repo_path)
print("current checked-out branch is: " + local_repo.head.shorthand)
remote = local_repo.remotes["origin"]
print(remote.name)
# callbacks class provides the git auth
remote.fetch(callbacks=ProgressCallback())
print("remote " + remote.name + " fetched")
# get the remote reference to the remote branch
# because this doesn't yet exist in /refs/heads/
remote_branch_ref = local_repo.lookup_reference(
"refs/remotes/origin/%s" % (branch_name)
)
print(
"The branch as created by github: "
+ str(remote_branch_ref.target)
+ " "
+ str(remote_branch_ref.name)
)
# TODO: This can be folded into a method and should return one
# thing: the known good local reference to the requested branch
# ready to be passed into local_repo.checkout
try:
print("looking inside refs/heads/ first")
existing_branch_ref = local_repo.lookup_reference(
"refs/heads/%s" % (branch_name)
)
print(
"The branch exists locally already, good: "
+ str(existing_branch_ref.target)
+ " "
+ str(existing_branch_ref.name)
)
# Now that we know we have a good reference "inside" refs/heads/
# run checkout, giving it the branch reference
local_repo.checkout(existing_branch_ref)
except KeyError:
print("branch reference isn't in refs/heads/, create it now")
# at this point we don't have a reference inside refs/heads/,
# just refs/remotes/$REMOTENAME/$BRANCHNAME
# so create a new ref in refs/heads to pass to local_repo.checkout:
new_local_ref = local_repo.references.create(
"refs/heads/" + branch_name, remote_branch_ref.target
)
print(
"The reference to the remote branch we created locally: "
+ str(new_local_ref.target)
+ " "
+ str(new_local_ref.name)
)
# Now that we know we have a good reference "inside" refs/heads/
# run checkout, giving it the branch reference
local_repo.checkout(new_local_ref)
print("current checked-out branch is: " + local_repo.head.shorthand)
raise SystemExit("Exiting...")
if __name__ == "__main__":
# after a clone, the only refs/heads/ reference is the default branch
repo_path = "/Users/redacted/Scratch/tmp/pygit2test"
branch_name = "dev"
dostuff(local_repo_path=repo_path, branch_name=branch_name)
After running this code, git show-ref
returns the following:
99c1e3df1a25a87cb055d6ee63d2a577b20f065e refs/heads/dev
658acf5334f4eefab270080945510d7090bda25e refs/heads/main
658acf5334f4eefab270080945510d7090bda25e refs/remotes/origin/HEAD
99c1e3df1a25a87cb055d6ee63d2a577b20f065e refs/remotes/origin/dev
658acf5334f4eefab270080945510d7090bda25e refs/remotes/origin/main
And my git prompt says that I have checked out dev correctly.
Question 2: Did I really checkout dev
correctly?
The git documentation for git checkout
says: "If no pathspec was given, git checkout will also update HEAD to set the specified branch as the current branch." and "To prepare for working on , switch to it by updating the index and the files in the working tree, and by pointing HEAD at the branch."
Question 3: In the 2 quotes above, what exactly is meant by "update HEAD to set the specified branch as the current branch" and "pointing HEAD at the branch"?
Question 4: What is git doing to accomplish the actions mentioned in question 3?
However, that show-ref output says that HEAD is "refs/remotes/origin/HEAD" and has a different commit SHA than "refs/heads/dev".
Question 5: Why do "refs/remotes/origin/HEAD" and "refs/heads/dev" not point at the same SHA?
Last Question: Is this going to break repositories? Am I on the right track?
Thank you for reading, please let me know how I can improve the readability of this text. All answers and feedback gratefully received.