Index: server/pifr_server.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue_file_review/server/pifr_server.test,v
retrieving revision 1.10
diff -u -r1.10 pifr_server.test
--- server/pifr_server.test	23 Apr 2009 05:49:13 -0000	1.10
+++ server/pifr_server.test	19 Aug 2009 06:33:20 -0000
@@ -376,3 +376,35 @@
     return $batch;
   }
 }
+
+class PIFRServerEnvironmentTestCase extends PIFRServerTestCase {
+
+  /**
+   * Implementation of getInfo().
+   */
+  public static function getInfo() {
+    return array(
+      'name' => t('PIFR environment'),
+      'description' => t('Confirm the various environment management interfaces function properly.'),
+      'group' => t('PIFR'),
+    );
+  }
+
+  /**
+   * Implementation of setUp().
+   */
+  public function setUp() {
+    parent::setUp('pifr', 'chart', 'views', 'pifr_server');
+  }
+
+  /**
+   * Test the environment management interfaces.
+   */
+  protected function testEnvironmentInterface() {
+    $user = $this->drupalCreateUser(array('pifr manage environments'));
+    $this->drupalLogin($user);
+
+    $this->drupalGet('admin/pifr/environment');
+    $this->clickLink(t('Add'));
+  }
+}
Index: server/pifr_server.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue_file_review/server/pifr_server.install,v
retrieving revision 1.28
diff -u -r1.28 pifr_server.install
--- server/pifr_server.install	7 Aug 2009 05:16:25 -0000	1.28
+++ server/pifr_server.install	19 Aug 2009 06:33:19 -0000
@@ -116,6 +116,69 @@
     ),
     'primary key' => array('branch_id'),
   );
