background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount
Eric.London's picture

In this article, I'll show an example of how to implement a Subversion pre-commit hook to integrate with Drupal. Pre-commit hooks can be used to execute any arbitrary code, such as deployment procedures, archiving databases, etc. For this example, I will show how to check for the creation of a subversion tag and archive the database.

To get started, I created a local subversion repository.

# create folder for subversion repositories
$ mkdir /var/subversion

# create the subversion repository
$ svnadmin create /var/subversion/project

Upon creating a new local svn repository, a hooks directory will be created. Example: /var/subversion/project/hooks

Inside this directory will be a bunch of sample scripts ending in ".tmpl" which contain example hook scripts. Here are the contents of the example pre-commit hook without comments:

$ cat pre-commit.tmpl | egrep -iv "(^#|^$)"
REPOS="$1"
TXN="$2"
SVNLOOK=/usr/bin/svnlook
$SVNLOOK log -t "$TXN" "$REPOS" | \
   grep "[a-zA-Z0-9]" > /dev/null || exit 1
commit-access-control.pl "$REPOS" "$TXN" commit-access-control.cfg || exit 1
exit 0

As noted in the pre-commit.tmpl file, there are 2 arguments being passed to the pre-commit script:

[1] REPOS-PATH   (the path to this repository)
[2] TXN-NAME     (the name of the txn about to be committed)

I created a new file called "pre-commit" and added the following contents:

#!/bin/bash

/var/www/vhosts/project.vm/scripts/svn-pre-commit.php "$1" "$2"

I then made the file executable.

$ chmod ug+w pre-commit

The above script simply passes the arguments to a PHP script contained with the Drupal project.

In my scripts folder (/var/www/vhosts/project.vm/scripts), I created the PHP script "svn-pre-commit.php" with the following contents:

#!/usr/bin/php
<?php

// get args
$repo = $argv[1];

// define path to mysql backups
$mysql_backups_path = '/var/www/vhosts/project.vm/database';

// define path to drupal docroot
$drupal_docroot_path = '/var/www/vhosts/project.vm/htdocs';

// get changed path
// example output:
// A   tags/20110503/
$svn_look = `svnlook changed $repo`;

// define pattern to break apart svnlook changed
$pattern = '/^\s*([A-Za-z])\s*(.*)$/';

// execute preg match
preg_match($pattern, $svn_look, $matches);
$svn_action = $matches[1];
$svn_changed_path = $matches[2];

// check if a tag is being created
if ($svn_action == 'A' && substr($svn_changed_path, 0, 5)=='tags/') {

  // get tag name
  $exploded = explode('/', $svn_changed_path);
  $tag_name = $exploded[1];
 
  // change dir to drupal docroot
  chdir($drupal_docroot_path);

  // backup mysql database using drush
  `/var/www/drush/drush sql-dump > {$mysql_backups_path}/tag_{$tag_name}.sql`;

}

I also made this script executable:

$ chmod ug+w svn-pre-commit.php

Now, assuming that my Drupal site is integrated with the subversion repository, and development is at a point to deploy/create a new tag, I executed the following command to create the subversion tag:

To verify, I entered the directory containing my database dumps to checkout the result:

$ cd /var/www/vhosts/project.vm/database

$ ls -1
tag_beta-0.1.sql

Sometimes there's no escaping hard coded hostnames being stored in your Drupal database. If you have ever setup language translations (i18n) in production, and then deployed the site to a development environment (with a different hostname), you'll know exactly what I am talking about. The first thing you'll notice in your dev site is that all your urls are linking back to the production site, and you may even have a hard time logging in.

If you query the languages table, you'll see the following:

select language, domain
from {languages}

Results:

language	domain
de		http://de.ericlondon.com
en		http://www.ericlondon.com
es		http://es.ericlondon.com
fr		http://fr.ericlondon.com

At first I thought it would make sense to write a module or a hook_update_n() function to resolve this problem when deploying to another environment, but this method seemed rather clunky, repeatedly running the same revision of a module's update script. In the end, I decided to simply write a PHP script to run via Drush. The following code fetches your default language and all records in the language table, loops through them, and updates the hostname records to match your new hostname...

<?php
// ensure this script is being called from Drush ONLY
// ensure HTTP_HOST is being used
if (
  !
is_array($_SERVER['argv'])
  ||
substr($_SERVER['argv'][0], -9) != 'drush.php'
 
|| substr($_SERVER['SCRIPT_FILENAME'], -9) != 'drush.php'
 
|| !strlen($_SERVER['HTTP_HOST'])
) {
 
header('HTTP/1.1 403 Forbidden');
  die(
'Access Denied.');
}

$output = "";

// fetch hostname from globals
$http_host = $_SERVER['HTTP_HOST'];

// ensure http host exists
if (!$http_host || !is_string($http_host)) {
  die(
'Error occurred fetching http host.');
}

// set default http host
$default_http_host = 'www.' . $http_host;

// define default language
$default_language = 'en';

// define SQL to fetch default language
$sql = "
  select *
  from {languages}
  where language = '%s'
"
;

// execute SQL
$default_language_record = db_fetch_object(db_query($sql, $default_language));

// ensure record exists
if (!is_object($default_language_record)) {
  die(
'Error occurred fetching default language.');
}

// update default language record
$default_language_record->domain = 'http://' . $default_http_host;

// set new variable for "language_default"
variable_set('language_default', $default_language_record);
$output .= "Setting language_default variable to: " . $default_language_record->domain . "\n";

// fetch language records
$sql = "
  select *
  from {languages}
"
;
$resource = db_query($sql);
$language_records = array();
while (
$row = db_fetch_object($resource)) {
 
$language_records[] = $row;
}

// correct language records
foreach ($language_records as $key => $value) {

 
// determine language prefix 
 
switch ($value->language) {
    case
$default_language:
     
$prefix = 'www.';
      break;
    default:
     
$prefix = $value->language . '.';
      break;
  }

 
$new_domain = 'http://' . $prefix . $http_host;

 
$sql = "
    update {languages}
    set domain = '%s'
    where language = '%s'
  "
;
 
db_query($sql, $new_domain, $value->language);

 
$output .= "Setting language record for language: \"" . $value->language . "\" to: " . $new_domain . "\n";

}

drupal_flush_all_caches();

$output .= "Executed: drupal_flush_all_caches()\n";

$output .= "DONE.\n";

echo
$output;
?>

I dropped this script (named: fix_env.php) in the root of my Drupal site, and then executed it via Drush using the following shell command:

drush --uri=http://tdb.erl.dev scr fix_env.php

The "--uri" flag allows you to specific a hostname for multi-site environments, and the "scr" flag allows you to specify a php script to execute.

NOTE: I'm in the habit of never using sites/default, because it's a real PITA if you ever try to move your site into a multi-site configuration ^_^

After running the script via Drush, my hostname database records have been corrected to match my development environment...

select language, domain
from {languages}

Results:

language	domain
de		http://de.tdb.erl.dev
en		http://tdb.erl.dev
es		http://es.tdb.erl.dev
fr		http://fr.tdb.erl.dev

There are some great development modules for Drupal (Devel, Coder, Reroute_Email, Demo, etc), but you probably don't want to have them enabled in a production environment. Deployments to production can be simplified by adding a hook_update_N function in your module's .install file. In this function you can take care of administrative functions such as importing views and CCK node type definitions (essentially, anything exportable). In this quick code snippet, I'll show how you can create a module update function to disable your development modules on update.

<?php
// NOTE: see the documentation on hook_update_N for version naming conventions
function MYMODULE_update_6100() {
 
 
// check for production environment hostname
 
if ($_SERVER['HTTP_HOST'] == 'your-production-hostname') {
   
   
// rebuild the module cache
   
module_rebuild_cache();
   
   
// define a list of development modules to disable
   
$modules_disable = array(
     
'reroute_email',
     
'coder',
     
'demo',
     
'performance',
     
'devel_node_access',
     
'devel_generate',
     
'devel_themer',
     
'devel',
    );
   
   
// disable modules
   
module_disable($modules_disable);

  }

}
?>

Now that your module update function is created, you can deploy your file updates to production (preferably using subversion) and run the update.php script, apply your module update, and disable your development modules.

Syndicate content