##// END OF EJS Templates
rhg: fix bugs around [use-dirstate-tracked-hint] and repo auto-upgrade...
Arseniy Alekseyev -
r50395:3a538710 stable
parent child Browse files
Show More
@@ -1,172 +1,173 b''
1 1 use crate::errors::{HgError, HgResultExt};
2 2 use crate::repo::Repo;
3 3 use crate::utils::join_display;
4 4 use crate::vfs::Vfs;
5 5 use std::collections::HashSet;
6 6
7 7 fn parse(bytes: &[u8]) -> Result<HashSet<String>, HgError> {
8 8 // The Python code reading this file uses `str.splitlines`
9 9 // which looks for a number of line separators (even including a couple of
10 10 // non-ASCII ones), but Python code writing it always uses `\n`.
11 11 let lines = bytes.split(|&byte| byte == b'\n');
12 12
13 13 lines
14 14 .filter(|line| !line.is_empty())
15 15 .map(|line| {
16 16 // Python uses Unicode `str.isalnum` but feature names are all
17 17 // ASCII
18 18 if line[0].is_ascii_alphanumeric() && line.is_ascii() {
19 19 Ok(String::from_utf8(line.into()).unwrap())
20 20 } else {
21 21 Err(HgError::corrupted("parse error in 'requires' file"))
22 22 }
23 23 })
24 24 .collect()
25 25 }
26 26
27 27 pub(crate) fn load(hg_vfs: Vfs) -> Result<HashSet<String>, HgError> {
28 28 parse(&hg_vfs.read("requires")?)
29 29 }
30 30
31 31 pub(crate) fn load_if_exists(hg_vfs: Vfs) -> Result<HashSet<String>, HgError> {
32 32 if let Some(bytes) = hg_vfs.read("requires").io_not_found_as_none()? {
33 33 parse(&bytes)
34 34 } else {
35 35 // Treat a missing file the same as an empty file.
36 36 // From `mercurial/localrepo.py`:
37 37 // > requires file contains a newline-delimited list of
38 38 // > features/capabilities the opener (us) must have in order to use
39 39 // > the repository. This file was introduced in Mercurial 0.9.2,
40 40 // > which means very old repositories may not have one. We assume
41 41 // > a missing file translates to no requirements.
42 42 Ok(HashSet::new())
43 43 }
44 44 }
45 45
46 46 pub(crate) fn check(repo: &Repo) -> Result<(), HgError> {
47 47 let unknown: Vec<_> = repo
48 48 .requirements()
49 49 .iter()
50 50 .map(String::as_str)
51 51 // .filter(|feature| !ALL_SUPPORTED.contains(feature.as_str()))
52 52 .filter(|feature| {
53 53 !REQUIRED.contains(feature) && !SUPPORTED.contains(feature)
54 54 })
55 55 .collect();
56 56 if !unknown.is_empty() {
57 57 return Err(HgError::unsupported(format!(
58 58 "repository requires feature unknown to this Mercurial: {}",
59 59 join_display(&unknown, ", ")
60 60 )));
61 61 }
62 62 let missing: Vec<_> = REQUIRED
63 63 .iter()
64 64 .filter(|&&feature| !repo.requirements().contains(feature))
65 65 .collect();
66 66 if !missing.is_empty() {
67 67 return Err(HgError::unsupported(format!(
68 68 "repository is missing feature required by this Mercurial: {}",
69 69 join_display(&missing, ", ")
70 70 )));
71 71 }
72 72 Ok(())
73 73 }
74 74
75 75 /// rhg does not support repositories that are *missing* any of these features
76 76 const REQUIRED: &[&str] = &["revlogv1", "store", "fncache", "dotencode"];
77 77
78 78 /// rhg supports repository with or without these
79 79 const SUPPORTED: &[&str] = &[
80 80 "generaldelta",
81 81 SHARED_REQUIREMENT,
82 82 SHARESAFE_REQUIREMENT,
83 83 SPARSEREVLOG_REQUIREMENT,
84 84 RELATIVE_SHARED_REQUIREMENT,
85 85 REVLOG_COMPRESSION_ZSTD,
86 86 DIRSTATE_V2_REQUIREMENT,
87 DIRSTATE_TRACKED_HINT_V1,
87 88 // As of this writing everything rhg does is read-only.
88 89 // When it starts writing to the repository, it’ll need to either keep the
89 90 // persistent nodemap up to date or remove this entry:
90 91 NODEMAP_REQUIREMENT,
91 92 // Not all commands support `sparse` and `narrow`. The commands that do
92 93 // not should opt out by checking `has_sparse` and `has_narrow`.
93 94 SPARSE_REQUIREMENT,
94 95 NARROW_REQUIREMENT,
95 96 // rhg doesn't care about bookmarks at all yet
96 97 BOOKMARKS_IN_STORE_REQUIREMENT,
97 98 ];
98 99
99 100 // Copied from mercurial/requirements.py:
100 101
101 102 pub const DIRSTATE_V2_REQUIREMENT: &str = "dirstate-v2";
102 103
103 104 /// A repository that uses the tracked hint dirstate file
104 105 #[allow(unused)]
105 106 pub const DIRSTATE_TRACKED_HINT_V1: &str = "dirstate-tracked-key-v1";
106 107
107 108 /// When narrowing is finalized and no longer subject to format changes,
108 109 /// we should move this to just "narrow" or similar.
109 110 #[allow(unused)]
110 111 pub const NARROW_REQUIREMENT: &str = "narrowhg-experimental";
111 112
112 113 /// Bookmarks must be stored in the `store` part of the repository and will be
113 114 /// share accross shares
114 115 #[allow(unused)]
115 116 pub const BOOKMARKS_IN_STORE_REQUIREMENT: &str = "bookmarksinstore";
116 117
117 118 /// Enables sparse working directory usage
118 119 #[allow(unused)]
119 120 pub const SPARSE_REQUIREMENT: &str = "exp-sparse";
120 121
121 122 /// Enables the internal phase which is used to hide changesets instead
122 123 /// of stripping them
123 124 #[allow(unused)]
124 125 pub const INTERNAL_PHASE_REQUIREMENT: &str = "internal-phase";
125 126
126 127 /// Stores manifest in Tree structure
127 128 #[allow(unused)]
128 129 pub const TREEMANIFEST_REQUIREMENT: &str = "treemanifest";
129 130
130 131 /// Increment the sub-version when the revlog v2 format changes to lock out old
131 132 /// clients.
132 133 #[allow(unused)]
133 134 pub const REVLOGV2_REQUIREMENT: &str = "exp-revlogv2.1";
134 135
135 136 /// A repository with the sparserevlog feature will have delta chains that
136 137 /// can spread over a larger span. Sparse reading cuts these large spans into
137 138 /// pieces, so that each piece isn't too big.
138 139 /// Without the sparserevlog capability, reading from the repository could use
139 140 /// huge amounts of memory, because the whole span would be read at once,
140 141 /// including all the intermediate revisions that aren't pertinent for the
141 142 /// chain. This is why once a repository has enabled sparse-read, it becomes
142 143 /// required.
143 144 #[allow(unused)]
144 145 pub const SPARSEREVLOG_REQUIREMENT: &str = "sparserevlog";
145 146
146 147 /// A repository with the the copies-sidedata-changeset requirement will store
147 148 /// copies related information in changeset's sidedata.
148 149 #[allow(unused)]
149 150 pub const COPIESSDC_REQUIREMENT: &str = "exp-copies-sidedata-changeset";
150 151
151 152 /// The repository use persistent nodemap for the changelog and the manifest.
152 153 #[allow(unused)]
153 154 pub const NODEMAP_REQUIREMENT: &str = "persistent-nodemap";
154 155
155 156 /// Denotes that the current repository is a share
156 157 #[allow(unused)]
157 158 pub const SHARED_REQUIREMENT: &str = "shared";
158 159
159 160 /// Denotes that current repository is a share and the shared source path is
160 161 /// relative to the current repository root path
161 162 #[allow(unused)]
162 163 pub const RELATIVE_SHARED_REQUIREMENT: &str = "relshared";
163 164
164 165 /// A repository with share implemented safely. The repository has different
165 166 /// store and working copy requirements i.e. both `.hg/requires` and
166 167 /// `.hg/store/requires` are present.
167 168 #[allow(unused)]
168 169 pub const SHARESAFE_REQUIREMENT: &str = "share-safe";
169 170
170 171 /// A repository that use zstd compression inside its revlog
171 172 #[allow(unused)]
172 173 pub const REVLOG_COMPRESSION_ZSTD: &str = "revlog-compression-zstd";
@@ -1,809 +1,809 b''
1 1 extern crate log;
2 2 use crate::error::CommandError;
3 3 use crate::ui::{local_to_utf8, Ui};
4 4 use clap::App;
5 5 use clap::AppSettings;
6 6 use clap::Arg;
7 7 use clap::ArgMatches;
8 8 use format_bytes::{format_bytes, join};
9 9 use hg::config::{Config, ConfigSource};
10 10 use hg::repo::{Repo, RepoError};
11 11 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
12 12 use hg::utils::SliceExt;
13 13 use hg::{exit_codes, requirements};
14 14 use std::collections::HashSet;
15 15 use std::ffi::OsString;
16 16 use std::os::unix::prelude::CommandExt;
17 17 use std::path::PathBuf;
18 18 use std::process::Command;
19 19
20 20 mod blackbox;
21 21 mod color;
22 22 mod error;
23 23 mod ui;
24 24 pub mod utils {
25 25 pub mod path_utils;
26 26 }
27 27
28 28 fn main_with_result(
29 29 argv: Vec<OsString>,
30 30 process_start_time: &blackbox::ProcessStartTime,
31 31 ui: &ui::Ui,
32 32 repo: Result<&Repo, &NoRepoInCwdError>,
33 33 config: &Config,
34 34 ) -> Result<(), CommandError> {
35 35 check_unsupported(config, repo)?;
36 36
37 37 let app = App::new("rhg")
38 38 .global_setting(AppSettings::AllowInvalidUtf8)
39 39 .global_setting(AppSettings::DisableVersion)
40 40 .setting(AppSettings::SubcommandRequired)
41 41 .setting(AppSettings::VersionlessSubcommands)
42 42 .arg(
43 43 Arg::with_name("repository")
44 44 .help("repository root directory")
45 45 .short("-R")
46 46 .long("--repository")
47 47 .value_name("REPO")
48 48 .takes_value(true)
49 49 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
50 50 .global(true),
51 51 )
52 52 .arg(
53 53 Arg::with_name("config")
54 54 .help("set/override config option (use 'section.name=value')")
55 55 .long("--config")
56 56 .value_name("CONFIG")
57 57 .takes_value(true)
58 58 .global(true)
59 59 // Ok: `--config section.key1=val --config section.key2=val2`
60 60 .multiple(true)
61 61 // Not ok: `--config section.key1=val section.key2=val2`
62 62 .number_of_values(1),
63 63 )
64 64 .arg(
65 65 Arg::with_name("cwd")
66 66 .help("change working directory")
67 67 .long("--cwd")
68 68 .value_name("DIR")
69 69 .takes_value(true)
70 70 .global(true),
71 71 )
72 72 .arg(
73 73 Arg::with_name("color")
74 74 .help("when to colorize (boolean, always, auto, never, or debug)")
75 75 .long("--color")
76 76 .value_name("TYPE")
77 77 .takes_value(true)
78 78 .global(true),
79 79 )
80 80 .version("0.0.1");
81 81 let app = add_subcommand_args(app);
82 82
83 83 let matches = app.clone().get_matches_from_safe(argv.iter())?;
84 84
85 85 let (subcommand_name, subcommand_matches) = matches.subcommand();
86 86
87 87 // Mercurial allows users to define "defaults" for commands, fallback
88 88 // if a default is detected for the current command
89 89 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
90 90 if defaults?.is_some() {
91 91 let msg = "`defaults` config set";
92 92 return Err(CommandError::unsupported(msg));
93 93 }
94 94
95 95 for prefix in ["pre", "post", "fail"].iter() {
96 96 // Mercurial allows users to define generic hooks for commands,
97 97 // fallback if any are detected
98 98 let item = format!("{}-{}", prefix, subcommand_name);
99 99 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
100 100 if hook_for_command.is_some() {
101 101 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
102 102 return Err(CommandError::unsupported(msg));
103 103 }
104 104 }
105 105 let run = subcommand_run_fn(subcommand_name)
106 106 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
107 107 let subcommand_args = subcommand_matches
108 108 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
109 109
110 110 let invocation = CliInvocation {
111 111 ui,
112 112 subcommand_args,
113 113 config,
114 114 repo,
115 115 };
116 116
117 117 if let Ok(repo) = repo {
118 118 // We don't support subrepos, fallback if the subrepos file is present
119 119 if repo.working_directory_vfs().join(".hgsub").exists() {
120 120 let msg = "subrepos (.hgsub is present)";
121 121 return Err(CommandError::unsupported(msg));
122 122 }
123 123 }
124 124
125 125 if config.is_extension_enabled(b"blackbox") {
126 126 let blackbox =
127 127 blackbox::Blackbox::new(&invocation, process_start_time)?;
128 128 blackbox.log_command_start(argv.iter());
129 129 let result = run(&invocation);
130 130 blackbox.log_command_end(
131 131 argv.iter(),
132 132 exit_code(
133 133 &result,
134 134 // TODO: show a warning or combine with original error if
135 135 // `get_bool` returns an error
136 136 config
137 137 .get_bool(b"ui", b"detailed-exit-code")
138 138 .unwrap_or(false),
139 139 ),
140 140 );
141 141 result
142 142 } else {
143 143 run(&invocation)
144 144 }
145 145 }
146 146
147 147 fn rhg_main(argv: Vec<OsString>) -> ! {
148 148 // Run this first, before we find out if the blackbox extension is even
149 149 // enabled, in order to include everything in-between in the duration
150 150 // measurements. Reading config files can be slow if they’re on NFS.
151 151 let process_start_time = blackbox::ProcessStartTime::now();
152 152
153 153 env_logger::init();
154 154
155 155 let early_args = EarlyArgs::parse(&argv);
156 156
157 157 let initial_current_dir = early_args.cwd.map(|cwd| {
158 158 let cwd = get_path_from_bytes(&cwd);
159 159 std::env::current_dir()
160 160 .and_then(|initial| {
161 161 std::env::set_current_dir(cwd)?;
162 162 Ok(initial)
163 163 })
164 164 .unwrap_or_else(|error| {
165 165 exit(
166 166 &argv,
167 167 &None,
168 168 &Ui::new_infallible(&Config::empty()),
169 169 OnUnsupported::Abort,
170 170 Err(CommandError::abort(format!(
171 171 "abort: {}: '{}'",
172 172 error,
173 173 cwd.display()
174 174 ))),
175 175 false,
176 176 )
177 177 })
178 178 });
179 179
180 180 let mut non_repo_config =
181 181 Config::load_non_repo().unwrap_or_else(|error| {
182 182 // Normally this is decided based on config, but we don’t have that
183 183 // available. As of this writing config loading never returns an
184 184 // "unsupported" error but that is not enforced by the type system.
185 185 let on_unsupported = OnUnsupported::Abort;
186 186
187 187 exit(
188 188 &argv,
189 189 &initial_current_dir,
190 190 &Ui::new_infallible(&Config::empty()),
191 191 on_unsupported,
192 192 Err(error.into()),
193 193 false,
194 194 )
195 195 });
196 196
197 197 non_repo_config
198 198 .load_cli_args(early_args.config, early_args.color)
199 199 .unwrap_or_else(|error| {
200 200 exit(
201 201 &argv,
202 202 &initial_current_dir,
203 203 &Ui::new_infallible(&non_repo_config),
204 204 OnUnsupported::from_config(&non_repo_config),
205 205 Err(error.into()),
206 206 non_repo_config
207 207 .get_bool(b"ui", b"detailed-exit-code")
208 208 .unwrap_or(false),
209 209 )
210 210 });
211 211
212 212 if let Some(repo_path_bytes) = &early_args.repo {
213 213 lazy_static::lazy_static! {
214 214 static ref SCHEME_RE: regex::bytes::Regex =
215 215 // Same as `_matchscheme` in `mercurial/util.py`
216 216 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
217 217 }
218 218 if SCHEME_RE.is_match(&repo_path_bytes) {
219 219 exit(
220 220 &argv,
221 221 &initial_current_dir,
222 222 &Ui::new_infallible(&non_repo_config),
223 223 OnUnsupported::from_config(&non_repo_config),
224 224 Err(CommandError::UnsupportedFeature {
225 225 message: format_bytes!(
226 226 b"URL-like --repository {}",
227 227 repo_path_bytes
228 228 ),
229 229 }),
230 230 // TODO: show a warning or combine with original error if
231 231 // `get_bool` returns an error
232 232 non_repo_config
233 233 .get_bool(b"ui", b"detailed-exit-code")
234 234 .unwrap_or(false),
235 235 )
236 236 }
237 237 }
238 238 let repo_arg = early_args.repo.unwrap_or(Vec::new());
239 239 let repo_path: Option<PathBuf> = {
240 240 if repo_arg.is_empty() {
241 241 None
242 242 } else {
243 243 let local_config = {
244 244 if std::env::var_os("HGRCSKIPREPO").is_none() {
245 245 // TODO: handle errors from find_repo_root
246 246 if let Ok(current_dir_path) = Repo::find_repo_root() {
247 247 let config_files = vec![
248 248 ConfigSource::AbsPath(
249 249 current_dir_path.join(".hg/hgrc"),
250 250 ),
251 251 ConfigSource::AbsPath(
252 252 current_dir_path.join(".hg/hgrc-not-shared"),
253 253 ),
254 254 ];
255 255 // TODO: handle errors from
256 256 // `load_from_explicit_sources`
257 257 Config::load_from_explicit_sources(config_files).ok()
258 258 } else {
259 259 None
260 260 }
261 261 } else {
262 262 None
263 263 }
264 264 };
265 265
266 266 let non_repo_config_val = {
267 267 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
268 268 match &non_repo_val {
269 269 Some(val) if val.len() > 0 => home::home_dir()
270 270 .unwrap_or_else(|| PathBuf::from("~"))
271 271 .join(get_path_from_bytes(val))
272 272 .canonicalize()
273 273 // TODO: handle error and make it similar to python
274 274 // implementation maybe?
275 275 .ok(),
276 276 _ => None,
277 277 }
278 278 };
279 279
280 280 let config_val = match &local_config {
281 281 None => non_repo_config_val,
282 282 Some(val) => {
283 283 let local_config_val = val.get(b"paths", &repo_arg);
284 284 match &local_config_val {
285 285 Some(val) if val.len() > 0 => {
286 286 // presence of a local_config assures that
287 287 // current_dir
288 288 // wont result in an Error
289 289 let canpath = hg::utils::current_dir()
290 290 .unwrap()
291 291 .join(get_path_from_bytes(val))
292 292 .canonicalize();
293 293 canpath.ok().or(non_repo_config_val)
294 294 }
295 295 _ => non_repo_config_val,
296 296 }
297 297 }
298 298 };
299 299 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
300 300 }
301 301 };
302 302
303 303 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
304 304 {
305 305 Ok(repo) => Ok(repo),
306 306 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
307 307 // Not finding a repo is not fatal yet, if `-R` was not given
308 308 Err(NoRepoInCwdError { cwd: at })
309 309 }
310 310 Err(error) => exit(
311 311 &argv,
312 312 &initial_current_dir,
313 313 &Ui::new_infallible(&non_repo_config),
314 314 OnUnsupported::from_config(&non_repo_config),
315 315 Err(error.into()),
316 316 // TODO: show a warning or combine with original error if
317 317 // `get_bool` returns an error
318 318 non_repo_config
319 319 .get_bool(b"ui", b"detailed-exit-code")
320 320 .unwrap_or(false),
321 321 ),
322 322 };
323 323
324 324 let config = if let Ok(repo) = &repo_result {
325 325 repo.config()
326 326 } else {
327 327 &non_repo_config
328 328 };
329 329 let ui = Ui::new(&config).unwrap_or_else(|error| {
330 330 exit(
331 331 &argv,
332 332 &initial_current_dir,
333 333 &Ui::new_infallible(&config),
334 334 OnUnsupported::from_config(&config),
335 335 Err(error.into()),
336 336 config
337 337 .get_bool(b"ui", b"detailed-exit-code")
338 338 .unwrap_or(false),
339 339 )
340 340 });
341 341 let on_unsupported = OnUnsupported::from_config(config);
342 342
343 343 let result = main_with_result(
344 344 argv.iter().map(|s| s.to_owned()).collect(),
345 345 &process_start_time,
346 346 &ui,
347 347 repo_result.as_ref(),
348 348 config,
349 349 );
350 350 exit(
351 351 &argv,
352 352 &initial_current_dir,
353 353 &ui,
354 354 on_unsupported,
355 355 result,
356 356 // TODO: show a warning or combine with original error if `get_bool`
357 357 // returns an error
358 358 config
359 359 .get_bool(b"ui", b"detailed-exit-code")
360 360 .unwrap_or(false),
361 361 )
362 362 }
363 363
364 364 fn main() -> ! {
365 365 rhg_main(std::env::args_os().collect())
366 366 }
367 367
368 368 fn exit_code(
369 369 result: &Result<(), CommandError>,
370 370 use_detailed_exit_code: bool,
371 371 ) -> i32 {
372 372 match result {
373 373 Ok(()) => exit_codes::OK,
374 374 Err(CommandError::Abort {
375 375 message: _,
376 376 detailed_exit_code,
377 377 }) => {
378 378 if use_detailed_exit_code {
379 379 *detailed_exit_code
380 380 } else {
381 381 exit_codes::ABORT
382 382 }
383 383 }
384 384 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
385 385 // Exit with a specific code and no error message to let a potential
386 386 // wrapper script fallback to Python-based Mercurial.
387 387 Err(CommandError::UnsupportedFeature { .. }) => {
388 388 exit_codes::UNIMPLEMENTED
389 389 }
390 390 Err(CommandError::InvalidFallback { .. }) => {
391 391 exit_codes::INVALID_FALLBACK
392 392 }
393 393 }
394 394 }
395 395
396 396 fn exit<'a>(
397 397 original_args: &'a [OsString],
398 398 initial_current_dir: &Option<PathBuf>,
399 399 ui: &Ui,
400 400 mut on_unsupported: OnUnsupported,
401 401 result: Result<(), CommandError>,
402 402 use_detailed_exit_code: bool,
403 403 ) -> ! {
404 404 if let (
405 405 OnUnsupported::Fallback { executable },
406 406 Err(CommandError::UnsupportedFeature { message }),
407 407 ) = (&on_unsupported, &result)
408 408 {
409 409 let mut args = original_args.iter();
410 410 let executable = match executable {
411 411 None => {
412 412 exit_no_fallback(
413 413 ui,
414 414 OnUnsupported::Abort,
415 415 Err(CommandError::abort(
416 416 "abort: 'rhg.on-unsupported=fallback' without \
417 417 'rhg.fallback-executable' set.",
418 418 )),
419 419 false,
420 420 );
421 421 }
422 422 Some(executable) => executable,
423 423 };
424 424 let executable_path = get_path_from_bytes(&executable);
425 425 let this_executable = args.next().expect("exepcted argv[0] to exist");
426 426 if executable_path == &PathBuf::from(this_executable) {
427 427 // Avoid spawning infinitely many processes until resource
428 428 // exhaustion.
429 429 let _ = ui.write_stderr(&format_bytes!(
430 430 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
431 431 points to `rhg` itself.\n",
432 432 executable
433 433 ));
434 434 on_unsupported = OnUnsupported::Abort
435 435 } else {
436 436 log::debug!("falling back (see trace-level log)");
437 437 log::trace!("{}", local_to_utf8(message));
438 438 if let Err(err) = which::which(executable_path) {
439 439 exit_no_fallback(
440 440 ui,
441 441 OnUnsupported::Abort,
442 442 Err(CommandError::InvalidFallback {
443 443 path: executable.to_owned(),
444 444 err: err.to_string(),
445 445 }),
446 446 use_detailed_exit_code,
447 447 )
448 448 }
449 449 // `args` is now `argv[1..]` since we’ve already consumed
450 450 // `argv[0]`
451 451 let mut command = Command::new(executable_path);
452 452 command.args(args);
453 453 if let Some(initial) = initial_current_dir {
454 454 command.current_dir(initial);
455 455 }
456 456 // We don't use subprocess because proper signal handling is harder
457 457 // and we don't want to keep `rhg` around after a fallback anyway.
458 458 // For example, if `rhg` is run in the background and falls back to
459 459 // `hg` which, in turn, waits for a signal, we'll get stuck if
460 460 // we're doing plain subprocess.
461 461 //
462 462 // If `exec` returns, we can only assume our process is very broken
463 463 // (see its documentation), so only try to forward the error code
464 464 // when exiting.
465 465 let err = command.exec();
466 466 std::process::exit(
467 467 err.raw_os_error().unwrap_or(exit_codes::ABORT),
468 468 );
469 469 }
470 470 }
471 471 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
472 472 }
473 473
474 474 fn exit_no_fallback(
475 475 ui: &Ui,
476 476 on_unsupported: OnUnsupported,
477 477 result: Result<(), CommandError>,
478 478 use_detailed_exit_code: bool,
479 479 ) -> ! {
480 480 match &result {
481 481 Ok(_) => {}
482 482 Err(CommandError::Unsuccessful) => {}
483 483 Err(CommandError::Abort {
484 484 message,
485 485 detailed_exit_code: _,
486 486 }) => {
487 487 if !message.is_empty() {
488 488 // Ignore errors when writing to stderr, we’re already exiting
489 489 // with failure code so there’s not much more we can do.
490 490 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
491 491 }
492 492 }
493 493 Err(CommandError::UnsupportedFeature { message }) => {
494 494 match on_unsupported {
495 495 OnUnsupported::Abort => {
496 496 let _ = ui.write_stderr(&format_bytes!(
497 497 b"unsupported feature: {}\n",
498 498 message
499 499 ));
500 500 }
501 501 OnUnsupported::AbortSilent => {}
502 502 OnUnsupported::Fallback { .. } => unreachable!(),
503 503 }
504 504 }
505 505 Err(CommandError::InvalidFallback { path, err }) => {
506 506 let _ = ui.write_stderr(&format_bytes!(
507 507 b"abort: invalid fallback '{}': {}\n",
508 508 path,
509 509 err.as_bytes(),
510 510 ));
511 511 }
512 512 }
513 513 std::process::exit(exit_code(&result, use_detailed_exit_code))
514 514 }
515 515
516 516 macro_rules! subcommands {
517 517 ($( $command: ident )+) => {
518 518 mod commands {
519 519 $(
520 520 pub mod $command;
521 521 )+
522 522 }
523 523
524 524 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
525 525 app
526 526 $(
527 527 .subcommand(commands::$command::args())
528 528 )+
529 529 }
530 530
531 531 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
532 532
533 533 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
534 534 match name {
535 535 $(
536 536 stringify!($command) => Some(commands::$command::run),
537 537 )+
538 538 _ => None,
539 539 }
540 540 }
541 541 };
542 542 }
543 543
544 544 subcommands! {
545 545 cat
546 546 debugdata
547 547 debugrequirements
548 548 debugignorerhg
549 549 files
550 550 root
551 551 config
552 552 status
553 553 }
554 554
555 555 pub struct CliInvocation<'a> {
556 556 ui: &'a Ui,
557 557 subcommand_args: &'a ArgMatches<'a>,
558 558 config: &'a Config,
559 559 /// References inside `Result` is a bit peculiar but allow
560 560 /// `invocation.repo?` to work out with `&CliInvocation` since this
561 561 /// `Result` type is `Copy`.
562 562 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
563 563 }
564 564
565 565 struct NoRepoInCwdError {
566 566 cwd: PathBuf,
567 567 }
568 568
569 569 /// CLI arguments to be parsed "early" in order to be able to read
570 570 /// configuration before using Clap. Ideally we would also use Clap for this,
571 571 /// see <https://github.com/clap-rs/clap/discussions/2366>.
572 572 ///
573 573 /// These arguments are still declared when we do use Clap later, so that Clap
574 574 /// does not return an error for their presence.
575 575 struct EarlyArgs {
576 576 /// Values of all `--config` arguments. (Possibly none)
577 577 config: Vec<Vec<u8>>,
578 578 /// Value of all the `--color` argument, if any.
579 579 color: Option<Vec<u8>>,
580 580 /// Value of the `-R` or `--repository` argument, if any.
581 581 repo: Option<Vec<u8>>,
582 582 /// Value of the `--cwd` argument, if any.
583 583 cwd: Option<Vec<u8>>,
584 584 }
585 585
586 586 impl EarlyArgs {
587 587 fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
588 588 let mut args = args.into_iter().map(get_bytes_from_os_str);
589 589 let mut config = Vec::new();
590 590 let mut color = None;
591 591 let mut repo = None;
592 592 let mut cwd = None;
593 593 // Use `while let` instead of `for` so that we can also call
594 594 // `args.next()` inside the loop.
595 595 while let Some(arg) = args.next() {
596 596 if arg == b"--config" {
597 597 if let Some(value) = args.next() {
598 598 config.push(value)
599 599 }
600 600 } else if let Some(value) = arg.drop_prefix(b"--config=") {
601 601 config.push(value.to_owned())
602 602 }
603 603
604 604 if arg == b"--color" {
605 605 if let Some(value) = args.next() {
606 606 color = Some(value)
607 607 }
608 608 } else if let Some(value) = arg.drop_prefix(b"--color=") {
609 609 color = Some(value.to_owned())
610 610 }
611 611
612 612 if arg == b"--cwd" {
613 613 if let Some(value) = args.next() {
614 614 cwd = Some(value)
615 615 }
616 616 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
617 617 cwd = Some(value.to_owned())
618 618 }
619 619
620 620 if arg == b"--repository" || arg == b"-R" {
621 621 if let Some(value) = args.next() {
622 622 repo = Some(value)
623 623 }
624 624 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
625 625 repo = Some(value.to_owned())
626 626 } else if let Some(value) = arg.drop_prefix(b"-R") {
627 627 repo = Some(value.to_owned())
628 628 }
629 629 }
630 630 Self {
631 631 config,
632 632 color,
633 633 repo,
634 634 cwd,
635 635 }
636 636 }
637 637 }
638 638
639 639 /// What to do when encountering some unsupported feature.
640 640 ///
641 641 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
642 642 enum OnUnsupported {
643 643 /// Print an error message describing what feature is not supported,
644 644 /// and exit with code 252.
645 645 Abort,
646 646 /// Silently exit with code 252.
647 647 AbortSilent,
648 648 /// Try running a Python implementation
649 649 Fallback { executable: Option<Vec<u8>> },
650 650 }
651 651
652 652 impl OnUnsupported {
653 653 const DEFAULT: Self = OnUnsupported::Abort;
654 654
655 655 fn from_config(config: &Config) -> Self {
656 656 match config
657 657 .get(b"rhg", b"on-unsupported")
658 658 .map(|value| value.to_ascii_lowercase())
659 659 .as_deref()
660 660 {
661 661 Some(b"abort") => OnUnsupported::Abort,
662 662 Some(b"abort-silent") => OnUnsupported::AbortSilent,
663 663 Some(b"fallback") => OnUnsupported::Fallback {
664 664 executable: config
665 665 .get(b"rhg", b"fallback-executable")
666 666 .map(|x| x.to_owned()),
667 667 },
668 668 None => Self::DEFAULT,
669 669 Some(_) => {
670 670 // TODO: warn about unknown config value
671 671 Self::DEFAULT
672 672 }
673 673 }
674 674 }
675 675 }
676 676
677 677 /// The `*` extension is an edge-case for config sub-options that apply to all
678 678 /// extensions. For now, only `:required` exists, but that may change in the
679 679 /// future.
680 680 const SUPPORTED_EXTENSIONS: &[&[u8]] =
681 681 &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
682 682
683 683 fn check_extensions(config: &Config) -> Result<(), CommandError> {
684 684 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
685 685 // All extensions are to be ignored, nothing to do here
686 686 return Ok(());
687 687 }
688 688
689 689 let enabled: HashSet<&[u8]> = config
690 690 .get_section_keys(b"extensions")
691 691 .into_iter()
692 692 .map(|extension| {
693 693 // Ignore extension suboptions. Only `required` exists for now.
694 694 // `rhg` either supports an extension or doesn't, so it doesn't
695 695 // make sense to consider the loading of an extension.
696 696 extension.split_2(b':').unwrap_or((extension, b"")).0
697 697 })
698 698 .collect();
699 699
700 700 let mut unsupported = enabled;
701 701 for supported in SUPPORTED_EXTENSIONS {
702 702 unsupported.remove(supported);
703 703 }
704 704
705 705 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
706 706 {
707 707 for ignored in ignored_list {
708 708 unsupported.remove(ignored.as_slice());
709 709 }
710 710 }
711 711
712 712 if unsupported.is_empty() {
713 713 Ok(())
714 714 } else {
715 715 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
716 716 // Sort the extensions to get a stable output
717 717 unsupported.sort();
718 718 Err(CommandError::UnsupportedFeature {
719 719 message: format_bytes!(
720 720 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
721 721 join(unsupported, b", ")
722 722 ),
723 723 })
724 724 }
725 725 }
726 726
727 727 /// Array of tuples of (auto upgrade conf, feature conf, local requirement)
728 728 const AUTO_UPGRADES: &[((&str, &str), (&str, &str), &str)] = &[
729 729 (
730 730 ("format", "use-share-safe.automatic-upgrade-of-mismatching-repositories"),
731 731 ("format", "use-share-safe"),
732 732 requirements::SHARESAFE_REQUIREMENT,
733 733 ),
734 734 (
735 735 ("format", "use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories"),
736 736 ("format", "use-dirstate-tracked-hint"),
737 737 requirements::DIRSTATE_TRACKED_HINT_V1,
738 738 ),
739 739 (
740 ("use-dirstate-v2", "automatic-upgrade-of-mismatching-repositories"),
740 ("format", "use-dirstate-v2.automatic-upgrade-of-mismatching-repositories"),
741 741 ("format", "use-dirstate-v2"),
742 742 requirements::DIRSTATE_V2_REQUIREMENT,
743 743 ),
744 744 ];
745 745
746 746 /// Mercurial allows users to automatically upgrade their repository.
747 747 /// `rhg` does not have the ability to upgrade yet, so fallback if an upgrade
748 748 /// is needed.
749 749 fn check_auto_upgrade(
750 750 config: &Config,
751 751 reqs: &HashSet<String>,
752 752 ) -> Result<(), CommandError> {
753 753 for (upgrade_conf, feature_conf, local_req) in AUTO_UPGRADES.iter() {
754 754 let auto_upgrade = config
755 755 .get_bool(upgrade_conf.0.as_bytes(), upgrade_conf.1.as_bytes())?;
756 756
757 757 if auto_upgrade {
758 758 let want_it = config.get_bool(
759 759 feature_conf.0.as_bytes(),
760 760 feature_conf.1.as_bytes(),
761 761 )?;
762 762 let have_it = reqs.contains(*local_req);
763 763
764 764 let action = match (want_it, have_it) {
765 765 (true, false) => Some("upgrade"),
766 766 (false, true) => Some("downgrade"),
767 767 _ => None,
768 768 };
769 769 if let Some(action) = action {
770 770 let message = format!(
771 771 "automatic {} {}.{}",
772 772 action, upgrade_conf.0, upgrade_conf.1
773 773 );
774 774 return Err(CommandError::unsupported(message));
775 775 }
776 776 }
777 777 }
778 778 Ok(())
779 779 }
780 780
781 781 fn check_unsupported(
782 782 config: &Config,
783 783 repo: Result<&Repo, &NoRepoInCwdError>,
784 784 ) -> Result<(), CommandError> {
785 785 check_extensions(config)?;
786 786
787 787 if std::env::var_os("HG_PENDING").is_some() {
788 788 // TODO: only if the value is `== repo.working_directory`?
789 789 // What about relative v.s. absolute paths?
790 790 Err(CommandError::unsupported("$HG_PENDING"))?
791 791 }
792 792
793 793 if let Ok(repo) = repo {
794 794 if repo.has_subrepos()? {
795 795 Err(CommandError::unsupported("sub-repositories"))?
796 796 }
797 797 check_auto_upgrade(config, repo.requirements())?;
798 798 }
799 799
800 800 if config.has_non_empty_section(b"encode") {
801 801 Err(CommandError::unsupported("[encode] config"))?
802 802 }
803 803
804 804 if config.has_non_empty_section(b"decode") {
805 805 Err(CommandError::unsupported("[decode] config"))?
806 806 }
807 807
808 808 Ok(())
809 809 }
@@ -1,238 +1,243 b''
1 1 ===============================
2 2 Test the "tracked hint" feature
3 3 ===============================
4 4
5 5 The tracked hint feature provide a file that get updated when the set of tracked
6 6 files get updated.
7 7
8 8 basic setup
9 9
10 10 $ cat << EOF >> $HGRCPATH
11 11 > [format]
12 12 > use-dirstate-tracked-hint=yes
13 13 > EOF
14 14
15 15 $ hg init tracked-hint-test
16 16 $ cd tracked-hint-test
17 17 $ hg debugbuilddag '.+10' -n
18 18 $ hg log -G -T '{rev} {desc} {files}\n'
19 19 o 10 r10 nf10
20 20 |
21 21 o 9 r9 nf9
22 22 |
23 23 o 8 r8 nf8
24 24 |
25 25 o 7 r7 nf7
26 26 |
27 27 o 6 r6 nf6
28 28 |
29 29 o 5 r5 nf5
30 30 |
31 31 o 4 r4 nf4
32 32 |
33 33 o 3 r3 nf3
34 34 |
35 35 o 2 r2 nf2
36 36 |
37 37 o 1 r1 nf1
38 38 |
39 39 o 0 r0 nf0
40 40
41 41 $ hg up tip
42 42 11 files updated, 0 files merged, 0 files removed, 0 files unresolved
43 43 $ hg files
44 44 nf0
45 45 nf1
46 46 nf10
47 47 nf2
48 48 nf3
49 49 nf4
50 50 nf5
51 51 nf6
52 52 nf7
53 53 nf8
54 54 nf9
55 55
56 56 key-file exists
57 57 -----------
58 58
59 59 The tracked hint file should exist
60 60
61 61 $ ls -1 .hg/dirstate*
62 62 .hg/dirstate
63 63 .hg/dirstate-tracked-hint
64 64
65 65 key-file stay the same if the tracked set is unchanged
66 66 ------------------------------------------------------
67 67
68 68 (copy its content for later comparison)
69 69
70 70 $ cp .hg/dirstate-tracked-hint ../key-bck
71 71 $ echo foo >> nf0
72 72 $ sleep 1
73 73 $ hg status
74 74 M nf0
75 75 $ diff --brief .hg/dirstate-tracked-hint ../key-bck
76 76 $ hg revert -C nf0
77 77 $ sleep 1
78 78 $ hg status
79 79 $ diff --brief .hg/dirstate-tracked-hint ../key-bck
80 80
81 81 key-file change if the tracked set is changed manually
82 82 ------------------------------------------------------
83 83
84 84 adding a file to tracking
85 85
86 86 $ cp .hg/dirstate-tracked-hint ../key-bck
87 87 $ echo x > x
88 88 $ hg add x
89 89 $ diff --brief .hg/dirstate-tracked-hint ../key-bck
90 90 Files .hg/dirstate-tracked-hint and ../key-bck differ
91 91 [1]
92 92
93 93 remove a file from tracking
94 94 (forget)
95 95
96 96 $ cp .hg/dirstate-tracked-hint ../key-bck
97 97 $ hg forget x
98 98 $ diff --brief .hg/dirstate-tracked-hint ../key-bck
99 99 Files .hg/dirstate-tracked-hint and ../key-bck differ
100 100 [1]
101 101
102 102 (remove)
103 103
104 104 $ cp .hg/dirstate-tracked-hint ../key-bck
105 105 $ hg remove nf1
106 106 $ diff --brief .hg/dirstate-tracked-hint ../key-bck
107 107 Files .hg/dirstate-tracked-hint and ../key-bck differ
108 108 [1]
109 109
110 110 key-file changes on revert (when applicable)
111 111 --------------------------------------------
112 112
113 113 $ cp .hg/dirstate-tracked-hint ../key-bck
114 114 $ hg status
115 115 R nf1
116 116 ? x
117 117 $ hg revert --all
118 118 undeleting nf1
119 119 $ hg status
120 120 ? x
121 121 $ diff --brief .hg/dirstate-tracked-hint ../key-bck
122 122 Files .hg/dirstate-tracked-hint and ../key-bck differ
123 123 [1]
124 124
125 125
126 126 `hg update` does affect the key-file (when needed)
127 127 --------------------------------------------------
128 128
129 129 update changing the tracked set
130 130
131 131 (removing)
132 132
133 133 $ cp .hg/dirstate-tracked-hint ../key-bck
134 134 $ hg status --rev . --rev '.#generations[-1]'
135 135 R nf10
136 136 $ hg up '.#generations[-1]'
137 137 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
138 138 $ diff --brief .hg/dirstate-tracked-hint ../key-bck
139 139 Files .hg/dirstate-tracked-hint and ../key-bck differ
140 140 [1]
141 141
142 142 (adding)
143 143
144 144 $ cp .hg/dirstate-tracked-hint ../key-bck
145 145 $ hg status --rev . --rev '.#generations[1]'
146 146 A nf10
147 147 $ hg up '.#generations[1]'
148 148 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
149 149 $ diff --brief .hg/dirstate-tracked-hint ../key-bck
150 150 Files .hg/dirstate-tracked-hint and ../key-bck differ
151 151 [1]
152 152
153 153 update not affecting the tracked set
154 154
155 155 $ echo foo >> nf0
156 156 $ hg commit -m foo
157 157
158 158 $ cp .hg/dirstate-tracked-hint ../key-bck
159 159 $ hg status --rev . --rev '.#generations[-1]'
160 160 M nf0
161 161 $ hg up '.#generations[-1]'
162 162 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
163 163 $ diff --brief .hg/dirstate-tracked-hint ../key-bck
164 164
165 165 Test upgrade and downgrade
166 166 ==========================
167 167
168 168 $ ls .hg/dirstate-tracked-hint
169 169 .hg/dirstate-tracked-hint
170 170 $ hg debugrequires | grep 'tracked'
171 171 dirstate-tracked-key-v1
172 172
173 173 downgrade
174 174
175 175 $ hg debugupgraderepo --config format.use-dirstate-tracked-hint=no --run --quiet
176 176 upgrade will perform the following actions:
177 177
178 178 requirements
179 179 preserved: * (glob)
180 180 removed: dirstate-tracked-key-v1
181 181
182 182 no revlogs to process
183 183
184 184 $ ls -1 .hg/dirstate-tracked-hint
185 185 ls: *.hg/dirstate-tracked-hint*: $ENOENT$ (glob)
186 186 [2]
187 187 $ hg debugrequires | grep 'tracked'
188 188 [1]
189 189
190 190 upgrade
191 191
192 192 $ hg debugupgraderepo --config format.use-dirstate-tracked-hint=yes --run --quiet
193 193 upgrade will perform the following actions:
194 194
195 195 requirements
196 196 preserved: * (glob)
197 197 added: dirstate-tracked-key-v1
198 198
199 199 no revlogs to process
200 200
201 201 $ ls -1 .hg/dirstate-tracked-hint
202 202 .hg/dirstate-tracked-hint
203 203 $ hg debugrequires | grep 'tracked'
204 204 dirstate-tracked-key-v1
205 205 $ cd ..
206 206
207 207 Test automatic upgrade and downgrade
208 208 ------------------------------------
209 209
210 210 create an initial repository
211 211
212 212 $ hg init auto-upgrade \
213 213 > --config format.use-dirstate-tracked-hint=no
214 214 $ hg debugbuilddag -R auto-upgrade --new-file .+5
215 215 $ hg -R auto-upgrade update
216 216 6 files updated, 0 files merged, 0 files removed, 0 files unresolved
217 217 $ hg debugformat -R auto-upgrade | grep tracked
218 218 tracked-hint: no
219 219
220 220 upgrade it to dirstate-tracked-hint automatically
221 221
222 222 $ hg status -R auto-upgrade \
223 223 > --config format.use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories=yes \
224 224 > --config format.use-dirstate-tracked-hint=yes
225 225 automatically upgrading repository to the `tracked-hint` feature
226 226 (see `hg help config.format.use-dirstate-tracked-hint` for details)
227 227 $ hg debugformat -R auto-upgrade | grep tracked
228 228 tracked-hint: yes
229 229
230 rhg supports this feature
231
232 $ hg status -R auto-upgrade \
233 > --config format.use-dirstate-tracked-hint=yes --config rhg.on-unsupported=abort
234
230 235 downgrade it from dirstate-tracked-hint automatically
231 236
232 237 $ hg status -R auto-upgrade \
233 238 > --config format.use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories=yes \
234 239 > --config format.use-dirstate-tracked-hint=no
235 240 automatically downgrading repository from the `tracked-hint` feature
236 241 (see `hg help config.format.use-dirstate-tracked-hint` for details)
237 242 $ hg debugformat -R auto-upgrade | grep tracked
238 243 tracked-hint: no
General Comments 0
You need to be logged in to leave comments. Login now