When I switched jobs four years ago, I went from using subversion (svn) to using git as the version control system. Even though I am a pretty quick learner, it took me a quite a while to really understand git. I read a lot on how git works, but even so, I didn’t always realize what the implications were for how to use git. Here are six big “aha moments” I had on how to use git.
Looking at these aha moments now, they all feel really obvious. But I remember literally thinking “aha” when I understood a concept and realized the ways in which I could then use git.
1. Local and remote changes are independent. I often used “svn status -u” to see what changes I would get if I ran svn update. In the beginning I was confused about the equivalent operation in git. You can run git status, and that will show the local changes compared to what was last pulled. But to see the changes on the server, I first need to do a git fetch. This concept of there being two independent sets of changes – local changes, and changes on the server (pushed there by other people) – took me a while to really grok.
2. Merge makes older commits accessible. It took me a long time to realize that merging simply makes already existing commits accessible. Initially my mental model of a merge was that all changes from the merged branch became commits at the time of the merge. Realizing that commits from a branch remain, and that merging simply allows them to “be seen” in the other branch helped explain some odd things. For example, showing git log of a file (for example in PyCharm) can show commits from two different branches interleaved. Showing a diff between two commits (adjacent in time, but on two different branches) can give a really big diff.
3. The meaning of merge commits. Since my understanding of merging was incomplete, merge commits used to be a bit of a mystery to me. I viewed commits only as “what is the diff of the code for this commit”, and from that perspective, merge commits didn’t make any sense. Often they are empty, with no code diff at all. Now of course I understand that it is important to see at what point commits from another branch were made reachable by a merge.
4. You can merge in both directions. If you have a long-running branch b that you eventually want to merge back into master, you can resolve the merge conflicts a bit at a time by periodically merging master into b (and fix any merge conflicts that arise). Eventually I want to merge all the changes from b into master, but that can only be done when b works as intended. It was a big conceptual aha moment for me when I realized that I don’t have to wait till b is done before I resolve some merge conflicts. Merging master into b at regular intervals means that when I finally merge b into master, most of the merge conflicts will already have been dealt with.
5. You can start over if a merge fails. When I used svn, I often used –dry-run to see what would happen if I did a certain operation (but without actually doing it). In git, I took me a while to realize that the same functionality exists, but it is not defined explicitly. For example, if I merge some changes, and I mess up resolving the merge conflicts, I can simply go back one step and do it again. As long as I did not push to the server, it is no problem to go back by doing a “git reset –hard HEAD~”. Now I can simply do the merge again, and hopefully resolve the conflicts correctly this time. It is very liberating to know that almost anything I do with git can be easily reverted, undone or redone.
6. Committed changes can be reset to local changes again. Sometimes I work on a branch b with local changes, and I need to check out another branch and do some work there. In the past, I would often stash the local changes. But these days I often commit the local changes, with a commit message that it is work in progress (WIP). I don’t push these changes to the server. When I come back to branch b, I do a “git reset HEAD~” to get the committed changes back as local changes again. Making WIP commits on a branch means I don’t have to keep track of which branch the stashed changes are meant for. It was a real aha moment for me to realize that I can “undo” the commit into local changes again.
I remember reading a lot of tutorials on how to use git, and how git is organized. But even knowing how git works didn’t immediately translate into understanding how I could use git. The above aha moments only depend on understanding a few things – that there are local changes, and how branches and merging works. Knowing what I know now, all these insights seem trivial. However, I remember quite clearly that these were not obvious to me when I started learning git. Hopefully these aha moments can be useful to somebody else new to git.
Thanks for the blog! I’ll try the “git reset HEAD~” as an alternative to stashing.
Have you tried rebasing to keep a branch up with master? A rebase replays all changes from master on the feature branch. You’ll get a clean merge (fast forward) when you merge the branch into master.
Thanks for your comment! Yes, sometimes I rebase, sometimes I merge, it depends on how big my local changes are.
Thanks for this article, I am sure your 6th point will be very useful .
1, Why not use rebase when you want to update your feature branch with new commits from master? It does a rollback of all your changes up to the point that you diverge from master, then adds all new commits to your branch, and lastly it recommit’s your changes onto the feature branch.
2, Instead of ‘git stash’ you can do ‘git stash -u’ to include new untracked files to the stash.
1 – either way you need to resolve conflicts if the same parts of the code has been touched.
2 – if you have several branches, it is still up to you to keep track of what the changes refer to if you use stash.
To me the biggest “a-ha” was when I realized branches were not big static silos on top of which you added commits, with merge conflicts being an disruptive event occurring when you tried to simultaneously pile on two different commits to the same static branch.
Instead, branches are simple pointers to commits (not unlike tags, except they autoadvance *on your local repo* when you add a commit).
And merges are just a particular way of creating a new commit, which happens to have more than one parent commit.
Then, since you can move the branch pointer to any commit at any time, if you’re not happy with a particular merge commit you can just move the branch pointer away to any other commit in history and let the failed merge be eaten by the garbage collector. Wow! Such freedom!
When you run “git reset HEAD~” you’re erasing a commit, effectively bypassing git’s safety net. Instead consider finishing your changes with additional commit(s), then using interactive rebase to a) squash them back into a single commit and b) change the commit message from WIP to something meaningful. This way you can always fall back to the original WIP commit if you mess something up.
Thanks Tim, that’s good advice!
After speaking with our resident expert I should retract my statement. I was under the impression that resets destroy commit objects but they don’t. Either of our methods is acceptable. Just don’t use stash! 🙂
“Till” is not short for “until”; it’s the word for cash-drawer.
” ’til ” is short for “until.”
Till is fine according to Merriam-Webster: https://www.merriam-webster.com/dictionary/till
I suppose that dictionary also spells colour as color 😦
For your mental model: Git stores snapshots, not changes/diffs. A commit represents a snapshot and also contains (any number of) references to previous snapshots. A diff can be computed by comparing one snapshot to another one. You don’t commit changes with git. You commit snapshots.
Maybe this clarifies some more oddities of git for you. 😉
Thanks copacetic, what I describe in this blog post is what I had trouble understanding when I was learning git – all of the above points make perfect sense to me now 🙂
Including that git stores snapshot, not diffs – but it is useful to emphasize.
Valuable post! I learned a few things.
The biggest “aha” moment for me was realizing that things like branch names, tags, stash, etc. are all merely *labels* on a giant graph of commits. What really matters is the graph, not the labels. The labels just help you keep the graph organized.
Pingback: 6 Git Aha Moments | Henrik Warne’s blog – Bitbucket Bits
No mystery, when you know what a DAG is!
You don’t really have to fetch origin to compare with a local branch. For instance, to see how your local development branch diverges from origin, do:
git diff development..origin/development
The same goes for any other public branch.
It also helps to track upstream branches so that git status will report their commit status such as your local branch is ahead of origin by x number of commits.
> You can start over if a merge fails
Git porcelain is hard. If merge or rebase fails, it is really really hard to recover. The only solution that ever worked for me is to just backup the entire git directory or clone again and redo the whole thing again!
Recovering from an error is much easier in mercurial. But the world revolves around git now.
Hi Arun, I you haven’t pushed to the server, try what I describe in point 5. You go back one step an do the same operation again.
Reddit discussion: https://redd.it/8to631
Lobsters discussion: https://lobste.rs/s/naubzi/6_git_aha_moments
Dev.to discussion: https://dev.to/henrikwarne/6-git-aha-moments-3097
Pingback: Weekly Links #119 – Useful Links For Developers