How I Roll with Git

Esperanto ▪ English
July 28, 2019
Last updated: August 1, 2019

Conversely, those with persistence can ignore what others think. They can press on in their own world, oblivious to the opinions of those around them.
―Daigo Umehara

Table of contents

Overview

In my toolbox are terminal simulator, shell, editor, browser, compiler, and git. When I encountered git several years ago, it became one of my most used tools. Because of its speed and range of use, it became as if my third arm.

Because I use Emacs, I also have Magit. However, in this article I will talk about how I use git daily on the command line.

The commands and functions that we are going to have are for Zsh and Bash. It is possible that they also work on other shells, however, I haven’t tested them.

Short commands

With git, if we want to see the status of a repository, we use the following command:

git status

However, there are many occasions when we want to use a shorter version:

git s

We use the shorter command maybe because we want to save typing time or it is physically easier instead of typing the whole command. In this section, I will talk about the existing methods to use shorter commands.

Aliases

Git already has faculties for defining short commands. Example, if instead of

git clone

we use

git c

we define an alias for the clone command using the alias system of git. We can do that using two methods.

The first method is directly with the command line:

git config --global alias.c clone

The second method is using the configuration file, which is found it ~/.gitconfig. Put the following text in that file:

[alias]
  c = clone

We can also use arbitrary commands inside the alias clause. There are two methods to do that.

The first method is directly as if a shell command:

[alias]
  hello = "! echo hello world"

If we run the command

git hello

it will seem as if the following command was ran:

echo hello world

Consequently, the output is

hello world

The second method is with defining shell functions:

[alias]
  hi = "! hi () { echo hi world; }; hi"

If we run the command

git hi

it will seem as if the command command was ran:

hi () { echo hi world; }
hi

With it, a shell function with the name hi was first defined, then we call that function. So, the output is:

hi world

Alternative

While those methods are already fine with many users, they do not suffice with me, because those methods do not have access to my full shell environment. There are other shell functions in my configuration that I want to call. More importantly, the it is limited only in the environment of the git configuration.

What I use instead is that I defined a shell function, wherein, I can call them with false aliases and with valid git commands. With that system, if I use the following command:

git clone

Git behaves just like as it was. However, if I use the following command:

git abc

I will call the subcommand abc which I defined. Even if I have defined an alias with the same name in ~/.gitconfig this subcommand will run with a higher priority.

Configuration

In this section are the definitions which we need to put in the Zsh and Bash configuration files. I have them in ~/.zshenv and ~/.bashrc, respectively.

Base function

Here is the base function:

