mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 00:42:41 +01:00
Move Conduit client construction logic into Repository
Summary: Ref T7020. I need this elsewhere, and it's relatively internal anyway. Test Plan: Browsed around my local, cluster-configured install and saw everything working fine. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T7020 Differential Revision: https://secure.phabricator.com/D11474
This commit is contained in:
parent
c40bc0c8bf
commit
7c2474bef7
3 changed files with 138 additions and 117 deletions
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
return array(
|
return array(
|
||||||
'names' => array(
|
'names' => array(
|
||||||
'core.pkg.css' => '4517260e',
|
'core.pkg.css' => '0ca60f7b',
|
||||||
'core.pkg.js' => '2d9bfc06',
|
'core.pkg.js' => '2d9bfc06',
|
||||||
'darkconsole.pkg.js' => '8ab24e01',
|
'darkconsole.pkg.js' => '8ab24e01',
|
||||||
'differential.pkg.css' => '8af45893',
|
'differential.pkg.css' => '8af45893',
|
||||||
|
@ -107,12 +107,11 @@ return array(
|
||||||
'rsrc/css/core/core.css' => 'd7f6ec35',
|
'rsrc/css/core/core.css' => 'd7f6ec35',
|
||||||
'rsrc/css/core/remarkup.css' => '0ee3d256',
|
'rsrc/css/core/remarkup.css' => '0ee3d256',
|
||||||
'rsrc/css/core/syntax.css' => '56c1ba38',
|
'rsrc/css/core/syntax.css' => '56c1ba38',
|
||||||
'rsrc/css/core/z-index.css' => 'a39e6f5a',
|
'rsrc/css/core/z-index.css' => '9c4313eb',
|
||||||
'rsrc/css/diviner/diviner-shared.css' => '38813222',
|
'rsrc/css/diviner/diviner-shared.css' => '38813222',
|
||||||
'rsrc/css/font/font-awesome.css' => '0c10d96b',
|
'rsrc/css/font/font-awesome.css' => '0c10d96b',
|
||||||
'rsrc/css/font/font-source-sans-pro.css' => '91d53463',
|
'rsrc/css/font/font-source-sans-pro.css' => '91d53463',
|
||||||
'rsrc/css/font/phui-font-icon-base.css' => '3dad2ae3',
|
'rsrc/css/font/phui-font-icon-base.css' => '3dad2ae3',
|
||||||
'rsrc/css/layout/phabricator-crumbs-view.css' => 'd5aa87e4',
|
|
||||||
'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82',
|
'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82',
|
||||||
'rsrc/css/layout/phabricator-hovercard-view.css' => '893f4783',
|
'rsrc/css/layout/phabricator-hovercard-view.css' => '893f4783',
|
||||||
'rsrc/css/layout/phabricator-side-menu-view.css' => '7e8c6341',
|
'rsrc/css/layout/phabricator-side-menu-view.css' => '7e8c6341',
|
||||||
|
@ -125,6 +124,7 @@ return array(
|
||||||
'rsrc/css/phui/phui-action-list.css' => '9ee9910a',
|
'rsrc/css/phui/phui-action-list.css' => '9ee9910a',
|
||||||
'rsrc/css/phui/phui-box.css' => '7b3a2eed',
|
'rsrc/css/phui/phui-box.css' => '7b3a2eed',
|
||||||
'rsrc/css/phui/phui-button.css' => '008ba5e2',
|
'rsrc/css/phui/phui-button.css' => '008ba5e2',
|
||||||
|
'rsrc/css/phui/phui-crumbs-view.css' => '1705bce6',
|
||||||
'rsrc/css/phui/phui-document.css' => 'bbeb1890',
|
'rsrc/css/phui/phui-document.css' => 'bbeb1890',
|
||||||
'rsrc/css/phui/phui-feed-story.css' => '582f0ec9',
|
'rsrc/css/phui/phui-feed-story.css' => '582f0ec9',
|
||||||
'rsrc/css/phui/phui-fontkit.css' => '9c3d2dce',
|
'rsrc/css/phui/phui-fontkit.css' => '9c3d2dce',
|
||||||
|
@ -152,7 +152,7 @@ return array(
|
||||||
'rsrc/css/sprite-gradient.css' => '4bdb98a7',
|
'rsrc/css/sprite-gradient.css' => '4bdb98a7',
|
||||||
'rsrc/css/sprite-login.css' => 'a355d921',
|
'rsrc/css/sprite-login.css' => 'a355d921',
|
||||||
'rsrc/css/sprite-main-header.css' => '92720ee2',
|
'rsrc/css/sprite-main-header.css' => '92720ee2',
|
||||||
'rsrc/css/sprite-menu.css' => '0ca5a908',
|
'rsrc/css/sprite-menu.css' => '661b879f',
|
||||||
'rsrc/css/sprite-projects.css' => 'b0d9e24f',
|
'rsrc/css/sprite-projects.css' => 'b0d9e24f',
|
||||||
'rsrc/css/sprite-tokens.css' => '1706b943',
|
'rsrc/css/sprite-tokens.css' => '1706b943',
|
||||||
'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '579d3140',
|
'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '579d3140',
|
||||||
|
@ -199,7 +199,7 @@ return array(
|
||||||
'rsrc/externals/javelin/lib/Resource.js' => '44959b73',
|
'rsrc/externals/javelin/lib/Resource.js' => '44959b73',
|
||||||
'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692',
|
'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692',
|
||||||
'rsrc/externals/javelin/lib/Router.js' => '29274e2b',
|
'rsrc/externals/javelin/lib/Router.js' => '29274e2b',
|
||||||
'rsrc/externals/javelin/lib/Scrollbar.js' => '5dd3e41b',
|
'rsrc/externals/javelin/lib/Scrollbar.js' => 'b403b668',
|
||||||
'rsrc/externals/javelin/lib/URI.js' => '6eff08aa',
|
'rsrc/externals/javelin/lib/URI.js' => '6eff08aa',
|
||||||
'rsrc/externals/javelin/lib/Vector.js' => 'cc1bd0b0',
|
'rsrc/externals/javelin/lib/Vector.js' => 'cc1bd0b0',
|
||||||
'rsrc/externals/javelin/lib/WebSocket.js' => '3f840822',
|
'rsrc/externals/javelin/lib/WebSocket.js' => '3f840822',
|
||||||
|
@ -673,7 +673,7 @@ return array(
|
||||||
'javelin-resource' => '44959b73',
|
'javelin-resource' => '44959b73',
|
||||||
'javelin-routable' => 'b3e7d692',
|
'javelin-routable' => 'b3e7d692',
|
||||||
'javelin-router' => '29274e2b',
|
'javelin-router' => '29274e2b',
|
||||||
'javelin-scrollbar' => '5dd3e41b',
|
'javelin-scrollbar' => 'b403b668',
|
||||||
'javelin-stratcom' => '8b0ad945',
|
'javelin-stratcom' => '8b0ad945',
|
||||||
'javelin-tokenizer' => '7644823e',
|
'javelin-tokenizer' => '7644823e',
|
||||||
'javelin-typeahead' => '70baed2f',
|
'javelin-typeahead' => '70baed2f',
|
||||||
|
@ -711,7 +711,6 @@ return array(
|
||||||
'phabricator-content-source-view-css' => '4b8b05d4',
|
'phabricator-content-source-view-css' => '4b8b05d4',
|
||||||
'phabricator-core-css' => 'd7f6ec35',
|
'phabricator-core-css' => 'd7f6ec35',
|
||||||
'phabricator-countdown-css' => '86b7b0a0',
|
'phabricator-countdown-css' => '86b7b0a0',
|
||||||
'phabricator-crumbs-view-css' => 'd5aa87e4',
|
|
||||||
'phabricator-dashboard-css' => 'a2bfdcbf',
|
'phabricator-dashboard-css' => 'a2bfdcbf',
|
||||||
'phabricator-drag-and-drop-file-upload' => '8c49f386',
|
'phabricator-drag-and-drop-file-upload' => '8c49f386',
|
||||||
'phabricator-draggable-list' => 'a16ec1c6',
|
'phabricator-draggable-list' => 'a16ec1c6',
|
||||||
|
@ -755,7 +754,7 @@ return array(
|
||||||
'phabricator-uiexample-reactor-select' => 'a155550f',
|
'phabricator-uiexample-reactor-select' => 'a155550f',
|
||||||
'phabricator-uiexample-reactor-sendclass' => '1def2711',
|
'phabricator-uiexample-reactor-sendclass' => '1def2711',
|
||||||
'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee',
|
'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee',
|
||||||
'phabricator-zindex-css' => 'a39e6f5a',
|
'phabricator-zindex-css' => '9c4313eb',
|
||||||
'phame-css' => '19ecc703',
|
'phame-css' => '19ecc703',
|
||||||
'pholio-css' => '95174bdd',
|
'pholio-css' => '95174bdd',
|
||||||
'pholio-edit-css' => '3ad9d1ee',
|
'pholio-edit-css' => '3ad9d1ee',
|
||||||
|
@ -772,6 +771,7 @@ return array(
|
||||||
'phui-calendar-day-css' => 'de035c8a',
|
'phui-calendar-day-css' => 'de035c8a',
|
||||||
'phui-calendar-list-css' => 'c1d0ca59',
|
'phui-calendar-list-css' => 'c1d0ca59',
|
||||||
'phui-calendar-month-css' => 'a92e47d2',
|
'phui-calendar-month-css' => 'a92e47d2',
|
||||||
|
'phui-crumbs-view-css' => '1705bce6',
|
||||||
'phui-document-view-css' => 'bbeb1890',
|
'phui-document-view-css' => 'bbeb1890',
|
||||||
'phui-feed-story-css' => '582f0ec9',
|
'phui-feed-story-css' => '582f0ec9',
|
||||||
'phui-font-icon-base-css' => '3dad2ae3',
|
'phui-font-icon-base-css' => '3dad2ae3',
|
||||||
|
@ -819,7 +819,7 @@ return array(
|
||||||
'sprite-gradient-css' => '4bdb98a7',
|
'sprite-gradient-css' => '4bdb98a7',
|
||||||
'sprite-login-css' => 'a355d921',
|
'sprite-login-css' => 'a355d921',
|
||||||
'sprite-main-header-css' => '92720ee2',
|
'sprite-main-header-css' => '92720ee2',
|
||||||
'sprite-menu-css' => '0ca5a908',
|
'sprite-menu-css' => '661b879f',
|
||||||
'sprite-projects-css' => 'b0d9e24f',
|
'sprite-projects-css' => 'b0d9e24f',
|
||||||
'sprite-tokens-css' => '1706b943',
|
'sprite-tokens-css' => '1706b943',
|
||||||
'syntax-highlighting-css' => '56c1ba38',
|
'syntax-highlighting-css' => '56c1ba38',
|
||||||
|
@ -1210,12 +1210,6 @@ return array(
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
'javelin-vector',
|
'javelin-vector',
|
||||||
),
|
),
|
||||||
'5dd3e41b' => array(
|
|
||||||
'javelin-install',
|
|
||||||
'javelin-dom',
|
|
||||||
'javelin-stratcom',
|
|
||||||
'javelin-vector',
|
|
||||||
),
|
|
||||||
'5fefb143' => array(
|
'5fefb143' => array(
|
||||||
'javelin-behavior',
|
'javelin-behavior',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
|
@ -1617,6 +1611,12 @@ return array(
|
||||||
'b3e7d692' => array(
|
'b3e7d692' => array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
),
|
),
|
||||||
|
'b403b668' => array(
|
||||||
|
'javelin-install',
|
||||||
|
'javelin-dom',
|
||||||
|
'javelin-stratcom',
|
||||||
|
'javelin-vector',
|
||||||
|
),
|
||||||
'b42eddc7' => array(
|
'b42eddc7' => array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
|
@ -2021,7 +2021,7 @@ return array(
|
||||||
'phabricator-filetree-view-css',
|
'phabricator-filetree-view-css',
|
||||||
'phabricator-nav-view-css',
|
'phabricator-nav-view-css',
|
||||||
'phabricator-side-menu-view-css',
|
'phabricator-side-menu-view-css',
|
||||||
'phabricator-crumbs-view-css',
|
'phui-crumbs-view-css',
|
||||||
'phui-object-item-list-view-css',
|
'phui-object-item-list-view-css',
|
||||||
'global-drag-and-drop-css',
|
'global-drag-and-drop-css',
|
||||||
'phui-spacing-css',
|
'phui-spacing-css',
|
||||||
|
|
|
@ -72,112 +72,14 @@ abstract class DiffusionQuery extends PhabricatorQuery {
|
||||||
|
|
||||||
$params = $params + $core_params;
|
$params = $params + $core_params;
|
||||||
|
|
||||||
$service_phid = $repository->getAlmanacServicePHID();
|
$client = $repository->newConduitClient($user);
|
||||||
if ($service_phid === null) {
|
if (!$client) {
|
||||||
return id(new ConduitCall($method, $params))
|
return id(new ConduitCall($method, $params))
|
||||||
->setUser($user)
|
->setUser($user)
|
||||||
->execute();
|
->execute();
|
||||||
}
|
|
||||||
|
|
||||||
$service = id(new AlmanacServiceQuery())
|
|
||||||
->setViewer(PhabricatorUser::getOmnipotentUser())
|
|
||||||
->withPHIDs(array($service_phid))
|
|
||||||
->needBindings(true)
|
|
||||||
->executeOne();
|
|
||||||
if (!$service) {
|
|
||||||
throw new Exception(
|
|
||||||
pht(
|
|
||||||
'The Alamnac service for this repository is invalid or could not '.
|
|
||||||
'be loaded.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$service_type = $service->getServiceType();
|
|
||||||
if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) {
|
|
||||||
throw new Exception(
|
|
||||||
pht(
|
|
||||||
'The Alamnac service for this repository does not have the correct '.
|
|
||||||
'service type.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$bindings = $service->getBindings();
|
|
||||||
if (!$bindings) {
|
|
||||||
throw new Exception(
|
|
||||||
pht(
|
|
||||||
'The Alamanc service for this repository is not bound to any '.
|
|
||||||
'interfaces.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$uris = array();
|
|
||||||
foreach ($bindings as $binding) {
|
|
||||||
$iface = $binding->getInterface();
|
|
||||||
|
|
||||||
$protocol = $binding->getAlmanacPropertyValue('protocol');
|
|
||||||
if ($protocol === 'http') {
|
|
||||||
$uris[] = 'http://'.$iface->renderDisplayAddress().'/';
|
|
||||||
} else if ($protocol === 'https' || $protocol === null) {
|
|
||||||
$uris[] = 'https://'.$iface->renderDisplayAddress().'/';
|
|
||||||
} else {
|
|
||||||
throw new Exception(
|
|
||||||
pht(
|
|
||||||
'The Almanac service for this repository has a binding to an '.
|
|
||||||
'invalid interface with an unknown protocol ("%s").',
|
|
||||||
$protocol));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shuffle($uris);
|
|
||||||
$uri = head($uris);
|
|
||||||
|
|
||||||
$domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain();
|
|
||||||
|
|
||||||
$client = id(new ConduitClient($uri))
|
|
||||||
->setHost($domain);
|
|
||||||
|
|
||||||
if ($user->isOmnipotent()) {
|
|
||||||
// If the caller is the omnipotent user (normally, a daemon), we will
|
|
||||||
// sign the request with this host's asymmetric keypair.
|
|
||||||
|
|
||||||
$public_path = AlmanacKeys::getKeyPath('device.pub');
|
|
||||||
try {
|
|
||||||
$public_key = Filesystem::readFile($public_path);
|
|
||||||
} catch (Exception $ex) {
|
|
||||||
throw new PhutilAggregateException(
|
|
||||||
pht(
|
|
||||||
'Unable to read device public key while attempting to make '.
|
|
||||||
'authenticated method call within the Phabricator cluster. '.
|
|
||||||
'Use `bin/almanac register` to register keys for this device. '.
|
|
||||||
'Exception: %s',
|
|
||||||
$ex->getMessage()),
|
|
||||||
array($ex));
|
|
||||||
}
|
|
||||||
|
|
||||||
$private_path = AlmanacKeys::getKeyPath('device.key');
|
|
||||||
try {
|
|
||||||
$private_key = Filesystem::readFile($private_path);
|
|
||||||
$private_key = new PhutilOpaqueEnvelope($private_key);
|
|
||||||
} catch (Exception $ex) {
|
|
||||||
throw new PhutilAggregateException(
|
|
||||||
pht(
|
|
||||||
'Unable to read device private key while attempting to make '.
|
|
||||||
'authenticated method call within the Phabricator cluster. '.
|
|
||||||
'Use `bin/almanac register` to register keys for this device. '.
|
|
||||||
'Exception: %s',
|
|
||||||
$ex->getMessage()),
|
|
||||||
array($ex));
|
|
||||||
}
|
|
||||||
|
|
||||||
$client->setSigningKeys($public_key, $private_key);
|
|
||||||
} else {
|
} else {
|
||||||
// If the caller is a normal user, we generate or retrieve a cluster
|
return $client->callMethodSynchronous($method, $params);
|
||||||
// API token.
|
|
||||||
|
|
||||||
$token = PhabricatorConduitToken::loadClusterTokenForUser($user);
|
|
||||||
if ($token) {
|
|
||||||
$client->setConduitToken($token->getToken());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $client->callMethodSynchronous($method, $params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute() {
|
public function execute() {
|
||||||
|
|
|
@ -1516,6 +1516,125 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a new Conduit client in order to make a service call to this
|
||||||
|
* repository.
|
||||||
|
*
|
||||||
|
* If the repository is hosted locally, this method returns `null`. The
|
||||||
|
* caller should use `ConduitCall` or other local logic to complete the
|
||||||
|
* request.
|
||||||
|
*
|
||||||
|
* @return ConduitClient|null Client, or `null` for local repositories.
|
||||||
|
*/
|
||||||
|
public function newConduitClient(PhabricatorUser $viewer) {
|
||||||
|
$service_phid = $this->getAlmanacServicePHID();
|
||||||
|
if (!$service_phid) {
|
||||||
|
// No service, so this is a local repository.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = id(new AlmanacServiceQuery())
|
||||||
|
->setViewer(PhabricatorUser::getOmnipotentUser())
|
||||||
|
->withPHIDs(array($service_phid))
|
||||||
|
->needBindings(true)
|
||||||
|
->executeOne();
|
||||||
|
if (!$service) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'The Alamnac service for this repository is invalid or could not '.
|
||||||
|
'be loaded.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$service_type = $service->getServiceType();
|
||||||
|
if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'The Alamnac service for this repository does not have the correct '.
|
||||||
|
'service type.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$bindings = $service->getBindings();
|
||||||
|
if (!$bindings) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'The Alamanc service for this repository is not bound to any '.
|
||||||
|
'interfaces.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$uris = array();
|
||||||
|
foreach ($bindings as $binding) {
|
||||||
|
$iface = $binding->getInterface();
|
||||||
|
|
||||||
|
$protocol = $binding->getAlmanacPropertyValue('protocol');
|
||||||
|
if ($protocol === 'http') {
|
||||||
|
$uris[] = 'http://'.$iface->renderDisplayAddress().'/';
|
||||||
|
} else if ($protocol === 'https' || $protocol === null) {
|
||||||
|
$uris[] = 'https://'.$iface->renderDisplayAddress().'/';
|
||||||
|
} else {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'The Almanac service for this repository has a binding to an '.
|
||||||
|
'invalid interface with an unknown protocol ("%s").',
|
||||||
|
$protocol));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffle($uris);
|
||||||
|
$uri = head($uris);
|
||||||
|
|
||||||
|
$domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain();
|
||||||
|
|
||||||
|
$client = id(new ConduitClient($uri))
|
||||||
|
->setHost($domain);
|
||||||
|
|
||||||
|
if ($viewer->isOmnipotent()) {
|
||||||
|
// If the caller is the omnipotent user (normally, a daemon), we will
|
||||||
|
// sign the request with this host's asymmetric keypair.
|
||||||
|
|
||||||
|
$public_path = AlmanacKeys::getKeyPath('device.pub');
|
||||||
|
try {
|
||||||
|
$public_key = Filesystem::readFile($public_path);
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
throw new PhutilAggregateException(
|
||||||
|
pht(
|
||||||
|
'Unable to read device public key while attempting to make '.
|
||||||
|
'authenticated method call within the Phabricator cluster. '.
|
||||||
|
'Use `bin/almanac register` to register keys for this device. '.
|
||||||
|
'Exception: %s',
|
||||||
|
$ex->getMessage()),
|
||||||
|
array($ex));
|
||||||
|
}
|
||||||
|
|
||||||
|
$private_path = AlmanacKeys::getKeyPath('device.key');
|
||||||
|
try {
|
||||||
|
$private_key = Filesystem::readFile($private_path);
|
||||||
|
$private_key = new PhutilOpaqueEnvelope($private_key);
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
throw new PhutilAggregateException(
|
||||||
|
pht(
|
||||||
|
'Unable to read device private key while attempting to make '.
|
||||||
|
'authenticated method call within the Phabricator cluster. '.
|
||||||
|
'Use `bin/almanac register` to register keys for this device. '.
|
||||||
|
'Exception: %s',
|
||||||
|
$ex->getMessage()),
|
||||||
|
array($ex));
|
||||||
|
}
|
||||||
|
|
||||||
|
$client->setSigningKeys($public_key, $private_key);
|
||||||
|
} else {
|
||||||
|
// If the caller is a normal user, we generate or retrieve a cluster
|
||||||
|
// API token.
|
||||||
|
|
||||||
|
$token = PhabricatorConduitToken::loadClusterTokenForUser($viewer);
|
||||||
|
if ($token) {
|
||||||
|
$client->setConduitToken($token->getToken());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
|
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue