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

Merge branch 'master' into redesign-2015

This commit is contained in:
epriestley 2015-06-16 19:34:36 -07:00
commit f1b7fd483e
64 changed files with 813 additions and 242 deletions

View file

@ -0,0 +1,17 @@
CREATE TABLE {$NAMESPACE}_diviner.edge (
src VARBINARY(64) NOT NULL,
type INT UNSIGNED NOT NULL,
dst VARBINARY(64) NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
seq INT UNSIGNED NOT NULL,
dataID INT UNSIGNED,
PRIMARY KEY (src, type, dst),
KEY src (src, type, dateCreated, seq),
UNIQUE KEY key_dst (dst, type, src)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
CREATE TABLE {$NAMESPACE}_diviner.edgedata (
id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,6 @@
ALTER TABLE {$NAMESPACE}_diviner.diviner_livebook
ADD COLUMN editPolicy VARBINARY(64) NOT NULL AFTER viewPolicy;
UPDATE {$NAMESPACE}_diviner.diviner_livebook
SET editPolicy = 'admin'
WHERE editPolicy = '';

View file

@ -0,0 +1,19 @@
CREATE TABLE {$NAMESPACE}_diviner.diviner_livebooktransaction (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
authorPHID VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
viewPolicy VARBINARY(64) NOT NULL,
editPolicy VARBINARY(64) NOT NULL,
commentPHID VARBINARY(64) DEFAULT NULL,
commentVersion INT UNSIGNED NOT NULL,
transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY key_phid (phid),
KEY key_object (objectPHID)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -648,19 +648,25 @@ phutil_register_library_map(array(
'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php', 'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php',
'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php', 'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php',
'DivinerBookController' => 'applications/diviner/controller/DivinerBookController.php', 'DivinerBookController' => 'applications/diviner/controller/DivinerBookController.php',
'DivinerBookEditController' => 'applications/diviner/controller/DivinerBookEditController.php',
'DivinerBookItemView' => 'applications/diviner/view/DivinerBookItemView.php', 'DivinerBookItemView' => 'applications/diviner/view/DivinerBookItemView.php',
'DivinerBookPHIDType' => 'applications/diviner/phid/DivinerBookPHIDType.php', 'DivinerBookPHIDType' => 'applications/diviner/phid/DivinerBookPHIDType.php',
'DivinerBookQuery' => 'applications/diviner/query/DivinerBookQuery.php', 'DivinerBookQuery' => 'applications/diviner/query/DivinerBookQuery.php',
'DivinerBookSearchIndexer' => 'applications/diviner/search/DivinerBookSearchIndexer.php', 'DivinerBookSearchIndexer' => 'applications/diviner/search/DivinerBookSearchIndexer.php',
'DivinerController' => 'applications/diviner/controller/DivinerController.php', 'DivinerController' => 'applications/diviner/controller/DivinerController.php',
'DivinerDAO' => 'applications/diviner/storage/DivinerDAO.php', 'DivinerDAO' => 'applications/diviner/storage/DivinerDAO.php',
'DivinerDefaultEditCapability' => 'applications/diviner/capability/DivinerDefaultEditCapability.php',
'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php', 'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php',
'DivinerDefaultViewCapability' => 'applications/diviner/capability/DivinerDefaultViewCapability.php',
'DivinerDiskCache' => 'applications/diviner/cache/DivinerDiskCache.php', 'DivinerDiskCache' => 'applications/diviner/cache/DivinerDiskCache.php',
'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php', 'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php',
'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php', 'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php',
'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php', 'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php',
'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php', 'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php',
'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php', 'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php',
'DivinerLiveBookEditor' => 'applications/diviner/editor/DivinerLiveBookEditor.php',
'DivinerLiveBookTransaction' => 'applications/diviner/storage/DivinerLiveBookTransaction.php',
'DivinerLiveBookTransactionQuery' => 'applications/diviner/query/DivinerLiveBookTransactionQuery.php',
'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php', 'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php',
'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php', 'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php',
'DivinerMainController' => 'applications/diviner/controller/DivinerMainController.php', 'DivinerMainController' => 'applications/diviner/controller/DivinerMainController.php',
@ -670,6 +676,7 @@ phutil_register_library_map(array(
'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php', 'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php',
'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php', 'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php',
'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php', 'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php',
'DivinerSchemaSpec' => 'applications/diviner/storage/DivinerSchemaSpec.php',
'DivinerSectionView' => 'applications/diviner/view/DivinerSectionView.php', 'DivinerSectionView' => 'applications/diviner/view/DivinerSectionView.php',
'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php', 'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php',
'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php', 'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php',
@ -1269,6 +1276,7 @@ phutil_register_library_map(array(
'PassphraseCredentialTypeTestCase' => 'applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php', 'PassphraseCredentialTypeTestCase' => 'applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php',
'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php', 'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php',
'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php', 'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php',
'PassphraseNoteCredentialType' => 'applications/passphrase/credentialtype/PassphraseNoteCredentialType.php',
'PassphrasePasswordCredentialType' => 'applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php', 'PassphrasePasswordCredentialType' => 'applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php',
'PassphrasePasswordKey' => 'applications/passphrase/keys/PassphrasePasswordKey.php', 'PassphrasePasswordKey' => 'applications/passphrase/keys/PassphrasePasswordKey.php',
'PassphraseQueryConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php', 'PassphraseQueryConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php',
@ -4005,13 +4013,16 @@ phutil_register_library_map(array(
'DivinerAtomizeWorkflow' => 'DivinerWorkflow', 'DivinerAtomizeWorkflow' => 'DivinerWorkflow',
'DivinerAtomizer' => 'Phobject', 'DivinerAtomizer' => 'Phobject',
'DivinerBookController' => 'DivinerController', 'DivinerBookController' => 'DivinerController',
'DivinerBookEditController' => 'DivinerController',
'DivinerBookItemView' => 'AphrontTagView', 'DivinerBookItemView' => 'AphrontTagView',
'DivinerBookPHIDType' => 'PhabricatorPHIDType', 'DivinerBookPHIDType' => 'PhabricatorPHIDType',
'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DivinerBookSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'DivinerBookSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
'DivinerController' => 'PhabricatorController', 'DivinerController' => 'PhabricatorController',
'DivinerDAO' => 'PhabricatorLiskDAO', 'DivinerDAO' => 'PhabricatorLiskDAO',
'DivinerDefaultEditCapability' => 'PhabricatorPolicyCapability',
'DivinerDefaultRenderer' => 'DivinerRenderer', 'DivinerDefaultRenderer' => 'DivinerRenderer',
'DivinerDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DivinerDiskCache' => 'Phobject', 'DivinerDiskCache' => 'Phobject',
'DivinerFileAtomizer' => 'DivinerAtomizer', 'DivinerFileAtomizer' => 'DivinerAtomizer',
'DivinerFindController' => 'DivinerController', 'DivinerFindController' => 'DivinerController',
@ -4020,8 +4031,13 @@ phutil_register_library_map(array(
'DivinerLiveBook' => array( 'DivinerLiveBook' => array(
'DivinerDAO', 'DivinerDAO',
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface', 'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
), ),
'DivinerLiveBookEditor' => 'PhabricatorApplicationTransactionEditor',
'DivinerLiveBookTransaction' => 'PhabricatorApplicationTransaction',
'DivinerLiveBookTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DivinerLivePublisher' => 'DivinerPublisher', 'DivinerLivePublisher' => 'DivinerPublisher',
'DivinerLiveSymbol' => array( 'DivinerLiveSymbol' => array(
'DivinerDAO', 'DivinerDAO',
@ -4036,6 +4052,7 @@ phutil_register_library_map(array(
'DivinerPublisher' => 'Phobject', 'DivinerPublisher' => 'Phobject',
'DivinerRenderer' => 'Phobject', 'DivinerRenderer' => 'Phobject',
'DivinerReturnTableView' => 'AphrontTagView', 'DivinerReturnTableView' => 'AphrontTagView',
'DivinerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DivinerSectionView' => 'AphrontTagView', 'DivinerSectionView' => 'AphrontTagView',
'DivinerStaticPublisher' => 'DivinerPublisher', 'DivinerStaticPublisher' => 'DivinerPublisher',
'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule', 'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule',
@ -4761,6 +4778,7 @@ phutil_register_library_map(array(
'PassphraseCredentialTypeTestCase' => 'PhabricatorTestCase', 'PassphraseCredentialTypeTestCase' => 'PhabricatorTestCase',
'PassphraseCredentialViewController' => 'PassphraseController', 'PassphraseCredentialViewController' => 'PassphraseController',
'PassphraseDAO' => 'PhabricatorLiskDAO', 'PassphraseDAO' => 'PhabricatorLiskDAO',
'PassphraseNoteCredentialType' => 'PassphraseCredentialType',
'PassphrasePasswordCredentialType' => 'PassphraseCredentialType', 'PassphrasePasswordCredentialType' => 'PassphraseCredentialType',
'PassphrasePasswordKey' => 'PassphraseAbstractKey', 'PassphrasePasswordKey' => 'PassphraseAbstractKey',
'PassphraseQueryConduitAPIMethod' => 'PassphraseConduitAPIMethod', 'PassphraseQueryConduitAPIMethod' => 'PassphraseConduitAPIMethod',

View file

@ -19,7 +19,7 @@ final class PhabricatorAuditActionConstants extends Phobject {
self::ACCEPT => pht("Accept Commit \xE2\x9C\x94"), self::ACCEPT => pht("Accept Commit \xE2\x9C\x94"),
self::RESIGN => pht('Resign from Audit'), self::RESIGN => pht('Resign from Audit'),
self::CLOSE => pht('Close Audit'), self::CLOSE => pht('Close Audit'),
self::ADD_CCS => pht('Add CCs'), self::ADD_CCS => pht('Add Subscribers'),
self::ADD_AUDITORS => pht('Add Auditors'), self::ADD_AUDITORS => pht('Add Auditors'),
); );

View file

@ -449,16 +449,27 @@ abstract class PhabricatorApplication
$class, $class,
PhabricatorUser $viewer) { PhabricatorUser $viewer) {
if (!self::isClassInstalled($class)) { $cache = PhabricatorCaches::getRequestCache();
return false; $viewer_phid = $viewer->getPHID();
} $key = 'app.'.$class.'.installed.'.$viewer_phid;
return PhabricatorPolicyFilter::hasCapability( $result = $cache->getKey($key);
if ($result === null) {
if (!self::isClassInstalled($class)) {
$result = false;
} else {
$result = PhabricatorPolicyFilter::hasCapability(
$viewer, $viewer,
self::getByClass($class), self::getByClass($class),
PhabricatorPolicyCapability::CAN_VIEW); PhabricatorPolicyCapability::CAN_VIEW);
} }
$cache->setKey($key, $result);
}
return $result;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -9,12 +9,16 @@ abstract class PhabricatorCalendarController extends PhabricatorController {
->setUser($this->getViewer()) ->setUser($this->getViewer())
->addAction( ->addAction(
id(new PhabricatorActionView()) id(new PhabricatorActionView())
->setName(pht('Create Private Event')) ->setName(pht('Create Event'))
->setHref('/calendar/event/create/?mode=private')) ->setHref('/calendar/event/create/'))
->addAction( ->addAction(
id(new PhabricatorActionView()) id(new PhabricatorActionView())
->setName(pht('Create Public Event')) ->setName(pht('Create Public Event'))
->setHref('/calendar/event/create/?mode=public')); ->setHref('/calendar/event/create/?mode=public'))
->addAction(
id(new PhabricatorActionView())
->setName(pht('Create Recurring Event'))
->setHref('/calendar/event/create/?mode=recurring'));
$crumbs->addAction( $crumbs->addAction(
id(new PHUIListItemView()) id(new PHUIListItemView())

View file

@ -291,7 +291,9 @@ final class PhabricatorCalendarEventViewController
if ($event->getInstanceOfEventPHID()) { if ($event->getInstanceOfEventPHID()) {
$properties->addProperty( $properties->addProperty(
pht('Recurrence of Event'), pht('Recurrence of Event'),
$viewer->renderHandle($event->getInstanceOfEventPHID())); pht('%s of %s',
$event->getSequenceIndex(),
$viewer->renderHandle($event->getInstanceOfEventPHID())->render()));
} }
} }

View file

@ -58,6 +58,9 @@ final class PhabricatorCalendarEventSearchEngine
$min_range = $this->getDateFrom($saved)->getEpoch(); $min_range = $this->getDateFrom($saved)->getEpoch();
$max_range = $this->getDateTo($saved)->getEpoch(); $max_range = $this->getDateTo($saved)->getEpoch();
$user_datasource = id(new PhabricatorPeopleUserFunctionDatasource())
->setViewer($viewer);
if ($this->isMonthView($saved) || if ($this->isMonthView($saved) ||
$this->isDayView($saved)) { $this->isDayView($saved)) {
list($start_year, $start_month, $start_day) = list($start_year, $start_month, $start_day) =
@ -124,11 +127,13 @@ final class PhabricatorCalendarEventSearchEngine
} }
$invited_phids = $saved->getParameter('invitedPHIDs'); $invited_phids = $saved->getParameter('invitedPHIDs');
$invited_phids = $user_datasource->evaluateTokens($invited_phids);
if ($invited_phids) { if ($invited_phids) {
$query->withInvitedPHIDs($invited_phids); $query->withInvitedPHIDs($invited_phids);
} }
$creator_phids = $saved->getParameter('creatorPHIDs'); $creator_phids = $saved->getParameter('creatorPHIDs');
$creator_phids = $user_datasource->evaluateTokens($creator_phids);
if ($creator_phids) { if ($creator_phids) {
$query->withCreatorPHIDs($creator_phids); $query->withCreatorPHIDs($creator_phids);
} }
@ -196,13 +201,13 @@ final class PhabricatorCalendarEventSearchEngine
$form $form
->appendControl( ->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleDatasource()) ->setDatasource(new PhabricatorPeopleUserFunctionDatasource())
->setName('creators') ->setName('creators')
->setLabel(pht('Created By')) ->setLabel(pht('Created By'))
->setValue($creator_phids)) ->setValue($creator_phids))
->appendControl( ->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleDatasource()) ->setDatasource(new PhabricatorPeopleUserFunctionDatasource())
->setName('invited') ->setName('invited')
->setLabel(pht('Invited')) ->setLabel(pht('Invited'))
->setValue($invited_phids)) ->setValue($invited_phids))

View file

@ -50,10 +50,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
if ($mode == 'public') { if ($mode == 'public') {
$view_policy = PhabricatorPolicies::getMostOpenPolicy(); $view_policy = PhabricatorPolicies::getMostOpenPolicy();
} else if ($mode == 'recurring') { }
if ($mode == 'recurring') {
$is_recurring = true; $is_recurring = true;
} else {
$view_policy = $actor->getPHID();
} }
return id(new PhabricatorCalendarEvent()) return id(new PhabricatorCalendarEvent())

View file

@ -27,14 +27,14 @@ final class PhabricatorAuthSetupCheck extends PhabricatorSetupCheck {
'You have not configured any authentication providers yet. You '. 'You have not configured any authentication providers yet. You '.
'should add a provider (like username/password, LDAP, or GitHub '. 'should add a provider (like username/password, LDAP, or GitHub '.
'OAuth) so users can register and log in. You can add and configure '. 'OAuth) so users can register and log in. You can add and configure '.
'providers using the [[%s | "Auth" application]].', 'providers using the Auth Application.');
'/auth/');
$this $this
->newIssue('auth.noproviders') ->newIssue('auth.noproviders')
->setShortName(pht('No Auth Providers')) ->setShortName(pht('No Auth Providers'))
->setName(pht('No Authentication Providers Configured')) ->setName(pht('No Authentication Providers Configured'))
->setMessage($message); ->setMessage($message)
->addLink('/auth/', pht('Auth Application'));
} }
} }
} }

View file

@ -355,6 +355,7 @@ final class ConpherenceThreadQuery
$start_epoch = $epochs['start_epoch']; $start_epoch = $epochs['start_epoch'];
$end_epoch = $epochs['end_epoch']; $end_epoch = $epochs['end_epoch'];
$events = array();
if ($participant_phids) { if ($participant_phids) {
$events = id(new PhabricatorCalendarEventQuery()) $events = id(new PhabricatorCalendarEventQuery())
->setViewer($this->getViewer()) ->setViewer($this->getViewer())
@ -363,8 +364,6 @@ final class ConpherenceThreadQuery
->withDateRange($start_epoch, $end_epoch) ->withDateRange($start_epoch, $end_epoch)
->execute(); ->execute();
$events = mpull($events, null, 'getPHID'); $events = mpull($events, null, 'getPHID');
} else {
$events = null;
} }
$invitees = array(); $invitees = array();

View file

@ -39,6 +39,7 @@ final class PhabricatorDivinerApplication extends PhabricatorApplication {
'find/' => 'DivinerFindController', 'find/' => 'DivinerFindController',
), ),
'/book/(?P<book>[^/]+)/' => 'DivinerBookController', '/book/(?P<book>[^/]+)/' => 'DivinerBookController',
'/book/(?P<book>[^/]+)/edit/' => 'DivinerBookEditController',
'/book/'. '/book/'.
'(?P<book>[^/]+)/'. '(?P<book>[^/]+)/'.
'(?P<type>[^/]+)/'. '(?P<type>[^/]+)/'.
@ -52,6 +53,18 @@ final class PhabricatorDivinerApplication extends PhabricatorApplication {
return self::GROUP_UTILITIES; return self::GROUP_UTILITIES;
} }
protected function getCustomCapabilities() {
return array(
DivinerDefaultViewCapability::CAPABILITY => array(
'template' => DivinerBookPHIDType::TYPECONST,
),
DivinerDefaultEditCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
'template' => DivinerBookPHIDType::TYPECONST,
),
);
}
public function getRemarkupRules() { public function getRemarkupRules() {
return array( return array(
new DivinerSymbolRemarkupRule(), new DivinerSymbolRemarkupRule(),

View file

@ -20,7 +20,7 @@ final class DivinerArticleAtomizer extends DivinerAtomizer {
$atom->setDocblockMetaValue('title', $title); $atom->setDocblockMetaValue('title', $title);
} }
// If the article has no @name, use the filename after stripping any // If the article has no `@name`, use the filename after stripping any
// extension. // extension.
$name = idx($meta, 'name'); $name = idx($meta, 'name');
if (!$name) { if (!$name) {

View file

@ -151,9 +151,9 @@ final class DivinerPHPAtomizer extends DivinerAtomizer {
if (count($docs) < count($params)) { if (count($docs) < count($params)) {
$atom->addWarning( $atom->addWarning(
pht( pht(
'This call takes %d parameters, but only %d are documented.', 'This call takes %s parameter(s), but only %s are documented.',
count($params), new PhutilNumber(count($params)),
count($docs))); new PhutilNumber(count($docs))));
} }
} }
@ -212,13 +212,13 @@ final class DivinerPHPAtomizer extends DivinerAtomizer {
if (preg_match('/@(return|param|task|author)/', $value, $matches)) { if (preg_match('/@(return|param|task|author)/', $value, $matches)) {
$atom->addWarning( $atom->addWarning(
pht( pht(
'Atom "%s" is preceded by a comment containing "@%s", but the '. 'Atom "%s" is preceded by a comment containing `%s`, but '.
'comment is not a documentation comment. Documentation '. 'the comment is not a documentation comment. Documentation '.
'comments must begin with "%s", followed by a newline. Did '. 'comments must begin with `%s`, followed by a newline. Did '.
'you mean to use a documentation comment? (As the comment is '. 'you mean to use a documentation comment? (As the comment is '.
'not a documentation comment, it will be ignored.)', 'not a documentation comment, it will be ignored.)',
$atom->getName(), $atom->getName(),
$matches[1], '@'.$matches[1],
'/**')); '/**'));
} }
} }
@ -248,8 +248,8 @@ final class DivinerPHPAtomizer extends DivinerAtomizer {
if ($matches[1] !== $name) { if ($matches[1] !== $name) {
$atom->addWarning( $atom->addWarning(
pht( pht(
'Parameter "%s" is named "%s" in the documentation. The '. 'Parameter "%s" is named "%s" in the documentation. '.
'documentation may be out of date.', 'The documentation may be out of date.',
$name, $name,
$matches[1])); $matches[1]));
} }
@ -292,8 +292,8 @@ final class DivinerPHPAtomizer extends DivinerAtomizer {
if ($return) { if ($return) {
$atom->addWarning( $atom->addWarning(
pht( pht(
'Method %s has explicitly documented %s. The %s method always '. 'Method `%s` has explicitly documented `%s`. The `%s` method '.
'returns %s. Diviner documents this implicitly.', 'always returns `%s`. Diviner documents this implicitly.',
'__construct()', '__construct()',
'@return', '@return',
'__construct()', '__construct()',

View file

@ -18,6 +18,7 @@ final class DivinerAtomCache extends DivinerDiskCache {
public function delete() { public function delete() {
parent::delete(); parent::delete();
$this->fileHashMap = null; $this->fileHashMap = null;
$this->atomMap = null; $this->atomMap = null;
$this->atoms = array(); $this->atoms = array();

View file

@ -26,8 +26,8 @@ abstract class DivinerDiskCache extends Phobject {
* Convert a long-form hash key like `ccbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaN` into * Convert a long-form hash key like `ccbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaN` into
* a shortened directory form, like `cc/bb/aaaaaaaaN`. In conjunction with * a shortened directory form, like `cc/bb/aaaaaaaaN`. In conjunction with
* @{class:PhutilDirectoryKeyValueCache}, this gives us nice directories * @{class:PhutilDirectoryKeyValueCache}, this gives us nice directories
* inside .divinercache instead of a million hash files with huge names at * inside `.divinercache` instead of a million hash files with huge names at
* top level. * the top level.
*/ */
protected function getHashKey($hash) { protected function getHashKey($hash) {
return implode( return implode(

View file

@ -45,6 +45,7 @@ final class DivinerPublishCache extends DivinerDiskCache {
/* -( Index )-------------------------------------------------------------- */ /* -( Index )-------------------------------------------------------------- */
public function getIndex() { public function getIndex() {
if ($this->index === null) { if ($this->index === null) {
$this->index = $this->getCache()->getKey('index', array()); $this->index = $this->getCache()->getKey('index', array());

View file

@ -0,0 +1,11 @@
<?php
final class DivinerDefaultEditCapability extends PhabricatorPolicyCapability {
const CAPABILITY = 'diviner.default.edit';
public function getCapabilityName() {
return pht('Default Edit Policy');
}
}

View file

@ -0,0 +1,15 @@
<?php
final class DivinerDefaultViewCapability extends PhabricatorPolicyCapability {
const CAPABILITY = 'diviner.default.view';
public function getCapabilityName() {
return pht('Default View Policy');
}
public function shouldAllowPublicPolicySetting() {
return true;
}
}

View file

@ -2,33 +2,24 @@
final class DivinerAtomController extends DivinerController { final class DivinerAtomController extends DivinerController {
private $bookName;
private $atomType;
private $atomName;
private $atomContext;
private $atomIndex;
public function shouldAllowPublic() { public function shouldAllowPublic() {
return true; return true;
} }
public function willProcessRequest(array $data) { public function handleRequest(AphrontRequest $request) {
$this->bookName = $data['book'];
$this->atomType = $data['type'];
$this->atomName = $data['name'];
$this->atomContext = nonempty(idx($data, 'context'), null);
$this->atomIndex = nonempty(idx($data, 'index'), null);
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser(); $viewer = $request->getUser();
$book_name = $request->getURIData('book');
$atom_type = $request->getURIData('type');
$atom_name = $request->getURIData('name');
$atom_context = nonempty($request->getURIData('context'), null);
$atom_index = nonempty($request->getURIData('index'), null);
require_celerity_resource('diviner-shared-css'); require_celerity_resource('diviner-shared-css');
$book = id(new DivinerBookQuery()) $book = id(new DivinerBookQuery())
->setViewer($viewer) ->setViewer($viewer)
->withNames(array($this->bookName)) ->withNames(array($book_name))
->executeOne(); ->executeOne();
if (!$book) { if (!$book) {
@ -38,10 +29,10 @@ final class DivinerAtomController extends DivinerController {
$symbol = id(new DivinerAtomQuery()) $symbol = id(new DivinerAtomQuery())
->setViewer($viewer) ->setViewer($viewer)
->withBookPHIDs(array($book->getPHID())) ->withBookPHIDs(array($book->getPHID()))
->withTypes(array($this->atomType)) ->withTypes(array($atom_type))
->withNames(array($this->atomName)) ->withNames(array($atom_name))
->withContexts(array($this->atomContext)) ->withContexts(array($atom_context))
->withIndexes(array($this->atomIndex)) ->withIndexes(array($atom_index))
->withIsDocumentable(true) ->withIsDocumentable(true)
->needAtoms(true) ->needAtoms(true)
->needExtends(true) ->needExtends(true)
@ -75,7 +66,7 @@ final class DivinerAtomController extends DivinerController {
->setName(DivinerAtom::getAtomTypeNameString( ->setName(DivinerAtom::getAtomTypeNameString(
$atom ? $atom->getType() : $symbol->getType()))); $atom ? $atom->getType() : $symbol->getType())));
$properties = id(new PHUIPropertyListView()); $properties = new PHUIPropertyListView();
$group = $atom ? $atom->getProperty('group') : $symbol->getGroupName(); $group = $atom ? $atom->getProperty('group') : $symbol->getGroupName();
if ($group) { if ($group) {
@ -133,9 +124,7 @@ final class DivinerAtomController extends DivinerController {
$document->appendChild( $document->appendChild(
id(new PHUIInfoView()) id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild( ->appendChild(pht('This atom no longer exists.')));
pht(
'This atom no longer exists.')));
} }
if ($atom) { if ($atom) {
@ -303,7 +292,6 @@ final class DivinerAtomController extends DivinerController {
pht('Implements'), pht('Implements'),
phutil_implode_html(phutil_tag('br'), $items)); phutil_implode_html(phutil_tag('br'), $items));
} }
} }
private function renderAtomTag(DivinerLiveSymbol $symbol) { private function renderAtomTag(DivinerLiveSymbol $symbol) {
@ -470,6 +458,7 @@ final class DivinerAtomController extends DivinerController {
private function renderFullSignature( private function renderFullSignature(
DivinerLiveSymbol $symbol, DivinerLiveSymbol $symbol,
$is_link = false) { $is_link = false) {
switch ($symbol->getType()) { switch ($symbol->getType()) {
case DivinerAtom::TYPE_CLASS: case DivinerAtom::TYPE_CLASS:
case DivinerAtom::TYPE_INTERFACE: case DivinerAtom::TYPE_INTERFACE:

View file

@ -2,20 +2,15 @@
final class DivinerAtomListController extends DivinerController { final class DivinerAtomListController extends DivinerController {
private $key;
public function shouldAllowPublic() { public function shouldAllowPublic() {
return true; return true;
} }
public function willProcessRequest(array $data) { public function handleRequest(AphrontRequest $request) {
$this->key = idx($data, 'key', 'all'); $query_key = $request->getURIData('key');
}
public function processRequest() {
$request = $this->getRequest();
$controller = id(new PhabricatorApplicationSearchController()) $controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($this->key) ->setQueryKey($query_key)
->setSearchEngine(new DivinerAtomSearchEngine()) ->setSearchEngine(new DivinerAtomSearchEngine())
->setNavigation($this->buildSideNavView()); ->setNavigation($this->buildSideNavView());

View file

@ -2,41 +2,46 @@
final class DivinerBookController extends DivinerController { final class DivinerBookController extends DivinerController {
private $bookName;
public function shouldAllowPublic() { public function shouldAllowPublic() {
return true; return true;
} }
public function willProcessRequest(array $data) { public function handleRequest(AphrontRequest $request) {
$this->bookName = $data['book']; $viewer = $request->getViewer();
}
public function processRequest() { $book_name = $request->getURIData('book');
$request = $this->getRequest();
$viewer = $request->getUser();
$book = id(new DivinerBookQuery()) $book = id(new DivinerBookQuery())
->setViewer($viewer) ->setViewer($viewer)
->withNames(array($this->bookName)) ->withNames(array($book_name))
->executeOne(); ->executeOne();
if (!$book) { if (!$book) {
return new Aphront404Response(); return new Aphront404Response();
} }
$actions = $this->buildActionView($viewer, $book);
$crumbs = $this->buildApplicationCrumbs(); $crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true); $crumbs->setBorder(true);
$crumbs->addTextCrumb( $crumbs->addTextCrumb(
$book->getShortTitle(), $book->getShortTitle(),
'/book/'.$book->getName().'/'); '/book/'.$book->getName().'/');
$action_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Actions'))
->setHref('#')
->setIconFont('fa-bars')
->addClass('phui-mobile-menu')
->setDropdownMenu($actions);
$header = id(new PHUIHeaderView()) $header = id(new PHUIHeaderView())
->setHeader($book->getTitle()) ->setHeader($book->getTitle())
->setUser($viewer) ->setUser($viewer)
->setPolicyObject($book) ->setPolicyObject($book)
->setEpoch($book->getDateModified()); ->setEpoch($book->getDateModified())
->addActionLink($action_button);
$document = new PHUIDocumentView(); $document = new PHUIDocumentView();
$document->setHeader($header); $document->setHeader($header);
@ -99,4 +104,28 @@ final class DivinerBookController extends DivinerController {
)); ));
} }
private function buildActionView(
PhabricatorUser $user,
DivinerLiveBook $book) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$book,
PhabricatorPolicyCapability::CAN_EDIT);
$action_view = id(new PhabricatorActionListView())
->setUser($user)
->setObject($book)
->setObjectURI($this->getRequest()->getRequestURI());
$action_view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Book'))
->setIcon('fa-pencil')
->setHref('/book/'.$book->getName().'/edit/')
->setDisabled(!$can_edit));
return $action_view;
}
} }

View file

@ -0,0 +1,117 @@
<?php
final class DivinerBookEditController extends DivinerController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$book_name = $request->getURIData('book');
$book = id(new DivinerBookQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->needProjectPHIDs(true)
->withNames(array($book_name))
->executeOne();
if (!$book) {
return new Aphront404Response();
}
$view_uri = '/book/'.$book->getName().'/';
if ($request->isFormPost()) {
$v_projects = $request->getArr('projectPHIDs');
$v_view = $request->getStr('viewPolicy');
$v_edit = $request->getStr('editPolicy');
$xactions = array();
$xactions[] = id(new DivinerLiveBookTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array_fuse($v_projects),
));
$xactions[] = id(new DivinerLiveBookTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($v_view);
$xactions[] = id(new DivinerLiveBookTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($v_edit);
id(new DivinerLiveBookEditor())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->setActor($viewer)
->applyTransactions($book, $xactions);
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Basics'));
$title = pht('Edit %s', $book->getTitle());
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($book)
->execute();
$view_capability = PhabricatorPolicyCapability::CAN_VIEW;
$edit_capability = PhabricatorPolicyCapability::CAN_EDIT;
$form = id(new AphrontFormView())
->setUser($viewer)
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorProjectDatasource())
->setName('projectPHIDs')
->setLabel(pht('Projects'))
->setValue($book->getProjectPHIDs()))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('viewPolicy')
->setPolicyObject($book)
->setCapability($view_capability)
->setPolicies($policies)
->setCaption($book->describeAutomaticCapability($view_capability)))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('editPolicy')
->setPolicyObject($book)
->setCapability($edit_capability)
->setPolicies($policies)
->setCaption($book->describeAutomaticCapability($edit_capability)))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save'))
->addCancelButton($view_uri));
$object_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setForm($form);
$timeline = $this->buildTransactionTimeline(
$book,
new DivinerLiveBookTransactionQuery());
$timeline->setShouldTerminate(true);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
$timeline,
),
array(
'title' => $title,
));
}
}

View file

@ -3,19 +3,15 @@
abstract class DivinerController extends PhabricatorController { abstract class DivinerController extends PhabricatorController {
protected function buildSideNavView() { protected function buildSideNavView() {
$menu = $this->buildMenu(); $menu = $this->buildApplicationMenu();
return AphrontSideNavFilterView::newFromMenu($menu); return AphrontSideNavFilterView::newFromMenu($menu);
} }
public function buildApplicationMenu() { public function buildApplicationMenu() {
return $this->buildMenu();
}
private function buildMenu() {
$menu = new PHUIListView(); $menu = new PHUIListView();
id(new DivinerAtomSearchEngine()) id(new DivinerAtomSearchEngine())
->setViewer($this->getRequest()->getUser()) ->setViewer($this->getRequest()->getViewer())
->addNavigationItems($menu); ->addNavigationItems($menu);
return $menu; return $menu;
@ -24,12 +20,8 @@ abstract class DivinerController extends PhabricatorController {
protected function renderAtomList(array $symbols) { protected function renderAtomList(array $symbols) {
assert_instances_of($symbols, 'DivinerLiveSymbol'); assert_instances_of($symbols, 'DivinerLiveSymbol');
$request = $this->getRequest();
$user = $request->getUser();
$list = array(); $list = array();
foreach ($symbols as $symbol) { foreach ($symbols as $symbol) {
switch ($symbol->getType()) { switch ($symbol->getType()) {
case DivinerAtom::TYPE_FUNCTION: case DivinerAtom::TYPE_FUNCTION:
$title = $symbol->getTitle().'()'; $title = $symbol->getTitle().'()';
@ -43,8 +35,7 @@ abstract class DivinerController extends PhabricatorController {
->setTitle($title) ->setTitle($title)
->setHref($symbol->getURI()) ->setHref($symbol->getURI())
->setSubtitle($symbol->getSummary()) ->setSubtitle($symbol->getSummary())
->setType(DivinerAtom::getAtomTypeNameString( ->setType(DivinerAtom::getAtomTypeNameString($symbol->getType()));
$symbol->getType()));
$list[] = $item; $list[] = $item;
} }

View file

@ -6,9 +6,8 @@ final class DivinerFindController extends DivinerController {
return true; return true;
} }
public function processRequest() { public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest(); $viewer = $request->getViewer();
$viewer = $request->getUser();
$book_name = $request->getStr('book'); $book_name = $request->getStr('book');
$query_text = $request->getStr('name'); $query_text = $request->getStr('name');
@ -19,6 +18,7 @@ final class DivinerFindController extends DivinerController {
->setViewer($viewer) ->setViewer($viewer)
->withNames(array($book_name)) ->withNames(array($book_name))
->executeOne(); ->executeOne();
if (!$book) { if (!$book) {
return new Aphront404Response(); return new Aphront404Response();
} }
@ -70,8 +70,8 @@ final class DivinerFindController extends DivinerController {
->setTitle(pht('Documentation Not Found')) ->setTitle(pht('Documentation Not Found'))
->appendChild( ->appendChild(
pht( pht(
'Unable to find the specified documentation. You may have '. 'Unable to find the specified documentation. '.
'followed a bad or outdated link.')) 'You may have followed a bad or outdated link.'))
->addCancelButton($not_found_uri, pht('Read More Documentation')); ->addCancelButton($not_found_uri, pht('Read More Documentation'));
return id(new AphrontDialogResponse())->setDialog($dialog); return id(new AphrontDialogResponse())->setDialog($dialog);

View file

@ -6,9 +6,8 @@ final class DivinerMainController extends DivinerController {
return true; return true;
} }
public function processRequest() { public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest(); $viewer = $request->getViewer();
$viewer = $request->getUser();
$books = id(new DivinerBookQuery()) $books = id(new DivinerBookQuery())
->setViewer($viewer) ->setViewer($viewer)
@ -53,24 +52,20 @@ final class DivinerMainController extends DivinerController {
->appendChild($list); ->appendChild($list);
$document->appendChild($list); $document->appendChild($list);
} else { } else {
$text = pht( $text = pht(
"(NOTE) **Looking for Phabricator documentation?** If you're looking ". "(NOTE) **Looking for Phabricator documentation?** ".
"for help and information about Phabricator, you can ". "If you're looking for help and information about Phabricator, ".
"[[ https://secure.phabricator.com/diviner/ | browse the public ". "you can [[https://secure.phabricator.com/diviner/ | ".
"Phabricator documentation ]] on the live site.\n\n". "browse the public Phabricator documentation]] on the live site.\n\n".
"Diviner is the documentation generator used to build the Phabricator ". "Diviner is the documentation generator used to build the ".
"documentation.\n\n". "Phabricator documentation.\n\n".
"You haven't generated any Diviner documentation books yet, so ". "You haven't generated any Diviner documentation books yet, so ".
"there's nothing to show here. If you'd like to generate your own ". "there's nothing to show here. If you'd like to generate your own ".
"local copy of the Phabricator documentation and have it appear ". "local copy of the Phabricator documentation and have it appear ".
"here, run this command:\n\n". "here, run this command:\n\n".
" phabricator/ $ ./bin/diviner generate\n\n". " %s\n\n",
"Right now, Diviner isn't very useful for generating documentation ". 'phabricator/ $ ./bin/diviner generate');
"for projects other than Phabricator. If you're interested in using ".
"it in your own projects, leave feedback for us on ".
"[[ https://secure.phabricator.com/T4558 | T4558 ]].");
$text = PhabricatorMarkupEngine::renderOneObject( $text = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent($text), id(new PhabricatorMarkupOneOff())->setContent($text),

View file

@ -0,0 +1,23 @@
<?php
final class DivinerLiveBookEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorDivinerApplication';
}
public function getEditorObjectsDescription() {
return pht('Diviner Books');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
}

View file

@ -12,6 +12,14 @@ final class DivinerAtomPHIDType extends PhabricatorPHIDType {
return new DivinerLiveSymbol(); return new DivinerLiveSymbol();
} }
public function getTypeIcon() {
return 'fa-cube';
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorDivinerApplication';
}
protected function buildQueryForObjects( protected function buildQueryForObjects(
PhabricatorObjectQuery $query, PhabricatorObjectQuery $query,
array $phids) { array $phids) {

View file

@ -12,6 +12,14 @@ final class DivinerBookPHIDType extends PhabricatorPHIDType {
return new DivinerLiveBook(); return new DivinerLiveBook();
} }
public function getTypeIcon() {
return 'fa-book';
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorDivinerApplication';
}
protected function buildQueryForObjects( protected function buildQueryForObjects(
PhabricatorObjectQuery $query, PhabricatorObjectQuery $query,
array $phids) { array $phids) {

View file

@ -8,11 +8,15 @@ final class DivinerLivePublisher extends DivinerPublisher {
if (!$this->book) { if (!$this->book) {
$book_name = $this->getConfig('name'); $book_name = $this->getConfig('name');
$book = id(new DivinerLiveBook())->loadOneWhere('name = %s', $book_name); $book = id(new DivinerLiveBook())->loadOneWhere(
'name = %s',
$book_name);
if (!$book) { if (!$book) {
$book = id(new DivinerLiveBook()) $book = id(new DivinerLiveBook())
->setName($book_name) ->setName($book_name)
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
->save(); ->save();
} }
@ -144,7 +148,6 @@ final class DivinerLivePublisher extends DivinerPublisher {
->setContent(null) ->setContent(null)
->save(); ->save();
} }
} }
} }

View file

@ -133,10 +133,20 @@ abstract class DivinerPublisher extends Phobject {
$created = array_keys($created); $created = array_keys($created);
} }
echo pht('Deleting %d documents.', count($deleted))."\n"; $console = PhutilConsole::getConsole();
$console->writeOut(
"%s\n",
pht(
'Deleting %s document(s).',
new PhutilNumber(count($deleted))));
$this->deleteDocumentsByHash($deleted); $this->deleteDocumentsByHash($deleted);
echo pht('Creating %d documents.', count($created))."\n"; $console->writeOut(
"%s\n",
pht(
'Creating %s document(s).',
new PhutilNumber(count($created))));
$this->createDocumentsByHash($created); $this->createDocumentsByHash($created);
} }

View file

@ -79,7 +79,6 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $this; return $this;
} }
/** /**
* Include or exclude "ghosts", which are symbols which used to exist but do * Include or exclude "ghosts", which are symbols which used to exist but do
* not exist currently (for example, a function which existed in an older * not exist currently (for example, a function which existed in an older
@ -137,6 +136,7 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
foreach ($atoms as $key => $atom) { foreach ($atoms as $key => $atom) {
$book = idx($books, $atom->getBookPHID()); $book = idx($books, $atom->getBookPHID());
if (!$book) { if (!$book) {
$this->didRejectResult($atom);
unset($atoms[$key]); unset($atoms[$key]);
continue; continue;
} }
@ -158,12 +158,9 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
// Load all of the symbols this symbol extends, recursively. Commonly, // Load all of the symbols this symbol extends, recursively. Commonly,
// this means all the ancestor classes and interfaces it extends and // this means all the ancestor classes and interfaces it extends and
// implements. // implements.
if ($this->needExtends) { if ($this->needExtends) {
// First, load all the matching symbols by name. This does 99% of the // First, load all the matching symbols by name. This does 99% of the
// work in most cases, assuming things are named at all reasonably. // work in most cases, assuming things are named at all reasonably.
$names = array(); $names = array();
foreach ($atoms as $atom) { foreach ($atoms as $atom) {
if (!$atom->getAtom()) { if (!$atom->getAtom()) {
@ -303,6 +300,7 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
if ($this->titles) { if ($this->titles) {
$hashes = array(); $hashes = array();
foreach ($this->titles as $title) { foreach ($this->titles as $title) {
$slug = DivinerAtomRef::normalizeTitleString($title); $slug = DivinerAtomRef::normalizeTitleString($title);
$hash = PhabricatorHash::digestForIndex($slug); $hash = PhabricatorHash::digestForIndex($slug);
@ -318,6 +316,7 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
if ($this->contexts) { if ($this->contexts) {
$with_null = false; $with_null = false;
$contexts = $this->contexts; $contexts = $this->contexts;
foreach ($contexts as $key => $value) { foreach ($contexts as $key => $value) {
if ($value === null) { if ($value === null) {
unset($contexts[$key]); unset($contexts[$key]);
@ -373,10 +372,9 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
} }
if ($this->nameContains) { if ($this->nameContains) {
// NOTE: This CONVERT() call makes queries case-insensitive, since the // NOTE: This `CONVERT()` call makes queries case-insensitive, since
// column has binary collation. Eventually, this should move into // the column has binary collation. Eventually, this should move into
// fulltext. // fulltext.
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn_r,
'CONVERT(name USING utf8) LIKE %~', 'CONVERT(name USING utf8) LIKE %~',
@ -388,7 +386,6 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $this->formatWhereClause($where); return $this->formatWhereClause($where);
} }
/** /**
* Walk a list of atoms and collect all the node hashes of the atoms' * Walk a list of atoms and collect all the node hashes of the atoms'
* children. When recursing, also walk up the tree and collect children of * children. When recursing, also walk up the tree and collect children of
@ -413,6 +410,7 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
foreach ($child_hashes as $hash) { foreach ($child_hashes as $hash) {
$hashes[$hash] = $hash; $hashes[$hash] = $hash;
} }
if ($recurse_up) { if ($recurse_up) {
$hashes += $this->getAllChildHashes($symbol->getExtends(), true); $hashes += $this->getAllChildHashes($symbol->getExtends(), true);
} }
@ -421,7 +419,6 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $hashes; return $hashes;
} }
/** /**
* Attach child atoms to existing atoms. In recursive mode, also attach child * Attach child atoms to existing atoms. In recursive mode, also attach child
* atoms to atoms that these atoms extend. * atoms to atoms that these atoms extend.
@ -452,7 +449,9 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
$symbol_children[] = $children[$hash]; $symbol_children[] = $children[$hash];
} }
} }
$symbol->attachChildren($symbol_children); $symbol->attachChildren($symbol_children);
if ($recurse_up) { if ($recurse_up) {
$this->attachAllChildren($symbol->getExtends(), $children, true); $this->attachAllChildren($symbol->getExtends(), $children, true);
} }

View file

@ -6,6 +6,8 @@ final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $phids; private $phids;
private $names; private $names;
private $needProjectPHIDs;
public function withIDs(array $ids) { public function withIDs(array $ids) {
$this->ids = $ids; $this->ids = $ids;
return $this; return $this;
@ -21,6 +23,11 @@ final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $this; return $this;
} }
public function needProjectPHIDs($need_phids) {
$this->needProjectPHIDs = $need_phids;
return $this;
}
protected function loadPage() { protected function loadPage() {
$table = new DivinerLiveBook(); $table = new DivinerLiveBook();
$conn_r = $table->establishConnection('r'); $conn_r = $table->establishConnection('r');
@ -36,6 +43,30 @@ final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $table->loadAllFromArray($data); return $table->loadAllFromArray($data);
} }
protected function didFilterPage(array $books) {
assert_instances_of($books, 'DivinerLiveBook');
if ($this->needProjectPHIDs) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($books, 'getPHID'))
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
foreach ($books as $book) {
$project_phids = $edge_query->getDestinationPHIDs(
array(
$book->getPHID(),
));
$book->attachProjectPHIDs($project_phids);
}
}
return $books;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array(); $where = array();

View file

@ -0,0 +1,10 @@
<?php
final class DivinerLiveBookTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new DivinerLiveBookTransaction();
}
}

View file

@ -202,8 +202,8 @@ final class DivinerDefaultRenderer extends DivinerRenderer {
} }
if ($ref->getBook() != $this->getConfig('name')) { if ($ref->getBook() != $this->getConfig('name')) {
// If the ref is from a different book, we can't normalize it. Just return // If the ref is from a different book, we can't normalize it.
// it as-is if it has enough information to resolve. // Just return it as-is if it has enough information to resolve.
if ($ref->getName() && $ref->getType()) { if ($ref->getName() && $ref->getType()) {
return $ref; return $ref;
} else { } else {
@ -260,5 +260,4 @@ final class DivinerDefaultRenderer extends DivinerRenderer {
$ref->getTitle()); $ref->getTitle());
} }
} }

View file

@ -27,7 +27,15 @@ final class DivinerAtomSearchIndexer extends PhabricatorSearchDocumentIndexer {
PhabricatorSearchRelationship::RELATIONSHIP_BOOK, PhabricatorSearchRelationship::RELATIONSHIP_BOOK,
$atom->getBookPHID(), $atom->getBookPHID(),
DivinerBookPHIDType::TYPECONST, DivinerBookPHIDType::TYPECONST,
$book->getDateCreated()); PhabricatorTime::getNow());
$doc->addRelationship(
$atom->getGraphHash()
? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED
: PhabricatorSearchRelationship::RELATIONSHIP_OPEN,
$atom->getBookPHID(),
DivinerBookPHIDType::TYPECONST,
PhabricatorTime::getNow());
return $doc; return $doc;
} }

View file

@ -18,6 +18,11 @@ final class DivinerBookSearchIndexer extends PhabricatorSearchDocumentIndexer {
PhabricatorSearchDocumentFieldType::FIELD_BODY, PhabricatorSearchDocumentFieldType::FIELD_BODY,
$book->getPreface()); $book->getPreface());
$this->indexTransactions(
$doc,
new DivinerLiveBookTransactionQuery(),
array($phid));
return $doc; return $doc;
} }

View file

@ -3,12 +3,17 @@
final class DivinerLiveBook extends DivinerDAO final class DivinerLiveBook extends DivinerDAO
implements implements
PhabricatorPolicyInterface, PhabricatorPolicyInterface,
PhabricatorDestructibleInterface { PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
protected $name; protected $name;
protected $viewPolicy; protected $viewPolicy;
protected $editPolicy;
protected $configurationData = array(); protected $configurationData = array();
private $projectPHIDs = self::ATTACHABLE;
protected function getConfiguration() { protected function getConfiguration() {
return array( return array(
self::CONFIG_AUX_PHID => true, self::CONFIG_AUX_PHID => true,
@ -63,16 +68,33 @@ final class DivinerLiveBook extends DivinerDAO
return idx($spec, 'name', $group); return idx($spec, 'name', $group);
} }
public function attachProjectPHIDs(array $project_phids) {
$this->projectPHIDs = $project_phids;
return $this;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() { public function getCapabilities() {
return array( return array(
PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
); );
} }
public function getPolicy($capability) { public function getPolicy($capability) {
return PhabricatorPolicies::getMostOpenPolicy(); switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
} }
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
@ -83,8 +105,10 @@ final class DivinerLiveBook extends DivinerDAO
return null; return null;
} }
/* -( PhabricatorDestructibleInterface )----------------------------------- */ /* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently( public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) { PhabricatorDestructionEngine $engine) {
@ -102,4 +126,27 @@ final class DivinerLiveBook extends DivinerDAO
$this->saveTransaction(); $this->saveTransaction();
} }
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DivinerLiveBookEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DivinerLiveBookTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
} }

View file

@ -0,0 +1,18 @@
<?php
final class DivinerLiveBookTransaction
extends PhabricatorApplicationTransaction {
public function getApplicationName() {
return 'diviner';
}
public function getApplicationTransactionType() {
return DivinerBookPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return null;
}
}

View file

@ -137,10 +137,9 @@ final class DivinerLiveSymbol extends DivinerDAO
} }
public function save() { public function save() {
// NOTE: The identity hash is just a sanity check because the unique tuple // NOTE: The identity hash is just a sanity check because the unique tuple
// on this table is way way too long to fit into a normal UNIQUE KEY. We // on this table is way way too long to fit into a normal `UNIQUE KEY`.
// don't use it directly, but its existence prevents duplicate records. // We don't use it directly, but its existence prevents duplicate records.
if (!$this->identityHash) { if (!$this->identityHash) {
$this->identityHash = PhabricatorHash::digestForIndex( $this->identityHash = PhabricatorHash::digestForIndex(
@ -159,14 +158,17 @@ final class DivinerLiveSymbol extends DivinerDAO
public function getTitle() { public function getTitle() {
$title = parent::getTitle(); $title = parent::getTitle();
if (!strlen($title)) { if (!strlen($title)) {
$title = $this->getName(); $title = $this->getName();
} }
return $title; return $title;
} }
public function setTitle($value) { public function setTitle($value) {
$this->writeField('title', $value); $this->writeField('title', $value);
if (strlen($value)) { if (strlen($value)) {
$slug = DivinerAtomRef::normalizeTitleString($value); $slug = DivinerAtomRef::normalizeTitleString($value);
$hash = PhabricatorHash::digestForIndex($slug); $hash = PhabricatorHash::digestForIndex($slug);
@ -174,6 +176,7 @@ final class DivinerLiveSymbol extends DivinerDAO
} else { } else {
$this->titleSlugHash = null; $this->titleSlugHash = null;
} }
return $this; return $this;
} }
@ -200,16 +203,15 @@ final class DivinerLiveSymbol extends DivinerDAO
/* -( PhabricatorPolicyInterface )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() { public function getCapabilities() {
return $this->getBook()->getCapabilities(); return $this->getBook()->getCapabilities();
} }
public function getPolicy($capability) { public function getPolicy($capability) {
return $this->getBook()->getPolicy($capability); return $this->getBook()->getPolicy($capability);
} }
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBook()->hasAutomaticCapability($capability, $viewer); return $this->getBook()->hasAutomaticCapability($capability, $viewer);
} }
@ -219,19 +221,17 @@ final class DivinerLiveSymbol extends DivinerDAO
} }
/* -( Markup Interface )--------------------------------------------------- */ /* -( PhabricatorMarkupInterface )------------------------------------------ */
public function getMarkupFieldKey($field) { public function getMarkupFieldKey($field) {
return $this->getPHID().':'.$field.':'.$this->getGraphHash(); return $this->getPHID().':'.$field.':'.$this->getGraphHash();
} }
public function newMarkupEngine($field) { public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::getEngine('diviner'); return PhabricatorMarkupEngine::getEngine('diviner');
} }
public function getMarkupText($field) { public function getMarkupText($field) {
if (!$this->getAtom()) { if (!$this->getAtom()) {
return; return;
@ -240,21 +240,18 @@ final class DivinerLiveSymbol extends DivinerDAO
return $this->getAtom()->getDocblockText(); return $this->getAtom()->getDocblockText();
} }
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output; return $output;
} }
public function shouldUseMarkupCache($field) { public function shouldUseMarkupCache($field) {
return true; return true;
} }
/* -( PhabricatorDestructibleInterface )----------------------------------- */ /* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently( public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) { PhabricatorDestructionEngine $engine) {

View file

@ -0,0 +1,9 @@
<?php
final class DivinerSchemaSpec extends PhabricatorConfigSchemaSpec {
public function buildSchemata() {
$this->buildEdgeSchemata(new DivinerLiveBook());
}
}

View file

@ -36,8 +36,10 @@ final class DivinerAtomizeWorkflow extends DivinerWorkflow {
$atomizer_class = $args->getArg('atomizer'); $atomizer_class = $args->getArg('atomizer');
if (!$atomizer_class) { if (!$atomizer_class) {
throw new Exception( throw new PhutilArgumentUsageException(
pht('Specify an atomizer class with %s.', '--atomizer')); pht(
'Specify an atomizer class with %s.',
'--atomizer'));
} }
$symbols = id(new PhutilSymbolLoader()) $symbols = id(new PhutilSymbolLoader())
@ -46,7 +48,7 @@ final class DivinerAtomizeWorkflow extends DivinerWorkflow {
->setAncestorClass('DivinerAtomizer') ->setAncestorClass('DivinerAtomizer')
->selectAndLoadSymbols(); ->selectAndLoadSymbols();
if (!$symbols) { if (!$symbols) {
throw new Exception( throw new PhutilArgumentUsageException(
pht( pht(
"Atomizer class '%s' must be a concrete subclass of %s.", "Atomizer class '%s' must be a concrete subclass of %s.",
$atomizer_class, $atomizer_class,

View file

@ -50,6 +50,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
} else { } else {
$cwd = getcwd(); $cwd = getcwd();
$this->log(pht('FINDING DOCUMENTATION BOOKS')); $this->log(pht('FINDING DOCUMENTATION BOOKS'));
$books = id(new FileFinder($cwd)) $books = id(new FileFinder($cwd))
->withType('f') ->withType('f')
->withSuffix('book') ->withSuffix('book')
@ -92,7 +93,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
// amount of work we can, so that regenerating documentation after minor // amount of work we can, so that regenerating documentation after minor
// changes is quick. // changes is quick.
// //
// = ATOM CACHE = // = Atom Cache =
// //
// In the first stage, we find all the direct changes to source code since // In the first stage, we find all the direct changes to source code since
// the last run. This stage relies on two data structures: // the last run. This stage relies on two data structures:
@ -118,7 +119,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
// its methods). The File Hash Map contains an exhaustive list of all atoms // its methods). The File Hash Map contains an exhaustive list of all atoms
// with type "file", but not child atoms of those top-level atoms.) // with type "file", but not child atoms of those top-level atoms.)
// //
// = GRAPH CACHE = // = Graph Cache =
// //
// We now know which atoms exist, and can compare the Atom Map to some // We now know which atoms exist, and can compare the Atom Map to some
// existing cache to figure out what has changed. However, this isn't // existing cache to figure out what has changed. However, this isn't
@ -176,8 +177,9 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
->setConcreteOnly(true) ->setConcreteOnly(true)
->setAncestorClass('DivinerPublisher') ->setAncestorClass('DivinerPublisher')
->selectAndLoadSymbols(); ->selectAndLoadSymbols();
if (!$symbols) { if (!$symbols) {
throw new Exception( throw new PhutilArgumentUsageException(
pht( pht(
"Publisher class '%s' must be a concrete subclass of %s.", "Publisher class '%s' must be a concrete subclass of %s.",
$publisher_class, $publisher_class,
@ -188,22 +190,37 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
$this->publishDocumentation($args->getArg('clean'), $publisher); $this->publishDocumentation($args->getArg('clean'), $publisher);
} }
/* -( Atom Cache )--------------------------------------------------------- */ /* -( Atom Cache )--------------------------------------------------------- */
private function buildAtomCache() { private function buildAtomCache() {
$this->log(pht('BUILDING ATOM CACHE')); $this->log(pht('BUILDING ATOM CACHE'));
$file_hashes = $this->findFilesInProject(); $file_hashes = $this->findFilesInProject();
$this->log(pht('Found %d file(s) in project.', count($file_hashes))); $this->log(
pht(
'Found %s file(s) in project.',
new PhutilNumber(count($file_hashes))));
$this->deleteDeadAtoms($file_hashes); $this->deleteDeadAtoms($file_hashes);
$atomize = $this->getFilesToAtomize($file_hashes); $atomize = $this->getFilesToAtomize($file_hashes);
$this->log(pht('Found %d unatomized, uncached file(s).', count($atomize))); $this->log(
pht(
'Found %s unatomized, uncached file(s).',
new PhutilNumber(count($atomize))));
$file_atomizers = $this->getAtomizersForFiles($atomize); $file_atomizers = $this->getAtomizersForFiles($atomize);
$this->log(pht('Found %d file(s) to atomize.', count($file_atomizers))); $this->log(
pht(
'Found %s file(s) to atomize.',
new PhutilNumber(count($file_atomizers))));
$futures = $this->buildAtomizerFutures($file_atomizers); $futures = $this->buildAtomizerFutures($file_atomizers);
$this->log(pht('Atomizing %d file(s).', count($file_atomizers))); $this->log(
pht(
'Atomizing %s file(s).',
new PhutilNumber(count($file_atomizers))));
if ($futures) { if ($futures) {
$this->resolveAtomizerFutures($futures, $file_hashes); $this->resolveAtomizerFutures($futures, $file_hashes);
@ -344,6 +361,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
->setTotal(count($futures)); ->setTotal(count($futures));
$futures = id(new FutureIterator($futures)) $futures = id(new FutureIterator($futures))
->limit(4); ->limit(4);
foreach ($futures as $key => $future) { foreach ($futures as $key => $future) {
try { try {
$atoms = $future->resolveJSON(); $atoms = $future->resolveJSON();
@ -396,6 +414,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
/* -( Graph Cache )-------------------------------------------------------- */ /* -( Graph Cache )-------------------------------------------------------- */
private function buildGraphCache() { private function buildGraphCache() {
$this->log(pht('BUILDING GRAPH CACHE')); $this->log(pht('BUILDING GRAPH CACHE'));
@ -407,7 +426,10 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
$dirty_nhashes = array(); $dirty_nhashes = array();
$del_atoms = array_diff_key($symbol_map, $atoms); $del_atoms = array_diff_key($symbol_map, $atoms);
$this->log(pht('Found %d obsolete atom(s) in graph.', count($del_atoms))); $this->log(
pht(
'Found %s obsolete atom(s) in graph.',
new PhutilNumber(count($del_atoms))));
foreach ($del_atoms as $nhash => $shash) { foreach ($del_atoms as $nhash => $shash) {
$atom_cache->deleteSymbol($nhash); $atom_cache->deleteSymbol($nhash);
@ -418,7 +440,10 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
} }
$new_atoms = array_diff_key($atoms, $symbol_map); $new_atoms = array_diff_key($atoms, $symbol_map);
$this->log(pht('Found %d new atom(s) in graph.', count($new_atoms))); $this->log(
pht(
'Found %s new atom(s) in graph.',
new PhutilNumber(count($new_atoms))));
foreach ($new_atoms as $nhash => $ignored) { foreach ($new_atoms as $nhash => $ignored) {
$shash = $this->computeSymbolHash($nhash); $shash = $this->computeSymbolHash($nhash);
@ -454,7 +479,10 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
} }
} }
$this->log(pht('Found %d affected atoms.', count($dirty_nhashes))); $this->log(
pht(
'Found %s affected atoms.',
new PhutilNumber(count($dirty_nhashes))));
foreach ($dirty_nhashes as $nhash => $ignored) { foreach ($dirty_nhashes as $nhash => $ignored) {
$atom_cache->addGraph($nhash, $this->computeGraphHash($nhash)); $atom_cache->addGraph($nhash, $this->computeGraphHash($nhash));

View file

@ -850,8 +850,8 @@ abstract class HeraldAdapter extends Phobject {
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
$standard = array( $standard = array(
self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add emails to CC'), self::ACTION_ADD_CC => pht('Add Subscribers'),
self::ACTION_REMOVE_CC => pht('Remove emails from CC'), self::ACTION_REMOVE_CC => pht('Remove Subscribers'),
self::ACTION_EMAIL => pht('Send an email to'), self::ACTION_EMAIL => pht('Send an email to'),
self::ACTION_AUDIT => pht('Trigger an Audit by'), self::ACTION_AUDIT => pht('Trigger an Audit by'),
self::ACTION_FLAG => pht('Mark with flag'), self::ACTION_FLAG => pht('Mark with flag'),
@ -868,8 +868,8 @@ abstract class HeraldAdapter extends Phobject {
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
$standard = array( $standard = array(
self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add me to CC'), self::ACTION_ADD_CC => pht('Add me as a subscriber'),
self::ACTION_REMOVE_CC => pht('Remove me from CC'), self::ACTION_REMOVE_CC => pht('Remove me as a subscriber'),
self::ACTION_EMAIL => pht('Send me an email'), self::ACTION_EMAIL => pht('Send me an email'),
self::ACTION_AUDIT => pht('Trigger an Audit by me'), self::ACTION_AUDIT => pht('Trigger an Audit by me'),
self::ACTION_FLAG => pht('Mark with flag'), self::ACTION_FLAG => pht('Mark with flag'),

View file

@ -3,6 +3,8 @@
final class PhabricatorMailImplementationPHPMailerAdapter final class PhabricatorMailImplementationPHPMailerAdapter
extends PhabricatorMailImplementationAdapter { extends PhabricatorMailImplementationAdapter {
private $mailer;
/** /**
* @phutil-external-symbol class PHPMailer * @phutil-external-symbol class PHPMailer
*/ */

View file

@ -55,7 +55,7 @@ final class PhabricatorMailTarget extends Phobject {
return $this->viewer; return $this->viewer;
} }
public function sendMail(PhabricatorMetaMTAMail $mail) { public function willSendMail(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer(); $viewer = $this->getViewer();
$mail->addPHIDHeaders('X-Phabricator-To', $this->rawToPHIDs); $mail->addPHIDHeaders('X-Phabricator-To', $this->rawToPHIDs);
@ -92,7 +92,7 @@ final class PhabricatorMailTarget extends Phobject {
$mail->addCCs($cc); $mail->addCCs($cc);
} }
return $mail->save(); return $mail;
} }
private function getRecipientsSummary( private function getRecipientsSummary(

View file

@ -47,7 +47,7 @@ final class PassphraseCredentialEditController extends PassphraseController {
$is_new = true; $is_new = true;
// Prefill username if provided. // Prefill username if provided.
$credential->setUsername($request->getStr('username')); $credential->setUsername((string)$request->getStr('username'));
if (!$request->getStr('isInitialized')) { if (!$request->getStr('isInitialized')) {
$type->didInitializeNewCredential($viewer, $credential); $type->didInitializeNewCredential($viewer, $credential);
@ -151,10 +151,11 @@ final class PassphraseCredentialEditController extends PassphraseController {
$credential->openTransaction(); $credential->openTransaction();
if (!$credential->getIsLocked()) { if (!$credential->getIsLocked()) {
if ($type->shouldRequireUsername()) {
$xactions[] = id(new PassphraseCredentialTransaction()) $xactions[] = id(new PassphraseCredentialTransaction())
->setTransactionType($type_username) ->setTransactionType($type_username)
->setNewValue($v_username); ->setNewValue($v_username);
}
// If some value other than a sequence of bullets was provided for // If some value other than a sequence of bullets was provided for
// the credential, update it. In particular, note that we are // the credential, update it. In particular, note that we are
// explicitly allowing empty secrets: one use case is HTTP auth where // explicitly allowing empty secrets: one use case is HTTP auth where
@ -263,6 +264,7 @@ final class PassphraseCredentialEditController extends PassphraseController {
pht('This credential is permanently locked and can not be edited.')); pht('This credential is permanently locked and can not be edited.'));
} }
if ($type->shouldRequireUsername()) {
$form $form
->appendChild( ->appendChild(
id(new AphrontFormTextControl()) id(new AphrontFormTextControl())
@ -270,7 +272,9 @@ final class PassphraseCredentialEditController extends PassphraseController {
->setLabel(pht('Login/Username')) ->setLabel(pht('Login/Username'))
->setValue($v_username) ->setValue($v_username)
->setDisabled($credential_is_locked) ->setDisabled($credential_is_locked)
->setError($e_username)) ->setError($e_username));
}
$form
->appendChild( ->appendChild(
$secret_control $secret_control
->setName('secret') ->setName('secret')

View file

@ -182,9 +182,11 @@ final class PassphraseCredentialViewController extends PassphraseController {
pht('Editable By'), pht('Editable By'),
$descriptions[PhabricatorPolicyCapability::CAN_EDIT]); $descriptions[PhabricatorPolicyCapability::CAN_EDIT]);
if ($type->shouldRequireUsername()) {
$properties->addProperty( $properties->addProperty(
pht('Username'), pht('Username'),
$credential->getUsername()); $credential->getUsername());
}
$used_by_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $used_by_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$credential->getPHID(), $credential->getPHID(),

View file

@ -131,4 +131,8 @@ abstract class PassphraseCredentialType extends Phobject {
return $secret; return $secret;
} }
public function shouldRequireUsername() {
return true;
}
} }

View file

@ -0,0 +1,37 @@
<?php
final class PassphraseNoteCredentialType
extends PassphraseCredentialType {
const CREDENTIAL_TYPE = 'note';
const PROVIDES_TYPE = 'provides/note';
public function getCredentialType() {
return self::CREDENTIAL_TYPE;
}
public function getProvidesType() {
return self::PROVIDES_TYPE;
}
public function getCredentialTypeName() {
return pht('Note');
}
public function getCredentialTypeDescription() {
return pht('Store a plaintext note.');
}
public function getSecretLabel() {
return pht('Note');
}
public function newSecretControl() {
return id(new AphrontFormTextAreaControl());
}
public function shouldRequireUsername() {
return false;
}
}

View file

@ -174,6 +174,10 @@ final class PassphraseCredentialTransactionEditor
} }
break; break;
case PassphraseCredentialTransaction::TYPE_USERNAME: case PassphraseCredentialTransaction::TYPE_USERNAME:
$credential_type = $object->getCredentialTypeImplementation();
if (!$credential_type->shouldRequireUsername()) {
break;
}
$missing = $this->validateIsEmptyTextField( $missing = $this->validateIsEmptyTextField(
$object->getUsername(), $object->getUsername(),
$xactions); $xactions);

View file

@ -36,13 +36,14 @@ final class PhrictionMoveController extends PhrictionController {
// about it. // about it.
if (strlen($v_slug)) { if (strlen($v_slug)) {
$normal_slug = PhabricatorSlug::normalize($v_slug); $normal_slug = PhabricatorSlug::normalize($v_slug);
if ($normal_slug !== $v_slug) { $no_slash_slug = rtrim($normal_slug, '/');
if ($normal_slug !== $v_slug && $no_slash_slug !== $v_slug) {
return $this->newDialog() return $this->newDialog()
->setTitle(pht('Adjust Path')) ->setTitle(pht('Adjust Path'))
->appendParagraph( ->appendParagraph(
pht( pht(
'The path you entered (%s) is not a valid wiki document '. 'The path you entered (%s) is not a valid wiki document '.
'path. Paths may not contain special characters.', 'path. Paths may not contain spaces or special characters.',
phutil_tag('strong', array(), $v_slug))) phutil_tag('strong', array(), $v_slug)))
->appendParagraph( ->appendParagraph(
pht( pht(

View file

@ -391,6 +391,10 @@ final class PhrictionTransactionEditor
pht("A document's content changes."), pht("A document's content changes."),
PhrictionTransaction::MAILTAG_DELETE => PhrictionTransaction::MAILTAG_DELETE =>
pht('A document is deleted.'), pht('A document is deleted.'),
PhrictionTransaction::MAILTAG_SUBSCRIBERS =>
pht('A document\'s subscribers change.'),
PhrictionTransaction::MAILTAG_OTHER =>
pht('Other document activity not listed above occurs.'),
); );
} }

View file

@ -12,6 +12,8 @@ final class PhrictionTransaction
const MAILTAG_TITLE = 'phriction-title'; const MAILTAG_TITLE = 'phriction-title';
const MAILTAG_CONTENT = 'phriction-content'; const MAILTAG_CONTENT = 'phriction-content';
const MAILTAG_DELETE = 'phriction-delete'; const MAILTAG_DELETE = 'phriction-delete';
const MAILTAG_SUBSCRIBERS = 'phriction-subscribers';
const MAILTAG_OTHER = 'phriction-other';
public function getApplicationName() { public function getApplicationName() {
return 'phriction'; return 'phriction';
@ -280,7 +282,12 @@ final class PhrictionTransaction
case self::TYPE_DELETE: case self::TYPE_DELETE:
$tags[] = self::MAILTAG_DELETE; $tags[] = self::MAILTAG_DELETE;
break; break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$tags[] = self::MAILTAG_SUBSCRIBERS;
break;
default:
$tags[] = self::MAILTAG_OTHER;
break;
} }
return $tags; return $tags;
} }

View file

@ -433,6 +433,8 @@ final class PhabricatorProjectTransactionEditor
pht('Project membership changes.'), pht('Project membership changes.'),
PhabricatorProjectTransaction::MAILTAG_WATCHERS => PhabricatorProjectTransaction::MAILTAG_WATCHERS =>
pht('Project watcher list changes.'), pht('Project watcher list changes.'),
PhabricatorProjectTransaction::MAILTAG_SUBSCRIBERS =>
pht('Project subscribers change.'),
PhabricatorProjectTransaction::MAILTAG_OTHER => PhabricatorProjectTransaction::MAILTAG_OTHER =>
pht('Other project activity not listed above occurs.'), pht('Other project activity not listed above occurs.'),
); );

View file

@ -16,6 +16,7 @@ final class PhabricatorProjectTransaction
const MAILTAG_METADATA = 'project-metadata'; const MAILTAG_METADATA = 'project-metadata';
const MAILTAG_MEMBERS = 'project-members'; const MAILTAG_MEMBERS = 'project-members';
const MAILTAG_SUBSCRIBERS = 'project-subscribers';
const MAILTAG_WATCHERS = 'project-watchers'; const MAILTAG_WATCHERS = 'project-watchers';
const MAILTAG_OTHER = 'project-other'; const MAILTAG_OTHER = 'project-other';
@ -369,6 +370,9 @@ final class PhabricatorProjectTransaction
case self::TYPE_COLOR: case self::TYPE_COLOR:
$tags[] = self::MAILTAG_METADATA; $tags[] = self::MAILTAG_METADATA;
break; break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$tags[] = self::MAILTAG_SUBSCRIBERS;
break;
case PhabricatorTransactions::TYPE_EDGE: case PhabricatorTransactions::TYPE_EDGE:
$type = $this->getMetadata('edge:type'); $type = $this->getMetadata('edge:type');
$type = head($type); $type = head($type);

View file

@ -166,28 +166,22 @@ final class ReleephRequestTransactionalEditor
protected function shouldSendMail( protected function shouldSendMail(
PhabricatorLiskDAO $object, PhabricatorLiskDAO $object,
array $xactions) { array $xactions) {
return true;
}
protected function sendMail(
PhabricatorLiskDAO $object,
array $xactions) {
// Avoid sending emails that only talk about commit discovery. // Avoid sending emails that only talk about commit discovery.
$types = array_unique(mpull($xactions, 'getTransactionType')); $types = array_unique(mpull($xactions, 'getTransactionType'));
if ($types === array(ReleephRequestTransaction::TYPE_DISCOVERY)) { if ($types === array(ReleephRequestTransaction::TYPE_DISCOVERY)) {
return null; return false;
} }
// Don't email people when we discover that something picks or reverts OK. // Don't email people when we discover that something picks or reverts OK.
if ($types === array(ReleephRequestTransaction::TYPE_PICK_STATUS)) { if ($types === array(ReleephRequestTransaction::TYPE_PICK_STATUS)) {
if (!mfilter($xactions, 'isBoringPickStatus', true /* negate */)) { if (!mfilter($xactions, 'isBoringPickStatus', true /* negate */)) {
// If we effectively call "isInterestingPickStatus" and get nothing... // If we effectively call "isInterestingPickStatus" and get nothing...
return null; return false;
} }
} }
return parent::sendMail($object, $xactions); return true;
} }
protected function buildReplyHandler(PhabricatorLiskDAO $object) { protected function buildReplyHandler(PhabricatorLiskDAO $object) {

View file

@ -29,18 +29,25 @@ final class PhabricatorCommitBranchesField
'callsign' => $this->getObject()->getRepository()->getCallsign(), 'callsign' => $this->getObject()->getRepository()->getCallsign(),
); );
try {
$branches_raw = id(new ConduitCall('diffusion.branchquery', $params)) $branches_raw = id(new ConduitCall('diffusion.branchquery', $params))
->setUser($this->getViewer()) ->setUser($this->getViewer())
->execute(); ->execute();
$branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches_raw); $branches = DiffusionRepositoryRef::loadAllFromDictionaries(
$branches_raw);
if (!$branches) { if (!$branches) {
return; return;
} }
$branch_names = mpull($branches, 'getShortName'); $branch_names = mpull($branches, 'getShortName');
sort($branch_names); sort($branch_names);
$branch_text = implode(', ', $branch_names);
} catch (Exception $ex) {
$branch_text = pht('<%s: %s>', get_class($ex), $ex->getMessage());
}
$body->addTextSection(pht('BRANCHES'), implode(', ', $branch_names)); $body->addTextSection(pht('BRANCHES'), $branch_text);
} }
} }

View file

@ -1040,10 +1040,10 @@ abstract class PhabricatorApplicationTransactionEditor
// Hook for edges or other properties that may need (re-)loading // Hook for edges or other properties that may need (re-)loading
$object = $this->willPublish($object, $xactions); $object = $this->willPublish($object, $xactions);
$mailed = array(); $messages = array();
if (!$this->getDisableEmail()) { if (!$this->getDisableEmail()) {
if ($this->shouldSendMail($object, $xactions)) { if ($this->shouldSendMail($object, $xactions)) {
$mailed = $this->sendMail($object, $xactions); $messages = $this->buildMail($object, $xactions);
} }
} }
@ -1055,10 +1055,21 @@ abstract class PhabricatorApplicationTransactionEditor
} }
if ($this->shouldPublishFeedStory($object, $xactions)) { if ($this->shouldPublishFeedStory($object, $xactions)) {
$this->publishFeedStory( $mailed = array();
$object, foreach ($messages as $mail) {
$xactions, foreach ($mail->buildRecipientList() as $phid) {
$mailed); $mailed[$phid] = true;
}
}
$this->publishFeedStory($object, $xactions, $mailed);
}
// NOTE: This actually sends the mail. We do this last to reduce the chance
// that we send some mail, hit an exception, then send the mail again when
// retrying.
foreach ($messages as $mail) {
$mail->save();
} }
return $xactions; return $xactions;
@ -2241,7 +2252,7 @@ abstract class PhabricatorApplicationTransactionEditor
/** /**
* @task mail * @task mail
*/ */
protected function sendMail( private function buildMail(
PhabricatorLiskDAO $object, PhabricatorLiskDAO $object,
array $xactions) { array $xactions) {
@ -2255,8 +2266,7 @@ abstract class PhabricatorApplicationTransactionEditor
// Set this explicitly before we start swapping out the effective actor. // Set this explicitly before we start swapping out the effective actor.
$this->setActingAsPHID($this->getActingAsPHID()); $this->setActingAsPHID($this->getActingAsPHID());
$messages = array();
$mailed = array();
foreach ($targets as $target) { foreach ($targets as $target) {
$original_actor = $this->getActor(); $original_actor = $this->getActor();
@ -2270,7 +2280,7 @@ abstract class PhabricatorApplicationTransactionEditor
// Reload handles for the new viewer. // Reload handles for the new viewer.
$this->loadHandles($xactions); $this->loadHandles($xactions);
$mail = $this->sendMailToTarget($object, $xactions, $target); $mail = $this->buildMailForTarget($object, $xactions, $target);
} catch (Exception $ex) { } catch (Exception $ex) {
$caught = $ex; $caught = $ex;
} }
@ -2283,16 +2293,14 @@ abstract class PhabricatorApplicationTransactionEditor
} }
if ($mail) { if ($mail) {
foreach ($mail->buildRecipientList() as $phid) { $messages[] = $mail;
$mailed[$phid] = true;
}
} }
} }
return array_keys($mailed); return $messages;
} }
private function sendMailToTarget( private function buildMailForTarget(
PhabricatorLiskDAO $object, PhabricatorLiskDAO $object,
array $xactions, array $xactions,
PhabricatorMailTarget $target) { PhabricatorMailTarget $target) {
@ -2354,7 +2362,7 @@ abstract class PhabricatorApplicationTransactionEditor
$mail->setParentMessageID($this->getParentMessageID()); $mail->setParentMessageID($this->getParentMessageID());
} }
return $target->sendMail($mail); return $target->willSendMail($mail);
} }
private function addMailProjectMetadata( private function addMailProjectMetadata(

View file

@ -1124,6 +1124,52 @@ final class PhabricatorUSEnglishTranslation
'%s changed package owners, added: %4$s; removed: %6$s.', '%s changed package owners, added: %4$s; removed: %6$s.',
), ),
'Found %s book(s).' => array(
'Found %s book.',
'Found %s books.',
),
'Found %s file(s) in project.' => array(
'Found %s file in project.',
'Found %s files in project.',
),
'Found %s unatomized, uncached file(s).' => array(
'Found %s unatomized, uncached file.',
'Found %s unatomized, uncached files.',
),
'Found %s file(s) to atomize.' => array(
'Found %s file to atomize.',
'Found %s files to atomize.',
),
'Atomizing %s file(s).' => array(
'Atomizing %s file.',
'Atomizing %s files.',
),
'Creating %s document(s).' => array(
'Creating %s document.',
'Creating %s documents.',
),
'Deleting %s document(s).' => array(
'Deleting %s document.',
'Deleting %s documents.',
),
'Found %s obsolete atom(s) in graph.' => array(
'Found %s obsolete atom in graph.',
'Found %s obsolete atoms in graph.',
),
'Found %s new atom(s) in graph.' => array(
'Found %s new atom in graph.',
'Found %s new atoms in graph.',
),
'This call takes %s parameter(s), but only %s are documented.' => array(
array(
'This call takes %s parameter, but only %s is documented.',
'This call takes %s parameter, but only %s are documented.',
),
array(
'This call takes %s parameters, but only %s is documented.',
'This call takes %s parameters, but only %s are documented.',
),
),
); );
} }

View file

@ -872,6 +872,15 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
* @task order * @task order
*/ */
public function getOrderableColumns() { public function getOrderableColumns() {
$cache = PhabricatorCaches::getRequestCache();
$class = get_class($this);
$cache_key = 'query.orderablecolumns.'.$class;
$columns = $cache->getKey($cache_key);
if ($columns !== null) {
return $columns;
}
$columns = array( $columns = array(
'id' => array( 'id' => array(
'table' => $this->getPrimaryTableAlias(), 'table' => $this->getPrimaryTableAlias(),
@ -909,6 +918,8 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
} }
} }
$cache->setKey($cache_key, $columns);
return $columns; return $columns;
} }

View file

@ -35,7 +35,6 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
private $workspace = array(); private $workspace = array();
private $inFlightPHIDs = array(); private $inFlightPHIDs = array();
private $policyFilteredPHIDs = array(); private $policyFilteredPHIDs = array();
private $canUseApplication;
/** /**
* Should we continue or throw an exception when a query result is filtered * Should we continue or throw an exception when a query result is filtered
@ -679,21 +678,13 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
* execute the query. * execute the query.
*/ */
public function canViewerUseQueryApplication() { public function canViewerUseQueryApplication() {
if ($this->canUseApplication === null) {
$class = $this->getQueryApplicationClass(); $class = $this->getQueryApplicationClass();
if (!$class) { if (!$class) {
$this->canUseApplication = true; return true;
} else {
$result = id(new PhabricatorApplicationQuery())
->setViewer($this->getViewer())
->withClasses(array($class))
->execute();
$this->canUseApplication = (bool)$result;
}
} }
return $this->canUseApplication; $viewer = $this->getViewer();
return PhabricatorApplication::isClassInstalledForViewer($class, $viewer);
} }
} }