diff --git a/resources/sql/autopatches/20190924.diffusion.01.permanent.sql b/resources/sql/autopatches/20190924.diffusion.01.permanent.sql new file mode 100644 index 0000000000..4c84f32a52 --- /dev/null +++ b/resources/sql/autopatches/20190924.diffusion.01.permanent.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_refcursor + ADD isPermanent BOOL NOT NULL; diff --git a/resources/sql/autopatches/20190924.diffusion.02.default.sql b/resources/sql/autopatches/20190924.diffusion.02.default.sql new file mode 100644 index 0000000000..2f639a5f84 --- /dev/null +++ b/resources/sql/autopatches/20190924.diffusion.02.default.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_repository.repository_refcursor + SET isPermanent = 1; diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php index f9e9f74774..197fa285dc 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php @@ -1,369 +1,371 @@ refs = $refs; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } protected function executeQuery() { if (!$this->refs) { return array(); } $repository = $this->getRepository(); if (!$repository->hasLocalWorkingCopy()) { return array(); } switch ($this->getRepository()->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->resolveGitRefs(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->resolveMercurialRefs(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = $this->resolveSubversionRefs(); break; default: throw new Exception(pht('Unsupported repository type!')); } if ($this->types !== null) { $result = $this->filterRefsByType($result, $this->types); } return $result; } private function resolveGitRefs() { $repository = $this->getRepository(); $unresolved = array_fuse($this->refs); $results = array(); // First, resolve branches and tags. $ref_map = id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withRefTypes( array( PhabricatorRepositoryRefCursor::TYPE_BRANCH, PhabricatorRepositoryRefCursor::TYPE_TAG, )) ->execute(); $ref_map = mgroup($ref_map, 'getShortName'); $tag_prefix = 'refs/tags/'; foreach ($unresolved as $ref) { if (empty($ref_map[$ref])) { continue; } foreach ($ref_map[$ref] as $result) { $fields = $result->getRawFields(); $objectname = idx($fields, 'refname'); if (!strncmp($objectname, $tag_prefix, strlen($tag_prefix))) { $type = 'tag'; } else { $type = 'branch'; } $info = array( 'type' => $type, 'identifier' => $result->getCommitIdentifier(), ); if ($type == 'tag') { $alternate = idx($fields, 'objectname'); if ($alternate) { $info['alternate'] = $alternate; } } $results[$ref][] = $info; } unset($unresolved[$ref]); } // If we resolved everything, we're done. if (!$unresolved) { return $results; } // Try to resolve anything else. This stuff either doesn't exist or is // some ref like "HEAD^^^". $future = $repository->getLocalCommandFuture('cat-file --batch-check'); $future->write(implode("\n", $unresolved)); list($stdout) = $future->resolvex(); $lines = explode("\n", rtrim($stdout, "\n")); if (count($lines) !== count($unresolved)) { throw new Exception( pht( 'Unexpected line count from `%s`!', 'git cat-file')); } $hits = array(); $tags = array(); $lines = array_combine($unresolved, $lines); foreach ($lines as $ref => $line) { $parts = explode(' ', $line); if (count($parts) < 2) { throw new Exception( pht( 'Failed to parse `%s` output: %s', 'git cat-file', $line)); } list($identifier, $type) = $parts; if ($type == 'missing') { // This is either an ambiguous reference which resolves to several // objects, or an invalid reference. For now, always treat it as // invalid. It would be nice to resolve all possibilities for // ambiguous references at some point, although the strategy for doing // so isn't clear to me. continue; } switch ($type) { case 'commit': break; case 'tag': $tags[] = $identifier; break; default: throw new Exception( pht( 'Unexpected object type from `%s`: %s', 'git cat-file', $line)); } $hits[] = array( 'ref' => $ref, 'type' => $type, 'identifier' => $identifier, ); } $tag_map = array(); if ($tags) { // If some of the refs were tags, just load every tag in order to figure // out which commits they map to. This might be somewhat inefficient in // repositories with a huge number of tags. $tag_refs = id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withRefTypes( array( PhabricatorRepositoryRefCursor::TYPE_TAG, )) ->executeQuery(); foreach ($tag_refs as $tag_ref) { $tag_map[$tag_ref->getShortName()] = $tag_ref->getCommitIdentifier(); } } $results = array(); foreach ($hits as $hit) { $type = $hit['type']; $ref = $hit['ref']; $alternate = null; if ($type == 'tag') { - $alternate = $identifier; - $identifier = idx($tag_map, $ref); - if (!$identifier) { - throw new Exception( - pht( - "Failed to look up tag '%s'!", - $ref)); + $tag_identifier = idx($tag_map, $ref); + if ($tag_identifier === null) { + // This can happen when we're asked to resolve the hash of a "tag" + // object created with "git tag --annotate" that isn't currently + // reachable from any ref. Just leave things as they are. + } else { + // Otherwise, we have a normal named tag. + $alternate = $identifier; + $identifier = $tag_identifier; } } $result = array( 'type' => $type, 'identifier' => $identifier, ); if ($alternate !== null) { $result['alternate'] = $alternate; } $results[$ref][] = $result; } return $results; } private function resolveMercurialRefs() { $repository = $this->getRepository(); // First, pull all of the branch heads in the repository. Doing this in // bulk is much faster than querying each individual head if we're // checking even a small number of refs. $branches = id(new DiffusionLowLevelMercurialBranchesQuery()) ->setRepository($repository) ->executeQuery(); $branches = mgroup($branches, 'getShortName'); $results = array(); $unresolved = $this->refs; foreach ($unresolved as $key => $ref) { if (empty($branches[$ref])) { continue; } foreach ($branches[$ref] as $branch) { $fields = $branch->getRawFields(); $results[$ref][] = array( 'type' => 'branch', 'identifier' => $branch->getCommitIdentifier(), 'closed' => idx($fields, 'closed', false), ); } unset($unresolved[$key]); } if (!$unresolved) { return $results; } // If some of the refs look like hashes, try to bulk resolve them. This // workflow happens via RefEngine and bulk resolution is dramatically // faster than individual resolution. See PHI158. $hashlike = array(); foreach ($unresolved as $key => $ref) { if (preg_match('/^[a-f0-9]{40}\z/', $ref)) { $hashlike[$key] = $ref; } } if (count($hashlike) > 1) { $hashlike_map = array(); $hashlike_groups = array_chunk($hashlike, 64, true); foreach ($hashlike_groups as $hashlike_group) { $hashlike_arg = array(); foreach ($hashlike_group as $hashlike_ref) { $hashlike_arg[] = hgsprintf('%s', $hashlike_ref); } $hashlike_arg = '('.implode(' or ', $hashlike_arg).')'; list($err, $refs) = $repository->execLocalCommand( 'log --template=%s --rev %s', '{node}\n', $hashlike_arg); if ($err) { // NOTE: If any ref fails to resolve, Mercurial will exit with an // error. We just give up on the whole group and resolve it // individually below. In theory, we could split it into subgroups // but the pathway where this bulk resolution matters rarely tries // to resolve missing refs (see PHI158). continue; } $refs = phutil_split_lines($refs, false); foreach ($refs as $ref) { $hashlike_map[$ref] = true; } } foreach ($unresolved as $key => $ref) { if (!isset($hashlike_map[$ref])) { continue; } $results[$ref][] = array( 'type' => 'commit', 'identifier' => $ref, ); unset($unresolved[$key]); } } if (!$unresolved) { return $results; } // If we still have unresolved refs (which might be things like "tip"), // try to resolve them individually. $futures = array(); foreach ($unresolved as $ref) { $futures[$ref] = $repository->getLocalCommandFuture( 'log --template=%s --rev %s', '{node}', hgsprintf('%s', $ref)); } foreach (new FutureIterator($futures) as $ref => $future) { try { list($stdout) = $future->resolvex(); } catch (CommandException $ex) { if (preg_match('/ambiguous identifier/', $ex->getStderr())) { // This indicates that the ref ambiguously matched several things. // Eventually, it would be nice to return all of them, but it is // unclear how to best do that. For now, treat it as a miss instead. continue; } if (preg_match('/unknown revision/', $ex->getStderr())) { // No matches for this ref. continue; } throw $ex; } // It doesn't look like we can figure out the type (commit/branch/rev) // from this output very easily. For now, just call everything a commit. $type = 'commit'; $results[$ref][] = array( 'type' => $type, 'identifier' => trim($stdout), ); } return $results; } private function resolveSubversionRefs() { // We don't have any VCS logic for Subversion, so just use the cached // query. return id(new DiffusionCachedResolveRefsQuery()) ->setRepository($this->getRepository()) ->withRefs($this->refs) ->execute(); } } diff --git a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php index 72fb683401..f823ead6d9 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php @@ -1,617 +1,682 @@ rebuild = $rebuild; + return $this; + } + + public function getRebuild() { + return $this->rebuild; + } public function updateRefs() { $this->newPositions = array(); $this->deadPositions = array(); $this->closeCommits = array(); $repository = $this->getRepository(); $viewer = $this->getViewer(); $branches_may_close = false; $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // No meaningful refs of any type in Subversion. $maps = array(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $branches = $this->loadMercurialBranchPositions($repository); $bookmarks = $this->loadMercurialBookmarkPositions($repository); $maps = array( PhabricatorRepositoryRefCursor::TYPE_BRANCH => $branches, PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => $bookmarks, ); $branches_may_close = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $maps = $this->loadGitRefPositions($repository); break; default: throw new Exception(pht('Unknown VCS "%s"!', $vcs)); } // Fill in any missing types with empty lists. $maps = $maps + array( PhabricatorRepositoryRefCursor::TYPE_BRANCH => array(), PhabricatorRepositoryRefCursor::TYPE_TAG => array(), PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => array(), PhabricatorRepositoryRefCursor::TYPE_REF => array(), ); $all_cursors = id(new PhabricatorRepositoryRefCursorQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->needPositions(true) ->execute(); $cursor_groups = mgroup($all_cursors, 'getRefType'); - $this->hasNoCursors = (!$all_cursors); - - // Find all the heads of closing refs. + // Find all the heads of permanent refs. $all_closing_heads = array(); foreach ($all_cursors as $cursor) { - $should_close = $this->shouldCloseRef( - $cursor->getRefType(), - $cursor->getRefName()); - if (!$should_close) { + + // See T13284. Note that we're considering whether this ref was a + // permanent ref or not the last time we updated refs for this + // repository. This allows us to handle things properly when a ref + // is reconfigured from non-permanent to permanent. + + $was_permanent = $cursor->getIsPermanent(); + if (!$was_permanent) { continue; } foreach ($cursor->getPositionIdentifiers() as $identifier) { $all_closing_heads[] = $identifier; } } + $all_closing_heads = array_unique($all_closing_heads); $all_closing_heads = $this->removeMissingCommits($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->newPositions || $this->deadPositions) { + $save_cursors = $this->getCursorsForUpdate($all_cursors); + + if ($this->newPositions || $this->deadPositions || $save_cursors) { $repository->openTransaction(); $this->saveNewPositions(); $this->deleteDeadPositions(); + foreach ($save_cursors as $cursor) { + $cursor->save(); + } + $repository->saveTransaction(); } $branches = $maps[PhabricatorRepositoryRefCursor::TYPE_BRANCH]; if ($branches && $branches_may_close) { $this->updateBranchStates($repository, $branches); } } + private function getCursorsForUpdate(array $cursors) { + assert_instances_of($cursors, 'PhabricatorRepositoryRefCursor'); + + $results = array(); + + foreach ($cursors as $cursor) { + $ref_type = $cursor->getRefType(); + $ref_name = $cursor->getRefName(); + + $is_permanent = $this->isPermanentRef($ref_type, $ref_name); + + if ($is_permanent == $cursor->getIsPermanent()) { + continue; + } + + $cursor->setIsPermanent((int)$is_permanent); + $results[] = $cursor; + } + + return $results; + } + private function updateBranchStates( PhabricatorRepository $repository, array $branches) { assert_instances_of($branches, 'DiffusionRepositoryRef'); $viewer = $this->getViewer(); $all_cursors = id(new PhabricatorRepositoryRefCursorQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->needPositions(true) ->execute(); $state_map = array(); $type_branch = PhabricatorRepositoryRefCursor::TYPE_BRANCH; foreach ($all_cursors as $cursor) { if ($cursor->getRefType() !== $type_branch) { continue; } $raw_name = $cursor->getRefNameRaw(); foreach ($cursor->getPositions() as $position) { $hash = $position->getCommitIdentifier(); $state_map[$raw_name][$hash] = $position; } } $updates = array(); foreach ($branches as $branch) { $position = idx($state_map, $branch->getShortName(), array()); $position = idx($position, $branch->getCommitIdentifier()); if (!$position) { continue; } $fields = $branch->getRawFields(); $position_state = (bool)$position->getIsClosed(); $branch_state = (bool)idx($fields, 'closed'); if ($position_state != $branch_state) { $updates[$position->getID()] = (int)$branch_state; } } if ($updates) { $position_table = id(new PhabricatorRepositoryRefPosition()); $conn = $position_table->establishConnection('w'); $position_table->openTransaction(); foreach ($updates as $position_id => $branch_state) { queryfx( $conn, 'UPDATE %T SET isClosed = %d WHERE id = %d', $position_table->getTableName(), $branch_state, $position_id); } $position_table->saveTransaction(); } } private function markPositionNew( PhabricatorRepositoryRefPosition $position) { $this->newPositions[] = $position; return $this; } private function markPositionDead( PhabricatorRepositoryRefPosition $position) { $this->deadPositions[] = $position; return $this; } private function markCloseCommits(array $identifiers) { foreach ($identifiers as $identifier) { $this->closeCommits[$identifier] = $identifier; } return $this; } /** * Remove commits which no longer exist in the repository from a list. * * After a force push and garbage collection, we may have branch cursors which * point at commits which no longer exist. This can make commands issued later * fail. See T5839 for discussion. * * @param list List of commit identifiers. * @return list List with nonexistent identifiers removed. */ private function removeMissingCommits(array $identifiers) { if (!$identifiers) { return array(); } $resolved = id(new DiffusionLowLevelResolveRefsQuery()) ->setRepository($this->getRepository()) ->withRefs($identifiers) ->execute(); foreach ($identifiers as $key => $identifier) { if (empty($resolved[$identifier])) { unset($identifiers[$key]); } } return $identifiers; } 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. $cursors = mpull($cursors, null, '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_cursor = idx($cursors, $name); if ($ref_cursor) { $old_positions = $ref_cursor->getPositions(); } else { $old_positions = array(); } // 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 ($old_positions as $old_position) { $hash = $old_position->getCommitIdentifier(); if (isset($new_commits[$hash])) { // This ref previously pointed at this commit, and still does. $this->log( pht( 'Ref %s "%s" still points at %s.', $ref_type, $name, $hash)); continue; } // 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, $hash)); // Nuke the obsolete cursor. $this->markPositionDead($old_position); } // Now, we're going to insert new cursors for all the commits which are // associated with this ref that don't currently have cursors. $old_commits = mpull($old_positions, 'getCommitIdentifier'); $old_commits = array_fuse($old_commits); $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)); if (!$ref_cursor) { // If this is the first time we've seen a particular ref (for // example, a new branch) we need to insert a RefCursor record // for it before we can insert a RefPosition. $ref_cursor = $this->newRefCursor( $repository, $ref_type, $name); } $new_position = id(new PhabricatorRepositoryRefPosition()) ->setCursorID($ref_cursor->getID()) ->setCommitIdentifier($identifier) ->setIsClosed(0); $this->markPositionNew($new_position); } - if ($this->shouldCloseRef($ref_type, $name)) { - foreach ($added_commits as $identifier) { + if ($this->isPermanentRef($ref_type, $name)) { + + // See T13284. If this cursor was already marked as permanent, we + // only need to publish the newly created ref positions. However, if + // this cursor was not previously permanent but has become permanent, + // we need to publish all the ref positions. + + // This corresponds to users reconfiguring a branch to make it + // permanent without pushing any new commits to it. + + $is_rebuild = $this->getRebuild(); + $was_permanent = $ref_cursor->getIsPermanent(); + + if ($is_rebuild || !$was_permanent) { + $update_all = true; + } else { + $update_all = false; + } + + if ($update_all) { + $update_commits = $new_commits; + } else { + $update_commits = $added_commits; + } + + if ($is_rebuild) { + $exclude = array(); + } else { + $exclude = $all_closing_heads; + } + + foreach ($update_commits as $identifier) { $new_identifiers = $this->loadNewCommitIdentifiers( $identifier, - $all_closing_heads); + $exclude); $this->markCloseCommits($new_identifiers); } } } // Find any cursors for refs which no longer exist. This happens when a // branch, tag or bookmark is deleted. foreach ($cursors as $name => $cursor) { if (!empty($ref_groups[$name])) { // This ref still has some positions, so we don't need to wipe it // out. Try the next one. continue; } foreach ($cursor->getPositions() as $position) { $this->log( pht( 'Ref %s "%s" no longer exists.', $cursor->getRefType(), $cursor->getRefName())); $this->markPositionDead($position); } } } - private function shouldCloseRef($ref_type, $ref_name) { + private function isPermanentRef($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()->isBranchPermanentRef($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) { $parts = array(); foreach ($all_closing_heads as $head) { $parts[] = hgsprintf('%s', $head); } // See T5896. Mercurial can not parse an "X or Y or ..." rev list // with more than about 300 items, because it exceeds the maximum // allowed recursion depth. Split all the heads into chunks of // 256, and build a query like this: // // ((1 or 2 or ... or 255) or (256 or 257 or ... 511)) // // If we have more than 65535 heads, we'll do that again: // // (((1 or ...) or ...) or ((65536 or ...) or ...)) $chunk_size = 256; while (count($parts) > $chunk_size) { $chunks = array_chunk($parts, $chunk_size); foreach ($chunks as $key => $chunk) { $chunks[$key] = '('.implode(' or ', $chunk).')'; } $parts = array_values($chunks); } $parts = '('.implode(' or ', $parts).')'; list($stdout) = $this->getRepository()->execxLocalCommand( 'log --template %s --rev %s', '{node}\n', hgsprintf('%s', $new_head).' - '.$parts); } 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(pht("Unknown repository type '%s'!", $vcs)); } $all_commits = queryfx_all( $conn_w, 'SELECT id, phid, commitIdentifier, importStatus FROM %T WHERE repositoryID = %d AND commitIdentifier IN (%Ls)', $commit_table->getTableName(), $repository->getID(), $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'], ); PhabricatorWorker::scheduleTask( $class, $data, array( 'priority' => PhabricatorWorker::PRIORITY_COMMIT, 'objectPHID' => $row['phid'], )); } } return $this; } private function newRefCursor( PhabricatorRepository $repository, $ref_type, $ref_name) { + $is_permanent = $this->isPermanentRef($ref_type, $ref_name); + $cursor = id(new PhabricatorRepositoryRefCursor()) ->setRepositoryPHID($repository->getPHID()) ->setRefType($ref_type) - ->setRefName($ref_name); + ->setRefName($ref_name) + ->setIsPermanent((int)$is_permanent); try { return $cursor->save(); } catch (AphrontDuplicateKeyQueryException $ex) { // If we raced another daemon to create this position and lost the race, // load the cursor the other daemon created instead. } $viewer = $this->getViewer(); $cursor = id(new PhabricatorRepositoryRefCursorQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withRefTypes(array($ref_type)) ->withRefNames(array($ref_name)) ->needPositions(true) ->executeOne(); if (!$cursor) { throw new Exception( pht( 'Failed to create a new ref cursor (for "%s", of type "%s", in '. 'repository "%s") because it collided with an existing cursor, '. 'but then failed to load that cursor.', $ref_name, $ref_type, $repository->getDisplayName())); } return $cursor; } private function saveNewPositions() { $positions = $this->newPositions; foreach ($positions as $position) { try { $position->save(); } catch (AphrontDuplicateKeyQueryException $ex) { // We may race another daemon to create this position. If we do, and // we lose the race, that's fine: the other daemon did our work for // us and we can continue. } } $this->newPositions = array(); } private function deleteDeadPositions() { $positions = $this->deadPositions; $repository = $this->getRepository(); foreach ($positions as $position) { // Shove this ref into the old refs table so the discovery engine // can check if any commits have been rendered unreachable. id(new PhabricatorRepositoryOldRef()) ->setRepositoryPHID($repository->getPHID()) ->setCommitIdentifier($position->getCommitIdentifier()) ->save(); $position->delete(); } $this->deadPositions = array(); } /* -( Updating Git Refs )-------------------------------------------------- */ /** * @task git */ private function loadGitRefPositions(PhabricatorRepository $repository) { $refs = id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->execute(); return mgroup($refs, 'getRefType'); } /* -( 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(); } } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php index 8c806edac8..8d3062195c 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php @@ -1,52 +1,59 @@ setName('refs') ->setExamples('**refs** [__options__] __repository__ ...') ->setSynopsis(pht('Update refs in __repository__.')) ->setArguments( array( array( 'name' => 'verbose', 'help' => pht('Show additional debugging information.'), ), + array( + 'name' => 'rebuild', + 'help' => pht( + 'Publish commits currently reachable from any permanent ref, '. + 'ignoring the cached ref state.'), + ), array( 'name' => 'repos', 'wildcard' => true, ), )); } public function execute(PhutilArgumentParser $args) { $repos = $this->loadLocalRepositories($args, 'repos'); if (!$repos) { throw new PhutilArgumentUsageException( pht( 'Specify one or more repositories to update refs for.')); } $console = PhutilConsole::getConsole(); foreach ($repos as $repo) { $console->writeOut( "%s\n", pht( 'Updating refs in "%s"...', $repo->getDisplayName())); $engine = id(new PhabricatorRepositoryRefEngine()) ->setRepository($repo) ->setVerbose($args->getArg('verbose')) + ->setRebuild($args->getArg('rebuild')) ->updateRefs(); } $console->writeOut("%s\n", pht('Done.')); return 0; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php b/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php index 87f737d86e..91261f2b93 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php +++ b/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php @@ -1,111 +1,113 @@ false, self::CONFIG_AUX_PHID => true, self::CONFIG_BINARY => array( 'refNameRaw' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'refType' => 'text32', 'refNameHash' => 'bytes12', 'refNameEncoding' => 'text16?', + 'isPermanent' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_ref' => array( 'columns' => array('repositoryPHID', 'refType', 'refNameHash'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryRefCursorPHIDType::TYPECONST); } public function getRefName() { return $this->getUTF8StringFromStorage( $this->getRefNameRaw(), $this->getRefNameEncoding()); } public function setRefName($ref_raw) { $this->setRefNameRaw($ref_raw); $this->setRefNameHash(PhabricatorHash::digestForIndex($ref_raw)); $this->setRefNameEncoding($this->detectEncodingForStorage($ref_raw)); return $this; } public function attachRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachPositions(array $positions) { assert_instances_of($positions, 'PhabricatorRepositoryRefPosition'); $this->positions = $positions; return $this; } public function getPositions() { return $this->assertAttached($this->positions); } public function getPositionIdentifiers() { return mpull($this->getPositions(), 'getCommitIdentifier'); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getRepository()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Repository refs have the same policies as their repository.'); } }