[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Manages execution of `git pull` and `hg pull` commands for 5 * @{class:PhabricatorRepository} objects. Used by 6 * @{class:PhabricatorRepositoryPullLocalDaemon}. 7 * 8 * This class also covers initial working copy setup through `git clone`, 9 * `git init`, `hg clone`, `hg init`, or `svnadmin create`. 10 * 11 * @task pull Pulling Working Copies 12 * @task git Pulling Git Working Copies 13 * @task hg Pulling Mercurial Working Copies 14 * @task svn Pulling Subversion Working Copies 15 * @task internal Internals 16 */ 17 final class PhabricatorRepositoryPullEngine 18 extends PhabricatorRepositoryEngine { 19 20 21 /* -( Pulling Working Copies )--------------------------------------------- */ 22 23 24 public function pullRepository() { 25 $repository = $this->getRepository(); 26 27 $is_hg = false; 28 $is_git = false; 29 $is_svn = false; 30 31 $vcs = $repository->getVersionControlSystem(); 32 $callsign = $repository->getCallsign(); 33 34 switch ($vcs) { 35 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 36 // We never pull a local copy of non-hosted Subversion repositories. 37 if (!$repository->isHosted()) { 38 $this->skipPull( 39 pht( 40 "Repository '%s' is a non-hosted Subversion repository, which ". 41 "does not require a local working copy to be pulled.", 42 $callsign)); 43 return; 44 } 45 $is_svn = true; 46 break; 47 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 48 $is_git = true; 49 break; 50 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 51 $is_hg = true; 52 break; 53 default: 54 $this->abortPull(pht('Unknown VCS "%s"!', $vcs)); 55 } 56 57 $callsign = $repository->getCallsign(); 58 $local_path = $repository->getLocalPath(); 59 if ($local_path === null) { 60 $this->abortPull( 61 pht( 62 "No local path is configured for repository '%s'.", 63 $callsign)); 64 } 65 66 try { 67 $dirname = dirname($local_path); 68 if (!Filesystem::pathExists($dirname)) { 69 Filesystem::createDirectory($dirname, 0755, $recursive = true); 70 } 71 72 if (!Filesystem::pathExists($local_path)) { 73 $this->logPull( 74 pht( 75 "Creating a new working copy for repository '%s'.", 76 $callsign)); 77 if ($is_git) { 78 $this->executeGitCreate(); 79 } else if ($is_hg) { 80 $this->executeMercurialCreate(); 81 } else { 82 $this->executeSubversionCreate(); 83 } 84 } else { 85 if (!$repository->isHosted()) { 86 $this->logPull( 87 pht( 88 "Updating the working copy for repository '%s'.", 89 $callsign)); 90 if ($is_git) { 91 $this->verifyGitOrigin($repository); 92 $this->executeGitUpdate(); 93 } else if ($is_hg) { 94 $this->executeMercurialUpdate(); 95 } 96 } 97 } 98 99 if ($repository->isHosted()) { 100 if ($is_git) { 101 $this->installGitHook(); 102 } else if ($is_svn) { 103 $this->installSubversionHook(); 104 } else if ($is_hg) { 105 $this->installMercurialHook(); 106 } 107 108 foreach ($repository->getHookDirectories() as $directory) { 109 $this->installHookDirectory($directory); 110 } 111 } 112 113 } catch (Exception $ex) { 114 $this->abortPull( 115 pht('Pull of "%s" failed: %s', $callsign, $ex->getMessage()), 116 $ex); 117 } 118 119 $this->donePull(); 120 121 return $this; 122 } 123 124 private function skipPull($message) { 125 $this->log('%s', $message); 126 $this->donePull(); 127 } 128 129 private function abortPull($message, Exception $ex = null) { 130 $code_error = PhabricatorRepositoryStatusMessage::CODE_ERROR; 131 $this->updateRepositoryInitStatus($code_error, $message); 132 if ($ex) { 133 throw $ex; 134 } else { 135 throw new Exception($message); 136 } 137 } 138 139 private function logPull($message) { 140 $code_working = PhabricatorRepositoryStatusMessage::CODE_WORKING; 141 $this->updateRepositoryInitStatus($code_working, $message); 142 $this->log('%s', $message); 143 } 144 145 private function donePull() { 146 $code_okay = PhabricatorRepositoryStatusMessage::CODE_OKAY; 147 $this->updateRepositoryInitStatus($code_okay); 148 } 149 150 private function updateRepositoryInitStatus($code, $message = null) { 151 $this->getRepository()->writeStatusMessage( 152 PhabricatorRepositoryStatusMessage::TYPE_INIT, 153 $code, 154 array( 155 'message' => $message, 156 )); 157 } 158 159 private function installHook($path) { 160 $this->log('%s', pht('Installing commit hook to "%s"...', $path)); 161 162 $repository = $this->getRepository(); 163 $callsign = $repository->getCallsign(); 164 165 $root = dirname(phutil_get_library_root('phabricator')); 166 $bin = $root.'/bin/commit-hook'; 167 168 $full_php_path = Filesystem::resolveBinary('php'); 169 $cmd = csprintf( 170 'exec %s -f %s -- %s "$@"', 171 $full_php_path, 172 $bin, 173 $callsign); 174 175 $hook = "#!/bin/sh\nexport TERM=dumb\n{$cmd}\n"; 176 177 Filesystem::writeFile($path, $hook); 178 Filesystem::changePermissions($path, 0755); 179 } 180 181 private function installHookDirectory($path) { 182 $readme = pht( 183 "To add custom hook scripts to this repository, add them to this ". 184 "directory.\n\nPhabricator will run any executables in this directory ". 185 "after running its own checks, as though they were normal hook ". 186 "scripts."); 187 188 Filesystem::createDirectory($path, 0755); 189 Filesystem::writeFile($path.'/README', $readme); 190 } 191 192 193 /* -( Pulling Git Working Copies )----------------------------------------- */ 194 195 196 /** 197 * @task git 198 */ 199 private function executeGitCreate() { 200 $repository = $this->getRepository(); 201 202 $path = rtrim($repository->getLocalPath(), '/'); 203 204 if ($repository->isHosted()) { 205 $repository->execxRemoteCommand( 206 'init --bare -- %s', 207 $path); 208 } else { 209 $repository->execxRemoteCommand( 210 'clone --bare -- %P %s', 211 $repository->getRemoteURIEnvelope(), 212 $path); 213 } 214 } 215 216 217 /** 218 * @task git 219 */ 220 private function executeGitUpdate() { 221 $repository = $this->getRepository(); 222 223 list($err, $stdout) = $repository->execLocalCommand( 224 'rev-parse --show-toplevel'); 225 226 $message = null; 227 $path = $repository->getLocalPath(); 228 if ($err) { 229 // Try to raise a more tailored error message in the more common case 230 // of the user creating an empty directory. (We could try to remove it, 231 // but might not be able to, and it's much simpler to raise a good 232 // message than try to navigate those waters.) 233 if (is_dir($path)) { 234 $files = Filesystem::listDirectory($path, $include_hidden = true); 235 if (!$files) { 236 $message = 237 "Expected to find a git repository at '{$path}', but there ". 238 "is an empty directory there. Remove the directory: the daemon ". 239 "will run 'git clone' for you."; 240 } else { 241 $message = 242 "Expected to find a git repository at '{$path}', but there is ". 243 "a non-repository directory (with other stuff in it) there. Move ". 244 "or remove this directory (or reconfigure the repository to use a ". 245 "different directory), and then either clone a repository ". 246 "yourself or let the daemon do it."; 247 } 248 } else if (is_file($path)) { 249 $message = 250 "Expected to find a git repository at '{$path}', but there is a ". 251 "file there instead. Remove it and let the daemon clone a ". 252 "repository for you."; 253 } else { 254 $message = 255 "Expected to find a git repository at '{$path}', but did not."; 256 } 257 } else { 258 $repo_path = rtrim($stdout, "\n"); 259 260 if (empty($repo_path)) { 261 // This can mean one of two things: we're in a bare repository, or 262 // we're inside a git repository inside another git repository. Since 263 // the first is dramatically more likely now that we perform bare 264 // clones and I don't have a great way to test for the latter, assume 265 // we're OK. 266 } else if (!Filesystem::pathsAreEquivalent($repo_path, $path)) { 267 $err = true; 268 $message = 269 "Expected to find repo at '{$path}', but the actual ". 270 "git repository root for this directory is '{$repo_path}'. ". 271 "Something is misconfigured. The repository's 'Local Path' should ". 272 "be set to some place where the daemon can check out a working ". 273 "copy, and should not be inside another git repository."; 274 } 275 } 276 277 if ($err && $repository->canDestroyWorkingCopy()) { 278 phlog("Repository working copy at '{$path}' failed sanity check; ". 279 "destroying and re-cloning. {$message}"); 280 Filesystem::remove($path); 281 $this->executeGitCreate(); 282 } else if ($err) { 283 throw new Exception($message); 284 } 285 286 $retry = false; 287 do { 288 // This is a local command, but needs credentials. 289 if ($repository->isWorkingCopyBare()) { 290 // For bare working copies, we need this magic incantation. 291 $future = $repository->getRemoteCommandFuture( 292 'fetch origin %s --prune', 293 '+refs/heads/*:refs/heads/*'); 294 } else { 295 $future = $repository->getRemoteCommandFuture( 296 'fetch --all --prune'); 297 } 298 299 $future->setCWD($path); 300 list($err, $stdout, $stderr) = $future->resolve(); 301 302 if ($err && !$retry && $repository->canDestroyWorkingCopy()) { 303 $retry = true; 304 // Fix remote origin url if it doesn't match our configuration 305 $origin_url = $repository->execLocalCommand( 306 'config --get remote.origin.url'); 307 $remote_uri = $repository->getRemoteURIEnvelope(); 308 if ($origin_url != $remote_uri->openEnvelope()) { 309 $repository->execLocalCommand( 310 'remote set-url origin %P', 311 $remote_uri); 312 } 313 } else if ($err) { 314 throw new Exception( 315 "git fetch failed with error #{$err}:\n". 316 "stdout:{$stdout}\n\n". 317 "stderr:{$stderr}\n"); 318 } else { 319 $retry = false; 320 } 321 } while ($retry); 322 } 323 324 325 /** 326 * @task git 327 */ 328 private function installGitHook() { 329 $repository = $this->getRepository(); 330 $root = $repository->getLocalPath(); 331 332 if ($repository->isWorkingCopyBare()) { 333 $path = '/hooks/pre-receive'; 334 } else { 335 $path = '/.git/hooks/pre-receive'; 336 } 337 338 $this->installHook($root.$path); 339 } 340 341 342 /* -( Pulling Mercurial Working Copies )----------------------------------- */ 343 344 345 /** 346 * @task hg 347 */ 348 private function executeMercurialCreate() { 349 $repository = $this->getRepository(); 350 351 $path = rtrim($repository->getLocalPath(), '/'); 352 353 if ($repository->isHosted()) { 354 $repository->execxRemoteCommand( 355 'init -- %s', 356 $path); 357 } else { 358 $repository->execxRemoteCommand( 359 'clone --noupdate -- %P %s', 360 $repository->getRemoteURIEnvelope(), 361 $path); 362 } 363 } 364 365 366 /** 367 * @task hg 368 */ 369 private function executeMercurialUpdate() { 370 $repository = $this->getRepository(); 371 $path = $repository->getLocalPath(); 372 373 // This is a local command, but needs credentials. 374 $future = $repository->getRemoteCommandFuture('pull -u'); 375 $future->setCWD($path); 376 377 try { 378 $future->resolvex(); 379 } catch (CommandException $ex) { 380 $err = $ex->getError(); 381 $stdout = $ex->getStdOut(); 382 383 // NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the behavior 384 // of "hg pull" to return 1 in case of a successful pull with no changes. 385 // This behavior has been reverted, but users who updated between Feb 1, 386 // 2012 and Mar 1, 2012 will have the erroring version. Do a dumb test 387 // against stdout to check for this possibility. 388 // See: https://github.com/phacility/phabricator/issues/101/ 389 390 // NOTE: Mercurial has translated versions, which translate this error 391 // string. In a translated version, the string will be something else, 392 // like "aucun changement trouve". There didn't seem to be an easy way 393 // to handle this (there are hard ways but this is not a common problem 394 // and only creates log spam, not application failures). Assume English. 395 396 // TODO: Remove this once we're far enough in the future that deployment 397 // of 2.1 is exceedingly rare? 398 if ($err == 1 && preg_match('/no changes found/', $stdout)) { 399 return; 400 } else { 401 throw $ex; 402 } 403 } 404 } 405 406 407 /** 408 * @task hg 409 */ 410 private function installMercurialHook() { 411 $repository = $this->getRepository(); 412 $path = $repository->getLocalPath().'/.hg/hgrc'; 413 414 $root = dirname(phutil_get_library_root('phabricator')); 415 $bin = $root.'/bin/commit-hook'; 416 417 $data = array(); 418 $data[] = '[hooks]'; 419 420 // This hook handles normal pushes. 421 $data[] = csprintf( 422 'pretxnchangegroup.phabricator = %s %s %s', 423 $bin, 424 $repository->getCallsign(), 425 'pretxnchangegroup'); 426 427 // This one handles creating bookmarks. 428 $data[] = csprintf( 429 'prepushkey.phabricator = %s %s %s', 430 $bin, 431 $repository->getCallsign(), 432 'prepushkey'); 433 434 $data[] = null; 435 436 $data = implode("\n", $data); 437 438 $this->log('%s', pht('Installing commit hook config to "%s"...', $path)); 439 440 Filesystem::writeFile($path, $data); 441 } 442 443 444 /* -( Pulling Subversion Working Copies )---------------------------------- */ 445 446 447 /** 448 * @task svn 449 */ 450 private function executeSubversionCreate() { 451 $repository = $this->getRepository(); 452 453 $path = rtrim($repository->getLocalPath(), '/'); 454 execx('svnadmin create -- %s', $path); 455 } 456 457 458 /** 459 * @task svn 460 */ 461 private function installSubversionHook() { 462 $repository = $this->getRepository(); 463 $root = $repository->getLocalPath(); 464 465 $path = '/hooks/pre-commit'; 466 467 $this->installHook($root.$path); 468 } 469 470 471 }
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 |