pre-commit Environment Issue in SourceTree

In the previous post, I mentioned pre-commit as a powerful tool to lint and format in a project.

It had worked seamlessly for me until I committed code using SourceTree. Just to clarify, I predominantly use git on terminal. I only use a GUI app such as SourceTree to view the diff, or to stage selective chunks in a file (which is a bit difficult to achieve when using terminal). Therefore, the issue went unnoticed during my usual workflow.

SourceTree and Ruby environment

The problem happened when pre-commit attempted to install Rubocop for Ruby linting, as highlighted in the error log from SourceTree.

Figure 1. Error log encountered when committing code in SourceTree.

Figure 1. Error log encountered when committing code in SourceTree.

The log revealed that it was using the System Ruby (version 2.6). This is unexpected as I manage Ruby versions with rbenv. Running which ruby on terminal showed a different path: /Users/thuyen/.rbenv/shims/ruby.

Investigating the PATH

My very first doubt was that the PATH variable might not be correctly configured. To verify, I modified the pre-commit hook script (located at .git/hooks/pre-commit) to print the PATH value and the resolved paths of ruby and python for debugging purposes.

#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03

echo $PATH                 # <--- INSERTED
which ruby && which python # <--- INSERTED

...

Figure 2. Echoing the PATH value along with the resolved paths of ruby and python.

Figure 2. Echoing the PATH value along with the resolved paths of ruby and python.

Meanwhile, the PATH we see when running on terminal (ie. creating a shell session) is as follows:

/opt/homebrew/opt/[email protected]/libexec/bin:/opt/bin:/Users/thuyen/.rbenv/shims:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Users/thuyen/.rugby/clt

Comparing the two PATH values, we noticed that the order of /Users/thuyen/.rbenv/shims and /usr/bin was reversed in SourceTree, causing ruby to resolve to /usr/bin/ruby.

Resolving the issue

Examining the log, we found that /usr/bin/ follows /usr/local/bin in the PATH. This gives us a hint to create a symlink in /usr/local/bin/ruby that points to the rbenv managed version.

$ ln -s $(which ruby) /usr/local/bin/ruby

As Ruby related flow heavily relies on gem and bundle. It’s strongly recommended to do the same for these programs.

$ ln -s $(which gem) /usr/local/bin/gem
$ ln -s $(which bundle) /usr/local/bin/bundle

Now the hook works perfectly in SourceTree.

Discussion

Similar issues may arise with other scripting languages included in macOS, such as Python and Perl. In my case, pre-commit worked perfectly fine with Python, mainly because it executes the program python which is not under /usr/bin. However, you might see unexpected behavior when using python3 because it’s present in /usr/bin, pointing to the System Python. To deal with this scenario, I’d also recommend creating a symlink /usr/local/bin/python3 to the version you’re using (managed by Homebrew, in my case).

One more thing I want to point out is how the PATH was handled in SourceTree. Typically, when starting a macOS app (but not from terminal), the app doesn’t inherit environment variables from a shell session. In other words, custom variables defined in ~/.zshrc (if your default shell is zsh, for example) will not included upon app launch. The question is how come /opt/homebrew/opt/[email protected]/libexec/bin appears in the PATH. SourceTree seems to modify the PATH environment variable. My hypothesis is that SourceTree initializes a shell session just for the sake of reading & updating PATH. You can test hypothesis by adding the following to ~/.zshrc and restarting SourceTree.

echo $(date +%s) load $0 >> ~/Downloads/trace

The below entry in the trace should confirm that ~/.zshrc was sourced during app launch.

1707125068 load /Users/thuyen/.zshrc