function git () {
  local git= self= op=

  if [[ -n "${BASH}" ]]; then
    git=$(which git)
    self=${FUNCNAME}
  elif [[ -n "${ZSH_NAME}" ]]; then
    git=$(whence -p git)
    self=$0
  else
    echo "Meh"
    return 1
  fi

  if [[ $# -eq 0 ]]; then
    if [[ -n "${BASH}" ]]; then
      type "${self}" | less
    elif [[ -n "${ZSH_NAME}" ]]; then
      which "${self}" | less
    else
      echo "Meh"
      return 1
    fi
  else
    op="$1"
    shift

    case "${op}" in
      # komandoj ĉi tie
      (*) =git "${op}" "[email protected]" ;;
    esac
  fi
}

If there are no commands for git:

git

the definition of function itself will be displayed.

The subcommands will live in the area marked # commands here. The fallback marked with * means that if there is no appropriate subcommand, git will use its internal command.

Important commands

Here are the most important commands that we need to have.

Main operations

      (s) "${git}" status ;;
      (c) "${git}" clone "[email protected]" ;;
      (h) "${git}" show "[email protected]" ;;
      (mv) "${git}" mv "[email protected]" ;;
      (mv!) "${git}" mv -f "[email protected]" ;;
      (me) "${git}" merge "[email protected]" ;;
      (ta) "${git}" tag "[email protected]" ;;
      (bl) "${git}" blame "[email protected]" ;;

      (a) "${git}" add "[email protected]" ;;
      (au) "${self}" a -u ;;
      (a.) "${self}" a . ;;

      (ci) "${git}" commit "[email protected]" ;;
      (cia) "${self}" ci --amend "[email protected]" ;;
      (cim) "${self}" ci --message "[email protected]" ;;

      (co) "${git}" checkout "[email protected]" ;;
      (com) "${self}" co master ;;
      (cot) "${self}" co trunk ;;
      (co!) "${self}" co --force "[email protected]" ;;
      (cob) "${self}" co -b "[email protected]" ;;

      (rt) "${git}" reset "[email protected]" ;;
      (rt!) "${self}" rt --hard "[email protected]" ;;
      (rv) "${git}" revert "[email protected]" ;;

      (g) "${git}" grep "[email protected]" ;;
      (gi) "${self}" g -i "[email protected]" ;;

      (f) "${git}" fetch "[email protected]" ;;
      (fa) "${self}" f --all "[email protected]" ;;

      (rm) "${git}" rm "[email protected]" ;;
      (rmr) "${self}" rm -r "[email protected]" ;;
      (rm!) "${self}" rm -rf "[email protected]" ;;

Pushing and pulling

      (ph) "${git}" push "[email protected]" ;;
      (phu) "${self}" ph -u "[email protected]" ;;
      (ph!) "${self}" ph --force "[email protected]" ;;
      (pho) "${self}" phu origin "[email protected]" ;;
      (phom) "${self}" pho master "[email protected]" ;;

      (pl) "${git}" pull "[email protected]" ;;
      (pl!) "${self}" pl --force "[email protected]" ;;
      (plr) "${self}" pl --rebase "[email protected]" ;;
      (plro) "${self}" plr origin "[email protected]" ;;
      (plru) "${self}" plr upstream "[email protected]" ;;
      (plrom) "${self}" plro master ;;
      (plrum) "${self}" plru master ;;
      (plrot) "${self}" plro trunk ;;
      (plrut) "${self}" plru trunk ;;

Branches and diffs

      (br) "${git}" branch "[email protected]" ;;
      (bra) "${self}" br -a ;;
      (brh) "${git}" rev-parse --abbrev-ref HEAD ;;
      (brm) "${self}" br -m "[email protected]" ;;
      (brmh) "${self}" brm "$(git brh)" ;;
      (brd) "${self}" br -d "[email protected]" ;;
      (brD) "${self}" br -D "[email protected]" ;;

      (d) "${git}" diff "[email protected]" ;;
      (dc) "${git}" diff --cached "[email protected]" ;;
      (dh) "${self}" d HEAD ;;
      (dhw) "${self}" d --word-diff=color ;;

Logs

      (l) "${git}" log "[email protected]" ;;
      (l1) "${self}" l -1 --pretty=%B ;;
      (lo) "${self}" l --oneline ;;
      (lp) "${self}" l --patch ;;
      (lp1) "${self}" lp -1 ;;
      (lpw) "${self}" lp --word-diff=color ;;

Other commands

Here are other commands that we also need to define:

Initialization and push helpers

      (i) touch .gitignore; "${git}" init; "${self}" a.; "${self}" cim "[email protected]" ;;
      (oo) "${self}" ph origin "$(git brh)" ;;
      (oo!) "${self}" ph! origin "$(git brh)" ;;

Whenever I create new repositories, I use the following command:

git i 'Initial commit'

What the oo subcommand does is that code will be pushed to the remote named origin under the name of the current branch. For example, if the current branch is trunk, then I run the following command:

git oo

the command becomes

git ph origin trunk

Rebasing

      (rb) "${git}" rebase "[email protected]" ;;
      (rbi) "${self}" rb --interactive "[email protected]" ;;
      (rbc) "${self}" rb --continue "[email protected]" ;;
      (rbs) "${self}" rb --skip "[email protected]" ;;
      (rba) "${self}" rb --abort "[email protected]" ;;
      (rbs) "${self}" rb --skip "[email protected]" ;;
      (rbi!) "${self}" rbi --root "[email protected]" ;;

      (ri) "${self}" rbi HEAD~"$1" ;;
      (rs) "${self}" rt --soft HEAD~"$1" && "${self}" cim "$(git log --format=%B --reverse [email protected]{1} | head -1)" ;;

I use the subcommand rs whenever I need to squash non-interactively. The argument is a digit indicating how many commits do we want to squash. For example, if I want to squash the last two commits, I run the following command:

git rs 2

Adding

      (aum) "${self}" au; "${self}" cim "[email protected]" ;;
      (aux) "${self}" aum "x" ;;
      (a.m) "${self}" a.; "${self}" cim "[email protected]" ;;
      (a.x) "${self}" a.m "x" ;;

      (auxx) "${self}" aux; "${self}" rs 2 ;;
      (au.x) "${self}" a.x; "${self}" rs 2 ;;
      (auxx!) "${self}" auxx; "${self}" oo! ;;

The subcommand aum becomes a shorthand for au and cm in order. I use the command auxx whenever I make small changes but I don’t want to add a new visible entry in the commit log.

Remote repositories

      (re) "${git}" remote "[email protected]" ;;
      (rea) "${self}" re add "[email protected]" ;;
      (reao) "${self}" rea origin "[email protected]" ;;
      (reau) "${self}" rea upstream "[email protected]" ;;
      (rer) "${self}" re remove "[email protected]" ;;
      (ren) "${self}" re rename "[email protected]" ;;
      (rero) "${self}" rer origin "[email protected]" ;;
      (reru) "${self}" rer upstream "[email protected]" ;;
      (res) "${self}" re show "[email protected]" ;;
      (reso) "${self}" res origin ;;
      (resu) "${self}" res upstream ;;

Revisions, filters, and stash

      (rl) "${git}" rev-list "[email protected]" ;;
      (rla) "${self}" rl --all "[email protected]" ;;
      (rl0) "${self}" rl --max-parents=0 HEAD ;;

      (cp) "${git}" cherry-pick "[email protected]" ;;
      (cpc) "${self}" cp --continue "[email protected]" ;;
      (cpa) "${self}" cp --abort "[email protected]" ;;

      (fb) "${git}" filter-branch "[email protected]" ;;
      (fb!) "${self}" fb -f "[email protected]" ;;
      (fbm) "${self}" fb! --msg-filter "[email protected]" ;;
      (fbi) "${self}" fb! --index-filter "[email protected]" ;;

      (rp) "${git}" rev-parse "[email protected]" ;;
      (rph) "${self}" rp HEAD ;;

      (sh) "${git}" stash "[email protected]" ;;
      (shp) "${self}" sh pop "[email protected]" ;;

Whenever I want to change to change a text from all commit messages, for example I want to change word dog to cat, I run the following command:

git fbm 'sed "s/dog/cat/"'

Whenever I want to completely remove a file from a repository, for example file.dat, I run the following command:

git fbi 'git rm --cached --ignore-unmatch file.dat' HEAD

Either way, I run the following command to make sure that the changes appear in the remote repository:

git oo!

Subtrees and submodules

      (st) "${git}" subtree "[email protected]" ;;
      (sta) "${self}" st add "[email protected]" ;;
      (stph) "${self}" st push "[email protected]" ;;
      (stpl) "${self}" st pull "[email protected]" ;;

      (sm) "${git}" submodule "[email protected]" ;;
      (sms) "${self}" sm status "[email protected]" ;;
      (smy) "${self}" sm summary "[email protected]" ;;
      (smu) "${self}" sm update "[email protected]" ;;
      (sma) "${self}" sm add "[email protected]" ;;
      (smi) "${self}" sm init "[email protected]" ;;

      (ref) "${git}" reflog "[email protected]" ;;

Putting them all together

Here are all the definitions in one location:

function git () {
  local git= self= op=

  if [[ -n "${BASH}" ]]; then
    git=$(which git)
    self=${FUNCNAME}
  elif [[ -n "${ZSH_NAME}" ]]; then
    git=$(whence -p git)
    self=$0
  else
    echo "Meh"
    return 1
  fi

  if [[ $# -eq 0 ]]; then
    if [[ -n "${BASH}" ]]; then
      type "${self}" | less
    elif [[ -n "${ZSH_NAME}" ]]; then
      which "${self}" | less
    else
      echo "Meh"
      return 1
    fi
  else
    op="$1"
    shift

    case "${op}" in
      (i) touch .gitignore; "${git}" init; "${self}" a.; "${self}" cim "[email protected]" ;;
      (i!) "${self}" i "Pravaloriziĝu" ;;

      (s) "${git}" status ;;
      (c) "${git}" clone "[email protected]" ;;
      (h) "${git}" show "[email protected]" ;;
      (mv) "${git}" mv "[email protected]" ;;
      (mv!) "${git}" mv -f "[email protected]" ;;
      (me) "${git}" merge "[email protected]" ;;
      (ta) "${git}" tag "[email protected]" ;;
      (bl) "${git}" blame "[email protected]" ;;

      (cl) "${git}" clean "[email protected]" ;;
      (cl!) "${self}" cl -f ;;

      (ci) "${git}" commit "[email protected]" ;;
      (cia) "${self}" ci --amend "[email protected]" ;;
      (cim) "${self}" ci --message "[email protected]" ;;

      (co) "${git}" checkout "[email protected]" ;;
      (com) "${self}" co master ;;
      (cot) "${self}" co trunk ;;
      (co!) "${self}" co --force "[email protected]" ;;
      (cob) "${self}" co -b "[email protected]" ;;

      (ls) "${git}" ls-files "[email protected]" ;;
      (lsm) "${self}" ls -m ;;
      (lsd) "${self}" ls -d ;;
      (lsdrm) "${self}" lsd | xargs "${git}" rm ;;

      (rt) "${git}" reset "[email protected]" ;;
      (rt!) "${self}" rt --hard "[email protected]" ;;
      (rv) "${git}" revert "[email protected]" ;;

      (g) "${git}" grep "[email protected]" ;;
      (gi) "${self}" g -i "[email protected]" ;;

      (f) "${git}" fetch "[email protected]" ;;
      (fa) "${self}" f --all "[email protected]" ;;

      (rm) "${git}" rm "[email protected]" ;;
      (rmr) "${self}" rm -r "[email protected]" ;;
      (rm!) "${self}" rm -rf "[email protected]" ;;

      (rb) "${git}" rebase "[email protected]" ;;
      (rbi) "${self}" rb --interactive "[email protected]" ;;
      (rbc) "${self}" rb --continue "[email protected]" ;;
      (rbs) "${self}" rb --skip "[email protected]" ;;
      (rba) "${self}" rb --abort "[email protected]" ;;
      (rbs) "${self}" rb --skip "[email protected]" ;;
      (rbi!) "${self}" rbi --root "[email protected]" ;;

      (ri) "${self}" rbi HEAD~"$1" ;;
      (rs) "${self}" rt --soft HEAD~"$1" && "${self}" cim "$(git log --format=%B --reverse [email protected]{1} | head -1)" ;;

      (ph) "${git}" push "[email protected]" ;;
      (phu) "${self}" ph -u "[email protected]" ;;
      (ph!) "${self}" ph --force "[email protected]" ;;
      (pho) "${self}" phu origin "[email protected]" ;;
      (phom) "${self}" pho master "[email protected]" ;;

      (oo) "${self}" ph origin "$(git brh)" ;;
      (oo!) "${self}" ph! origin "$(git brh)" ;;

      (pl) "${git}" pull "[email protected]" ;;
      (pl!) "${self}" pl --force "[email protected]" ;;
      (plr) "${self}" pl --rebase "[email protected]" ;;
      (plro) "${self}" plr origin "[email protected]" ;;
      (plru) "${self}" plr upstream "[email protected]" ;;
      (plrom) "${self}" plro master ;;
      (plrum) "${self}" plru master ;;
      (plrot) "${self}" plro trunk ;;
      (plrut) "${self}" plru trunk ;;

      (a) "${git}" add "[email protected]" ;;
      (au) "${self}" a -u ;;
      (a.) "${self}" a . ;;

      (aum) "${self}" au; "${self}" cim "[email protected]" ;;
      (aux) "${self}" aum "x" ;;
      (a.m) "${self}" a.; "${self}" cim "[email protected]" ;;
      (a.x) "${self}" a.m "x" ;;

      (auxx) "${self}" aux; "${self}" rs 2 ;;
      (au.x) "${self}" a.x; "${self}" rs 2 ;;
      (auxx!) "${self}" auxx; "${self}" oo! ;;

      (l) "${git}" log "[email protected]" ;;
      (l1) "${self}" l -1 --pretty=%B ;;
      (lo) "${self}" l --oneline ;;
      (lp) "${self}" l --patch ;;
      (lp1) "${self}" lp -1 ;;
      (lpw) "${self}" lp --word-diff=color ;;

      (br) "${git}" branch "[email protected]" ;;
      (bra) "${self}" br -a ;;
      (brm) "${self}" br -m "[email protected]" ;;
      (brmh) "${self}" brm "$(git brh)" ;;
      (brd) "${self}" br -d "[email protected]" ;;
      (brD) "${self}" br -D "[email protected]" ;;
      (brh) "${git}" rev-parse --abbrev-ref HEAD ;;

      (d) "${git}" diff "[email protected]" ;;
      (dc) "${git}" diff --cached "[email protected]" ;;
      (dh) "${self}" d HEAD ;;
      (dhw) "${self}" d --word-diff=color ;;

      (re) "${git}" remote "[email protected]" ;;
      (rea) "${self}" re add "[email protected]" ;;
      (reao) "${self}" rea origin "[email protected]" ;;
      (reau) "${self}" rea upstream "[email protected]" ;;
      (rer) "${self}" re remove "[email protected]" ;;
      (ren) "${self}" re rename "[email protected]" ;;
      (rero) "${self}" rer origin "[email protected]" ;;
      (reru) "${self}" rer upstream "[email protected]" ;;
      (res) "${self}" re show "[email protected]" ;;
      (reso) "${self}" res origin ;;
      (resu) "${self}" res upstream ;;

      (rl) "${git}" rev-list "[email protected]" ;;
      (rla) "${self}" rl --all "[email protected]" ;;
      (rl0) "${self}" rl --max-parents=0 HEAD ;;

      (cp) "${git}" cherry-pick "[email protected]" ;;
      (cpc) "${self}" cp --continue "[email protected]" ;;
      (cpa) "${self}" cp --abort "[email protected]" ;;

      (fb) "${git}" filter-branch "[email protected]" ;;
      (fb!) "${self}" fb -f "[email protected]" ;;
      (fbm) "${self}" fb! --msg-filter "[email protected]" ;;
      (fbi) "${self}" fb! --index-filter "[email protected]" ;;

      (rp) "${git}" rev-parse "[email protected]" ;;
      (rph) "${self}" rp HEAD ;;

      (sh) "${git}" stash "[email protected]" ;;
      (shp) "${self}" sh pop "[email protected]" ;;

      (st) "${git}" subtree "[email protected]" ;;
      (sta) "${self}" st add "[email protected]" ;;
      (stph) "${self}" st push "[email protected]" ;;
      (stpl) "${self}" st pull "[email protected]" ;;

      (sm) "${git}" submodule "[email protected]" ;;
      (sms) "${self}" sm status "[email protected]" ;;
      (smy) "${self}" sm summary "[email protected]" ;;
      (smu) "${self}" sm update "[email protected]" ;;
      (sma) "${self}" sm add "[email protected]" ;;
      (smi) "${self}" sm init "[email protected]" ;;

      (ref) "${git}" reflog "[email protected]" ;;

      (*) "${git}" "${op}" "[email protected]" ;;
    esac
  fi
}

I must mention, that if we already have the function above in the shell configuration and we have the following alias in ~/.gitconfig:

[alias]
  ls = "! echo hello world"

then we run the following command

git ls

the list of files managed by git will still appear on the screen, instead of the text hello world.

Closing remarks

So, with that function, I can work with git easily because I only need to think about the short names. Additionally, they have access to my other commands and functions. Since I use tmux, whenever I need to git, I only need to press a keyboard shortcut to open another tmux window below. There, I can easily use the git commands without changing my view which Magit unfortunately does. Because of it, it also enables me to think separately between code and the management of code.