Git tips and tricks

I was recently asked about the “prompt git status trick”, so I thought this might be a good opportunity to dump some git tips and tricks I’ve found useful.

First, the prompt git status trick. This uses a bash function defined in git-prompt.sh. It may be automatically sourced on your system. If not, you can source it yourself, or I’ve got a basic (i.e., not full-featured) alternative implementation you can just put in your ~/.bashrc. But the essence is that you just stick $(__git_ps1) in your definition of PS1 (the shell variable, not the telescope). I like to stick it in with red coloring after the working directory, which is what’s implemented below. It tells you the current branch, and the status of the work on that branch through a series of symbols:

  • < means you’re behind and can git merge additional commits.
  • > means you’re ahead and can git push your commits.
  • = means you have all the commits.
  • <> means you’ve diverged, so you may want to git push --force if you’re on your ticket branch or git toss if you’re on master.
  • $ means you have work stashed (so you can git stash pop).
  • + means you have changes added but not yet committed.
  • * means you have changes that have not yet been added or committed.
# The following two functions provide a basic alternative for git.git/contrib/completion/git-prompt.sh
# in case it's not available
function prompt_git_dirty {
    local gitstat=`git status 2> /dev/null`
    local charstat=""
    [[ -z $(echo $gitstat | grep "nothing to commit") ]] && charstat="\%"
    [[ -n $(echo $gitstat | grep "Your branch and '.*' have diverged") ]] && echo "${charstat}\<\>" && return
    [[ -n $(echo $gitstat | grep 'Your branch is ahead of') ]] && echo "${charstat}\>" && return
    [[ -n $(echo $gitstat | grep 'Your branch is behind') ]] && echo "${charstat}\<" && return
    echo $charstat
}
function prompt_git_branch {
  git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e "s/* \(.*\)/\[\1$(prompt_git_dirty)\]/"
}

# Setup for git.git/contrib/completion/git-prompt.sh
export GIT_PS1_SHOWDIRTYSTATE=1
export GIT_PS1_SHOWSTASHSTATE=1
export GIT_PS1_SHOWUNTRACKEDFILES=1
export GIT_PS1_SHOWUPSTREAM="auto"
type __git_ps1 1>/dev/null 2>&1 || alias __git_ps1=prompt_git_branch

if [[ ${EUID} == 0 ]] ; then
    PS1='\[\033[01;31m\]\u@\h\[\033[01;34m\]:\W \#\[\033[00m\] '
else
    PS1='\[\e[1;32m\]\u@\h\[\e[0;39m\]:\[\e[1;34m\]\w\[\e[1;31m\]$(__git_ps1)\[\e[0;1m\] \$ \[\e[0;39m\]'
fi

Next, a simple but helpful bash alias for updating lots of repos. Before I go offline, I like to run lsst-fetch, which gets all the work that’s on the remote servers. It doesn’t actually update any of the branches I’m working on (it’s a fetch, not a pull), so it’s the best of all worlds: I’ve got the information but it hasn’t corrupted my working state. And I immediately know the state when I cd into the repo because it’s right there on my shell prompt.

function git-recursive-fetch {
    for g in `find "$1" -name .git`; do
        echo Working on $g
        git --git-dir $g fetch --all
        git --git-dir $g fetch --all --tags
    done
}
alias lsst-fetch="git-recursive-fetch $HOME/LSST"

Now, some git aliases from my ~/.gitconfig. I have more that I’ve stolen from various places, but these are the ones I actually use.

  • The first three (stat, co, cp) are basic aliases that save typing.
  • git desc describes the branch state in the same way that scons does when it chooses a version name on install, so this allows me to compare my working state with eups versions.
  • git recent provides a commit history, with one line per commit, and graphing the merges. This is what I typically use to trace what’s been going on in a repo.
  • git hist provides a fuller commit history, with the full commit messages. If I’m trying to find particular changes, I often use git hist -p to also include the actual changes that have been made.
  • git fixup creates a fixup to the most recent commit. You’ll need to git rebase -i later to squash those commits, so more often I use git amend
  • git amend applies work to the last commit without modifying the commit message. So if I’ve discovered something left out of the most recent commit, I fix it and do git amend -a (to throw all changes into the commit) or git add followed by git amend (to be more selective).
  • git in shows commits that will come in from the remote (if I did a git pull).
  • git out shows commits that will go out to the remote (if I did a git push).
  • git toss throws away any new commits. If I have made and pushed changes on my laptop and now I need to update my state on the same repo on the cluster I will do git fetch && git toss.
  • git nuke cleans up files that don’t belong and throws away any new commits.
  • git new shows new commits created by the last command (e.g., git pull).
  • git alias lists the aliases. To find a specific alias you have to grep the output.
  • git oldest-ancestor finds the oldest ancestor of two commits or branches (based on this). This is useful for finding the base of a branch that we need to rebase against.
  • git review displays commits (the message and patch) for careful review. You can specify the branches involved, or it will default to the current branch against origin/master. This is very helpful for checking your work before sending it off for review.
  • git sub lists the commit messages for work you want to submit for review. I like to run this and paste it into the Jira ticket so the reviewer knows exactly what I’m submitting.
  • git sub-patch is the same, except the patch is included instead of just a stat of files changed.
  • git follow lets you follow the evolution of particular lines of code. The syntax is git follow <file> <firstLine> [<lastLine>]. If the <lastLine> is not specified, just the single line will be followed.
