[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class DiffusionBrowseQueryConduitAPIMethod 4 extends DiffusionQueryConduitAPIMethod { 5 6 public function getAPIMethodName() { 7 return 'diffusion.browsequery'; 8 } 9 10 public function getMethodDescription() { 11 return 12 'File(s) information for a repository at an (optional) path and '. 13 '(optional) commit.'; 14 } 15 16 public function defineReturnType() { 17 return 'array'; 18 } 19 20 protected function defineCustomParamTypes() { 21 return array( 22 'path' => 'optional string', 23 'commit' => 'optional string', 24 'needValidityOnly' => 'optional bool', 25 ); 26 } 27 28 protected function getResult(ConduitAPIRequest $request) { 29 $result = parent::getResult($request); 30 return $result->toDictionary(); 31 } 32 33 protected function getGitResult(ConduitAPIRequest $request) { 34 $drequest = $this->getDiffusionRequest(); 35 $repository = $drequest->getRepository(); 36 $path = $request->getValue('path'); 37 $commit = $request->getValue('commit'); 38 $result = $this->getEmptyResultSet(); 39 40 if ($path == '') { 41 // Fast path to improve the performance of the repository view; we know 42 // the root is always a tree at any commit and always exists. 43 $stdout = 'tree'; 44 } else { 45 try { 46 list($stdout) = $repository->execxLocalCommand( 47 'cat-file -t %s:%s', 48 $commit, 49 $path); 50 } catch (CommandException $e) { 51 $stderr = $e->getStdErr(); 52 if (preg_match('/^fatal: Not a valid object name/', $stderr)) { 53 // Grab two logs, since the first one is when the object was deleted. 54 list($stdout) = $repository->execxLocalCommand( 55 'log -n2 --format="%%H" %s -- %s', 56 $commit, 57 $path); 58 $stdout = trim($stdout); 59 if ($stdout) { 60 $commits = explode("\n", $stdout); 61 $result 62 ->setReasonForEmptyResultSet( 63 DiffusionBrowseResultSet::REASON_IS_DELETED) 64 ->setDeletedAtCommit(idx($commits, 0)) 65 ->setExistedAtCommit(idx($commits, 1)); 66 return $result; 67 } 68 69 $result->setReasonForEmptyResultSet( 70 DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); 71 return $result; 72 } else { 73 throw $e; 74 } 75 } 76 } 77 78 if (trim($stdout) == 'blob') { 79 $result->setReasonForEmptyResultSet( 80 DiffusionBrowseResultSet::REASON_IS_FILE); 81 return $result; 82 } 83 84 $result->setIsValidResults(true); 85 if ($this->shouldOnlyTestValidity($request)) { 86 return $result; 87 } 88 89 list($stdout) = $repository->execxLocalCommand( 90 'ls-tree -z -l %s:%s', 91 $commit, 92 $path); 93 94 $submodules = array(); 95 96 if (strlen($path)) { 97 $prefix = rtrim($path, '/').'/'; 98 } else { 99 $prefix = ''; 100 } 101 102 $results = array(); 103 foreach (explode("\0", rtrim($stdout)) as $line) { 104 // NOTE: Limit to 5 components so we parse filenames with spaces in them 105 // correctly. 106 // NOTE: The output uses a mixture of tabs and one-or-more spaces to 107 // delimit fields. 108 $parts = preg_split('/\s+/', $line, 5); 109 if (count($parts) < 5) { 110 throw new Exception( 111 pht( 112 'Expected "<mode> <type> <hash> <size>\t<name>", for ls-tree of '. 113 '"%s:%s", got: %s', 114 $commit, 115 $path, 116 $line)); 117 } 118 119 list($mode, $type, $hash, $size, $name) = $parts; 120 121 $path_result = new DiffusionRepositoryPath(); 122 123 if ($type == 'tree') { 124 $file_type = DifferentialChangeType::FILE_DIRECTORY; 125 } else if ($type == 'commit') { 126 $file_type = DifferentialChangeType::FILE_SUBMODULE; 127 $submodules[] = $path_result; 128 } else { 129 $mode = intval($mode, 8); 130 if (($mode & 0120000) == 0120000) { 131 $file_type = DifferentialChangeType::FILE_SYMLINK; 132 } else { 133 $file_type = DifferentialChangeType::FILE_NORMAL; 134 } 135 } 136 137 $path_result->setFullPath($prefix.$name); 138 $path_result->setPath($name); 139 $path_result->setHash($hash); 140 $path_result->setFileType($file_type); 141 $path_result->setFileSize($size); 142 143 $results[] = $path_result; 144 } 145 146 // If we identified submodules, lookup the module info at this commit to 147 // find their source URIs. 148 149 if ($submodules) { 150 151 // NOTE: We need to read the file out of git and write it to a temporary 152 // location because "git config -f" doesn't accept a "commit:path"-style 153 // argument. 154 155 // NOTE: This file may not exist, e.g. because the commit author removed 156 // it when they added the submodule. See T1448. If it's not present, just 157 // show the submodule without enriching it. If ".gitmodules" was removed 158 // it seems to partially break submodules, but the repository as a whole 159 // continues to work fine and we've seen at least two cases of this in 160 // the wild. 161 162 list($err, $contents) = $repository->execLocalCommand( 163 'cat-file blob %s:.gitmodules', 164 $commit); 165 166 if (!$err) { 167 $tmp = new TempFile(); 168 Filesystem::writeFile($tmp, $contents); 169 list($module_info) = $repository->execxLocalCommand( 170 'config -l -f %s', 171 $tmp); 172 173 $dict = array(); 174 $lines = explode("\n", trim($module_info)); 175 foreach ($lines as $line) { 176 list($key, $value) = explode('=', $line, 2); 177 $parts = explode('.', $key); 178 $dict[$key] = $value; 179 } 180 181 foreach ($submodules as $path) { 182 $full_path = $path->getFullPath(); 183 $key = 'submodule.'.$full_path.'.url'; 184 if (isset($dict[$key])) { 185 $path->setExternalURI($dict[$key]); 186 } 187 } 188 } 189 } 190 191 return $result->setPaths($results); 192 } 193 194 protected function getMercurialResult(ConduitAPIRequest $request) { 195 $drequest = $this->getDiffusionRequest(); 196 $repository = $drequest->getRepository(); 197 $path = $request->getValue('path'); 198 $commit = $request->getValue('commit'); 199 $result = $this->getEmptyResultSet(); 200 201 202 $entire_manifest = id(new DiffusionLowLevelMercurialPathsQuery()) 203 ->setRepository($repository) 204 ->withCommit($commit) 205 ->withPath($path) 206 ->execute(); 207 208 $results = array(); 209 210 $match_against = trim($path, '/'); 211 $match_len = strlen($match_against); 212 213 // For the root, don't trim. For other paths, trim the "/" after we match. 214 // We need this because Mercurial's canonical paths have no leading "/", 215 // but ours do. 216 $trim_len = $match_len ? $match_len + 1 : 0; 217 218 foreach ($entire_manifest as $path) { 219 if (strncmp($path, $match_against, $match_len)) { 220 continue; 221 } 222 if (!strlen($path)) { 223 continue; 224 } 225 $remainder = substr($path, $trim_len); 226 if (!strlen($remainder)) { 227 // There is a file with this exact name in the manifest, so clearly 228 // it's a file. 229 $result->setReasonForEmptyResultSet( 230 DiffusionBrowseResultSet::REASON_IS_FILE); 231 return $result; 232 } 233 $parts = explode('/', $remainder); 234 if (count($parts) == 1) { 235 $type = DifferentialChangeType::FILE_NORMAL; 236 } else { 237 $type = DifferentialChangeType::FILE_DIRECTORY; 238 } 239 $results[reset($parts)] = $type; 240 } 241 242 foreach ($results as $key => $type) { 243 $path_result = new DiffusionRepositoryPath(); 244 $path_result->setPath($key); 245 $path_result->setFileType($type); 246 $path_result->setFullPath(ltrim($match_against.'/', '/').$key); 247 248 $results[$key] = $path_result; 249 } 250 251 $valid_results = true; 252 if (empty($results)) { 253 // TODO: Detect "deleted" by issuing "hg log"? 254 $result->setReasonForEmptyResultSet( 255 DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); 256 $valid_results = false; 257 } 258 259 return $result 260 ->setPaths($results) 261 ->setIsValidResults($valid_results); 262 } 263 264 protected function getSVNResult(ConduitAPIRequest $request) { 265 $drequest = $this->getDiffusionRequest(); 266 $repository = $drequest->getRepository(); 267 $path = $request->getValue('path'); 268 $commit = $request->getValue('commit'); 269 $result = $this->getEmptyResultSet(); 270 271 $subpath = $repository->getDetail('svn-subpath'); 272 if ($subpath && strncmp($subpath, $path, strlen($subpath))) { 273 // If we have a subpath and the path isn't a child of it, it (almost 274 // certainly) won't exist since we don't track commits which affect 275 // it. (Even if it exists, return a consistent result.) 276 $result->setReasonForEmptyResultSet( 277 DiffusionBrowseResultSet::REASON_IS_UNTRACKED_PARENT); 278 return $result; 279 } 280 281 $conn_r = $repository->establishConnection('r'); 282 283 $parent_path = DiffusionPathIDQuery::getParentPath($path); 284 $path_query = new DiffusionPathIDQuery( 285 array( 286 $path, 287 $parent_path, 288 )); 289 $path_map = $path_query->loadPathIDs(); 290 291 $path_id = $path_map[$path]; 292 $parent_path_id = $path_map[$parent_path]; 293 294 if (empty($path_id)) { 295 $result->setReasonForEmptyResultSet( 296 DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); 297 return $result; 298 } 299 300 if ($commit) { 301 $slice_clause = 'AND svnCommit <= '.(int)$commit; 302 } else { 303 $slice_clause = ''; 304 } 305 306 $index = queryfx_all( 307 $conn_r, 308 'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE 309 repositoryID = %d AND parentID = %d 310 %Q GROUP BY pathID', 311 PhabricatorRepository::TABLE_FILESYSTEM, 312 $repository->getID(), 313 $path_id, 314 $slice_clause); 315 316 if (!$index) { 317 if ($path == '/') { 318 $result->setReasonForEmptyResultSet( 319 DiffusionBrowseResultSet::REASON_IS_EMPTY); 320 } else { 321 322 // NOTE: The parent path ID is included so this query can take 323 // advantage of the table's primary key; it is uniquely determined by 324 // the pathID but if we don't do the lookup ourselves MySQL doesn't have 325 // the information it needs to avoid a table scan. 326 327 $reasons = queryfx_all( 328 $conn_r, 329 'SELECT * FROM %T WHERE repositoryID = %d 330 AND parentID = %d 331 AND pathID = %d 332 %Q ORDER BY svnCommit DESC LIMIT 2', 333 PhabricatorRepository::TABLE_FILESYSTEM, 334 $repository->getID(), 335 $parent_path_id, 336 $path_id, 337 $slice_clause); 338 339 $reason = reset($reasons); 340 341 if (!$reason) { 342 $result->setReasonForEmptyResultSet( 343 DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); 344 } else { 345 $file_type = $reason['fileType']; 346 if (empty($reason['existed'])) { 347 $result->setReasonForEmptyResultSet( 348 DiffusionBrowseResultSet::REASON_IS_DELETED); 349 $result->setDeletedAtCommit($reason['svnCommit']); 350 if (!empty($reasons[1])) { 351 $result->setExistedAtCommit($reasons[1]['svnCommit']); 352 } 353 } else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { 354 $result->setReasonForEmptyResultSet( 355 DiffusionBrowseResultSet::REASON_IS_EMPTY); 356 } else { 357 $result->setReasonForEmptyResultSet( 358 DiffusionBrowseResultSet::REASON_IS_FILE); 359 } 360 } 361 } 362 return $result; 363 } 364 365 $result->setIsValidResults(true); 366 if ($this->shouldOnlyTestValidity($request)) { 367 return $result; 368 } 369 370 $sql = array(); 371 foreach ($index as $row) { 372 $sql[] = 373 '(pathID = '.(int)$row['pathID'].' AND '. 374 'svnCommit = '.(int)$row['maxCommit'].')'; 375 } 376 377 $browse = queryfx_all( 378 $conn_r, 379 'SELECT *, p.path pathName 380 FROM %T f JOIN %T p ON f.pathID = p.id 381 WHERE repositoryID = %d 382 AND parentID = %d 383 AND existed = 1 384 AND (%Q) 385 ORDER BY pathName', 386 PhabricatorRepository::TABLE_FILESYSTEM, 387 PhabricatorRepository::TABLE_PATH, 388 $repository->getID(), 389 $path_id, 390 implode(' OR ', $sql)); 391 392 $loadable_commits = array(); 393 foreach ($browse as $key => $file) { 394 // We need to strip out directories because we don't store last-modified 395 // in the filesystem table. 396 if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) { 397 $loadable_commits[] = $file['svnCommit']; 398 $browse[$key]['hasCommit'] = true; 399 } 400 } 401 402 $commits = array(); 403 $commit_data = array(); 404 if ($loadable_commits) { 405 // NOTE: Even though these are integers, use '%Ls' because MySQL doesn't 406 // use the second part of the key otherwise! 407 $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( 408 'repositoryID = %d AND commitIdentifier IN (%Ls)', 409 $repository->getID(), 410 $loadable_commits); 411 $commits = mpull($commits, null, 'getCommitIdentifier'); 412 if ($commits) { 413 $commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere( 414 'commitID in (%Ld)', 415 mpull($commits, 'getID')); 416 $commit_data = mpull($commit_data, null, 'getCommitID'); 417 } else { 418 $commit_data = array(); 419 } 420 } 421 422 $path_normal = DiffusionPathIDQuery::normalizePath($path); 423 424 $results = array(); 425 foreach ($browse as $file) { 426 427 $full_path = $file['pathName']; 428 $file_path = ltrim(substr($full_path, strlen($path_normal)), '/'); 429 $full_path = ltrim($full_path, '/'); 430 431 $result_path = new DiffusionRepositoryPath(); 432 $result_path->setPath($file_path); 433 $result_path->setFullPath($full_path); 434 // $result_path->setHash($hash); 435 $result_path->setFileType($file['fileType']); 436 // $result_path->setFileSize($size); 437 438 if (!empty($file['hasCommit'])) { 439 $commit = idx($commits, $file['svnCommit']); 440 if ($commit) { 441 $data = idx($commit_data, $commit->getID()); 442 $result_path->setLastModifiedCommit($commit); 443 $result_path->setLastCommitData($data); 444 } 445 } 446 447 $results[] = $result_path; 448 } 449 450 if (empty($results)) { 451 $result->setReasonForEmptyResultSet( 452 DiffusionBrowseResultSet::REASON_IS_EMPTY); 453 } 454 455 return $result->setPaths($results); 456 } 457 458 private function getEmptyResultSet() { 459 return id(new DiffusionBrowseResultSet()) 460 ->setPaths(array()) 461 ->setReasonForEmptyResultSet(null) 462 ->setIsValidResults(false); 463 } 464 465 private function shouldOnlyTestValidity(ConduitAPIRequest $request) { 466 return $request->getValue('needValidityOnly', false); 467 } 468 469 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |