diff --git a/checkout/drush/provision_git_checkout.drush.inc b/checkout/drush/provision_git_checkout.drush.inc index a0aa1c5..42c8d02 100644 --- a/checkout/drush/provision_git_checkout.drush.inc +++ b/checkout/drush/provision_git_checkout.drush.inc @@ -4,6 +4,10 @@ * Implementation of hook_drush_command(). */ function provision_git_checkout_drush_command() { + + $hooks = provision_git_hooks(); + $available_hooks = implode(', ', array_keys($hooks)); + $items['provision-git-checkout'] = array( 'description' => 'Git checkout a branch or tag in a specified location.', 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, @@ -11,8 +15,10 @@ function provision_git_checkout_drush_command() { 'git_ref' => dt('Parameter for the git checkout command'), ), 'options' => array( - 'new_branch' => "Optional: If the branch doesn't exist, create it. Equivalent to git checkout -b branchname" - ) + 'new_branch' => "Optional: If the branch doesn't exist, create it. Equivalent to git checkout -b branchname", + 'hooks' => "Override the hooks to run after git checkout. Separate with a comma. Available hooks include ". $available_hooks + + ), ); return $items; } @@ -77,6 +83,10 @@ function drush_provision_git_checkout($git_ref = '') { * Implements drush_hook_post_COMMAND(). */ function drush_provision_git_checkout_post_provision_git_checkout() { + + // Run all hooks on updated sites + provision_git_run_git_hooks(); + // Re-verify the site, this corrects the file permission when necessary. $options = array(); $target = d()->uri; diff --git a/checkout/hosting_git_checkout.module b/checkout/hosting_git_checkout.module index 4ff255f..14d07a6 100644 --- a/checkout/hosting_git_checkout.module +++ b/checkout/hosting_git_checkout.module @@ -74,6 +74,73 @@ function hosting_task_git_checkout_form($node) { '#weight' => '-1', '#default_value' => TRUE, ); + $form['skip_hooks'] = array( + '#title' => t('Skip git hooks'), + '#description' => t("Do not run git hooks after running git checkout.") . ' ' . t('Warning: This may result in an unstable site if a database update is required') . '', + '#type' => 'checkbox', + '#weight' => '0', + '#default_value' => FALSE, + '#access' => variable_get('hosting_git_allow_skip_hooks', FALSE), + ); + $form['git_hooks'] = array( + '#title' => t('Git Hooks'), + '#weight' => '1', + '#type' => 'checkboxes', + '#default_value' => $node->git['git_hooks'], + '#access' => variable_get('hosting_git_allow_task_override_hooks', FALSE), + '#options' => hosting_git_get_hook_options(), + '#states' => array( + 'invisible' => array( + ':input[name="parameters[skip_hooks]"]' => array('checked' => TRUE), + ), + ), + '#element_validate' => array( + 'hosting_git_hooks_implode' + ) + ); + + $form['hooks'] = array( + '#type' => 'container', + '#states' => array( + 'invisible' => array( + ':input[name="parameters[skip_hooks]"]' => array('checked' => TRUE), + ), + ), + ); + + $hooks = module_invoke_all('hosting_git_hooks'); + foreach ($hooks as $name => $hook) { + $name = ucfirst($name); + $items[] = " " . $hook['description']; + } + $form['hooks']['hooks_display'] = array( + '#type' => 'item', + '#title' => t('Git Hooks'), + '#markup' => theme('item_list', array('items' => $items)), + '#access' => !variable_get('hosting_git_allow_task_override_hooks', FALSE), + ); return $form; } + +/** + * Element validate for the hooks field. Implode the string. + */ +function hosting_git_hooks_implode($element, &$form_state) { + if (variable_get('hosting_git_allow_task_override_hooks', FALSE)) { + $value = implode(',', $element['#value']); + if (empty($value)) { + $value = 'none'; + } + } + else { + $value = NULL; + } + + // if skip hooks is set, unset values for this element. + if ($form_state['values']['parameters']['skip_hooks']) { + $value = NULL; + } + + form_set_value($element, $value, $form_state); +} \ No newline at end of file diff --git a/drush/Provision/Service/git.php b/drush/Provision/Service/git.php index aaf310d..b916047 100644 --- a/drush/Provision/Service/git.php +++ b/drush/Provision/Service/git.php @@ -19,6 +19,7 @@ class Provision_Service_git extends Provision_Service { $context->setProperty('repo_url'); $context->setProperty('deploy_from_git'); $context->setProperty('git_ref'); + $context->setProperty('git_hooks'); } } diff --git a/drush/provision_git.drush.inc b/drush/provision_git.drush.inc index 37c88ae..f782e48 100644 --- a/drush/provision_git.drush.inc +++ b/drush/provision_git.drush.inc @@ -498,3 +498,136 @@ function provision_git_provision_services() { provision_git_register_autoload(); return array('git' => NULL); } + +/** + * Load all available git hooks. + * @return mixed + */ +function provision_git_hooks() { + $hooks = drush_command_invoke_all('provision_git_hooks'); + return $hooks; +} + +/** + * Implements hook_provision_git_hooks(); + * @return array + */ +function provision_git_provision_git_hooks() { + return array( + 'update' => 'provision-update', + 'cache' => 'provision-flush_cache', + 'registry' => 'provision-rebuild_registry', + 'revert' => 'provision-features_revert_all', + ); +} + +/** + * Advertise what hooks are available to be called after a git pull or checkout. + * + * @return + * An associative array of type => drush command. + * + * @see provision.service.inc + */ +function hook_provision_git_hooks() { + return array( + 'update' => 'provision-update', + ); +} + +/** + * Run all drush commands listed in the 'git_hooks' property of the drush alias. + * + * If run when a platform is active, + */ +function provision_git_run_git_hooks() { + + if (drush_get_option('skip-hooks', FALSE)) { + drush_log('Skipping git hooks, as requested. Be aware the site might need database updates.', 'ok'); + return; + } + + // Run post-git-pull hooks. + $sites = array(); + if (d()->type == 'site') { + $sites[] = d()->name; + } + elseif (d()->type == 'platform') { + + // Get a list of all that use this platform. + $aliases_files = _drush_sitealias_find_alias_files(); + $aliases = array(); + foreach ($aliases_files as $filename) { + if ((@include $filename) === FALSE) { + drush_log(dt('Cannot open alias file "!alias", ignoring.', array('!alias' => realpath($filename))), LogLevel::BOOTSTRAP); + continue; + } + } + + foreach ($aliases as $alias_name => $alias) { + // If the alias is a site and platform is a match, load it into our sites list. + if (isset($alias['context_type']) && $alias['context_type'] == 'site' && $alias['platform'] == d()->name) { + + // Use the URI as the key to prevent duplicate sites. Project aliases get included. + // I'm using URI as the value as well because DevShop writes additional aliases which are copies. We want to be sure to use the Aegir-generated alias name + $sites[$alias['uri']] = $alias['uri']; + } + } + + drush_log(dt('Found %num sites on this platform: %sites', array( + '%num' => count($sites), + '%sites' => implode(', ', $sites), + )), 'ok'); + } + + drush_log(dt('Running git hooks for %num sites.', array( + '%num' => count($sites), + )), 'ok'); + + $hook_commands = provision_git_hooks(); + foreach ($sites as $site) { + // @TODO: Gotta figure out a way to put this in the Git service. + if (is_array(d($site)->git_hooks)) { + + // Allow overriding what hooks to run on the command line. + $hooks = drush_get_option('hooks', d($site)->git_hooks); + if (is_string($hooks)) { + $hooks = explode(',', $hooks); + } + $hooks = array_intersect($hooks, array_keys($hook_commands)); + + if (empty($hooks)) { + drush_log(dt('No hooks to invoke on site %site. Check drush context.', array( + '%site' => $site, + )), 'ok'); + } + elseif (drush_get_option('hooks')) { + drush_log(dt('hooks option detected: Running %num git hooks on site %site: %hooks.', array( + '%num' => count($hooks), + '%hooks' => implode(', ', $hooks), + '%site' => $site, + )), 'ok'); + } + else { + drush_log(dt('Using default git hooks for the site %site: %hooks.', array( + '%num' => count($hooks), + '%hooks' => implode(', ', $hooks), + '%site' => $site, + )), 'ok'); + } + + foreach ($hooks as $hook) { + drush_log(dt('Invoking git hook "%hook" on site %site: %command', array( + '%site' => $site, + '%hook' => $hook, + '%command' => $hook_commands[$hook], + )), 'ok'); + provision_backend_invoke($site, $hook_commands[$hook]); + } + + if (empty(d($site)->git_hooks)) { + drush_log(dt('No hooks to invoke. Check drush context.'), 'ok'); + } + } + } +} \ No newline at end of file diff --git a/hosting_git.admin.inc b/hosting_git.admin.inc index feee185..d39b9f3 100644 --- a/hosting_git.admin.inc +++ b/hosting_git.admin.inc @@ -24,5 +24,19 @@ function hosting_git_settings_form() { '#title' => t('Allow deploying platforms from git repositories.'), '#default_value' => variable_get('hosting_git_allow_deploy_platform', TRUE), ); + $form['hooks'] = array( + '#type' => 'fieldset', + '#title' => t('Git Hooks'), + ); + $form['hooks']['hosting_git_allow_skip_hooks'] = array( + '#type' => 'checkbox', + '#title' => t('Allow users to skip git hooks when running Git Checkout or Git Pull tasks.'), + '#default_value' => variable_get('hosting_git_allow_skip_hooks', FALSE), + ); + $form['hooks']['hosting_git_allow_task_override_hooks'] = array( + '#type' => 'checkbox', + '#title' => t('Allow users to configure what git hooks are run when running Git Checkout or Git Pull tasks.'), + '#default_value' => variable_get('hosting_git_allow_task_override_hooks', FALSE), + ); return system_settings_form($form); } diff --git a/hosting_git.drush.inc b/hosting_git.drush.inc index b1b85b6..e84a69b 100644 --- a/hosting_git.drush.inc +++ b/hosting_git.drush.inc @@ -44,6 +44,14 @@ function drush_hosting_git_pre_hosting_task($task) { } } + // --skip-hooks option. + $task->options['skip-hooks'] = $task->task_args['skip_hooks']; + + // --commands option. We pass a list of drush commands so we don't need a duplicate hook_hosting_git_hooks() in provision. + if (!empty($task->task_args['git_hooks'])) { + $task->options['hooks'] = $task->task_args['git_hooks']; + } + // Force the repository path. // Otherwise git climbs the tree to find a platform dir under git control. $task->options['force_repo_path_as_toplevel'] = TRUE; @@ -71,6 +79,11 @@ function hosting_git_hosting_platform_context_options(&$task) { */ function hosting_git_hosting_site_context_options(&$task) { hosting_git_hosting_platform_context_options($task); + + // Transform the list of selected hooks to pass the command to the context_option. + foreach (array_filter($task->ref->git['git_hooks']) as $hook_name) { + $task->context_options['git_hooks'][] = $hook_name; + } } /** @@ -81,6 +94,7 @@ function hosting_git_drush_context_import($context, &$node) { $node->git['repo_url'] = $context->repo_url; $node->git['git_ref'] = $context->git_ref; $node->git['repo_path'] = $context->repo_path; + $node->git['git_hooks'] = $context->git_hooks; } } @@ -104,3 +118,58 @@ function hosting_git_post_hosting_verify_task($task, $data) { } } } + +/** + * Implements hook_post_hosting_TASK_TYPE_task(). + * + * When git checkout occurs, code may change, so run verify tasks. + */ +function hosting_git_post_hosting_git_checkout_task($task, $data) { + hosting_git_run_verify_tasks($task); +} + +/** + * Implements hook_post_hosting_TASK_TYPE_task(). + * + * When git pull occurs, code may change, so run verify tasks. + */ +function hosting_git_post_hosting_git_pull_task($task, $data) { + hosting_git_run_verify_tasks($task); +} + +/** + * Helper for post-checkout and post-pull tasks to trigger verify tasks for sites and platform. + * @param $task + */ +function hosting_git_run_verify_tasks($task){ + if (isset($task->ref->nid)) { + hosting_add_task($task->ref->nid, 'verify'); + drush_log(dt('Verify task queued for %name', array( + '%name' => $task->ref->title, + )), 'ok'); + + // If task was on a site, queue up a verify for it's platform. + if ($task->ref->type == 'site') { + hosting_add_task($task->ref->platform, 'verify'); + drush_log(dt('Verify task queued for platform %nid', array( + '%nid' => $task->ref->platform, + )), 'ok'); + } + // If task was on a platform, queue up a verify for all sites on the platform. + elseif ($task->ref->type == 'platform') { + + $sites = db_select('hosting_site', s) + ->fields('s', array('nid')) + ->condition('platform', $task->ref->nid) + ->execute() + ->fetchAllKeyed(0, 0); + + foreach ($sites as $site) { + hosting_add_task($site, 'verify'); + drush_log(dt('Verify task queued for site %nid', array( + '%nid' => $site, + )), 'ok'); + } + } + } +} diff --git a/hosting_git.install b/hosting_git.install index b8c141c..567e127 100644 --- a/hosting_git.install +++ b/hosting_git.install @@ -47,6 +47,13 @@ function hosting_git_schema() { 'not null' => TRUE, 'default' => '', ), + 'git_hooks' => array( + 'description' => 'Serialized list of commands to run after a git checkout or git pull.', + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'serialize' => TRUE, + ), ), 'primary key' => array('nid'), ); @@ -73,3 +80,16 @@ function hosting_git_update_7301 () { 'default' => '', )); } + +/** + * Add git_hooks fields. + */ +function hosting_git_update_7303 () { + db_add_field('hosting_git', 'git_hooks', array( + 'description' => 'Serialized list of commands to run after a git checkout or git pull.', + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'serialize' => TRUE, + )); +} diff --git a/hosting_git.module b/hosting_git.module index a8087d7..909cf17 100644 --- a/hosting_git.module +++ b/hosting_git.module @@ -100,7 +100,7 @@ function hosting_git_form_alter(&$form, &$form_state, $form_id) { '#default_value' => isset($node->git['git_ref']) ? $node->git['git_ref'] : 'master', ); } - else { + elseif (!empty($node->git['repo_url'])) { // Display it. $form['git']['repo_url_display'] = array( '#type' => 'item', @@ -131,6 +131,21 @@ function hosting_git_form_alter(&$form, &$form_state, $form_id) { ); } + // Git Hooks field + $options = hosting_git_get_hook_options(); + if (empty($node->nid) && empty($node->git['git_hooks'])) { + $node->git['git_hooks'] = array('update', 'cache'); + } + if (count($options)) { + $form['git']['git_hooks'] = array( + '#type' => 'checkboxes', + '#title' => t('Git Hooks'), + '#options' => $options, + '#description' => t('These hooks will be run after every Git Pull or Git Checkout.'), + '#default_value' => $node->git['git_hooks'], + ); + } + // Default collapse one fieldset based on the current node. if (empty($node->frommakefile['makefile'])) { $form['frommakefile']['#collapsed'] = TRUE; @@ -138,6 +153,12 @@ function hosting_git_form_alter(&$form, &$form_state, $form_id) { elseif (empty($node->git['repo_url'])) { $form['git']['#collapsed'] = TRUE; } + + // Skip Git forms for existing sites without a repo URL. + // Adding one later is currently not supported. + if (isset($node->nid) && $node->verified && empty($node->git['repo_url'])) { + $form['git']['#access'] = FALSE; + } } } @@ -181,6 +202,7 @@ function hosting_git_node_update($node) { 'repo_path' => $node->git['repo_path'], 'repo_docroot' => $node->git['repo_docroot'], 'git_ref' => $node->git['git_ref'], + 'git_hooks' => serialize($node->git['git_hooks']), )) ->execute(); } @@ -209,6 +231,7 @@ function hosting_git_node_load($nodes, $types) { $node->git['git_ref'] = $result->git_ref; $node->git['repo_path'] = $result->repo_path; $node->git['repo_docroot'] = $result->repo_docroot; + $node->git['git_hooks'] = unserialize($result->git_hooks); // If platform repo path is empty, then load the publish_path in it's place. if ( $node->type == 'platform' && empty($node->git['repo_path'])) { @@ -245,6 +268,10 @@ function _hosting_git_node_load_defaults(&$node) { if (!isset($node->git['git_ref'])) { $node->git['git_ref'] = 'master'; } + + if (!isset($node->git['git_hooks'])) { + $node->git['git_hooks'] = array('update', 'cache'); + } } /** @@ -350,3 +377,48 @@ function _hosting_git_site_or_platform_enabled($node) { ) ); } + +/** + * Retrive a list of available Git Hooks ready for a Forms API checkboxes element. + */ +function hosting_git_get_hook_options() { + $options = array(); + $hooks = module_invoke_all('hosting_git_hooks'); + foreach ($hooks as $name => $hook) { + $options[$name] = $hook['description']; + } + return $options; +} + +/** + * Implements hook_hosting_git_hooks() + */ +function hosting_git_hosting_git_hooks() { + return array( + 'update' => array( + 'description' => t('Run database updates'), + ), + 'cache' => array( + 'description' => t('Flush all caches'), + ), + 'registry' => array( + 'description' => t('Rebuild Registry'), + ), + 'revert' => array( + 'description' => t('Revert all features'), + ), + ); +} + +/** + * Return am array of arrays, + * + * @api + */ +function hook_hosting_git_hooks() { + return array( + 'cache' => array( + 'description' => t('Flush all caches'), + ), + ); +} \ No newline at end of file diff --git a/pull/drush/provision_git_pull.drush.inc b/pull/drush/provision_git_pull.drush.inc index ae6bb69..f2ab20d 100644 --- a/pull/drush/provision_git_pull.drush.inc +++ b/pull/drush/provision_git_pull.drush.inc @@ -4,6 +4,10 @@ * Implementation of hook_drush_command(). */ function provision_git_pull_drush_command() { + + $hooks = provision_git_hooks(); + $available_hooks = implode(', ', array_keys($hooks)); + $items['provision-git-pull'] = array( 'description' => 'Executes "git pull".', 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, @@ -11,6 +15,8 @@ function provision_git_pull_drush_command() { 'reset' => 'Resets any working tree changes that would block a rebase. USE WITH CAUTION', 'repo_path' => 'Optional: force a repository path. Defaults to the site or platform dir.', 'force_repo_path_as_toplevel' => 'Stop Git from looking in parent directories. Defaults to FALSE.', + 'skip-hooks' => "Do not run the site's git hooks.", + 'hooks' => "Override the hooks to run after git pull. Separate with a comma. Available hooks include ". $available_hooks ), 'aliases' => array('pull'), ); @@ -60,7 +66,11 @@ function drush_provision_git_pull() { * Implements drush_hook_post_COMMAND(). */ function drush_provision_git_pull_post_provision_git_pull() { - // Re-verify the site, this corrects the file permission when necessary. + + // Run all hooks on updated sites + provision_git_run_git_hooks(); + + // Re-verify the site or platform, this corrects the file permission when necessary. $options = array(); $target = d()->uri; provision_backend_invoke($target, 'provision-verify', array(), $options); diff --git a/pull/hosting_git_pull.module b/pull/hosting_git_pull.module index 877bf1f..52c7fca 100644 --- a/pull/hosting_git_pull.module +++ b/pull/hosting_git_pull.module @@ -114,6 +114,52 @@ function hosting_task_git_pull_form($node) { '#default_value' => TRUE, ); + $form['skip_hooks'] = array( + '#title' => t('Skip git hooks'), + '#description' => t("Do not run git hooks after running git checkout.") . ' ' . t('Warning: This may result in an unstable site if a database update is required') . '', + '#weight' => '0', + '#type' => 'checkbox', + '#weight' => '-1', + '#default_value' => FALSE, + '#access' => variable_get('hosting_git_allow_skip_hooks', FALSE), + ); + $form['git_hooks'] = array( + '#title' => t('Git Hooks'), + '#weight' => '1', + '#type' => 'checkboxes', + '#default_value' => $node->git['git_hooks'], + '#access' => variable_get('hosting_git_allow_task_override_hooks', FALSE), + '#options' => hosting_git_get_hook_options(), + '#states' => array( + 'invisible' => array( + ':input[name="parameters[skip_hooks]"]' => array('checked' => TRUE), + ), + ), + '#element_validate' => array( + 'hosting_git_hooks_implode' + ) + ); + + $form['hooks'] = array( + '#type' => 'container', + '#states' => array( + 'invisible' => array( + ':input[name="parameters[skip_hooks]"]' => array('checked' => TRUE), + ), + ), + ); + + $hooks = module_invoke_all('hosting_git_hooks'); + foreach ($hooks as $name => $hook) { + $name = ucfirst($name); + $items[] = " " . $hook['description']; + } + $form['hooks']['hooks_display'] = array( + '#type' => 'item', + '#title' => t('Git Hooks'), + '#markup' => theme('item_list', array('items' => $items)), + '#access' => !variable_get('hosting_git_allow_task_override_hooks', FALSE), + ); return $form; }