Tips for the Git Power User

Tips for the Git Power User

After developing for so long working with Git has become second nature to me. I started out using a graphical Git client for Windows called Git Extensions. Git Extensions was great for starting out but as I learned and got used to the command line tool, I slowly stopped using the graphical interface. I've been getting a lot of questions about Git lately, both on YouTube and from some of my colleagues at work. I decided to put this little guide together to show off some of the Git tips that I employ in my workflow.

In this guide I'm going to assume that you already know the basics of Git, including what a rebase is. If I try to explain those topics here then this guide will get insanely long and we'll never get to the actual workflow. With that said, let's move on to my first tip.


Install Source Tree

http://www.sourcetreeapp.com/

If you're a big fan of just using Git from the command line like me then you might be wondering why this is my first tip. Having a good graphical Git client is just plain productive. When you're inevitably trying to do something that should be simple and you just can't seem to remember the proper syntax on the command line, open up Source Tree. Oftentimes I will take the time to search Google for the proper command line syntax to accomplish what I'm trying to do; just as often, I simply don't have the time. Having a good GUI ready to go when you need it will save you, a lot. There have been plenty of times when I just need to whip my repository into shape so I can get on with my work.

One nice thing about using a GUI is that it seemlessly works with your current Git installation. You don't have to do any weird juggling of your Git GUI and your Git command line tool. The two are one and the same. You're definitely welcome to use any GUI you wish but my favorite is Source Tree. It's cross-platform and has the cleanest interface of any Git GUI I've used. To top it off, I haven't found any missing features, which is my top complaint in most of the GUIs I've used in the past.

Source Tree presents you with a simple repository bookmark window. You can create folders to group your repositories in and you can even nest them. It's nice for a quick visual of the state of all your repositories.

Once you open up a repository you are presented with a simple and clean interface that exposes pretty much every feature of Git that you need.

All your standard Git options are available. You can push, pull, rebase, merge, commit, tag, you name it. You can even force push if you enable it in the Source Tree preferences.

You can easily see all the remote repositories linked to yours as well as all the remote branches on those remotes. Checking out a remote branch is as easy as right-clicking one of them and selecting "Checkout".

The commit history window is easily my favorite part of having a Git GUI to work with and Source Tree's is pretty awesome. On the left you get a nice visualization of how commits are related to each other. It really helps to clarify confusion in a way that the command line just can't. You can easily see what commits your remote and local branches are pointing to. You can even right-click commits in the history and check them out with ease. It really makes narrowing down difficult bugs a breeze.

Clicking on commits in the history window will show you the files that were changed in the clicked commit. It's easy to revert changes to a single file or browse through all the files in a commit to find what you're looking for.

Clicking on files in a commit will show you an easy to read diff in the bottom right pane. Being able to do all of these things at a glance is just too invaluable sometimes to not have some form of Git GUI installed on your machine.

You can even access your repository's settings with the settings button in the top right. From there you can easily add or edit the remotes that your repository is aware of. You can even override global config settings for specific repositories easily.

If you use Alfred, there's even an awesome workflow for Source Tree that will allow you to open a repository directly :D

I don't need to go super deep into Source Tree, but hopefully you can see already why having a tool like this can really simplify working with Git. A lot of terminal purists will tell you that you shouldn't ever use a GUI and that real programmers stick to the terminal. I think that's a silly attitude (as well as a No True Scotsman fallacy) and you should use whatever tools make you the most productive. In addition, I've found that using a GUI simultaneously with the command line has really pulled back the curtains on how Git works. The GUI abstraction hasn't turned me into a wet noodle of a developer, it has actually strengthened my understanding of Git as a whole.


Git Aliases

Even though I love Source Tree, I still mostly use the command line for a variety of reasons. For one, I use Vim as my editor which means I'm never more than a couple keystrokes away from my terminal. I'm also just really fast at using Git in the terminal. I may not know every possible argument I could pass to Git from the command line but I know enough that I definitely see productivity gains. Most of the time the GUI is just overkill.

