public function processRequest() { $drequest = $this->getDiffusionRequest(); $request = $this->getRequest(); $user = $request->getUser(); if (!$request->isAjax()) { // This request came out of the dropdown menu, either "View Standalone" // or "View Raw File". $view = $request->getStr('view'); if ($view == 'r') { $uri = $drequest->generateURI(array('action' => 'browse', 'params' => array('view' => 'raw'))); } else { $uri = $drequest->generateURI(array('action' => 'change')); } return id(new AphrontRedirectResponse())->setURI($uri); } $diff_query = DiffusionDiffQuery::newFromDiffusionRequest($drequest); $changeset = $diff_query->loadChangeset(); if (!$changeset) { return new Aphront404Response(); } $parser = new DifferentialChangesetParser(); $parser->setUser($user); $parser->setChangeset($changeset); $parser->setRenderingReference($diff_query->getRenderingReference()); $parser->setMarkupEngine(PhabricatorMarkupEngine::newDiffusionMarkupEngine()); $pquery = new DiffusionPathIDQuery(array($changeset->getFilename())); $ids = $pquery->loadPathIDs(); $path_id = $ids[$changeset->getFilename()]; $parser->setLeftSideCommentMapping($path_id, false); $parser->setRightSideCommentMapping($path_id, true); $parser->setWhitespaceMode(DifferentialChangesetParser::WHITESPACE_SHOW_ALL); $inlines = id(new PhabricatorAuditInlineComment())->loadAllWhere('commitPHID = %s AND pathID = %d AND (authorPHID = %s OR auditCommentID IS NOT NULL)', $drequest->loadCommit()->getPHID(), $path_id, $user->getPHID()); if ($inlines) { foreach ($inlines as $inline) { $parser->parseInlineComment($inline); } $phids = mpull($inlines, 'getAuthorPHID'); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $parser->setHandles($handles); } $spec = $request->getStr('range'); list($range_s, $range_e, $mask) = DifferentialChangesetParser::parseRangeSpecification($spec); $output = $parser->render($range_s, $range_e, $mask); return id(new PhabricatorChangesetResponse())->setRenderedChangeset($output); }
public function testExpandEdgeCases() { $this->assertEqual(array('/'), DiffusionPathIDQuery::expandPathToRoot('/')); $this->assertEqual(array('/'), DiffusionPathIDQuery::expandPathToRoot('//')); $this->assertEqual(array('/a/b', '/a', '/'), DiffusionPathIDQuery::expandPathToRoot('/a/b')); $this->assertEqual(array('/a/b', '/a', '/'), DiffusionPathIDQuery::expandPathToRoot('/a//b')); $this->assertEqual(array('/a/b', '/a', '/'), DiffusionPathIDQuery::expandPathToRoot('a/b')); }
protected function executeQuery() { $drequest = $this->getRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $commit_hash = $drequest->getCommit(); $path = DiffusionPathIDQuery::normalizePath($path); list($stdout) = $repository->execxLocalCommand('log --template %s --limit %d --branch %s --rev %s:0 -- %s', '{node}\\n', $this->getOffset() + $this->getLimit(), $drequest->getBranch(), $commit_hash, nonempty(ltrim($path, '/'), '.')); $hashes = explode("\n", $stdout); $hashes = array_filter($hashes); $hashes = array_slice($hashes, $this->getOffset()); return $this->loadHistoryForCommitIdentifiers($hashes); }
protected function executeQuery() { $drequest = $this->getRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $commit_hash = $drequest->getStableCommitName(); $path = DiffusionPathIDQuery::normalizePath($path); // NOTE: Using '' as a default path produces the correct behavior if HEAD // is a merge commit; using '.' does not (the merge commit is not included // in the log). $default_path = ''; list($stdout) = $repository->execxLocalCommand('log --debug --template %s --limit %d --branch %s --rev %s:0 -- %s', '{node};{parents}\\n', $this->getOffset() + $this->getLimit(), $drequest->getBranch(), $commit_hash, nonempty(ltrim($path, '/'), $default_path)); $lines = explode("\n", trim($stdout)); $lines = array_slice($lines, $this->getOffset()); $hash_list = array(); $parent_map = array(); $last = null; foreach (array_reverse($lines) as $line) { list($hash, $parents) = explode(';', $line); $parents = trim($parents); if (!$parents) { if ($last === null) { $parent_map[$hash] = array('...'); } else { $parent_map[$hash] = array($last); } } else { $parents = preg_split('/\\s+/', $parents); foreach ($parents as $parent) { list($plocal, $phash) = explode(':', $parent); if (!preg_match('/^0+$/', $phash)) { $parent_map[$hash][] = $phash; } } // This may happen for the zeroth commit in repository, both hashes // are "000000000...". if (empty($parent_map[$hash])) { $parent_map[$hash] = array('...'); } } // The rendering code expects the first commit to be "mainline", like // Git. Flip the order so it does the right thing. $parent_map[$hash] = array_reverse($parent_map[$hash]); $hash_list[] = $hash; $last = $hash; } $hash_list = array_reverse($hash_list); $this->parents = $parent_map; return $this->loadHistoryForCommitIdentifiers($hash_list); }
protected final function loadHistoryForCommitIdentifiers(array $identifiers) { if (!$identifiers) { return array(); } $drequest = $this->getRequest(); $repository = $drequest->getRepository(); $commits = self::loadCommitsByIdentifiers($identifiers); $path = $drequest->getPath(); $conn_r = $repository->establishConnection('r'); $path_normal = DiffusionPathIDQuery::normalizePath($path); $paths = queryfx_all($conn_r, 'SELECT id, path FROM %T WHERE pathHash IN (%Ls)', PhabricatorRepository::TABLE_PATH, array(md5($path_normal))); $paths = ipull($paths, 'id', 'path'); $path_id = idx($paths, $path_normal); $path_changes = queryfx_all($conn_r, 'SELECT * FROM %T WHERE commitID IN (%Ld) AND pathID = %d', PhabricatorRepository::TABLE_PATHCHANGE, mpull($commits, 'getID'), $path_id); $path_changes = ipull($path_changes, null, 'commitID'); $history = array(); foreach ($identifiers as $identifier) { $item = new DiffusionPathChange(); $item->setCommitIdentifier($identifier); $commit = idx($commits, $identifier); if ($commit) { $item->setCommit($commit); try { $item->setCommitData($commit->getCommitData()); } catch (Exception $ex) { // Ignore, commit just doesn't have data. } $change = idx($path_changes, $commit->getID()); if ($change) { $item->setChangeType($change['changeType']); $item->setFileType($change['fileType']); } } $history[] = $item; } return $history; }
protected function executeQuery() { $drequest = $this->getRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $commit = $drequest->getCommit(); $subpath = $repository->getDetail('svn-subpath'); if ($subpath && strncmp($subpath, $path, strlen($subpath))) { // If we have a subpath and the path isn't a child of it, it (almost // certainly) won't exist since we don't track commits which affect // it. (Even if it exists, return a consistent result.) $this->reason = self::REASON_IS_UNTRACKED_PARENT; return array(); } $conn_r = $repository->establishConnection('r'); $parent_path = DiffusionPathIDQuery::getParentPath($path); $path_query = new DiffusionPathIDQuery(array($path, $parent_path)); $path_map = $path_query->loadPathIDs(); $path_id = $path_map[$path]; $parent_path_id = $path_map[$parent_path]; if (empty($path_id)) { $this->reason = self::REASON_IS_NONEXISTENT; return array(); } if ($commit) { $slice_clause = 'AND svnCommit <= ' . (int) $commit; } else { $slice_clause = ''; } $index = queryfx_all($conn_r, 'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE repositoryID = %d AND parentID = %d %Q GROUP BY pathID', PhabricatorRepository::TABLE_FILESYSTEM, $repository->getID(), $path_id, $slice_clause); if (!$index) { if ($path == '/') { $this->reason = self::REASON_IS_EMPTY; } else { // NOTE: The parent path ID is included so this query can take // advantage of the table's primary key; it is uniquely determined by // the pathID but if we don't do the lookup ourselves MySQL doesn't have // the information it needs to avoid a table scan. $reasons = queryfx_all($conn_r, 'SELECT * FROM %T WHERE repositoryID = %d AND parentID = %d AND pathID = %d %Q ORDER BY svnCommit DESC LIMIT 2', PhabricatorRepository::TABLE_FILESYSTEM, $repository->getID(), $parent_path_id, $path_id, $slice_clause); $reason = reset($reasons); if (!$reason) { $this->reason = self::REASON_IS_NONEXISTENT; } else { $file_type = $reason['fileType']; if (empty($reason['existed'])) { $this->reason = self::REASON_IS_DELETED; $this->deletedAtCommit = $reason['svnCommit']; if (!empty($reasons[1])) { $this->existedAtCommit = $reasons[1]['svnCommit']; } } else { if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { $this->reason = self::REASON_IS_EMPTY; } else { $this->reason = self::REASON_IS_FILE; } } } } return array(); } if ($this->shouldOnlyTestValidity()) { return true; } $sql = array(); foreach ($index as $row) { $sql[] = '(' . (int) $row['pathID'] . ', ' . (int) $row['maxCommit'] . ')'; } $browse = queryfx_all($conn_r, 'SELECT *, p.path pathName FROM %T f JOIN %T p ON f.pathID = p.id WHERE repositoryID = %d AND parentID = %d AND existed = 1 AND (pathID, svnCommit) in (%Q) ORDER BY pathName', PhabricatorRepository::TABLE_FILESYSTEM, PhabricatorRepository::TABLE_PATH, $repository->getID(), $path_id, implode(', ', $sql)); $loadable_commits = array(); foreach ($browse as $key => $file) { // We need to strip out directories because we don't store last-modified // in the filesystem table. if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) { $loadable_commits[] = $file['svnCommit']; $browse[$key]['hasCommit'] = true; } } $commits = array(); $commit_data = array(); if ($loadable_commits) { // NOTE: Even though these are integers, use '%Ls' because MySQL doesn't // use the second part of the key otherwise! $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere('repositoryID = %d AND commitIdentifier IN (%Ls)', $repository->getID(), $loadable_commits); $commits = mpull($commits, null, 'getCommitIdentifier'); if ($commits) { $commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere('commitID in (%Ld)', mpull($commits, 'getID')); $commit_data = mpull($commit_data, null, 'getCommitID'); } else { $commit_data = array(); } } $path_normal = DiffusionPathIDQuery::normalizePath($path); $results = array(); foreach ($browse as $file) { $full_path = $file['pathName']; $file_path = ltrim(substr($full_path, strlen($path_normal)), '/'); $full_path = ltrim($full_path, '/'); $result = new DiffusionRepositoryPath(); $result->setPath($file_path); $result->setFullPath($full_path); // $result->setHash($hash); $result->setFileType($file['fileType']); // $result->setFileSize($size); if (!empty($file['hasCommit'])) { $commit = idx($commits, $file['svnCommit']); if ($commit) { $data = idx($commit_data, $commit->getID()); $result->setLastModifiedCommit($commit); $result->setLastCommitData($data); } } $results[] = $result; } if (empty($results)) { $this->reason = self::REASON_IS_EMPTY; } return $results; }
/** * Update the table which links Differential revisions to paths they affect, * so Diffusion can efficiently find pending revisions for a given file. */ private function updateAffectedPathTable(DifferentialRevision $revision, DifferentialDiff $diff) { $repository = $revision->getRepository(); if (!$repository) { // The repository where the code lives is untracked. return; } $path_prefix = null; $local_root = $diff->getSourceControlPath(); if ($local_root) { // We're in a working copy which supports subdirectory checkouts (e.g., // SVN) so we need to figure out what prefix we should add to each path // (e.g., trunk/projects/example/) to get the absolute path from the // root of the repository. DVCS systems like Git and Mercurial are not // affected. // Normalize both paths and check if the repository root is a prefix of // the local root. If so, throw it away. Note that this correctly handles // the case where the remote path is "/". $local_root = id(new PhutilURI($local_root))->getPath(); $local_root = rtrim($local_root, '/'); $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath(); $repo_root = rtrim($repo_root, '/'); if (!strncmp($repo_root, $local_root, strlen($repo_root))) { $path_prefix = substr($local_root, strlen($repo_root)); } } $changesets = $diff->getChangesets(); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $path_prefix . '/' . $changeset->getFilename(); } // Mark this as also touching all parent paths, so you can see all pending // changes to any file within a directory. $all_paths = array(); foreach ($paths as $local) { foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) { $all_paths[$path] = true; } } $all_paths = array_keys($all_paths); $path_ids = PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths($all_paths); $table = new DifferentialAffectedPath(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($path_ids as $path_id) { $sql[] = qsprintf($conn_w, '(%d, %d, %d, %d)', $repository->getID(), $path_id, time(), $revision->getID()); } queryfx($conn_w, 'DELETE FROM %T WHERE revisionID = %d', $table->getTableName(), $revision->getID()); foreach (array_chunk($sql, 256) as $chunk) { queryfx($conn_w, 'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q', $table->getTableName(), implode(', ', $chunk)); } }
public function processRequest() { $drequest = $this->getDiffusionRequest(); $request = $this->getRequest(); $user = $request->getUser(); $callsign = $drequest->getRepository()->getCallsign(); $content = array(); $content[] = $this->buildCrumbs(array('commit' => true)); $repository = $drequest->getRepository(); $commit = $drequest->loadCommit(); if (!$commit) { // TODO: Make more user-friendly. throw new Exception('This commit has not parsed yet.'); } $commit_data = $drequest->loadCommitData(); $commit->attachCommitData($commit_data); $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub'); if ($is_foreign) { $subpath = $commit_data->getCommitDetail('svn-subpath'); $error_panel = new AphrontErrorView(); $error_panel->setWidth(AphrontErrorView::WIDTH_WIDE); $error_panel->setTitle('Commit Not Tracked'); $error_panel->setSeverity(AphrontErrorView::SEVERITY_WARNING); $error_panel->appendChild("This Diffusion repository is configured to track only one " . "subdirectory of the entire Subversion repository, and this commit " . "didn't affect the tracked subdirectory ('" . phutil_escape_html($subpath) . "'), so no information is available."); $content[] = $error_panel; } else { $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); require_celerity_resource('diffusion-commit-view-css'); require_celerity_resource('phabricator-remarkup-css'); $parent_query = DiffusionCommitParentsQuery::newFromDiffusionRequest($drequest); $headsup_panel = new AphrontHeadsupView(); $headsup_panel->setHeader('Commit Detail'); $headsup_panel->setActionList($this->renderHeadsupActionList($commit)); $headsup_panel->setProperties($this->getCommitProperties($commit, $commit_data, $parent_query->loadParents())); $headsup_panel->appendChild('<div class="diffusion-commit-message phabricator-remarkup">' . $engine->markupText($commit_data->getCommitMessage()) . '</div>'); $content[] = $headsup_panel; } $query = new PhabricatorAuditQuery(); $query->withCommitPHIDs(array($commit->getPHID())); $audit_requests = $query->execute(); $this->auditAuthorityPHIDs = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user); $content[] = $this->buildAuditTable($commit, $audit_requests); $content[] = $this->buildComments($commit); $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest($drequest); $changes = $change_query->loadChanges(); $content[] = $this->buildMergesTable($commit); $original_changes_count = count($changes); if ($request->getStr('show_all') !== 'true' && $original_changes_count > self::CHANGES_LIMIT) { $changes = array_slice($changes, 0, self::CHANGES_LIMIT); } $change_table = new DiffusionCommitChangeTableView(); $change_table->setDiffusionRequest($drequest); $change_table->setPathChanges($changes); $count = count($changes); $bad_commit = null; if ($count == 0) { $bad_commit = queryfx_one(id(new PhabricatorRepository())->establishConnection('r'), 'SELECT * FROM %T WHERE fullCommitName = %s', PhabricatorRepository::TABLE_BADCOMMIT, 'r' . $callsign . $commit->getCommitIdentifier()); } if ($bad_commit) { $error_panel = new AphrontErrorView(); $error_panel->setWidth(AphrontErrorView::WIDTH_WIDE); $error_panel->setTitle('Bad Commit'); $error_panel->appendChild(phutil_escape_html($bad_commit['description'])); $content[] = $error_panel; } else { if ($is_foreign) { // Don't render anything else. } else { if (!count($changes)) { $no_changes = new AphrontErrorView(); $no_changes->setWidth(AphrontErrorView::WIDTH_WIDE); $no_changes->setSeverity(AphrontErrorView::SEVERITY_WARNING); $no_changes->setTitle('Not Yet Parsed'); // TODO: This can also happen with weird SVN changes that don't do // anything (or only alter properties?), although the real no-changes case // is extremely rare and might be impossible to produce organically. We // should probably write some kind of "Nothing Happened!" change into the // DB once we parse these changes so we can distinguish between // "not parsed yet" and "no changes". $no_changes->appendChild("This commit hasn't been fully parsed yet (or doesn't affect any " . "paths)."); $content[] = $no_changes; } else { $change_panel = new AphrontPanelView(); $change_panel->setHeader("Changes (" . number_format($count) . ")"); if ($count !== $original_changes_count) { $show_all_button = phutil_render_tag('a', array('class' => 'button green', 'href' => '?show_all=true'), phutil_escape_html('Show All Changes')); $warning_view = id(new AphrontErrorView())->setSeverity(AphrontErrorView::SEVERITY_WARNING)->setTitle(sprintf("Showing only the first %d changes out of %s!", self::CHANGES_LIMIT, number_format($original_changes_count))); $change_panel->appendChild($warning_view); $change_panel->addButton($show_all_button); } $change_panel->appendChild($change_table); $content[] = $change_panel; $changesets = DiffusionPathChange::convertToDifferentialChangesets($changes); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $vcs_supports_directory_changes = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $vcs_supports_directory_changes = false; break; default: throw new Exception("Unknown VCS."); } $references = array(); foreach ($changesets as $key => $changeset) { $file_type = $changeset->getFileType(); if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { if (!$vcs_supports_directory_changes) { unset($changesets[$key]); continue; } } $references[$key] = $drequest->generateURI(array('action' => 'rendering-ref', 'path' => $changeset->getFilename())); } // TODO: Some parts of the views still rely on properties of the // DifferentialChangeset. Make the objects ephemeral to make sure we don't // accidentally save them, and then set their ID to the appropriate ID for // this application (the path IDs). $pquery = new DiffusionPathIDQuery(mpull($changesets, 'getFilename')); $path_ids = $pquery->loadPathIDs(); foreach ($changesets as $changeset) { $changeset->makeEphemeral(); $changeset->setID($path_ids[$changeset->getFilename()]); } $change_list = new DifferentialChangesetListView(); $change_list->setChangesets($changesets); $change_list->setRenderingReferences($references); $change_list->setRenderURI('/diffusion/' . $callsign . '/diff/'); $change_list->setRepository($repository); $change_list->setUser($user); $change_list->setStandaloneURI('/diffusion/' . $callsign . '/diff/'); $change_list->setRawFileURIs(null, '/diffusion/' . $callsign . '/diff/?view=r'); $change_list->setInlineCommentControllerURI('/diffusion/inline/' . phutil_escape_uri($commit->getPHID()) . '/'); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); $change_list = '<div class="differential-primary-pane">' . $change_list->render() . '</div>'; $content[] = $change_list; } } } $content[] = $this->buildAddCommentView($commit, $audit_requests); return $this->buildStandardPageResponse($content, array('title' => 'r' . $callsign . $commit->getCommitIdentifier())); }
/** * Update the table which links Differential revisions to paths they affect, * so Diffusion can efficiently find pending revisions for a given file. */ private function updateAffectedPathTable(DifferentialRevision $revision, DifferentialDiff $diff, array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $project = $diff->loadArcanistProject(); if (!$project) { // Probably an old revision from before projects. return; } $repository = $project->loadRepository(); if (!$repository) { // Probably no project <-> repository link, or the repository where the // project lives is untracked. return; } $path_prefix = null; $local_root = $diff->getSourceControlPath(); if ($local_root) { // We're in a working copy which supports subdirectory checkouts (e.g., // SVN) so we need to figure out what prefix we should add to each path // (e.g., trunk/projects/example/) to get the absolute path from the // root of the repository. DVCS systems like Git and Mercurial are not // affected. // Normalize both paths and check if the repository root is a prefix of // the local root. If so, throw it away. Note that this correctly handles // the case where the remote path is "/". $local_root = id(new PhutilURI($local_root))->getPath(); $local_root = rtrim($local_root, '/'); $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath(); $repo_root = rtrim($repo_root, '/'); if (!strncmp($repo_root, $local_root, strlen($repo_root))) { $path_prefix = substr($local_root, strlen($repo_root)); } } $paths = array(); foreach ($changesets as $changeset) { $paths[] = $path_prefix . '/' . $changeset->getFilename(); } // Mark this as also touching all parent paths, so you can see all pending // changes to any file within a directory. $all_paths = array(); foreach ($paths as $local) { foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) { $all_paths[$path] = true; } } $all_paths = array_keys($all_paths); $path_map = id(new DiffusionPathIDQuery($all_paths))->loadPathIDs(); $table = new DifferentialAffectedPath(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($all_paths as $path) { $path_id = idx($path_map, $path); if (!$path_id) { // Don't bother creating these, it probably means we're either adding // a file (in which case having this row is irrelevant since Diffusion // won't be querying for it) or something is misconfigured (in which // case we'd just be writing garbage). continue; } $sql[] = qsprintf($conn_w, '(%d, %d, %d, %d)', $repository->getID(), $path_id, time(), $revision->getID()); } queryfx($conn_w, 'DELETE FROM %T WHERE revisionID = %d', $table->getTableName(), $revision->getID()); foreach (array_chunk($sql, 256) as $chunk) { queryfx($conn_w, 'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q', $table->getTableName(), implode(', ', $chunk)); } }
public function handleRequest(AphrontRequest $request) { $response = $this->loadDiffusionContext(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); if (!$request->isAjax()) { // This request came out of the dropdown menu, either "View Standalone" // or "View Raw File". $view = $request->getStr('view'); if ($view == 'r') { $uri = $drequest->generateURI(array('action' => 'browse', 'params' => array('view' => 'raw'))); } else { $uri = $drequest->generateURI(array('action' => 'change')); } return id(new AphrontRedirectResponse())->setURI($uri); } $data = $this->callConduitWithDiffusionRequest('diffusion.diffquery', array('commit' => $drequest->getCommit(), 'path' => $drequest->getPath())); $drequest->updateSymbolicCommit($data['effectiveCommit']); $raw_changes = ArcanistDiffChange::newFromConduit($data['changes']); $diff = DifferentialDiff::newEphemeralFromRawChanges($raw_changes); $changesets = $diff->getChangesets(); $changeset = reset($changesets); if (!$changeset) { return new Aphront404Response(); } $parser = new DifferentialChangesetParser(); $parser->setUser($viewer); $parser->setChangeset($changeset); $parser->setRenderingReference($drequest->generateURI(array('action' => 'rendering-ref'))); $parser->readParametersFromRequest($request); $coverage = $drequest->loadCoverage(); if ($coverage) { $parser->setCoverage($coverage); } $commit = $drequest->loadCommit(); $pquery = new DiffusionPathIDQuery(array($changeset->getFilename())); $ids = $pquery->loadPathIDs(); $path_id = $ids[$changeset->getFilename()]; $parser->setLeftSideCommentMapping($path_id, false); $parser->setRightSideCommentMapping($path_id, true); $parser->setCanMarkDone($commit->getAuthorPHID() && $viewer->getPHID() == $commit->getAuthorPHID()); $parser->setObjectOwnerPHID($commit->getAuthorPHID()); $parser->setWhitespaceMode(DifferentialChangesetParser::WHITESPACE_SHOW_ALL); $inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments($viewer, $commit->getPHID(), $path_id); if ($inlines) { foreach ($inlines as $inline) { $parser->parseInlineComment($inline); } $phids = mpull($inlines, 'getAuthorPHID'); $handles = $this->loadViewerHandles($phids); $parser->setHandles($handles); } $engine = new PhabricatorMarkupEngine(); $engine->setViewer($viewer); foreach ($inlines as $inline) { $engine->addObject($inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); $parser->setMarkupEngine($engine); $spec = $request->getStr('range'); list($range_s, $range_e, $mask) = DifferentialChangesetParser::parseRangeSpecification($spec); $parser->setRange($range_s, $range_e); $parser->setMask($mask); return id(new PhabricatorChangesetResponse())->setRenderedChangeset($parser->renderChangeset())->setUndoTemplates($parser->getRenderer()->renderUndoTemplates()); }
protected function getMercurialResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $commit_hash = $request->getValue('commit'); $path = $request->getValue('path'); $offset = $request->getValue('offset'); $limit = $request->getValue('limit'); $path = DiffusionPathIDQuery::normalizePath($path); $path = ltrim($path, '/'); // NOTE: Older versions of Mercurial give different results for these // commands (see T1268): // // $ hg log -- '' // $ hg log // // All versions of Mercurial give different results for these commands // (merge commits are excluded with the "." version): // // $ hg log -- . // $ hg log // // If we don't have a path component in the query, omit it from the command // entirely to avoid these inconsistencies. // NOTE: When viewing the history of a file, we don't use "-b", because // Mercurial stops history at the branchpoint but we're interested in all // ancestors. When viewing history of a branch, we do use "-b", and thus // stop history (this is more consistent with the Mercurial worldview of // branches). if (strlen($path)) { $path_arg = csprintf('-- %s', $path); $branch_arg = ''; } else { $path_arg = ''; // NOTE: --branch used to be called --only-branch; use -b for // compatibility. $branch_arg = csprintf('-b %s', $drequest->getBranch()); } list($stdout) = $repository->execxLocalCommand('log --debug --template %s --limit %d %C --rev %s %C', '{node};{parents}\\n', $offset + $limit, $branch_arg, hgsprintf('reverse(ancestors(%s))', $commit_hash), $path_arg); $stdout = PhabricatorRepository::filterMercurialDebugOutput($stdout); $lines = explode("\n", trim($stdout)); $lines = array_slice($lines, $offset); $hash_list = array(); $parent_map = array(); $last = null; foreach (array_reverse($lines) as $line) { list($hash, $parents) = explode(';', $line); $parents = trim($parents); if (!$parents) { if ($last === null) { $parent_map[$hash] = array('...'); } else { $parent_map[$hash] = array($last); } } else { $parents = preg_split('/\\s+/', $parents); foreach ($parents as $parent) { list($plocal, $phash) = explode(':', $parent); if (!preg_match('/^0+$/', $phash)) { $parent_map[$hash][] = $phash; } } // This may happen for the zeroth commit in repository, both hashes // are "000000000...". if (empty($parent_map[$hash])) { $parent_map[$hash] = array('...'); } } // The rendering code expects the first commit to be "mainline", like // Git. Flip the order so it does the right thing. $parent_map[$hash] = array_reverse($parent_map[$hash]); $hash_list[] = $hash; $last = $hash; } $hash_list = array_reverse($hash_list); $this->parents = $parent_map; return DiffusionQuery::loadHistoryForCommitIdentifiers($hash_list, $drequest); }
protected function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $full_name = 'r' . $repository->getCallsign() . $commit->getCommitIdentifier(); echo "Parsing {$full_name}...\n"; if ($this->isBadCommit($full_name)) { echo "This commit is marked bad!\n"; return; } list($stdout) = $repository->execxLocalCommand('status -C --change %s', $commit->getCommitIdentifier()); $status = ArcanistMercurialParser::parseMercurialStatusDetails($stdout); $common_attributes = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'commitSequence' => $commit->getEpoch()); $changes = array(); // Like Git, Mercurial doesn't track directories directly. We need to infer // directory creation and removal by observing file creation and removal // and testing if the directories in question are previously empty (thus, // created) or subsequently empty (thus, removed). $maybe_new_directories = array(); $maybe_del_directories = array(); $all_directories = array(); // Parse the basic information from "hg status", which shows files that // were directly affected by the change. foreach ($status as $path => $path_info) { $path = '/' . $path; $flags = $path_info['flags']; $change_target = $path_info['from'] ? '/' . $path_info['from'] : null; $changes[$path] = array('path' => $path, 'isDirect' => true, 'targetPath' => $change_target, 'targetCommitID' => $change_target ? $commit->getID() : null, 'changeType' => null, 'fileType' => null, 'flags' => $flags) + $common_attributes; if ($flags & ArcanistRepositoryAPI::FLAG_ADDED) { $maybe_new_directories[] = dirname($path); } else { if ($flags & ArcanistRepositoryAPI::FLAG_DELETED) { $maybe_del_directories[] = dirname($path); } } $all_directories[] = dirname($path); } // Add change information for each source path which doesn't appear in the // status. These files were copied, but were not modified. We also know they // must exist. foreach ($changes as $path => $change) { $from = $change['targetPath']; if ($from && empty($changes[$from])) { $changes[$from] = array('path' => $from, 'isDirect' => false, 'targetPath' => null, 'targetCommitID' => null, 'changeType' => DifferentialChangeType::TYPE_COPY_AWAY, 'fileType' => null, 'flags' => 0) + $common_attributes; } } $away = array(); foreach ($changes as $path => $change) { $target_path = $change['targetPath']; if ($target_path) { $away[$target_path][] = $path; } } // Now that we have all the direct changes, figure out change types. foreach ($changes as $path => $change) { $flags = $change['flags']; $from = $change['targetPath']; if ($from) { $target = $changes[$from]; } else { $target = null; } if ($flags & ArcanistRepositoryAPI::FLAG_ADDED) { if ($target) { if ($target['flags'] & ArcanistRepositoryAPI::FLAG_DELETED) { $change_type = DifferentialChangeType::TYPE_MOVE_HERE; } else { $change_type = DifferentialChangeType::TYPE_COPY_HERE; } } else { $change_type = DifferentialChangeType::TYPE_ADD; } } else { if ($flags & ArcanistRepositoryAPI::FLAG_DELETED) { if (isset($away[$path])) { if (count($away[$path]) > 1) { $change_type = DifferentialChangeType::TYPE_MULTICOPY; } else { $change_type = DifferentialChangeType::TYPE_MOVE_AWAY; } } else { $change_type = DifferentialChangeType::TYPE_DELETE; } } else { if (isset($away[$path])) { $change_type = DifferentialChangeType::TYPE_COPY_AWAY; } else { $change_type = DifferentialChangeType::TYPE_CHANGE; } } } $changes[$path]['changeType'] = $change_type; } // Go through all the affected directories and identify any which were // actually added or deleted. $dir_status = array(); foreach ($maybe_del_directories as $dir) { $exists = false; foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) { if (isset($dir_status[$path])) { break; } // If we know some child exists, we know this path exists. If we don't // know that a child exists, test if this directory still exists. if (!$exists) { $exists = $this->mercurialPathExists($repository, $path, $commit->getCommitIdentifier()); } if ($exists) { $dir_status[$path] = DifferentialChangeType::TYPE_CHILD; } else { $dir_status[$path] = DifferentialChangeType::TYPE_DELETE; } } } list($stdout) = $repository->execxLocalCommand('parents --rev %s --style default', $commit->getCommitIdentifier()); $parents = ArcanistMercurialParser::parseMercurialLog($stdout); $parent = reset($parents); if ($parent) { // TODO: We should expand this to a full 40-character hash using "hg id". $parent = $parent['rev']; } foreach ($maybe_new_directories as $dir) { $exists = false; foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) { if (isset($dir_status[$path])) { break; } if (!$exists) { if ($parent) { $exists = $this->mercurialPathExists($repository, $path, $parent); } else { $exists = false; } } if ($exists) { $dir_status[$path] = DifferentialChangeType::TYPE_CHILD; } else { $dir_status[$path] = DifferentialChangeType::TYPE_ADD; } } } foreach ($all_directories as $dir) { foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) { if (isset($dir_status[$path])) { break; } $dir_status[$path] = DifferentialChangeType::TYPE_CHILD; } } // Merge all the directory statuses into the path statuses. foreach ($dir_status as $path => $status) { if (isset($changes[$path])) { // TODO: The UI probably doesn't handle any of these cases with // terrible elegance, but they are exceedingly rare. $existing_type = $changes[$path]['changeType']; if ($existing_type == DifferentialChangeType::TYPE_DELETE) { // This change removes a file, replaces it with a directory, and then // adds children of that directory. Mark it as a "change" instead, // and make the type a directory. $changes[$path]['fileType'] = DifferentialChangeType::FILE_DIRECTORY; $changes[$path]['changeType'] = DifferentialChangeType::TYPE_CHANGE; } else { if ($existing_type == DifferentialChangeType::TYPE_MOVE_AWAY || $existing_type == DifferentialChangeType::TYPE_MULTICOPY) { // This change moves or copies a file, replaces it with a directory, // and then adds children to that directory. Mark it as "copy away" // instead of whatever it was, and make the type a directory. $changes[$path]['fileType'] = DifferentialChangeType::FILE_DIRECTORY; $changes[$path]['changeType'] = DifferentialChangeType::TYPE_COPY_AWAY; } else { if ($existing_type == DifferentialChangeType::TYPE_ADD) { // This change removes a diretory and replaces it with a file. Mark // it as "change" instead of "add". $changes[$path]['changeType'] = DifferentialChangeType::TYPE_CHANGE; } } } continue; } $changes[$path] = array('path' => $path, 'isDirect' => $status == DifferentialChangeType::TYPE_CHILD ? false : true, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, 'changeType' => $status, 'targetPath' => null, 'targetCommitID' => null) + $common_attributes; } // TODO: use "hg diff --git" to figure out which files are symlinks. foreach ($changes as $path => $change) { if (empty($change['fileType'])) { $changes[$path]['fileType'] = DifferentialChangeType::FILE_NORMAL; } } $all_paths = array(); foreach ($changes as $path => $change) { $all_paths[$path] = true; if ($change['targetPath']) { $all_paths[$change['targetPath']] = true; } } $path_map = $this->lookupOrCreatePaths(array_keys($all_paths)); foreach ($changes as $key => $change) { $changes[$key]['pathID'] = $path_map[$change['path']]; if ($change['targetPath']) { $changes[$key]['targetPathID'] = $path_map[$change['targetPath']]; } else { $changes[$key]['targetPathID'] = null; } } $conn_w = $repository->establishConnection('w'); $changes_sql = array(); foreach ($changes as $change) { $values = array((int) $change['repositoryID'], (int) $change['pathID'], (int) $change['commitID'], $change['targetPathID'] ? (int) $change['targetPathID'] : 'null', $change['targetCommitID'] ? (int) $change['targetCommitID'] : 'null', (int) $change['changeType'], (int) $change['fileType'], (int) $change['isDirect'], (int) $change['commitSequence']); $changes_sql[] = '(' . implode(', ', $values) . ')'; } queryfx($conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit->getID()); foreach (array_chunk($changes_sql, 256) as $sql_chunk) { queryfx($conn_w, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, implode(', ', $sql_chunk)); } $this->finishParse(); }
protected function executeQuery() { $drequest = $this->getRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $commit_hash = $drequest->getStableCommitName(); $path = DiffusionPathIDQuery::normalizePath($path); $path = ltrim($path, '/'); // NOTE: Older versions of Mercurial give different results for these // commands (see T1268): // // $ hg log -- '' // $ hg log // // All versions of Mercurial give different results for these commands // (merge commits are excluded with the "." version): // // $ hg log -- . // $ hg log // // If we don't have a path component in the query, omit it from the command // entirely to avoid these inconsistencies. $path_arg = ''; if (strlen($path)) { $path_arg = csprintf('-- %s', $path); } // NOTE: --branch used to be called --only-branch; use -b for compatibility. list($stdout) = $repository->execxLocalCommand('log --debug --template %s --limit %d -b %s --rev %s:0 %C', '{node};{parents}\\n', $this->getOffset() + $this->getLimit(), $drequest->getBranch(), $commit_hash, $path_arg); $lines = explode("\n", trim($stdout)); $lines = array_slice($lines, $this->getOffset()); $hash_list = array(); $parent_map = array(); $last = null; foreach (array_reverse($lines) as $line) { list($hash, $parents) = explode(';', $line); $parents = trim($parents); if (!$parents) { if ($last === null) { $parent_map[$hash] = array('...'); } else { $parent_map[$hash] = array($last); } } else { $parents = preg_split('/\\s+/', $parents); foreach ($parents as $parent) { list($plocal, $phash) = explode(':', $parent); if (!preg_match('/^0+$/', $phash)) { $parent_map[$hash][] = $phash; } } // This may happen for the zeroth commit in repository, both hashes // are "000000000...". if (empty($parent_map[$hash])) { $parent_map[$hash] = array('...'); } } // The rendering code expects the first commit to be "mainline", like // Git. Flip the order so it does the right thing. $parent_map[$hash] = array_reverse($parent_map[$hash]); $hash_list[] = $hash; $last = $hash; } $hash_list = array_reverse($hash_list); $this->parents = $parent_map; return $this->loadHistoryForCommitIdentifiers($hash_list); }
protected final function loadHistoryForCommitIdentifiers(array $identifiers) { if (!$identifiers) { return array(); } $commits = array(); $commit_data = array(); $path_changes = array(); $drequest = $this->getRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere('repositoryID = %d AND commitIdentifier IN (%Ls)', $repository->getID(), $identifiers); $commits = mpull($commits, null, 'getCommitIdentifier'); if (!$commits) { return array(); } $commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere('commitID in (%Ld)', mpull($commits, 'getID')); $commit_data = mpull($commit_data, null, 'getCommitID'); $conn_r = $repository->establishConnection('r'); $path_normal = DiffusionPathIDQuery::normalizePath($path); $paths = queryfx_all($conn_r, 'SELECT id, path FROM %T WHERE path IN (%Ls)', PhabricatorRepository::TABLE_PATH, array($path_normal)); $paths = ipull($paths, 'id', 'path'); $path_id = idx($paths, $path_normal); $path_changes = queryfx_all($conn_r, 'SELECT * FROM %T WHERE commitID IN (%Ld) AND pathID = %d', PhabricatorRepository::TABLE_PATHCHANGE, mpull($commits, 'getID'), $path_id); $path_changes = ipull($path_changes, null, 'commitID'); $history = array(); foreach ($identifiers as $identifier) { $item = new DiffusionPathChange(); $item->setCommitIdentifier($identifier); $commit = idx($commits, $identifier); if ($commit) { $item->setCommit($commit); $data = idx($commit_data, $commit->getID()); if ($data) { $item->setCommitData($data); } $change = idx($path_changes, $commit->getID()); if ($change) { $item->setChangeType($change['changeType']); $item->setFileType($change['fileType']); } } $history[] = $item; } return $history; }