##// END OF EJS Templates
rhg: refactor to pass argv down, instead of caling args_os()...
Arseniy Alekseyev -
r49943:86c49b00 default draft
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,730 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(argv.iter(), exit_code(
129 &result,
130 &result,
130 // TODO: show a warning or combine with original error if
131 // TODO: show a warning or combine with original error if
131 // `get_bool` returns an error
132 // `get_bool` returns an error
132 config
133 config
133 .get_bool(b"ui", b"detailed-exit-code")
134 .get_bool(b"ui", b"detailed-exit-code")
134 .unwrap_or(false),
135 .unwrap_or(false),
135 ));
136 ));
136 result
137 result
137 } else {
138 } else {
138 run(&invocation)
139 run(&invocation)
139 }
140 }
140 }
141 }
141
142
142 fn main() {
143 fn rhg_main(argv: Vec<OsString>) -> ! {
143 // Run this first, before we find out if the blackbox extension is even
144 // 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
145 // enabled, in order to include everything in-between in the duration
145 // measurements. Reading config files can be slow if they’re on NFS.
146 // measurements. Reading config files can be slow if they’re on NFS.
146 let process_start_time = blackbox::ProcessStartTime::now();
147 let process_start_time = blackbox::ProcessStartTime::now();
147
148
148 env_logger::init();
149 env_logger::init();
149
150
150 let early_args = EarlyArgs::parse(std::env::args_os());
151 let early_args = EarlyArgs::parse(&argv);
151
152
152 let initial_current_dir = early_args.cwd.map(|cwd| {
153 let initial_current_dir = early_args.cwd.map(|cwd| {
153 let cwd = get_path_from_bytes(&cwd);
154 let cwd = get_path_from_bytes(&cwd);
154 std::env::current_dir()
155 std::env::current_dir()
155 .and_then(|initial| {
156 .and_then(|initial| {
156 std::env::set_current_dir(cwd)?;
157 std::env::set_current_dir(cwd)?;
157 Ok(initial)
158 Ok(initial)
158 })
159 })
159 .unwrap_or_else(|error| {
160 .unwrap_or_else(|error| {
160 exit(
161 exit(
162 &argv,
161 &None,
163 &None,
162 &Ui::new_infallible(&Config::empty()),
164 &Ui::new_infallible(&Config::empty()),
163 OnUnsupported::Abort,
165 OnUnsupported::Abort,
164 Err(CommandError::abort(format!(
166 Err(CommandError::abort(format!(
165 "abort: {}: '{}'",
167 "abort: {}: '{}'",
166 error,
168 error,
167 cwd.display()
169 cwd.display()
168 ))),
170 ))),
169 false,
171 false,
170 )
172 )
171 })
173 })
172 });
174 });
173
175
174 let mut non_repo_config =
176 let mut non_repo_config =
175 Config::load_non_repo().unwrap_or_else(|error| {
177 Config::load_non_repo().unwrap_or_else(|error| {
176 // Normally this is decided based on config, but we don’t have that
178 // Normally this is decided based on config, but we don’t have that
177 // available. As of this writing config loading never returns an
179 // available. As of this writing config loading never returns an
178 // "unsupported" error but that is not enforced by the type system.
180 // "unsupported" error but that is not enforced by the type system.
179 let on_unsupported = OnUnsupported::Abort;
181 let on_unsupported = OnUnsupported::Abort;
180
182
181 exit(
183 exit(
184 &argv,
182 &initial_current_dir,
185 &initial_current_dir,
183 &Ui::new_infallible(&Config::empty()),
186 &Ui::new_infallible(&Config::empty()),
184 on_unsupported,
187 on_unsupported,
185 Err(error.into()),
188 Err(error.into()),
186 false,
189 false,
187 )
190 )
188 });
191 });
189
192
190 non_repo_config
193 non_repo_config
191 .load_cli_args(early_args.config, early_args.color)
194 .load_cli_args(early_args.config, early_args.color)
192 .unwrap_or_else(|error| {
195 .unwrap_or_else(|error| {
193 exit(
196 exit(
197 &argv,
194 &initial_current_dir,
198 &initial_current_dir,
195 &Ui::new_infallible(&non_repo_config),
199 &Ui::new_infallible(&non_repo_config),
196 OnUnsupported::from_config(&non_repo_config),
200 OnUnsupported::from_config(&non_repo_config),
197 Err(error.into()),
201 Err(error.into()),
198 non_repo_config
202 non_repo_config
199 .get_bool(b"ui", b"detailed-exit-code")
203 .get_bool(b"ui", b"detailed-exit-code")
200 .unwrap_or(false),
204 .unwrap_or(false),
201 )
205 )
202 });
206 });
203
207
204 if let Some(repo_path_bytes) = &early_args.repo {
208 if let Some(repo_path_bytes) = &early_args.repo {
205 lazy_static::lazy_static! {
209 lazy_static::lazy_static! {
206 static ref SCHEME_RE: regex::bytes::Regex =
210 static ref SCHEME_RE: regex::bytes::Regex =
207 // Same as `_matchscheme` in `mercurial/util.py`
211 // Same as `_matchscheme` in `mercurial/util.py`
208 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
212 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
209 }
213 }
210 if SCHEME_RE.is_match(&repo_path_bytes) {
214 if SCHEME_RE.is_match(&repo_path_bytes) {
211 exit(
215 exit(
216 &argv,
212 &initial_current_dir,
217 &initial_current_dir,
213 &Ui::new_infallible(&non_repo_config),
218 &Ui::new_infallible(&non_repo_config),
214 OnUnsupported::from_config(&non_repo_config),
219 OnUnsupported::from_config(&non_repo_config),
215 Err(CommandError::UnsupportedFeature {
220 Err(CommandError::UnsupportedFeature {
216 message: format_bytes!(
221 message: format_bytes!(
217 b"URL-like --repository {}",
222 b"URL-like --repository {}",
218 repo_path_bytes
223 repo_path_bytes
219 ),
224 ),
220 }),
225 }),
221 // TODO: show a warning or combine with original error if
226 // TODO: show a warning or combine with original error if
222 // `get_bool` returns an error
227 // `get_bool` returns an error
223 non_repo_config
228 non_repo_config
224 .get_bool(b"ui", b"detailed-exit-code")
229 .get_bool(b"ui", b"detailed-exit-code")
225 .unwrap_or(false),
230 .unwrap_or(false),
226 )
231 )
227 }
232 }
228 }
233 }
229 let repo_arg = early_args.repo.unwrap_or(Vec::new());
234 let repo_arg = early_args.repo.unwrap_or(Vec::new());
230 let repo_path: Option<PathBuf> = {
235 let repo_path: Option<PathBuf> = {
231 if repo_arg.is_empty() {
236 if repo_arg.is_empty() {
232 None
237 None
233 } else {
238 } else {
234 let local_config = {
239 let local_config = {
235 if std::env::var_os("HGRCSKIPREPO").is_none() {
240 if std::env::var_os("HGRCSKIPREPO").is_none() {
236 // TODO: handle errors from find_repo_root
241 // TODO: handle errors from find_repo_root
237 if let Ok(current_dir_path) = Repo::find_repo_root() {
242 if let Ok(current_dir_path) = Repo::find_repo_root() {
238 let config_files = vec![
243 let config_files = vec![
239 ConfigSource::AbsPath(
244 ConfigSource::AbsPath(
240 current_dir_path.join(".hg/hgrc"),
245 current_dir_path.join(".hg/hgrc"),
241 ),
246 ),
242 ConfigSource::AbsPath(
247 ConfigSource::AbsPath(
243 current_dir_path.join(".hg/hgrc-not-shared"),
248 current_dir_path.join(".hg/hgrc-not-shared"),
244 ),
249 ),
245 ];
250 ];
246 // TODO: handle errors from
251 // TODO: handle errors from
247 // `load_from_explicit_sources`
252 // `load_from_explicit_sources`
248 Config::load_from_explicit_sources(config_files).ok()
253 Config::load_from_explicit_sources(config_files).ok()
249 } else {
254 } else {
250 None
255 None
251 }
256 }
252 } else {
257 } else {
253 None
258 None
254 }
259 }
255 };
260 };
256
261
257 let non_repo_config_val = {
262 let non_repo_config_val = {
258 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
263 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
259 match &non_repo_val {
264 match &non_repo_val {
260 Some(val) if val.len() > 0 => home::home_dir()
265 Some(val) if val.len() > 0 => home::home_dir()
261 .unwrap_or_else(|| PathBuf::from("~"))
266 .unwrap_or_else(|| PathBuf::from("~"))
262 .join(get_path_from_bytes(val))
267 .join(get_path_from_bytes(val))
263 .canonicalize()
268 .canonicalize()
264 // TODO: handle error and make it similar to python
269 // TODO: handle error and make it similar to python
265 // implementation maybe?
270 // implementation maybe?
266 .ok(),
271 .ok(),
267 _ => None,
272 _ => None,
268 }
273 }
269 };
274 };
270
275
271 let config_val = match &local_config {
276 let config_val = match &local_config {
272 None => non_repo_config_val,
277 None => non_repo_config_val,
273 Some(val) => {
278 Some(val) => {
274 let local_config_val = val.get(b"paths", &repo_arg);
279 let local_config_val = val.get(b"paths", &repo_arg);
275 match &local_config_val {
280 match &local_config_val {
276 Some(val) if val.len() > 0 => {
281 Some(val) if val.len() > 0 => {
277 // presence of a local_config assures that
282 // presence of a local_config assures that
278 // current_dir
283 // current_dir
279 // wont result in an Error
284 // wont result in an Error
280 let canpath = hg::utils::current_dir()
285 let canpath = hg::utils::current_dir()
281 .unwrap()
286 .unwrap()
282 .join(get_path_from_bytes(val))
287 .join(get_path_from_bytes(val))
283 .canonicalize();
288 .canonicalize();
284 canpath.ok().or(non_repo_config_val)
289 canpath.ok().or(non_repo_config_val)
285 }
290 }
286 _ => non_repo_config_val,
291 _ => non_repo_config_val,
287 }
292 }
288 }
293 }
289 };
294 };
290 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
295 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
291 }
296 }
292 };
297 };
293
298
294 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
299 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
295 {
300 {
296 Ok(repo) => Ok(repo),
301 Ok(repo) => Ok(repo),
297 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
302 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
298 // Not finding a repo is not fatal yet, if `-R` was not given
303 // Not finding a repo is not fatal yet, if `-R` was not given
299 Err(NoRepoInCwdError { cwd: at })
304 Err(NoRepoInCwdError { cwd: at })
300 }
305 }
301 Err(error) => exit(
306 Err(error) => exit(
307 &argv,
302 &initial_current_dir,
308 &initial_current_dir,
303 &Ui::new_infallible(&non_repo_config),
309 &Ui::new_infallible(&non_repo_config),
304 OnUnsupported::from_config(&non_repo_config),
310 OnUnsupported::from_config(&non_repo_config),
305 Err(error.into()),
311 Err(error.into()),
306 // TODO: show a warning or combine with original error if
312 // TODO: show a warning or combine with original error if
307 // `get_bool` returns an error
313 // `get_bool` returns an error
308 non_repo_config
314 non_repo_config
309 .get_bool(b"ui", b"detailed-exit-code")
315 .get_bool(b"ui", b"detailed-exit-code")
310 .unwrap_or(false),
316 .unwrap_or(false),
311 ),
317 ),
312 };
318 };
313
319
314 let config = if let Ok(repo) = &repo_result {
320 let config = if let Ok(repo) = &repo_result {
315 repo.config()
321 repo.config()
316 } else {
322 } else {
317 &non_repo_config
323 &non_repo_config
318 };
324 };
319 let ui = Ui::new(&config).unwrap_or_else(|error| {
325 let ui = Ui::new(&config).unwrap_or_else(|error| {
320 exit(
326 exit(
327 &argv,
321 &initial_current_dir,
328 &initial_current_dir,
322 &Ui::new_infallible(&config),
329 &Ui::new_infallible(&config),
323 OnUnsupported::from_config(&config),
330 OnUnsupported::from_config(&config),
324 Err(error.into()),
331 Err(error.into()),
325 config
332 config
326 .get_bool(b"ui", b"detailed-exit-code")
333 .get_bool(b"ui", b"detailed-exit-code")
327 .unwrap_or(false),
334 .unwrap_or(false),
328 )
335 )
329 });
336 });
330 let on_unsupported = OnUnsupported::from_config(config);
337 let on_unsupported = OnUnsupported::from_config(config);
331
338
332 let result = main_with_result(
339 let result = main_with_result(
340 argv.iter().map(|s| s.to_owned()).collect(),
333 &process_start_time,
341 &process_start_time,
334 &ui,
342 &ui,
335 repo_result.as_ref(),
343 repo_result.as_ref(),
336 config,
344 config,
337 );
345 );
338 exit(
346 exit(
347 &argv,
339 &initial_current_dir,
348 &initial_current_dir,
340 &ui,
349 &ui,
341 on_unsupported,
350 on_unsupported,
342 result,
351 result,
343 // TODO: show a warning or combine with original error if `get_bool`
352 // TODO: show a warning or combine with original error if `get_bool`
344 // returns an error
353 // returns an error
345 config
354 config
346 .get_bool(b"ui", b"detailed-exit-code")
355 .get_bool(b"ui", b"detailed-exit-code")
347 .unwrap_or(false),
356 .unwrap_or(false),
348 )
357 )
349 }
358 }
350
359
360 fn main() -> ! {
361 rhg_main(std::env::args_os().collect())
362 }
363
351 fn exit_code(
364 fn exit_code(
352 result: &Result<(), CommandError>,
365 result: &Result<(), CommandError>,
353 use_detailed_exit_code: bool,
366 use_detailed_exit_code: bool,
354 ) -> i32 {
367 ) -> i32 {
355 match result {
368 match result {
356 Ok(()) => exit_codes::OK,
369 Ok(()) => exit_codes::OK,
357 Err(CommandError::Abort {
370 Err(CommandError::Abort {
358 message: _,
371 message: _,
359 detailed_exit_code,
372 detailed_exit_code,
360 }) => {
373 }) => {
361 if use_detailed_exit_code {
374 if use_detailed_exit_code {
362 *detailed_exit_code
375 *detailed_exit_code
363 } else {
376 } else {
364 exit_codes::ABORT
377 exit_codes::ABORT
365 }
378 }
366 }
379 }
367 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
380 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
368
381
369 // Exit with a specific code and no error message to let a potential
382 // Exit with a specific code and no error message to let a potential
370 // wrapper script fallback to Python-based Mercurial.
383 // wrapper script fallback to Python-based Mercurial.
371 Err(CommandError::UnsupportedFeature { .. }) => {
384 Err(CommandError::UnsupportedFeature { .. }) => {
372 exit_codes::UNIMPLEMENTED
385 exit_codes::UNIMPLEMENTED
373 }
386 }
374 }
387 }
375 }
388 }
376
389
377 fn exit(
390 fn exit<'a>(
391 original_args: &'a [OsString],
378 initial_current_dir: &Option<PathBuf>,
392 initial_current_dir: &Option<PathBuf>,
379 ui: &Ui,
393 ui: &Ui,
380 mut on_unsupported: OnUnsupported,
394 mut on_unsupported: OnUnsupported,
381 result: Result<(), CommandError>,
395 result: Result<(), CommandError>,
382 use_detailed_exit_code: bool,
396 use_detailed_exit_code: bool,
383 ) -> ! {
397 ) -> ! {
384 if let (
398 if let (
385 OnUnsupported::Fallback { executable },
399 OnUnsupported::Fallback { executable },
386 Err(CommandError::UnsupportedFeature { message }),
400 Err(CommandError::UnsupportedFeature { message }),
387 ) = (&on_unsupported, &result)
401 ) = (&on_unsupported, &result)
388 {
402 {
389 let mut args = std::env::args_os();
403 let mut args = original_args.iter();
390 let executable = match executable {
404 let executable = match executable {
391 None => {
405 None => {
392 exit_no_fallback(
406 exit_no_fallback(
393 ui,
407 ui,
394 OnUnsupported::Abort,
408 OnUnsupported::Abort,
395 Err(CommandError::abort(
409 Err(CommandError::abort(
396 "abort: 'rhg.on-unsupported=fallback' without \
410 "abort: 'rhg.on-unsupported=fallback' without \
397 'rhg.fallback-executable' set.",
411 'rhg.fallback-executable' set.",
398 )),
412 )),
399 false,
413 false,
400 );
414 );
401 }
415 }
402 Some(executable) => executable,
416 Some(executable) => executable,
403 };
417 };
404 let executable_path = get_path_from_bytes(&executable);
418 let executable_path = get_path_from_bytes(&executable);
405 let this_executable = args.next().expect("exepcted argv[0] to exist");
419 let this_executable = args.next().expect("exepcted argv[0] to exist");
406 if executable_path == &PathBuf::from(this_executable) {
420 if executable_path == &PathBuf::from(this_executable) {
407 // Avoid spawning infinitely many processes until resource
421 // Avoid spawning infinitely many processes until resource
408 // exhaustion.
422 // exhaustion.
409 let _ = ui.write_stderr(&format_bytes!(
423 let _ = ui.write_stderr(&format_bytes!(
410 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
424 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
411 points to `rhg` itself.\n",
425 points to `rhg` itself.\n",
412 executable
426 executable
413 ));
427 ));
414 on_unsupported = OnUnsupported::Abort
428 on_unsupported = OnUnsupported::Abort
415 } else {
429 } else {
416 log::debug!("falling back (see trace-level log)");
430 log::debug!("falling back (see trace-level log)");
417 log::trace!("{}", local_to_utf8(message));
431 log::trace!("{}", local_to_utf8(message));
418 // `args` is now `argv[1..]` since we’ve already consumed
432 // `args` is now `argv[1..]` since we’ve already consumed
419 // `argv[0]`
433 // `argv[0]`
420 let mut command = Command::new(executable_path);
434 let mut command = Command::new(executable_path);
421 command.args(args);
435 command.args(args);
422 if let Some(initial) = initial_current_dir {
436 if let Some(initial) = initial_current_dir {
423 command.current_dir(initial);
437 command.current_dir(initial);
424 }
438 }
425 let result = command.status();
439 let result = command.status();
426 match result {
440 match result {
427 Ok(status) => std::process::exit(
441 Ok(status) => std::process::exit(
428 status.code().unwrap_or(exit_codes::ABORT),
442 status.code().unwrap_or(exit_codes::ABORT),
429 ),
443 ),
430 Err(error) => {
444 Err(error) => {
431 let _ = ui.write_stderr(&format_bytes!(
445 let _ = ui.write_stderr(&format_bytes!(
432 b"tried to fall back to a '{}' sub-process but got error {}\n",
446 b"tried to fall back to a '{}' sub-process but got error {}\n",
433 executable, format_bytes::Utf8(error)
447 executable, format_bytes::Utf8(error)
434 ));
448 ));
435 on_unsupported = OnUnsupported::Abort
449 on_unsupported = OnUnsupported::Abort
436 }
450 }
437 }
451 }
438 }
452 }
439 }
453 }
440 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
454 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
441 }
455 }
442
456
443 fn exit_no_fallback(
457 fn exit_no_fallback(
444 ui: &Ui,
458 ui: &Ui,
445 on_unsupported: OnUnsupported,
459 on_unsupported: OnUnsupported,
446 result: Result<(), CommandError>,
460 result: Result<(), CommandError>,
447 use_detailed_exit_code: bool,
461 use_detailed_exit_code: bool,
448 ) -> ! {
462 ) -> ! {
449 match &result {
463 match &result {
450 Ok(_) => {}
464 Ok(_) => {}
451 Err(CommandError::Unsuccessful) => {}
465 Err(CommandError::Unsuccessful) => {}
452 Err(CommandError::Abort {
466 Err(CommandError::Abort {
453 message,
467 message,
454 detailed_exit_code: _,
468 detailed_exit_code: _,
455 }) => {
469 }) => {
456 if !message.is_empty() {
470 if !message.is_empty() {
457 // Ignore errors when writing to stderr, we’re already exiting
471 // Ignore errors when writing to stderr, we’re already exiting
458 // with failure code so there’s not much more we can do.
472 // with failure code so there’s not much more we can do.
459 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
473 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
460 }
474 }
461 }
475 }
462 Err(CommandError::UnsupportedFeature { message }) => {
476 Err(CommandError::UnsupportedFeature { message }) => {
463 match on_unsupported {
477 match on_unsupported {
464 OnUnsupported::Abort => {
478 OnUnsupported::Abort => {
465 let _ = ui.write_stderr(&format_bytes!(
479 let _ = ui.write_stderr(&format_bytes!(
466 b"unsupported feature: {}\n",
480 b"unsupported feature: {}\n",
467 message
481 message
468 ));
482 ));
469 }
483 }
470 OnUnsupported::AbortSilent => {}
484 OnUnsupported::AbortSilent => {}
471 OnUnsupported::Fallback { .. } => unreachable!(),
485 OnUnsupported::Fallback { .. } => unreachable!(),
472 }
486 }
473 }
487 }
474 }
488 }
475 std::process::exit(exit_code(&result, use_detailed_exit_code))
489 std::process::exit(exit_code(&result, use_detailed_exit_code))
476 }
490 }
477
491
478 macro_rules! subcommands {
492 macro_rules! subcommands {
479 ($( $command: ident )+) => {
493 ($( $command: ident )+) => {
480 mod commands {
494 mod commands {
481 $(
495 $(
482 pub mod $command;
496 pub mod $command;
483 )+
497 )+
484 }
498 }
485
499
486 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
500 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
487 app
501 app
488 $(
502 $(
489 .subcommand(commands::$command::args())
503 .subcommand(commands::$command::args())
490 )+
504 )+
491 }
505 }
492
506
493 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
507 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
494
508
495 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
509 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
496 match name {
510 match name {
497 $(
511 $(
498 stringify!($command) => Some(commands::$command::run),
512 stringify!($command) => Some(commands::$command::run),
499 )+
513 )+
500 _ => None,
514 _ => None,
501 }
515 }
502 }
516 }
503 };
517 };
504 }
518 }
505
519
506 subcommands! {
520 subcommands! {
507 cat
521 cat
508 debugdata
522 debugdata
509 debugrequirements
523 debugrequirements
510 debugignorerhg
524 debugignorerhg
511 files
525 files
512 root
526 root
513 config
527 config
514 status
528 status
515 }
529 }
516
530
517 pub struct CliInvocation<'a> {
531 pub struct CliInvocation<'a> {
518 ui: &'a Ui,
532 ui: &'a Ui,
519 subcommand_args: &'a ArgMatches<'a>,
533 subcommand_args: &'a ArgMatches<'a>,
520 config: &'a Config,
534 config: &'a Config,
521 /// References inside `Result` is a bit peculiar but allow
535 /// References inside `Result` is a bit peculiar but allow
522 /// `invocation.repo?` to work out with `&CliInvocation` since this
536 /// `invocation.repo?` to work out with `&CliInvocation` since this
523 /// `Result` type is `Copy`.
537 /// `Result` type is `Copy`.
524 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
538 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
525 }
539 }
526
540
527 struct NoRepoInCwdError {
541 struct NoRepoInCwdError {
528 cwd: PathBuf,
542 cwd: PathBuf,
529 }
543 }
530
544
531 /// CLI arguments to be parsed "early" in order to be able to read
545 /// 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,
546 /// configuration before using Clap. Ideally we would also use Clap for this,
533 /// see <https://github.com/clap-rs/clap/discussions/2366>.
547 /// see <https://github.com/clap-rs/clap/discussions/2366>.
534 ///
548 ///
535 /// These arguments are still declared when we do use Clap later, so that Clap
549 /// These arguments are still declared when we do use Clap later, so that Clap
536 /// does not return an error for their presence.
550 /// does not return an error for their presence.
537 struct EarlyArgs {
551 struct EarlyArgs {
538 /// Values of all `--config` arguments. (Possibly none)
552 /// Values of all `--config` arguments. (Possibly none)
539 config: Vec<Vec<u8>>,
553 config: Vec<Vec<u8>>,
540 /// Value of all the `--color` argument, if any.
554 /// Value of all the `--color` argument, if any.
541 color: Option<Vec<u8>>,
555 color: Option<Vec<u8>>,
542 /// Value of the `-R` or `--repository` argument, if any.
556 /// Value of the `-R` or `--repository` argument, if any.
543 repo: Option<Vec<u8>>,
557 repo: Option<Vec<u8>>,
544 /// Value of the `--cwd` argument, if any.
558 /// Value of the `--cwd` argument, if any.
545 cwd: Option<Vec<u8>>,
559 cwd: Option<Vec<u8>>,
546 }
560 }
547
561
548 impl EarlyArgs {
562 impl EarlyArgs {
549 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
563 fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
550 let mut args = args.into_iter().map(get_bytes_from_os_str);
564 let mut args = args.into_iter().map(get_bytes_from_os_str);
551 let mut config = Vec::new();
565 let mut config = Vec::new();
552 let mut color = None;
566 let mut color = None;
553 let mut repo = None;
567 let mut repo = None;
554 let mut cwd = None;
568 let mut cwd = None;
555 // Use `while let` instead of `for` so that we can also call
569 // Use `while let` instead of `for` so that we can also call
556 // `args.next()` inside the loop.
570 // `args.next()` inside the loop.
557 while let Some(arg) = args.next() {
571 while let Some(arg) = args.next() {
558 if arg == b"--config" {
572 if arg == b"--config" {
559 if let Some(value) = args.next() {
573 if let Some(value) = args.next() {
560 config.push(value)
574 config.push(value)
561 }
575 }
562 } else if let Some(value) = arg.drop_prefix(b"--config=") {
576 } else if let Some(value) = arg.drop_prefix(b"--config=") {
563 config.push(value.to_owned())
577 config.push(value.to_owned())
564 }
578 }
565
579
566 if arg == b"--color" {
580 if arg == b"--color" {
567 if let Some(value) = args.next() {
581 if let Some(value) = args.next() {
568 color = Some(value)
582 color = Some(value)
569 }
583 }
570 } else if let Some(value) = arg.drop_prefix(b"--color=") {
584 } else if let Some(value) = arg.drop_prefix(b"--color=") {
571 color = Some(value.to_owned())
585 color = Some(value.to_owned())
572 }
586 }
573
587
574 if arg == b"--cwd" {
588 if arg == b"--cwd" {
575 if let Some(value) = args.next() {
589 if let Some(value) = args.next() {
576 cwd = Some(value)
590 cwd = Some(value)
577 }
591 }
578 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
592 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
579 cwd = Some(value.to_owned())
593 cwd = Some(value.to_owned())
580 }
594 }
581
595
582 if arg == b"--repository" || arg == b"-R" {
596 if arg == b"--repository" || arg == b"-R" {
583 if let Some(value) = args.next() {
597 if let Some(value) = args.next() {
584 repo = Some(value)
598 repo = Some(value)
585 }
599 }
586 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
600 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
587 repo = Some(value.to_owned())
601 repo = Some(value.to_owned())
588 } else if let Some(value) = arg.drop_prefix(b"-R") {
602 } else if let Some(value) = arg.drop_prefix(b"-R") {
589 repo = Some(value.to_owned())
603 repo = Some(value.to_owned())
590 }
604 }
591 }
605 }
592 Self {
606 Self {
593 config,
607 config,
594 color,
608 color,
595 repo,
609 repo,
596 cwd,
610 cwd,
597 }
611 }
598 }
612 }
599 }
613 }
600
614
601 /// What to do when encountering some unsupported feature.
615 /// What to do when encountering some unsupported feature.
602 ///
616 ///
603 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
617 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
604 enum OnUnsupported {
618 enum OnUnsupported {
605 /// Print an error message describing what feature is not supported,
619 /// Print an error message describing what feature is not supported,
606 /// and exit with code 252.
620 /// and exit with code 252.
607 Abort,
621 Abort,
608 /// Silently exit with code 252.
622 /// Silently exit with code 252.
609 AbortSilent,
623 AbortSilent,
610 /// Try running a Python implementation
624 /// Try running a Python implementation
611 Fallback { executable: Option<Vec<u8>> },
625 Fallback { executable: Option<Vec<u8>> },
612 }
626 }
613
627
614 impl OnUnsupported {
628 impl OnUnsupported {
615 const DEFAULT: Self = OnUnsupported::Abort;
629 const DEFAULT: Self = OnUnsupported::Abort;
616
630
617 fn from_config(config: &Config) -> Self {
631 fn from_config(config: &Config) -> Self {
618 match config
632 match config
619 .get(b"rhg", b"on-unsupported")
633 .get(b"rhg", b"on-unsupported")
620 .map(|value| value.to_ascii_lowercase())
634 .map(|value| value.to_ascii_lowercase())
621 .as_deref()
635 .as_deref()
622 {
636 {
623 Some(b"abort") => OnUnsupported::Abort,
637 Some(b"abort") => OnUnsupported::Abort,
624 Some(b"abort-silent") => OnUnsupported::AbortSilent,
638 Some(b"abort-silent") => OnUnsupported::AbortSilent,
625 Some(b"fallback") => OnUnsupported::Fallback {
639 Some(b"fallback") => OnUnsupported::Fallback {
626 executable: config
640 executable: config
627 .get(b"rhg", b"fallback-executable")
641 .get(b"rhg", b"fallback-executable")
628 .map(|x| x.to_owned()),
642 .map(|x| x.to_owned()),
629 },
643 },
630 None => Self::DEFAULT,
644 None => Self::DEFAULT,
631 Some(_) => {
645 Some(_) => {
632 // TODO: warn about unknown config value
646 // TODO: warn about unknown config value
633 Self::DEFAULT
647 Self::DEFAULT
634 }
648 }
635 }
649 }
636 }
650 }
637 }
651 }
638
652
639 /// The `*` extension is an edge-case for config sub-options that apply to all
653 /// 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
654 /// extensions. For now, only `:required` exists, but that may change in the
641 /// future.
655 /// future.
642 const SUPPORTED_EXTENSIONS: &[&[u8]] =
656 const SUPPORTED_EXTENSIONS: &[&[u8]] =
643 &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
657 &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
644
658
645 fn check_extensions(config: &Config) -> Result<(), CommandError> {
659 fn check_extensions(config: &Config) -> Result<(), CommandError> {
646 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
660 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
647 // All extensions are to be ignored, nothing to do here
661 // All extensions are to be ignored, nothing to do here
648 return Ok(());
662 return Ok(());
649 }
663 }
650
664
651 let enabled: HashSet<&[u8]> = config
665 let enabled: HashSet<&[u8]> = config
652 .get_section_keys(b"extensions")
666 .get_section_keys(b"extensions")
653 .into_iter()
667 .into_iter()
654 .map(|extension| {
668 .map(|extension| {
655 // Ignore extension suboptions. Only `required` exists for now.
669 // Ignore extension suboptions. Only `required` exists for now.
656 // `rhg` either supports an extension or doesn't, so it doesn't
670 // `rhg` either supports an extension or doesn't, so it doesn't
657 // make sense to consider the loading of an extension.
671 // make sense to consider the loading of an extension.
658 extension.split_2(b':').unwrap_or((extension, b"")).0
672 extension.split_2(b':').unwrap_or((extension, b"")).0
659 })
673 })
660 .collect();
674 .collect();
661
675
662 let mut unsupported = enabled;
676 let mut unsupported = enabled;
663 for supported in SUPPORTED_EXTENSIONS {
677 for supported in SUPPORTED_EXTENSIONS {
664 unsupported.remove(supported);
678 unsupported.remove(supported);
665 }
679 }
666
680
667 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
681 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
668 {
682 {
669 for ignored in ignored_list {
683 for ignored in ignored_list {
670 unsupported.remove(ignored.as_slice());
684 unsupported.remove(ignored.as_slice());
671 }
685 }
672 }
686 }
673
687
674 if unsupported.is_empty() {
688 if unsupported.is_empty() {
675 Ok(())
689 Ok(())
676 } else {
690 } else {
677 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
691 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
678 // Sort the extensions to get a stable output
692 // Sort the extensions to get a stable output
679 unsupported.sort();
693 unsupported.sort();
680 Err(CommandError::UnsupportedFeature {
694 Err(CommandError::UnsupportedFeature {
681 message: format_bytes!(
695 message: format_bytes!(
682 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
696 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
683 join(unsupported, b", ")
697 join(unsupported, b", ")
684 ),
698 ),
685 })
699 })
686 }
700 }
687 }
701 }
688
702
689 fn check_unsupported(
703 fn check_unsupported(
690 config: &Config,
704 config: &Config,
691 repo: Result<&Repo, &NoRepoInCwdError>,
705 repo: Result<&Repo, &NoRepoInCwdError>,
692 ) -> Result<(), CommandError> {
706 ) -> Result<(), CommandError> {
693 check_extensions(config)?;
707 check_extensions(config)?;
694
708
695 if std::env::var_os("HG_PENDING").is_some() {
709 if std::env::var_os("HG_PENDING").is_some() {
696 // TODO: only if the value is `== repo.working_directory`?
710 // TODO: only if the value is `== repo.working_directory`?
697 // What about relative v.s. absolute paths?
711 // What about relative v.s. absolute paths?
698 Err(CommandError::unsupported("$HG_PENDING"))?
712 Err(CommandError::unsupported("$HG_PENDING"))?
699 }
713 }
700
714
701 if let Ok(repo) = repo {
715 if let Ok(repo) = repo {
702 if repo.has_subrepos()? {
716 if repo.has_subrepos()? {
703 Err(CommandError::unsupported("sub-repositories"))?
717 Err(CommandError::unsupported("sub-repositories"))?
704 }
718 }
705 }
719 }
706
720
707 if config.has_non_empty_section(b"encode") {
721 if config.has_non_empty_section(b"encode") {
708 Err(CommandError::unsupported("[encode] config"))?
722 Err(CommandError::unsupported("[encode] config"))?
709 }
723 }
710
724
711 if config.has_non_empty_section(b"decode") {
725 if config.has_non_empty_section(b"decode") {
712 Err(CommandError::unsupported("[decode] config"))?
726 Err(CommandError::unsupported("[decode] config"))?
713 }
727 }
714
728
715 Ok(())
729 Ok(())
716 }
730 }
General Comments 0
You need to be logged in to leave comments. Login now