##// END OF EJS Templates
rhg: Add support for automatic fallback to Python...
Simon Sapin -
r47425:93e9f448 default
parent child Browse files
Show More
@@ -1,286 +1,336
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;
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 use std::process::Command;
14 15
15 16 mod blackbox;
16 17 mod error;
17 18 mod exitcode;
18 19 mod ui;
19 20 use error::CommandError;
20 21
21 22 fn main_with_result(
22 23 process_start_time: &blackbox::ProcessStartTime,
23 24 ui: &ui::Ui,
24 25 repo: Result<&Repo, &NoRepoInCwdError>,
25 26 config: &Config,
26 27 ) -> Result<(), CommandError> {
27 28 let app = App::new("rhg")
28 29 .global_setting(AppSettings::AllowInvalidUtf8)
29 30 .setting(AppSettings::SubcommandRequired)
30 31 .setting(AppSettings::VersionlessSubcommands)
31 32 .arg(
32 33 Arg::with_name("repository")
33 34 .help("repository root directory")
34 35 .short("-R")
35 36 .long("--repository")
36 37 .value_name("REPO")
37 38 .takes_value(true)
38 39 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
39 40 .global(true),
40 41 )
41 42 .arg(
42 43 Arg::with_name("config")
43 44 .help("set/override config option (use 'section.name=value')")
44 45 .long("--config")
45 46 .value_name("CONFIG")
46 47 .takes_value(true)
47 48 .global(true)
48 49 // Ok: `--config section.key1=val --config section.key2=val2`
49 50 .multiple(true)
50 51 // Not ok: `--config section.key1=val section.key2=val2`
51 52 .number_of_values(1),
52 53 )
53 54 .version("0.0.1");
54 55 let app = add_subcommand_args(app);
55 56
56 57 let matches = app.clone().get_matches_safe()?;
57 58
58 59 let (subcommand_name, subcommand_matches) = matches.subcommand();
59 60 let run = subcommand_run_fn(subcommand_name)
60 61 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
61 62 let subcommand_args = subcommand_matches
62 63 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
63 64
64 65 let invocation = CliInvocation {
65 66 ui,
66 67 subcommand_args,
67 68 config,
68 69 repo,
69 70 };
70 71 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
71 72 blackbox.log_command_start();
72 73 let result = run(&invocation);
73 74 blackbox.log_command_end(exit_code(&result));
74 75 result
75 76 }
76 77
77 78 fn main() {
78 79 // Run this first, before we find out if the blackbox extension is even
79 80 // enabled, in order to include everything in-between in the duration
80 81 // measurements. Reading config files can be slow if they’re on NFS.
81 82 let process_start_time = blackbox::ProcessStartTime::now();
82 83
83 84 env_logger::init();
84 85 let ui = ui::Ui::new();
85 86
86 87 let early_args = EarlyArgs::parse(std::env::args_os());
87 88 let non_repo_config =
88 89 Config::load(early_args.config).unwrap_or_else(|error| {
89 90 // Normally this is decided based on config, but we don’t have that
90 91 // available. As of this writing config loading never returns an
91 92 // "unsupported" error but that is not enforced by the type system.
92 93 let on_unsupported = OnUnsupported::Abort;
93 94
94 95 exit(&ui, on_unsupported, Err(error.into()))
95 96 });
96 97
97 98 let repo_path = early_args.repo.as_deref().map(get_path_from_bytes);
98 99 let repo_result = match Repo::find(&non_repo_config, repo_path) {
99 100 Ok(repo) => Ok(repo),
100 101 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
101 102 // Not finding a repo is not fatal yet, if `-R` was not given
102 103 Err(NoRepoInCwdError { cwd: at })
103 104 }
104 105 Err(error) => exit(
105 106 &ui,
106 107 OnUnsupported::from_config(&non_repo_config),
107 108 Err(error.into()),
108 109 ),
109 110 };
110 111
111 112 let config = if let Ok(repo) = &repo_result {
112 113 repo.config()
113 114 } else {
114 115 &non_repo_config
115 116 };
116 117
117 118 let result = main_with_result(
118 119 &process_start_time,
119 120 &ui,
120 121 repo_result.as_ref(),
121 122 config,
122 123 );
123 124 exit(&ui, OnUnsupported::from_config(config), result)
124 125 }
125 126
126 127 fn exit_code(result: &Result<(), CommandError>) -> i32 {
127 128 match result {
128 129 Ok(()) => exitcode::OK,
129 130 Err(CommandError::Abort { .. }) => exitcode::ABORT,
130 131
131 132 // Exit with a specific code and no error message to let a potential
132 133 // wrapper script fallback to Python-based Mercurial.
133 134 Err(CommandError::UnsupportedFeature { .. }) => {
134 135 exitcode::UNIMPLEMENTED
135 136 }
136 137 }
137 138 }
138 139
139 140 fn exit(
140 141 ui: &Ui,
141 on_unsupported: OnUnsupported,
142 mut on_unsupported: OnUnsupported,
142 143 result: Result<(), CommandError>,
143 144 ) -> ! {
145 if let (
146 OnUnsupported::Fallback { executable },
147 Err(CommandError::UnsupportedFeature { .. }),
148 ) = (&on_unsupported, &result)
149 {
150 let mut args = std::env::args_os();
151 let executable_path = get_path_from_bytes(&executable);
152 let this_executable = args.next().expect("exepcted argv[0] to exist");
153 if executable_path == &PathBuf::from(this_executable) {
154 // Avoid spawning infinitely many processes until resource
155 // exhaustion.
156 let _ = ui.write_stderr(&format_bytes!(
157 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
158 points to `rhg` itself.\n",
159 executable
160 ));
161 on_unsupported = OnUnsupported::Abort
162 } else {
163 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
164 let result = Command::new(executable_path).args(args).status();
165 match result {
166 Ok(status) => std::process::exit(
167 status.code().unwrap_or(exitcode::ABORT),
168 ),
169 Err(error) => {
170 let _ = ui.write_stderr(&format_bytes!(
171 b"tried to fall back to a '{}' sub-process but got error {}\n",
172 executable, format_bytes::Utf8(error)
173 ));
174 on_unsupported = OnUnsupported::Abort
175 }
176 }
177 }
178 }
144 179 match &result {
145 180 Ok(_) => {}
146 181 Err(CommandError::Abort { message }) => {
147 182 if !message.is_empty() {
148 183 // Ignore errors when writing to stderr, we’re already exiting
149 184 // with failure code so there’s not much more we can do.
150 185 let _ =
151 186 ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
152 187 }
153 188 }
154 189 Err(CommandError::UnsupportedFeature { message }) => {
155 190 match on_unsupported {
156 191 OnUnsupported::Abort => {
157 192 let _ = ui.write_stderr(&format_bytes!(
158 193 b"unsupported feature: {}\n",
159 194 message
160 195 ));
161 196 }
162 197 OnUnsupported::AbortSilent => {}
198 OnUnsupported::Fallback { .. } => unreachable!(),
163 199 }
164 200 }
165 201 }
166 202 std::process::exit(exit_code(&result))
167 203 }
168 204
169 205 macro_rules! subcommands {
170 206 ($( $command: ident )+) => {
171 207 mod commands {
172 208 $(
173 209 pub mod $command;
174 210 )+
175 211 }
176 212
177 213 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
178 214 app
179 215 $(
180 216 .subcommand(commands::$command::args())
181 217 )+
182 218 }
183 219
184 220 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
185 221
186 222 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
187 223 match name {
188 224 $(
189 225 stringify!($command) => Some(commands::$command::run),
190 226 )+
191 227 _ => None,
192 228 }
193 229 }
194 230 };
195 231 }
196 232
197 233 subcommands! {
198 234 cat
199 235 debugdata
200 236 debugrequirements
201 237 files
202 238 root
203 239 config
204 240 }
205 241 pub struct CliInvocation<'a> {
206 242 ui: &'a Ui,
207 243 subcommand_args: &'a ArgMatches<'a>,
208 244 config: &'a Config,
209 245 /// References inside `Result` is a bit peculiar but allow
210 246 /// `invocation.repo?` to work out with `&CliInvocation` since this
211 247 /// `Result` type is `Copy`.
212 248 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
213 249 }
214 250
215 251 struct NoRepoInCwdError {
216 252 cwd: PathBuf,
217 253 }
218 254
219 255 /// CLI arguments to be parsed "early" in order to be able to read
220 256 /// configuration before using Clap. Ideally we would also use Clap for this,
221 257 /// see <https://github.com/clap-rs/clap/discussions/2366>.
222 258 ///
223 259 /// These arguments are still declared when we do use Clap later, so that Clap
224 260 /// does not return an error for their presence.
225 261 struct EarlyArgs {
226 262 /// Values of all `--config` arguments. (Possibly none)
227 263 config: Vec<Vec<u8>>,
228 264 /// Value of the `-R` or `--repository` argument, if any.
229 265 repo: Option<Vec<u8>>,
230 266 }
231 267
232 268 impl EarlyArgs {
233 269 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
234 270 let mut args = args.into_iter().map(get_bytes_from_os_str);
235 271 let mut config = Vec::new();
236 272 let mut repo = None;
237 273 // Use `while let` instead of `for` so that we can also call
238 274 // `args.next()` inside the loop.
239 275 while let Some(arg) = args.next() {
240 276 if arg == b"--config" {
241 277 if let Some(value) = args.next() {
242 278 config.push(value)
243 279 }
244 280 } else if let Some(value) = arg.drop_prefix(b"--config=") {
245 281 config.push(value.to_owned())
246 282 }
247 283
248 284 if arg == b"--repository" || arg == b"-R" {
249 285 if let Some(value) = args.next() {
250 286 repo = Some(value)
251 287 }
252 288 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
253 289 repo = Some(value.to_owned())
254 290 } else if let Some(value) = arg.drop_prefix(b"-R") {
255 291 repo = Some(value.to_owned())
256 292 }
257 293 }
258 294 Self { config, repo }
259 295 }
260 296 }
261 297
262 298 /// What to do when encountering some unsupported feature.
263 299 ///
264 300 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
265 301 enum OnUnsupported {
266 302 /// Print an error message describing what feature is not supported,
267 303 /// and exit with code 252.
268 304 Abort,
269 305 /// Silently exit with code 252.
270 306 AbortSilent,
307 /// Try running a Python implementation
308 Fallback { executable: Vec<u8> },
271 309 }
272 310
273 311 impl OnUnsupported {
312 const DEFAULT: Self = OnUnsupported::Abort;
313 const DEFAULT_FALLBACK_EXECUTABLE: &'static [u8] = b"hg";
314
274 315 fn from_config(config: &Config) -> Self {
275 let default = OnUnsupported::Abort;
276 match config.get(b"rhg", b"on-unsupported") {
316 match config
317 .get(b"rhg", b"on-unsupported")
318 .map(|value| value.to_ascii_lowercase())
319 .as_deref()
320 {
277 321 Some(b"abort") => OnUnsupported::Abort,
278 322 Some(b"abort-silent") => OnUnsupported::AbortSilent,
279 None => default,
323 Some(b"fallback") => OnUnsupported::Fallback {
324 executable: config
325 .get(b"rhg", b"fallback-executable")
326 .unwrap_or(Self::DEFAULT_FALLBACK_EXECUTABLE)
327 .to_owned(),
328 },
329 None => Self::DEFAULT,
280 330 Some(_) => {
281 331 // TODO: warn about unknown config value
282 default
332 Self::DEFAULT
283 333 }
284 334 }
285 335 }
286 336 }
@@ -1,287 +1,309
1 1 #require rust
2 2
3 3 Define an rhg function that will only run if rhg exists
4 $ RHG="$RUNTESTDIR/../rust/target/release/rhg"
4 5 $ rhg() {
5 > if [ -f "$RUNTESTDIR/../rust/target/release/rhg" ]; then
6 > "$RUNTESTDIR/../rust/target/release/rhg" "$@"
6 > if [ -f "$RHG" ]; then
7 > "$RHG" "$@"
7 8 > else
8 9 > echo "skipped: Cannot find rhg. Try to run cargo build in rust/rhg."
9 10 > exit 80
10 11 > fi
11 12 > }
12 13
13 14 Unimplemented command
14 15 $ rhg unimplemented-command
15 16 unsupported feature: error: Found argument 'unimplemented-command' which wasn't expected, or isn't valid in this context
16 17
17 18 USAGE:
18 19 rhg [OPTIONS] <SUBCOMMAND>
19 20
20 21 For more information try --help
21 22
22 23 [252]
23 24 $ rhg unimplemented-command --config rhg.on-unsupported=abort-silent
24 25 [252]
25 26
26 27 Finding root
27 28 $ rhg root
28 29 abort: no repository found in '$TESTTMP' (.hg not found)!
29 30 [255]
30 31
31 32 $ hg init repository
32 33 $ cd repository
33 34 $ rhg root
34 35 $TESTTMP/repository
35 36
36 37 Reading and setting configuration
37 38 $ echo "[ui]" >> $HGRCPATH
38 39 $ echo "username = user1" >> $HGRCPATH
39 40 $ rhg config ui.username
40 41 user1
41 42 $ echo "[ui]" >> .hg/hgrc
42 43 $ echo "username = user2" >> .hg/hgrc
43 44 $ rhg config ui.username
44 45 user2
45 46 $ rhg --config ui.username=user3 config ui.username
46 47 user3
47 48
48 49 Unwritable file descriptor
49 50 $ rhg root > /dev/full
50 51 abort: No space left on device (os error 28)
51 52 [255]
52 53
53 54 Deleted repository
54 55 $ rm -rf `pwd`
55 56 $ rhg root
56 57 abort: $ENOENT$: current directory
57 58 [255]
58 59
59 60 Listing tracked files
60 61 $ cd $TESTTMP
61 62 $ hg init repository
62 63 $ cd repository
63 64 $ for i in 1 2 3; do
64 65 > echo $i >> file$i
65 66 > hg add file$i
66 67 > done
67 68 > hg commit -m "commit $i" -q
68 69
69 70 Listing tracked files from root
70 71 $ rhg files
71 72 file1
72 73 file2
73 74 file3
74 75
75 76 Listing tracked files from subdirectory
76 77 $ mkdir -p path/to/directory
77 78 $ cd path/to/directory
78 79 $ rhg files
79 80 ../../../file1
80 81 ../../../file2
81 82 ../../../file3
82 83
83 84 Listing tracked files through broken pipe
84 85 $ rhg files | head -n 1
85 86 ../../../file1
86 87
87 88 Debuging data in inline index
88 89 $ cd $TESTTMP
89 90 $ rm -rf repository
90 91 $ hg init repository
91 92 $ cd repository
92 93 $ for i in 1 2 3 4 5 6; do
93 94 > echo $i >> file-$i
94 95 > hg add file-$i
95 96 > hg commit -m "Commit $i" -q
96 97 > done
97 98 $ rhg debugdata -c 2
98 99 8d0267cb034247ebfa5ee58ce59e22e57a492297
99 100 test
100 101 0 0
101 102 file-3
102 103
103 104 Commit 3 (no-eol)
104 105 $ rhg debugdata -m 2
105 106 file-1\x00b8e02f6433738021a065f94175c7cd23db5f05be (esc)
106 107 file-2\x005d9299349fc01ddd25d0070d149b124d8f10411e (esc)
107 108 file-3\x002661d26c649684b482d10f91960cc3db683c38b4 (esc)
108 109
109 110 Debuging with full node id
110 111 $ rhg debugdata -c `hg log -r 0 -T '{node}'`
111 112 d1d1c679d3053e8926061b6f45ca52009f011e3f
112 113 test
113 114 0 0
114 115 file-1
115 116
116 117 Commit 1 (no-eol)
117 118
118 119 Specifying revisions by changeset ID
119 120 $ hg log -T '{node}\n'
120 121 c6ad58c44207b6ff8a4fbbca7045a5edaa7e908b
121 122 d654274993d0149eecc3cc03214f598320211900
122 123 f646af7e96481d3a5470b695cf30ad8e3ab6c575
123 124 cf8b83f14ead62b374b6e91a0e9303b85dfd9ed7
124 125 91c6f6e73e39318534dc415ea4e8a09c99cd74d6
125 126 6ae9681c6d30389694d8701faf24b583cf3ccafe
126 127 $ rhg files -r cf8b83
127 128 file-1
128 129 file-2
129 130 file-3
130 131 $ rhg cat -r cf8b83 file-2
131 132 2
132 133 $ rhg cat -r c file-2
133 134 abort: ambiguous revision identifier c
134 135 [255]
135 136 $ rhg cat -r d file-2
136 137 2
137 138
138 139 Cat files
139 140 $ cd $TESTTMP
140 141 $ rm -rf repository
141 142 $ hg init repository
142 143 $ cd repository
143 144 $ echo "original content" > original
144 145 $ hg add original
145 146 $ hg commit -m "add original" original
146 147 $ rhg cat -r 0 original
147 148 original content
148 149 Cat copied file should not display copy metadata
149 150 $ hg copy original copy_of_original
150 151 $ hg commit -m "add copy of original"
151 152 $ rhg cat -r 1 copy_of_original
152 153 original content
153 154
155 Fallback to Python
156 $ rhg cat original
157 unsupported feature: `rhg cat` without `--rev` / `-r`
158 [252]
159 $ FALLBACK="--config rhg.on-unsupported=fallback"
160 $ rhg cat original $FALLBACK
161 original content
162
163 $ rhg cat original $FALLBACK --config rhg.fallback-executable=false
164 [1]
165
166 $ rhg cat original $FALLBACK --config rhg.fallback-executable=hg-non-existent
167 tried to fall back to a 'hg-non-existent' sub-process but got error $ENOENT$
168 unsupported feature: `rhg cat` without `--rev` / `-r`
169 [252]
170
171 $ rhg cat original $FALLBACK --config rhg.fallback-executable="$RHG"
172 Blocking recursive fallback. The 'rhg.fallback-executable = */rust/target/release/rhg' config points to `rhg` itself. (glob)
173 unsupported feature: `rhg cat` without `--rev` / `-r`
174 [252]
175
154 176 Requirements
155 177 $ rhg debugrequirements
156 178 dotencode
157 179 fncache
158 180 generaldelta
159 181 revlogv1
160 182 sparserevlog
161 183 store
162 184
163 185 $ echo indoor-pool >> .hg/requires
164 186 $ rhg files
165 187 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
166 188 [252]
167 189
168 190 $ rhg cat -r 1 copy_of_original
169 191 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
170 192 [252]
171 193
172 194 $ rhg debugrequirements
173 195 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
174 196 [252]
175 197
176 198 $ echo -e '\xFF' >> .hg/requires
177 199 $ rhg debugrequirements
178 200 abort: corrupted repository: parse error in 'requires' file
179 201 [255]
180 202
181 203 Persistent nodemap
182 204 $ cd $TESTTMP
183 205 $ rm -rf repository
184 206 $ hg init repository
185 207 $ cd repository
186 208 $ rhg debugrequirements | grep nodemap
187 209 [1]
188 210 $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn"
189 211 $ hg id -r tip
190 212 c3ae8dec9fad tip
191 213 $ ls .hg/store/00changelog*
192 214 .hg/store/00changelog.d
193 215 .hg/store/00changelog.i
194 216 $ rhg files -r c3ae8dec9fad
195 217 of
196 218
197 219 $ cd $TESTTMP
198 220 $ rm -rf repository
199 221 $ hg --config format.use-persistent-nodemap=True init repository
200 222 $ cd repository
201 223 $ rhg debugrequirements | grep nodemap
202 224 persistent-nodemap
203 225 $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn"
204 226 $ hg id -r tip
205 227 c3ae8dec9fad tip
206 228 $ ls .hg/store/00changelog*
207 229 .hg/store/00changelog-*.nd (glob)
208 230 .hg/store/00changelog.d
209 231 .hg/store/00changelog.i
210 232 .hg/store/00changelog.n
211 233
212 234 Specifying revisions by changeset ID
213 235 $ rhg files -r c3ae8dec9fad
214 236 of
215 237 $ rhg cat -r c3ae8dec9fad of
216 238 r5000
217 239
218 240 Crate a shared repository
219 241
220 242 $ echo "[extensions]" >> $HGRCPATH
221 243 $ echo "share = " >> $HGRCPATH
222 244
223 245 $ cd $TESTTMP
224 246 $ hg init repo1
225 247 $ echo a > repo1/a
226 248 $ hg -R repo1 commit -A -m'init'
227 249 adding a
228 250
229 251 $ hg share repo1 repo2
230 252 updating working directory
231 253 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
232 254
233 255 And check that basic rhg commands work with sharing
234 256
235 257 $ rhg files -R repo2
236 258 repo2/a
237 259 $ rhg -R repo2 cat -r 0 repo2/a
238 260 a
239 261
240 262 Same with relative sharing
241 263
242 264 $ hg share repo2 repo3 --relative
243 265 updating working directory
244 266 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
245 267
246 268 $ rhg files -R repo3
247 269 repo3/a
248 270 $ rhg -R repo3 cat -r 0 repo3/a
249 271 a
250 272
251 273 Same with share-safe
252 274
253 275 $ echo "[format]" >> $HGRCPATH
254 276 $ echo "use-share-safe = True" >> $HGRCPATH
255 277
256 278 $ cd $TESTTMP
257 279 $ hg init repo4
258 280 $ cd repo4
259 281 $ echo a > a
260 282 $ hg commit -A -m'init'
261 283 adding a
262 284
263 285 $ cd ..
264 286 $ hg share repo4 repo5
265 287 updating working directory
266 288 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
267 289
268 290 And check that basic rhg commands work with sharing
269 291
270 292 $ cd repo5
271 293 $ rhg files
272 294 a
273 295 $ rhg cat -r 0 a
274 296 a
275 297
276 298 The blackbox extension is supported
277 299
278 300 $ echo "[extensions]" >> $HGRCPATH
279 301 $ echo "blackbox =" >> $HGRCPATH
280 302 $ echo "[blackbox]" >> $HGRCPATH
281 303 $ echo "maxsize = 1" >> $HGRCPATH
282 304 $ rhg files > /dev/null
283 305 $ cat .hg/blackbox.log
284 306 ????/??/?? ??:??:??.??? * @d3873e73d99ef67873dac33fbcc66268d5d2b6f4 (*)> (rust) files exited 0 after 0.??? seconds (glob)
285 307 $ cat .hg/blackbox.log.1
286 308 ????/??/?? ??:??:??.??? * @d3873e73d99ef67873dac33fbcc66268d5d2b6f4 (*)> (rust) files (glob)
287 309
General Comments 0
You need to be logged in to leave comments. Login now