grn-8.x-2.x-dev/GitReleaseNotesDrushCommands.php

GitReleaseNotesDrushCommands.php
<?php

namespace Drush\Commands\grn;

use Drush\Commands\DrushCommands;
use Drush\Drush;

/**
 * Drush 9+ commands for the grn project.
 */
class GitReleaseNotesDrushCommands extends DrushCommands {

  /**
   * Options passed to the release-notes command.
   *
   * @var array
   */
  protected $options;

  /**
   * Generates release notes using all commits between two tags.
   *
   * @param string $tag_1
   *   The previous tag, the starting point for the log.
   * @param string $tag_2
   *   The current tag, the ending point for the log. This can be also be a
   *   branch.
   * @param array $options
   *   An associative array of options whose values come from cli, aliases,
   *   config, etc.
   *
   * @command release:notes
   * @aliases rn,relnotes,release-notes
   *
   * @option baseurl
   *   Set the base url for all issue links. Defaults to /node/ for drupal.org
   *   usage. Issue number will be appended to path or replace "%s".
   * @option changelog
   *   Display the commits in the format for CHANGELOG.txt as expected by
   *   drupal.org.
   * @option md
   *   Display the commits in MD format.
   * @option commit-count
   *   If set, output will show the number of commits between the two tags.
   * @option commit-links
   *   Attach a link to the commit in drupalcode.org repository viewer to the
   *   end of the commit lines.
   * @option git
   *   Path to the git binary, defaults to "git".
   * @option nouser
   *   Do not try to link to user page using the /u/alias as used on drupal.org.
   * @option pretty
   *   Pretty format of the message, see the git-log man page (section
   *   "PRETTY FORMATS")
   * @option reverse
   *   Display the commits from old to new instead of the default Git behavior
   *   that is new to old.
   *
   * @usage drush release-notes
   *   Generates release notes from all commits between the two last tags.
   * @usage drush rn 8.x-1.1
   *   Generates release notes from all commits between 8.x-1.1 (as end) and
   *   the previous tag (as start).
   * @usage drush rn 8.x-1.0 8.x-1.1
   *   Generates release notes from all commits between 8.x-1.0 and 8.x-1.1
   * @usage drush rn 8.x-1.0 8.x-1.x
   *   Use a branch for tag2 (8.x-1.x)
   * @usage drush rn 8.x-1.0 origin/8.x-1.x
   *   If you don't have the branch locally, you might need to use
   *   "[remote-name]/[branch-name]".
   * @usage drush rn 8.x-1.0 8.x-1.x --baseurl="http://community.openatrium.com/node/"
   *   You can specify the changelog to direct issues to other issue trackers.
   * @usage drush rn 8.x-1.0 8.x-1.x --changelog
   *   Generates release notes from the commits between the two tags in the
   *   format for CHANGELOG.txt as expected by drupal.org.
   * @usage drush rn 8.x-1.0 8.x-1.1 --git=/usr/local/git/bin/git
   *   Use git in /usr/local/git/bin/git, and using alias
   * @usage drush rn 8.x-1.0 8.x-1.x --pretty="%s (%h)"
   *   Generates release notes using a custom pretty format string.
   * @usage drush rn 8.x-1.0 8.x-1.x --reverse
   *   Generates release notes from the commits between the two tags in reverse
   *   order.
   *
   * phpcs:disable Drupal.Arrays.Array.LongLineDeclaration
   */
  public function releaseNotes($tag_1 = NULL, $tag_2 = NULL, array $options = ['baseurl' => '/node/', 'changelog' => NULL, 'md' => NULL, 'commit-count' => NULL, 'commit-links' => NULL, 'git' => 'git', 'nouser' => FALSE, 'pretty' => '%s', 'reverse' => FALSE]) {
    $this->options = $options;

    // Make sure we are in the working directory started.
    $cwd = $this->config->get('env.cwd');
    if (!chdir($cwd)) {
      throw new \Exception('Failed to change current working directory to your Git project.');
    }

    if (!is_dir('.git')) {
      throw new \Exception('This must be run from the root directory of your Git project.');
    }

    $git = $options['git'];

    // Fill in calculated tags if both are not given.
    if (!isset($tag_2)) {
      // Get all the defined tags in this repository and sort them.
      $process = Drush::process([$git, 'rev-parse', '--abbrev-ref', 'HEAD']);
      $process->run();
      $branch = substr(array_shift(explode(PHP_EOL, $process->getOutput())), 0, -1);
      if ($branch === 'HEA') {
        // Working from a detached HEAD.
        throw new \Exception("Can't run this command from a detached HEAD.");
      }

      $process = Drush::process([$git, 'tag', '-l', $branch . '*']);
      $process->run();
      $tags = explode(PHP_EOL, trim($process->getOutput()));
      usort($tags, 'version_compare');

      if (!isset($tag_1) && count($tags)) {
        // If no tags are provided, use the two most recent ones.
        $tag_2 = array_pop($tags);

        $tag_1 = count($tags) ? array_pop($tags) : $tag_2;
      }
      else {
        // If only one tag is given, it is considered to be <end> and <start> is
        // taken to be one tag before it.
        $key = array_search($tag_1, $tags);
        if (is_int($key)) {
          if ($key > 0) {
            // Rearrange our tags: the given tag is in fact tag 2.
            $tag_2 = $tag_1;
            // The <start> tag is one before the given <end> tag.
            $tag_1 = $tags[$key - 1];
          }
          else {
            throw new \Exception(dt('@tag is the first tag in the branch.', ['@tag' => $tag_1]));
          }
        }
        else {
          throw new \Exception(dt('@tag is not a valid Git tag.', ['@tag' => $tag_1]));
        }
      }
    }

    // '^' is the escape character on Windows (like '\' on *nix) - has to be
    // contained in the escaped shell argument string ("%s").
    $process = Drush::process(
      [$git, 'show', '-s', '--pretty=format:%H', $tag_1 . '^{commit}']
    );
    $process->run();
    if (!$process->isSuccessful()) {
      throw new \Exception(dt('@tag is not a valid Git tag.', ['@tag' => $tag_1]));
    }
    $tag1 = $process->getOutput();
    // '^' is the escape character on Windows (like '\' on *nix) - has to be
    // contained in the escaped shell argument string ("%s").
    $process = Drush::process(
      [$git, 'show', '-s', '--pretty=format:%H', $tag_2 . '^{commit}']
    );
    $process->run();
    if (!$process->isSuccessful()) {
      throw new \Exception(dt('@tag is not a valid Git tag.', ['@tag' => $tag_2]));
    }
    $tag2 = $process->getOutput();
    $changes = $this->grnGetChanges($tag1, $tag2, $git);

    // Check if we need to produce commit links.
    $commit_path = $this->options['commit-links'] ? $this->getCommitPath($git) : '';

    $items = $this->getItemsArray($changes, $commit_path);
    if ($this->options['changelog']) {
      $formatted_items = $this->formatChangelog($items, $tag_2);
    }
    elseif ($this->options['md']) {
      $formatted_items = $this->formatMd($items, $tag_1, $tag1[0], $tag2[0], $git);
    }
    else {
      $formatted_items = $this->formatChanges($items, $tag_1, $tag1[0], $tag2[0], $git);
    }

    $this->output->writeln($formatted_items['rendered']);

    return TRUE;
  }