[alias]
        stat = status
        co = checkout
        cp = cherry-pick
        desc = describe --tags --always
        recent = "log --graph --date-order --pretty=format:'%C(yellow)%h%d%Creset %Cred%an%Creset %Cblue%cr%Creset %s'"
        hist = "log --decorate --graph --date-order --stat"
        fixup = commit --fixup=HEAD
        amend = "!git log -n 1 --pretty=tformat:%s%n%n%b | git commit -F - --amend"
        in = "!git remote update -p; git log ..@{u}"
        out = "log @{u}.."
        toss = "reset --hard @{u}"
        nuke = "!git clean -df && git reset --hard @{u}"
        new = !sh -c 'git log $1@{1}..$1@{0} "$@"'
        alias = "config --get-regexp '^alias.*'"
        oldest-ancestor = !bash -c 'diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1' -
        review = !sh -c 'git log --patch --reverse "${2:-origin/master}".."${1:-HEAD}"' -
        sub = !sh -c 'git --no-pager log --stat --reverse "${2:-origin/master}".."${1:-HEAD}"' -
        sub-patch = !sh -c 'git --no-pager log --patch --reverse ${2:-origin/master}..${1:-HEAD}' -
        follow = "!sh -c 'git log --topo-order -u -L $2,${3:-$2}:"$1"'" -

Some other things from my ~/.gitconfig that I find useful:

[color]
        ui = true
[help]
        autocorrect = 1
[push]
        default = tracking
[rebase]
        autosquash = true
        autostash = true
5 Likes

If you like the idea of git status in your bash prompt, but don’t want to maintain your own or customize as much, you can do what I do and just use https://github.com/magicmonty/bash-git-prompt.

1 Like

Following a suggestion by @timj, I use the following a lot to view branches.

alias glc='git log --graph --full-history --all --color --pretty=format:"%x1b[31m%h%x09%x1b[32m%d%x1b[0m%x20%s"' 

But the git recent alias through .gitconfig that @price lists above is nicer on a wider display.

Note that while glc definition above is a shell alias rather than a .gitconfig alias, the .gitconfig alias commands are exactly the thing that gets expanded onto the command line.

Yes, I do set it in my .gitconfig though:

[alias]
	lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)%Creset' --abbrev-commit

You can adjust the format to suit, as in git recent above. Being able to look at the graph is incredibly useful when trying to work out what’s going on (and github does not show this, and tools like gitx are overkill at times).

IncludeIf is also really useful. It allows you to have per-directory tree git settings. I have this in my ~/.gitconfig:

[includeIf "gitdir:~/work/lsst/"]
path = .gitconfig-lsst

and ~.gitconfig-lsst defines my user.email for LSST work, meaning that my personal work can use my personal email.

I’ll just add another git workflow suggestion that I’ve found to be super handy. It’s not an alias, but maybe something that not everyone knows about already.

git add -p interactively selects exactly which changes you want to add for committing.

This is great for when you finally get some code working and don’t want to commit all the print statements and such that you put in to help figure out what was going on. Once you’ve done this and committed the code, you can just do git checkout . to drop the now-irrelevant extra bits. Often much quicker than inspecting a git diff and manually removing lines you don’t want to include.

A similar thing when you want to drop some print statements you don’t need anymore, but maybe you aren’t quite ready to commit the rest is git checkout -p which will interactively ask you about whether to drop each change.

3 Likes

git gui provides a graphical interface for doing git add -p. I find it more convenient than git add -p when I’ve got a bunch of commits to make, but unlike git add -p you can’t edit the patch directly.

I fully endorse this. I can’t live without it.

And for those working in zsh, oh-my-zsh has lots of themes Git status prompts.

I also run my own:

git add -p is also great when you find an unrelated bug while working on something else. You can put the bug fix on its own commit without having to roll back (or stash) your main changes.

1 Like