+  $schema['pifr_environment'] = array(
+    'description' => t('Store client environment information.'),
+    'fields' => array(
+      'environment_id' => array(
+        'description' => t('Unique environment ID.'),
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'environment_key' => array(
+        'description' => t('Unique environment key.'),
+        'type' => 'varchar',
+        'not null' => TRUE,
+        'length' => 255,
+        'default' => '',
+      ),
+      'description' => array(
+        'description' => t('Description of environment.'),
+        'type' => 'varchar',
+        'not null' => TRUE,
+        'length' => 255,
+        'default' => '',
+      ),
+      'require_pass' => array(
+        'description' => t('Require the environment to pass in order to pass the review.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'client' => array(
+        'description' => t('The client ID(s) to restrict environment to.'),
+        'type' => 'varchar',
+        'not null' => TRUE,
+        'length' => 255,
+        'default' => '',
+      ),
+      'project' => array(
+        'description' => t('The project ID(s) to restrict environment to.'),
+        'type' => 'varchar',
+        'not null' => TRUE,
+        'length' => 255,
+        'default' => '',
+      ),
+      'branch' => array(
+        'description' => t('The branch ID(s) to restrict environment to.'),
+        'type' => 'varchar',
+        'not null' => TRUE,
+        'length' => 255,
+        'default' => '',
+      ),
+      'type' => array(
+        'description' => t('The type(es) of tests(s) to restrict environment to.'),
+        'type' => 'varchar',
+        'not null' => TRUE,
+        'length' => 255,
+        'default' => '',
+      ),
+    ),
+    'indexes' => array(
+      'environment_key' => array('environment_key'),
+    ),
+    'primary key' => array('environment_id'),
+  );
   $schema['pifr_file'] = array(
     'description' => t('Store file test results.'),
     'fields' => array(
Index: server/pifr_server.menu.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue_file_review/server/pifr_server.menu.inc,v
retrieving revision 1.9
diff -u -r1.9 pifr_server.menu.inc
--- server/pifr_server.menu.inc	18 Jul 2009 06:48:18 -0000	1.9
+++ server/pifr_server.menu.inc	19 Aug 2009 06:33:19 -0000
@@ -37,6 +37,40 @@
     'file' => 'pifr_server.test.inc'
   );
 
+  // Environment management: administrative.
+  $items['admin/pifr/environment/add'] = array(
+    'title' => 'Add',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('pifr_server_environment_form'),
+    'access arguments' => array('pifr manage environments'),
+    'file' => 'pifr_server.environment.inc',
+    'type' => MENU_LOCAL_TASK,
+  );
+  $items['admin/pifr/environment/edit/%'] = array(
+    'title' => 'Environment',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('pifr_server_environment_form', 4),
+    'access arguments' => array('pifr manage environments'),
+    'file' => 'pifr_server.environment.inc',
+    'type' => MENU_CALLBACK,
+  );
+  $items['admin/pifr/environment/delete/%'] = array(
+    'title' => 'Environment',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('pifr_server_environment_delete_confirm_form', 4),
+    'access arguments' => array('pifr manage environments'),
+    'file' => 'pifr_server.environment.inc',
+    'type' => MENU_CALLBACK,
+  );
+  $items['admin/pifr/environment/ahah/%/%'] = array(
+    'title' => 'Environment',
+    'page callback' => 'pifr_server_environment_ahah',
+    'page arguments' => array(4, 5),
+    'access arguments' => array('pifr manage environments'),
+    'file' => 'pifr_server.environment.inc',
+    'type' => MENU_CALLBACK,
+  );
+
   // Client management: administrative.
   $items['admin/pifr/server'] = array(
     'title' => 'Server',
Index: server/pifr_server.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue_file_review/server/pifr_server.module,v
retrieving revision 1.43
diff -u -r1.43 pifr_server.module
--- server/pifr_server.module	13 Aug 2009 20:34:06 -0000	1.43
+++ server/pifr_server.module	19 Aug 2009 06:33:20 -0000
@@ -90,6 +90,7 @@
 module_load_include('test.inc', 'pifr_server');
 module_load_include('branch.inc', 'pifr_server');
 module_load_include('client.inc', 'pifr_server');
+module_load_include('environment.inc', 'pifr_server');
 module_load_include('file.inc', 'pifr_server');
 module_load_include('log.inc', 'pifr_server');
 module_load_include('project.inc', 'pifr_server');
@@ -139,7 +140,8 @@
     'pifr add project client',
     'pifr add test client',
     'pifr manage own client',
-    'pifr manage clients'
+    'pifr manage clients',
+    'pifr manage environments',
   );
 }
 
Index: server/views/pifr_server.views_default.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue_file_review/server/views/pifr_server.views_default.inc,v
retrieving revision 1.6
diff -u -r1.6 pifr_server.views_default.inc
--- server/views/pifr_server.views_default.inc	7 Aug 2009 23:01:23 -0000	1.6
+++ server/views/pifr_server.views_default.inc	19 Aug 2009 06:33:20 -0000
@@ -1037,5 +1037,233 @@
 
   $views[$view->name] = $view;
 
+  // ----------------------------------------------------------------
+  // pifr_server_environment
+
+  $view = new view;
+  $view->name = 'pifr_server_environment';
+  $view->description = '';
+  $view->tag = 'pifr_server';
+  $view->view_php = '';
+  $view->base_table = 'pifr_environment';
+  $view->is_cacheable = FALSE;
+  $view->api_version = 2;
+  $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+  $handler = $view->new_display('default', 'Defaults', 'default');
+  $handler->override_option('fields', array(
+    'environment_id' => array(
+      'label' => 'Environment ID',
+      'alter' => array(
+        'alter_text' => 0,
+        'text' => '',
+        'make_link' => 0,
+        'path' => '',
+        'link_class' => '',
+        'alt' => '',
+        'prefix' => '',
+        'suffix' => '',
+        'target' => '',
+        'help' => '',
+        'trim' => 0,
+        'max_length' => '',
+        'word_boundary' => 1,
+        'ellipsis' => 1,
+        'strip_tags' => 0,
+        'html' => 0,
+      ),
+      'empty' => '',
+      'hide_empty' => 0,
+      'empty_zero' => 0,
+      'set_precision' => FALSE,
+      'precision' => 0,
+      'decimal' => '.',
+      'separator' => ',',
+      'prefix' => '',
+      'suffix' => '',
+      'exclude' => 0,
+      'id' => 'environment_id',
+      'table' => 'pifr_environment',
+      'field' => 'environment_id',
+      'relationship' => 'none',
+    ),
+    'environment_key' => array(
+      'label' => 'Environment key',
+      'alter' => array(
+        'alter_text' => 0,
+        'text' => '',
+        'make_link' => 0,
+        'path' => '',
+        'link_class' => '',
+        'alt' => '',
+        'prefix' => '',
+        'suffix' => '',
+        'target' => '',
+        'help' => '',
+        'trim' => 1,
+        'max_length' => '50',
+        'word_boundary' => 1,
+        'ellipsis' => 1,
+        'strip_tags' => 0,
+        'html' => 0,
+      ),
+      'empty' => '',
+      'hide_empty' => 0,
+      'empty_zero' => 0,
+      'exclude' => 0,
+      'id' => 'environment_key',
+      'table' => 'pifr_environment',
+      'field' => 'environment_key',
+      'relationship' => 'none',
+    ),
+    'description' => array(
+      'label' => 'Environment description',
+      'alter' => array(
+        'alter_text' => 0,
+        'text' => '',
+        'make_link' => 0,
+        'path' => '',
+        'link_class' => '',
+        'alt' => '',
+        'prefix' => '',
+        'suffix' => '',
+        'target' => '',
+        'help' => '',
+        'trim' => 1,
+        'max_length' => '150',
+        'word_boundary' => 1,
+        'ellipsis' => 1,
+        'strip_tags' => 0,
+        'html' => 0,
+      ),
+      'empty' => '',
+      'hide_empty' => 0,
+      'empty_zero' => 0,
+      'exclude' => 0,
+      'id' => 'description',
+      'table' => 'pifr_environment',
+      'field' => 'description',
+      'relationship' => 'none',
+    ),
+    'require_pass' => array(
+      'label' => 'Require pass',
+      'alter' => array(
+        'alter_text' => 0,
+        'text' => '',
+        'make_link' => 0,
+        'path' => '',
+        'link_class' => '',
+        'alt' => '',
+        'prefix' => '',
+        'suffix' => '',
+        'target' => '',
+        'help' => '',
+        'trim' => 0,
+        'max_length' => '',
+        'word_boundary' => 1,
+        'ellipsis' => 1,
+        'strip_tags' => 0,
+        'html' => 0,
+      ),
+      'empty' => '',
+      'hide_empty' => 0,
+      'empty_zero' => 0,
+      'type' => 'yes-no',
+      'not' => 0,
+      'exclude' => 0,
+      'id' => 'require_pass',
+      'table' => 'pifr_environment',
+      'field' => 'require_pass',
+      'relationship' => 'none',
+    ),
+    'nothing' => array(
+      'label' => 'Operations',
+      'alter' => array(
+        'text' => '<a href="/admin/pifr/environment/edit/[environment_id]">edit</a> | <a href="/admin/pifr/environment/delete/[environment_id]">delete</a>',
+        'make_link' => 0,
+        'path' => '',
+        'link_class' => '',
+        'alt' => '',
+        'prefix' => '',
+        'suffix' => '',
+        'target' => '',
+        'help' => '',
+        'trim' => 0,
+        'max_length' => '',
+        'word_boundary' => 1,
+        'ellipsis' => 1,
+        'strip_tags' => 0,
+        'html' => 0,
+      ),
+      'empty' => '',
+      'hide_empty' => 0,
+      'empty_zero' => 0,
+      'exclude' => 0,
+      'id' => 'nothing',
+      'table' => 'views',
+      'field' => 'nothing',
+      'relationship' => 'none',
+      'override' => array(
+        'button' => 'Override',
+      ),
+    ),
+  ));
+  $handler->override_option('access', array(
+    'type' => 'perm',
+    'perm' => 'pifr manage environments',
+  ));
+  $handler->override_option('cache', array(
+    'type' => 'none',
+  ));
+  $handler->override_option('title', 'Environment ');
+  $handler->override_option('empty', '<em>No environments.</em>');
+  $handler->override_option('empty_format', '1');
+  $handler->override_option('style_plugin', 'table');
+  $handler->override_option('style_options', array(
+    'grouping' => '',
+    'override' => 1,
+    'sticky' => 1,
+    'order' => 'asc',
+    'columns' => array(
+      'environment_id' => 'environment_id',
+      'description' => 'description',
+      'environment_key' => 'environment_key',
+      'require_pass' => 'require_pass',
+    ),
+    'info' => array(
+      'environment_id' => array(
+        'sortable' => 1,
+        'separator' => '',
+      ),
+      'description' => array(
+        'separator' => '',
+      ),
+      'environment_key' => array(
+        'separator' => '',
+      ),
+      'require_pass' => array(
+        'sortable' => 1,
+        'separator' => '',
+      ),
+    ),
+    'default' => 'environment_id',
+  ));
+  $handler = $view->new_display('page', 'Environment', 'page_1');
+  $handler->override_option('path', 'admin/pifr/environment/list');
+  $handler->override_option('menu', array(
+    'type' => 'default tab',
+    'title' => 'List',
+    'description' => '',
+    'weight' => '-10',
+    'name' => 'navigation',
+  ));
+  $handler->override_option('tab_options', array(
+    'type' => 'normal',
+    'title' => 'Environment',
+    'description' => '',
+    'weight' => '0',
+  ));
+
+  $views[$view->name] = $view;
+
   return $views;
 }
Index: server/views/pifr_server.views.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue_file_review/server/views/pifr_server.views.inc,v
retrieving revision 1.10
diff -u -r1.10 pifr_server.views.inc
--- server/views/pifr_server.views.inc	18 Jul 2009 07:06:44 -0000	1.10
+++ server/views/pifr_server.views.inc	19 Aug 2009 06:33:20 -0000
@@ -318,6 +318,82 @@
       'label' => t('PIFR test'),
     ),
   );
