In part one of my two-part blog post, I took you on a journey from the very humble basics, introducing Git aliases, showing how they can replace long and complex commands, and even a few concepts that most users don't know about, like aliases that send parameters to Git itself and advanced log formatting with custom pretty formats.

Before you go any further,  I strongly suggest that you read part one first.

In part two, I introduce even more advanced concepts and venture into truly “out there” uses of Git aliases, including some downright crazy examples. Finally I will introduce some alternatives for addressing the same needs.

So, let's dive in.

Level 6: ! Non-Git commands

More bang for the buck.

Git aliases allow us to expand our vocabulary of things we can do within our Git context. This section, and most of the following, explores what we are capable of when we step outside the boundaries of Git subcommands.

The "bang" feature is a game-changer for what Git aliases are capable of, as it allows us to call out to the shell and run any shell command on our system. We do this simply by prefixing our alias expansion with an exclamation mark (or "bang" in the Unix/shell world).

Let's start with a very simple example that clearly demonstrates the concept and is actually useful.

On some platforms, Git comes preinstalled with two useful GUI tools, "Git gui" for staging and committing and "Gitk" for viewing history. But why is  git gui a Git subcommand, and gitk isn't? Quite confusing but easily fixable with an alias:

 
[alias]
    # Make git k call gitk
    k = !gitk

And while we are at it, I have had a similar problem. When my brain is deep in "Git" mode, my fingers often type "Git" before I have made up my mind about which command to use. Then suddenly, I decide I need to list the files in the folder and quickly type ll <enter> (my daily shell alias for ls -al), and suddenly I get:

$ git ll
git: 'll' is not a git command. See 'git --help'.
This should be solvable with an appropriate alias:
[alias]
    # This should make git ll work like ll
    ll = !ls -al

Now, the "typo" git ll  just works but leads to the discovery of a very important caveat with the bang feature, which can be both a blessing and a curse, as shown.

The bang exclamation mark starts a shell context from the root folder of the Git repository!

So while my git ll  alias seems to work, it will always show me the content of the repo root folder, even if I am in a subfolder.

To illustrate how this feature might be useful, let's try the alias:
[alias]
   rootpath = !pwd

The shell command pwd prints the current working directory; this alias provides a quick shortcut to print the path where my repository lives.

Admittedly this specific problem could be better solved with a normal Git alias, avoiding the creation of a new shell session (the jury is still out on which one runs faster, though).

[alias]
   rootpath = rev-parse --show-toplevel   

But I have at least one very good use of this side effect. Sometimes, when you are cd'ed deep into a subfolder for other reasons, it gets really annoying to read the Git status output because all other paths are printed relative to your current folder, so you will see changes mentioned like:

new file:   ../../../../foo 

So let's utilize the "root folder" side effect and add alternatives to the regular st alias:

[alias]
  st = status
  rootstatus = !git status
  sr = !git status

Which will always show the status output relative to the repo root.

Git-1

(-s is just for the less verbose "short" format output).

Note that a similar effect could be achieved using the config variable status.relativePaths and the -c feature discussed in "level 5" in the first part of this blog post.

To close off this section, let's have a little fun. If I unconsciously type Git before ll, I can sometimes also type Git before deciding to go for, e.g., Git status, leading to the unfortunate:

$ git git status
git: 'git' is not a git command.
So, my brain came up with the idea to create:

[alias]
   git = !git

Yes, this makes  git git status  work, and due to the recursive nature of aliases, even git git git git git status now works… on paper.

In reality, it is more a good joke than a good idea, as it comes with two major consequences. The first issue, as seen above, is that the command will now run from the root repo folder, which can cause confusion. The second problem is that it breaks the rather important ability to run git help git to get , the help page for the actual Git command. Now, instead, it would print the much less useful:

'git' is aliased to '!git' 

Level 7: Reusing Git aliases

Build on that which came before
… Using that which came before

