@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 574 lines 18 kB view raw
1<?php 2 3final class DiffusionBrowseQueryConduitAPIMethod 4 extends DiffusionQueryConduitAPIMethod { 5 6 public function getAPIMethodName() { 7 return 'diffusion.browsequery'; 8 } 9 10 public function getMethodDescription() { 11 return pht( 12 'File(s) information for a repository at an (optional) path and '. 13 '(optional) commit.'); 14 } 15 16 protected 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 'limit' => 'optional int', 26 'offset' => 'optional int', 27 ); 28 } 29 30 protected function getResult(ConduitAPIRequest $request) { 31 $result = parent::getResult($request); 32 return $result->toDictionary(); 33 } 34 35 protected function getGitResult(ConduitAPIRequest $request) { 36 $drequest = $this->getDiffusionRequest(); 37 $repository = $drequest->getRepository(); 38 39 $path = $request->getValue('path'); 40 if (!phutil_nonempty_string($path) || $path === '/') { 41 $path = null; 42 } 43 44 $commit = $request->getValue('commit'); 45 $offset = (int)$request->getValue('offset'); 46 $limit = (int)$request->getValue('limit'); 47 $result = $this->getEmptyResultSet(); 48 49 if ($path === null) { 50 // Fast path to improve the performance of the repository view; we know 51 // the root is always a tree at any commit and always exists. 52 $path_type = 'tree'; 53 } else { 54 try { 55 list($stdout) = $repository->execxLocalCommand( 56 'cat-file -t -- %s', 57 sprintf('%s:%s', $commit, $path)); 58 $path_type = trim($stdout); 59 } catch (CommandException $e) { 60 // The "cat-file" command may fail if the path legitimately does not 61 // exist, but it may also fail if the path is a submodule. This can 62 // produce either "Not a valid object name" or "could not get object 63 // info". 64 65 // To detect if we have a submodule, use `git ls-tree`. If the path 66 // is a submodule, we'll get a "160000" mode mask with type "commit". 67 68 list($sub_err, $sub_stdout) = $repository->execLocalCommand( 69 'ls-tree %s -- %s', 70 gitsprintf('%s', $commit), 71 $path); 72 if (!$sub_err) { 73 // If the path failed "cat-file" but "ls-tree" worked, we assume it 74 // must be a submodule. If it is, the output will look something 75 // like this: 76 // 77 // 160000 commit <hash> <path> 78 // 79 // We make sure it has the 160000 mode mask to confirm that it's 80 // definitely a submodule. 81 $mode = (int)$sub_stdout; 82 if ($mode & 160000) { 83 $submodule_reason = DiffusionBrowseResultSet::REASON_IS_SUBMODULE; 84 $result 85 ->setReasonForEmptyResultSet($submodule_reason); 86 return $result; 87 } 88 } 89 90 $stderr = $e->getStderr(); 91 if (preg_match('/^fatal: Not a valid object name/', $stderr)) { 92 // Grab two logs, since the first one is when the object was deleted. 93 list($stdout) = $repository->execxLocalCommand( 94 'log -n2 %s %s -- %s', 95 '--format=%H', 96 gitsprintf('%s', $commit), 97 $path); 98 $stdout = trim($stdout); 99 if ($stdout) { 100 $commits = explode("\n", $stdout); 101 $result 102 ->setReasonForEmptyResultSet( 103 DiffusionBrowseResultSet::REASON_IS_DELETED) 104 ->setDeletedAtCommit(idx($commits, 0)) 105 ->setExistedAtCommit(idx($commits, 1)); 106 return $result; 107 } 108 109 $result->setReasonForEmptyResultSet( 110 DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); 111 return $result; 112 } else { 113 throw $e; 114 } 115 } 116 } 117 118 if ($path_type === 'blob') { 119 $result->setReasonForEmptyResultSet( 120 DiffusionBrowseResultSet::REASON_IS_FILE); 121 return $result; 122 } 123 124 $result->setIsValidResults(true); 125 if ($this->shouldOnlyTestValidity($request)) { 126 return $result; 127 } 128 129 if ($path === null) { 130 list($stdout) = $repository->execxLocalCommand( 131 'ls-tree -z -l %s --', 132 gitsprintf('%s', $commit)); 133 } else { 134 if ($path_type === 'tree') { 135 $path = rtrim($path, '/').'/'; 136 } else { 137 $path = rtrim($path, '/'); 138 } 139 140 list($stdout) = $repository->execxLocalCommand( 141 'ls-tree -z -l %s -- %s', 142 gitsprintf('%s', $commit), 143 $path); 144 } 145 146 $submodules = array(); 147 148 $count = 0; 149 $results = array(); 150 $lines = empty($stdout) 151 ? array() 152 : explode("\0", rtrim($stdout)); 153 154 foreach ($lines as $line) { 155 // NOTE: Limit to 5 components so we parse filenames with spaces in them 156 // correctly. 157 // NOTE: The output uses a mixture of tabs and one-or-more spaces to 158 // delimit fields. 159 $parts = preg_split('/\s+/', $line, 5); 160 if (count($parts) < 5) { 161 throw new Exception( 162 pht( 163 'Expected "<mode> <type> <hash> <size>\t<name>", for ls-tree of '. 164 '"%s:%s", got: %s', 165 $commit, 166 $path, 167 $line)); 168 } 169 170 list($mode, $type, $hash, $size, $full_path) = $parts; 171 172 $path_result = new DiffusionRepositoryPath(); 173 174 if ($type == 'tree') { 175 $file_type = DifferentialChangeType::FILE_DIRECTORY; 176 } else if ($type == 'commit') { 177 $file_type = DifferentialChangeType::FILE_SUBMODULE; 178 $submodules[] = $path_result; 179 } else { 180 $mode = intval($mode, 8); 181 if (($mode & 0120000) == 0120000) { 182 $file_type = DifferentialChangeType::FILE_SYMLINK; 183 } else { 184 $file_type = DifferentialChangeType::FILE_NORMAL; 185 } 186 } 187 188 if ($path === null) { 189 $local_path = $full_path; 190 } else { 191 $local_path = basename($full_path); 192 } 193 194 $path_result->setFullPath($full_path); 195 $path_result->setPath($local_path); 196 $path_result->setHash($hash); 197 $path_result->setFileType($file_type); 198 $path_result->setFileSize($size); 199 200 if ($count >= $offset) { 201 $results[] = $path_result; 202 } 203 204 $count++; 205 206 if ($limit && $count >= ($offset + $limit)) { 207 break; 208 } 209 } 210 211 // If we identified submodules, lookup the module info at this commit to 212 // find their source URIs. 213 214 if ($submodules) { 215 216 // NOTE: We need to read the file out of git and write it to a temporary 217 // location because "git config -f" doesn't accept a "commit:path"-style 218 // argument. 219 220 // NOTE: This file may not exist, e.g. because the commit author removed 221 // it when they added the submodule. See T1448. If it's not present, just 222 // show the submodule without enriching it. If ".gitmodules" was removed 223 // it seems to partially break submodules, but the repository as a whole 224 // continues to work fine and we've seen at least two cases of this in 225 // the wild. 226 227 list($err, $contents) = $repository->execLocalCommand( 228 'cat-file blob -- %s:.gitmodules', 229 $commit); 230 231 if (!$err) { 232 233 // NOTE: After T13673, the user executing "git" may not be the same 234 // as the user this process is running as (usually the webserver user), 235 // so we can't reliably use a temporary file: the daemon user may not 236 // be able to use it. 237 238 // Use "--file -" to read from stdin instead. If this fails in some 239 // older versions of Git, we could exempt this particular command from 240 // sudoing to the daemon user. 241 242 $future = $repository->getLocalCommandFuture('config -l --file - --'); 243 $future->write($contents); 244 list($module_info) = $future->resolvex(); 245 246 $dict = array(); 247 $lines = explode("\n", trim($module_info)); 248 foreach ($lines as $line) { 249 list($key, $value) = explode('=', $line, 2); 250 $parts = explode('.', $key); 251 $dict[$key] = $value; 252 } 253 254 foreach ($submodules as $submodule_path) { 255 $full_path = $submodule_path->getFullPath(); 256 $key = 'submodule.'.$full_path.'.url'; 257 if (isset($dict[$key])) { 258 $submodule_path->setExternalURI($dict[$key]); 259 } 260 } 261 } 262 } 263 264 return $result->setPaths($results); 265 } 266 267 protected function getMercurialResult(ConduitAPIRequest $request) { 268 $drequest = $this->getDiffusionRequest(); 269 $repository = $drequest->getRepository(); 270 $path = $request->getValue('path'); 271 $commit = $request->getValue('commit'); 272 $offset = (int)$request->getValue('offset'); 273 $limit = (int)$request->getValue('limit'); 274 $result = $this->getEmptyResultSet(); 275 276 277 $entire_manifest = id(new DiffusionLowLevelMercurialPathsQuery()) 278 ->setRepository($repository) 279 ->withCommit($commit) 280 ->withPath($path) 281 ->execute(); 282 283 $results = array(); 284 285 $match_against = trim($path, '/'); 286 $match_len = strlen($match_against); 287 288 // For the root, don't trim. For other paths, trim the "/" after we match. 289 // We need this because Mercurial's canonical paths have no leading "/", 290 // but ours do. 291 $trim_len = $match_len ? $match_len + 1 : 0; 292 293 $count = 0; 294 foreach ($entire_manifest as $path) { 295 if (strncmp($path, $match_against, $match_len)) { 296 continue; 297 } 298 if (!strlen($path)) { 299 continue; 300 } 301 $remainder = substr($path, $trim_len); 302 if (!strlen($remainder)) { 303 // There is a file with this exact name in the manifest, so clearly 304 // it's a file. 305 $result->setReasonForEmptyResultSet( 306 DiffusionBrowseResultSet::REASON_IS_FILE); 307 return $result; 308 } 309 310 $parts = explode('/', $remainder); 311 $name = reset($parts); 312 313 // If we've already seen this path component, we're looking at a file 314 // inside a directory we already processed. Just move on. 315 if (isset($results[$name])) { 316 continue; 317 } 318 319 if (count($parts) == 1) { 320 $type = DifferentialChangeType::FILE_NORMAL; 321 } else { 322 $type = DifferentialChangeType::FILE_DIRECTORY; 323 } 324 325 if ($count >= $offset) { 326 $results[$name] = $type; 327 } 328 329 $count++; 330 331 if ($limit && ($count >= ($offset + $limit))) { 332 break; 333 } 334 } 335 336 foreach ($results as $key => $type) { 337 $path_result = new DiffusionRepositoryPath(); 338 $path_result->setPath($key); 339 $path_result->setFileType($type); 340 $path_result->setFullPath(ltrim($match_against.'/', '/').$key); 341 342 $results[$key] = $path_result; 343 } 344 345 $valid_results = true; 346 if (empty($results)) { 347 // TODO: Detect "deleted" by issuing "hg log"? 348 $result->setReasonForEmptyResultSet( 349 DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); 350 $valid_results = false; 351 } 352 353 return $result 354 ->setPaths($results) 355 ->setIsValidResults($valid_results); 356 } 357 358 protected function getSVNResult(ConduitAPIRequest $request) { 359 $drequest = $this->getDiffusionRequest(); 360 $repository = $drequest->getRepository(); 361 $path = $request->getValue('path'); 362 $commit = $request->getValue('commit'); 363 $offset = (int)$request->getValue('offset'); 364 $limit = (int)$request->getValue('limit'); 365 $result = $this->getEmptyResultSet(); 366 367 $subpath = $repository->getDetail('svn-subpath'); 368 if ($subpath && strncmp($subpath, $path, strlen($subpath))) { 369 // If we have a subpath and the path isn't a child of it, it (almost 370 // certainly) won't exist since we don't track commits which affect 371 // it. (Even if it exists, return a consistent result.) 372 $result->setReasonForEmptyResultSet( 373 DiffusionBrowseResultSet::REASON_IS_UNTRACKED_PARENT); 374 return $result; 375 } 376 377 $conn_r = $repository->establishConnection('r'); 378 379 $parent_path = DiffusionPathIDQuery::getParentPath($path); 380 $path_query = new DiffusionPathIDQuery( 381 array( 382 $path, 383 $parent_path, 384 )); 385 $path_map = $path_query->loadPathIDs(); 386 387 $path_id = $path_map[$path]; 388 $parent_path_id = $path_map[$parent_path]; 389 390 if (empty($path_id)) { 391 $result->setReasonForEmptyResultSet( 392 DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); 393 return $result; 394 } 395 396 if ($commit) { 397 $slice_clause = qsprintf($conn_r, 'AND svnCommit <= %d', $commit); 398 } else { 399 $slice_clause = qsprintf($conn_r, ''); 400 } 401 402 $index = queryfx_all( 403 $conn_r, 404 'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE 405 repositoryID = %d AND parentID = %d 406 %Q GROUP BY pathID', 407 PhabricatorRepository::TABLE_FILESYSTEM, 408 $repository->getID(), 409 $path_id, 410 $slice_clause); 411 412 if (!$index) { 413 if ($path == '/') { 414 $result->setReasonForEmptyResultSet( 415 DiffusionBrowseResultSet::REASON_IS_EMPTY); 416 } else { 417 418 // NOTE: The parent path ID is included so this query can take 419 // advantage of the table's primary key; it is uniquely determined by 420 // the pathID but if we don't do the lookup ourselves MySQL doesn't have 421 // the information it needs to avoid a table scan. 422 423 $reasons = queryfx_all( 424 $conn_r, 425 'SELECT * FROM %T WHERE repositoryID = %d 426 AND parentID = %d 427 AND pathID = %d 428 %Q ORDER BY svnCommit DESC LIMIT 2', 429 PhabricatorRepository::TABLE_FILESYSTEM, 430 $repository->getID(), 431 $parent_path_id, 432 $path_id, 433 $slice_clause); 434 435 $reason = reset($reasons); 436 437 if (!$reason) { 438 $result->setReasonForEmptyResultSet( 439 DiffusionBrowseResultSet::REASON_IS_NONEXISTENT); 440 } else { 441 $file_type = $reason['fileType']; 442 if (empty($reason['existed'])) { 443 $result->setReasonForEmptyResultSet( 444 DiffusionBrowseResultSet::REASON_IS_DELETED); 445 $result->setDeletedAtCommit($reason['svnCommit']); 446 if (!empty($reasons[1])) { 447 $result->setExistedAtCommit($reasons[1]['svnCommit']); 448 } 449 } else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { 450 $result->setReasonForEmptyResultSet( 451 DiffusionBrowseResultSet::REASON_IS_EMPTY); 452 } else { 453 $result->setReasonForEmptyResultSet( 454 DiffusionBrowseResultSet::REASON_IS_FILE); 455 } 456 } 457 } 458 return $result; 459 } 460 461 $result->setIsValidResults(true); 462 if ($this->shouldOnlyTestValidity($request)) { 463 return $result; 464 } 465 466 $sql = array(); 467 foreach ($index as $row) { 468 $sql[] = qsprintf( 469 $conn_r, 470 '(pathID = %d AND svnCommit = %d)', 471 $row['pathID'], 472 $row['maxCommit']); 473 } 474 475 $browse = queryfx_all( 476 $conn_r, 477 'SELECT *, p.path pathName 478 FROM %T f JOIN %T p ON f.pathID = p.id 479 WHERE repositoryID = %d 480 AND parentID = %d 481 AND existed = 1 482 AND (%LO) 483 ORDER BY pathName', 484 PhabricatorRepository::TABLE_FILESYSTEM, 485 PhabricatorRepository::TABLE_PATH, 486 $repository->getID(), 487 $path_id, 488 $sql); 489 490 $loadable_commits = array(); 491 foreach ($browse as $key => $file) { 492 // We need to strip out directories because we don't store last-modified 493 // in the filesystem table. 494 if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) { 495 $loadable_commits[] = $file['svnCommit']; 496 $browse[$key]['hasCommit'] = true; 497 } 498 } 499 500 $commits = array(); 501 $commit_data = array(); 502 if ($loadable_commits) { 503 // NOTE: Even though these are integers, use '%Ls' because MySQL doesn't 504 // use the second part of the key otherwise! 505 $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( 506 'repositoryID = %d AND commitIdentifier IN (%Ls)', 507 $repository->getID(), 508 $loadable_commits); 509 $commits = mpull($commits, null, 'getCommitIdentifier'); 510 if ($commits) { 511 $commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere( 512 'commitID in (%Ld)', 513 mpull($commits, 'getID')); 514 $commit_data = mpull($commit_data, null, 'getCommitID'); 515 } else { 516 $commit_data = array(); 517 } 518 } 519 520 $path_normal = DiffusionPathIDQuery::normalizePath($path); 521 522 $results = array(); 523 $count = 0; 524 foreach ($browse as $file) { 525 526 $full_path = $file['pathName']; 527 $file_path = ltrim(substr($full_path, strlen($path_normal)), '/'); 528 $full_path = ltrim($full_path, '/'); 529 530 $result_path = new DiffusionRepositoryPath(); 531 $result_path->setPath($file_path); 532 $result_path->setFullPath($full_path); 533 $result_path->setFileType($file['fileType']); 534 535 if (!empty($file['hasCommit'])) { 536 $commit = idx($commits, $file['svnCommit']); 537 if ($commit) { 538 $data = idx($commit_data, $commit->getID()); 539 $result_path->setLastModifiedCommit($commit); 540 $result_path->setLastCommitData($data); 541 } 542 } 543 544 if ($count >= $offset) { 545 $results[] = $result_path; 546 } 547 548 $count++; 549 550 if ($limit && ($count >= ($offset + $limit))) { 551 break; 552 } 553 } 554 555 if (empty($results)) { 556 $result->setReasonForEmptyResultSet( 557 DiffusionBrowseResultSet::REASON_IS_EMPTY); 558 } 559 560 return $result->setPaths($results); 561 } 562 563 private function getEmptyResultSet() { 564 return id(new DiffusionBrowseResultSet()) 565 ->setPaths(array()) 566 ->setReasonForEmptyResultSet(null) 567 ->setIsValidResults(false); 568 } 569 570 private function shouldOnlyTestValidity(ConduitAPIRequest $request) { 571 return $request->getValue('needValidityOnly', false); 572 } 573 574}