Git koans
stevelosh.com
stevelosh.com
Git is conceptually straightforward[1]. It is difficult if not impossible to discern those concepts from its baroque CLI. So learning git from its built-in help and/or man pages is suboptimal if not a complete waste of time. Either read the Pro Git book[2] or pop the hood[3]. Then the inconsistent CLI is no longer a big deal. If it were, someone would have come up with a new porcelain (high-level CLI) that would have taken hold by now. And no one really has. There is Easy Git[4] but I don't know anyone who uses it.
And really, I see git as not much different than Unix in this regard. Unix commands vary widely in their usage. Does knowing awk help with sed? cpio and tar? wget vs curl? Eventually you understand concepts such as pipes, regular expressions, file descriptors, etc and you become familiar with the commands where you are able to apply these concepts, at which point does strict interface consistency between them matter? I dunno, but I started with both git and hg around the same time and ended up happier with git. Mercurial had the "better" CLI, but I found git was better at doing what I wanted. Before that I used clearcase, which has a very consistent CLI and yet I was constantly cursing at it.
Or maybe I just have stockholm syndrome and I'd feel differently if I'd ever used plan 9. :)
1. http://tom.preston-werner.com/2009/05/19/the-git-parable.htm...
---
Master Git and a novice API developer stood together in a crowded office.
"I am trying to merge two massive XML files," explained the API developer, "and I am getting many tedious conflicts. You must have a strategy for making the lines match up in a human-readable manner. What is my best option?"
"Patience," said Master Git, turning away from the monitor.
"But I have been manually resolving for hours!" protested the API developer. "My mergetool ought to be able to imply context and match lines properly!"
Master Git sighed and left the room without a word. Hours later, he returned to find the office empty and silent. Only the API developer remained, his head bowed in defeat before the monitor.
"git merge --strategy-option=patience," whispered Master Git.
Upon hearing the same word in explicit context, the novice was enlightened.
A particular commit isn't made 'on' a branch, so to speak; it's just a SHA hash that has a particular SHA as a parent. And then a branch is a pointer to a particular SHA. So there's no way to know which branch a commit originated 'on', merely which branches happen to contain a commit at present (because their current commit has the desired commit in its history).
So, if you merge two branches together, of course you can not tell which branch a given commit originated on just by looking at history. That information was never in the history in the first place.
I could see it being useful to track this in the merge somehow, though.
Git is revolutionary, in part, because instead of asking "what's the mental model we want to emulate and then never mind if that matches the implementation", they gave developers credit that they can understand a handful of simple concepts and made the actual design and data model as simple as possible. Programmers know what pointers, SHA-1 hashes, and DAGs are, so just use them.
1) don't magically know that they're supposed to understand the data model in order to use it
2) Come for help when they screwed up because they don't understand it and are basically told "You screwed up because you're an idiot"; since they are already in a bad mood, they then ragequit.
Well, the fact that I can't easily find what commits were done on a branch, for one. And yes, I understand why it's not possible, they way git does branches.
Or that it doesn't track renames/moves, other than on similarity basis that often fails to detect rename when viewing a file history, for example.
The git answer to both of these is usually 'well your viewer could be doing that', except they don't, because it's hard when you don't track that information in commits.
It also means I can easily request all commits made in the master branch, to see what features were merged in recently.
How is the branch name going to give you sufficient information on that anyway? You're going to have to look at the commit message and probably the surrounding history anyway if you want context for the entire branch.
> It also means I can easily request all commits made in the master branch, to see what features were merged in recently.
In this use-case, you're probably viewing some form of "git log" anyway, so it's not hard to turn on the option that shows the tree view, which should make it readily obvious.
Well, in my workflow it will give me the issue number, since branches in my repo all look like '530-issue-description'. I suppose that's because we used to work with mercurial, where that is actually meaningful and helpful.
>In this use-case, you're probably viewing some form of "git log" anyway, so it's not hard to turn on the option that shows the tree view, which should make it readily obvious
The tree view will still interleave master commits with all the other commits, and if there's a lot of parallel branches and significant distance between master commits it's easy to get lost.
Tracking your single 'pipe' character down a couple pages and not losing which one you're watching... Not what I call good UI.
Unless you like manually tracking parents of a commit, and trying to guess which branch is which based on commit messages. Fun!
I've seen many workarounds to this clear deficiency, the simplest of which is simply putting the branch name at the start of the commit title. Of course that usually cuts the space available for the actual commit title down from ~60 characters to 20-40, depending on how verbosely you call your branches.
Git tracks the parents of a commit. There are ways to visualize that, even with command-line git. When you do this, it's usually clear which branch is master and which branch was the feature branch, and since the merge commit has a default commit message like "merge <<branch-name>> to master" it's even easier to figure out.
It's also entirely possible to use a Git workflow where you rebase your branches onto master before merging then in with a fast-forward, which means there are no merge commits. This workflow usually involves squashing. Conversely, you can use a workflow where you never fast-forward merge to master, in which case master is the branch that consists of a series of commits called "merge <<branch-name>> to master".
That means that to see commits that were done in a particular feature branch I need to do some magic that walks up the commit tree from a given merge commit, guesses which one of the parents is the master and which one is the feature branch, remembers the commit ids, keeps track of merge commits it hits and makes sure it is still tracking the right branch (merges of master into a feature branch are common, merges of feature branches into other feature branches are occasional), etc.
All because gits branches aren't really branches at all.
As to fast forwarding a feature branch onto master, I can't believe anyone would actually do that. You might as well be developing on the master branch from the beginning there, what's the point of having branches if they disappear from history?
The reason for the rebase/fastforward workflow is to basically synchronize changes. You might have five developers working on seven different feature branches at a time. But they're only going to be merged back to mainline in the order they're completed, which you don't know ahead of time. Once they're merged back, you don't necessarily need to record the history of the feature branch separately. Often you're going to squash changes anyway.
It's also a better idea to rebase from master and merge to master, but even if you merge both ways, the commit message still reports the source and destination branches. If you're disciplined about using feature branches, master is the branch with commit after commit saying "merged to master".
I shouldn't have manually filter the commit history to get the information that interests me.
>The reason for the rebase/fastforward workflow is to basically synchronize changes. You might have five developers working on seven different feature branches at a time. But they're only going to be merged back to mainline in the order they're completed, which you don't know ahead of time. Once they're merged back, you don't necessarily need to record the history of the feature branch separately. Often you're going to squash changes anyway.
But if you fast forward your changes on top of master and push that out as the new master, you lose all the benefits of branching. You could have as well made your changes on master, do pull --rebase once you've completed and then push out. That's how the history looks like.
>It's also a better idea to rebase from master and merge to master, but even if you merge both ways, the commit message still reports the source and destination branches.
Sure, if the rebase is clean, but conflicts are common.
>If you're disciplined about using feature branches, master is the branch with commit after commit saying "merged to master".
Unless you use the github UI, in which case it will say 'Merge pull request ...'. Or you had to hotfix something on master - it happens.
Which benefits?
To me, the main benefits of branching happen as I'm developing a feature. I can work on something and quickly context-switch either back to master or to a different feature branch. For instance, if I'm working on a feature but a high-priority bug fix needs to be done first, I can switch back to master, check out a bug fix branch, and keep my feature work separate from it. When the bug fix is done, I merge it in and rebase my feature branch from it. The only thing I really care about historically is that features land in master all-at-once, which squashing, rebasing, and fast-forwarding does for you.
> Sure, if the rebase is clean, but conflicts are common.
You have to resolve conflicts either way though.
> Unless you use the github UI, in which case it will say 'Merge pull request ...'.
Still a recognizable pattern.
> Or you had to hotfix something on master - it happens.
That's called "not being disciplined about using feature branches". I suspect you run into this problem because you're suffering under a self-imposed constraint that branch names have to be globally unique and meaningful throughout your version history.
This isn't a problem unless you want to enforce a workflow where you always merge to master--the rebase/squash/ff workflow wouldn't actually pose a problem here, since all ancestor commits to master were deliberately put on master. My point is, if you follow an inconsistent workflow, no wonder your history is difficult to look through.
Most developers think of branches as something they commit to. Therefore they are surprised when later they can't tell which branch they committed to.
This could trivially be fixed with porcelain that puts the name of the branch the commit was made to at the end of the commit message.
The issue of branches not really being branches is not unique to git; most VCs I've ever used have had significant differences between what you would think of as a branch and actual branches (SVN doesn't even have branches, except as a convention).
That, and constantly creating your own test repositories to test new commands/concepts. With a distributed VCS it's easy and fast. Just run "git init test" somewhere and start committing!
I also recommend you to use "gitk" when you're learning to constantly look at your history tree. That should clarify the concept "branches are pointers" after a few tests.
Quick introduction to git internals for people who are not scared by words like Directed Acyclic Graph.
For example, the multiple use cases of 'checkout' or 'reset' are quite intuitive, if you have the implementation in mind.
The somewhat inconsistent command line interface / option / argument parsing in git can sometimes lead to very unexpected results.
One Thing Well demonstrates how one command (git checkout), while it does do 'one thing well' (changing your working directory to match something in the index), can handle a wide variety of seemingly unrelated use cases.
Only the Gods demonstrates how history can mean different things: commit parentage, which is usually immutable, and branch history, which is ephemeral.
The Hobgoblin probably refers to the Emerson quote "A foolish consistency is the hobgoblin of little minds."
The Long and the Short of It is about how git accepts `git --help command`, `git command --help`, and `git command -h` but `git -h command` returns an argument error.
A theme here is that git does not try to anticipate your workflow. Its commands and options are named based on what they do, rather than how you should probably use them. This makes git a very powerful tool, but with an interface that is often counter-intuitive.
Apparently, git does not allow you to alias default git commands (like "pull", "push", "commit", etc) to something else. In the given situation, a user is trying to alias the default "pull" command to "pull --ff-only" so that pull will not automatically merge changes and will only fast-forward.
However, since git does not allow you to alias default commands, when the Git Master runs "git pull" it ignores the alias entirely and does a normal pull, and in so doing merges changes. The student is confused, but eventually figures it out.
From "git --help config": "To avoid confusion and troubles with script usage, aliases that hide existing git commands are ignored."
> “Use git checkout.”
This is just a jab at how "checkout" does too many different things.
> “I have a historical record of a merge commit with two parents. How can I find out which branch each parent was originally made on?”
Git doesn't let you do this.
> “Surely some of these could be made more consistent, so as to be easier to remember in the heat of coding?”
Git commands can be inconsistent.
> “git -h branch“.
This command tells you nothing about "branch" (throws error), whereas "git --help branch" launches the browser at git-branch's manpage. This goes against what people usually expect (that "-h" is just shorthand for "--help".
1. Silence: you can't alias built-in commands — for good reason. You don't change the defaults of commands, that's just going to cause problems with scripts. Why is it ignored silently? For starters because new git versions can add a command that would conflict with your config that came from an older version. You can trivially see that an alias is ignored by just adding ``--help`` to the command and it won't show you the alias.
2. One Thing Well: I fail to see the problem with that? All of these do the same thing. They check out a revision. With the exception of branch creation which is actually not a feature of `git checkout` but `git branch`. There is just a convenience operation on `git checkout` that also changes branches. The better question is why branches in mercurial are called bookmarks :P
3. Only the Gods: thankfully you don't know which branch something came from. Mercurial fucked me over more than once where I accidentally pulled a patch that came from a named branch that was conflicting with one I already had (you would not believe how many people name their branches "bugfix"). Mercurial's named branches are among the stupidest idea in software maintenance.
4. The Hobgoblin: these are all different commands. "How can I view a list of all tags": that's not what all tags is, that is "what's the list of tags I know locally". In mercurial you can't even figure that out properly because depending on the branch you're on you get different defined tags since tags are stored in the tree. Git's tag system is much superior to that.
"How can I view a list of all branches": that's a question you will never ask because it's the wrong question. A branch could show up multiple times. The correct question to ask is "How can I view a list of all local branches?" `git branch`, the second question is "how can I view a list of all remote branches?" And for that the answer is `git branch -r` or `git branch --remote` which makes a lot of sense in my mind.
"And how can I view the current branch": Same way you show all branches, just `git branch`. Shows you your current branch as well as all other branches and neatly colorizes it. `git rev-parse` is a command you really never have to use unless you script something.
Yeah, some could probably more consistent. The odd one out are `git submodule` and `git remote` which are sub commands instead of using parameters to remove/modify them. That being said, you rarely need to use those.
In direct comparison with hg, git's UI and functionality is a present from god. I'm pretty sure hg only has a better UI reputation because git's UI used to be pretty bad. Now however? Pretty sure it wins over hg hands down.
I don't understand why many git apologists continue to try to paper over the atrocious CLI that comes with git by blaming the user for not knowing his tools well enough. The tool is supposed to serve the user, not the other way around. Personally I think everything about the git CLI sucks, plain and simple. The command structure and naming, the switches, the error messages, the documentation, all of it. Compared to git, the hg CLI is a thing of beauty. Yet I still prefer git over Hg. No need to sugarcoat it and pretend git is flawless though.
This is a bold claim, and depends on a lot of variables. On very large repos, git is completely unusable.
Isn't that violating 'doing one thing'? (Your other examples in this point are valid, and serve to show how powerful building concepts on top of a few primitives can be, if confusing at times)
Yes. But that violation would also be there if `git branch` would checkout. And in that sense, what's more odd "git checkout -b new-branch" or "git branch --checkout new-branch"?
Git checkout is a command to read something from your .git-folder to your working directory.
Especially for people coming from ClearCase, the concept of Git checkout needs to be clarified as fast as possible. (In CC checkout means you make an element (file) editable for you, and possibly reserve it to you too)
2. `git checkout -- somefile.py` does not have anything to do with checking out a revision. It changes a file's contents, but doesn't change the parent of your working tree at all. Unlike `git checkout -b` which is inconsistent for convenience like you mentioned, the "make file x look like it was in rev y" function here is unique to git checkout as far as I can tell. Then again this is git, so there's probably at least four other ways to do this with cryptic plumbing commands.
3. I like Mercurial's named branches because I don't need to create a branch for every single commit. But even if you don't like them, the "record which branch X was made on" and "use that recorded information to group branches and such in the UI" are two different things. The second requires the first, but git could record which branch was checked out when each commit was made as some metadata in the commit, instead of dropping it on the floor. It would be a godsend when trying to figure out what actually happened during a merge.
4. `git branch` doesn't show you the current branch. It shows you all the local branches, with colors and padding spaces and asterisks and crap. What if you want the current branch in your prompt? Or just want to grab it in a script? Your choices are "pipe to grep and sed" or "use this ugly-ass rev-parse thing" instead of "git branch --current".
Git's UI is still abysmal. Mercurial preserves more data (branches), gives you the same power as Git and has a UI where "--help x", "-h x", "help x", "x -h", and "x --help" all do the same thing (pop quiz: how many different things does git do for those? hint: it's more than two).
Yes. Otherwise command line scripts will start doing horrible things. It was never intended that you can alias builtins, the only reason you might think you can is because it is allowed in mercurial which I think is a horrible idea.
> `git checkout -- somefile.py` does not have anything to do with checking out a revision.
Of course not. `git checkout` checks out things. You can check out a lot of things. Just because it works differently in hg does not mean it's wrong in git. git checkout does what it says on the tin: it checks out a branch or paths to the working tree.
> I like Mercurial's named branches because I don't need to create a branch for every single commit.
I think the concept of named branches in mercurial are hugely flawed and were the final nail in the coffin of why I migrated over to git. And I never looked back.
> What if you want the current branch in your prompt?
Then you use the underlying low-level commands or even better, you cat `.git/HEAD` which will be even faster. Not sure why for your prompt to look nice you need to have non-cryptic commands. Exactly for things like prompts there is the plumbing.
> gives you the same power as Git and has a UI where "--help x", "-h x", "help x", "x -h", and "x --help" all do the same thing
How is this different from mercurial? If anything mercurial has even more ways to show help: `man hg` which shows the manpage for all the builtin commands. `hg help` which shows a short version or `hg -v help` which shows a long version (which is not the man page). git only has two help pages: man pages for long and short text for short.
> gives you the same power as Git
I have yet to see this.
You should understand what you are responding to before typing your answer.
Say today I create an alias “boom = <whatever>” and next week git introduces a boom command.
Now my alias is ignored, my scripts are broken and I might destroy my working copy or kill my cat by using it without knowing it now means something else.
Git gives some very stupid hints “branhc is not a git command, did you mean branch?” it should also warn me when I defined an invalid alias.
Maybe that alias was valid two versions ago or I just don’t know that command exists, since there are eleventy thousand of them.
Your scripts should not rely on aliases you have in your config. Scripts are intended to be used by different people.
Also, the issue of having an alias changing behavior without warning on my command line still exists.
This is the thing that really infuriates me - having a message from my tool that tells me there's an error is one thing, but offering a (frequently incorrect) correction forces me stop and try to figure out why this suggestion was offered, since it frequently has NOTHING to do with what I actually (mis)typed.
Lets put it this way - if this autocorrection feature is so great, then it should prompt you Y/N after giving the error message. Would it then be a better feature? Or would it make more people want to stab it in the eyeballs? Or if this suggestion feature is so great, then it should just DWIM in the first place.
Someone needs to take gits levenshtein algorithm away.
It can do this if you tell it do. The following tells git to wait 10 tenths of a second before executing the corrected command, giving you time to cancel if it is incorrect.
git config --global help.autocorrect 10The question was how to do it programmatically, and there are multiple ways to do it. You might still call that user interface, but it's that's a stretch; fancy shell magic like branch-in-prompt is typically write-once.
Printing all the branches and having me scroll two screens up to find the one that's in color is not a solution to 'what branch am i on'
It prints the branch as the first line of git status: $git status # On branch Foo ...
It also ships with a 'bash-competion' file which gives you a very simple way to disply the current branch (among other things) rather simply.
And of course you can also create your own alias if it is something you wish to do often.
How does having more information available ever hurt you?
Now, fair enough, the fact that mercurial expects branches to be unique might be annoying if you have the tendency to call all branches 'bugfix', but you seem to imply that simply having the branch name in which the commit was originally created as metadata on it would be somehow bad.
>git checkout does what it says on the tin: it checks out a branch or paths to the working tree.
'git checkout is good because checkout means the thing that git checkout does' riiiiight
If you see each of these stories as criticism's about git, then you are the novice. Once you receive enlightenment on each story you will realize why each story is not criticism--you will see the error of the reasoning of the novice in the story and you will understand why Master Git's actions make sense.
Personally It feels a bit dirty. I started out enjoying them, now they feel petty.
1. http://stevelosh.com/blog/ Scroll down. Mercurial everywhere.
However well designed software shouldn't need the context. Well designed software is intuitive. We shouldn't need to know the intricacies in the git data model to able to understand the major commands and tools. As we begin to do more complex operations then more knowledge can be a prerequisite.
The fact that these commands all made sense with a detailed explanation is only a symptom of the problem, not an excuse.
I _still_ have to use Google whenever I try to do anything with git that falls outside my normal workflow.
I agree that git's internal design is very well thought out. Its interface is very inconsistent though.
1 235 026 expert users, my eye
Branches in mercurial are called Named Branches. And they have the benefit that commits made in the branch will always know where they were created.
They have Bookmarks which really are a better name than "git branches" anyway because you can't really understand branching in Git until you understand that a branch name is just a pointer--a bookmark if you will--to a specific HEAD commit.
Basically, people are wrapping stuff up in justifications that didn't even exist when the commands came to be.
That doesn't make it bad, of course, but uh, retroactively justifying things is always a bit odd to watch.
I actually could care less about either git or mercurial; having helped write subversion, the majority of us were/are glad that we were replaced by not just one, but two things that people like more. The fact that it "destroyed" our software is irrelevant, Subversion made what we set out to make, something better than CVS, succeeded, and then the world got even better through git and hg.
The fact that git and hg proponents seem to simply enjoy fighting about which VCS is better/worse, rather than concentrating on making actual progress, is fascinating to me.
I don't see people doing too often, to be honest.
I do see git proponents saying that commands like git-checkout are misunderstood, but that isn't contradictory to "organic growth". From the beginning Linus was claiming that git was more of a content-addressable filesystem than an actual designed VCS.
If anything defenses like "git checkout -b is just shorthand for git branch && git checkout" are nothing but an acknowledgment of that organic growth. There's no reason to add a shorthand for a command sequence that no one uses.
> The experience taught me that sometimes (perhaps often) we don't make choices based on our explicit preferences. Instead, we have a gut feeling about what we want, and we go through a process of mental gymnastics, applying all kinds of justifications to manipulate the criteria. That ways, we can get what we really want, but at the same time keep up the appearance -- to ourselves and to others -- that we are acting in accordance with our rational and well-reasoned preferences.
Simply replace all occurrence of "we" with "mercurial/git fanboys" based on your own preferences ;)
The Long and the Short of It: -h is not a valid argument. Passing -h therefore results in providing usage information.
But I still use git because I already grokked it before I learned hg.
What about the index? Or the branching (bookmarks kinda work now I think), or the better remote management?
Now, what can I do to see all changes I have made so far?
I have done this many times with subversion (svn diff) and mercurial (hg diff). Git's index just get in the way.
That's the default of what git does even without a command.
[alias]
addremove = !git add . && git add -u
That will do it though, so it's not too hard to add. -a, --all
Tell the command to automatically stage files
that have been modified and deleted, but new
files you have not told Git about are not affected.Why? I find that in the middle of a big project I have lots of stupid files strewn about that I absolutely don't want checked in (TODO lists, patches I'm considering, copies of large log files I'm investigating, etc). I always add things explicitly and delete explicitly, otherwise I'd get random junk checked into my repo. How do you keep your repo clean enough to actually use automatic commands like that?
I guess other people just aren't as messy as I am?
That said hg is the better choice for big software shops IMO, because of the lower training cost and because they probably won't use github anyway.
Considering more people know git I would assume the training cost for hg is higher.
And yet I have noticed that people find merging and branching much easier than in mercurial. Few people understand the implications of named branches in hg.
In contrast, git branches symlink to a different ref, whenever you commit to it.
A git tag could be called bookmark intuitively.
That's the first time I've heard this opinion - do you know what kind of complaints people have?
In my experience, it's much easier to say 'When you start work on an issue, start a branch called 'issue-XXX', then once you're done we will merge it into the default branch'. I've never seen much confusion in response to that.
Something that leaps out to me as horribly wrong is that aliasing a builtin doesn't cause the config file to be "invalid". Doing anything else is just doing spooky things that are explained only several hundred lines into the man page by a single sentence.
Maybe better would be to choke on config lines that alias a builtin, and to print error messages before or after the output of each git command while the offending line(s) are present.
Further,
git config alias.pull "pull --ff-only"
should definitely be an error. Instead, git gladly makes the (no-op) confusing edit to your config file.Thanks, Master Git!
The critique is quite well established. One needs to understand these quirks to be an enlightened Git user.
However we make up with them, as the overall gain from Git's approach to version control is so big.
Some things could be improved though, like introduce something like "git branch --current" to avoid the Hobgoblin...
Also of course I had to test the help commands!
git branch --help opens man
git branch -h throws those short commandline help summaries.
Interestingly, -h does not mention -h itself or --help, so the only way to know that --help exists, is someone else telling you, you will never find on your own.
(Disclaimer: This is a hyperbole.)
$ git -h branch
[blargh]I've found the --help option on probably a dozen commands without ever being specifically told that --help was a valid option.
If you have trouble discovering it you might want to look inward, at least for that particular well-known option.
Using rebase to clean you local history is ok. Maybe even recommended or mandatory depending on the project. Changing history, which is already in other people's repos, will lead confusion and should really be avoided.