Make direnv work with sapling

Oliver Nguyen
3 min readDec 21, 2022

--

In the last post, I shared my experience with Sapling, Meta’s new Git client. It requires some workaround with direnv. This post explains the change in detail.

At work, we are using direnv for quickly managing environment variables on our repository. When we cd into a directory, or a subtree, it automatically detects the file .envrc in the current directory (or the nearest parent directory), loads environment variables, and executes predefined commands.

Our typical .envrc:

export PROJECT_ROOT=$(git rev-parse --show-toplevel)
export REL_PATH_FROM_PROJECT_ROOT=$(git rev-parse --show-prefix)

PATH_add $(pwd)

With this file, when we cd to a subtree, it will:

  • Set PROJECT_ROOT to the root of the git repository, of course, our monorepo.
  • Set REL_PATH_FROM_PROJECT_ROOT to the relative path from the root of the git repository.
  • Add the current directory to PATH. It allows executing commands in the current directory by typing command instead of ./command. We usually define a run command, to define some common commands for the current directory.

It works well. Until one day, I want to use sapling!

Make sapling work with direnv

The above .envrc uses git rev-parse --show-top-level to get the root of the git repository. Sapling is a new Git client, it works with Git repositories, but it’s not Git. It doesn’t have git rev-parse command. So, we need to find a way to make it work with direnv.

Hence, I come up with this function:

sl_project_root() {
local pdir=./; # project directory
while [[ ${#pdir} -lt 30 ]] && ! ls "${pdir}.sl" >/dev/null 2>&1; do
pdir="${pdir}../"
done
if [[ ${#pdir} -lt 30 ]] ; then
# shellcheck disable=SC2155
export PROJECT_ROOT=$(realpath "${pdir}") ;
# shellcheck disable=SC2155
export REL_PATH_FROM_PROJECT_ROOT=\
$(python -c "import os; print(os.path.relpath(\"$(pwd)\",\"${pdir}\"))")
fi
}

if [[ -z "${PROJECT_ROOT}" ]]; then
sl_project_root
fi

The first part is to find the nearest .sl directory, which is where sapling stores its local data. When found, the PROJECT_ROOT will be set to the parent directory of .sl.

The second part empowers Python to calculate the relative path from the root of the git repository, with os.path.relpath(). Python is pre-installed on Mac and most Linux distributions, so it’s a good choice.

Put all things together

Another problem is that the above snippet is a bit long. And we have many .envrc files around the monorepo. When the .envrc file is changed, we need to update all of them. And other people is asked to run direnv allow again in every directory with .envrc. It’s the policy of direnv to make sure that the user is aware of the changes. And what if we want to make changes to the snippet in the future? We need to update all of them and make other people annoyed again.

Let’s fix it. So we’ll create a new file called .project_root.sh with the above snippet. And people who want to use sapling just need to add the soft-link of the file to their $PATH. Something like this:

# assume that ~/bin is included in $PATH
ln -s /path/to/repo/.project_root.sh ~/bin/.project_root.sh

Finally, we can update the .envrc file to:

export PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
export REL_PATH_FROM_PROJECT_ROOT=$(git rev-parse --show-prefix 2>/dev/null)
git rev-parse 2>/dev/null || source .project_root.sh

PATH_add $(pwd)

The next time .project_root.sh get updated, people don’t need to do anything as it will automatically load. And only people with sapling will be affected. Other people don’t need to add the soft link. Of course, it’s required that we trust the .project_root.sh file. But it’s our repository anyway.

Read more: My first impressions of Sapling — Meta’s new Git client

Also published at OliverNguyen.io/sapling.direnv

Want to Connect?

Connect with me twitter.com/@_OliverNguyen. I share information about
software development, JavaScript, Go, and other interesting things I learn.

--

--