[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/repository/storage/ -> PhabricatorRepository.php (source)

   1  <?php
   2  
   3  /**
   4   * @task uri        Repository URI Management
   5   * @task autoclose  Autoclose
   6   */
   7  final class PhabricatorRepository extends PhabricatorRepositoryDAO
   8    implements
   9      PhabricatorPolicyInterface,
  10      PhabricatorFlaggableInterface,
  11      PhabricatorMarkupInterface,
  12      PhabricatorDestructibleInterface,
  13      PhabricatorProjectInterface {
  14  
  15    /**
  16     * Shortest hash we'll recognize in raw "a829f32" form.
  17     */
  18    const MINIMUM_UNQUALIFIED_HASH = 7;
  19  
  20    /**
  21     * Shortest hash we'll recognize in qualified "rXab7ef2f8" form.
  22     */
  23    const MINIMUM_QUALIFIED_HASH = 5;
  24  
  25    const TABLE_PATH = 'repository_path';
  26    const TABLE_PATHCHANGE = 'repository_pathchange';
  27    const TABLE_FILESYSTEM = 'repository_filesystem';
  28    const TABLE_SUMMARY = 'repository_summary';
  29    const TABLE_BADCOMMIT = 'repository_badcommit';
  30    const TABLE_LINTMESSAGE = 'repository_lintmessage';
  31    const TABLE_PARENTS = 'repository_parents';
  32    const TABLE_COVERAGE = 'repository_coverage';
  33  
  34    const SERVE_OFF = 'off';
  35    const SERVE_READONLY = 'readonly';
  36    const SERVE_READWRITE = 'readwrite';
  37  
  38    const BECAUSE_REPOSITORY_IMPORTING = 'auto/importing';
  39    const BECAUSE_AUTOCLOSE_DISABLED = 'auto/disabled';
  40    const BECAUSE_NOT_ON_AUTOCLOSE_BRANCH = 'auto/nobranch';
  41    const BECAUSE_BRANCH_UNTRACKED = 'auto/notrack';
  42    const BECAUSE_BRANCH_NOT_AUTOCLOSE = 'auto/noclose';
  43  
  44    protected $name;
  45    protected $callsign;
  46    protected $uuid;
  47    protected $viewPolicy;
  48    protected $editPolicy;
  49    protected $pushPolicy;
  50  
  51    protected $versionControlSystem;
  52    protected $details = array();
  53    protected $credentialPHID;
  54  
  55    private $commitCount = self::ATTACHABLE;
  56    private $mostRecentCommit = self::ATTACHABLE;
  57    private $projectPHIDs = self::ATTACHABLE;
  58  
  59    public static function initializeNewRepository(PhabricatorUser $actor) {
  60      $app = id(new PhabricatorApplicationQuery())
  61        ->setViewer($actor)
  62        ->withClasses(array('PhabricatorDiffusionApplication'))
  63        ->executeOne();
  64  
  65      $view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY);
  66      $edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY);
  67      $push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY);
  68  
  69      return id(new PhabricatorRepository())
  70        ->setViewPolicy($view_policy)
  71        ->setEditPolicy($edit_policy)
  72        ->setPushPolicy($push_policy);
  73    }
  74  
  75    public function getConfiguration() {
  76      return array(
  77        self::CONFIG_AUX_PHID => true,
  78        self::CONFIG_SERIALIZATION => array(
  79          'details' => self::SERIALIZATION_JSON,
  80        ),
  81        self::CONFIG_COLUMN_SCHEMA => array(
  82          'name' => 'sort255',
  83          'callsign' => 'sort32',
  84          'versionControlSystem' => 'text32',
  85          'uuid' => 'text64?',
  86          'pushPolicy' => 'policy',
  87          'credentialPHID' => 'phid?',
  88        ),
  89        self::CONFIG_KEY_SCHEMA => array(
  90          'key_phid' => null,
  91          'phid' => array(
  92            'columns' => array('phid'),
  93            'unique' => true,
  94          ),
  95          'callsign' => array(
  96            'columns' => array('callsign'),
  97            'unique' => true,
  98          ),
  99          'key_name' => array(
 100            'columns' => array('name(128)'),
 101          ),
 102          'key_vcs' => array(
 103            'columns' => array('versionControlSystem'),
 104          ),
 105        ),
 106      ) + parent::getConfiguration();
 107    }
 108  
 109    public function generatePHID() {
 110      return PhabricatorPHID::generateNewPHID(
 111        PhabricatorRepositoryRepositoryPHIDType::TYPECONST);
 112    }
 113  
 114    public function toDictionary() {
 115      return array(
 116        'id'          => $this->getID(),
 117        'name'        => $this->getName(),
 118        'phid'        => $this->getPHID(),
 119        'callsign'    => $this->getCallsign(),
 120        'monogram'    => $this->getMonogram(),
 121        'vcs'         => $this->getVersionControlSystem(),
 122        'uri'         => PhabricatorEnv::getProductionURI($this->getURI()),
 123        'remoteURI'   => (string)$this->getRemoteURI(),
 124        'description' => $this->getDetail('description'),
 125        'isActive'    => $this->isTracked(),
 126        'isHosted'    => $this->isHosted(),
 127        'isImporting' => $this->isImporting(),
 128      );
 129    }
 130  
 131    public function getMonogram() {
 132      return 'r'.$this->getCallsign();
 133    }
 134  
 135    public function getDetail($key, $default = null) {
 136      return idx($this->details, $key, $default);
 137    }
 138  
 139    public function getHumanReadableDetail($key, $default = null) {
 140      $value = $this->getDetail($key, $default);
 141  
 142      switch ($key) {
 143        case 'branch-filter':
 144        case 'close-commits-filter':
 145          $value = array_keys($value);
 146          $value = implode(', ', $value);
 147          break;
 148      }
 149  
 150      return $value;
 151    }
 152  
 153    public function setDetail($key, $value) {
 154      $this->details[$key] = $value;
 155      return $this;
 156    }
 157  
 158    public function attachCommitCount($count) {
 159      $this->commitCount = $count;
 160      return $this;
 161    }
 162  
 163    public function getCommitCount() {
 164      return $this->assertAttached($this->commitCount);
 165    }
 166  
 167    public function attachMostRecentCommit(
 168      PhabricatorRepositoryCommit $commit = null) {
 169      $this->mostRecentCommit = $commit;
 170      return $this;
 171    }
 172  
 173    public function getMostRecentCommit() {
 174      return $this->assertAttached($this->mostRecentCommit);
 175    }
 176  
 177    public function getDiffusionBrowseURIForPath(
 178      PhabricatorUser $user,
 179      $path,
 180      $line = null,
 181      $branch = null) {
 182  
 183      $drequest = DiffusionRequest::newFromDictionary(
 184        array(
 185          'user' => $user,
 186          'repository' => $this,
 187          'path' => $path,
 188          'branch' => $branch,
 189        ));
 190  
 191      return $drequest->generateURI(
 192        array(
 193          'action' => 'browse',
 194          'line'   => $line,
 195        ));
 196    }
 197  
 198    public function getLocalPath() {
 199      return $this->getDetail('local-path');
 200    }
 201  
 202    public function getSubversionBaseURI($commit = null) {
 203      $subpath = $this->getDetail('svn-subpath');
 204      if (!strlen($subpath)) {
 205        $subpath = null;
 206      }
 207      return $this->getSubversionPathURI($subpath, $commit);
 208    }
 209  
 210    public function getSubversionPathURI($path = null, $commit = null) {
 211      $vcs = $this->getVersionControlSystem();
 212      if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) {
 213        throw new Exception('Not a subversion repository!');
 214      }
 215  
 216      if ($this->isHosted()) {
 217        $uri = 'file://'.$this->getLocalPath();
 218      } else {
 219        $uri = $this->getDetail('remote-uri');
 220      }
 221  
 222      $uri = rtrim($uri, '/');
 223  
 224      if (strlen($path)) {
 225        $path = rawurlencode($path);
 226        $path = str_replace('%2F', '/', $path);
 227        $uri = $uri.'/'.ltrim($path, '/');
 228      }
 229  
 230      if ($path !== null || $commit !== null) {
 231        $uri .= '@';
 232      }
 233  
 234      if ($commit !== null) {
 235        $uri .= $commit;
 236      }
 237  
 238      return $uri;
 239    }
 240  
 241    public function attachProjectPHIDs(array $project_phids) {
 242      $this->projectPHIDs = $project_phids;
 243      return $this;
 244    }
 245  
 246    public function getProjectPHIDs() {
 247      return $this->assertAttached($this->projectPHIDs);
 248    }
 249  
 250  
 251    /**
 252     * Get the name of the directory this repository should clone or checkout
 253     * into. For example, if the repository name is "Example Repository", a
 254     * reasonable name might be "example-repository". This is used to help users
 255     * get reasonable results when cloning repositories, since they generally do
 256     * not want to clone into directories called "X/" or "Example Repository/".
 257     *
 258     * @return string
 259     */
 260    public function getCloneName() {
 261      $name = $this->getDetail('clone-name');
 262  
 263      // Make some reasonable effort to produce reasonable default directory
 264      // names from repository names.
 265      if (!strlen($name)) {
 266        $name = $this->getName();
 267        $name = phutil_utf8_strtolower($name);
 268        $name = preg_replace('@[/ -:]+@', '-', $name);
 269        $name = trim($name, '-');
 270        if (!strlen($name)) {
 271          $name = $this->getCallsign();
 272        }
 273      }
 274  
 275      return $name;
 276    }
 277  
 278  
 279  /* -(  Remote Command Execution  )------------------------------------------- */
 280  
 281  
 282    public function execRemoteCommand($pattern /* , $arg, ... */) {
 283      $args = func_get_args();
 284      return $this->newRemoteCommandFuture($args)->resolve();
 285    }
 286  
 287    public function execxRemoteCommand($pattern /* , $arg, ... */) {
 288      $args = func_get_args();
 289      return $this->newRemoteCommandFuture($args)->resolvex();
 290    }
 291  
 292    public function getRemoteCommandFuture($pattern /* , $arg, ... */) {
 293      $args = func_get_args();
 294      return $this->newRemoteCommandFuture($args);
 295    }
 296  
 297    public function passthruRemoteCommand($pattern /* , $arg, ... */) {
 298      $args = func_get_args();
 299      return $this->newRemoteCommandPassthru($args)->execute();
 300    }
 301  
 302    private function newRemoteCommandFuture(array $argv) {
 303      $argv = $this->formatRemoteCommand($argv);
 304      $future = newv('ExecFuture', $argv);
 305      $future->setEnv($this->getRemoteCommandEnvironment());
 306      return $future;
 307    }
 308  
 309    private function newRemoteCommandPassthru(array $argv) {
 310      $argv = $this->formatRemoteCommand($argv);
 311      $passthru = newv('PhutilExecPassthru', $argv);
 312      $passthru->setEnv($this->getRemoteCommandEnvironment());
 313      return $passthru;
 314    }
 315  
 316  
 317  /* -(  Local Command Execution  )-------------------------------------------- */
 318  
 319  
 320    public function execLocalCommand($pattern /* , $arg, ... */) {
 321      $args = func_get_args();
 322      return $this->newLocalCommandFuture($args)->resolve();
 323    }
 324  
 325    public function execxLocalCommand($pattern /* , $arg, ... */) {
 326      $args = func_get_args();
 327      return $this->newLocalCommandFuture($args)->resolvex();
 328    }
 329  
 330    public function getLocalCommandFuture($pattern /* , $arg, ... */) {
 331      $args = func_get_args();
 332      return $this->newLocalCommandFuture($args);
 333    }
 334  
 335    public function passthruLocalCommand($pattern /* , $arg, ... */) {
 336      $args = func_get_args();
 337      return $this->newLocalCommandPassthru($args)->execute();
 338    }
 339  
 340    private function newLocalCommandFuture(array $argv) {
 341      $this->assertLocalExists();
 342  
 343      $argv = $this->formatLocalCommand($argv);
 344      $future = newv('ExecFuture', $argv);
 345      $future->setEnv($this->getLocalCommandEnvironment());
 346  
 347      if ($this->usesLocalWorkingCopy()) {
 348        $future->setCWD($this->getLocalPath());
 349      }
 350  
 351      return $future;
 352    }
 353  
 354    private function newLocalCommandPassthru(array $argv) {
 355      $this->assertLocalExists();
 356  
 357      $argv = $this->formatLocalCommand($argv);
 358      $future = newv('PhutilExecPassthru', $argv);
 359      $future->setEnv($this->getLocalCommandEnvironment());
 360  
 361      if ($this->usesLocalWorkingCopy()) {
 362        $future->setCWD($this->getLocalPath());
 363      }
 364  
 365      return $future;
 366    }
 367  
 368  
 369  /* -(  Command Infrastructure  )--------------------------------------------- */
 370  
 371  
 372    private function getSSHWrapper() {
 373      $root = dirname(phutil_get_library_root('phabricator'));
 374      return $root.'/bin/ssh-connect';
 375    }
 376  
 377    private function getCommonCommandEnvironment() {
 378      $env = array(
 379        // NOTE: Force the language to "en_US.UTF-8", which overrides locale
 380        // settings. This makes stuff print in English instead of, e.g., French,
 381        // so we can parse the output of some commands, error messages, etc.
 382        'LANG' => 'en_US.UTF-8',
 383  
 384        // Propagate PHABRICATOR_ENV explicitly. For discussion, see T4155.
 385        'PHABRICATOR_ENV' => PhabricatorEnv::getSelectedEnvironmentName(),
 386      );
 387  
 388      switch ($this->getVersionControlSystem()) {
 389        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
 390          break;
 391        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 392          // NOTE: See T2965. Some time after Git 1.7.5.4, Git started fataling if
 393          // it can not read $HOME. For many users, $HOME points at /root (this
 394          // seems to be a default result of Apache setup). Instead, explicitly
 395          // point $HOME at a readable, empty directory so that Git looks for the
 396          // config file it's after, fails to locate it, and moves on. This is
 397          // really silly, but seems like the least damaging approach to
 398          // mitigating the issue.
 399  
 400          $root = dirname(phutil_get_library_root('phabricator'));
 401          $env['HOME'] = $root.'/support/empty/';
 402          break;
 403        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 404          // NOTE: This overrides certain configuration, extensions, and settings
 405          // which make Mercurial commands do random unusual things.
 406          $env['HGPLAIN'] = 1;
 407          break;
 408        default:
 409          throw new Exception('Unrecognized version control system.');
 410      }
 411  
 412      return $env;
 413    }
 414  
 415    private function getLocalCommandEnvironment() {
 416      return $this->getCommonCommandEnvironment();
 417    }
 418  
 419    private function getRemoteCommandEnvironment() {
 420      $env = $this->getCommonCommandEnvironment();
 421  
 422      if ($this->shouldUseSSH()) {
 423        // NOTE: This is read by `bin/ssh-connect`, and tells it which credentials
 424        // to use.
 425        $env['PHABRICATOR_CREDENTIAL'] = $this->getCredentialPHID();
 426        switch ($this->getVersionControlSystem()) {
 427          case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
 428            // Force SVN to use `bin/ssh-connect`.
 429            $env['SVN_SSH'] = $this->getSSHWrapper();
 430            break;
 431          case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 432            // Force Git to use `bin/ssh-connect`.
 433            $env['GIT_SSH'] = $this->getSSHWrapper();
 434            break;
 435          case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 436            // We force Mercurial through `bin/ssh-connect` too, but it uses a
 437            // command-line flag instead of an environmental variable.
 438            break;
 439          default:
 440            throw new Exception('Unrecognized version control system.');
 441        }
 442      }
 443  
 444      return $env;
 445    }
 446  
 447    private function formatRemoteCommand(array $args) {
 448      $pattern = $args[0];
 449      $args = array_slice($args, 1);
 450  
 451      switch ($this->getVersionControlSystem()) {
 452        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
 453          if ($this->shouldUseHTTP() || $this->shouldUseSVNProtocol()) {
 454            $flags = array();
 455            $flag_args = array();
 456            $flags[] = '--non-interactive';
 457            $flags[] = '--no-auth-cache';
 458            if ($this->shouldUseHTTP()) {
 459              $flags[] = '--trust-server-cert';
 460            }
 461  
 462            $credential_phid = $this->getCredentialPHID();
 463            if ($credential_phid) {
 464              $key = PassphrasePasswordKey::loadFromPHID(
 465                $credential_phid,
 466                PhabricatorUser::getOmnipotentUser());
 467              $flags[] = '--username %P';
 468              $flags[] = '--password %P';
 469              $flag_args[] = $key->getUsernameEnvelope();
 470              $flag_args[] = $key->getPasswordEnvelope();
 471            }
 472  
 473            $flags = implode(' ', $flags);
 474            $pattern = "svn {$flags} {$pattern}";
 475            $args = array_mergev(array($flag_args, $args));
 476          } else {
 477            $pattern = "svn --non-interactive {$pattern}";
 478          }
 479          break;
 480        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 481          $pattern = "git {$pattern}";
 482          break;
 483        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 484          if ($this->shouldUseSSH()) {
 485            $pattern = "hg --config ui.ssh=%s {$pattern}";
 486            array_unshift(
 487              $args,
 488              $this->getSSHWrapper());
 489          } else {
 490            $pattern = "hg {$pattern}";
 491          }
 492          break;
 493        default:
 494          throw new Exception('Unrecognized version control system.');
 495      }
 496  
 497      array_unshift($args, $pattern);
 498  
 499      return $args;
 500    }
 501  
 502    private function formatLocalCommand(array $args) {
 503      $pattern = $args[0];
 504      $args = array_slice($args, 1);
 505  
 506      switch ($this->getVersionControlSystem()) {
 507        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
 508          $pattern = "svn --non-interactive {$pattern}";
 509          break;
 510        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 511          $pattern = "git {$pattern}";
 512          break;
 513        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 514          $pattern = "hg {$pattern}";
 515          break;
 516        default:
 517          throw new Exception('Unrecognized version control system.');
 518      }
 519  
 520      array_unshift($args, $pattern);
 521  
 522      return $args;
 523    }
 524  
 525    /**
 526     * Sanitize output of an `hg` command invoked with the `--debug` flag to make
 527     * it usable.
 528     *
 529     * @param string Output from `hg --debug ...`
 530     * @return string Usable output.
 531     */
 532    public static function filterMercurialDebugOutput($stdout) {
 533      // When hg commands are run with `--debug` and some config file isn't
 534      // trusted, Mercurial prints out a warning to stdout, twice, after Feb 2011.
 535      //
 536      // http://selenic.com/pipermail/mercurial-devel/2011-February/028541.html
 537  
 538      $lines = preg_split('/(?<=\n)/', $stdout);
 539      $regex = '/ignoring untrusted configuration option .*\n$/';
 540  
 541      foreach ($lines as $key => $line) {
 542        $lines[$key] = preg_replace($regex, '', $line);
 543      }
 544  
 545      return implode('', $lines);
 546    }
 547  
 548    public function getURI() {
 549      return '/diffusion/'.$this->getCallsign().'/';
 550    }
 551  
 552    public function getNormalizedPath() {
 553      $uri = (string)$this->getCloneURIObject();
 554  
 555      switch ($this->getVersionControlSystem()) {
 556        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 557          $normalized_uri = new PhabricatorRepositoryURINormalizer(
 558            PhabricatorRepositoryURINormalizer::TYPE_GIT,
 559            $uri);
 560          break;
 561        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
 562          $normalized_uri = new PhabricatorRepositoryURINormalizer(
 563            PhabricatorRepositoryURINormalizer::TYPE_SVN,
 564            $uri);
 565          break;
 566        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 567          $normalized_uri = new PhabricatorRepositoryURINormalizer(
 568            PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL,
 569            $uri);
 570          break;
 571        default:
 572          throw new Exception('Unrecognized version control system.');
 573      }
 574  
 575      return $normalized_uri->getNormalizedPath();
 576    }
 577  
 578    public function isTracked() {
 579      return $this->getDetail('tracking-enabled', false);
 580    }
 581  
 582    public function getDefaultBranch() {
 583      $default = $this->getDetail('default-branch');
 584      if (strlen($default)) {
 585        return $default;
 586      }
 587  
 588      $default_branches = array(
 589        PhabricatorRepositoryType::REPOSITORY_TYPE_GIT        => 'master',
 590        PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL  => 'default',
 591      );
 592  
 593      return idx($default_branches, $this->getVersionControlSystem());
 594    }
 595  
 596    public function getDefaultArcanistBranch() {
 597      return coalesce($this->getDefaultBranch(), 'svn');
 598    }
 599  
 600    private function isBranchInFilter($branch, $filter_key) {
 601      $vcs = $this->getVersionControlSystem();
 602  
 603      $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
 604  
 605      $use_filter = ($is_git);
 606      if (!$use_filter) {
 607        // If this VCS doesn't use filters, pass everything through.
 608        return true;
 609      }
 610  
 611  
 612      $filter = $this->getDetail($filter_key, array());
 613  
 614      // If there's no filter set, let everything through.
 615      if (!$filter) {
 616        return true;
 617      }
 618  
 619      // If this branch isn't literally named `regexp(...)`, and it's in the
 620      // filter list, let it through.
 621      if (isset($filter[$branch])) {
 622        if (self::extractBranchRegexp($branch) === null) {
 623          return true;
 624        }
 625      }
 626  
 627      // If the branch matches a regexp, let it through.
 628      foreach ($filter as $pattern => $ignored) {
 629        $regexp = self::extractBranchRegexp($pattern);
 630        if ($regexp !== null) {
 631          if (preg_match($regexp, $branch)) {
 632            return true;
 633          }
 634        }
 635      }
 636  
 637      // Nothing matched, so filter this branch out.
 638      return false;
 639    }
 640  
 641    public static function extractBranchRegexp($pattern) {
 642      $matches = null;
 643      if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) {
 644        return $matches[1];
 645      }
 646      return null;
 647    }
 648  
 649    public function shouldTrackBranch($branch) {
 650      return $this->isBranchInFilter($branch, 'branch-filter');
 651    }
 652  
 653    public function formatCommitName($commit_identifier) {
 654      $vcs = $this->getVersionControlSystem();
 655  
 656      $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
 657      $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
 658  
 659      $is_git = ($vcs == $type_git);
 660      $is_hg = ($vcs == $type_hg);
 661      if ($is_git || $is_hg) {
 662        $short_identifier = substr($commit_identifier, 0, 12);
 663      } else {
 664        $short_identifier = $commit_identifier;
 665      }
 666  
 667      return 'r'.$this->getCallsign().$short_identifier;
 668    }
 669  
 670    public function isImporting() {
 671      return (bool)$this->getDetail('importing', false);
 672    }
 673  
 674  
 675  /* -(  Autoclose  )---------------------------------------------------------- */
 676  
 677  
 678    /**
 679     * Determine if autoclose is active for a branch.
 680     *
 681     * For more details about why, use @{method:shouldSkipAutocloseBranch}.
 682     *
 683     * @param string Branch name to check.
 684     * @return bool True if autoclose is active for the branch.
 685     * @task autoclose
 686     */
 687    public function shouldAutocloseBranch($branch) {
 688      return ($this->shouldSkipAutocloseBranch($branch) === null);
 689    }
 690  
 691    /**
 692     * Determine if autoclose is active for a commit.
 693     *
 694     * For more details about why, use @{method:shouldSkipAutocloseCommit}.
 695     *
 696     * @param PhabricatorRepositoryCommit Commit to check.
 697     * @return bool True if autoclose is active for the commit.
 698     * @task autoclose
 699     */
 700    public function shouldAutocloseCommit(PhabricatorRepositoryCommit $commit) {
 701      return ($this->shouldSkipAutocloseCommit($commit) === null);
 702    }
 703  
 704  
 705    /**
 706     * Determine why autoclose should be skipped for a branch.
 707     *
 708     * This method gives a detailed reason why autoclose will be skipped. To
 709     * perform a simple test, use @{method:shouldAutocloseBranch}.
 710     *
 711     * @param string Branch name to check.
 712     * @return const|null Constant identifying reason to skip this branch, or null
 713     *   if autoclose is active.
 714     * @task autoclose
 715     */
 716    public function shouldSkipAutocloseBranch($branch) {
 717      $all_reason = $this->shouldSkipAllAutoclose();
 718      if ($all_reason) {
 719        return $all_reason;
 720      }
 721  
 722      if (!$this->shouldTrackBranch($branch)) {
 723        return self::BECAUSE_BRANCH_UNTRACKED;
 724      }
 725  
 726      if (!$this->isBranchInFilter($branch, 'close-commits-filter')) {
 727        return self::BECAUSE_BRANCH_NOT_AUTOCLOSE;
 728      }
 729  
 730      return null;
 731    }
 732  
 733  
 734    /**
 735     * Determine why autoclose should be skipped for a commit.
 736     *
 737     * This method gives a detailed reason why autoclose will be skipped. To
 738     * perform a simple test, use @{method:shouldAutocloseCommit}.
 739     *
 740     * @param PhabricatorRepositoryCommit Commit to check.
 741     * @return const|null Constant identifying reason to skip this commit, or null
 742     *   if autoclose is active.
 743     * @task autoclose
 744     */
 745    public function shouldSkipAutocloseCommit(
 746      PhabricatorRepositoryCommit $commit) {
 747  
 748      $all_reason = $this->shouldSkipAllAutoclose();
 749      if ($all_reason) {
 750        return $all_reason;
 751      }
 752  
 753      switch ($this->getVersionControlSystem()) {
 754        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
 755        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 756          return null;
 757        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 758          break;
 759        default:
 760          throw new Exception('Unrecognized version control system.');
 761      }
 762  
 763      $closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE;
 764      if (!$commit->isPartiallyImported($closeable_flag)) {
 765        return self::BECAUSE_NOT_ON_AUTOCLOSE_BRANCH;
 766      }
 767  
 768      return null;
 769    }
 770  
 771  
 772    /**
 773     * Determine why all autoclose operations should be skipped for this
 774     * repository.
 775     *
 776     * @return const|null Constant identifying reason to skip all autoclose
 777     *   operations, or null if autoclose operations are not blocked at the
 778     *   repository level.
 779     * @task autoclose
 780     */
 781    private function shouldSkipAllAutoclose() {
 782      if ($this->isImporting()) {
 783        return self::BECAUSE_REPOSITORY_IMPORTING;
 784      }
 785  
 786      if ($this->getDetail('disable-autoclose', false)) {
 787        return self::BECAUSE_AUTOCLOSE_DISABLED;
 788      }
 789  
 790      return null;
 791    }
 792  
 793  
 794  /* -(  Repository URI Management  )------------------------------------------ */
 795  
 796  
 797    /**
 798     * Get the remote URI for this repository.
 799     *
 800     * @return string
 801     * @task uri
 802     */
 803    public function getRemoteURI() {
 804      return (string)$this->getRemoteURIObject();
 805    }
 806  
 807  
 808    /**
 809     * Get the remote URI for this repository, including credentials if they're
 810     * used by this repository.
 811     *
 812     * @return PhutilOpaqueEnvelope URI, possibly including credentials.
 813     * @task uri
 814     */
 815    public function getRemoteURIEnvelope() {
 816      $uri = $this->getRemoteURIObject();
 817  
 818      $remote_protocol = $this->getRemoteProtocol();
 819      if ($remote_protocol == 'http' || $remote_protocol == 'https') {
 820        // For SVN, we use `--username` and `--password` flags separately, so
 821        // don't add any credentials here.
 822        if (!$this->isSVN()) {
 823          $credential_phid = $this->getCredentialPHID();
 824          if ($credential_phid) {
 825            $key = PassphrasePasswordKey::loadFromPHID(
 826              $credential_phid,
 827              PhabricatorUser::getOmnipotentUser());
 828  
 829            $uri->setUser($key->getUsernameEnvelope()->openEnvelope());
 830            $uri->setPass($key->getPasswordEnvelope()->openEnvelope());
 831          }
 832        }
 833      }
 834  
 835      return new PhutilOpaqueEnvelope((string)$uri);
 836    }
 837  
 838  
 839    /**
 840     * Get the clone (or checkout) URI for this repository, without authentication
 841     * information.
 842     *
 843     * @return string Repository URI.
 844     * @task uri
 845     */
 846    public function getPublicCloneURI() {
 847      $uri = $this->getCloneURIObject();
 848  
 849      // Make sure we don't leak anything if this repo is using HTTP Basic Auth
 850      // with the credentials in the URI or something zany like that.
 851  
 852      // If repository is not accessed over SSH we remove both username and
 853      // password.
 854      if (!$this->isHosted()) {
 855        if (!$this->shouldUseSSH()) {
 856          $uri->setUser(null);
 857  
 858          // This might be a Git URI or a normal URI. If it's Git, there's no
 859          // password support.
 860          if ($uri instanceof PhutilURI) {
 861            $uri->setPass(null);
 862          }
 863        }
 864      }
 865  
 866      return (string)$uri;
 867    }
 868  
 869  
 870    /**
 871     * Get the protocol for the repository's remote.
 872     *
 873     * @return string Protocol, like "ssh" or "git".
 874     * @task uri
 875     */
 876    public function getRemoteProtocol() {
 877      $uri = $this->getRemoteURIObject();
 878  
 879      if ($uri instanceof PhutilGitURI) {
 880        return 'ssh';
 881      } else {
 882        return $uri->getProtocol();
 883      }
 884    }
 885  
 886  
 887    /**
 888     * Get a parsed object representation of the repository's remote URI. This
 889     * may be a normal URI (returned as a @{class@libphutil:PhutilURI}) or a git
 890     * URI (returned as a @{class@libphutil:PhutilGitURI}).
 891     *
 892     * @return wild A @{class@libphutil:PhutilURI} or
 893     *              @{class@libphutil:PhutilGitURI}.
 894     * @task uri
 895     */
 896    public function getRemoteURIObject() {
 897      $raw_uri = $this->getDetail('remote-uri');
 898      if (!$raw_uri) {
 899        return new PhutilURI('');
 900      }
 901  
 902      if (!strncmp($raw_uri, '/', 1)) {
 903        return new PhutilURI('file://'.$raw_uri);
 904      }
 905  
 906      $uri = new PhutilURI($raw_uri);
 907      if ($uri->getProtocol()) {
 908        return $uri;
 909      }
 910  
 911      $uri = new PhutilGitURI($raw_uri);
 912      if ($uri->getDomain()) {
 913        return $uri;
 914      }
 915  
 916      throw new Exception("Remote URI '{$raw_uri}' could not be parsed!");
 917    }
 918  
 919  
 920    /**
 921     * Get the "best" clone/checkout URI for this repository, on any protocol.
 922     */
 923    public function getCloneURIObject() {
 924      if (!$this->isHosted()) {
 925        if ($this->isSVN()) {
 926          // Make sure we pick up the "Import Only" path for Subversion, so
 927          // the user clones the repository starting at the correct path, not
 928          // from the root.
 929          $base_uri = $this->getSubversionBaseURI();
 930          $base_uri = new PhutilURI($base_uri);
 931          $path = $base_uri->getPath();
 932          if (!$path) {
 933            $path = '/';
 934          }
 935  
 936          // If the trailing "@" is not required to escape the URI, strip it for
 937          // readability.
 938          if (!preg_match('/@.*@/', $path)) {
 939            $path = rtrim($path, '@');
 940          }
 941  
 942          $base_uri->setPath($path);
 943          return $base_uri;
 944        } else {
 945          return $this->getRemoteURIObject();
 946        }
 947      }
 948  
 949      // Choose the best URI: pick a read/write URI over a URI which is not
 950      // read/write, and SSH over HTTP.
 951  
 952      $serve_ssh = $this->getServeOverSSH();
 953      $serve_http = $this->getServeOverHTTP();
 954  
 955      if ($serve_ssh === self::SERVE_READWRITE) {
 956        return $this->getSSHCloneURIObject();
 957      } else if ($serve_http === self::SERVE_READWRITE) {
 958        return $this->getHTTPCloneURIObject();
 959      } else if ($serve_ssh !== self::SERVE_OFF) {
 960        return $this->getSSHCloneURIObject();
 961      } else if ($serve_http !== self::SERVE_OFF) {
 962        return $this->getHTTPCloneURIObject();
 963      } else {
 964        return null;
 965      }
 966    }
 967  
 968  
 969    /**
 970     * Get the repository's SSH clone/checkout URI, if one exists.
 971     */
 972    public function getSSHCloneURIObject() {
 973      if (!$this->isHosted()) {
 974        if ($this->shouldUseSSH()) {
 975          return $this->getRemoteURIObject();
 976        } else {
 977          return null;
 978        }
 979      }
 980  
 981      $serve_ssh = $this->getServeOverSSH();
 982      if ($serve_ssh === self::SERVE_OFF) {
 983        return null;
 984      }
 985  
 986      $uri = new PhutilURI(PhabricatorEnv::getProductionURI($this->getURI()));
 987  
 988      if ($this->isSVN()) {
 989        $uri->setProtocol('svn+ssh');
 990      } else {
 991        $uri->setProtocol('ssh');
 992      }
 993  
 994      if ($this->isGit()) {
 995        $uri->setPath($uri->getPath().$this->getCloneName().'.git');
 996      } else if ($this->isHg()) {
 997        $uri->setPath($uri->getPath().$this->getCloneName().'/');
 998      }
 999  
1000      $ssh_user = PhabricatorEnv::getEnvConfig('diffusion.ssh-user');
1001      if ($ssh_user) {
1002        $uri->setUser($ssh_user);
1003      }
1004  
1005      $uri->setPort(PhabricatorEnv::getEnvConfig('diffusion.ssh-port'));
1006  
1007      return $uri;
1008    }
1009  
1010  
1011    /**
1012     * Get the repository's HTTP clone/checkout URI, if one exists.
1013     */
1014    public function getHTTPCloneURIObject() {
1015      if (!$this->isHosted()) {
1016        if ($this->shouldUseHTTP()) {
1017          return $this->getRemoteURIObject();
1018        } else {
1019          return null;
1020        }
1021      }
1022  
1023      $serve_http = $this->getServeOverHTTP();
1024      if ($serve_http === self::SERVE_OFF) {
1025        return null;
1026      }
1027  
1028      $uri = PhabricatorEnv::getProductionURI($this->getURI());
1029      $uri = new PhutilURI($uri);
1030  
1031      if ($this->isGit()) {
1032        $uri->setPath($uri->getPath().$this->getCloneName().'.git');
1033      } else if ($this->isHg()) {
1034        $uri->setPath($uri->getPath().$this->getCloneName().'/');
1035      }
1036  
1037      return $uri;
1038    }
1039  
1040  
1041    /**
1042     * Determine if we should connect to the remote using SSH flags and
1043     * credentials.
1044     *
1045     * @return bool True to use the SSH protocol.
1046     * @task uri
1047     */
1048    private function shouldUseSSH() {
1049      if ($this->isHosted()) {
1050        return false;
1051      }
1052  
1053      $protocol = $this->getRemoteProtocol();
1054      if ($this->isSSHProtocol($protocol)) {
1055        return true;
1056      }
1057  
1058      return false;
1059    }
1060  
1061  
1062    /**
1063     * Determine if we should connect to the remote using HTTP flags and
1064     * credentials.
1065     *
1066     * @return bool True to use the HTTP protocol.
1067     * @task uri
1068     */
1069    private function shouldUseHTTP() {
1070      if ($this->isHosted()) {
1071        return false;
1072      }
1073  
1074      $protocol = $this->getRemoteProtocol();
1075      return ($protocol == 'http' || $protocol == 'https');
1076    }
1077  
1078  
1079    /**
1080     * Determine if we should connect to the remote using SVN flags and
1081     * credentials.
1082     *
1083     * @return bool True to use the SVN protocol.
1084     * @task uri
1085     */
1086    private function shouldUseSVNProtocol() {
1087      if ($this->isHosted()) {
1088        return false;
1089      }
1090  
1091      $protocol = $this->getRemoteProtocol();
1092      return ($protocol == 'svn');
1093    }
1094  
1095  
1096    /**
1097     * Determine if a protocol is SSH or SSH-like.
1098     *
1099     * @param string A protocol string, like "http" or "ssh".
1100     * @return bool True if the protocol is SSH-like.
1101     * @task uri
1102     */
1103    private function isSSHProtocol($protocol) {
1104      return ($protocol == 'ssh' || $protocol == 'svn+ssh');
1105    }
1106  
1107    public function delete() {
1108      $this->openTransaction();
1109  
1110        $paths = id(new PhabricatorOwnersPath())
1111          ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
1112        foreach ($paths as $path) {
1113          $path->delete();
1114        }
1115  
1116        $projects = id(new PhabricatorRepositoryArcanistProject())
1117          ->loadAllWhere('repositoryID = %d', $this->getID());
1118        foreach ($projects as $project) {
1119          // note each project deletes its PhabricatorRepositorySymbols
1120          $project->delete();
1121        }
1122  
1123        $commits = id(new PhabricatorRepositoryCommit())
1124          ->loadAllWhere('repositoryID = %d', $this->getID());
1125        foreach ($commits as $commit) {
1126          // note PhabricatorRepositoryAuditRequests and
1127          // PhabricatorRepositoryCommitData are deleted here too.
1128          $commit->delete();
1129        }
1130  
1131        $mirrors = id(new PhabricatorRepositoryMirror())
1132          ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
1133        foreach ($mirrors as $mirror) {
1134          $mirror->delete();
1135        }
1136  
1137        $ref_cursors = id(new PhabricatorRepositoryRefCursor())
1138          ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
1139        foreach ($ref_cursors as $cursor) {
1140          $cursor->delete();
1141        }
1142  
1143        $conn_w = $this->establishConnection('w');
1144  
1145        queryfx(
1146          $conn_w,
1147          'DELETE FROM %T WHERE repositoryID = %d',
1148          self::TABLE_FILESYSTEM,
1149          $this->getID());
1150  
1151        queryfx(
1152          $conn_w,
1153          'DELETE FROM %T WHERE repositoryID = %d',
1154          self::TABLE_PATHCHANGE,
1155          $this->getID());
1156  
1157        queryfx(
1158          $conn_w,
1159          'DELETE FROM %T WHERE repositoryID = %d',
1160          self::TABLE_SUMMARY,
1161          $this->getID());
1162  
1163        $result = parent::delete();
1164  
1165      $this->saveTransaction();
1166      return $result;
1167    }
1168  
1169    public function isGit() {
1170      $vcs = $this->getVersionControlSystem();
1171      return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
1172    }
1173  
1174    public function isSVN() {
1175      $vcs = $this->getVersionControlSystem();
1176      return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
1177    }
1178  
1179    public function isHg() {
1180      $vcs = $this->getVersionControlSystem();
1181      return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
1182    }
1183  
1184    public function isHosted() {
1185      return (bool)$this->getDetail('hosting-enabled', false);
1186    }
1187  
1188    public function setHosted($enabled) {
1189      return $this->setDetail('hosting-enabled', $enabled);
1190    }
1191  
1192    public function getServeOverHTTP() {
1193      if ($this->isSVN()) {
1194        return self::SERVE_OFF;
1195      }
1196      $serve = $this->getDetail('serve-over-http', self::SERVE_OFF);
1197      return $this->normalizeServeConfigSetting($serve);
1198    }
1199  
1200    public function setServeOverHTTP($mode) {
1201      return $this->setDetail('serve-over-http', $mode);
1202    }
1203  
1204    public function getServeOverSSH() {
1205      $serve = $this->getDetail('serve-over-ssh', self::SERVE_OFF);
1206      return $this->normalizeServeConfigSetting($serve);
1207    }
1208  
1209    public function setServeOverSSH($mode) {
1210      return $this->setDetail('serve-over-ssh', $mode);
1211    }
1212  
1213    public static function getProtocolAvailabilityName($constant) {
1214      switch ($constant) {
1215        case self::SERVE_OFF:
1216          return pht('Off');
1217        case self::SERVE_READONLY:
1218          return pht('Read Only');
1219        case self::SERVE_READWRITE:
1220          return pht('Read/Write');
1221        default:
1222          return pht('Unknown');
1223      }
1224    }
1225  
1226    private function normalizeServeConfigSetting($value) {
1227      switch ($value) {
1228        case self::SERVE_OFF:
1229        case self::SERVE_READONLY:
1230          return $value;
1231        case self::SERVE_READWRITE:
1232          if ($this->isHosted()) {
1233            return self::SERVE_READWRITE;
1234          } else {
1235            return self::SERVE_READONLY;
1236          }
1237        default:
1238          return self::SERVE_OFF;
1239      }
1240    }
1241  
1242  
1243    /**
1244     * Raise more useful errors when there are basic filesystem problems.
1245     */
1246    private function assertLocalExists() {
1247      if (!$this->usesLocalWorkingCopy()) {
1248        return;
1249      }
1250  
1251      $local = $this->getLocalPath();
1252      Filesystem::assertExists($local);
1253      Filesystem::assertIsDirectory($local);
1254      Filesystem::assertReadable($local);
1255    }
1256  
1257    /**
1258     * Determine if the working copy is bare or not. In Git, this corresponds
1259     * to `--bare`. In Mercurial, `--noupdate`.
1260     */
1261    public function isWorkingCopyBare() {
1262      switch ($this->getVersionControlSystem()) {
1263        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1264        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
1265          return false;
1266        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
1267          $local = $this->getLocalPath();
1268          if (Filesystem::pathExists($local.'/.git')) {
1269            return false;
1270          } else {
1271            return true;
1272          }
1273      }
1274    }
1275  
1276    public function usesLocalWorkingCopy() {
1277      switch ($this->getVersionControlSystem()) {
1278        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1279          return $this->isHosted();
1280        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
1281        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
1282          return true;
1283      }
1284    }
1285  
1286    public function getHookDirectories() {
1287      $directories = array();
1288      if (!$this->isHosted()) {
1289        return $directories;
1290      }
1291  
1292      $root = $this->getLocalPath();
1293  
1294      switch ($this->getVersionControlSystem()) {
1295        case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
1296          if ($this->isWorkingCopyBare()) {
1297            $directories[] = $root.'/hooks/pre-receive-phabricator.d/';
1298          } else {
1299            $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/';
1300          }
1301          break;
1302        case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1303          $directories[] = $root.'/hooks/pre-commit-phabricator.d/';
1304          break;
1305        case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
1306          // NOTE: We don't support custom Mercurial hooks for now because they're
1307          // messy and we can't easily just drop a `hooks.d/` directory next to
1308          // the hooks.
1309          break;
1310      }
1311  
1312      return $directories;
1313    }
1314  
1315    public function canDestroyWorkingCopy() {
1316      if ($this->isHosted()) {
1317        // Never destroy hosted working copies.
1318        return false;
1319      }
1320  
1321      $default_path = PhabricatorEnv::getEnvConfig(
1322        'repository.default-local-path');
1323      return Filesystem::isDescendant($this->getLocalPath(), $default_path);
1324    }
1325  
1326    public function canUsePathTree() {
1327      return !$this->isSVN();
1328    }
1329  
1330    public function canMirror() {
1331      if ($this->isGit() || $this->isHg()) {
1332        return true;
1333      }
1334  
1335      return false;
1336    }
1337  
1338    public function canAllowDangerousChanges() {
1339      if (!$this->isHosted()) {
1340        return false;
1341      }
1342  
1343      if ($this->isGit() || $this->isHg()) {
1344        return true;
1345      }
1346  
1347      return false;
1348    }
1349  
1350    public function shouldAllowDangerousChanges() {
1351      return (bool)$this->getDetail('allow-dangerous-changes');
1352    }
1353  
1354    public function writeStatusMessage(
1355      $status_type,
1356      $status_code,
1357      array $parameters = array()) {
1358  
1359      $table = new PhabricatorRepositoryStatusMessage();
1360      $conn_w = $table->establishConnection('w');
1361      $table_name = $table->getTableName();
1362  
1363      if ($status_code === null) {
1364        queryfx(
1365          $conn_w,
1366          'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s',
1367          $table_name,
1368          $this->getID(),
1369          $status_type);
1370      } else {
1371        queryfx(
1372          $conn_w,
1373          'INSERT INTO %T
1374            (repositoryID, statusType, statusCode, parameters, epoch)
1375            VALUES (%d, %s, %s, %s, %d)
1376            ON DUPLICATE KEY UPDATE
1377              statusCode = VALUES(statusCode),
1378              parameters = VALUES(parameters),
1379              epoch = VALUES(epoch)',
1380          $table_name,
1381          $this->getID(),
1382          $status_type,
1383          $status_code,
1384          json_encode($parameters),
1385          time());
1386      }
1387  
1388      return $this;
1389    }
1390  
1391    public static function getRemoteURIProtocol($raw_uri) {
1392      $uri = new PhutilURI($raw_uri);
1393      if ($uri->getProtocol()) {
1394        return strtolower($uri->getProtocol());
1395      }
1396  
1397      $git_uri = new PhutilGitURI($raw_uri);
1398      if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) {
1399        return 'ssh';
1400      }
1401  
1402      return null;
1403    }
1404  
1405    public static function assertValidRemoteURI($uri) {
1406      if (trim($uri) != $uri) {
1407        throw new Exception(
1408          pht(
1409            'The remote URI has leading or trailing whitespace.'));
1410      }
1411  
1412      $protocol = self::getRemoteURIProtocol($uri);
1413  
1414      // Catch confusion between Git/SCP-style URIs and normal URIs. See T3619
1415      // for discussion. This is usually a user adding "ssh://" to an implicit
1416      // SSH Git URI.
1417      if ($protocol == 'ssh') {
1418        if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) {
1419          throw new Exception(
1420            pht(
1421              "The remote URI is not formatted correctly. Remote URIs ".
1422              "with an explicit protocol should be in the form ".
1423              "'proto://domain/path', not 'proto://domain:/path'. ".
1424              "The ':/path' syntax is only valid in SCP-style URIs."));
1425        }
1426      }
1427  
1428      switch ($protocol) {
1429        case 'ssh':
1430        case 'http':
1431        case 'https':
1432        case 'git':
1433        case 'svn':
1434        case 'svn+ssh':
1435          break;
1436        default:
1437          // NOTE: We're explicitly rejecting 'file://' because it can be
1438          // used to clone from the working copy of another repository on disk
1439          // that you don't normally have permission to access.
1440  
1441          throw new Exception(
1442            pht(
1443              "The URI protocol is unrecognized. It should begin ".
1444              "'ssh://', 'http://', 'https://', 'git://', 'svn://', ".
1445              "'svn+ssh://', or be in the form '[email protected]:path'."));
1446      }
1447  
1448      return true;
1449    }
1450  
1451  
1452    /**
1453     * Load the pull frequency for this repository, based on the time since the
1454     * last activity.
1455     *
1456     * We pull rarely used repositories less frequently. This finds the most
1457     * recent commit which is older than the current time (which prevents us from
1458     * spinning on repositories with a silly commit post-dated to some time in
1459     * 2037). We adjust the pull frequency based on when the most recent commit
1460     * occurred.
1461     *
1462     * @param   int   The minimum update interval to use, in seconds.
1463     * @return  int   Repository update interval, in seconds.
1464     */
1465    public function loadUpdateInterval($minimum = 15) {
1466      // If a repository is still importing, always pull it as frequently as
1467      // possible. This prevents us from hanging for a long time at 99.9% when
1468      // importing an inactive repository.
1469      if ($this->isImporting()) {
1470        return $minimum;
1471      }
1472  
1473      $window_start = (PhabricatorTime::getNow() + $minimum);
1474  
1475      $table = id(new PhabricatorRepositoryCommit());
1476      $last_commit = queryfx_one(
1477        $table->establishConnection('r'),
1478        'SELECT epoch FROM %T
1479          WHERE repositoryID = %d AND epoch <= %d
1480          ORDER BY epoch DESC LIMIT 1',
1481        $table->getTableName(),
1482        $this->getID(),
1483        $window_start);
1484      if ($last_commit) {
1485        $time_since_commit = ($window_start - $last_commit['epoch']);
1486  
1487        $last_few_days = phutil_units('3 days in seconds');
1488  
1489        if ($time_since_commit <= $last_few_days) {
1490          // For repositories with activity in the recent past, we wait one
1491          // extra second for every 10 minutes since the last commit. This
1492          // shorter backoff is intended to handle weekends and other short
1493          // breaks from development.
1494          $smart_wait = ($time_since_commit / 600);
1495        } else {
1496          // For repositories without recent activity, we wait one extra second
1497          // for every 4 minutes since the last commit. This longer backoff
1498          // handles rarely used repositories, up to the maximum.
1499          $smart_wait = ($time_since_commit / 240);
1500        }
1501  
1502        // We'll never wait more than 6 hours to pull a repository.
1503        $longest_wait = phutil_units('6 hours in seconds');
1504        $smart_wait = min($smart_wait, $longest_wait);
1505  
1506        $smart_wait = max($minimum, $smart_wait);
1507      } else {
1508        $smart_wait = $minimum;
1509      }
1510  
1511      return $smart_wait;
1512    }
1513  
1514  
1515  /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
1516  
1517  
1518    public function getCapabilities() {
1519      return array(
1520        PhabricatorPolicyCapability::CAN_VIEW,
1521        PhabricatorPolicyCapability::CAN_EDIT,
1522        DiffusionPushCapability::CAPABILITY,
1523      );
1524    }
1525  
1526    public function getPolicy($capability) {
1527      switch ($capability) {
1528        case PhabricatorPolicyCapability::CAN_VIEW:
1529          return $this->getViewPolicy();
1530        case PhabricatorPolicyCapability::CAN_EDIT:
1531          return $this->getEditPolicy();
1532        case DiffusionPushCapability::CAPABILITY:
1533          return $this->getPushPolicy();
1534      }
1535    }
1536  
1537    public function hasAutomaticCapability($capability, PhabricatorUser $user) {
1538      return false;
1539    }
1540  
1541    public function describeAutomaticCapability($capability) {
1542      return null;
1543    }
1544  
1545  
1546  
1547  /* -(  PhabricatorMarkupInterface  )----------------------------------------- */
1548  
1549  
1550    public function getMarkupFieldKey($field) {
1551      $hash = PhabricatorHash::digestForIndex($this->getMarkupText($field));
1552      return "repo:{$hash}";
1553    }
1554  
1555    public function newMarkupEngine($field) {
1556      return PhabricatorMarkupEngine::newMarkupEngine(array());
1557    }
1558  
1559    public function getMarkupText($field) {
1560      return $this->getDetail('description');
1561    }
1562  
1563    public function didMarkupText(
1564      $field,
1565      $output,
1566      PhutilMarkupEngine $engine) {
1567      require_celerity_resource('phabricator-remarkup-css');
1568      return phutil_tag(
1569        'div',
1570        array(
1571          'class' => 'phabricator-remarkup',
1572        ),
1573        $output);
1574    }
1575  
1576    public function shouldUseMarkupCache($field) {
1577      return true;
1578    }
1579  
1580  
1581  /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
1582  
1583    public function destroyObjectPermanently(
1584      PhabricatorDestructionEngine $engine) {
1585  
1586      $this->openTransaction();
1587      $this->delete();
1588      $this->saveTransaction();
1589    }
1590  
1591  }


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