I like Git. I use it all the time. As I sometimes do, I recently took some time to really dig in, read through documentation, and review my global Git configuration. Welcome to my fourth stack improvements post!
I started coding in the bad old days of plain filesystem copies and Visual SourceSafe with its exclusive locks on checkout. Even so, back then the concept of source control was so amazing to me I wished I had access to it when coding at home.
Later, at Cal Poly, I was exposed to Concurrent Versions System (CVS) as I worked on group projects. There were only so many group projects, so I never got very good at it.
During my Microsoft years, I used Team Foundation Server for source control during what we called “App Week,” where non-developers in Visual Studio would spend a whole week dogfooding the product to make sure it was ready to ship. But all of my personal programming during that time was with Subversion. It was free and easy to run locally. I used it to keep track of all my local changes over time.
Fall of 2010 was when things changed. I learned Ruby on Rails to develop a side project, and the comprehensive tutorial I went through covered Heroku and a new source control system: Git. It was amazing - I could treat it like it was hosted locally, but also interact with others. No exclusive locking, full productivity when offline, and intelligent merging. I was hooked.
Since then Git has truly taken off. It is the de facto standard for open source. It is supported by every major player in cloud source code hosting. It is supported by many GUI tools - dedicated source control tools, as well as code editors.
It’s important to know, and to know well.
Whether you know it or not, you’ve got global Git configuration. A .gitconfig
file in your home directory. Most .gitconfig
files have your name and email, as established by your average ‘getting started with Git’ tutorial. But there’s so much more configuration available for that file!
My entire global .gitconfig
is available via a gist with inline comments. Jump directly there if you like, but here we’ll talk about each of the sections in a bit more detail.
The bread and butter of a custom .gitconfig
is the [alias]
section, where you can create your own commands. Feel like something is missing? Add it here! Something doesn’t quite work the way you want? Add your own version of it!
prune = fetch --prune
- When working on a project where others push branches to the main repository, I end up with lots of random local branches. Prune deletes any local branch which has been deleted from the remote. It’s here because I always forget about it.undo = reset --soft HEAD^
- If I’ve made a mistake in making a commit, this command sets things up the way they were before the commit. To be fair, usually I just amend the existing commit in this situation, since that keeps the commit message.stash-all = stash save --include-untracked
- Stash is extremely useful when someone randomly asks you to check out another branch, but you’re right in the middle of something. This command ensures that when you stash, you catch the new files you haven’t caught with a git add
yet.glog = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset'
- Commit history shown via the default git log
is space-inefficient, and doesn’t really focus on the most important information. This colorful, graphical display is easier to parse, especially when branching gets complex.ff = only
ensures that you get an error unless every merge is fast-forward. No merge commits, no joining together of two histories, just a smooth progression from commit to commit.
You might be wondering how you’d get real work done doing this! The answer is git rebase
, a way to take a branch of the commit tree and attach it to a different part, giving it a new ‘base.’ It’s an extremely useful tool.
It’s how I update a pull request when it has conflicts with master
. Essentially I get a chance to re-apply all of my commits in a future world, where others have made changes. I think it’s a better metaphor than “merge everyone else’s changes into my changes,” which is what you get if you merge the latest master
into your branch.
This option is great because I will never accidentally create a merge commit. If I intend to create a merge commit I can force that behavior with an explicit git merge --ff
.
conflictstyle = diff3
gives you a little more information when a merge conflict happens. Normally you get two sections - the intended changes from the ‘left’ and the intended changes from the ‘right.’ With this option you get a third section, the original changes before ‘left’ and ‘right’ tried to change it.
gpgSign = true
ensures that all of your commits are signed by your GPG key. This is generally a good idea because there’s no verification of the user information in your .gitconfig
file, which means that commits that look like they are from you could easily show up in someone else’s pull requests.
In fact, for a contract I once had to use someone else’s credentials because account and machine provisioning were taking too long. My pull requests were submitted through someone else’s account, but all the commits inside had my real name on them.
You can remove all question about where a commit came from by adding your public GPG key to your GitHub account. Your signed commits will have a little “Verified” badge on them.
Note:
user.signingKey
option.gpg-agent
can save your passphrase for a time, making things a little easier. Use it!default = simple
is an option you probably already have set. It makes it easier to push your local branch to a branch named the same thing in your target remote.
followTags = true
is quite simple. Instead of manually pushing new tags with --follow-tags
, you always send your local tags up along with a git push
. I don’t know about you, but once I have tags created locally, I want them included when I push.
showUntrackedFiles = all
Normally, when you’ve added a new directory, but haven’t staged it yet with git add
, your git status
will just show the directory name. This has tripped me up quite a few times, since a new, large directory shows up as just one line. This option shows you all of the files underneath that new directory during a git status
.
Note: This can make things slower in very large repositories.
fsckobjects = true
tells Git that you’d like it to do some extra checks when receiving or sending changes. Why do extra checks? You want to be alerted to evidence of data corruption sooner rather than later, right?
Note: This can make transfers a bit slower.
In addition to the built-in git diff
command, Git allows you to specify an external tool to visualize your diffs. This collection of entries sets Git up to use icdiff
to display the differences between two states of your repository:
[diff]
tool = icdiff
[difftool]
prompt = false
[difftool "icdiff"]
cmd = /usr/local/bin/icdiff --line-numbers $LOCAL $REMOTE
You can use it just like normal: git difftool master branch
icdiff
is interesting in that it attempts to replicate colorful, GitHub-style, split diffs right in your console. A little easier to read than the normal chunk-based diff style.
Note:
icdiff
. Happily, there is a simple workaround.git diff
in your back pocket - icdiff
doesn’t seem to handle comparisons against /dev/null
. For example, try git difftool --cached
after you’ve staged a new file.You type git checkout master
all the time, right? In that command, master
is an example of a revision, specifically a shorthand referring to the latest commit in the master
branch. These are the common revision formats:
# Check out a branch
git checkout branchname
# Check out a remote branch
git checkout remotes/origin/branchname
# Check out a specific commit
git checkout 158e4ef8409a7f115250309e1234567a44341404
# Check out most recent commit for current branch
git checkout HEAD
But it turns out that there’s a whole language to specify revisions. All of these operators apply to any of the nouns used above:
# ^ means 'first parent commit,' therefore the second-most recent commit in the master branch
git checkout master^
# If it's a merge commit, with more than one parent, this gets the second parent
git checkout master^2
# Same thing as three ^ characters - three 'first-parent' steps up the tree
git checkout master~3
# The first commit prior to a day ago
git checkout master@{yesterday}
# The first commit prior to 5 minutes ago
git checkout master@{5.minutes.ago}
You can find the whole set of supported revision formats here: https://git-scm.com/docs/gitrevisions. I was surprised how comprehensive it is!
Remember, you can use a revision with most Git commands. Try this: git glog master@{10.days.ago}..master
Start with my .gitconfig
or just use it as an inspiration for your own customizations. You could even do a deep-dive like I did - the command list and the list of options are extremely educational.
Make Git maximally useful to you. Comfort with Git leaves more room for the hard bugs and the actual merge conflicts. Dig in!
Some good references:
project-dir/.git/config
git log
options: https://git-scm.com/docs/git-log#_optionsAfter five years working with Node.js, I’ve learned a lot. I’ve already shared a few stories, but this time I wanted to focus on the ones I learned the hard way. Bugs, challenges, surprises, and... Read more »
I’ve been told that I’m a very productive developer. And I’m sharing how I do it. Welcome to the fifth in my developer productivity tips series: Think in alternatives. Your solution works, yes. Did... Read more »