tekin.co.uk

Git Tips you (Possibly) Didn't Know You Needed

Although Git is a really powerful distributed source control system, it is far from intuitive. Getting the most out of it often means learning obscure Terminal commands and option flags. These tips distill some of the weird incantations that have helped me to improve my Git workflow over the years.

If you don’t know what Git is or want to understand it better, I would highly recommend checking out Alice Bartlett’s excellent Git for humans slides.

Warning: Terminal required

These tips are aimed at folks who work with Git using the command line. That said I would like to think that those who use a graphical interaface may also find them useful.

1. Run the latest version of Git

This one seems almost too obvious to mention. That said, Git is constantly being updated with new features, bug fixes and performance enhancements and it pays to be running the latest version. At the very least, make sure you are running version 2 or above, as this introduced a number of important improvements and enhancements to the default behaviour. Download the latest Git from the Git website, or update it using your package manager of choice.

2. Git Completion in your Terminal

There’s a great bash script that adds tab-completion to Git commands, options, tags, and branch. I’m always creating branches with long names, and being able to tab-complete them is very handy. There’s versions for Z shell and tsch available too.

3. Interactively stage changes with --patch

Keeping a clean and atomic commit history is a good habit to get into. It makes reviewing code simpler, rolling back changes less painful, and writing focused, useful commit messages easier. But sometimes our fingers get ahead of us and we end up with a bunch of unrelated changes in a file that really should be added as separate commits.

  $ git diff
  diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
  index 0b9d83d..85af2e2 100644
  --- a/app/views/layouts/application.html.erb
  +++ b/app/views/layouts/application.html.erb
  @@ -3,7 +3,7 @@
   <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  -  <title><%= yield :meta_title %></title>
  +  <title><%= yield :meta_title || 'North West Ruby User Group' %></title>
     <meta name="description" content="<%= yield :meta_description %>">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
  @@ -73,34 +73,6 @@
       </div>
     </div>

  -   <script>
  -    var navigation = responsiveNav(".nav-collapse", {
  -      animate: true,                    // Boolean: Use CSS3 transitions, true or false
  -      transition: 284,                  // Integer: Speed of the transition, in milliseconds
  -      label: "Menu",                    // String: Label for the navigation toggle
  -      insert: "after",                  // String: Insert the toggle before or after the navigation
  -      customToggle: "",                 // Selector: Specify the ID of a custom toggle
  -      closeOnNavClick: false,           // Boolean: Close the navigation when one of the links are clicked
  -      openPos: "relative",              // String: Position of the opened nav, relative or static
  -      navClass: "nav-collapse",         // String: Default CSS class. If changed, you need to edit the CSS too!
  -      navActiveClass: "js-nav-active",  // String: Class that is added to <html> element when nav is active
  -      jsClass: "js",                    // String: 'JS enabled' class which is

In these situations, the git add --patch command (or git add -p for short) is incredibly useful. It moves through “hunks” of changes one by one, allowing you to make a decision as to whether to stage each change in turn:

  $ git add --patch

  diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
  index 0b9d83d..85af2e2 100644
  --- a/app/views/layouts/application.html.erb
  +++ b/app/views/layouts/application.html.erb
  @@ -3,7 +3,7 @@
   <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  -  <title><%= yield :meta_title %></title>
  +  <title><%= yield :meta_title || 'North West Ruby User Group' %></title>
     <meta name="description" content="<%= yield :meta_description %>">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">

  Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

Hitting y will stage the displayed change; hitting n will skip it, leaving it unstaged ready for a future commit.

There are additional options you can choose for each hunk and hitting ? will show them all. For example, the s option can be used to split the current hunk into even smaller ones.

4. Amend the most recent commit with --amend

If you ever find yourself having to amend the most recent commit, then you will find git commit --amend useful. Rather than creating a new commit, it merges the staged changes into the previous commit. It opens your editor with the previous commit message so you can also edit it.

I often find that I’m amending the same commit as I refactor my code and don’t need to change the commit message. In these situations, there’s an additional flag that is useful: --reuse-message, or -C for short, which lets you specify a commit SHA for the message you want to use. So the following command will amend the previous commit in one step, keep the same commit message:

  $ git commit --amend -C HEAD

I do this so often that I’ve created a git alias (see tip 7) for it called amend:

  $ git config --global alias.amend 'commit --amend -C HEAD

5. Rewriting history with interactive rebase

This is possibly my favourite git trick and is a really powerful way to keep a clean git history. Say you’ve spotted a typo or found a bug that was introduced in a recent commit on your local branch and you want to fix it.

Some code that contains a typo

The problem is, you’ve made additional commits since, so you can’t use git commit --amend as the commit you want to change isn’t the most recent one.

Now you could simply create a new commit that fixes the issue and move on. But that wouldn’t make for a very clean git history.

  $ git log --pretty=oneline -n3
  04fcdd05382d84438c7f37326f6899f903355be7 (HEAD -> messin) Correct tyop
  bd4aa76722b58d6b7d878208b2c629f5a6f3f48e Don't inline JavaScript for responsive navigation
  3aeecfa992f1dc4e8e8f8eb271564f258a5b465a Add a default page meta title

As long as the commits are only on your local branch and haven’t been remotely shared with others, it makes sense to rewrite these commits so that the typo never existed. To do this more complex commit rewriting, you can use git rebase --interactive, or git rebase -i for short.

The interactive rebase command lets you to interactively rewrite your commits, reordering and merging them as you see fit. To use it, call the command with the revision you want to rebase to. For example, if you wanted to change the last three commits, you can call git rebase -i HEAD~3. I often find it easiest to simply rebase as far back as the branch I branched from, which is usually master: git rebase -i master. Running this command will give you a list of the commits in your editor that looks something like this:

  pick f682d4c Add a default page meta title
  pick a15516e Don't inline JavaScript for responsive navigation
  pick ec2898c Fix typo

  # Rebase 49ddbc8..ec2898c onto 49ddbc8 (3 commands)
  #
  # Commands:
  # p, pick = use commit
  # r, reword = use commit, but edit the commit message
  # e, edit = use commit, but stop for amending
  # s, squash = use commit, but meld into previous commit
  # f, fixup = like "squash", but discard this commit's log message
  # x, exec = run command (the rest of the line) using shell
  # d, drop = remove commit
  #
  # These lines can be re-ordered; they are executed from top to bottom.
  #
  # If you remove a line here THAT COMMIT WILL BE LOST.
  #
  # However, if you remove everything, the rebase will be aborted.
  #
  # Note that empty commits are commented out

To rewrite this commit history, we can edit the file, reordering the commits and changing the commands as necessary. So in this case, we can move the line representing most recent commit with the typo fix so that it sits below the first commit, then change the command to fixup, telling Git to squash the typo commit into the previous one. We end up with a file that looks like this:

  pick f682d4c Add a default page meta title
  fixup ec2898c Fix typo
  pick a15516e Don't inline JavaScript for responsive navigation

  # Rebase 49ddbc8..ec2898c onto 49ddbc8 (3 commands)
  #
  # Commands:  # p, pick = use commit
  # r, reword = use commit, but edit the commit message
  # e, edit = use commit, but stop for amending
  # s, squash = use commit, but meld into previous commit
  # f, fixup = like "squash", but discard this commit's log message
  # x, exec = run command (the rest of the line) using shell
  # d, drop = remove commit
  #
  # These lines can be re-ordered; they are executed from top to bottom.
  #
  # If you remove a line here THAT COMMIT WILL BE LOST.
  #
  # However, if you remove everything, the rebase will be aborted.
  #
  # Note that empty commits are commented out

Saving and closing the file will trigger the rebase command to rewrite history, leaving us with a clean two-commit history minus the “fix typo” commit. You can learn more about rewriting history in the Rewriting History section of the Pro Git book.

6. Rewriting history redux: --fixup & --autosquash

Fixing a previous commit in this way is something I find myself doing often. Turns out there is an even easier way to perform this kind of commit rewriting using the --fixup and --autosquash flags.

Firstly, when make the commit that fixes the issue, include the --fixup option and pass in the SHA reference for the commit you want to fix against:

  $ git commit --fixup f682d4c
  [git-demo 7643c3b] fixup! Add a default page meta title
   1 file changed, 1 insertion(+), 1 deletion(-)

This creates a special commit with a commit message prefixed with “fixup!”, which gives Git a clue that this is a fixup commit. Then when we call rebase, we call it with the --autosquash option:

  $ git rebase -i --autosquash HEAD~

This time when the editor is opened, the commits will already be reordered and the fixup commit will already be marked as fixup. You can simply save the file and close the editor without having to make any changes and the rebase will complete as before.

There’s even a way to configure your Git so that it will --autosquash by default if it sees the “fixup!” prefix when you interactively rebase:

  $ git config --global rebase.autosquash tru

7. My favourite Git aliases

Git aliases are a great way to make helpful command and option combinations easier to type and remember. Here are some I find useful:

  $ git config --global alias.co checkout

A simple one that saves you a few key strokes; now you can type git co instead of git checkout.

  $ git config --global alias.cm 'commit --verbose'

The --verbose option makes Git include the diff of the current set of changes at the bottom of the commit message template, which is a useful way to sanity check the changes and also helps me write better commit messages as I can see the changes right there in my editor as I write.

  $ git config --global alias.amend 'commit --amend -C HEAD'

As mentioned in Tip 5, this lets you update the previous commit with the currently staged changes in a single command.

  $ git config --global alias.pretty 'log --pretty=oneline'

With this, git pretty outputs a simpler one-line-per-commit log output.

  $ git config --global alias.upstream '!git push \
    --set-upstream origin $(git rev-parse --abbrev-ref HEAD)'

This is probably my favourite – it means you can push your local branch to the remote origin and set the remote as upstream with git upstream.

8. Get yourself a global Git ignore file

A helpful thing to do is configure yourself a global .gitignore file. You can use this file to list ignore rules that will apply across all your Git repositories. This is a good way to globally ignore files that are specific only to you and your system, for example Vim swap files, Pow configuration files or Guard files. That way you can avoid littering your application’s .gitignore file with entries that might not be relevant to other developers or to the application itself.

Create a .gitigore file in your home directory and register it with the following command:

  git config --global core.excludesfile ~/.gitignore

9. Configure your colours

Whilst Git colours some of its outputs by default, it’s possible to configure the colours to suite your tastes. I like to tweak the output of the status command to give a more intuitive colour output by adding the following to .gitconfig:

  [color "status"]
  changed = yellow
  added = green
  untracked = cyan

Frustratingly there isn’t currently a way to distinguished between new untracked files from deleted ones in the colour options. If there was then I would most definitely configure deleted files to be coloured red.

10. Read flippin’ the manual

I’ve only really scratched the surface with these tips and I can thoroughly recommend browsing through the official documentation website. Not only is it a complete reference of Git, it also includes discussions and additional information that you won’t find in the man pages. You’ll also find the entirety of the Pro Git book by Scott Chacon and Ben Straub on the website, which is a fantastic way to level up your Git skills.

Bonus tip: jumping between branches

A quick bonus tip: in the same way cd - will take you to the previous directory you were in, git co - will checkout the previous branch you were on. I find this useful for jumping back and forth between two branches.

Bonus tip 2: Using reflog to recover lost commits

I was going to include a tip about git reflog, but Graham Ashton has already done a great writeup on using reflog to unearth lost commits.

That’s all folks

Hopefully I’ve shared at least one thing that was new to you and will help you improve your Git workflow. I’m always looking to get better at Git. If you have anything interesting that you’d like to share, hit me up on Twitter.

Want more juicy Git tips like this straight to your inbox?

You'll get an email whenever I have a fresh insight or tip to share. Zero spam, and you can unsubscribe whenever you like with a single click.

More articles on Git

Authored by Published by