0

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.

Mikko Rantalainen
  • 14,132
  • 10
  • 74
  • 112

1 Answers1

2

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

Mikko Rantalainen
  • 14,132
  • 10
  • 74
  • 112
  • 2
    "*Git only has branches…*" Well, not exactly true. `refs/remotes/origin/master` are special kind of branches — remote-tracking branches. They're not equivalent to local branches. You cannot checkout or move them at will; the only way to move them is to sync (fetch/pull/push) with a remote repository. – phd Dec 02 '21 at 18:42
  • You can do stuff like `git push . master:refs/remotes/origin/master2` (local→local update) but you're right that if you run `git checkout refs/remotes/origin/master` you end up with "detached HEAD" at the SHA-1 of `origin/master` instead of having that branch checked out similar to normal branch. I guess Git does this because it assumes `refs/remotes/origin/*` is okay to overwrite at will when you do `git fetch` so allowing working with those locally is probably a bad idea. – Mikko Rantalainen Dec 02 '21 at 18:47
  • I just tested and it appears that this magic about remote-tracking branches is implemented in `git checkout` only. I can run `git checkout origin/master && echo "ref: refs/remotes/origin/master" > .git/HEAD` and then create new commits just fine with the Git updating the branch `refs/remotes/origin/master` in my local copy. Of course, it'll be overwritten if I later do `git fetch` so this is more about safeguarding your work than these branches being special. – Mikko Rantalainen Dec 02 '21 at 18:59
  • 2
    The feature where `git checkout` or `git switch` will create a *branch* name from a *remote-tracking* name (e.g., create `dev` from `origin/dev`) used to be called *DWIM mode* and is now controlled by `--guess` / `--no-guess`. Git basically does a thing where, just before it emits a "what branch are you talking about" error, do this guessing in checkout/switch, if guessing is enabled. If the guess finds a good guess, it creates the branch and then checks it out / switches to it. – torek Dec 02 '21 at 19:13
  • 3
    Note that unlike `git checkout` (which will do a "detached HEAD" checkout on any non-branch-name commit specifier), `git switch` demands the `--detach` flag for this purpose. That makes `git switch` a lot friendlier to newbies. Git is rather newbie-hostile due to some ... questionable design choices way back when, that must now be preserved forever for compatibility. :-) – torek Dec 02 '21 at 19:15
  • Yes, I agree that if Git developers didn't mind backwards compatibility, fixing many user interface issues would be much easier. However, because Git developers are very very strict about backwards compatibility (similar to Linux kernel, see Torvalds' rants about breaking backwards compatibility) you can always safely upgrade Git to latest version no matter which version you're currently using. The same cannot be said about many other software that other developers are using daily. I've been using Git for over 10 years now and even all the old scripts I wrote to be used with Git still work. – Mikko Rantalainen Dec 02 '21 at 21:12
  • That said, the original Git UI rationale was "can a Linux kernel developer learn to use this"? Kernel developers being pretty smart folks, they can handle pretty weird stuff... – Mikko Rantalainen Dec 02 '21 at 21:14
  • By "Mercurial has multiple things that are logically branches" I think you mean "Mercurial has multiple things that would all be branches in git" .. ? Because in HG itself the first two examples (unnamed and named branches) are nearly indistinct and bookmarks are not branches at all, though they can be used similarly to some extent. – StayOnTarget Dec 03 '21 at 13:22
  • I improved the wording. I have to add that I think Mercurial bookmarks are actually the closest thing to Git branch because those can name a branch without storing the original branch name in the version history. – Mikko Rantalainen Dec 03 '21 at 16:01