@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Move Diviner further toward usability

Summary:
- Complete the "project" -> "book" stuff. This is cleaner conceptually and keeps us from having yet another meaning for the word "project".
- Normalize symbols during atomization. This simplifies publishing a great deal, and allows static documentation to link to dynamic documentation and vice versa, because the canonical names of symbols are agreed upon (we can tweak the actual algorithm).
- Give articles a specifiable name distinct from the title, and default to something like "support" instead of "Get Help! Get Support!" so URIs end up more readable (not "Get_Help!_Get_Support!").
- Have the atomizers set book information on atoms.
- Implement very basic publishers. Publishers are basically glue code between the atomization process and the rendering process -- the two we'll have initially are "static" (publish to files on disk) and "phabricator" (or similar -- publish into the database).
- Handle duplicate symbol definitions in the atomize and publish pipelines. This fixes the issue where a project defines two functions named "idx()" and we currently tell them not to do that and break. Realistically, this is common in the real world and we should just roll our eyes and do the legwork to generate documentation as best we can.
- Particularly, dirty all atoms with the same name as a dirty atom (e.g., if 'function f()' is updated, regnerate the documentation for all functions named f() in the book).
- When publishing, we publish these at "function/f/@1", "function/f/@2". The base page will offer to disambiguate ("There are 8 functions named 'f' in this codebase, which one do you want?").
- Implement a very very basic renderer. This generates the actual HTML (or text, or XML, or whatever else) for the documentation, which the publisher dumps onto disk or into a database or whatever.
- The atomize workflow actually needs to depend on books, at least sort of, so make it load config and use it properly.
- Propagate multilevel dirties through the graph. If "C extends B" and "B extends A", we should regenerate C when A changes. Prior to this diff, we would regnerate B only.

Test Plan: Generated some documentation. Named two articles "feedback", generated docs, saw "article/feedback/@1/" and "article/feedback/@2/" created.

Reviewers: btrahan, vrana, chad

Reviewed By: chad

CC: aran

Maniphest Tasks: T988

Differential Revision: https://secure.phabricator.com/D4896

+460 -77
+6
src/__phutil_library_map__.php
··· 467 467 'DivinerAtomRef' => 'applications/diviner/atom/DivinerAtomRef.php', 468 468 'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php', 469 469 'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php', 470 + 'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php', 470 471 'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php', 471 472 'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php', 472 473 'DivinerListController' => 'applications/diviner/controller/DivinerListController.php', 474 + 'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php', 475 + 'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php', 476 + 'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php', 473 477 'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php', 474 478 'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php', 475 479 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', ··· 1962 1966 'DiffusionView' => 'AphrontView', 1963 1967 'DivinerArticleAtomizer' => 'DivinerAtomizer', 1964 1968 'DivinerAtomizeWorkflow' => 'DivinerWorkflow', 1969 + 'DivinerDefaultRenderer' => 'DivinerRenderer', 1965 1970 'DivinerFileAtomizer' => 'DivinerAtomizer', 1966 1971 'DivinerGenerateWorkflow' => 'DivinerWorkflow', 1967 1972 'DivinerListController' => 'PhabricatorController', 1973 + 'DivinerStaticPublisher' => 'DivinerPublisher', 1968 1974 'DivinerWorkflow' => 'PhutilArgumentWorkflow', 1969 1975 'DrydockAllocatorWorker' => 'PhabricatorWorker', 1970 1976 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
+36 -6
src/applications/diviner/atom/DivinerAtom.php
··· 22 22 private $context; 23 23 private $extends = array(); 24 24 private $links = array(); 25 - private $project; 25 + private $book; 26 26 27 - public function setProject($project) { 28 - $this->project = $project; 27 + /** 28 + * Returns a sorting key which imposes an unambiguous, stable order on atoms. 29 + */ 30 + public function getSortKey() { 31 + return implode( 32 + "\0", 33 + array( 34 + $this->getBook(), 35 + $this->getType(), 36 + $this->getContext(), 37 + $this->getName(), 38 + $this->getFile(), 39 + sprintf('%08', $this->getLine()), 40 + )); 41 + } 42 + 43 + public function setBook($book) { 44 + $this->book = $book; 29 45 return $this; 30 46 } 31 47 32 - public function getProject() { 33 - return $this->project; 48 + public function getBook() { 49 + return $this->book; 34 50 } 35 51 36 52 public function setContext($context) { ··· 82 98 throw new Exception("Call setDocblockRaw() before getDocblockMeta()!"); 83 99 } 84 100 return $this->docblockMeta; 101 + } 102 + 103 + public function getDocblockMetaValue($key, $default = null) { 104 + $meta = $this->getDocblockMeta(); 105 + return idx($meta, $key, $default); 106 + } 107 + 108 + public function setDocblockMetaValue($key, $value) { 109 + $meta = $this->getDocblockMeta(); 110 + $meta[$key] = $value; 111 + $this->docblockMeta = $meta; 112 + return $this; 85 113 } 86 114 87 115 public function setType($type) { ··· 235 263 // getAtomSerializationVersion(). 236 264 237 265 return array( 266 + 'book' => $this->getBook(), 238 267 'type' => $this->getType(), 239 268 'name' => $this->getName(), 240 269 'file' => $this->getFile(), ··· 256 285 257 286 public function getRef() { 258 287 return id(new DivinerAtomRef()) 259 - ->setProject($this->getProject()) 288 + ->setBook($this->getBook()) 260 289 ->setContext($this->getContext()) 261 290 ->setType($this->getType()) 262 291 ->setName($this->getName()); ··· 264 293 265 294 public static function newFromDictionary(array $dictionary) { 266 295 $atom = id(new DivinerAtom()) 296 + ->setBook(idx($dictionary, 'book')) 267 297 ->setType(idx($dictionary, 'type')) 268 298 ->setName(idx($dictionary, 'name')) 269 299 ->setFile(idx($dictionary, 'file'))
+66 -9
src/applications/diviner/atom/DivinerAtomRef.php
··· 2 2 3 3 final class DivinerAtomRef { 4 4 5 - private $project; 5 + private $book; 6 6 private $context; 7 7 private $type; 8 8 private $name; 9 9 10 10 public function setName($name) { 11 + $normal_name = self::normalizeString($name); 12 + if (preg_match('/^@[0-9]+$/', $normal_name)) { 13 + throw new Exception( 14 + "Atom names must not be in the form '/@\d+/'. This pattern is ". 15 + "reserved for disambiguating atoms with similar names."); 16 + } 11 17 $this->name = $name; 12 18 return $this; 13 19 } ··· 17 23 } 18 24 19 25 public function setType($type) { 20 - $this->type = $type; 26 + $this->type = self::normalizeString($type); 21 27 return $this; 22 28 } 23 29 ··· 26 32 } 27 33 28 34 public function setContext($context) { 29 - $this->context = $context; 35 + if ($context === null) { 36 + $this->context = $context; 37 + } else { 38 + $this->context = self::normalizeString($context); 39 + } 30 40 return $this; 31 41 } 32 42 ··· 34 44 return $this->context; 35 45 } 36 46 37 - public function setProject($project) { 38 - $this->project = $project; 47 + public function setBook($book) { 48 + $this->book = self::normalizeString($book); 39 49 return $this; 40 50 } 41 51 42 - public function getProject() { 43 - return $this->project; 52 + public function getBook() { 53 + return $this->book; 44 54 } 45 55 46 56 public function toDictionary() { 47 57 return array( 48 - 'project' => $this->getProject(), 58 + 'book' => $this->getBook(), 49 59 'context' => $this->getContext(), 50 60 'type' => $this->getType(), 51 61 'name' => $this->getName(), ··· 60 70 61 71 public static function newFromDictionary(array $dict) { 62 72 $obj = new DivinerAtomRef(); 63 - $obj->project = idx($dict, 'project'); 73 + $obj->book = idx($dict, 'book'); 64 74 $obj->context = idx($dict, 'context'); 65 75 $obj->type = idx($dict, 'type'); 66 76 $obj->name = idx($dict, 'name'); 67 77 return $obj; 68 78 } 79 + 80 + public static function normalizeString($str) { 81 + // These characters create problems on the filesystem or in URIs. Replace 82 + // them with non-problematic appoximations (instead of simply removing them) 83 + // to keep the URIs fairly useful and avoid unnecessary collisions. These 84 + // approximations are selected based on some domain knowledge of common 85 + // languages: where a character is used as a delimiter, it is more helpful 86 + // to replace it with a "." or a ":" or similar, while it's better if 87 + // operator overloads read as, e.g., "operator_div". 88 + 89 + $map = array( 90 + // Hopefully not used anywhere by anything. 91 + '#' => '.', 92 + 93 + // Used in Ruby methods. 94 + '?' => 'Q', 95 + 96 + // Used in PHP namespaces. 97 + '\\' => '.', 98 + 99 + // Used in "operator +" in C++. 100 + '+' => 'plus', 101 + 102 + // Used in "operator %" in C++. 103 + '%' => 'mod', 104 + 105 + // Used in "operator /" in C++. 106 + '/' => 'div', 107 + ); 108 + $str = str_replace(array_keys($map), array_values($map), $str); 109 + 110 + // Replace all spaces with underscores. 111 + $str = preg_replace('/ +/', '_', $str); 112 + 113 + // Replace control characters with "@". 114 + $str = preg_replace('/[\x00-\x19]/', '@', $str); 115 + 116 + // Replace specific problematic names with alternative names. 117 + $alternates = array( 118 + '.' => 'dot', 119 + '..' => 'dotdot', 120 + '' => 'null', 121 + ); 122 + 123 + return idx($alternates, $str, $str); 124 + } 125 + 69 126 }
+12 -2
src/applications/diviner/atomizer/DivinerArticleAtomizer.php
··· 12 12 $atom->setDocblockRaw($block); 13 13 14 14 $meta = $atom->getDocblockMeta(); 15 + 15 16 $title = idx($meta, 'title'); 16 17 if (!strlen($title)) { 17 - $title = 'Untitled Article "'.basename($file_name).'"'; 18 + $title = pht('Untitled Article "%s"', basename($file_name)); 18 19 $atom->addWarning("Article has no @title!"); 20 + $atom->setDocblockMetaValue('title', $title); 19 21 } 20 - $atom->setName($title); 22 + 23 + // If the article has no @name, use the filename after stripping any 24 + // extension. 25 + $name = idx($meta, 'name'); 26 + if (!$name) { 27 + $name = basename($file_name); 28 + $name = preg_replace('/\\.[^.]+$/', '', $name); 29 + } 30 + $atom->setName($name); 21 31 22 32 return array($atom); 23 33 }
+9 -9
src/applications/diviner/atomizer/DivinerAtomizer.php
··· 5 5 */ 6 6 abstract class DivinerAtomizer { 7 7 8 - private $project; 8 + private $book; 9 9 10 10 /** 11 11 * If you make a significant change to an atomizer, you can bump this ··· 17 17 18 18 abstract public function atomize($file_name, $file_data); 19 19 20 - final public function setProject($project) { 21 - $this->project = $project; 20 + final public function setBook($book) { 21 + $this->book = $book; 22 22 return $this; 23 23 } 24 24 25 - final public function getProject() { 26 - return $this->project; 25 + final public function getBook() { 26 + return $this->book; 27 27 } 28 28 29 29 protected function newAtom($type) { 30 30 return id(new DivinerAtom()) 31 - ->setProject($this->getProject()) 31 + ->setBook($this->getBook()) 32 32 ->setType($type); 33 33 } 34 34 35 - protected function newRef($type, $name, $project = null, $context = null) { 36 - $project = coalesce($project, $this->getProject()); 35 + protected function newRef($type, $name, $book = null, $context = null) { 36 + $book = coalesce($book, $this->getBook()); 37 37 38 38 return id(new DivinerAtomRef()) 39 - ->setProject($project) 39 + ->setBook($book) 40 40 ->setContext($context) 41 41 ->setType($type) 42 42 ->setName($name);
+132
src/applications/diviner/publisher/DivinerPublisher.php
··· 1 + <?php 2 + 3 + abstract class DivinerPublisher { 4 + 5 + private $atomCache; 6 + private $atomGraphHashToNodeHashMap; 7 + private $atomMap = array(); 8 + private $renderer; 9 + private $config; 10 + private $symbolReverseMap; 11 + 12 + public function setRenderer(DivinerRenderer $renderer) { 13 + $this->renderer = $renderer; 14 + return $this; 15 + } 16 + 17 + public function getRenderer() { 18 + return $this->renderer; 19 + } 20 + 21 + public function setConfig(array $config) { 22 + $this->config = $config; 23 + return $this; 24 + } 25 + 26 + public function getConfig($key, $default = null) { 27 + return idx($this->config, $key, $default); 28 + } 29 + 30 + public function setAtomCache(DivinerAtomCache $cache) { 31 + $this->atomCache = $cache; 32 + $graph_map = $this->atomCache->getGraphMap(); 33 + $this->atomGraphHashToNodeHashMap = array_flip($graph_map); 34 + } 35 + 36 + protected function getAtomFromGraphHash($graph_hash) { 37 + if (empty($this->atomGraphHashToNodeHashMap[$graph_hash])) { 38 + throw new Exception("No such atom '{$graph_hash}'!"); 39 + } 40 + 41 + return $this->getAtomFromNodeHash( 42 + $this->atomGraphHashToNodeHashMap[$graph_hash]); 43 + } 44 + 45 + protected function getAtomFromNodeHash($node_hash) { 46 + if (empty($this->atomMap[$node_hash])) { 47 + $dict = $this->atomCache->getAtom($node_hash); 48 + $this->atomMap[$node_hash] = DivinerAtom::newFromDictionary($dict); 49 + } 50 + return $this->atomMap[$node_hash]; 51 + } 52 + 53 + protected function getSimilarAtoms(DivinerAtom $atom) { 54 + if ($this->symbolReverseMap === null) { 55 + $rmap = array(); 56 + $smap = $this->atomCache->getSymbolMap(); 57 + foreach ($smap as $nhash => $shash) { 58 + $rmap[$shash][$nhash] = true; 59 + } 60 + $this->symbolReverseMap = $rmap; 61 + } 62 + 63 + $shash = $atom->getRef()->toHash(); 64 + 65 + if (empty($this->symbolReverseMap[$shash])) { 66 + throw new Exception("Atom has no symbol map entry!"); 67 + } 68 + 69 + $hashes = $this->symbolReverseMap[$shash]; 70 + 71 + $atoms = array(); 72 + foreach ($hashes as $hash => $ignored) { 73 + $atoms[] = $this->getAtomFromNodeHash($hash); 74 + } 75 + 76 + $atoms = msort($atoms, 'getSortKey'); 77 + return $atoms; 78 + } 79 + 80 + /** 81 + * If a book contains multiple definitions of some atom, like some function 82 + * "f()", we assign them an arbitrary (but fairly stable) order and publish 83 + * them as "function/f/1/", "function/f/2/", etc., or similar. 84 + */ 85 + protected function getAtomSimilarIndex(DivinerAtom $atom) { 86 + $atoms = $this->getSimilarAtoms($atom); 87 + if (count($atoms) == 1) { 88 + return null; 89 + } 90 + 91 + $index = 1; 92 + foreach ($atoms as $similar_atom) { 93 + if ($atom === $similar_atom) { 94 + return $index; 95 + } 96 + $index++; 97 + } 98 + 99 + throw new Exception("Expected to find atom while disambiguating!"); 100 + } 101 + 102 + 103 + abstract protected function loadAllPublishedHashes(); 104 + abstract protected function deleteDocumentsByHash(array $hashes); 105 + abstract protected function createDocumentsByHash(array $hashes); 106 + 107 + final public function publishAtoms(array $hashes) { 108 + $existing = $this->loadAllPublishedHashes(); 109 + 110 + $existing_map = array_fill_keys($existing, true); 111 + $hashes_map = array_fill_keys($hashes, true); 112 + 113 + $deleted = array_diff_key($existing_map, $hashes_map); 114 + $created = array_diff_key($hashes_map, $existing_map); 115 + 116 + $this->createDocumentsByHash(array_keys($created)); 117 + $this->deleteDocumentsByHash(array_keys($deleted)); 118 + } 119 + 120 + protected function shouldGenerateDocumentForAtom(DivinerAtom $atom) { 121 + switch ($atom->getType()) { 122 + case DivinerAtom::TYPE_FILE: 123 + return false; 124 + case DivinerAtom::TYPE_ARTICLE: 125 + default: 126 + break; 127 + } 128 + 129 + return true; 130 + } 131 + 132 + }
+65
src/applications/diviner/publisher/DivinerStaticPublisher.php
··· 1 + <?php 2 + 3 + final class DivinerStaticPublisher extends DivinerPublisher { 4 + 5 + protected function loadAllPublishedHashes() { 6 + return array(); 7 + } 8 + 9 + protected function deleteDocumentsByHash(array $hashes) { 10 + return; 11 + } 12 + 13 + protected function createDocumentsByHash(array $hashes) { 14 + foreach ($hashes as $hash) { 15 + $atom = $this->getAtomFromGraphHash($hash); 16 + 17 + if (!$this->shouldGenerateDocumentForAtom($atom)) { 18 + continue; 19 + } 20 + 21 + $content = $this->getRenderer()->renderAtom($atom); 22 + $this->writeDocument($atom, $content); 23 + } 24 + } 25 + 26 + private function writeDocument(DivinerAtom $atom, $content) { 27 + $root = $this->getConfig('root'); 28 + $path = $root.DIRECTORY_SEPARATOR.$this->getAtomRelativePath($atom); 29 + 30 + if (!Filesystem::pathExists($path)) { 31 + Filesystem::createDirectory($path, $umask = 0755, $recursive = true); 32 + } 33 + 34 + Filesystem::writeFile($path.'index.html', $content); 35 + } 36 + 37 + private function getAtomRelativePath(DivinerAtom $atom) { 38 + $ref = $atom->getRef(); 39 + 40 + $book = $ref->getBook(); 41 + $type = $ref->getType(); 42 + $context = $ref->getContext(); 43 + $name = $ref->getName(); 44 + 45 + $path = array( 46 + 'docs', 47 + $book, 48 + $type, 49 + ); 50 + if ($context !== null) { 51 + $path[] = $context; 52 + } 53 + $path[] = $name; 54 + 55 + $index = $this->getAtomSimilarIndex($atom); 56 + if ($index !== null) { 57 + $path[] = '@'.$index; 58 + } 59 + 60 + $path[] = null; 61 + 62 + return implode(DIRECTORY_SEPARATOR, $path); 63 + } 64 + 65 + }
+9
src/applications/diviner/renderer/DivinerDefaultRenderer.php
··· 1 + <?php 2 + 3 + final class DivinerDefaultRenderer extends DivinerRenderer { 4 + 5 + public function renderAtom(DivinerAtom $atom) { 6 + return "ATOM: ".$atom->getType()." ".$atom->getName()."!"; 7 + } 8 + 9 + }
+7
src/applications/diviner/renderer/DivinerRenderer.php
··· 1 + <?php 2 + 3 + abstract class DivinerRenderer { 4 + 5 + abstract public function renderAtom(DivinerAtom $atom); 6 + 7 + }
+19 -7
src/applications/diviner/workflow/DivinerAtomizeWorkflow.php
··· 9 9 ->setArguments( 10 10 array( 11 11 array( 12 - 'name' => 'atomizer', 13 - 'param' => 'class', 14 - 'help' => 'Specify a subclass of DivinerAtomizer.', 12 + 'name' => 'atomizer', 13 + 'param' => 'class', 14 + 'help' => pht('Specify a subclass of DivinerAtomizer.'), 15 + ), 16 + array( 17 + 'name' => 'book', 18 + 'param' => 'path', 19 + 'help' => pht('Path to a Diviner book configuration.'), 15 20 ), 16 21 array( 17 - 'name' => 'files', 18 - 'wildcard' => true, 22 + 'name' => 'files', 23 + 'wildcard' => true, 19 24 ), 20 25 array( 21 - 'name' => 'ugly', 22 - 'help' => 'Produce ugly (but faster) output.', 26 + 'name' => 'ugly', 27 + 'help' => pht('Produce ugly (but faster) output.'), 23 28 ), 24 29 )); 25 30 } 26 31 27 32 public function execute(PhutilArgumentParser $args) { 33 + $this->readBookConfiguration($args); 34 + 28 35 $console = PhutilConsole::getConsole(); 29 36 30 37 $atomizer_class = $args->getArg('atomizer'); ··· 81 88 } 82 89 83 90 $all_atoms = array_mergev($all_atoms); 91 + 92 + foreach ($all_atoms as $atom) { 93 + $atom->setBook($this->getConfig('name')); 94 + } 95 + 84 96 $all_atoms = mpull($all_atoms, 'toDictionary'); 85 97 $all_atoms = ipull($all_atoms, null, 'hash'); 86 98
+44 -44
src/applications/diviner/workflow/DivinerGenerateWorkflow.php
··· 2 2 3 3 final class DivinerGenerateWorkflow extends DivinerWorkflow { 4 4 5 - private $config; 6 5 private $atomCache; 7 6 8 7 public function didConstruct() { ··· 21 20 'help' => 'Path to a Diviner book configuration.', 22 21 ), 23 22 )); 24 - } 25 - 26 - protected function getConfig($key, $default = null) { 27 - return idx($this->config, $key, $default); 28 23 } 29 24 30 25 protected function getAtomCache() { ··· 49 44 if ($args->getArg('clean')) { 50 45 $this->log(pht('CLEARING CACHES')); 51 46 $this->getAtomCache()->delete(); 47 + $this->log(pht('Done.')."\n"); 52 48 } 53 49 54 50 // The major challenge of documentation generation is one of dependency ··· 134 130 135 131 $this->buildAtomCache(); 136 132 $this->buildGraphCache(); 133 + 134 + $this->publishDocumentation(); 137 135 } 138 136 139 137 /* -( Atom Cache )--------------------------------------------------------- */ ··· 167 165 168 166 $this->getAtomCache()->saveAtoms(); 169 167 170 - $this->log(pht("Done.")); 168 + $this->log(pht('Done.')."\n"); 171 169 } 172 170 173 171 private function getAtomizersForFiles(array $files) { ··· 252 250 foreach ($atomizers as $class => $files) { 253 251 foreach (array_chunk($files, 32) as $chunk) { 254 252 $future = new ExecFuture( 255 - '%s atomize --ugly --atomizer %s -- %Ls', 253 + '%s atomize --ugly --book %s --atomizer %s -- %Ls', 256 254 dirname(phutil_get_library_root('phabricator')).'/bin/diviner', 255 + $this->getBookConfigPath(), 257 256 $class, 258 257 $chunk); 259 258 $future->setCWD($this->getConfig('root')); ··· 352 351 353 352 $this->log(pht('Propagating changes through the graph.')); 354 353 355 - foreach ($dirty_symbols as $symbol => $ignored) { 356 - foreach ($atom_cache->getEdgesWithDestination($symbol) as $edge) { 354 + // Find all the nodes which point at a dirty node, and dirty them. Then 355 + // find all the nodes which point at those nodes and dirty them, and so 356 + // on. (This is slightly overkill since we probably don't need to propagate 357 + // dirtiness across documentation "links" between symbols, but we do want 358 + // to propagate it across "extends", and we suffer only a little bit of 359 + // collateral damage by over-dirtying as long as the documentation isn't 360 + // too well-connected.) 361 + 362 + $symbol_stack = array_keys($dirty_symbols); 363 + while ($symbol_stack) { 364 + $symbol_hash = array_pop($symbol_stack); 365 + 366 + foreach ($atom_cache->getEdgesWithDestination($symbol_hash) as $edge) { 357 367 $dirty_nhashes[$edge] = true; 368 + $src_hash = $this->computeSymbolHash($edge); 369 + if (empty($dirty_symbols[$src_hash])) { 370 + $dirty_symbols[$src_hash] = true; 371 + $symbol_stack[] = $src_hash; 372 + } 358 373 } 359 374 } 360 375 ··· 370 385 $atom_cache->saveEdges(); 371 386 $atom_cache->saveSymbols(); 372 387 373 - $this->log(pht('Done.')); 388 + $this->log(pht('Done.')."\n"); 374 389 } 375 390 376 391 private function computeSymbolHash($node_hash) { ··· 386 401 $atom = $atom_cache->getAtom($node_hash); 387 402 388 403 $refs = array(); 404 + 405 + // Make the atom depend on its own symbol, so that all atoms with the same 406 + // symbol are dirtied (e.g., if a codebase defines the function "f()" 407 + // several times, all of them should be dirtied when one is dirtied). 408 + $refs[DivinerAtomRef::newFromDictionary($atom)->toHash()] = true; 409 + 389 410 foreach (array_merge($atom['extends'], $atom['links']) as $ref_dict) { 390 411 $ref = DivinerAtomRef::newFromDictionary($ref_dict); 391 - if ($ref->getProject() == $atom['project']) { 412 + if ($ref->getBook() == $atom['book']) { 392 413 $refs[$ref->toHash()] = true; 393 414 } 394 415 } ··· 411 432 return md5(serialize($inputs)).'G'; 412 433 } 413 434 414 - private function readBookConfiguration(PhutilArgumentParser $args) { 415 - $book_path = $args->getArg('book'); 416 - if ($book_path === null) { 417 - throw new PhutilArgumentUsageException( 418 - "Specify a Diviner book configuration file with --book."); 419 - } 420 435 421 - $book_data = Filesystem::readFile($book_path); 422 - $book = json_decode($book_data, true); 423 - if (!is_array($book)) { 424 - throw new PhutilArgumentUsageException( 425 - "Book configuration '{$book_path}' is not in JSON format."); 426 - } 427 - 428 - // If the book specifies a "root", resolve it; otherwise, use the directory 429 - // the book configuration file lives in. 430 - $full_path = dirname(Filesystem::resolvePath($book_path)); 431 - if (empty($book['root'])) { 432 - $book['root'] = '.'; 433 - } 434 - $book['root'] = Filesystem::resolvePath($book['root'], $full_path); 436 + private function publishDocumentation() { 437 + $atom_cache = $this->getAtomCache(); 438 + $graph_map = $atom_cache->getGraphMap(); 435 439 436 - // Make sure we have a valid book name. 437 - if (!isset($book['name'])) { 438 - throw new PhutilArgumentUsageException( 439 - "Book configuration '{$book_path}' is missing required ". 440 - "property 'name'."); 441 - } 440 + $this->log(pht('PUBLISHING DOCUMENTATION')); 442 441 443 - if (!preg_match('/^[a-z][a-z-]*$/', $book['name'])) { 444 - $name = $book['name']; 445 - throw new PhutilArgumentUsageException( 446 - "Book configuration '{$book_path}' has name '{$name}', but book names ". 447 - "must include only lowercase letters and hyphens."); 448 - } 442 + $publisher = new DivinerStaticPublisher(); 443 + $publisher->setConfig($this->getAllConfig()); 444 + $publisher->setAtomCache($atom_cache); 445 + $publisher->setRenderer(new DivinerDefaultRenderer()); 446 + $publisher->publishAtoms(array_values($graph_map)); 449 447 450 - $this->config = $book; 448 + $this->log(pht('Done.')); 451 449 } 450 + 451 + 452 452 }
+55
src/applications/diviner/workflow/DivinerWorkflow.php
··· 2 2 3 3 abstract class DivinerWorkflow extends PhutilArgumentWorkflow { 4 4 5 + private $config; 6 + private $bookConfigPath; 7 + 8 + public function getBookConfigPath() { 9 + return $this->bookConfigPath; 10 + } 11 + 5 12 public function isExecutable() { 6 13 return true; 14 + } 15 + 16 + protected function getConfig($key, $default = null) { 17 + return idx($this->config, $key, $default); 18 + } 19 + 20 + protected function getAllConfig() { 21 + return $this->config; 22 + } 23 + 24 + protected function readBookConfiguration(PhutilArgumentParser $args) { 25 + $book_path = $args->getArg('book'); 26 + if ($book_path === null) { 27 + throw new PhutilArgumentUsageException( 28 + "Specify a Diviner book configuration file with --book."); 29 + } 30 + 31 + $book_data = Filesystem::readFile($book_path); 32 + $book = json_decode($book_data, true); 33 + if (!is_array($book)) { 34 + throw new PhutilArgumentUsageException( 35 + "Book configuration '{$book_path}' is not in JSON format."); 36 + } 37 + 38 + // If the book specifies a "root", resolve it; otherwise, use the directory 39 + // the book configuration file lives in. 40 + $full_path = dirname(Filesystem::resolvePath($book_path)); 41 + if (empty($book['root'])) { 42 + $book['root'] = '.'; 43 + } 44 + $book['root'] = Filesystem::resolvePath($book['root'], $full_path); 45 + 46 + // Make sure we have a valid book name. 47 + if (!isset($book['name'])) { 48 + throw new PhutilArgumentUsageException( 49 + "Book configuration '{$book_path}' is missing required ". 50 + "property 'name'."); 51 + } 52 + 53 + if (!preg_match('/^[a-z][a-z-]*$/', $book['name'])) { 54 + $name = $book['name']; 55 + throw new PhutilArgumentUsageException( 56 + "Book configuration '{$book_path}' has name '{$name}', but book names ". 57 + "must include only lowercase letters and hyphens."); 58 + } 59 + 60 + $this->bookConfigPath = $book_path; 61 + $this->config = $book; 7 62 } 8 63 9 64 }