##// END OF EJS Templates
rust: add support for hints in error messages...
Raphaël Gomès -
r50382:9f14126c default
parent child Browse files
Show More
@@ -1,343 +1,344 b''
1 1 // layer.rs
2 2 //
3 3 // Copyright 2020
4 4 // Valentin Gatien-Baron,
5 5 // Raphaël Gomès <rgomes@octobus.net>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 use crate::errors::HgError;
11 11 use crate::exit_codes::CONFIG_PARSE_ERROR_ABORT;
12 12 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
13 13 use format_bytes::{format_bytes, write_bytes, DisplayBytes};
14 14 use lazy_static::lazy_static;
15 15 use regex::bytes::Regex;
16 16 use std::collections::HashMap;
17 17 use std::path::{Path, PathBuf};
18 18
19 19 lazy_static! {
20 20 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
21 21 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
22 22 /// Continuation whitespace
23 23 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
24 24 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
25 25 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
26 26 /// A directive that allows for removing previous entries
27 27 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
28 28 /// A directive that allows for including other config files
29 29 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
30 30 }
31 31
32 32 /// All config values separated by layers of precedence.
33 33 /// Each config source may be split in multiple layers if `%include` directives
34 34 /// are used.
35 35 /// TODO detail the general precedence
36 36 #[derive(Clone)]
37 37 pub struct ConfigLayer {
38 38 /// Mapping of the sections to their items
39 39 sections: HashMap<Vec<u8>, ConfigItem>,
40 40 /// All sections (and their items/values) in a layer share the same origin
41 41 pub origin: ConfigOrigin,
42 42 /// Whether this layer comes from a trusted user or group
43 43 pub trusted: bool,
44 44 }
45 45
46 46 impl ConfigLayer {
47 47 pub fn new(origin: ConfigOrigin) -> Self {
48 48 ConfigLayer {
49 49 sections: HashMap::new(),
50 50 trusted: true, // TODO check
51 51 origin,
52 52 }
53 53 }
54 54
55 55 /// Parse `--config` CLI arguments and return a layer if there’s any
56 56 pub(crate) fn parse_cli_args(
57 57 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
58 58 ) -> Result<Option<Self>, ConfigError> {
59 59 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
60 60 use crate::utils::SliceExt;
61 61
62 62 let (section_and_item, value) = arg.split_2(b'=')?;
63 63 let (section, item) = section_and_item.trim().split_2(b'.')?;
64 64 Some((
65 65 section.to_owned(),
66 66 item.to_owned(),
67 67 value.trim().to_owned(),
68 68 ))
69 69 }
70 70
71 71 let mut layer = Self::new(ConfigOrigin::CommandLine);
72 72 for arg in cli_config_args {
73 73 let arg = arg.as_ref();
74 74 if let Some((section, item, value)) = parse_one(arg) {
75 75 layer.add(section, item, value, None);
76 76 } else {
77 77 Err(HgError::abort(
78 78 format!(
79 79 "abort: malformed --config option: '{}' \
80 80 (use --config section.name=value)",
81 81 String::from_utf8_lossy(arg),
82 82 ),
83 83 CONFIG_PARSE_ERROR_ABORT,
84 None,
84 85 ))?
85 86 }
86 87 }
87 88 if layer.sections.is_empty() {
88 89 Ok(None)
89 90 } else {
90 91 Ok(Some(layer))
91 92 }
92 93 }
93 94
94 95 /// Returns whether this layer comes from `--config` CLI arguments
95 96 pub(crate) fn is_from_command_line(&self) -> bool {
96 97 if let ConfigOrigin::CommandLine = self.origin {
97 98 true
98 99 } else {
99 100 false
100 101 }
101 102 }
102 103
103 104 /// Add an entry to the config, overwriting the old one if already present.
104 105 pub fn add(
105 106 &mut self,
106 107 section: Vec<u8>,
107 108 item: Vec<u8>,
108 109 value: Vec<u8>,
109 110 line: Option<usize>,
110 111 ) {
111 112 self.sections
112 113 .entry(section)
113 114 .or_insert_with(|| HashMap::new())
114 115 .insert(item, ConfigValue { bytes: value, line });
115 116 }
116 117
117 118 /// Returns the config value in `<section>.<item>` if it exists
118 119 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
119 120 Some(self.sections.get(section)?.get(item)?)
120 121 }
121 122
122 123 /// Returns the keys defined in the given section
123 124 pub fn iter_keys(&self, section: &[u8]) -> impl Iterator<Item = &[u8]> {
124 125 self.sections
125 126 .get(section)
126 127 .into_iter()
127 128 .flat_map(|section| section.keys().map(|vec| &**vec))
128 129 }
129 130
130 131 /// Returns the (key, value) pairs defined in the given section
131 132 pub fn iter_section<'layer>(
132 133 &'layer self,
133 134 section: &[u8],
134 135 ) -> impl Iterator<Item = (&'layer [u8], &'layer [u8])> {
135 136 self.sections
136 137 .get(section)
137 138 .into_iter()
138 139 .flat_map(|section| section.iter().map(|(k, v)| (&**k, &*v.bytes)))
139 140 }
140 141
141 142 /// Returns whether any key is defined in the given section
142 143 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
143 144 self.sections
144 145 .get(section)
145 146 .map_or(false, |section| !section.is_empty())
146 147 }
147 148
148 149 pub fn is_empty(&self) -> bool {
149 150 self.sections.is_empty()
150 151 }
151 152
152 153 /// Returns a `Vec` of layers in order of precedence (so, in read order),
153 154 /// recursively parsing the `%include` directives if any.
154 155 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
155 156 let mut layers = vec![];
156 157
157 158 // Discard byte order mark if any
158 159 let data = if data.starts_with(b"\xef\xbb\xbf") {
159 160 &data[3..]
160 161 } else {
161 162 data
162 163 };
163 164
164 165 // TODO check if it's trusted
165 166 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
166 167
167 168 let mut lines_iter =
168 169 data.split(|b| *b == b'\n').enumerate().peekable();
169 170 let mut section = b"".to_vec();
170 171
171 172 while let Some((index, bytes)) = lines_iter.next() {
172 173 let line = Some(index + 1);
173 174 if let Some(m) = INCLUDE_RE.captures(&bytes) {
174 175 let filename_bytes = &m[1];
175 176 let filename_bytes = crate::utils::expand_vars(filename_bytes);
176 177 // `Path::parent` only fails for the root directory,
177 178 // which `src` can’t be since we’ve managed to open it as a
178 179 // file.
179 180 let dir = src
180 181 .parent()
181 182 .expect("Path::parent fail on a file we’ve read");
182 183 // `Path::join` with an absolute argument correctly ignores the
183 184 // base path
184 185 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
185 186 match std::fs::read(&filename) {
186 187 Ok(data) => {
187 188 layers.push(current_layer);
188 189 layers.extend(Self::parse(&filename, &data)?);
189 190 current_layer =
190 191 Self::new(ConfigOrigin::File(src.to_owned()));
191 192 }
192 193 Err(error) => {
193 194 if error.kind() != std::io::ErrorKind::NotFound {
194 195 return Err(ConfigParseError {
195 196 origin: ConfigOrigin::File(src.to_owned()),
196 197 line,
197 198 message: format_bytes!(
198 199 b"cannot include {} ({})",
199 200 filename_bytes,
200 201 format_bytes::Utf8(error)
201 202 ),
202 203 }
203 204 .into());
204 205 }
205 206 }
206 207 }
207 208 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
208 209 } else if let Some(m) = SECTION_RE.captures(&bytes) {
209 210 section = m[1].to_vec();
210 211 } else if let Some(m) = ITEM_RE.captures(&bytes) {
211 212 let item = m[1].to_vec();
212 213 let mut value = m[2].to_vec();
213 214 loop {
214 215 match lines_iter.peek() {
215 216 None => break,
216 217 Some((_, v)) => {
217 218 if let Some(_) = COMMENT_RE.captures(&v) {
218 219 } else if let Some(_) = CONT_RE.captures(&v) {
219 220 value.extend(b"\n");
220 221 value.extend(&m[1]);
221 222 } else {
222 223 break;
223 224 }
224 225 }
225 226 };
226 227 lines_iter.next();
227 228 }
228 229 current_layer.add(section.clone(), item, value, line);
229 230 } else if let Some(m) = UNSET_RE.captures(&bytes) {
230 231 if let Some(map) = current_layer.sections.get_mut(&section) {
231 232 map.remove(&m[1]);
232 233 }
233 234 } else {
234 235 let message = if bytes.starts_with(b" ") {
235 236 format_bytes!(b"unexpected leading whitespace: {}", bytes)
236 237 } else {
237 238 bytes.to_owned()
238 239 };
239 240 return Err(ConfigParseError {
240 241 origin: ConfigOrigin::File(src.to_owned()),
241 242 line,
242 243 message,
243 244 }
244 245 .into());
245 246 }
246 247 }
247 248 if !current_layer.is_empty() {
248 249 layers.push(current_layer);
249 250 }
250 251 Ok(layers)
251 252 }
252 253 }
253 254
254 255 impl DisplayBytes for ConfigLayer {
255 256 fn display_bytes(
256 257 &self,
257 258 out: &mut dyn std::io::Write,
258 259 ) -> std::io::Result<()> {
259 260 let mut sections: Vec<_> = self.sections.iter().collect();
260 261 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
261 262
262 263 for (section, items) in sections.into_iter() {
263 264 let mut items: Vec<_> = items.into_iter().collect();
264 265 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
265 266
266 267 for (item, config_entry) in items {
267 268 write_bytes!(
268 269 out,
269 270 b"{}.{}={} # {}\n",
270 271 section,
271 272 item,
272 273 &config_entry.bytes,
273 274 &self.origin,
274 275 )?
275 276 }
276 277 }
277 278 Ok(())
278 279 }
279 280 }
280 281
281 282 /// Mapping of section item to value.
282 283 /// In the following:
283 284 /// ```text
284 285 /// [ui]
285 286 /// paginate=no
286 287 /// ```
287 288 /// "paginate" is the section item and "no" the value.
288 289 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
289 290
290 291 #[derive(Clone, Debug, PartialEq)]
291 292 pub struct ConfigValue {
292 293 /// The raw bytes of the value (be it from the CLI, env or from a file)
293 294 pub bytes: Vec<u8>,
294 295 /// Only present if the value comes from a file, 1-indexed.
295 296 pub line: Option<usize>,
296 297 }
297 298
298 299 #[derive(Clone, Debug, PartialEq, Eq)]
299 300 pub enum ConfigOrigin {
300 301 /// From a configuration file
301 302 File(PathBuf),
302 303 /// From a `--config` CLI argument
303 304 CommandLine,
304 305 /// From a `--color` CLI argument
305 306 CommandLineColor,
306 307 /// From environment variables like `$PAGER` or `$EDITOR`
307 308 Environment(Vec<u8>),
308 309 /* TODO defaults (configitems.py)
309 310 * TODO extensions
310 311 * TODO Python resources?
311 312 * Others? */
312 313 }
313 314
314 315 impl DisplayBytes for ConfigOrigin {
315 316 fn display_bytes(
316 317 &self,
317 318 out: &mut dyn std::io::Write,
318 319 ) -> std::io::Result<()> {
319 320 match self {
320 321 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
321 322 ConfigOrigin::CommandLine => out.write_all(b"--config"),
322 323 ConfigOrigin::CommandLineColor => out.write_all(b"--color"),
323 324 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
324 325 }
325 326 }
326 327 }
327 328
328 329 #[derive(Debug)]
329 330 pub struct ConfigParseError {
330 331 pub origin: ConfigOrigin,
331 332 pub line: Option<usize>,
332 333 pub message: Vec<u8>,
333 334 }
334 335
335 336 #[derive(Debug, derive_more::From)]
336 337 pub enum ConfigError {
337 338 Parse(ConfigParseError),
338 339 Other(HgError),
339 340 }
340 341
341 342 fn make_regex(pattern: &'static str) -> Regex {
342 343 Regex::new(pattern).expect("expected a valid regex")
343 344 }
@@ -1,211 +1,214 b''
1 1 use crate::config::ConfigValueParseError;
2 2 use crate::exit_codes;
3 3 use std::fmt;
4 4
5 5 /// Common error cases that can happen in many different APIs
6 6 #[derive(Debug, derive_more::From)]
7 7 pub enum HgError {
8 8 IoError {
9 9 error: std::io::Error,
10 10 context: IoErrorContext,
11 11 },
12 12
13 13 /// A file under `.hg/` normally only written by Mercurial is not in the
14 14 /// expected format. This indicates a bug in Mercurial, filesystem
15 15 /// corruption, or hardware failure.
16 16 ///
17 17 /// The given string is a short explanation for users, not intended to be
18 18 /// machine-readable.
19 19 CorruptedRepository(String),
20 20
21 21 /// The respository or requested operation involves a feature not
22 22 /// supported by the Rust implementation. Falling back to the Python
23 23 /// implementation may or may not work.
24 24 ///
25 25 /// The given string is a short explanation for users, not intended to be
26 26 /// machine-readable.
27 27 UnsupportedFeature(String),
28 28
29 29 /// Operation cannot proceed for some other reason.
30 30 ///
31 31 /// The message is a short explanation for users, not intended to be
32 32 /// machine-readable.
33 33 Abort {
34 34 message: String,
35 35 detailed_exit_code: exit_codes::ExitCode,
36 hint: Option<String>,
36 37 },
37 38
38 39 /// A configuration value is not in the expected syntax.
39 40 ///
40 41 /// These errors can happen in many places in the code because values are
41 42 /// parsed lazily as the file-level parser does not know the expected type
42 43 /// and syntax of each value.
43 44 #[from]
44 45 ConfigValueParseError(ConfigValueParseError),
45 46
46 47 /// Censored revision data.
47 48 CensoredNodeError,
48 49 }
49 50
50 51 /// Details about where an I/O error happened
51 52 #[derive(Debug)]
52 53 pub enum IoErrorContext {
53 54 /// `std::fs::metadata`
54 55 ReadingMetadata(std::path::PathBuf),
55 56 ReadingFile(std::path::PathBuf),
56 57 WritingFile(std::path::PathBuf),
57 58 RemovingFile(std::path::PathBuf),
58 59 RenamingFile {
59 60 from: std::path::PathBuf,
60 61 to: std::path::PathBuf,
61 62 },
62 63 /// `std::fs::canonicalize`
63 64 CanonicalizingPath(std::path::PathBuf),
64 65 /// `std::env::current_dir`
65 66 CurrentDir,
66 67 /// `std::env::current_exe`
67 68 CurrentExe,
68 69 }
69 70
70 71 impl HgError {
71 72 pub fn corrupted(explanation: impl Into<String>) -> Self {
72 73 // TODO: capture a backtrace here and keep it in the error value
73 74 // to aid debugging?
74 75 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
75 76 HgError::CorruptedRepository(explanation.into())
76 77 }
77 78
78 79 pub fn unsupported(explanation: impl Into<String>) -> Self {
79 80 HgError::UnsupportedFeature(explanation.into())
80 81 }
81 82
82 83 pub fn abort(
83 84 explanation: impl Into<String>,
84 85 exit_code: exit_codes::ExitCode,
86 hint: Option<String>,
85 87 ) -> Self {
86 88 HgError::Abort {
87 89 message: explanation.into(),
88 90 detailed_exit_code: exit_code,
91 hint,
89 92 }
90 93 }
91 94 }
92 95
93 96 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
94 97 impl fmt::Display for HgError {
95 98 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
96 99 match self {
97 100 HgError::Abort { message, .. } => write!(f, "{}", message),
98 101 HgError::IoError { error, context } => {
99 102 write!(f, "abort: {}: {}", context, error)
100 103 }
101 104 HgError::CorruptedRepository(explanation) => {
102 105 write!(f, "abort: {}", explanation)
103 106 }
104 107 HgError::UnsupportedFeature(explanation) => {
105 108 write!(f, "unsupported feature: {}", explanation)
106 109 }
107 110 HgError::CensoredNodeError => {
108 111 write!(f, "encountered a censored node")
109 112 }
110 113 HgError::ConfigValueParseError(error) => error.fmt(f),
111 114 }
112 115 }
113 116 }
114 117
115 118 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
116 119 impl fmt::Display for IoErrorContext {
117 120 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
118 121 match self {
119 122 IoErrorContext::ReadingMetadata(path) => {
120 123 write!(f, "when reading metadata of {}", path.display())
121 124 }
122 125 IoErrorContext::ReadingFile(path) => {
123 126 write!(f, "when reading {}", path.display())
124 127 }
125 128 IoErrorContext::WritingFile(path) => {
126 129 write!(f, "when writing {}", path.display())
127 130 }
128 131 IoErrorContext::RemovingFile(path) => {
129 132 write!(f, "when removing {}", path.display())
130 133 }
131 134 IoErrorContext::RenamingFile { from, to } => write!(
132 135 f,
133 136 "when renaming {} to {}",
134 137 from.display(),
135 138 to.display()
136 139 ),
137 140 IoErrorContext::CanonicalizingPath(path) => {
138 141 write!(f, "when canonicalizing {}", path.display())
139 142 }
140 143 IoErrorContext::CurrentDir => {
141 144 write!(f, "error getting current working directory")
142 145 }
143 146 IoErrorContext::CurrentExe => {
144 147 write!(f, "error getting current executable")
145 148 }
146 149 }
147 150 }
148 151 }
149 152
150 153 pub trait IoResultExt<T> {
151 154 /// Annotate a possible I/O error as related to a reading a file at the
152 155 /// given path.
153 156 ///
154 157 /// This allows printing something like “File not found when reading
155 158 /// example.txt” instead of just “File not found”.
156 159 ///
157 160 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
158 161 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError>;
159 162
160 163 fn when_writing_file(self, path: &std::path::Path) -> Result<T, HgError>;
161 164
162 165 fn with_context(
163 166 self,
164 167 context: impl FnOnce() -> IoErrorContext,
165 168 ) -> Result<T, HgError>;
166 169 }
167 170
168 171 impl<T> IoResultExt<T> for std::io::Result<T> {
169 172 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError> {
170 173 self.with_context(|| IoErrorContext::ReadingFile(path.to_owned()))
171 174 }
172 175
173 176 fn when_writing_file(self, path: &std::path::Path) -> Result<T, HgError> {
174 177 self.with_context(|| IoErrorContext::WritingFile(path.to_owned()))
175 178 }
176 179
177 180 fn with_context(
178 181 self,
179 182 context: impl FnOnce() -> IoErrorContext,
180 183 ) -> Result<T, HgError> {
181 184 self.map_err(|error| HgError::IoError {
182 185 error,
183 186 context: context(),
184 187 })
185 188 }
186 189 }
187 190
188 191 pub trait HgResultExt<T> {
189 192 /// Handle missing files separately from other I/O error cases.
190 193 ///
191 194 /// Wraps the `Ok` type in an `Option`:
192 195 ///
193 196 /// * `Ok(x)` becomes `Ok(Some(x))`
194 197 /// * An I/O "not found" error becomes `Ok(None)`
195 198 /// * Other errors are unchanged
196 199 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
197 200 }
198 201
199 202 impl<T> HgResultExt<T> for Result<T, HgError> {
200 203 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
201 204 match self {
202 205 Ok(x) => Ok(Some(x)),
203 206 Err(HgError::IoError { error, .. })
204 207 if error.kind() == std::io::ErrorKind::NotFound =>
205 208 {
206 209 Ok(None)
207 210 }
208 211 Err(other_error) => Err(other_error),
209 212 }
210 213 }
211 214 }
@@ -1,257 +1,277 b''
1 1 use crate::ui::utf8_to_local;
2 2 use crate::ui::UiError;
3 3 use crate::NoRepoInCwdError;
4 4 use format_bytes::format_bytes;
5 5 use hg::config::{ConfigError, ConfigParseError, ConfigValueParseError};
6 6 use hg::dirstate_tree::on_disk::DirstateV2ParseError;
7 7 use hg::errors::HgError;
8 8 use hg::exit_codes;
9 9 use hg::repo::RepoError;
10 10 use hg::revlog::revlog::RevlogError;
11 11 use hg::sparse::SparseConfigError;
12 12 use hg::utils::files::get_bytes_from_path;
13 13 use hg::{DirstateError, DirstateMapError, StatusError};
14 14 use std::convert::From;
15 15
16 16 /// The kind of command error
17 17 #[derive(Debug)]
18 18 pub enum CommandError {
19 19 /// Exit with an error message and "standard" failure exit code.
20 20 Abort {
21 21 message: Vec<u8>,
22 22 detailed_exit_code: exit_codes::ExitCode,
23 hint: Option<Vec<u8>>,
23 24 },
24 25
25 26 /// Exit with a failure exit code but no message.
26 27 Unsuccessful,
27 28
28 29 /// Encountered something (such as a CLI argument, repository layout, …)
29 30 /// not supported by this version of `rhg`. Depending on configuration
30 31 /// `rhg` may attempt to silently fall back to Python-based `hg`, which
31 32 /// may or may not support this feature.
32 33 UnsupportedFeature { message: Vec<u8> },
33 34 /// The fallback executable does not exist (or has some other problem if
34 35 /// we end up being more precise about broken fallbacks).
35 36 InvalidFallback { path: Vec<u8>, err: String },
36 37 }
37 38
38 39 impl CommandError {
39 40 pub fn abort(message: impl AsRef<str>) -> Self {
40 41 CommandError::abort_with_exit_code(message, exit_codes::ABORT)
41 42 }
42 43
43 44 pub fn abort_with_exit_code(
44 45 message: impl AsRef<str>,
45 46 detailed_exit_code: exit_codes::ExitCode,
46 47 ) -> Self {
47 48 CommandError::Abort {
48 49 // TODO: bytes-based (instead of Unicode-based) formatting
49 50 // of error messages to handle non-UTF-8 filenames etc:
50 51 // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output
51 52 message: utf8_to_local(message.as_ref()).into(),
52 53 detailed_exit_code: detailed_exit_code,
54 hint: None,
55 }
56 }
57
58 pub fn abort_with_exit_code_and_hint(
59 message: impl AsRef<str>,
60 detailed_exit_code: exit_codes::ExitCode,
61 hint: Option<impl AsRef<str>>,
62 ) -> Self {
63 CommandError::Abort {
64 message: utf8_to_local(message.as_ref()).into(),
65 detailed_exit_code,
66 hint: hint.map(|h| utf8_to_local(h.as_ref()).into()),
53 67 }
54 68 }
55 69
56 70 pub fn abort_with_exit_code_bytes(
57 71 message: impl AsRef<[u8]>,
58 72 detailed_exit_code: exit_codes::ExitCode,
59 73 ) -> Self {
60 74 // TODO: use this everywhere it makes sense instead of the string
61 75 // version.
62 76 CommandError::Abort {
63 77 message: message.as_ref().into(),
64 78 detailed_exit_code,
79 hint: None,
65 80 }
66 81 }
67 82
68 83 pub fn unsupported(message: impl AsRef<str>) -> Self {
69 84 CommandError::UnsupportedFeature {
70 85 message: utf8_to_local(message.as_ref()).into(),
71 86 }
72 87 }
73 88 }
74 89
75 90 /// For now we don’t differenciate between invalid CLI args and valid for `hg`
76 91 /// but not supported yet by `rhg`.
77 92 impl From<clap::Error> for CommandError {
78 93 fn from(error: clap::Error) -> Self {
79 94 CommandError::unsupported(error.to_string())
80 95 }
81 96 }
82 97
83 98 impl From<HgError> for CommandError {
84 99 fn from(error: HgError) -> Self {
85 100 match error {
86 101 HgError::UnsupportedFeature(message) => {
87 102 CommandError::unsupported(message)
88 103 }
89 104 HgError::CensoredNodeError => {
90 105 CommandError::unsupported("Encountered a censored node")
91 106 }
92 107 HgError::Abort {
93 108 message,
94 109 detailed_exit_code,
95 } => {
96 CommandError::abort_with_exit_code(message, detailed_exit_code)
97 }
110 hint,
111 } => CommandError::abort_with_exit_code_and_hint(
112 message,
113 detailed_exit_code,
114 hint,
115 ),
98 116 _ => CommandError::abort(error.to_string()),
99 117 }
100 118 }
101 119 }
102 120
103 121 impl From<ConfigValueParseError> for CommandError {
104 122 fn from(error: ConfigValueParseError) -> Self {
105 123 CommandError::abort_with_exit_code(
106 124 error.to_string(),
107 125 exit_codes::CONFIG_ERROR_ABORT,
108 126 )
109 127 }
110 128 }
111 129
112 130 impl From<UiError> for CommandError {
113 131 fn from(_error: UiError) -> Self {
114 132 // If we already failed writing to stdout or stderr,
115 133 // writing an error message to stderr about it would be likely to fail
116 134 // too.
117 135 CommandError::abort("")
118 136 }
119 137 }
120 138
121 139 impl From<RepoError> for CommandError {
122 140 fn from(error: RepoError) -> Self {
123 141 match error {
124 RepoError::NotFound { at } => CommandError::Abort {
125 message: format_bytes!(
126 b"abort: repository {} not found",
127 get_bytes_from_path(at)
128 ),
129 detailed_exit_code: exit_codes::ABORT,
130 },
142 RepoError::NotFound { at } => {
143 CommandError::abort_with_exit_code_bytes(
144 format_bytes!(
145 b"abort: repository {} not found",
146 get_bytes_from_path(at)
147 ),
148 exit_codes::ABORT,
149 )
150 }
131 151 RepoError::ConfigParseError(error) => error.into(),
132 152 RepoError::Other(error) => error.into(),
133 153 }
134 154 }
135 155 }
136 156
137 157 impl<'a> From<&'a NoRepoInCwdError> for CommandError {
138 158 fn from(error: &'a NoRepoInCwdError) -> Self {
139 159 let NoRepoInCwdError { cwd } = error;
140 CommandError::Abort {
141 message: format_bytes!(
160 CommandError::abort_with_exit_code_bytes(
161 format_bytes!(
142 162 b"abort: no repository found in '{}' (.hg not found)!",
143 163 get_bytes_from_path(cwd)
144 164 ),
145 detailed_exit_code: exit_codes::ABORT,
146 }
165 exit_codes::ABORT,
166 )
147 167 }
148 168 }
149 169
150 170 impl From<ConfigError> for CommandError {
151 171 fn from(error: ConfigError) -> Self {
152 172 match error {
153 173 ConfigError::Parse(error) => error.into(),
154 174 ConfigError::Other(error) => error.into(),
155 175 }
156 176 }
157 177 }
158 178
159 179 impl From<ConfigParseError> for CommandError {
160 180 fn from(error: ConfigParseError) -> Self {
161 181 let ConfigParseError {
162 182 origin,
163 183 line,
164 184 message,
165 185 } = error;
166 186 let line_message = if let Some(line_number) = line {
167 187 format_bytes!(b":{}", line_number.to_string().into_bytes())
168 188 } else {
169 189 Vec::new()
170 190 };
171 CommandError::Abort {
172 message: format_bytes!(
191 CommandError::abort_with_exit_code_bytes(
192 format_bytes!(
173 193 b"config error at {}{}: {}",
174 194 origin,
175 195 line_message,
176 196 message
177 197 ),
178 detailed_exit_code: exit_codes::CONFIG_ERROR_ABORT,
179 }
198 exit_codes::CONFIG_ERROR_ABORT,
199 )
180 200 }
181 201 }
182 202
183 203 impl From<(RevlogError, &str)> for CommandError {
184 204 fn from((err, rev): (RevlogError, &str)) -> CommandError {
185 205 match err {
186 206 RevlogError::WDirUnsupported => CommandError::abort(
187 207 "abort: working directory revision cannot be specified",
188 208 ),
189 209 RevlogError::InvalidRevision => CommandError::abort(format!(
190 210 "abort: invalid revision identifier: {}",
191 211 rev
192 212 )),
193 213 RevlogError::AmbiguousPrefix => CommandError::abort(format!(
194 214 "abort: ambiguous revision identifier: {}",
195 215 rev
196 216 )),
197 217 RevlogError::Other(error) => error.into(),
198 218 }
199 219 }
200 220 }
201 221
202 222 impl From<StatusError> for CommandError {
203 223 fn from(error: StatusError) -> Self {
204 224 CommandError::abort(format!("{}", error))
205 225 }
206 226 }
207 227
208 228 impl From<DirstateMapError> for CommandError {
209 229 fn from(error: DirstateMapError) -> Self {
210 230 CommandError::abort(format!("{}", error))
211 231 }
212 232 }
213 233
214 234 impl From<DirstateError> for CommandError {
215 235 fn from(error: DirstateError) -> Self {
216 236 match error {
217 237 DirstateError::Common(error) => error.into(),
218 238 DirstateError::Map(error) => error.into(),
219 239 }
220 240 }
221 241 }
222 242
223 243 impl From<DirstateV2ParseError> for CommandError {
224 244 fn from(error: DirstateV2ParseError) -> Self {
225 245 HgError::from(error).into()
226 246 }
227 247 }
228 248
229 249 impl From<SparseConfigError> for CommandError {
230 250 fn from(e: SparseConfigError) -> Self {
231 251 match e {
232 252 SparseConfigError::IncludesAfterExcludes { context } => {
233 253 Self::abort_with_exit_code_bytes(
234 254 format_bytes!(
235 255 b"{} config cannot have includes after excludes",
236 256 context
237 257 ),
238 258 exit_codes::CONFIG_PARSE_ERROR_ABORT,
239 259 )
240 260 }
241 261 SparseConfigError::EntryOutsideSection { context, line } => {
242 262 Self::abort_with_exit_code_bytes(
243 263 format_bytes!(
244 264 b"{} config entry outside of section: {}",
245 265 context,
246 266 &line,
247 267 ),
248 268 exit_codes::CONFIG_PARSE_ERROR_ABORT,
249 269 )
250 270 }
251 271 SparseConfigError::HgError(e) => Self::from(e),
252 272 SparseConfigError::PatternError(e) => {
253 273 Self::unsupported(format!("{}", e))
254 274 }
255 275 }
256 276 }
257 277 }
@@ -1,822 +1,821 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 message: _,
376 detailed_exit_code,
375 detailed_exit_code, ..
377 376 }) => {
378 377 if use_detailed_exit_code {
379 378 *detailed_exit_code
380 379 } else {
381 380 exit_codes::ABORT
382 381 }
383 382 }
384 383 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
385 384 // Exit with a specific code and no error message to let a potential
386 385 // wrapper script fallback to Python-based Mercurial.
387 386 Err(CommandError::UnsupportedFeature { .. }) => {
388 387 exit_codes::UNIMPLEMENTED
389 388 }
390 389 Err(CommandError::InvalidFallback { .. }) => {
391 390 exit_codes::INVALID_FALLBACK
392 391 }
393 392 }
394 393 }
395 394
396 395 fn exit<'a>(
397 396 original_args: &'a [OsString],
398 397 initial_current_dir: &Option<PathBuf>,
399 398 ui: &Ui,
400 399 mut on_unsupported: OnUnsupported,
401 400 result: Result<(), CommandError>,
402 401 use_detailed_exit_code: bool,
403 402 ) -> ! {
404 403 if let (
405 404 OnUnsupported::Fallback { executable },
406 405 Err(CommandError::UnsupportedFeature { message }),
407 406 ) = (&on_unsupported, &result)
408 407 {
409 408 let mut args = original_args.iter();
410 409 let executable = match executable {
411 410 None => {
412 411 exit_no_fallback(
413 412 ui,
414 413 OnUnsupported::Abort,
415 414 Err(CommandError::abort(
416 415 "abort: 'rhg.on-unsupported=fallback' without \
417 416 'rhg.fallback-executable' set.",
418 417 )),
419 418 false,
420 419 );
421 420 }
422 421 Some(executable) => executable,
423 422 };
424 423 let executable_path = get_path_from_bytes(&executable);
425 424 let this_executable = args.next().expect("exepcted argv[0] to exist");
426 425 if executable_path == &PathBuf::from(this_executable) {
427 426 // Avoid spawning infinitely many processes until resource
428 427 // exhaustion.
429 428 let _ = ui.write_stderr(&format_bytes!(
430 429 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
431 430 points to `rhg` itself.\n",
432 431 executable
433 432 ));
434 433 on_unsupported = OnUnsupported::Abort
435 434 } else {
436 435 log::debug!("falling back (see trace-level log)");
437 436 log::trace!("{}", local_to_utf8(message));
438 437 if let Err(err) = which::which(executable_path) {
439 438 exit_no_fallback(
440 439 ui,
441 440 OnUnsupported::Abort,
442 441 Err(CommandError::InvalidFallback {
443 442 path: executable.to_owned(),
444 443 err: err.to_string(),
445 444 }),
446 445 use_detailed_exit_code,
447 446 )
448 447 }
449 448 // `args` is now `argv[1..]` since we’ve already consumed
450 449 // `argv[0]`
451 450 let mut command = Command::new(executable_path);
452 451 command.args(args);
453 452 if let Some(initial) = initial_current_dir {
454 453 command.current_dir(initial);
455 454 }
456 455 // We don't use subprocess because proper signal handling is harder
457 456 // and we don't want to keep `rhg` around after a fallback anyway.
458 457 // For example, if `rhg` is run in the background and falls back to
459 458 // `hg` which, in turn, waits for a signal, we'll get stuck if
460 459 // we're doing plain subprocess.
461 460 //
462 461 // If `exec` returns, we can only assume our process is very broken
463 462 // (see its documentation), so only try to forward the error code
464 463 // when exiting.
465 464 let err = command.exec();
466 465 std::process::exit(
467 466 err.raw_os_error().unwrap_or(exit_codes::ABORT),
468 467 );
469 468 }
470 469 }
471 470 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
472 471 }
473 472
474 473 fn exit_no_fallback(
475 474 ui: &Ui,
476 475 on_unsupported: OnUnsupported,
477 476 result: Result<(), CommandError>,
478 477 use_detailed_exit_code: bool,
479 478 ) -> ! {
480 479 match &result {
481 480 Ok(_) => {}
482 481 Err(CommandError::Unsuccessful) => {}
483 Err(CommandError::Abort {
484 message,
485 detailed_exit_code: _,
486 }) => {
482 Err(CommandError::Abort { message, hint, .. }) => {
483 // Ignore errors when writing to stderr, we’re already exiting
484 // with failure code so there’s not much more we can do.
487 485 if !message.is_empty() {
488 // Ignore errors when writing to stderr, we’re already exiting
489 // with failure code so there’s not much more we can do.
490 486 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
491 487 }
488 if let Some(hint) = hint {
489 let _ = ui.write_stderr(&format_bytes!(b"({})\n", hint));
490 }
492 491 }
493 492 Err(CommandError::UnsupportedFeature { message }) => {
494 493 match on_unsupported {
495 494 OnUnsupported::Abort => {
496 495 let _ = ui.write_stderr(&format_bytes!(
497 496 b"unsupported feature: {}\n",
498 497 message
499 498 ));
500 499 }
501 500 OnUnsupported::AbortSilent => {}
502 501 OnUnsupported::Fallback { .. } => unreachable!(),
503 502 }
504 503 }
505 504 Err(CommandError::InvalidFallback { path, err }) => {
506 505 let _ = ui.write_stderr(&format_bytes!(
507 506 b"abort: invalid fallback '{}': {}\n",
508 507 path,
509 508 err.as_bytes(),
510 509 ));
511 510 }
512 511 }
513 512 std::process::exit(exit_code(&result, use_detailed_exit_code))
514 513 }
515 514
516 515 macro_rules! subcommands {
517 516 ($( $command: ident )+) => {
518 517 mod commands {
519 518 $(
520 519 pub mod $command;
521 520 )+
522 521 }
523 522
524 523 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
525 524 app
526 525 $(
527 526 .subcommand(commands::$command::args())
528 527 )+
529 528 }
530 529
531 530 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
532 531
533 532 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
534 533 match name {
535 534 $(
536 535 stringify!($command) => Some(commands::$command::run),
537 536 )+
538 537 _ => None,
539 538 }
540 539 }
541 540 };
542 541 }
543 542
544 543 subcommands! {
545 544 cat
546 545 debugdata
547 546 debugrequirements
548 547 debugignorerhg
549 548 debugrhgsparse
550 549 files
551 550 root
552 551 config
553 552 status
554 553 }
555 554
556 555 pub struct CliInvocation<'a> {
557 556 ui: &'a Ui,
558 557 subcommand_args: &'a ArgMatches<'a>,
559 558 config: &'a Config,
560 559 /// References inside `Result` is a bit peculiar but allow
561 560 /// `invocation.repo?` to work out with `&CliInvocation` since this
562 561 /// `Result` type is `Copy`.
563 562 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
564 563 }
565 564
566 565 struct NoRepoInCwdError {
567 566 cwd: PathBuf,
568 567 }
569 568
570 569 /// CLI arguments to be parsed "early" in order to be able to read
571 570 /// configuration before using Clap. Ideally we would also use Clap for this,
572 571 /// see <https://github.com/clap-rs/clap/discussions/2366>.
573 572 ///
574 573 /// These arguments are still declared when we do use Clap later, so that Clap
575 574 /// does not return an error for their presence.
576 575 struct EarlyArgs {
577 576 /// Values of all `--config` arguments. (Possibly none)
578 577 config: Vec<Vec<u8>>,
579 578 /// Value of all the `--color` argument, if any.
580 579 color: Option<Vec<u8>>,
581 580 /// Value of the `-R` or `--repository` argument, if any.
582 581 repo: Option<Vec<u8>>,
583 582 /// Value of the `--cwd` argument, if any.
584 583 cwd: Option<Vec<u8>>,
585 584 }
586 585
587 586 impl EarlyArgs {
588 587 fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
589 588 let mut args = args.into_iter().map(get_bytes_from_os_str);
590 589 let mut config = Vec::new();
591 590 let mut color = None;
592 591 let mut repo = None;
593 592 let mut cwd = None;
594 593 // Use `while let` instead of `for` so that we can also call
595 594 // `args.next()` inside the loop.
596 595 while let Some(arg) = args.next() {
597 596 if arg == b"--config" {
598 597 if let Some(value) = args.next() {
599 598 config.push(value)
600 599 }
601 600 } else if let Some(value) = arg.drop_prefix(b"--config=") {
602 601 config.push(value.to_owned())
603 602 }
604 603
605 604 if arg == b"--color" {
606 605 if let Some(value) = args.next() {
607 606 color = Some(value)
608 607 }
609 608 } else if let Some(value) = arg.drop_prefix(b"--color=") {
610 609 color = Some(value.to_owned())
611 610 }
612 611
613 612 if arg == b"--cwd" {
614 613 if let Some(value) = args.next() {
615 614 cwd = Some(value)
616 615 }
617 616 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
618 617 cwd = Some(value.to_owned())
619 618 }
620 619
621 620 if arg == b"--repository" || arg == b"-R" {
622 621 if let Some(value) = args.next() {
623 622 repo = Some(value)
624 623 }
625 624 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
626 625 repo = Some(value.to_owned())
627 626 } else if let Some(value) = arg.drop_prefix(b"-R") {
628 627 repo = Some(value.to_owned())
629 628 }
630 629 }
631 630 Self {
632 631 config,
633 632 color,
634 633 repo,
635 634 cwd,
636 635 }
637 636 }
638 637 }
639 638
640 639 /// What to do when encountering some unsupported feature.
641 640 ///
642 641 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
643 642 enum OnUnsupported {
644 643 /// Print an error message describing what feature is not supported,
645 644 /// and exit with code 252.
646 645 Abort,
647 646 /// Silently exit with code 252.
648 647 AbortSilent,
649 648 /// Try running a Python implementation
650 649 Fallback { executable: Option<Vec<u8>> },
651 650 }
652 651
653 652 impl OnUnsupported {
654 653 const DEFAULT: Self = OnUnsupported::Abort;
655 654
656 655 fn from_config(config: &Config) -> Self {
657 656 match config
658 657 .get(b"rhg", b"on-unsupported")
659 658 .map(|value| value.to_ascii_lowercase())
660 659 .as_deref()
661 660 {
662 661 Some(b"abort") => OnUnsupported::Abort,
663 662 Some(b"abort-silent") => OnUnsupported::AbortSilent,
664 663 Some(b"fallback") => OnUnsupported::Fallback {
665 664 executable: config
666 665 .get(b"rhg", b"fallback-executable")
667 666 .map(|x| x.to_owned()),
668 667 },
669 668 None => Self::DEFAULT,
670 669 Some(_) => {
671 670 // TODO: warn about unknown config value
672 671 Self::DEFAULT
673 672 }
674 673 }
675 674 }
676 675 }
677 676
678 677 /// The `*` extension is an edge-case for config sub-options that apply to all
679 678 /// extensions. For now, only `:required` exists, but that may change in the
680 679 /// future.
681 680 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[
682 681 b"blackbox",
683 682 b"share",
684 683 b"sparse",
685 684 b"narrow",
686 685 b"*",
687 686 b"strip",
688 687 b"rebase",
689 688 ];
690 689
691 690 fn check_extensions(config: &Config) -> Result<(), CommandError> {
692 691 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
693 692 // All extensions are to be ignored, nothing to do here
694 693 return Ok(());
695 694 }
696 695
697 696 let enabled: HashSet<&[u8]> = config
698 697 .iter_section(b"extensions")
699 698 .filter_map(|(extension, value)| {
700 699 if value == b"!" {
701 700 // Filter out disabled extensions
702 701 return None;
703 702 }
704 703 // Ignore extension suboptions. Only `required` exists for now.
705 704 // `rhg` either supports an extension or doesn't, so it doesn't
706 705 // make sense to consider the loading of an extension.
707 706 let actual_extension =
708 707 extension.split_2(b':').unwrap_or((extension, b"")).0;
709 708 Some(actual_extension)
710 709 })
711 710 .collect();
712 711
713 712 let mut unsupported = enabled;
714 713 for supported in SUPPORTED_EXTENSIONS {
715 714 unsupported.remove(supported);
716 715 }
717 716
718 717 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
719 718 {
720 719 for ignored in ignored_list {
721 720 unsupported.remove(ignored.as_slice());
722 721 }
723 722 }
724 723
725 724 if unsupported.is_empty() {
726 725 Ok(())
727 726 } else {
728 727 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
729 728 // Sort the extensions to get a stable output
730 729 unsupported.sort();
731 730 Err(CommandError::UnsupportedFeature {
732 731 message: format_bytes!(
733 732 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
734 733 join(unsupported, b", ")
735 734 ),
736 735 })
737 736 }
738 737 }
739 738
740 739 /// Array of tuples of (auto upgrade conf, feature conf, local requirement)
741 740 const AUTO_UPGRADES: &[((&str, &str), (&str, &str), &str)] = &[
742 741 (
743 742 ("format", "use-share-safe.automatic-upgrade-of-mismatching-repositories"),
744 743 ("format", "use-share-safe"),
745 744 requirements::SHARESAFE_REQUIREMENT,
746 745 ),
747 746 (
748 747 ("format", "use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories"),
749 748 ("format", "use-dirstate-tracked-hint"),
750 749 requirements::DIRSTATE_TRACKED_HINT_V1,
751 750 ),
752 751 (
753 752 ("use-dirstate-v2", "automatic-upgrade-of-mismatching-repositories"),
754 753 ("format", "use-dirstate-v2"),
755 754 requirements::DIRSTATE_V2_REQUIREMENT,
756 755 ),
757 756 ];
758 757
759 758 /// Mercurial allows users to automatically upgrade their repository.
760 759 /// `rhg` does not have the ability to upgrade yet, so fallback if an upgrade
761 760 /// is needed.
762 761 fn check_auto_upgrade(
763 762 config: &Config,
764 763 reqs: &HashSet<String>,
765 764 ) -> Result<(), CommandError> {
766 765 for (upgrade_conf, feature_conf, local_req) in AUTO_UPGRADES.iter() {
767 766 let auto_upgrade = config
768 767 .get_bool(upgrade_conf.0.as_bytes(), upgrade_conf.1.as_bytes())?;
769 768
770 769 if auto_upgrade {
771 770 let want_it = config.get_bool(
772 771 feature_conf.0.as_bytes(),
773 772 feature_conf.1.as_bytes(),
774 773 )?;
775 774 let have_it = reqs.contains(*local_req);
776 775
777 776 let action = match (want_it, have_it) {
778 777 (true, false) => Some("upgrade"),
779 778 (false, true) => Some("downgrade"),
780 779 _ => None,
781 780 };
782 781 if let Some(action) = action {
783 782 let message = format!(
784 783 "automatic {} {}.{}",
785 784 action, upgrade_conf.0, upgrade_conf.1
786 785 );
787 786 return Err(CommandError::unsupported(message));
788 787 }
789 788 }
790 789 }
791 790 Ok(())
792 791 }
793 792
794 793 fn check_unsupported(
795 794 config: &Config,
796 795 repo: Result<&Repo, &NoRepoInCwdError>,
797 796 ) -> Result<(), CommandError> {
798 797 check_extensions(config)?;
799 798
800 799 if std::env::var_os("HG_PENDING").is_some() {
801 800 // TODO: only if the value is `== repo.working_directory`?
802 801 // What about relative v.s. absolute paths?
803 802 Err(CommandError::unsupported("$HG_PENDING"))?
804 803 }
805 804
806 805 if let Ok(repo) = repo {
807 806 if repo.has_subrepos()? {
808 807 Err(CommandError::unsupported("sub-repositories"))?
809 808 }
810 809 check_auto_upgrade(config, repo.requirements())?;
811 810 }
812 811
813 812 if config.has_non_empty_section(b"encode") {
814 813 Err(CommandError::unsupported("[encode] config"))?
815 814 }
816 815
817 816 if config.has_non_empty_section(b"decode") {
818 817 Err(CommandError::unsupported("[decode] config"))?
819 818 }
820 819
821 820 Ok(())
822 821 }
General Comments 0
You need to be logged in to leave comments. Login now