[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/releeph/differential/ -> DifferentialReleephRequestFieldSpecification.php (source)

   1  <?php
   2  
   3  /**
   4   * This DifferentialFieldSpecification exists for two reason:
   5   *
   6   * 1: To parse "Releeph: picks RQ<nn>" headers in commits created by
   7   * arc-releeph so that RQs committed by arc-releeph have real
   8   * PhabricatorRepositoryCommits associated with them (instaed of just the SHA
   9   * of the commit, as seen by the pusher).
  10   *
  11   * 2: If requestors want to commit directly to their release branch, they can
  12   * use this header to (i) indicate on a differential revision that this
  13   * differential revision is for the release branch, and (ii) when they land
  14   * their diff on to the release branch manually, the ReleephRequest is
  15   * automatically updated (instead of having to use the "Mark Manually Picked"
  16   * button.)
  17   *
  18   */
  19  final class DifferentialReleephRequestFieldSpecification {
  20  
  21    // TODO: This class is essentially dead right now, see T2222.
  22  
  23    const ACTION_PICKS    = 'picks';
  24    const ACTION_REVERTS  = 'reverts';
  25  
  26    private $releephAction;
  27    private $releephPHIDs = array();
  28  
  29    public function getStorageKey() {
  30      return 'releeph:actions';
  31    }
  32  
  33    public function getValueForStorage() {
  34      return json_encode(array(
  35        'releephAction' => $this->releephAction,
  36        'releephPHIDs'  => $this->releephPHIDs,
  37      ));
  38    }
  39  
  40    public function setValueFromStorage($json) {
  41      if ($json) {
  42        $dict = json_decode($json, true);
  43        $this->releephAction = idx($dict, 'releephAction');
  44        $this->releephPHIDs = idx($dict, 'releephPHIDs');
  45      }
  46      return $this;
  47    }
  48  
  49    public function shouldAppearOnRevisionView() {
  50      return true;
  51    }
  52  
  53    public function renderLabelForRevisionView() {
  54      return 'Releeph';
  55    }
  56  
  57    public function getRequiredHandlePHIDs() {
  58      return mpull($this->loadReleephRequests(), 'getPHID');
  59    }
  60  
  61    public function renderValueForRevisionView() {
  62      static $tense = array(
  63        self::ACTION_PICKS => array(
  64          'future'  => 'Will pick',
  65          'past'    => 'Picked',
  66        ),
  67        self::ACTION_REVERTS => array(
  68          'future'  => 'Will revert',
  69          'past'    => 'Reverted',
  70        ),
  71      );
  72  
  73      $releeph_requests = $this->loadReleephRequests();
  74      if (!$releeph_requests) {
  75        return null;
  76      }
  77  
  78      $status = $this->getRevision()->getStatus();
  79      if ($status == ArcanistDifferentialRevisionStatus::CLOSED) {
  80        $verb = $tense[$this->releephAction]['past'];
  81      } else {
  82        $verb = $tense[$this->releephAction]['future'];
  83      }
  84  
  85      $parts = hsprintf('%s...', $verb);
  86      foreach ($releeph_requests as $releeph_request) {
  87        $parts->appendHTML(phutil_tag('br'));
  88        $parts->appendHTML(
  89          $this->getHandle($releeph_request->getPHID())->renderLink());
  90      }
  91  
  92      return $parts;
  93    }
  94  
  95    public function shouldAppearOnCommitMessage() {
  96      return true;
  97    }
  98  
  99    public function getCommitMessageKey() {
 100      return 'releephActions';
 101    }
 102  
 103    public function setValueFromParsedCommitMessage($dict) {
 104      $this->releephAction = $dict['releephAction'];
 105      $this->releephPHIDs = $dict['releephPHIDs'];
 106      return $this;
 107    }
 108  
 109    public function renderValueForCommitMessage($is_edit) {
 110      $releeph_requests = $this->loadReleephRequests();
 111      if (!$releeph_requests) {
 112        return null;
 113      }
 114  
 115      $parts = array($this->releephAction);
 116      foreach ($releeph_requests as $releeph_request) {
 117        $parts[] = 'RQ'.$releeph_request->getID();
 118      }
 119  
 120      return implode(' ', $parts);
 121    }
 122  
 123    /**
 124     * Releeph fields should look like:
 125     *
 126     *   Releeph: picks RQ1 RQ2, RQ3
 127     *   Releeph: reverts RQ1
 128     */
 129    public function parseValueFromCommitMessage($value) {
 130      /**
 131       * Releeph commit messages look like this (but with more blank lines,
 132       * omitted here):
 133       *
 134       *   Make CaptainHaddock more reasonable
 135       *   Releeph: picks RQ1
 136       *   Requested By: edward
 137       *   Approved By: edward (requestor)
 138       *   Request Reason: x
 139       *   Summary: Make the Haddock implementation more reasonable.
 140       *   Test Plan: none
 141       *   Reviewers: user1
 142       *
 143       * Some of these fields are recognized by Differential (e.g. "Requested
 144       * By"). They are folded up into the "Releeph" field, parsed by this
 145       * class. As such $value includes more than just the first-line:
 146       *
 147       *   "picks RQ1\n\nRequested By: edward\n\nApproved By: edward (requestor)"
 148       *
 149       * To hack around this, just consider the first line of $value when
 150       * determining what Releeph actions the parsed commit is performing.
 151       */
 152      $first_line = head(array_filter(explode("\n", $value)));
 153  
 154      $tokens = preg_split('/\s*,?\s+/', $first_line);
 155      $raw_action = array_shift($tokens);
 156      $action = strtolower($raw_action);
 157  
 158      if (!$action) {
 159        return null;
 160      }
 161  
 162      switch ($action) {
 163        case self::ACTION_REVERTS:
 164        case self::ACTION_PICKS:
 165          break;
 166  
 167        default:
 168          throw new DifferentialFieldParseException(
 169            "Commit message contains unknown Releeph action '{$raw_action}'!");
 170          break;
 171      }
 172  
 173      $releeph_requests = array();
 174      foreach ($tokens as $token) {
 175        $match = array();
 176        if (!preg_match('/^(?:RQ)?(\d+)$/i', $token, $match)) {
 177          $label = $this->renderLabelForCommitMessage();
 178          throw new DifferentialFieldParseException(
 179            "Commit message contains unparseable ".
 180            "Releeph request token '{$token}'!");
 181        }
 182  
 183        $id = (int) $match[1];
 184        $releeph_request = id(new ReleephRequest())->load($id);
 185  
 186        if (!$releeph_request) {
 187          throw new DifferentialFieldParseException(
 188            "Commit message references non existent releeph request: {$value}!");
 189        }
 190  
 191        $releeph_requests[] = $releeph_request;
 192      }
 193  
 194      if (count($releeph_requests) > 1) {
 195        $rqs_seen = array();
 196        $groups = array();
 197        foreach ($releeph_requests as $releeph_request) {
 198          $releeph_branch = $releeph_request->getBranch();
 199          $branch_name = $releeph_branch->getName();
 200          $rq_id = 'RQ'.$releeph_request->getID();
 201  
 202          if (idx($rqs_seen, $rq_id)) {
 203            throw new DifferentialFieldParseException(
 204              "Commit message refers to {$rq_id} multiple times!");
 205          }
 206          $rqs_seen[$rq_id] = true;
 207  
 208          if (!isset($groups[$branch_name])) {
 209            $groups[$branch_name] = array();
 210          }
 211          $groups[$branch_name][] = $rq_id;
 212        }
 213  
 214        if (count($groups) > 1) {
 215          $lists = array();
 216          foreach ($groups as $branch_name => $rq_ids) {
 217            $lists[] = implode(', ', $rq_ids).' in '.$branch_name;
 218          }
 219          throw new DifferentialFieldParseException(
 220            'Commit message references multiple Releeph requests, '.
 221            'but the requests are in different branches: '.
 222            implode('; ', $lists));
 223        }
 224      }
 225  
 226      $phids = mpull($releeph_requests, 'getPHID');
 227  
 228      $data = array(
 229        'releephAction' => $action,
 230        'releephPHIDs'  => $phids,
 231      );
 232      return $data;
 233    }
 234  
 235    public function renderLabelForCommitMessage() {
 236      return 'Releeph';
 237    }
 238  
 239    public function shouldAppearOnCommitMessageTemplate() {
 240      return false;
 241    }
 242  
 243    public function didParseCommit(PhabricatorRepository $repo,
 244                                   PhabricatorRepositoryCommit $commit,
 245                                   PhabricatorRepositoryCommitData $data) {
 246  
 247      // NOTE: This is currently dead code. See T2222.
 248  
 249      $releeph_requests = $this->loadReleephRequests();
 250  
 251      if (!$releeph_requests) {
 252        return;
 253      }
 254  
 255      $releeph_branch = head($releeph_requests)->getBranch();
 256      if (!$this->isCommitOnBranch($repo, $commit, $releeph_branch)) {
 257        return;
 258      }
 259  
 260      foreach ($releeph_requests as $releeph_request) {
 261        if ($this->releephAction === self::ACTION_PICKS) {
 262          $action = 'pick';
 263        } else {
 264          $action = 'revert';
 265        }
 266  
 267        $actor_phid = coalesce(
 268          $data->getCommitDetail('committerPHID'),
 269          $data->getCommitDetail('authorPHID'));
 270  
 271        $actor = id(new PhabricatorUser())
 272          ->loadOneWhere('phid = %s', $actor_phid);
 273  
 274        $xactions = array();
 275  
 276        $xactions[] = id(new ReleephRequestTransaction())
 277          ->setTransactionType(ReleephRequestTransaction::TYPE_DISCOVERY)
 278          ->setMetadataValue('action', $action)
 279          ->setMetadataValue('authorPHID',
 280            $data->getCommitDetail('authorPHID'))
 281          ->setMetadataValue('committerPHID',
 282            $data->getCommitDetail('committerPHID'))
 283          ->setNewValue($commit->getPHID());
 284  
 285        $editor = id(new ReleephRequestTransactionalEditor())
 286          ->setActor($actor)
 287          ->setContinueOnNoEffect(true)
 288          ->setContentSource(
 289            PhabricatorContentSource::newForSource(
 290              PhabricatorContentSource::SOURCE_UNKNOWN,
 291              array()));
 292  
 293        $editor->applyTransactions($releeph_request, $xactions);
 294      }
 295    }
 296  
 297    private function loadReleephRequests() {
 298      if (!$this->releephPHIDs) {
 299        return array();
 300      }
 301  
 302      return id(new ReleephRequestQuery())
 303        ->setViewer($this->getViewer())
 304        ->withPHIDs($this->releephPHIDs)
 305        ->execute();
 306    }
 307  
 308    private function isCommitOnBranch(PhabricatorRepository $repo,
 309                                      PhabricatorRepositoryCommit $commit,
 310                                      ReleephBranch $releeph_branch) {
 311  
 312      switch ($repo->getVersionControlSystem()) {
 313        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 314          list($output) = $repo->execxLocalCommand(
 315            'branch --all --no-color --contains %s',
 316            $commit->getCommitIdentifier());
 317  
 318          $remote_prefix = 'remotes/origin/';
 319          $branches = array();
 320          foreach (array_filter(explode("\n", $output)) as $line) {
 321            $tokens = explode(' ', $line);
 322            $ref = last($tokens);
 323            if (strncmp($ref, $remote_prefix, strlen($remote_prefix)) === 0) {
 324              $branch = substr($ref, strlen($remote_prefix));
 325              $branches[$branch] = $branch;
 326            }
 327          }
 328  
 329          return idx($branches, $releeph_branch->getName());
 330          break;
 331  
 332        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
 333          $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
 334            DiffusionRequest::newFromDictionary(array(
 335              'user' => $this->getUser(),
 336              'repository' => $repo,
 337              'commit' => $commit->getCommitIdentifier(),
 338            )));
 339          $path_changes = $change_query->loadChanges();
 340          $commit_paths = mpull($path_changes, 'getPath');
 341  
 342          $branch_path = $releeph_branch->getName();
 343  
 344          $in_branch = array();
 345          $ex_branch = array();
 346          foreach ($commit_paths as $path) {
 347            if (strncmp($path, $branch_path, strlen($branch_path)) === 0) {
 348              $in_branch[] = $path;
 349            } else {
 350              $ex_branch[] = $path;
 351            }
 352          }
 353  
 354          if ($in_branch && $ex_branch) {
 355            $error = sprintf(
 356              'CONFUSION: commit %s in %s contains %d path change(s) that were '.
 357              'part of a Releeph branch, but also has %d path change(s) not '.
 358              'part of a Releeph branch!',
 359              $commit->getCommitIdentifier(),
 360              $repo->getCallsign(),
 361              count($in_branch),
 362              count($ex_branch));
 363            phlog($error);
 364          }
 365  
 366          return !empty($in_branch);
 367          break;
 368      }
 369    }
 370  
 371  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1