I've previous experience with Mercurial and I'm having trouble understanding the Git branching. Why does the
git branch
only list some of the branches and I need to run
git branch -a
to get list of all branches? It seems to me that Git has multiple different branch types whereas Mercurial has only one.
As I wrote at https://stackoverflow.com/a/11223644/334451 it's actually the other way around. Mercurial has multiple things that are logically similar to Git branches: unnamed branches, named branches and bookmarks. Git only has branches but it uses namespaces for branch names (basically namespace path for a branch + the name of branch). Git users often speak about "master
" branch (or "main
" nowadays) and you have to decipher from the context if they actually mean refs/heads/master
or refs/remotes/origin/master
or something else. Git is also smart enough to guess the correct path if you use just the last part of the full name and in some context the interpretation of short names is explicitly defined. For example, when you say
git push origin foo:bar
it will actually execute (assuming foo
is local branch and not a tag)
git push origin refs/heads/foo:refs/heads/bar
which means 'take the local "refs/heads/foo
" and push it to remote server "origin
" using name "refs/heads/bar
" on the remote server'. You can use commit SHA-1 for the source as long as the target is a branch that already exists (otherwise git cannot know if you want to create a new tag or branch). I prefer to use this syntax if I ever need to force (overwrite) things on the remote server so that I can accurately specify the new state of the target branch.
As everything actually has full namespaced name (refname), you can also do stuff like having a branch called "master
" (actually refs/heads/master
) and tag called "master
" (actually refs/tags/master
) but that's just asking for trouble. Git always uses the full refnames under the hood but allows using shorter names in the user interface.
By default, when you run "git branch
" it lists only refs/heads/*
without the full refname. For full branch names you have to run something like
git branch --format="%(refname)"
or for all branches, local or remote no matter how many remote servers you've configured
git branch -a --format="%(refname)"
which will list full names of all the known branches. All those names are actually file paths under the directory .git/refs/
in your working directory so the whole system is actually really simple (the file at the end of that path contains just the SHA-1 of the commit that's the tip of that branch). When you "create a new branch" in Git, you literally create one new file with 41 bytes containing the currently checked out commit SHA-1 (output of "git rev-parse HEAD
") with trailing linefeed and the name of the file is the name of the branch you created. The file .git/HEAD
contains textual reference to currently checked out commit or head or tag in your working directory.
Git also supports using branch names that contains slashes in which case there will be additional directory levels under the refs/heads/
hierarchy but everything else works just the same. The official git repository where the git is actually developed uses branch names prefixed with extra directory levels.
Git tags are implemented similarly but those are stored in .git/refs/tags/
and will not be automatically modified when you create new commits after checking out a tag. Note that tags are not kept in separate namespaces but when you fetch changes, you automatically get all the tags, too, and those are always in the prefix refs/tags/
.
You can list all known tags with full refnames using command
git tag --format='%(refname)'
Note that "git tag -a
" does exist but it doesn't mean "list all" but "create annotated tag" (a tag that has more info attached to it instead of just the name) because tags do not have namespaces, so there's no need for "list all tags".
The branches starting with refs/remote/
will be updated automatically when you run "git fetch
" (or do "git pull
" which will run "git fetch
" behind your back).
Git will be much easier to understand if you never ever use "git pull
" for anything. Always run "git fetch
" (or "git fetch --all
" if you have multiple remote servers) instead and it will only update the refs/remote/
hierarchy only and download the required pack
/object files to actually know what all those SHA-1's mean. After you have executed "git fetch
" you can use "gitk --all
", "gitg
" or some other repository viewer that can show both local and remote branches. If you don't have any GUI tools, you can run something like
git log --oneline --decorate --graph --all
or (everything on one line)
git log --graph --all --pretty=format:"%C(auto)%h%d%Creset %s %Cgreen(%cr)%Creset"
Then you can sanely decide if you want to merge
, rebase
or do something else.
As a party trick, you can also do stuff like
git push . HEAD:foo
which means push to local repository, update local branch HEAD
as the new value for branch "foo
" (fast forward), where HEAD
is currently checked out version as usual. You can use SHA-1 here, too. This is mostly helpful when you use feature branches and want to include your current feature branch to local "master
" branch. Instead of checking out master and "merging in" the changes from your feature branch, you can simply push current branch to local master branch. This is better than forcing master
branch to HEAD
because Git will show an error if the change wouldn't be fast-forward. I have aliased "git update-master
" to "git push . HEAD:master
" just for this purpose. (And I don't actually type git update-master
but git up
followed by TAB
which autocompletes the rest. Be sure to enable autocompletion for all git
commands in your shell unless enabled by default.)