Path: blob/master/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
12242 views
<?php12final class DiffusionBrowseQueryConduitAPIMethod3extends DiffusionQueryConduitAPIMethod {45public function getAPIMethodName() {6return 'diffusion.browsequery';7}89public function getMethodDescription() {10return pht(11'File(s) information for a repository at an (optional) path and '.12'(optional) commit.');13}1415protected function defineReturnType() {16return 'array';17}1819protected function defineCustomParamTypes() {20return array(21'path' => 'optional string',22'commit' => 'optional string',23'needValidityOnly' => 'optional bool',24'limit' => 'optional int',25'offset' => 'optional int',26);27}2829protected function getResult(ConduitAPIRequest $request) {30$result = parent::getResult($request);31return $result->toDictionary();32}3334protected function getGitResult(ConduitAPIRequest $request) {35$drequest = $this->getDiffusionRequest();36$repository = $drequest->getRepository();3738$path = $request->getValue('path');39if ($path === null || !strlen($path) || $path === '/') {40$path = null;41}4243$commit = $request->getValue('commit');44$offset = (int)$request->getValue('offset');45$limit = (int)$request->getValue('limit');46$result = $this->getEmptyResultSet();4748if ($path === null) {49// Fast path to improve the performance of the repository view; we know50// the root is always a tree at any commit and always exists.51$path_type = 'tree';52} else {53try {54list($stdout) = $repository->execxLocalCommand(55'cat-file -t -- %s',56sprintf('%s:%s', $commit, $path));57$path_type = trim($stdout);58} catch (CommandException $e) {59// The "cat-file" command may fail if the path legitimately does not60// exist, but it may also fail if the path is a submodule. This can61// produce either "Not a valid object name" or "could not get object62// info".6364// To detect if we have a submodule, use `git ls-tree`. If the path65// is a submodule, we'll get a "160000" mode mask with type "commit".6667list($sub_err, $sub_stdout) = $repository->execLocalCommand(68'ls-tree %s -- %s',69gitsprintf('%s', $commit),70$path);71if (!$sub_err) {72// If the path failed "cat-file" but "ls-tree" worked, we assume it73// must be a submodule. If it is, the output will look something74// like this:75//76// 160000 commit <hash> <path>77//78// We make sure it has the 160000 mode mask to confirm that it's79// definitely a submodule.80$mode = (int)$sub_stdout;81if ($mode & 160000) {82$submodule_reason = DiffusionBrowseResultSet::REASON_IS_SUBMODULE;83$result84->setReasonForEmptyResultSet($submodule_reason);85return $result;86}87}8889$stderr = $e->getStderr();90if (preg_match('/^fatal: Not a valid object name/', $stderr)) {91// Grab two logs, since the first one is when the object was deleted.92list($stdout) = $repository->execxLocalCommand(93'log -n2 %s %s -- %s',94'--format=%H',95gitsprintf('%s', $commit),96$path);97$stdout = trim($stdout);98if ($stdout) {99$commits = explode("\n", $stdout);100$result101->setReasonForEmptyResultSet(102DiffusionBrowseResultSet::REASON_IS_DELETED)103->setDeletedAtCommit(idx($commits, 0))104->setExistedAtCommit(idx($commits, 1));105return $result;106}107108$result->setReasonForEmptyResultSet(109DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);110return $result;111} else {112throw $e;113}114}115}116117if ($path_type === 'blob') {118$result->setReasonForEmptyResultSet(119DiffusionBrowseResultSet::REASON_IS_FILE);120return $result;121}122123$result->setIsValidResults(true);124if ($this->shouldOnlyTestValidity($request)) {125return $result;126}127128if ($path === null) {129list($stdout) = $repository->execxLocalCommand(130'ls-tree -z -l %s --',131gitsprintf('%s', $commit));132} else {133if ($path_type === 'tree') {134$path = rtrim($path, '/').'/';135} else {136$path = rtrim($path, '/');137}138139list($stdout) = $repository->execxLocalCommand(140'ls-tree -z -l %s -- %s',141gitsprintf('%s', $commit),142$path);143}144145$submodules = array();146147$count = 0;148$results = array();149$lines = empty($stdout)150? array()151: explode("\0", rtrim($stdout));152153foreach ($lines as $line) {154// NOTE: Limit to 5 components so we parse filenames with spaces in them155// correctly.156// NOTE: The output uses a mixture of tabs and one-or-more spaces to157// delimit fields.158$parts = preg_split('/\s+/', $line, 5);159if (count($parts) < 5) {160throw new Exception(161pht(162'Expected "<mode> <type> <hash> <size>\t<name>", for ls-tree of '.163'"%s:%s", got: %s',164$commit,165$path,166$line));167}168169list($mode, $type, $hash, $size, $full_path) = $parts;170171$path_result = new DiffusionRepositoryPath();172173if ($type == 'tree') {174$file_type = DifferentialChangeType::FILE_DIRECTORY;175} else if ($type == 'commit') {176$file_type = DifferentialChangeType::FILE_SUBMODULE;177$submodules[] = $path_result;178} else {179$mode = intval($mode, 8);180if (($mode & 0120000) == 0120000) {181$file_type = DifferentialChangeType::FILE_SYMLINK;182} else {183$file_type = DifferentialChangeType::FILE_NORMAL;184}185}186187if ($path === null) {188$local_path = $full_path;189} else {190$local_path = basename($full_path);191}192193$path_result->setFullPath($full_path);194$path_result->setPath($local_path);195$path_result->setHash($hash);196$path_result->setFileType($file_type);197$path_result->setFileSize($size);198199if ($count >= $offset) {200$results[] = $path_result;201}202203$count++;204205if ($limit && $count >= ($offset + $limit)) {206break;207}208}209210// If we identified submodules, lookup the module info at this commit to211// find their source URIs.212213if ($submodules) {214215// NOTE: We need to read the file out of git and write it to a temporary216// location because "git config -f" doesn't accept a "commit:path"-style217// argument.218219// NOTE: This file may not exist, e.g. because the commit author removed220// it when they added the submodule. See T1448. If it's not present, just221// show the submodule without enriching it. If ".gitmodules" was removed222// it seems to partially break submodules, but the repository as a whole223// continues to work fine and we've seen at least two cases of this in224// the wild.225226list($err, $contents) = $repository->execLocalCommand(227'cat-file blob -- %s:.gitmodules',228$commit);229230if (!$err) {231232// NOTE: After T13673, the user executing "git" may not be the same233// as the user this process is running as (usually the webserver user),234// so we can't reliably use a temporary file: the daemon user may not235// be able to use it.236237// Use "--file -" to read from stdin instead. If this fails in some238// older versions of Git, we could exempt this particular command from239// sudoing to the daemon user.240241$future = $repository->getLocalCommandFuture('config -l --file - --');242$future->write($contents);243list($module_info) = $future->resolvex();244245$dict = array();246$lines = explode("\n", trim($module_info));247foreach ($lines as $line) {248list($key, $value) = explode('=', $line, 2);249$parts = explode('.', $key);250$dict[$key] = $value;251}252253foreach ($submodules as $submodule_path) {254$full_path = $submodule_path->getFullPath();255$key = 'submodule.'.$full_path.'.url';256if (isset($dict[$key])) {257$submodule_path->setExternalURI($dict[$key]);258}259}260}261}262263return $result->setPaths($results);264}265266protected function getMercurialResult(ConduitAPIRequest $request) {267$drequest = $this->getDiffusionRequest();268$repository = $drequest->getRepository();269$path = $request->getValue('path');270$commit = $request->getValue('commit');271$offset = (int)$request->getValue('offset');272$limit = (int)$request->getValue('limit');273$result = $this->getEmptyResultSet();274275276$entire_manifest = id(new DiffusionLowLevelMercurialPathsQuery())277->setRepository($repository)278->withCommit($commit)279->withPath($path)280->execute();281282$results = array();283284if ($path !== null) {285$match_against = trim($path, '/');286$match_len = strlen($match_against);287} else {288$match_against = '';289$match_len = 0;290}291292// For the root, don't trim. For other paths, trim the "/" after we match.293// We need this because Mercurial's canonical paths have no leading "/",294// but ours do.295$trim_len = $match_len ? $match_len + 1 : 0;296297$count = 0;298foreach ($entire_manifest as $path) {299if (strncmp($path, $match_against, $match_len)) {300continue;301}302if ($path === null || !strlen($path)) {303continue;304}305$remainder = substr($path, $trim_len);306if (!strlen($remainder)) {307// There is a file with this exact name in the manifest, so clearly308// it's a file.309$result->setReasonForEmptyResultSet(310DiffusionBrowseResultSet::REASON_IS_FILE);311return $result;312}313314$parts = explode('/', $remainder);315$name = reset($parts);316317// If we've already seen this path component, we're looking at a file318// inside a directory we already processed. Just move on.319if (isset($results[$name])) {320continue;321}322323if (count($parts) == 1) {324$type = DifferentialChangeType::FILE_NORMAL;325} else {326$type = DifferentialChangeType::FILE_DIRECTORY;327}328329if ($count >= $offset) {330$results[$name] = $type;331}332333$count++;334335if ($limit && ($count >= ($offset + $limit))) {336break;337}338}339340foreach ($results as $key => $type) {341$path_result = new DiffusionRepositoryPath();342$path_result->setPath($key);343$path_result->setFileType($type);344$path_result->setFullPath(ltrim($match_against.'/', '/').$key);345346$results[$key] = $path_result;347}348349$valid_results = true;350if (empty($results)) {351// TODO: Detect "deleted" by issuing "hg log"?352$result->setReasonForEmptyResultSet(353DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);354$valid_results = false;355}356357return $result358->setPaths($results)359->setIsValidResults($valid_results);360}361362protected function getSVNResult(ConduitAPIRequest $request) {363$drequest = $this->getDiffusionRequest();364$repository = $drequest->getRepository();365$path = $request->getValue('path');366$commit = $request->getValue('commit');367$offset = (int)$request->getValue('offset');368$limit = (int)$request->getValue('limit');369$result = $this->getEmptyResultSet();370371$subpath = $repository->getDetail('svn-subpath');372if ($subpath && strncmp($subpath, $path, strlen($subpath))) {373// If we have a subpath and the path isn't a child of it, it (almost374// certainly) won't exist since we don't track commits which affect375// it. (Even if it exists, return a consistent result.)376$result->setReasonForEmptyResultSet(377DiffusionBrowseResultSet::REASON_IS_UNTRACKED_PARENT);378return $result;379}380381$conn_r = $repository->establishConnection('r');382383$parent_path = DiffusionPathIDQuery::getParentPath($path);384$path_query = new DiffusionPathIDQuery(385array(386$path,387$parent_path,388));389$path_map = $path_query->loadPathIDs();390391$path_id = $path_map[$path];392$parent_path_id = $path_map[$parent_path];393394if (empty($path_id)) {395$result->setReasonForEmptyResultSet(396DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);397return $result;398}399400if ($commit) {401$slice_clause = qsprintf($conn_r, 'AND svnCommit <= %d', $commit);402} else {403$slice_clause = qsprintf($conn_r, '');404}405406$index = queryfx_all(407$conn_r,408'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE409repositoryID = %d AND parentID = %d410%Q GROUP BY pathID',411PhabricatorRepository::TABLE_FILESYSTEM,412$repository->getID(),413$path_id,414$slice_clause);415416if (!$index) {417if ($path == '/') {418$result->setReasonForEmptyResultSet(419DiffusionBrowseResultSet::REASON_IS_EMPTY);420} else {421422// NOTE: The parent path ID is included so this query can take423// advantage of the table's primary key; it is uniquely determined by424// the pathID but if we don't do the lookup ourselves MySQL doesn't have425// the information it needs to avoid a table scan.426427$reasons = queryfx_all(428$conn_r,429'SELECT * FROM %T WHERE repositoryID = %d430AND parentID = %d431AND pathID = %d432%Q ORDER BY svnCommit DESC LIMIT 2',433PhabricatorRepository::TABLE_FILESYSTEM,434$repository->getID(),435$parent_path_id,436$path_id,437$slice_clause);438439$reason = reset($reasons);440441if (!$reason) {442$result->setReasonForEmptyResultSet(443DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);444} else {445$file_type = $reason['fileType'];446if (empty($reason['existed'])) {447$result->setReasonForEmptyResultSet(448DiffusionBrowseResultSet::REASON_IS_DELETED);449$result->setDeletedAtCommit($reason['svnCommit']);450if (!empty($reasons[1])) {451$result->setExistedAtCommit($reasons[1]['svnCommit']);452}453} else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {454$result->setReasonForEmptyResultSet(455DiffusionBrowseResultSet::REASON_IS_EMPTY);456} else {457$result->setReasonForEmptyResultSet(458DiffusionBrowseResultSet::REASON_IS_FILE);459}460}461}462return $result;463}464465$result->setIsValidResults(true);466if ($this->shouldOnlyTestValidity($request)) {467return $result;468}469470$sql = array();471foreach ($index as $row) {472$sql[] = qsprintf(473$conn_r,474'(pathID = %d AND svnCommit = %d)',475$row['pathID'],476$row['maxCommit']);477}478479$browse = queryfx_all(480$conn_r,481'SELECT *, p.path pathName482FROM %T f JOIN %T p ON f.pathID = p.id483WHERE repositoryID = %d484AND parentID = %d485AND existed = 1486AND (%LO)487ORDER BY pathName',488PhabricatorRepository::TABLE_FILESYSTEM,489PhabricatorRepository::TABLE_PATH,490$repository->getID(),491$path_id,492$sql);493494$loadable_commits = array();495foreach ($browse as $key => $file) {496// We need to strip out directories because we don't store last-modified497// in the filesystem table.498if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) {499$loadable_commits[] = $file['svnCommit'];500$browse[$key]['hasCommit'] = true;501}502}503504$commits = array();505$commit_data = array();506if ($loadable_commits) {507// NOTE: Even though these are integers, use '%Ls' because MySQL doesn't508// use the second part of the key otherwise!509$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(510'repositoryID = %d AND commitIdentifier IN (%Ls)',511$repository->getID(),512$loadable_commits);513$commits = mpull($commits, null, 'getCommitIdentifier');514if ($commits) {515$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(516'commitID in (%Ld)',517mpull($commits, 'getID'));518$commit_data = mpull($commit_data, null, 'getCommitID');519} else {520$commit_data = array();521}522}523524$path_normal = DiffusionPathIDQuery::normalizePath($path);525526$results = array();527$count = 0;528foreach ($browse as $file) {529530$full_path = $file['pathName'];531$file_path = ltrim(substr($full_path, strlen($path_normal)), '/');532$full_path = ltrim($full_path, '/');533534$result_path = new DiffusionRepositoryPath();535$result_path->setPath($file_path);536$result_path->setFullPath($full_path);537$result_path->setFileType($file['fileType']);538539if (!empty($file['hasCommit'])) {540$commit = idx($commits, $file['svnCommit']);541if ($commit) {542$data = idx($commit_data, $commit->getID());543$result_path->setLastModifiedCommit($commit);544$result_path->setLastCommitData($data);545}546}547548if ($count >= $offset) {549$results[] = $result_path;550}551552$count++;553554if ($limit && ($count >= ($offset + $limit))) {555break;556}557}558559if (empty($results)) {560$result->setReasonForEmptyResultSet(561DiffusionBrowseResultSet::REASON_IS_EMPTY);562}563564return $result->setPaths($results);565}566567private function getEmptyResultSet() {568return id(new DiffusionBrowseResultSet())569->setPaths(array())570->setReasonForEmptyResultSet(null)571->setIsValidResults(false);572}573574private function shouldOnlyTestValidity(ConduitAPIRequest $request) {575return $request->getValue('needValidityOnly', false);576}577578}579580581