diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php index be5f6bae3c..7747a73866 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php @@ -1,330 +1,331 @@ setArguments( array( array( 'name' => 'client-host', 'param' => 'hostname', 'help' => pht('Hostname to bind to for the client server.'), ), array( 'name' => 'client-port', 'param' => 'port', 'help' => pht('Port to bind to for the client server.'), ), )); } public function execute(PhutilArgumentParser $args) { $this->clientHost = $args->getArg('client-host'); $this->clientPort = $args->getArg('client-port'); return 0; } final public function getPIDPath() { $path = PhabricatorEnv::getEnvConfig('notification.pidfile'); try { $dir = dirname($path); if (!Filesystem::pathExists($dir)) { Filesystem::createDirectory($dir, 0755, true); } } catch (FilesystemException $ex) { throw new Exception( pht( "Failed to create '%s'. You should manually create this directory.", $dir)); } return $path; } final public function getLogPath() { $path = PhabricatorEnv::getEnvConfig('notification.log'); try { $dir = dirname($path); if (!Filesystem::pathExists($dir)) { Filesystem::createDirectory($dir, 0755, true); } } catch (FilesystemException $ex) { throw new Exception( pht( "Failed to create '%s'. You should manually create this directory.", $dir)); } return $path; } final public function getPID() { $pid = null; if (Filesystem::pathExists($this->getPIDPath())) { $pid = (int)Filesystem::readFile($this->getPIDPath()); } return $pid; } final public function cleanup($signo = '?') { global $g_future; if ($g_future) { $g_future->resolveKill(); $g_future = null; } Filesystem::remove($this->getPIDPath()); exit(1); } final protected function setDebug($debug) { $this->debug = $debug; + return $this; } public static function requireExtensions() { self::mustHaveExtension('pcntl'); self::mustHaveExtension('posix'); } private static function mustHaveExtension($ext) { if (!extension_loaded($ext)) { echo pht( "ERROR: The PHP extension '%s' is not installed. You must ". "install it to run Aphlict on this machine.", $ext)."\n"; exit(1); } $extension = new ReflectionExtension($ext); foreach ($extension->getFunctions() as $function) { $function = $function->name; if (!function_exists($function)) { echo pht( 'ERROR: The PHP function %s is disabled. You must '. 'enable it to run Aphlict on this machine.', $function.'()')."\n"; exit(1); } } } final protected function willLaunch() { $console = PhutilConsole::getConsole(); $pid = $this->getPID(); if ($pid) { throw new PhutilArgumentUsageException( pht( 'Unable to start notifications server because it is already '. 'running. Use `%s` to restart it.', 'aphlict restart')); } if (posix_getuid() == 0) { throw new PhutilArgumentUsageException( pht( // TODO: Update this message after a while. 'The notification server should not be run as root. It no '. 'longer requires access to privileged ports.')); } // Make sure we can write to the PID file. if (!$this->debug) { Filesystem::writeFile($this->getPIDPath(), ''); } // First, start the server in configuration test mode with --test. This // will let us error explicitly if there are missing modules, before we // fork and lose access to the console. $test_argv = $this->getServerArgv(); $test_argv[] = '--test=true'; execx( '%s %s %Ls', $this->getNodeBinary(), $this->getAphlictScriptPath(), $test_argv); } private function getServerArgv() { $ssl_key = PhabricatorEnv::getEnvConfig('notification.ssl-key'); $ssl_cert = PhabricatorEnv::getEnvConfig('notification.ssl-cert'); $server_uri = PhabricatorEnv::getEnvConfig('notification.server-uri'); $server_uri = new PhutilURI($server_uri); $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri'); $client_uri = new PhutilURI($client_uri); $log = $this->getLogPath(); $server_argv = array(); $server_argv[] = '--client-port='.coalesce( $this->clientPort, $client_uri->getPort()); $server_argv[] = '--admin-port='.$server_uri->getPort(); $server_argv[] = '--admin-host='.$server_uri->getDomain(); if ($ssl_key) { $server_argv[] = '--ssl-key='.$ssl_key; } if ($ssl_cert) { $server_argv[] = '--ssl-cert='.$ssl_cert; } $server_argv[] = '--log='.$log; if ($this->clientHost) { $server_argv[] = '--client-host='.$this->clientHost; } return $server_argv; } private function getAphlictScriptPath() { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/support/aphlict/server/aphlict_server.js'; } final protected function launch() { $console = PhutilConsole::getConsole(); if ($this->debug) { $console->writeOut( "%s\n", pht('Starting Aphlict server in foreground...')); } else { Filesystem::writeFile($this->getPIDPath(), getmypid()); } $command = csprintf( '%s %s %Ls', $this->getNodeBinary(), $this->getAphlictScriptPath(), $this->getServerArgv()); if (!$this->debug) { declare(ticks = 1); pcntl_signal(SIGINT, array($this, 'cleanup')); pcntl_signal(SIGTERM, array($this, 'cleanup')); } register_shutdown_function(array($this, 'cleanup')); if ($this->debug) { $console->writeOut( "%s\n\n $ %s\n\n", pht('Launching server:'), $command); $err = phutil_passthru('%C', $command); $console->writeOut(">>> %s\n", pht('Server exited!')); exit($err); } else { while (true) { global $g_future; $g_future = new ExecFuture('exec %C', $command); $g_future->resolve(); // If the server exited, wait a couple of seconds and restart it. unset($g_future); sleep(2); } } } /* -( Commands )----------------------------------------------------------- */ final protected function executeStartCommand() { $console = PhutilConsole::getConsole(); $this->willLaunch(); $pid = pcntl_fork(); if ($pid < 0) { throw new Exception( pht( 'Failed to %s!', 'fork()')); } else if ($pid) { $console->writeErr("%s\n", pht('Aphlict Server started.')); exit(0); } // When we fork, the child process will inherit its parent's set of open // file descriptors. If the parent process of bin/aphlict is waiting for // bin/aphlict's file descriptors to close, it will be stuck waiting on // the daemonized process. (This happens if e.g. bin/aphlict is started // in another script using passthru().) fclose(STDOUT); fclose(STDERR); $this->launch(); return 0; } final protected function executeStopCommand() { $console = PhutilConsole::getConsole(); $pid = $this->getPID(); if (!$pid) { $console->writeErr("%s\n", pht('Aphlict is not running.')); return 0; } $console->writeErr("%s\n", pht('Stopping Aphlict Server (%s)...', $pid)); posix_kill($pid, SIGINT); $start = time(); do { if (!PhabricatorDaemonReference::isProcessRunning($pid)) { $console->writeOut( "%s\n", pht('Aphlict Server (%s) exited normally.', $pid)); $pid = null; break; } usleep(100000); } while (time() < $start + 5); if ($pid) { $console->writeErr("%s\n", pht('Sending %s a SIGKILL.', $pid)); posix_kill($pid, SIGKILL); unset($pid); } Filesystem::remove($this->getPIDPath()); return 0; } private function getNodeBinary() { if (Filesystem::binaryExists('nodejs')) { return 'nodejs'; } if (Filesystem::binaryExists('node')) { return 'node'; } throw new PhutilArgumentUsageException( pht( 'No `%s` or `%s` binary was found in %s. You must install '. 'Node.js to start the Aphlict server.', 'nodejs', 'node', '$PATH')); } } diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index 6b5ea1c0de..b5e4545d85 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -1,1571 +1,1572 @@ rangeStart = $start; $this->rangeEnd = $end; return $this; } public function setMask(array $mask) { $this->mask = $mask; return $this; } public function renderChangeset() { return $this->render($this->rangeStart, $this->rangeEnd, $this->mask); } public function setShowEditAndReplyLinks($bool) { $this->showEditAndReplyLinks = $bool; return $this; } public function getShowEditAndReplyLinks() { return $this->showEditAndReplyLinks; } public function setHighlightAs($highlight_as) { $this->highlightAs = $highlight_as; return $this; } public function getHighlightAs() { return $this->highlightAs; } public function setCharacterEncoding($character_encoding) { $this->characterEncoding = $character_encoding; return $this; } public function getCharacterEncoding() { return $this->characterEncoding; } public function setRenderer(DifferentialChangesetRenderer $renderer) { $this->renderer = $renderer; return $this; } public function getRenderer() { if (!$this->renderer) { return new DifferentialChangesetTwoUpRenderer(); } return $this->renderer; } public function setDisableCache($disable_cache) { $this->disableCache = $disable_cache; return $this; } public function getDisableCache() { return $this->disableCache; } public function setCanMarkDone($can_mark_done) { $this->canMarkDone = $can_mark_done; return $this; } public function getCanMarkDone() { return $this->canMarkDone; } public function setObjectOwnerPHID($phid) { $this->objectOwnerPHID = $phid; return $this; } public function getObjectOwnerPHID() { return $this->objectOwnerPHID; } public static function getDefaultRendererForViewer(PhabricatorUser $viewer) { $prefs = $viewer->loadPreferences(); $pref_unified = PhabricatorUserPreferences::PREFERENCE_DIFF_UNIFIED; if ($prefs->getPreference($pref_unified) == 'unified') { return '1up'; } return null; } public function readParametersFromRequest(AphrontRequest $request) { $this->setWhitespaceMode($request->getStr('whitespace')); $this->setCharacterEncoding($request->getStr('encoding')); $this->setHighlightAs($request->getStr('highlight')); $renderer = null; // If the viewer prefers unified diffs, always set the renderer to unified. // Otherwise, we leave it unspecified and the client will choose a // renderer based on the screen size. if ($request->getStr('renderer')) { $renderer = $request->getStr('renderer'); } else { $renderer = self::getDefaultRendererForViewer($request->getViewer()); } switch ($renderer) { case '1up': $this->setRenderer(new DifferentialChangesetOneUpRenderer()); break; default: $this->setRenderer(new DifferentialChangesetTwoUpRenderer()); break; } return $this; } const CACHE_VERSION = 11; const CACHE_MAX_SIZE = 8e6; const ATTR_GENERATED = 'attr:generated'; const ATTR_DELETED = 'attr:deleted'; const ATTR_UNCHANGED = 'attr:unchanged'; const ATTR_WHITELINES = 'attr:white'; const ATTR_MOVEAWAY = 'attr:moveaway'; const LINES_CONTEXT = 8; const WHITESPACE_SHOW_ALL = 'show-all'; const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing'; const WHITESPACE_IGNORE_MOST = 'ignore-most'; const WHITESPACE_IGNORE_ALL = 'ignore-all'; public function setOldLines(array $lines) { $this->old = $lines; return $this; } public function setNewLines(array $lines) { $this->new = $lines; return $this; } public function setSpecialAttributes(array $attributes) { $this->specialAttributes = $attributes; return $this; } public function setIntraLineDiffs(array $diffs) { $this->intra = $diffs; return $this; } public function setVisibileLinesMask(array $mask) { $this->visible = $mask; return $this; } /** * Configure which Changeset comments added to the right side of the visible * diff will be attached to. The ID must be the ID of a real Differential * Changeset. * * The complexity here is that we may show an arbitrary side of an arbitrary * changeset as either the left or right part of a diff. This method allows * the left and right halves of the displayed diff to be correctly mapped to * storage changesets. * * @param id The Differential Changeset ID that comments added to the right * side of the visible diff should be attached to. * @param bool If true, attach new comments to the right side of the storage * changeset. Note that this may be false, if the left side of * some storage changeset is being shown as the right side of * a display diff. * @return this */ public function setRightSideCommentMapping($id, $is_new) { $this->rightSideChangesetID = $id; $this->rightSideAttachesToNewFile = $is_new; return $this; } /** * See setRightSideCommentMapping(), but this sets information for the left * side of the display diff. */ public function setLeftSideCommentMapping($id, $is_new) { $this->leftSideChangesetID = $id; $this->leftSideAttachesToNewFile = $is_new; return $this; } public function setOriginals( DifferentialChangeset $left, DifferentialChangeset $right) { $this->originalLeft = $left; $this->originalRight = $right; + return $this; } public function diffOriginals() { $engine = new PhabricatorDifferenceEngine(); $changeset = $engine->generateChangesetFromFileContent( implode('', mpull($this->originalLeft->getHunks(), 'getChanges')), implode('', mpull($this->originalRight->getHunks(), 'getChanges'))); $parser = new DifferentialHunkParser(); return $parser->parseHunksForHighlightMasks( $changeset->getHunks(), $this->originalLeft->getHunks(), $this->originalRight->getHunks()); } /** * Set a key for identifying this changeset in the render cache. If set, the * parser will attempt to use the changeset render cache, which can improve * performance for frequently-viewed changesets. * * By default, there is no render cache key and parsers do not use the cache. * This is appropriate for rarely-viewed changesets. * * NOTE: Currently, this key must be a valid Differential Changeset ID. * * @param string Key for identifying this changeset in the render cache. * @return this */ public function setRenderCacheKey($key) { $this->renderCacheKey = $key; return $this; } private function getRenderCacheKey() { return $this->renderCacheKey; } public function setChangeset(DifferentialChangeset $changeset) { $this->changeset = $changeset; $this->setFilename($changeset->getFilename()); return $this; } public function setWhitespaceMode($whitespace_mode) { $this->whitespaceMode = $whitespace_mode; return $this; } public function setRenderingReference($ref) { $this->renderingReference = $ref; return $this; } private function getRenderingReference() { return $this->renderingReference; } public function getChangeset() { return $this->changeset; } public function setFilename($filename) { $this->filename = $filename; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } public function setCoverage($coverage) { $this->coverage = $coverage; return $this; } private function getCoverage() { return $this->coverage; } public function parseInlineComment( PhabricatorInlineCommentInterface $comment) { // Parse only comments which are actually visible. if ($this->isCommentVisibleOnRenderedDiff($comment)) { $this->comments[] = $comment; } return $this; } private function loadCache() { $render_cache_key = $this->getRenderCacheKey(); if (!$render_cache_key) { return false; } $data = null; $changeset = new DifferentialChangeset(); $conn_r = $changeset->establishConnection('r'); $data = queryfx_one( $conn_r, 'SELECT * FROM %T WHERE id = %d', $changeset->getTableName().'_parse_cache', $render_cache_key); if (!$data) { return false; } if ($data['cache'][0] == '{') { // This is likely an old-style JSON cache which we will not be able to // deserialize. return false; } $data = unserialize($data['cache']); if (!is_array($data) || !$data) { return false; } foreach (self::getCacheableProperties() as $cache_key) { if (!array_key_exists($cache_key, $data)) { // If we're missing a cache key, assume we're looking at an old cache // and ignore it. return false; } } if ($data['cacheVersion'] !== self::CACHE_VERSION) { return false; } // Someone displays contents of a partially cached shielded file. if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) { return false; } unset($data['cacheVersion'], $data['cacheHost']); $cache_prop = array_select_keys($data, self::getCacheableProperties()); foreach ($cache_prop as $cache_key => $v) { $this->$cache_key = $v; } return true; } protected static function getCacheableProperties() { return array( 'visible', 'new', 'old', 'intra', 'newRender', 'oldRender', 'specialAttributes', 'hunkStartLines', 'cacheVersion', 'cacheHost', 'highlightingDisabled', ); } public function saveCache() { if ($this->highlightErrors) { return false; } $render_cache_key = $this->getRenderCacheKey(); if (!$render_cache_key) { return false; } $cache = array(); foreach (self::getCacheableProperties() as $cache_key) { switch ($cache_key) { case 'cacheVersion': $cache[$cache_key] = self::CACHE_VERSION; break; case 'cacheHost': $cache[$cache_key] = php_uname('n'); break; default: $cache[$cache_key] = $this->$cache_key; break; } } $cache = serialize($cache); // We don't want to waste too much space by a single changeset. if (strlen($cache) > self::CACHE_MAX_SIZE) { return; } $changeset = new DifferentialChangeset(); $conn_w = $changeset->establishConnection('w'); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { queryfx( $conn_w, 'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %B, %d) ON DUPLICATE KEY UPDATE cache = VALUES(cache)', DifferentialChangeset::TABLE_CACHE, $render_cache_key, $cache, time()); } catch (AphrontQueryException $ex) { // Ignore these exceptions. A common cause is that the cache is // larger than 'max_allowed_packet', in which case we're better off // not writing it. // TODO: It would be nice to tailor this more narrowly. } unset($unguarded); } private function markGenerated($new_corpus_block = '') { $generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false); if (!$generated_guess) { $generated_path_regexps = PhabricatorEnv::getEnvConfig( 'differential.generated-paths'); foreach ($generated_path_regexps as $regexp) { if (preg_match($regexp, $this->changeset->getFilename())) { $generated_guess = true; break; } } } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED, array( 'corpus' => $new_corpus_block, 'is_generated' => $generated_guess, ) ); PhutilEventEngine::dispatchEvent($event); $generated = $event->getValue('is_generated'); $this->specialAttributes[self::ATTR_GENERATED] = $generated; } public function isGenerated() { return idx($this->specialAttributes, self::ATTR_GENERATED, false); } public function isDeleted() { return idx($this->specialAttributes, self::ATTR_DELETED, false); } public function isUnchanged() { return idx($this->specialAttributes, self::ATTR_UNCHANGED, false); } public function isWhitespaceOnly() { return idx($this->specialAttributes, self::ATTR_WHITELINES, false); } public function isMoveAway() { return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false); } private function applyIntraline(&$render, $intra, $corpus) { foreach ($render as $key => $text) { if (isset($intra[$key])) { $render[$key] = ArcanistDiffUtils::applyIntralineDiff( $text, $intra[$key]); } } } private function getHighlightFuture($corpus) { $language = $this->highlightAs; if (!$language) { $language = $this->highlightEngine->getLanguageFromFilename( $this->filename); if (($language != 'txt') && (strlen($corpus) > self::HIGHLIGHT_BYTE_LIMIT)) { $this->highlightingDisabled = true; $language = 'txt'; } } return $this->highlightEngine->getHighlightFuture( $language, $corpus); } protected function processHighlightedSource($data, $result) { $result_lines = phutil_split_lines($result); foreach ($data as $key => $info) { if (!$info) { unset($result_lines[$key]); } } return $result_lines; } private function tryCacheStuff() { $whitespace_mode = $this->whitespaceMode; switch ($whitespace_mode) { case self::WHITESPACE_SHOW_ALL: case self::WHITESPACE_IGNORE_TRAILING: case self::WHITESPACE_IGNORE_ALL: break; default: $whitespace_mode = self::WHITESPACE_IGNORE_MOST; break; } $skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_MOST); if ($this->disableCache) { $skip_cache = true; } if ($this->characterEncoding) { $skip_cache = true; } if ($this->highlightAs) { $skip_cache = true; } $this->whitespaceMode = $whitespace_mode; $changeset = $this->changeset; if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT && $changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) { $this->markGenerated(); } else { if ($skip_cache || !$this->loadCache()) { $this->process(); if (!$skip_cache) { $this->saveCache(); } } } } private function process() { $whitespace_mode = $this->whitespaceMode; $changeset = $this->changeset; $ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_MOST) || ($whitespace_mode == self::WHITESPACE_IGNORE_ALL)); $force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_ALL); if (!$force_ignore) { if ($ignore_all && $changeset->getWhitespaceMatters()) { $ignore_all = false; } } // The "ignore all whitespace" algorithm depends on rediffing the // files, and we currently need complete representations of both // files to do anything reasonable. If we only have parts of the files, // don't use the "ignore all" algorithm. if ($ignore_all) { $hunks = $changeset->getHunks(); if (count($hunks) !== 1) { $ignore_all = false; } else { $first_hunk = reset($hunks); if ($first_hunk->getOldOffset() != 1 || $first_hunk->getNewOffset() != 1) { $ignore_all = false; } } } if ($ignore_all) { $old_file = $changeset->makeOldFile(); $new_file = $changeset->makeNewFile(); if ($old_file == $new_file) { // If the old and new files are exactly identical, the synthetic // diff below will give us nonsense and whitespace modes are // irrelevant anyway. This occurs when you, e.g., copy a file onto // itself in Subversion (see T271). $ignore_all = false; } } $hunk_parser = new DifferentialHunkParser(); $hunk_parser->setWhitespaceMode($whitespace_mode); $hunk_parser->parseHunksForLineData($changeset->getHunks()); // Depending on the whitespace mode, we may need to compute a different // set of changes than the set of changes in the hunk data (specificaly, // we might want to consider changed lines which have only whitespace // changes as unchanged). if ($ignore_all) { $engine = new PhabricatorDifferenceEngine(); $engine->setIgnoreWhitespace(true); $no_whitespace_changeset = $engine->generateChangesetFromFileContent( $old_file, $new_file); $type_parser = new DifferentialHunkParser(); $type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks()); $hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap()); $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap()); } $hunk_parser->reparseHunksForSpecialAttributes(); $unchanged = false; if (!$hunk_parser->getHasAnyChanges()) { $filetype = $this->changeset->getFileType(); if ($filetype == DifferentialChangeType::FILE_TEXT || $filetype == DifferentialChangeType::FILE_SYMLINK) { $unchanged = true; } } $moveaway = false; $changetype = $this->changeset->getChangeType(); if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) { $moveaway = true; } $this->setSpecialAttributes(array( self::ATTR_UNCHANGED => $unchanged, self::ATTR_DELETED => $hunk_parser->getIsDeleted(), self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(), self::ATTR_MOVEAWAY => $moveaway, )); $hunk_parser->generateIntraLineDiffs(); $hunk_parser->generateVisibileLinesMask(); $this->setOldLines($hunk_parser->getOldLines()); $this->setNewLines($hunk_parser->getNewLines()); $this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs()); $this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask()); $this->hunkStartLines = $hunk_parser->getHunkStartLines( $changeset->getHunks()); $new_corpus = $hunk_parser->getNewCorpus(); $new_corpus_block = implode('', $new_corpus); $this->markGenerated($new_corpus_block); if ($this->isTopLevel && !$this->comments && ($this->isGenerated() || $this->isUnchanged() || $this->isDeleted())) { return; } $old_corpus = $hunk_parser->getOldCorpus(); $old_corpus_block = implode('', $old_corpus); $old_future = $this->getHighlightFuture($old_corpus_block); $new_future = $this->getHighlightFuture($new_corpus_block); $futures = array( 'old' => $old_future, 'new' => $new_future, ); $corpus_blocks = array( 'old' => $old_corpus_block, 'new' => $new_corpus_block, ); $this->highlightErrors = false; foreach (new FutureIterator($futures) as $key => $future) { try { try { $highlighted = $future->resolve(); } catch (PhutilSyntaxHighlighterException $ex) { $this->highlightErrors = true; $highlighted = id(new PhutilDefaultSyntaxHighlighter()) ->getHighlightFuture($corpus_blocks[$key]) ->resolve(); } switch ($key) { case 'old': $this->oldRender = $this->processHighlightedSource( $this->old, $highlighted); break; case 'new': $this->newRender = $this->processHighlightedSource( $this->new, $highlighted); break; } } catch (Exception $ex) { phlog($ex); throw $ex; } } $this->applyIntraline( $this->oldRender, ipull($this->intra, 0), $old_corpus); $this->applyIntraline( $this->newRender, ipull($this->intra, 1), $new_corpus); } private function shouldRenderPropertyChangeHeader($changeset) { if (!$this->isTopLevel) { // We render properties only at top level; otherwise we get multiple // copies of them when a user clicks "Show More". return false; } return true; } public function render( $range_start = null, $range_len = null, $mask_force = array()) { // "Top level" renders are initial requests for the whole file, versus // requests for a specific range generated by clicking "show more". We // generate property changes and "shield" UI elements only for toplevel // requests. $this->isTopLevel = (($range_start === null) && ($range_len === null)); $this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine(); $encoding = null; if ($this->characterEncoding) { // We are forcing this changeset to be interpreted with a specific // character encoding, so force all the hunks into that encoding and // propagate it to the renderer. $encoding = $this->characterEncoding; foreach ($this->changeset->getHunks() as $hunk) { $hunk->forceEncoding($this->characterEncoding); } } else { // We're just using the default, so tell the renderer what that is // (by reading the encoding from the first hunk). foreach ($this->changeset->getHunks() as $hunk) { $encoding = $hunk->getDataEncoding(); break; } } $this->tryCacheStuff(); $render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset); $rows = max( count($this->old), count($this->new)); $renderer = $this->getRenderer() ->setUser($this->getUser()) ->setChangeset($this->changeset) ->setRenderPropertyChangeHeader($render_pch) ->setIsTopLevel($this->isTopLevel) ->setOldRender($this->oldRender) ->setNewRender($this->newRender) ->setHunkStartLines($this->hunkStartLines) ->setOldChangesetID($this->leftSideChangesetID) ->setNewChangesetID($this->rightSideChangesetID) ->setOldAttachesToNewFile($this->leftSideAttachesToNewFile) ->setNewAttachesToNewFile($this->rightSideAttachesToNewFile) ->setCodeCoverage($this->getCoverage()) ->setRenderingReference($this->getRenderingReference()) ->setMarkupEngine($this->markupEngine) ->setHandles($this->handles) ->setOldLines($this->old) ->setNewLines($this->new) ->setOriginalCharacterEncoding($encoding) ->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks()) ->setCanMarkDone($this->getCanMarkDone()) ->setObjectOwnerPHID($this->getObjectOwnerPHID()) ->setHighlightingDisabled($this->highlightingDisabled); $shield = null; if ($this->isTopLevel && !$this->comments) { if ($this->isGenerated()) { $shield = $renderer->renderShield( pht( 'This file contains generated code, which does not normally '. 'need to be reviewed.')); } else if ($this->isMoveAway()) { // We put an empty shield on these files. Normally, they do not have // any diff content anyway. However, if they come through `arc`, they // may have content. We don't want to show it (it's not useful) and // we bailed out of fully processing it earlier anyway. // We could show a message like "this file was moved", but we show // that as a change header anyway, so it would be redundant. Instead, // just render an empty shield to skip rendering the diff body. $shield = ''; } else if ($this->isUnchanged()) { $type = 'text'; if (!$rows) { // NOTE: Normally, diffs which don't change files do not include // file content (for example, if you "chmod +x" a file and then // run "git show", the file content is not available). Similarly, // if you move a file from A to B without changing it, diffs normally // do not show the file content. In some cases `arc` is able to // synthetically generate content for these diffs, but for raw diffs // we'll never have it so we need to be prepared to not render a link. $type = 'none'; } $type_add = DifferentialChangeType::TYPE_ADD; if ($this->changeset->getChangeType() == $type_add) { // Although the generic message is sort of accurate in a technical // sense, this more-tailored message is less confusing. $shield = $renderer->renderShield( pht('This is an empty file.'), $type); } else { $shield = $renderer->renderShield( pht('The contents of this file were not changed.'), $type); } } else if ($this->isWhitespaceOnly()) { $shield = $renderer->renderShield( pht('This file was changed only by adding or removing whitespace.'), 'whitespace'); } else if ($this->isDeleted()) { $shield = $renderer->renderShield( pht('This file was completely deleted.')); } else if ($this->changeset->getAffectedLineCount() > 2500) { $shield = $renderer->renderShield( pht( 'This file has a very large number of changes (%s lines).', new PhutilNumber($this->changeset->getAffectedLineCount()))); } } if ($shield !== null) { return $renderer->renderChangesetTable($shield); } // This request should render the "undershield" headers if it's a top-level // request which made it this far (indicating the changeset has no shield) // or it's a request with no mask information (indicating it's the request // that removes the rendering shield). Possibly, this second class of // request might need to be made more explicit. $is_undershield = (empty($mask_force) || $this->isTopLevel); $renderer->setIsUndershield($is_undershield); $old_comments = array(); $new_comments = array(); $old_mask = array(); $new_mask = array(); $feedback_mask = array(); if ($this->comments) { // If there are any comments which appear in sections of the file which // we don't have, we're going to move them backwards to the closest // earlier line. Two cases where this may happen are: // // - Porting ghost comments forward into a file which was mostly // deleted. // - Porting ghost comments forward from a full-context diff to a // partial-context diff. list($old_backmap, $new_backmap) = $this->buildLineBackmaps(); foreach ($this->comments as $comment) { $new_side = $this->isCommentOnRightSideWhenDisplayed($comment); $line = $comment->getLineNumber(); if ($new_side) { $back_line = $new_backmap[$line]; } else { $back_line = $old_backmap[$line]; } if ($back_line != $line) { // TODO: This should probably be cleaner, but just be simple and // obvious for now. $ghost = $comment->getIsGhost(); if ($ghost) { $moved = pht( 'This comment originally appeared on line %s, but that line '. 'does not exist in this version of the diff. It has been '. 'moved backward to the nearest line.', new PhutilNumber($line)); $ghost['reason'] = $ghost['reason']."\n\n".$moved; $comment->setIsGhost($ghost); } $comment->setLineNumber($back_line); $comment->setLineLength(0); } $start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0); $end = $comment->getLineNumber() + $comment->getLineLength() + self::LINES_CONTEXT; for ($ii = $start; $ii <= $end; $ii++) { if ($new_side) { $new_mask[$ii] = true; } else { $old_mask[$ii] = true; } } } foreach ($this->old as $ii => $old) { if (isset($old['line']) && isset($old_mask[$old['line']])) { $feedback_mask[$ii] = true; } } foreach ($this->new as $ii => $new) { if (isset($new['line']) && isset($new_mask[$new['line']])) { $feedback_mask[$ii] = true; } } $this->comments = msort($this->comments, 'getID'); foreach ($this->comments as $comment) { $final = $comment->getLineNumber() + $comment->getLineLength(); $final = max(1, $final); if ($this->isCommentOnRightSideWhenDisplayed($comment)) { $new_comments[$final][] = $comment; } else { $old_comments[$final][] = $comment; } } } $renderer ->setOldComments($old_comments) ->setNewComments($new_comments); switch ($this->changeset->getFileType()) { case DifferentialChangeType::FILE_IMAGE: $old = null; $new = null; // TODO: Improve the architectural issue as discussed in D955 // https://secure.phabricator.com/D955 $reference = $this->getRenderingReference(); $parts = explode('/', $reference); if (count($parts) == 2) { list($id, $vs) = $parts; } else { $id = $parts[0]; $vs = 0; } $id = (int)$id; $vs = (int)$vs; if (!$vs) { $metadata = $this->changeset->getMetadata(); $data = idx($metadata, 'attachment-data'); $old_phid = idx($metadata, 'old:binary-phid'); $new_phid = idx($metadata, 'new:binary-phid'); } else { $vs_changeset = id(new DifferentialChangeset())->load($vs); $old_phid = null; $new_phid = null; // TODO: This is spooky, see D6851 if ($vs_changeset) { $vs_metadata = $vs_changeset->getMetadata(); $old_phid = idx($vs_metadata, 'new:binary-phid'); } $changeset = id(new DifferentialChangeset())->load($id); if ($changeset) { $metadata = $changeset->getMetadata(); $new_phid = idx($metadata, 'new:binary-phid'); } } if ($old_phid || $new_phid) { // grab the files, (micro) optimization for 1 query not 2 $file_phids = array(); if ($old_phid) { $file_phids[] = $old_phid; } if ($new_phid) { $file_phids[] = $new_phid; } $files = id(new PhabricatorFileQuery()) ->setViewer($this->getUser()) ->withPHIDs($file_phids) ->execute(); foreach ($files as $file) { if (empty($file)) { continue; } if ($file->getPHID() == $old_phid) { $old = $file; } else if ($file->getPHID() == $new_phid) { $new = $file; } } } $renderer->attachOldFile($old); $renderer->attachNewFile($new); return $renderer->renderFileChange($old, $new, $id, $vs); case DifferentialChangeType::FILE_DIRECTORY: case DifferentialChangeType::FILE_BINARY: $output = $renderer->renderChangesetTable(null); return $output; } if ($this->originalLeft && $this->originalRight) { list($highlight_old, $highlight_new) = $this->diffOriginals(); $highlight_old = array_flip($highlight_old); $highlight_new = array_flip($highlight_new); $renderer ->setHighlightOld($highlight_old) ->setHighlightNew($highlight_new); } $renderer ->setOriginalOld($this->originalLeft) ->setOriginalNew($this->originalRight); if ($range_start === null) { $range_start = 0; } if ($range_len === null) { $range_len = $rows; } $range_len = min($range_len, $rows - $range_start); list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths( $mask_force, $feedback_mask, $range_start, $range_len); $renderer ->setGaps($gaps) ->setMask($mask) ->setDepths($depths); $html = $renderer->renderTextChange( $range_start, $range_len, $rows); return $renderer->renderChangesetTable($html); } /** * This function calculates a lot of stuff we need to know to display * the diff: * * Gaps - compute gaps in the visible display diff, where we will render * "Show more context" spacers. If a gap is smaller than the context size, * we just display it. Otherwise, we record it into $gaps and will render a * "show more context" element instead of diff text below. A given $gap * is a tuple of $gap_line_number_start and $gap_length. * * Mask - compute the actual lines that need to be shown (because they * are near changes lines, near inline comments, or the request has * explicitly asked for them, i.e. resulting from the user clicking * "show more"). The $mask returned is a sparesely populated dictionary * of $visible_line_number => true. * * Depths - compute how indented any given line is. The $depths returned * is a sparesely populated dictionary of $visible_line_number => $depth. * * This function also has the side effect of modifying member variable * new such that tabs are normalized to spaces for each line of the diff. * * @return array($gaps, $mask, $depths) */ private function calculateGapsMaskAndDepths( $mask_force, $feedback_mask, $range_start, $range_len) { // Calculate gaps and mask first $gaps = array(); $gap_start = 0; $in_gap = false; $base_mask = $this->visible + $mask_force + $feedback_mask; $base_mask[$range_start + $range_len] = true; for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) { if (isset($base_mask[$ii])) { if ($in_gap) { $gap_length = $ii - $gap_start; if ($gap_length <= self::LINES_CONTEXT) { for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) { $base_mask[$jj] = true; } } else { $gaps[] = array($gap_start, $gap_length); } $in_gap = false; } } else { if (!$in_gap) { $gap_start = $ii; $in_gap = true; } } } $gaps = array_reverse($gaps); $mask = $base_mask; // Time to calculate depth. // We need to go backwards to properly indent whitespace in this code: // // 0: class C { // 1: // 1: function f() { // 2: // 2: return; // 1: // 1: } // 0: // 0: } // $depths = array(); $last_depth = 0; $range_end = $range_start + $range_len; if (!isset($this->new[$range_end])) { $range_end--; } for ($ii = $range_end; $ii >= $range_start; $ii--) { // We need to expand tabs to process mixed indenting and to round // correctly later. $line = str_replace("\t", ' ', $this->new[$ii]['text']); $trimmed = ltrim($line); if ($trimmed != '') { // We round down to flatten "/**" and " *". $last_depth = floor((strlen($line) - strlen($trimmed)) / 2); } $depths[$ii] = $last_depth; } return array($gaps, $mask, $depths); } /** * Determine if an inline comment will appear on the rendered diff, * taking into consideration which halves of which changesets will actually * be shown. * * @param PhabricatorInlineCommentInterface Comment to test for visibility. * @return bool True if the comment is visible on the rendered diff. */ private function isCommentVisibleOnRenderedDiff( PhabricatorInlineCommentInterface $comment) { $changeset_id = $comment->getChangesetID(); $is_new = $comment->getIsNewFile(); if ($changeset_id == $this->rightSideChangesetID && $is_new == $this->rightSideAttachesToNewFile) { return true; } if ($changeset_id == $this->leftSideChangesetID && $is_new == $this->leftSideAttachesToNewFile) { return true; } return false; } /** * Determine if a comment will appear on the right side of the display diff. * Note that the comment must appear somewhere on the rendered changeset, as * per isCommentVisibleOnRenderedDiff(). * * @param PhabricatorInlineCommentInterface Comment to test for display * location. * @return bool True for right, false for left. */ private function isCommentOnRightSideWhenDisplayed( PhabricatorInlineCommentInterface $comment) { if (!$this->isCommentVisibleOnRenderedDiff($comment)) { throw new Exception(pht('Comment is not visible on changeset!')); } $changeset_id = $comment->getChangesetID(); $is_new = $comment->getIsNewFile(); if ($changeset_id == $this->rightSideChangesetID && $is_new == $this->rightSideAttachesToNewFile) { return true; } return false; } /** * Parse the 'range' specification that this class and the client-side JS * emit to indicate that a user clicked "Show more..." on a diff. Generally, * use is something like this: * * $spec = $request->getStr('range'); * $parsed = DifferentialChangesetParser::parseRangeSpecification($spec); * list($start, $end, $mask) = $parsed; * $parser->render($start, $end, $mask); * * @param string Range specification, indicating the range of the diff that * should be rendered. * @return tuple List of suitable for passing to * @{method:render}. */ public static function parseRangeSpecification($spec) { $range_s = null; $range_e = null; $mask = array(); if ($spec) { $match = null; if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) { $range_s = (int)$match[1]; $range_e = (int)$match[2]; if (count($match) > 3) { $start = (int)$match[3]; $len = (int)$match[4]; for ($ii = $start; $ii < $start + $len; $ii++) { $mask[$ii] = true; } } } } return array($range_s, $range_e, $mask); } /** * Render "modified coverage" information; test coverage on modified lines. * This synthesizes diff information with unit test information into a useful * indicator of how well tested a change is. */ public function renderModifiedCoverage() { $na = phutil_tag('em', array(), '-'); $coverage = $this->getCoverage(); if (!$coverage) { return $na; } $covered = 0; $not_covered = 0; foreach ($this->new as $k => $new) { if (!$new['line']) { continue; } if (!$new['type']) { continue; } if (empty($coverage[$new['line'] - 1])) { continue; } switch ($coverage[$new['line'] - 1]) { case 'C': $covered++; break; case 'U': $not_covered++; break; } } if (!$covered && !$not_covered) { return $na; } return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered))); } public function detectCopiedCode( array $changesets, $min_width = 30, $min_lines = 3) { assert_instances_of($changesets, 'DifferentialChangeset'); $map = array(); $files = array(); $types = array(); foreach ($changesets as $changeset) { $file = $changeset->getFilename(); foreach ($changeset->getHunks() as $hunk) { $lines = $hunk->getStructuredOldFile(); foreach ($lines as $line => $info) { $type = $info['type']; if ($type == '\\') { continue; } $types[$file][$line] = $type; $text = $info['text']; $text = trim($text); $files[$file][$line] = $text; if (strlen($text) >= $min_width) { $map[$text][] = array($file, $line); } } } } foreach ($changesets as $changeset) { $copies = array(); foreach ($changeset->getHunks() as $hunk) { $added = $hunk->getStructuredNewFile(); $atype = array(); foreach ($added as $line => $info) { $atype[$line] = $info['type']; $added[$line] = trim($info['text']); } $skip_lines = 0; foreach ($added as $line => $code) { if ($skip_lines) { // We're skipping lines that we already processed because we // extended a block above them downward to include them. $skip_lines--; continue; } if ($atype[$line] !== '+') { // This line hasn't been changed in the new file, so don't try // to figure out where it came from. continue; } if (empty($map[$code])) { // This line was too short to trigger copy/move detection. continue; } if (count($map[$code]) > 16) { // If there are a large number of identical lines in this diff, // don't try to figure out where this block came from: the analysis // is O(N^2), since we need to compare every line against every // other line. Even if we arrive at a result, it is unlikely to be // meaningful. See T5041. continue; } $best_length = 0; // Explore all candidates. foreach ($map[$code] as $val) { list($file, $orig_line) = $val; $length = 1; // Search backward and forward to find all of the adjacent lines // which match. foreach (array(-1, 1) as $direction) { $offset = $direction; while (true) { if (isset($copies[$line + $offset])) { // If we run into a block above us which we've already // attributed to a move or copy from elsewhere, stop // looking. break; } if (!isset($added[$line + $offset])) { // If we've run off the beginning or end of the new file, // stop looking. break; } if (!isset($files[$file][$orig_line + $offset])) { // If we've run off the beginning or end of the original // file, we also stop looking. break; } $old = $files[$file][$orig_line + $offset]; $new = $added[$line + $offset]; if ($old !== $new) { // If the old line doesn't match the new line, stop // looking. break; } $length++; $offset += $direction; } } if ($length < $best_length) { // If we already know of a better source (more matching lines) // for this move/copy, stick with that one. We prefer long // copies/moves which match a lot of context over short ones. continue; } if ($length == $best_length) { if (idx($types[$file], $orig_line) != '-') { // If we already know of an equally good source (same number // of matching lines) and this isn't a move, stick with the // other one. We prefer moves over copies. continue; } } $best_length = $length; // ($offset - 1) contains number of forward matching lines. $best_offset = $offset - 1; $best_file = $file; $best_line = $orig_line; } $file = ($best_file == $changeset->getFilename() ? '' : $best_file); for ($i = $best_length; $i--; ) { $type = idx($types[$best_file], $best_line + $best_offset - $i); $copies[$line + $best_offset - $i] = ($best_length < $min_lines ? array() // Ignore short blocks. : array($file, $best_line + $best_offset - $i, $type)); } $skip_lines = $best_offset; } } $copies = array_filter($copies); if ($copies) { $metadata = $changeset->getMetadata(); $metadata['copy:lines'] = $copies; $changeset->setMetadata($metadata); } } return $changesets; } /** * Build maps from lines comments appear on to actual lines. */ private function buildLineBackmaps() { $old_back = array(); $new_back = array(); foreach ($this->old as $ii => $old) { $old_back[$old['line']] = $old['line']; } foreach ($this->new as $ii => $new) { $new_back[$new['line']] = $new['line']; } $max_old_line = 0; $max_new_line = 0; foreach ($this->comments as $comment) { if ($this->isCommentOnRightSideWhenDisplayed($comment)) { $max_new_line = max($max_new_line, $comment->getLineNumber()); } else { $max_old_line = max($max_old_line, $comment->getLineNumber()); } } $cursor = 1; for ($ii = 1; $ii <= $max_old_line; $ii++) { if (empty($old_back[$ii])) { $old_back[$ii] = $cursor; } else { $cursor = $old_back[$ii]; } } $cursor = 1; for ($ii = 1; $ii <= $max_new_line; $ii++) { if (empty($new_back[$ii])) { $new_back[$ii] = $cursor; } else { $cursor = $new_back[$ii]; } } return array($old_back, $new_back); } } diff --git a/src/applications/diviner/atom/DivinerAtom.php b/src/applications/diviner/atom/DivinerAtom.php index 68a0d80cae..520a51e26b 100644 --- a/src/applications/diviner/atom/DivinerAtom.php +++ b/src/applications/diviner/atom/DivinerAtom.php @@ -1,435 +1,436 @@ 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 PhutilInvalidStateException('setDocblockRaw'); } return $this->docblockText; } public function getDocblockMeta() { if ($this->docblockMeta === null) { throw new PhutilInvalidStateException('setDocblockRaw'); } 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->getBook(), $this->getType(), $this->getName(), $this->getFile(), $this->getLine(), $this->getLength(), $this->getLanguage(), $this->getContentRaw(), $this->getDocblockRaw(), $this->getProperties(), $this->getChildHashes(), mpull($this->extends, 'toHash'), mpull($this->links, 'toHash'), ); $this->hash = md5(serialize($parts)).'N'; return $this->hash; } 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() { if (!$this->childHashes && $this->children) { $this->childHashes = mpull($this->children, 'getHash'); } return $this->childHashes; } public function setParentHash($parent_hash) { if ($this->parentHash) { throw new Exception(pht('Atom already has a parent!')); } $this->parentHash = $parent_hash; return $this; } public function hasParent() { return $this->parent || $this->parentHash; } public function setParent(DivinerAtom $atom) { if ($this->parentHash) { throw new Exception(pht('Parent hash has already been computed!')); } $this->parent = $atom; return $this; } public function getParentHash() { if ($this->parent && !$this->parentHash) { $this->parentHash = $this->parent->getHash(); } return $this->parentHash; } public function addChild(DivinerAtom $atom) { if ($this->childHashes) { throw new Exception(pht('Child hashes have already been computed!')); } $atom->setParent($this); $this->children[] = $atom; 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 // @{method: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() { $title = null; if ($this->docblockMeta) { $title = $this->getDocblockMetaValue('title'); } return id(new DivinerAtomRef()) ->setBook($this->getBook()) ->setContext($this->getContext()) ->setType($this->getType()) ->setName($this->getName()) ->setTitle($title) ->setGroup($this->getProperty('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; + return $this; } public function getProperties() { return $this->properties; } public function setProperties(array $properties) { $this->properties = $properties; return $this; } public static function getThisAtomIsNotDocumentedString($type) { switch ($type) { case self::TYPE_ARTICLE: return pht('This article is not documented.'); case self::TYPE_CLASS: return pht('This class is not documented.'); case self::TYPE_FILE: return pht('This file is not documented.'); case self::TYPE_FUNCTION: return pht('This function is not documented.'); case self::TYPE_INTERFACE: return pht('This interface is not documented.'); case self::TYPE_METHOD: return pht('This method is not documented.'); default: phlog(pht("Need translation for '%s'.", $type)); return pht('This %s is not documented.', $type); } } public static function getAllTypes() { return array( self::TYPE_ARTICLE, self::TYPE_CLASS, self::TYPE_FILE, self::TYPE_FUNCTION, self::TYPE_INTERFACE, self::TYPE_METHOD, ); } public static function getAtomTypeNameString($type) { switch ($type) { case self::TYPE_ARTICLE: return pht('Article'); case self::TYPE_CLASS: return pht('Class'); case self::TYPE_FILE: return pht('File'); case self::TYPE_FUNCTION: return pht('Function'); case self::TYPE_INTERFACE: return pht('Interface'); case self::TYPE_METHOD: return pht('Method'); default: phlog(pht("Need translation for '%s'.", $type)); return ucwords($type); } } } diff --git a/src/applications/legalpad/editor/LegalpadDocumentEditor.php b/src/applications/legalpad/editor/LegalpadDocumentEditor.php index 27207a3aa2..5e319b5905 100644 --- a/src/applications/legalpad/editor/LegalpadDocumentEditor.php +++ b/src/applications/legalpad/editor/LegalpadDocumentEditor.php @@ -1,241 +1,242 @@ isContribution = $is_contribution; + return $this; } private function isContribution() { return $this->isContribution; } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = LegalpadTransaction::TYPE_TITLE; $types[] = LegalpadTransaction::TYPE_TEXT; $types[] = LegalpadTransaction::TYPE_SIGNATURE_TYPE; $types[] = LegalpadTransaction::TYPE_PREAMBLE; $types[] = LegalpadTransaction::TYPE_REQUIRE_SIGNATURE; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case LegalpadTransaction::TYPE_TITLE: return $object->getDocumentBody()->getTitle(); case LegalpadTransaction::TYPE_TEXT: return $object->getDocumentBody()->getText(); case LegalpadTransaction::TYPE_SIGNATURE_TYPE: return $object->getSignatureType(); case LegalpadTransaction::TYPE_PREAMBLE: return $object->getPreamble(); case LegalpadTransaction::TYPE_REQUIRE_SIGNATURE: return (bool)$object->getRequireSignature(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case LegalpadTransaction::TYPE_TITLE: case LegalpadTransaction::TYPE_TEXT: case LegalpadTransaction::TYPE_SIGNATURE_TYPE: case LegalpadTransaction::TYPE_PREAMBLE: return $xaction->getNewValue(); case LegalpadTransaction::TYPE_REQUIRE_SIGNATURE: return (bool)$xaction->getNewValue(); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case LegalpadTransaction::TYPE_TITLE: $object->setTitle($xaction->getNewValue()); $body = $object->getDocumentBody(); $body->setTitle($xaction->getNewValue()); $this->setIsContribution(true); break; case LegalpadTransaction::TYPE_TEXT: $body = $object->getDocumentBody(); $body->setText($xaction->getNewValue()); $this->setIsContribution(true); break; case LegalpadTransaction::TYPE_SIGNATURE_TYPE: $object->setSignatureType($xaction->getNewValue()); break; case LegalpadTransaction::TYPE_PREAMBLE: $object->setPreamble($xaction->getNewValue()); break; case LegalpadTransaction::TYPE_REQUIRE_SIGNATURE: $object->setRequireSignature((int)$xaction->getNewValue()); break; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case LegalpadTransaction::TYPE_REQUIRE_SIGNATURE: if ($xaction->getNewValue()) { $session = new PhabricatorAuthSession(); queryfx( $session->establishConnection('w'), 'UPDATE %T SET signedLegalpadDocuments = 0', $session->getTableName()); } break; } return; } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { if ($this->isContribution()) { $object->setVersions($object->getVersions() + 1); $body = $object->getDocumentBody(); $body->setVersion($object->getVersions()); $body->setDocumentPHID($object->getPHID()); $body->save(); $object->setDocumentBodyPHID($body->getPHID()); $actor = $this->getActor(); $type = PhabricatorContributedToObjectEdgeType::EDGECONST; id(new PhabricatorEdgeEditor()) ->addEdge($actor->getPHID(), $type, $object->getPHID()) ->save(); $type = PhabricatorObjectHasContributorEdgeType::EDGECONST; $contributors = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), $type); $object->setRecentContributorPHIDs(array_slice($contributors, 0, 3)); $object->setContributorCount(count($contributors)); $object->save(); } return $xactions; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { case LegalpadTransaction::TYPE_TITLE: case LegalpadTransaction::TYPE_TEXT: case LegalpadTransaction::TYPE_SIGNATURE_TYPE: case LegalpadTransaction::TYPE_PREAMBLE: case LegalpadTransaction::TYPE_REQUIRE_SIGNATURE: return $v; } return parent::mergeTransactions($u, $v); } /* -( Sending Mail )------------------------------------------------------- */ protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new LegalpadReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $phid = $object->getPHID(); $title = $object->getDocumentBody()->getTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject("L{$id}: {$title}") ->addHeader('Thread-Topic', "L{$id}: {$phid}"); } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getCreatorPHID(), $this->requireActor()->getPHID(), ); } protected function shouldImplyCC( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case LegalpadTransaction::TYPE_TEXT: case LegalpadTransaction::TYPE_TITLE: case LegalpadTransaction::TYPE_PREAMBLE: case LegalpadTransaction::TYPE_REQUIRE_SIGNATURE: return true; } return parent::shouldImplyCC($object, $xaction); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $body->addLinkSection( pht('DOCUMENT DETAIL'), PhabricatorEnv::getProductionURI('/legalpad/view/'.$object->getID().'/')); return $body; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.legalpad.subject-prefix'); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return false; } protected function supportsSearch() { return false; } } diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php index 3e0a6d9b1d..18d827f4c0 100644 --- a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php +++ b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php @@ -1,215 +1,216 @@ viewer; } public function setViewer($viewer) { $this->viewer = $viewer; + return $this; } /* -( Composition )-------------------------------------------------------- */ /** * Add a raw block of text to the email. This will be rendered as-is. * * @param string Block of text. * @return this * @task compose */ public function addRawSection($text) { if (strlen($text)) { $text = rtrim($text); $this->sections[] = $text; $this->htmlSections[] = phutil_escape_html_newlines( phutil_tag('div', array(), $text)); } return $this; } public function addRemarkupSection($text) { try { $engine = PhabricatorMarkupEngine::newMarkupEngine(array()); $engine->setConfig('viewer', $this->getViewer()); $engine->setMode(PhutilRemarkupEngine::MODE_TEXT); $styled_text = $engine->markupText($text); $this->sections[] = $styled_text; } catch (Exception $ex) { phlog($ex); $this->sections[] = $text; } try { $mail_engine = PhabricatorMarkupEngine::newMarkupEngine(array()); $mail_engine->setConfig('viewer', $this->getViewer()); $mail_engine->setMode(PhutilRemarkupEngine::MODE_HTML_MAIL); $mail_engine->setConfig( 'uri.base', PhabricatorEnv::getProductionURI('/')); $html = $mail_engine->markupText($text); $this->htmlSections[] = $html; } catch (Exception $ex) { phlog($ex); $this->htmlSections[] = phutil_escape_html_newlines( phutil_tag( 'div', array(), $text)); } return $this; } public function addRawPlaintextSection($text) { if (strlen($text)) { $text = rtrim($text); $this->sections[] = $text; } return $this; } public function addRawHTMLSection($html) { $this->htmlSections[] = phutil_safe_html($html); return $this; } /** * Add a block of text with a section header. This is rendered like this: * * HEADER * Text is indented. * * @param string Header text. * @param string Section text. * @return this * @task compose */ public function addTextSection($header, $section) { if ($section instanceof PhabricatorMetaMTAMailSection) { $plaintext = $section->getPlaintext(); $html = $section->getHTML(); } else { $plaintext = $section; $html = phutil_escape_html_newlines(phutil_tag('div', array(), $section)); } $this->addPlaintextSection($header, $plaintext); $this->addHTMLSection($header, $html); return $this; } public function addPlaintextSection($header, $text) { $this->sections[] = $header."\n".$this->indent($text); return $this; } public function addHTMLSection($header, $html_fragment) { $this->htmlSections[] = array( phutil_tag( 'div', array(), array( phutil_tag('strong', array(), $header), phutil_tag('div', array(), $html_fragment), )), ); return $this; } public function addLinkSection($header, $link) { $html = phutil_tag('a', array('href' => $link), $link); $this->addPlaintextSection($header, $link); $this->addHTMLSection($header, $html); return $this; } /** * Add a Herald section with a rule management URI and a transcript URI. * * @param string URI to rule transcripts. * @return this * @task compose */ public function addHeraldSection($xscript_uri) { if (!PhabricatorEnv::getEnvConfig('metamta.herald.show-hints')) { return $this; } $this->addLinkSection( pht('WHY DID I GET THIS EMAIL?'), PhabricatorEnv::getProductionURI($xscript_uri)); return $this; } /** * Add an attachment. * * @param PhabricatorMetaMTAAttachment Attachment. * @return this * @task compose */ public function addAttachment(PhabricatorMetaMTAAttachment $attachment) { $this->attachments[] = $attachment; return $this; } /* -( Rendering )---------------------------------------------------------- */ /** * Render the email body. * * @return string Rendered body. * @task render */ public function render() { return implode("\n\n", $this->sections)."\n"; } public function renderHTML() { $br = phutil_tag('br'); $body = phutil_implode_html($br, $this->htmlSections); return (string)hsprintf('%s', array($body, $br)); } /** * Retrieve attachments. * * @return list Attachments. * @task render */ public function getAttachments() { return $this->attachments; } /** * Indent a block of text for rendering under a section heading. * * @param string Text to indent. * @return string Indented text. * @task render */ private function indent($text) { return rtrim(" ".str_replace("\n", "\n ", $text)); } } diff --git a/src/applications/paste/view/PasteEmbedView.php b/src/applications/paste/view/PasteEmbedView.php index 650905c46c..e5121b496b 100644 --- a/src/applications/paste/view/PasteEmbedView.php +++ b/src/applications/paste/view/PasteEmbedView.php @@ -1,70 +1,71 @@ paste = $paste; return $this; } public function setHandle(PhabricatorObjectHandle $handle) { $this->handle = $handle; return $this; } public function setHighlights(array $highlights) { $this->highlights = $highlights; return $this; } public function setLines($lines) { $this->lines = $lines; + return $this; } public function render() { if (!$this->paste) { throw new PhutilInvalidStateException('setPaste'); } $lines = phutil_split_lines($this->paste->getContent()); require_celerity_resource('paste-css'); $link = phutil_tag( 'a', array( 'href' => '/P'.$this->paste->getID(), ), $this->handle->getFullName()); $head = phutil_tag( 'div', array( 'class' => 'paste-embed-head', ), $link); $body_attributes = array('class' => 'paste-embed-body'); if ($this->lines != null) { $body_attributes['style'] = 'max-height: '.$this->lines * (1.15).'em;'; } $body = phutil_tag( 'div', $body_attributes, id(new PhabricatorSourceCodeView()) ->setLines($lines) ->setHighlights($this->highlights) ->disableHighlightOnClick()); return phutil_tag( 'div', array('class' => 'paste-embed'), array($head, $body)); } } diff --git a/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php b/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php index 17ae6fcc35..ecfa4c5b6b 100644 --- a/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php +++ b/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php @@ -1,307 +1,309 @@ getTransactionType()) { case ReleephRequestTransaction::TYPE_REQUEST: return $object->getRequestCommitPHID(); case ReleephRequestTransaction::TYPE_EDIT_FIELD: $field = newv($xaction->getMetadataValue('fieldClass'), array()); $value = $field->setReleephRequest($object)->getValue(); return $value; case ReleephRequestTransaction::TYPE_USER_INTENT: $user_phid = $xaction->getAuthorPHID(); return idx($object->getUserIntents(), $user_phid); case ReleephRequestTransaction::TYPE_PICK_STATUS: return (int)$object->getPickStatus(); break; case ReleephRequestTransaction::TYPE_COMMIT: return $object->getCommitIdentifier(); case ReleephRequestTransaction::TYPE_DISCOVERY: return $object->getCommitPHID(); case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH: return $object->getInBranch(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case ReleephRequestTransaction::TYPE_REQUEST: case ReleephRequestTransaction::TYPE_USER_INTENT: case ReleephRequestTransaction::TYPE_EDIT_FIELD: case ReleephRequestTransaction::TYPE_PICK_STATUS: case ReleephRequestTransaction::TYPE_COMMIT: case ReleephRequestTransaction::TYPE_DISCOVERY: case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH: return $xaction->getNewValue(); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $new = $xaction->getNewValue(); switch ($xaction->getTransactionType()) { case ReleephRequestTransaction::TYPE_REQUEST: $object->setRequestCommitPHID($new); break; case ReleephRequestTransaction::TYPE_USER_INTENT: $user_phid = $xaction->getAuthorPHID(); $intents = $object->getUserIntents(); $intents[$user_phid] = $new; $object->setUserIntents($intents); break; case ReleephRequestTransaction::TYPE_EDIT_FIELD: $field = newv($xaction->getMetadataValue('fieldClass'), array()); $field ->setReleephRequest($object) ->setValue($new); break; case ReleephRequestTransaction::TYPE_PICK_STATUS: $object->setPickStatus($new); break; case ReleephRequestTransaction::TYPE_COMMIT: $this->setInBranchFromAction($object, $xaction); $object->setCommitIdentifier($new); break; case ReleephRequestTransaction::TYPE_DISCOVERY: $this->setInBranchFromAction($object, $xaction); $object->setCommitPHID($new); break; case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH: $object->setInBranch((int)$new); break; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return; } protected function filterTransactions( PhabricatorLiskDAO $object, array $xactions) { // Remove TYPE_DISCOVERY xactions that are the result of a reparse. $previously_discovered_commits = array(); $discovery_xactions = idx( mgroup($xactions, 'getTransactionType'), ReleephRequestTransaction::TYPE_DISCOVERY); if ($discovery_xactions) { $previous_xactions = id(new ReleephRequestTransactionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withObjectPHIDs(array($object->getPHID())) ->execute(); foreach ($previous_xactions as $xaction) { if ($xaction->getTransactionType() === ReleephRequestTransaction::TYPE_DISCOVERY) { $commit_phid = $xaction->getNewValue(); $previously_discovered_commits[$commit_phid] = true; } } } foreach ($xactions as $key => $xaction) { if ($xaction->getTransactionType() === ReleephRequestTransaction::TYPE_DISCOVERY && idx($previously_discovered_commits, $xaction->getNewValue())) { unset($xactions[$key]); } } return parent::filterTransactions($object, $xactions); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { // Avoid sending emails that only talk about commit discovery. $types = array_unique(mpull($xactions, 'getTransactionType')); if ($types === array(ReleephRequestTransaction::TYPE_DISCOVERY)) { return false; } // Don't email people when we discover that something picks or reverts OK. if ($types === array(ReleephRequestTransaction::TYPE_PICK_STATUS)) { if (!mfilter($xactions, 'isBoringPickStatus', true /* negate */)) { // If we effectively call "isInterestingPickStatus" and get nothing... return false; } } return true; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new ReleephRequestReplyHandler()) ->setActor($this->getActor()) ->setMailReceiver($object); } protected function getMailSubjectPrefix() { return '[Releeph]'; } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $phid = $object->getPHID(); $title = $object->getSummaryForDisplay(); return id(new PhabricatorMetaMTAMail()) ->setSubject("RQ{$id}: {$title}") ->addHeader('Thread-Topic', "RQ{$id}: {$phid}"); } protected function getMailTo(PhabricatorLiskDAO $object) { $to_phids = array(); $product = $object->getBranch()->getProduct(); foreach ($product->getPushers() as $phid) { $to_phids[] = $phid; } foreach ($object->getUserIntents() as $phid => $intent) { $to_phids[] = $phid; } return $to_phids; } protected function getMailCC(PhabricatorLiskDAO $object) { return array(); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $rq = $object; $releeph_branch = $rq->getBranch(); $releeph_project = $releeph_branch->getProduct(); /** * If any of the events we are emailing about were about a pick failure * (and/or a revert failure?), include pick failure instructions. */ $has_pick_failure = false; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() === ReleephRequestTransaction::TYPE_PICK_STATUS && $xaction->getNewValue() === ReleephRequest::PICK_FAILED) { $has_pick_failure = true; break; } } if ($has_pick_failure) { $instructions = $releeph_project->getDetail('pick_failure_instructions'); if ($instructions) { $body->addTextSection( pht('PICK FAILURE INSTRUCTIONS'), $instructions); } } $name = sprintf('RQ%s: %s', $rq->getID(), $rq->getSummaryForDisplay()); $body->addTextSection( pht('RELEEPH REQUEST'), $name."\n". PhabricatorEnv::getProductionURI('/RQ'.$rq->getID())); $project_and_branch = sprintf( '%s - %s', $releeph_project->getName(), $releeph_branch->getDisplayNameWithDetail()); $body->addTextSection( pht('RELEEPH BRANCH'), $project_and_branch."\n". PhabricatorEnv::getProductionURI($releeph_branch->getURI())); return $body; } private function setInBranchFromAction( ReleephRequest $rq, ReleephRequestTransaction $xaction) { $action = $xaction->getMetadataValue('action'); switch ($action) { case 'pick': $rq->setInBranch(1); break; case 'revert': $rq->setInBranch(0); break; default: $id = $rq->getID(); $type = $xaction->getTransactionType(); $new = $xaction->getNewValue(); phlog( pht( "Unknown discovery action '%s' for xaction of type %s ". "with new value %s mentioning %s!", $action, $type, $new, 'RQ'.$id)); break; } + + return $this; } } diff --git a/src/applications/releeph/field/specification/ReleephFieldSpecification.php b/src/applications/releeph/field/specification/ReleephFieldSpecification.php index c0cb64b05d..df458ced56 100644 --- a/src/applications/releeph/field/specification/ReleephFieldSpecification.php +++ b/src/applications/releeph/field/specification/ReleephFieldSpecification.php @@ -1,262 +1,263 @@ requestValue = $request->getStr($this->getRequiredStorageKey()); return $this; } public function shouldAppearInPropertyView() { return true; } public function renderPropertyViewLabel() { return $this->getName(); } public function renderPropertyViewValue(array $handles) { $key = $this->getRequiredStorageKey(); $value = $this->getReleephRequest()->getDetail($key); if ($value === '') { return null; } return $value; } abstract public function getName(); /* -( Storage )------------------------------------------------------------ */ public function getStorageKey() { return null; } public function getRequiredStorageKey() { $key = $this->getStorageKey(); if ($key === null) { throw new PhabricatorCustomFieldImplementationIncompleteException($this); } if (strpos($key, '.') !== false) { /** * Storage keys are reused for form controls, and periods in form control * names break HTML forms. */ throw new Exception(pht("You can't use '%s' in storage keys!", '.')); } return $key; } public function shouldAppearInEditView() { return $this->isEditable(); } final public function isEditable() { return $this->getStorageKey() !== null; } final public function getValue() { if ($this->requestValue !== null) { return $this->requestValue; } $key = $this->getRequiredStorageKey(); return $this->getReleephRequest()->getDetail($key); } final public function setValue($value) { $key = $this->getRequiredStorageKey(); return $this->getReleephRequest()->setDetail($key, $value); } /** * @throws ReleephFieldParseException, to show an error. */ public function validate($value) { return; } /** * Turn values as they are stored in a ReleephRequest into a text that can be * rendered as a transactions old/new values. */ public function normalizeForTransactionView( PhabricatorApplicationTransaction $xaction, $value) { return $value; } /* -( Conduit )------------------------------------------------------------ */ public function getKeyForConduit() { return $this->getRequiredStorageKey(); } public function getValueForConduit() { return $this->getValue(); } public function setValueFromConduitAPIRequest(ConduitAPIRequest $request) { $value = idx( $request->getValue('fields', array()), $this->getRequiredStorageKey()); $this->validate($value); $this->setValue($value); + return $this; } /* -( Arcanist )----------------------------------------------------------- */ public function renderHelpForArcanist() { return ''; } /* -( Context )------------------------------------------------------------ */ private $releephProject; private $releephBranch; private $releephRequest; private $user; final public function setReleephProject(ReleephProject $rp) { $this->releephProject = $rp; return $this; } final public function setReleephBranch(ReleephBranch $rb) { $this->releephRequest = $rb; return $this; } final public function setReleephRequest(ReleephRequest $rr) { $this->releephRequest = $rr; return $this; } final public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } final public function getReleephProject() { if (!$this->releephProject) { return $this->getReleephBranch()->getProduct(); } return $this->releephProject; } final public function getReleephBranch() { if (!$this->releephBranch) { return $this->getReleephRequest()->getBranch(); } return $this->releephBranch; } final public function getReleephRequest() { if (!$this->releephRequest) { return $this->getObject(); } return $this->releephRequest; } final public function getUser() { if (!$this->user) { return $this->getViewer(); } return $this->user; } /* -( Commit Messages )---------------------------------------------------- */ public function shouldAppearOnCommitMessage() { return false; } public function renderLabelForCommitMessage() { throw new PhabricatorCustomFieldImplementationIncompleteException($this); } public function renderValueForCommitMessage() { throw new PhabricatorCustomFieldImplementationIncompleteException($this); } public function shouldAppearOnRevertMessage() { return false; } public function renderLabelForRevertMessage() { return $this->renderLabelForCommitMessage(); } public function renderValueForRevertMessage() { return $this->renderValueForCommitMessage(); } /* -( Markup Interface )--------------------------------------------------- */ const MARKUP_FIELD_GENERIC = 'releeph:generic-markup-field'; private $engine; /** * @{class:ReleephFieldSpecification} implements much of * @{interface:PhabricatorMarkupInterface} for you. If you return true from * `shouldMarkup()`, and implement `getMarkupText()` then your text will be * rendered through the Phabricator markup pipeline. * * Output is retrievable with `getMarkupEngineOutput()`. */ public function shouldMarkup() { return false; } public function getMarkupText($field) { throw new PhabricatorCustomFieldImplementationIncompleteException($this); } final public function getMarkupEngineOutput() { return $this->engine->getOutput($this, self::MARKUP_FIELD_GENERIC); } final public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->engine = $engine; $engine->addObject($this, self::MARKUP_FIELD_GENERIC); return $this; } final public function getMarkupFieldKey($field) { return sprintf( '%s:%s:%s:%s', $this->getReleephRequest()->getPHID(), $this->getStorageKey(), $field, PhabricatorHash::digest($this->getMarkupText($field))); } final public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newDifferentialMarkupEngine(); } final public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } final public function shouldUseMarkupCache($field) { return true; } } diff --git a/src/applications/releeph/field/specification/ReleephLevelFieldSpecification.php b/src/applications/releeph/field/specification/ReleephLevelFieldSpecification.php index 7306067dc4..395636c983 100644 --- a/src/applications/releeph/field/specification/ReleephLevelFieldSpecification.php +++ b/src/applications/releeph/field/specification/ReleephLevelFieldSpecification.php @@ -1,136 +1,137 @@ getNameForLevel($this->getValue()); } public function renderEditControl(array $handles) { $control_name = $this->getRequiredStorageKey(); $all_levels = $this->getLevels(); $level = $this->getValue(); if (!$level) { $level = $this->getDefaultLevel(); } $control = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Level')) ->setName($control_name) ->setValue($level); if ($this->error) { $control->setError($this->error); } else if ($this->getDefaultLevel()) { $control->setError(true); } foreach ($all_levels as $level) { $name = $this->getNameForLevel($level); $description = $this->getDescriptionForLevel($level); $control->addButton($level, $name, $description); } return $control; } public function renderHelpForArcanist() { $text = ''; $levels = $this->getLevels(); $default = $this->getDefaultLevel(); foreach ($levels as $level) { $name = $this->getNameForLevel($level); $description = $this->getDescriptionForLevel($level); $default_marker = ' '; if ($level === $default) { $default_marker = '*'; } $text .= " {$default_marker} **{$name}**\n"; $text .= phutil_console_wrap($description."\n", 8); } return $text; } public function validate($value) { if ($value === null) { $this->error = pht('Required'); $label = $this->getName(); throw new ReleephFieldParseException( $this, pht('You must provide a %s level.', $label)); } $levels = $this->getLevels(); if (!in_array($value, $levels)) { $label = $this->getName(); throw new ReleephFieldParseException( $this, pht( "Level '%s' is not a valid %s level in this project.", $value, $label)); } } public function setValueFromConduitAPIRequest(ConduitAPIRequest $request) { $key = $this->getRequiredStorageKey(); $label = $this->getName(); $name = idx($request->getValue('fields', array()), $key); if (!$name) { $level = $this->getDefaultLevel(); if (!$level) { throw new ReleephFieldParseException( $this, pht( 'No value given for %s, and no default is given for this level!', $label)); } } else { $level = $this->getLevelByName($name); } if (!$level) { throw new ReleephFieldParseException( $this, pht("Unknown %s level name '%s'", $label, $name)); } $this->setValue($level); + return $this; } private $nameMap = array(); public function getLevelByName($name) { // Build this once if (!$this->nameMap) { foreach ($this->getLevels() as $level) { $level_name = $this->getNameForLevel($level); $this->nameMap[$level_name] = $level; } } return idx($this->nameMap, $name); } } diff --git a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php index 0eab00b5c9..3909cc83b0 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php @@ -1,492 +1,494 @@ newRefs = array(); $this->deadRefs = array(); $this->closeCommits = array(); $repository = $this->getRepository(); $branches_may_close = false; $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // No meaningful refs of any type in Subversion. $branches = array(); $bookmarks = array(); $tags = array(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $branches = $this->loadMercurialBranchPositions($repository); $bookmarks = $this->loadMercurialBookmarkPositions($repository); $tags = array(); $branches_may_close = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $branches = $this->loadGitBranchPositions($repository); $bookmarks = array(); $tags = $this->loadGitTagPositions($repository); break; default: throw new Exception(pht('Unknown VCS "%s"!', $vcs)); } $maps = array( PhabricatorRepositoryRefCursor::TYPE_BRANCH => $branches, PhabricatorRepositoryRefCursor::TYPE_TAG => $tags, PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => $bookmarks, ); $all_cursors = id(new PhabricatorRepositoryRefCursorQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRepositoryPHIDs(array($repository->getPHID())) ->execute(); $cursor_groups = mgroup($all_cursors, 'getRefType'); $this->hasNoCursors = (!$all_cursors); // Find all the heads of closing refs. $all_closing_heads = array(); foreach ($all_cursors as $cursor) { if ($this->shouldCloseRef($cursor->getRefType(), $cursor->getRefName())) { $all_closing_heads[] = $cursor->getCommitIdentifier(); } } $all_closing_heads = array_unique($all_closing_heads); $all_closing_heads = $this->removeMissingCommits($all_closing_heads); foreach ($maps as $type => $refs) { $cursor_group = idx($cursor_groups, $type, array()); $this->updateCursors($cursor_group, $refs, $type, $all_closing_heads); } if ($this->closeCommits) { $this->setCloseFlagOnCommits($this->closeCommits); } if ($this->newRefs || $this->deadRefs) { $repository->openTransaction(); foreach ($this->newRefs as $ref) { $ref->save(); } foreach ($this->deadRefs as $ref) { $ref->delete(); } $repository->saveTransaction(); $this->newRefs = array(); $this->deadRefs = array(); } if ($branches && $branches_may_close) { $this->updateBranchStates($repository, $branches); } } private function updateBranchStates( PhabricatorRepository $repository, array $branches) { assert_instances_of($branches, 'DiffusionRepositoryRef'); $all_cursors = id(new PhabricatorRepositoryRefCursorQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRepositoryPHIDs(array($repository->getPHID())) ->execute(); $state_map = array(); $type_branch = PhabricatorRepositoryRefCursor::TYPE_BRANCH; foreach ($all_cursors as $cursor) { if ($cursor->getRefType() !== $type_branch) { continue; } $raw_name = $cursor->getRefNameRaw(); $hash = $cursor->getCommitIdentifier(); $state_map[$raw_name][$hash] = $cursor; } foreach ($branches as $branch) { $cursor = idx($state_map, $branch->getShortName(), array()); $cursor = idx($cursor, $branch->getCommitIdentifier()); if (!$cursor) { continue; } $fields = $branch->getRawFields(); $cursor_state = (bool)$cursor->getIsClosed(); $branch_state = (bool)idx($fields, 'closed'); if ($cursor_state != $branch_state) { $cursor->setIsClosed((int)$branch_state)->save(); } } } private function markRefNew(PhabricatorRepositoryRefCursor $cursor) { $this->newRefs[] = $cursor; return $this; } private function markRefDead(PhabricatorRepositoryRefCursor $cursor) { $this->deadRefs[] = $cursor; return $this; } private function markCloseCommits(array $identifiers) { foreach ($identifiers as $identifier) { $this->closeCommits[$identifier] = $identifier; } return $this; } /** * Remove commits which no longer exist in the repository from a list. * * After a force push and garbage collection, we may have branch cursors which * point at commits which no longer exist. This can make commands issued later * fail. See T5839 for discussion. * * @param list List of commit identifiers. * @return list List with nonexistent identifiers removed. */ private function removeMissingCommits(array $identifiers) { if (!$identifiers) { return array(); } $resolved = id(new DiffusionLowLevelResolveRefsQuery()) ->setRepository($this->getRepository()) ->withRefs($identifiers) ->execute(); foreach ($identifiers as $key => $identifier) { if (empty($resolved[$identifier])) { unset($identifiers[$key]); } } return $identifiers; } private function updateCursors( array $cursors, array $new_refs, $ref_type, array $all_closing_heads) { $repository = $this->getRepository(); // NOTE: Mercurial branches may have multiple branch heads; this logic // is complex primarily to account for that. // Group all the cursors by their ref name, like "master". Since Mercurial // branches may have multiple heads, there could be several cursors with // the same name. $cursor_groups = mgroup($cursors, 'getRefNameRaw'); // Group all the new ref values by their name. As above, these groups may // have multiple members in Mercurial. $ref_groups = mgroup($new_refs, 'getShortName'); foreach ($ref_groups as $name => $refs) { $new_commits = mpull($refs, 'getCommitIdentifier', 'getCommitIdentifier'); $ref_cursors = idx($cursor_groups, $name, array()); $old_commits = mpull($ref_cursors, null, 'getCommitIdentifier'); // We're going to delete all the cursors pointing at commits which are // no longer associated with the refs. This primarily makes the Mercurial // multiple head case easier, and means that when we update a ref we // delete the old one and write a new one. foreach ($ref_cursors as $cursor) { if (isset($new_commits[$cursor->getCommitIdentifier()])) { // This ref previously pointed at this commit, and still does. $this->log( pht( 'Ref %s "%s" still points at %s.', $ref_type, $name, $cursor->getCommitIdentifier())); } else { // This ref previously pointed at this commit, but no longer does. $this->log( pht( 'Ref %s "%s" no longer points at %s.', $ref_type, $name, $cursor->getCommitIdentifier())); // Nuke the obsolete cursor. $this->markRefDead($cursor); } } // Now, we're going to insert new cursors for all the commits which are // associated with this ref that don't currently have cursors. $added_commits = array_diff_key($new_commits, $old_commits); foreach ($added_commits as $identifier) { $this->log( pht( 'Ref %s "%s" now points at %s.', $ref_type, $name, $identifier)); $this->markRefNew( id(new PhabricatorRepositoryRefCursor()) ->setRepositoryPHID($repository->getPHID()) ->setRefType($ref_type) ->setRefName($name) ->setCommitIdentifier($identifier)); } if ($this->shouldCloseRef($ref_type, $name)) { foreach ($added_commits as $identifier) { $new_identifiers = $this->loadNewCommitIdentifiers( $identifier, $all_closing_heads); $this->markCloseCommits($new_identifiers); } } } // Find any cursors for refs which no longer exist. This happens when a // branch, tag or bookmark is deleted. foreach ($cursor_groups as $name => $cursor_group) { if (idx($ref_groups, $name) === null) { foreach ($cursor_group as $cursor) { $this->log( pht( 'Ref %s "%s" no longer exists.', $cursor->getRefType(), $cursor->getRefName())); $this->markRefDead($cursor); } } } } private function shouldCloseRef($ref_type, $ref_name) { if ($ref_type !== PhabricatorRepositoryRefCursor::TYPE_BRANCH) { return false; } if ($this->hasNoCursors) { // If we don't have any cursors, don't close things. Particularly, this // corresponds to the case where you've just updated to this code on an // existing repository: we don't want to requeue message steps for every // commit on a closeable ref. return false; } return $this->getRepository()->shouldAutocloseBranch($ref_name); } /** * Find all ancestors of a new closing branch head which are not ancestors * of any old closing branch head. */ private function loadNewCommitIdentifiers( $new_head, array $all_closing_heads) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: if ($all_closing_heads) { $parts = array(); foreach ($all_closing_heads as $head) { $parts[] = hgsprintf('%s', $head); } // See T5896. Mercurial can not parse an "X or Y or ..." rev list // with more than about 300 items, because it exceeds the maximum // allowed recursion depth. Split all the heads into chunks of // 256, and build a query like this: // // ((1 or 2 or ... or 255) or (256 or 257 or ... 511)) // // If we have more than 65535 heads, we'll do that again: // // (((1 or ...) or ...) or ((65536 or ...) or ...)) $chunk_size = 256; while (count($parts) > $chunk_size) { $chunks = array_chunk($parts, $chunk_size); foreach ($chunks as $key => $chunk) { $chunks[$key] = '('.implode(' or ', $chunk).')'; } $parts = array_values($chunks); } $parts = '('.implode(' or ', $parts).')'; list($stdout) = $this->getRepository()->execxLocalCommand( 'log --template %s --rev %s', '{node}\n', hgsprintf('%s', $new_head).' - '.$parts); } else { list($stdout) = $this->getRepository()->execxLocalCommand( 'log --template %s --rev %s', '{node}\n', hgsprintf('%s', $new_head)); } $stdout = trim($stdout); if (!strlen($stdout)) { return array(); } return phutil_split_lines($stdout, $retain_newlines = false); case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: if ($all_closing_heads) { list($stdout) = $this->getRepository()->execxLocalCommand( 'log --format=%s %s --not %Ls', '%H', $new_head, $all_closing_heads); } else { list($stdout) = $this->getRepository()->execxLocalCommand( 'log --format=%s %s', '%H', $new_head); } $stdout = trim($stdout); if (!strlen($stdout)) { return array(); } return phutil_split_lines($stdout, $retain_newlines = false); default: throw new Exception(pht('Unsupported VCS "%s"!', $vcs)); } } /** * Mark a list of commits as closeable, and queue workers for those commits * which don't already have the flag. */ private function setCloseFlagOnCommits(array $identifiers) { $repository = $this->getRepository(); $commit_table = new PhabricatorRepositoryCommit(); $conn_w = $commit_table->establishConnection('w'); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $class = 'PhabricatorRepositoryGitCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $class = 'PhabricatorRepositorySvnCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; break; default: throw new Exception(pht("Unknown repository type '%s'!", $vcs)); } $all_commits = queryfx_all( $conn_w, 'SELECT id, commitIdentifier, importStatus FROM %T WHERE repositoryID = %d AND commitIdentifier IN (%Ls)', $commit_table->getTableName(), $repository->getID(), $identifiers); $closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE; $all_commits = ipull($all_commits, null, 'commitIdentifier'); foreach ($identifiers as $identifier) { $row = idx($all_commits, $identifier); if (!$row) { throw new Exception( pht( 'Commit "%s" has not been discovered yet! Run discovery before '. 'updating refs.', $identifier)); } if (!($row['importStatus'] & $closeable_flag)) { queryfx( $conn_w, 'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d', $commit_table->getTableName(), $closeable_flag, $row['id']); $data = array( 'commitID' => $row['id'], 'only' => true, ); PhabricatorWorker::scheduleTask($class, $data); } } + + return $this; } /* -( Updating Git Refs )-------------------------------------------------- */ /** * @task git */ private function loadGitBranchPositions(PhabricatorRepository $repository) { return id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withIsOriginBranch(true) ->execute(); } /** * @task git */ private function loadGitTagPositions(PhabricatorRepository $repository) { return id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withIsTag(true) ->execute(); } /* -( Updating Mercurial Refs )-------------------------------------------- */ /** * @task hg */ private function loadMercurialBranchPositions( PhabricatorRepository $repository) { return id(new DiffusionLowLevelMercurialBranchesQuery()) ->setRepository($repository) ->execute(); } /** * @task hg */ private function loadMercurialBookmarkPositions( PhabricatorRepository $repository) { // TODO: Implement support for Mercurial bookmarks. return array(); } } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index e5eceee793..90123ee7c1 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1,1135 +1,1137 @@ newQuery(); if ($query) { $object = $query->newResultObject(); if ($object) { return $object; } } return null; } public function newQuery() { return null; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } protected function requireViewer() { if (!$this->viewer) { throw new PhutilInvalidStateException('setViewer'); } return $this->viewer; } public function setContext($context) { $this->context = $context; return $this; } public function isPanelContext() { return ($this->context == self::CONTEXT_PANEL); } public function canUseInPanelContext() { return true; } public function saveQuery(PhabricatorSavedQuery $query) { $query->setEngineClassName(get_class($this)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { $query->save(); } catch (AphrontDuplicateKeyQueryException $ex) { // Ignore, this is just a repeated search. } unset($unguarded); } /** * Create a saved query object from the request. * * @param AphrontRequest The search request. * @return PhabricatorSavedQuery */ public function buildSavedQueryFromRequest(AphrontRequest $request) { $fields = $this->buildSearchFields(); $viewer = $this->requireViewer(); $saved = new PhabricatorSavedQuery(); foreach ($fields as $field) { $field->setViewer($viewer); $value = $field->readValueFromRequest($request); $saved->setParameter($field->getKey(), $value); } return $saved; } /** * Executes the saved query. * * @param PhabricatorSavedQuery The saved query to operate on. * @return The result of the query. */ public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $saved = clone $saved; $this->willUseSavedQuery($saved); $fields = $this->buildSearchFields(); $viewer = $this->requireViewer(); $map = array(); foreach ($fields as $field) { $field->setViewer($viewer); $field->readValueFromSavedQuery($saved); $value = $field->getValueForQuery($field->getValue()); $map[$field->getKey()] = $value; } $query = $this->buildQueryFromParameters($map); $object = $this->newResultObject(); if (!$object) { return $query; } if ($object instanceof PhabricatorSubscribableInterface) { if (!empty($map['subscriberPHIDs'])) { $query->withEdgeLogicPHIDs( PhabricatorObjectHasSubscriberEdgeType::EDGECONST, PhabricatorQueryConstraint::OPERATOR_OR, $map['subscriberPHIDs']); } } if ($object instanceof PhabricatorProjectInterface) { if (!empty($map['projectPHIDs'])) { $query->withEdgeLogicConstraints( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $map['projectPHIDs']); } } if ($object instanceof PhabricatorSpacesInterface) { if (!empty($map['spacePHIDs'])) { $query->withSpacePHIDs($map['spacePHIDs']); } else { // If the user doesn't search for objects in specific spaces, we // default to "all active spaces you have permission to view". $query->withSpaceIsArchived(false); } } if ($object instanceof PhabricatorCustomFieldInterface) { $this->applyCustomFieldsToQuery($query, $saved); } $order = $saved->getParameter('order'); $builtin = $query->getBuiltinOrderAliasMap(); if (strlen($order) && isset($builtin[$order])) { $query->setOrder($order); } else { // If the order is invalid or not available, we choose the first // builtin order. This isn't always the default order for the query, // but is the first value in the "Order" dropdown, and makes the query // behavior more consistent with the UI. In queries where the two // orders differ, this order is the preferred order for humans. $query->setOrder(head_key($builtin)); } return $query; } /** * Hook for subclasses to adjust saved queries prior to use. * * If an application changes how queries are saved, it can implement this * hook to keep old queries working the way users expect, by reading, * adjusting, and overwriting parameters. * * @param PhabricatorSavedQuery Saved query which will be executed. * @return void */ protected function willUseSavedQuery(PhabricatorSavedQuery $saved) { return; } protected function buildQueryFromParameters(array $parameters) { throw new PhutilMethodNotImplementedException(); } /** * Builds the search form using the request. * * @param AphrontFormView Form to populate. * @param PhabricatorSavedQuery The query from which to build the form. * @return void */ public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $saved = clone $saved; $this->willUseSavedQuery($saved); $fields = $this->buildSearchFields(); $fields = $this->adjustFieldsForDisplay($fields); $viewer = $this->requireViewer(); foreach ($fields as $field) { $field->setViewer($viewer); $field->readValueFromSavedQuery($saved); } foreach ($fields as $field) { foreach ($field->getErrors() as $error) { $this->addError(last($error)); } } foreach ($fields as $field) { $field->appendToForm($form); } } protected function buildSearchFields() { $fields = array(); foreach ($this->buildCustomSearchFields() as $field) { $fields[] = $field; } $object = $this->newResultObject(); if ($object) { if ($object instanceof PhabricatorSubscribableInterface) { $fields[] = id(new PhabricatorSearchSubscribersField()) ->setLabel(pht('Subscribers')) ->setKey('subscriberPHIDs') ->setAliases(array('subscriber', 'subscribers')); } if ($object instanceof PhabricatorProjectInterface) { $fields[] = id(new PhabricatorProjectSearchField()) ->setKey('projectPHIDs') ->setAliases(array('project', 'projects')) ->setLabel(pht('Projects')); } if ($object instanceof PhabricatorSpacesInterface) { if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) { $fields[] = id(new PhabricatorSpacesSearchField()) ->setKey('spacePHIDs') ->setAliases(array('space', 'spaces')) ->setLabel(pht('Spaces')); } } } foreach ($this->buildCustomFieldSearchFields() as $custom_field) { $fields[] = $custom_field; } $query = $this->newQuery(); if ($query && $this->shouldShowOrderField()) { $orders = $query->getBuiltinOrders(); $orders = ipull($orders, 'name'); $fields[] = id(new PhabricatorSearchOrderField()) ->setLabel(pht('Order By')) ->setKey('order') ->setOrderAliases($query->getBuiltinOrderAliasMap()) ->setOptions($orders); } $field_map = array(); foreach ($fields as $field) { $key = $field->getKey(); if (isset($field_map[$key])) { throw new Exception( pht( 'Two fields in this SearchEngine use the same key ("%s"), but '. 'each field must use a unique key.', $key)); } $field_map[$key] = $field; } return $field_map; } protected function shouldShowOrderField() { return true; } private function adjustFieldsForDisplay(array $field_map) { $order = $this->getDefaultFieldOrder(); $head_keys = array(); $tail_keys = array(); $seen_tail = false; foreach ($order as $order_key) { if ($order_key === '...') { $seen_tail = true; continue; } if (!$seen_tail) { $head_keys[] = $order_key; } else { $tail_keys[] = $order_key; } } $head = array_select_keys($field_map, $head_keys); $body = array_diff_key($field_map, array_fuse($tail_keys)); $tail = array_select_keys($field_map, $tail_keys); $result = $head + $body + $tail; foreach ($this->getHiddenFields() as $hidden_key) { unset($result[$hidden_key]); } return $result; } protected function buildCustomSearchFields() { throw new PhutilMethodNotImplementedException(); } /** * Define the default display order for fields by returning a list of * field keys. * * You can use the special key `...` to mean "all unspecified fields go * here". This lets you easily put important fields at the top of the form, * standard fields in the middle of the form, and less important fields at * the bottom. * * For example, you might return a list like this: * * return array( * 'authorPHIDs', * 'reviewerPHIDs', * '...', * 'createdAfter', * 'createdBefore', * ); * * Any unspecified fields (including custom fields and fields added * automatically by infrastruture) will be put in the middle. * * @return list Default ordering for field keys. */ protected function getDefaultFieldOrder() { return array(); } /** * Return a list of field keys which should be hidden from the viewer. * * @return list Fields to hide. */ protected function getHiddenFields() { return array(); } public function getErrors() { return $this->errors; } public function addError($error) { $this->errors[] = $error; return $this; } /** * Return an application URI corresponding to the results page of a query. * Normally, this is something like `/application/query/QUERYKEY/`. * * @param string The query key to build a URI for. * @return string URI where the query can be executed. * @task uri */ public function getQueryResultsPageURI($query_key) { return $this->getURI('query/'.$query_key.'/'); } /** * Return an application URI for query management. This is used when, e.g., * a query deletion operation is cancelled. * * @return string URI where queries can be managed. * @task uri */ public function getQueryManagementURI() { return $this->getURI('query/edit/'); } /** * Return the URI to a path within the application. Used to construct default * URIs for management and results. * * @return string URI to path. * @task uri */ abstract protected function getURI($path); /** * Return a human readable description of the type of objects this query * searches for. * * For example, "Tasks" or "Commits". * * @return string Human-readable description of what this engine is used to * find. */ abstract public function getResultTypeDescription(); public function newSavedQuery() { return id(new PhabricatorSavedQuery()) ->setEngineClassName(get_class($this)); } public function addNavigationItems(PHUIListView $menu) { $viewer = $this->requireViewer(); $menu->newLabel(pht('Queries')); $named_queries = $this->loadEnabledNamedQueries(); foreach ($named_queries as $query) { $key = $query->getQueryKey(); $uri = $this->getQueryResultsPageURI($key); $menu->newLink($query->getQueryName(), $uri, 'query/'.$key); } if ($viewer->isLoggedIn()) { $manage_uri = $this->getQueryManagementURI(); $menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit'); } $menu->newLabel(pht('Search')); $advanced_uri = $this->getQueryResultsPageURI('advanced'); $menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced'); return $this; } public function loadAllNamedQueries() { $viewer = $this->requireViewer(); $named_queries = id(new PhabricatorNamedQueryQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withEngineClassNames(array(get_class($this))) ->execute(); $named_queries = mpull($named_queries, null, 'getQueryKey'); $builtin = $this->getBuiltinQueries($viewer); $builtin = mpull($builtin, null, 'getQueryKey'); foreach ($named_queries as $key => $named_query) { if ($named_query->getIsBuiltin()) { if (isset($builtin[$key])) { $named_queries[$key]->setQueryName($builtin[$key]->getQueryName()); unset($builtin[$key]); } else { unset($named_queries[$key]); } } unset($builtin[$key]); } $named_queries = msort($named_queries, 'getSortKey'); return $named_queries + $builtin; } public function loadEnabledNamedQueries() { $named_queries = $this->loadAllNamedQueries(); foreach ($named_queries as $key => $named_query) { if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) { unset($named_queries[$key]); } } return $named_queries; } protected function setQueryProjects( PhabricatorCursorPagedPolicyAwareQuery $query, PhabricatorSavedQuery $saved) { $datasource = id(new PhabricatorProjectLogicalDatasource()) ->setViewer($this->requireViewer()); $projects = $saved->getParameter('projects', array()); $constraints = $datasource->evaluateTokens($projects); if ($constraints) { $query->withEdgeLogicConstraints( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $constraints); } + + return $this; } /* -( Applications )------------------------------------------------------- */ protected function getApplicationURI($path = '') { return $this->getApplication()->getApplicationURI($path); } protected function getApplication() { if (!$this->application) { $class = $this->getApplicationClassName(); $this->application = id(new PhabricatorApplicationQuery()) ->setViewer($this->requireViewer()) ->withClasses(array($class)) ->withInstalled(true) ->executeOne(); if (!$this->application) { throw new Exception( pht( 'Application "%s" is not installed!', $class)); } } return $this->application; } abstract public function getApplicationClassName(); /* -( Constructing Engines )----------------------------------------------- */ /** * Load all available application search engines. * * @return list All available engines. * @task construct */ public static function getAllEngines() { $engines = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); return $engines; } /** * Get an engine by class name, if it exists. * * @return PhabricatorApplicationSearchEngine|null Engine, or null if it does * not exist. * @task construct */ public static function getEngineByClassName($class_name) { return idx(self::getAllEngines(), $class_name); } /* -( Builtin Queries )---------------------------------------------------- */ /** * @task builtin */ public function getBuiltinQueries() { $names = $this->getBuiltinQueryNames(); $queries = array(); $sequence = 0; foreach ($names as $key => $name) { $queries[$key] = id(new PhabricatorNamedQuery()) ->setUserPHID($this->requireViewer()->getPHID()) ->setEngineClassName(get_class($this)) ->setQueryName($name) ->setQueryKey($key) ->setSequence((1 << 24) + $sequence++) ->setIsBuiltin(true); } return $queries; } /** * @task builtin */ public function getBuiltinQuery($query_key) { if (!$this->isBuiltinQuery($query_key)) { throw new Exception(pht("'%s' is not a builtin!", $query_key)); } return idx($this->getBuiltinQueries(), $query_key); } /** * @task builtin */ protected function getBuiltinQueryNames() { return array(); } /** * @task builtin */ public function isBuiltinQuery($query_key) { $builtins = $this->getBuiltinQueries(); return isset($builtins[$query_key]); } /** * @task builtin */ public function buildSavedQueryFromBuiltin($query_key) { throw new Exception(pht("Builtin '%s' is not supported!", $query_key)); } /* -( Reading Utilities )--------------------------------------------------- */ /** * Read a list of user PHIDs from a request in a flexible way. This method * supports either of these forms: * * users[]=alincoln&users[]=htaft * users=alincoln,htaft * * Additionally, users can be specified either by PHID or by name. * * The main goal of this flexibility is to allow external programs to generate * links to pages (like "alincoln's open revisions") without needing to make * API calls. * * @param AphrontRequest Request to read user PHIDs from. * @param string Key to read in the request. * @param list Other permitted PHID types. * @return list List of user PHIDs and selector functions. * @task read */ protected function readUsersFromRequest( AphrontRequest $request, $key, array $allow_types = array()) { $list = $this->readListFromRequest($request, $key); $phids = array(); $names = array(); $allow_types = array_fuse($allow_types); $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; foreach ($list as $item) { $type = phid_get_type($item); if ($type == $user_type) { $phids[] = $item; } else if (isset($allow_types[$type])) { $phids[] = $item; } else { if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) { // If this is a function, pass it through unchanged; we'll evaluate // it later. $phids[] = $item; } else { $names[] = $item; } } } if ($names) { $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->requireViewer()) ->withUsernames($names) ->execute(); foreach ($users as $user) { $phids[] = $user->getPHID(); } $phids = array_unique($phids); } return $phids; } /** * Read a list of project PHIDs from a request in a flexible way. * * @param AphrontRequest Request to read user PHIDs from. * @param string Key to read in the request. * @return list List of projet PHIDs and selector functions. * @task read */ protected function readProjectsFromRequest(AphrontRequest $request, $key) { $list = $this->readListFromRequest($request, $key); $phids = array(); $slugs = array(); $project_type = PhabricatorProjectProjectPHIDType::TYPECONST; foreach ($list as $item) { $type = phid_get_type($item); if ($type == $project_type) { $phids[] = $item; } else { if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) { // If this is a function, pass it through unchanged; we'll evaluate // it later. $phids[] = $item; } else { $slugs[] = $item; } } } if ($slugs) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->requireViewer()) ->withSlugs($slugs) ->execute(); foreach ($projects as $project) { $phids[] = $project->getPHID(); } $phids = array_unique($phids); } return $phids; } /** * Read a list of subscribers from a request in a flexible way. * * @param AphrontRequest Request to read PHIDs from. * @param string Key to read in the request. * @return list List of object PHIDs. * @task read */ protected function readSubscribersFromRequest( AphrontRequest $request, $key) { return $this->readUsersFromRequest( $request, $key, array( PhabricatorProjectProjectPHIDType::TYPECONST, )); } /** * Read a list of generic PHIDs from a request in a flexible way. Like * @{method:readUsersFromRequest}, this method supports either array or * comma-delimited forms. Objects can be specified either by PHID or by * object name. * * @param AphrontRequest Request to read PHIDs from. * @param string Key to read in the request. * @param list Optional, list of permitted PHID types. * @return list List of object PHIDs. * * @task read */ protected function readPHIDsFromRequest( AphrontRequest $request, $key, array $allow_types = array()) { $list = $this->readListFromRequest($request, $key); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->requireViewer()) ->withNames($list) ->execute(); $list = mpull($objects, 'getPHID'); if (!$list) { return array(); } // If only certain PHID types are allowed, filter out all the others. if ($allow_types) { $allow_types = array_fuse($allow_types); foreach ($list as $key => $phid) { if (empty($allow_types[phid_get_type($phid)])) { unset($list[$key]); } } } return $list; } /** * Read a list of items from the request, in either array format or string * format: * * list[]=item1&list[]=item2 * list=item1,item2 * * This provides flexibility when constructing URIs, especially from external * sources. * * @param AphrontRequest Request to read strings from. * @param string Key to read in the request. * @return list List of values. */ protected function readListFromRequest( AphrontRequest $request, $key) { $list = $request->getArr($key, null); if ($list === null) { $list = $request->getStrList($key); } if (!$list) { return array(); } return $list; } protected function readDateFromRequest( AphrontRequest $request, $key) { $value = AphrontFormDateControlValue::newFromRequest($request, $key); if ($value->isEmpty()) { return null; } return $value->getDictionary(); } protected function readBoolFromRequest( AphrontRequest $request, $key) { if (!strlen($request->getStr($key))) { return null; } return $request->getBool($key); } protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) { $value = $query->getParameter($key); if ($value === null) { return $value; } return $value ? 'true' : 'false'; } /* -( Dates )-------------------------------------------------------------- */ /** * @task dates */ protected function parseDateTime($date_time) { if (!strlen($date_time)) { return null; } return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer()); } /** * @task dates */ protected function buildDateRange( AphrontFormView $form, PhabricatorSavedQuery $saved_query, $start_key, $start_name, $end_key, $end_name) { $start_str = $saved_query->getParameter($start_key); $start = null; if (strlen($start_str)) { $start = $this->parseDateTime($start_str); if (!$start) { $this->addError( pht( '"%s" date can not be parsed.', $start_name)); } } $end_str = $saved_query->getParameter($end_key); $end = null; if (strlen($end_str)) { $end = $this->parseDateTime($end_str); if (!$end) { $this->addError( pht( '"%s" date can not be parsed.', $end_name)); } } if ($start && $end && ($start >= $end)) { $this->addError( pht( '"%s" must be a date before "%s".', $start_name, $end_name)); } $form ->appendChild( id(new PHUIFormFreeformDateControl()) ->setName($start_key) ->setLabel($start_name) ->setValue($start_str)) ->appendChild( id(new AphrontFormTextControl()) ->setName($end_key) ->setLabel($end_name) ->setValue($end_str)); } /* -( Paging and Executing Queries )--------------------------------------- */ public function getPageSize(PhabricatorSavedQuery $saved) { $limit = (int)$saved->getParameter('limit'); if ($limit > 0) { return $limit; } return 100; } public function shouldUseOffsetPaging() { return false; } public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) { if ($this->shouldUseOffsetPaging()) { $pager = new PHUIPagerView(); } else { $pager = new AphrontCursorPagerView(); } $page_size = $this->getPageSize($saved); if (is_finite($page_size)) { $pager->setPageSize($page_size); } else { // Consider an INF pagesize to mean a large finite pagesize. // TODO: It would be nice to handle this more gracefully, but math // with INF seems to vary across PHP versions, systems, and runtimes. $pager->setPageSize(0xFFFF); } return $pager; } public function executeQuery( PhabricatorPolicyAwareQuery $query, AphrontView $pager) { $query->setViewer($this->requireViewer()); if ($this->shouldUseOffsetPaging()) { $objects = $query->executeWithOffsetPager($pager); } else { $objects = $query->executeWithCursorPager($pager); } return $objects; } /* -( Rendering )---------------------------------------------------------- */ public function setRequest(AphrontRequest $request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } public function renderResults( array $objects, PhabricatorSavedQuery $query) { $phids = $this->getRequiredHandlePHIDsForResultList($objects, $query); if ($phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->witHPHIDs($phids) ->execute(); } else { $handles = array(); } return $this->renderResultList($objects, $query, $handles); } protected function getRequiredHandlePHIDsForResultList( array $objects, PhabricatorSavedQuery $query) { return array(); } protected function renderResultList( array $objects, PhabricatorSavedQuery $query, array $handles) { throw new Exception(pht('Not supported here yet!')); } /* -( Application Search )------------------------------------------------- */ /** * Retrieve an object to use to define custom fields for this search. * * To integrate with custom fields, subclasses should override this method * and return an instance of the application object which implements * @{interface:PhabricatorCustomFieldInterface}. * * @return PhabricatorCustomFieldInterface|null Object with custom fields. * @task appsearch */ public function getCustomFieldObject() { $object = $this->newResultObject(); if ($object instanceof PhabricatorCustomFieldInterface) { return $object; } return null; } /** * Get the custom fields for this search. * * @return PhabricatorCustomFieldList|null Custom fields, if this search * supports custom fields. * @task appsearch */ public function getCustomFieldList() { if ($this->customFields === false) { $object = $this->getCustomFieldObject(); if ($object) { $fields = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); $fields->setViewer($this->requireViewer()); } else { $fields = null; } $this->customFields = $fields; } return $this->customFields; } /** * Applies data from a saved query to an executable query. * * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. * @param PhabricatorSavedQuery Saved query to read. * @return void */ protected function applyCustomFieldsToQuery( PhabricatorCursorPagedPolicyAwareQuery $query, PhabricatorSavedQuery $saved) { $list = $this->getCustomFieldList(); if (!$list) { return; } foreach ($list->getFields() as $field) { $value = $field->applyApplicationSearchConstraintToQuery( $this, $query, $saved->getParameter('custom:'.$field->getFieldIndex())); } } private function buildCustomFieldSearchFields() { $list = $this->getCustomFieldList(); if (!$list) { return array(); } $fields = array(); foreach ($list->getFields() as $field) { $fields[] = id(new PhabricatorSearchCustomFieldProxyField()) ->setSearchEngine($this) ->setCustomField($field); } return $fields; } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php index 82087bbaee..dc7ed2d1e4 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php @@ -1,168 +1,169 @@ getFieldValue(); if (is_array($value)) { foreach ($value as $phid) { $indexes[] = $this->newStringIndex($phid); } } return $indexes; } public function readValueFromRequest(AphrontRequest $request) { $value = $request->getArr($this->getFieldKey()); $this->setFieldValue($value); } public function getValueForStorage() { $value = $this->getFieldValue(); if (!$value) { return null; } return json_encode(array_values($value)); } public function setValueFromStorage($value) { $result = array(); if ($value) { $value = json_decode($value, true); if (is_array($value)) { $result = array_values($value); } } $this->setFieldValue($value); + return $this; } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return $request->getArr($this->getFieldKey()); } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if ($value) { $query->withApplicationSearchContainsConstraint( $this->newStringIndex(null), $value); } } public function getRequiredHandlePHIDsForPropertyView() { $value = $this->getFieldValue(); if ($value) { return $value; } return array(); } public function renderPropertyViewValue(array $handles) { $value = $this->getFieldValue(); if (!$value) { return null; } $handles = mpull($handles, 'renderLink'); $handles = phutil_implode_html(', ', $handles); return $handles; } public function getRequiredHandlePHIDsForEdit() { $value = $this->getFieldValue(); if ($value) { return $value; } else { return array(); } } public function getApplicationTransactionRequiredHandlePHIDs( PhabricatorApplicationTransaction $xaction) { $old = json_decode($xaction->getOldValue()); if (!is_array($old)) { $old = array(); } $new = json_decode($xaction->getNewValue()); if (!is_array($new)) { $new = array(); } $add = array_diff($new, $old); $rem = array_diff($old, $new); return array_merge($add, $rem); } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = json_decode($xaction->getOldValue()); if (!is_array($old)) { $old = array(); } $new = phutil_json_decode($xaction->getNewValue()); if (!is_array($new)) { $new = array(); } $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && !$rem) { return pht( '%s updated %s, added %d: %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), new PhutilNumber(count($add)), $xaction->renderHandleList($add)); } else if ($rem && !$add) { return pht( '%s updated %s, removed %d: %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), new PhutilNumber(count($rem)), $xaction->renderHandleList($rem)); } else { return pht( '%s updated %s, added %d: %s; removed %d: %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), new PhutilNumber(count($add)), $xaction->renderHandleList($add), new PhutilNumber(count($rem)), $xaction->renderHandleList($rem)); } } public function shouldAppearInHerald() { return true; } public function getHeraldFieldConditions() { return array( HeraldAdapter::CONDITION_INCLUDE_ALL, HeraldAdapter::CONDITION_INCLUDE_ANY, HeraldAdapter::CONDITION_INCLUDE_NONE, HeraldAdapter::CONDITION_EXISTS, HeraldAdapter::CONDITION_NOT_EXISTS, ); } }