The Git for-each-ref command (Documentation) — if not aware — is a powerful plumbing command for obtaining information about your repository references (i.e. .git/refs). An example of this is shown in the screenshot above. Don’t worry, we’ll discuss what the command is doing shortly.
As of Git 2.47.0, the for-each-ref command has gained a new superpower in the form of a new is-base field which can dynamically calculate the current parent branch. This is a most welcomed enhancement so I want to spend time talking this and more in this article. 🎉
Basics
Out of the box, you can immediately view all references in your repository by running git for-each-ref in your terminal. In my case, I’m using my Dotfiles project which produces the following (truncated for brevity):
8a9a7c0e3a5eb5eb65f245ae214d6721ed77d2f5 commit refs/heads/main 8fb0eeb1acd08790266b690257a1224a8db2a21c commit refs/heads/release 8a9a7c0e3a5eb5eb65f245ae214d6721ed77d2f5 commit refs/remotes/origin/HEAD 8a9a7c0e3a5eb5eb65f245ae214d6721ed77d2f5 commit refs/remotes/origin/main 8fb0eeb1acd08790266b690257a1224a8db2a21c commit refs/remotes/origin/release a76698d6a35c87f190ffc6dac5cbc1a9c47c86ae commit refs/remotes/pull_requests/1 67df3091e4a1ab49274579d93c92c530e20cfd1e commit refs/remotes/pull_requests/3 76521548a3e91b7f856aab3f1d15a04c3b9f386a commit refs/remotes/pull_requests/4 f754e69239fec4bdc01deae01aa4c97953e985d4 commit refs/remotes/pull_requests/5 03054345743fa8330f0e248422d709dffd13a2b4 tag refs/tags/31.2.0 9d16c4366e7e920a50a34447762ce963de346091 tag refs/tags/31.3.0 4a23e358d4b6c4a1facea9abac7284cb25902e2d tag refs/tags/32.0.0
When studying the above, you’ll notice there are three columns of information produced for each reference:
-
SHA: The commit SHA from which the branch/tag was created or the current SHA (in the case of the
mainbranch). -
Type: The reference type. In my case, this is either
commitandtag. -
Path: The reference path as found in your
.git/refsfolder.
While the default output is useful, there are better ways to format and make use of this information but the above is the basics.
Options
We don’t have time to explore all for-each-ref options but I want to highlight what I like the most:
-
--count: Allows you to limit the output. By default, you’ll see everything but, if you wanted to limit the output to only two references you could use--count 2. Quite handy since you don’t have to pipe usinghead -n 2ortail -n 2, for example. -
--sort: Allows you to sort by field name likerefname(default),authordate, andobjecttypeto name a few. More can be found in the Examples section of the Documentation. Even better, you can reverse the sort by prefixing your sort with a minus sign (-). Example:--sort=-taggerdate. -
--format: Allows you to format by field name including the color coding of each field. This is the option I use the most and will get into this more shortly. -
--color: Allows you to list references in color and pairs will with--format. The default is--color=alwaysbut you can use--color, as shorthand, for the same effect. Other options areneverandauto.
Formats
There is a lot of ground to cover when talking about the kinds of formats you can use with for-each-ref. Delving into each is outside the scope of this article but I do want to highlight some of the most useful formats that you can immediately apply to your own workflow.
🎗️ Don’t forget, you can find the list of supported fields in the Field Names section of the Documentation. The Examples section is great too.
Parent Branch
Obtaining the parent branch of the current branch you are working on has always been a hassle. Thankfully, with the release of Git 2.47.0, solving this problem has vastly improved! The solution is to use the new is-base field.
This is what is used in the screenshot at the top of this article. Here’s the full command:
git for-each-ref --format="%(refname:lstrip=2)%(is-base:$(git log --pretty=format:%h -1))"
# main
# release(8fb0eeb1acd0)
# origin/HEAD
# origin/main
# origin/release
# pull_requests/1
# pull_requests/3
# pull_requests/4
# pull_requests/5
# 31.2.0
# 31.3.0
# 32.0.0
We can break down the above into steps:
-
First, we format by reference name and then left strip the path by two segments. So if the full reference path is
refs/heads/main, thenlstrip=2will ensurerefs/headsis removed so we only end up withmainas the reference name. -
Next we use
is-basewhich takes a commit-like value. This could be a commit SHA, branch name, tag, etc. In my case, I’m using a commit SHA which is the last commit on my feature branch via thegit log --pretty=format:%h -1)subcommand (the%his the short SHA. Example:8fb0eeb1acd0).
That’s it. If we collapse this down, this equates to:
git for-each-ref --format="%(refname:lstrip=2)%(is-base:8fb0eeb1acd0)"
This then yields:
main release(8fb0eeb1acd0) origin/HEAD origin/main origin/release pull_requests/1 pull_requests/3 pull_requests/4 pull_requests/5 31.2.0 31.3.0 32.0.0
If you didn’t catch the branch I’m using in the screenshot, I’m working on my release branch. So the output above shows my current commit SHA (8fb0eeb1acd0) ancestry so-to-speak. All we care about is the first entry because main is the parent of my release branch. We can clean this up using the --count option like so:
git for-each-ref --format="%(refname:lstrip=2)%(is-base:$(git log --pretty=format:%h -1))" --count 1
This one-liner yields main as the parent branch and is a powerful way to obtain the parent branch of the current branch you are working on. I ended up expanding this one-liner into a few private Bash functions, as building blocks, in my Dotfiles as follows:
Git Branch Parent
_git_branch_parent() {
local format=""
local name=""
format="%(refname:lstrip=2)%(is-base:$(_git_sha))"
if [[ "$(_git_branch_name)" == "$(_git_branch_default)" ]]; then
_git_branch_default
else
name="$(git for-each-ref --format="$format" --count=1 | sed 's/([^)]*)//')"
if [[ "$name" == "$(_git_branch_name)" ]]; then
_git_branch_default
else
printf "%s" "$name"
fi
fi
}
The above obtains my parent branch via the following:
-
Checks if the current branch is equal to the default branch (i.e.
main) and answers the default branch if so. -
Checks if the current branch name is equal to the
is-basefield result. In other words, the current branch and result are identical which means we can answer the default branch as the parent. I usesedto strip the commit SHA because theis-basefield can yield your branch with a SHA in parenthesis (example:release(8fb0eeb1acd0)) but only the branch name is needed for comparison purposes. -
Otherwise, answers the parent branch as calculated by the
is-basefield.
Git Branch Range
_git_branch_range() {
local range=""
if [[ "$(_git_branch_name)" == "$(_git_branch_default)" ]]; then
range="$(_git_sha)"
else
range="$(_git_branch_parent)..$(_git_sha)"
fi
printf "%s" "$range"
}
The above calculates the commit SHA range for my current branch. This means, if on the default branch, the range will be from the very first commit made on the repository to the current commit’s SHA (when you don’t specify the range start). Otherwise, we’ll use the parent branch as the range start and the current commit SHA as the range end.
Git Branch SHAs
_git_branch_shas() {
git log --pretty=format:%h "$(_git_branch_range)"
}
This last function leverages the above functions to acquire all commit SHAs that belong to the current branch. This can then be used for Git Rebase purposes, selecting individual commits, displaying the details of every commit in the branch, and so much more.
Code Reviews
In some situations, listing all code reviews via your command line can be handy. I should note that this does require adding the following to your global Git Configuration to always fetch this information:
[remote "origin"] fetch = +refs/pull/*/head:refs/remotes/pull_requests/*
With the above in hand, here’s an example of using for-each-ref to list all code reviews within your terminal:
format="%(refname) %(color:yellow)%(refname)%(color:reset) %(subject) %(color:blue bold)%(authorname) %(color:green)(%(committerdate:relative))"
git for-each-ref --color \
--format="$format" \
refs/remotes/pull_requests \
| sed 's/refs\/remotes\/pull_requests\///g' \
| sort --numeric-sort \
| cut -d' ' -f2-
The above produces the following output which provides the code review ID along with the subject, author, and length of time since the code review was opened/closed:
By selecting the number above (let’s say 1), I can jump to the code review using this URL:
https://github.com/bkuhlmann/dotfiles/pull/1
Definitely handy when working from the command line.
In terms of formatting, you’ll notice I use %(color:yellow) to start using a specific color, like yellow, and then %(color:reset) to reset back to white. Everything else is the field names which are either wrapped in a specific color or default back to white. You can learn more about this in the Documentation.
Once the color and formatting is applied, I pipe the output for further processing. Here’s the breakdown via code comments:
# Format in color by plucking out specific fields.
git for-each-ref --color \
--format="$format" \
# Only select the pull request references.
refs/remotes/pull_requests \
# Use sed to delete the reference name but not the ID. This must be done after we obtain all pull requests or we won't be able to obtain the ID.
| sed 's/refs\/remotes\/pull_requests\///g' \
# Sort all results by ID.
| sort --numeric-sort \
# Delete the non-colored ID, otherwise we'd have a duplicate which is confusing.
| cut -d' ' -f2-
That’s it. Not bad with a little bit of additional formatting to clean things up.
Branches
This is one of my favorite uses of for-each-ref which is listing all of my branches as a table of information.
format="%(refname)|%(color:yellow)%(objectname)|%(color:reset)|%(color:blue bold)%(authorname)|%(color:green)|%(committerdate:relative)"
git for-each-ref --sort="authordate:iso8601" \
--sort="authorname" \
--color \
--format="$format" \
refs/heads \
refs/remotes/origin \
| sed '/HEAD/d' \
| sed 's/refs\/heads\///g' \
| sed 's/refs\/remotes\/origin\///g' \
| uniq \
| sort \
| column -s '|' -t
The above produces the following output:
Let’s break down what is happening here but, this time, I’ll skip detailing the formatting and color coding because you should be familiar with that syntax by now. Here’s a snippet of the code with code comments:
# We sort first by author date using ISO 8601 format. Then we, secondarily, sort by author name. Yep, you can pass specific sub-sorts for finer grained control.
git for-each-ref --sort="authordate:iso8601" \
--sort="authorname" \
# As before, we enable color and supply our custom format.
--color \
--format="$format" \
# I'm using two patterns here. The first is for local branches and the second is for remote branches so I can get the full picture.
refs/heads \
refs/remotes/origin \
# Sed is used to delete the HEAD reference since we don't need it.
| sed '/HEAD/d' \
# Sed is used to delete the head and remote refs paths since we only care about the actual branch names.
| sed 's/refs\/heads\///g' \
| sed 's/refs\/remotes\/origin\///g' \
# Since the results will include local and remote branches, we need to remove the duplicates.
| uniq \
# A nice alphabetical sort ensures consistency.
| sort \
# Finally, we use the column command to tabularize our output by splitting each column by the pipe (`|`) which was used in our original format string. You could use any character as a delimiter but I find the pipe to be more akin to using ASCII Doc or Markdown syntax for table columns.
| column -s '|' -t
There you have it. Now you can browse through your branches with grace all via the power of the for-each-ref command.
Conclusion
I hope the above has leveled you up so you can do more with Git from the command line. Using for-each-ref is one of many powerful commands to have in your toolkit. Spending time folding for-each-ref into your own workflow will not only make you more efficient but help you master Git and maximize the potential of what you can do with a little shell scripting.
Enjoy!