+  
+  // ----------------------------------------------------------------
+  // pifr_environment table -- basic table information.
+
+  $data['pifr_environment']['table']['group']  = t('PIFR environment');
+
+  $data['pifr_environment']['table']['base'] = array(
+    'field' => 'environment_id',
+    'title' => t('PIFR environment'),
+    'help' => t('Represents a client environment.'),
+    'weight' => -10,
+  );
+
+  $data['pifr_environment']['table']['join'] = array(
+    'pifr_client' => array(
+      'left_field' => 'environment_id',
+      'field' => 'environment_id',
+    ),
+    'pifr_test' => array(
+      'left_field' => 'environment_id',
+      'field' => 'environment_id',
+    ),
+  );
+
+  $data['pifr_environment']['environment_id'] = array(
+    'title' => t('Environment ID'),
+    'help' => t('Unique indentifier given to each environment.'),
+    'field' => array(
+      'handler' => 'views_handler_field_numeric',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_numeric',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+
+  $data['pifr_environment']['environment_key'] = array(
+    'title' => t('Environment key'),
+    'help' => t('The unique environment key used communicate the environment to the client.'),
+    'field' => array(
+      'handler' => 'views_handler_field',
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+  );
+
+  $data['pifr_environment']['description'] = array(
+    'title' => t('Description'),
+    'help' => t('A clear description of the environment.'),
+    'field' => array(
+      'handler' => 'views_handler_field',
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+  );
+
+  $data['pifr_environment']['require_pass'] = array(
+    'title' => t('Require pass'),
+    'help' => t('Require the environment to pass in order for the test to pass.'),
+    'field' => array(
+     'handler' => 'views_handler_field_boolean',
+     'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_boolean_operator',
+      'label' => t('Require pass'),
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
 
   // ----------------------------------------------------------------
   // pifr_branch table -- basic table information.
Index: server/pifr_server.environment.inc
===================================================================
RCS file: server/pifr_server.environment.inc
diff -N server/pifr_server.environment.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ server/pifr_server.environment.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,416 @@
+<?php
+// $Id: pifr_server.admin.inc,v 1.10 2009/04/19 09:19:55 boombatower Exp $
+/**
+ * @file
+ * Provide environment management and convience functions.
+ *
+ * Copyright 2008-2009 by Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
+
+/**
+ * Environment form.
+ */
+function pifr_server_environment_form(&$form_state, $environment_id = NULL) {
+  if ($environment_id) {
+    $environment = pifr_server_environment_get($environment_id);
+    foreach (array('client', 'project', 'branch') as $key) {
+      $environment[$key] = pifr_server_environment_format($key, $environment[$key]);
+    }
+  }
+  else {
+    $environment = array(
+      'environment_key' => '',
+      'description' => '',
+      'require_pass' => FALSE,
+      'client' => '',
+      'project' => '',
+      'branch' => '',
+      'type' => '',
+    );
+  }
+
+  $form = array();
+
+  $form['environment_id'] = array(
+    '#type' => 'value',
+    '#value' => $environment_id,
+  );
+
+  $form['environment'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Environment'),
+  );
+  $form['environment']['environment_key'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Key'),
+    '#description' => t('Machine readable key used to distinguish the environment on the client.'),
+    '#default_value' => $environment['environment_key'],
+    '#required' => TRUE,
+  );
+  $form['environment']['description'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Description'),
+    '#description' => t('Description of the environment.'),
+    '#default_value' => $environment['description'],
+  );
+  $form['environment']['require_pass'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Require pass'),
+    '#description' => t('Require the environment to pass in order for the test to be marked as a success..'),
+    '#default_value' => $environment['require_pass'],
+  );
+
+  $form['environment']['condition'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Conditions'),
+    '#description' => t('If you leave a condition blank it will be considered a wildcard. ' .
+                        'Separate multiple values with a comma which will be treated as an OR condition.'),
+  );
+  $form['environment']['condition']['client'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Client ID(s)'),
+    '#description' => t('The client ID(s) to restrict environment to.'),
+    '#default_value' => $environment['client'],
+    '#autocomplete_path' => 'admin/pifr/environment/ahah/client',
+  );
+  $form['environment']['condition']['project'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Project(s)'),
+    '#description' => t('The project(s) to restrict environment to.'),
+    '#default_value' => $environment['project'],
+    '#autocomplete_path' => 'admin/pifr/environment/ahah/project',
+  );
+  $form['environment']['condition']['branch'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Branch(s)'),
+    '#description' => t('The branch(es) to restrict environment to.'),
+    '#default_value' => $environment['branch'],
+    '#autocomplete_path' => 'admin/pifr/environment/ahah/branch',
+  );
+  $form['environment']['condition']['type'] = array(
+    '#type' => 'select',
+    '#title' => t('Type(s)'),
+    '#description' => t('The type(s) of test(s) to restrict environment to.'),
+    '#default_value' => $environment['type'],
+    '#multiple' => TRUE,
+    '#options' => array(
+      PIFR_SERVER_TEST_TYPE_CLIENT,
+      PIFR_SERVER_TEST_TYPE_BRANCH,
+      PIFR_SERVER_TEST_TYPE_FILE,
+    ),
+  );
+  $form['environment']['condition']['type']['#options'] = drupal_map_assoc($form['environment']['condition']['type']['#options'], 'pifr_server_test_type');
+
+  $form['op'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Validate environment.
+ */
+function pifr_server_environment_form_validate($form, &$form_state) {
+  if (strpos($form_state['values']['environment_key'], ' ') !== FALSE) {
+    form_set_error('environment_key', t('Environment key must not contain spaces.'));
+  }
+
+  // Ensure that conditions are valid.
+  if (pifr_server_environment_lookup('client', $form_state['values']['client']) === FALSE) {
+    form_set_error('client', t('Invalid client ID(s).'));
+  }
+  if (pifr_server_environment_lookup('project', $form_state['values']['project']) === FALSE) {
+    form_set_error('project', t('Invalid project(s).'));
+  }
+  if (pifr_server_environment_lookup('branch', $form_state['values']['branch']) === FALSE) {
+    form_set_error('branch', t('Invalid branch(s).'));
+  }
+}
+
+/**
+ * Save environment.
+ */
+function pifr_server_environment_form_submit($form, &$form_state) {
+  foreach (array('client', 'project', 'branch') as $key) {
+    $form_state['values'][$key] = pifr_server_environment_lookup($key, $form_state['values'][$key]);
+  }
+
+  pifr_server_environment_save($form_state['values']);
+  drupal_set_message('Environment saved successfully.');
+
+  $form_state['redirect'] = 'admin/pifr/environment';
+}
+
+/**
+ * Provide suggestions for environment conditions via AHAH.
+ *
+ * @param string $type Type of suggestions being requested.
+ * @param string $query Query string to find suggestions for.
+ */
+function pifr_server_environment_ahah($type, $query) {
+  if (!in_array($type, array('client', 'project', 'branch'))) {
+    return;
+  }
+
+  $parts = array_map('trim', explode(',', $query));
+  $query = array_pop($parts);
+  $prefix = implode(', ', $parts);
+  $prefix = $prefix ? $prefix . ', ' : '';
+
+  if ($type == 'client') {
+    $result = db_query('SELECT client_id
+                        FROM {pifr_client}
+                        WHERE client_id LIKE %d
+                        ORDER BY client_id
+                        LIMIT 10', $query);
+  }
+  elseif ($type == 'project') {
+    $result = db_query("SELECT name
+                        FROM {pifr_project}
+                        WHERE name LIKE '%%%s%%'
+                        ORDER BY name
+                        LIMIT 10", $query);
+  }
+  elseif ($type == 'branch') {
+    $result = db_query("SELECT identifier
+                        FROM {pifr_branch}
+                        WHERE identifier LIKE '%%%s%%'
+                        ORDER BY identifier
+                        LIMIT 10", $query);
+  }
+
+  $suggestions = array();
+  while ($suggestion = db_result($result)) {
+    $suggestions[$prefix . $suggestion] = $suggestion;
+  }
+  drupal_json($suggestions);
+}
+
+/**
+ * Lookup IDs from input string.
+ *
+ * @param string $type Type of IDs being requested.
+ * @param string $string Input string.
+ * @return array List of matching IDs, or FALSE if not all matched.
+ */
+function pifr_server_environment_lookup($type, $string) {
+  if (!$string) {
+    return array();
+  }
+
+  $parts = array_map('trim', explode(',', $string));
+
+  if ($type == 'client') {
+    $result = db_query('SELECT client_id
+                        FROM {pifr_client}
+                        WHERE client_id IN (' . db_placeholders($parts, 'int') . ')
+                        ORDER BY client_id', $parts);
+  }
+  elseif ($type == 'project') {
+    $result = db_query("SELECT project_id
+                        FROM {pifr_project}
+                        WHERE name IN (" . db_placeholders($parts, 'text') . ")
+                        ORDER BY name
+                        LIMIT 10", $parts);
+  }
+  elseif ($type == 'branch') {
+    $result = db_query("SELECT branch_id
+                        FROM {pifr_branch}
+                        WHERE identifier IN (" . db_placeholders($parts, 'text') . ")
+                        ORDER BY identifier
+                        LIMIT 10", $parts);
+  }
+
+  $ids = array();
+  while ($id = db_result($result)) {
+    $ids[] = $id;
+  }
+  return count($ids) == count($parts) ? $ids : FALSE;
+}
+
+/**
+ * Format IDs as input string.
+ *
+ * @param string $type Type of IDs to be formatted.
+ * @param array $data List of Ids to be formated.
+ * @return string Formatted string.
+ */
+function pifr_server_environment_format($type, array $data) {
+  if (!$data) {
+    return '';
+  }
+
+  if ($type == 'client') {
+    $result = db_query('SELECT client_id
+                        FROM {pifr_client}
+                        WHERE client_id IN (' . db_placeholders($data, 'int') . ')
+                        ORDER BY client_id', $data);
+  }
+  elseif ($type == 'project') {
+    $result = db_query("SELECT name
+                        FROM {pifr_project}
+                        WHERE project_id IN (" . db_placeholders($data, 'text') . ")
+                        ORDER BY name
+                        LIMIT 10", $data);
+  }
+  elseif ($type == 'branch') {
+    $result = db_query("SELECT identifier
+                        FROM {pifr_branch}
+                        WHERE branch_id IN (" . db_placeholders($data, 'text') . ")
+                        ORDER BY identifier
+                        LIMIT 10", $data);
+  }
+
+  $values = array();
+  while ($value = db_result($result)) {
+    $values[] = $value;
+  }
+  return implode(', ', $values);
+}
+
+/**
+ * Environment deletion confirmation form.
+ */
+function pifr_server_environment_delete_confirm_form(&$form_state, $environment_id) {
+  $environment = pifr_server_environment_get($environment_id);
+  if (!$environment) {
+    drupal_set_message(t('Invalid environment.'), 'error');
+    return array();
+  }
+
+  $form = array();
+  $form['#redirect'] = 'admin/pifr/environment';
+  $form['environment_id'] = array(
+    '#type' => 'value',
+    '#value' => $environment_id,
+  );
+
+  return confirm_form($form,
+                      t('Are you sure you want to delete the @environment environment?', array('@environment' => $environment['environment_key'])),
+                      $form['#redirect']);
+}
+
+/**
+ * Delete environment.
+ */
+function pifr_server_environment_delete_confirm_form_submit($form, &$form_state) {
+  $environment = pifr_server_environment_get($form_state['values']['environment_id']);
+  db_query('DELETE FROM {pifr_environment}
+            WHERE environment_id = %d', $form_state['values']['environment_id']);
+  drupal_set_message(t('@environment environment deleted', array('@environment' => $environment['environment_key'])));
+}
+
+/**
+ * Get the environment with the specifed ID.
+ *
+ * @param integer $environment_id The environment ID to retrieve.
+ * @return array Environment information.
+ */
+function pifr_server_environment_get($environment_id) {
+  $environment = db_fetch_array(db_query('SELECT *
+                                          FROM {pifr_environment}
+                                          WHERE environment_id = %d', $environment_id));
+
+  $environment['client'] = unserialize($environment['client']);
+  $environment['project'] = unserialize($environment['project']);
+  $environment['branch'] = unserialize($environment['branch']);
+  $environment['type'] = unserialize($environment['type']);
+
+  return $environment;
+}
+
+/**
+ * Get all environments.
+ *
+ * @return array List of environments, each an array of environment
+ *   information.
+ * @see pifr_server_environment_get()
+ */
+function pifr_server_environment_get_all() {
+  $environments = array();
+  $result = db_query('SELECT environment_id FROM {pifr_environment}');
+  while ($environment_id = db_result($result)) {
+    $environments[] = pifr_server_environment_get($environment_id);
+  }
+  return $environments;
+}
+
+/**
+ * Get the relevant environments for the test.
+ *
+ * @param integer $test_id The test ID to get relevant environments for.
+ * @return array List of environment IDs.
+ */
+function pifr_server_environment_get_all_test($test_id) {
+  // Get tree of test related information.
+  $test = pifr_server_test_get($test_id);
+  if ($test['type'] == PIFR_SERVER_TEST_TYPE_CLIENT) {
+    // The only environment a client test should run in is its own.
+    $client = pifr_server_client_get_test($test['test_id']);
+    return array(pifr_server_environment_get($client['environment_id']));
+  }
+  elseif ($test['type'] == PIFR_SERVER_TEST_TYPE_BRANCH) {
+    $branch = pifr_server_branch_get_test($test['test_id']);
+  }
+  elseif ($test['type'] == PIFR_SERVER_TEST_TYPE_FILE) {
+    $file = pifr_server_file_get_test($test['test_id']);
+    $branch = pifr_server_branch_get($file['branch_id']);
+  }
+  $project = pifr_server_project_get($branch['project_id']);
+  $client = pifr_server_client_get($project['client_id']);
+
+  // Cycle through all environments and find the relevant ones.
+  $environments = pifr_server_environment_get_all();
+  $relevant = array();
+  foreach ($environments as $environment) {
+    if ((empty($environment['client']) || in_array($client['client_id'], $environment['client'])) &&
+        (empty($environment['project']) || in_array($project['project_id'], $environment['project'])) &&
+        (empty($environment['branch']) || in_array($branch['branch_id'], $environment['branch'])) &&
+        (empty($environment['type']) || in_array($test['type_id'], $environment['type']))) {
+      $relevant[] = $environment;
+    }
+  }
+  return $relevant;
+}
+
+/**
+ * Save an environment.
+ *
+ * If no environment exists the environment will be inserted, otherwise the
+ * environment will be updated.
+ *
+ * @param array $environment Environment information, see the pifr_environment
+ *   table schema for more information. If the environment ID is blank a new
+ *   environment will be created.
+ * @return Environment information after saving, if inserted it will include
+ *   the new environment ID.
+ */
+function pifr_server_environment_save(array $environment) {
+  if (empty($environment['environment_id'])) {
+    db_query("INSERT INTO {pifr_environment} (environment_key, description, require_pass, client, project, branch, type)
+              VALUES ('%s', '%s', %d, '%s', '%s', '%s', '%s')",
+              $environment['environment_key'], $environment['description'],
+              $environment['require_pass'], serialize($environment['client']),
+              serialize($environment['project']), serialize($environment['branch']),
+              serialize($environment['type']));
+    $environment['environment_id'] = db_last_insert_id('pifr_environment', 'environment_id');
+  }
+  else {
+    db_query("UPDATE {pifr_environment}
+              SET environment_key = '%s',
+              description = '%s',
+              require_pass = %d,
+              client = '%s',
+              project = '%s',
+              branch = '%s',
+              type = '%s'
+              WHERE environment_id = %d",
+              $environment['environment_key'], $environment['description'],
+              $environment['require_pass'], serialize($environment['client']),
+              serialize($environment['project']), serialize($environment['branch']),
+              serialize($environment['type']), $environment['environment_id']);
+  }
+  return $environment;
+}
