Show More
@@ -1,161 +1,163 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 |
|
8 | |||
9 | const ONE_MEBIBYTE: u64 = 1 << 20; |
|
9 | const ONE_MEBIBYTE: u64 = 1 << 20; | |
10 |
|
10 | |||
11 | // TODO: somehow keep defaults in sync with `configitem` in `hgext/blackbox.py` |
|
11 | // TODO: somehow keep defaults in sync with `configitem` in `hgext/blackbox.py` | |
12 | const DEFAULT_MAX_SIZE: u64 = ONE_MEBIBYTE; |
|
12 | const DEFAULT_MAX_SIZE: u64 = ONE_MEBIBYTE; | |
13 | const DEFAULT_MAX_FILES: u32 = 7; |
|
13 | const DEFAULT_MAX_FILES: u32 = 7; | |
14 |
|
14 | |||
15 | // Python does not support %.3f, only %f |
|
15 | // Python does not support %.3f, only %f | |
16 | const DEFAULT_DATE_FORMAT: &str = "%Y/%m/%d %H:%M:%S%.3f"; |
|
16 | const DEFAULT_DATE_FORMAT: &str = "%Y/%m/%d %H:%M:%S%.3f"; | |
17 |
|
17 | |||
18 | type DateTime = chrono::DateTime<chrono::Local>; |
|
18 | type DateTime = chrono::DateTime<chrono::Local>; | |
19 |
|
19 | |||
20 | pub struct ProcessStartTime { |
|
20 | pub struct ProcessStartTime { | |
21 | /// For measuring duration |
|
21 | /// For measuring duration | |
22 | monotonic_clock: std::time::Instant, |
|
22 | monotonic_clock: std::time::Instant, | |
23 | /// For formatting with year, month, day, etc. |
|
23 | /// For formatting with year, month, day, etc. | |
24 | calendar_based: DateTime, |
|
24 | calendar_based: DateTime, | |
25 | } |
|
25 | } | |
26 |
|
26 | |||
27 | impl ProcessStartTime { |
|
27 | impl ProcessStartTime { | |
28 | pub fn now() -> Self { |
|
28 | pub fn now() -> Self { | |
29 | Self { |
|
29 | Self { | |
30 | monotonic_clock: std::time::Instant::now(), |
|
30 | monotonic_clock: std::time::Instant::now(), | |
31 | calendar_based: chrono::Local::now(), |
|
31 | calendar_based: chrono::Local::now(), | |
32 | } |
|
32 | } | |
33 | } |
|
33 | } | |
34 | } |
|
34 | } | |
35 |
|
35 | |||
36 | pub struct Blackbox<'a> { |
|
36 | pub struct Blackbox<'a> { | |
37 | process_start_time: &'a ProcessStartTime, |
|
37 | process_start_time: &'a ProcessStartTime, | |
38 | /// Do nothing if this is `None` |
|
38 | /// Do nothing if this is `None` | |
39 | configured: Option<ConfiguredBlackbox<'a>>, |
|
39 | configured: Option<ConfiguredBlackbox<'a>>, | |
40 | } |
|
40 | } | |
41 |
|
41 | |||
42 | struct ConfiguredBlackbox<'a> { |
|
42 | struct ConfiguredBlackbox<'a> { | |
43 | repo: &'a Repo, |
|
43 | repo: &'a Repo, | |
44 | max_size: u64, |
|
44 | max_size: u64, | |
45 | max_files: u32, |
|
45 | max_files: u32, | |
46 | date_format: &'a str, |
|
46 | date_format: &'a str, | |
47 | } |
|
47 | } | |
48 |
|
48 | |||
49 | impl<'a> Blackbox<'a> { |
|
49 | impl<'a> Blackbox<'a> { | |
50 | pub fn new( |
|
50 | pub fn new( | |
51 | invocation: &'a CliInvocation<'a>, |
|
51 | invocation: &'a CliInvocation<'a>, | |
52 | process_start_time: &'a ProcessStartTime, |
|
52 | process_start_time: &'a ProcessStartTime, | |
53 | ) -> Result<Self, HgError> { |
|
53 | ) -> Result<Self, HgError> { | |
54 | let configured = if let Ok(repo) = invocation.repo { |
|
54 | let configured = if let Ok(repo) = invocation.repo { | |
55 | let config = invocation.config(); |
|
55 | if invocation.config.get(b"extensions", b"blackbox").is_none() { | |
56 | if config.get(b"extensions", b"blackbox").is_none() { |
|
|||
57 | // The extension is not enabled |
|
56 | // The extension is not enabled | |
58 | None |
|
57 | None | |
59 | } else { |
|
58 | } else { | |
60 | Some(ConfiguredBlackbox { |
|
59 | Some(ConfiguredBlackbox { | |
61 | repo, |
|
60 | repo, | |
62 |
max_size: con |
|
61 | max_size: invocation | |
|
62 | .config | |||
63 | .get_byte_size(b"blackbox", b"maxsize")? |
|
63 | .get_byte_size(b"blackbox", b"maxsize")? | |
64 | .unwrap_or(DEFAULT_MAX_SIZE), |
|
64 | .unwrap_or(DEFAULT_MAX_SIZE), | |
65 |
max_files: con |
|
65 | max_files: invocation | |
|
66 | .config | |||
66 | .get_u32(b"blackbox", b"maxfiles")? |
|
67 | .get_u32(b"blackbox", b"maxfiles")? | |
67 | .unwrap_or(DEFAULT_MAX_FILES), |
|
68 | .unwrap_or(DEFAULT_MAX_FILES), | |
68 |
date_format: con |
|
69 | date_format: invocation | |
|
70 | .config | |||
69 | .get_str(b"blackbox", b"date-format")? |
|
71 | .get_str(b"blackbox", b"date-format")? | |
70 | .unwrap_or(DEFAULT_DATE_FORMAT), |
|
72 | .unwrap_or(DEFAULT_DATE_FORMAT), | |
71 | }) |
|
73 | }) | |
72 | } |
|
74 | } | |
73 | } else { |
|
75 | } else { | |
74 | // Without a local repository thereβs no `.hg/blackbox.log` to |
|
76 | // Without a local repository thereβs no `.hg/blackbox.log` to | |
75 | // write to. |
|
77 | // write to. | |
76 | None |
|
78 | None | |
77 | }; |
|
79 | }; | |
78 | Ok(Self { |
|
80 | Ok(Self { | |
79 | process_start_time, |
|
81 | process_start_time, | |
80 | configured, |
|
82 | configured, | |
81 | }) |
|
83 | }) | |
82 | } |
|
84 | } | |
83 |
|
85 | |||
84 | pub fn log_command_start(&self) { |
|
86 | pub fn log_command_start(&self) { | |
85 | if let Some(configured) = &self.configured { |
|
87 | if let Some(configured) = &self.configured { | |
86 | let message = format_bytes!(b"(rust) {}", format_cli_args()); |
|
88 | let message = format_bytes!(b"(rust) {}", format_cli_args()); | |
87 | configured.log(&self.process_start_time.calendar_based, &message); |
|
89 | configured.log(&self.process_start_time.calendar_based, &message); | |
88 | } |
|
90 | } | |
89 | } |
|
91 | } | |
90 |
|
92 | |||
91 | pub fn log_command_end(&self, exit_code: i32) { |
|
93 | pub fn log_command_end(&self, exit_code: i32) { | |
92 | if let Some(configured) = &self.configured { |
|
94 | if let Some(configured) = &self.configured { | |
93 | let now = chrono::Local::now(); |
|
95 | let now = chrono::Local::now(); | |
94 | let duration = self |
|
96 | let duration = self | |
95 | .process_start_time |
|
97 | .process_start_time | |
96 | .monotonic_clock |
|
98 | .monotonic_clock | |
97 | .elapsed() |
|
99 | .elapsed() | |
98 | .as_secs_f64(); |
|
100 | .as_secs_f64(); | |
99 | let message = format_bytes!( |
|
101 | let message = format_bytes!( | |
100 | b"(rust) {} exited {} after {} seconds", |
|
102 | b"(rust) {} exited {} after {} seconds", | |
101 | format_cli_args(), |
|
103 | format_cli_args(), | |
102 | exit_code, |
|
104 | exit_code, | |
103 | format_bytes::Utf8(format_args!("{:.03}", duration)) |
|
105 | format_bytes::Utf8(format_args!("{:.03}", duration)) | |
104 | ); |
|
106 | ); | |
105 | configured.log(&now, &message); |
|
107 | configured.log(&now, &message); | |
106 | } |
|
108 | } | |
107 | } |
|
109 | } | |
108 | } |
|
110 | } | |
109 |
|
111 | |||
110 | impl ConfiguredBlackbox<'_> { |
|
112 | impl ConfiguredBlackbox<'_> { | |
111 | fn log(&self, date_time: &DateTime, message: &[u8]) { |
|
113 | fn log(&self, date_time: &DateTime, message: &[u8]) { | |
112 | let date = format_bytes::Utf8(date_time.format(self.date_format)); |
|
114 | let date = format_bytes::Utf8(date_time.format(self.date_format)); | |
113 | let user = users::get_current_username().map(get_bytes_from_os_str); |
|
115 | let user = users::get_current_username().map(get_bytes_from_os_str); | |
114 | let user = user.as_deref().unwrap_or(b"???"); |
|
116 | let user = user.as_deref().unwrap_or(b"???"); | |
115 | let rev = format_bytes::Utf8(match self.repo.dirstate_parents() { |
|
117 | let rev = format_bytes::Utf8(match self.repo.dirstate_parents() { | |
116 | Ok(parents) if parents.p2 == hg::revlog::node::NULL_NODE => { |
|
118 | Ok(parents) if parents.p2 == hg::revlog::node::NULL_NODE => { | |
117 | format!("{:x}", parents.p1) |
|
119 | format!("{:x}", parents.p1) | |
118 | } |
|
120 | } | |
119 | Ok(parents) => format!("{:x}+{:x}", parents.p1, parents.p2), |
|
121 | Ok(parents) => format!("{:x}+{:x}", parents.p1, parents.p2), | |
120 | Err(_dirstate_corruption_error) => { |
|
122 | Err(_dirstate_corruption_error) => { | |
121 | // TODO: log a non-fatal warning to stderr |
|
123 | // TODO: log a non-fatal warning to stderr | |
122 | "???".to_owned() |
|
124 | "???".to_owned() | |
123 | } |
|
125 | } | |
124 | }); |
|
126 | }); | |
125 | let pid = std::process::id(); |
|
127 | let pid = std::process::id(); | |
126 | let line = format_bytes!( |
|
128 | let line = format_bytes!( | |
127 | b"{} {} @{} ({})> {}\n", |
|
129 | b"{} {} @{} ({})> {}\n", | |
128 | date, |
|
130 | date, | |
129 | user, |
|
131 | user, | |
130 | rev, |
|
132 | rev, | |
131 | pid, |
|
133 | pid, | |
132 | message |
|
134 | message | |
133 | ); |
|
135 | ); | |
134 | let result = |
|
136 | let result = | |
135 | hg::logging::LogFile::new(self.repo.hg_vfs(), "blackbox.log") |
|
137 | hg::logging::LogFile::new(self.repo.hg_vfs(), "blackbox.log") | |
136 | .max_size(Some(self.max_size)) |
|
138 | .max_size(Some(self.max_size)) | |
137 | .max_files(self.max_files) |
|
139 | .max_files(self.max_files) | |
138 | .write(&line); |
|
140 | .write(&line); | |
139 | match result { |
|
141 | match result { | |
140 | Ok(()) => {} |
|
142 | Ok(()) => {} | |
141 | Err(_io_error) => { |
|
143 | Err(_io_error) => { | |
142 | // TODO: log a non-fatal warning to stderr |
|
144 | // TODO: log a non-fatal warning to stderr | |
143 | } |
|
145 | } | |
144 | } |
|
146 | } | |
145 | } |
|
147 | } | |
146 | } |
|
148 | } | |
147 |
|
149 | |||
148 | fn format_cli_args() -> Vec<u8> { |
|
150 | fn format_cli_args() -> Vec<u8> { | |
149 | let mut args = std::env::args_os(); |
|
151 | let mut args = std::env::args_os(); | |
150 | let _ = args.next(); // Skip the first (or zeroth) arg, the name of the `rhg` executable |
|
152 | let _ = args.next(); // Skip the first (or zeroth) arg, the name of the `rhg` executable | |
151 | let mut args = args.map(|arg| shell_quote(&get_bytes_from_os_str(arg))); |
|
153 | let mut args = args.map(|arg| shell_quote(&get_bytes_from_os_str(arg))); | |
152 | let mut formatted = Vec::new(); |
|
154 | let mut formatted = Vec::new(); | |
153 | if let Some(arg) = args.next() { |
|
155 | if let Some(arg) = args.next() { | |
154 | formatted.extend(arg) |
|
156 | formatted.extend(arg) | |
155 | } |
|
157 | } | |
156 | for arg in args { |
|
158 | for arg in args { | |
157 | formatted.push(b' '); |
|
159 | formatted.push(b' '); | |
158 | formatted.extend(arg) |
|
160 | formatted.extend(arg) | |
159 | } |
|
161 | } | |
160 | formatted |
|
162 | formatted | |
161 | } |
|
163 | } |
@@ -1,36 +1,36 b'' | |||||
1 | use crate::error::CommandError; |
|
1 | use crate::error::CommandError; | |
2 | use clap::Arg; |
|
2 | use clap::Arg; | |
3 | use format_bytes::format_bytes; |
|
3 | use format_bytes::format_bytes; | |
4 | use hg::errors::HgError; |
|
4 | use hg::errors::HgError; | |
5 | use hg::utils::SliceExt; |
|
5 | use hg::utils::SliceExt; | |
6 |
|
6 | |||
7 | pub const HELP_TEXT: &str = " |
|
7 | pub const HELP_TEXT: &str = " | |
8 | With one argument of the form section.name, print just the value of that config item. |
|
8 | With one argument of the form section.name, print just the value of that config item. | |
9 | "; |
|
9 | "; | |
10 |
|
10 | |||
11 | pub fn args() -> clap::App<'static, 'static> { |
|
11 | pub fn args() -> clap::App<'static, 'static> { | |
12 | clap::SubCommand::with_name("config") |
|
12 | clap::SubCommand::with_name("config") | |
13 | .arg( |
|
13 | .arg( | |
14 | Arg::with_name("name") |
|
14 | Arg::with_name("name") | |
15 | .help("the section.name to print") |
|
15 | .help("the section.name to print") | |
16 | .value_name("NAME") |
|
16 | .value_name("NAME") | |
17 | .required(true) |
|
17 | .required(true) | |
18 | .takes_value(true), |
|
18 | .takes_value(true), | |
19 | ) |
|
19 | ) | |
20 | .about(HELP_TEXT) |
|
20 | .about(HELP_TEXT) | |
21 | } |
|
21 | } | |
22 |
|
22 | |||
23 | pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> { |
|
23 | pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> { | |
24 | let (section, name) = invocation |
|
24 | let (section, name) = invocation | |
25 | .subcommand_args |
|
25 | .subcommand_args | |
26 | .value_of("name") |
|
26 | .value_of("name") | |
27 | .expect("missing required CLI argument") |
|
27 | .expect("missing required CLI argument") | |
28 | .as_bytes() |
|
28 | .as_bytes() | |
29 | .split_2(b'.') |
|
29 | .split_2(b'.') | |
30 | .ok_or_else(|| HgError::abort(""))?; |
|
30 | .ok_or_else(|| HgError::abort(""))?; | |
31 |
|
31 | |||
32 |
let value = invocation.config |
|
32 | let value = invocation.config.get(section, name).unwrap_or(b""); | |
33 |
|
33 | |||
34 | invocation.ui.write_stdout(&format_bytes!(b"{}\n", value))?; |
|
34 | invocation.ui.write_stdout(&format_bytes!(b"{}\n", value))?; | |
35 | Ok(()) |
|
35 | Ok(()) | |
36 | } |
|
36 | } |
@@ -1,179 +1,228 b'' | |||||
1 | extern crate log; |
|
1 | extern crate log; | |
2 | use crate::ui::Ui; |
|
2 | use crate::ui::Ui; | |
3 | use clap::App; |
|
3 | use clap::App; | |
4 | use clap::AppSettings; |
|
4 | use clap::AppSettings; | |
5 | use clap::Arg; |
|
5 | use clap::Arg; | |
6 | use clap::ArgMatches; |
|
6 | use clap::ArgMatches; | |
7 | use format_bytes::format_bytes; |
|
7 | use format_bytes::format_bytes; | |
8 | use hg::config::Config; |
|
8 | use hg::config::Config; | |
9 | use hg::repo::{Repo, RepoError}; |
|
9 | use hg::repo::{Repo, RepoError}; | |
10 | use std::path::{Path, PathBuf}; |
|
10 | use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; | |
|
11 | use hg::utils::SliceExt; | |||
|
12 | use std::ffi::OsString; | |||
|
13 | use std::path::PathBuf; | |||
11 |
|
14 | |||
12 | mod blackbox; |
|
15 | mod blackbox; | |
13 | mod error; |
|
16 | mod error; | |
14 | mod exitcode; |
|
17 | mod exitcode; | |
15 | mod ui; |
|
18 | mod ui; | |
16 | use error::CommandError; |
|
19 | use error::CommandError; | |
17 |
|
20 | |||
18 | fn main_with_result( |
|
21 | fn main_with_result( | |
|
22 | process_start_time: &blackbox::ProcessStartTime, | |||
19 | ui: &ui::Ui, |
|
23 | ui: &ui::Ui, | |
20 | process_start_time: &blackbox::ProcessStartTime, |
|
24 | repo: Result<&Repo, &NoRepoInCwdError>, | |
|
25 | config: &Config, | |||
21 | ) -> Result<(), CommandError> { |
|
26 | ) -> Result<(), CommandError> { | |
22 | env_logger::init(); |
|
|||
23 | let app = App::new("rhg") |
|
27 | let app = App::new("rhg") | |
24 | .global_setting(AppSettings::AllowInvalidUtf8) |
|
28 | .global_setting(AppSettings::AllowInvalidUtf8) | |
25 | .setting(AppSettings::SubcommandRequired) |
|
29 | .setting(AppSettings::SubcommandRequired) | |
26 | .setting(AppSettings::VersionlessSubcommands) |
|
30 | .setting(AppSettings::VersionlessSubcommands) | |
27 | .arg( |
|
31 | .arg( | |
28 | Arg::with_name("repository") |
|
32 | Arg::with_name("repository") | |
29 | .help("repository root directory") |
|
33 | .help("repository root directory") | |
30 | .short("-R") |
|
34 | .short("-R") | |
31 | .long("--repository") |
|
35 | .long("--repository") | |
32 | .value_name("REPO") |
|
36 | .value_name("REPO") | |
33 | .takes_value(true) |
|
37 | .takes_value(true) | |
34 | // Both ok: `hg -R ./foo log` or `hg log -R ./foo` |
|
38 | // Both ok: `hg -R ./foo log` or `hg log -R ./foo` | |
35 | .global(true), |
|
39 | .global(true), | |
36 | ) |
|
40 | ) | |
37 | .arg( |
|
41 | .arg( | |
38 | Arg::with_name("config") |
|
42 | Arg::with_name("config") | |
39 | .help("set/override config option (use 'section.name=value')") |
|
43 | .help("set/override config option (use 'section.name=value')") | |
40 | .long("--config") |
|
44 | .long("--config") | |
41 | .value_name("CONFIG") |
|
45 | .value_name("CONFIG") | |
42 | .takes_value(true) |
|
46 | .takes_value(true) | |
43 | .global(true) |
|
47 | .global(true) | |
44 | // Ok: `--config section.key1=val --config section.key2=val2` |
|
48 | // Ok: `--config section.key1=val --config section.key2=val2` | |
45 | .multiple(true) |
|
49 | .multiple(true) | |
46 | // Not ok: `--config section.key1=val section.key2=val2` |
|
50 | // Not ok: `--config section.key1=val section.key2=val2` | |
47 | .number_of_values(1), |
|
51 | .number_of_values(1), | |
48 | ) |
|
52 | ) | |
49 | .version("0.0.1"); |
|
53 | .version("0.0.1"); | |
50 | let app = add_subcommand_args(app); |
|
54 | let app = add_subcommand_args(app); | |
51 |
|
55 | |||
52 | let matches = app.clone().get_matches_safe()?; |
|
56 | let matches = app.clone().get_matches_safe()?; | |
53 |
|
57 | |||
54 | let (subcommand_name, subcommand_matches) = matches.subcommand(); |
|
58 | let (subcommand_name, subcommand_matches) = matches.subcommand(); | |
55 | let run = subcommand_run_fn(subcommand_name) |
|
59 | let run = subcommand_run_fn(subcommand_name) | |
56 | .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired"); |
|
60 | .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired"); | |
57 | let subcommand_args = subcommand_matches |
|
61 | let subcommand_args = subcommand_matches | |
58 | .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired"); |
|
62 | .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired"); | |
59 |
|
63 | |||
60 | let config_args = matches |
|
|||
61 | .values_of_os("config") |
|
|||
62 | // Turn `Option::None` into an empty iterator: |
|
|||
63 | .into_iter() |
|
|||
64 | .flatten() |
|
|||
65 | .map(hg::utils::files::get_bytes_from_os_str); |
|
|||
66 | let non_repo_config = &hg::config::Config::load(config_args)?; |
|
|||
67 |
|
||||
68 | let repo_path = matches.value_of_os("repository").map(Path::new); |
|
|||
69 | let repo = match Repo::find(non_repo_config, repo_path) { |
|
|||
70 | Ok(repo) => Ok(repo), |
|
|||
71 | Err(RepoError::NotFound { at }) if repo_path.is_none() => { |
|
|||
72 | // Not finding a repo is not fatal yet, if `-R` was not given |
|
|||
73 | Err(NoRepoInCwdError { cwd: at }) |
|
|||
74 | } |
|
|||
75 | Err(error) => return Err(error.into()), |
|
|||
76 | }; |
|
|||
77 |
|
||||
78 | let invocation = CliInvocation { |
|
64 | let invocation = CliInvocation { | |
79 | ui, |
|
65 | ui, | |
80 | subcommand_args, |
|
66 | subcommand_args, | |
81 |
|
|
67 | config, | |
82 |
repo |
|
68 | repo, | |
83 | }; |
|
69 | }; | |
84 | let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?; |
|
70 | let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?; | |
85 | blackbox.log_command_start(); |
|
71 | blackbox.log_command_start(); | |
86 | let result = run(&invocation); |
|
72 | let result = run(&invocation); | |
87 | blackbox.log_command_end(exit_code(&result)); |
|
73 | blackbox.log_command_end(exit_code(&result)); | |
88 | result |
|
74 | result | |
89 | } |
|
75 | } | |
90 |
|
76 | |||
91 | fn main() { |
|
77 | fn main() { | |
92 | // Run this first, before we find out if the blackbox extension is even |
|
78 | // Run this first, before we find out if the blackbox extension is even | |
93 | // enabled, in order to include everything in-between in the duration |
|
79 | // enabled, in order to include everything in-between in the duration | |
94 | // measurements. Reading config files can be slow if theyβre on NFS. |
|
80 | // measurements. Reading config files can be slow if theyβre on NFS. | |
95 | let process_start_time = blackbox::ProcessStartTime::now(); |
|
81 | let process_start_time = blackbox::ProcessStartTime::now(); | |
96 |
|
82 | |||
|
83 | env_logger::init(); | |||
97 | let ui = ui::Ui::new(); |
|
84 | let ui = ui::Ui::new(); | |
98 |
|
85 | |||
99 | let result = main_with_result(&ui, &process_start_time); |
|
86 | let early_args = EarlyArgs::parse(std::env::args_os()); | |
100 | if let Err(CommandError::Abort { message }) = &result { |
|
87 | let non_repo_config = Config::load(early_args.config) | |
101 | if !message.is_empty() { |
|
88 | .unwrap_or_else(|error| exit(&ui, Err(error.into()))); | |
102 | // Ignore errors when writing to stderr, weβre already exiting |
|
89 | ||
103 | // with failure code so thereβs not much more we can do. |
|
90 | let repo_path = early_args.repo.as_deref().map(get_path_from_bytes); | |
104 | let _ = ui.write_stderr(&format_bytes!(b"abort: {}\n", message)); |
|
91 | let repo_result = match Repo::find(&non_repo_config, repo_path) { | |
|
92 | Ok(repo) => Ok(repo), | |||
|
93 | Err(RepoError::NotFound { at }) if repo_path.is_none() => { | |||
|
94 | // Not finding a repo is not fatal yet, if `-R` was not given | |||
|
95 | Err(NoRepoInCwdError { cwd: at }) | |||
105 | } |
|
96 | } | |
106 | } |
|
97 | Err(error) => exit(&ui, Err(error.into())), | |
107 | std::process::exit(exit_code(&result)) |
|
98 | }; | |
|
99 | ||||
|
100 | let config = if let Ok(repo) = &repo_result { | |||
|
101 | repo.config() | |||
|
102 | } else { | |||
|
103 | &non_repo_config | |||
|
104 | }; | |||
|
105 | ||||
|
106 | let result = main_with_result( | |||
|
107 | &process_start_time, | |||
|
108 | &ui, | |||
|
109 | repo_result.as_ref(), | |||
|
110 | config, | |||
|
111 | ); | |||
|
112 | exit(&ui, result) | |||
108 | } |
|
113 | } | |
109 |
|
114 | |||
110 | fn exit_code(result: &Result<(), CommandError>) -> i32 { |
|
115 | fn exit_code(result: &Result<(), CommandError>) -> i32 { | |
111 | match result { |
|
116 | match result { | |
112 | Ok(()) => exitcode::OK, |
|
117 | Ok(()) => exitcode::OK, | |
113 | Err(CommandError::Abort { .. }) => exitcode::ABORT, |
|
118 | Err(CommandError::Abort { .. }) => exitcode::ABORT, | |
114 |
|
119 | |||
115 | // Exit with a specific code and no error message to let a potential |
|
120 | // Exit with a specific code and no error message to let a potential | |
116 | // wrapper script fallback to Python-based Mercurial. |
|
121 | // wrapper script fallback to Python-based Mercurial. | |
117 | Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED, |
|
122 | Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED, | |
118 | } |
|
123 | } | |
119 | } |
|
124 | } | |
120 |
|
125 | |||
|
126 | fn exit(ui: &Ui, result: Result<(), CommandError>) -> ! { | |||
|
127 | if let Err(CommandError::Abort { message }) = &result { | |||
|
128 | if !message.is_empty() { | |||
|
129 | // Ignore errors when writing to stderr, weβre already exiting | |||
|
130 | // with failure code so thereβs not much more we can do. | |||
|
131 | let _ = ui.write_stderr(&format_bytes!(b"abort: {}\n", message)); | |||
|
132 | } | |||
|
133 | } | |||
|
134 | std::process::exit(exit_code(&result)) | |||
|
135 | } | |||
|
136 | ||||
121 | macro_rules! subcommands { |
|
137 | macro_rules! subcommands { | |
122 | ($( $command: ident )+) => { |
|
138 | ($( $command: ident )+) => { | |
123 | mod commands { |
|
139 | mod commands { | |
124 | $( |
|
140 | $( | |
125 | pub mod $command; |
|
141 | pub mod $command; | |
126 | )+ |
|
142 | )+ | |
127 | } |
|
143 | } | |
128 |
|
144 | |||
129 | fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { |
|
145 | fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { | |
130 | app |
|
146 | app | |
131 | $( |
|
147 | $( | |
132 | .subcommand(commands::$command::args()) |
|
148 | .subcommand(commands::$command::args()) | |
133 | )+ |
|
149 | )+ | |
134 | } |
|
150 | } | |
135 |
|
151 | |||
136 | pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>; |
|
152 | pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>; | |
137 |
|
153 | |||
138 | fn subcommand_run_fn(name: &str) -> Option<RunFn> { |
|
154 | fn subcommand_run_fn(name: &str) -> Option<RunFn> { | |
139 | match name { |
|
155 | match name { | |
140 | $( |
|
156 | $( | |
141 | stringify!($command) => Some(commands::$command::run), |
|
157 | stringify!($command) => Some(commands::$command::run), | |
142 | )+ |
|
158 | )+ | |
143 | _ => None, |
|
159 | _ => None, | |
144 | } |
|
160 | } | |
145 | } |
|
161 | } | |
146 | }; |
|
162 | }; | |
147 | } |
|
163 | } | |
148 |
|
164 | |||
149 | subcommands! { |
|
165 | subcommands! { | |
150 | cat |
|
166 | cat | |
151 | debugdata |
|
167 | debugdata | |
152 | debugrequirements |
|
168 | debugrequirements | |
153 | files |
|
169 | files | |
154 | root |
|
170 | root | |
155 | config |
|
171 | config | |
156 | } |
|
172 | } | |
157 | pub struct CliInvocation<'a> { |
|
173 | pub struct CliInvocation<'a> { | |
158 | ui: &'a Ui, |
|
174 | ui: &'a Ui, | |
159 | subcommand_args: &'a ArgMatches<'a>, |
|
175 | subcommand_args: &'a ArgMatches<'a>, | |
160 |
|
|
176 | config: &'a Config, | |
161 | /// References inside `Result` is a bit peculiar but allow |
|
177 | /// References inside `Result` is a bit peculiar but allow | |
162 | /// `invocation.repo?` to work out with `&CliInvocation` since this |
|
178 | /// `invocation.repo?` to work out with `&CliInvocation` since this | |
163 | /// `Result` type is `Copy`. |
|
179 | /// `Result` type is `Copy`. | |
164 | repo: Result<&'a Repo, &'a NoRepoInCwdError>, |
|
180 | repo: Result<&'a Repo, &'a NoRepoInCwdError>, | |
165 | } |
|
181 | } | |
166 |
|
182 | |||
167 | struct NoRepoInCwdError { |
|
183 | struct NoRepoInCwdError { | |
168 | cwd: PathBuf, |
|
184 | cwd: PathBuf, | |
169 | } |
|
185 | } | |
170 |
|
186 | |||
171 | impl CliInvocation<'_> { |
|
187 | /// CLI arguments to be parsed "early" in order to be able to read | |
172 | fn config(&self) -> &Config { |
|
188 | /// configuration before using Clap. Ideally we would also use Clap for this, | |
173 | if let Ok(repo) = self.repo { |
|
189 | /// see <https://github.com/clap-rs/clap/discussions/2366>. | |
174 | repo.config() |
|
190 | /// | |
175 | } else { |
|
191 | /// These arguments are still declared when we do use Clap later, so that Clap | |
176 | self.non_repo_config |
|
192 | /// does not return an error for their presence. | |
|
193 | struct EarlyArgs { | |||
|
194 | /// Values of all `--config` arguments. (Possibly none) | |||
|
195 | config: Vec<Vec<u8>>, | |||
|
196 | /// Value of the `-R` or `--repository` argument, if any. | |||
|
197 | repo: Option<Vec<u8>>, | |||
|
198 | } | |||
|
199 | ||||
|
200 | impl EarlyArgs { | |||
|
201 | fn parse(args: impl IntoIterator<Item = OsString>) -> Self { | |||
|
202 | let mut args = args.into_iter().map(get_bytes_from_os_str); | |||
|
203 | let mut config = Vec::new(); | |||
|
204 | let mut repo = None; | |||
|
205 | // Use `while let` instead of `for` so that we can also call | |||
|
206 | // `args.next()` inside the loop. | |||
|
207 | while let Some(arg) = args.next() { | |||
|
208 | if arg == b"--config" { | |||
|
209 | if let Some(value) = args.next() { | |||
|
210 | config.push(value) | |||
|
211 | } | |||
|
212 | } else if let Some(value) = arg.drop_prefix(b"--config=") { | |||
|
213 | config.push(value.to_owned()) | |||
|
214 | } | |||
|
215 | ||||
|
216 | if arg == b"--repository" || arg == b"-R" { | |||
|
217 | if let Some(value) = args.next() { | |||
|
218 | repo = Some(value) | |||
|
219 | } | |||
|
220 | } else if let Some(value) = arg.drop_prefix(b"--repository=") { | |||
|
221 | repo = Some(value.to_owned()) | |||
|
222 | } else if let Some(value) = arg.drop_prefix(b"-R") { | |||
|
223 | repo = Some(value.to_owned()) | |||
|
224 | } | |||
177 | } |
|
225 | } | |
|
226 | Self { config, repo } | |||
178 | } |
|
227 | } | |
179 | } |
|
228 | } |
General Comments 0
You need to be logged in to leave comments.
Login now