diff --git a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php index 41092c1030..56826186a1 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php @@ -1,382 +1,392 @@ <?php /** * Update the ref cursors for a repository, which track the positions of * branches, bookmarks, and tags. */ final class PhabricatorRepositoryRefEngine extends PhabricatorRepositoryEngine { private $newRefs = array(); private $deadRefs = array(); private $closeCommits = array(); private $hasNoCursors; public function updateRefs() { $this->newRefs = array(); $this->deadRefs = array(); $this->closeCommits = array(); $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // No meaningful refs of any type in Subversion. $branches = array(); $bookmarks = array(); $tags = array(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $branches = $this->loadMercurialBranchPositions($repository); $bookmarks = $this->loadMercurialBookmarkPositions($repository); $tags = array(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $branches = $this->loadGitBranchPositions($repository); $bookmarks = array(); $tags = $this->loadGitTagPositions($repository); break; default: throw new Exception(pht('Unknown VCS "%s"!', $vcs)); } $maps = array( PhabricatorRepositoryRefCursor::TYPE_BRANCH => $branches, PhabricatorRepositoryRefCursor::TYPE_TAG => $tags, PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => $bookmarks, ); $all_cursors = id(new PhabricatorRepositoryRefCursorQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRepositoryPHIDs(array($repository->getPHID())) ->execute(); $cursor_groups = mgroup($all_cursors, 'getRefType'); $this->hasNoCursors = (!$all_cursors); // Find all the heads of closing refs. $all_closing_heads = array(); foreach ($all_cursors as $cursor) { if ($this->shouldCloseRef($cursor->getRefType(), $cursor->getRefName())) { $all_closing_heads[] = $cursor->getCommitIdentifier(); } } $all_closing_heads = array_unique($all_closing_heads); foreach ($maps as $type => $refs) { $cursor_group = idx($cursor_groups, $type, array()); $this->updateCursors($cursor_group, $refs, $type, $all_closing_heads); } if ($this->closeCommits) { $this->setCloseFlagOnCommits($this->closeCommits); } if ($this->newRefs || $this->deadRefs) { $repository->openTransaction(); foreach ($this->newRefs as $ref) { $ref->save(); } foreach ($this->deadRefs as $ref) { $ref->delete(); } $repository->saveTransaction(); $this->newRefs = array(); $this->deadRefs = array(); } } private function markRefNew(PhabricatorRepositoryRefCursor $cursor) { $this->newRefs[] = $cursor; return $this; } private function markRefDead(PhabricatorRepositoryRefCursor $cursor) { $this->deadRefs[] = $cursor; return $this; } private function markCloseCommits(array $identifiers) { foreach ($identifiers as $identifier) { $this->closeCommits[$identifier] = $identifier; } return $this; } private function updateCursors( array $cursors, array $new_refs, $ref_type, array $all_closing_heads) { $repository = $this->getRepository(); // NOTE: Mercurial branches may have multiple branch heads; this logic // is complex primarily to account for that. // Group all the cursors by their ref name, like "master". Since Mercurial // branches may have multiple heads, there could be several cursors with // the same name. $cursor_groups = mgroup($cursors, 'getRefNameRaw'); // Group all the new ref values by their name. As above, these groups may // have multiple members in Mercurial. $ref_groups = mgroup($new_refs, 'getShortName'); foreach ($ref_groups as $name => $refs) { $new_commits = mpull($refs, 'getCommitIdentifier', 'getCommitIdentifier'); $ref_cursors = idx($cursor_groups, $name, array()); $old_commits = mpull($ref_cursors, null, 'getCommitIdentifier'); // We're going to delete all the cursors pointing at commits which are // no longer associated with the refs. This primarily makes the Mercurial // multiple head case easier, and means that when we update a ref we // delete the old one and write a new one. foreach ($ref_cursors as $cursor) { if (isset($new_commits[$cursor->getCommitIdentifier()])) { // This ref previously pointed at this commit, and still does. $this->log( pht( 'Ref %s "%s" still points at %s.', $ref_type, $name, $cursor->getCommitIdentifier())); } else { // This ref previously pointed at this commit, but no longer does. $this->log( pht( 'Ref %s "%s" no longer points at %s.', $ref_type, $name, $cursor->getCommitIdentifier())); // Nuke the obsolete cursor. $this->markRefDead($cursor); } } // Now, we're going to insert new cursors for all the commits which are // associated with this ref that don't currently have cursors. $added_commits = array_diff_key($new_commits, $old_commits); foreach ($added_commits as $identifier) { $this->log( pht( 'Ref %s "%s" now points at %s.', $ref_type, $name, $identifier)); $this->markRefNew( id(new PhabricatorRepositoryRefCursor()) ->setRepositoryPHID($repository->getPHID()) ->setRefType($ref_type) ->setRefName($name) ->setCommitIdentifier($identifier)); } if ($this->shouldCloseRef($ref_type, $name)) { foreach ($added_commits as $identifier) { $new_identifiers = $this->loadNewCommitIdentifiers( $identifier, $all_closing_heads); $this->markCloseCommits($new_identifiers); } } } // Find any cursors for refs which no longer exist. This happens when a // branch, tag or bookmark is deleted. foreach ($cursor_groups as $name => $cursor_group) { if (idx($ref_groups, $name) === null) { foreach ($cursor_group as $cursor) { $this->log( pht( 'Ref %s "%s" no longer exists.', $cursor->getRefType(), $cursor->getRefName())); $this->markRefDead($cursor); } } } } private function shouldCloseRef($ref_type, $ref_name) { if ($ref_type !== PhabricatorRepositoryRefCursor::TYPE_BRANCH) { return false; } if ($this->hasNoCursors) { // If we don't have any cursors, don't close things. Particularly, this // corresponds to the case where you've just updated to this code on an // existing repository: we don't want to requeue message steps for every // commit on a closeable ref. return false; } return $this->getRepository()->shouldAutocloseBranch($ref_name); } /** * Find all ancestors of a new closing branch head which are not ancestors * of any old closing branch head. */ private function loadNewCommitIdentifiers( $new_head, array $all_closing_heads) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: if ($all_closing_heads) { $escheads = array(); foreach ($all_closing_heads as $head) { $escheads[] = hgsprintf('%s', $head); } $escheads = implode(' or ', $escheads); list($stdout) = $this->getRepository()->execxLocalCommand( 'log --template %s --rev %s', '{node}\n', hgsprintf('%s', $new_head).' - ('.$escheads.')'); } else { list($stdout) = $this->getRepository()->execxLocalCommand( 'log --template %s --rev %s', '{node}\n', hgsprintf('%s', $new_head)); } + + $stdout = trim($stdout); + if (!strlen($stdout)) { + return array(); + } return phutil_split_lines($stdout, $retain_newlines = false); case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: if ($all_closing_heads) { list($stdout) = $this->getRepository()->execxLocalCommand( 'log --format=%s %s --not %Ls', '%H', $new_head, $all_closing_heads); } else { list($stdout) = $this->getRepository()->execxLocalCommand( 'log --format=%s %s', '%H', $new_head); } + + $stdout = trim($stdout); + if (!strlen($stdout)) { + return array(); + } return phutil_split_lines($stdout, $retain_newlines = false); default: throw new Exception(pht('Unsupported VCS "%s"!', $vcs)); } } /** * Mark a list of commits as closeable, and queue workers for those commits * which don't already have the flag. */ private function setCloseFlagOnCommits(array $identifiers) { $repository = $this->getRepository(); $commit_table = new PhabricatorRepositoryCommit(); $conn_w = $commit_table->establishConnection('w'); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $class = 'PhabricatorRepositoryGitCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $class = 'PhabricatorRepositorySvnCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; break; default: throw new Exception("Unknown repository type '{$vcs}'!"); } $all_commits = queryfx_all( $conn_w, 'SELECT id, commitIdentifier, importStatus FROM %T WHERE commitIdentifier IN (%Ls)', $commit_table->getTableName(), $identifiers); $closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE; $all_commits = ipull($all_commits, null, 'commitIdentifier'); foreach ($identifiers as $identifier) { $row = idx($all_commits, $identifier); if (!$row) { throw new Exception( pht( 'Commit "%s" has not been discovered yet! Run discovery before '. 'updating refs.', $identifier)); } if (!($row['importStatus'] & $closeable_flag)) { queryfx( $conn_w, 'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d', $commit_table->getTableName(), $closeable_flag, $row['id']); $data = array( 'commitID' => $row['id'], 'only' => true, ); PhabricatorWorker::scheduleTask($class, $data); } } } /* -( Updating Git Refs )-------------------------------------------------- */ /** * @task git */ private function loadGitBranchPositions(PhabricatorRepository $repository) { return id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withIsOriginBranch(true) ->execute(); } /** * @task git */ private function loadGitTagPositions(PhabricatorRepository $repository) { return id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withIsTag(true) ->execute(); } /* -( Updating Mercurial Refs )-------------------------------------------- */ /** * @task hg */ private function loadMercurialBranchPositions( PhabricatorRepository $repository) { return id(new DiffusionLowLevelMercurialBranchesQuery()) ->setRepository($repository) ->execute(); } /** * @task hg */ private function loadMercurialBookmarkPositions( PhabricatorRepository $repository) { // TODO: Implement support for Mercurial bookmarks. return array(); } }