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