diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php index 4c22b30000..87e328949b 100644 --- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php +++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php @@ -1,798 +1,805 @@ <?php /** * Run pull commands on local working copies to keep them up to date. This * daemon handles all repository types. * * By default, the daemon pulls **every** repository. If you want it to be * responsible for only some repositories, you can launch it with a list of * PHIDs or callsigns: * * ./phd launch repositorypulllocal -- X Q Z * * You can also launch a daemon which is responsible for all //but// one or * more repositories: * * ./phd launch repositorypulllocal -- --not A --not B * * If you have a very large number of repositories and some aren't being pulled * as frequently as you'd like, you can either change the pull frequency of * the less-important repositories to a larger number (so the daemon will skip * them more often) or launch one daemon for all the less-important repositories * and one for the more important repositories (or one for each more important * repository). * * @task pull Pulling Repositories * @task git Git Implementation * @task hg Mercurial Implementation */ final class PhabricatorRepositoryPullLocalDaemon extends PhabricatorDaemon { private $commitCache = array(); private $repair; private $discoveryEngines = array(); public function setRepair($repair) { $this->repair = $repair; return $this; } /* -( Pulling Repositories )----------------------------------------------- */ /** * @task pull */ public function run() { $argv = $this->getArgv(); array_unshift($argv, __CLASS__); $args = new PhutilArgumentParser($argv); $args->parse( array( array( 'name' => 'no-discovery', 'help' => 'Pull only, without discovering commits.', ), array( 'name' => 'not', 'param' => 'repository', 'repeat' => true, 'help' => 'Do not pull __repository__.', ), array( 'name' => 'repositories', 'wildcard' => true, 'help' => 'Pull specific __repositories__ instead of all.', ), )); $no_discovery = $args->getArg('no-discovery'); $repo_names = $args->getArg('repositories'); $exclude_names = $args->getArg('not'); // Each repository has an individual pull frequency; after we pull it, // wait that long to pull it again. When we start up, try to pull everything // serially. $retry_after = array(); $min_sleep = 15; while (true) { $repositories = $this->loadRepositories($repo_names); if ($exclude_names) { $exclude = $this->loadRepositories($exclude_names); $repositories = array_diff_key($repositories, $exclude); } // Shuffle the repositories, then re-key the array since shuffle() // discards keys. This is mostly for startup, we'll use soft priorities // later. shuffle($repositories); $repositories = mpull($repositories, null, 'getID'); // If any repositories have the NEEDS_UPDATE flag set, pull them // as soon as possible. $type_need_update = PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE; $need_update_messages = id(new PhabricatorRepositoryStatusMessage()) ->loadAllWhere('statusType = %s', $type_need_update); foreach ($need_update_messages as $message) { $retry_after[$message->getRepositoryID()] = time(); } // If any repositories were deleted, remove them from the retry timer map // so we don't end up with a retry timer that never gets updated and // causes us to sleep for the minimum amount of time. $retry_after = array_select_keys( $retry_after, array_keys($repositories)); // Assign soft priorities to repositories based on how frequently they // should pull again. asort($retry_after); $repositories = array_select_keys( $repositories, array_keys($retry_after)) + $repositories; foreach ($repositories as $id => $repository) { $after = idx($retry_after, $id, 0); if ($after > time()) { continue; } $tracked = $repository->isTracked(); if (!$tracked) { continue; } $callsign = $repository->getCallsign(); try { $this->log("Updating repository '{$callsign}'."); id(new PhabricatorRepositoryPullEngine()) ->setRepository($repository) ->pullRepository(); if (!$no_discovery) { // TODO: It would be nice to discover only if we pulled something, // but this isn't totally trivial. It's slightly more complicated // with hosted repositories, too. $lock_name = get_class($this).':'.$callsign; $lock = PhabricatorGlobalLock::newLock($lock_name); $lock->lock(); $repository->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, null); try { $this->discoverRepository($repository); $repository->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_FETCH, PhabricatorRepositoryStatusMessage::CODE_OKAY); } catch (Exception $ex) { $repository->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_FETCH, PhabricatorRepositoryStatusMessage::CODE_ERROR, array( 'message' => pht( 'Error updating working copy: %s', $ex->getMessage()), )); $lock->unlock(); throw $ex; } $lock->unlock(); } $sleep_for = $repository->getDetail('pull-frequency', $min_sleep); $retry_after[$id] = time() + $sleep_for; } catch (PhutilLockException $ex) { $retry_after[$id] = time() + $min_sleep; $this->log("Failed to acquire lock."); } catch (Exception $ex) { $retry_after[$id] = time() + $min_sleep; $proxy = new PhutilProxyException( "Error while fetching changes to the '{$callsign}' repository.", $ex); phlog($proxy); } $this->stillWorking(); } if ($retry_after) { $sleep_until = max(min($retry_after), time() + $min_sleep); } else { $sleep_until = time() + $min_sleep; } $this->sleep($sleep_until - time()); } } /** * @task pull */ protected function loadRepositories(array $names) { $query = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()); if ($names) { $query->withCallsigns($names); } $repos = $query->execute(); if ($names) { $by_callsign = mpull($repos, null, 'getCallsign'); foreach ($names as $name) { if (empty($by_callsign[$name])) { throw new Exception( "No repository exists with callsign '{$name}'!"); } } } return $repos; } public function discoverRepository(PhabricatorRepository $repository) { $vcs = $repository->getVersionControlSystem(); $result = null; $refs = null; switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->executeGitDiscover($repository); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $refs = $this->getDiscoveryEngine($repository) ->discoverCommits(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->executeHgDiscover($repository); break; default: throw new Exception("Unknown VCS '{$vcs}'!"); } if ($refs !== null) { foreach ($refs as $ref) { $this->recordCommit( $repository, $ref->getIdentifier(), $ref->getEpoch(), $ref->getBranch()); } } $this->checkIfRepositoryIsFullyImported($repository); if ($refs !== null) { return (bool)count($refs); } else { return $result; } } private function getDiscoveryEngine(PhabricatorRepository $repository) { $id = $repository->getID(); if (empty($this->discoveryEngines[$id])) { $engine = id(new PhabricatorRepositoryDiscoveryEngine()) ->setRepository($repository) ->setVerbose($this->getVerbose()) ->setRepairMode($this->repair); $this->discoveryEngines[$id] = $engine; } return $this->discoveryEngines[$id]; } private function isKnownCommit( PhabricatorRepository $repository, $target) { if ($this->getCache($repository, $target)) { return true; } if ($this->repair) { // In repair mode, rediscover the entire repository, ignoring the // database state. We can hit the local cache above, but if we miss it // stop the script from going to the database cache. return false; } $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( 'repositoryID = %d AND commitIdentifier = %s', $repository->getID(), $target); if (!$commit) { return false; } $this->setCache($repository, $target); while (count($this->commitCache) > 2048) { array_shift($this->commitCache); } return true; } private function isKnownCommitOnAnyAutocloseBranch( PhabricatorRepository $repository, $target) { $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( 'repositoryID = %d AND commitIdentifier = %s', $repository->getID(), $target); if (!$commit) { $callsign = $repository->getCallsign(); $console = PhutilConsole::getConsole(); $console->writeErr( "WARNING: Repository '%s' is missing commits ('%s' is missing from ". "history). Run '%s' to repair the repository.\n", $callsign, $target, "bin/repository discover --repair {$callsign}"); return false; } $data = $commit->loadCommitData(); if (!$data) { return false; } if ($repository->shouldAutocloseCommit($commit, $data)) { return true; } return false; } private function recordCommit( PhabricatorRepository $repository, $commit_identifier, $epoch, $branch = null) { $commit = new PhabricatorRepositoryCommit(); $commit->setRepositoryID($repository->getID()); $commit->setCommitIdentifier($commit_identifier); $commit->setEpoch($epoch); $data = new PhabricatorRepositoryCommitData(); if ($branch) { $data->setCommitDetail('seenOnBranches', array($branch)); } try { $commit->openTransaction(); $commit->save(); $data->setCommitID($commit->getID()); $data->save(); $commit->saveTransaction(); $this->insertTask($repository, $commit); queryfx( $repository->establishConnection('w'), 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch) VALUES (%d, 1, %d, %d) ON DUPLICATE KEY UPDATE size = size + 1, lastCommitID = IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID), epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)', PhabricatorRepository::TABLE_SUMMARY, $repository->getID(), $commit->getID(), $epoch); if ($this->repair) { // Normally, the query should throw a duplicate key exception. If we // reach this in repair mode, we've actually performed a repair. $this->log("Repaired commit '{$commit_identifier}'."); } $this->setCache($repository, $commit_identifier); PhutilEventEngine::dispatchEvent( new PhabricatorEvent( PhabricatorEventType::TYPE_DIFFUSION_DIDDISCOVERCOMMIT, array( 'repository' => $repository, 'commit' => $commit, ))); } catch (AphrontQueryDuplicateKeyException $ex) { $commit->killTransaction(); // Ignore. This can happen because we discover the same new commit // more than once when looking at history, or because of races or // data inconsistency or cosmic radiation; in any case, we're still // in a good state if we ignore the failure. $this->setCache($repository, $commit_identifier); } } private function updateCommit( PhabricatorRepository $repository, $commit_identifier, $branch) { $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( 'repositoryID = %d AND commitIdentifier = %s', $repository->getID(), $commit_identifier); if (!$commit) { // This can happen if the phabricator DB doesn't have the commit info, // or the commit is so big that phabricator couldn't parse it. In this // case we just ignore it. return; } $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); if (!$data) { $data = new PhabricatorRepositoryCommitData(); $data->setCommitID($commit->getID()); } $branches = $data->getCommitDetail('seenOnBranches', array()); $branches[] = $branch; $data->setCommitDetail('seenOnBranches', $branches); $data->save(); $this->insertTask( $repository, $commit, array( 'only' => true )); } private function insertTask( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, $data = array()) { $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $class = 'PhabricatorRepositoryGitCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $class = 'PhabricatorRepositorySvnCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; break; default: throw new Exception("Unknown repository type '{$vcs}'!"); } $data['commitID'] = $commit->getID(); PhabricatorWorker::scheduleTask($class, $data); } private function setCache( PhabricatorRepository $repository, $commit_identifier) { $key = $this->getCacheKey($repository, $commit_identifier); $this->commitCache[$key] = true; } private function getCache( PhabricatorRepository $repository, $commit_identifier) { $key = $this->getCacheKey($repository, $commit_identifier); return idx($this->commitCache, $key, false); } private function getCacheKey( PhabricatorRepository $repository, $commit_identifier) { return $repository->getID().':'.$commit_identifier; } private function checkIfRepositoryIsFullyImported( PhabricatorRepository $repository) { // Check if the repository has the "Importing" flag set. We want to clear // the flag if we can. $importing = $repository->getDetail('importing'); if (!$importing) { // This repository isn't marked as "Importing", so we're done. return; } // Look for any commit which hasn't imported. $unparsed_commit = queryfx_one( $repository->establishConnection('r'), 'SELECT * FROM %T WHERE repositoryID = %d AND importStatus != %d LIMIT 1', id(new PhabricatorRepositoryCommit())->getTableName(), $repository->getID(), PhabricatorRepositoryCommit::IMPORTED_ALL); if ($unparsed_commit) { // We found a commit which still needs to import, so we can't clear the // flag. return; } // Clear the "importing" flag. $repository->openTransaction(); $repository->beginReadLocking(); $repository = $repository->reload(); $repository->setDetail('importing', false); $repository->save(); $repository->endReadLocking(); $repository->saveTransaction(); } /* -( Git Implementation )------------------------------------------------- */ /** * @task git */ private function executeGitDiscover( PhabricatorRepository $repository) { - list($remotes) = $repository->execxLocalCommand( - 'remote show -n origin'); + if (!$repository->isHosted()) { + list($remotes) = $repository->execxLocalCommand( + 'remote show -n origin'); + + $matches = null; + if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) { + throw new Exception( + "Expected 'Fetch URL' in 'git remote show -n origin'."); + } - $matches = null; - if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) { - throw new Exception( - "Expected 'Fetch URL' in 'git remote show -n origin'."); + self::executeGitVerifySameOrigin( + $matches[1], + $repository->getRemoteURI(), + $repository->getLocalPath()); } - self::executeGitVerifySameOrigin( - $matches[1], - $repository->getRemoteURI(), - $repository->getLocalPath()); - $refs = id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withIsOriginBranch(true) ->execute(); $branches = mpull($refs, 'getCommitIdentifier', 'getShortName'); if (!$branches) { // This repository has no branches at all, so we don't need to do // anything. Generally, this means the repository is empty. return; } $callsign = $repository->getCallsign(); $tracked_something = false; $this->log("Discovering commits in repository '{$callsign}'..."); foreach ($branches as $name => $commit) { $this->log("Examining branch '{$name}', at {$commit}."); if (!$repository->shouldTrackBranch($name)) { $this->log("Skipping, branch is untracked."); continue; } $tracked_something = true; if ($this->isKnownCommit($repository, $commit)) { $this->log("Skipping, HEAD is known."); continue; } $this->log("Looking for new commits."); $this->executeGitDiscoverCommit($repository, $commit, $name, false); } if (!$tracked_something) { $repo_name = $repository->getName(); $repo_callsign = $repository->getCallsign(); throw new Exception( "Repository r{$repo_callsign} '{$repo_name}' has no tracked branches! ". "Verify that your branch filtering settings are correct."); } $this->log("Discovering commits on autoclose branches..."); foreach ($branches as $name => $commit) { $this->log("Examining branch '{$name}', at {$commit}'."); if (!$repository->shouldTrackBranch($name)) { $this->log("Skipping, branch is untracked."); continue; } if (!$repository->shouldAutocloseBranch($name)) { $this->log("Skipping, branch is not autoclose."); continue; } if ($this->isKnownCommitOnAnyAutocloseBranch($repository, $commit)) { $this->log("Skipping, commit is known on an autoclose branch."); continue; } $this->log("Looking for new autoclose commits."); $this->executeGitDiscoverCommit($repository, $commit, $name, true); } } /** * @task git */ private function executeGitDiscoverCommit( PhabricatorRepository $repository, $commit, $branch, $autoclose) { $discover = array($commit); $insert = array($commit); $seen_parent = array(); $stream = new PhabricatorGitGraphStream($repository, $commit); while (true) { $target = array_pop($discover); $parents = $stream->getParents($target); foreach ($parents as $parent) { if (isset($seen_parent[$parent])) { // We end up in a loop here somehow when we parse Arcanist if we // don't do this. TODO: Figure out why and draw a pretty diagram // since it's not evident how parsing a DAG with this causes the // loop to stop terminating. continue; } $seen_parent[$parent] = true; if ($autoclose) { $known = $this->isKnownCommitOnAnyAutocloseBranch( $repository, $parent); } else { $known = $this->isKnownCommit($repository, $parent); } if (!$known) { $this->log("Discovered commit '{$parent}'."); $discover[] = $parent; $insert[] = $parent; } } if (empty($discover)) { break; } } $n = count($insert); if ($autoclose) { $this->log("Found {$n} new autoclose commits on branch '{$branch}'."); } else { $this->log("Found {$n} new commits on branch '{$branch}'."); } while (true) { $target = array_pop($insert); $epoch = $stream->getCommitDate($target); $epoch = trim($epoch); if ($autoclose) { $this->updateCommit($repository, $target, $branch); } else { $this->recordCommit($repository, $target, $epoch, $branch); } if (empty($insert)) { break; } } } /** * @task git */ public static function executeGitVerifySameOrigin($remote, $expect, $where) { $remote_path = self::getPathFromGitURI($remote); $expect_path = self::getPathFromGitURI($expect); $remote_match = self::executeGitNormalizePath($remote_path); $expect_match = self::executeGitNormalizePath($expect_path); if ($remote_match != $expect_match) { throw new Exception( "Working copy at '{$where}' has a mismatched origin URL. It has ". "origin URL '{$remote}' (with remote path '{$remote_path}'), but the ". "configured URL '{$expect}' (with remote path '{$expect_path}') is ". "expected. Refusing to proceed because this may indicate that the ". "working copy is actually some other repository."); } } private static function getPathFromGitURI($raw_uri) { $uri = new PhutilURI($raw_uri); if ($uri->getProtocol()) { return $uri->getPath(); } $uri = new PhutilGitURI($raw_uri); if ($uri->getDomain()) { return $uri->getPath(); } return $raw_uri; } /** * @task git */ private static function executeGitNormalizePath($path) { // Strip away "/" and ".git", so similar paths correctly match. $path = trim($path, '/'); $path = preg_replace('/\.git$/', '', $path); return $path; } /* -( Mercurial Implementation )------------------------------------------- */ private function executeHgDiscover(PhabricatorRepository $repository) { // NOTE: "--debug" gives us 40-character hashes. list($stdout) = $repository->execxLocalCommand('--debug branches'); + if (!trim($stdout)) { + // No branches; likely a newly initialized repository. + return false; + } + $branches = ArcanistMercurialParser::parseMercurialBranches($stdout); $got_something = false; foreach ($branches as $name => $branch) { $commit = $branch['rev']; if ($this->isKnownCommit($repository, $commit)) { continue; } else { $this->executeHgDiscoverCommit($repository, $commit); $got_something = true; } } return $got_something; } private function executeHgDiscoverCommit( PhabricatorRepository $repository, $commit) { $discover = array($commit); $insert = array($commit); $seen_parent = array(); $stream = new PhabricatorMercurialGraphStream($repository); // For all the new commits at the branch heads, walk backward until we // find only commits we've aleady seen. while ($discover) { $target = array_pop($discover); $parents = $stream->getParents($target); foreach ($parents as $parent) { if (isset($seen_parent[$parent])) { continue; } $seen_parent[$parent] = true; if (!$this->isKnownCommit($repository, $parent)) { $discover[] = $parent; $insert[] = $parent; } } } foreach ($insert as $target) { $epoch = $stream->getCommitDate($target); $this->recordCommit($repository, $target, $epoch); } } } diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index 2fa02c6832..154ca4342f 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -1,330 +1,370 @@ <?php /** * Manages execution of `git pull` and `hg pull` commands for * @{class:PhabricatorRepository} objects. Used by * @{class:PhabricatorRepositoryPullLocalDaemon}. * + * This class also covers initial working copy setup through `git clone`, + * `git init`, `hg clone`, `hg init`, or `svnadmin create`. + * * @task pull Pulling Working Copies * @task git Pulling Git Working Copies * @task hg Pulling Mercurial Working Copies + * @task svn Pulling Subversion Working Copies * @task internal Internals */ final class PhabricatorRepositoryPullEngine extends PhabricatorRepositoryEngine { /* -( Pulling Working Copies )--------------------------------------------- */ public function pullRepository() { $repository = $this->getRepository(); $is_hg = false; $is_git = false; + $is_svn = true; $vcs = $repository->getVersionControlSystem(); $callsign = $repository->getCallsign(); - if ($repository->isHosted()) { - $this->skipPull( - pht( - 'Repository "%s" is hosted, so Phabricator does not pull updates '. - 'for it.', - $callsign)); - return; - } - switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - // We never pull a local copy of Subversion repositories. - $this->skipPull( - pht( - "Repository '%s' is a Subversion repository, which does not ". - "require a local working copy to be pulled.", - $callsign)); - return; + // We never pull a local copy of non-hosted Subversion repositories. + if (!$repository->isHosted()) { + $this->skipPull( + pht( + "Repository '%s' is a non-hosted Subversion repository, which ". + "does not require a local working copy to be pulled.", + $callsign)); + return; + } + $is_svn = true; + break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $is_git = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $is_hg = true; break; default: $this->abortPull(pht('Unknown VCS "%s"!', $vcs)); } $callsign = $repository->getCallsign(); $local_path = $repository->getLocalPath(); if ($local_path === null) { $this->abortPull( pht( "No local path is configured for repository '%s'.", $callsign)); } try { $dirname = dirname($local_path); if (!Filesystem::pathExists($dirname)) { Filesystem::createDirectory($dirname, 0755, $recursive = true); } if (!Filesystem::pathExists($local_path)) { $this->logPull( pht( "Creating a new working copy for repository '%s'.", $callsign)); if ($is_git) { $this->executeGitCreate(); - } else { + } else if ($is_hg) { $this->executeMercurialCreate(); + } else { + $this->executeSubversionCreate(); } } else { - $this->logPull( - pht( - "Updating the working copy for repository '%s'.", - $callsign)); - if ($is_git) { - $this->executeGitUpdate(); + if ($repository->isHosted()) { + $this->logPull( + pht( + "Repository '%s' is hosted, so Phabricator does not pull ". + "updates for it.", + $callsign)); } else { - $this->executeMercurialUpdate(); + $this->logPull( + pht( + "Updating the working copy for repository '%s'.", + $callsign)); + if ($is_git) { + $this->executeGitUpdate(); + } else { + $this->executeMercurialUpdate(); + } } } } catch (Exception $ex) { $this->abortPull( pht('Pull of "%s" failed: %s', $callsign, $ex->getMessage()), $ex); } $this->donePull(); return $this; } private function skipPull($message) { - $this->updateRepositoryInitStatus(null); $this->log('%s', $message); + $this->donePull(); } private function abortPull($message, Exception $ex = null) { $code_error = PhabricatorRepositoryStatusMessage::CODE_ERROR; $this->updateRepositoryInitStatus($code_error, $message); if ($ex) { throw $ex; } else { throw new Exception($message); } } private function logPull($message) { $code_working = PhabricatorRepositoryStatusMessage::CODE_WORKING; $this->updateRepositoryInitStatus($code_working, $message); $this->log('%s', $message); } private function donePull() { $code_okay = PhabricatorRepositoryStatusMessage::CODE_OKAY; $this->updateRepositoryInitStatus($code_okay); } private function updateRepositoryInitStatus($code, $message = null) { $this->getRepository()->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_INIT, $code, array( 'message' => $message )); } /* -( Pulling Git Working Copies )----------------------------------------- */ /** * @task git */ private function executeGitCreate() { $repository = $this->getRepository(); - $repository->execxRemoteCommand( - 'clone --bare %s %s', - $repository->getRemoteURI(), - rtrim($repository->getLocalPath(), '/')); + $path = rtrim($repository->getLocalPath(), '/'); + + if ($repository->isHosted()) { + $repository->execxRemoteCommand( + 'init --bare -- %s', + $path); + } else { + $repository->execxRemoteCommand( + 'clone --bare -- %s %s', + $repository->getRemoteURI(), + $path); + } } /** * @task git */ private function executeGitUpdate() { $repository = $this->getRepository(); list($err, $stdout) = $repository->execLocalCommand( 'rev-parse --show-toplevel'); $message = null; $path = $repository->getLocalPath(); if ($err) { // Try to raise a more tailored error message in the more common case // of the user creating an empty directory. (We could try to remove it, // but might not be able to, and it's much simpler to raise a good // message than try to navigate those waters.) if (is_dir($path)) { $files = Filesystem::listDirectory($path, $include_hidden = true); if (!$files) { $message = "Expected to find a git repository at '{$path}', but there ". "is an empty directory there. Remove the directory: the daemon ". "will run 'git clone' for you."; } else { $message = "Expected to find a git repository at '{$path}', but there is ". "a non-repository directory (with other stuff in it) there. Move ". "or remove this directory (or reconfigure the repository to use a ". "different directory), and then either clone a repository ". "yourself or let the daemon do it."; } } else if (is_file($path)) { $message = "Expected to find a git repository at '{$path}', but there is a ". "file there instead. Remove it and let the daemon clone a ". "repository for you."; } else { $message = "Expected to find a git repository at '{$path}', but did not."; } } else { $repo_path = rtrim($stdout, "\n"); if (empty($repo_path)) { // This can mean one of two things: we're in a bare repository, or // we're inside a git repository inside another git repository. Since // the first is dramatically more likely now that we perform bare // clones and I don't have a great way to test for the latter, assume // we're OK. } else if (!Filesystem::pathsAreEquivalent($repo_path, $path)) { $err = true; $message = "Expected to find repo at '{$path}', but the actual ". "git repository root for this directory is '{$repo_path}'. ". "Something is misconfigured. The repository's 'Local Path' should ". "be set to some place where the daemon can check out a working ". "copy, and should not be inside another git repository."; } } if ($err && $this->canDestroyWorkingCopy($path)) { phlog("Repository working copy at '{$path}' failed sanity check; ". "destroying and re-cloning. {$message}"); Filesystem::remove($path); $this->executeGitCreate(); } else if ($err) { throw new Exception($message); } $retry = false; do { // This is a local command, but needs credentials. if ($repository->isWorkingCopyBare()) { // For bare working copies, we need this magic incantation. $future = $repository->getRemoteCommandFuture( 'fetch origin %s --prune', '+refs/heads/*:refs/heads/*'); } else { $future = $repository->getRemoteCommandFuture( 'fetch --all --prune'); } $future->setCWD($path); list($err, $stdout, $stderr) = $future->resolve(); if ($err && !$retry && $this->canDestroyWorkingCopy($path)) { $retry = true; // Fix remote origin url if it doesn't match our configuration $origin_url = $repository->execLocalCommand( 'config --get remote.origin.url'); $remote_uri = $repository->getDetail('remote-uri'); if ($origin_url != $remote_uri) { $repository->execLocalCommand( 'remote set-url origin %s', $remote_uri); } } else if ($err) { throw new Exception( "git fetch failed with error #{$err}:\n". "stdout:{$stdout}\n\n". "stderr:{$stderr}\n"); } else { $retry = false; } } while ($retry); } /* -( Pulling Mercurial Working Copies )----------------------------------- */ /** * @task hg */ private function executeMercurialCreate() { $repository = $this->getRepository(); - $repository->execxRemoteCommand( - 'clone %s %s', - $repository->getRemoteURI(), - rtrim($repository->getLocalPath(), '/')); + $path = rtrim($repository->getLocalPath(), '/'); + + if ($repository->isHosted()) { + $repository->execxRemoteCommand( + 'init -- %s', + $path); + } else { + $repository->execxRemoteCommand( + 'clone -- %s %s', + $repository->getRemoteURI(), + $path); + } } /** * @task hg */ private function executeMercurialUpdate() { $repository = $this->getRepository(); $path = $repository->getLocalPath(); // This is a local command, but needs credentials. $future = $repository->getRemoteCommandFuture('pull -u'); $future->setCWD($path); try { $future->resolvex(); } catch (CommandException $ex) { $err = $ex->getError(); $stdout = $ex->getStdOut(); // NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the behavior // of "hg pull" to return 1 in case of a successful pull with no changes. // This behavior has been reverted, but users who updated between Feb 1, // 2012 and Mar 1, 2012 will have the erroring version. Do a dumb test // against stdout to check for this possibility. // See: https://github.com/facebook/phabricator/issues/101/ // NOTE: Mercurial has translated versions, which translate this error // string. In a translated version, the string will be something else, // like "aucun changement trouve". There didn't seem to be an easy way // to handle this (there are hard ways but this is not a common problem // and only creates log spam, not application failures). Assume English. // TODO: Remove this once we're far enough in the future that deployment // of 2.1 is exceedingly rare? if ($err == 1 && preg_match('/no changes found/', $stdout)) { return; } else { throw $ex; } } } +/* -( Pulling Subversion Working Copies )---------------------------------- */ + + + /** + * @task svn + */ + private function executeSubversionCreate() { + $repository = $this->getRepository(); + + $path = rtrim($repository->getLocalPath(), '/'); + execx('svnadmin create -- %s', $path); + } + + /* -( Internals )---------------------------------------------------------- */ private function canDestroyWorkingCopy($path) { $default_path = PhabricatorEnv::getEnvConfig( 'repository.default-local-path'); return Filesystem::isDescendant($path, $default_path); } } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 1db2a5cc41..2174bdbadf 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1,939 +1,953 @@ <?php /** * @task uri Repository URI Management */ final class PhabricatorRepository extends PhabricatorRepositoryDAO implements PhabricatorPolicyInterface, PhabricatorFlaggableInterface, PhabricatorMarkupInterface { /** * Shortest hash we'll recognize in raw "a829f32" form. */ const MINIMUM_UNQUALIFIED_HASH = 7; /** * Shortest hash we'll recognize in qualified "rXab7ef2f8" form. */ const MINIMUM_QUALIFIED_HASH = 5; const TABLE_PATH = 'repository_path'; const TABLE_PATHCHANGE = 'repository_pathchange'; const TABLE_FILESYSTEM = 'repository_filesystem'; const TABLE_SUMMARY = 'repository_summary'; const TABLE_BADCOMMIT = 'repository_badcommit'; const TABLE_LINTMESSAGE = 'repository_lintmessage'; const SERVE_OFF = 'off'; const SERVE_READONLY = 'readonly'; const SERVE_READWRITE = 'readwrite'; protected $name; protected $callsign; protected $uuid; protected $viewPolicy; protected $editPolicy; protected $pushPolicy; protected $versionControlSystem; protected $details = array(); private $sshKeyfile; private $commitCount = self::ATTACHABLE; private $mostRecentCommit = self::ATTACHABLE; public static function initializeNewRepository(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorApplicationDiffusion')) ->executeOne(); $view_policy = $app->getPolicy(DiffusionCapabilityDefaultView::CAPABILITY); $edit_policy = $app->getPolicy(DiffusionCapabilityDefaultEdit::CAPABILITY); $push_policy = $app->getPolicy(DiffusionCapabilityDefaultPush::CAPABILITY); return id(new PhabricatorRepository()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setPushPolicy($push_policy); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryPHIDTypeRepository::TYPECONST); } public function toDictionary() { return array( 'name' => $this->getName(), 'phid' => $this->getPHID(), 'callsign' => $this->getCallsign(), 'vcs' => $this->getVersionControlSystem(), 'uri' => PhabricatorEnv::getProductionURI($this->getURI()), 'remoteURI' => (string)$this->getPublicRemoteURI(), 'tracking' => $this->getDetail('tracking-enabled'), 'description' => $this->getDetail('description'), ); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function getHumanReadableDetail($key, $default = null) { $value = $this->getDetail($key, $default); switch ($key) { case 'branch-filter': case 'close-commits-filter': $value = array_keys($value); $value = implode(', ', $value); break; } return $value; } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function attachCommitCount($count) { $this->commitCount = $count; return $this; } public function getCommitCount() { return $this->assertAttached($this->commitCount); } public function attachMostRecentCommit( PhabricatorRepositoryCommit $commit = null) { $this->mostRecentCommit = $commit; return $this; } public function getMostRecentCommit() { return $this->assertAttached($this->mostRecentCommit); } public function getDiffusionBrowseURIForPath( PhabricatorUser $user, $path, $line = null, $branch = null) { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $this, 'path' => $path, 'branch' => $branch, )); return $drequest->generateURI( array( 'action' => 'browse', 'line' => $line, )); } public function getLocalPath() { return $this->getDetail('local-path'); } public function getSubversionBaseURI() { $vcs = $this->getVersionControlSystem(); if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) { throw new Exception("Not a subversion repository!"); } - $uri = $this->getDetail('remote-uri'); + if ($this->isHosted()) { + $uri = 'file://'.$this->getLocalPath(); + } else { + $uri = $this->getDetail('remote-uri'); + } + $subpath = $this->getDetail('svn-subpath'); + if ($subpath) { + $subpath = '/'.ltrim($subpath, '/'); + } return $uri.$subpath; } public function execRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return call_user_func_array('exec_manual', $args); } public function execxRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return call_user_func_array('execx', $args); } public function getRemoteCommandFuture($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return newv('ExecFuture', $args); } public function passthruRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return call_user_func_array('phutil_passthru', $args); } public function execLocalCommand($pattern /* , $arg, ... */) { $this->assertLocalExists(); $args = func_get_args(); $args = $this->formatLocalCommand($args); return call_user_func_array('exec_manual', $args); } public function execxLocalCommand($pattern /* , $arg, ... */) { $this->assertLocalExists(); $args = func_get_args(); $args = $this->formatLocalCommand($args); return call_user_func_array('execx', $args); } public function getLocalCommandFuture($pattern /* , $arg, ... */) { $this->assertLocalExists(); $args = func_get_args(); $args = $this->formatLocalCommand($args); return newv('ExecFuture', $args); } public function passthruLocalCommand($pattern /* , $arg, ... */) { $this->assertLocalExists(); $args = func_get_args(); $args = $this->formatLocalCommand($args); return call_user_func_array('phutil_passthru', $args); } private function formatRemoteCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); $empty = $this->getEmptyReadableDirectoryPath(); if ($this->shouldUseSSH()) { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "SVN_SSH=%s svn --non-interactive {$pattern}"; array_unshift( $args, csprintf( 'ssh -l %P -i %P', new PhutilOpaqueEnvelope($this->getSSHLogin()), new PhutilOpaqueEnvelope($this->getSSHKeyfile()))); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $command = call_user_func_array( 'csprintf', array_merge( array( "(ssh-add %P && HOME=%s git {$pattern})", new PhutilOpaqueEnvelope($this->getSSHKeyfile()), $empty, ), $args)); $pattern = "ssh-agent sh -c %s"; $args = array($command); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $pattern = "hg --config ui.ssh=%s {$pattern}"; array_unshift( $args, csprintf( 'ssh -l %P -i %P', new PhutilOpaqueEnvelope($this->getSSHLogin()), new PhutilOpaqueEnvelope($this->getSSHKeyfile()))); break; default: throw new Exception("Unrecognized version control system."); } } else if ($this->shouldUseHTTP()) { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "svn ". "--non-interactive ". "--no-auth-cache ". "--trust-server-cert ". "--username %P ". "--password %P ". $pattern; array_unshift( $args, new PhutilOpaqueEnvelope($this->getDetail('http-login')), new PhutilOpaqueEnvelope($this->getDetail('http-pass'))); break; default: throw new Exception( "No support for HTTP Basic Auth in this version control system."); } } else if ($this->shouldUseSVNProtocol()) { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "svn ". "--non-interactive ". "--no-auth-cache ". "--username %P ". "--password %P ". $pattern; array_unshift( $args, new PhutilOpaqueEnvelope($this->getDetail('http-login')), new PhutilOpaqueEnvelope($this->getDetail('http-pass'))); break; default: throw new Exception( "SVN protocol is SVN only."); } } else { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "svn --non-interactive {$pattern}"; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $pattern = "HOME=%s git {$pattern}"; array_unshift($args, $empty); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $pattern = "hg {$pattern}"; break; default: throw new Exception("Unrecognized version control system."); } } array_unshift($args, $pattern); return $args; } private function formatLocalCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); $empty = $this->getEmptyReadableDirectoryPath(); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "(cd %s && svn --non-interactive {$pattern})"; array_unshift($args, $this->getLocalPath()); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $pattern = "(cd %s && HOME=%s git {$pattern})"; array_unshift($args, $this->getLocalPath(), $empty); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $hgplain = (phutil_is_windows() ? "set HGPLAIN=1 &&" : "HGPLAIN=1"); $pattern = "(cd %s && {$hgplain} hg {$pattern})"; array_unshift($args, $this->getLocalPath()); break; default: throw new Exception("Unrecognized version control system."); } array_unshift($args, $pattern); return $args; } private function getEmptyReadableDirectoryPath() { // See T2965. Some time after Git 1.7.5.4, Git started fataling if it can // not read $HOME. For many users, $HOME points at /root (this seems to be // a default result of Apache setup). Instead, explicitly point $HOME at a // readable, empty directory so that Git looks for the config file it's // after, fails to locate it, and moves on. This is really silly, but seems // like the least damaging approach to mitigating the issue. $root = dirname(phutil_get_library_root('phabricator')); return $root.'/support/empty/'; } private function getSSHLogin() { return $this->getDetail('ssh-login'); } private function getSSHKeyfile() { if ($this->sshKeyfile === null) { $key = $this->getDetail('ssh-key'); $keyfile = $this->getDetail('ssh-keyfile'); if ($keyfile) { // Make sure we can read the file, that it exists, etc. Filesystem::readFile($keyfile); $this->sshKeyfile = $keyfile; } else if ($key) { $keyfile = new TempFile('phabricator-repository-ssh-key'); chmod($keyfile, 0600); Filesystem::writeFile($keyfile, $key); $this->sshKeyfile = $keyfile; } else { $this->sshKeyfile = ''; } } return (string)$this->sshKeyfile; } public function getURI() { return '/diffusion/'.$this->getCallsign().'/'; } public function isTracked() { return $this->getDetail('tracking-enabled', false); } public function getDefaultBranch() { $default = $this->getDetail('default-branch'); if (strlen($default)) { return $default; } $default_branches = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master', PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default', ); return idx($default_branches, $this->getVersionControlSystem()); } public function getDefaultArcanistBranch() { return coalesce($this->getDefaultBranch(), 'svn'); } private function isBranchInFilter($branch, $filter_key) { $vcs = $this->getVersionControlSystem(); $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); $use_filter = ($is_git); if ($use_filter) { $filter = $this->getDetail($filter_key, array()); if ($filter && empty($filter[$branch])) { return false; } } // By default, all branches pass. return true; } public function shouldTrackBranch($branch) { return $this->isBranchInFilter($branch, 'branch-filter'); } public function shouldAutocloseBranch($branch) { if ($this->isImporting()) { return false; } if ($this->getDetail('disable-autoclose', false)) { return false; } return $this->isBranchInFilter($branch, 'close-commits-filter'); } public function shouldAutocloseCommit( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { if ($this->getDetail('disable-autoclose', false)) { return false; } switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return true; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return true; default: throw new Exception("Unrecognized version control system."); } $branches = $data->getCommitDetail('seenOnBranches', array()); foreach ($branches as $branch) { if ($this->shouldAutocloseBranch($branch)) { return true; } } return false; } public function formatCommitName($commit_identifier) { $vcs = $this->getVersionControlSystem(); $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; $is_git = ($vcs == $type_git); $is_hg = ($vcs == $type_hg); if ($is_git || $is_hg) { $short_identifier = substr($commit_identifier, 0, 12); } else { $short_identifier = $commit_identifier; } return 'r'.$this->getCallsign().$short_identifier; } public function isImporting() { return (bool)$this->getDetail('importing', false); } /* -( Repository URI Management )------------------------------------------ */ /** * Get the remote URI for this repository. * * @return string * @task uri */ public function getRemoteURI() { return (string)$this->getRemoteURIObject(); } /** * Get the remote URI for this repository, without authentication information. * * @return string Repository URI. * @task uri */ public function getPublicRemoteURI() { $uri = $this->getRemoteURIObject(); // Make sure we don't leak anything if this repo is using HTTP Basic Auth // with the credentials in the URI or something zany like that. if ($uri instanceof PhutilGitURI) { if (!$this->getDetail('show-user', false)) { $uri->setUser(null); } } else { if (!$this->getDetail('show-user', false)) { $uri->setUser(null); } $uri->setPass(null); } return (string)$uri; } /** * Get the protocol for the repository's remote. * * @return string Protocol, like "ssh" or "git". * @task uri */ public function getRemoteProtocol() { $uri = $this->getRemoteURIObject(); if ($uri instanceof PhutilGitURI) { return 'ssh'; } else { return $uri->getProtocol(); } } /** * Get a parsed object representation of the repository's remote URI. This * may be a normal URI (returned as a @{class@libphutil:PhutilURI}) or a git * URI (returned as a @{class@libphutil:PhutilGitURI}). * * @return wild A @{class@libphutil:PhutilURI} or * @{class@libphutil:PhutilGitURI}. * @task uri */ private function getRemoteURIObject() { $raw_uri = $this->getDetail('remote-uri'); if (!$raw_uri) { return new PhutilURI(''); } if (!strncmp($raw_uri, '/', 1)) { return new PhutilURI('file://'.$raw_uri); } $uri = new PhutilURI($raw_uri); if ($uri->getProtocol()) { if ($this->isSSHProtocol($uri->getProtocol())) { if ($this->getSSHLogin()) { $uri->setUser($this->getSSHLogin()); } } return $uri; } $uri = new PhutilGitURI($raw_uri); if ($uri->getDomain()) { if ($this->getSSHLogin()) { $uri->setUser($this->getSSHLogin()); } return $uri; } throw new Exception("Remote URI '{$raw_uri}' could not be parsed!"); } /** * Determine if we should connect to the remote using SSH flags and * credentials. * * @return bool True to use the SSH protocol. * @task uri */ private function shouldUseSSH() { + if ($this->isHosted()) { + return false; + } + $protocol = $this->getRemoteProtocol(); if ($this->isSSHProtocol($protocol)) { return (bool)$this->getSSHKeyfile(); } else { return false; } } /** * Determine if we should connect to the remote using HTTP flags and * credentials. * * @return bool True to use the HTTP protocol. * @task uri */ private function shouldUseHTTP() { + if ($this->isHosted()) { + return false; + } + $protocol = $this->getRemoteProtocol(); if ($protocol == 'http' || $protocol == 'https') { return (bool)$this->getDetail('http-login'); } else { return false; } } /** * Determine if we should connect to the remote using SVN flags and * credentials. * * @return bool True to use the SVN protocol. * @task uri */ private function shouldUseSVNProtocol() { + if ($this->isHosted()) { + return false; + } + $protocol = $this->getRemoteProtocol(); if ($protocol == 'svn') { return (bool)$this->getDetail('http-login'); } else { return false; } } /** * Determine if a protocol is SSH or SSH-like. * * @param string A protocol string, like "http" or "ssh". * @return bool True if the protocol is SSH-like. * @task uri */ private function isSSHProtocol($protocol) { return ($protocol == 'ssh' || $protocol == 'svn+ssh'); } public function delete() { $this->openTransaction(); $paths = id(new PhabricatorOwnersPath()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($paths as $path) { $path->delete(); } $projects = id(new PhabricatorRepositoryArcanistProject()) ->loadAllWhere('repositoryID = %d', $this->getID()); foreach ($projects as $project) { // note each project deletes its PhabricatorRepositorySymbols $project->delete(); } $commits = id(new PhabricatorRepositoryCommit()) ->loadAllWhere('repositoryID = %d', $this->getID()); foreach ($commits as $commit) { // note PhabricatorRepositoryAuditRequests and // PhabricatorRepositoryCommitData are deleted here too. $commit->delete(); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_FILESYSTEM, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_PATHCHANGE, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_SUMMARY, $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function isGit() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); } public function isSVN() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN); } public function isHg() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL); } public function isHosted() { return (bool)$this->getDetail('hosting-enabled', false); } public function setHosted($enabled) { return $this->setDetail('hosting-enabled', $enabled); } public function getServeOverHTTP() { $serve = $this->getDetail('serve-over-http', self::SERVE_OFF); return $this->normalizeServeConfigSetting($serve); } public function setServeOverHTTP($mode) { return $this->setDetail('serve-over-http', $mode); } public function getServeOverSSH() { $serve = $this->getDetail('serve-over-ssh', self::SERVE_OFF); return $this->normalizeServeConfigSetting($serve); } public function setServeOverSSH($mode) { return $this->setDetail('serve-over-ssh', $mode); } public static function getProtocolAvailabilityName($constant) { switch ($constant) { case self::SERVE_OFF: return pht('Off'); case self::SERVE_READONLY: return pht('Read Only'); case self::SERVE_READWRITE: return pht('Read/Write'); default: return pht('Unknown'); } } private function normalizeServeConfigSetting($value) { switch ($value) { case self::SERVE_OFF: case self::SERVE_READONLY: return $value; case self::SERVE_READWRITE: if ($this->isHosted()) { return self::SERVE_READWRITE; } else { return self::SERVE_READONLY; } default: return self::SERVE_OFF; } } /** * Raise more useful errors when there are basic filesystem problems. */ private function assertLocalExists() { - switch ($this->getVersionControlSystem()) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - if (!$this->isHosted()) { - // For non-hosted SVN repositories, we don't expect a local directory - // to exist. - return; - } - break; + if (!$this->usesLocalWorkingCopy()) { + return; } $local = $this->getLocalPath(); Filesystem::assertExists($local); Filesystem::assertIsDirectory($local); Filesystem::assertReadable($local); } /** * Determine if the working copy is bare or not. In Git, this corresponds * to `--bare`. In Mercurial, `--noupdate`. */ public function isWorkingCopyBare() { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return false; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $local = $this->getLocalPath(); if (Filesystem::pathExists($local.'/.git')) { return false; } else { return true; } } } public function usesLocalWorkingCopy() { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return $this->isHosted(); case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return true; } } public function writeStatusMessage( $status_type, $status_code, array $parameters = array()) { $table = new PhabricatorRepositoryStatusMessage(); $conn_w = $table->establishConnection('w'); $table_name = $table->getTableName(); if ($status_code === null) { queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s', $table_name, $this->getID(), $status_type); } else { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, statusType, statusCode, parameters, epoch) VALUES (%d, %s, %s, %s, %d) ON DUPLICATE KEY UPDATE statusCode = VALUES(statusCode), parameters = VALUES(parameters), epoch = VALUES(epoch)', $table_name, $this->getID(), $status_type, $status_code, json_encode($parameters), time()); } return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, DiffusionCapabilityPush::CAPABILITY, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case DiffusionCapabilityPush::CAPABILITY: return $this->getPushPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digestForIndex($this->getMarkupText($field)); return "repo:{$hash}"; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { return $this->getDetail('description'); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { require_celerity_resource('phabricator-remarkup-css'); return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return true; } }