mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-22 06:42:41 +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:
parent
c84d6255b4
commit
cbbd798e48
11 changed files with 322 additions and 105 deletions
|
@ -57,6 +57,8 @@ phutil_register_library_map(array(
|
|||
'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',
|
||||
|
@ -123,6 +125,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistListWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
|
||||
'ArcanistMercurialParserTestCase' => 'ArcanistPhutilTestCase',
|
||||
'ArcanistMergeWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistNoEffectException' => 'ArcanistUsageException',
|
||||
'ArcanistNoEngineException' => 'ArcanistUsageException',
|
||||
|
|
|
@ -71,7 +71,7 @@ class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
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!");
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
'(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(
|
||||
|
@ -106,7 +106,7 @@ class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
$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.
|
||||
|
@ -182,7 +182,7 @@ class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
'(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])) {
|
||||
|
@ -251,107 +251,6 @@ class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
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
|
||||
|
|
|
@ -10,6 +10,7 @@ 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');
|
||||
|
|
192
src/repository/parser/mercurial/ArcanistMercurialParser.php
Normal file
192
src/repository/parser/mercurial/ArcanistMercurialParser.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
12
src/repository/parser/mercurial/__init__.php
Normal file
12
src/repository/parser/mercurial/__init__.php
Normal 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');
|
|
@ -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}'!");
|
||||
}
|
||||
}
|
||||
}
|
16
src/repository/parser/mercurial/__tests__/__init__.php
Normal file
16
src/repository/parser/mercurial/__tests__/__init__.php
Normal 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');
|
|
@ -0,0 +1,2 @@
|
|||
default 15101:a21ccf4412d5
|
||||
stable 15095:ec222a29bdf0 (inactive)
|
16
src/repository/parser/mercurial/__tests__/data/log-basic.txt
Normal file
16
src/repository/parser/mercurial/__tests__/data/log-basic.txt
Normal 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
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
M changed
|
||||
A added
|
||||
! removed
|
||||
? untracked
|
Loading…
Reference in a new issue