If you regularly work with package managers like NPM, Yarn or Composer you’ve probably forgotten at one time or another to update the packages after checking out another branch or pulling from a remote Git repository. There can be noticeable differences between the required packages of two different branches that mean you need to fetch the relevant files when you switch between them. Similarly when you pull from a remote branch perhaps another developer has updated a package that you now need to fetch in order to continue developing.
Forgetting to run the package manager whenever you run
git checkout or
git pull can cause all sorts of issues. What would be great is if every time we performed one of these Git commands if the package managers could automatically run. Well we can achieve this automation by using Git hooks.
I’ve previously mentioned using Git hooks on this blog to automate git commit messages with an issue number. Git hooks are just small scripts that are triggered by various Git processes and usually reside in your local copy of a repository in the
.git/hooks directory. For security reasons they don’t get directly committed to the repository, but we will look at that a little more in a bit.
For what we want to achieve here we’ll want to use the
post-checkoutis triggered after
git checkoutis successfully completed. It won’t affect the checkout process.
post-mergeis triggered after
git pullis successfully completed on a local repository. Like
post-checkoutit has no affect on the action that triggered it.
Automating the Package Managers
Let’s start by focusing on NPM. We want to run
npm install each time we switch branches in our Git repository. To do this we could add the following to the file
Now if we use
git checkout the
post-checkout hook is going to get triggered and
npm install should automatically run. Just remember to make sure that your hook script is executable, otherwise nothing is going to happen other than a normal switch of branch. We also want this to happen when we pull from a remote branch.
post-checkout won’t be triggered in this case, so we need to add the code above to
.git/hooks/post-merge as well.
Hopefully, this should be making sense to you. We’ve got a couple of simple Bash scripts that are triggered by Git after we successfully checkout or pull a branch; and these scripts will run
npm install every time. We could also add Composer to the mix and make sure we have the required PHP packages for the current branch.
The problem is that this is going to slow things down a bit as we’re going to have to wait for NPM and Composer to install all the packages each time we use
git checkout and
git pull now that we’ve set up the hooks. Automation is great, but not if it slows us down unnecessarily. It would be more ideal if we could only install packages when needed.
One way of achieving this is to check whether the lock files of our package managers have changed; if they have then we know we need to do something, otherwise we can just carry on like normal and not run the package managers. So let’s implement that.
We’re going to need both hooks to perform the same actions and at the moment we have just been copying the functionality between the two Bash scripts. Let’s make things a little more DRY. We’ll put everything into a new script that the two hooks can call. So we’ll create a new
compile.sh executable file in our
.git/hooks folder. It’s going to look like this:-
changedFiles="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"
echo "$changedFiles" | grep -q "$1" && eval "$2"
runOnChange package-lock.json "npm install"
runOnChange composer.lock "composer install"
What’s happening here is that we’ve defined a function called
runOnChange that will only run the passed command if a given file has changed as a result of our
git checkout or
git pull. We utilise
git diff-tree to check for content changes between two commits, in our case the before and after our Git action; this gets piped into Grep using the
-q flag to suppress the output, this is where we check for the file we are interested in. For example, if
package-lock.json is found in the changes we know that we’re going to want to run NPM which is where
eval comes in.
We need to now make sure we call this new script from our hooks. So we need to add the following to both our
Even More Automation
We’ve got our package managers running and installing packages whenever their respective lock files change, but what else could we do with our script? What if we use something like Webpack or Gulp to compile assets? Ideally we’d keep compiled assets out of our repositories and only keep the source files, but that means we’re going to need to remember to run the compilers.
With our new
compile.sh script this should be simple. We can use the
runOnChange method to check if our source files have been modified and then run whatever task manager we’re using. So for example, if we use Webpack we could add this to the script.
runOnChange resources/assets/ "npm run production"
runOnChange will check for any changes within the passed directory and then run Webpack if needed.
Sharing the Hooks
Let’s wrap things up by looking at how we can share the hooks we’ve set up by adding them to our repository.
When we first started to talk about using these hooks I mentioned that they don’t get committed to the repository. This isn’t always ideal. If you’re working as part of a team you want everyone to be able to benefit from this automation. So we need a workaround.
If you think about it, it makes sense that Git prevents the hooks from being committed. Would you really want a collection of hidden scripts (remember they live within the hidden
.git directory) being downloaded to your machine and then run the minute you do something with Git?
So what we need to do is keep the scripts in a separate and more visible folder that can be committed and then we’ll tell Git about them. The approach I take is to put my hook scripts in a folder conveniently called
git-hooks. Then I can tell Git where these are using the following:
git config core.hooksPath git-hooks
Now Git will use
./git-hooks instead of
./.git/hooks as the folder for the hook scripts.
If you go down this route then it might also be nice to add a set up script to the repository so that other devs can get set up quickly (they’re going to need to run the above command in order for the hooks to get picked up).
git config core.hooksPath hooks