As the title suggests, I'm trying to figure out how to create a local branch using go-git
in a way that gives the same result as the Git CLI command git branch <branchname>
.
As far as I've been able to tell, git branch <branchname>
(without an explicit <start-point>
argument) does two things:
- Creates
.git/refs/heads/<branchname>
to point to the currentHEAD
commit - Creates
.git/logs/refs/heads/<branchname>
with a single line recording the creation of the branch.
It may do more, but these two things I know it does for sure. (If you know something more that it does, please share!)
Most of what follows documents my journey of discovery as I researched my options, and I think I might now have a handle on #1 above. For #2, though, I am starting to think I may be SOL, at least using go-git
.
First Thought: Repository.CreateBranch
My initial naive thought was to just call Repository.CreateBranch
, and there's an answer to a similar SO question ("How to checkout a new local branch using go-git?") that would seem to lend credence to that idea. But once I started looking into the details, things got very confusing.
First, Repository.CreateBranch
takes a config.Config
as input (why?), and also seems to modify the repository's .git/config
file (again, why?). I've verified that the git branch <branchname>
command doesn't touch the repo's config, and I certainly don't need to mention anything about the config when I invoke that command.
Second, the SO answer that I linked above cites code in go-git
's repository_test.go
that does the following:
r, _ := Init(memory.NewStorage(), nil) // init repo
testBranch := &config.Branch{
Name: "foo",
Remote: "origin",
Merge: "refs/heads/foo",
}
err := r.CreateBranch(testBranch)
But the definition of config.Branch
is:
type Branch struct {
// Name of branch
Name string
// Remote name of remote to track
Remote string
// Merge is the local refspec for the branch <=== ???
Merge plumbing.ReferenceName
...
}
and "refs/heads/foo"
isn't a refspec (since a refspec has a :
separating its src
and dst
components).
After much head-scratching and code-reading I've come to the (very) tentative conclusion that the word "refspec" in the comment must be wrong, and it should instead just be "ref". But I'm not at all sure about this: if I'm right, then why is this field named Merge
instead of just Ref
?
Another tentative conclusion is that Repository.CreateBranch
isn't really for creating a purely local branch, but rather, for creating a local branch that stands in some sort of relation to a branch on a remote -- for example, if I were pulling someone else's branch from the remote.
Actually, on a re-reading of the Repository.CreateBranch method, I'm not at all convinced that it really creates a branch at all (that is, that it creates .git/refs/heads/<branchname>
). Unless I'm missing something (entirely possible), it seems that all it does is create a [branch "<name>"]
section in .git/config
. But if that's true, why is it a method of Repository
at all? Why is it not a method of config.Config
?
Similarly, there's a related function:
func (r *Repository) Branch(name string) (*config.Branch, error)
that will only return branch information from the config. Yet, the very next function in the documentation of Repository
is:
func (r *Repository) Branches() (storer.ReferenceIter, error)
which really does return an iterator over all the entries in .git/refs/heads/
.
This is horribly confusing, and the documentation (such as it is) doesn't help matters. In any case, unless someone can convince me otherwise, I'm pretty sure that CreateBranch
won't be of much help in actually creating a branch.
Worktree.Checkout ???
Some additional web-searching turned up these two issues from the old d-src/go-git
repo:
Both of these posts suggest this basic approach to creating the local branch:
wt, err := repo.Worktree()
if err != nil {
// deal with it
}
err = w.Checkout(&git.CheckoutOptions{
Create: true,
Force: false,
Branch: plumbing.ReferenceName("refs/heads/<branchname>"),
})
Apart from the fact that this checks out the new branch, which git branch <branchname>
doesn't do, it also fails to create .git/logs/refs/heads/<branchname>
.
Also -- as a potentially very nasty surprise -- it blows away all the untracked files in the worktree. By default, git checkout
keeps local modifications to the files in the working tree, but in go-git
you need to explicitly specify Keep: true
, even if you've specified Force: false
.
Definitely a violation of the "Principle of Least Astonishment." Thankfully, in the local repo I tested this in, they were all old editor backup files or fragments of old projects that I'd long ago abandoned.
storer.ReferenceStorer
As it happened, one of the go-git
authors/maintainers responded to the second issue, and suggested:
In order to create and remove references independent of the Worktree, you should do this using the
storer.ReferenceStorer
.Please take a look at the branch example: https://github.com/src-d/go-git/blob/master/_examples/branch/main.go
Which is fine and straightforward, but it only addresses creation of the branch's ref.
All occurrences of the word "log" that I have been able to find in the go-git
source code seem to refer to commit logs, not ref logs. Given that reflog entries don't look anything like other artifacts in the .git
tree, I'd imagine that a different kind of storer would be necessary to create/update them -- and none of the existing storers look like (to me) they do that.
So...
Any suggestions on how I should get a proper reflog to go with the ref?
(Or, maybe I've misunderstood horribly, and there is some way of creating branches in go-git
, apart from those I've listed above, that would do what I want.)
Whe way I've done it:
Create a local reference to the new branch
Create the branch
In case you actually need to change to that branch... just do a checkout (can't remember if it actualy changes to the created branch with the create)
Get the working tree
Checkout
if you want to track against a remote branch
Create a remote reference
track remote