One of the nicer things I found was an article called Everyday Git Aliases by Joe Wright. He goes over a list of very simple Git aliases that you can setup in your .gitconfig. He lists about five or six aliases but three of them are ones that I use constantly so I'll talk about them below. Here is the alias section from my .gitconfig if you're interested in just copying it.

[alias]
  st = status -sb
  ci = commit
  br = branch
  co = checkout
  df = diff
  ready = rebase -i @{u}
  lg = log --no-merges --pretty=format:'%Cred%h%Creset -%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset'
  standup = log --pretty=format:'%Cred%h%Creset -%Creset %s %Cgreen(%cD) %C(bold blue)<%an>%Creset' --since yesterday --author Alex
  purr = pull --rebase

GIT PURR

Running git purr is the same as running git pull --rebase. Where I work we are encouraged to rebase wherever possible in order to avoid cluttering the commit log with silly merge commits. We also use a fork pattern where each developer creates a fork of the main repository and submits pull requests from their fork to the main repository when he/she is ready to submit code for review. This means that it's very common for me to run git pull --rebase upstream followed by a git push origin. Typing --rebase all the time can get tedious which is why git purr makes things really convenient.

GIT ST

If you use Git from the terminal then you're likely used to running git status all the time. I've always felt like the information displayed from git status is pretty verbose. git st simplifies the output so that it gives you a simple compact list.

It really helps when you have a lot of changes in your working directory. It comes particularly in handy when using my giterate bash function I put together. It iterates over several repositories and executes git commands in each one.

# ~/.bash_profile

giterate() {
    echo ""
    for repo in */ ; do
        echo "Repository: $repo"
        git -C $repo "$@"
        echo ""
    done
}

Here's an example where I run giterate st in my node-projects directory.

Compare that titdy little status report to this blob of garble:

GIT LG

This alias offers the same benefits as git st, but for git log. Compare this:

To this:

Without the ability to squash that information into a more readable display my giterate function is rather useless to me; it just spits out walls of text that I have to slowly scan through to find what I need.


Beyond Compare

One of the important tools you'll find yourself needing when using any form of source control is a diff tool. I've tried quite a few diff tools over the years and I still haven't found anything that tops Beyond Comapre. It offers both 2-way and 3-way diffs, though it's not free. The standard edition is $30 and offers only 2-way merges, while the pro edition is $50 and offers 3-way merges. I definitely get my money's worth out of the pro edition but 50 bucks can be a lot to shell out for just a diff tool. If you don't want to pay for Beyond Compare I've found that Kdiff3 is an adequate alternative.

If you do want to install Beyond Compare then you'll want to assign it as the difftool for git. Doing so makes it possible to execute git difftool from the command line to automatically run Beyond Compare to show the diff you're asking for.

To set Beyond Compare as your difftool on a Mac, open up the program and select "Install Command Line Tools..." from the Beyond Compare menu.

Once you've done that it's simply a matter of adding the following to your .gitconfig:

[diff]
  tool = bc4
[merge]
  tool = bc4
[difftool "bc4"]
  cmd = /usr/local/bin/bcomp $LOCAL $REMOTE
[mergetool "bc4"]
  cmd = /usr/local/bin/bcomp $LOCAL $REMOTE $BASE $MERGED
  trustExitCode = true

This will tell git to use it as your difftool as well as your mergetool. In a diff, you only need a 2-way merge window since you're just comparing differences. This is why we only pass in $LOCAL and $REMOTE as arguments to bcomp. In a merge however, it's nice to have the base commit that existed before the two branched off so we pass in $LOCAL, $REMOTE, and $BASE. The last argument we pass in is $MERGED which points to the file where git will expect the merged file to be written to. Now that Beyond Compare is all setup to work with Git, if you encounter a merge conflict you can now invoke your mergetool :)


Fork Pattern Let's You Force Push

