@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

Add a "{src ...}" Remarkup rule to provide a more flexible way to reference source files in Diffusion

Summary: Depends on D20538. Ref T13291. We now recognize full source URIs, but encoding full URIs isn't super human-friendly and we can't do stuff like relative links with them. Add `{src ...}` as a way to get to this behavior that supports options and more flexible syntax.

Test Plan: {F6463607}

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13291

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

+224
+2
src/__phutil_library_map__.php
··· 1000 1000 'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php', 1001 1001 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', 1002 1002 'DiffusionSourceHyperlinkEngineExtension' => 'applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php', 1003 + 'DiffusionSourceLinkRemarkupRule' => 'applications/diffusion/remarkup/DiffusionSourceLinkRemarkupRule.php', 1003 1004 'DiffusionSourceLinkView' => 'applications/diffusion/view/DiffusionSourceLinkView.php', 1004 1005 'DiffusionSubversionCommandEngine' => 'applications/diffusion/protocol/DiffusionSubversionCommandEngine.php', 1005 1006 'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php', ··· 6685 6686 'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel', 6686 6687 'DiffusionSetupException' => 'Exception', 6687 6688 'DiffusionSourceHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension', 6689 + 'DiffusionSourceLinkRemarkupRule' => 'PhutilRemarkupRule', 6688 6690 'DiffusionSourceLinkView' => 'AphrontView', 6689 6691 'DiffusionSubversionCommandEngine' => 'DiffusionCommandEngine', 6690 6692 'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow',
+1
src/applications/diffusion/application/PhabricatorDiffusionApplication.php
··· 40 40 new DiffusionCommitRemarkupRule(), 41 41 new DiffusionRepositoryRemarkupRule(), 42 42 new DiffusionRepositoryByIDRemarkupRule(), 43 + new DiffusionSourceLinkRemarkupRule(), 43 44 ); 44 45 } 45 46
+221
src/applications/diffusion/remarkup/DiffusionSourceLinkRemarkupRule.php
··· 1 + <?php 2 + 3 + final class DiffusionSourceLinkRemarkupRule 4 + extends PhutilRemarkupRule { 5 + 6 + const KEY_SOURCELINKS = 'diffusion.links'; 7 + 8 + public function getPriority() { 9 + return 200.0; 10 + } 11 + 12 + public function apply($text) { 13 + return preg_replace_callback( 14 + '@{(?:src|source)\b((?:[^}\\\\]+|\\\\.)*)}@m', 15 + array($this, 'markupSourceLink'), 16 + $text); 17 + } 18 + 19 + public function markupSourceLink(array $matches) { 20 + $engine = $this->getEngine(); 21 + $text_mode = $engine->isTextMode(); 22 + $mail_mode = $engine->isHTMLMailMode(); 23 + 24 + if (!$this->isFlatText($matches[0]) || $text_mode || $mail_mode) { 25 + // We could do better than this in text mode and mail mode, but focus 26 + // on web mode first. 27 + return $matches[0]; 28 + } 29 + 30 + $metadata_key = self::KEY_SOURCELINKS; 31 + $metadata = $engine->getTextMetadata($metadata_key, array()); 32 + 33 + $token = $engine->storeText($matches[0]); 34 + 35 + $metadata[] = array( 36 + 'token' => $token, 37 + 'raw' => $matches[0], 38 + 'input' => $matches[1], 39 + ); 40 + 41 + $engine->setTextMetadata($metadata_key, $metadata); 42 + 43 + return $token; 44 + } 45 + 46 + public function didMarkupText() { 47 + $engine = $this->getEngine(); 48 + $metadata_key = self::KEY_SOURCELINKS; 49 + $metadata = $engine->getTextMetadata($metadata_key, array()); 50 + 51 + if (!$metadata) { 52 + return; 53 + } 54 + 55 + $viewer = $engine->getConfig('viewer'); 56 + if (!$viewer) { 57 + return; 58 + } 59 + 60 + $defaults = array( 61 + 'repository' => null, 62 + 'line' => null, 63 + 'commit' => null, 64 + 'ref' => null, 65 + ); 66 + 67 + $tags = array(); 68 + foreach ($metadata as $ref) { 69 + $token = $ref['token']; 70 + $raw = $ref['raw']; 71 + $input = $ref['input']; 72 + 73 + $pattern = 74 + '(^'. 75 + '[\s,]*'. 76 + '(?:"(?P<quotedpath>(?:[^\\\\"]+|\\.)+)"|(?P<rawpath>[^\s,]+))'. 77 + '[\s,]*'. 78 + '(?P<options>.*)'. 79 + '\z)'; 80 + $matches = null; 81 + if (!preg_match($pattern, $input, $matches)) { 82 + $hint_text = pht( 83 + 'Missing path, expected "{src path ...}" in: %s', 84 + $raw); 85 + $hint = $this->newSyntaxHint($hint_text); 86 + 87 + $engine->overwriteStoredText($token, $hint); 88 + continue; 89 + } 90 + 91 + $path = idx($matches, 'rawpath'); 92 + if (!strlen($path)) { 93 + $path = idx($matches, 'quotedpath'); 94 + $path = stripcslashes($path); 95 + } 96 + 97 + $parts = explode(':', $path, 2); 98 + if (count($parts) == 2) { 99 + $repository = nonempty($parts[0], null); 100 + $path = $parts[1]; 101 + } else { 102 + $repository = null; 103 + $path = $parts[0]; 104 + } 105 + 106 + $options = $matches['options']; 107 + 108 + $parser = new PhutilSimpleOptions(); 109 + $options = $parser->parse($options) + $defaults; 110 + 111 + foreach ($options as $key => $value) { 112 + if (!array_key_exists($key, $defaults)) { 113 + $hint_text = pht( 114 + 'Unknown option "%s" in: %s', 115 + $key, 116 + $raw); 117 + $hint = $this->newSyntaxHint($hint_text); 118 + 119 + $engine->overwriteStoredText($token, $hint); 120 + continue 2; 121 + } 122 + } 123 + 124 + if ($options['repository'] !== null) { 125 + $repository = $options['repository']; 126 + } 127 + 128 + if ($repository === null) { 129 + $hint_text = pht( 130 + 'Missing repository, expected "{src repository:path ...}" '. 131 + 'or "{src path repository=...}" in: %s', 132 + $raw); 133 + $hint = $this->newSyntaxHint($hint_text); 134 + 135 + $engine->overwriteStoredText($token, $hint); 136 + continue; 137 + } 138 + 139 + $tags[] = array( 140 + 'token' => $token, 141 + 'raw' => $raw, 142 + 'identifier' => $repository, 143 + 'path' => $path, 144 + 'options' => $options, 145 + ); 146 + } 147 + 148 + if (!$tags) { 149 + return; 150 + } 151 + 152 + $query = id(new PhabricatorRepositoryQuery()) 153 + ->setViewer($viewer) 154 + ->withIdentifiers(ipull($tags, 'identifier')); 155 + 156 + $query->execute(); 157 + 158 + $repository_map = $query->getIdentifierMap(); 159 + 160 + foreach ($tags as $tag) { 161 + $token = $tag['token']; 162 + 163 + $identifier = $tag['identifier']; 164 + $repository = idx($repository_map, $identifier); 165 + if (!$repository) { 166 + // For now, just bail out here. Ideally, we should distingiush between 167 + // restricted and invalid repositories. 168 + continue; 169 + } 170 + 171 + $drequest = DiffusionRequest::newFromDictionary( 172 + array( 173 + 'user' => $viewer, 174 + 'repository' => $repository, 175 + )); 176 + 177 + $options = $tag['options']; 178 + 179 + $line = $options['line']; 180 + $commit = $options['commit']; 181 + $ref_name = $options['ref']; 182 + 183 + $link_uri = $drequest->generateURI( 184 + array( 185 + 'action' => 'browse', 186 + 'path' => $tag['path'], 187 + 'commit' => $commit, 188 + 'line' => $line, 189 + 'branch' => $ref_name, 190 + )); 191 + 192 + $view = id(new DiffusionSourceLinkView()) 193 + ->setRepository($repository) 194 + ->setPath($tag['path']) 195 + ->setURI($link_uri); 196 + 197 + if ($line !== null) { 198 + $view->setLine($line); 199 + } 200 + 201 + if ($commit !== null) { 202 + $view->setCommit($commit); 203 + } 204 + 205 + if ($ref_name !== null) { 206 + $view->setRefName($ref_name); 207 + } 208 + 209 + $engine->overwriteStoredText($token, $view); 210 + } 211 + } 212 + 213 + private function newSyntaxHint($text) { 214 + return id(new PHUITagView()) 215 + ->setType(PHUITagView::TYPE_SHADE) 216 + ->setColor('red') 217 + ->setIcon('fa-exclamation-triangle') 218 + ->setName($text); 219 + } 220 + 221 + }