Want to see the full-length video right now for free?
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.
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
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
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
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.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.
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.
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:
b
> branch
, co
> checkout
, for
these common commands less (typing) is more!reset
is the command to upstage a file, but my trusty unstage
alias
means I don't have to!sla
is much easier to remember and type
than log --oneline --decorate --graph --all
.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.
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.
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:
git-
, e.g. git-cpr
.$PATH
.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
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.
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.
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.