##// END OF EJS Templates
rhg: refactor to pass argv down, instead of caling args_os()...
Arseniy Alekseyev -
r49961:137a9312 default
parent child Browse files
Show More
@@ -1,163 +1,172 b''
1 //! Logging for repository events, including commands run in the repository.
1 //! Logging for repository events, including commands run in the repository.
2
2
3 use crate::CliInvocation;
3 use crate::CliInvocation;
4 use format_bytes::format_bytes;
4 use format_bytes::format_bytes;
5 use hg::errors::HgError;
5 use hg::errors::HgError;
6 use hg::repo::Repo;
6 use hg::repo::Repo;
7 use hg::utils::{files::get_bytes_from_os_str, shell_quote};
7 use hg::utils::{files::get_bytes_from_os_str, shell_quote};
8 use std::ffi::OsString;
8
9
9 const ONE_MEBIBYTE: u64 = 1 << 20;
10 const ONE_MEBIBYTE: u64 = 1 << 20;
10
11
11 // TODO: somehow keep defaults in sync with `configitem` in `hgext/blackbox.py`
12 // TODO: somehow keep defaults in sync with `configitem` in `hgext/blackbox.py`
12 const DEFAULT_MAX_SIZE: u64 = ONE_MEBIBYTE;
13 const DEFAULT_MAX_SIZE: u64 = ONE_MEBIBYTE;
13 const DEFAULT_MAX_FILES: u32 = 7;
14 const DEFAULT_MAX_FILES: u32 = 7;
14
15
15 // Python does not support %.3f, only %f
16 // Python does not support %.3f, only %f
16 const DEFAULT_DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.3f";
17 const DEFAULT_DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.3f";
17
18
18 type DateTime = chrono::DateTime<chrono::Local>;
19 type DateTime = chrono::DateTime<chrono::Local>;
19
20
20 pub struct ProcessStartTime {
21 pub struct ProcessStartTime {
21 /// For measuring duration
22 /// For measuring duration
22 monotonic_clock: std::time::Instant,
23 monotonic_clock: std::time::Instant,
23 /// For formatting with year, month, day, etc.
24 /// For formatting with year, month, day, etc.
24 calendar_based: DateTime,
25 calendar_based: DateTime,
25 }
26 }
26
27
27 impl ProcessStartTime {
28 impl ProcessStartTime {
28 pub fn now() -> Self {
29 pub fn now() -> Self {
29 Self {
30 Self {
30 monotonic_clock: std::time::Instant::now(),
31 monotonic_clock: std::time::Instant::now(),
31 calendar_based: chrono::Local::now(),
32 calendar_based: chrono::Local::now(),
32 }
33 }
33 }
34 }
34 }
35 }
35
36
36 pub struct Blackbox<'a> {
37 pub struct Blackbox<'a> {
37 process_start_time: &'a ProcessStartTime,
38 process_start_time: &'a ProcessStartTime,
38 /// Do nothing if this is `None`
39 /// Do nothing if this is `None`
39 configured: Option<ConfiguredBlackbox<'a>>,
40 configured: Option<ConfiguredBlackbox<'a>>,
40 }
41 }
41
42
42 struct ConfiguredBlackbox<'a> {
43 struct ConfiguredBlackbox<'a> {
43 repo: &'a Repo,
44 repo: &'a Repo,
44 max_size: u64,
45 max_size: u64,
45 max_files: u32,
46 max_files: u32,
46 date_format: &'a str,
47 date_format: &'a str,
47 }
48 }
48
49
49 impl<'a> Blackbox<'a> {
50 impl<'a> Blackbox<'a> {
50 pub fn new(
51 pub fn new(
51 invocation: &'a CliInvocation<'a>,
52 invocation: &'a CliInvocation<'a>,
52 process_start_time: &'a ProcessStartTime,
53 process_start_time: &'a ProcessStartTime,
53 ) -> Result<Self, HgError> {
54 ) -> Result<Self, HgError> {
54 let configured = if let Ok(repo) = invocation.repo {
55 let configured = if let Ok(repo) = invocation.repo {
55 if invocation.config.get(b"extensions", b"blackbox").is_none() {
56 if invocation.config.get(b"extensions", b"blackbox").is_none() {
56 // The extension is not enabled
57 // The extension is not enabled
57 None
58 None
58 } else {
59 } else {
59 Some(ConfiguredBlackbox {
60 Some(ConfiguredBlackbox {
60 repo,
61 repo,
61 max_size: invocation
62 max_size: invocation
62 .config
63 .config
63 .get_byte_size(b"blackbox", b"maxsize")?
64 .get_byte_size(b"blackbox", b"maxsize")?
64 .unwrap_or(DEFAULT_MAX_SIZE),
65 .unwrap_or(DEFAULT_MAX_SIZE),
65 max_files: invocation
66 max_files: invocation
66 .config
67 .config
67 .get_u32(b"blackbox", b"maxfiles")?
68 .get_u32(b"blackbox", b"maxfiles")?
68 .unwrap_or(DEFAULT_MAX_FILES),
69 .unwrap_or(DEFAULT_MAX_FILES),
69 date_format: invocation
70 date_format: invocation
70 .config
71 .config
71 .get_str(b"blackbox", b"date-format")?
72 .get_str(b"blackbox", b"date-format")?
72 .unwrap_or(DEFAULT_DATE_FORMAT),
73 .unwrap_or(DEFAULT_DATE_FORMAT),
73 })
74 })
74 }
75 }
75 } else {
76 } else {
76 // Without a local repository there’s no `.hg/blackbox.log` to
77 // Without a local repository there’s no `.hg/blackbox.log` to
77 // write to.
78 // write to.
78 None
79 None
79 };
80 };
80 Ok(Self {
81 Ok(Self {
81 process_start_time,
82 process_start_time,
82 configured,
83 configured,
83 })
84 })
84 }
85 }
85
86
86 pub fn log_command_start(&self) {
87 pub fn log_command_start<'arg>(
88 &self,
89 argv: impl Iterator<Item = &'arg OsString>,
90 ) {
87 if let Some(configured) = &self.configured {
91 if let Some(configured) = &self.configured {
88 let message = format_bytes!(b"(rust) {}", format_cli_args());
92 let message = format_bytes!(b"(rust) {}", format_cli_args(argv));
89 configured.log(&self.process_start_time.calendar_based, &message);
93 configured.log(&self.process_start_time.calendar_based, &message);
90 }
94 }
91 }
95 }
92
96
93 pub fn log_command_end(&self, exit_code: i32) {
97 pub fn log_command_end<'arg>(
98 &self,
99 argv: impl Iterator<Item = &'arg OsString>,
100 exit_code: i32,
101 ) {
94 if let Some(configured) = &self.configured {
102 if let Some(configured) = &self.configured {
95 let now = chrono::Local::now();
103 let now = chrono::Local::now();
96 let duration = self
104 let duration = self
97 .process_start_time
105 .process_start_time
98 .monotonic_clock
106 .monotonic_clock
99 .elapsed()
107 .elapsed()
100 .as_secs_f64();
108 .as_secs_f64();
101 let message = format_bytes!(
109 let message = format_bytes!(
102 b"(rust) {} exited {} after {} seconds",
110 b"(rust) {} exited {} after {} seconds",
103 format_cli_args(),
111 format_cli_args(argv),
104 exit_code,
112 exit_code,
105 format_bytes::Utf8(format_args!("{:.03}", duration))
113 format_bytes::Utf8(format_args!("{:.03}", duration))
106 );
114 );
107 configured.log(&now, &message);
115 configured.log(&now, &message);
108 }
116 }
109 }
117 }
110 }
118 }
111
119
112 impl ConfiguredBlackbox<'_> {
120 impl ConfiguredBlackbox<'_> {
113 fn log(&self, date_time: &DateTime, message: &[u8]) {
121 fn log(&self, date_time: &DateTime, message: &[u8]) {
114 let date = format_bytes::Utf8(date_time.format(self.date_format));
122 let date = format_bytes::Utf8(date_time.format(self.date_format));
115 let user = users::get_current_username().map(get_bytes_from_os_str);
123 let user = users::get_current_username().map(get_bytes_from_os_str);
116 let user = user.as_deref().unwrap_or(b"???");
124 let user = user.as_deref().unwrap_or(b"???");
117 let rev = format_bytes::Utf8(match self.repo.dirstate_parents() {
125 let rev = format_bytes::Utf8(match self.repo.dirstate_parents() {
118 Ok(parents) if parents.p2 == hg::revlog::node::NULL_NODE => {
126 Ok(parents) if parents.p2 == hg::revlog::node::NULL_NODE => {
119 format!("{:x}", parents.p1)
127 format!("{:x}", parents.p1)
120 }
128 }
121 Ok(parents) => format!("{:x}+{:x}", parents.p1, parents.p2),
129 Ok(parents) => format!("{:x}+{:x}", parents.p1, parents.p2),
122 Err(_dirstate_corruption_error) => {
130 Err(_dirstate_corruption_error) => {
123 // TODO: log a non-fatal warning to stderr
131 // TODO: log a non-fatal warning to stderr
124 "???".to_owned()
132 "???".to_owned()
125 }
133 }
126 });
134 });
127 let pid = std::process::id();
135 let pid = std::process::id();
128 let line = format_bytes!(
136 let line = format_bytes!(
129 b"{} {} @{} ({})> {}\n",
137 b"{} {} @{} ({})> {}\n",
130 date,
138 date,
131 user,
139 user,
132 rev,
140 rev,
133 pid,
141 pid,
134 message
142 message
135 );
143 );
136 let result =
144 let result =
137 hg::logging::LogFile::new(self.repo.hg_vfs(), "blackbox.log")
145 hg::logging::LogFile::new(self.repo.hg_vfs(), "blackbox.log")
138 .max_size(Some(self.max_size))
146 .max_size(Some(self.max_size))
139 .max_files(self.max_files)
147 .max_files(self.max_files)
140 .write(&line);
148 .write(&line);
141 match result {
149 match result {
142 Ok(()) => {}
150 Ok(()) => {}
143 Err(_io_error) => {
151 Err(_io_error) => {
144 // TODO: log a non-fatal warning to stderr
152 // TODO: log a non-fatal warning to stderr
145 }
153 }
146 }
154 }
147 }
155 }
148 }
156 }
149
157
150 fn format_cli_args() -> Vec<u8> {
158 fn format_cli_args<'a>(
151 let mut args = std::env::args_os();
159 mut args: impl Iterator<Item = &'a OsString>,
160 ) -> Vec<u8> {
152 let _ = args.next(); // Skip the first (or zeroth) arg, the name of the `rhg` executable
161 let _ = args.next(); // Skip the first (or zeroth) arg, the name of the `rhg` executable
153 let mut args = args.map(|arg| shell_quote(&get_bytes_from_os_str(arg)));
162 let mut args = args.map(|arg| shell_quote(&get_bytes_from_os_str(arg)));
154 let mut formatted = Vec::new();
163 let mut formatted = Vec::new();
155 if let Some(arg) = args.next() {
164 if let Some(arg) = args.next() {
156 formatted.extend(arg)
165 formatted.extend(arg)
157 }
166 }
158 for arg in args {
167 for arg in args {
159 formatted.push(b' ');
168 formatted.push(b' ');
160 formatted.extend(arg)
169 formatted.extend(arg)
161 }
170 }
162 formatted
171 formatted
163 }
172 }
@@ -1,716 +1,733 b''
1 extern crate log;
1 extern crate log;
2 use crate::error::CommandError;
2 use crate::error::CommandError;
3 use crate::ui::{local_to_utf8, Ui};
3 use crate::ui::{local_to_utf8, Ui};
4 use clap::App;
4 use clap::App;
5 use clap::AppSettings;
5 use clap::AppSettings;
6 use clap::Arg;
6 use clap::Arg;
7 use clap::ArgMatches;
7 use clap::ArgMatches;
8 use format_bytes::{format_bytes, join};
8 use format_bytes::{format_bytes, join};
9 use hg::config::{Config, ConfigSource};
9 use hg::config::{Config, ConfigSource};
10 use hg::exit_codes;
10 use hg::exit_codes;
11 use hg::repo::{Repo, RepoError};
11 use hg::repo::{Repo, RepoError};
12 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
12 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
13 use hg::utils::SliceExt;
13 use hg::utils::SliceExt;
14 use std::collections::HashSet;
14 use std::collections::HashSet;
15 use std::ffi::OsString;
15 use std::ffi::OsString;
16 use std::path::PathBuf;
16 use std::path::PathBuf;
17 use std::process::Command;
17 use std::process::Command;
18
18
19 mod blackbox;
19 mod blackbox;
20 mod color;
20 mod color;
21 mod error;
21 mod error;
22 mod ui;
22 mod ui;
23 pub mod utils {
23 pub mod utils {
24 pub mod path_utils;
24 pub mod path_utils;
25 }
25 }
26
26
27 fn main_with_result(
27 fn main_with_result(
28 argv: Vec<OsString>,
28 process_start_time: &blackbox::ProcessStartTime,
29 process_start_time: &blackbox::ProcessStartTime,
29 ui: &ui::Ui,
30 ui: &ui::Ui,
30 repo: Result<&Repo, &NoRepoInCwdError>,
31 repo: Result<&Repo, &NoRepoInCwdError>,
31 config: &Config,
32 config: &Config,
32 ) -> Result<(), CommandError> {
33 ) -> Result<(), CommandError> {
33 check_unsupported(config, repo)?;
34 check_unsupported(config, repo)?;
34
35
35 let app = App::new("rhg")
36 let app = App::new("rhg")
36 .global_setting(AppSettings::AllowInvalidUtf8)
37 .global_setting(AppSettings::AllowInvalidUtf8)
37 .global_setting(AppSettings::DisableVersion)
38 .global_setting(AppSettings::DisableVersion)
38 .setting(AppSettings::SubcommandRequired)
39 .setting(AppSettings::SubcommandRequired)
39 .setting(AppSettings::VersionlessSubcommands)
40 .setting(AppSettings::VersionlessSubcommands)
40 .arg(
41 .arg(
41 Arg::with_name("repository")
42 Arg::with_name("repository")
42 .help("repository root directory")
43 .help("repository root directory")
43 .short("-R")
44 .short("-R")
44 .long("--repository")
45 .long("--repository")
45 .value_name("REPO")
46 .value_name("REPO")
46 .takes_value(true)
47 .takes_value(true)
47 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
48 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
48 .global(true),
49 .global(true),
49 )
50 )
50 .arg(
51 .arg(
51 Arg::with_name("config")
52 Arg::with_name("config")
52 .help("set/override config option (use 'section.name=value')")
53 .help("set/override config option (use 'section.name=value')")
53 .long("--config")
54 .long("--config")
54 .value_name("CONFIG")
55 .value_name("CONFIG")
55 .takes_value(true)
56 .takes_value(true)
56 .global(true)
57 .global(true)
57 // Ok: `--config section.key1=val --config section.key2=val2`
58 // Ok: `--config section.key1=val --config section.key2=val2`
58 .multiple(true)
59 .multiple(true)
59 // Not ok: `--config section.key1=val section.key2=val2`
60 // Not ok: `--config section.key1=val section.key2=val2`
60 .number_of_values(1),
61 .number_of_values(1),
61 )
62 )
62 .arg(
63 .arg(
63 Arg::with_name("cwd")
64 Arg::with_name("cwd")
64 .help("change working directory")
65 .help("change working directory")
65 .long("--cwd")
66 .long("--cwd")
66 .value_name("DIR")
67 .value_name("DIR")
67 .takes_value(true)
68 .takes_value(true)
68 .global(true),
69 .global(true),
69 )
70 )
70 .arg(
71 .arg(
71 Arg::with_name("color")
72 Arg::with_name("color")
72 .help("when to colorize (boolean, always, auto, never, or debug)")
73 .help("when to colorize (boolean, always, auto, never, or debug)")
73 .long("--color")
74 .long("--color")
74 .value_name("TYPE")
75 .value_name("TYPE")
75 .takes_value(true)
76 .takes_value(true)
76 .global(true),
77 .global(true),
77 )
78 )
78 .version("0.0.1");
79 .version("0.0.1");
79 let app = add_subcommand_args(app);
80 let app = add_subcommand_args(app);
80
81
81 let matches = app.clone().get_matches_safe()?;
82 let matches = app.clone().get_matches_from_safe(argv.iter())?;
82
83
83 let (subcommand_name, subcommand_matches) = matches.subcommand();
84 let (subcommand_name, subcommand_matches) = matches.subcommand();
84
85
85 // Mercurial allows users to define "defaults" for commands, fallback
86 // Mercurial allows users to define "defaults" for commands, fallback
86 // if a default is detected for the current command
87 // if a default is detected for the current command
87 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
88 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
88 if defaults?.is_some() {
89 if defaults?.is_some() {
89 let msg = "`defaults` config set";
90 let msg = "`defaults` config set";
90 return Err(CommandError::unsupported(msg));
91 return Err(CommandError::unsupported(msg));
91 }
92 }
92
93
93 for prefix in ["pre", "post", "fail"].iter() {
94 for prefix in ["pre", "post", "fail"].iter() {
94 // Mercurial allows users to define generic hooks for commands,
95 // Mercurial allows users to define generic hooks for commands,
95 // fallback if any are detected
96 // fallback if any are detected
96 let item = format!("{}-{}", prefix, subcommand_name);
97 let item = format!("{}-{}", prefix, subcommand_name);
97 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
98 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
98 if hook_for_command.is_some() {
99 if hook_for_command.is_some() {
99 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
100 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
100 return Err(CommandError::unsupported(msg));
101 return Err(CommandError::unsupported(msg));
101 }
102 }
102 }
103 }
103 let run = subcommand_run_fn(subcommand_name)
104 let run = subcommand_run_fn(subcommand_name)
104 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
105 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
105 let subcommand_args = subcommand_matches
106 let subcommand_args = subcommand_matches
106 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
107 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
107
108
108 let invocation = CliInvocation {
109 let invocation = CliInvocation {
109 ui,
110 ui,
110 subcommand_args,
111 subcommand_args,
111 config,
112 config,
112 repo,
113 repo,
113 };
114 };
114
115
115 if let Ok(repo) = repo {
116 if let Ok(repo) = repo {
116 // We don't support subrepos, fallback if the subrepos file is present
117 // We don't support subrepos, fallback if the subrepos file is present
117 if repo.working_directory_vfs().join(".hgsub").exists() {
118 if repo.working_directory_vfs().join(".hgsub").exists() {
118 let msg = "subrepos (.hgsub is present)";
119 let msg = "subrepos (.hgsub is present)";
119 return Err(CommandError::unsupported(msg));
120 return Err(CommandError::unsupported(msg));
120 }
121 }
121 }
122 }
122
123
123 if config.is_extension_enabled(b"blackbox") {
124 if config.is_extension_enabled(b"blackbox") {
124 let blackbox =
125 let blackbox =
125 blackbox::Blackbox::new(&invocation, process_start_time)?;
126 blackbox::Blackbox::new(&invocation, process_start_time)?;
126 blackbox.log_command_start();
127 blackbox.log_command_start(argv.iter());
127 let result = run(&invocation);
128 let result = run(&invocation);
128 blackbox.log_command_end(exit_code(
129 blackbox.log_command_end(
129 &result,
130 argv.iter(),
130 // TODO: show a warning or combine with original error if
131 exit_code(
131 // `get_bool` returns an error
132 &result,
132 config
133 // TODO: show a warning or combine with original error if
133 .get_bool(b"ui", b"detailed-exit-code")
134 // `get_bool` returns an error
134 .unwrap_or(false),
135 config
135 ));
136 .get_bool(b"ui", b"detailed-exit-code")
137 .unwrap_or(false),
138 ),
139 );
136 result
140 result
137 } else {
141 } else {
138 run(&invocation)
142 run(&invocation)
139 }
143 }
140 }
144 }
141
145
142 fn main() {
146 fn rhg_main(argv: Vec<OsString>) -> ! {
143 // Run this first, before we find out if the blackbox extension is even
147 // Run this first, before we find out if the blackbox extension is even
144 // enabled, in order to include everything in-between in the duration
148 // enabled, in order to include everything in-between in the duration
145 // measurements. Reading config files can be slow if they’re on NFS.
149 // measurements. Reading config files can be slow if they’re on NFS.
146 let process_start_time = blackbox::ProcessStartTime::now();
150 let process_start_time = blackbox::ProcessStartTime::now();
147
151
148 env_logger::init();
152 env_logger::init();
149
153
150 let early_args = EarlyArgs::parse(std::env::args_os());
154 let early_args = EarlyArgs::parse(&argv);
151
155
152 let initial_current_dir = early_args.cwd.map(|cwd| {
156 let initial_current_dir = early_args.cwd.map(|cwd| {
153 let cwd = get_path_from_bytes(&cwd);
157 let cwd = get_path_from_bytes(&cwd);
154 std::env::current_dir()
158 std::env::current_dir()
155 .and_then(|initial| {
159 .and_then(|initial| {
156 std::env::set_current_dir(cwd)?;
160 std::env::set_current_dir(cwd)?;
157 Ok(initial)
161 Ok(initial)
158 })
162 })
159 .unwrap_or_else(|error| {
163 .unwrap_or_else(|error| {
160 exit(
164 exit(
165 &argv,
161 &None,
166 &None,
162 &Ui::new_infallible(&Config::empty()),
167 &Ui::new_infallible(&Config::empty()),
163 OnUnsupported::Abort,
168 OnUnsupported::Abort,
164 Err(CommandError::abort(format!(
169 Err(CommandError::abort(format!(
165 "abort: {}: '{}'",
170 "abort: {}: '{}'",
166 error,
171 error,
167 cwd.display()
172 cwd.display()
168 ))),
173 ))),
169 false,
174 false,
170 )
175 )
171 })
176 })
172 });
177 });
173
178
174 let mut non_repo_config =
179 let mut non_repo_config =
175 Config::load_non_repo().unwrap_or_else(|error| {
180 Config::load_non_repo().unwrap_or_else(|error| {
176 // Normally this is decided based on config, but we don’t have that
181 // Normally this is decided based on config, but we don’t have that
177 // available. As of this writing config loading never returns an
182 // available. As of this writing config loading never returns an
178 // "unsupported" error but that is not enforced by the type system.
183 // "unsupported" error but that is not enforced by the type system.
179 let on_unsupported = OnUnsupported::Abort;
184 let on_unsupported = OnUnsupported::Abort;
180
185
181 exit(
186 exit(
187 &argv,
182 &initial_current_dir,
188 &initial_current_dir,
183 &Ui::new_infallible(&Config::empty()),
189 &Ui::new_infallible(&Config::empty()),
184 on_unsupported,
190 on_unsupported,
185 Err(error.into()),
191 Err(error.into()),
186 false,
192 false,
187 )
193 )
188 });
194 });
189
195
190 non_repo_config
196 non_repo_config
191 .load_cli_args(early_args.config, early_args.color)
197 .load_cli_args(early_args.config, early_args.color)
192 .unwrap_or_else(|error| {
198 .unwrap_or_else(|error| {
193 exit(
199 exit(
200 &argv,
194 &initial_current_dir,
201 &initial_current_dir,
195 &Ui::new_infallible(&non_repo_config),
202 &Ui::new_infallible(&non_repo_config),
196 OnUnsupported::from_config(&non_repo_config),
203 OnUnsupported::from_config(&non_repo_config),
197 Err(error.into()),
204 Err(error.into()),
198 non_repo_config
205 non_repo_config
199 .get_bool(b"ui", b"detailed-exit-code")
206 .get_bool(b"ui", b"detailed-exit-code")
200 .unwrap_or(false),
207 .unwrap_or(false),
201 )
208 )
202 });
209 });
203
210
204 if let Some(repo_path_bytes) = &early_args.repo {
211 if let Some(repo_path_bytes) = &early_args.repo {
205 lazy_static::lazy_static! {
212 lazy_static::lazy_static! {
206 static ref SCHEME_RE: regex::bytes::Regex =
213 static ref SCHEME_RE: regex::bytes::Regex =
207 // Same as `_matchscheme` in `mercurial/util.py`
214 // Same as `_matchscheme` in `mercurial/util.py`
208 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
215 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
209 }
216 }
210 if SCHEME_RE.is_match(&repo_path_bytes) {
217 if SCHEME_RE.is_match(&repo_path_bytes) {
211 exit(
218 exit(
219 &argv,
212 &initial_current_dir,
220 &initial_current_dir,
213 &Ui::new_infallible(&non_repo_config),
221 &Ui::new_infallible(&non_repo_config),
214 OnUnsupported::from_config(&non_repo_config),
222 OnUnsupported::from_config(&non_repo_config),
215 Err(CommandError::UnsupportedFeature {
223 Err(CommandError::UnsupportedFeature {
216 message: format_bytes!(
224 message: format_bytes!(
217 b"URL-like --repository {}",
225 b"URL-like --repository {}",
218 repo_path_bytes
226 repo_path_bytes
219 ),
227 ),
220 }),
228 }),
221 // TODO: show a warning or combine with original error if
229 // TODO: show a warning or combine with original error if
222 // `get_bool` returns an error
230 // `get_bool` returns an error
223 non_repo_config
231 non_repo_config
224 .get_bool(b"ui", b"detailed-exit-code")
232 .get_bool(b"ui", b"detailed-exit-code")
225 .unwrap_or(false),
233 .unwrap_or(false),
226 )
234 )
227 }
235 }
228 }
236 }
229 let repo_arg = early_args.repo.unwrap_or(Vec::new());
237 let repo_arg = early_args.repo.unwrap_or(Vec::new());
230 let repo_path: Option<PathBuf> = {
238 let repo_path: Option<PathBuf> = {
231 if repo_arg.is_empty() {
239 if repo_arg.is_empty() {
232 None
240 None
233 } else {
241 } else {
234 let local_config = {
242 let local_config = {
235 if std::env::var_os("HGRCSKIPREPO").is_none() {
243 if std::env::var_os("HGRCSKIPREPO").is_none() {
236 // TODO: handle errors from find_repo_root
244 // TODO: handle errors from find_repo_root
237 if let Ok(current_dir_path) = Repo::find_repo_root() {
245 if let Ok(current_dir_path) = Repo::find_repo_root() {
238 let config_files = vec![
246 let config_files = vec![
239 ConfigSource::AbsPath(
247 ConfigSource::AbsPath(
240 current_dir_path.join(".hg/hgrc"),
248 current_dir_path.join(".hg/hgrc"),
241 ),
249 ),
242 ConfigSource::AbsPath(
250 ConfigSource::AbsPath(
243 current_dir_path.join(".hg/hgrc-not-shared"),
251 current_dir_path.join(".hg/hgrc-not-shared"),
244 ),
252 ),
245 ];
253 ];
246 // TODO: handle errors from
254 // TODO: handle errors from
247 // `load_from_explicit_sources`
255 // `load_from_explicit_sources`
248 Config::load_from_explicit_sources(config_files).ok()
256 Config::load_from_explicit_sources(config_files).ok()
249 } else {
257 } else {
250 None
258 None
251 }
259 }
252 } else {
260 } else {
253 None
261 None
254 }
262 }
255 };
263 };
256
264
257 let non_repo_config_val = {
265 let non_repo_config_val = {
258 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
266 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
259 match &non_repo_val {
267 match &non_repo_val {
260 Some(val) if val.len() > 0 => home::home_dir()
268 Some(val) if val.len() > 0 => home::home_dir()
261 .unwrap_or_else(|| PathBuf::from("~"))
269 .unwrap_or_else(|| PathBuf::from("~"))
262 .join(get_path_from_bytes(val))
270 .join(get_path_from_bytes(val))
263 .canonicalize()
271 .canonicalize()
264 // TODO: handle error and make it similar to python
272 // TODO: handle error and make it similar to python
265 // implementation maybe?
273 // implementation maybe?
266 .ok(),
274 .ok(),
267 _ => None,
275 _ => None,
268 }
276 }
269 };
277 };
270
278
271 let config_val = match &local_config {
279 let config_val = match &local_config {
272 None => non_repo_config_val,
280 None => non_repo_config_val,
273 Some(val) => {
281 Some(val) => {
274 let local_config_val = val.get(b"paths", &repo_arg);
282 let local_config_val = val.get(b"paths", &repo_arg);
275 match &local_config_val {
283 match &local_config_val {
276 Some(val) if val.len() > 0 => {
284 Some(val) if val.len() > 0 => {
277 // presence of a local_config assures that
285 // presence of a local_config assures that
278 // current_dir
286 // current_dir
279 // wont result in an Error
287 // wont result in an Error
280 let canpath = hg::utils::current_dir()
288 let canpath = hg::utils::current_dir()
281 .unwrap()
289 .unwrap()
282 .join(get_path_from_bytes(val))
290 .join(get_path_from_bytes(val))
283 .canonicalize();
291 .canonicalize();
284 canpath.ok().or(non_repo_config_val)
292 canpath.ok().or(non_repo_config_val)
285 }
293 }
286 _ => non_repo_config_val,
294 _ => non_repo_config_val,
287 }
295 }
288 }
296 }
289 };
297 };
290 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
298 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
291 }
299 }
292 };
300 };
293
301
294 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
302 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
295 {
303 {
296 Ok(repo) => Ok(repo),
304 Ok(repo) => Ok(repo),
297 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
305 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
298 // Not finding a repo is not fatal yet, if `-R` was not given
306 // Not finding a repo is not fatal yet, if `-R` was not given
299 Err(NoRepoInCwdError { cwd: at })
307 Err(NoRepoInCwdError { cwd: at })
300 }
308 }
301 Err(error) => exit(
309 Err(error) => exit(
310 &argv,
302 &initial_current_dir,
311 &initial_current_dir,
303 &Ui::new_infallible(&non_repo_config),
312 &Ui::new_infallible(&non_repo_config),
304 OnUnsupported::from_config(&non_repo_config),
313 OnUnsupported::from_config(&non_repo_config),
305 Err(error.into()),
314 Err(error.into()),
306 // TODO: show a warning or combine with original error if
315 // TODO: show a warning or combine with original error if
307 // `get_bool` returns an error
316 // `get_bool` returns an error
308 non_repo_config
317 non_repo_config
309 .get_bool(b"ui", b"detailed-exit-code")
318 .get_bool(b"ui", b"detailed-exit-code")
310 .unwrap_or(false),
319 .unwrap_or(false),
311 ),
320 ),
312 };
321 };
313
322
314 let config = if let Ok(repo) = &repo_result {
323 let config = if let Ok(repo) = &repo_result {
315 repo.config()
324 repo.config()
316 } else {
325 } else {
317 &non_repo_config
326 &non_repo_config
318 };
327 };
319 let ui = Ui::new(&config).unwrap_or_else(|error| {
328 let ui = Ui::new(&config).unwrap_or_else(|error| {
320 exit(
329 exit(
330 &argv,
321 &initial_current_dir,
331 &initial_current_dir,
322 &Ui::new_infallible(&config),
332 &Ui::new_infallible(&config),
323 OnUnsupported::from_config(&config),
333 OnUnsupported::from_config(&config),
324 Err(error.into()),
334 Err(error.into()),
325 config
335 config
326 .get_bool(b"ui", b"detailed-exit-code")
336 .get_bool(b"ui", b"detailed-exit-code")
327 .unwrap_or(false),
337 .unwrap_or(false),
328 )
338 )
329 });
339 });
330 let on_unsupported = OnUnsupported::from_config(config);
340 let on_unsupported = OnUnsupported::from_config(config);
331
341
332 let result = main_with_result(
342 let result = main_with_result(
343 argv.iter().map(|s| s.to_owned()).collect(),
333 &process_start_time,
344 &process_start_time,
334 &ui,
345 &ui,
335 repo_result.as_ref(),
346 repo_result.as_ref(),
336 config,
347 config,
337 );
348 );
338 exit(
349 exit(
350 &argv,
339 &initial_current_dir,
351 &initial_current_dir,
340 &ui,
352 &ui,
341 on_unsupported,
353 on_unsupported,
342 result,
354 result,
343 // TODO: show a warning or combine with original error if `get_bool`
355 // TODO: show a warning or combine with original error if `get_bool`
344 // returns an error
356 // returns an error
345 config
357 config
346 .get_bool(b"ui", b"detailed-exit-code")
358 .get_bool(b"ui", b"detailed-exit-code")
347 .unwrap_or(false),
359 .unwrap_or(false),
348 )
360 )
349 }
361 }
350
362
363 fn main() -> ! {
364 rhg_main(std::env::args_os().collect())
365 }
366
351 fn exit_code(
367 fn exit_code(
352 result: &Result<(), CommandError>,
368 result: &Result<(), CommandError>,
353 use_detailed_exit_code: bool,
369 use_detailed_exit_code: bool,
354 ) -> i32 {
370 ) -> i32 {
355 match result {
371 match result {
356 Ok(()) => exit_codes::OK,
372 Ok(()) => exit_codes::OK,
357 Err(CommandError::Abort {
373 Err(CommandError::Abort {
358 message: _,
374 message: _,
359 detailed_exit_code,
375 detailed_exit_code,
360 }) => {
376 }) => {
361 if use_detailed_exit_code {
377 if use_detailed_exit_code {
362 *detailed_exit_code
378 *detailed_exit_code
363 } else {
379 } else {
364 exit_codes::ABORT
380 exit_codes::ABORT
365 }
381 }
366 }
382 }
367 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
383 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
368
384
369 // Exit with a specific code and no error message to let a potential
385 // Exit with a specific code and no error message to let a potential
370 // wrapper script fallback to Python-based Mercurial.
386 // wrapper script fallback to Python-based Mercurial.
371 Err(CommandError::UnsupportedFeature { .. }) => {
387 Err(CommandError::UnsupportedFeature { .. }) => {
372 exit_codes::UNIMPLEMENTED
388 exit_codes::UNIMPLEMENTED
373 }
389 }
374 }
390 }
375 }
391 }
376
392
377 fn exit(
393 fn exit<'a>(
394 original_args: &'a [OsString],
378 initial_current_dir: &Option<PathBuf>,
395 initial_current_dir: &Option<PathBuf>,
379 ui: &Ui,
396 ui: &Ui,
380 mut on_unsupported: OnUnsupported,
397 mut on_unsupported: OnUnsupported,
381 result: Result<(), CommandError>,
398 result: Result<(), CommandError>,
382 use_detailed_exit_code: bool,
399 use_detailed_exit_code: bool,
383 ) -> ! {
400 ) -> ! {
384 if let (
401 if let (
385 OnUnsupported::Fallback { executable },
402 OnUnsupported::Fallback { executable },
386 Err(CommandError::UnsupportedFeature { message }),
403 Err(CommandError::UnsupportedFeature { message }),
387 ) = (&on_unsupported, &result)
404 ) = (&on_unsupported, &result)
388 {
405 {
389 let mut args = std::env::args_os();
406 let mut args = original_args.iter();
390 let executable = match executable {
407 let executable = match executable {
391 None => {
408 None => {
392 exit_no_fallback(
409 exit_no_fallback(
393 ui,
410 ui,
394 OnUnsupported::Abort,
411 OnUnsupported::Abort,
395 Err(CommandError::abort(
412 Err(CommandError::abort(
396 "abort: 'rhg.on-unsupported=fallback' without \
413 "abort: 'rhg.on-unsupported=fallback' without \
397 'rhg.fallback-executable' set.",
414 'rhg.fallback-executable' set.",
398 )),
415 )),
399 false,
416 false,
400 );
417 );
401 }
418 }
402 Some(executable) => executable,
419 Some(executable) => executable,
403 };
420 };
404 let executable_path = get_path_from_bytes(&executable);
421 let executable_path = get_path_from_bytes(&executable);
405 let this_executable = args.next().expect("exepcted argv[0] to exist");
422 let this_executable = args.next().expect("exepcted argv[0] to exist");
406 if executable_path == &PathBuf::from(this_executable) {
423 if executable_path == &PathBuf::from(this_executable) {
407 // Avoid spawning infinitely many processes until resource
424 // Avoid spawning infinitely many processes until resource
408 // exhaustion.
425 // exhaustion.
409 let _ = ui.write_stderr(&format_bytes!(
426 let _ = ui.write_stderr(&format_bytes!(
410 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
427 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
411 points to `rhg` itself.\n",
428 points to `rhg` itself.\n",
412 executable
429 executable
413 ));
430 ));
414 on_unsupported = OnUnsupported::Abort
431 on_unsupported = OnUnsupported::Abort
415 } else {
432 } else {
416 log::debug!("falling back (see trace-level log)");
433 log::debug!("falling back (see trace-level log)");
417 log::trace!("{}", local_to_utf8(message));
434 log::trace!("{}", local_to_utf8(message));
418 // `args` is now `argv[1..]` since we’ve already consumed
435 // `args` is now `argv[1..]` since we’ve already consumed
419 // `argv[0]`
436 // `argv[0]`
420 let mut command = Command::new(executable_path);
437 let mut command = Command::new(executable_path);
421 command.args(args);
438 command.args(args);
422 if let Some(initial) = initial_current_dir {
439 if let Some(initial) = initial_current_dir {
423 command.current_dir(initial);
440 command.current_dir(initial);
424 }
441 }
425 let result = command.status();
442 let result = command.status();
426 match result {
443 match result {
427 Ok(status) => std::process::exit(
444 Ok(status) => std::process::exit(
428 status.code().unwrap_or(exit_codes::ABORT),
445 status.code().unwrap_or(exit_codes::ABORT),
429 ),
446 ),
430 Err(error) => {
447 Err(error) => {
431 let _ = ui.write_stderr(&format_bytes!(
448 let _ = ui.write_stderr(&format_bytes!(
432 b"tried to fall back to a '{}' sub-process but got error {}\n",
449 b"tried to fall back to a '{}' sub-process but got error {}\n",
433 executable, format_bytes::Utf8(error)
450 executable, format_bytes::Utf8(error)
434 ));
451 ));
435 on_unsupported = OnUnsupported::Abort
452 on_unsupported = OnUnsupported::Abort
436 }
453 }
437 }
454 }
438 }
455 }
439 }
456 }
440 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
457 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
441 }
458 }
442
459
443 fn exit_no_fallback(
460 fn exit_no_fallback(
444 ui: &Ui,
461 ui: &Ui,
445 on_unsupported: OnUnsupported,
462 on_unsupported: OnUnsupported,
446 result: Result<(), CommandError>,
463 result: Result<(), CommandError>,
447 use_detailed_exit_code: bool,
464 use_detailed_exit_code: bool,
448 ) -> ! {
465 ) -> ! {
449 match &result {
466 match &result {
450 Ok(_) => {}
467 Ok(_) => {}
451 Err(CommandError::Unsuccessful) => {}
468 Err(CommandError::Unsuccessful) => {}
452 Err(CommandError::Abort {
469 Err(CommandError::Abort {
453 message,
470 message,
454 detailed_exit_code: _,
471 detailed_exit_code: _,
455 }) => {
472 }) => {
456 if !message.is_empty() {
473 if !message.is_empty() {
457 // Ignore errors when writing to stderr, we’re already exiting
474 // Ignore errors when writing to stderr, we’re already exiting
458 // with failure code so there’s not much more we can do.
475 // with failure code so there’s not much more we can do.
459 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
476 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
460 }
477 }
461 }
478 }
462 Err(CommandError::UnsupportedFeature { message }) => {
479 Err(CommandError::UnsupportedFeature { message }) => {
463 match on_unsupported {
480 match on_unsupported {
464 OnUnsupported::Abort => {
481 OnUnsupported::Abort => {
465 let _ = ui.write_stderr(&format_bytes!(
482 let _ = ui.write_stderr(&format_bytes!(
466 b"unsupported feature: {}\n",
483 b"unsupported feature: {}\n",
467 message
484 message
468 ));
485 ));
469 }
486 }
470 OnUnsupported::AbortSilent => {}
487 OnUnsupported::AbortSilent => {}
471 OnUnsupported::Fallback { .. } => unreachable!(),
488 OnUnsupported::Fallback { .. } => unreachable!(),
472 }
489 }
473 }
490 }
474 }
491 }
475 std::process::exit(exit_code(&result, use_detailed_exit_code))
492 std::process::exit(exit_code(&result, use_detailed_exit_code))
476 }
493 }
477
494
478 macro_rules! subcommands {
495 macro_rules! subcommands {
479 ($( $command: ident )+) => {
496 ($( $command: ident )+) => {
480 mod commands {
497 mod commands {
481 $(
498 $(
482 pub mod $command;
499 pub mod $command;
483 )+
500 )+
484 }
501 }
485
502
486 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
503 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
487 app
504 app
488 $(
505 $(
489 .subcommand(commands::$command::args())
506 .subcommand(commands::$command::args())
490 )+
507 )+
491 }
508 }
492
509
493 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
510 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
494
511
495 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
512 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
496 match name {
513 match name {
497 $(
514 $(
498 stringify!($command) => Some(commands::$command::run),
515 stringify!($command) => Some(commands::$command::run),
499 )+
516 )+
500 _ => None,
517 _ => None,
501 }
518 }
502 }
519 }
503 };
520 };
504 }
521 }
505
522
506 subcommands! {
523 subcommands! {
507 cat
524 cat
508 debugdata
525 debugdata
509 debugrequirements
526 debugrequirements
510 debugignorerhg
527 debugignorerhg
511 files
528 files
512 root
529 root
513 config
530 config
514 status
531 status
515 }
532 }
516
533
517 pub struct CliInvocation<'a> {
534 pub struct CliInvocation<'a> {
518 ui: &'a Ui,
535 ui: &'a Ui,
519 subcommand_args: &'a ArgMatches<'a>,
536 subcommand_args: &'a ArgMatches<'a>,
520 config: &'a Config,
537 config: &'a Config,
521 /// References inside `Result` is a bit peculiar but allow
538 /// References inside `Result` is a bit peculiar but allow
522 /// `invocation.repo?` to work out with `&CliInvocation` since this
539 /// `invocation.repo?` to work out with `&CliInvocation` since this
523 /// `Result` type is `Copy`.
540 /// `Result` type is `Copy`.
524 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
541 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
525 }
542 }
526
543
527 struct NoRepoInCwdError {
544 struct NoRepoInCwdError {
528 cwd: PathBuf,
545 cwd: PathBuf,
529 }
546 }
530
547
531 /// CLI arguments to be parsed "early" in order to be able to read
548 /// CLI arguments to be parsed "early" in order to be able to read
532 /// configuration before using Clap. Ideally we would also use Clap for this,
549 /// configuration before using Clap. Ideally we would also use Clap for this,
533 /// see <https://github.com/clap-rs/clap/discussions/2366>.
550 /// see <https://github.com/clap-rs/clap/discussions/2366>.
534 ///
551 ///
535 /// These arguments are still declared when we do use Clap later, so that Clap
552 /// These arguments are still declared when we do use Clap later, so that Clap
536 /// does not return an error for their presence.
553 /// does not return an error for their presence.
537 struct EarlyArgs {
554 struct EarlyArgs {
538 /// Values of all `--config` arguments. (Possibly none)
555 /// Values of all `--config` arguments. (Possibly none)
539 config: Vec<Vec<u8>>,
556 config: Vec<Vec<u8>>,
540 /// Value of all the `--color` argument, if any.
557 /// Value of all the `--color` argument, if any.
541 color: Option<Vec<u8>>,
558 color: Option<Vec<u8>>,
542 /// Value of the `-R` or `--repository` argument, if any.
559 /// Value of the `-R` or `--repository` argument, if any.
543 repo: Option<Vec<u8>>,
560 repo: Option<Vec<u8>>,
544 /// Value of the `--cwd` argument, if any.
561 /// Value of the `--cwd` argument, if any.
545 cwd: Option<Vec<u8>>,
562 cwd: Option<Vec<u8>>,
546 }
563 }
547
564
548 impl EarlyArgs {
565 impl EarlyArgs {
549 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
566 fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
550 let mut args = args.into_iter().map(get_bytes_from_os_str);
567 let mut args = args.into_iter().map(get_bytes_from_os_str);
551 let mut config = Vec::new();
568 let mut config = Vec::new();
552 let mut color = None;
569 let mut color = None;
553 let mut repo = None;
570 let mut repo = None;
554 let mut cwd = None;
571 let mut cwd = None;
555 // Use `while let` instead of `for` so that we can also call
572 // Use `while let` instead of `for` so that we can also call
556 // `args.next()` inside the loop.
573 // `args.next()` inside the loop.
557 while let Some(arg) = args.next() {
574 while let Some(arg) = args.next() {
558 if arg == b"--config" {
575 if arg == b"--config" {
559 if let Some(value) = args.next() {
576 if let Some(value) = args.next() {
560 config.push(value)
577 config.push(value)
561 }
578 }
562 } else if let Some(value) = arg.drop_prefix(b"--config=") {
579 } else if let Some(value) = arg.drop_prefix(b"--config=") {
563 config.push(value.to_owned())
580 config.push(value.to_owned())
564 }
581 }
565
582
566 if arg == b"--color" {
583 if arg == b"--color" {
567 if let Some(value) = args.next() {
584 if let Some(value) = args.next() {
568 color = Some(value)
585 color = Some(value)
569 }
586 }
570 } else if let Some(value) = arg.drop_prefix(b"--color=") {
587 } else if let Some(value) = arg.drop_prefix(b"--color=") {
571 color = Some(value.to_owned())
588 color = Some(value.to_owned())
572 }
589 }
573
590
574 if arg == b"--cwd" {
591 if arg == b"--cwd" {
575 if let Some(value) = args.next() {
592 if let Some(value) = args.next() {
576 cwd = Some(value)
593 cwd = Some(value)
577 }
594 }
578 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
595 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
579 cwd = Some(value.to_owned())
596 cwd = Some(value.to_owned())
580 }
597 }
581
598
582 if arg == b"--repository" || arg == b"-R" {
599 if arg == b"--repository" || arg == b"-R" {
583 if let Some(value) = args.next() {
600 if let Some(value) = args.next() {
584 repo = Some(value)
601 repo = Some(value)
585 }
602 }
586 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
603 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
587 repo = Some(value.to_owned())
604 repo = Some(value.to_owned())
588 } else if let Some(value) = arg.drop_prefix(b"-R") {
605 } else if let Some(value) = arg.drop_prefix(b"-R") {
589 repo = Some(value.to_owned())
606 repo = Some(value.to_owned())
590 }
607 }
591 }
608 }
592 Self {
609 Self {
593 config,
610 config,
594 color,
611 color,
595 repo,
612 repo,
596 cwd,
613 cwd,
597 }
614 }
598 }
615 }
599 }
616 }
600
617
601 /// What to do when encountering some unsupported feature.
618 /// What to do when encountering some unsupported feature.
602 ///
619 ///
603 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
620 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
604 enum OnUnsupported {
621 enum OnUnsupported {
605 /// Print an error message describing what feature is not supported,
622 /// Print an error message describing what feature is not supported,
606 /// and exit with code 252.
623 /// and exit with code 252.
607 Abort,
624 Abort,
608 /// Silently exit with code 252.
625 /// Silently exit with code 252.
609 AbortSilent,
626 AbortSilent,
610 /// Try running a Python implementation
627 /// Try running a Python implementation
611 Fallback { executable: Option<Vec<u8>> },
628 Fallback { executable: Option<Vec<u8>> },
612 }
629 }
613
630
614 impl OnUnsupported {
631 impl OnUnsupported {
615 const DEFAULT: Self = OnUnsupported::Abort;
632 const DEFAULT: Self = OnUnsupported::Abort;
616
633
617 fn from_config(config: &Config) -> Self {
634 fn from_config(config: &Config) -> Self {
618 match config
635 match config
619 .get(b"rhg", b"on-unsupported")
636 .get(b"rhg", b"on-unsupported")
620 .map(|value| value.to_ascii_lowercase())
637 .map(|value| value.to_ascii_lowercase())
621 .as_deref()
638 .as_deref()
622 {
639 {
623 Some(b"abort") => OnUnsupported::Abort,
640 Some(b"abort") => OnUnsupported::Abort,
624 Some(b"abort-silent") => OnUnsupported::AbortSilent,
641 Some(b"abort-silent") => OnUnsupported::AbortSilent,
625 Some(b"fallback") => OnUnsupported::Fallback {
642 Some(b"fallback") => OnUnsupported::Fallback {
626 executable: config
643 executable: config
627 .get(b"rhg", b"fallback-executable")
644 .get(b"rhg", b"fallback-executable")
628 .map(|x| x.to_owned()),
645 .map(|x| x.to_owned()),
629 },
646 },
630 None => Self::DEFAULT,
647 None => Self::DEFAULT,
631 Some(_) => {
648 Some(_) => {
632 // TODO: warn about unknown config value
649 // TODO: warn about unknown config value
633 Self::DEFAULT
650 Self::DEFAULT
634 }
651 }
635 }
652 }
636 }
653 }
637 }
654 }
638
655
639 /// The `*` extension is an edge-case for config sub-options that apply to all
656 /// The `*` extension is an edge-case for config sub-options that apply to all
640 /// extensions. For now, only `:required` exists, but that may change in the
657 /// extensions. For now, only `:required` exists, but that may change in the
641 /// future.
658 /// future.
642 const SUPPORTED_EXTENSIONS: &[&[u8]] =
659 const SUPPORTED_EXTENSIONS: &[&[u8]] =
643 &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
660 &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
644
661
645 fn check_extensions(config: &Config) -> Result<(), CommandError> {
662 fn check_extensions(config: &Config) -> Result<(), CommandError> {
646 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
663 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
647 // All extensions are to be ignored, nothing to do here
664 // All extensions are to be ignored, nothing to do here
648 return Ok(());
665 return Ok(());
649 }
666 }
650
667
651 let enabled: HashSet<&[u8]> = config
668 let enabled: HashSet<&[u8]> = config
652 .get_section_keys(b"extensions")
669 .get_section_keys(b"extensions")
653 .into_iter()
670 .into_iter()
654 .map(|extension| {
671 .map(|extension| {
655 // Ignore extension suboptions. Only `required` exists for now.
672 // Ignore extension suboptions. Only `required` exists for now.
656 // `rhg` either supports an extension or doesn't, so it doesn't
673 // `rhg` either supports an extension or doesn't, so it doesn't
657 // make sense to consider the loading of an extension.
674 // make sense to consider the loading of an extension.
658 extension.split_2(b':').unwrap_or((extension, b"")).0
675 extension.split_2(b':').unwrap_or((extension, b"")).0
659 })
676 })
660 .collect();
677 .collect();
661
678
662 let mut unsupported = enabled;
679 let mut unsupported = enabled;
663 for supported in SUPPORTED_EXTENSIONS {
680 for supported in SUPPORTED_EXTENSIONS {
664 unsupported.remove(supported);
681 unsupported.remove(supported);
665 }
682 }
666
683
667 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
684 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
668 {
685 {
669 for ignored in ignored_list {
686 for ignored in ignored_list {
670 unsupported.remove(ignored.as_slice());
687 unsupported.remove(ignored.as_slice());
671 }
688 }
672 }
689 }
673
690
674 if unsupported.is_empty() {
691 if unsupported.is_empty() {
675 Ok(())
692 Ok(())
676 } else {
693 } else {
677 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
694 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
678 // Sort the extensions to get a stable output
695 // Sort the extensions to get a stable output
679 unsupported.sort();
696 unsupported.sort();
680 Err(CommandError::UnsupportedFeature {
697 Err(CommandError::UnsupportedFeature {
681 message: format_bytes!(
698 message: format_bytes!(
682 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
699 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
683 join(unsupported, b", ")
700 join(unsupported, b", ")
684 ),
701 ),
685 })
702 })
686 }
703 }
687 }
704 }
688
705
689 fn check_unsupported(
706 fn check_unsupported(
690 config: &Config,
707 config: &Config,
691 repo: Result<&Repo, &NoRepoInCwdError>,
708 repo: Result<&Repo, &NoRepoInCwdError>,
692 ) -> Result<(), CommandError> {
709 ) -> Result<(), CommandError> {
693 check_extensions(config)?;
710 check_extensions(config)?;
694
711
695 if std::env::var_os("HG_PENDING").is_some() {
712 if std::env::var_os("HG_PENDING").is_some() {
696 // TODO: only if the value is `== repo.working_directory`?
713 // TODO: only if the value is `== repo.working_directory`?
697 // What about relative v.s. absolute paths?
714 // What about relative v.s. absolute paths?
698 Err(CommandError::unsupported("$HG_PENDING"))?
715 Err(CommandError::unsupported("$HG_PENDING"))?
699 }
716 }
700
717
701 if let Ok(repo) = repo {
718 if let Ok(repo) = repo {
702 if repo.has_subrepos()? {
719 if repo.has_subrepos()? {
703 Err(CommandError::unsupported("sub-repositories"))?
720 Err(CommandError::unsupported("sub-repositories"))?
704 }
721 }
705 }
722 }
706
723
707 if config.has_non_empty_section(b"encode") {
724 if config.has_non_empty_section(b"encode") {
708 Err(CommandError::unsupported("[encode] config"))?
725 Err(CommandError::unsupported("[encode] config"))?
709 }
726 }
710
727
711 if config.has_non_empty_section(b"decode") {
728 if config.has_non_empty_section(b"decode") {
712 Err(CommandError::unsupported("[decode] config"))?
729 Err(CommandError::unsupported("[decode] config"))?
713 }
730 }
714
731
715 Ok(())
732 Ok(())
716 }
733 }
General Comments 0
You need to be logged in to leave comments. Login now