diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php index a288d6a70b..7c7072dbf5 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php @@ -1,139 +1,139 @@ null, 'foundURI' => null, 'validDomain' => null, 'matchHashType' => null, 'matchHashValue' => null, ); public function withCommitRef(DiffusionCommitRef $ref) { $this->ref = $ref; return $this; } public function getRevisionMatchData() { return $this->revisionMatchData; } private function setRevisionMatchData($key, $value) { $this->revisionMatchData[$key] = $value; return $this; } - public function executeQuery() { + protected function executeQuery() { $ref = $this->ref; $message = $ref->getMessage(); $hashes = $ref->getHashes(); $params = array( 'corpus' => $message, 'partial' => true, ); $result = id(new ConduitCall('differential.parsecommitmessage', $params)) ->setUser(PhabricatorUser::getOmnipotentUser()) ->execute(); $fields = $result['fields']; $revision_id = idx($fields, 'revisionID'); if ($revision_id) { $this->setRevisionMatchData('usedURI', true); } else { $this->setRevisionMatchData('usedURI', false); } $revision_id_info = $result['revisionIDFieldInfo']; $this->setRevisionMatchData('foundURI', $revision_id_info['value']); $this->setRevisionMatchData( 'validDomain', $revision_id_info['validDomain']); // If there is no "Differential Revision:" field in the message, try to // identify the revision by doing a hash lookup. if (!$revision_id && $hashes) { $hash_list = array(); foreach ($hashes as $hash) { $hash_list[] = array($hash->getHashType(), $hash->getHashValue()); } $revisions = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->needHashes(true) ->withCommitHashes($hash_list) ->execute(); if (!empty($revisions)) { $revision = $this->pickBestRevision($revisions); $fields['revisionID'] = $revision->getID(); $revision_hashes = $revision->getHashes(); $revision_hashes = DiffusionCommitHash::convertArrayToObjects( $revision_hashes); $revision_hashes = mpull($revision_hashes, 'getHashType'); // sort the hashes in the order the mighty // @{class:ArcanstDifferentialRevisionHash} does; probably unnecessary // but should future proof things nicely. $revision_hashes = array_select_keys( $revision_hashes, ArcanistDifferentialRevisionHash::getTypes()); foreach ($hashes as $hash) { $revision_hash = idx($revision_hashes, $hash->getHashType()); if (!$revision_hash) { continue; } if ($revision_hash->getHashValue() == $hash->getHashValue()) { $this->setRevisionMatchData( 'matchHashType', $hash->getHashType()); $this->setRevisionMatchData( 'matchHashValue', $hash->getHashValue()); break; } } } } return $fields; } /** * When querying for revisions by hash, more than one revision may be found. * This function identifies the "best" revision from such a set. Typically, * there is only one revision found. Otherwise, we try to pick an accepted * revision first, followed by an open revision, and otherwise we go with a * closed or abandoned revision as a last resort. */ private function pickBestRevision(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); // If we have more than one revision of a given status, choose the most // recently updated one. $revisions = msort($revisions, 'getDateModified'); $revisions = array_reverse($revisions); // Try to find an accepted revision first. $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; foreach ($revisions as $revision) { if ($revision->getStatus() == $status_accepted) { return $revision; } } // Try to find an open revision. foreach ($revisions as $revision) { if (!$revision->isClosed()) { return $revision; } } // Settle for whatever's left. return head($revisions); } } diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php index 54f42ef7d7..4aaf85d930 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php @@ -1,160 +1,160 @@ identifier = $identifier; return $this; } - public function executeQuery() { + protected function executeQuery() { if (!strlen($this->identifier)) { throw new Exception( pht('You must provide an identifier with withIdentifier()!')); } $type = $this->getRepository()->getVersionControlSystem(); switch ($type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->loadGitCommitRef(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->loadMercurialCommitRef(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = $this->loadSubversionCommitRef(); break; default: throw new Exception(pht('Unsupported repository type "%s"!', $type)); } return $result; } private function loadGitCommitRef() { $repository = $this->getRepository(); // NOTE: %B was introduced somewhat recently in git's history, so pull // commit message information with %s and %b instead. // Even though we pass --encoding here, git doesn't always succeed, so // we try a little harder, since git *does* tell us what the actual encoding // is correctly (unless it doesn't; encoding is sometimes empty). list($info) = $repository->execxLocalCommand( 'log -n 1 --encoding=%s --format=%s %s --', 'UTF-8', implode( '%x00', array('%e', '%cn', '%ce', '%an', '%ae', '%T', '%s%n%n%b')), $this->identifier); $parts = explode("\0", $info); $encoding = array_shift($parts); foreach ($parts as $key => $part) { if ($encoding) { $part = phutil_utf8_convert($part, 'UTF-8', $encoding); } $parts[$key] = phutil_utf8ize($part); if (!strlen($parts[$key])) { $parts[$key] = null; } } $hashes = array( id(new DiffusionCommitHash()) ->setHashType(ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT) ->setHashValue($this->identifier), id(new DiffusionCommitHash()) ->setHashType(ArcanistDifferentialRevisionHash::HASH_GIT_TREE) ->setHashValue($parts[4]), ); return id(new DiffusionCommitRef()) ->setCommitterName($parts[0]) ->setCommitterEmail($parts[1]) ->setAuthorName($parts[2]) ->setAuthorEmail($parts[3]) ->setHashes($hashes) ->setMessage($parts[5]); } private function loadMercurialCommitRef() { $repository = $this->getRepository(); list($stdout) = $repository->execxLocalCommand( 'log --template %s --rev %s', '{author}\\n{desc}', hgsprintf('%s', $this->identifier)); list($author, $message) = explode("\n", $stdout, 2); $author = phutil_utf8ize($author); $message = phutil_utf8ize($message); list($author_name, $author_email) = $this->splitUserIdentifier($author); $hashes = array( id(new DiffusionCommitHash()) ->setHashType(ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT) ->setHashValue($this->identifier), ); return id(new DiffusionCommitRef()) ->setAuthorName($author_name) ->setAuthorEmail($author_email) ->setMessage($message) ->setHashes($hashes); } private function loadSubversionCommitRef() { $repository = $this->getRepository(); list($xml) = $repository->execxRemoteCommand( 'log --xml --limit 1 %s', $repository->getSubversionPathURI(null, $this->identifier)); // Subversion may send us back commit messages which won't parse because // they have non UTF-8 garbage in them. Slam them into valid UTF-8. $xml = phutil_utf8ize($xml); $log = new SimpleXMLElement($xml); $entry = $log->logentry[0]; $author = (string)$entry->author; $message = (string)$entry->msg; list($author_name, $author_email) = $this->splitUserIdentifier($author); // No hashes in Subversion. $hashes = array(); return id(new DiffusionCommitRef()) ->setAuthorName($author_name) ->setAuthorEmail($author_email) ->setMessage($message) ->setHashes($hashes); } private function splitUserIdentifier($user) { $email = new PhutilEmailAddress($user); if ($email->getDisplayName() || $email->getDomainName()) { $user_name = $email->getDisplayName(); $user_email = $email->getAddress(); } else { $user_name = $email->getAddress(); $user_email = null; } return array($user_name, $user_email); } } diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php index b7ecf5a5c4..2b264bc35c 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php @@ -1,84 +1,84 @@ identifier = $identifier; return $this; } - public function executeQuery() { + protected function executeQuery() { if (!strlen($this->identifier)) { throw new Exception( pht('You must provide an identifier with withIdentifier()!')); } $type = $this->getRepository()->getVersionControlSystem(); switch ($type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->loadGitParents(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->loadMercurialParents(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = $this->loadSubversionParents(); break; default: throw new Exception(pht('Unsupported repository type "%s"!', $type)); } return $result; } private function loadGitParents() { $repository = $this->getRepository(); list($stdout) = $repository->execxLocalCommand( 'log -n 1 --format=%s %s', '%P', $this->identifier); return preg_split('/\s+/', trim($stdout)); } private function loadMercurialParents() { $repository = $this->getRepository(); list($stdout) = $repository->execxLocalCommand( 'log --debug --limit 1 --template={parents} --rev %s', $this->identifier); $stdout = PhabricatorRepository::filterMercurialDebugOutput($stdout); $hashes = preg_split('/\s+/', trim($stdout)); foreach ($hashes as $key => $value) { // Mercurial parents look like "23:ad9f769d6f786fad9f76d9a" -- we want // to strip out the local rev part. list($local, $global) = explode(':', $value); $hashes[$key] = $global; // With --debug we get 40-character hashes but also get the "000000..." // hash for missing parents; ignore it. if (preg_match('/^0+$/', $global)) { unset($hashes[$key]); } } return $hashes; } private function loadSubversionParents() { $n = (int)$this->identifier; if ($n > 1) { $ids = array($n - 1); } else { $ids = array(); } return $ids; } } diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php index 72f2d18533..25973562e0 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php @@ -1,219 +1,219 @@ refs = $refs; return $this; } - public function executeQuery() { + protected function executeQuery() { if (!$this->refs) { return array(); } switch ($this->getRepository()->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->resolveGitRefs(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->resolveMercurialRefs(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = $this->resolveSubversionRefs(); break; default: throw new Exception('Unsupported repository type!'); } return $result; } private function resolveGitRefs() { $repository = $this->getRepository(); $future = $repository->getLocalCommandFuture('cat-file --batch-check'); $future->write(implode("\n", $this->refs)); list($stdout) = $future->resolvex(); $lines = explode("\n", rtrim($stdout, "\n")); if (count($lines) !== count($this->refs)) { throw new Exception('Unexpected line count from `git cat-file`!'); } $hits = array(); $tags = array(); $lines = array_combine($this->refs, $lines); foreach ($lines as $ref => $line) { $parts = explode(' ', $line); if (count($parts) < 2) { throw new Exception("Failed to parse `git cat-file` output: {$line}"); } list($identifier, $type) = $parts; if ($type == 'missing') { // This is either an ambiguous reference which resolves to several // objects, or an invalid reference. For now, always treat it as // invalid. It would be nice to resolve all possibilities for // ambiguous references at some point, although the strategy for doing // so isn't clear to me. continue; } switch ($type) { case 'commit': break; case 'tag': $tags[] = $identifier; break; default: throw new Exception( "Unexpected object type from `git cat-file`: {$line}"); } $hits[] = array( 'ref' => $ref, 'type' => $type, 'identifier' => $identifier, ); } $tag_map = array(); if ($tags) { // If some of the refs were tags, just load every tag in order to figure // out which commits they map to. This might be somewhat inefficient in // repositories with a huge number of tags. $tag_refs = id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withIsTag(true) ->executeQuery(); foreach ($tag_refs as $tag_ref) { $tag_map[$tag_ref->getShortName()] = $tag_ref->getCommitIdentifier(); } } $results = array(); foreach ($hits as $hit) { $type = $hit['type']; $ref = $hit['ref']; $alternate = null; if ($type == 'tag') { $alternate = $identifier; $identifier = idx($tag_map, $ref); if (!$identifier) { throw new Exception("Failed to look up tag '{$ref}'!"); } } $result = array( 'type' => $type, 'identifier' => $identifier, ); if ($alternate !== null) { $result['alternate'] = $alternate; } $results[$ref][] = $result; } return $results; } private function resolveMercurialRefs() { $repository = $this->getRepository(); $futures = array(); foreach ($this->refs as $ref) { $futures[$ref] = $repository->getLocalCommandFuture( 'log --template=%s --rev %s', '{node}', hgsprintf('%s', $ref)); } $results = array(); foreach (new FutureIterator($futures) as $ref => $future) { try { list($stdout) = $future->resolvex(); } catch (CommandException $ex) { if (preg_match('/ambiguous identifier/', $ex->getStdErr())) { // This indicates that the ref ambiguously matched several things. // Eventually, it would be nice to return all of them, but it is // unclear how to best do that. For now, treat it as a miss instead. continue; } throw $ex; } // It doesn't look like we can figure out the type (commit/branch/rev) // from this output very easily. For now, just call everything a commit. $type = 'commit'; $results[$ref][] = array( 'type' => $type, 'identifier' => trim($stdout), ); } return $results; } private function resolveSubversionRefs() { $repository = $this->getRepository(); $max_commit = id(new PhabricatorRepositoryCommit()) ->loadOneWhere( 'repositoryID = %d ORDER BY epoch DESC, id DESC LIMIT 1', $repository->getID()); if (!$max_commit) { // This repository is empty or hasn't parsed yet, so none of the refs are // going to resolve. return array(); } $max_commit_id = (int)$max_commit->getCommitIdentifier(); $results = array(); foreach ($this->refs as $ref) { if ($ref == 'HEAD') { // Resolve "HEAD" to mean "the most recent commit". $results[$ref][] = array( 'type' => 'commit', 'identifier' => $max_commit_id, ); continue; } if (!preg_match('/^\d+$/', $ref)) { // This ref is non-numeric, so it doesn't resolve to anything. continue; } // Resolve other commits if we can deduce their existence. // TODO: When we import only part of a repository, we won't necessarily // have all of the smaller commits. Should we fail to resolve them here // for repositories with a subpath? It might let us simplify other things // elsewhere. if ((int)$ref <= $max_commit_id) { $results[$ref][] = array( 'type' => 'commit', 'identifier' => (int)$ref, ); } } return $results; } }