##// END OF EJS Templates
rhg: look for repository in ancestors also instead of cwd only...
Pulkit Goyal -
r48197:88119fff default
parent child Browse files
Show More
@@ -1,277 +1,284 b''
1 1 use crate::config::{Config, ConfigError, ConfigParseError};
2 2 use crate::errors::{HgError, IoErrorContext, IoResultExt};
3 3 use crate::requirements;
4 4 use crate::utils::files::get_path_from_bytes;
5 5 use crate::utils::SliceExt;
6 6 use memmap::{Mmap, MmapOptions};
7 7 use std::collections::HashSet;
8 8 use std::path::{Path, PathBuf};
9 9
10 10 /// A repository on disk
11 11 pub struct Repo {
12 12 working_directory: PathBuf,
13 13 dot_hg: PathBuf,
14 14 store: PathBuf,
15 15 requirements: HashSet<String>,
16 16 config: Config,
17 17 }
18 18
19 19 #[derive(Debug, derive_more::From)]
20 20 pub enum RepoError {
21 21 NotFound {
22 22 at: PathBuf,
23 23 },
24 24 #[from]
25 25 ConfigParseError(ConfigParseError),
26 26 #[from]
27 27 Other(HgError),
28 28 }
29 29
30 30 impl From<ConfigError> for RepoError {
31 31 fn from(error: ConfigError) -> Self {
32 32 match error {
33 33 ConfigError::Parse(error) => error.into(),
34 34 ConfigError::Other(error) => error.into(),
35 35 }
36 36 }
37 37 }
38 38
39 39 /// Filesystem access abstraction for the contents of a given "base" diretory
40 40 #[derive(Clone, Copy)]
41 41 pub struct Vfs<'a> {
42 42 pub(crate) base: &'a Path,
43 43 }
44 44
45 45 impl Repo {
46 /// tries to find nearest repository root in current working directory or
47 /// its ancestors
48 pub fn find_repo_root() -> Result<PathBuf, RepoError> {
49 let current_directory = crate::utils::current_dir()?;
50 // ancestors() is inclusive: it first yields `current_directory`
51 // as-is.
52 for ancestor in current_directory.ancestors() {
53 if ancestor.join(".hg").is_dir() {
54 return Ok(ancestor.to_path_buf());
55 }
56 }
57 return Err(RepoError::NotFound {
58 at: current_directory,
59 });
60 }
61
46 62 /// Find a repository, either at the given path (which must contain a `.hg`
47 63 /// sub-directory) or by searching the current directory and its
48 64 /// ancestors.
49 65 ///
50 66 /// A method with two very different "modes" like this usually a code smell
51 67 /// to make two methods instead, but in this case an `Option` is what rhg
52 68 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
53 69 /// Having two methods would just move that `if` to almost all callers.
54 70 pub fn find(
55 71 config: &Config,
56 72 explicit_path: Option<PathBuf>,
57 73 ) -> Result<Self, RepoError> {
58 74 if let Some(root) = explicit_path {
59 75 if root.join(".hg").is_dir() {
60 76 Self::new_at_path(root.to_owned(), config)
61 77 } else if root.is_file() {
62 78 Err(HgError::unsupported("bundle repository").into())
63 79 } else {
64 80 Err(RepoError::NotFound {
65 81 at: root.to_owned(),
66 82 })
67 83 }
68 84 } else {
69 let current_directory = crate::utils::current_dir()?;
70 // ancestors() is inclusive: it first yields `current_directory`
71 // as-is.
72 for ancestor in current_directory.ancestors() {
73 if ancestor.join(".hg").is_dir() {
74 return Self::new_at_path(ancestor.to_owned(), config);
75 }
76 }
77 Err(RepoError::NotFound {
78 at: current_directory,
79 })
85 let root = Self::find_repo_root()?;
86 Self::new_at_path(root, config)
80 87 }
81 88 }
82 89
83 90 /// To be called after checking that `.hg` is a sub-directory
84 91 fn new_at_path(
85 92 working_directory: PathBuf,
86 93 config: &Config,
87 94 ) -> Result<Self, RepoError> {
88 95 let dot_hg = working_directory.join(".hg");
89 96
90 97 let mut repo_config_files = Vec::new();
91 98 repo_config_files.push(dot_hg.join("hgrc"));
92 99 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
93 100
94 101 let hg_vfs = Vfs { base: &dot_hg };
95 102 let mut reqs = requirements::load_if_exists(hg_vfs)?;
96 103 let relative =
97 104 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
98 105 let shared =
99 106 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
100 107
101 108 // From `mercurial/localrepo.py`:
102 109 //
103 110 // if .hg/requires contains the sharesafe requirement, it means
104 111 // there exists a `.hg/store/requires` too and we should read it
105 112 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
106 113 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
107 114 // is not present, refer checkrequirementscompat() for that
108 115 //
109 116 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
110 117 // repository was shared the old way. We check the share source
111 118 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
112 119 // current repository needs to be reshared
113 120 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
114 121
115 122 let store_path;
116 123 if !shared {
117 124 store_path = dot_hg.join("store");
118 125 } else {
119 126 let bytes = hg_vfs.read("sharedpath")?;
120 127 let mut shared_path =
121 128 get_path_from_bytes(bytes.trim_end_newlines()).to_owned();
122 129 if relative {
123 130 shared_path = dot_hg.join(shared_path)
124 131 }
125 132 if !shared_path.is_dir() {
126 133 return Err(HgError::corrupted(format!(
127 134 ".hg/sharedpath points to nonexistent directory {}",
128 135 shared_path.display()
129 136 ))
130 137 .into());
131 138 }
132 139
133 140 store_path = shared_path.join("store");
134 141
135 142 let source_is_share_safe =
136 143 requirements::load(Vfs { base: &shared_path })?
137 144 .contains(requirements::SHARESAFE_REQUIREMENT);
138 145
139 146 if share_safe && !source_is_share_safe {
140 147 return Err(match config
141 148 .get(b"share", b"safe-mismatch.source-not-safe")
142 149 {
143 150 Some(b"abort") | None => HgError::abort(
144 151 "abort: share source does not support share-safe requirement\n\
145 152 (see `hg help config.format.use-share-safe` for more information)",
146 153 ),
147 154 _ => HgError::unsupported("share-safe downgrade"),
148 155 }
149 156 .into());
150 157 } else if source_is_share_safe && !share_safe {
151 158 return Err(
152 159 match config.get(b"share", b"safe-mismatch.source-safe") {
153 160 Some(b"abort") | None => HgError::abort(
154 161 "abort: version mismatch: source uses share-safe \
155 162 functionality while the current share does not\n\
156 163 (see `hg help config.format.use-share-safe` for more information)",
157 164 ),
158 165 _ => HgError::unsupported("share-safe upgrade"),
159 166 }
160 167 .into(),
161 168 );
162 169 }
163 170
164 171 if share_safe {
165 172 repo_config_files.insert(0, shared_path.join("hgrc"))
166 173 }
167 174 }
168 175 if share_safe {
169 176 reqs.extend(requirements::load(Vfs { base: &store_path })?);
170 177 }
171 178
172 179 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
173 180 config.combine_with_repo(&repo_config_files)?
174 181 } else {
175 182 config.clone()
176 183 };
177 184
178 185 let repo = Self {
179 186 requirements: reqs,
180 187 working_directory,
181 188 store: store_path,
182 189 dot_hg,
183 190 config: repo_config,
184 191 };
185 192
186 193 requirements::check(&repo)?;
187 194
188 195 Ok(repo)
189 196 }
190 197
191 198 pub fn working_directory_path(&self) -> &Path {
192 199 &self.working_directory
193 200 }
194 201
195 202 pub fn requirements(&self) -> &HashSet<String> {
196 203 &self.requirements
197 204 }
198 205
199 206 pub fn config(&self) -> &Config {
200 207 &self.config
201 208 }
202 209
203 210 /// For accessing repository files (in `.hg`), except for the store
204 211 /// (`.hg/store`).
205 212 pub fn hg_vfs(&self) -> Vfs<'_> {
206 213 Vfs { base: &self.dot_hg }
207 214 }
208 215
209 216 /// For accessing repository store files (in `.hg/store`)
210 217 pub fn store_vfs(&self) -> Vfs<'_> {
211 218 Vfs { base: &self.store }
212 219 }
213 220
214 221 /// For accessing the working copy
215 222 pub fn working_directory_vfs(&self) -> Vfs<'_> {
216 223 Vfs {
217 224 base: &self.working_directory,
218 225 }
219 226 }
220 227
221 228 pub fn has_dirstate_v2(&self) -> bool {
222 229 self.requirements
223 230 .contains(requirements::DIRSTATE_V2_REQUIREMENT)
224 231 }
225 232
226 233 pub fn dirstate_parents(
227 234 &self,
228 235 ) -> Result<crate::dirstate::DirstateParents, HgError> {
229 236 let dirstate = self.hg_vfs().mmap_open("dirstate")?;
230 237 if dirstate.is_empty() {
231 238 return Ok(crate::dirstate::DirstateParents::NULL);
232 239 }
233 240 let parents = if self.has_dirstate_v2() {
234 241 crate::dirstate_tree::on_disk::parse_dirstate_parents(&dirstate)?
235 242 } else {
236 243 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?
237 244 };
238 245 Ok(parents.clone())
239 246 }
240 247 }
241 248
242 249 impl Vfs<'_> {
243 250 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
244 251 self.base.join(relative_path)
245 252 }
246 253
247 254 pub fn read(
248 255 &self,
249 256 relative_path: impl AsRef<Path>,
250 257 ) -> Result<Vec<u8>, HgError> {
251 258 let path = self.join(relative_path);
252 259 std::fs::read(&path).when_reading_file(&path)
253 260 }
254 261
255 262 pub fn mmap_open(
256 263 &self,
257 264 relative_path: impl AsRef<Path>,
258 265 ) -> Result<Mmap, HgError> {
259 266 let path = self.base.join(relative_path);
260 267 let file = std::fs::File::open(&path).when_reading_file(&path)?;
261 268 // TODO: what are the safety requirements here?
262 269 let mmap = unsafe { MmapOptions::new().map(&file) }
263 270 .when_reading_file(&path)?;
264 271 Ok(mmap)
265 272 }
266 273
267 274 pub fn rename(
268 275 &self,
269 276 relative_from: impl AsRef<Path>,
270 277 relative_to: impl AsRef<Path>,
271 278 ) -> Result<(), HgError> {
272 279 let from = self.join(relative_from);
273 280 let to = self.join(relative_to);
274 281 std::fs::rename(&from, &to)
275 282 .with_context(|| IoErrorContext::RenamingFile { from, to })
276 283 }
277 284 }
@@ -1,575 +1,574 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, ConfigSource};
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 32 .global_setting(AppSettings::DisableVersion)
33 33 .setting(AppSettings::SubcommandRequired)
34 34 .setting(AppSettings::VersionlessSubcommands)
35 35 .arg(
36 36 Arg::with_name("repository")
37 37 .help("repository root directory")
38 38 .short("-R")
39 39 .long("--repository")
40 40 .value_name("REPO")
41 41 .takes_value(true)
42 42 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
43 43 .global(true),
44 44 )
45 45 .arg(
46 46 Arg::with_name("config")
47 47 .help("set/override config option (use 'section.name=value')")
48 48 .long("--config")
49 49 .value_name("CONFIG")
50 50 .takes_value(true)
51 51 .global(true)
52 52 // Ok: `--config section.key1=val --config section.key2=val2`
53 53 .multiple(true)
54 54 // Not ok: `--config section.key1=val section.key2=val2`
55 55 .number_of_values(1),
56 56 )
57 57 .arg(
58 58 Arg::with_name("cwd")
59 59 .help("change working directory")
60 60 .long("--cwd")
61 61 .value_name("DIR")
62 62 .takes_value(true)
63 63 .global(true),
64 64 )
65 65 .version("0.0.1");
66 66 let app = add_subcommand_args(app);
67 67
68 68 let matches = app.clone().get_matches_safe()?;
69 69
70 70 let (subcommand_name, subcommand_matches) = matches.subcommand();
71 71 let run = subcommand_run_fn(subcommand_name)
72 72 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
73 73 let subcommand_args = subcommand_matches
74 74 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
75 75
76 76 let invocation = CliInvocation {
77 77 ui,
78 78 subcommand_args,
79 79 config,
80 80 repo,
81 81 };
82 82 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
83 83 blackbox.log_command_start();
84 84 let result = run(&invocation);
85 85 blackbox.log_command_end(exit_code(
86 86 &result,
87 87 // TODO: show a warning or combine with original error if `get_bool`
88 88 // returns an error
89 89 config
90 90 .get_bool(b"ui", b"detailed-exit-code")
91 91 .unwrap_or(false),
92 92 ));
93 93 result
94 94 }
95 95
96 96 fn main() {
97 97 // Run this first, before we find out if the blackbox extension is even
98 98 // enabled, in order to include everything in-between in the duration
99 99 // measurements. Reading config files can be slow if they’re on NFS.
100 100 let process_start_time = blackbox::ProcessStartTime::now();
101 101
102 102 env_logger::init();
103 103 let ui = ui::Ui::new();
104 104
105 105 let early_args = EarlyArgs::parse(std::env::args_os());
106 106
107 107 let initial_current_dir = early_args.cwd.map(|cwd| {
108 108 let cwd = get_path_from_bytes(&cwd);
109 109 std::env::current_dir()
110 110 .and_then(|initial| {
111 111 std::env::set_current_dir(cwd)?;
112 112 Ok(initial)
113 113 })
114 114 .unwrap_or_else(|error| {
115 115 exit(
116 116 &None,
117 117 &ui,
118 118 OnUnsupported::Abort,
119 119 Err(CommandError::abort(format!(
120 120 "abort: {}: '{}'",
121 121 error,
122 122 cwd.display()
123 123 ))),
124 124 false,
125 125 )
126 126 })
127 127 });
128 128
129 129 let non_repo_config =
130 130 Config::load(early_args.config).unwrap_or_else(|error| {
131 131 // Normally this is decided based on config, but we don’t have that
132 132 // available. As of this writing config loading never returns an
133 133 // "unsupported" error but that is not enforced by the type system.
134 134 let on_unsupported = OnUnsupported::Abort;
135 135
136 136 exit(
137 137 &initial_current_dir,
138 138 &ui,
139 139 on_unsupported,
140 140 Err(error.into()),
141 141 false,
142 142 )
143 143 });
144 144
145 145 if let Some(repo_path_bytes) = &early_args.repo {
146 146 lazy_static::lazy_static! {
147 147 static ref SCHEME_RE: regex::bytes::Regex =
148 148 // Same as `_matchscheme` in `mercurial/util.py`
149 149 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
150 150 }
151 151 if SCHEME_RE.is_match(&repo_path_bytes) {
152 152 exit(
153 153 &initial_current_dir,
154 154 &ui,
155 155 OnUnsupported::from_config(&ui, &non_repo_config),
156 156 Err(CommandError::UnsupportedFeature {
157 157 message: format_bytes!(
158 158 b"URL-like --repository {}",
159 159 repo_path_bytes
160 160 ),
161 161 }),
162 162 // TODO: show a warning or combine with original error if
163 163 // `get_bool` returns an error
164 164 non_repo_config
165 165 .get_bool(b"ui", b"detailed-exit-code")
166 166 .unwrap_or(false),
167 167 )
168 168 }
169 169 }
170 170 let repo_arg = early_args.repo.unwrap_or(Vec::new());
171 171 let repo_path: Option<PathBuf> = {
172 172 if repo_arg.is_empty() {
173 173 None
174 174 } else {
175 175 let local_config = {
176 176 if std::env::var_os("HGRCSKIPREPO").is_none() {
177 let current_dir = hg::utils::current_dir();
178 // TODO: handle errors from current_dir
179 if let Ok(current_dir_path) = current_dir {
177 // TODO: handle errors from find_repo_root
178 if let Ok(current_dir_path) = Repo::find_repo_root() {
180 179 let config_files = vec![
181 180 ConfigSource::AbsPath(
182 181 current_dir_path.join(".hg/hgrc"),
183 182 ),
184 183 ConfigSource::AbsPath(
185 184 current_dir_path.join(".hg/hgrc-not-shared"),
186 185 ),
187 186 ];
188 187 // TODO: handle errors from
189 188 // `load_from_explicit_sources`
190 189 Config::load_from_explicit_sources(config_files).ok()
191 190 } else {
192 191 None
193 192 }
194 193 } else {
195 194 None
196 195 }
197 196 };
198 197
199 198 let non_repo_config_val = {
200 199 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
201 200 match &non_repo_val {
202 201 Some(val) if val.len() > 0 => home::home_dir()
203 202 .unwrap_or_else(|| PathBuf::from("~"))
204 203 .join(get_path_from_bytes(val))
205 204 .canonicalize()
206 205 // TODO: handle error and make it similar to python
207 206 // implementation maybe?
208 207 .ok(),
209 208 _ => None,
210 209 }
211 210 };
212 211
213 212 let config_val = match &local_config {
214 213 None => non_repo_config_val,
215 214 Some(val) => {
216 215 let local_config_val = val.get(b"paths", &repo_arg);
217 216 match &local_config_val {
218 217 Some(val) if val.len() > 0 => {
219 218 // presence of a local_config assures that
220 219 // current_dir
221 220 // wont result in an Error
222 221 let canpath = hg::utils::current_dir()
223 222 .unwrap()
224 223 .join(get_path_from_bytes(val))
225 224 .canonicalize();
226 225 canpath.ok().or(non_repo_config_val)
227 226 }
228 227 _ => non_repo_config_val,
229 228 }
230 229 }
231 230 };
232 231 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
233 232 }
234 233 };
235 234
236 235 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
237 236 {
238 237 Ok(repo) => Ok(repo),
239 238 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
240 239 // Not finding a repo is not fatal yet, if `-R` was not given
241 240 Err(NoRepoInCwdError { cwd: at })
242 241 }
243 242 Err(error) => exit(
244 243 &initial_current_dir,
245 244 &ui,
246 245 OnUnsupported::from_config(&ui, &non_repo_config),
247 246 Err(error.into()),
248 247 // TODO: show a warning or combine with original error if
249 248 // `get_bool` returns an error
250 249 non_repo_config
251 250 .get_bool(b"ui", b"detailed-exit-code")
252 251 .unwrap_or(false),
253 252 ),
254 253 };
255 254
256 255 let config = if let Ok(repo) = &repo_result {
257 256 repo.config()
258 257 } else {
259 258 &non_repo_config
260 259 };
261 260 let on_unsupported = OnUnsupported::from_config(&ui, config);
262 261
263 262 let result = main_with_result(
264 263 &process_start_time,
265 264 &ui,
266 265 repo_result.as_ref(),
267 266 config,
268 267 );
269 268 exit(
270 269 &initial_current_dir,
271 270 &ui,
272 271 on_unsupported,
273 272 result,
274 273 // TODO: show a warning or combine with original error if `get_bool`
275 274 // returns an error
276 275 config
277 276 .get_bool(b"ui", b"detailed-exit-code")
278 277 .unwrap_or(false),
279 278 )
280 279 }
281 280
282 281 fn exit_code(
283 282 result: &Result<(), CommandError>,
284 283 use_detailed_exit_code: bool,
285 284 ) -> i32 {
286 285 match result {
287 286 Ok(()) => exitcode::OK,
288 287 Err(CommandError::Abort {
289 288 message: _,
290 289 detailed_exit_code,
291 290 }) => {
292 291 if use_detailed_exit_code {
293 292 *detailed_exit_code
294 293 } else {
295 294 exitcode::ABORT
296 295 }
297 296 }
298 297 Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL,
299 298
300 299 // Exit with a specific code and no error message to let a potential
301 300 // wrapper script fallback to Python-based Mercurial.
302 301 Err(CommandError::UnsupportedFeature { .. }) => {
303 302 exitcode::UNIMPLEMENTED
304 303 }
305 304 }
306 305 }
307 306
308 307 fn exit(
309 308 initial_current_dir: &Option<PathBuf>,
310 309 ui: &Ui,
311 310 mut on_unsupported: OnUnsupported,
312 311 result: Result<(), CommandError>,
313 312 use_detailed_exit_code: bool,
314 313 ) -> ! {
315 314 if let (
316 315 OnUnsupported::Fallback { executable },
317 316 Err(CommandError::UnsupportedFeature { .. }),
318 317 ) = (&on_unsupported, &result)
319 318 {
320 319 let mut args = std::env::args_os();
321 320 let executable_path = get_path_from_bytes(&executable);
322 321 let this_executable = args.next().expect("exepcted argv[0] to exist");
323 322 if executable_path == &PathBuf::from(this_executable) {
324 323 // Avoid spawning infinitely many processes until resource
325 324 // exhaustion.
326 325 let _ = ui.write_stderr(&format_bytes!(
327 326 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
328 327 points to `rhg` itself.\n",
329 328 executable
330 329 ));
331 330 on_unsupported = OnUnsupported::Abort
332 331 } else {
333 332 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
334 333 let mut command = Command::new(executable_path);
335 334 command.args(args);
336 335 if let Some(initial) = initial_current_dir {
337 336 command.current_dir(initial);
338 337 }
339 338 let result = command.status();
340 339 match result {
341 340 Ok(status) => std::process::exit(
342 341 status.code().unwrap_or(exitcode::ABORT),
343 342 ),
344 343 Err(error) => {
345 344 let _ = ui.write_stderr(&format_bytes!(
346 345 b"tried to fall back to a '{}' sub-process but got error {}\n",
347 346 executable, format_bytes::Utf8(error)
348 347 ));
349 348 on_unsupported = OnUnsupported::Abort
350 349 }
351 350 }
352 351 }
353 352 }
354 353 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
355 354 }
356 355
357 356 fn exit_no_fallback(
358 357 ui: &Ui,
359 358 on_unsupported: OnUnsupported,
360 359 result: Result<(), CommandError>,
361 360 use_detailed_exit_code: bool,
362 361 ) -> ! {
363 362 match &result {
364 363 Ok(_) => {}
365 364 Err(CommandError::Unsuccessful) => {}
366 365 Err(CommandError::Abort {
367 366 message,
368 367 detailed_exit_code: _,
369 368 }) => {
370 369 if !message.is_empty() {
371 370 // Ignore errors when writing to stderr, we’re already exiting
372 371 // with failure code so there’s not much more we can do.
373 372 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
374 373 }
375 374 }
376 375 Err(CommandError::UnsupportedFeature { message }) => {
377 376 match on_unsupported {
378 377 OnUnsupported::Abort => {
379 378 let _ = ui.write_stderr(&format_bytes!(
380 379 b"unsupported feature: {}\n",
381 380 message
382 381 ));
383 382 }
384 383 OnUnsupported::AbortSilent => {}
385 384 OnUnsupported::Fallback { .. } => unreachable!(),
386 385 }
387 386 }
388 387 }
389 388 std::process::exit(exit_code(&result, use_detailed_exit_code))
390 389 }
391 390
392 391 macro_rules! subcommands {
393 392 ($( $command: ident )+) => {
394 393 mod commands {
395 394 $(
396 395 pub mod $command;
397 396 )+
398 397 }
399 398
400 399 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
401 400 app
402 401 $(
403 402 .subcommand(commands::$command::args())
404 403 )+
405 404 }
406 405
407 406 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
408 407
409 408 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
410 409 match name {
411 410 $(
412 411 stringify!($command) => Some(commands::$command::run),
413 412 )+
414 413 _ => None,
415 414 }
416 415 }
417 416 };
418 417 }
419 418
420 419 subcommands! {
421 420 cat
422 421 debugdata
423 422 debugrequirements
424 423 files
425 424 root
426 425 config
427 426 status
428 427 }
429 428
430 429 pub struct CliInvocation<'a> {
431 430 ui: &'a Ui,
432 431 subcommand_args: &'a ArgMatches<'a>,
433 432 config: &'a Config,
434 433 /// References inside `Result` is a bit peculiar but allow
435 434 /// `invocation.repo?` to work out with `&CliInvocation` since this
436 435 /// `Result` type is `Copy`.
437 436 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
438 437 }
439 438
440 439 struct NoRepoInCwdError {
441 440 cwd: PathBuf,
442 441 }
443 442
444 443 /// CLI arguments to be parsed "early" in order to be able to read
445 444 /// configuration before using Clap. Ideally we would also use Clap for this,
446 445 /// see <https://github.com/clap-rs/clap/discussions/2366>.
447 446 ///
448 447 /// These arguments are still declared when we do use Clap later, so that Clap
449 448 /// does not return an error for their presence.
450 449 struct EarlyArgs {
451 450 /// Values of all `--config` arguments. (Possibly none)
452 451 config: Vec<Vec<u8>>,
453 452 /// Value of the `-R` or `--repository` argument, if any.
454 453 repo: Option<Vec<u8>>,
455 454 /// Value of the `--cwd` argument, if any.
456 455 cwd: Option<Vec<u8>>,
457 456 }
458 457
459 458 impl EarlyArgs {
460 459 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
461 460 let mut args = args.into_iter().map(get_bytes_from_os_str);
462 461 let mut config = Vec::new();
463 462 let mut repo = None;
464 463 let mut cwd = None;
465 464 // Use `while let` instead of `for` so that we can also call
466 465 // `args.next()` inside the loop.
467 466 while let Some(arg) = args.next() {
468 467 if arg == b"--config" {
469 468 if let Some(value) = args.next() {
470 469 config.push(value)
471 470 }
472 471 } else if let Some(value) = arg.drop_prefix(b"--config=") {
473 472 config.push(value.to_owned())
474 473 }
475 474
476 475 if arg == b"--cwd" {
477 476 if let Some(value) = args.next() {
478 477 cwd = Some(value)
479 478 }
480 479 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
481 480 cwd = Some(value.to_owned())
482 481 }
483 482
484 483 if arg == b"--repository" || arg == b"-R" {
485 484 if let Some(value) = args.next() {
486 485 repo = Some(value)
487 486 }
488 487 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
489 488 repo = Some(value.to_owned())
490 489 } else if let Some(value) = arg.drop_prefix(b"-R") {
491 490 repo = Some(value.to_owned())
492 491 }
493 492 }
494 493 Self { config, repo, cwd }
495 494 }
496 495 }
497 496
498 497 /// What to do when encountering some unsupported feature.
499 498 ///
500 499 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
501 500 enum OnUnsupported {
502 501 /// Print an error message describing what feature is not supported,
503 502 /// and exit with code 252.
504 503 Abort,
505 504 /// Silently exit with code 252.
506 505 AbortSilent,
507 506 /// Try running a Python implementation
508 507 Fallback { executable: Vec<u8> },
509 508 }
510 509
511 510 impl OnUnsupported {
512 511 const DEFAULT: Self = OnUnsupported::Abort;
513 512
514 513 fn from_config(ui: &Ui, config: &Config) -> Self {
515 514 match config
516 515 .get(b"rhg", b"on-unsupported")
517 516 .map(|value| value.to_ascii_lowercase())
518 517 .as_deref()
519 518 {
520 519 Some(b"abort") => OnUnsupported::Abort,
521 520 Some(b"abort-silent") => OnUnsupported::AbortSilent,
522 521 Some(b"fallback") => OnUnsupported::Fallback {
523 522 executable: config
524 523 .get(b"rhg", b"fallback-executable")
525 524 .unwrap_or_else(|| {
526 525 exit_no_fallback(
527 526 ui,
528 527 Self::Abort,
529 528 Err(CommandError::abort(
530 529 "abort: 'rhg.on-unsupported=fallback' without \
531 530 'rhg.fallback-executable' set."
532 531 )),
533 532 false,
534 533 )
535 534 })
536 535 .to_owned(),
537 536 },
538 537 None => Self::DEFAULT,
539 538 Some(_) => {
540 539 // TODO: warn about unknown config value
541 540 Self::DEFAULT
542 541 }
543 542 }
544 543 }
545 544 }
546 545
547 546 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"];
548 547
549 548 fn check_extensions(config: &Config) -> Result<(), CommandError> {
550 549 let enabled = config.get_section_keys(b"extensions");
551 550
552 551 let mut unsupported = enabled;
553 552 for supported in SUPPORTED_EXTENSIONS {
554 553 unsupported.remove(supported);
555 554 }
556 555
557 556 if let Some(ignored_list) =
558 557 config.get_simple_list(b"rhg", b"ignored-extensions")
559 558 {
560 559 for ignored in ignored_list {
561 560 unsupported.remove(ignored);
562 561 }
563 562 }
564 563
565 564 if unsupported.is_empty() {
566 565 Ok(())
567 566 } else {
568 567 Err(CommandError::UnsupportedFeature {
569 568 message: format_bytes!(
570 569 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
571 570 join(unsupported, b", ")
572 571 ),
573 572 })
574 573 }
575 574 }
General Comments 0
You need to be logged in to leave comments. Login now