[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class DiffusionCommitQuery 4 extends PhabricatorCursorPagedPolicyAwareQuery { 5 6 private $ids; 7 private $phids; 8 private $authorPHIDs; 9 private $defaultRepository; 10 private $identifiers; 11 private $repositoryIDs; 12 private $repositoryPHIDs; 13 private $identifierMap; 14 15 private $needAuditRequests; 16 private $auditIDs; 17 private $auditorPHIDs; 18 private $auditAwaitingUser; 19 private $auditStatus; 20 21 const AUDIT_STATUS_ANY = 'audit-status-any'; 22 const AUDIT_STATUS_OPEN = 'audit-status-open'; 23 const AUDIT_STATUS_CONCERN = 'audit-status-concern'; 24 const AUDIT_STATUS_ACCEPTED = 'audit-status-accepted'; 25 const AUDIT_STATUS_PARTIAL = 'audit-status-partial'; 26 27 private $needCommitData; 28 29 public function withIDs(array $ids) { 30 $this->ids = $ids; 31 return $this; 32 } 33 34 public function withPHIDs(array $phids) { 35 $this->phids = $phids; 36 return $this; 37 } 38 39 public function withAuthorPHIDs(array $phids) { 40 $this->authorPHIDs = $phids; 41 return $this; 42 } 43 44 /** 45 * Load commits by partial or full identifiers, e.g. "rXab82393", "rX1234", 46 * or "a9caf12". When an identifier matches multiple commits, they will all 47 * be returned; callers should be prepared to deal with more results than 48 * they queried for. 49 */ 50 public function withIdentifiers(array $identifiers) { 51 $this->identifiers = $identifiers; 52 return $this; 53 } 54 55 /** 56 * Look up commits in a specific repository. This is a shorthand for calling 57 * @{method:withDefaultRepository} and @{method:withRepositoryIDs}. 58 */ 59 public function withRepository(PhabricatorRepository $repository) { 60 $this->withDefaultRepository($repository); 61 $this->withRepositoryIDs(array($repository->getID())); 62 return $this; 63 } 64 65 /** 66 * Look up commits in a specific repository. Prefer 67 * @{method:withRepositoryIDs}; the underyling table is keyed by ID such 68 * that this method requires a separate initial query to map PHID to ID. 69 */ 70 public function withRepositoryPHIDs(array $phids) { 71 $this->repositoryPHIDs = $phids; 72 } 73 74 /** 75 * If a default repository is provided, ambiguous commit identifiers will 76 * be assumed to belong to the default repository. 77 * 78 * For example, "r123" appearing in a commit message in repository X is 79 * likely to be unambiguously "rX123". Normally the reference would be 80 * considered ambiguous, but if you provide a default repository it will 81 * be correctly resolved. 82 */ 83 public function withDefaultRepository(PhabricatorRepository $repository) { 84 $this->defaultRepository = $repository; 85 return $this; 86 } 87 88 public function withRepositoryIDs(array $repository_ids) { 89 $this->repositoryIDs = $repository_ids; 90 return $this; 91 } 92 93 public function needCommitData($need) { 94 $this->needCommitData = $need; 95 return $this; 96 } 97 98 public function needAuditRequests($need) { 99 $this->needAuditRequests = $need; 100 return $this; 101 } 102 103 /** 104 * Returns true if we should join the audit table, either because we're 105 * interested in the information if it's available or because matching rows 106 * must always have it. 107 */ 108 private function shouldJoinAudits() { 109 return $this->auditStatus || 110 $this->rowsMustHaveAudits(); 111 } 112 113 /** 114 * Return true if we should `JOIN` (vs `LEFT JOIN`) the audit table, because 115 * matching commits will always have audit rows. 116 */ 117 private function rowsMustHaveAudits() { 118 return 119 $this->auditIDs || 120 $this->auditorPHIDs || 121 $this->auditAwaitingUser; 122 } 123 124 public function withAuditIDs(array $ids) { 125 $this->auditIDs = $ids; 126 return $this; 127 } 128 129 public function withAuditorPHIDs(array $auditor_phids) { 130 $this->auditorPHIDs = $auditor_phids; 131 return $this; 132 } 133 134 public function withAuditAwaitingUser(PhabricatorUser $user) { 135 $this->auditAwaitingUser = $user; 136 return $this; 137 } 138 139 public function withAuditStatus($status) { 140 $this->auditStatus = $status; 141 return $this; 142 } 143 144 public function getIdentifierMap() { 145 if ($this->identifierMap === null) { 146 throw new Exception( 147 'You must execute() the query before accessing the identifier map.'); 148 } 149 return $this->identifierMap; 150 } 151 152 protected function getPagingColumn() { 153 return 'commit.id'; 154 } 155 156 protected function willExecute() { 157 if ($this->identifierMap === null) { 158 $this->identifierMap = array(); 159 } 160 } 161 162 protected function loadPage() { 163 $table = new PhabricatorRepositoryCommit(); 164 $conn_r = $table->establishConnection('r'); 165 166 $data = queryfx_all( 167 $conn_r, 168 'SELECT commit.* FROM %T commit %Q %Q %Q %Q %Q', 169 $table->getTableName(), 170 $this->buildJoinClause($conn_r), 171 $this->buildWhereClause($conn_r), 172 $this->buildGroupClause($conn_r), 173 $this->buildOrderClause($conn_r), 174 $this->buildLimitClause($conn_r)); 175 176 return $table->loadAllFromArray($data); 177 } 178 179 protected function willFilterPage(array $commits) { 180 $repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID'); 181 $repos = id(new PhabricatorRepositoryQuery()) 182 ->setViewer($this->getViewer()) 183 ->withIDs($repository_ids) 184 ->execute(); 185 186 foreach ($commits as $key => $commit) { 187 $repo = idx($repos, $commit->getRepositoryID()); 188 if ($repo) { 189 $commit->attachRepository($repo); 190 } else { 191 unset($commits[$key]); 192 } 193 } 194 195 if ($this->identifiers !== null) { 196 $ids = array_fuse($this->identifiers); 197 $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; 198 199 $result = array(); 200 foreach ($commits as $commit) { 201 $prefix = 'r'.$commit->getRepository()->getCallsign(); 202 $suffix = $commit->getCommitIdentifier(); 203 204 if ($commit->getRepository()->isSVN()) { 205 if (isset($ids[$prefix.$suffix])) { 206 $result[$prefix.$suffix][] = $commit; 207 } 208 } else { 209 // This awkward construction is so we can link the commits up in O(N) 210 // time instead of O(N^2). 211 for ($ii = $min_qualified; $ii <= strlen($suffix); $ii++) { 212 $part = substr($suffix, 0, $ii); 213 if (isset($ids[$prefix.$part])) { 214 $result[$prefix.$part][] = $commit; 215 } 216 if (isset($ids[$part])) { 217 $result[$part][] = $commit; 218 } 219 } 220 } 221 } 222 223 foreach ($result as $identifier => $matching_commits) { 224 if (count($matching_commits) == 1) { 225 $result[$identifier] = head($matching_commits); 226 } else { 227 // This reference is ambiguous (it matches more than one commit) so 228 // don't link it. 229 unset($result[$identifier]); 230 } 231 } 232 233 $this->identifierMap += $result; 234 } 235 236 return $commits; 237 } 238 239 protected function didFilterPage(array $commits) { 240 if ($this->needCommitData) { 241 $data = id(new PhabricatorRepositoryCommitData())->loadAllWhere( 242 'commitID in (%Ld)', 243 mpull($commits, 'getID')); 244 $data = mpull($data, null, 'getCommitID'); 245 foreach ($commits as $commit) { 246 $commit_data = idx($data, $commit->getID()); 247 if (!$commit_data) { 248 $commit_data = new PhabricatorRepositoryCommitData(); 249 } 250 $commit->attachCommitData($commit_data); 251 } 252 } 253 254 // TODO: This should just be `needAuditRequests`, not `shouldJoinAudits()`, 255 // but leave that for a future diff. 256 257 if ($this->needAuditRequests || $this->shouldJoinAudits()) { 258 $requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere( 259 'commitPHID IN (%Ls)', 260 mpull($commits, 'getPHID')); 261 262 $requests = mgroup($requests, 'getCommitPHID'); 263 foreach ($commits as $commit) { 264 $audit_requests = idx($requests, $commit->getPHID(), array()); 265 $commit->attachAudits($audit_requests); 266 foreach ($audit_requests as $audit_request) { 267 $audit_request->attachCommit($commit); 268 } 269 } 270 } 271 272 return $commits; 273 } 274 275 private function buildWhereClause(AphrontDatabaseConnection $conn_r) { 276 $where = array(); 277 278 if ($this->repositoryPHIDs !== null) { 279 $map_repositories = id (new PhabricatorRepositoryQuery()) 280 ->setViewer($this->getViewer()) 281 ->withPHIDs($this->repositoryPHIDs) 282 ->execute(); 283 284 if (!$map_repositories) { 285 throw new PhabricatorEmptyQueryException(); 286 } 287 $repository_ids = mpull($map_repositories, 'getID'); 288 if ($this->repositoryIDs !== null) { 289 $repository_ids = array_merge($repository_ids, $this->repositoryIDs); 290 } 291 $this->withRepositoryIDs($repository_ids); 292 } 293 294 if ($this->ids !== null) { 295 $where[] = qsprintf( 296 $conn_r, 297 'commit.id IN (%Ld)', 298 $this->ids); 299 } 300 301 if ($this->phids !== null) { 302 $where[] = qsprintf( 303 $conn_r, 304 'commit.phid IN (%Ls)', 305 $this->phids); 306 } 307 308 if ($this->repositoryIDs !== null) { 309 $where[] = qsprintf( 310 $conn_r, 311 'commit.repositoryID IN (%Ld)', 312 $this->repositoryIDs); 313 } 314 315 if ($this->authorPHIDs !== null) { 316 $where[] = qsprintf( 317 $conn_r, 318 'commit.authorPHID IN (%Ls)', 319 $this->authorPHIDs); 320 } 321 322 if ($this->identifiers !== null) { 323 $min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH; 324 $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; 325 326 $refs = array(); 327 $bare = array(); 328 foreach ($this->identifiers as $identifier) { 329 $matches = null; 330 preg_match('/^(?:r([A-Z]+))?(.*)$/', $identifier, $matches); 331 $repo = nonempty($matches[1], null); 332 $identifier = nonempty($matches[2], null); 333 334 if ($repo === null) { 335 if ($this->defaultRepository) { 336 $repo = $this->defaultRepository->getCallsign(); 337 } 338 } 339 340 if ($repo === null) { 341 if (strlen($identifier) < $min_unqualified) { 342 continue; 343 } 344 $bare[] = $identifier; 345 } else { 346 $refs[] = array( 347 'callsign' => $repo, 348 'identifier' => $identifier, 349 ); 350 } 351 } 352 353 $sql = array(); 354 355 foreach ($bare as $identifier) { 356 $sql[] = qsprintf( 357 $conn_r, 358 '(commit.commitIdentifier LIKE %> AND '. 359 'LENGTH(commit.commitIdentifier) = 40)', 360 $identifier); 361 } 362 363 if ($refs) { 364 $callsigns = ipull($refs, 'callsign'); 365 $repos = id(new PhabricatorRepositoryQuery()) 366 ->setViewer($this->getViewer()) 367 ->withCallsigns($callsigns) 368 ->execute(); 369 $repos = mpull($repos, null, 'getCallsign'); 370 371 foreach ($refs as $key => $ref) { 372 $repo = idx($repos, $ref['callsign']); 373 if (!$repo) { 374 continue; 375 } 376 377 if ($repo->isSVN()) { 378 if (!ctype_digit($ref['identifier'])) { 379 continue; 380 } 381 $sql[] = qsprintf( 382 $conn_r, 383 '(commit.repositoryID = %d AND commit.commitIdentifier = %s)', 384 $repo->getID(), 385 // NOTE: Because the 'commitIdentifier' column is a string, MySQL 386 // ignores the index if we hand it an integer. Hand it a string. 387 // See T3377. 388 (int)$ref['identifier']); 389 } else { 390 if (strlen($ref['identifier']) < $min_qualified) { 391 continue; 392 } 393 $sql[] = qsprintf( 394 $conn_r, 395 '(commit.repositoryID = %d AND commit.commitIdentifier LIKE %>)', 396 $repo->getID(), 397 $ref['identifier']); 398 } 399 } 400 } 401 402 if (!$sql) { 403 // If we discarded all possible identifiers (e.g., they all referenced 404 // bogus repositories or were all too short), make sure the query finds 405 // nothing. 406 throw new PhabricatorEmptyQueryException( 407 pht('No commit identifiers.')); 408 } 409 410 $where[] = '('.implode(' OR ', $sql).')'; 411 } 412 413 if ($this->auditIDs !== null) { 414 $where[] = qsprintf( 415 $conn_r, 416 'audit.id IN (%Ld)', 417 $this->auditIDs); 418 } 419 420 if ($this->auditorPHIDs !== null) { 421 $where[] = qsprintf( 422 $conn_r, 423 'audit.auditorPHID IN (%Ls)', 424 $this->auditorPHIDs); 425 } 426 427 if ($this->auditAwaitingUser) { 428 $awaiting_user_phid = $this->auditAwaitingUser->getPHID(); 429 // Exclude package and project audits associated with commits where 430 // the user is the author. 431 $where[] = qsprintf( 432 $conn_r, 433 '(commit.authorPHID IS NULL OR commit.authorPHID != %s) 434 OR (audit.auditorPHID = %s)', 435 $awaiting_user_phid, 436 $awaiting_user_phid); 437 } 438 439 $status = $this->auditStatus; 440 if ($status !== null) { 441 switch ($status) { 442 case self::AUDIT_STATUS_PARTIAL: 443 $where[] = qsprintf( 444 $conn_r, 445 'commit.auditStatus = %d', 446 PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED); 447 break; 448 case self::AUDIT_STATUS_ACCEPTED: 449 $where[] = qsprintf( 450 $conn_r, 451 'commit.auditStatus = %d', 452 PhabricatorAuditCommitStatusConstants::FULLY_AUDITED); 453 break; 454 case self::AUDIT_STATUS_CONCERN: 455 $where[] = qsprintf( 456 $conn_r, 457 'audit.auditStatus = %s', 458 PhabricatorAuditStatusConstants::CONCERNED); 459 break; 460 case self::AUDIT_STATUS_OPEN: 461 $where[] = qsprintf( 462 $conn_r, 463 'audit.auditStatus in (%Ls)', 464 PhabricatorAuditStatusConstants::getOpenStatusConstants()); 465 if ($this->auditAwaitingUser) { 466 $where[] = qsprintf( 467 $conn_r, 468 'awaiting.auditStatus IS NULL OR awaiting.auditStatus != %s', 469 PhabricatorAuditStatusConstants::RESIGNED); 470 } 471 break; 472 case self::AUDIT_STATUS_ANY: 473 break; 474 default: 475 $valid = array( 476 self::AUDIT_STATUS_ANY, 477 self::AUDIT_STATUS_OPEN, 478 self::AUDIT_STATUS_CONCERN, 479 self::AUDIT_STATUS_ACCEPTED, 480 self::AUDIT_STATUS_PARTIAL, 481 ); 482 throw new Exception( 483 "Unknown audit status '{$status}'! Valid statuses are: ". 484 implode(', ', $valid)); 485 } 486 } 487 488 $where[] = $this->buildPagingClause($conn_r); 489 490 return $this->formatWhereClause($where); 491 } 492 493 public function didFilterResults(array $filtered) { 494 if ($this->identifierMap) { 495 foreach ($this->identifierMap as $name => $commit) { 496 if (isset($filtered[$commit->getPHID()])) { 497 unset($this->identifierMap[$name]); 498 } 499 } 500 } 501 } 502 503 private function buildJoinClause($conn_r) { 504 $joins = array(); 505 $audit_request = new PhabricatorRepositoryAuditRequest(); 506 507 if ($this->shouldJoinAudits()) { 508 $joins[] = qsprintf( 509 $conn_r, 510 '%Q %T audit ON commit.phid = audit.commitPHID', 511 ($this->rowsMustHaveAudits() ? 'JOIN' : 'LEFT JOIN'), 512 $audit_request->getTableName()); 513 } 514 515 if ($this->auditAwaitingUser) { 516 // Join the request table on the awaiting user's requests, so we can 517 // filter out package and project requests which the user has resigned 518 // from. 519 $joins[] = qsprintf( 520 $conn_r, 521 'LEFT JOIN %T awaiting ON audit.commitPHID = awaiting.commitPHID AND 522 awaiting.auditorPHID = %s', 523 $audit_request->getTableName(), 524 $this->auditAwaitingUser->getPHID()); 525 } 526 527 if ($joins) { 528 return implode(' ', $joins); 529 } else { 530 return ''; 531 } 532 } 533 534 private function buildGroupClause(AphrontDatabaseConnection $conn_r) { 535 $should_group = $this->shouldJoinAudits(); 536 537 // TODO: Currently, the audit table is missing a unique key, so we may 538 // require a GROUP BY if we perform this join. See T1768. This can be 539 // removed once the table has the key. 540 if ($this->auditAwaitingUser) { 541 $should_group = true; 542 } 543 544 if ($should_group) { 545 return 'GROUP BY commit.id'; 546 } else { 547 return ''; 548 } 549 } 550 551 public function getQueryApplicationClass() { 552 return 'PhabricatorDiffusionApplication'; 553 } 554 555 }
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 |