clidispatch: replace Dispatcher with CommandTable

Summary:
The `Dispatcher` provides lots of features but its internal state only contains
the command table. Replace it with `CommandTable` and make the methods free
functions.

This makes function dependencies more cleaner, for example things like "locating
a repo", "getting the args" etc. won't require a `Dispatcher`.

A side effect of this change is the non-utf8 command line arguments are no longer
supported. This is already broken since our hg wrapper (accidentally) enforced
utf-8 command line. Therefore related tests are removed.

Reviewed By: sfilipco

Differential Revision: D16796395

fbshipit-source-id: a793ce7b8befe7caf62405c582fc932eb3daa099
This commit is contained in:
Jun Wu 2019-08-21 12:08:01 -07:00 committed by Facebook Github Bot
parent 6aeaa40b40
commit 210bf49f5e
9 changed files with 287 additions and 462 deletions

View File

@ -1,6 +1,6 @@
// Copyright Facebook, Inc. 2018
use clidispatch::dispatch::Dispatcher;
use clidispatch::dispatch;
use hgcommands::{commands, HgPython};
mod buildinfo;
@ -9,8 +9,6 @@ mod chg;
#[cfg(feature = "with_chg")]
use chg::maybe_call_chg;
use commands::{create_dispatcher, dispatch};
use std::env;
#[cfg(windows)]
@ -20,10 +18,10 @@ use windows::disable_standard_handle_inheritability;
/// Execute a command, using an embedded interpreter
/// This function does not return
fn call_embedded_python(dispatcher: Dispatcher) {
fn call_embedded_python() {
let code = {
let hgpython = HgPython::new();
hgpython.run(dispatcher)
hgpython.run()
};
std::process::exit(code);
}
@ -47,10 +45,10 @@ fn main() {
disable_standard_handle_inheritability().unwrap();
let cwd = env::current_dir().unwrap();
let table = commands::table();
let args: Vec<String> = env::args().skip(1).collect();
let mut dispatcher = create_dispatcher();
match dispatch(&mut dispatcher) {
match dispatch::dispatch(&table, args) {
Ok(ret) => std::process::exit(ret as i32),
Err(_e) => {
// Change the current dir back to the original so it is not surprising to the Python
@ -60,7 +58,7 @@ fn main() {
#[cfg(feature = "with_chg")]
maybe_call_chg();
call_embedded_python(dispatcher);
call_embedded_python();
}
}
}

View File

@ -2,18 +2,17 @@
//
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2 or any later version.
use crate::command::{CommandDefinition, CommandFunc};
use crate::command::{CommandDefinition, CommandFunc, CommandTable};
use crate::errors::{DispatchError, HighLevelError};
use crate::global_flags::HG_GLOBAL_FLAGS;
use crate::io::IO;
use crate::repo::Repo;
use bytes::Bytes;
use cliparser::alias::{expand_aliases, expand_prefix};
use cliparser::parser::{ParseOptions, ParseOutput, StructFlags, Value};
use cliparser::parser::{ParseOptions, ParseOutput, Value};
use configparser::config::ConfigSet;
use configparser::hg::{parse_list, ConfigSetHgExt};
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::convert::TryInto;
use std::env;
use std::path::{Path, PathBuf};
@ -95,389 +94,305 @@ pub fn find_hg_repo_root(current_path: &Path) -> Option<&Path> {
}
}
pub fn args() -> Result<Vec<String>, DispatchError> {
let os_args = env::args_os();
let resultant: Result<Vec<String>, _> = os_args.skip(1).map(|os| os.into_string()).collect();
resultant.map_err(|_| DispatchError::InvalidCommandLineArguments)
}
pub struct Dispatcher {
commands: BTreeMap<String, CommandDefinition>,
}
impl Dispatcher {
pub fn new() -> Self {
Dispatcher {
commands: BTreeMap::new(),
}
}
fn add_command(&mut self, command: CommandDefinition) {
let name = command.name();
if !self.commands.contains_key(name) {
self.commands.insert(name.to_string(), command);
}
}
pub fn get_command_table(&self) -> Vec<&CommandDefinition> {
self.commands.values().collect()
}
fn find_command_name(&self, args: Vec<String>) -> Option<String> {
let mut command_name = None;
for arg in args {
if command_name.is_none() {
if self.commands.contains_key(&arg) {
command_name = Some(arg);
} else {
return None;
}
fn find_command_name(has_command: impl Fn(&str) -> bool, args: Vec<String>) -> Option<String> {
let mut command_name = None;
for arg in args {
if command_name.is_none() {
if has_command(&arg) {
command_name = Some(arg);
} else {
// To check for subcommands we continue iterating to see if a longer valid command
// is able to be created.
//
// $ hg cloud sync -> will become Some("cloud") then attempt "cloud sync".
let orig = command_name.unwrap();
let curr = orig.clone() + &arg;
if self.commands.contains_key(&curr) {
command_name = Some(curr.to_string())
} else {
return Some(orig);
}
return None;
}
} else {
// To check for subcommands we continue iterating to see if a longer valid command
// is able to be created.
//
// $ hg cloud sync -> will become Some("cloud") then attempt "cloud sync".
let orig = command_name.unwrap();
let curr = orig.clone() + &arg;
if has_command(&curr) {
command_name = Some(curr.to_string())
} else {
return Some(orig);
}
}
command_name
}
command_name
}
fn create_repo(repository_path: String) -> Result<Option<Repo>, DispatchError> {
if repository_path == "" {
let cwd = env::current_dir().unwrap();
let root = match find_hg_repo_root(&cwd) {
Some(r) => r,
None => return Ok(None),
};
return Ok(Some(Repo::new(root)));
} else if let Ok(repo_path) = Path::new(&repository_path).canonicalize() {
if repo_path.join(".hg").is_dir() {
return Ok(Some(Repo::new(repo_path)));
}
}
Err(DispatchError::RepoNotFound {
path: repository_path,
})
}
fn last_chance_to_abort(opts: &ParseOutput) -> Result<(), DispatchError> {
if opts.pick::<bool>("profile") {
return Err(DispatchError::ProfileFlagNotSupported);
}
if opts.pick::<bool>("help") {
return Err(DispatchError::HelpFlagNotSupported);
}
Ok(())
}
fn early_parse(&self, args: &Vec<String>) -> Result<ParseOutput, DispatchError> {
let parser = ParseOptions::new()
.ignore_prefix(true)
.early_parse(true)
.flags(HG_GLOBAL_FLAGS.clone())
.flag_alias("repo", "repository")
.into_parser();
parser
.parse_args(args)
.map_err(|_| DispatchError::EarlyParseFailed)
}
fn change_workdir(opts: &HashMap<String, Value>) -> Result<(), DispatchError> {
if let Some(cwd_val) = opts.get("cwd") {
let cwd: String = cwd_val.clone().into();
if cwd != "" {
env::set_current_dir(cwd).map_err(|_| DispatchError::EarlyParseFailed)?;
}
}
Ok(())
}
fn repo_from(opts: &HashMap<String, Value>) -> Result<Option<Repo>, DispatchError> {
match opts.get("repository") {
Some(repo_val) => {
let repo_path: String = repo_val.clone().try_into().unwrap();
Dispatcher::create_repo(repo_path)
}
_ => Err(DispatchError::ProgrammingError {
root_cause: "global flag repository should always be present in options"
.to_string(),
}),
fn create_repo(repository_path: String) -> Result<Option<Repo>, DispatchError> {
if repository_path == "" {
let cwd = env::current_dir().unwrap();
let root = match find_hg_repo_root(&cwd) {
Some(r) => r,
None => return Ok(None),
};
return Ok(Some(Repo::new(root)));
} else if let Ok(repo_path) = Path::new(&repository_path).canonicalize() {
if repo_path.join(".hg").is_dir() {
return Ok(Some(Repo::new(repo_path)));
}
}
Err(DispatchError::RepoNotFound {
path: repository_path,
})
}
fn configs(opts: &HashMap<String, Value>) -> Vec<String> {
opts.get("config")
.map(|c| c.clone().try_into().unwrap_or(Vec::new()))
.unwrap_or(Vec::new())
fn last_chance_to_abort(opts: &ParseOutput) -> Result<(), DispatchError> {
if opts.pick::<bool>("profile") {
return Err(DispatchError::ProfileFlagNotSupported);
}
fn configfiles(opts: &HashMap<String, Value>) -> Vec<PathBuf> {
opts.get("configfile")
.map(|c| c.clone().try_into().unwrap_or(Vec::new()))
.unwrap_or(Vec::new())
.into_iter()
.map(|s| PathBuf::from(s))
.collect()
if opts.pick::<bool>("help") {
return Err(DispatchError::HelpFlagNotSupported);
}
fn command_map(&self, cfg: &ConfigSet) -> BTreeMap<String, isize> {
let mut command_map = BTreeMap::new();
let mut i = 1;
Ok(())
}
for command in (&self.commands).values() {
let name = command.name();
fn early_parse(args: &Vec<String>) -> Result<ParseOutput, DispatchError> {
ParseOptions::new()
.ignore_prefix(true)
.early_parse(true)
.flags(HG_GLOBAL_FLAGS.clone())
.flag_alias("repo", "repository")
.parse_args(args)
.map_err(|_| DispatchError::EarlyParseFailed)
}
fn change_workdir(opts: &HashMap<String, Value>) -> Result<(), DispatchError> {
if let Some(cwd_val) = opts.get("cwd") {
let cwd: String = cwd_val.clone().into();
if cwd != "" {
env::set_current_dir(cwd).map_err(|_| DispatchError::EarlyParseFailed)?;
}
}
Ok(())
}
fn repo_from(opts: &HashMap<String, Value>) -> Result<Option<Repo>, DispatchError> {
match opts.get("repository") {
Some(repo_val) => {
let repo_path: String = repo_val.clone().try_into().unwrap();
create_repo(repo_path)
}
_ => Err(DispatchError::ProgrammingError {
root_cause: "global flag repository should always be present in options".to_string(),
}),
}
}
fn configs(opts: &HashMap<String, Value>) -> Vec<String> {
opts.get("config")
.map(|c| c.clone().try_into().unwrap_or(Vec::new()))
.unwrap_or(Vec::new())
}
fn configfiles(opts: &HashMap<String, Value>) -> Vec<PathBuf> {
opts.get("configfile")
.map(|c| c.clone().try_into().unwrap_or(Vec::new()))
.unwrap_or(Vec::new())
.into_iter()
.map(|s| PathBuf::from(s))
.collect()
}
fn command_map<'a>(
definitions: impl IntoIterator<Item = &'a CommandDefinition>,
cfg: &ConfigSet,
) -> BTreeMap<String, isize> {
let mut command_map = BTreeMap::new();
let mut i = 1;
for command in definitions {
let name = command.name();
let is_debug = name.starts_with("debug");
command_map.insert(name.to_string(), if is_debug { -i } else { i });
i = i + 1;
}
// adding aliases into the command map is what Python does, so copying this behavior
// allows alias expansion to not behave differently for Rust or Python.
for name in cfg.keys("alias") {
if let Ok(name) = String::from_utf8(name.to_vec()) {
let is_debug = name.starts_with("debug");
command_map.insert(name.to_string(), if is_debug { -i } else { i });
command_map.insert(name, if is_debug { -i } else { i });
i = i + 1;
}
// adding aliases into the command map is what Python does, so copying this behavior
// allows alias expansion to not behave differently for Rust or Python.
for name in cfg.keys("alias") {
if let Ok(name) = String::from_utf8(name.to_vec()) {
let is_debug = name.starts_with("debug");
command_map.insert(name, if is_debug { -i } else { i });
i = i + 1;
}
}
}
// Names from `commands.name` config.
// This is a fast (but inaccurate) way to know Python command names.
let config_commands = parse_list(cfg.get("commands", "names").unwrap_or_default());
for b_name in config_commands {
if let Ok(name) = String::from_utf8(b_name.to_vec()) {
let is_debug = name.starts_with("debug");
for name in name.split("|") {
command_map.insert(name.to_string(), if is_debug { -i } else { i });
// Names from `commands.name` config.
// This is a fast (but inaccurate) way to know Python command names.
let config_commands = parse_list(cfg.get("commands", "names").unwrap_or_default());
for b_name in config_commands {
if let Ok(name) = String::from_utf8(b_name.to_vec()) {
let is_debug = name.starts_with("debug");
for name in name.split("|") {
command_map.insert(name.to_string(), if is_debug { -i } else { i });
}
i = i + 1;
}
}
command_map
}
fn parse(definition: &CommandDefinition, args: &Vec<String>) -> Result<ParseOutput, DispatchError> {
let flags = definition
.flags()
.iter()
.chain(HG_GLOBAL_FLAGS.iter())
.cloned()
.collect();
ParseOptions::new()
.error_on_unknown_opts(true)
.flags(flags)
.flag_alias("repo", "repository")
.parse_args(args)
.map_err(|_| DispatchError::ParseFailed)
}
pub fn dispatch(command_table: &CommandTable, args: Vec<String>) -> Result<u8, DispatchError> {
let mut io = IO::stdio();
match _dispatch(command_table, args, &mut io) {
Ok(ret) => {
return Ok(ret);
}
Err(err) => {
let high_level: HighLevelError = err.into();
match high_level {
HighLevelError::UnsupportedError { cause } => {
return Err(cause);
}
i = i + 1;
}
}
command_map
}
fn parse(
&self,
args: &Vec<String>,
command_name: &String,
) -> Result<ParseOutput, DispatchError> {
let mut command_flags = self
.commands
.get(command_name)
.map(|command| command.flags().clone())
.unwrap_or_default();
command_flags.extend(HG_GLOBAL_FLAGS.clone());
let parser = ParseOptions::new()
.error_on_unknown_opts(true)
.flags(command_flags)
.flag_alias("repo", "repository")
.into_parser();
parser
.parse_args(args)
.map_err(|_| DispatchError::ParseFailed)
}
pub fn dispatch(&mut self, args: Vec<String>) -> Result<u8, DispatchError> {
let mut io = IO::stdio();
match self._dispatch(args, &mut io) {
Ok(ret) => {
return Ok(ret);
}
Err(err) => {
let high_level: HighLevelError = err.into();
match high_level {
HighLevelError::UnsupportedError { cause } => {
return Err(cause);
}
HighLevelError::SupportedError { cause } => {
let msg = format!("{}\n", cause);
io.write(msg)?;
return Ok(255);
}
HighLevelError::SupportedError { cause } => {
let msg = format!("{}\n", cause);
io.write(msg)?;
return Ok(255);
}
}
}
}
}
pub fn _dispatch(&mut self, mut args: Vec<String>, io: &mut IO) -> Result<u8, DispatchError> {
let early_result = self.early_parse(&args)?;
pub fn _dispatch(
command_table: &CommandTable,
mut args: Vec<String>,
io: &mut IO,
) -> Result<u8, DispatchError> {
let early_result = early_parse(&args)?;
let early_opts = early_result.opts();
let early_opts = early_result.opts();
Dispatcher::change_workdir(&early_opts)?;
change_workdir(&early_opts)?;
let repo_res = Dispatcher::repo_from(&early_opts);
let repo_res = repo_from(&early_opts);
let (repo, repo_err) = match repo_res {
Ok(opt) => (opt, Ok(())),
Err(err) => (None, Err(err)),
};
let (repo, repo_err) = match repo_res {
Ok(opt) => (opt, Ok(())),
Err(err) => (None, Err(err)),
};
let config_set = load_config()?;
let config_set = load_config()?;
let opt_path = repo.as_ref().map(|r| r.path());
let opt_path = repo.as_ref().map(|r| r.path());
let config_set = load_repo_config(config_set, opt_path);
let config_set = load_repo_config(config_set, opt_path);
let configs = Dispatcher::configs(&early_opts);
let configs = configs(&early_opts);
let configfiles = Dispatcher::configfiles(&early_opts);
let configfiles = configfiles(&early_opts);
let config_set = override_config(config_set, &configfiles[..], &configs)?;
let config_set = override_config(config_set, &configfiles[..], &configs)?;
let alias_lookup = |name: &str| match (
config_set.get("alias", name),
config_set.get("defaults", name),
) {
(None, None) => None,
(Some(v), None) => String::from_utf8(v.to_vec()).ok(),
(None, Some(v)) => String::from_utf8(v.to_vec())
.ok()
.map(|v| format!("{} {}", name, v)),
(Some(a), Some(d)) => {
if let (Ok(a), Ok(d)) =
(String::from_utf8(a.to_vec()), String::from_utf8(d.to_vec()))
{
// XXX: This makes defaults override alias if there are conflicted
// flags. The desired behavior is to make alias override defaults.
// However, [defaults] is deprecated and is likely only used
// by tests. So this might be fine.
Some(format!("{} {}", a, d))
} else {
None
}
let alias_lookup = |name: &str| match (
config_set.get("alias", name),
config_set.get("defaults", name),
) {
(None, None) => None,
(Some(v), None) => String::from_utf8(v.to_vec()).ok(),
(None, Some(v)) => String::from_utf8(v.to_vec())
.ok()
.map(|v| format!("{} {}", name, v)),
(Some(a), Some(d)) => {
if let (Ok(a), Ok(d)) = (String::from_utf8(a.to_vec()), String::from_utf8(d.to_vec())) {
// XXX: This makes defaults override alias if there are conflicted
// flags. The desired behavior is to make alias override defaults.
// However, [defaults] is deprecated and is likely only used
// by tests. So this might be fine.
Some(format!("{} {}", a, d))
} else {
None
}
};
let command_map = self.command_map(&config_set);
let early_args = early_result.args();
let first_arg = early_args
.get(0)
.ok_or_else(|| DispatchError::NoCommandFound)?;
let replace = early_result.first_arg_index();
// This should hold true since `first_arg` is not empty (tested above).
// Therefore positional args is non-empty and first_arg_index should be
// an index in args.
debug_assert!(replace < args.len());
debug_assert_eq!(&args[replace], first_arg);
// FIXME: DispatchError::AliasExpansionFailed should contain information about
// ambiguous commands.
let command_name = expand_prefix(&command_map, first_arg)
.map_err(|_| DispatchError::AliasExpansionFailed)?;
args[replace] = command_name;
let (expanded, _replaced) = expand_aliases(alias_lookup, &args[replace..])
.map_err(|_| DispatchError::AliasExpansionFailed)?;
let mut new_args = Vec::new();
new_args.extend_from_slice(&args[..replace]);
new_args.extend_from_slice(&expanded[..]);
let command_name = self
.find_command_name(expanded)
.ok_or_else(|| DispatchError::NoCommandFound)?;
repo_err?;
let full_args = new_args;
let result = self.parse(&full_args, &command_name)?;
Dispatcher::last_chance_to_abort(&result)?;
let handler = self.commands.get(&command_name).unwrap().func();
match handler {
CommandFunc::Repo(f) => {
let mut r = repo.ok_or_else(|| DispatchError::RepoRequired {
cwd: env::current_dir()
.ok()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or("".to_string()),
})?;
r.set_config(config_set);
f(result, io, r)
}
CommandFunc::InferRepo(f) => {
let r = match repo {
Some(mut re) => {
re.set_config(config_set);
Some(re)
}
None => None,
};
f(result, io, r)
}
CommandFunc::NoRepo(f) => f(result, io),
}
}
}
};
pub trait Register<FN, T> {
fn register(&mut self, f: FN, name: &str, doc: &str);
}
let command_map = command_map(command_table.values(), &config_set);
// No Repo
impl<S, FN> Register<FN, (S,)> for Dispatcher
where
S: From<ParseOutput> + StructFlags,
FN: Fn(S, &mut IO) -> Result<u8, DispatchError> + 'static,
{
fn register(&mut self, f: FN, name: &str, doc: &str) {
let func = move |opts: ParseOutput, io: &mut IO| f(opts.into(), io);
let func = CommandFunc::NoRepo(Box::new(func));
let def = CommandDefinition::new(name, doc, S::flags(), func);
self.add_command(def);
}
}
let early_args = early_result.args();
// Infer Repo
impl<S, FN> Register<FN, ((), S)> for Dispatcher
where
S: From<ParseOutput> + StructFlags,
FN: Fn(S, &mut IO, Option<Repo>) -> Result<u8, DispatchError> + 'static,
{
fn register(&mut self, f: FN, name: &str, doc: &str) {
let func =
move |opts: ParseOutput, io: &mut IO, repo: Option<Repo>| f(opts.into(), io, repo);
let func = CommandFunc::InferRepo(Box::new(func));
let def = CommandDefinition::new(name, doc, S::flags(), func);
self.add_command(def);
}
}
let first_arg = early_args
.get(0)
.ok_or_else(|| DispatchError::NoCommandFound)?;
// Repo
impl<S, FN> Register<FN, ((), (), S)> for Dispatcher
where
S: From<ParseOutput> + StructFlags,
FN: Fn(S, &mut IO, Repo) -> Result<u8, DispatchError> + 'static,
{
fn register(&mut self, f: FN, name: &str, doc: &str) {
let func = move |opts: ParseOutput, io: &mut IO, repo: Repo| f(opts.into(), io, repo);
let func = CommandFunc::Repo(Box::new(func));
let def = CommandDefinition::new(name, doc, S::flags(), func);
self.add_command(def);
let replace = early_result.first_arg_index();
// This should hold true since `first_arg` is not empty (tested above).
// Therefore positional args is non-empty and first_arg_index should be
// an index in args.
debug_assert!(replace < args.len());
debug_assert_eq!(&args[replace], first_arg);
// FIXME: DispatchError::AliasExpansionFailed should contain information about
// ambiguous commands.
let command_name =
expand_prefix(&command_map, first_arg).map_err(|_| DispatchError::AliasExpansionFailed)?;
args[replace] = command_name;
let (expanded, _replaced) = expand_aliases(alias_lookup, &args[replace..])
.map_err(|_| DispatchError::AliasExpansionFailed)?;
let mut new_args = Vec::new();
new_args.extend_from_slice(&args[..replace]);
new_args.extend_from_slice(&expanded[..]);
let command_name = find_command_name(|name| command_table.contains_key(name), expanded)
.ok_or_else(|| DispatchError::NoCommandFound)?;
repo_err?;
let full_args = new_args;
let def = &command_table[&command_name];
let result = parse(&def, &full_args)?;
last_chance_to_abort(&result)?;
let handler = def.func();
match handler {
CommandFunc::Repo(f) => {
let mut r = repo.ok_or_else(|| DispatchError::RepoRequired {
cwd: env::current_dir()
.ok()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or("".to_string()),
})?;
r.set_config(config_set);
f(result, io, r)
}
CommandFunc::InferRepo(f) => {
let r = match repo {
Some(mut re) => {
re.set_config(config_set);
Some(re)
}
None => None,
};
f(result, io, r)
}
CommandFunc::NoRepo(f) => f(result, io),
}
}

View File

@ -1,6 +1,6 @@
// Copyright Facebook, Inc. 2019
use clidispatch::dispatch::*;
use clidispatch::command::{CommandTable, Register};
use clidispatch::errors::DispatchError;
use clidispatch::io::IO;
use clidispatch::repo::Repo;
@ -10,9 +10,10 @@ use revisionstore::{DataPackStore, DataStore, IndexedLogDataStore, UnionDataStor
use types::{Key, Node, RepoPathBuf};
#[allow(dead_code)]
pub fn create_dispatcher() -> Dispatcher {
let mut dispatcher = Dispatcher::new();
dispatcher.register(
/// Return the main command table including all Rust commands.
pub fn table() -> CommandTable {
let mut table = CommandTable::new();
table.register(
root,
"root",
r#"print the root (top) of the current working directory
@ -21,20 +22,13 @@ pub fn create_dispatcher() -> Dispatcher {
Returns 0 on success."#,
);
dispatcher.register(
table.register(
debugstore,
"debugstore",
"print information about blobstore",
);
dispatcher
}
#[allow(dead_code)]
pub fn dispatch(dispatcher: &mut Dispatcher) -> Result<u8, DispatchError> {
let args = args()?;
dispatcher.dispatch(args)
table
}
define_flags! {

View File

@ -1,8 +1,8 @@
// Copyright Facebook, Inc. 2018
use crate::commands;
use crate::python::{
py_finalize, py_init_threads, py_initialize, py_set_argv, py_set_program_name,
};
use clidispatch::dispatch::Dispatcher;
use cpython::{exc, NoArgs, ObjectProtocol, PyDict, PyResult, Python, PythonObject};
use cpython_ext::Bytes;
use encoding::osstring_to_local_cstring;
@ -49,32 +49,31 @@ impl HgPython {
.collect()
}
pub fn run_py(&self, py: Python<'_>, dispatcher: Dispatcher) -> PyResult<()> {
self.set_command_table(py, dispatcher)?;
fn run_py(&self, py: Python<'_>) -> PyResult<()> {
self.update_python_command_table(py)?;
let entry_point_mod = py.import(HGPYENTRYPOINT_MOD)?;
entry_point_mod.call(py, "run", NoArgs, None)?;
Ok(())
}
/// Update the Python command table so it knows commands implemented in Rust.
pub fn set_command_table(&self, py: Python<'_>, dispatcher: Dispatcher) -> PyResult<()> {
fn update_python_command_table(&self, py: Python<'_>) -> PyResult<()> {
let table = commands::table();
let table_mod = py.import("edenscm.mercurial.commands")?;
let table: PyDict = table_mod.get(py, "table")?.extract::<PyDict>(py)?;
let py_table: PyDict = table_mod.get(py, "table")?.extract::<PyDict>(py)?;
let rust_commands = dispatcher.get_command_table();
for command in rust_commands {
let doc = Bytes::from(command.doc().to_string());
table.set_item(py, command.name(), (doc, command.flags()))?;
for def in table.values() {
let doc = Bytes::from(def.doc().to_string());
py_table.set_item(py, def.name(), (doc, def.flags()))?;
}
Ok(())
}
pub fn run(&self, dispatcher: Dispatcher) -> i32 {
pub fn run(&self) -> i32 {
let gil = Python::acquire_gil();
let py = gil.python();
match self.run_py(py, dispatcher) {
match self.run_py(py) {
// The code below considers the following exit scenarios:
// - `PyResult` is `Ok`. This means that the Python code returned
// successfully, without calling `sys.exit` or raising an

View File

@ -4181,19 +4181,10 @@ sh % "'HGENCODING=ascii' hg log -T '{desc|json}\\n' -r0" == '"non-ascii branch:
# json filter takes input as utf-8b:
sh % ("'HGENCODING=ascii' hg log -T '{'\\''%s'\\''|json}\\n' -l1" % utf8) == '"\\u00e9"'
sh % (
"'HGENCODING=ascii' hg log -T '{'\\''%s'\\''|json}\\n' -l1" % latin1
) == "abort: cannot decode command line arguments\n[255]"
# utf8 filter:
sh % "'HGENCODING=ascii' hg log -T 'round-trip: {bookmarks % '\\''{bookmark|utf8|hex}'\\''}\\n' -r0" == "round-trip: c3a9"
sh % (
"'HGENCODING=latin1' hg log -T 'decoded: {'\\''%s'\\''|utf8|hex}\\n' -l1" % latin1
) == "abort: cannot decode command line arguments\n[255]"
sh % (
"'HGENCODING=ascii' hg log -T 'replaced: {'\\''%s'\\''|utf8|hex}\\n' -l1" % latin1
) == "abort: cannot decode command line arguments\n[255]"
sh % "hg log -T 'invalid type: {rev|utf8}\\n' -r0" == r"""
abort: template filter 'utf8' is not compatible with keyword 'rev'
[255]"""

View File

@ -39,9 +39,6 @@ these should work
$ HGENCODING=latin-1 hg ci -l latin-1
$ echo "utf-8" > a
$ HGENCODING=utf-8 hg ci -l utf-8
$ HGENCODING=latin-1 hg tag `cat latin-1-tag`
abort: cannot decode command line arguments
[255]
hg log (ascii)
@ -179,12 +176,6 @@ hg log (dolphin)
$ cp latin-1-tag .hg/branch
$ HGENCODING=latin-1 hg ci -m 'auto-promote legacy name'
Test roundtrip encoding of lookup tables when not using UTF-8 (issue2763)
$ HGENCODING=latin-1 hg up `cat latin-1-tag`
abort: cannot decode command line arguments
[255]
$ cd ..
Test roundtrip encoding/decoding of utf8b for generated data

View File

@ -411,12 +411,3 @@ Test handling of non-ASCII paths in generated docstrings (issue5301)
[255]
$ HGPLAIN=1 hg --config hgext.extdiff= --config extdiff.cmd.td=hi help td > /dev/null
$ LC_MESSAGES=ja_JP.UTF-8 hg --config hgext.extdiff= --config extdiff.cmd.td=$U help -k xyzzy
abort: cannot decode command line arguments
[255]
$ LC_MESSAGES=ja_JP.UTF-8 hg --config hgext.extdiff= --config extdiff.cmd.td=$U help td \
> | grep "^ '"
abort: cannot decode command line arguments
[1]

View File

@ -1566,30 +1566,6 @@ such str.lower().
> """ % (escape(upper), escape(lower)))
> EOF
$ cat >> $HGRCPATH <<EOF
> [extensions]
> ambiguous = ./ambiguous.py
> EOF
$ $PYTHON <<EOF | sh
> upper = "\x8bL\x98^"
> print("hg --encoding cp932 help -e ambiguous.%s" % upper)
> EOF
abort: cannot decode command line arguments
[255]
$ $PYTHON <<EOF | sh
> lower = "\x8bl\x98^"
> print("hg --encoding cp932 help -e ambiguous.%s" % lower)
> EOF
abort: cannot decode command line arguments
[255]
$ cat >> $HGRCPATH <<EOF
> [extensions]
> ambiguous = !
> EOF
Show help content of disabled extensions
$ cat >> $HGRCPATH <<EOF

View File

@ -1566,36 +1566,6 @@ test author/desc/keyword in problematic encoding
> EOF
$ sh < setup.sh
test in problematic encoding
$ $PYTHON > test.sh <<EOF
> print u'''
> hg --encoding cp932 log --template '{rev}\\n' -r 'author(\u30A2)'
> echo ====
> hg --encoding cp932 log --template '{rev}\\n' -r 'author(\u30C2)'
> echo ====
> hg --encoding cp932 log --template '{rev}\\n' -r 'desc(\u30A2)'
> echo ====
> hg --encoding cp932 log --template '{rev}\\n' -r 'desc(\u30C2)'
> echo ====
> hg --encoding cp932 log --template '{rev}\\n' -r 'keyword(\u30A2)'
> echo ====
> hg --encoding cp932 log --template '{rev}\\n' -r 'keyword(\u30C2)'
> '''.encode('cp932')
> EOF
$ sh < test.sh
abort: cannot decode command line arguments
====
abort: cannot decode command line arguments
====
abort: cannot decode command line arguments
====
abort: cannot decode command line arguments
====
abort: cannot decode command line arguments
====
abort: cannot decode command line arguments
[255]
test error message of bad revset
$ hg log -r 'foo\\'
hg: parse error at 3: syntax error in revset 'foo\\'