Bash One-liner to Edit All Files from a Git Commit

Over the years I've greatly enjoyed learning how to simplify common actions in the terminal. I want to share a quick, simple example of a handy little helper-script, and encourage you to add these kinds of helpers to your workflow. It's a great way to improve or maintain your Bash skills, and anyway making a task easier or faster is probably the foundational joy of programming.

I found myself in a situation last week where I wanted to quickly reopen all the files from my last Git commit. It's easy to do this, but in my experience one of the difficulties of Bash is knowing what keywords to search for to find help, so I'm going to put the pieces of this together one by one, with some other helpful tricks, so that you don't have to wait as long to find each piece as I did.

It took me a while to wrap my head around command substitution, but now it's a Bash feature I can't live without. In short, it's a shortcut to simplify using the output of evaluating a command as input to some other command. This is possible with pipes, but often more verbose. The short form is simple -- $(command). For example, one might be looking around for something in a project dir with rg[1] - rg 'function_name' - and then decide it's time to edit. rg lists matching files with the -l switch, so you can invoke vim $(rg -l 'function_name') to open all the search results as vim buffers.[2]

So, if we have a command that can produce a list of files from the last commit with no other data, we know how to easily bring them into vim. Luckily we have options! When I set this up I definitely just found the relevant StackOverflow[3] and ran with it, but I hate to miss an opportunity to recommend spending some time with the Git docs[4], where with a little determination and trial-and-error you could find your way to the right answer as well. The recommended answer that we'll use here is: git diff-tree --no-commit-id --name-only -r <commit-ish>

Now it's almost as simple as combining those two pieces! Yes, vim $(git diff-tree --no-commit-id --name-only -r HEAD) works, but it's verbose and a bit tricky to remember. We could fix this with an alias in .bashrc or .bash_profile -- alias edit_last_commit=vim $(git diff-tree --no-commit-id --name-only -r HEAD) -- cool! BUT, what if we want to pull up some other set of changes? I'm not sure if this is possible with an alias[5], but it definitely is with a little script.

I don't know if there's a more recommended or standard way to organize scripts like this[5:1], but my approach is to make a dir at ~/scripts and make sure it's on my $PATH. So, we'll put this little buddy at ~/scripts/edit_last_commit.

The only other detail to comment on here is one I've elided so far: how to refer to whatever set of changed files we want to edit. What I'm getting at is, what is a ? It's... something that looks kinda like a commit! For a very detailed explanation we can turn again to the Git documentation[6] but in short, most any way of identifying "the set of files last changed here" should work.

OK. Let's roll this all into a bash script, with some annotation:

#!/bin/bash                           # shebang line to ensure we end up evaluating this as bash!
set -e # only here out of habit; this ensures the script will exit on errors rather than causing mayhem (unexpected behavior)

if [[ -z "$1" ]]; then # if the first argument to the script is null (in other words, called with no args)
commitish="HEAD" # default rev is HEAD
commitish="$1" # but if the first argument *does* exist, use it!

vim $(git diff-tree --no-commit-id --name-only -r $commitish)

Once the script is saved, give it the ol' chmod +x[7] and you're all set.

I'll say it again: Bash has some damn near impenetrable and difficult-to-search syntax. Please do take a look at documentation for set -e[8] or that if statement[9] to learn more; these are bread-and-butter for small helpful scripts such as this one so it's worth understanding them and related options (set -x can be very handy too!)

I have called this script "simple" but it's only simple to me because I've spent years muddling about and haphazardly learning about these components along the way. If this is helpful to you, I'm so glad and I hope you can do more, faster for not having to piece all of this together yourself. If you've been muddling longer than me and have suggestions for improvements? I'd love to hear about them, within reason[10].

  1. ripgrep, my current favorite grep-alike ↩︎

  2. This could be shortened or further manipulated with history commands. Here's a good quick-reference for those on StackExchange: What are your favorite command line features or tricks? - Unix & Linux StackExchange ↩︎

  3. How to list all the files in a commit? - StackOverflow ↩︎

  4. I find that I'm constantly re-learning the value of spending some time reading documentation, manual pages, etc. for commonly used tools - for me, recently this means git, tmux, and vim - so, here's the Git Book section on viewing commit history, and I strongly recommend taking some time now and then to read up on the amazing tools at your disposal! ↩︎

  5. If you do know, I'd be glad to hear about it! ↩︎ ↩︎

  6. gitrevisions - Git Reference ↩︎

  7. chmod +x $filename makes $filename executable for all users! probably fine on your own computer, but better to be more specific with permissions on other systems, to maintain a habit of care around possible security issues. ↩︎

  8. The set Builtin - GNU Bash Manual ↩︎

  9. Bash Conditional Expressions - GNU Bash Manual ↩︎

  10. please don't tell me I ought to use emacs or zsh, for example ↩︎

← Posts