Make sapling work with direnv

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 details.

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 execute 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
  if [[ ${#pdir} -lt 30 ]] ; then
    # shellcheck disable=SC2155
    export PROJECT_ROOT=$(realpath "${pdir}") ;
    # shellcheck disable=SC2155
      $(python -c "import os; print(os.path.relpath(\"$(pwd)\",\"${pdir}\"))")

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

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. 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 change. And what if we want to make change 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 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/ ~/bin/

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

PATH_add $(pwd)

The next time get updated, people don’t need to do anything as it will be automatically loaded. 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 file. But it’s our repository anyway.

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


I'm Oliver Nguyen. A software maker working mostly in Go and JavaScript. I enjoy learning and seeing a better version of myself each day. Occasionally spin off new open source projects. Share knowledge and thoughts during my journey. Connect with me on , , , and .

Back Back