In older versions of Git, before 2.20, Git aliases were not allowed to refer to other Git aliases. This often led to many similar aliases in the config file, as seen when we discussed custom Git log commands.

The only workaround available was to utilize the bang feature, as it turns out that it is perfectly workable to have:

[alias]
  l1 = !git slog -1 
  l5 = !git slog -5

This is not recursively calling an existing alias but starting a new shell session that just happens to use Git with an alias.

Luckily, since Git 2.20 (2018), recursive aliasing has been allowed, and since 2.30 (Q1 2021), even the bash-completion (tab-completion) comprehends and translates.

So, for a much cleaner Git configuration, I can now have:

[alias]
  slog = log --pretty=format:'%C(auto)%h %C(red)%as %C(blue)%aN%C(auto)%d%C(green) %s'
  l = slog
  l1 = l -1 
  l5 = l -5
I often use this for readability in my config. When I add a new fancy alias, I give it a good descriptive name and then add a short form for daily use below. For instance, in the previous post, I showed short daily use versions of the "incoming" and "outgoing" changes:
[alias]
   in = incoming 
   out = outgoing

Level 8: Pipelining the action

Chaining Unix commands for more control (or madness)

I mentioned earlier that the "bang" feature was a game changer, but we only scratched the surface. The next step comes when you realize that calling out to a shell opens the ability to chain together multiple commands.

The simplest use of this is aliases that do multiple separate things after each other using the shell and operator &&.

[alias]
  # init new git repo with empty initial commit
  start =
   !git init && git commit --allow-empty -m \"Initial commit\"

  # create a git repo including everything in this dir
  initthis =
   !git init && git add . && git commit -m \"Bootstrap commit\"
We can take this a step further by using the shell command substitution feature, which allows the output of a command to be used inline in another command. Let's take a look at some examples that demonstrate this:
[alias]
  # Alternative version of the mycommits alias from level 3 
  # Better/worse?: It is less hardcoded but only finds commits
  # matching current config.
  lome = "!git slog --author=$(git config --get user.name)"
  # Switch to master/main/trunk or whatever is the default branch is
  # in this repo
  swm = !git switch $(basename $(git symbolic-ref --short             refs/remotes/origin/HEAD))

Note: The alias above is wrapped for readability. It needs to be on one line in the config. More on this issue later.

We can even use existing environment variables or define new ones inline in an alias.

Years ago, I came across this "meme" on the wall of a customer’s office:
In case of fire

I printed a copy for our own office, which spawned a long and humorous discussion on Slack about why this wouldn't work and all the ways it needed to be improved.

The final result was the following gem of an alias that nicely demonstrated the use of variables:

[alias]
  panic = !PD=$(date +%d%m%y-%H%M) 
   && git add -A 
   && git commit -mWAAAAAAAAGH!! 
   && git switch -C PANIC-$USER-$PD 
   && git push -f origin PANIC-$USER-$PD

But the real power comes when we take this one step further and start using Unix pipes to pass output from one command to another.

First, I define a quick remoteurl alias to make it easier to get the URL of my Git remote. But what if I cloned the repo with ssh, and have a "git@" formatted URL when I wanted to use it to find the website for the repo?

Let's send the output of the first command to sed, which does search/replace and exchanges git@ with  https://.

To round this off, let's add an alias that ends the output of that directly to my clipboard using the Mac pbcopy command. (On Windows, we could do the same with clip).

[alias]
  remoteurl  = "remote get-url origin"
  remotehttps = "!git remoteurl | sed  -e 's/git@/https:\\/\\//'"
  remotecopy   = "!git remotehttps | pbcopy"
  

Similarly, we can use other shell features like redirecting output to files. I sometimes find myself wanting to create a .mailmap file in my repos. This allows you to e.g., "map" some contributors' old email to their new one or ensure that different spellings of a user get combined.

To have a good starting point for a mailmap, I need a list of authors with their names and emails in the standard "Jan Krag <jan.krag@example.com>" format, somewhat similar to what I could get from git shortlog -sne, but without the first column of numbers.

