[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/repository/engine/ -> PhabricatorRepositoryPullEngine.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1