Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F9583740
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
33 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index b89c0d4c..c6e80dcb 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,156 +1,159 @@
<?php
/**
* This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
* @generated
*/
phutil_register_library_map(array(
'class' =>
array(
'ArcanistAmendWorkflow' => 'workflow/amend',
'ArcanistApacheLicenseLinter' => 'lint/linter/apachelicense',
'ArcanistApacheLicenseLinterTestCase' => 'lint/linter/apachelicense/__tests__',
'ArcanistBaseUnitTestEngine' => 'unit/engine/base',
'ArcanistBaseWorkflow' => 'workflow/base',
'ArcanistBranchWorkflow' => 'workflow/branch',
'ArcanistBundle' => 'parser/bundle',
'ArcanistCallConduitWorkflow' => 'workflow/call-conduit',
'ArcanistCapabilityNotSupportedException' => 'workflow/exception/notsupported',
'ArcanistChooseInvalidRevisionException' => 'exception',
'ArcanistChooseNoRevisionsException' => 'exception',
'ArcanistCommitWorkflow' => 'workflow/commit',
'ArcanistConfiguration' => 'configuration',
'ArcanistCoverWorkflow' => 'workflow/cover',
'ArcanistDiffChange' => 'parser/diff/change',
'ArcanistDiffChangeType' => 'parser/diff/changetype',
'ArcanistDiffHunk' => 'parser/diff/hunk',
'ArcanistDiffParser' => 'parser/diff',
'ArcanistDiffParserTestCase' => 'parser/diff/__tests__',
'ArcanistDiffUtils' => 'difference',
'ArcanistDiffWorkflow' => 'workflow/diff',
'ArcanistDifferentialCommitMessage' => 'differential/commitmessage',
'ArcanistDifferentialCommitMessageParserException' => 'differential/commitmessage',
'ArcanistDifferentialRevisionRef' => 'differential/revision',
'ArcanistDownloadWorkflow' => 'workflow/download',
'ArcanistExportWorkflow' => 'workflow/export',
'ArcanistFilenameLinter' => 'lint/linter/filename',
'ArcanistGeneratedLinter' => 'lint/linter/generated',
'ArcanistGitAPI' => 'repository/api/git',
'ArcanistGitHookPreReceiveWorkflow' => 'workflow/git-hook-pre-receive',
'ArcanistHelpWorkflow' => 'workflow/help',
'ArcanistInstallCertificateWorkflow' => 'workflow/install-certificate',
'ArcanistLiberateLintEngine' => 'lint/engine/liberate',
'ArcanistLiberateWorkflow' => 'workflow/liberate',
'ArcanistLicenseLinter' => 'lint/linter/license',
'ArcanistLintEngine' => 'lint/engine/base',
'ArcanistLintJSONRenderer' => 'lint/renderer',
'ArcanistLintMessage' => 'lint/message',
'ArcanistLintPatcher' => 'lint/patcher',
'ArcanistLintRenderer' => 'lint/renderer',
'ArcanistLintResult' => 'lint/result',
'ArcanistLintSeverity' => 'lint/severity',
'ArcanistLintSummaryRenderer' => 'lint/renderer',
'ArcanistLintWorkflow' => 'workflow/lint',
'ArcanistLinter' => 'lint/linter/base',
'ArcanistLinterTestCase' => 'lint/linter/base/test',
'ArcanistListWorkflow' => 'workflow/list',
'ArcanistMarkCommittedWorkflow' => 'workflow/mark-committed',
'ArcanistMercurialAPI' => 'repository/api/mercurial',
+ 'ArcanistMercurialParser' => 'repository/parser/mercurial',
+ 'ArcanistMercurialParserTestCase' => 'repository/parser/mercurial/__tests__',
'ArcanistMergeWorkflow' => 'workflow/merge',
'ArcanistNoEffectException' => 'exception/usage/noeffect',
'ArcanistNoEngineException' => 'exception/usage/noengine',
'ArcanistNoLintLinter' => 'lint/linter/nolint',
'ArcanistNoLintTestCaseMisnamed' => 'lint/linter/nolint/__tests__',
'ArcanistPEP8Linter' => 'lint/linter/pep8',
'ArcanistPasteWorkflow' => 'workflow/paste',
'ArcanistPatchWorkflow' => 'workflow/patch',
'ArcanistPhutilModuleLinter' => 'lint/linter/phutilmodule',
'ArcanistPhutilTestCase' => 'unit/engine/phutil/testcase',
'ArcanistPhutilTestTerminatedException' => 'unit/engine/phutil/testcase/exception',
'ArcanistPyFlakesLinter' => 'lint/linter/pyflakes',
'ArcanistPyLintLinter' => 'lint/linter/pylint',
'ArcanistRepositoryAPI' => 'repository/api/base',
'ArcanistShellCompleteWorkflow' => 'workflow/shell-complete',
'ArcanistSubversionAPI' => 'repository/api/subversion',
'ArcanistSvnHookPreCommitWorkflow' => 'workflow/svn-hook-pre-commit',
'ArcanistTextLinter' => 'lint/linter/text',
'ArcanistTextLinterTestCase' => 'lint/linter/text/__tests__',
'ArcanistUnitTestResult' => 'unit/result',
'ArcanistUnitWorkflow' => 'workflow/unit',
'ArcanistUploadWorkflow' => 'workflow/upload',
'ArcanistUsageException' => 'exception/usage',
'ArcanistUserAbortException' => 'exception/usage/userabort',
'ArcanistWorkingCopyIdentity' => 'workingcopyidentity',
'ArcanistXHPASTLintNamingHook' => 'lint/linter/xhpast/naminghook',
'ArcanistXHPASTLinter' => 'lint/linter/xhpast',
'ArcanistXHPASTLinterTestCase' => 'lint/linter/xhpast/__tests__',
'BranchInfo' => 'branch',
'ExampleLintEngine' => 'lint/engine/example',
'PhutilLintEngine' => 'lint/engine/phutil',
'PhutilModuleRequirements' => 'parser/phutilmodule',
'PhutilUnitTestEngine' => 'unit/engine/phutil',
'PhutilUnitTestEngineTestCase' => 'unit/engine/phutil/__tests__',
'UnitTestableArcanistLintEngine' => 'lint/engine/test',
),
'function' =>
array(
),
'requires_class' =>
array(
'ArcanistAmendWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistApacheLicenseLinter' => 'ArcanistLicenseLinter',
'ArcanistApacheLicenseLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistBranchWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCallConduitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCommitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCoverWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistDiffParserTestCase' => 'ArcanistPhutilTestCase',
'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistDownloadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistFilenameLinter' => 'ArcanistLinter',
'ArcanistGeneratedLinter' => 'ArcanistLinter',
'ArcanistGitAPI' => 'ArcanistRepositoryAPI',
'ArcanistGitHookPreReceiveWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistHelpWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistInstallCertificateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLiberateLintEngine' => 'ArcanistLintEngine',
'ArcanistLiberateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLicenseLinter' => 'ArcanistLinter',
'ArcanistLintWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase',
'ArcanistListWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
+ 'ArcanistMercurialParserTestCase' => 'ArcanistPhutilTestCase',
'ArcanistMergeWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistNoEffectException' => 'ArcanistUsageException',
'ArcanistNoEngineException' => 'ArcanistUsageException',
'ArcanistNoLintLinter' => 'ArcanistLinter',
'ArcanistNoLintTestCaseMisnamed' => 'ArcanistLinterTestCase',
'ArcanistPEP8Linter' => 'ArcanistLinter',
'ArcanistPasteWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPhutilModuleLinter' => 'ArcanistLinter',
'ArcanistPyFlakesLinter' => 'ArcanistLinter',
'ArcanistPyLintLinter' => 'ArcanistLinter',
'ArcanistShellCompleteWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI',
'ArcanistSvnHookPreCommitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistTextLinter' => 'ArcanistLinter',
'ArcanistTextLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistUnitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUploadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUserAbortException' => 'ArcanistUsageException',
'ArcanistXHPASTLinter' => 'ArcanistLinter',
'ArcanistXHPASTLinterTestCase' => 'ArcanistLinterTestCase',
'ExampleLintEngine' => 'ArcanistLintEngine',
'PhutilLintEngine' => 'ArcanistLintEngine',
'PhutilUnitTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhutilUnitTestEngineTestCase' => 'ArcanistPhutilTestCase',
'UnitTestableArcanistLintEngine' => 'ArcanistLintEngine',
),
'requires_interface' =>
array(
),
));
diff --git a/src/repository/api/mercurial/ArcanistMercurialAPI.php b/src/repository/api/mercurial/ArcanistMercurialAPI.php
index 8a02ae97..c288e37f 100644
--- a/src/repository/api/mercurial/ArcanistMercurialAPI.php
+++ b/src/repository/api/mercurial/ArcanistMercurialAPI.php
@@ -1,427 +1,326 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Interfaces with the Mercurial working copies.
*
* @group workingcopy
*/
class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
private $status;
private $base;
private $relativeCommit;
public function getSourceControlSystemName() {
return 'hg';
}
public function getSourceControlBaseRevision() {
list($stdout) = execx(
'(cd %s && hg id -ir %s)',
$this->getPath(),
$this->getRelativeCommit());
return $stdout;
}
public function getSourceControlPath() {
return '/';
}
public function getBranchName() {
// TODO: I have nearly no idea how hg local branches work.
list($stdout) = execx(
'(cd %s && hg branch)',
$this->getPath());
return $stdout;
}
public function setRelativeCommit($commit) {
list($err) = exec_manual(
'(cd %s && hg id -ir %s)',
$this->getPath(),
$commit);
if ($err) {
throw new ArcanistUsageException(
"Commit '{$commit}' is not a valid Mercurial commit identifier.");
}
$this->relativeCommit = $commit;
return $this;
}
public function getRelativeCommit() {
if (empty($this->relativeCommit)) {
list($stdout) = execx(
'(cd %s && hg outgoing --branch `hg branch` --limit 1 --style default)',
$this->getPath());
- $logs = $this->parseMercurialLog($stdout);
+ $logs = ArcanistMercurialParser::parseMercurialLog($stdout);
if (!count($logs)) {
throw new ArcanistUsageException("You have no outgoing changes!");
}
$oldest_log = head($logs);
$oldest_rev = $oldest_log['rev'];
// NOTE: The "^" and "~" syntaxes were only added in hg 1.9, which is new
// as of July 2011, so do this in a compatible way. Also, "hg log" and
// "hg outgoing" don't necessarily show parents (even if given an explicit
// template consisting of just the parents token) so we need to separately
// execute "hg parents".
list($stdout) = execx(
'(cd %s && hg parents --style default --rev %s)',
$this->getPath(),
$oldest_rev);
- $parents_logs = $this->parseMercurialLog($stdout);
+ $parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout);
$first_parent = head($parents_logs);
if (!$first_parent) {
throw new ArcanistUsageException(
"Oldest outgoing change has no parent revision!");
}
$this->relativeCommit = $first_parent['rev'];
}
return $this->relativeCommit;
}
public function getLocalCommitInformation() {
list($info) = execx(
'(cd %s && hg log --style default --rev %s..%s --)',
$this->getPath(),
$this->getRelativeCommit(),
$this->getWorkingCopyRevision());
- $logs = $this->parseMercurialLog($info);
+ $logs = ArcanistMercurialParser::parseMercurialLog($info);
// Get rid of the first log, it's not actually part of the diff. "hg log"
// is inclusive, while "hg diff" is exclusive.
array_shift($logs);
return $logs;
}
public function getBlame($path) {
list($stdout) = execx(
'(cd %s && hg blame -u -v -c --rev %s -- %s)',
$this->getPath(),
$this->getRelativeCommit(),
$path);
$blame = array();
foreach (explode("\n", trim($stdout)) as $line) {
if (!strlen($line)) {
continue;
}
$matches = null;
$ok = preg_match('^/\s*([^:]+?) [a-f0-9]{12}: (.*)$/', $line, $matches);
if (!$ok) {
throw new Exception("Unable to parse Mercurial blame line: {$line}");
}
$revision = $matches[2];
$author = trim($matches[1]);
$blame[] = array($author, $revision);
}
return $blame;
}
public function getWorkingCopyStatus() {
if (!isset($this->status)) {
// A reviewable revision spans multiple local commits in Mercurial, but
// there is no way to get file change status across multiple commits, so
// just take the entire diff and parse it to figure out what's changed.
$diff = $this->getFullMercurialDiff();
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($diff);
$status_map = array();
foreach ($changes as $change) {
$flags = 0;
switch ($change->getType()) {
case ArcanistDiffChangeType::TYPE_ADD:
case ArcanistDiffChangeType::TYPE_MOVE_HERE:
case ArcanistDiffChangeType::TYPE_COPY_HERE:
$flags |= self::FLAG_ADDED;
break;
case ArcanistDiffChangeType::TYPE_CHANGE:
case ArcanistDiffChangeType::TYPE_COPY_AWAY: // Check for changes?
$flags |= self::FLAG_MODIFIED;
break;
case ArcanistDiffChangeType::TYPE_DELETE:
case ArcanistDiffChangeType::TYPE_MOVE_AWAY:
case ArcanistDiffChangeType::TYPE_MULTICOPY:
$flags |= self::FLAG_DELETED;
break;
}
$status_map[$change->getCurrentPath()] = $flags;
}
list($stdout) = execx(
'(cd %s && hg status)',
$this->getPath());
- $working_status = $this->parseMercurialStatus($stdout);
+ $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
foreach ($working_status as $path => $status) {
$status |= self::FLAG_UNCOMMITTED;
if (!empty($status_map[$path])) {
$status_map[$path] |= $status;
} else {
$status_map[$path] = $status;
}
}
$this->status = $status_map;
}
return $this->status;
}
private function getDiffOptions() {
$options = array(
'--git',
'-U'.$this->getDiffLinesOfContext(),
);
return implode(' ', $options);
}
public function getRawDiffText($path) {
$options = $this->getDiffOptions();
list($stdout) = execx(
'(cd %s && hg diff %C --rev %s --rev %s -- %s)',
$this->getPath(),
$options,
$this->getRelativeCommit(),
$this->getWorkingCopyRevision(),
$path);
return $stdout;
}
public function getFullMercurialDiff() {
$options = $this->getDiffOptions();
list($stdout) = execx(
'(cd %s && hg diff %C --rev %s --rev %s --)',
$this->getPath(),
$options,
$this->getRelativeCommit(),
$this->getWorkingCopyRevision());
return $stdout;
}
public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getRelativeCommit());
}
public function getCurrentFileData($path) {
return $this->getFileDataAtRevision(
$path,
$this->getWorkingCopyRevision());
}
private function getFileDataAtRevision($path, $revision) {
list($stdout) = execx(
'(cd %s && hg cat --rev %s -- %s)',
$this->getPath(),
$path);
return $stdout;
}
- private function parseMercurialStatus($status) {
- $result = array();
-
- $status = trim($status);
- if (!strlen($status)) {
- return $result;
- }
-
- $lines = explode("\n", $status);
- foreach ($lines as $line) {
- $flags = 0;
- list($code, $path) = explode(' ', $line, 2);
- switch ($code) {
- case 'A':
- $flags |= self::FLAG_ADDED;
- break;
- case 'R':
- $flags |= self::FLAG_REMOVED;
- break;
- case 'M':
- $flags |= self::FLAG_MODIFIED;
- break;
- case 'C':
- // This is "clean" and included only for completeness, these files
- // have not been changed.
- break;
- case '!':
- $flags |= self::FLAG_MISSING;
- break;
- case '?':
- $flags |= self::FLAG_UNTRACKED;
- break;
- case 'I':
- // This is "ignored" and included only for completeness.
- break;
- default:
- throw new Exception("Unknown Mercurial status '{$code}'.");
- }
-
- $result[$path] = $flags;
- }
-
- return $result;
- }
-
- private function parseMercurialLog($log) {
- $result = array();
-
- $chunks = explode("\n\n", trim($log));
- foreach ($chunks as $chunk) {
- $commit = array();
- $lines = explode("\n", $chunk);
- foreach ($lines as $line) {
- if (preg_match('/^(comparing with|searching for changes)/', $line)) {
- // These are sent to stdout when you run "hg outgoing" although the
- // format is otherwise identical to "hg log".
- continue;
- }
- list($name, $value) = explode(':', $line, 2);
- $value = trim($value);
- switch ($name) {
- case 'user':
- $commit['user'] = $value;
- break;
- case 'date':
- $commit['date'] = strtotime($value);
- break;
- case 'summary':
- $commit['summary'] = $value;
- break;
- case 'changeset':
- list($local, $rev) = explode(':', $value, 2);
- $commit['local'] = $local;
- $commit['rev'] = $rev;
- break;
- case 'parent':
- if (empty($commit['parents'])) {
- $commit['parents'] = array();
- }
- list($local, $rev) = explode(':', $value, 2);
- $commit['parents'][] = array(
- 'local' => $local,
- 'rev' => $rev,
- );
- break;
- case 'branch':
- $commit['branch'] = $value;
- break;
- case 'tag':
- $commit['tag'] = $value;
- break;
- default:
- throw new Exception("Unknown Mercurial log field '{$name}'!");
- }
- }
- $result[] = $commit;
- }
-
- return $result;
- }
-
private function getWorkingCopyRevision() {
// In Mercurial, "tip" means the tip of the current branch, not what's in
// the working copy. The tip may be ahead of the working copy. We need to
// use "hg summary" to figure out what is actually in the working copy.
// For instance, "hg up 4 && arc diff" should not show commits 5 and above.
// The output of "hg summary" is different from the output of other hg
// commands so just parse it manually.
list($stdout) = execx(
'(cd %s && hg summary)',
$this->getPath());
$lines = explode("\n", $stdout);
$first = head($lines);
$match = null;
if (!preg_match('/^parent: \d+:([^ ]+)( |$)/', $first, $match)) {
throw new Exception("Unable to parse 'hg summary'.");
}
return trim($match[1]);
}
public function supportsRelativeLocalCommits() {
return true;
}
public function parseRelativeLocalCommit(array $argv) {
if (count($argv) == 0) {
return;
}
if (count($argv) != 1) {
throw new ArcanistUsageException("Specify only one commit.");
}
// This does the "hg id" call we need to normalize/validate the revision
// identifier.
$this->setRelativeCommit(reset($argv));
}
public function getAllLocalChanges() {
$diff = $this->getFullMercurialDiff();
$parser = new ArcanistDiffParser();
return $parser->parseDiff($diff);
}
public function supportsLocalBranchMerge() {
return true;
}
public function performLocalBranchMerge($branch, $message) {
if ($branch) {
$err = phutil_passthru(
'(cd %s && hg merge --rev %s && hg commit -m %s)',
$this->getPath(),
$branch,
$message);
} else {
$err = phutil_passthru(
'(cd %s && hg merge && hg commit -m %s)',
$this->getPath(),
$message);
}
if ($err) {
throw new ArcanistUsageException("Merge failed!");
}
}
public function getFinalizedRevisionMessage() {
return "You may now push this commit upstream, as appropriate (e.g. with ".
"'hg push' or by printing and faxing it).";
}
}
diff --git a/src/repository/api/mercurial/__init__.php b/src/repository/api/mercurial/__init__.php
index f4ae19bc..4fad8a3a 100644
--- a/src/repository/api/mercurial/__init__.php
+++ b/src/repository/api/mercurial/__init__.php
@@ -1,18 +1,19 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'parser/diff');
phutil_require_module('arcanist', 'parser/diff/changetype');
phutil_require_module('arcanist', 'repository/api/base');
+phutil_require_module('arcanist', 'repository/parser/mercurial');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistMercurialAPI.php');
diff --git a/src/repository/parser/mercurial/ArcanistMercurialParser.php b/src/repository/parser/mercurial/ArcanistMercurialParser.php
new file mode 100644
index 00000000..a5831e29
--- /dev/null
+++ b/src/repository/parser/mercurial/ArcanistMercurialParser.php
@@ -0,0 +1,192 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Parses output from various "hg" commands into structured data. This class
+ * provides low-level APIs for reading "hg" output.
+ *
+ * @task parse Parsing "hg" Output
+ * @group workingcopy
+ */
+final class ArcanistMercurialParser {
+
+
+/* -( Parsing "hg" Output )------------------------------------------------ */
+
+
+ /**
+ * Parse the output of "hg status".
+ *
+ * @param string The stdout from running an "hg status" command.
+ * @return dict Map of paths to ArcanistRepositoryAPI status flags.
+ * @task parse
+ */
+ public static function parseMercurialStatus($stdout) {
+ $result = array();
+
+ $stdout = trim($stdout);
+ if (!strlen($stdout)) {
+ return $result;
+ }
+
+ $lines = explode("\n", $stdout);
+ foreach ($lines as $line) {
+ $flags = 0;
+ list($code, $path) = explode(' ', $line, 2);
+ switch ($code) {
+ case 'A':
+ $flags |= ArcanistRepositoryAPI::FLAG_ADDED;
+ break;
+ case 'R':
+ $flags |= ArcanistRepositoryAPI::FLAG_REMOVED;
+ break;
+ case 'M':
+ $flags |= ArcanistRepositoryAPI::FLAG_MODIFIED;
+ break;
+ case 'C':
+ // This is "clean" and included only for completeness, these files
+ // have not been changed.
+ break;
+ case '!':
+ $flags |= ArcanistRepositoryAPI::FLAG_MISSING;
+ break;
+ case '?':
+ $flags |= ArcanistRepositoryAPI::FLAG_UNTRACKED;
+ break;
+ case 'I':
+ // This is "ignored" and included only for completeness.
+ break;
+ default:
+ throw new Exception("Unknown Mercurial status '{$code}'.");
+ }
+
+ $result[$path] = $flags;
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * Parse the output of "hg log". This also parses "hg outgoing", "hg parents",
+ * and other similar commands. This assumes "--style default".
+ *
+ * @param string The stdout from running an "hg log" command.
+ * @return list List of dictionaries with commit information.
+ * @task parse
+ */
+ public static function parseMercurialLog($stdout) {
+ $result = array();
+
+ $stdout = trim($stdout);
+ if (!strlen($stdout)) {
+ return $result;
+ }
+
+ $chunks = explode("\n\n", $stdout);
+ foreach ($chunks as $chunk) {
+ $commit = array();
+ $lines = explode("\n", $chunk);
+ foreach ($lines as $line) {
+ if (preg_match('/^(comparing with|searching for changes)/', $line)) {
+ // These are sent to stdout when you run "hg outgoing" although the
+ // format is otherwise identical to "hg log".
+ continue;
+ }
+ list($name, $value) = explode(':', $line, 2);
+ $value = trim($value);
+ switch ($name) {
+ case 'user':
+ $commit['user'] = $value;
+ break;
+ case 'date':
+ $commit['date'] = strtotime($value);
+ break;
+ case 'summary':
+ $commit['summary'] = $value;
+ break;
+ case 'changeset':
+ list($local, $rev) = explode(':', $value, 2);
+ $commit['local'] = $local;
+ $commit['rev'] = $rev;
+ break;
+ case 'parent':
+ if (empty($commit['parents'])) {
+ $commit['parents'] = array();
+ }
+ list($local, $rev) = explode(':', $value, 2);
+ $commit['parents'][] = array(
+ 'local' => $local,
+ 'rev' => $rev,
+ );
+ break;
+ case 'branch':
+ $commit['branch'] = $value;
+ break;
+ case 'tag':
+ $commit['tag'] = $value;
+ break;
+ default:
+ throw new Exception("Unknown Mercurial log field '{$name}'!");
+ }
+ }
+ $result[] = $commit;
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * Parse the output of "hg branches".
+ *
+ * @param string The stdout from running an "hg branches" command.
+ * @return list A list of dictionaries with branch information.
+ * @task parse
+ */
+ public static function parseMercurialBranches($stdout) {
+ $lines = explode("\n", trim($stdout));
+
+ $branches = array();
+ foreach ($lines as $line) {
+ $matches = null;
+
+ // Output of "hg branches" normally looks like:
+ //
+ // default 15101:a21ccf4412d5
+ //
+ // ...but may also have human-readable cues like:
+ //
+ // stable 15095:ec222a29bdf0 (inactive)
+ //
+ // See the unit tests for more examples.
+ $regexp = '/^([^ ]+)\s+(\d+):([a-f0-9]+)(\s|$)/';
+
+ if (!preg_match($regexp, $line, $matches)) {
+ throw new Exception("Failed to parse 'hg branches' output: {$line}");
+ }
+ $branches[$matches[1]] = array(
+ 'local' => $matches[2],
+ 'rev' => $matches[3],
+ );
+ }
+
+ return $branches;
+ }
+
+}
diff --git a/src/repository/parser/mercurial/__init__.php b/src/repository/parser/mercurial/__init__.php
new file mode 100644
index 00000000..9de7b75e
--- /dev/null
+++ b/src/repository/parser/mercurial/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('arcanist', 'repository/api/base');
+
+
+phutil_require_source('ArcanistMercurialParser.php');
diff --git a/src/repository/parser/mercurial/__tests__/ArcanistMercurialParserTestCase.php b/src/repository/parser/mercurial/__tests__/ArcanistMercurialParserTestCase.php
new file mode 100644
index 00000000..939efdad
--- /dev/null
+++ b/src/repository/parser/mercurial/__tests__/ArcanistMercurialParserTestCase.php
@@ -0,0 +1,71 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+final class ArcanistMercurialParserTestCase extends ArcanistPhutilTestCase {
+
+ public function testParseAll() {
+ $root = dirname(__FILE__).'/data/';
+ foreach (Filesystem::listDirectory($root) as $file) {
+ $this->parseData(
+ basename($file),
+ Filesystem::readFile($root.'/'.$file));
+ }
+ }
+
+ private function parseData($name, $data) {
+ switch ($name) {
+ case 'branches-basic.txt':
+ $output = ArcanistMercurialParser::parseMercurialBranches($data);
+ $this->assertEqual(
+ array('default', 'stable'),
+ array_keys($output));
+ $this->assertEqual(
+ array('a21ccf4412d5', 'ec222a29bdf0'),
+ array_values(ipull($output, 'rev')));
+ break;
+ case 'log-basic.txt':
+ $output = ArcanistMercurialParser::parseMercurialLog($data);
+ $this->assertEqual(
+ 3,
+ count($output));
+ $this->assertEqual(
+ array('a21ccf4412d5', 'a051f8a6a7cc', 'b1f49efeab65'),
+ array_values(ipull($output, 'rev')));
+ break;
+ case 'log-empty.txt':
+ // Empty logs (e.g., "hg parents" for a root revision) should parse
+ // correctly.
+ $output = ArcanistMercurialParser::parseMercurialLog($data);
+ $this->assertEqual(
+ array(),
+ $output);
+ break;
+ case 'status-basic.txt':
+ $output = ArcanistMercurialParser::parseMercurialStatus($data);
+ $this->assertEqual(
+ 4,
+ count($output));
+ $this->assertEqual(
+ array('changed', 'added', 'removed', 'untracked'),
+ array_keys($output));
+ break;
+ default:
+ throw new Exception("No test information for test data '{$name}'!");
+ }
+ }
+}
diff --git a/src/repository/parser/mercurial/__tests__/__init__.php b/src/repository/parser/mercurial/__tests__/__init__.php
new file mode 100644
index 00000000..254c2aba
--- /dev/null
+++ b/src/repository/parser/mercurial/__tests__/__init__.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('arcanist', 'repository/parser/mercurial');
+phutil_require_module('arcanist', 'unit/engine/phutil/testcase');
+
+phutil_require_module('phutil', 'filesystem');
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('ArcanistMercurialParserTestCase.php');
diff --git a/src/repository/parser/mercurial/__tests__/data/branches-basic.txt b/src/repository/parser/mercurial/__tests__/data/branches-basic.txt
new file mode 100644
index 00000000..706ac29c
--- /dev/null
+++ b/src/repository/parser/mercurial/__tests__/data/branches-basic.txt
@@ -0,0 +1,2 @@
+default 15101:a21ccf4412d5
+stable 15095:ec222a29bdf0 (inactive)
diff --git a/src/repository/parser/mercurial/__tests__/data/log-basic.txt b/src/repository/parser/mercurial/__tests__/data/log-basic.txt
new file mode 100644
index 00000000..4997f328
--- /dev/null
+++ b/src/repository/parser/mercurial/__tests__/data/log-basic.txt
@@ -0,0 +1,16 @@
+changeset: 15101:a21ccf4412d5
+tag: tip
+user: Greg Ward <greg@gerg.ca>
+date: Wed Sep 14 22:28:27 2011 -0400
+summary: share: allow trailing newline on .hg/sharedpath.
+
+changeset: 15100:a051f8a6a7cc
+user: Ben Hockey <neonstalwart@gmail.com>
+date: Wed Sep 07 10:24:26 2011 -0400
+summary: contrib: some support for named branches in zsh_completion (issue2988)
+
+changeset: 15099:b1f49efeab65
+user: Simon Heimberg <simohe@besonet.ch>
+date: Wed Sep 14 17:06:33 2011 +0200
+summary: test: test for options duplicate with global options
+
diff --git a/src/repository/parser/mercurial/__tests__/data/log-empty.txt b/src/repository/parser/mercurial/__tests__/data/log-empty.txt
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/src/repository/parser/mercurial/__tests__/data/log-empty.txt
@@ -0,0 +1 @@
+
diff --git a/src/repository/parser/mercurial/__tests__/data/status-basic.txt b/src/repository/parser/mercurial/__tests__/data/status-basic.txt
new file mode 100644
index 00000000..c49407f2
--- /dev/null
+++ b/src/repository/parser/mercurial/__tests__/data/status-basic.txt
@@ -0,0 +1,4 @@
+M changed
+A added
+! removed
+? untracked
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Oct 11, 10:37 (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
984220
Default Alt Text
(33 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment