Saturday, September 12, 2009

Git Hate

I like Git, but sometimes I also really hate it.

My friend Sartak asked if there is a better way of finding a merge base in a command than hard coding master as the merge point, and I stupidly said "of course, you just check what that branch tracks". That is the same logic that git pull would use, so in theory you can apply the same concise logic to merge or rebase your branch without running git fetch.

In principle that's correct, but in practice it's utterly worthless.

The first step is to figure out what branch you are on.

You could easily do this with the git-current-branch command. Except that it doesn't actually exist. Instead you need to resolve the symbolic ref in head and truncate that string:

$(git symbolic-ref -q HEAD | sed -e 's/^refs\/heads\///'

Except that that's actually broken if the symbolic ref points at something not under heads.

Ignoring that, we now have a string with which we can get our merge metadata:

branch="$( git current-branch )"

git config --get "branch.$branch.remote"
git config --get "branch.$branch.merge"

The problem lies in the fact that the tracking refers to the remote, and the ref on the remote:

[branch "master"]
 remote = origin
 merge = refs/heads/master

But we want to use the local ref. Based on git config, we can see that this value should be refs/remotes/origin/master:

[remote "origin"]
 url = git://
 fetch = +refs/heads/*:refs/remotes/origin/*

If you read the documentation for git-fetch you can see the description of the refspec for humans, but it's pretty feature rich.

Fortunately Git already implements this, so it shouldn't be too hard. Too bad that it is.

In builtin-fetch.c are a number of static functions that you could easily copy and paste to do this refspec evaluation in your own code.

So in short, what should have been a simple helper command has degenerated into an epic yak shave involving cargo culting C and reimplementing commands that should have built in to begin with.

And for what?

git merge-base $( git-merge-branch $( git current-branch ) ) HEAD
instead of
git merge-base master HEAD

In short, it's completely non-viable to do the right thing, nobody has that much spare time. Instead people just kludge it. These kludges later come back with a vengeance when you assume something does the right thing, and it actually doesn't handle an edge case you didn't think about before you invoked that command.

I'm not trying to propose a solution, it's been obvious what the solution is for years (a proper libgit). What makes me sad is that this is still a problem today. I just can't fathom any reason that would justify not having a proper way to do this that outweighs the benefit of having one.


Anonymous said...

Please don't complain about software you don't understand

Alok said...

Doesn't $ git branch # list all the branches and highlight the current one with an asterisk '*'?

nothingmuch said...

parsing that is a lot more work than parsing the output of git symbolic-ref

Jakub Narebski said...

Don't use sed; use POSIX shell parameter expansion, i.e. (if BR is the name of variable you save `git symbolic-ref HEAD` output) 'BR=${BR#refs/heads/}' to strip 'refs/heads/' prefix, then 'BR=${BR:-HEAD}' to deal with the case of detached HEAD (HEAD pointing directly to commit, instead of pointing to a branch). BTW. if HEAD points outside 'refs/heads/' it is an error.

Also there is in modern Git you can simply use 'git rev-parse --abbrev-ref --verify HEAD'.

Jakub Narebski said...

You can probably get name of upstream (local tracking branch) with

git for-each-ref --format="%(upstream:short)" "$(git symbolic-ref -q HEAD)"

(or perhaps without ':short' modifier, perhaps with 'git rev-parse --symbolic-full-name HEAD' in place of 'git symbolic-ref -q HEAD').

nothingmuch said...

Thanks Jakub!