Page MenuHomeSealhub

No OneTemporary

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

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)

Event Timeline