diff --git a/resources/sql/patches/20130826.divinernode.sql b/resources/sql/patches/20130826.divinernode.sql new file mode 100644 index 0000000000..265a7769b5 --- /dev/null +++ b/resources/sql/patches/20130826.divinernode.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_diviner.diviner_livesymbol + ADD nodeHash VARCHAR(64) COLLATE utf8_bin; + +ALTER TABLE {$NAMESPACE}_diviner.diviner_livesymbol + ADD UNIQUE KEY (nodeHash); diff --git a/src/applications/diviner/atom/DivinerAtom.php b/src/applications/diviner/atom/DivinerAtom.php index b4190ca7f9..22b0618162 100644 --- a/src/applications/diviner/atom/DivinerAtom.php +++ b/src/applications/diviner/atom/DivinerAtom.php @@ -1,365 +1,374 @@ getBook(), $this->getType(), $this->getContext(), $this->getName(), $this->getFile(), sprintf('%08', $this->getLine()), )); } public function setBook($book) { $this->book = $book; return $this; } public function getBook() { return $this->book; } public function setContext($context) { $this->context = $context; return $this; } public function getContext() { return $this->context; } public static function getAtomSerializationVersion() { return 2; } public function addWarning($warning) { $this->warnings[] = $warning; return $this; } public function getWarnings() { return $this->warnings; } public function setDocblockRaw($docblock_raw) { $this->docblockRaw = $docblock_raw; $parser = new PhutilDocblockParser(); list($text, $meta) = $parser->parse($docblock_raw); $this->docblockText = $text; $this->docblockMeta = $meta; return $this; } public function getDocblockRaw() { return $this->docblockRaw; } public function getDocblockText() { if ($this->docblockText === null) { throw new Exception("Call setDocblockRaw() before getDocblockText()!"); } return $this->docblockText; } public function getDocblockMeta() { if ($this->docblockMeta === null) { throw new Exception("Call setDocblockRaw() before getDocblockMeta()!"); } return $this->docblockMeta; } public function getDocblockMetaValue($key, $default = null) { $meta = $this->getDocblockMeta(); return idx($meta, $key, $default); } public function setDocblockMetaValue($key, $value) { $meta = $this->getDocblockMeta(); $meta[$key] = $value; $this->docblockMeta = $meta; return $this; } public function setType($type) { $this->type = $type; return $this; } public function getType() { return $this->type; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setFile($file) { $this->file = $file; return $this; } public function getFile() { return $this->file; } public function setLine($line) { $this->line = $line; return $this; } public function getLine() { return $this->line; } public function setContentRaw($content_raw) { $this->contentRaw = $content_raw; return $this; } public function getContentRaw() { return $this->contentRaw; } public function setHash($hash) { $this->hash = $hash; return $this; } public function addLink(DivinerAtomRef $ref) { $this->links[] = $ref; return $this; } public function addExtends(DivinerAtomRef $ref) { $this->extends[] = $ref; return $this; } public function getLinkDictionaries() { return mpull($this->links, 'toDictionary'); } public function getExtendsDictionaries() { return mpull($this->extends, 'toDictionary'); } + public function getExtends() { + return $this->extends; + } + public function getHash() { if ($this->hash) { return $this->hash; } $parts = array( $this->getType(), $this->getName(), $this->getFile(), $this->getLine(), $this->getLength(), $this->getLanguage(), $this->getContentRaw(), $this->getDocblockRaw(), $this->getProperties(), mpull($this->extends, 'toHash'), mpull($this->links, 'toHash'), ); return md5(serialize($parts)).'N'; } public function setLength($length) { $this->length = $length; return $this; } public function getLength() { return $this->length; } public function setLanguage($language) { $this->language = $language; return $this; } public function getLanguage() { return $this->language; } public function addChildHash($child_hash) { $this->childHashes[] = $child_hash; return $this; } public function getChildHashes() { return $this->childHashes; } public function setParentHash($parent_hash) { if ($this->parentHash) { throw new Exception("Atom already has a parent!"); } $this->parentHash = $parent_hash; return $this; } public function getParentHash() { return $this->parentHash; } public function addChild(DivinerAtom $atom) { $atom->setParentHash($this->getHash()); $this->addChildHash($atom->getHash()); return $this; } public function getURI() { $parts = array(); $parts[] = phutil_escape_uri_path_component($this->getType()); if ($this->getContext()) { $parts[] = phutil_escape_uri_path_component($this->getContext()); } $parts[] = phutil_escape_uri_path_component($this->getName()); $parts[] = null; return implode('/', $parts); } public function toDictionary() { // NOTE: If you change this format, bump the format version in // getAtomSerializationVersion(). return array( 'book' => $this->getBook(), 'type' => $this->getType(), 'name' => $this->getName(), 'file' => $this->getFile(), 'line' => $this->getLine(), 'hash' => $this->getHash(), 'uri' => $this->getURI(), 'length' => $this->getLength(), 'context' => $this->getContext(), 'language' => $this->getLanguage(), 'docblockRaw' => $this->getDocblockRaw(), 'warnings' => $this->getWarnings(), 'parentHash' => $this->getParentHash(), 'childHashes' => $this->getChildHashes(), 'extends' => $this->getExtendsDictionaries(), 'links' => $this->getLinkDictionaries(), 'ref' => $this->getRef()->toDictionary(), 'properties' => $this->getProperties(), ); } public function getRef() { $group = null; $title = null; if ($this->docblockMeta) { $group = $this->getDocblockMetaValue('group'); $title = $this->getDocblockMetaValue('title'); } return id(new DivinerAtomRef()) ->setBook($this->getBook()) ->setContext($this->getContext()) ->setType($this->getType()) ->setName($this->getName()) ->setTitle($title) ->setGroup($group); } public static function newFromDictionary(array $dictionary) { $atom = id(new DivinerAtom()) ->setBook(idx($dictionary, 'book')) ->setType(idx($dictionary, 'type')) ->setName(idx($dictionary, 'name')) ->setFile(idx($dictionary, 'file')) ->setLine(idx($dictionary, 'line')) ->setHash(idx($dictionary, 'hash')) ->setLength(idx($dictionary, 'length')) ->setContext(idx($dictionary, 'context')) ->setLanguage(idx($dictionary, 'language')) ->setParentHash(idx($dictionary, 'parentHash')) ->setDocblockRaw(idx($dictionary, 'docblockRaw')) ->setProperties(idx($dictionary, 'properties')); foreach (idx($dictionary, 'warnings', array()) as $warning) { $atom->addWarning($warning); } foreach (idx($dictionary, 'childHashes', array()) as $child) { $atom->addChildHash($child); } + foreach (idx($dictionary, 'extends', array()) as $extends) { + $atom->addExtends(DivinerAtomRef::newFromDictionary($extends)); + } + return $atom; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; } public function getProperties() { return $this->properties; } public function setProperties(array $properties) { $this->properties = $properties; return $this; } public static function getThisAtomIsNotDocumentedString($type) { switch ($type) { case 'function': return pht('This function is not documented.'); case 'class': return pht('This class is not documented.'); case 'article': return pht('This article is not documented.'); case 'method': return pht('This method is not documented.'); default: phlog("Need translation for '{$type}'."); return pht('This %s is not documented.', $type); } } } diff --git a/src/applications/diviner/atomizer/DivinerPHPAtomizer.php b/src/applications/diviner/atomizer/DivinerPHPAtomizer.php index e9161ac3f2..30928fe319 100644 --- a/src/applications/diviner/atomizer/DivinerPHPAtomizer.php +++ b/src/applications/diviner/atomizer/DivinerPHPAtomizer.php @@ -1,229 +1,313 @@ resolve()); $atoms = array(); $root = $tree->getRootNode(); $func_decl = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($func_decl as $func) { $name = $func->getChildByIndex(2); $atom = id(new DivinerAtom()) ->setType('function') ->setName($name->getConcreteString()) ->setLine($func->getLineNumber()) ->setFile($file_name); $this->findAtomDocblock($atom, $func); $this->parseParams($atom, $func); $this->parseReturnType($atom, $func); $atoms[] = $atom; } + $class_types = array( + 'class' => 'n_CLASS_DECLARATION', + 'interface' => 'n_INTERFACE_DECLARATION', + ); + foreach ($class_types as $atom_type => $node_type) { + $class_decls = $root->selectDescendantsOfType($node_type); + foreach ($class_decls as $class) { + $name = $class->getChildByIndex(1, 'n_CLASS_NAME'); + + $atom = id(new DivinerAtom()) + ->setType($atom_type) + ->setName($name->getConcreteString()) + ->setFile($file_name) + ->setLine($class->getLineNumber()); + + // If this exists, it is n_EXTENDS_LIST. + $extends = $class->getChildByIndex(2); + $extends_class = $extends->selectDescendantsOfType('n_CLASS_NAME'); + foreach ($extends_class as $parent_class) { + $atom->addExtends( + DivinerAtomRef::newFromDictionary( + array( + 'type' => 'class', + 'name' => $parent_class->getConcreteString(), + ))); + } + + // If this exists, it is n_IMPLEMENTS_LIST. + $implements = $class->getChildByIndex(3); + $iface_names = $implements->selectDescendantsOfType('n_CLASS_NAME'); + foreach ($iface_names as $iface_name) { + $atom->addExtends( + DivinerAtomRef::newFromDictionary( + array( + 'type' => 'interface', + 'name' => $iface_name->getConcreteString(), + ))); + } + + $this->findAtomDocblock($atom, $class); + + $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); + foreach ($methods as $method) { + $matom = id(new DivinerAtom()) + ->setType('method'); + + $this->findAtomDocblock($matom, $method); + + $attribute_list = $method->getChildByIndex(0); + $attributes = $attribute_list->selectDescendantsOfType('n_STRING'); + if ($attributes) { + foreach ($attributes as $attribute) { + $attr = strtolower($attribute->getConcreteString()); + switch ($attr) { + case 'static': + $matom->setProperty($attr, true); + break; + case 'public': + case 'protected': + case 'private': + $matom->setProperty('access', $attr); + break; + } + } + } else { + $matom->setProperty('access', 'public'); + } + + $this->parseParams($matom, $method); + + $matom->setName($method->getChildByIndex(2)->getConcreteString()); + $matom->setLine($method->getLineNumber()); + $matom->setFile($file_name); + + $this->parseReturnType($matom, $method); + $atom->addChild($matom); + + $atoms[] = $matom; + } + + $atoms[] = $atom; + } + } + return $atoms; } private function parseParams(DivinerAtom $atom, AASTNode $func) { $params = $func ->getChildByIndex(3, 'n_DECLARATAION_PARAMETER_LIST') ->selectDescendantsOfType('n_DECLARATION_PARAMETER'); $param_spec = array(); if ($atom->getDocblockRaw()) { $metadata = $atom->getDocblockMeta(); } else { $metadata = array(); } $docs = idx($metadata, 'param'); if ($docs) { $docs = explode("\n", $docs); $docs = array_filter($docs); } else { $docs = array(); } if (count($docs)) { if (count($docs) < count($params)) { $atom->addWarning( pht( 'This call takes %d parameters, but only %d are documented.', count($params), count($docs))); } } foreach ($params as $param) { $name = $param->getChildByIndex(1)->getConcreteString(); $dict = array( 'type' => $param->getChildByIndex(0)->getConcreteString(), 'default' => $param->getChildByIndex(2)->getConcreteString(), ); if ($docs) { $doc = array_shift($docs); if ($doc) { $dict += $this->parseParamDoc($atom, $doc, $name); } } $param_spec[] = array( 'name' => $name, ) + $dict; } if ($docs) { foreach ($docs as $doc) { if ($doc) { $param_spec[] = $this->parseParamDoc($atom, $doc, null); } } } // TODO: Find `assert_instances_of()` calls in the function body and // add their type information here. See T1089. $atom->setProperty('parameters', $param_spec); } private function findAtomDocblock(DivinerAtom $atom, XHPASTNode $node) { $token = $node->getDocblockToken(); if ($token) { $atom->setDocblockRaw($token->getValue()); return true; } else { $tokens = $node->getTokens(); if ($tokens) { $prev = head($tokens); while ($prev = $prev->getPrevToken()) { if ($prev->isAnyWhitespace()) { continue; } break; } if ($prev && $prev->isComment()) { $value = $prev->getValue(); $matches = null; if (preg_match('/@(return|param|task|author)/', $value, $matches)) { $atom->addWarning( pht( 'Atom "%s" is preceded by a comment containing "@%s", but the '. 'comment is not a documentation comment. Documentation '. 'comments must begin with "/**", followed by a newline. Did '. 'you mean to use a documentation comment? (As the comment is '. 'not a documentation comment, it will be ignored.)', $atom->getName(), $matches[1])); } } } $atom->setDocblockRaw(''); return false; } } protected function parseParamDoc(DivinerAtom $atom, $doc, $name) { $dict = array(); $split = preg_split('/\s+/', trim($doc), $limit = 2); if (!empty($split[0])) { $dict['doctype'] = $split[0]; } if (!empty($split[1])) { $docs = $split[1]; // If the parameter is documented like "@param int $num Blah blah ..", // get rid of the `$num` part (which Diviner considers optional). If it // is present and different from the declared name, raise a warning. $matches = null; if (preg_match('/^(\\$\S+)\s+/', $docs, $matches)) { if ($name !== null) { if ($matches[1] !== $name) { $atom->addWarning( pht( 'Parameter "%s" is named "%s" in the documentation. The '. 'documentation may be out of date.', $name, $matches[1])); } } $docs = substr($docs, strlen($matches[0])); } $dict['docs'] = $docs; } return $dict; } private function parseReturnType(DivinerAtom $atom, XHPASTNode $decl) { $return_spec = array(); $metadata = $atom->getDocblockMeta(); $return = idx($metadata, 'return'); if (!$return) { $return = idx($metadata, 'returns'); if ($return) { $atom->addWarning( pht('Documentation uses `@returns`, but should use `@return`.')); } } if ($atom->getName() == '__construct' && $atom->getType() == 'method') { $return_spec = array( 'doctype' => 'this', 'docs' => '//Implicit.//', ); if ($return) { $atom->addWarning( 'Method __construct() has explicitly documented @return. The '. '__construct() method always returns $this. Diviner documents '. 'this implicitly.'); } } else if ($return) { $split = preg_split('/\s+/', trim($return), $limit = 2); if (!empty($split[0])) { $type = $split[0]; } if ($decl->getChildByIndex(1)->getTypeName() == 'n_REFERENCE') { $type = $type.' &'; } $docs = null; if (!empty($split[1])) { $docs = $split[1]; } $return_spec = array( 'doctype' => $type, 'docs' => $docs, ); } else { $return_spec = array( 'type' => 'wild', ); } $atom->setProperty('return', $return_spec); } } diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index d5be8213c2..028800f28d 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -1,191 +1,284 @@ 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(); $book = id(new DivinerBookQuery()) ->setViewer($viewer) ->withNames(array($this->bookName)) ->executeOne(); if (!$book) { return new Aphront404Response(); } // TODO: This query won't load ghosts, because they'll fail `needAtoms()`. // Instead, we might want to load ghosts and render a message like // "this thing existed in an older version, but no longer does", especially // if we add content like comments. $symbol = id(new DivinerAtomQuery()) ->setViewer($viewer) ->withBookPHIDs(array($book->getPHID())) ->withTypes(array($this->atomType)) ->withNames(array($this->atomName)) ->withContexts(array($this->atomContext)) ->withIndexes(array($this->atomIndex)) ->needAtoms(true) + ->needExtends(true) ->executeOne(); if (!$symbol) { return new Aphront404Response(); } $atom = $symbol->getAtom(); + $extends = $atom->getExtends(); + + $child_hashes = $atom->getChildHashes(); + if ($child_hashes) { + $children = id(new DivinerAtomQuery()) + ->setViewer($viewer) + ->withIncludeUndocumentable(true) + ->withNodeHashes($child_hashes) + ->execute(); + } else { + $children = array(); + } + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($book->getShortTitle()) ->setHref('/book/'.$book->getName().'/')); $atom_short_title = $atom->getDocblockMetaValue( 'short', $symbol->getTitle()); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($atom_short_title)); $header = id(new PhabricatorHeaderView()) ->setHeader($symbol->getTitle()) ->addTag( id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_STATE) ->setBackgroundColor(PhabricatorTagView::COLOR_BLUE) ->setName($this->renderAtomTypeName($atom->getType()))); $properties = id(new PhabricatorPropertyListView()); $group = $atom->getDocblockMetaValue('group'); if ($group) { $group_name = $book->getGroupName($group); } else { $group_name = null; } $properties->addProperty( pht('Defined'), $atom->getFile().':'.$atom->getLine()); + + $this->buildExtendsAndImplements($properties, $symbol); + $warnings = $atom->getWarnings(); if ($warnings) { $warnings = id(new AphrontErrorView()) ->setErrors($warnings) ->setTitle(pht('Documentation Warnings')) ->setSeverity(AphrontErrorView::SEVERITY_WARNING); } $field = 'default'; $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->addObject($symbol, $field) ->process(); $content = $engine->getOutput($symbol, $field); if (strlen(trim($symbol->getMarkupText($field)))) { $content = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), array( $content, )); } else { $undoc = DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType()); $content = id(new AphrontErrorView()) ->appendChild($undoc) ->setSeverity(AphrontErrorView::SEVERITY_NODATA); } $toc = $engine->getEngineMetadata( $symbol, $field, PhutilRemarkupEngineRemarkupHeaderBlockRule::KEY_HEADER_TOC, array()); $document = id(new PHUIDocumentView()) ->setBook($book->getTitle(), $group_name) ->setHeader($header) ->appendChild($properties) ->appendChild($warnings) ->appendChild($content); $parameters = $atom->getProperty('parameters'); if ($parameters !== null) { $document->appendChild( id(new PhabricatorHeaderView()) ->setHeader(pht('Parameters'))); $document->appendChild( id(new DivinerParameterTableView()) ->setParameters($parameters)); } $return = $atom->getProperty('return'); if ($return !== null) { $document->appendChild( id(new PhabricatorHeaderView()) ->setHeader(pht('Return'))); $document->appendChild( id(new DivinerReturnTableView()) ->setReturn($return)); } + if ($children) { + $document->appendChild( + id(new PhabricatorHeaderView()) + ->setHeader(pht('Methods'))); + foreach ($children as $child) { + $document->appendChild( + id(new PhabricatorHeaderView()) + ->setHeader($child->getName())); + } + } + if ($toc) { $side = new PHUIListView(); $side->addMenuItem( id(new PHUIListItemView()) ->setName(pht('Contents')) ->setType(PHUIListItemView::TYPE_LABEL)); foreach ($toc as $key => $entry) { $side->addMenuItem( id(new PHUIListItemView()) ->setName($entry[1]) ->setHref('#'.$key)); } $document->setSideNav($side, PHUIDocumentView::NAV_TOP); } return $this->buildApplicationPage( array( $crumbs, $document, ), array( 'title' => $symbol->getTitle(), 'device' => true, )); } private function renderAtomTypeName($name) { return phutil_utf8_ucwords($name); } + private function buildExtendsAndImplements( + PhabricatorPropertyListView $view, + DivinerLiveSymbol $symbol) { + + $lineage = $this->getExtendsLineage($symbol); + if ($lineage) { + $lineage = mpull($lineage, 'getName'); + $lineage = implode(' > ', $lineage); + $view->addProperty(pht('Extends'), $lineage); + } + + $implements = $this->getImplementsLineage($symbol); + if ($implements) { + $items = array(); + foreach ($implements as $spec) { + $via = $spec['via']; + $iface = $spec['interface']; + if ($via == $symbol) { + $items[] = $iface->getName(); + } else { + $items[] = $iface->getName().' (via '.$via->getName().')'; + } + } + + $view->addProperty( + pht('Implements'), + phutil_implode_html(phutil_tag('br'), $items)); + } + + } + + private function getExtendsLineage(DivinerLiveSymbol $symbol) { + foreach ($symbol->getExtends() as $extends) { + if ($extends->getType() == 'class') { + $lineage = $this->getExtendsLineage($extends); + $lineage[] = $extends; + return $lineage; + } + } + return array(); + } + + private function getImplementsLineage(DivinerLiveSymbol $symbol) { + $implements = array(); + + // Do these first so we get interfaces ordered from most to least specific. + foreach ($symbol->getExtends() as $extends) { + if ($extends->getType() == 'interface') { + $implements[$extends->getName()] = array( + 'interface' => $extends, + 'via' => $symbol, + ); + } + } + + // Now do parent interfaces. + foreach ($symbol->getExtends() as $extends) { + if ($extends->getType() == 'class') { + $implements += $this->getImplementsLineage($extends); + } + } + + return $implements; + } + } diff --git a/src/applications/diviner/publisher/DivinerLivePublisher.php b/src/applications/diviner/publisher/DivinerLivePublisher.php index 6e2fc92170..164aa6f548 100644 --- a/src/applications/diviner/publisher/DivinerLivePublisher.php +++ b/src/applications/diviner/publisher/DivinerLivePublisher.php @@ -1,140 +1,144 @@ book) { $book_name = $this->getConfig('name'); $book = id(new DivinerLiveBook())->loadOneWhere( 'name = %s', $book_name); if (!$book) { $book = id(new DivinerLiveBook()) ->setName($book_name) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->save(); } $book->setConfigurationData($this->getConfigurationData())->save(); $this->book = $book; } return $this->book; } private function loadSymbolForAtom(DivinerAtom $atom) { $symbol = id(new DivinerAtomQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBookPHIDs(array($this->loadBook()->getPHID())) ->withTypes(array($atom->getType())) ->withNames(array($atom->getName())) ->withContexts(array($atom->getContext())) ->withIndexes(array($this->getAtomSimilarIndex($atom))) ->withIncludeUndocumentable(true) ->withIncludeGhosts(true) ->executeOne(); if ($symbol) { return $symbol; } return id(new DivinerLiveSymbol()) ->setBookPHID($this->loadBook()->getPHID()) ->setType($atom->getType()) ->setName($atom->getName()) ->setContext($atom->getContext()) ->setAtomIndex($this->getAtomSimilarIndex($atom)); } private function loadAtomStorageForSymbol(DivinerLiveSymbol $symbol) { $storage = id(new DivinerLiveAtom())->loadOneWhere( 'symbolPHID = %s', $symbol->getPHID()); if ($storage) { return $storage; } return id(new DivinerLiveAtom()) ->setSymbolPHID($symbol->getPHID()); } protected function loadAllPublishedHashes() { - $symbols = id(new DivinerLiveSymbol())->loadAllWhere( - 'bookPHID = %s AND graphHash IS NOT NULL', - $this->loadBook()->getPHID()); + $symbols = id(new DivinerAtomQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withBookPHIDs(array($this->loadBook()->getPHID())) + ->withIncludeUndocumentable(true) + ->execute(); return mpull($symbols, 'getGraphHash'); } protected function deleteDocumentsByHash(array $hashes) { $atom_table = new DivinerLiveAtom(); $symbol_table = new DivinerLiveSymbol(); $conn_w = $symbol_table->establishConnection('w'); $strings = array(); foreach ($hashes as $hash) { $strings[] = qsprintf($conn_w, '%s', $hash); } foreach (PhabricatorLiskDAO::chunkSQL($strings, ', ') as $chunk) { queryfx( $conn_w, - 'UPDATE %T SET graphHash = NULL WHERE graphHash IN (%Q)', + 'UPDATE %T SET graphHash = NULL, nodeHash = NULL + WHERE graphHash IN (%Q)', $symbol_table->getTableName(), $chunk); } queryfx( $conn_w, 'DELETE a FROM %T a LEFT JOIN %T s ON a.symbolPHID = s.phid WHERE s.graphHash IS NULL', $atom_table->getTableName(), $symbol_table->getTableName()); } protected function createDocumentsByHash(array $hashes) { foreach ($hashes as $hash) { $atom = $this->getAtomFromGraphHash($hash); $ref = $atom->getRef(); $symbol = $this->loadSymbolForAtom($atom); $is_documentable = $this->shouldGenerateDocumentForAtom($atom); $symbol ->setGraphHash($hash) ->setIsDocumentable((int)$is_documentable) ->setTitle($ref->getTitle()) - ->setGroupName($ref->getGroup()); + ->setGroupName($ref->getGroup()) + ->setNodeHash($atom->getHash()); if ($is_documentable) { $renderer = $this->getRenderer(); $summary = $renderer->renderAtomSummary($atom); $summary = (string)phutil_safe_html($summary); $symbol->setSummary($summary); } $symbol->save(); if ($is_documentable) { $storage = $this->loadAtomStorageForSymbol($symbol) ->setAtomData($atom->toDictionary()) ->setContent(null) ->save(); } } } public function findAtomByRef(DivinerAtomRef $ref) { // TODO: Actually implement this. return null; } } diff --git a/src/applications/diviner/publisher/DivinerPublisher.php b/src/applications/diviner/publisher/DivinerPublisher.php index 150cd42918..33109933a2 100644 --- a/src/applications/diviner/publisher/DivinerPublisher.php +++ b/src/applications/diviner/publisher/DivinerPublisher.php @@ -1,155 +1,156 @@ dropCaches = $drop_caches; return $this; } public function setRenderer(DivinerRenderer $renderer) { $renderer->setPublisher($this); $this->renderer = $renderer; return $this; } public function getRenderer() { return $this->renderer; } public function setConfig(array $config) { $this->config = $config; return $this; } public function getConfig($key, $default = null) { return idx($this->config, $key, $default); } public function getConfigurationData() { return $this->config; } public function setAtomCache(DivinerAtomCache $cache) { $this->atomCache = $cache; $graph_map = $this->atomCache->getGraphMap(); $this->atomGraphHashToNodeHashMap = array_flip($graph_map); } protected function getAtomFromGraphHash($graph_hash) { if (empty($this->atomGraphHashToNodeHashMap[$graph_hash])) { throw new Exception("No such atom '{$graph_hash}'!"); } return $this->getAtomFromNodeHash( $this->atomGraphHashToNodeHashMap[$graph_hash]); } protected function getAtomFromNodeHash($node_hash) { if (empty($this->atomMap[$node_hash])) { $dict = $this->atomCache->getAtom($node_hash); $this->atomMap[$node_hash] = DivinerAtom::newFromDictionary($dict); } return $this->atomMap[$node_hash]; } protected function getSimilarAtoms(DivinerAtom $atom) { if ($this->symbolReverseMap === null) { $rmap = array(); $smap = $this->atomCache->getSymbolMap(); foreach ($smap as $nhash => $shash) { $rmap[$shash][$nhash] = true; } $this->symbolReverseMap = $rmap; } $shash = $atom->getRef()->toHash(); if (empty($this->symbolReverseMap[$shash])) { throw new Exception("Atom has no symbol map entry!"); } $hashes = $this->symbolReverseMap[$shash]; $atoms = array(); foreach ($hashes as $hash => $ignored) { $atoms[] = $this->getAtomFromNodeHash($hash); } $atoms = msort($atoms, 'getSortKey'); return $atoms; } /** * If a book contains multiple definitions of some atom, like some function * "f()", we assign them an arbitrary (but fairly stable) order and publish * them as "function/f/1/", "function/f/2/", etc., or similar. */ protected function getAtomSimilarIndex(DivinerAtom $atom) { $atoms = $this->getSimilarAtoms($atom); if (count($atoms) == 1) { return 0; } $index = 1; foreach ($atoms as $similar_atom) { if ($atom === $similar_atom) { return $index; } $index++; } throw new Exception("Expected to find atom while disambiguating!"); } abstract protected function loadAllPublishedHashes(); abstract protected function deleteDocumentsByHash(array $hashes); abstract protected function createDocumentsByHash(array $hashes); abstract public function findAtomByRef(DivinerAtomRef $ref); final public function publishAtoms(array $hashes) { $existing = $this->loadAllPublishedHashes(); if ($this->dropCaches) { $deleted = $existing; $created = $hashes; } else { $existing_map = array_fill_keys($existing, true); $hashes_map = array_fill_keys($hashes, true); $deleted = array_diff_key($existing_map, $hashes_map); $created = array_diff_key($hashes_map, $existing_map); $deleted = array_keys($deleted); $created = array_keys($created); } echo pht('Deleting %d documents.', count($deleted))."\n"; $this->deleteDocumentsByHash($deleted); echo pht('Creating %d documents.', count($created))."\n"; $this->createDocumentsByHash($created); } protected function shouldGenerateDocumentForAtom(DivinerAtom $atom) { switch ($atom->getType()) { + case DivinerAtom::TYPE_METHOD: case DivinerAtom::TYPE_FILE: return false; case DivinerAtom::TYPE_ARTICLE: default: break; } return true; } } diff --git a/src/applications/diviner/query/DivinerAtomQuery.php b/src/applications/diviner/query/DivinerAtomQuery.php index a461c40c51..06e6fda1e3 100644 --- a/src/applications/diviner/query/DivinerAtomQuery.php +++ b/src/applications/diviner/query/DivinerAtomQuery.php @@ -1,228 +1,325 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withBookPHIDs(array $phids) { $this->bookPHIDs = $phids; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withContexts(array $contexts) { $this->contexts = $contexts; return $this; } public function withIndexes(array $indexes) { $this->indexes = $indexes; return $this; } + public function withNodeHashes(array $hashes) { + $this->nodeHashes = $hashes; + return $this; + } + public function needAtoms($need) { $this->needAtoms = $need; return $this; } + /** * Include "ghosts", which are symbols which used to exist but do not exist * currently (for example, a function which existed in an older version of * the codebase but was deleted). * * These symbols had PHIDs assigned to them, and may have other sorts of * metadata that we don't want to lose (like comments or flags), so we don't * delete them outright. They might also come back in the future: the change * which deleted the symbol might be reverted, or the documentation might * have been generated incorrectly by accident. In these cases, we can * restore the original data. * * However, most callers are not interested in these symbols, so they are * excluded by default. You can use this method to include them in results. * * @param bool True to include ghosts. * @return this */ public function withIncludeGhosts($include) { $this->includeGhosts = $include; return $this; } + + public function needExtends($need) { + $this->needExtends = $need; + return $this; + } + public function withIncludeUndocumentable($include) { $this->includeUndocumentable = $include; return $this; } protected function loadPage() { $table = new DivinerLiveSymbol(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $atoms) { $books = array_unique(mpull($atoms, 'getBookPHID')); $books = id(new DivinerBookQuery()) ->setViewer($this->getViewer()) ->withPHIDs($books) ->execute(); $books = mpull($books, null, 'getPHID'); foreach ($atoms as $key => $atom) { $book = idx($books, $atom->getBookPHID()); if (!$book) { unset($atoms[$key]); continue; } $atom->attachBook($book); } + $need_atoms = $this->needAtoms; + if ($this->needAtoms) { $atom_data = id(new DivinerLiveAtom())->loadAllWhere( 'symbolPHID IN (%Ls)', mpull($atoms, 'getPHID')); $atom_data = mpull($atom_data, null, 'getSymbolPHID'); foreach ($atoms as $key => $atom) { $data = idx($atom_data, $atom->getPHID()); if (!$data) { unset($atoms[$key]); continue; } $atom->attachAtom($data); } } + // Load all of the symbols this symbol extends, recursively. Commonly, + // this means all the ancestor classes and interfaces it extends and + // implements. + + if ($this->needExtends) { + + // First, load all the matching symbols by name. This does 99% of the + // work in most cases, assuming things are named at all reasonably. + + $names = array(); + foreach ($atoms as $atom) { + foreach ($atom->getAtom()->getExtends() as $xref) { + $names[] = $xref->getName(); + } + } + + if ($names) { + $xatoms = id(new DivinerAtomQuery()) + ->setViewer($this->getViewer()) + ->withNames($names) + ->needExtends(true) + ->needAtoms(true) + ->execute(); + $xatoms = mgroup($xatoms, 'getName', 'getType', 'getBookPHID'); + } else { + $xatoms = array(); + } + + foreach ($atoms as $atom) { + $alang = $atom->getAtom()->getLanguage(); + $extends = array(); + foreach ($atom->getAtom()->getExtends() as $xref) { + + // If there are no symbols of the matching name and type, we can't + // resolve this. + if (empty($xatoms[$xref->getName()][$xref->getType()])) { + continue; + } + + // If we found matches in the same documentation book, prefer them + // over other matches. Otherwise, look at all the the matches. + $matches = $xatoms[$xref->getName()][$xref->getType()]; + if (isset($matches[$atom->getBookPHID()])) { + $maybe = $matches[$atom->getBookPHID()]; + } else { + $maybe = array_mergev($matches); + } + + if (!$maybe) { + continue; + } + + // Filter out matches in a different language, since, e.g., PHP + // classes can not implement JS classes. + $same_lang = array(); + foreach ($maybe as $xatom) { + if ($xatom->getAtom()->getLanguage() == $alang) { + $same_lang[] = $xatom; + } + } + + if (!$same_lang) { + continue; + } + + // If we have duplicates remaining, just pick the first one. There's + // nothing more we can do to figure out which is the real one. + $extends[] = head($same_lang); + } + + $atom->attachExtends($extends); + } + } + return $atoms; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->bookPHIDs) { $where[] = qsprintf( $conn_r, 'bookPHID IN (%Ls)', $this->bookPHIDs); } if ($this->types) { $where[] = qsprintf( $conn_r, 'type IN (%Ls)', $this->types); } if ($this->names) { $where[] = qsprintf( $conn_r, 'name IN (%Ls)', $this->names); } if ($this->contexts) { $with_null = false; $contexts = $this->contexts; foreach ($contexts as $key => $value) { if ($value === null) { unset($contexts[$key]); $with_null = true; continue; } } if ($contexts && $with_null) { $where[] = qsprintf( $conn_r, 'context IN (%Ls) OR context IS NULL', $contexts); } else if ($contexts) { $where[] = qsprintf( $conn_r, 'context IN (%Ls)', $contexts); } else if ($with_null) { $where[] = qsprintf( $conn_r, 'context IS NULL'); } } if ($this->indexes) { $where[] = qsprintf( $conn_r, 'atomIndex IN (%Ld)', $this->indexes); } if (!$this->includeUndocumentable) { $where[] = qsprintf( $conn_r, 'isDocumentable = 1'); } if (!$this->includeGhosts) { $where[] = qsprintf( $conn_r, 'graphHash IS NOT NULL'); } + if ($this->nodeHashes) { + $where[] = qsprintf( + $conn_r, + 'nodeHash IN (%Ls)', + $this->nodeHashes); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } } diff --git a/src/applications/diviner/storage/DivinerLiveSymbol.php b/src/applications/diviner/storage/DivinerLiveSymbol.php index a4eef16282..8a8a4bb79f 100644 --- a/src/applications/diviner/storage/DivinerLiveSymbol.php +++ b/src/applications/diviner/storage/DivinerLiveSymbol.php @@ -1,166 +1,172 @@ true, self::CONFIG_TIMESTAMPS => false, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DivinerPHIDTypeAtom::TYPECONST); } public function getBook() { - if ($this->book === null) { - throw new Exception("Call attachBook() before getBook()!"); - } - return $this->book; + return $this->assertAttached($this->book); } public function attachBook(DivinerLiveBook $book) { $this->book = $book; return $this; } public function getAtom() { - if ($this->atom === null) { - throw new Exception("Call attachAtom() before getAtom()!"); - } - return $this->atom; + return $this->assertAttached($this->atom); } public function attachAtom(DivinerLiveAtom $atom) { $this->atom = DivinerAtom::newFromDictionary($atom->getAtomData()); return $this; } public function getURI() { $parts = array( 'book', $this->getBook()->getName(), $this->getType(), ); if ($this->getContext()) { $parts[] = $this->getContext(); } $parts[] = $this->getName(); if ($this->getAtomIndex()) { $parts[] = $this->getAtomIndex(); } return '/'.implode('/', $parts).'/'; } public function getSortKey() { return $this->getTitle(); } public function save() { // 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 // don't use it directly, but its existence prevents duplicate records. if (!$this->identityHash) { $this->identityHash = PhabricatorHash::digestForIndex( serialize( array( 'bookPHID' => $this->getBookPHID(), 'context' => $this->getContext(), 'type' => $this->getType(), 'name' => $this->getName(), 'index' => $this->getAtomIndex(), ))); } return parent::save(); } public function getTitle() { $title = parent::getTitle(); if (!strlen($title)) { $title = $this->getName(); } return $title; } + public function attachExtends(array $extends) { + assert_instances_of($extends, 'DivinerLiveSymbol'); + $this->extends = $extends; + return $this; + } + + public function getExtends() { + return $this->assertAttached($this->extends); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return $this->getBook()->getCapabilities(); } public function getPolicy($capability) { return $this->getBook()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBook()->hasAutomaticCapability($capability, $viewer); } /* -( Markup Interface )--------------------------------------------------- */ public function getMarkupFieldKey($field) { return $this->getPHID().':'.$field.':'.$this->getGraphHash(); } public function newMarkupEngine($field) { $engine = PhabricatorMarkupEngine::newMarkupEngine(array()); $engine->setConfig('preserve-linebreaks', false); // $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer()); $engine->setConfig('header.generate-toc', true); return $engine; } public function getMarkupText($field) { return $this->getAtom()->getDocblockText(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return false; } } diff --git a/src/applications/diviner/workflow/DivinerAtomizeWorkflow.php b/src/applications/diviner/workflow/DivinerAtomizeWorkflow.php index ace4b1dd15..1b1c272944 100644 --- a/src/applications/diviner/workflow/DivinerAtomizeWorkflow.php +++ b/src/applications/diviner/workflow/DivinerAtomizeWorkflow.php @@ -1,116 +1,118 @@ setName('atomize') ->setSynopsis(pht('Build atoms from source.')) ->setArguments( array( array( 'name' => 'atomizer', 'param' => 'class', 'help' => pht('Specify a subclass of DivinerAtomizer.'), ), array( 'name' => 'book', 'param' => 'path', 'help' => pht('Path to a Diviner book configuration.'), ), array( 'name' => 'files', 'wildcard' => true, ), array( 'name' => 'ugly', 'help' => pht('Produce ugly (but faster) output.'), ), )); } public function execute(PhutilArgumentParser $args) { $this->readBookConfiguration($args); $console = PhutilConsole::getConsole(); $atomizer_class = $args->getArg('atomizer'); if (!$atomizer_class) { throw new Exception("Specify an atomizer class with --atomizer."); } $symbols = id(new PhutilSymbolLoader()) ->setName($atomizer_class) ->setConcreteOnly(true) ->setAncestorClass('DivinerAtomizer') ->selectAndLoadSymbols(); if (!$symbols) { throw new Exception( "Atomizer class '{$atomizer_class}' must be a concrete subclass of ". "DivinerAtomizer."); } $atomizer = newv($atomizer_class, array()); $files = $args->getArg('files'); if (!$files) { throw new Exception("Specify one or more files to atomize."); } $file_atomizer = new DivinerFileAtomizer(); foreach (array($atomizer, $file_atomizer) as $configure) { $configure->setBook($this->getConfig('name')); } $all_atoms = array(); foreach ($files as $file) { $abs_path = Filesystem::resolvePath($file, $this->getConfig('root')); $data = Filesystem::readFile($abs_path); if (!$this->shouldAtomizeFile($file, $data)) { $console->writeLog("Skipping %s...\n", $file); continue; } else { $console->writeLog("Atomizing %s...\n", $file); } $file_atoms = $file_atomizer->atomize($file, $data); $all_atoms[] = $file_atoms; if (count($file_atoms) !== 1) { throw new Exception("Expected exactly one atom from file atomizer."); } $file_atom = head($file_atoms); $atoms = $atomizer->atomize($file, $data); foreach ($atoms as $atom) { - $file_atom->addChild($atom); + if (!$atom->getParentHash()) { + $file_atom->addChild($atom); + } } $all_atoms[] = $atoms; } $all_atoms = array_mergev($all_atoms); $all_atoms = mpull($all_atoms, 'toDictionary'); $all_atoms = ipull($all_atoms, null, 'hash'); if ($args->getArg('ugly')) { $json = json_encode($all_atoms); } else { $json_encoder = new PhutilJSON(); $json = $json_encoder->encodeFormatted($all_atoms); } $console->writeOut('%s', $json); return 0; } private function shouldAtomizeFile($file_name, $file_data) { return (strpos($file_data, '@'.'undivinable') === false); } } diff --git a/src/applications/diviner/workflow/DivinerGenerateWorkflow.php b/src/applications/diviner/workflow/DivinerGenerateWorkflow.php index afaaab8615..8f23257069 100644 --- a/src/applications/diviner/workflow/DivinerGenerateWorkflow.php +++ b/src/applications/diviner/workflow/DivinerGenerateWorkflow.php @@ -1,489 +1,506 @@ setName('generate') ->setSynopsis(pht('Generate documentation.')) ->setArguments( array( array( 'name' => 'clean', 'help' => 'Clear the caches before generating documentation.', ), array( 'name' => 'book', 'param' => 'path', 'help' => 'Path to a Diviner book configuration.', ), )); } protected function getAtomCache() { if (!$this->atomCache) { $book_root = $this->getConfig('root'); $book_name = $this->getConfig('name'); $cache_directory = $book_root.'/.divinercache/'.$book_name; $this->atomCache = new DivinerAtomCache($cache_directory); } return $this->atomCache; } protected function log($message) { $console = PhutilConsole::getConsole(); - $console->getServer()->setEnableLog(true); - $console->writeLog($message."\n"); + $console->writeErr($message."\n"); } public function execute(PhutilArgumentParser $args) { $this->readBookConfiguration($args); if ($args->getArg('clean')) { $this->log(pht('CLEARING CACHES')); $this->getAtomCache()->delete(); $this->log(pht('Done.')."\n"); } // The major challenge of documentation generation is one of dependency // management. When regenerating documentation, we want to do the smallest // amount of work we can, so that regenerating documentation after minor // changes is quick. // // ATOM CACHE // // In the first stage, we find all the direct changes to source code since // the last run. This stage relies on two data structures: // // - File Hash Map: map // - Atom Map: map // // First, we hash all the source files in the project to detect any which // have changed since the previous run (i.e., their hash is not present in // the File Hash Map). If a file's content hash appears in the map, it has // not changed, so we don't need to reparse it. // // We break the contents of each file into "atoms", which represent a unit // of source code (like a function, method, class or file). Each atom has a // "node hash" based on the content of the atom: if a function definition // changes, the node hash of the atom changes too. The primary output of // the atom cache is a list of node hashes which exist in the project. This // is the Atom Map. The node hash depends only on the definition of the atom // and the atomizer implementation. It ends with an "N", for "node". // // (We need the Atom Map in addition to the File Hash Map because each file // may have several atoms in it (e.g., multiple functions, or a class and // 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.) // // GRAPH CACHE // // 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 // sufficient to figure out which documentation actually needs to be // regnerated, because atoms depend on other atoms. For example, if "B // extends A" and the definition for A changes, we need to regenerate the // documentation in B. Similarly, if X links to Y and Y changes, we should // regenerate X. (In both these cases, the documentation for the connected // atom may not acutally change, but in some cases it will, and the extra // work we need to do is generally very small compared to the size of the // project.) // // To figure out which other nodes have changed, we compute a "graph hash" // for each node. This hash combines the "node hash" with the node hashes // of connected nodes. Our primary output is a list of graph hashes, which // a documentation generator can use to easily determine what work needs // to be done by comparing the list with a list of cached graph hashes, // then generating documentation for new hashes and deleting documentation // for missing hashes. The graph hash ends with a "G", for "graph". // // In this stage, we rely on three data structures: // // - Symbol Map: map // - Edge Map: map> // - Graph Map: map // // Calculating the graph hash requires several steps, because we need to // figure out which nodes an atom is attached to. The atom contains symbolic // references to other nodes by name (e.g., "extends SomeClass") in the form // of DivinerAtomRefs. We can also build a symbolic reference for any atom // from the atom itself. Each DivinerAtomRef generates a symbol hash, // which ends with an "S", for "symbol". // // First, we update the symbol map. We remove (and mark dirty) any symbols // associated with node hashes which no longer exist (e.g., old/dead nodes). // Second, we add (and mark dirty) any symbols associated with new nodes. // We also add edges defined by new nodes to the graph. // // We initialize a list of dirty nodes to the list of new nodes, then // find all nodes connected to dirty symbols and add them to the dirty // node list. This list now contains every node with a new or changed // graph hash. // // We walk the dirty list and compute the new graph hashes, adding them // to the graph hash map. This Graph Map can then be passed to an actual // documentation generator, which can compare the graph hashes to a list // of already-generated graph hashes and easily assess which documents need // to be regenerated and which can be deleted. $this->buildAtomCache(); $this->buildGraphCache(); $this->publishDocumentation($args->getArg('clean')); } /* -( Atom Cache )--------------------------------------------------------- */ private function buildAtomCache() { $this->log(pht('BUILDING ATOM CACHE')); $file_hashes = $this->findFilesInProject(); $this->log(pht('Found %d file(s) in project.', count($file_hashes))); $this->deleteDeadAtoms($file_hashes); $atomize = $this->getFilesToAtomize($file_hashes); $this->log(pht('Found %d unatomized, uncached file(s).', count($atomize))); $file_atomizers = $this->getAtomizersForFiles($atomize); $this->log(pht('Found %d file(s) to atomize.', count($file_atomizers))); $futures = $this->buildAtomizerFutures($file_atomizers); + + $this->log(pht('Atomizing %d file(s).', count($file_atomizers))); + if ($futures) { $this->resolveAtomizerFutures($futures, $file_hashes); $this->log(pht("Atomization complete.")); } else { $this->log(pht("Atom cache is up to date, no files to atomize.")); } $this->log(pht("Writing atom cache.")); $this->getAtomCache()->saveAtoms(); $this->log(pht('Done.')."\n"); } private function getAtomizersForFiles(array $files) { $rules = $this->getRules(); $exclude = $this->getExclude(); $atomizers = array(); foreach ($files as $file) { foreach ($exclude as $pattern) { if (preg_match($pattern, $file)) { continue 2; } } foreach ($rules as $rule => $atomizer) { $ok = preg_match($rule, $file); if ($ok === false) { throw new Exception( "Rule '{$rule}' is not a valid regular expression."); } if ($ok) { $atomizers[$file] = $atomizer; continue; } } } return $atomizers; } private function getRules() { $rules = $this->getConfig('rules', array( '/\\.diviner$/' => 'DivinerArticleAtomizer', '/\\.php$/' => 'DivinerPHPAtomizer', )); foreach ($rules as $rule => $atomizer) { if (@preg_match($rule, '') === false) { throw new Exception( "Rule '{$rule}' is not a valid regular expression!"); } } return $rules; } private function getExclude() { $exclude = $this->getConfig('exclude', array()); foreach ($exclude as $rule) { if (@preg_match($rule, '') === false) { throw new Exception( "Exclude rule '{$rule}' is not a valid regular expression!"); } } return $exclude; } private function findFilesInProject() { $raw_hashes = id(new FileFinder($this->getConfig('root'))) ->excludePath('*/.*') ->withType('f') ->setGenerateChecksums(true) ->find(); $version = $this->getDivinerAtomWorldVersion(); $file_hashes = array(); foreach ($raw_hashes as $file => $md5_hash) { $rel_file = Filesystem::readablePath($file, $this->getConfig('root')); // We want the hash to change if the file moves or Diviner gets updated, // not just if the file content changes. Derive a hash from everything // we care about. $file_hashes[$rel_file] = md5("{$rel_file}\0{$md5_hash}\0{$version}").'F'; } return $file_hashes; } private function deleteDeadAtoms(array $file_hashes) { $atom_cache = $this->getAtomCache(); $hash_to_file = array_flip($file_hashes); foreach ($atom_cache->getFileHashMap() as $hash => $atom) { if (empty($hash_to_file[$hash])) { $atom_cache->deleteFileHash($hash); } } } private function getFilesToAtomize(array $file_hashes) { $atom_cache = $this->getAtomCache(); $atomize = array(); foreach ($file_hashes as $file => $hash) { if (!$atom_cache->fileHashExists($hash)) { $atomize[] = $file; } } return $atomize; } private function buildAtomizerFutures(array $file_atomizers) { $atomizers = array(); foreach ($file_atomizers as $file => $atomizer) { $atomizers[$atomizer][] = $file; } + $root = dirname(phutil_get_library_root('phabricator')); + $config_root = $this->getConfig('root'); + + $bar = id(new PhutilConsoleProgressBar()) + ->setTotal(count($file_atomizers)); + $futures = array(); foreach ($atomizers as $class => $files) { foreach (array_chunk($files, 32) as $chunk) { $future = new ExecFuture( '%s atomize --ugly --book %s --atomizer %s -- %Ls', - dirname(phutil_get_library_root('phabricator')).'/bin/diviner', + $root.'/bin/diviner', $this->getBookConfigPath(), $class, $chunk); - $future->setCWD($this->getConfig('root')); + $future->setCWD($config_root); $futures[] = $future; + + $bar->update(count($chunk)); } } + $bar->done(); + return $futures; } private function resolveAtomizerFutures(array $futures, array $file_hashes) { assert_instances_of($futures, 'Future'); $atom_cache = $this->getAtomCache(); + $bar = id(new PhutilConsoleProgressBar()) + ->setTotal(count($futures)); foreach (Futures($futures)->limit(4) as $key => $future) { $atoms = $future->resolveJSON(); foreach ($atoms as $atom) { if ($atom['type'] == DivinerAtom::TYPE_FILE) { $file_hash = $file_hashes[$atom['file']]; $atom_cache->addFileHash($file_hash, $atom['hash']); } $atom_cache->addAtom($atom); } + + $bar->update(1); } + $bar->done(); } /** * Get a global version number, which changes whenever any atom or atomizer * implementation changes in a way which is not backward-compatible. */ private function getDivinerAtomWorldVersion() { $version = array(); $version['atom'] = DivinerAtom::getAtomSerializationVersion(); $version['rules'] = $this->getRules(); $atomizers = id(new PhutilSymbolLoader()) ->setAncestorClass('DivinerAtomizer') ->setConcreteOnly(true) ->selectAndLoadSymbols(); $atomizer_versions = array(); foreach ($atomizers as $atomizer) { $atomizer_versions[$atomizer['name']] = call_user_func( array( $atomizer['name'], 'getAtomizerVersion', )); } ksort($atomizer_versions); $version['atomizers'] = $atomizer_versions; return md5(serialize($version)); } /* -( Graph Cache )-------------------------------------------------------- */ private function buildGraphCache() { $this->log(pht('BUILDING GRAPH CACHE')); $atom_cache = $this->getAtomCache(); $symbol_map = $atom_cache->getSymbolMap(); $atoms = $atom_cache->getAtomMap(); $dirty_symbols = array(); $dirty_nhashes = array(); $del_atoms = array_diff_key($symbol_map, $atoms); $this->log(pht('Found %d obsolete atom(s) in graph.', count($del_atoms))); foreach ($del_atoms as $nhash => $shash) { $atom_cache->deleteSymbol($nhash); $dirty_symbols[$shash] = true; $atom_cache->deleteEdges($nhash); $atom_cache->deleteGraph($nhash); } $new_atoms = array_diff_key($atoms, $symbol_map); $this->log(pht('Found %d new atom(s) in graph.', count($new_atoms))); foreach ($new_atoms as $nhash => $ignored) { $shash = $this->computeSymbolHash($nhash); $atom_cache->addSymbol($nhash, $shash); $dirty_symbols[$shash] = true; $atom_cache->addEdges( $nhash, $this->getEdges($nhash)); $dirty_nhashes[$nhash] = true; } $this->log(pht('Propagating changes through the graph.')); // Find all the nodes which point at a dirty node, and dirty them. Then // find all the nodes which point at those nodes and dirty them, and so // on. (This is slightly overkill since we probably don't need to propagate // dirtiness across documentation "links" between symbols, but we do want // to propagate it across "extends", and we suffer only a little bit of // collateral damage by over-dirtying as long as the documentation isn't // too well-connected.) $symbol_stack = array_keys($dirty_symbols); while ($symbol_stack) { $symbol_hash = array_pop($symbol_stack); foreach ($atom_cache->getEdgesWithDestination($symbol_hash) as $edge) { $dirty_nhashes[$edge] = true; $src_hash = $this->computeSymbolHash($edge); if (empty($dirty_symbols[$src_hash])) { $dirty_symbols[$src_hash] = true; $symbol_stack[] = $src_hash; } } } $this->log(pht('Found %d affected atoms.', count($dirty_nhashes))); foreach ($dirty_nhashes as $nhash => $ignored) { $atom_cache->addGraph($nhash, $this->computeGraphHash($nhash)); } $this->log(pht('Writing graph cache.')); $atom_cache->saveGraph(); $atom_cache->saveEdges(); $atom_cache->saveSymbols(); $this->log(pht('Done.')."\n"); } private function computeSymbolHash($node_hash) { $atom_cache = $this->getAtomCache(); $atom = $atom_cache->getAtom($node_hash); if (!$atom) { throw new Exception("No such atom with node hash '{$node_hash}'!"); } $ref = DivinerAtomRef::newFromDictionary($atom['ref']); return $ref->toHash(); } private function getEdges($node_hash) { $atom_cache = $this->getAtomCache(); $atom = $atom_cache->getAtom($node_hash); $refs = array(); // Make the atom depend on its own symbol, so that all atoms with the same // symbol are dirtied (e.g., if a codebase defines the function "f()" // several times, all of them should be dirtied when one is dirtied). $refs[DivinerAtomRef::newFromDictionary($atom)->toHash()] = true; foreach (array_merge($atom['extends'], $atom['links']) as $ref_dict) { $ref = DivinerAtomRef::newFromDictionary($ref_dict); if ($ref->getBook() == $atom['book']) { $refs[$ref->toHash()] = true; } } return array_keys($refs); } private function computeGraphHash($node_hash) { $atom_cache = $this->getAtomCache(); $atom = $atom_cache->getAtom($node_hash); $edges = $this->getEdges($node_hash); sort($edges); $inputs = array( 'atomHash' => $atom['hash'], 'edges' => $edges, ); return md5(serialize($inputs)).'G'; } private function publishDocumentation($clean) { $atom_cache = $this->getAtomCache(); $graph_map = $atom_cache->getGraphMap(); $this->log(pht('PUBLISHING DOCUMENTATION')); $publisher = new DivinerLivePublisher(); $publisher->setDropCaches($clean); $publisher->setConfig($this->getAllConfig()); $publisher->setAtomCache($atom_cache); $publisher->setRenderer(new DivinerDefaultRenderer()); $publisher->publishAtoms(array_values($graph_map)); $this->log(pht('Done.')); } } diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index b1d5e98370..7fcd71c1cf 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1,1560 +1,1564 @@ array( 'type' => 'db', 'name' => 'audit', 'after' => array( /* First Patch */ ), ), 'db.calendar' => array( 'type' => 'db', 'name' => 'calendar', ), 'db.chatlog' => array( 'type' => 'db', 'name' => 'chatlog', ), 'db.conduit' => array( 'type' => 'db', 'name' => 'conduit', ), 'db.countdown' => array( 'type' => 'db', 'name' => 'countdown', ), 'db.daemon' => array( 'type' => 'db', 'name' => 'daemon', ), 'db.differential' => array( 'type' => 'db', 'name' => 'differential', ), 'db.draft' => array( 'type' => 'db', 'name' => 'draft', ), 'db.drydock' => array( 'type' => 'db', 'name' => 'drydock', ), 'db.feed' => array( 'type' => 'db', 'name' => 'feed', ), 'db.file' => array( 'type' => 'db', 'name' => 'file', ), 'db.flag' => array( 'type' => 'db', 'name' => 'flag', ), 'db.harbormaster' => array( 'type' => 'db', 'name' => 'harbormaster', ), 'db.herald' => array( 'type' => 'db', 'name' => 'herald', ), 'db.maniphest' => array( 'type' => 'db', 'name' => 'maniphest', ), 'db.meta_data' => array( 'type' => 'db', 'name' => 'meta_data', ), 'db.metamta' => array( 'type' => 'db', 'name' => 'metamta', ), 'db.oauth_server' => array( 'type' => 'db', 'name' => 'oauth_server', ), 'db.owners' => array( 'type' => 'db', 'name' => 'owners', ), 'db.pastebin' => array( 'type' => 'db', 'name' => 'pastebin', ), 'db.phame' => array( 'type' => 'db', 'name' => 'phame', ), 'db.phriction' => array( 'type' => 'db', 'name' => 'phriction', ), 'db.project' => array( 'type' => 'db', 'name' => 'project', ), 'db.repository' => array( 'type' => 'db', 'name' => 'repository', ), 'db.search' => array( 'type' => 'db', 'name' => 'search', ), 'db.slowvote' => array( 'type' => 'db', 'name' => 'slowvote', ), 'db.timeline' => array( 'type' => 'db', 'name' => 'timeline', 'dead' => true, ), 'db.user' => array( 'type' => 'db', 'name' => 'user', ), 'db.worker' => array( 'type' => 'db', 'name' => 'worker', ), 'db.xhpastview' => array( 'type' => 'db', 'name' => 'xhpastview', ), 'db.cache' => array( 'type' => 'db', 'name' => 'cache', ), 'db.fact' => array( 'type' => 'db', 'name' => 'fact', ), 'db.ponder' => array( 'type' => 'db', 'name' => 'ponder', ), 'db.xhprof' => array( 'type' => 'db', 'name' => 'xhprof', ), 'db.pholio' => array( 'type' => 'db', 'name' => 'pholio', ), 'db.conpherence' => array( 'type' => 'db', 'name' => 'conpherence', ), 'db.config' => array( 'type' => 'db', 'name' => 'config', ), 'db.token' => array( 'type' => 'db', 'name' => 'token', ), 'db.releeph' => array( 'type' => 'db', 'name' => 'releeph', ), 'db.phlux' => array( 'type' => 'db', 'name' => 'phlux', ), 'db.phortune' => array( 'type' => 'db', 'name' => 'phortune', ), 'db.phrequent' => array( 'type' => 'db', 'name' => 'phrequent', ), 'db.diviner' => array( 'type' => 'db', 'name' => 'diviner', ), 'db.auth' => array( 'type' => 'db', 'name' => 'auth', ), 'db.doorkeeper' => array( 'type' => 'db', 'name' => 'doorkeeper', ), 'db.legalpad' => array( 'type' => 'db', 'name' => 'legalpad', ), '0000.legacy.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('0000.legacy.sql'), 'legacy' => 0, ), '000.project.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('000.project.sql'), 'legacy' => 0, ), '001.maniphest_projects.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('001.maniphest_projects.sql'), 'legacy' => 1, ), '002.oauth.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('002.oauth.sql'), 'legacy' => 2, ), '003.more_oauth.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('003.more_oauth.sql'), 'legacy' => 3, ), '004.daemonrepos.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('004.daemonrepos.sql'), 'legacy' => 4, ), '005.workers.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('005.workers.sql'), 'legacy' => 5, ), '006.repository.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('006.repository.sql'), 'legacy' => 6, ), '007.daemonlog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('007.daemonlog.sql'), 'legacy' => 7, ), '008.repoopt.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('008.repoopt.sql'), 'legacy' => 8, ), '009.repo_summary.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('009.repo_summary.sql'), 'legacy' => 9, ), '010.herald.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('010.herald.sql'), 'legacy' => 10, ), '011.badcommit.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('011.badcommit.sql'), 'legacy' => 11, ), '012.dropphidtype.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('012.dropphidtype.sql'), 'legacy' => 12, ), '013.commitdetail.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('013.commitdetail.sql'), 'legacy' => 13, ), '014.shortcuts.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('014.shortcuts.sql'), 'legacy' => 14, ), '015.preferences.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('015.preferences.sql'), 'legacy' => 15, ), '016.userrealnameindex.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('016.userrealnameindex.sql'), 'legacy' => 16, ), '017.sessionkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('017.sessionkeys.sql'), 'legacy' => 17, ), '018.owners.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('018.owners.sql'), 'legacy' => 18, ), '019.arcprojects.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('019.arcprojects.sql'), 'legacy' => 19, ), '020.pathcapital.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('020.pathcapital.sql'), 'legacy' => 20, ), '021.xhpastview.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('021.xhpastview.sql'), 'legacy' => 21, ), '022.differentialcommit.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('022.differentialcommit.sql'), 'legacy' => 22, ), '023.dxkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('023.dxkeys.sql'), 'legacy' => 23, ), '024.mlistkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('024.mlistkeys.sql'), 'legacy' => 24, ), '025.commentopt.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('025.commentopt.sql'), 'legacy' => 25, ), '026.diffpropkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('026.diffpropkey.sql'), 'legacy' => 26, ), '027.metamtakeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('027.metamtakeys.sql'), 'legacy' => 27, ), '028.systemagent.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('028.systemagent.sql'), 'legacy' => 28, ), '029.cursors.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('029.cursors.sql'), 'legacy' => 29, ), '030.imagemacro.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('030.imagemacro.sql'), 'legacy' => 30, ), '031.workerrace.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('031.workerrace.sql'), 'legacy' => 31, ), '032.viewtime.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('032.viewtime.sql'), 'legacy' => 32, ), '033.privtest.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('033.privtest.sql'), 'legacy' => 33, ), '034.savedheader.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('034.savedheader.sql'), 'legacy' => 34, ), '035.proxyimage.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('035.proxyimage.sql'), 'legacy' => 35, ), '036.mailkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('036.mailkey.sql'), 'legacy' => 36, ), '037.setuptest.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('037.setuptest.sql'), 'legacy' => 37, ), '038.admin.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('038.admin.sql'), 'legacy' => 38, ), '039.userlog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('039.userlog.sql'), 'legacy' => 39, ), '040.transform.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('040.transform.sql'), 'legacy' => 40, ), '041.heraldrepetition.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('041.heraldrepetition.sql'), 'legacy' => 41, ), '042.commentmetadata.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('042.commentmetadata.sql'), 'legacy' => 42, ), '043.pastebin.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('043.pastebin.sql'), 'legacy' => 43, ), '044.countdown.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('044.countdown.sql'), 'legacy' => 44, ), '045.timezone.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('045.timezone.sql'), 'legacy' => 45, ), '046.conduittoken.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('046.conduittoken.sql'), 'legacy' => 46, ), '047.projectstatus.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('047.projectstatus.sql'), 'legacy' => 47, ), '048.relationshipkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('048.relationshipkeys.sql'), 'legacy' => 48, ), '049.projectowner.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('049.projectowner.sql'), 'legacy' => 49, ), '050.taskdenormal.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('050.taskdenormal.sql'), 'legacy' => 50, ), '051.projectfilter.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('051.projectfilter.sql'), 'legacy' => 51, ), '052.pastelanguage.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('052.pastelanguage.sql'), 'legacy' => 52, ), '053.feed.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('053.feed.sql'), 'legacy' => 53, ), '054.subscribers.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('054.subscribers.sql'), 'legacy' => 54, ), '055.add_author_to_files.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('055.add_author_to_files.sql'), 'legacy' => 55, ), '056.slowvote.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('056.slowvote.sql'), 'legacy' => 56, ), '057.parsecache.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('057.parsecache.sql'), 'legacy' => 57, ), '058.missingkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('058.missingkeys.sql'), 'legacy' => 58, ), '059.engines.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('059.engines.php'), 'legacy' => 59, ), '060.phriction.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('060.phriction.sql'), 'legacy' => 60, ), '061.phrictioncontent.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('061.phrictioncontent.sql'), 'legacy' => 61, ), '062.phrictionmenu.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('062.phrictionmenu.sql'), 'legacy' => 62, ), '063.pasteforks.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('063.pasteforks.sql'), 'legacy' => 63, ), '064.subprojects.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('064.subprojects.sql'), 'legacy' => 64, ), '065.sshkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('065.sshkeys.sql'), 'legacy' => 65, ), '066.phrictioncontent.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('066.phrictioncontent.sql'), 'legacy' => 66, ), '067.preferences.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('067.preferences.sql'), 'legacy' => 67, ), '068.maniphestauxiliarystorage.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('068.maniphestauxiliarystorage.sql'), 'legacy' => 68, ), '069.heraldxscript.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('069.heraldxscript.sql'), 'legacy' => 69, ), '070.differentialaux.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('070.differentialaux.sql'), 'legacy' => 70, ), '071.contentsource.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('071.contentsource.sql'), 'legacy' => 71, ), '072.blamerevert.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('072.blamerevert.sql'), 'legacy' => 72, ), '073.reposymbols.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('073.reposymbols.sql'), 'legacy' => 73, ), '074.affectedpath.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('074.affectedpath.sql'), 'legacy' => 74, ), '075.revisionhash.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('075.revisionhash.sql'), 'legacy' => 75, ), '076.indexedlanguages.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('076.indexedlanguages.sql'), 'legacy' => 76, ), '077.originalemail.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('077.originalemail.sql'), 'legacy' => 77, ), '078.nametoken.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('078.nametoken.sql'), 'legacy' => 78, ), '079.nametokenindex.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('079.nametokenindex.php'), 'legacy' => 79, ), '080.filekeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('080.filekeys.sql'), 'legacy' => 80, ), '081.filekeys.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('081.filekeys.php'), 'legacy' => 81, ), '082.xactionkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('082.xactionkey.sql'), 'legacy' => 82, ), '083.dxviewtime.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('083.dxviewtime.sql'), 'legacy' => 83, ), '084.pasteauthorkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('084.pasteauthorkey.sql'), 'legacy' => 84, ), '085.packagecommitrelationship.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('085.packagecommitrelationship.sql'), 'legacy' => 85, ), '086.formeraffil.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('086.formeraffil.sql'), 'legacy' => 86, ), '087.phrictiondelete.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('087.phrictiondelete.sql'), 'legacy' => 87, ), '088.audit.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('088.audit.sql'), 'legacy' => 88, ), '089.projectwiki.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('089.projectwiki.sql'), 'legacy' => 89, ), '090.forceuniqueprojectnames.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('090.forceuniqueprojectnames.php'), 'legacy' => 90, ), '091.uniqueslugkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('091.uniqueslugkey.sql'), 'legacy' => 91, ), '092.dropgithubnotification.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('092.dropgithubnotification.sql'), 'legacy' => 92, ), '093.gitremotes.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('093.gitremotes.php'), 'legacy' => 93, ), '094.phrictioncolumn.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('094.phrictioncolumn.sql'), 'legacy' => 94, ), '095.directory.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('095.directory.sql'), 'legacy' => 95, ), '096.filename.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('096.filename.sql'), 'legacy' => 96, ), '097.heraldruletypes.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('097.heraldruletypes.sql'), 'legacy' => 97, ), '098.heraldruletypemigration.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('098.heraldruletypemigration.php'), 'legacy' => 98, ), '099.drydock.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('099.drydock.sql'), 'legacy' => 99, ), '100.projectxaction.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('100.projectxaction.sql'), 'legacy' => 100, ), '101.heraldruleapplied.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('101.heraldruleapplied.sql'), 'legacy' => 101, ), '102.heraldcleanup.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('102.heraldcleanup.php'), 'legacy' => 102, ), '103.heraldedithistory.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('103.heraldedithistory.sql'), 'legacy' => 103, ), '104.searchkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('104.searchkey.sql'), 'legacy' => 104, ), '105.mimetype.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('105.mimetype.sql'), 'legacy' => 105, ), '106.chatlog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('106.chatlog.sql'), 'legacy' => 106, ), '107.oauthserver.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('107.oauthserver.sql'), 'legacy' => 107, ), '108.oauthscope.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('108.oauthscope.sql'), 'legacy' => 108, ), '109.oauthclientphidkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('109.oauthclientphidkey.sql'), 'legacy' => 109, ), '110.commitaudit.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('110.commitaudit.sql'), 'legacy' => 110, ), '111.commitauditmigration.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('111.commitauditmigration.php'), 'legacy' => 111, ), '112.oauthaccesscoderedirecturi.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('112.oauthaccesscoderedirecturi.sql'), 'legacy' => 112, ), '113.lastreviewer.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('113.lastreviewer.sql'), 'legacy' => 113, ), '114.auditrequest.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('114.auditrequest.sql'), 'legacy' => 114, ), '115.prepareutf8.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('115.prepareutf8.sql'), 'legacy' => 115, ), '116.utf8-backup-first-expect-wait.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('116.utf8-backup-first-expect-wait.sql'), 'legacy' => 116, ), '117.repositorydescription.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('117.repositorydescription.php'), 'legacy' => 117, ), '118.auditinline.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('118.auditinline.sql'), 'legacy' => 118, ), '119.filehash.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('119.filehash.sql'), 'legacy' => 119, ), '120.noop.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('120.noop.sql'), 'legacy' => 120, ), '121.drydocklog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('121.drydocklog.sql'), 'legacy' => 121, ), '122.flag.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('122.flag.sql'), 'legacy' => 122, ), '123.heraldrulelog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('123.heraldrulelog.sql'), 'legacy' => 123, ), '124.subpriority.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('124.subpriority.sql'), 'legacy' => 124, ), '125.ipv6.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('125.ipv6.sql'), 'legacy' => 125, ), '126.edges.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('126.edges.sql'), 'legacy' => 126, ), '127.userkeybody.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('127.userkeybody.sql'), 'legacy' => 127, ), '128.phabricatorcom.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('128.phabricatorcom.sql'), 'legacy' => 128, ), '129.savedquery.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('129.savedquery.sql'), 'legacy' => 129, ), '130.denormalrevisionquery.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('130.denormalrevisionquery.sql'), 'legacy' => 130, ), '131.migraterevisionquery.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('131.migraterevisionquery.php'), 'legacy' => 131, ), '132.phame.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('132.phame.sql'), 'legacy' => 132, ), '133.imagemacro.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('133.imagemacro.sql'), 'legacy' => 133, ), '134.emptysearch.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('134.emptysearch.sql'), 'legacy' => 134, ), '135.datecommitted.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('135.datecommitted.sql'), 'legacy' => 135, ), '136.sex.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('136.sex.sql'), 'legacy' => 136, ), '137.auditmetadata.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('137.auditmetadata.sql'), 'legacy' => 137, ), '138.notification.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('138.notification.sql'), ), 'holidays.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('holidays.sql'), ), 'userstatus.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('userstatus.sql'), ), 'emailtable.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('emailtable.sql'), ), 'emailtableport.sql' => array( 'type' => 'php', 'name' => $this->getPatchPath('emailtableport.php'), ), 'emailtableremove.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('emailtableremove.sql'), ), 'phiddrop.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('phiddrop.sql'), ), 'testdatabase.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('testdatabase.sql'), ), 'ldapinfo.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('ldapinfo.sql'), ), 'threadtopic.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('threadtopic.sql'), ), 'usertranslation.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('usertranslation.sql'), ), 'differentialbookmarks.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('differentialbookmarks.sql'), ), 'harbormasterobject.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('harbormasterobject.sql'), ), 'markupcache.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('markupcache.sql'), ), 'maniphestxcache.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('maniphestxcache.sql'), ), 'migrate-maniphest-dependencies.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('migrate-maniphest-dependencies.php'), ), 'migrate-differential-dependencies.php' => array( 'type' => 'php', 'name' => $this->getPatchPath( 'migrate-differential-dependencies.php'), ), 'phameblog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('phameblog.sql'), ), 'migrate-maniphest-revisions.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('migrate-maniphest-revisions.php'), ), 'daemonstatus.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('daemonstatus.sql'), ), 'symbolcontexts.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('symbolcontexts.sql'), ), 'migrate-project-edges.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('migrate-project-edges.php'), ), 'fact-raw.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('fact-raw.sql'), ), 'ponder.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('ponder.sql') ), 'policy-project.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('policy-project.sql'), ), 'daemonstatuskey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('daemonstatuskey.sql'), ), 'edgetype.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('edgetype.sql'), ), 'ponder-comments.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('ponder-comments.sql'), ), 'pastepolicy.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('pastepolicy.sql'), ), 'xhprof.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('xhprof.sql'), ), 'draft-metadata.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('draft-metadata.sql'), ), 'phamedomain.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('phamedomain.sql'), ), 'ponder-mailkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('ponder-mailkey.sql'), ), 'ponder-mailkey-populate.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('ponder-mailkey-populate.php'), ), 'phamepolicy.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('phamepolicy.sql'), ), 'phameoneblog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('phameoneblog.sql'), ), 'statustxt.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('statustxt.sql'), ), 'daemontaskarchive.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('daemontaskarchive.sql'), ), 'drydocktaskid.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('drydocktaskid.sql'), ), 'drydockresoucetype.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('drydockresourcetype.sql'), ), 'liskcounters.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('liskcounters.sql'), ), 'liskcounters.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('liskcounters.php'), ), 'dropfileproxyimage.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('dropfileproxyimage.sql'), ), 'repository-lint.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('repository-lint.sql'), ), 'liskcounters-task.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('liskcounters-task.sql'), ), 'pholio.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('pholio.sql'), ), 'owners-exclude.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('owners-exclude.sql'), ), '20121209.pholioxactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20121209.pholioxactions.sql'), ), '20121209.xmacroadd.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20121209.xmacroadd.sql'), ), '20121209.xmacromigrate.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20121209.xmacromigrate.php'), ), '20121209.xmacromigratekey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20121209.xmacromigratekey.sql'), ), '20121220.generalcache.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20121220.generalcache.sql'), ), '20121226.config.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20121226.config.sql'), ), '20130101.confxaction.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130101.confxaction.sql'), ), '20130102.metamtareceivedmailmessageidhash.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130102.metamtareceivedmailmessageidhash.sql'), ), '20130103.filemetadata.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130103.filemetadata.sql'), ), '20130111.conpherence.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130111.conpherence.sql'), ), '20130127.altheraldtranscript.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130127.altheraldtranscript.sql'), ), '20130201.revisionunsubscribed.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130201.revisionunsubscribed.php'), ), '20130201.revisionunsubscribed.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130201.revisionunsubscribed.sql'), ), '20130131.conpherencepics.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130131.conpherencepics.sql'), ), '20130214.chatlogchannel.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130214.chatlogchannel.sql'), ), '20130214.chatlogchannelid.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130214.chatlogchannelid.sql'), ), '20130214.token.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130214.token.sql'), ), '20130215.phabricatorfileaddttl.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130215.phabricatorfileaddttl.sql'), ), '20130217.cachettl.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130217.cachettl.sql'), ), '20130218.updatechannelid.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130218.updatechannelid.php'), ), '20130218.longdaemon.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130218.longdaemon.sql'), ), '20130219.commitsummary.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130219.commitsummary.sql'), ), '20130219.commitsummarymig.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130219.commitsummarymig.php'), ), '20130222.dropchannel.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130222.dropchannel.sql'), ), '20130226.commitkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130226.commitkey.sql'), ), '20131302.maniphestvalue.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20131302.maniphestvalue.sql'), ), '20130304.lintauthor.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130304.lintauthor.sql'), ), 'releeph.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('releeph.sql'), ), '20130319.phabricatorfileexplicitupload.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath( '20130319.phabricatorfileexplicitupload.sql') ), '20130319.conpherence.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130319.conpherence.sql'), ), '20130320.phlux.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130320.phlux.sql'), ), '20130317.phrictionedge.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130317.phrictionedge.sql'), ), '20130321.token.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130321.token.sql'), ), '20130310.xactionmeta.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130310.xactionmeta.sql'), ), '20130322.phortune.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130322.phortune.sql'), ), '20130323.phortunepayment.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130323.phortunepayment.sql'), ), '20130324.phortuneproduct.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130324.phortuneproduct.sql'), ), '20130330.phrequent.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130330.phrequent.sql'), ), '20130403.conpherencecache.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130403.conpherencecache.sql'), ), '20130403.conpherencecachemig.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130403.conpherencecachemig.php'), ), '20130409.commitdrev.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130409.commitdrev.php'), ), '20130417.externalaccount.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130417.externalaccount.sql'), ), '20130423.updateexternalaccount.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130423.updateexternalaccount.sql'), ), '20130423.phortunepaymentrevised.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130423.phortunepaymentrevised.sql'), ), '20130423.conpherenceindices.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130423.conpherenceindices.sql'), ), '20130426.search_savedquery.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130426.search_savedquery.sql'), ), '20130502.countdownrevamp1.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130502.countdownrevamp1.sql'), ), '20130502.countdownrevamp2.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130502.countdownrevamp2.php'), ), '20130502.countdownrevamp3.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130502.countdownrevamp3.sql'), ), '20130507.releephrqsimplifycols.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130507.releephrqsimplifycols.sql'), ), '20130507.releephrqmailkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130507.releephrqmailkey.sql'), ), '20130507.releephrqmailkeypop.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130507.releephrqmailkeypop.php'), ), '20130508.search_namedquery.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130508.search_namedquery.sql'), ), '20130508.releephtransactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130508.releephtransactions.sql'), ), '20130508.releephtransactionsmig.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130508.releephtransactionsmig.php'), ), '20130513.receviedmailstatus.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130513.receviedmailstatus.sql'), ), '20130519.diviner.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130519.diviner.sql'), ), '20130521.dropconphimages.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130521.dropconphimages.sql'), ), '20130523.maniphest_owners.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130523.maniphest_owners.sql'), ), '20130524.repoxactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130524.repoxactions.sql'), ), '20130529.macroauthor.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130529.macroauthor.sql'), ), '20130529.macroauthormig.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130529.macroauthormig.php'), ), '20130530.sessionhash.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130530.sessionhash.php'), ), '20130530.macrodatekey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130530.macrodatekey.sql'), ), '20130530.pastekeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130530.pastekeys.sql'), ), '20130531.filekeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130531.filekeys.sql'), ), '20130602.morediviner.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130602.morediviner.sql'), ), '20130602.namedqueries.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130602.namedqueries.sql'), ), '20130606.userxactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130606.userxactions.sql'), ), '20130607.xaccount.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130607.xaccount.sql'), ), '20130611.migrateoauth.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130611.migrateoauth.php'), ), '20130611.nukeldap.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130611.nukeldap.php'), ), '20130613.authdb.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130613.authdb.sql'), ), '20130619.authconf.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130619.authconf.php'), ), '20130620.diffxactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130620.diffxactions.sql'), ), '20130621.diffcommentphid.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130621.diffcommentphid.sql'), ), '20130621.diffcommentphidmig.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130621.diffcommentphidmig.php'), ), '20130621.diffcommentunphid.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130621.diffcommentunphid.sql'), ), '20130622.doorkeeper.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130622.doorkeeper.sql'), ), '20130628.legalpadv0.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130628.legalpadv0.sql'), ), '20130701.conduitlog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130701.conduitlog.sql'), ), 'legalpad-mailkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('legalpad-mailkey.sql'), ), 'legalpad-mailkey-populate.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('legalpad-mailkey-populate.php'), ), '20130703.legalpaddocdenorm.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130703.legalpaddocdenorm.sql'), ), '20130703.legalpaddocdenorm.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130703.legalpaddocdenorm.php'), ), '20130709.legalpadsignature.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130709.legalpadsignature.sql'), ), '20130709.droptimeline.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130709.droptimeline.sql'), ), '20130711.trimrealnames.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130711.trimrealnames.php'), ), '20130714.votexactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130714.votexactions.sql'), ), '20130715.votecomments.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130715.votecomments.php'), ), '20130715.voteedges.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130715.voteedges.sql'), ), '20130711.pholioimageobsolete.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130711.pholioimageobsolete.sql'), ), '20130711.pholioimageobsolete.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130711.pholioimageobsolete.php'), ), '20130711.pholioimageobsolete2.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130711.pholioimageobsolete2.sql'), ), '20130716.archivememberlessprojects.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130716.archivememberlessprojects.php'), ), '20130722.pholioreplace.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130722.pholioreplace.sql'), ), '20130723.taskstarttime.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130723.taskstarttime.sql'), ), '20130727.ponderquestionstatus.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130727.ponderquestionstatus.sql'), ), '20130726.ponderxactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130726.ponderxactions.sql'), ), '20130728.ponderunique.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130728.ponderunique.php'), ), '20130728.ponderuniquekey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130728.ponderuniquekey.sql'), ), '20130728.ponderxcomment.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130728.ponderxcomment.php'), ), '20130801.pastexactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130801.pastexactions.sql'), ), '20130801.pastexactions.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130801.pastexactions.php'), ), '20130805.pastemailkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130805.pastemailkey.sql'), ), '20130805.pasteedges.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130805.pasteedges.sql'), ), '20130805.pastemailkeypop.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130805.pastemailkeypop.php'), ), '20130802.heraldphid.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130802.heraldphid.sql'), ), '20130802.heraldphids.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130802.heraldphids.php'), ), '20130802.heraldphidukey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130802.heraldphidukey.sql'), ), '20130802.heraldxactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130802.heraldxactions.sql'), ), '20130731.releephrepoid.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130731.releephrepoid.sql'), ), '20130731.releephproject.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130731.releephproject.sql'), ), '20130731.releephcutpointidentifier.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130731.releephcutpointidentifier.sql'), ), '20130814.usercustom.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130814.usercustom.sql'), ), '20130820.releephxactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130820.releephxactions.sql'), ), + '20130826.divinernode.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20130826.divinernode.sql'), + ), ); } }