[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/repository/engine/ -> PhabricatorRepositoryRefEngine.php (source)

   1  <?php
   2  
   3  /**
   4   * Update the ref cursors for a repository, which track the positions of
   5   * branches, bookmarks, and tags.
   6   */
   7  final class PhabricatorRepositoryRefEngine
   8    extends PhabricatorRepositoryEngine {
   9  
  10    private $newRefs = array();
  11    private $deadRefs = array();
  12    private $closeCommits = array();
  13    private $hasNoCursors;
  14  
  15    public function updateRefs() {
  16      $this->newRefs = array();
  17      $this->deadRefs = array();
  18      $this->closeCommits = array();
  19  
  20      $repository = $this->getRepository();
  21  
  22      $vcs = $repository->getVersionControlSystem();
  23      switch ($vcs) {
  24        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
  25          // No meaningful refs of any type in Subversion.
  26          $branches = array();
  27          $bookmarks = array();
  28          $tags = array();
  29          break;
  30        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
  31          $branches = $this->loadMercurialBranchPositions($repository);
  32          $bookmarks = $this->loadMercurialBookmarkPositions($repository);
  33          $tags = array();
  34          break;
  35        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
  36          $branches = $this->loadGitBranchPositions($repository);
  37          $bookmarks = array();
  38          $tags = $this->loadGitTagPositions($repository);
  39          break;
  40        default:
  41          throw new Exception(pht('Unknown VCS "%s"!', $vcs));
  42      }
  43  
  44      $maps = array(
  45        PhabricatorRepositoryRefCursor::TYPE_BRANCH => $branches,
  46        PhabricatorRepositoryRefCursor::TYPE_TAG => $tags,
  47        PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => $bookmarks,
  48      );
  49  
  50      $all_cursors = id(new PhabricatorRepositoryRefCursorQuery())
  51        ->setViewer(PhabricatorUser::getOmnipotentUser())
  52        ->withRepositoryPHIDs(array($repository->getPHID()))
  53        ->execute();
  54      $cursor_groups = mgroup($all_cursors, 'getRefType');
  55  
  56      $this->hasNoCursors = (!$all_cursors);
  57  
  58      // Find all the heads of closing refs.
  59      $all_closing_heads = array();
  60      foreach ($all_cursors as $cursor) {
  61        if ($this->shouldCloseRef($cursor->getRefType(), $cursor->getRefName())) {
  62          $all_closing_heads[] = $cursor->getCommitIdentifier();
  63        }
  64      }
  65      $all_closing_heads = array_unique($all_closing_heads);
  66      $all_closing_heads = $this->removeMissingCommits($all_closing_heads);
  67  
  68      foreach ($maps as $type => $refs) {
  69        $cursor_group = idx($cursor_groups, $type, array());
  70        $this->updateCursors($cursor_group, $refs, $type, $all_closing_heads);
  71      }
  72  
  73      if ($this->closeCommits) {
  74        $this->setCloseFlagOnCommits($this->closeCommits);
  75      }
  76  
  77      if ($this->newRefs || $this->deadRefs) {
  78        $repository->openTransaction();
  79          foreach ($this->newRefs as $ref) {
  80            $ref->save();
  81          }
  82          foreach ($this->deadRefs as $ref) {
  83            $ref->delete();
  84          }
  85        $repository->saveTransaction();
  86  
  87        $this->newRefs = array();
  88        $this->deadRefs = array();
  89      }
  90    }
  91  
  92    private function markRefNew(PhabricatorRepositoryRefCursor $cursor) {
  93      $this->newRefs[] = $cursor;
  94      return $this;
  95    }
  96  
  97    private function markRefDead(PhabricatorRepositoryRefCursor $cursor) {
  98      $this->deadRefs[] = $cursor;
  99      return $this;
 100    }
 101  
 102    private function markCloseCommits(array $identifiers) {
 103      foreach ($identifiers as $identifier) {
 104        $this->closeCommits[$identifier] = $identifier;
 105      }
 106      return $this;
 107    }
 108  
 109    /**
 110     * Remove commits which no longer exist in the repository from a list.
 111     *
 112     * After a force push and garbage collection, we may have branch cursors which
 113     * point at commits which no longer exist. This can make commands issued later
 114     * fail. See T5839 for discussion.
 115     *
 116     * @param list<string>    List of commit identifiers.
 117     * @return list<string>   List with nonexistent identifiers removed.
 118     */
 119    private function removeMissingCommits(array $identifiers) {
 120      if (!$identifiers) {
 121        return array();
 122      }
 123  
 124      $resolved = id(new DiffusionLowLevelResolveRefsQuery())
 125        ->setRepository($this->getRepository())
 126        ->withRefs($identifiers)
 127        ->execute();
 128  
 129      foreach ($identifiers as $key => $identifier) {
 130        if (empty($resolved[$identifier])) {
 131          unset($identifiers[$key]);
 132        }
 133      }
 134  
 135      return $identifiers;
 136    }
 137  
 138    private function updateCursors(
 139      array $cursors,
 140      array $new_refs,
 141      $ref_type,
 142      array $all_closing_heads) {
 143      $repository = $this->getRepository();
 144  
 145      // NOTE: Mercurial branches may have multiple branch heads; this logic
 146      // is complex primarily to account for that.
 147  
 148      // Group all the cursors by their ref name, like "master". Since Mercurial
 149      // branches may have multiple heads, there could be several cursors with
 150      // the same name.
 151      $cursor_groups = mgroup($cursors, 'getRefNameRaw');
 152  
 153      // Group all the new ref values by their name. As above, these groups may
 154      // have multiple members in Mercurial.
 155      $ref_groups = mgroup($new_refs, 'getShortName');
 156  
 157      foreach ($ref_groups as $name => $refs) {
 158        $new_commits = mpull($refs, 'getCommitIdentifier', 'getCommitIdentifier');
 159  
 160        $ref_cursors = idx($cursor_groups, $name, array());
 161        $old_commits = mpull($ref_cursors, null, 'getCommitIdentifier');
 162  
 163        // We're going to delete all the cursors pointing at commits which are
 164        // no longer associated with the refs. This primarily makes the Mercurial
 165        // multiple head case easier, and means that when we update a ref we
 166        // delete the old one and write a new one.
 167        foreach ($ref_cursors as $cursor) {
 168          if (isset($new_commits[$cursor->getCommitIdentifier()])) {
 169            // This ref previously pointed at this commit, and still does.
 170            $this->log(
 171              pht(
 172                'Ref %s "%s" still points at %s.',
 173                $ref_type,
 174                $name,
 175                $cursor->getCommitIdentifier()));
 176          } else {
 177            // This ref previously pointed at this commit, but no longer does.
 178            $this->log(
 179              pht(
 180                'Ref %s "%s" no longer points at %s.',
 181                $ref_type,
 182                $name,
 183                $cursor->getCommitIdentifier()));
 184  
 185            // Nuke the obsolete cursor.
 186            $this->markRefDead($cursor);
 187          }
 188        }
 189  
 190        // Now, we're going to insert new cursors for all the commits which are
 191        // associated with this ref that don't currently have cursors.
 192        $added_commits = array_diff_key($new_commits, $old_commits);
 193        foreach ($added_commits as $identifier) {
 194          $this->log(
 195            pht(
 196              'Ref %s "%s" now points at %s.',
 197              $ref_type,
 198              $name,
 199              $identifier));
 200          $this->markRefNew(
 201            id(new PhabricatorRepositoryRefCursor())
 202              ->setRepositoryPHID($repository->getPHID())
 203              ->setRefType($ref_type)
 204              ->setRefName($name)
 205              ->setCommitIdentifier($identifier));
 206        }
 207  
 208        if ($this->shouldCloseRef($ref_type, $name)) {
 209          foreach ($added_commits as $identifier) {
 210            $new_identifiers = $this->loadNewCommitIdentifiers(
 211              $identifier,
 212              $all_closing_heads);
 213  
 214            $this->markCloseCommits($new_identifiers);
 215          }
 216        }
 217      }
 218  
 219      // Find any cursors for refs which no longer exist. This happens when a
 220      // branch, tag or bookmark is deleted.
 221  
 222      foreach ($cursor_groups as $name => $cursor_group) {
 223        if (idx($ref_groups, $name) === null) {
 224          foreach ($cursor_group as $cursor) {
 225            $this->log(
 226              pht(
 227                'Ref %s "%s" no longer exists.',
 228                $cursor->getRefType(),
 229                $cursor->getRefName()));
 230            $this->markRefDead($cursor);
 231          }
 232        }
 233      }
 234    }
 235  
 236    private function shouldCloseRef($ref_type, $ref_name) {
 237      if ($ref_type !== PhabricatorRepositoryRefCursor::TYPE_BRANCH) {
 238        return false;
 239      }
 240  
 241      if ($this->hasNoCursors) {
 242        // If we don't have any cursors, don't close things. Particularly, this
 243        // corresponds to the case where you've just updated to this code on an
 244        // existing repository: we don't want to requeue message steps for every
 245        // commit on a closeable ref.
 246        return false;
 247      }
 248  
 249      return $this->getRepository()->shouldAutocloseBranch($ref_name);
 250    }
 251  
 252    /**
 253     * Find all ancestors of a new closing branch head which are not ancestors
 254     * of any old closing branch head.
 255     */
 256    private function loadNewCommitIdentifiers(
 257      $new_head,
 258      array $all_closing_heads) {
 259  
 260      $repository = $this->getRepository();
 261      $vcs = $repository->getVersionControlSystem();
 262      switch ($vcs) {
 263        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 264          if ($all_closing_heads) {
 265            $escheads = array();
 266            foreach ($all_closing_heads as $head) {
 267              $escheads[] = hgsprintf('%s', $head);
 268            }
 269            $escheads = implode(' or ', $escheads);
 270            list($stdout) = $this->getRepository()->execxLocalCommand(
 271              'log --template %s --rev %s',
 272              '{node}\n',
 273              hgsprintf('%s', $new_head).' - ('.$escheads.')');
 274          } else {
 275            list($stdout) = $this->getRepository()->execxLocalCommand(
 276              'log --template %s --rev %s',
 277              '{node}\n',
 278              hgsprintf('%s', $new_head));
 279          }
 280  
 281          $stdout = trim($stdout);
 282          if (!strlen($stdout)) {
 283            return array();
 284          }
 285          return phutil_split_lines($stdout, $retain_newlines = false);
 286        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 287          if ($all_closing_heads) {
 288            list($stdout) = $this->getRepository()->execxLocalCommand(
 289              'log --format=%s %s --not %Ls',
 290              '%H',
 291              $new_head,
 292              $all_closing_heads);
 293          } else {
 294            list($stdout) = $this->getRepository()->execxLocalCommand(
 295              'log --format=%s %s',
 296              '%H',
 297              $new_head);
 298          }
 299  
 300          $stdout = trim($stdout);
 301          if (!strlen($stdout)) {
 302            return array();
 303          }
 304          return phutil_split_lines($stdout, $retain_newlines = false);
 305        default:
 306          throw new Exception(pht('Unsupported VCS "%s"!', $vcs));
 307      }
 308    }
 309  
 310    /**
 311     * Mark a list of commits as closeable, and queue workers for those commits
 312     * which don't already have the flag.
 313     */
 314    private function setCloseFlagOnCommits(array $identifiers) {
 315      $repository = $this->getRepository();
 316      $commit_table = new PhabricatorRepositoryCommit();
 317      $conn_w = $commit_table->establishConnection('w');
 318  
 319      $vcs = $repository->getVersionControlSystem();
 320      switch ($vcs) {
 321        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 322          $class = 'PhabricatorRepositoryGitCommitMessageParserWorker';
 323          break;
 324        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
 325          $class = 'PhabricatorRepositorySvnCommitMessageParserWorker';
 326          break;
 327        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 328          $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker';
 329          break;
 330        default:
 331          throw new Exception("Unknown repository type '{$vcs}'!");
 332      }
 333  
 334      $all_commits = queryfx_all(
 335        $conn_w,
 336        'SELECT id, commitIdentifier, importStatus FROM %T
 337          WHERE repositoryID = %d AND commitIdentifier IN (%Ls)',
 338        $commit_table->getTableName(),
 339        $repository->getID(),
 340        $identifiers);
 341  
 342      $closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE;
 343  
 344      $all_commits = ipull($all_commits, null, 'commitIdentifier');
 345      foreach ($identifiers as $identifier) {
 346        $row = idx($all_commits, $identifier);
 347  
 348        if (!$row) {
 349          throw new Exception(
 350            pht(
 351              'Commit "%s" has not been discovered yet! Run discovery before '.
 352              'updating refs.',
 353              $identifier));
 354        }
 355  
 356        if (!($row['importStatus'] & $closeable_flag)) {
 357          queryfx(
 358            $conn_w,
 359            'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d',
 360            $commit_table->getTableName(),
 361            $closeable_flag,
 362            $row['id']);
 363  
 364          $data = array(
 365            'commitID' => $row['id'],
 366            'only' => true,
 367          );
 368  
 369          PhabricatorWorker::scheduleTask($class, $data);
 370        }
 371      }
 372    }
 373  
 374  
 375  /* -(  Updating Git Refs  )-------------------------------------------------- */
 376  
 377  
 378    /**
 379     * @task git
 380     */
 381    private function loadGitBranchPositions(PhabricatorRepository $repository) {
 382      return id(new DiffusionLowLevelGitRefQuery())
 383        ->setRepository($repository)
 384        ->withIsOriginBranch(true)
 385        ->execute();
 386    }
 387  
 388  
 389    /**
 390     * @task git
 391     */
 392    private function loadGitTagPositions(PhabricatorRepository $repository) {
 393      return id(new DiffusionLowLevelGitRefQuery())
 394        ->setRepository($repository)
 395        ->withIsTag(true)
 396        ->execute();
 397    }
 398  
 399  
 400  /* -(  Updating Mercurial Refs  )-------------------------------------------- */
 401  
 402  
 403    /**
 404     * @task hg
 405     */
 406    private function loadMercurialBranchPositions(
 407      PhabricatorRepository $repository) {
 408      return id(new DiffusionLowLevelMercurialBranchesQuery())
 409        ->setRepository($repository)
 410        ->execute();
 411    }
 412  
 413  
 414    /**
 415     * @task hg
 416     */
 417    private function loadMercurialBookmarkPositions(
 418      PhabricatorRepository $repository) {
 419      // TODO: Implement support for Mercurial bookmarks.
 420      return array();
 421    }
 422  
 423  }


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