  /**
   * Gets the changes and returns them in an array.
   */
  protected function grnGetChanges($tag1, $tag2, $git) {
    $changes = [];

    $reverse = $this->options['reverse'] ? '--reverse' : '--date-order';
    $pretty = $this->options['pretty'];
    if ($pretty === TRUE) {
      // Use default format if pretty option is used with no format.
      $pretty = '%s';
    }
    if (strpos($pretty, '%H') !== 0) {
      // Prepend the commit, which we will need when we parse the output.
      $pretty = '%H ' . $pretty;
    }

    $process = Drush::process([
      $git,
      'log',
      '-s',
      '--pretty=format:' . $pretty,
      $reverse, $tag1 . '..' . $tag2,
    ]);
    $process->mustRun();
    if (!$process->isSuccessful()) {
      throw new \Exception('git log returned an error.');
    }
    $output = explode(PHP_EOL, $process->getOutput());
    foreach ($output as $line) {
      if (empty($line)) {
        // Skip blank lines that are left behind in the messages.
        continue;
      }
      [$hash, $message] = explode(' ', $line, 2);
      $changes[$hash] = $message;
    }
    if ($this->options['commit-count']) {
      $this->options['commit-count'] = count($changes);
    }

    return $changes;
  }

  /**
   * Get array of items.
   */
  protected function getItemsArray($issues, $commit_path) {
    $baseurl = $this->options['baseurl'];
    if (strpos($baseurl, '%s') == FALSE) {
      $baseurl .= '%s';
    }

    $items = [];
    foreach ($issues as $hash => $line) {
      // Pattern is "Issue #[issue number] by [comma-separated usernames]:
      // [Short summary of the change]."
      // Clean up commit log.
      $raw = preg_replace('/^(Patch |- |Issue ){0,3}/', '', $line);
      // Add issue links.
      $item = ($this->options['md']) ? preg_replace('/#(\d+)/S', '[#$1](' . str_replace('%s', '$1', $baseurl) . ')', $raw) :
        preg_replace('/#(\d+)/S', '<a href="' . str_replace('%s', '$1', $baseurl) . '">#$1</a>', $raw);
      // Replace usernames with link to user page.
      if ($this->options['nouser'] === FALSE) {
        // Anything between by and ':' is a comma-separated list of usernames.
        $item = preg_replace_callback('/by ([^:]+):/S',
          function ($matches) {
            $out = [];
            // Separate the different usernames.
            foreach (explode(',', $matches[1]) as $user) {
              // Trim spaces, convert to lowercase, and spaces in middle are
              // replaced with dashes.
              $ualias = str_replace(' ', '-', strtolower(trim($user)));
              $out[] = ($this->options['md']) ? "[$ualias](/u/$ualias)" :
                ("<a href='/u/$ualias'>" . trim($user) . '</a>');
            }

            return 'by ' . implode(', ', $out) . ':';
          },
          $item);
      }
      // If the command was invoked with the --commit-links option.
      if (!empty($commit_path)) {
        $item .= ' (<a href="' . $commit_path . $hash . '" title="View commit">#</a>)';
      }
      $items[$raw] = $item;
    }

    return $items;
  }

  /**
   * Generated output in Changelog format.
   */
  protected function formatChangelog($items, $tag) {
    if ($infos = glob('*.info')) {
      foreach ($infos as $info) {
        $ini_array = parse_ini_file($info);
        $name = trim($ini_array['name'], " \t\n\r\0\x0B\"");
      }
    }
    $process = Drush::process(
      ['git', 'show', '-s', '--pretty=format:%ad', '--date=short', $tag]
    );
    $process->run();
    $date = explode(PHP_EOL, $process->getOutput());
    $changelog = empty($name) ? '' : ($name . " ");
    $changelog .= $tag . ", " . $date[0];
    if ($this->options['commit-count']) {
      $changelog .= ' (' . trim($this->options['commit-count']) . ' commits)';
    }
    $changelog .= "\n" . str_pad("", strlen($changelog), "-");
    $changelog .= "\n";
    foreach ($items as $raw => $html) {
      $changelog .= '- ';
      $line = ucfirst(trim((strpos($raw, "#") === 0) ? substr(strstr($raw, ':'), 1) : $raw));
      $changelog .= substr($line, -1) == "." ? $line : $line . ".";
      $changelog .= "\n";
    }
    return ['raw' => $items, 'rendered' => $changelog];
  }

  /**
   * Generated output in MD format.
   */
  protected function formatMd($items, $prev_tag, $tag1, $tag2, $git) {
    $rendered = "**Changes since $prev_tag";
    if ($this->options['commit-count']) {
      $rendered .= ' (' . trim($this->options['commit-count']) . ' commits)';
    }
    $rendered .= "**\n\n";

    if (!empty($items)) {
      $rendered .= "* " . implode("\n* ", $items) . "\n";
    }

    return ['rendered' => $rendered, 'raw' => $items];
  }

  /**
   * Generates the output.
   */
  protected function formatChanges($items, $prev_tag, $tag1, $tag2, $git) {
    $rendered = "<p>Changes since $prev_tag";
    if ($this->options['commit-count']) {
      $rendered .= ' (' . trim($this->options['commit-count']) . ' commits)';
    }
    $rendered .= ":</p>\n";

    if (!empty($items)) {
      $rendered .= "<ul>\n  <li>" . implode("</li>\n  <li>", $items) . "</li>\n</ul>";
    }

    return ['rendered' => $rendered, 'raw' => $items];
  }

  /**
   * Discover the URL to the 'origin' repository viewer on drupalcode.org.
   *
   * @param string $git
   *   The actual git command we are going to use.
   *
   * @return string
   *   The URL to the repository viewer.
   *
   * @throws \Exception
   */
  protected function getCommitPath($git) {
    // Execute the git config --get remote.origin.url.
    $process = Drush::process([$git, 'config', '--get', 'remote.origin.url']);
    $process->run();
    if (!$process->isSuccessful()) {
      throw new \Exception('git config returned an error.');
    }

    // Get the output of the above command.
    $remote_origin_url = trim($process->getOutput());

    // Check to see if this project is hosted by Drupal.
    if (!strpos($remote_origin_url, 'drupal.org') && !strpos($remote_origin_url, 'drupalcode.org')) {
      throw new \Exception('The remote origin url is not on drupal.org or drupalcode.org.');
    }

    // Format the URL of the project's repository.
    $repository_url = '';
    if (stripos($remote_origin_url, '@git.drupal.org:') !== FALSE) {
      // If the user is a maintainer, the remote origin url will look like:
      // git@git.drupal.org:project/grn.git (for a full project)
      // or
      // git@git.drupal.org:sandbox/user-123456.git (for a sandbox)
      // Replace 'git@git.drupal.org' with 'https://git.drupalcode.org'.
      $repository_url = preg_replace(
        '/git@git\.drupal\.org:/',
        'https://git.drupalcode.org/',
        $remote_origin_url
      );
    }
    elseif (stripos($remote_origin_url, 'git.drupalcode.org') !== FALSE) {
      // If the user is not a maintainer, the remote origin url will look like:
      // https://git.drupalcode.org/project/grn.git (for a full project)
      // or
      // https://git.drupalcode.org/sandbox/user-123456.git (for a sandbox)
      // then we will just need to strip the .git from the end, which is done
      // after this conditional.
      $repository_url = $remote_origin_url;
    }
    else {
      throw new \Exception('The remote origin url is not in the expected format.');
    }

    // Remove .git from the end of the repository url.
    $repository_url = substr($repository_url, 0, -4);

    // Add '/commit/' to the end of the repository url.
    $repository_url = $repository_url . '/commit/';

    return $repository_url;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc