As a modern software engineer, you probably have a good working knowledge of the most common Git commands—committing, branching, and merging. But there's actually much more hidden depth to Git than you might expect. So much so, in fact, that it could change the way you work. For one thing, your commit history is much more fluid than you might assume—it isn't quite written in stone the way most beginner tutorials might lead you to believe.
This post is about understanding the fundamental ideas behind Git, unlocking some hidden features, and taking your version control to the next level. As such, it assumes you already have a decent grasp of committing, branching, and merging. It's also a helpful reference for me personally—there's a few super rarely-used but helpful commands here that I repeatedly forget the syntax for.
Global Gitignore
Let's start with something you may already know. You're probably
aware that if you create a file called
.gitignore
in your Git project and add some ignore
rules, Git will automatically ignore any files that match those
rules. What you may not know is that you can create a global
ignore file which will apply globally across all projects. Try
creating one in your home directory that looks like this:
Note that you can name your file anything and keep it anywhere. Then, simply run:
and all files that match will be ignored in all situations. It's recommended to keep editor-specific ignore rules here; this prevents every team member from adding their own editor- and workspace-specific rules to every project.
Git Grep
In large projects, it's often very slow to use regular grep to
find a piece of text in a folder. Fortunately, Git ships with a
tool called git grep
, which will skip untracked
files to speed up search. Run:
Remotes
You might have also heard about the distributed nature of Git, but have you ever actually taken advantage of it? It's good to know how to handle remotes flexibly in case you ever run into the odd situation where you need it. For example, if your Git server goes down for a day or you're in a place without network access, you can continue collaborating with your teammate by adding their machine as a remote. This can also be useful, for example, if you're working with a contractor developing code for you who isn't allowed access to the corporate intranet.
If it's a new repository, rather than cloning from GitHub, you can clone from a peer machine on the same network over SSH using: Then you'll have to ask the owner of the peer machine to enter their password, and you're good to go! Push and pull like normal. Pro-tip: Ask them to set up a new user on their machine with a shared password and have them push/pull to a repository owned by that user. You generally don't want to push/pull to a repository someone works on directly.
Alternatively, the repository may already exist on your machine
and you want to add your colleague's machine as another remote.
In this case, use:
Check to make sure it got added correctly with git remote
-vv
—you should now see two remotes listed. Now you
can simply git pull my_other_remote master
and
git push origin master
whenever you need to pull
code from your colleague's machine and sync it with the origin.
Stashing
Git's stash is incredibly helpful. It's used to save uncommitted
changes to be applied later, even to a completely different
branch. For example, very often when you're trying to git
pull
it will fail because you have a conflicting change
that's preventing Git from pulling. In this case, you can
git stash
your changes, then
git pull
, then run git stash pop
or
git stash apply
(pop is the same as apply but also
also deletes the stash unless there is a conflict). Now you can
properly deal with merge conflicts if there are any.
You can have multiple stashed changes. Run git stash
list
to see them all and git stash show -p
stash@{#}
to view the stash in diff form, replacing
#
with the ID of the stash you want. git
stash drop stash@{#}
is used to delete a stash. It's
very helpful when you have uncommitted changes preventing you
from performing a Git action, e.g. checking out another branch.
Use git stash clear
to delete all stashes.
All of these commands can be run without a stash ID—the
action will simply apply to the topmost stash
(stash@{0}
). To keep your stashes from getting out
of control, you should attach memorable messages when stashing.
Use
Patches
Sometimes you need to move code around but can't or don't want to push it to a remote. Maybe it's test code that you want to send your colleague to debug, and you'd rather not push it, even as a WIP commit. In this case, you can write your changes into a patch, which is just a text file containing the diff. Simply run
and your diff will be saved into the file. Send it to your colleague, and let them apply it to their copy of the project with
Branching
As a mindful developer you should strive to keep your Git history as maintanable as your code. There's a variety of techniques you can use, but be forewarned—some of them sound like heresey because they result in altered Git histories. It's common to hear colleagues call these techniques evil, but the truth is that anything can seem evil if attempting to use it without fully understanding how it works. Bear with me and you'll find they actually result in cleaner code bases and easier debugging when used properly.
The only rule you need to follow is, if you do choose to alter your Git history, either make sure it only affects a local set of unpushed commits, or wait until you're about to merge and your branch will be deleted right after the merge. Unless you know what you're doing, do not ever use
to push your changes to the remote when collaborating. This will overwrite commits on the remote branch and could unintentionally destroy someone else's work, so be extremely careful.
Amend
Say you've just committed your code, only to realize it doesn't compile because you missed a semicolon somewhere. Should you make another commit just for the semicolon fix? Well, if you haven't pushed yet, you're in luck! Make the fix, stage the file(s), and use:
You'll end up with the fix amended to your last commit. Note that the hash of that commit is no longer the same after the amend, which is why this technique only works if you haven't pushed—you're changing Git's history. Note that you can also use this technique to just edit the last commit's message.
What happens if you have fast fingers and accidentally amended a
commit instead of creating a new one? Fortunately, Git has your
back. The old commit is still in Git's reflog, which keeps a
history of where your HEAD
was recently. View the
reflog with:
HEAD
should currently be pointing to your amended
commit, but HEAD@{1}
should be pointing to your
pre-amend commit, which still exists but is not part of any
branch. To undo the amend, you need to reset back to
HEAD@{1}
, i.e. the commit HEAD
was
pointing to before now. You don't want to lose the amended
changes, so we'll use a soft reset. Finally, we commit the reset
files, but only with the details of that HEAD@{1}
commit. This should restore you back to your pre-amend state:
Moving Commits
Has it ever happened to you that you've committed to the wrong
branch? It's okay, it happens to the best of us! Just don't
git push
yet. Run:
to copy over the accidental commit to this branch. Now run
Now you can checkout your topic
branch and push as
normal. Phew, no problem. Cherry-pick can help you out of tight
spots, but be aware that it's very easy to abuse and get you
into even more trouble.
For context, git reset --hard
will delete commits
locally—you'd have to git pull
to get them
back from the remote. Using git reset --soft
, on
the other hand, will undo the commit but leave the changes from
that commit staged.
Rebase
Finally, one of the most powerful tools in Git is called rebase. It has two main uses—rebasing on top of another branch and interactive rebase. Both involve changing history, so should be not be used after you've pushed.
Say you've been working locally on an unpushed branch called topic, which initially branched from master. You see some new commits appear on master that you'd like to have in your branch. At this point, you have two options—conventional wisdom would have you merge master into your branch, but this tends to make your commit history a tangled mess of back-and-forth merges. Instead, you can rebase if you haven't yet pushed. Once you've fetched the new commits on master, simply run
and voila! Your commits have now moved on top of the latest
master. If you encounter a conflict, don't fret; just resolve it
like you would for a merge, then run git rebase
--continue
(Git will prompt you). Now your branch history
will stay clean, you'll be able to trace commit history easily
across the project, and you'll be guaranteed to have a clean
fast-forward when you merge back into master. As an added
benefit, the commits related to your feature will stay together
as a group instead of interspersed with many others in master.
As an aside, if you're working on a pushed branch but have some
unpushed commits, you can pull your teammates' changes to that
branch with git pull --rebase
to rebase your
commits on top of theirs.
Interactive rebase, on the other hand, is a great feature that comes with great power/responsibility. Imagine you're working again on a branch called topic. You've been diligently marking commits with WIP to signify that they don't necessarily build, and you're getting ready to merge back to master soon. At this point, you might want to combine multiple WIP commits into one that actually builds correctly. This is called squashing; it's a feature within Git's interactive rebase. If all your commits are unpushed, you can clean up your commits as you go without messing up anyone's history. Run:
replacing #
with the number of commits you want
to rearrange. You'll enter the interactive rebase interface.
You'll see instructions there for squashing, rewording, editing,
etc commits. You can also rearrange and delete commits in this
interface, but be careful not to lose any work. And that's it!
You've changed Git history. If you do change history after it's
been pushed, you can use
git push --force
, but be extremely careful because
you risk undoing someone else's work.
Conclusion
I hope that these techniques, life-changing or not, will help
make you a better developer. For a more thorough guide, I'd
recommend checking out Pro Git. I believe a
great developer should maintain their source tree just as
fastidiously as their code. Not only does a clean history help
out your teammates, but it also helps you when bug-hunting
(enter git bisect
, a topic for another post). Try
out the commands in a test project to get used to them and
you'll reap the rewards. Now git outta here!