##// END OF EJS Templates
rhg: Fall back to Python for --version...
Simon Sapin -
r47480:eb14264b default
parent child Browse files
Show More
@@ -1,442 +1,443 b''
1 1 extern crate log;
2 2 use crate::ui::Ui;
3 3 use clap::App;
4 4 use clap::AppSettings;
5 5 use clap::Arg;
6 6 use clap::ArgMatches;
7 7 use format_bytes::{format_bytes, join};
8 8 use hg::config::Config;
9 9 use hg::repo::{Repo, RepoError};
10 10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
11 11 use hg::utils::SliceExt;
12 12 use std::ffi::OsString;
13 13 use std::path::PathBuf;
14 14 use std::process::Command;
15 15
16 16 mod blackbox;
17 17 mod error;
18 18 mod exitcode;
19 19 mod ui;
20 20 use error::CommandError;
21 21
22 22 fn main_with_result(
23 23 process_start_time: &blackbox::ProcessStartTime,
24 24 ui: &ui::Ui,
25 25 repo: Result<&Repo, &NoRepoInCwdError>,
26 26 config: &Config,
27 27 ) -> Result<(), CommandError> {
28 28 check_extensions(config)?;
29 29
30 30 let app = App::new("rhg")
31 31 .global_setting(AppSettings::AllowInvalidUtf8)
32 .global_setting(AppSettings::DisableVersion)
32 33 .setting(AppSettings::SubcommandRequired)
33 34 .setting(AppSettings::VersionlessSubcommands)
34 35 .arg(
35 36 Arg::with_name("repository")
36 37 .help("repository root directory")
37 38 .short("-R")
38 39 .long("--repository")
39 40 .value_name("REPO")
40 41 .takes_value(true)
41 42 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
42 43 .global(true),
43 44 )
44 45 .arg(
45 46 Arg::with_name("config")
46 47 .help("set/override config option (use 'section.name=value')")
47 48 .long("--config")
48 49 .value_name("CONFIG")
49 50 .takes_value(true)
50 51 .global(true)
51 52 // Ok: `--config section.key1=val --config section.key2=val2`
52 53 .multiple(true)
53 54 // Not ok: `--config section.key1=val section.key2=val2`
54 55 .number_of_values(1),
55 56 )
56 57 .arg(
57 58 Arg::with_name("cwd")
58 59 .help("change working directory")
59 60 .long("--cwd")
60 61 .value_name("DIR")
61 62 .takes_value(true)
62 63 .global(true),
63 64 )
64 65 .version("0.0.1");
65 66 let app = add_subcommand_args(app);
66 67
67 68 let matches = app.clone().get_matches_safe()?;
68 69
69 70 let (subcommand_name, subcommand_matches) = matches.subcommand();
70 71 let run = subcommand_run_fn(subcommand_name)
71 72 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
72 73 let subcommand_args = subcommand_matches
73 74 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
74 75
75 76 let invocation = CliInvocation {
76 77 ui,
77 78 subcommand_args,
78 79 config,
79 80 repo,
80 81 };
81 82 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
82 83 blackbox.log_command_start();
83 84 let result = run(&invocation);
84 85 blackbox.log_command_end(exit_code(&result));
85 86 result
86 87 }
87 88
88 89 fn main() {
89 90 // Run this first, before we find out if the blackbox extension is even
90 91 // enabled, in order to include everything in-between in the duration
91 92 // measurements. Reading config files can be slow if they’re on NFS.
92 93 let process_start_time = blackbox::ProcessStartTime::now();
93 94
94 95 env_logger::init();
95 96 let ui = ui::Ui::new();
96 97
97 98 let early_args = EarlyArgs::parse(std::env::args_os());
98 99
99 100 let initial_current_dir = early_args.cwd.map(|cwd| {
100 101 let cwd = get_path_from_bytes(&cwd);
101 102 std::env::current_dir()
102 103 .and_then(|initial| {
103 104 std::env::set_current_dir(cwd)?;
104 105 Ok(initial)
105 106 })
106 107 .unwrap_or_else(|error| {
107 108 exit(
108 109 &None,
109 110 &ui,
110 111 OnUnsupported::Abort,
111 112 Err(CommandError::abort(format!(
112 113 "abort: {}: '{}'",
113 114 error,
114 115 cwd.display()
115 116 ))),
116 117 )
117 118 })
118 119 });
119 120
120 121 let non_repo_config =
121 122 Config::load(early_args.config).unwrap_or_else(|error| {
122 123 // Normally this is decided based on config, but we don’t have that
123 124 // available. As of this writing config loading never returns an
124 125 // "unsupported" error but that is not enforced by the type system.
125 126 let on_unsupported = OnUnsupported::Abort;
126 127
127 128 exit(&initial_current_dir, &ui, on_unsupported, Err(error.into()))
128 129 });
129 130
130 131 if let Some(repo_path_bytes) = &early_args.repo {
131 132 lazy_static::lazy_static! {
132 133 static ref SCHEME_RE: regex::bytes::Regex =
133 134 // Same as `_matchscheme` in `mercurial/util.py`
134 135 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
135 136 }
136 137 if SCHEME_RE.is_match(&repo_path_bytes) {
137 138 exit(
138 139 &initial_current_dir,
139 140 &ui,
140 141 OnUnsupported::from_config(&non_repo_config),
141 142 Err(CommandError::UnsupportedFeature {
142 143 message: format_bytes!(
143 144 b"URL-like --repository {}",
144 145 repo_path_bytes
145 146 ),
146 147 }),
147 148 )
148 149 }
149 150 }
150 151 let repo_path = early_args.repo.as_deref().map(get_path_from_bytes);
151 152 let repo_result = match Repo::find(&non_repo_config, repo_path) {
152 153 Ok(repo) => Ok(repo),
153 154 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
154 155 // Not finding a repo is not fatal yet, if `-R` was not given
155 156 Err(NoRepoInCwdError { cwd: at })
156 157 }
157 158 Err(error) => exit(
158 159 &initial_current_dir,
159 160 &ui,
160 161 OnUnsupported::from_config(&non_repo_config),
161 162 Err(error.into()),
162 163 ),
163 164 };
164 165
165 166 let config = if let Ok(repo) = &repo_result {
166 167 repo.config()
167 168 } else {
168 169 &non_repo_config
169 170 };
170 171
171 172 let result = main_with_result(
172 173 &process_start_time,
173 174 &ui,
174 175 repo_result.as_ref(),
175 176 config,
176 177 );
177 178 exit(
178 179 &initial_current_dir,
179 180 &ui,
180 181 OnUnsupported::from_config(config),
181 182 result,
182 183 )
183 184 }
184 185
185 186 fn exit_code(result: &Result<(), CommandError>) -> i32 {
186 187 match result {
187 188 Ok(()) => exitcode::OK,
188 189 Err(CommandError::Abort { .. }) => exitcode::ABORT,
189 190 Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL,
190 191
191 192 // Exit with a specific code and no error message to let a potential
192 193 // wrapper script fallback to Python-based Mercurial.
193 194 Err(CommandError::UnsupportedFeature { .. }) => {
194 195 exitcode::UNIMPLEMENTED
195 196 }
196 197 }
197 198 }
198 199
199 200 fn exit(
200 201 initial_current_dir: &Option<PathBuf>,
201 202 ui: &Ui,
202 203 mut on_unsupported: OnUnsupported,
203 204 result: Result<(), CommandError>,
204 205 ) -> ! {
205 206 if let (
206 207 OnUnsupported::Fallback { executable },
207 208 Err(CommandError::UnsupportedFeature { .. }),
208 209 ) = (&on_unsupported, &result)
209 210 {
210 211 let mut args = std::env::args_os();
211 212 let executable_path = get_path_from_bytes(&executable);
212 213 let this_executable = args.next().expect("exepcted argv[0] to exist");
213 214 if executable_path == &PathBuf::from(this_executable) {
214 215 // Avoid spawning infinitely many processes until resource
215 216 // exhaustion.
216 217 let _ = ui.write_stderr(&format_bytes!(
217 218 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
218 219 points to `rhg` itself.\n",
219 220 executable
220 221 ));
221 222 on_unsupported = OnUnsupported::Abort
222 223 } else {
223 224 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
224 225 let mut command = Command::new(executable_path);
225 226 command.args(args);
226 227 if let Some(initial) = initial_current_dir {
227 228 command.current_dir(initial);
228 229 }
229 230 let result = command.status();
230 231 match result {
231 232 Ok(status) => std::process::exit(
232 233 status.code().unwrap_or(exitcode::ABORT),
233 234 ),
234 235 Err(error) => {
235 236 let _ = ui.write_stderr(&format_bytes!(
236 237 b"tried to fall back to a '{}' sub-process but got error {}\n",
237 238 executable, format_bytes::Utf8(error)
238 239 ));
239 240 on_unsupported = OnUnsupported::Abort
240 241 }
241 242 }
242 243 }
243 244 }
244 245 match &result {
245 246 Ok(_) => {}
246 247 Err(CommandError::Unsuccessful) => {}
247 248 Err(CommandError::Abort { message }) => {
248 249 if !message.is_empty() {
249 250 // Ignore errors when writing to stderr, we’re already exiting
250 251 // with failure code so there’s not much more we can do.
251 252 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
252 253 }
253 254 }
254 255 Err(CommandError::UnsupportedFeature { message }) => {
255 256 match on_unsupported {
256 257 OnUnsupported::Abort => {
257 258 let _ = ui.write_stderr(&format_bytes!(
258 259 b"unsupported feature: {}\n",
259 260 message
260 261 ));
261 262 }
262 263 OnUnsupported::AbortSilent => {}
263 264 OnUnsupported::Fallback { .. } => unreachable!(),
264 265 }
265 266 }
266 267 }
267 268 std::process::exit(exit_code(&result))
268 269 }
269 270
270 271 macro_rules! subcommands {
271 272 ($( $command: ident )+) => {
272 273 mod commands {
273 274 $(
274 275 pub mod $command;
275 276 )+
276 277 }
277 278
278 279 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
279 280 app
280 281 $(
281 282 .subcommand(commands::$command::args())
282 283 )+
283 284 }
284 285
285 286 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
286 287
287 288 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
288 289 match name {
289 290 $(
290 291 stringify!($command) => Some(commands::$command::run),
291 292 )+
292 293 _ => None,
293 294 }
294 295 }
295 296 };
296 297 }
297 298
298 299 subcommands! {
299 300 cat
300 301 debugdata
301 302 debugrequirements
302 303 files
303 304 root
304 305 config
305 306 }
306 307 pub struct CliInvocation<'a> {
307 308 ui: &'a Ui,
308 309 subcommand_args: &'a ArgMatches<'a>,
309 310 config: &'a Config,
310 311 /// References inside `Result` is a bit peculiar but allow
311 312 /// `invocation.repo?` to work out with `&CliInvocation` since this
312 313 /// `Result` type is `Copy`.
313 314 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
314 315 }
315 316
316 317 struct NoRepoInCwdError {
317 318 cwd: PathBuf,
318 319 }
319 320
320 321 /// CLI arguments to be parsed "early" in order to be able to read
321 322 /// configuration before using Clap. Ideally we would also use Clap for this,
322 323 /// see <https://github.com/clap-rs/clap/discussions/2366>.
323 324 ///
324 325 /// These arguments are still declared when we do use Clap later, so that Clap
325 326 /// does not return an error for their presence.
326 327 struct EarlyArgs {
327 328 /// Values of all `--config` arguments. (Possibly none)
328 329 config: Vec<Vec<u8>>,
329 330 /// Value of the `-R` or `--repository` argument, if any.
330 331 repo: Option<Vec<u8>>,
331 332 /// Value of the `--cwd` argument, if any.
332 333 cwd: Option<Vec<u8>>,
333 334 }
334 335
335 336 impl EarlyArgs {
336 337 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
337 338 let mut args = args.into_iter().map(get_bytes_from_os_str);
338 339 let mut config = Vec::new();
339 340 let mut repo = None;
340 341 let mut cwd = None;
341 342 // Use `while let` instead of `for` so that we can also call
342 343 // `args.next()` inside the loop.
343 344 while let Some(arg) = args.next() {
344 345 if arg == b"--config" {
345 346 if let Some(value) = args.next() {
346 347 config.push(value)
347 348 }
348 349 } else if let Some(value) = arg.drop_prefix(b"--config=") {
349 350 config.push(value.to_owned())
350 351 }
351 352
352 353 if arg == b"--cwd" {
353 354 if let Some(value) = args.next() {
354 355 cwd = Some(value)
355 356 }
356 357 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
357 358 cwd = Some(value.to_owned())
358 359 }
359 360
360 361 if arg == b"--repository" || arg == b"-R" {
361 362 if let Some(value) = args.next() {
362 363 repo = Some(value)
363 364 }
364 365 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
365 366 repo = Some(value.to_owned())
366 367 } else if let Some(value) = arg.drop_prefix(b"-R") {
367 368 repo = Some(value.to_owned())
368 369 }
369 370 }
370 371 Self { config, repo, cwd }
371 372 }
372 373 }
373 374
374 375 /// What to do when encountering some unsupported feature.
375 376 ///
376 377 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
377 378 enum OnUnsupported {
378 379 /// Print an error message describing what feature is not supported,
379 380 /// and exit with code 252.
380 381 Abort,
381 382 /// Silently exit with code 252.
382 383 AbortSilent,
383 384 /// Try running a Python implementation
384 385 Fallback { executable: Vec<u8> },
385 386 }
386 387
387 388 impl OnUnsupported {
388 389 const DEFAULT: Self = OnUnsupported::Abort;
389 390 const DEFAULT_FALLBACK_EXECUTABLE: &'static [u8] = b"hg";
390 391
391 392 fn from_config(config: &Config) -> Self {
392 393 match config
393 394 .get(b"rhg", b"on-unsupported")
394 395 .map(|value| value.to_ascii_lowercase())
395 396 .as_deref()
396 397 {
397 398 Some(b"abort") => OnUnsupported::Abort,
398 399 Some(b"abort-silent") => OnUnsupported::AbortSilent,
399 400 Some(b"fallback") => OnUnsupported::Fallback {
400 401 executable: config
401 402 .get(b"rhg", b"fallback-executable")
402 403 .unwrap_or(Self::DEFAULT_FALLBACK_EXECUTABLE)
403 404 .to_owned(),
404 405 },
405 406 None => Self::DEFAULT,
406 407 Some(_) => {
407 408 // TODO: warn about unknown config value
408 409 Self::DEFAULT
409 410 }
410 411 }
411 412 }
412 413 }
413 414
414 415 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"];
415 416
416 417 fn check_extensions(config: &Config) -> Result<(), CommandError> {
417 418 let enabled = config.get_section_keys(b"extensions");
418 419
419 420 let mut unsupported = enabled;
420 421 for supported in SUPPORTED_EXTENSIONS {
421 422 unsupported.remove(supported);
422 423 }
423 424
424 425 if let Some(ignored_list) =
425 426 config.get_simple_list(b"rhg", b"ignored-extensions")
426 427 {
427 428 for ignored in ignored_list {
428 429 unsupported.remove(ignored);
429 430 }
430 431 }
431 432
432 433 if unsupported.is_empty() {
433 434 Ok(())
434 435 } else {
435 436 Err(CommandError::UnsupportedFeature {
436 437 message: format_bytes!(
437 438 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
438 439 join(unsupported, b", ")
439 440 ),
440 441 })
441 442 }
442 443 }
General Comments 0
You need to be logged in to leave comments. Login now