TODO- start with the Usage
, Summary
, and Help
comments in the top of each file, before reading the tests.
This is the file that gets called by this block of the rbenv
file when the user types either rbenv -v
or rbenv --version
in their terminal.
As we’ve done before, we’ll start with the tests first:
Tests
The first two lines are:
#!/usr/bin/env bats
load test_helper
We’ve seen these two lines before- the bats
shebang and the loading of the test_helper
file. We’ll see these in every test file we read, so this will be the last time we include them in the read-through of a set of tests.
Setting GIT_DIR
After these two lines, the first line of code is:
export GIT_DIR="${RBENV_TEST_DIR}/.git"
Here we set an environment variable named GIT_DIR
to equal a “.git” hidden directory inside our test dir. I don’t see any other references to GIT_DIR
in the RBENV codebase, so I Google it and find that it’s mentioned here in the Git docs:
Repository Locations
Git uses several environment variables to determine how it interfaces with the current repository.
GIT_DIR is the location of the .git folder. If this isn’t specified, Git walks up the directory tree until it gets to ~ or /, looking for a .git directory at every step.
It appears that we use git
commands several times throughout the tests for rbenv --version
, so setting this env var is part of the overall setup we need for our tests to pass.
Test Setup
Next block of code:
setup() {
mkdir -p "$HOME"
git config --global user.name "Tester"
git config --global user.email "tester@test.local"
cd "$RBENV_TEST_DIR"
}
This is our setup
hook function which gets called here, before the tests are run.
Inside the test, we first create the $HOME
directory. According to test_helper.bash, this env var resolves to "${RBENV_TEST_DIR}/home"
.
Next, we set two of git’s config values- the git user’s name and email address. Lastly, we navigate into our RBENV_TEST_DIR
.
Defining the git_commit()
helper function
Next block of code:
git_commit() {
git commit --quiet --allow-empty -m "empty"
}
This is a helper function to make an empty git commit.
Skipping ahead to the command file itself, part of its code uses git commands to pull RBENV’s version number using metadata from git. So that we have some fake git metadata to work with, we’ll occasionally call this git_commit
helper function in our tests.
Fetching the default version
First test:
@test "default version" {
assert [ ! -e "$RBENV_ROOT" ]
run rbenv---version
assert_success
[[ $output == "rbenv "?.?.? ]]
}
We start by asserting that $RBENV_ROOT
does not exist on our machine. Since RBENV_ROOT
does not appear in the command file, it’s not immediately apparent why we do this. There’s no explanation in the PR which introduced this test, either.
I suspect this is happening because part of running rbenv --version
is running the rbenv
file, which includes this block of code here. We likely assert that RBENV_ROOT
is empty because, if it weren’t, the non-default version of RBENV that RBENV_ROOT
pointed to could be using an unexpected version number, and therefore our test would have unexpected output. We want RBENV_ROOT
to be set to the default value by the rbenv
command, thereby making our version number easy to predict.
The rest of the test is pretty straight-forward. We run the --version
command, assert that:
- the command completed successfully, and
- the value stored in the
$output
variable frombats
matches the patternrbenv ?.?.?
.
Each question mark in the pattern corresponds to a single character, so we’re checking that the printed output starts with “rbenv “ followed by a single character, a period, a single character, another period, and a final single character. For example, 1.2.0
. In other words, the typical format of a version number (major, minor, and patch numbers).
Reading the correct version number, regardless of current git directory
Next test:
@test "doesn't read version from non-rbenv repo" {
git init
git remote add origin https://github.com/homebrew/homebrew.git
git_commit
git tag v1.0
run rbenv---version
assert_success
[[ $output == "rbenv "?.?.? ]]
}
This test appears to cover this line of code. The code does the following:
- We
cd
into${BASH_SOURCE%/*}
, which on my machine resolves to/Users/myusername/.rbenv/test/../libexec
or simply/Users/myusername/.rbenv/libexec
. - We run
git remote -v
, which (in the above directory on my machine) returns:
origin https://github.com/rbenv/rbenv.git (fetch)
origin https://github.com/rbenv/rbenv.git (push)
- We pipe the results of the
git remote
command togrep -q rbenv
(note that the-q
flag stands for “quiet mode”, according toman grep
, which causes the terminal to suppress normal output).
If the exit codes for each of those commands is 0
, then we reach the inside of the if
block. Otherwise, the block is skipped.
In our test, we do the following:
- We create a git repo and set its remote equal to the remote git repo of a non-RBENV project (specifically, the Homebrew project),
- We make an empty git commit and we tag it with the tag “v1.0”.
When we run the --version
command, we expect it to finish successfully and for the output to match the same pattern as the last test, i.e. the format ?.?.?
(NOT the v1.0
that we tagged our git repo with).
If we look up the git history of this test, we see it was introduced to avoid pulling the viersion number directly from the git repo if the RBENV installation came from an installation source such as Homebrew (i.e., if it wasn’t installed by pulling down the Github repo).
Reading the version number from the git repo
Next test:
@test "reads version from git repo" {
git init
git remote add origin https://github.com/rbenv/rbenv.git
git_commit
git tag v0.4.1
git_commit
git_commit
run rbenv---version
assert_success "rbenv 0.4.1-2-g$(git rev-parse --short HEAD)"
}
This test is a bit similar to the last one, except this time our remote origin
output does contain rbenv.git
, so we will reach the inside of the if
conditional, and therefore set the git_revision
variable.
In addition to the commit which we tag with “v0.4.1”, we make two more git commits, so that the expected value of git_revision
will contain both the version number and the number of commits that have happened since the version number was tagged.
We then run the --version
command and assert that:
- it completes successfully, and
- that the printed output contains:
- the string “rbenv”
- the version number (aka
0.4.1
) - the # of commits since the version number (aka
2
), and - the shortened version of the most recent commit SHA (aka the output of
git rev-parse --short HEAD
)
Together, these 4 pieces of information constitute the output of the git describe --tags HEAD
command that we see in this line of code. Note that the “v” from “v0.4.1” is removed by the #v
syntax from this line of code.
Printing the default version if no git tags are found
Last test:
@test "prints default version if no tags in git repo" {
git init
git remote add origin https://github.com/rbenv/rbenv.git
git_commit
run rbenv---version
[[ $output == "rbenv "?.?.? ]]
}
This test is similar to the previous test, except this time we don’t tag the repo with a version number.
We’ll still reach the inside of this if
statement, because the output of git remote -v
contains the string rbenv
. But because there are no tags, the command git describe --tags HEAD
will be empty, so the git_revision
variable will be empty as well.
Because of this, the :-$version
syntax on this line of code causes the parameter expansion to default to the value of the version
variable, meaning version
’s value is what gets printed by the echo
command. This is why the value of $output
is expected to match the “?.?.?” pattern.
With the tests wrapped up, let’s look at the code next.
Code
The first block of “code” is just the shebang (which we’ve already seen by now) followed by some comments:
#!/usr/bin/env bash
# Summary: Display the version of rbenv
#
# Displays the version number of this rbenv release, including the
# current revision from git, if available.
#
# The format of the git revision is:
# <version>-<num_commits>-<git_sha>
# where `num_commits` is the number of commits since `version` was
# tagged.
The comments tell the user that the intent is of this script file is to display the version number of this rbenv release, including the current revision from git, if available.
Exiting upon first error, and setting debug mode
Next few lines of code:
set -e
[ -n "$RBENV_DEBUG" ] && set -x
Again, this is code that we’ve seen elsewhere. The first line tells bash to exit immediately if an error occurs, and the 2nd line tells bash to read the $RBENV_DEBUG
environment variable, and to output verbose debugging information if that env var has been set previously.
Setting the default version number
Next few lines of code:
version="1.2.0"
git_revision=""
The first line sets a variable named “version” equal to the string “1.2.0”, and sets the variable “git_revision” equal to the empty string. These variables will be used below.
Checking if RBENV has a git remote
value
Next few lines of code:
if cd "${BASH_SOURCE%/*}" 2>/dev/null && git remote -v 2>/dev/null | grep -q rbenv; then
...
fi
Here we do the following:
- We attempt to cd into a directory specified by the value of the
$BASH_SOURCE
env var (piping any errors to/dev/null
). - We try to run
git remote -v
inside that directory (again, piping any errors to/dev/null
). - We pipe the results of the previous
git remote
command to thegrep
command andgrep
ping for the string “rbenv” in quiet mode (i.e. passing the-q
flag).
After Googling “grep quiet mode”, the first result I see says that the purpose of quiet mode is:
Sometimes, instead of examining at the matched strings, we are only interested in whether there was a match or not. The quiet option (-q), causes grep to run silently and not generate any output. Instead, it runs the command and returns an exit status based on success or failure. The return status is 0 for success and nonzero for failure.
So this implies that we don’t actually care what the match for grep rbenv
is, only whether there was a match. If there wasn’t, then the if
condition returns false, so we don’t execute the code inside said condition.
Setting a non-default RBENV version if a tag was found
However, if our directory does have a git remote which matches rbenv
, we execute the following code:
git_revision="$(git describe --tags HEAD 2>/dev/null || true)"
git_revision="${git_revision#v}"
Here we set the git_revision
string to the either:
- the result of
git describe --tags HEAD 2>/dev/null
as the happy path, or (if this happy path has no result), - to the boolean
true
. We’ve seen this trick before, and last time we did, the trick was used to ensure that any error raised by the code before||
didn’t trigger an exit of the process, due to theset -e
at the top of the file.
The 2nd line simply deletes any v
character at the beginning of the version value. For example, if the previous command set git_revision
equal to v1.2.0
, then this 2nd line of code just trims the v
off the front, leaving us with 1.2.0
.
I’ve never used the command git describe --tags HEAD
before, but I know I’ll only be able to use it if I’m inside a git
repo. Since I installed my RBENV code from source for the purposes of these posts, I know I can simply navigate to cd ~/.rbenv
and run the above command:
$ cd ~/.rbenv
$ git describe --tags HEAD
v1.2.0-16-gc4395e5
I get v1.2.0-16-gc4395e5
as my version number. Which, by the way, is the same output I get if I simply run rbenv --version
:
$ rbenv --version
rbenv 1.2.0-16-gc4395e5
Note that, if I had installed RBENV via Homebrew or another source, git_revision
would have retained its original value of ""
, and (as we’ll see on the next line of code) we would have fallen back to the default value stored in $version
.
Printing the RBENV version
Next line of code:
echo "rbenv ${git_revision:-$version}"
Here is where the rbenv
prefix before the version number comes from.
After the rbenv
prefix, we echo
the value of git_revision
if it was populated from the insides of that if
block we just looked at. If we never reached the insides of that block, then our value of git_revision
should still be the empty string that it was initialized to, in which case we use the value of our other variable, version
, which was initialized to 1.2.0
.
And that’s what our rbenv---version
file does!