Video

Want to see the full-length video right now for free?

Sign In with GitHub for Free Access

Notes

Git is incredibly powerful, but is often criticized for its complex and at-times inconsistent command interface. Thankfully, Git is very amenable to configuration and in this video we'll cover the variety of ways we can configure and customize Git to make our interactions more intuitive and repeatable. So, let's dive in.

g Fall Through Shell Alias

The first configuration I want to share is actually a shell function that we use here at thoughtbot in place of the normal git command. Obviously three letters is way too many to type considering how often we run git commands, so we've chosen to expose it as the single letter command g.

With arguments, g will simply pass the arguments on to git.

$ g branch
  cjt-feature-branch
* master
  video-trail-index

This works for any number of arguments and flags as the g function simply passes everything on to git:

$ g log --oneline -10
46a3475 Fix comments from Github
32d7ac3 Adjust the flexbox tiles to be aligned
ca2f179 Display a message when no results found
...

And we've even mapped the zsh-provided tab complete for Git to our g function, so all the existing tab completion still works!

One last trick: if we call it just as the bare g command then it will run git status. status is one of the most common commands that we'll run as we interact with Git so it's nice having it a single letter command away.

The configuration for this wrapper function lives in the thoughtbot dotfiles.

# No arguments: `git status`
# With arguments: acts like `git`
g() {
  if [[ $# > 0 ]]; then
    git $@
  else
    git status
  fi
}

# Complete g like git
compdef g=git

Git Config File

The gitconfig file is where Git stores and reads all configuration. The file lives in your home directory as a "dotfile", ~/.gitconfig.

The gitconfig file is read automatically before any Git command is run. That turns out to be very handy as it means you never have to reload or experience out-of-sync commands. Additionally, git automatically writes to it when we run commands like git config --global alias.sla

Config File Sections

The config file is split into a few sections to help organize the various configuration options. Some of the sections are color, alias, core, push, etc. For a given configuration, e.g. push.default upstream, the actual entry in the gitconfig file would look like:

[push]
  default = upstream

This can be set by manually editing the file, or by running the git config subcommand, passing the desired options and values:

$ git config --global push.default upstream

Useful Configurations

We'll be looking into aliases in detail in just a moment, but for now we can review a few of the other useful configurations we can set:

  • push.default upstream this instructs Git how to respond when you run git push with no arguments. With the upstream configuration, it will push the configured upstream tracking branch (set up with git push -u).
  • merge.ff only this configuration tells Git to reject merges that are non-fastforward. With fast-forward merges, no new commits are created, but instead the merging branch (typically master) is only moved to point at the commits on the target branch (typically our feature branch).
  • fetch.prune true this instructs Git to clear local references to remote branches which have been deleted when you pull.

Store it In Git

The last tip we can review about the config file itself is that we really should be storing it in a Git repository (how meta!). This is an important configuration file and we want to have a backup, and a history of the changes (and reasons for the changes) saved.

Check out our Weekly Iteration episode on Dotfiles for a bit more info on this.

Custom Commands

Now we get to the good stuff: aliases and custom commands. These are the really powerful configuration points that let you build the command interface and workflows you want, rather than taking what Git gives you.

Aliases

To start, we'll revisit aliases. We've already seen a few aliases in previous videos in this course, added using Git's config command, but now we can see a more complete list.

Aliases can server a few different purposes:

  1. Provide Shorter Commands - b > branch, co > checkout, for these common commands less (typing) is more!
  2. Refining The Command Interface - I can never quite remember that reset is the command to upstage a file, but my trusty unstage alias means I don't have to!
  3. Collect Options and Flags - sla is much easier to remember and type than log --oneline --decorate --graph --all.

Aliases As Documentation

One of the really great features of aliases is that Git can describe them for us via the help command, essentially acting as documentation. For example we can run:

$ git help unstage
`git unstage' is aliased to `reset'

and Git will expand the alias, showing what it maps to. Similarly, running

$ git help df
`git df' is aliased to `diff --word-diff --color-words'

will print out the alias, reminding us of the particular options we've configured for diff viewing.

A very handy little feature.

Bang Aliases

As great as Git aliases are, with the default usage, they have some limitations. Namely, they can only execute a single Git command.

Thankfully, we have a way around this. If we start an alias with a ! then we are executing an arbitrary shell command. This means we can use && or ||, we can pipe, we can use Git in subshells and then work from the data. We can do pretty much anything.

As an example, I have the alias mup (which is a mnemonic for "master up"), configured to check out master, pull, then return to the previous branch:

$ git help mup
`git mup' is aliased to `!git checkout master && git pull && git checkout -'

This requires multiple Git commands to be run in sequence, but by using a bang alias, I can do this just as I would with a normal alias.

Similarly, I have a command to perform a hard reset of the current branch, making it point to whatever commit its upstream points to. This is useful in the case that someone has rebased a branch that you started, and you need to work from that point.

$ git help ureset
`git ureset' is aliased to `!git upstream && git reset --hard $(git upstream)'

The command itself combines two commands and also uses a subshell to run a second Git process to pull out the upstream name.

Custom Git Subcommands

With aliases and bang aliases we're essentially unlimited in what we can do, but we are constrained in that we are writing shell code which for most of us is not our preferred language. Also, we're authoring the command as a single line in the Git config file. Thankfully, there's one more level we can move up in order to gain complete freedom: using custom Git subcommands.

For a script to be a valid Git subcommand, it must meet three criteria:

  1. The script name must be prefixed with git-, e.g. git-cpr.
  2. The script file must be on your $PATH.
  3. The script must be marked as executable.

Outside of that, we are essentially free to use any language we want to author the script. Each subcommand can be run just like an alias, e.g. git subcommand-name, despite the fact that file is named as git-subcommand-name.

The following sample code shows how we could create a simple Git subcommand using ruby:

#!/usr/bin/env ruby

current_sha = `git rev-parse HEAD`.chomp

puts "The current sha is: #{current_sha}"
# Create the ~/bin directory if it doesn't already exist
$ mkdir -p ~/bin

# Add the ~/bin directory to our path
$ echo 'export PATH="$HOME/bin:$PATH"' > ~/.zshenv

# Populate the script with code above
$ vim ~/bin/git-current-sha

# Mark it as executable
$ chmod +x ~/bin/git

# Run our fancy ruby script through Git!
$ git current-sha
The current sha is: 284aeca561c5be5ec7b81123ac625e02308d09e8

Sample Custom Subcommands

Let's take a look at an example: git-cm.

#!/bin/bash
#
# Small wrapper around git commit. Bare 'cm' will enter normal git commit
# editor, but with args it will do a direct `commit -m`

if [[ $# > 0 ]]; then
    git commit -m "$@"
else
    git commit -v
fi

This is a relatively simple command, written in bash, that wraps around git commit just like the g function wrapper we showed at the start of this video. If we pass any arguments to git-cm then they are passed as the commit message with the -m flag, but if no arguments are passed, then it opens the editor to compose our commit message.

While this isn't terribly complicated, it's still the sort of thing we'd rather not put in one line in our Git config file.

Let's take a look at another file (the full source code of this script is available on github):


#!/usr/bin/env ruby
# Usage: git cpr
#
# Run this from a branch which has an upstream remote branch, and an associated
# pull request.
#
# The script will merge the branch into master, push master (which will
# automatically close the pull request), and delete both the local and remote
# branches.

class ClosesPullRequests
  def run
    remember_current_branch
    confirm_upstream_tracking_branch
    ensure_working_dir_and_index_clean
    fetch_origin
    ensure_feature_branch_in_sync
    ensure_master_in_sync
    checkout_master
    merge_local_banch
    push_master
    delete_remote_branch
    delete_local_branch
  end

  private

  # ... implmentation ommited for brevity's sake
end

ClosesPullRequests.run

cpr stands for "close pull request", and its job is to automate all the steps needed to close a pull request. For this command, I chose to use ruby as I am much more comfortable in it than in bash. The only step needed to use ruby was to configure a proper shebang line at the top.

The main method essentially acts as an index of all the steps, and we can see it does a whole bunch.

This is something where I wanted to be in the warm comfort of ruby rather than in bash, and thankfully subcommands allow me to do this.

Repo Hooks

The final configuration to cover is the use of hooks. Hooks are scripts that can be set to run in response to certain events in Git such as before committing or after checkout. Some uses might be to enforce certain commit message standards on pushed commits, or to run the tests before committing.

We can see a more specific example in the thoughtbot dotfiles used to generates ctags. Ctags act as a sort of index of the methods in your code, allowing editors like Vim to quickly jump to the definition.

The ctags script will run the ctags command, passing a number of options. Each of the other scripts, for instance the post-checkout script, will run the ctags script when the given Git event is triggered. By hooking into the various Git event hooks, we can ensure that our ctags file is regularly updated.

For additional detail on using Git hooks, check out our blog post, Use Git Hooks to Automate Necessary but Annoying Tasks, as well as this great collection of Tips for using a git pre-commit hook.

Conclusion

And with that, we've seen a whole array of ways we can configure Git to better match our workflow. Admittedly Git is a little rough around the edges by default, but by taking advantage of the many configuration points like the various aliases, custom subcommands, and hooks, we can smooth out those edges and make Git a pleasure to work with.