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