@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 392 lines 11 kB view raw
1<?php 2 3/** 4 * Resolves references (like short commit names, branch names, tag names, etc.) 5 * into canonical, stable commit identifiers. This query works for all 6 * repository types. 7 * 8 * This query will always resolve refs which can be resolved, but may need to 9 * perform VCS operations. A faster (but less complete) counterpart query is 10 * available in @{class:DiffusionCachedResolveRefsQuery}; that query can 11 * resolve most refs without VCS operations. 12 */ 13final class DiffusionLowLevelResolveRefsQuery 14 extends DiffusionLowLevelQuery { 15 16 private $refs; 17 private $types; 18 19 public function withRefs(array $refs) { 20 $this->refs = $refs; 21 return $this; 22 } 23 24 public function withTypes(array $types) { 25 $this->types = $types; 26 return $this; 27 } 28 29 protected function executeQuery() { 30 if (!$this->refs) { 31 return array(); 32 } 33 34 $repository = $this->getRepository(); 35 if (!$repository->hasLocalWorkingCopy()) { 36 return array(); 37 } 38 39 switch ($this->getRepository()->getVersionControlSystem()) { 40 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 41 $result = $this->resolveGitRefs(); 42 break; 43 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 44 $result = $this->resolveMercurialRefs(); 45 break; 46 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 47 $result = $this->resolveSubversionRefs(); 48 break; 49 default: 50 throw new Exception(pht('Unsupported repository type!')); 51 } 52 53 if ($this->types !== null) { 54 $result = $this->filterRefsByType($result, $this->types); 55 } 56 57 return $result; 58 } 59 60 private function resolveGitRefs() { 61 $repository = $this->getRepository(); 62 63 $unresolved = array_fuse($this->refs); 64 $results = array(); 65 66 $possible_symbols = array(); 67 foreach ($unresolved as $ref) { 68 69 // See T13647. If this symbol is exactly 40 hex characters long, it may 70 // never resolve as a branch or tag name. Filter these symbols out for 71 // consistency with Git behavior -- and to avoid an expensive 72 // "git for-each-ref" when resolving only commit hashes, which happens 73 // during repository updates. 74 75 if (preg_match('(^[a-f0-9]{40}\z)', $ref)) { 76 continue; 77 } 78 79 $possible_symbols[$ref] = $ref; 80 } 81 82 // First, resolve branches and tags. 83 if ($possible_symbols) { 84 $ref_map = id(new DiffusionLowLevelGitRefQuery()) 85 ->setRepository($repository) 86 ->withRefTypes( 87 array( 88 PhabricatorRepositoryRefCursor::TYPE_BRANCH, 89 PhabricatorRepositoryRefCursor::TYPE_TAG, 90 )) 91 ->execute(); 92 $ref_map = mgroup($ref_map, 'getShortName'); 93 94 $tag_prefix = 'refs/tags/'; 95 foreach ($possible_symbols as $ref) { 96 if (empty($ref_map[$ref])) { 97 continue; 98 } 99 100 foreach ($ref_map[$ref] as $result) { 101 $fields = $result->getRawFields(); 102 $objectname = idx($fields, 'refname'); 103 if (!strncmp($objectname, $tag_prefix, strlen($tag_prefix))) { 104 $type = 'tag'; 105 } else { 106 $type = 'branch'; 107 } 108 109 $info = array( 110 'type' => $type, 111 'identifier' => $result->getCommitIdentifier(), 112 ); 113 114 if ($type == 'tag') { 115 $alternate = idx($fields, 'objectname'); 116 if ($alternate) { 117 $info['alternate'] = $alternate; 118 } 119 } 120 121 $results[$ref][] = $info; 122 } 123 124 unset($unresolved[$ref]); 125 } 126 } 127 128 // If we resolved everything, we're done. 129 if (!$unresolved) { 130 return $results; 131 } 132 133 // Try to resolve anything else. This stuff either doesn't exist or is 134 // some ref like "HEAD^^^". 135 $future = $repository->getLocalCommandFuture('cat-file --batch-check'); 136 $future->write(implode("\n", $unresolved)); 137 list($stdout) = $future->resolvex(); 138 139 $lines = explode("\n", rtrim($stdout, "\n")); 140 if (count($lines) !== count($unresolved)) { 141 throw new Exception( 142 pht( 143 'Unexpected line count from `%s` in %s!', 144 'git cat-file', 145 $repository->getMonogram())); 146 } 147 148 $hits = array(); 149 $tags = array(); 150 151 $lines = array_combine($unresolved, $lines); 152 foreach ($lines as $ref => $line) { 153 $parts = explode(' ', $line); 154 if (count($parts) < 2) { 155 throw new Exception( 156 pht( 157 'Failed to parse `%s` output in %s: %s', 158 'git cat-file', 159 $repository->getMonogram(), 160 $line)); 161 } 162 list($identifier, $type) = $parts; 163 164 if ($type == 'missing') { 165 // This is either an ambiguous reference which resolves to several 166 // objects, or an invalid reference. For now, always treat it as 167 // invalid. It would be nice to resolve all possibilities for 168 // ambiguous references at some point, although the strategy for doing 169 // so isn't clear to me. 170 continue; 171 } 172 173 switch ($type) { 174 case 'commit': 175 break; 176 case 'tag': 177 $tags[] = $identifier; 178 break; 179 default: 180 throw new Exception( 181 pht( 182 'Unexpected object type from `%s` in %s: %s', 183 'git cat-file', 184 $repository->getMonogram(), 185 $line)); 186 } 187 188 $hits[] = array( 189 'ref' => $ref, 190 'type' => $type, 191 'identifier' => $identifier, 192 ); 193 } 194 195 $tag_map = array(); 196 if ($tags) { 197 // If some of the refs were tags, just load every tag in order to figure 198 // out which commits they map to. This might be somewhat inefficient in 199 // repositories with a huge number of tags. 200 $tag_refs = id(new DiffusionLowLevelGitRefQuery()) 201 ->setRepository($repository) 202 ->withRefTypes( 203 array( 204 PhabricatorRepositoryRefCursor::TYPE_TAG, 205 )) 206 ->executeQuery(); 207 foreach ($tag_refs as $tag_ref) { 208 $tag_map[$tag_ref->getShortName()] = $tag_ref->getCommitIdentifier(); 209 } 210 } 211 212 $results = array(); 213 foreach ($hits as $hit) { 214 $type = $hit['type']; 215 $ref = $hit['ref']; 216 217 $alternate = null; 218 if ($type == 'tag') { 219 $tag_identifier = idx($tag_map, $ref); 220 if ($tag_identifier === null) { 221 // This can happen when we're asked to resolve the hash of a "tag" 222 // object created with "git tag --annotate" that isn't currently 223 // reachable from any ref. Just leave things as they are. 224 } else { 225 // Otherwise, we have a normal named tag. 226 $alternate = $identifier; 227 $identifier = $tag_identifier; 228 } 229 } 230 231 $result = array( 232 'type' => $type, 233 'identifier' => $identifier, 234 ); 235 236 if ($alternate !== null) { 237 $result['alternate'] = $alternate; 238 } 239 240 $results[$ref][] = $result; 241 } 242 243 return $results; 244 } 245 246 private function resolveMercurialRefs() { 247 $repository = $this->getRepository(); 248 249 // First, pull all of the branch heads in the repository. Doing this in 250 // bulk is much faster than querying each individual head if we're 251 // checking even a small number of refs. 252 $branches = id(new DiffusionLowLevelMercurialBranchesQuery()) 253 ->setRepository($repository) 254 ->executeQuery(); 255 256 $branches = mgroup($branches, 'getShortName'); 257 258 $results = array(); 259 $unresolved = $this->refs; 260 foreach ($unresolved as $key => $ref) { 261 if (empty($branches[$ref])) { 262 continue; 263 } 264 265 foreach ($branches[$ref] as $branch) { 266 $fields = $branch->getRawFields(); 267 268 $results[$ref][] = array( 269 'type' => 'branch', 270 'identifier' => $branch->getCommitIdentifier(), 271 'closed' => idx($fields, 'closed', false), 272 ); 273 } 274 275 unset($unresolved[$key]); 276 } 277 278 if (!$unresolved) { 279 return $results; 280 } 281 282 // If some of the refs look like hashes, try to bulk resolve them. This 283 // workflow happens via RefEngine and bulk resolution is dramatically 284 // faster than individual resolution. See PHI158. 285 286 $hashlike = array(); 287 foreach ($unresolved as $key => $ref) { 288 if (preg_match('/^[a-f0-9]{40}\z/', $ref)) { 289 $hashlike[$key] = $ref; 290 } 291 } 292 293 if (count($hashlike) > 1) { 294 $hashlike_map = array(); 295 296 $hashlike_groups = array_chunk($hashlike, 64, true); 297 foreach ($hashlike_groups as $hashlike_group) { 298 $hashlike_arg = array(); 299 foreach ($hashlike_group as $hashlike_ref) { 300 $hashlike_arg[] = hgsprintf('%s', $hashlike_ref); 301 } 302 $hashlike_arg = '('.implode(' or ', $hashlike_arg).')'; 303 304 list($err, $refs) = $repository->execLocalCommand( 305 'log --template=%s --rev %s', 306 '{node}\n', 307 $hashlike_arg); 308 if ($err) { 309 // NOTE: If any ref fails to resolve, Mercurial will exit with an 310 // error. We just give up on the whole group and resolve it 311 // individually below. In theory, we could split it into subgroups 312 // but the pathway where this bulk resolution matters rarely tries 313 // to resolve missing refs (see PHI158). 314 continue; 315 } 316 317 $refs = phutil_split_lines($refs, false); 318 319 foreach ($refs as $ref) { 320 $hashlike_map[$ref] = true; 321 } 322 } 323 324 foreach ($unresolved as $key => $ref) { 325 if (!isset($hashlike_map[$ref])) { 326 continue; 327 } 328 329 $results[$ref][] = array( 330 'type' => 'commit', 331 'identifier' => $ref, 332 ); 333 334 unset($unresolved[$key]); 335 } 336 } 337 338 if (!$unresolved) { 339 return $results; 340 } 341 342 // If we still have unresolved refs (which might be things like "tip"), 343 // try to resolve them individually. 344 345 $futures = array(); 346 foreach ($unresolved as $ref) { 347 $futures[$ref] = $repository->getLocalCommandFuture( 348 'log --template=%s --rev %s', 349 '{node}', 350 hgsprintf('%s', $ref)); 351 } 352 353 foreach (new FutureIterator($futures) as $ref => $future) { 354 try { 355 list($stdout) = $future->resolvex(); 356 } catch (CommandException $ex) { 357 if (preg_match('/ambiguous identifier/', $ex->getStderr())) { 358 // This indicates that the ref ambiguously matched several things. 359 // Eventually, it would be nice to return all of them, but it is 360 // unclear how to best do that. For now, treat it as a miss instead. 361 continue; 362 } 363 if (preg_match('/unknown revision/', $ex->getStderr())) { 364 // No matches for this ref. 365 continue; 366 } 367 throw $ex; 368 } 369 370 // It doesn't look like we can figure out the type (commit/branch/rev) 371 // from this output very easily. For now, just call everything a commit. 372 $type = 'commit'; 373 374 $results[$ref][] = array( 375 'type' => $type, 376 'identifier' => trim($stdout), 377 ); 378 } 379 380 return $results; 381 } 382 383 private function resolveSubversionRefs() { 384 // We don't have any VCS logic for Subversion, so just use the cached 385 // query. 386 return id(new DiffusionCachedResolveRefsQuery()) 387 ->setRepository($this->getRepository()) 388 ->withRefs($this->refs) 389 ->execute(); 390 } 391 392}