Drush command example in php storm

Git Hooks, Round 2: Post-checkout

A different set of settings? For each branch of your repository? Pointing to different databases per branch? Easy.

Photo of Salvador Molina Moreno
Wed, 2015-02-18 10:53By salva

In my last blog entry about git hooks, I talked about the post-merge hook, as a way to automate certain tasks every time a "git pull" command is issued on the command line. Today, I'd like to talk about a different hook that I've found incredibly useful in the past, the post-checkout hook. You can read the precise details about how it works in the git documentation, but for what we care, let's say that in today's example I'm going to use it to trigger a particular task every time I switch to another branch in my repository.

Motivation:

The use case that made me dig into this hook was a (possibly) familiar one to you. I was working on a client site that was going through a complete redesign (structural and visual). While most of the time I was working in the branches related to the mentioned redesign, I was getting frequent requests to make small amends or changes on the live site. At that point, databases between the redesign branches and the live ones, were totally different, and some environment settings had changed as well. I needed a way to quickly switch between them. Yes, I could have separate projects, but in this particular case it wasn't ideal. Also, being able to dynamically generate a different set of environmental settings per branch, is not something you can just disregard. 

Flexible cat picture

With that in mind, I set out to write a really basic script to trigger a drush command to perform the switch of settings and database.

Getting set up:

  1. First things, first. I needed a folder to keep the files for my specific settings. Of course, we don't want them to be tracked by git, so, create a "branch_settings" folder under sites/default directory, and add it to your .gitignore file.
  2. Add a custom "local.settings.php", to your sites/default directory, and .gitignore it too. This will be used as the effective settings file for each branch. While settings.php could work as well, since you shouldn't be tracking it anyway, I'm not using it here because I didn't want it to be replaced on every switch branch.
  3. Include the local.settings.php file in your settings.php file, like this:
// If there's a local settings file, load it.
if (file_exists(__DIR__ . '/local.settings.php')) {
  require_once __DIR__ . '/local.settings.php';
}

Drush command:

Easy, wasn't it? Now, with this in place, let's write the drush command to execute everytime we want to switch to a different set of settings (either manually, or automatically when we switch branches).

/**
 * Implements hook_drush_command().
 */
function mysite_updater_drush_command() {
  return array(
    'switch-branch-settings' => array(
      'description' => "Switches the local.settings.php file with a different settings file from the settings-folder chosen (optional).",
      'arguments' => array(
        'branch' => 'Branch-specific settings to enable (if present).',
      ),
      'options' => array(
        'settings-folder' => 'The folder (within the sites/default path) containing the alternative settings files.',
      ),
      'aliases' => array('sbs'),
    )
  );
}
 
/**
 * Command callback for 'drush switch-branch-settings' command ("drush sbs").
 */
function drush_mysite_updater_switch_branch_settings() {
  if ($args = func_get_args()) {
    // Assemble needed variables.
    $branch = array_shift($args);
    $alternative_settings_folder = 'branch_settings';
    $alternative_settings_folder = (drush_get_option('settings-folder') != NULL) 
        ? drush_get_option('settings-folder') 
        : $alternative_settings_folder;
 
    // Sensible to assume this, but might change per environment.
    $result_settings_file = 'local.settings.php';
 
    drush_print("Checking settings for branch: " . $branch);
 
    // Get the conf path for the current context, and the settings file to be
    // set up as 'local.settings.php'.
    $conf_path = drush_bootstrap_value('conf_path', conf_path(TRUE, TRUE));
    $alternative_conf_file = $conf_path . DIRECTORY_SEPARATOR . $alternative_settings_folder . DIRECTORY_SEPARATOR . $branch . ".settings.php";
 
    // If the alternative file is not found, exit with an error.
    if (!file_exists($alternative_conf_file)) {
      return drush_set_error('', dt("Could not find an alternative Drupal settings.php file at !file.",
        array('!file' => $alternative_conf_file)));
    }
    // Alternative file exists. Replace (or create) the result settings file.
    else {
      $new_conf_file = $conf_path . DIRECTORY_SEPARATOR . $result_settings_file;
 
      // Print message according to existence of the result file.
      if (file_exists($new_conf_file)) {
        drush_log($new_conf_file . " will be replaced with the contents of " . $alternative_conf_file, 'ok');
      }
      else {
        drush_log($new_conf_file . " will be created with the contents of " . $alternative_conf_file, 'notice');
      }
 
      // Try to perform the copy of the settings file.
      if (copy($alternative_conf_file, $new_conf_file)) {
        drush_log($new_conf_file . ' successfully (re)created.', 'success');
      }
      else {
        return drush_set_error('', "Could not perform the settings replacement.");
      }
    }
  }
  else {
    return drush_set_error('', 'No branch supplied.');
  }
}

Quick explanation of the drush command:

  • It accepts one main argument: the branch whose settings we want to make effective.
  • It accepts an optional "settings-folder" parameter. I've described it all this time as the "branch_settings" folder, as that's what the command will use by default, but "flyinghamwitheggs" would work as well, if you like that one.
  • The command will always look inside the mentioned settings folder for a file named "{branch}.settings.php".
  • If the mentioned file exists, your local.settings.php file will be replaced with it. If it doesn't, drush will exist with an error message, and nothing will happen. Admittedly, it could be just an info message, but the point is that it's ok to fail. If you don't have a branch.settings.php file defined for a given branch, drush will just use the current one.

Now for the last part. The post-checkout hook. Code snippet follows:

#!/bin/sh
#
# To disable this hook, rename this file to "post-update.sample".
 
## Set up needed vars.
## DRUPAL ROOT FOLDER (within the repo root).
DRUPAL_ROOT=www
 
## Get the new git branch, and assemble the command to change drupal settings file
NEW_BRANCH=`git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/*\ //'`
COMMAND="drush sbs $NEW_BRANCH"
 
## CD into drupal root, 
cd $DRUPAL_ROOT
eval $COMMAND

Simple, huh? Quick points:

  • As you know, "git branch" will put an asterisk on the current branch. We use "sed" to get that line, and then to remove the asterisk.
  • There's really nothing else to it.
  • Yes, you can consider it a "quick & dirty" solution. I won't feel offended, because that's what it is!

Room for improvement:

Granted: this is another really simple implementation and use of git hooks, but in my opinion, a very useful and effective one for the problem that motivated me to write it. Of course, it can be improved with some strawberries on top of it:

  • For multi-sites, it'd be ideal to grab the directory from which the user performed a branch switch, so the settings replacement happens in the right site.
  • With the code in this post, you'll need to create a particular settings file for any new branch for which you want to incorporate custom settings. The drush command could be improved to generate new files on the fly. In that case, which file should be used as the base one for the new file? The current local.settings.php, or a different, "master" file? Should it spawn the clone of a renamed database for the new branch, too? As you can see, you can make this pretty flexible!
  • I created it for a particular project, yet nothing prevents you from refactoring it a bit, and keeping the drush command as a global command in your system.

On a final note, I'd like to point you at the git documentation again, as there's a minor caveat on the post-checkout implementation above. When git calls that hook, it passes three arguments to it, one of which is used to indicate whether the checkout is a branch checkout, or a file checkout. Although it's not something to really worry about, for this use case, we don't really want to call drush every time the hook is executed, but only when it's executed due to a branch checkout. I didn't bother adding that check back in the day, but since it's so simple to implement, I'll leave that up to you.

Don't forget to share your thoughts with me on twitter.