1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-10 00:42:40 +01:00

Rebuild "arc browse" using refs and hardpoints

Summary:
Ref T10895. This mostly modularizes `arc browse` and puts it on ref/hardpoint infrastructure. Feels okay-ish? Major gripes:

  - Messaging for "some stuff won't work because you're in a random directory, not a working copy" could be better, but I think I want something like the "Guidance" infrastructure for this.
  - The `requiresStuff()` / `desiresStuff()` interactions on Workflow continue to feel bad, but I think I can sneak by without fixing those for now.
  - I want to improve some of the other UI/UX stuff but this diff is already gigantic.

Test Plan: Ran `arc browse .`, `arc browse master`, `arc browse README.md`, inside and outside working directories.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10895

Differential Revision: https://secure.phabricator.com/D16925
This commit is contained in:
epriestley 2016-11-22 17:03:34 -08:00
parent 71473af895
commit 909668082e
13 changed files with 868 additions and 240 deletions

View file

@ -42,7 +42,13 @@ phutil_register_library_map(array(
'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBraceFormattingXHPASTLinterRuleTestCase.php',
'ArcanistBranchRef' => 'ref/ArcanistBranchRef.php',
'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php',
'ArcanistBrowseWorkflow' => 'workflow/ArcanistBrowseWorkflow.php',
'ArcanistBrowseCommitURIHardpointLoader' => 'browse/loader/ArcanistBrowseCommitURIHardpointLoader.php',
'ArcanistBrowseObjectNameURIHardpointLoader' => 'browse/loader/ArcanistBrowseObjectNameURIHardpointLoader.php',
'ArcanistBrowsePathURIHardpointLoader' => 'browse/loader/ArcanistBrowsePathURIHardpointLoader.php',
'ArcanistBrowseRef' => 'browse/ref/ArcanistBrowseRef.php',
'ArcanistBrowseURIHardpointLoader' => 'browse/loader/ArcanistBrowseURIHardpointLoader.php',
'ArcanistBrowseURIRef' => 'browse/ref/ArcanistBrowseURIRef.php',
'ArcanistBrowseWorkflow' => 'browse/workflow/ArcanistBrowseWorkflow.php',
'ArcanistBundle' => 'parser/ArcanistBundle.php',
'ArcanistBundleTestCase' => 'parser/__tests__/ArcanistBundleTestCase.php',
'ArcanistCSSLintLinter' => 'lint/linter/ArcanistCSSLintLinter.php',
@ -322,6 +328,7 @@ phutil_register_library_map(array(
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php',
'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php',
'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php',
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php',
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php',
'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php',
@ -472,6 +479,12 @@ phutil_register_library_map(array(
'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistBranchRef' => 'ArcanistRef',
'ArcanistBranchWorkflow' => 'ArcanistFeatureWorkflow',
'ArcanistBrowseCommitURIHardpointLoader' => 'ArcanistBrowseURIHardpointLoader',
'ArcanistBrowseObjectNameURIHardpointLoader' => 'ArcanistBrowseURIHardpointLoader',
'ArcanistBrowsePathURIHardpointLoader' => 'ArcanistBrowseURIHardpointLoader',
'ArcanistBrowseRef' => 'ArcanistRef',
'ArcanistBrowseURIHardpointLoader' => 'ArcanistHardpointLoader',
'ArcanistBrowseURIRef' => 'ArcanistRef',
'ArcanistBrowseWorkflow' => 'ArcanistWorkflow',
'ArcanistBundle' => 'Phobject',
'ArcanistBundleTestCase' => 'PhutilTestCase',
@ -752,6 +765,7 @@ phutil_register_library_map(array(
'ArcanistRepositoryAPI' => 'Phobject',
'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase',
'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase',
'ArcanistRepositoryRef' => 'ArcanistRef',
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',

View file

@ -0,0 +1,107 @@
<?php
final class ArcanistBrowseCommitURIHardpointLoader
extends ArcanistBrowseURIHardpointLoader {
const LOADERKEY = 'browse.uri.commit';
const BROWSETYPE = 'commit';
public function willLoadBrowseURIRefs(array $refs) {
$refs = $this->getRefsWithSupportedTypes($refs);
if (!$refs) {
return;
}
$query = $this->getQuery();
$working_ref = $query->getWorkingCopyRef();
if (!$working_ref) {
// If we aren't in a working copy, don't warn about this.
return;
}
$repository_ref = $this->getQuery()->getRepositoryRef();
if (!$repository_ref) {
echo pht(
'NO REPOSITORY: Unable to determine which repository this working '.
'copy belongs to, so arguments can not be resolved as commits. Use '.
'"%s" to understand how repositories are resolved.',
'arc which');
echo "\n";
return;
}
}
public function loadHardpoints(array $refs, $hardpoint) {
$api = $this->getQuery()->getRepositoryAPI();
if (!$api) {
return array();
}
$repository_ref = $this->getQuery()->getRepositoryRef();
if (!$repository_ref) {
return array();
}
$repository_phid = $repository_ref->getPHID();
$refs = $this->getRefsWithSupportedTypes($refs);
$commit_map = array();
foreach ($refs as $key => $ref) {
$is_commit = $ref->hasType('commit');
$token = $ref->getToken();
if ($token === '.') {
// Git resolves "." like HEAD, but we want to treat it as "browse the
// current directory" instead in all cases.
continue;
}
if ($token === null) {
if ($is_commit) {
$token = $api->getHeadCommit();
} else {
continue;
}
}
try {
$commit = $api->getCanonicalRevisionName($token);
if ($commit) {
$commit_map[$commit][] = $key;
}
} catch (Exception $ex) {
// Ignore anything we can't resolve.
}
}
if (!$commit_map) {
return array();
}
$commit_info = $this->resolveCall(
'diffusion.querycommits',
array(
'repositoryPHID' => $repository_phid,
'names' => array_keys($commit_map),
));
$results = array();
foreach ($commit_info['identifierMap'] as $commit_key => $commit_phid) {
foreach ($commit_map[$commit_key] as $key) {
$commit_uri = $commit_info['data'][$commit_phid]['uri'];
$results[$key][] = id(new ArcanistBrowseURIRef())
->setURI($commit_uri)
->setType('commit');
}
}
return $results;
}
}

View file

@ -0,0 +1,54 @@
<?php
final class ArcanistBrowseObjectNameURIHardpointLoader
extends ArcanistBrowseURIHardpointLoader {
const LOADERKEY = 'browse.uri.name';
const BROWSETYPE = 'object';
public function loadHardpoints(array $refs, $hardpoint) {
$refs = $this->getRefsWithSupportedTypes($refs);
$name_map = array();
foreach ($refs as $key => $ref) {
$token = $ref->getToken();
if (!strlen($token)) {
continue;
}
$name_map[$key] = $token;
}
if (!$name_map) {
return array();
}
$objects = $this->resolveCall(
'phid.lookup',
array(
'names' => $name_map,
));
$result = array();
$reverse_map = array_flip($name_map);
foreach ($objects as $name => $object) {
$key = idx($reverse_map, $name);
if ($key === null) {
continue;
}
$uri = idx($object, 'uri');
if (!strlen($uri)) {
continue;
}
$result[$key][] = id(new ArcanistBrowseURIRef())
->setURI($object['uri'])
->setType('object');
}
return $result;
}
}

View file

@ -0,0 +1,132 @@
<?php
final class ArcanistBrowsePathURIHardpointLoader
extends ArcanistBrowseURIHardpointLoader {
const LOADERKEY = 'browse.uri.path';
const BROWSETYPE = 'path';
public function willLoadBrowseURIRefs(array $refs) {
$refs = $this->getRefsWithSupportedTypes($refs);
if (!$refs) {
return;
}
$query = $this->getQuery();
$working_ref = $query->getWorkingCopyRef();
if (!$working_ref) {
echo pht(
'NO WORKING COPY: The current directory is not a repository '.
'working copy, so arguments can not be resolved as paths. Run '.
'this command inside a working copy to resolve paths.');
echo "\n";
return;
}
$repository_ref = $query->getRepositoryRef();
if (!$repository_ref) {
echo pht(
'NO REPOSITORY: Unable to determine which repository this working '.
'copy belongs to, so arguments can not be resolved as paths. Use '.
'"%s" to understand how repositories are resolved.',
'arc which');
echo "\n";
return;
}
}
public function didFailToLoadBrowseURIRefs(array $refs) {
$refs = $this->getRefsWithSupportedTypes($refs);
if (!$refs) {
return;
}
$query = $this->getQuery();
$working_ref = $query->getWorkingCopyRef();
if (!$working_ref) {
return;
}
$repository_ref = $query->getRepositoryRef();
if (!$repository_ref) {
return;
}
echo pht(
'Use "--types path" to force arguments to be interpreted as paths.');
echo "\n";
}
public function loadHardpoints(array $refs, $hardpoint) {
$query = $this->getQuery();
$working_ref = $query->getWorkingCopyRef();
if (!$working_ref) {
return array();
}
$repository_ref = $query->getRepositoryRef();
if (!$repository_ref) {
return array();
}
$refs = $this->getRefsWithSupportedTypes($refs);
$project_root = $working_ref->getRootDirectory();
$results = array();
foreach ($refs as $key => $ref) {
$is_path = $ref->hasType(self::BROWSETYPE);
$path = $ref->getToken();
if ($path === null) {
// If we're explicitly resolving no arguments as a path, treat it
// as the current working directory.
if ($is_path) {
$path = '.';
} else {
continue;
}
}
$lines = null;
$parts = explode(':', $path);
if (count($parts) > 1) {
$lines = array_pop($parts);
}
$path = implode(':', $parts);
$full_path = Filesystem::resolvePath($path);
if (!Filesystem::pathExists($full_path)) {
if (!$is_path) {
continue;
}
}
if ($full_path == $project_root) {
$path = '';
} else {
$path = Filesystem::readablePath($full_path, $project_root);
}
$params = array(
'path' => $path,
'lines' => $lines,
'branch' => $ref->getBranch(),
);
$uri = $repository_ref->newBrowseURI($params);
$results[$key][] = id(new ArcanistBrowseURIRef())
->setURI($uri)
->setType(self::BROWSETYPE);
}
return $results;
}
}

View file

@ -0,0 +1,55 @@
<?php
abstract class ArcanistBrowseURIHardpointLoader
extends ArcanistHardpointLoader {
public function getSupportedBrowseType() {
return $this->getPhobjectClassConstant('BROWSETYPE', 32);
}
public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api) {
return true;
}
public function canLoadRef(ArcanistRef $ref) {
return ($ref instanceof ArcanistBrowseRef);
}
public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
return ($hardpoint == 'uris');
}
public function willLoadBrowseURIRefs(array $refs) {
return;
}
public function didFailToLoadBrowseURIRefs(array $refs) {
return;
}
public function getRefsWithSupportedTypes(array $refs) {
$type = $this->getSupportedBrowseType();
foreach ($refs as $key => $ref) {
if ($ref->isUntyped()) {
continue;
}
if ($ref->hasType($type)) {
continue;
}
unset($refs[$key]);
}
return $refs;
}
public static function getAllBrowseLoaders() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getLoaderKey')
->execute();
}
}

View file

@ -0,0 +1,64 @@
<?php
final class ArcanistBrowseRef
extends ArcanistRef {
private $token;
private $types;
private $branch;
public function getRefIdentifier() {
return pht('Browse Query "%s"', $this->getToken());
}
public function defineHardpoints() {
return array(
'uris' => array(
'type' => 'ArcanistBrowseURIRef',
'vector' => true,
),
);
}
public function setToken($token) {
$this->token = $token;
return $this;
}
public function getToken() {
return $this->token;
}
public function setTypes(array $types) {
$this->types = $types;
return $this;
}
public function getTypes() {
return $this->types;
}
public function hasType($type) {
$map = $this->getTypes();
$map = array_fuse($map);
return isset($map[$type]);
}
public function isUntyped() {
return !$this->types;
}
public function setBranch($branch) {
$this->branch = $branch;
return $this;
}
public function getBranch() {
return $this->branch;
}
public function getURIs() {
return $this->getHardpoint('uris');
}
}

View file

@ -0,0 +1,35 @@
<?php
final class ArcanistBrowseURIRef
extends ArcanistRef {
private $uri;
private $type;
public function getRefIdentifier() {
return pht('Browse URI "%s"', $this->getURI());
}
public function defineHardpoints() {
return array();
}
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function getURI() {
return $this->uri;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
}

View file

@ -0,0 +1,224 @@
<?php
/**
* Browse files or objects in the Phabricator web interface.
*/
final class ArcanistBrowseWorkflow extends ArcanistWorkflow {
public function getWorkflowName() {
return 'browse';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**browse** [__options__] __path__ ...
**browse** [__options__] __object__ ...
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, hg, svn
Open a file or object (like a task or revision) in your web browser.
$ arc browse README # Open a file in Diffusion.
$ arc browse T123 # View a task.
$ arc browse HEAD # View a symbolic commit.
Set the 'browser' value using 'arc set-config' to select a browser. If
no browser is set, the command will try to guess which browser to use.
EOTEXT
);
}
public function getArguments() {
return array(
'branch' => array(
'param' => 'branch_name',
'help' => pht(
'Default branch name to view on server. Defaults to "%s".',
'master'),
),
'types' => array(
'param' => 'types',
'aliases' => array('type'),
'help' => pht(
'Parse arguments with particular types.'),
),
'force' => array(
'help' => pht(
'(DEPRECATED) Obsolete, use "--types path" instead.'),
),
'*' => 'targets',
);
}
public function desiresWorkingCopy() {
return true;
}
public function desiresRepositoryAPI() {
return true;
}
public function run() {
$conduit = $this->getConduitEngine();
$console = PhutilConsole::getConsole();
$targets = $this->getArgument('targets');
if (!$targets) {
throw new ArcanistUsageException(
pht(
'Specify one or more paths or objects to browse. Use the '.
'command "%s" if you want to browse this directory.',
'arc browse .'));
}
$targets = array_fuse($targets);
if (!$targets) {
$refs = array(
new ArcanistBrowseRef(),
);
} else {
$refs = array();
foreach ($targets as $target) {
$refs[] = id(new ArcanistBrowseRef())
->setToken($target);
}
}
$is_force = $this->getArgument('force');
if ($is_force) {
// TODO: Remove this completely.
$this->writeWarn(
pht('DEPRECATED'),
pht(
'Argument "--force" for "arc browse" is deprecated. Use '.
'"--type %s" instead.',
ArcanistBrowsePathURIHardpointLoader::BROWSETYPE));
}
$types = $this->getArgument('types');
if ($types !== null) {
$types = preg_split('/[\s,]+/', $types);
} else {
if ($is_force) {
$types = array(ArcanistBrowsePathURIHardpointLoader::BROWSETYPE);
} else {
$types = array();
}
}
foreach ($refs as $ref) {
$ref->setTypes($types);
}
$branch = $this->getArgument('branch');
if ($branch) {
foreach ($refs as $ref) {
$ref->setBranch($branch);
}
}
$loaders = ArcanistBrowseURIHardpointLoader::getAllBrowseLoaders();
foreach ($loaders as $key => $loader) {
$loaders[$key] = clone $loader;
}
$query = $this->newRefQuery($refs)
->needHardpoints(
array(
'uris',
))
->setLoaders($loaders);
foreach ($loaders as $loader) {
$loader->willLoadBrowseURIRefs($refs);
}
$query->execute();
$zero_hits = array();
$open_uris = array();
$many_hits = array();
foreach ($refs as $ref) {
$uris = $ref->getURIs();
if (!$uris) {
$zero_hits[] = $ref;
} else if (count($uris) == 1) {
$open_uris[] = $ref;
} else {
$many_hits[] = $ref;
}
}
if ($many_hits) {
foreach ($many_hits as $ref) {
$token = $ref->getToken();
if (strlen($token)) {
$message = pht('Argument "%s" is ambiguous.', $token);
} else {
$message = pht('Default behavior is ambiguous.');
}
$this->writeWarn(pht('AMBIGUOUS'), $message);
}
$table = id(new PhutilConsoleTable())
->addColumn('argument', array('title' => pht('Argument')))
->addColumn('type', array('title' => pht('Type')))
->addColumn('uri', array('title' => pht('URI')));
foreach ($many_hits as $ref) {
$token_display = $ref->getToken();
if (!strlen($token)) {
$token_display = pht('<default>');
}
foreach ($ref->getURIs() as $uri) {
$row = array(
'argument' => $token_display,
'type' => $uri->getType(),
'uri' => $uri->getURI(),
);
$table->addRow($row);
}
}
$table->draw();
$this->writeInfo(
pht('CHOOSE'),
pht('Use "--types" to select between alternatives.'));
}
// If anything failed to resolve, this is also an error.
if ($zero_hits) {
foreach ($zero_hits as $ref) {
echo tsprintf(
"%s\n",
pht(
'Unable to resolve argument "%s".',
$ref->getToken()));
}
foreach ($loaders as $loader) {
$loader->didFailToLoadBrowseURIRefs($refs);
}
}
$uris = array();
foreach ($open_uris as $ref) {
$ref_uri = head($ref->getURIs());
$uris[] = $ref_uri->getURI();
}
$this->openURIsInBrowser($uris);
return 0;
}
}

View file

@ -4,9 +4,12 @@ final class ArcanistRefQuery extends Phobject {
private $repositoryAPI;
private $conduitEngine;
private $repositoryRef;
private $workingCopyRef;
private $refs;
private $hardpoints;
private $loaders;
public function setRefs(array $refs) {
assert_instances_of($refs, 'ArcanistRef');
@ -27,6 +30,14 @@ final class ArcanistRefQuery extends Phobject {
return $this->repositoryAPI;
}
public function setRepositoryRef(ArcanistRepositoryRef $repository_ref) {
$this->repositoryRef = $repository_ref;
return $this;
}
public function getRepositoryRef() {
return $this->repositoryRef;
}
public function setConduitEngine(ArcanistConduitEngine $conduit_engine) {
$this->conduitEngine = $conduit_engine;
return $this;
@ -36,11 +47,31 @@ final class ArcanistRefQuery extends Phobject {
return $this->conduitEngine;
}
public function setWorkingCopyRef(ArcanistWorkingCopyStateRef $working_ref) {
$this->workingCopyRef = $working_ref;
return $this;
}
public function getWorkingCopyRef() {
return $this->workingCopyRef;
}
public function needHardpoints(array $hardpoints) {
$this->hardpoints = $hardpoints;
return $this;
}
public function setLoaders(array $loaders) {
assert_instances_of($loaders, 'ArcanistHardpointLoader');
foreach ($loaders as $key => $loader) {
$loader->setQuery($this);
}
$this->loaders = $loaders;
return $this;
}
public function execute() {
$refs = $this->getRefs();
@ -52,13 +83,23 @@ final class ArcanistRefQuery extends Phobject {
throw new PhutilInvalidStateException('needHardpoints');
}
$api = $this->getRepositoryAPI();
$all_loaders = ArcanistHardpointLoader::getAllLoaders();
if ($this->loaders == null) {
$all_loaders = ArcanistHardpointLoader::getAllLoaders();
foreach ($all_loaders as $key => $loader) {
$all_loaders[$key] = clone $loader;
}
$this->setLoaders($all_loaders);
}
$all_loaders = $this->loaders;
$api = $this->getRepositoryAPI();
$loaders = array();
foreach ($all_loaders as $loader_key => $loader) {
if (!$loader->canLoadRepositoryAPI($api)) {
continue;
if ($api) {
if (!$loader->canLoadRepositoryAPI($api)) {
continue;
}
}
$loaders[$loader_key] = id(clone $loader)

View file

@ -0,0 +1,79 @@
<?php
final class ArcanistRepositoryRef
extends ArcanistRef {
private $phid;
private $browseURI;
public function getRefIdentifier() {
return pht('Remote Repository');
}
public function defineHardpoints() {
return array();
}
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function getPHID() {
return $this->phid;
}
public function setBrowseURI($browse_uri) {
$this->browseURI = $browse_uri;
return $this;
}
public function newBrowseURI(array $params) {
PhutilTypeSpec::checkMap(
$params,
array(
'path' => 'optional string|null',
'branch' => 'optional string|null',
'lines' => 'optional string|null',
));
foreach ($params as $key => $value) {
if (!strlen($value)) {
unset($params[$key]);
}
}
$defaults = array(
'path' => '/',
'branch' => $this->getDefaultBranch(),
'lines' => null,
);
$params = $params + $defaults;
$uri_base = $this->browseURI;
$uri_base = rtrim($uri_base, '/');
$uri_branch = phutil_escape_uri_path_component($params['branch']);
$uri_path = ltrim($params['path'], '/');
$uri_path = phutil_escape_uri($uri_path);
$uri_lines = null;
if ($params['lines']) {
$uri_lines = '$'.phutil_escape_uri($params['lines']);
}
// TODO: This construction, which includes a branch, is probably wrong for
// Subversion.
return "{$uri_base}/browse/{$uri_branch}/{$uri_path}{$uri_lines}";
}
public function getDefaultBranch() {
// TODO: This should read from the remote, and is not correct for
// Mercurial anyway, as "default" would be a better default branch.
return 'master';
}
}

View file

@ -3,6 +3,8 @@
final class ArcanistWorkingCopyStateRef
extends ArcanistRef {
private $rootDirectory;
public function getRefIdentifier() {
// TODO: This could check attached hardpoints and render something more
// insightful.
@ -24,6 +26,15 @@ final class ArcanistWorkingCopyStateRef
);
}
public function setRootDirectory($root_directory) {
$this->rootDirectory = $root_directory;
return $this;
}
public function getRootDirectory() {
return $this->rootDirectory;
}
public function attachBranchRef(ArcanistBranchRef $branch_ref) {
return $this->attachHardpoint('branchRef', $branch_ref);
}

View file

@ -1,232 +0,0 @@
<?php
/**
* Browse files or objects in the Phabricator web interface.
*/
final class ArcanistBrowseWorkflow extends ArcanistWorkflow {
public function getWorkflowName() {
return 'browse';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**browse** [__options__] __path__ ...
**browse** [__options__] __object__ ...
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, hg, svn
Open a file or object (like a task or revision) in your web browser.
$ arc browse README # Open a file in Diffusion.
$ arc browse T123 # View a task.
$ arc browse HEAD # View a symbolic commit.
Set the 'browser' value using 'arc set-config' to select a browser. If
no browser is set, the command will try to guess which browser to use.
EOTEXT
);
}
public function getArguments() {
return array(
'branch' => array(
'param' => 'branch_name',
'help' => pht(
'Default branch name to view on server. Defaults to "%s".',
'master'),
),
'force' => array(
'help' => pht(
'Open arguments as paths, even if they do not exist in the '.
'working copy.'),
),
'*' => 'paths',
);
}
public function desiresWorkingCopy() {
return true;
}
public function desiresRepositoryAPI() {
return true;
}
public function run() {
$conduit = $this->getConduitEngine();
$console = PhutilConsole::getConsole();
$is_force = $this->getArgument('force');
$things = $this->getArgument('paths');
if (!$things) {
throw new ArcanistUsageException(
pht(
'Specify one or more paths or objects to browse. Use the command '.
'"%s" if you want to browse this directory.',
'arc browse .'));
}
$things = array_fuse($things);
$method = 'phid.lookup';
$params = array(
'names' => array_keys($things),
);
$objects = $conduit->newCall($method, $params)
->resolve();
$uris = array();
foreach ($objects as $name => $object) {
$uris[] = $object['uri'];
$console->writeOut(
pht(
'Opening **%s** as an object.',
$name)."\n");
unset($things[$name]);
}
if ($this->hasRepositoryAPI()) {
$repository_api = $this->getRepositoryAPI();
$project_root = $this->getWorkingCopy()->getProjectRoot();
// First, try to resolve arguments as symbolic commits.
$commits = array();
foreach ($things as $key => $thing) {
if ($thing == '.') {
// Git resolves '.' like HEAD, but it should be interpreted to mean
// "the current directory". Just skip resolution and fall through.
continue;
}
try {
$commit = $repository_api->getCanonicalRevisionName($thing);
if ($commit) {
$commits[$commit] = $key;
}
} catch (Exception $ex) {
// Ignore.
}
}
if ($commits) {
$method = 'diffusion.querycommits';
$params = array(
'repositoryPHID' => $this->getRepositoryPHID(),
'names' => array_keys($commits),
);
$commit_info = $conduit->newCall($method, $params)
->resolve();
foreach ($commit_info['identifierMap'] as $ckey => $cphid) {
$thing = $commits[$ckey];
unset($things[$thing]);
$uris[] = $commit_info['data'][$cphid]['uri'];
$console->writeOut(
pht(
'Opening **%s** as a commit.',
$thing)."\n");
}
}
// If we fail, try to resolve them as paths.
foreach ($things as $key => $path) {
$lines = null;
$parts = explode(':', $path);
if (count($parts) > 1) {
$lines = array_pop($parts);
}
$path = implode(':', $parts);
$full_path = Filesystem::resolvePath($path);
if (!$is_force && !Filesystem::pathExists($full_path)) {
continue;
}
$console->writeOut(
pht(
'Opening **%s** as a repository path.',
$key)."\n");
unset($things[$key]);
if ($full_path == $project_root) {
$path = '';
} else {
$path = Filesystem::readablePath($full_path, $project_root);
}
$base_uri = $this->getBaseURI();
$uri = $base_uri.$path;
if ($lines) {
$uri = $uri.'$'.$lines;
}
$uris[] = $uri;
}
} else {
if ($things) {
$console->writeOut(
"%s\n",
pht(
"The current working directory is not a repository working ".
"copy, so remaining arguments can not be resolved as paths or ".
"commits. To browse paths or symbolic commits in Diffusion, run ".
"'%s' from inside a working copy.",
'arc browse'));
}
}
foreach ($things as $thing) {
$console->writeOut(
"%s\n",
pht(
'Unable to find an object named **%s**, no such commit exists in '.
'the remote, and no such path exists in the working copy. Use '.
'__%s__ to treat this as a path anyway.',
$thing,
'--force'));
}
if ($uris) {
$this->openURIsInBrowser($uris);
}
return 0;
}
private function getBaseURI() {
$repo_uri = $this->getRepositoryURI();
if ($repo_uri === null) {
throw new ArcanistUsageException(
pht(
'arc is unable to determine which repository in Diffusion '.
'this working copy belongs to. Use "%s" to understand how '.
'%s looks for a repository.',
'arc which',
'arc'));
}
$branch = $this->getArgument('branch', 'master');
$branch = phutil_escape_uri_path_component($branch);
return $repo_uri.'browse/'.$branch.'/';
}
}

View file

@ -62,6 +62,7 @@ abstract class ArcanistWorkflow extends Phobject {
private $projectInfo;
private $repositoryInfo;
private $repositoryReasons;
private $repositoryRef;
private $arcanistConfiguration;
private $parentWorkflow;
@ -583,6 +584,7 @@ abstract class ArcanistWorkflow extends Phobject {
$arc_config = $this->getArcanistConfiguration();
$workflow = $arc_config->buildWorkflow($command);
$workflow->setParentWorkflow($this);
$workflow->setConduitEngine($this->getConduitEngine());
$workflow->setCommand($command);
$workflow->setConfigurationManager($this->getConfigurationManager());
@ -2074,15 +2076,57 @@ abstract class ArcanistWorkflow extends Phobject {
}
final protected function newWorkingCopyStateRef() {
return new ArcanistWorkingCopyStateRef();
$ref = new ArcanistWorkingCopyStateRef();
$working_copy = $this->getWorkingCopy();
$ref->setRootDirectory($working_copy->getProjectRoot());
return $ref;
}
final protected function newRefQuery(array $refs) {
assert_instances_of($refs, 'ArcanistRef');
return id(new ArcanistRefQuery())
->setRepositoryAPI($this->getRepositoryAPI())
$query = id(new ArcanistRefQuery())
->setConduitEngine($this->getConduitEngine())
->setRefs($refs);
if ($this->hasRepositoryAPI()) {
$query->setRepositoryAPI($this->getRepositoryAPI());
}
$repository_ref = $this->getRepositoryRef();
if ($repository_ref) {
$query->setRepositoryRef($repository_ref);
}
$working_copy = $this->getConfigurationManager()->getWorkingCopyIdentity();
if ($working_copy) {
$working_ref = $this->newWorkingCopyStateRef();
$query->setWorkingCopyRef($working_ref);
}
return $query;
}
final public function getRepositoryRef() {
if (!$this->getConfigurationManager()->getWorkingCopyIdentity()) {
return null;
}
if (!$this->repositoryAPI) {
return null;
}
if (!$this->repositoryRef) {
$ref = id(new ArcanistRepositoryRef())
->setPHID($this->getRepositoryPHID())
->setBrowseURI($this->getRepositoryURI());
$this->repositoryRef = $ref;
}
return $this->repositoryRef;
}
}