diff --git a/.arcconfig b/.arcconfig --- a/.arcconfig +++ b/.arcconfig @@ -1,6 +1,5 @@ { - "project_id" : "arcanist", - "conduit_uri" : "https://secure.phabricator.com/", + "phabricator.uri" : "https://secure.phabricator.com/", "lint.engine" : "PhutilLintEngine", "unit.engine" : "PhutilUnitTestEngine", "load" : [ diff --git a/scripts/arcanist.php b/scripts/arcanist.php --- a/scripts/arcanist.php +++ b/scripts/arcanist.php @@ -204,8 +204,8 @@ if ($force_conduit) { $conduit_uri = $force_conduit; } else { - $project_conduit_uri = - $configuration_manager->getProjectConfig('conduit_uri'); + $project_conduit_uri = $configuration_manager->getProjectConfig( + 'phabricator.uri'); if ($project_conduit_uri) { $conduit_uri = $project_conduit_uri; } else { diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -205,7 +205,7 @@ 'ArcanistBundleTestCase' => 'ArcanistTestCase', 'ArcanistCSSLintLinter' => 'ArcanistExternalLinter', 'ArcanistCSSLintLinterTestCase' => 'ArcanistArcanistLinterTestCase', - 'ArcanistCSharpLinter' => 'ArcanistFutureLinter', + 'ArcanistCSharpLinter' => 'ArcanistLinter', 'ArcanistCallConduitWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistCapabilityNotSupportedException' => 'Exception', 'ArcanistChooseInvalidRevisionException' => 'Exception', diff --git a/src/configuration/ArcanistSettings.php b/src/configuration/ArcanistSettings.php --- a/src/configuration/ArcanistSettings.php +++ b/src/configuration/ArcanistSettings.php @@ -32,6 +32,32 @@ 'unit test engines.', 'example' => '["/var/arc/customlib/src"]', ), + 'repository.callsign' => array( + 'type' => 'string', + 'example' => '"X"', + 'help' => pht( + 'Associate the working copy with a specific Phabricator repository. '. + 'Normally, arc can figure this association out on its own, but if '. + 'your setup is unusual you can use this option to tell it what the '. + 'desired value is.'), + ), + 'phabricator.uri' => array( + 'type' => 'string', + 'legacy' => 'conduit_uri', + 'example' => '"https://phabricator.mycompany.com/"', + 'help' => pht( + 'Associates this working copy with a specific installation of '. + 'Phabricator.'), + ), + 'project.name' => array( + 'type' => 'string', + 'legacy' => 'project_id', + 'example' => '"arcanist"', + 'help' => pht( + 'Associates this working copy with a named Arcanist Project. '. + 'This is primarily useful if you use SVN and have several different '. + 'projects in the same repository.'), + ), 'lint.engine' => array( 'type' => 'string', 'legacy' => 'lint_engine', diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -369,6 +369,18 @@ if (preg_match('/^\* ([^\(].*)$/m', $stdout, $matches)) { return $matches[1]; } + + return null; + } + + public function getRemoteURI() { + list($stdout) = $this->execxLocal('remote show -n origin'); + + $matches = null; + if (preg_match('/^\s*Fetch URL: (.*)$/m', $stdout, $matches)) { + return trim($matches[1]); + } + return null; } diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -1038,4 +1038,16 @@ return array(trim($name), trim($rev)); } + + public function getRemoteURI() { + list($stdout) = $this->execxLocal('paths default'); + + $stdout = trim($stdout); + if (strlen($stdout)) { + return $stdout; + } + + return null; + } + } diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -330,6 +330,8 @@ abstract public function loadWorkingCopyDifferentialRevisions( ConduitClient $conduit, array $query); + abstract public function getRemoteURI(); + public function getUnderlyingWorkingCopyRevision() { return $this->getWorkingCopyRevision(); @@ -643,4 +645,8 @@ return $commit; } + public function getRepositoryUUID() { + return null; + } + } diff --git a/src/repository/api/ArcanistSubversionAPI.php b/src/repository/api/ArcanistSubversionAPI.php --- a/src/repository/api/ArcanistSubversionAPI.php +++ b/src/repository/api/ArcanistSubversionAPI.php @@ -243,6 +243,10 @@ return 'svn'; } + public function getRemoteURI() { + return idx($this->getSVNInfo('/'), 'Repository Root'); + } + public function buildInfoFuture($path) { if ($path == '/') { // When the root of a working copy is referenced by a symlink and you @@ -587,7 +591,7 @@ return null; } - public function getRepositorySVNUUID() { + public function getRepositoryUUID() { $info = $this->getSVNInfo('/'); return $info['Repository UUID']; } diff --git a/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php b/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php --- a/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php +++ b/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php @@ -87,6 +87,7 @@ 'UNSTAGED' => $f_mod | $f_uns | $f_unc, 'UNTRACKED' => $f_unt, ); + $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); $expect_range = array( diff --git a/src/workflow/ArcanistBaseWorkflow.php b/src/workflow/ArcanistBaseWorkflow.php --- a/src/workflow/ArcanistBaseWorkflow.php +++ b/src/workflow/ArcanistBaseWorkflow.php @@ -31,7 +31,8 @@ * * @task conduit Conduit * @task scratch Scratch Files - * @group workflow + * @task phabrep Phabricator Repositories + * * @stable */ abstract class ArcanistBaseWorkflow extends Phobject { @@ -64,6 +65,8 @@ private $stashed; private $projectInfo; + private $repositoryInfo; + private $repositoryReasons; private $arcanistConfiguration; private $parentWorkflow; @@ -1518,4 +1521,187 @@ return $this->repositoryVersion; } + +/* -( Phabricator Repositories )------------------------------------------- */ + + + /** + * Get the PHID of the Phabricator repository this working copy corresponds + * to. Returns `null` no repository can be identified. + * + * @return phid|null Repository PHID, or null if no repository can be + * identified. + * + * @task phabrep + */ + protected function getRepositoryPHID() { + return idx($this->getRepositoryInformation(), 'phid'); + } + + + /** + * Get the callsign of the Phabricator repository this working copy + * corresponds to. Returns `null` no repository can be identified. + * + * @return string|null Repository callsign, or null if no repository can be + * identified. + * + * @task phabrep + */ + protected function getRepositoryCallsign() { + return idx($this->getRepositoryInformation(), 'callsign'); + } + + + /** + * Get human-readable reasoning explaining how `arc` evaluated which + * Phabricator repository corresponds to this working copy. Used by + * `arc which` to explain the process to users. + * + * @return list Human-readable explanation of the repository + * association process. + * + * @task phabrep + */ + protected function getRepositoryReasons() { + $this->getRepositoryInformation(); + return $this->repositoryReasons; + } + + + /** + * @task phabrep + */ + private function getRepositoryInformation() { + if ($this->repositoryInfo === null) { + list($info, $reasons) = $this->loadRepositoryInformation(); + $this->repositoryInfo = $info; + $this->repositoryReasons = $reasons; + } + + return $this->repositoryInfo; + } + + + /** + * @task phabrep + */ + private function loadRepositoryInformation() { + list($query, $reasons) = $this->getRepositoryQuery(); + if (!$query) { + return array(null, $reasons); + } + + try { + $results = $this->getConduit()->callMethodSynchronous( + 'repository.query', + $query); + } catch (ConduitClientException $ex) { + if ($ex->getErrorCode() == 'ERR-CONDUIT-CALL') { + $reasons[] = pht( + 'This version of Arcanist is more recent than the version of '. + 'Phabricator you are connecting to: the Phabricator install is '. + 'out of date and does not have support for identifying '. + 'repositories by callsign or URI. Update Phabricator to enable '. + 'these features.'); + return array(null, $reasons); + } + throw $ex; + } + + $result = null; + if (!$results) { + $reasons[] = pht( + 'No repositories matched the query. Check that your configuration '. + 'is correct, or use "repository.callsign" to select a repository '. + 'explicitly.'); + } else if (count($results) > 1) { + $reasons[] = pht( + 'Multiple repostories (%s) matched the query. You can use the '. + '"repository.callsign" configuration to select the one you want.', + implode(', ', ipull($results, 'callsign'))); + } else { + $result = head($results); + $reasons[] = pht('Found a unique matching repository.'); + } + + return array($result, $reasons); + } + + + /** + * @task phabrep + */ + private function getRepositoryQuery() { + $reasons = array(); + + $callsign = $this->getConfigFromAnySource('repository.callsign'); + if ($callsign) { + $query = array( + 'callsigns' => array($callsign), + ); + $reasons[] = pht( + 'Configuration value "repository.callsign" is set to "%s".', + $callsign); + return array($query, $reasons); + } else { + $reasons[] = pht( + 'Configuration value "repository.callsign" is empty.'); + } + + $project_info = $this->getProjectInfo(); + if ($this->getProjectInfo()) { + if (!empty($project_info['repository']['callsign'])) { + $callsign = $project_info['repository']['callsign']; + $query = array( + 'callsigns' => array($callsign), + ); + $reasons[] = pht( + 'Configuration value "project.id" is set to "%s"; this project '. + 'is associated with the "%s" repository.', + $this->getWorkingCopy()->getProjectID(), + $callsign); + return array($query, $reasons); + } else { + $reasons[] = pht( + 'Configuration value "project.id" is set to "%s", but this '. + 'project is not associated with a repository.'); + } + } else { + $reasons[] = pht( + 'Configuration value "project.id" is empty.'); + } + + $uuid = $this->getRepositoryAPI()->getRepositoryUUID(); + if ($uuid !== null) { + $query = array( + 'uuids' => array($uuid), + ); + $reasons[] = pht( + 'The UUID for this working copy is "%s".', + $uuid); + return array($query, $reasons); + } else { + $reasons[] = pht( + 'This repository has no VCS UUID (this is normal for git/hg).'); + } + + $remote_uri = $this->getRepositoryAPI()->getRemoteURI(); + if ($remote_uri !== null) { + $query = array( + 'remoteURIs' => array($remote_uri), + ); + $reasons[] = pht( + 'The remote URI for this working copy is "%s".', + $remote_uri); + return array($query, $reasons); + } else { + $reasons[] = pht( + 'Unable to determine the remote URI for this repository.'); + } + + return array(null, $reasons); + } + + } diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php --- a/src/workflow/ArcanistDiffWorkflow.php +++ b/src/workflow/ArcanistDiffWorkflow.php @@ -508,9 +508,9 @@ } $diff_spec = array( - 'changes' => mpull($changes, 'toDictionary'), - 'lintStatus' => $this->getLintStatus($lint_result), - 'unitStatus' => $this->getUnitStatus($unit_result), + 'changes' => mpull($changes, 'toDictionary'), + 'lintStatus' => $this->getLintStatus($lint_result), + 'unitStatus' => $this->getUnitStatus($unit_result), ) + $this->buildDiffSpecification(); $conduit = $this->getConduit(); @@ -2243,6 +2243,7 @@ $vcs = $repository_api->getSourceControlSystemName(); $source_path = $repository_api->getPath(); $branch = $repository_api->getBranchName(); + $repo_uuid = $repository_api->getRepositoryUUID(); if ($repository_api instanceof ArcanistGitAPI) { $info = $this->getGitParentLogInfo(); @@ -2258,8 +2259,6 @@ if ($info['uuid']) { $repo_uuid = $info['uuid']; } - } else if ($repository_api instanceof ArcanistSubversionAPI) { - $repo_uuid = $repository_api->getRepositorySVNUUID(); } else if ($repository_api instanceof ArcanistMercurialAPI) { $bookmark = $repository_api->getActiveBookmark(); @@ -2280,7 +2279,7 @@ $project_id = $this->getWorkingCopy()->getProjectID(); } - return array( + $data = array( 'sourceMachine' => php_uname('n'), 'sourcePath' => $source_path, 'branch' => $branch, @@ -2288,12 +2287,16 @@ 'sourceControlSystem' => $vcs, 'sourceControlPath' => $base_path, 'sourceControlBaseRevision' => $base_revision, - 'parentRevisionID' => $parent, - 'repositoryUUID' => $repo_uuid, 'creationMethod' => 'arc', 'arcanistProject' => $project_id, - 'authorPHID' => $this->getUserPHID(), ); + + $repository_phid = $this->getRepositoryPHID(); + if ($repository_phid) { + $data['repositoryPHID'] = $repository_phid; + } + + return $data; } diff --git a/src/workflow/ArcanistWhichWorkflow.php b/src/workflow/ArcanistWhichWorkflow.php --- a/src/workflow/ArcanistWhichWorkflow.php +++ b/src/workflow/ArcanistWhichWorkflow.php @@ -22,7 +22,8 @@ public function getCommandHelp() { return phutil_console_format(<<printRepositorySection(); + $console->writeOut("\n"); + $repository_api = $this->getRepositoryAPI(); $arg_commit = $this->getArgument('commit'); @@ -184,4 +190,34 @@ return 0; } + + private function printRepositorySection() { + $console = PhutilConsole::getConsole(); + $console->writeOut("**%s**\n", pht('REPOSITORY')); + + $callsign = $this->getRepositoryCallsign(); + + $console->writeOut( + "%s\n\n", + pht( + 'To identify the repository associated with this working copy, '. + 'arc followed this process:')); + + foreach ($this->getRepositoryReasons() as $reason) { + $reason = phutil_console_wrap($reason, 4); + $console->writeOut("%s\n\n", $reason); + } + + if ($callsign) { + $console->writeOut( + "%s\n", + pht('This working copy is associated with the %s repository.', + phutil_console_format('**%s**', $callsign))); + } else { + $console->writeOut( + "%s\n", + pht('This working copy is not associated with any repository.')); + } + } + } diff --git a/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php b/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php --- a/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php +++ b/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php @@ -187,6 +187,7 @@ private static function parseRawConfigFile($raw_config, $from_where) { $proj = json_decode($raw_config, true); + if (!is_array($proj)) { throw new Exception( "Unable to parse '.arcconfig' file '{$from_where}'. The file contents ". @@ -194,16 +195,7 @@ "FILE CONTENTS\n". substr($raw_config, 0, 2048)); } - $required_keys = array( - 'project_id', - ); - foreach ($required_keys as $key) { - if (!array_key_exists($key, $proj)) { - throw new Exception( - "Required key '{$key}' is missing from '.arcconfig' file ". - "'{$from_where}'."); - } - } + return $proj; } @@ -213,6 +205,12 @@ } public function getProjectID() { + $project_id = $this->getProjectConfig('project.name'); + if ($project_id) { + return $project_id; + } + + // This is an older name for the setting. return $this->getProjectConfig('project_id'); }