To help with this, I came up with the following aliases:

[alias]
  mm   = "!git log  --format='%aN ' | sort -u"
  mmm  = "!git mm >> .mailmap"
  mmme = "!git mmm && code .mailmap"

The log simply prints out the author and email for every commit, then uses the "unique" switch on the shell sort command to remove duplicates and sort the output. The first alias prints them out to the console, while the second redirects the output and creates or appends to an existing .mailmap file.

The third is the "I am in a hurry" convenience version that immediately opens the new .mailmap file in my VSCode editor. So here we are combining pipes, redirects and the && feature in one alias.

Level 9: Functions

Bash functions for the win!

Let's level up yet again on what we can do. Now that we have this shell context, another feature that becomes available to us is the ability to define functions. Why would I want to do that, you may ask? To some degree, it does make complex aliases a bit cleaner, but the most important aspect is the ability to read command-line arguments and use them in a more controlled way.

In normal aliases, you can choose to append whatever switches and arguments you want after the alias, and these are added after the expansion. For instance, going back to my Git slog alias from the previous post, it is perfectly fine to use this as git slog -3 or git slog --all. But what if I want to create an alias where I need some argument in multiple places?

A very simple example of this is my alias for deleting a branch both locally and on the remote:

[alias]
  rmbranch = 
  "!f(){ git branch -d ${1} && git push origin --delete ${1}; }; f"

Note the syntax. In my shell context, I start by defining a function f() that executes the statements from the opening { to the final };.

Then, finally, I just call this function by its function name f. The nice thing is that we can now pass arguments to the function, and these are made available in the function scope as "positional parameters," numbered from one and up. So ${1} simply refers to the first argument provided by the user when calling the function, and as you can see in the example, we can use a parameter as many times as we like.

Let's look at another simple example:

[alias]
  # Easy add a GitHub repo as new remote 
  ghremote
"!f(){ git remote add $1 https://github.com/$2.git; }; f"

In this example, I am using a function not to reuse an argument but because I want to take multiple arguments and use them in very specific places inline in the alias.

You might notice that this example uses the positional parameters without the curly braces. This is just to illustrate that by bash standards, the braces are only needed for positional parameters over nine, so it is down to your preference.

Let's try out this ghremote alias:

$ git ghremote jan jkrag/git-katas

$ git remote get-url jan
https://github.com/jkrag/git-katas.git

$ git fetch jan
remote: Enumerating objects: 6, done.

Level 10: Going overboard

Introducing multi-line formatting.

We have previously seen a few examples of aliases that end up being very long lines in your config and, thus, are close to “unreadable.” There is, however, a way to actually have real multiline aliases. Simply add a backslash at the end as this escapes the newline.

This, however, doesn't really answer the question, "should I?"

At some point, we reach the limits of what is actually sane to do in aliases and where you should consider some of the alternative options that I will present in "Level 11." But, after all, this is a blog post about aliases, so let us push the boundaries a bit, and you can make your own judgment.

Let us start this section with a quite complex but sometimes useful example that I am not going to explain in detail.

[alias]
  # Delete all branches merged into master. 
  # With -f also include branches merged into current
  sweep = ! \

        git branch --merged $( \
        [ $1 != \"-f\" ] \\\n \
            && git rev-parse master \
        ) \
        | egrep -v \"(^\\*|^\\s*(master|develop)$)\" \\\n \
        | xargs git branch -d

Combining this with the symbolic-ref trick from the swm alias for finding the default branch is left as an exercise for you, dear reader.

There is not much more to explain, so let's just have a look at a few more examples that can inspire you to try writing your own aliases.

"I just want to open the website for this repo."

But what to do if the Git remote is using ssh?

[alias]
  # Open repo in browser  
  browse = "!f() { \
          open `git remote -v \
          | awk '/fetch/{print $2}' \
          | sed -Ee 's#(git@|git://)#http://#' -e 's@com:@com/@'` \
          | head -n1; \
     }; f"
Why doesn’t git diff include new files?
[alias]
  udiff = 
    "!f() { \
            for next in \
                $(git ls-files --others --exclude-standard); \
            do \ 
              git --no-pager diff --no-index /dev/null $next; \   
            done;
      }; f"

Let's see it in use:

git-1

Which files get the most love?

I found this one originally on a .gitconfig blog post by Michael Wales and slightly modified it for my preference.

[alias]
  churn = !git -p log --all -M -C --name-only \
      --format='format:' $@ \
        | sort \
        | grep -v '^$' \
        | uniq -c \
        | sort -r \
        | awk 'BEGIN {print count,file} {print $1 , $2}'

And in use on the git-katas repository:

$ git churn
42 README.md
24 basic-commits/README.md
21 basic-branching/README.md
19 ignore/README.md
19 basic-staging/README.md
17 submodules/README.md
17 3-way-merge/README.md
16 configure-git/README.md
15 Overview.md
14 ff-merge/README.md

Why do I need to know how to continue?

I frequently deliver Git training, and every time we talk about resolving merge/rebase conflicts, I have the dubious pleasure of introducing:

git merge --continue
git rebase --continue
git cherry-pick --continue
git revert --continue

At some point, I started wondering, “Why, oh why, do I, as the user, need to specify ‘what’ I am continuing when Git obviously knows that it is in a merge state, a rebase state, etc? Why isn't there just a continue command that ‘does the right thing?’”

As I recall, I did what we used to do before ChatGPT and searched the net and found a bash script that does this. This is probably also the sane solution, as mentioned in the next chapter, but as a fan, I did what had to be done and made it work as a pure alias. I can, therefore, bring to you this monstrosity that truly encompasses the spirit of going overboard:

[alias]
  # merge --continue, rebase --continue 
  # whatever --continue
  continue = "!f() { \
        repo_path=$(git rev-parse --git-dir) && \
        [ -d \"${repo_path}/rebase-merge\" ] && git rebase --continue && return; \
        [ -d \"${repo_path}/rebase-apply\" ] && git rebase --continue && return; \
        [ -f \"${repo_path}/MERGE_HEAD\" ] && git merge --continue && return; \
        [ -f \"${repo_path}/CHERRY_PICK_HEAD\" ] && git cherry-pick --continue && return; \
        [ -f \"${repo_path}/REVERT_HEAD\" ] && git revert --continue && return; \
        echo \"Nothing to continue?\"; \
    }; f"

Sharing Git aliases

To close off this section, I will share with you my "holy grail" alias that I developed about 10 years ago through hard work and determination.

The problem I wanted to solve was sharing aliases with co-workers on Slack, for example. If I want to provide a quick alias to a less experienced Git user, it is overly complex to have to explain each time how to find and edit the global config file and where to put the alias code in the file, etc.

It would be so much cleaner to just send the appropriate git config –global alias.foo "do this Git thing" command that they can run directly. For simple aliases, I just bang this out from memory, but for even slightly complex aliases involving quotes and maybe variables, it is non-trivial to get right, so I came up with the idea to write an "exportalias" alias. Little did I know how hard it would be to get this one right, and along the way, one of the success criteria was that it should be able to export itself. I haven't battle-tested it with all the really crazy multi-line aliases, but for those, I would always share the .gitconfig snippet anyway.

[alias]
  exportalias = "!f() { in=${1}; out=$(git config --get alias.$in) ;      printf 'git config --global alias.%s %q\n' $in \"$out\";};f"

I left this one without line breaks on purpose to be sure that it was in a form that is exportable.

Let's try it out on a non-trivial alias from earlier in this post—one that contains not only special characters but also escaped quotes.

$ git exportalias start
git config --global alias.start \!git\ init\ \&\&\ git\ commit\ --allow-empty\ -m\ \"Initial\ commit\"

Level 11: Bonus round

Is there a limit to the madness?

In this final bonus section, I will introduce you to a few alternative approaches to extending Git functionality—options that might turn out to be more sane than full-page multi-line aliases.

Custom executables

First off is the realization that Git is wisely constructed so that any executable in your path called git-something will turn into a git something command.

This means that some of our longer aliases could instead be implemented as a simple bash script. Why does this matter, you might ask? Well in part because you avoid a lot of the formatting hassle of staying within the bounds of the .gitconfig file, like having to escape newlines and mess with unnecessary quoting.

Another advantage is that we can better write "real" scripts with proper argument parsing, error handling, usage help text, and so on.

#!/usr/bin/env bash

usage() {
cat <<HERE
usage: git alias                         # list all aliases
   or: git alias         # show aliases matching pattern
   or: git alias    # alias a command
HERE
}

case $# in
  0) git config --get-regexp 'alias.*' | sed 's/^alias\.//' | sed 's/[ ]/ = /' | sort ;;
  1) git alias | grep -e "$1" ;;
  2) git config --global alias."$1" "$2" ;;
  *) >&2 echo "error: too many arguments." && usage && exit 1 ;;
esac

Actually, Git doesn't even require you to use Bash. You could write your new Git commands in any language, although you might want to stick to one that has a good Git library, maybe Python, Java, or Go-lang. 

Git extension packages

If we can write custom Git commands as binaries in our path, then we can also write and distribute whole collections of them. There are even open-source projects that provide these kinds of "Git extensions," either for specific purposes or just general collections of "useful" commands.

One of the biggest of these generic collections is https://github.com/tj/git-extras, which, when installed, adds over 70 new commands to your portfolio, including the self-serving git extras, which lists all of them. It can be installed with most of the common package managers or manually cloned if that is not an option, e.g.:$ sudo apt-get install git-extras

or

$ brew install git-extras

I won't list all 70 commands, but here are a few selected mentions:

  • git-abort: Abort current Git operation.
  • git-alias: Define, search, and show aliases.
  • git-magic: Automate add/commit/push routines.
  • git-repl: Git read-eval-print-loop.
  • git-standup: Recall what you did on the last working day.

Another extension package that might be worth looking at is Git Pastiche, which contains a much smaller selection of commands, maybe a bit more low-level, but I find especially git stats and git activity quite useful on occasion.

In this context, it might also be worth mentioning that something that we consider a "core" extension of Git, like Git LFS, aka Git Large File Storage is also built as an extension using this concept and written in Go-lang. The core Git lfs command is just a git-lfs executable that, in turn, takes care of redirecting all the lfs subcommands to other Go programs.

Help

On a closing note, I will cover help very briefly. If you really want to go all in on your custom Git commands, as indeed "git extras" and others have done, you can provide custom help pages.

Typing git help mycustom will indeed get Git to check if there is a man page for mycustom on your system. Covering how to create man pages is out of the scope of this blog post, but it is easy to find good resources that cover it.

For Git aliases, you might have noticed that something like git help st simply prints out:

'st' is aliased to 'status'

This may or may not be useful, depending on your needs.

For basic aliases like this one, i.e., ones that don't shell out to bash, you can also use the format git st --help to bring up the normal help page for the aliases command, in this case, git status.

Credits and disclaimers

The examples in this post have come from my personal collection of over a decade of Git use. Some have been modified for what I am trying to demonstrate in each section.

As for the origins of these aliases, some I wrote myself to scratch an itch, others have been passed on to me by co-workers, participants in my Git classes, and friends. Some I simply found online over the years and either copied as-is or adapted for my own use. In general, there is a community spirit of sharing these things, so I hope this falls under "fair use."

Lately, I have started adding comments in my config for very complex aliases stating where I found them, but even then, it is hard to trace the origins of these code samples as others do the same as me (borrow from somewhere and adapt as needed).

If you found an alias in this blog post for which you consider yourself the original author, please let me know, and I will either cite you for it or remove it upon request.

Remember to check out part one of this blog post if you haven't done so already!

Published: Jun 17, 2024

Software developmentDevOpsCI/CD