You are here

class GitDirtyTreeSensorPlugin in Monitoring 8

Monitors the git repository for dirty files.

@SensorPlugin( id = "monitoring_git_dirty_tree", label = @Translation("Git Dirty Tree"), description = @Translation("Monitors the git repository for dirty files."), addable = FALSE )

Tracks both changed and untracked files. Supports git submodules and alerts if them are not initialized. Also checks branches.

Limitations:

  • Does not check tag.

Hierarchy

Expanded class hierarchy of GitDirtyTreeSensorPlugin

File

src/Plugin/monitoring/SensorPlugin/GitDirtyTreeSensorPlugin.php, line 31
Contains \Drupal\monitoring\Plugin\monitoring\SensorPlugin\GitDirtyTreeSensorPlugin.

Namespace

Drupal\monitoring\Plugin\monitoring\SensorPlugin
View source
class GitDirtyTreeSensorPlugin extends SensorPluginBase implements ExtendedInfoSensorPluginInterface {

  /**
   * The status_cmd command output.
   *
   * @var array
   */
  protected $status;

  /**
   * The ahead_cmd command output.
   *
   * @var array
   */
  protected $distance;

  /**
   * The actual_branch_cmd command output.
   *
   * @var array
   */
  protected $actualBranch;

  /**
   * The submodules_cmd command output.
   *
   * @var array
   */
  protected $submodules;

  /**
   * {@inheritdoc}
   */
  public function runSensor(SensorResultInterface $result) {

    // For security reasons, some hostings disable exec() related functions.
    // Since they fail silently, we challenge exec() and check if the result
    // matches the expectations.
    if (exec('echo "enabled"') != 'enabled') {
      $result
        ->addStatusMessage('The function exec() is disabled. You need to enable it.');
      $result
        ->setStatus(SensorResultInterface::STATUS_CRITICAL);
      return;
    }

    // Run commands.
    $branch_control = $this->sensorConfig
      ->getSetting('check_branch');
    if ($branch_control) {
      $branch = $this
        ->runSensorCommand($result, 'actual_branch_cmd');
      $this->actualBranch = $branch[0];
    }
    $this->status = $this
      ->runSensorCommand($result, 'status_cmd');
    $this->distance = $this
      ->runSensorCommand($result, 'ahead_cmd');
    $this->submodules = $this
      ->runSensorCommand($result, 'submodules_cmd');
    $wrong_submodules = [];
    foreach ($this->submodules as $submodule) {
      $prefix = substr($submodule, 0, 1);
      if ($prefix == '-' || $prefix == '+') {
        $wrong_submodules[] = $submodule;
      }
    }
    $is_expected_branch = $this->actualBranch === $this->sensorConfig
      ->getSetting('expected_branch');
    if ($this->status || $this->distance || !$is_expected_branch && $branch_control || $wrong_submodules) {

      // Critical situations.
      if ($this->status) {
        $result
          ->addStatusMessage('@num_files files in unexpected state: @files', array(
          '@num_files' => count($this->status),
          '@files' => $this
            ->getShortFileList($this->status),
        ));
        $result
          ->setStatus(SensorResultInterface::STATUS_CRITICAL);
      }
      if ($wrong_submodules) {
        $result
          ->addStatusMessage('@num_submodules submodules in unexpected state: @submodules', array(
          '@num_submodules' => count($wrong_submodules),
          '@submodules' => $this
            ->getShortFileList($wrong_submodules),
        ));
        $result
          ->setStatus(SensorResultInterface::STATUS_CRITICAL);
      }

      // Warnings.
      if ($this->distance) {
        $result
          ->addStatusMessage('Branch is @distance ahead of origin', array(
          '@distance' => count($this->distance),
        ));
        if ($result
          ->getStatus() != SensorResultInterface::STATUS_CRITICAL) {
          $result
            ->setStatus(SensorResultInterface::STATUS_WARNING);
        }
      }
      if (!$is_expected_branch && $branch_control) {
        $result
          ->addStatusMessage('Active branch @actual_branch, expected @expected_branch', array(
          '@actual_branch' => $this->actualBranch,
          '@expected_branch' => $this->sensorConfig
            ->getSetting('expected_branch'),
        ));
        if ($result
          ->getStatus() != SensorResultInterface::STATUS_CRITICAL) {
          $result
            ->setStatus(SensorResultInterface::STATUS_WARNING);
        }
      }
    }
    else {
      $result
        ->addStatusMessage('Git repository clean');
      $result
        ->setStatus(SensorResultInterface::STATUS_OK);
    }
  }

