Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

go-git: Correct way to create a local branch, emulating behavior of "git branch <branchname>"?

Tags:

git

go

go-git

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:

  1. Creates .git/refs/heads/<branchname> to point to the current HEAD commit
  2. 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:

  • Example of creating a branch #551
  • Create Branch? #713

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

like image 421
Hephaestus Avatar asked Apr 18 '21 19:04

Hephaestus


People also ask

How do I create a local branch in git?

New Branches The git branch command can be used to create a new branch. When you want to start a new feature, you create a new branch off main using git branch new_branch . Once created you can then use git checkout new_branch to switch to that branch.

What is git checkout Branchname?

You can not only create a new branch but also switch it simultaneously by a single command. The git checkout -b option is a convenience flag that performs run git branch <new-branch>operation before running git checkout <new-branch>. Syntax: $ git checkout -b <branchname>

How do I manually create a branch in git?

To create a new Git branch in GitKraken, you will simply right-click on any branch or commit and select Create branch here . ProTip: GitKraken will automatically checkout the branch for you immediately after the branch has been created, so you can get straight to work on the right file.


2 Answers

Firstly, I don't have enough reputation to comment on Pedro's answer, but his approach fails on the Checkout phase as no branch is actually created on the storage (the repo's Storer was never invoked).

Secondly, it's the first time I heard about .git/log dir, so no, git branch does not create a record for the branch in that dir.

This leads me to the actual solution which is the one provided as an example of branching at the go-git repo.

  • To create a branch (off of HEAD):
Info("git branch test")
branchName := plumbing.NewBranchReferenceName("test")
headRef, err := r.Head()
CheckIfError(err)
ref := plumbing.NewHashReference(branchName, headRef.Hash())
err = r.Storer.SetReference(ref)
CheckIfError(err)
  • To checkout a branch
Info("git checkout test")
w, err := r.Worktree()
CheckIfError(err)
err = w.Checkout(&git.CheckoutOptions{Branch: ref.Name()})
CheckIfError(err)

This way, however, there is no config for this branch at .git/config, so there should be a call to repo.Branch function, but this is really comically unintuitive.

like image 74
Bojan Popržen Avatar answered Nov 09 '22 06:11

Bojan Popržen


Whe way I've done it:

Create a local reference to the new branch

branchName := "new-branch"
localRef := plumbing.NewBranchReferenceName(branchName)

Create the branch

opts := &gitConfig.Branch{
    Name:   branchName,
    Remote: "origin",
    Merge:  localRef,
}

if err := repo.CreateBranch(opts); err != nil {
    return err
}

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

w, err := repo.Worktree()
if err != nil {
    return rest.InternalServerError(err.Error())
}

Checkout

if err := w.Checkout(&git.CheckoutOptions{Branch: plumbing.ReferenceName(localRef.String())}); err != nil {
    return nil
}

if you want to track against a remote branch

Create a remote reference

remoteRef := plumbing.NewRemoteReferenceName("origin", branchName)

track remote

newReference := plumbing.NewSymbolicReference(localRef, remoteRef)

if err := repo.Storer.SetReference(newReference); err != nil {
   return err
}
like image 38
Pedro Luz Avatar answered Nov 09 '22 05:11

Pedro Luz