In this article, we're not going to review the basics of source control management, regardless of which one you use. Let's just assume that you already know how to get around. What we are going to cover is how the pros use git. We'll take a look at some of the advanced features and workflows that you might not already be familiar with. Hopefully, you'll walk away with your mouth agape at the sheer possibilities that git provides!
If you are anything like me, you love to explore how other developers work.
For the uninitiated, or those coming from another SCM, Git is a distributed version control system. It is free and open source, has a tiny footprint, and can fit within the workflow that suits you best. Generally, it doesn't force you to work in a particular way, which means there are many different methodologies on how to use its features, like staging areas, branching and tagging releases. If you are anything like me, you love to explore how other developers work. So get ready to start modifying your
.gitconfig, because you're in for a treat. Let's see how the pros use git.
Stage Your Changes in Hunks
You are likely familiar with accidentally modifying a single file for two different reasons without committing in between.
You are certainly familiar with adding files to the staging area with the appropriately named
add command. And you are likely familiar with accidentally modifying a single file for two different reasons without committing in between. So you will sure have a
git log filled with messages like "Edit X and change unrelated Y". If this sounds like your workflow, then interactive adding is your new best friend.
Interactive adding, or adding a patch, walks you through your changes one hunk at a time. When you add a file with the
-p command, you will be prompted at each logical change (i.e., successively edited lines will be grouped together). There are a number of choices you can make on each hunk, from splitting the current hunk into smaller ones, skipping a hunk, or even manually editing it. Use the
? option to see a complete list of commands.
Getting started with staging hunks is as simple as:
git add -p <FILE>
Checkout Your Last Branch
As a good coding citizen, when you come across something that needs a quick fix or cleanup, you should probably take a moment to change it. But if you are using a heavy feature-branch workflow, then you don't want that unrelated fix in your feature branch. This means you'll need to
stash your current changes, change to your master branch and then make the fix there. Bouncing around between branches can be tedious, but, luckily, there is a quick shortcut for switching to your last branch. (via Zach Holman)
git checkout -
This syntax should look pretty familiar to *NIX users. The
cd command has a similar shortcut (
cd -) that will jump to the last directory you were in. You'll never have to remember what you named that feature branch when you need to switch back; just
git checkout -.
Show Which Branches are Merged (or not)
When working with feature branches, you can quickly create so many that clutter up the output of
git branch --list. Every now and again you want to get rid of the branches that have made it into master. But you probably have a quick pause before you
git branch -d <BRANCH>, but with the below commands you can confidently delete them without a second thought. (via Zach Holman)
If you want to see which local branches you have that are merged into the branch you are currently on, then all you need is:
git branch --merged
The reverse is also available. Show which branches haven't been merged into the currently selected branch with:
git branch --no-merged
Mash this up with a couple easy UNIX tools and you can quickly delete everything that has already been merged:
git branch --merged | xargs git branch -d
Grab a File from Another Branch without Switching Branches
Let's say that you're experimenting with some refactorings, and you have a few branches that have various changes you've made. If you have changes in a file in some distant branch that you want to bring into your current working branch, then you could do any number of steps. Without the below tip, you'd probably stash your current changes, switch branches and grab the file contents you want to change, switch back (with
git checkout - of course) and make your edits. Or you could simply checkout just that file which will merge it into your current branch (via Zach Holman):
git checkout <BRANCH> -- path/to/file.rb
Git Branches Sorted by Last Commit
So you've got the cluttered branch list that we talked about before; some of those you've cleaned up with the
--merged flag. But what about all those other branches? How do you know which ones are useful or entirely out of date? The
for-each-ref command will output a list for each branch and show the reference information for the last commit. We can customize the output to include some useful information, but, more importantly, we can sort the list by date. This command will give us a list of branches with the last commit message and committer, sorted in descending date order. (via Rein Henrichs)
git for-each-ref --sort=-committerdate --format='%(committerdate:short) %(refname:short) [%(committername)]'
While you could type this command each time, I highly recommend making it an alias and save yourself some serious headaches.
git config --global alias.latest "for-each-ref --sort=-committerdate --format='%(committerdate:short) %(refname:short) [%(committername)]'"
People in Glass Houses Shouldn't Use Git Blame
Or at least they shouldn't use
git blame without one of the options flags below. Git blame is powerful; it's basically like using science to prove you're right. But be careful, many changes are superficial and to find the real source of the in-question code takes a bit more hunting. Things like removing white space, moving text to new lines, or even moving text from another file can be ignored to get to the original author of the code much easier.
Before you git blame someone, make sure you check one of these:
git blame -w # ignores white space git blame -M # ignores moving text git blame -C # ignores moving text into other files
Find a String in the Entire Git History (and Remove It)
From time to time, you need to hunt down a line of code you know you wrote but just can't find. It could be stuck in some distant branch, deleted a long long time ago, or hiding in plain site; but either way you can find any string in your entire git history by mashing up a few commands. First, we're going to get a list of all commits, and then grep each of them for our string.
git rev-list --all | xargs git grep -F '<YOUR STRING>'
You probably have a friend who has accidentally committed sensitive data to a repo: access keys, passwords, your grandmother's secret marinara recipe. The first thing they should do is change their passwords and revoke access with those keys (and apologize to your grandmother). Next, you'll want to hunt down the offending file and remove it from the entire git history, which sounds far easier than it actually is. After this process is complete, anyone that pulls in the cleaned changes will have the sensitive data removed as well. Forks of your repo that do not merge your upstream changes will still contain the compromised files (so don't skip changing passwords and revoking access keys).
First, we'll rewrite the git history for each branch, removing the file with the sensitive data.
git filter-branch --index-filter 'git rm --cached --ignore-unmatch <FILENAME>' --prune-empty --tag-name-filter cat -- --all
Add the file to
.gitignore and commit to update
echo <FILENAME> >> .gitignore git add .gitignore git commit -m "Add sensitive <FILENAME> file to gitignore"
Since we are rewriting history, you'll need to force push the changes to your remote.
git push origin master --force
The compromised files still exist in your local repo, so you'll need to do a few clean-up tasks to purge them entirely.
rm -rf .git/refs/original/ git reflog expire --expire=now --all git gc --prune=now git gc --aggressive --prune=now
Ignore Changes in a Tracked File
Working with someone else's code in your environment can mean that you need to make any number of config changes to get the application running. It is all too easy to accidentally commit a change to those configs that were meant exclusively for your environment. So, instead of always watching out for those files and having them linger in the "modified" staging area, you can simply tell the git index to ignore changes to that file. You can think of this somewhat like a git ignored file that stays with the repo. (via Arnaud Coomans)
git update-index --assume-unchanged <FILENAME>
Zero Out a Branch's History
Sometimes starting from scratch is exactly what you need to do, for any number of reasons. Maybe you've inherited a codebase that you can't ensure is safe to open source, maybe you're just going to try something entirely new, or maybe you're adding a branch that serves a separate purpose that you want maintained with the repo (like GitHub Pages). For this case, there is a very simple way to create a new branch in your repo that essentially has no history. (via Nicola Paolucci)
git checkout --orphan <NEWBRANCH>
Aliases You Can't Live Without
Stop wasting time typing long commands and make yourself a few useful aliases.
No discussion of git would be complete without talking about various aliases that will literally save you minutes a year in saved keystrokes. Stop wasting time typing long commands and make yourself a few useful aliases. Aliases can be made by adding them to your .gitconfig file or using the command-line
git config --global alias.<NAME> "<COMMAND>". Below are just a sample of alias that you can use as a springboard for ideas.
co: with a feature branch workflow, you'll be moving between branches regularly. Save yourself six characters every time.
co = checkout
ds: it is always best practice to review the changes you're going to commit before making the actual commit. This allows you to catch typos, accidental inclusion of sensitive data and grouping code into logical groups. Stage your changes and then use
git ds to see the diff of those changes.
ds = diff --staged
st: you should be pretty familiar with the verbose output of git status. At some point you'll want to skip all the formality and get down to business. This alias shows the short form of status and includes the branch details.
st = status -sb
amend: did you forget to include a file with your last commit, or maybe you had one tweak you needed to make? Amend the staged changes to your last commit.
amend = commit --amend -C HEAD
undo: sometimes, amending your last commit isn't enough and you'll need to undo it instead. This alias will step back one commit and leave the changes from that commit staged. Now you can make additional changes, or recommit with a new message.
undo = reset --soft HEAD^
ls: working on a codebase with a group of developers means trying to keep up with what people are working on. This alias will provide a one line git log including date and committer name.
ls = log --pretty=format:"%C(yellow)%h %C(blue)%ad%C(red)%d %C(reset)%s%C(green) [%cn]" --decorate --date=short
standup: this alias is great for reviewing what you worked on yesterday for any type of daily standup, or just to refresh your memory in the morning.
standup = log --since '1 day ago' --oneline --author <YOUREMAIL>
graph: a complex git history can be difficult to review in a straight line. Using the graph flag shows you how and when commits were added to the current branch.
graph = log --graph --pretty=format':%C(yellow)%h%Cblue%d%Creset %s %C(white) %an, %ar%Creset'
Git can be both amazingly simple and mind-blowingly complex. You can start with the basics and work yourself into more complex graph manipulation over time. There's no need to grok all of it before you can use it. The command that will be most powerful as you learn is
man git-<command>-<name>. Try using it before you refer to Google for an answer.