  /**
   * Returns a shortened file list for the status message.
   *
   * @param string $input
   *   Result from running the git command.
   * @param int $max_files
   *   Limit the number of files returned.
   * @param int $max_length
   *   Limit the length of the path to the file.
   *
   * @return string
   *   File names from $output.
   */
  protected function getShortFileList($input, $max_files = 2, $max_length = 50) {
    $output = array();

    // Remove unnecessary whitespace.
    foreach (array_slice($input, 0, $max_files) as $line) {

      // Separate type of modification and path to file.
      $parts = explode(' ', $line, 2);
      if (strlen($parts[1]) > $max_length) {

        // Put together type of modification and path to file limited by
        // $pathLength.
        $output[] = $parts[0] . ' …' . substr($parts[1], -$max_length);
      }
      else {

        // Return whole line if path is shorter then $pathLength.
        $output[] = $line;
      }
    }
    return implode(', ', $output);
  }

  /**
   * {@inheritdoc}
   */
  public function resultVerbose(SensorResultInterface $result) {
    $output = [];
    $branch_control = $this->sensorConfig
      ->getSetting('check_branch');
    if ($branch_control) {
      $output['check_branch'] = array(
        '#type' => 'fieldset',
        '#title' => t('Check branch'),
        '#attributes' => array(),
      );
      $output['check_branch']['cmd'] = array(
        '#type' => 'item',
        '#title' => t('Command'),
        '#markup' => $this
          ->buildCommand('actual_branch_cmd'),
      );
      $output['check_branch']['output'] = array(
        '#type' => 'item',
        '#title' => t('Output'),
        '#markup' => $this->actualBranch,
        '#description' => t('Shows the current branch.'),
        '#description_display' => 'after',
      );
    }
    $output['ahead'] = array(
      '#type' => 'fieldset',
      '#title' => t('Ahead'),
      '#attributes' => array(),
    );
    $output['ahead']['cmd'] = array(
      '#type' => 'item',
      '#title' => t('Command'),
      '#markup' => $this
        ->buildCommand('ahead_cmd'),
    );
    $output['ahead']['output'] = array(
      '#type' => 'item',
      '#title' => t('Output'),
      '#markup' => '<pre>' . implode("\n", $this->distance) . '</pre>',
      '#description' => t('Shows local commits that have not been pushed.'),
      '#description_display' => 'after',
    );
    $output['status'] = array(
      '#type' => 'fieldset',
      '#title' => t('Status'),
      '#attributes' => array(),
    );
    $output['status']['cmd'] = array(
      '#type' => 'item',
      '#title' => t('Command'),
      '#markup' => $this
        ->buildCommand('status_cmd'),
    );
    $output['status']['output'] = array(
      '#type' => 'item',
      '#title' => t('Output'),
      '#markup' => '<pre>' . implode("\n", $this->status) . '</pre>',
      '#description' => t('Shows uncommitted, changed and deleted files.'),
      '#description_display' => 'after',
    );
    $output['submodules'] = array(
      '#type' => 'fieldset',
      '#title' => t('Submodules'),
      '#attributes' => array(),
    );
    $output['submodules']['cmd'] = array(
      '#type' => 'item',
      '#title' => t('Command'),
      '#markup' => $this
        ->buildCommand('submodules_cmd'),
    );
    $output['submodules']['output'] = array(
      '#type' => 'item',
      '#title' => t('Output'),
      '#markup' => '<pre>' . implode("\n", $this->submodules) . '</pre>',
      '#description' => t('Run "git submodule init" to initialize missing submodules ("-" prefix) or "git submodule update" to update submodules to the correct commit ("+" prefix).'),
      '#description_display' => 'after',
    );
    return $output;
  }

  /**
   * Build the command to be passed into shell_exec().
   *
   * @param string $cmd
   *   Command we want to run.
   *
   * @return string
   *   Shell command.
   */
  protected function buildCommand($cmd) {
    $repo_path = DRUPAL_ROOT . '/' . $this->sensorConfig
      ->getSetting('repo_path');
    $cmd = $this->sensorConfig
      ->getSetting($cmd);
    return 'cd ' . escapeshellarg($repo_path) . "\n{$cmd}  2>&1";
  }