If you do any open source development then there's a good chance you're used to developing using a fork of a main repository and submitting pull requests. However, I've noticed that a lot of people are still confused about some of the things you can do with this pattern. One of my favorite things to do is squash and amend my commits to create a clean commit history. The fork pattern makes it really easy to do these things, even if you've pushed your changes to your fork.

Most people will advise you that you should never git push --force. There's a good reason people tell this to people who are not totally familiar with Git. When you do a force push you are essentially telling the remote repository to mirror your local repository regardless of the status of the remote repository. You're pretty much blowing away the remote repository's history and replacing it with the history of your local repository. This means if a team member pushes a commit up to a central repository, doing a force push from your machine without doing a git pull first will cause your teammate's commit to vanish. In a team environment you must be extremely careful about git push --force.

One reason why I love the fork pattern for development is that I have a remote repository that is all my own. Nobody else is committing to my repository but me. If someone does have some changes that they only want me to have, they will simply submit a pull request to my fork and I will merge it in myself. This situation comes up very rarely, usually when someone has a change that is specific to a feature I'm actively developing, but even then the pull request workflow keeps things easy to manage.

Because my fork is my own I can do some pretty neat things with it. For instance, let's say I just finished a feature and pushed the commits to my fork. I really thought I was done with the feature so I go ahead and create a pull request from my fork to the main repository. Shortly after doing that I realize that I made a typo or forgot something small. You might think that I need to close my pull request and create a new one, but that is a misunderstanding about the way pull requests work. A pull request is not a snapshot of your code at the time you made the request. Rather, a pull request represents an "intent to merge" from one repository/branch to another. This means that when the pull request gets merged in, it will merge all the code in that repository/branch at the time of the merge, not the time the pull request was submitted.

With this knowledge of how pull requests work you can safely push up new changes to your fork, go back to your pull request, and see that the "files changed" tab there will now show your new changes. But wait, it gets better. I'm a little meticulous about my commits and I hate submitting pull requests where one or more of my commits say something like "fixed a typo" or "forgot a line". If the pull request hasn't yet been merged in then you can correct your typo and commit it with git commit --amend. This will basically destroy your last commit and create a new commit with the same commit message, but that includes your latest change in addition to the changes from the original commit.

At this point it would be a bad idea to do a simple git push. The new commit created by --amend has a new commit hash and the old commit hash is destroyed, however you already pushed the old commit hash to your fork. If you do a simple push now things are going to get weird. Time for git push --force! Remember, you're only force pushing to your own fork. The only commit you're going to blow away at this point is the old commit with the old commit hash, effectively replacing it with the new commit and your new commit hash. The only reason you can do this is because you know two things: 1) you know your pull request has not yet been merged in, meaning the main repository doesn't yet have your old commit with the old hash, 2) nobody else is pulling from your fork, which means no developers should have the commit you're about to blow away either.

As long as you know that your pull request hasn't yet been merged in then you're safe to force push to your fork; even if it has been merged and you accidnetally force push to your fork, it won't immediately hurt anyone. When that happens you'll just have to do a pull from upstream (the main repository) to get the commits that were merged from your request. After that you'll have to do another force push to your fork to force it to reflect the exact history of your local machine (which now matches the history of upstream).

Hopefully all of that makes sense. I highly recommend checking out git ready, which is one of the aliases on Joe Wright's blog post. An interactive rebase is one of the coolest ways to spruce up your commit history. While a git commit --amend is a quick way to effectively rewrite your latest commit, an interactive rebase will allow you to rewrite a bunch of your history. Just remember that anytime you modify Git history all of the commit hashes will change. This is because a commit hash is a direct tie to the commit before it; it's literally encoded in the hash itself. In order to rewrite history it has to build a new set of commits and string them all together, creating new hashes that represent these new relationships to one another. If you've already pushed commits that you're now amending or squashing and other developers or a central repository already has those old commit hashes, DO NOT rewrite your history. This is only useful if you haven't pushed yet or you've only pushed to your fork and you know nobody else has those commits. The only safe time to force push is if it's to a repository that only you control.