1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-29 02:02:40 +01:00

Split mercurial parsing and API

Summary: Move code to actually parse "hg" output into a separate class with some
tests, so I can reuse it in the import scripts. We should probably do this for
Git/SVN at some point, too.

Test Plan: Ran unit tests, used this class in Phabricator importers, grepped for
calls to removed private methods.

Reviewers: Makinde, jungejason, nh, tuomaspelkonen, aran

Reviewed By: Makinde

CC: aran, Makinde

Differential Revision: 942
This commit is contained in:
epriestley 2011-09-16 03:53:52 -07:00
parent c84d6255b4
commit cbbd798e48
11 changed files with 322 additions and 105 deletions

View file

@ -57,6 +57,8 @@ phutil_register_library_map(array(
'ArcanistListWorkflow' => 'workflow/list', 'ArcanistListWorkflow' => 'workflow/list',
'ArcanistMarkCommittedWorkflow' => 'workflow/mark-committed', 'ArcanistMarkCommittedWorkflow' => 'workflow/mark-committed',
'ArcanistMercurialAPI' => 'repository/api/mercurial', 'ArcanistMercurialAPI' => 'repository/api/mercurial',
'ArcanistMercurialParser' => 'repository/parser/mercurial',
'ArcanistMercurialParserTestCase' => 'repository/parser/mercurial/__tests__',
'ArcanistMergeWorkflow' => 'workflow/merge', 'ArcanistMergeWorkflow' => 'workflow/merge',
'ArcanistNoEffectException' => 'exception/usage/noeffect', 'ArcanistNoEffectException' => 'exception/usage/noeffect',
'ArcanistNoEngineException' => 'exception/usage/noengine', 'ArcanistNoEngineException' => 'exception/usage/noengine',
@ -123,6 +125,7 @@ phutil_register_library_map(array(
'ArcanistListWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistListWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI', 'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
'ArcanistMercurialParserTestCase' => 'ArcanistPhutilTestCase',
'ArcanistMergeWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistMergeWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistNoEffectException' => 'ArcanistUsageException', 'ArcanistNoEffectException' => 'ArcanistUsageException',
'ArcanistNoEngineException' => 'ArcanistUsageException', 'ArcanistNoEngineException' => 'ArcanistUsageException',

View file

@ -71,7 +71,7 @@ class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
list($stdout) = execx( list($stdout) = execx(
'(cd %s && hg outgoing --branch `hg branch` --limit 1 --style default)', '(cd %s && hg outgoing --branch `hg branch` --limit 1 --style default)',
$this->getPath()); $this->getPath());
$logs = $this->parseMercurialLog($stdout); $logs = ArcanistMercurialParser::parseMercurialLog($stdout);
if (!count($logs)) { if (!count($logs)) {
throw new ArcanistUsageException("You have no outgoing changes!"); throw new ArcanistUsageException("You have no outgoing changes!");
} }
@ -88,7 +88,7 @@ class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
'(cd %s && hg parents --style default --rev %s)', '(cd %s && hg parents --style default --rev %s)',
$this->getPath(), $this->getPath(),
$oldest_rev); $oldest_rev);
$parents_logs = $this->parseMercurialLog($stdout); $parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout);
$first_parent = head($parents_logs); $first_parent = head($parents_logs);
if (!$first_parent) { if (!$first_parent) {
throw new ArcanistUsageException( throw new ArcanistUsageException(
@ -106,7 +106,7 @@ class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
$this->getPath(), $this->getPath(),
$this->getRelativeCommit(), $this->getRelativeCommit(),
$this->getWorkingCopyRevision()); $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" // Get rid of the first log, it's not actually part of the diff. "hg log"
// is inclusive, while "hg diff" is exclusive. // is inclusive, while "hg diff" is exclusive.
@ -182,7 +182,7 @@ class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
'(cd %s && hg status)', '(cd %s && hg status)',
$this->getPath()); $this->getPath());
$working_status = $this->parseMercurialStatus($stdout); $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
foreach ($working_status as $path => $status) { foreach ($working_status as $path => $status) {
$status |= self::FLAG_UNCOMMITTED; $status |= self::FLAG_UNCOMMITTED;
if (!empty($status_map[$path])) { if (!empty($status_map[$path])) {
@ -251,107 +251,6 @@ class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
return $stdout; 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() { private function getWorkingCopyRevision() {
// In Mercurial, "tip" means the tip of the current branch, not what's in // 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 // the working copy. The tip may be ahead of the working copy. We need to

View file

@ -10,6 +10,7 @@ phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'parser/diff'); phutil_require_module('arcanist', 'parser/diff');
phutil_require_module('arcanist', 'parser/diff/changetype'); phutil_require_module('arcanist', 'parser/diff/changetype');
phutil_require_module('arcanist', 'repository/api/base'); phutil_require_module('arcanist', 'repository/api/base');
phutil_require_module('arcanist', 'repository/parser/mercurial');
phutil_require_module('phutil', 'future/exec'); phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'utils'); phutil_require_module('phutil', 'utils');

View file

@ -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;
}
}

View file

@ -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');

View file

@ -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}'!");
}
}
}

View file

@ -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');

View file

@ -0,0 +1,2 @@
default 15101:a21ccf4412d5
stable 15095:ec222a29bdf0 (inactive)

View file

@ -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

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,4 @@
M changed
A added
! removed
? untracked