  /**
   * Run the command and set the status message and the status to the result.
   *
   * @param \Drupal\monitoring\Result\SensorResultInterface $result
   *   Sensor result object.
   * @param string $cmd
   *   Command we want to run.
   *
   * @return array
   *   Output of executing the Shell command.
   */
  protected function runSensorCommand(SensorResultInterface &$result, $cmd) {
    $exit_code = 0;
    $command = $this
      ->buildCommand($cmd);
    exec($command, $output, $exit_code);
    if ($exit_code > 0) {
      $result
        ->addStatusMessage('Non-zero exit code @exit_code for command @command', array(
        '@exit_code' => $exit_code,
        '@command' => $command,
      ));
      $result
        ->setStatus(SensorResultInterface::STATUS_CRITICAL);
    }
    return $output;
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);
    $form['repo_path'] = array(
      '#type' => 'textfield',
      '#default_value' => $this->sensorConfig
        ->getSetting('repo_path'),
      '#title' => t('Repository path'),
      '#description' => t('Path to the Git repository relative to the Drupal root directory.'),
    );
    $branches = $this
      ->runCommand('branches_cmd', t('Failing to get Git branches, Git might not be available.'));
    $expected_branch = $this->sensorConfig
      ->getSetting('expected_branch');
    if (empty($expected_branch)) {
      $expected_branch = $this
        ->runCommand('actual_branch_cmd', t('Failing to get the actual branch, Git might not be available.'));
    }
    $options = array();
    foreach ($branches as $branch) {
      $options[$branch] = $branch;
    }
    $form['check_branch'] = array(
      '#type' => 'checkbox',
      '#default_value' => $this->sensorConfig
        ->getSetting('check_branch'),
      '#title' => t('Branch control'),
      '#description' => t('Check if the current branch is different from the selected.'),
      '#disabled' => !$options,
    );
    $form['expected_branch'] = array(
      '#type' => 'select',
      '#default_value' => $expected_branch,
      '#maxlength' => 255,
      '#empty_option' => t('- Select -'),
      '#options' => $options,
      '#title' => t('Expected active branch'),
      '#description' => t('The branch that is going to be checked out.'),
      '#states' => array(
        // Hide the branch selector when the check_branch checkbox is disabled.
        'invisible' => array(
          ':input[name="settings[check_branch]"]' => array(
            'checked' => FALSE,
          ),
        ),
      ),
    );
    return $form;
  }

  /**
   * Run a command providing an error message.
   *
   * @param string $cmd
   *   Command we want to run.
   * @param string $error
   *   Error message to show when failing.
   *
   * @return array
   *   Output of executing the Shell command.
   */
  private function runCommand($cmd, $error) {
    $exit_code = 0;
    exec($this
      ->buildCommand($cmd), $output, $exit_code);
    if ($exit_code > 0) {
      $this
        ->messenger()
        ->addError($error);
    }
    return $output;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
GitDirtyTreeSensorPlugin::$actualBranch protected property The actual_branch_cmd command output.
GitDirtyTreeSensorPlugin::$distance protected property The ahead_cmd command output.
GitDirtyTreeSensorPlugin::$status protected property The status_cmd command output.
GitDirtyTreeSensorPlugin::$submodules protected property The submodules_cmd command output.
GitDirtyTreeSensorPlugin::buildCommand protected function Build the command to be passed into shell_exec().
GitDirtyTreeSensorPlugin::buildConfigurationForm public function Form constructor. Overrides SensorPluginBase::buildConfigurationForm
GitDirtyTreeSensorPlugin::getShortFileList protected function Returns a shortened file list for the status message.
GitDirtyTreeSensorPlugin::resultVerbose public function Provide additional info about sensor call. Overrides ExtendedInfoSensorPluginInterface::resultVerbose
GitDirtyTreeSensorPlugin::runCommand private function Run a command providing an error message.
GitDirtyTreeSensorPlugin::runSensor public function Runs the sensor, updating $sensor_result. Overrides SensorPluginInterface::runSensor
GitDirtyTreeSensorPlugin::runSensorCommand protected function Run the command and set the status message and the status to the result.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
SensorPluginBase::$configurableValueType protected property Allows plugins to control if the value type can be configured. 6
SensorPluginBase::$pluginDefinition protected property The plugin implementation definition.
SensorPluginBase::$pluginId protected property The plugin_id.
SensorPluginBase::$sensorConfig protected property Current sensor config object.
SensorPluginBase::$services protected property
SensorPluginBase::addService public function Service setter. Overrides SensorPluginInterface::addService
SensorPluginBase::calculateDependencies public function Calculates dependencies for the configured plugin. Overrides SensorPluginInterface::calculateDependencies 4
SensorPluginBase::create public static function Creates an instance of the sensor with config. Overrides SensorPluginInterface::create 7
SensorPluginBase::getConfigurableValueType public function Configurable value type. Overrides SensorPluginInterface::getConfigurableValueType
SensorPluginBase::getDefaultConfiguration public function Default configuration for a sensor. Overrides SensorPluginInterface::getDefaultConfiguration 8
SensorPluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition
SensorPluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
SensorPluginBase::getSensorId public function Gets sensor name (not the label). Overrides SensorPluginInterface::getSensorId
SensorPluginBase::getService public function @todo: Replace with injection Overrides SensorPluginInterface::getService
SensorPluginBase::isEnabled public function Determines if sensor is enabled. Overrides SensorPluginInterface::isEnabled
SensorPluginBase::submitConfigurationForm public function Form submission handler. Overrides PluginFormInterface::submitConfigurationForm 3
SensorPluginBase::validateConfigurationForm public function Form validation handler. Overrides PluginFormInterface::validateConfigurationForm 2
SensorPluginBase::__construct function Instantiates a sensor object. 8
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.