I wrote a blog post about git-number and how useful it was for my command line git usage. In an on-going effort to simplify my lifestyle & setup, I replaced git-number with a few git aliases. I’ll explain how I recreated the functionality in this blog post.

Why replace it?

Looking at git-number’s source I realize it is written in Perl. I’m not here to cast judgement on another programming language but reading the code, it felt like overkill for my use cases.

It also meant I could eliminate one additional 3rd party library that I have to install every time.

Let’s go ahead and recreate it with some good old unix command line fu.

Building git add <number>

git status showing files

To git add the first file, I have to manually type the entire path git add 0.collapse-categories-into.... This is tedious and error-prone. If your file is nested in a series of directories (like an Android or iOS app), it becomes worse.

With git-number, this is as simple as

git-number add 1
# where 1 stands for the order
# in which a file shows up in git status

Let’s recreate that with a git alias.

# in your .gitconfig file
[alias]
    a = !"a() { <UNIX COMMAND GOES HERE> ; }; a"

The key now is to come up with a unix command that will take in a number and pluck that file for you. Here’s a simple version of that command:

pluck line

You can now make it a git alias by adding it to your .gitconfig file:

# .gitconfig
[alias]
    a = !"a() { git add $(git status --porcelain | cut -b4- | sed -n \"$1p;$1q\"); }; a"
git add demo

You can systematically recreate all of the git-number functionality in pure git aliases!

Bonus tweaks

Over time, I’ve tweaked the above command to a behemoth shell command that accounts for a variety of use cases:

  • g a - git add all files
  • g a . - git add all files
  • g a 2-3 - git add second and third file
  • g a 3-5 - git add second to fifth file

Here’s what my actual alias is, in all its glory:

# .gitconfig
[alias]
    a = "!f() { \
        if [[ $# -eq 0 || \"$1\" == \".\" ]]; then \
            git add .; \
        else \
          files_to_add=(); \
          for arg in $(seq $(git status --porcelain | wc -l)); do \
              files_to_add+=($(git status --porcelain | sed -n ${arg}p | awk '{print $2}')); \
          done; \
          if [[ $1 == *'-'* ]]; then \
              IFS='-' read -ra RANGE <<< \"$1\"; \
              for i in $(seq ${RANGE[0]} ${RANGE[1]}); do \
                  git add \"${files_to_add[$i-1]}\"; \
              done; \
          elif [[ $1 == *','* ]]; then \
              IFS=',' read -ra NUMS <<< \"$1\"; \
              for i in \"${NUMS[@]}\"; do \
                  git add \"${files_to_add[$i-1]}\"; \
              done; \
          else \
              git add \"${files_to_add[$1-1]}\"; \
          fi; \
        fi; \
    }; f"

Understanding the command:

In this day of chat-gpt you don’t need to understand the above command. The idea and how to leverage it, is far more important.

Other git commands?

An earlier version of me would have put this in a tidy shell script, made it executable & repeatable; then called the script from my gitconfig alias.

A more experienced version of current me knows, I shouldn’t waste more time on prettifying this. It works, is zippy, and fits in my .gitconfig without adding any more dependencies. I’m probably not going to touch this for a long time.

# .gitconfig
[alias]
    a = "!f() { \
        if [[ $# -eq 0 || \"$1\" == \".\" ]]; then \
            git add .; \
        else \
          files_to_add=(); \
          for arg in $(seq $(git status --porcelain | wc -l)); do \
              files_to_add+=($(git status --porcelain | sed -n ${arg}p | awk '{print $2}')); \
          done; \
          if [[ $1 == *'-'* ]]; then \
              IFS='-' read -ra RANGE <<< \"$1\"; \
              for i in $(seq ${RANGE[0]} ${RANGE[1]}); do \
                  git add \"${files_to_add[$i-1]}\"; \
              done; \
          elif [[ $1 == *','* ]]; then \
              IFS=',' read -ra NUMS <<< \"$1\"; \
              for i in \"${NUMS[@]}\"; do \
                  git add \"${files_to_add[$i-1]}\"; \
              done; \
          else \
              git add \"${files_to_add[$1-1]}\"; \
          fi; \
        fi; \
    }; f"
    r = "!f() { \
        if [[ $# -eq 0 || \"$1\" == \".\" ]]; then \
            git reset .; \
        else \
          files_to_add=(); \
          for arg in $(seq $(git status --porcelain | wc -l)); do \
              files_to_add+=($(git status --porcelain | sed -n ${arg}p | awk '{print $2}')); \
          done; \
          if [[ $1 == *'-'* ]]; then \
              IFS='-' read -ra RANGE <<< \"$1\"; \
              for i in $(seq ${RANGE[0]} ${RANGE[1]}); do \
                  git reset \"${files_to_add[$i-1]}\"; \
              done; \
          elif [[ $1 == *','* ]]; then \
              IFS=',' read -ra NUMS <<< \"$1\"; \
              for i in \"${NUMS[@]}\"; do \
                  git reset \"${files_to_add[$i-1]}\"; \
              done; \
          else \
              git reset \"${files_to_add[$1-1]}\"; \
          fi; \
        fi; \
    }; f"
    ch = "!f() { \
        if [[ $# -eq 0 || \"$1\" == \".\" ]]; then \
            git checkout .; \
        else \
          files_to_add=(); \
          for arg in $(seq $(git status --porcelain | wc -l)); do \
              files_to_add+=($(git status --porcelain | sed -n ${arg}p | awk '{print $2}')); \
          done; \
          if [[ $1 == *'-'* ]]; then \
              IFS='-' read -ra RANGE <<< \"$1\"; \
              for i in $(seq ${RANGE[0]} ${RANGE[1]}); do \
                  git checkout \"${files_to_add[$i-1]}\"; \
              done; \
          elif [[ $1 == *','* ]]; then \
              IFS=',' read -ra NUMS <<< \"$1\"; \
              for i in \"${NUMS[@]}\"; do \
                  git checkout \"${files_to_add[$i-1]}\"; \
              done; \
          else \
              git checkout \"${files_to_add[$1-1]}\"; \
          fi; \
        fi; \
    }; f"
    # and so on ...

It took me 5 seconds to add the other commands. I’m not going to waste time tidying it up, cause it just works. You can find my full .gitconfig in my dotfiles.

One less dependency in this journey.