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 @@ -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', 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 @@ -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 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 @@ -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'); 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 @@ + $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 @@ +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 @@ + +date: Wed Sep 14 22:28:27 2011 -0400 +summary: share: allow trailing newline on .hg/sharedpath. + +changeset: 15100:a051f8a6a7cc +user: Ben Hockey +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 +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