diff --git a/src/applications/repository/engine/PhabricatorRepositoryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryEngine.php index c1ad203f0b..4d89d03140 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryEngine.php @@ -1,196 +1,197 @@ repository = $repository; return $this; } /** * @task config */ protected function getRepository() { if ($this->repository === null) { throw new PhutilInvalidStateException('setRepository'); } return $this->repository; } /** * @task config */ public function setVerbose($verbose) { $this->verbose = $verbose; return $this; } /** * @task config */ public function getVerbose() { return $this->verbose; } public function getViewer() { return PhabricatorUser::getOmnipotentUser(); } protected function newRepositoryLock( PhabricatorRepository $repository, $lock_key, $lock_device_only) { $lock_parts = array( 'repositoryPHID' => $repository->getPHID(), ); if ($lock_device_only) { $device = AlmanacKeys::getLiveDevice(); if ($device) { $lock_parts['devicePHID'] = $device->getPHID(); } } return PhabricatorGlobalLock::newLock($lock_key, $lock_parts); } /** * @task internal */ protected function log($pattern /* ... */) { if ($this->getVerbose()) { $console = PhutilConsole::getConsole(); $argv = func_get_args(); array_unshift($argv, "%s\n"); call_user_func_array(array($console, 'writeOut'), $argv); } return $this; } final protected function queueCommitImportTask( PhabricatorRepository $repository, $commit_id, $commit_phid, $task_priority, $via = null) { $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)); } $data = array( 'commitID' => $commit_id, ); if ($via !== null) { $data['via'] = $via; } $options = array( 'priority' => $task_priority, 'objectPHID' => $commit_phid, + 'containerPHID' => $repository->getPHID(), ); PhabricatorWorker::scheduleTask($class, $data, $options); } final protected function getImportTaskPriority( PhabricatorRepository $repository, array $refs) { assert_instances_of($refs, 'PhabricatorRepositoryCommitRef'); // If the repository is importing for the first time, we schedule tasks // at IMPORT priority, which is very low. Making progress on importing a // new repository for the first time is less important than any other // daemon task. // If the repository has finished importing and we're just catching up // on recent commits, we usually schedule discovery at COMMIT priority, // which is slightly below the default priority. // Note that followup tasks and triggered tasks (like those generated by // Herald or Harbormaster) will queue at DEFAULT priority, so that each // commit tends to fully import before we start the next one. This tends // to give imports fairly predictable progress. See T11677 for some // discussion. if ($repository->isImporting()) { $this->log( pht( 'Importing %s commit(s) at low priority ("PRIORITY_IMPORT") '. 'because this repository is still importing.', phutil_count($refs))); return PhabricatorWorker::PRIORITY_IMPORT; } // See T13369. If we've discovered a lot of commits at once, import them // at lower priority. // This is mostly aimed at reducing the impact that synchronizing thousands // of commits from a remote upstream has on other repositories. The queue // is "mostly FIFO", so queueing a thousand commit imports can stall other // repositories. // In a perfect world we'd probably give repositories round-robin queue // priority, but we don't currently have the primitives for this and there // isn't a strong case for building them. // Use "a whole lot of commits showed up at once" as a heuristic for // detecting "someone synchronized an upstream", and import them at a lower // priority to more closely approximate fair scheduling. if (count($refs) >= PhabricatorRepository::LOWPRI_THRESHOLD) { $this->log( pht( 'Importing %s commit(s) at low priority ("PRIORITY_IMPORT") '. 'because many commits were discovered at once.', phutil_count($refs))); return PhabricatorWorker::PRIORITY_IMPORT; } // Otherwise, import at normal priority. if ($refs) { $this->log( pht( 'Importing %s commit(s) at normal priority ("PRIORITY_COMMIT").', phutil_count($refs))); } return PhabricatorWorker::PRIORITY_COMMIT; } } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php index ebd6edff97..e9e1cd1d6c 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php @@ -1,283 +1,286 @@ setName('reparse') ->setExamples('**reparse** [options] __commit__') ->setSynopsis( pht( '**reparse** __what__ __which_parts__ [--trace] [--force]'."\n\n". 'Rerun the Diffusion parser on specific commits and repositories. '. 'Mostly useful for debugging changes to Diffusion.'."\n\n". 'e.g. do same but exclude before yesterday (local time):'."\n". 'repository reparse --all TEST --change --min-date yesterday'."\n". 'repository reparse --all TEST --change --min-date "today -1 day".'. "\n\n". 'e.g. do same but exclude before 03/31/2013 (local time):'."\n". 'repository reparse --all TEST --change --min-date "03/31/2013"')) ->setArguments( array( array( 'name' => 'revision', 'wildcard' => true, ), array( 'name' => 'all', 'param' => 'repository', 'help' => pht( 'Reparse all commits in the specified repository.'), ), array( 'name' => 'min-date', 'param' => 'date', 'help' => pht( "Must be used with __%s__, this will exclude commits which ". "are earlier than __date__.\n". "Valid examples:\n". " 'today', 'today 2pm', '-1 hour', '-2 hours', '-24 hours',\n". " 'yesterday', 'today -1 day', 'yesterday 2pm', '2pm -1 day',\n". " 'last Monday', 'last Monday 14:00', 'last Monday 2pm',\n". " '31 March 2013', '31 Mar', '03/31', '03/31/2013',\n". "See __%s__ for more.", '--all', 'http://www.php.net/manual/en/datetime.formats.php'), ), array( 'name' => 'message', 'help' => pht('Reparse commit messages.'), ), array( 'name' => 'change', 'help' => pht('Reparse source changes.'), ), array( 'name' => 'publish', 'help' => pht( 'Publish changes: send email, publish Feed stories, run '. 'Herald rules, etc.'), ), array( 'name' => 'force', 'short' => 'f', 'help' => pht('Act noninteractively, without prompting.'), ), array( 'name' => 'background', 'help' => pht( 'Queue tasks for the daemons instead of running them in the '. 'foreground.'), ), array( 'name' => 'importing', 'help' => pht('Reparse all steps which have not yet completed.'), ), )); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $all_from_repo = $args->getArg('all'); $reparse_message = $args->getArg('message'); $reparse_change = $args->getArg('change'); $reparse_publish = $args->getArg('publish'); $reparse_what = $args->getArg('revision'); $force = $args->getArg('force'); $background = $args->getArg('background'); $min_date = $args->getArg('min-date'); $importing = $args->getArg('importing'); if (!$all_from_repo && !$reparse_what) { throw new PhutilArgumentUsageException( pht('Specify a commit or repository to reparse.')); } if ($all_from_repo && $reparse_what) { $commits = implode(', ', $reparse_what); throw new PhutilArgumentUsageException( pht( "Specify a commit or repository to reparse, not both:\n". "All from repo: %s\n". "Commit(s) to reparse: %s", $all_from_repo, $commits)); } $any_step = ($reparse_message || $reparse_change || $reparse_publish); if ($any_step && $importing) { throw new PhutilArgumentUsageException( pht( 'Choosing steps with "--importing" conflicts with flags which '. 'select specific steps.')); } else if ($any_step) { // OK. } else if ($importing) { // OK. } else if (!$any_step && !$importing) { throw new PhutilArgumentUsageException( pht( 'Specify which steps to reparse with "--message", "--change", '. 'and/or "--publish"; or "--importing" to run all missing steps.')); } $min_timestamp = false; if ($min_date) { $min_timestamp = strtotime($min_date); if (!$all_from_repo) { throw new PhutilArgumentUsageException( pht( 'You must use "--all" if you specify "--min-date".')); } // previous to PHP 5.1.0 you would compare with -1, instead of false if (false === $min_timestamp) { throw new PhutilArgumentUsageException( pht( "Supplied --min-date is not valid. See help for valid examples.\n". "Supplied value: '%s'\n", $min_date)); } } $commits = array(); if ($all_from_repo) { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIdentifiers(array($all_from_repo)) ->executeOne(); if (!$repository) { throw new PhutilArgumentUsageException( pht('Unknown repository "%s"!', $all_from_repo)); } $query = id(new DiffusionCommitQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRepository($repository); if ($min_timestamp) { $query->withEpochRange($min_timestamp, null); } if ($importing) { $query->withImporting(true); } $commits = $query->execute(); if (!$commits) { throw new PhutilArgumentUsageException( pht( 'No commits have been discovered in the "%s" repository!', $repository->getDisplayName())); } } else { $commits = $this->loadNamedCommits($reparse_what); } if (!$background) { PhabricatorWorker::setRunAllTasksInProcess(true); } $progress = new PhutilConsoleProgressBar(); $progress->setTotal(count($commits)); $tasks = array(); foreach ($commits as $commit) { $repository = $commit->getRepository(); if ($importing) { $status = $commit->getImportStatus(); // Find the first missing import step and queue that up. $reparse_message = false; $reparse_change = false; $reparse_publish = false; if (!($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE)) { $reparse_message = true; } else if (!($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE)) { $reparse_change = true; } else if (!($status & PhabricatorRepositoryCommit::IMPORTED_PUBLISH)) { $reparse_publish = true; } else { continue; } } $classes = array(); switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: if ($reparse_message) { $classes[] = 'PhabricatorRepositoryGitCommitMessageParserWorker'; } if ($reparse_change) { $classes[] = 'PhabricatorRepositoryGitCommitChangeParserWorker'; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: if ($reparse_message) { $classes[] = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; } if ($reparse_change) { $classes[] = 'PhabricatorRepositoryMercurialCommitChangeParserWorker'; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: if ($reparse_message) { $classes[] = 'PhabricatorRepositorySvnCommitMessageParserWorker'; } if ($reparse_change) { $classes[] = 'PhabricatorRepositorySvnCommitChangeParserWorker'; } break; } if ($reparse_publish) { $classes[] = 'PhabricatorRepositoryCommitPublishWorker'; } // NOTE: With "--importing", we queue the first unparsed step and let // it queue the other ones normally. Without "--importing", we queue // all the requested steps explicitly. $spec = array( 'commitID' => $commit->getID(), 'only' => !$importing, + 'via' => 'reparse', ); foreach ($classes as $class) { try { PhabricatorWorker::scheduleTask( $class, $spec, array( 'priority' => PhabricatorWorker::PRIORITY_IMPORT, + 'objectPHID' => $commit->getPHID(), + 'containerPHID' => $repository->getPHID(), )); } catch (PhabricatorWorkerPermanentFailureException $ex) { // See T13315. We expect some reparse steps to occasionally raise // permanent failures: for example, because they are no longer // reachable. This is a routine condition, not a catastrophic // failure, so let the user know something happened but continue // reparsing any remaining commits. echo tsprintf( "** %s ** %s\n", pht('WARN'), $ex->getMessage()); } } $progress->update(1); } $progress->done(); return 0; } } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php index e1990eaec3..5fe8e03f75 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php @@ -1,152 +1,184 @@ commit) { return $this->commit; } $commit_id = idx($this->getTaskData(), 'commitID'); if (!$commit_id) { throw new PhabricatorWorkerPermanentFailureException( pht('No "%s" in task data.', 'commitID')); } $commit = id(new DiffusionCommitQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($commit_id)) ->executeOne(); if (!$commit) { throw new PhabricatorWorkerPermanentFailureException( pht('Commit "%s" does not exist.', $commit_id)); } if ($commit->isUnreachable()) { throw new PhabricatorWorkerPermanentFailureException( pht( 'Commit "%s" (with internal ID "%s") is no longer reachable from '. 'any branch, tag, or ref in this repository, so it will not be '. 'imported. This usually means that the branch the commit was on '. 'was deleted or overwritten.', $commit->getMonogram(), $commit_id)); } $this->commit = $commit; return $commit; } final protected function doWork() { $commit = $this->loadCommit(); $repository = $commit->getRepository(); $this->repository = $repository; $this->parseCommit($repository, $this->commit); } - final protected function shouldQueueFollowupTasks() { + private function shouldQueueFollowupTasks() { return !idx($this->getTaskData(), 'only'); } + final protected function queueCommitTask($task_class) { + if (!$this->shouldQueueFollowupTasks()) { + return; + } + + $commit = $this->loadCommit(); + $repository = $commit->getRepository(); + + $data = array( + 'commitID' => $commit->getID(), + ); + + $task_data = $this->getTaskData(); + if (isset($task_data['via'])) { + $data['via'] = $task_data['via']; + } + + $options = array( + // We queue followup tasks at default priority so that the queue finishes + // work it has started before starting more work. If followups are queued + // at the same priority level, we do all message parses first, then all + // change parses, etc. This makes progress uneven. See T11677 for + // discussion. + 'priority' => parent::PRIORITY_DEFAULT, + + 'objectPHID' => $commit->getPHID(), + 'containerPHID' => $repository->getPHID(), + ); + + $this->queueTask($task_class, $data, $options); + } + protected function getImportStepFlag() { return null; } final protected function shouldSkipImportStep() { // If this step has already been performed and this is a "natural" task // which was queued by the normal daemons, decline to do the work again. // This mitigates races if commits are rapidly deleted and revived. $flag = $this->getImportStepFlag(); if (!$flag) { // This step doesn't have an associated flag. return false; } $commit = $this->commit; if (!$commit->isPartiallyImported($flag)) { // This commit doesn't have the flag set yet. return false; } if (!$this->shouldQueueFollowupTasks()) { // This task was queued by administrative tools, so do the work even // if it duplicates existing work. return false; } $this->log( "%s\n", pht( 'Skipping import step; this step was previously completed for '. 'this commit.')); return true; } abstract protected function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit); protected function loadCommitHint(PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = $commit->getRepository(); return id(new DiffusionCommitHintQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withOldCommitIdentifiers(array($commit->getCommitIdentifier())) ->executeOne(); } public function renderForDisplay(PhabricatorUser $viewer) { $suffix = parent::renderForDisplay($viewer); $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIDs(array(idx($this->getTaskData(), 'commitID'))) ->executeOne(); if (!$commit) { return $suffix; } $link = DiffusionView::linkCommit( $commit->getRepository(), $commit->getCommitIdentifier()); return array($link, $suffix); } final protected function loadCommitData(PhabricatorRepositoryCommit $commit) { if ($commit->hasCommitData()) { return $commit->getCommitData(); } $commit_id = $commit->getID(); $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit_id); if (!$data) { $data = id(new PhabricatorRepositoryCommitData()) ->setCommitID($commit_id); } $commit->attachCommitData($data); return $data; } final public function getViewer() { return PhabricatorUser::getOmnipotentUser(); } } diff --git a/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php b/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php index 452d02ae40..c3dd6f3d60 100644 --- a/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php +++ b/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php @@ -1,1237 +1,1245 @@ buildDiscoveredRepository('CHA'); $viewer = PhabricatorUser::getOmnipotentUser(); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepositoryIDs(array($repository->getID())) ->execute(); $this->expectChanges( $repository, $commits, array( // 8ebb73c add +x '8ebb73c3f127625ad090472f4f3bfc72804def54' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1389892449, ), array( '/file_moved', null, null, DifferentialChangeType::TYPE_CHANGE, DifferentialChangeType::FILE_NORMAL, 1, 1389892449, ), ), // ee9c790 add symlink 'ee9c7909e012da7d75e8e1293c7803a6e73ac26a' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1389892436, ), array( '/file_link', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_SYMLINK, 1, 1389892436, ), ), // 7260ca4 add directory file '7260ca4b6cec35e755bb5365c4ccdd3f1977772e' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1389892408, ), array( '/dir', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_DIRECTORY, 1, 1389892408, ), array( '/dir/subfile', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 1389892408, ), ), // 1fe783c move a file '1fe783cf207c1e5f3e01650d2d9cb80b8a707f0e' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1389892388, ), array( '/file', null, null, DifferentialChangeType::TYPE_MOVE_AWAY, DifferentialChangeType::FILE_NORMAL, 1, 1389892388, ), array( '/file_moved', '/file', '1fe783cf207c1e5f3e01650d2d9cb80b8a707f0e', DifferentialChangeType::TYPE_MOVE_HERE, DifferentialChangeType::FILE_NORMAL, 1, 1389892388, ), ), // 376af8c copy a file '376af8cd8f5b96ec55b7d9a86ccc85b8df8fb833' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1389892377, ), array( '/file', null, null, DifferentialChangeType::TYPE_COPY_AWAY, DifferentialChangeType::FILE_NORMAL, 0, 1389892377, ), array( '/file_copy', '/file', '376af8cd8f5b96ec55b7d9a86ccc85b8df8fb833', DifferentialChangeType::TYPE_COPY_HERE, DifferentialChangeType::FILE_NORMAL, 1, 1389892377, ), ), // ece6ea6 changed a file 'ece6ea6c6836e8b11a103e21707b8f30e6840c94' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1389892352, ), array( '/file', null, null, DifferentialChangeType::TYPE_CHANGE, DifferentialChangeType::FILE_NORMAL, 1, 1389892352, ), ), // 513103f added a file '513103f65b8413dd2f1a1b5c1d4852a4a598540f' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, // This is the initial commit and technically created this // directory; arguably the parser should figure this out and // mark this as a direct change. 0, 1389892330, ), array( '/file', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 1389892330, ), ), )); } public function testMercurialParser() { $this->requireBinaryForTest('hg'); $repository = $this->buildDiscoveredRepository('CHB'); $viewer = PhabricatorUser::getOmnipotentUser(); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepositoryIDs(array($repository->getID())) ->execute(); $this->expectChanges( $repository, $commits, array( '970357a2dc4264060e65d68e42240bb4e5984085' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1390249395, ), array( '/file_moved', null, null, DifferentialChangeType::TYPE_CHANGE, DifferentialChangeType::FILE_NORMAL, 1, 1390249395, ), ), 'fbb49af9788e5dbffbc05a060b680df1fd457be3' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1390249380, ), array( '/file_link', null, null, DifferentialChangeType::TYPE_ADD, // TODO: This is not correct, and should be FILE_SYMLINK. See // note in the parser about this. This is a known bug. DifferentialChangeType::FILE_NORMAL, 1, 1390249380, ), ), '0e8d3465944c7ed7a7c139da7edc652cf80dba69' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1390249342, ), array( '/dir', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_DIRECTORY, 1, 1390249342, ), array( '/dir/subfile', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 1390249342, ), ), '22c75131ff15c8a44d7a729c4542b7f4c8ed27f4' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1390249320, ), array( '/file', null, null, DifferentialChangeType::TYPE_MOVE_AWAY, DifferentialChangeType::FILE_NORMAL, 1, 1390249320, ), array( '/file_moved', '/file', '22c75131ff15c8a44d7a729c4542b7f4c8ed27f4', DifferentialChangeType::TYPE_MOVE_HERE, DifferentialChangeType::FILE_NORMAL, 1, 1390249320, ), ), 'd9d252df30cb7251ad3ea121eff30c7d2e36dd67' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1390249308, ), array( '/file', null, null, DifferentialChangeType::TYPE_COPY_AWAY, DifferentialChangeType::FILE_NORMAL, 0, 1390249308, ), array( '/file_copy', '/file', 'd9d252df30cb7251ad3ea121eff30c7d2e36dd67', DifferentialChangeType::TYPE_COPY_HERE, DifferentialChangeType::FILE_NORMAL, 1, 1390249308, ), ), '1fc0445d5e3d0f33e9dcbb68bbe419a847460d25' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1390249294, ), array( '/file', null, null, DifferentialChangeType::TYPE_CHANGE, DifferentialChangeType::FILE_NORMAL, 1, 1390249294, ), ), '61518e196efb7f80700333cc0d00634c2578871a' => array( array( '/', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_DIRECTORY, 1, 1390249286, ), array( '/file', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 1390249286, ), ), )); } public function testSubversionParser() { + $this->requireBinaryForTest('svn'); + $repository = $this->buildDiscoveredRepository('CHC'); $viewer = PhabricatorUser::getOmnipotentUser(); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepositoryIDs(array($repository->getID())) ->execute(); $this->expectChanges( $repository, $commits, array( '15' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 15, ), array( '/file_copy', null, null, DifferentialChangeType::TYPE_MULTICOPY, DifferentialChangeType::FILE_NORMAL, 1, 15, ), array( '/file_copy_x', '/file_copy', '12', DifferentialChangeType::TYPE_MOVE_HERE, DifferentialChangeType::FILE_NORMAL, 1, 15, ), array( '/file_copy_y', '/file_copy', '12', DifferentialChangeType::TYPE_MOVE_HERE, DifferentialChangeType::FILE_NORMAL, 1, 15, ), array( '/file_copy_z', '/file_copy', '12', DifferentialChangeType::TYPE_MOVE_HERE, DifferentialChangeType::FILE_NORMAL, 1, 15, ), ), // Add a file from a different revision '14' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 14, ), array( '/file', null, null, DifferentialChangeType::TYPE_COPY_AWAY, DifferentialChangeType::FILE_NORMAL, 0, 14, ), array( '/file_1', '/file', '1', DifferentialChangeType::TYPE_COPY_HERE, DifferentialChangeType::FILE_NORMAL, 1, 14, ), ), // Property change on "/" '13' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHANGE, DifferentialChangeType::FILE_DIRECTORY, 1, 13, ), ), // Copy a directory, removing and adding files to the copy '12' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 12, ), array( '/dir', null, null, // TODO: This might reasonbly be considered a bug in the parser; it // should probably be COPY_AWAY. DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 12, ), array( '/dir/a', null, null, DifferentialChangeType::TYPE_COPY_AWAY, DifferentialChangeType::FILE_NORMAL, 0, 12, ), array( '/dir/b', null, null, DifferentialChangeType::TYPE_COPY_AWAY, DifferentialChangeType::FILE_NORMAL, 0, 12, ), array( '/dir/subdir', null, null, DifferentialChangeType::TYPE_COPY_AWAY, DifferentialChangeType::FILE_DIRECTORY, 0, 12, ), array( '/dir/subdir/a', null, null, DifferentialChangeType::TYPE_COPY_AWAY, DifferentialChangeType::FILE_NORMAL, 0, 12, ), array( '/dir/subdir/b', null, null, DifferentialChangeType::TYPE_COPY_AWAY, DifferentialChangeType::FILE_NORMAL, 0, 12, ), array( '/dir_copy', '/dir', '11', DifferentialChangeType::TYPE_COPY_HERE, DifferentialChangeType::FILE_DIRECTORY, 1, 12, ), array( '/dir_copy/a', '/dir/a', '11', DifferentialChangeType::TYPE_COPY_HERE, DifferentialChangeType::FILE_NORMAL, 1, 12, ), array( '/dir_copy/b', '/dir/b', '11', DifferentialChangeType::TYPE_COPY_HERE, DifferentialChangeType::FILE_NORMAL, 1, 12, ), array( '/dir_copy/subdir', '/dir/subdir', '11', DifferentialChangeType::TYPE_COPY_HERE, DifferentialChangeType::FILE_DIRECTORY, 1, 12, ), array( '/dir_copy/subdir/a', '/dir/subdir/a', '11', DifferentialChangeType::TYPE_COPY_HERE, DifferentialChangeType::FILE_NORMAL, 1, 12, ), array( '/dir_copy/subdir/b', '/dir/subdir/b', '11', DifferentialChangeType::TYPE_DELETE, DifferentialChangeType::FILE_NORMAL, 1, 12, ), array( '/dir_copy/subdir/c', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 12, ), ), // Add a directory with a subdirectory and files, sets up next commit '11' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 11, ), array( '/dir', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_DIRECTORY, 1, 11, ), array( '/dir/a', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 11, ), array( '/dir/b', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 11, ), array( '/dir/subdir', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_DIRECTORY, 1, 11, ), array( '/dir/subdir/a', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 11, ), array( '/dir/subdir/b', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 11, ), ), // Remove directory '10' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 10, ), array( '/dir', null, null, DifferentialChangeType::TYPE_DELETE, DifferentialChangeType::FILE_DIRECTORY, 1, 10, ), array( '/dir/subfile', null, null, DifferentialChangeType::TYPE_DELETE, DifferentialChangeType::FILE_NORMAL, 1, 10, ), ), // Replace directory with file '9' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 9, ), array( '/file_moved', null, null, DifferentialChangeType::TYPE_CHANGE, DifferentialChangeType::FILE_DIRECTORY, 1, 9, ), ), // Replace file with file '8' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 8, ), array( '/file_moved', null, null, DifferentialChangeType::TYPE_CHANGE, DifferentialChangeType::FILE_NORMAL, 1, 8, ), ), '7' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 7, ), array( '/file_moved', null, null, DifferentialChangeType::TYPE_CHANGE, DifferentialChangeType::FILE_NORMAL, 1, 7, ), ), '6' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 6, ), array( '/file_link', null, null, DifferentialChangeType::TYPE_ADD, // TODO: This is not correct, and should be FILE_SYMLINK. DifferentialChangeType::FILE_NORMAL, 1, 6, ), ), '5' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 5, ), array( '/dir', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_DIRECTORY, 1, 5, ), array( '/dir/subfile', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 5, ), ), '4' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 4, ), array( '/file', null, null, DifferentialChangeType::TYPE_MOVE_AWAY, DifferentialChangeType::FILE_NORMAL, 1, 4, ), array( '/file_moved', '/file', '2', DifferentialChangeType::TYPE_MOVE_HERE, DifferentialChangeType::FILE_NORMAL, 1, 4, ), ), '3' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 3, ), array( '/file', null, null, DifferentialChangeType::TYPE_COPY_AWAY, DifferentialChangeType::FILE_NORMAL, 0, 3, ), array( '/file_copy', '/file', '2', DifferentialChangeType::TYPE_COPY_HERE, DifferentialChangeType::FILE_NORMAL, 1, 3, ), ), '2' => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 2, ), array( '/file', null, null, DifferentialChangeType::TYPE_CHANGE, DifferentialChangeType::FILE_NORMAL, 1, 2, ), ), '1' => array( array( '/', null, null, // The Git and Svn parsers don't recognize the first commit as // creating "/", while the Mercurial parser does. All the parsers // should probably behave like the Mercurial parser. DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 1, ), array( '/file', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 1, ), ), )); } public function testSubversionPartialParser() { + $this->requireBinaryForTest('svn'); + $repository = $this->buildBareRepository('CHD'); $repository->setDetail('svn-subpath', 'trunk/'); id(new PhabricatorRepositoryPullEngine()) ->setRepository($repository) ->pullRepository(); id(new PhabricatorRepositoryDiscoveryEngine()) ->setRepository($repository) ->discoverCommits(); $viewer = PhabricatorUser::getOmnipotentUser(); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepositoryIDs(array($repository->getID())) ->execute(); $this->expectChanges( $repository, $commits, array( // Copy of a file outside of the subpath from an earlier revision // into the subpath. 4 => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 4, ), array( '/goat', null, null, DifferentialChangeType::TYPE_COPY_AWAY, DifferentialChangeType::FILE_NORMAL, 0, 4, ), array( '/trunk', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 4, ), array( '/trunk/goat', '/goat', '1', DifferentialChangeType::TYPE_COPY_HERE, DifferentialChangeType::FILE_NORMAL, 1, 4, ), ), 3 => array( array( '/', null, null, DifferentialChangeType::TYPE_CHILD, DifferentialChangeType::FILE_DIRECTORY, 0, 3, ), array( '/trunk', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_DIRECTORY, 1, 3, ), array( '/trunk/apple', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 3, ), array( '/trunk/banana', null, null, DifferentialChangeType::TYPE_ADD, DifferentialChangeType::FILE_NORMAL, 1, 3, ), ), )); } public function testSubversionValidRootParser() { + $this->requireBinaryForTest('svn'); + // First, automatically configure the root correctly. $repository = $this->buildBareRepository('CHD'); id(new PhabricatorRepositoryPullEngine()) ->setRepository($repository) ->pullRepository(); $caught = null; try { id(new PhabricatorRepositoryDiscoveryEngine()) ->setRepository($repository) ->discoverCommits(); } catch (Exception $ex) { $caught = $ex; } $this->assertFalse( ($caught instanceof Exception), pht('Natural SVN root should work properly.')); // This time, artificially break the root. We expect this to fail. $repository = $this->buildBareRepository('CHD'); $repository->setDetail( 'remote-uri', $repository->getDetail('remote-uri').'trunk/'); id(new PhabricatorRepositoryPullEngine()) ->setRepository($repository) ->pullRepository(); $caught = null; try { id(new PhabricatorRepositoryDiscoveryEngine()) ->setRepository($repository) ->discoverCommits(); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue( ($caught instanceof Exception), pht('Artificial SVN root should fail.')); } public function testSubversionForeignStubsParser() { + $this->requireBinaryForTest('svn'); + $repository = $this->buildBareRepository('CHE'); $repository->setDetail('svn-subpath', 'branch/'); id(new PhabricatorRepositoryPullEngine()) ->setRepository($repository) ->pullRepository(); id(new PhabricatorRepositoryDiscoveryEngine()) ->setRepository($repository) ->discoverCommits(); $viewer = PhabricatorUser::getOmnipotentUser(); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepositoryIDs(array($repository->getID())) ->execute(); foreach ($commits as $commit) { $this->parseCommit($repository, $commit); } // As a side effect, we expect parsing these commits to have created // foreign stubs of other commits. $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepositoryIDs(array($repository->getID())) ->execute(); $commits = mpull($commits, null, 'getCommitIdentifier'); $this->assertTrue( isset($commits['2']), pht('Expect %s to exist as a foreign stub.', 'rCHE2')); // The foreign stub should be marked imported. $commit = $commits['2']; $this->assertEqual( PhabricatorRepositoryCommit::IMPORTED_ALL, (int)$commit->getImportStatus()); } private function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $parser = 'PhabricatorRepositoryGitCommitChangeParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $parser = 'PhabricatorRepositoryMercurialCommitChangeParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $parser = 'PhabricatorRepositorySvnCommitChangeParserWorker'; break; default: throw new Exception(pht('No support yet.')); } $parser_object = newv($parser, array(array())); return $parser_object->parseChangesForUnitTest($repository, $commit); } private function expectChanges( PhabricatorRepository $repository, array $commits, array $expect) { foreach ($commits as $commit) { $commit_identifier = $commit->getCommitIdentifier(); $expect_changes = idx($expect, $commit_identifier); if ($expect_changes === null) { $this->assertEqual( $commit_identifier, null, pht( 'No test entry for commit "%s" in repository "%s"!', $commit_identifier, $repository->getDisplayName())); } $changes = $this->parseCommit($repository, $commit); $path_map = id(new DiffusionPathQuery()) ->withPathIDs(mpull($changes, 'getPathID')) ->execute(); $path_map = ipull($path_map, 'path'); $target_commits = array_filter(mpull($changes, 'getTargetCommitID')); if ($target_commits) { $commits = id(new DiffusionCommitQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs($target_commits) ->execute(); $target_commits = mpull($commits, 'getCommitIdentifier', 'getID'); } $dicts = array(); foreach ($changes as $key => $change) { $target_path = idx($path_map, $change->getTargetPathID()); $target_commit = idx($target_commits, $change->getTargetCommitID()); $dicts[$key] = array( $path_map[(int)$change->getPathID()], $target_path, $target_commit ? (string)$target_commit : null, (int)$change->getChangeType(), (int)$change->getFileType(), (int)$change->getIsDirect(), (int)$change->getCommitSequence(), ); } $dicts = ipull($dicts, null, 0); $expect_changes = ipull($expect_changes, null, 0); ksort($dicts); ksort($expect_changes); $this->assertEqual( $expect_changes, $dicts, pht('Commit %s', $commit_identifier)); } } } diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php index 0e135cb66f..577f620dde 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php @@ -1,156 +1,149 @@ log("%s\n", pht('Parsing "%s"...', $commit->getMonogram())); $hint = $this->loadCommitHint($commit); if ($hint && $hint->isUnreadable()) { $this->log( pht( 'This commit is marked as unreadable, so changes will not be '. 'parsed.')); return; } if (!$this->shouldSkipImportStep()) { $results = $this->parseCommitChanges($repository, $commit); if ($results) { $this->writeCommitChanges($repository, $commit, $results); } $commit->writeImportStatusFlag($this->getImportStepFlag()); PhabricatorSearchWorker::queueDocumentForIndexing($commit->getPHID()); } $this->finishParse(); } public function parseChangesForUnitTest( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { return $this->parseCommitChanges($repository, $commit); } public static function lookupOrCreatePaths(array $paths) { $repository = new PhabricatorRepository(); $conn_w = $repository->establishConnection('w'); $result_map = self::lookupPaths($paths); $missing_paths = array_fill_keys($paths, true); $missing_paths = array_diff_key($missing_paths, $result_map); $missing_paths = array_keys($missing_paths); if ($missing_paths) { foreach (array_chunk($missing_paths, 128) as $path_chunk) { $sql = array(); foreach ($path_chunk as $path) { $sql[] = qsprintf($conn_w, '(%s, %s)', $path, md5($path)); } queryfx( $conn_w, 'INSERT IGNORE INTO %T (path, pathHash) VALUES %LQ', PhabricatorRepository::TABLE_PATH, $sql); } $result_map += self::lookupPaths($missing_paths); } return $result_map; } private static function lookupPaths(array $paths) { $repository = new PhabricatorRepository(); $conn_w = $repository->establishConnection('w'); $result_map = array(); foreach (array_chunk($paths, 128) as $path_chunk) { $chunk_map = queryfx_all( $conn_w, 'SELECT path, id FROM %T WHERE pathHash IN (%Ls)', PhabricatorRepository::TABLE_PATH, array_map('md5', $path_chunk)); foreach ($chunk_map as $row) { $result_map[$row['path']] = $row['id']; } } return $result_map; } protected function finishParse() { - $commit = $this->commit; - if ($this->shouldQueueFollowupTasks()) { - $this->queueTask( - 'PhabricatorRepositoryCommitPublishWorker', - array( - 'commitID' => $commit->getID(), - )); - } + $this->queueCommitTask('PhabricatorRepositoryCommitPublishWorker'); } private function writeCommitChanges( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, array $changes) { $conn = $repository->establishConnection('w'); $repository_id = (int)$repository->getID(); $commit_id = (int)$commit->getID(); $changes_sql = array(); foreach ($changes as $change) { $changes_sql[] = qsprintf( $conn, '(%d, %d, %d, %nd, %nd, %d, %d, %d, %d)', $repository_id, (int)$change->getPathID(), $commit_id, nonempty((int)$change->getTargetPathID(), null), nonempty((int)$change->getTargetCommitID(), null), (int)$change->getChangeType(), (int)$change->getFileType(), (int)$change->getIsDirect(), (int)$change->getCommitSequence()); } queryfx( $conn, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit_id); foreach (PhabricatorLiskDAO::chunkSQL($changes_sql) as $chunk) { queryfx( $conn, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) VALUES %LQ', PhabricatorRepository::TABLE_PATHCHANGE, $chunk); } } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index ea277a84c9..a0ea2d84b2 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -1,132 +1,118 @@ shouldSkipImportStep()) { $viewer = $this->getViewer(); $ref = $commit->newCommitRef($viewer); $data = $this->loadCommitData($commit); $data->setCommitRef($ref); $this->updateCommitData($commit, $data); } - if ($this->shouldQueueFollowupTasks()) { - $this->queueTask( - $this->getFollowupTaskClass(), - array( - 'commitID' => $commit->getID(), - ), - array( - // We queue followup tasks at default priority so that the queue - // finishes work it has started before starting more work. If - // followups are queued at the same priority level, we do all - // message parses first, then all change parses, etc. This makes - // progress uneven. See T11677 for discussion. - 'priority' => PhabricatorWorker::PRIORITY_DEFAULT, - )); - } + $this->queueCommitTask($this->getFollowupTaskClass()); } final protected function updateCommitData( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { $ref = $data->getCommitRef(); $viewer = $this->getViewer(); $author = $ref->getAuthor(); $committer = $ref->getCommitter(); $has_committer = (bool)strlen($committer); $identity_engine = id(new DiffusionRepositoryIdentityEngine()) ->setViewer($viewer) ->setSourcePHID($commit->getPHID()); // See T13538. It is possible to synthetically construct a Git commit with // no author and arrive here with NULL for the author value. // This is distinct from a commit with an empty author. Because both these // cases are degenerate and we can't resolve NULL into an identity, cast // NULL to the empty string and merge the flows. $author = phutil_string_cast($author); $author_identity = $identity_engine->newResolvedIdentity($author); if ($has_committer) { $committer_identity = $identity_engine->newResolvedIdentity($committer); } else { $committer_identity = null; } $data->setAuthorName(id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(255) ->truncateString((string)$author)); $data->setCommitDetail('authorEpoch', $ref->getAuthorEpoch()); $data->setCommitDetail('authorName', $ref->getAuthorName()); $data->setCommitDetail('authorEmail', $ref->getAuthorEmail()); $data->setCommitDetail( 'authorIdentityPHID', $author_identity->getPHID()); $data->setCommitDetail( 'authorPHID', $author_identity->getCurrentEffectiveUserPHID()); // See T13538. It is possible to synthetically construct a Git commit with // no message. As above, treat this as though it is the same as the empty // message. $message = $ref->getMessage(); $message = phutil_string_cast($message); $data->setCommitMessage($message); if ($has_committer) { $data->setCommitDetail('committer', $committer); $data->setCommitDetail('committerName', $ref->getCommitterName()); $data->setCommitDetail('committerEmail', $ref->getCommitterEmail()); $data->setCommitDetail( 'committerPHID', $committer_identity->getCurrentEffectiveUserPHID()); $data->setCommitDetail( 'committerIdentityPHID', $committer_identity->getPHID()); $commit->setCommitterIdentityPHID($committer_identity->getPHID()); } $repository = $this->repository; $author_phid = $data->getCommitDetail('authorPHID'); $committer_phid = $data->getCommitDetail('committerPHID'); if ($author_phid != $commit->getAuthorPHID()) { $commit->setAuthorPHID($author_phid); } $commit->setAuthorIdentityPHID($author_identity->getPHID()); $commit->setSummary($data->getSummary()); $commit->save(); $data->save(); $commit->writeImportStatusFlag( PhabricatorRepositoryCommit::IMPORTED_MESSAGE); } }