mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-10 00:42:40 +01:00
Allow arc
to identify repositories without "project_id"
Summary: Ref T4343. Continues the process of reducing the prominence of Arcanist Projects. Primarily: - Query Phabricator to identify the working copy based on explicit configuration, or guess based on heuristics. - Enhance `arc which` to explain the process to the user. - The `project_id` key is no longer required in `.arcconfig`. Minor/cleanup changes: - Rename `project_id` to `project.name` (consistency, clarity). - Rename `conduit_uri` to `phabricator.uri` (consistency, clairty). - These both need documentation updates. - Add `repository.callsign` to explicitly bind to a repository. - Updated `.arcconfig` for the new values. - Fix a unit test which broke a while ago when we fixed a rare definition of "unstaged". - Make `getRepositoryUUID()` generic so we can get rid of one `instanceof`. Test Plan: - Ran `arc which`. - Ran `arc diff`. - This doesn't really change anything, so the only real risk is version compatibility breaks. This //does// introduce such a break, but the window is very narrow: if you upgrade `arc` after this commit, and try to diff against a Phabricator which was updated after yesterday (D8068) but before D8072 lands, the lookup will work so we'll add `repositoryPHID` to the `differential.creatediff` call, but it won't exist in Phabricator yet. This window is so narrow that I'm not going to try to fix it, as I'd guess there is a significant chance that no users will be affected. I don't see a clever way to fix it that doesn't involve a lot of work, either. Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T4343 Differential Revision: https://secure.phabricator.com/D8073
This commit is contained in:
parent
e4b1e8e681
commit
a7376624b4
13 changed files with 310 additions and 27 deletions
|
@ -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" : [
|
||||
|
|
|
@ -204,8 +204,8 @@ try {
|
|||
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 {
|
||||
|
|
|
@ -205,7 +205,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistBundleTestCase' => 'ArcanistTestCase',
|
||||
'ArcanistCSSLintLinter' => 'ArcanistExternalLinter',
|
||||
'ArcanistCSSLintLinterTestCase' => 'ArcanistArcanistLinterTestCase',
|
||||
'ArcanistCSharpLinter' => 'ArcanistFutureLinter',
|
||||
'ArcanistCSharpLinter' => 'ArcanistLinter',
|
||||
'ArcanistCallConduitWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistCapabilityNotSupportedException' => 'Exception',
|
||||
'ArcanistChooseInvalidRevisionException' => 'Exception',
|
||||
|
|
|
@ -32,6 +32,32 @@ final class ArcanistSettings {
|
|||
'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',
|
||||
|
|
|
@ -369,6 +369,18 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1038,4 +1038,16 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -330,6 +330,8 @@ abstract class ArcanistRepositoryAPI {
|
|||
abstract public function loadWorkingCopyDifferentialRevisions(
|
||||
ConduitClient $conduit,
|
||||
array $query);
|
||||
abstract public function getRemoteURI();
|
||||
|
||||
|
||||
public function getUnderlyingWorkingCopyRevision() {
|
||||
return $this->getWorkingCopyRevision();
|
||||
|
@ -643,4 +645,8 @@ abstract class ArcanistRepositoryAPI {
|
|||
return $commit;
|
||||
}
|
||||
|
||||
public function getRepositoryUUID() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -243,6 +243,10 @@ final class ArcanistSubversionAPI extends ArcanistRepositoryAPI {
|
|||
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 @@ EODIFF;
|
|||
return null;
|
||||
}
|
||||
|
||||
public function getRepositorySVNUUID() {
|
||||
public function getRepositoryUUID() {
|
||||
$info = $this->getSVNInfo('/');
|
||||
return $info['Repository UUID'];
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@ final class ArcanistRepositoryAPIStateTestCase extends ArcanistTestCase {
|
|||
'UNSTAGED' => $f_mod | $f_uns | $f_unc,
|
||||
'UNTRACKED' => $f_unt,
|
||||
);
|
||||
|
||||
$this->assertEqual($expect_uncommitted, $api->getUncommittedStatus());
|
||||
|
||||
$expect_range = array(
|
||||
|
|
|
@ -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 @@ abstract class ArcanistBaseWorkflow extends Phobject {
|
|||
private $stashed;
|
||||
|
||||
private $projectInfo;
|
||||
private $repositoryInfo;
|
||||
private $repositoryReasons;
|
||||
|
||||
private $arcanistConfiguration;
|
||||
private $parentWorkflow;
|
||||
|
@ -1518,4 +1521,187 @@ abstract class ArcanistBaseWorkflow extends Phobject {
|
|||
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<string> 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -2243,6 +2243,7 @@ EOTEXT
|
|||
$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 @@ EOTEXT
|
|||
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 @@ EOTEXT
|
|||
$project_id = $this->getWorkingCopy()->getProjectID();
|
||||
}
|
||||
|
||||
return array(
|
||||
$data = array(
|
||||
'sourceMachine' => php_uname('n'),
|
||||
'sourcePath' => $source_path,
|
||||
'branch' => $branch,
|
||||
|
@ -2288,12 +2287,16 @@ EOTEXT
|
|||
'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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -22,7 +22,8 @@ EOTEXT
|
|||
public function getCommandHelp() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
Supports: svn, git, hg
|
||||
Shows which commits 'arc diff' will select, and which revision is in
|
||||
Shows which repository the current working copy corresponds to,
|
||||
which commits 'arc diff' will select, and which revision is in
|
||||
the working copy (or which revisions, if more than one matches).
|
||||
EOTEXT
|
||||
);
|
||||
|
@ -69,6 +70,11 @@ EOTEXT
|
|||
|
||||
public function run() {
|
||||
|
||||
$console = PhutilConsole::getConsole();
|
||||
|
||||
$this->printRepositorySection();
|
||||
$console->writeOut("\n");
|
||||
|
||||
$repository_api = $this->getRepositoryAPI();
|
||||
|
||||
$arg_commit = $this->getArgument('commit');
|
||||
|
@ -184,4 +190,34 @@ EOTEXT
|
|||
|
||||
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.'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -187,6 +187,7 @@ final class ArcanistWorkingCopyIdentity {
|
|||
|
||||
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 @@ final class ArcanistWorkingCopyIdentity {
|
|||
"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 @@ final class ArcanistWorkingCopyIdentity {
|
|||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue