##// END OF EJS Templates
rhg: Align config file parse error formatting with Python...
Simon Sapin -
r47465:3d692e72 default
parent child Browse files
Show More
@@ -1,292 +1,297 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, IoResultExt};
10 use crate::errors::{HgError, IoResultExt};
11 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
11 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
12 use format_bytes::{write_bytes, DisplayBytes};
12 use format_bytes::{format_bytes, write_bytes, DisplayBytes};
13 use lazy_static::lazy_static;
13 use lazy_static::lazy_static;
14 use regex::bytes::Regex;
14 use regex::bytes::Regex;
15 use std::collections::HashMap;
15 use std::collections::HashMap;
16 use std::path::{Path, PathBuf};
16 use std::path::{Path, PathBuf};
17
17
18 lazy_static! {
18 lazy_static! {
19 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
19 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
20 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
20 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
21 /// Continuation whitespace
21 /// Continuation whitespace
22 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
22 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
23 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
23 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
24 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
24 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
25 /// A directive that allows for removing previous entries
25 /// A directive that allows for removing previous entries
26 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
26 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
27 /// A directive that allows for including other config files
27 /// A directive that allows for including other config files
28 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
28 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
29 }
29 }
30
30
31 /// All config values separated by layers of precedence.
31 /// All config values separated by layers of precedence.
32 /// Each config source may be split in multiple layers if `%include` directives
32 /// Each config source may be split in multiple layers if `%include` directives
33 /// are used.
33 /// are used.
34 /// TODO detail the general precedence
34 /// TODO detail the general precedence
35 #[derive(Clone)]
35 #[derive(Clone)]
36 pub struct ConfigLayer {
36 pub struct ConfigLayer {
37 /// Mapping of the sections to their items
37 /// Mapping of the sections to their items
38 sections: HashMap<Vec<u8>, ConfigItem>,
38 sections: HashMap<Vec<u8>, ConfigItem>,
39 /// All sections (and their items/values) in a layer share the same origin
39 /// All sections (and their items/values) in a layer share the same origin
40 pub origin: ConfigOrigin,
40 pub origin: ConfigOrigin,
41 /// Whether this layer comes from a trusted user or group
41 /// Whether this layer comes from a trusted user or group
42 pub trusted: bool,
42 pub trusted: bool,
43 }
43 }
44
44
45 impl ConfigLayer {
45 impl ConfigLayer {
46 pub fn new(origin: ConfigOrigin) -> Self {
46 pub fn new(origin: ConfigOrigin) -> Self {
47 ConfigLayer {
47 ConfigLayer {
48 sections: HashMap::new(),
48 sections: HashMap::new(),
49 trusted: true, // TODO check
49 trusted: true, // TODO check
50 origin,
50 origin,
51 }
51 }
52 }
52 }
53
53
54 /// Parse `--config` CLI arguments and return a layer if there’s any
54 /// Parse `--config` CLI arguments and return a layer if there’s any
55 pub(crate) fn parse_cli_args(
55 pub(crate) fn parse_cli_args(
56 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
56 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
57 ) -> Result<Option<Self>, ConfigError> {
57 ) -> Result<Option<Self>, ConfigError> {
58 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
58 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
59 use crate::utils::SliceExt;
59 use crate::utils::SliceExt;
60
60
61 let (section_and_item, value) = arg.split_2(b'=')?;
61 let (section_and_item, value) = arg.split_2(b'=')?;
62 let (section, item) = section_and_item.trim().split_2(b'.')?;
62 let (section, item) = section_and_item.trim().split_2(b'.')?;
63 Some((
63 Some((
64 section.to_owned(),
64 section.to_owned(),
65 item.to_owned(),
65 item.to_owned(),
66 value.trim().to_owned(),
66 value.trim().to_owned(),
67 ))
67 ))
68 }
68 }
69
69
70 let mut layer = Self::new(ConfigOrigin::CommandLine);
70 let mut layer = Self::new(ConfigOrigin::CommandLine);
71 for arg in cli_config_args {
71 for arg in cli_config_args {
72 let arg = arg.as_ref();
72 let arg = arg.as_ref();
73 if let Some((section, item, value)) = parse_one(arg) {
73 if let Some((section, item, value)) = parse_one(arg) {
74 layer.add(section, item, value, None);
74 layer.add(section, item, value, None);
75 } else {
75 } else {
76 Err(HgError::abort(format!(
76 Err(HgError::abort(format!(
77 "malformed --config option: '{}' \
77 "malformed --config option: '{}' \
78 (use --config section.name=value)",
78 (use --config section.name=value)",
79 String::from_utf8_lossy(arg),
79 String::from_utf8_lossy(arg),
80 )))?
80 )))?
81 }
81 }
82 }
82 }
83 if layer.sections.is_empty() {
83 if layer.sections.is_empty() {
84 Ok(None)
84 Ok(None)
85 } else {
85 } else {
86 Ok(Some(layer))
86 Ok(Some(layer))
87 }
87 }
88 }
88 }
89
89
90 /// Returns whether this layer comes from `--config` CLI arguments
90 /// Returns whether this layer comes from `--config` CLI arguments
91 pub(crate) fn is_from_command_line(&self) -> bool {
91 pub(crate) fn is_from_command_line(&self) -> bool {
92 if let ConfigOrigin::CommandLine = self.origin {
92 if let ConfigOrigin::CommandLine = self.origin {
93 true
93 true
94 } else {
94 } else {
95 false
95 false
96 }
96 }
97 }
97 }
98
98
99 /// Add an entry to the config, overwriting the old one if already present.
99 /// Add an entry to the config, overwriting the old one if already present.
100 pub fn add(
100 pub fn add(
101 &mut self,
101 &mut self,
102 section: Vec<u8>,
102 section: Vec<u8>,
103 item: Vec<u8>,
103 item: Vec<u8>,
104 value: Vec<u8>,
104 value: Vec<u8>,
105 line: Option<usize>,
105 line: Option<usize>,
106 ) {
106 ) {
107 self.sections
107 self.sections
108 .entry(section)
108 .entry(section)
109 .or_insert_with(|| HashMap::new())
109 .or_insert_with(|| HashMap::new())
110 .insert(item, ConfigValue { bytes: value, line });
110 .insert(item, ConfigValue { bytes: value, line });
111 }
111 }
112
112
113 /// Returns the config value in `<section>.<item>` if it exists
113 /// Returns the config value in `<section>.<item>` if it exists
114 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
114 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
115 Some(self.sections.get(section)?.get(item)?)
115 Some(self.sections.get(section)?.get(item)?)
116 }
116 }
117
117
118 pub fn is_empty(&self) -> bool {
118 pub fn is_empty(&self) -> bool {
119 self.sections.is_empty()
119 self.sections.is_empty()
120 }
120 }
121
121
122 /// Returns a `Vec` of layers in order of precedence (so, in read order),
122 /// Returns a `Vec` of layers in order of precedence (so, in read order),
123 /// recursively parsing the `%include` directives if any.
123 /// recursively parsing the `%include` directives if any.
124 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
124 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
125 let mut layers = vec![];
125 let mut layers = vec![];
126
126
127 // Discard byte order mark if any
127 // Discard byte order mark if any
128 let data = if data.starts_with(b"\xef\xbb\xbf") {
128 let data = if data.starts_with(b"\xef\xbb\xbf") {
129 &data[3..]
129 &data[3..]
130 } else {
130 } else {
131 data
131 data
132 };
132 };
133
133
134 // TODO check if it's trusted
134 // TODO check if it's trusted
135 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
135 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
136
136
137 let mut lines_iter =
137 let mut lines_iter =
138 data.split(|b| *b == b'\n').enumerate().peekable();
138 data.split(|b| *b == b'\n').enumerate().peekable();
139 let mut section = b"".to_vec();
139 let mut section = b"".to_vec();
140
140
141 while let Some((index, bytes)) = lines_iter.next() {
141 while let Some((index, bytes)) = lines_iter.next() {
142 if let Some(m) = INCLUDE_RE.captures(&bytes) {
142 if let Some(m) = INCLUDE_RE.captures(&bytes) {
143 let filename_bytes = &m[1];
143 let filename_bytes = &m[1];
144 // `Path::parent` only fails for the root directory,
144 // `Path::parent` only fails for the root directory,
145 // which `src` can’t be since we’ve managed to open it as a
145 // which `src` can’t be since we’ve managed to open it as a
146 // file.
146 // file.
147 let dir = src
147 let dir = src
148 .parent()
148 .parent()
149 .expect("Path::parent fail on a file we’ve read");
149 .expect("Path::parent fail on a file we’ve read");
150 // `Path::join` with an absolute argument correctly ignores the
150 // `Path::join` with an absolute argument correctly ignores the
151 // base path
151 // base path
152 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
152 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
153 let data =
153 let data =
154 std::fs::read(&filename).when_reading_file(&filename)?;
154 std::fs::read(&filename).when_reading_file(&filename)?;
155 layers.push(current_layer);
155 layers.push(current_layer);
156 layers.extend(Self::parse(&filename, &data)?);
156 layers.extend(Self::parse(&filename, &data)?);
157 current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
157 current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
158 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
158 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
159 } else if let Some(m) = SECTION_RE.captures(&bytes) {
159 } else if let Some(m) = SECTION_RE.captures(&bytes) {
160 section = m[1].to_vec();
160 section = m[1].to_vec();
161 } else if let Some(m) = ITEM_RE.captures(&bytes) {
161 } else if let Some(m) = ITEM_RE.captures(&bytes) {
162 let item = m[1].to_vec();
162 let item = m[1].to_vec();
163 let mut value = m[2].to_vec();
163 let mut value = m[2].to_vec();
164 loop {
164 loop {
165 match lines_iter.peek() {
165 match lines_iter.peek() {
166 None => break,
166 None => break,
167 Some((_, v)) => {
167 Some((_, v)) => {
168 if let Some(_) = COMMENT_RE.captures(&v) {
168 if let Some(_) = COMMENT_RE.captures(&v) {
169 } else if let Some(_) = CONT_RE.captures(&v) {
169 } else if let Some(_) = CONT_RE.captures(&v) {
170 value.extend(b"\n");
170 value.extend(b"\n");
171 value.extend(&m[1]);
171 value.extend(&m[1]);
172 } else {
172 } else {
173 break;
173 break;
174 }
174 }
175 }
175 }
176 };
176 };
177 lines_iter.next();
177 lines_iter.next();
178 }
178 }
179 current_layer.add(
179 current_layer.add(
180 section.clone(),
180 section.clone(),
181 item,
181 item,
182 value,
182 value,
183 Some(index + 1),
183 Some(index + 1),
184 );
184 );
185 } else if let Some(m) = UNSET_RE.captures(&bytes) {
185 } else if let Some(m) = UNSET_RE.captures(&bytes) {
186 if let Some(map) = current_layer.sections.get_mut(&section) {
186 if let Some(map) = current_layer.sections.get_mut(&section) {
187 map.remove(&m[1]);
187 map.remove(&m[1]);
188 }
188 }
189 } else {
189 } else {
190 let message = if bytes.starts_with(b" ") {
191 format_bytes!(b"unexpected leading whitespace: {}", bytes)
192 } else {
193 bytes.to_owned()
194 };
190 return Err(ConfigParseError {
195 return Err(ConfigParseError {
191 origin: ConfigOrigin::File(src.to_owned()),
196 origin: ConfigOrigin::File(src.to_owned()),
192 line: Some(index + 1),
197 line: Some(index + 1),
193 bytes: bytes.to_owned(),
198 message,
194 }
199 }
195 .into());
200 .into());
196 }
201 }
197 }
202 }
198 if !current_layer.is_empty() {
203 if !current_layer.is_empty() {
199 layers.push(current_layer);
204 layers.push(current_layer);
200 }
205 }
201 Ok(layers)
206 Ok(layers)
202 }
207 }
203 }
208 }
204
209
205 impl DisplayBytes for ConfigLayer {
210 impl DisplayBytes for ConfigLayer {
206 fn display_bytes(
211 fn display_bytes(
207 &self,
212 &self,
208 out: &mut dyn std::io::Write,
213 out: &mut dyn std::io::Write,
209 ) -> std::io::Result<()> {
214 ) -> std::io::Result<()> {
210 let mut sections: Vec<_> = self.sections.iter().collect();
215 let mut sections: Vec<_> = self.sections.iter().collect();
211 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
216 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
212
217
213 for (section, items) in sections.into_iter() {
218 for (section, items) in sections.into_iter() {
214 let mut items: Vec<_> = items.into_iter().collect();
219 let mut items: Vec<_> = items.into_iter().collect();
215 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
220 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
216
221
217 for (item, config_entry) in items {
222 for (item, config_entry) in items {
218 write_bytes!(
223 write_bytes!(
219 out,
224 out,
220 b"{}.{}={} # {}\n",
225 b"{}.{}={} # {}\n",
221 section,
226 section,
222 item,
227 item,
223 &config_entry.bytes,
228 &config_entry.bytes,
224 &self.origin,
229 &self.origin,
225 )?
230 )?
226 }
231 }
227 }
232 }
228 Ok(())
233 Ok(())
229 }
234 }
230 }
235 }
231
236
232 /// Mapping of section item to value.
237 /// Mapping of section item to value.
233 /// In the following:
238 /// In the following:
234 /// ```text
239 /// ```text
235 /// [ui]
240 /// [ui]
236 /// paginate=no
241 /// paginate=no
237 /// ```
242 /// ```
238 /// "paginate" is the section item and "no" the value.
243 /// "paginate" is the section item and "no" the value.
239 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
244 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
240
245
241 #[derive(Clone, Debug, PartialEq)]
246 #[derive(Clone, Debug, PartialEq)]
242 pub struct ConfigValue {
247 pub struct ConfigValue {
243 /// The raw bytes of the value (be it from the CLI, env or from a file)
248 /// The raw bytes of the value (be it from the CLI, env or from a file)
244 pub bytes: Vec<u8>,
249 pub bytes: Vec<u8>,
245 /// Only present if the value comes from a file, 1-indexed.
250 /// Only present if the value comes from a file, 1-indexed.
246 pub line: Option<usize>,
251 pub line: Option<usize>,
247 }
252 }
248
253
249 #[derive(Clone, Debug)]
254 #[derive(Clone, Debug)]
250 pub enum ConfigOrigin {
255 pub enum ConfigOrigin {
251 /// From a configuration file
256 /// From a configuration file
252 File(PathBuf),
257 File(PathBuf),
253 /// From a `--config` CLI argument
258 /// From a `--config` CLI argument
254 CommandLine,
259 CommandLine,
255 /// From environment variables like `$PAGER` or `$EDITOR`
260 /// From environment variables like `$PAGER` or `$EDITOR`
256 Environment(Vec<u8>),
261 Environment(Vec<u8>),
257 /* TODO cli
262 /* TODO cli
258 * TODO defaults (configitems.py)
263 * TODO defaults (configitems.py)
259 * TODO extensions
264 * TODO extensions
260 * TODO Python resources?
265 * TODO Python resources?
261 * Others? */
266 * Others? */
262 }
267 }
263
268
264 impl DisplayBytes for ConfigOrigin {
269 impl DisplayBytes for ConfigOrigin {
265 fn display_bytes(
270 fn display_bytes(
266 &self,
271 &self,
267 out: &mut dyn std::io::Write,
272 out: &mut dyn std::io::Write,
268 ) -> std::io::Result<()> {
273 ) -> std::io::Result<()> {
269 match self {
274 match self {
270 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
275 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
271 ConfigOrigin::CommandLine => out.write_all(b"--config"),
276 ConfigOrigin::CommandLine => out.write_all(b"--config"),
272 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
277 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
273 }
278 }
274 }
279 }
275 }
280 }
276
281
277 #[derive(Debug)]
282 #[derive(Debug)]
278 pub struct ConfigParseError {
283 pub struct ConfigParseError {
279 pub origin: ConfigOrigin,
284 pub origin: ConfigOrigin,
280 pub line: Option<usize>,
285 pub line: Option<usize>,
281 pub bytes: Vec<u8>,
286 pub message: Vec<u8>,
282 }
287 }
283
288
284 #[derive(Debug, derive_more::From)]
289 #[derive(Debug, derive_more::From)]
285 pub enum ConfigError {
290 pub enum ConfigError {
286 Parse(ConfigParseError),
291 Parse(ConfigParseError),
287 Other(HgError),
292 Other(HgError),
288 }
293 }
289
294
290 fn make_regex(pattern: &'static str) -> Regex {
295 fn make_regex(pattern: &'static str) -> Regex {
291 Regex::new(pattern).expect("expected a valid regex")
296 Regex::new(pattern).expect("expected a valid regex")
292 }
297 }
@@ -1,192 +1,196 b''
1 use crate::config::ConfigValueParseError;
1 use crate::config::ConfigValueParseError;
2 use std::fmt;
2 use std::fmt;
3
3
4 /// Common error cases that can happen in many different APIs
4 /// Common error cases that can happen in many different APIs
5 #[derive(Debug, derive_more::From)]
5 #[derive(Debug, derive_more::From)]
6 pub enum HgError {
6 pub enum HgError {
7 IoError {
7 IoError {
8 error: std::io::Error,
8 error: std::io::Error,
9 context: IoErrorContext,
9 context: IoErrorContext,
10 },
10 },
11
11
12 /// A file under `.hg/` normally only written by Mercurial is not in the
12 /// A file under `.hg/` normally only written by Mercurial is not in the
13 /// expected format. This indicates a bug in Mercurial, filesystem
13 /// expected format. This indicates a bug in Mercurial, filesystem
14 /// corruption, or hardware failure.
14 /// corruption, or hardware failure.
15 ///
15 ///
16 /// The given string is a short explanation for users, not intended to be
16 /// The given string is a short explanation for users, not intended to be
17 /// machine-readable.
17 /// machine-readable.
18 CorruptedRepository(String),
18 CorruptedRepository(String),
19
19
20 /// The respository or requested operation involves a feature not
20 /// The respository or requested operation involves a feature not
21 /// supported by the Rust implementation. Falling back to the Python
21 /// supported by the Rust implementation. Falling back to the Python
22 /// implementation may or may not work.
22 /// implementation may or may not work.
23 ///
23 ///
24 /// The given string is a short explanation for users, not intended to be
24 /// The given string is a short explanation for users, not intended to be
25 /// machine-readable.
25 /// machine-readable.
26 UnsupportedFeature(String),
26 UnsupportedFeature(String),
27
27
28 /// Operation cannot proceed for some other reason.
28 /// Operation cannot proceed for some other reason.
29 ///
29 ///
30 /// The given string is a short explanation for users, not intended to be
30 /// The given string is a short explanation for users, not intended to be
31 /// machine-readable.
31 /// machine-readable.
32 Abort(String),
32 Abort(String),
33
33
34 /// A configuration value is not in the expected syntax.
34 /// A configuration value is not in the expected syntax.
35 ///
35 ///
36 /// These errors can happen in many places in the code because values are
36 /// These errors can happen in many places in the code because values are
37 /// parsed lazily as the file-level parser does not know the expected type
37 /// parsed lazily as the file-level parser does not know the expected type
38 /// and syntax of each value.
38 /// and syntax of each value.
39 #[from]
39 #[from]
40 ConfigValueParseError(ConfigValueParseError),
40 ConfigValueParseError(ConfigValueParseError),
41 }
41 }
42
42
43 /// Details about where an I/O error happened
43 /// Details about where an I/O error happened
44 #[derive(Debug)]
44 #[derive(Debug)]
45 pub enum IoErrorContext {
45 pub enum IoErrorContext {
46 ReadingFile(std::path::PathBuf),
46 ReadingFile(std::path::PathBuf),
47 WritingFile(std::path::PathBuf),
47 WritingFile(std::path::PathBuf),
48 RemovingFile(std::path::PathBuf),
48 RemovingFile(std::path::PathBuf),
49 RenamingFile {
49 RenamingFile {
50 from: std::path::PathBuf,
50 from: std::path::PathBuf,
51 to: std::path::PathBuf,
51 to: std::path::PathBuf,
52 },
52 },
53 /// `std::env::current_dir`
53 /// `std::env::current_dir`
54 CurrentDir,
54 CurrentDir,
55 /// `std::env::current_exe`
55 /// `std::env::current_exe`
56 CurrentExe,
56 CurrentExe,
57 }
57 }
58
58
59 impl HgError {
59 impl HgError {
60 pub fn corrupted(explanation: impl Into<String>) -> Self {
60 pub fn corrupted(explanation: impl Into<String>) -> Self {
61 // TODO: capture a backtrace here and keep it in the error value
61 // TODO: capture a backtrace here and keep it in the error value
62 // to aid debugging?
62 // to aid debugging?
63 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
63 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
64 HgError::CorruptedRepository(explanation.into())
64 HgError::CorruptedRepository(explanation.into())
65 }
65 }
66
66
67 pub fn unsupported(explanation: impl Into<String>) -> Self {
67 pub fn unsupported(explanation: impl Into<String>) -> Self {
68 HgError::UnsupportedFeature(explanation.into())
68 HgError::UnsupportedFeature(explanation.into())
69 }
69 }
70 pub fn abort(explanation: impl Into<String>) -> Self {
70 pub fn abort(explanation: impl Into<String>) -> Self {
71 HgError::Abort(explanation.into())
71 HgError::Abort(explanation.into())
72 }
72 }
73 }
73 }
74
74
75 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
75 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
76 impl fmt::Display for HgError {
76 impl fmt::Display for HgError {
77 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
77 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78 match self {
78 match self {
79 HgError::Abort(explanation) => write!(f, "{}", explanation),
79 HgError::Abort(explanation) => write!(f, "{}", explanation),
80 HgError::IoError { error, context } => {
80 HgError::IoError { error, context } => {
81 write!(f, "{}: {}", error, context)
81 write!(f, "abort: {}: {}", context, error)
82 }
82 }
83 HgError::CorruptedRepository(explanation) => {
83 HgError::CorruptedRepository(explanation) => {
84 write!(f, "corrupted repository: {}", explanation)
84 write!(f, "abort: corrupted repository: {}", explanation)
85 }
85 }
86 HgError::UnsupportedFeature(explanation) => {
86 HgError::UnsupportedFeature(explanation) => {
87 write!(f, "unsupported feature: {}", explanation)
87 write!(f, "unsupported feature: {}", explanation)
88 }
88 }
89 HgError::ConfigValueParseError(ConfigValueParseError {
89 HgError::ConfigValueParseError(ConfigValueParseError {
90 origin: _,
90 origin: _,
91 line: _,
91 line: _,
92 section,
92 section,
93 item,
93 item,
94 value,
94 value,
95 expected_type,
95 expected_type,
96 }) => {
96 }) => {
97 // TODO: add origin and line number information, here and in
97 // TODO: add origin and line number information, here and in
98 // corresponding python code
98 // corresponding python code
99 write!(
99 write!(
100 f,
100 f,
101 "config error: {}.{} is not a {} ('{}')",
101 "config error: {}.{} is not a {} ('{}')",
102 String::from_utf8_lossy(section),
102 String::from_utf8_lossy(section),
103 String::from_utf8_lossy(item),
103 String::from_utf8_lossy(item),
104 expected_type,
104 expected_type,
105 String::from_utf8_lossy(value)
105 String::from_utf8_lossy(value)
106 )
106 )
107 }
107 }
108 }
108 }
109 }
109 }
110 }
110 }
111
111
112 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
112 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
113 impl fmt::Display for IoErrorContext {
113 impl fmt::Display for IoErrorContext {
114 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
115 match self {
115 match self {
116 IoErrorContext::ReadingFile(path) => {
116 IoErrorContext::ReadingFile(path) => {
117 write!(f, "when reading {}", path.display())
117 write!(f, "when reading {}", path.display())
118 }
118 }
119 IoErrorContext::WritingFile(path) => {
119 IoErrorContext::WritingFile(path) => {
120 write!(f, "when writing {}", path.display())
120 write!(f, "when writing {}", path.display())
121 }
121 }
122 IoErrorContext::RemovingFile(path) => {
122 IoErrorContext::RemovingFile(path) => {
123 write!(f, "when removing {}", path.display())
123 write!(f, "when removing {}", path.display())
124 }
124 }
125 IoErrorContext::RenamingFile { from, to } => write!(
125 IoErrorContext::RenamingFile { from, to } => write!(
126 f,
126 f,
127 "when renaming {} to {}",
127 "when renaming {} to {}",
128 from.display(),
128 from.display(),
129 to.display()
129 to.display()
130 ),
130 ),
131 IoErrorContext::CurrentDir => write!(f, "current directory"),
131 IoErrorContext::CurrentDir => {
132 IoErrorContext::CurrentExe => write!(f, "current executable"),
132 write!(f, "error getting current working directory")
133 }
134 IoErrorContext::CurrentExe => {
135 write!(f, "error getting current executable")
136 }
133 }
137 }
134 }
138 }
135 }
139 }
136
140
137 pub trait IoResultExt<T> {
141 pub trait IoResultExt<T> {
138 /// Annotate a possible I/O error as related to a reading a file at the
142 /// Annotate a possible I/O error as related to a reading a file at the
139 /// given path.
143 /// given path.
140 ///
144 ///
141 /// This allows printing something like β€œFile not found when reading
145 /// This allows printing something like β€œFile not found when reading
142 /// example.txt” instead of just β€œFile not found”.
146 /// example.txt” instead of just β€œFile not found”.
143 ///
147 ///
144 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
148 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
145 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError>;
149 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError>;
146
150
147 fn with_context(
151 fn with_context(
148 self,
152 self,
149 context: impl FnOnce() -> IoErrorContext,
153 context: impl FnOnce() -> IoErrorContext,
150 ) -> Result<T, HgError>;
154 ) -> Result<T, HgError>;
151 }
155 }
152
156
153 impl<T> IoResultExt<T> for std::io::Result<T> {
157 impl<T> IoResultExt<T> for std::io::Result<T> {
154 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError> {
158 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError> {
155 self.with_context(|| IoErrorContext::ReadingFile(path.to_owned()))
159 self.with_context(|| IoErrorContext::ReadingFile(path.to_owned()))
156 }
160 }
157
161
158 fn with_context(
162 fn with_context(
159 self,
163 self,
160 context: impl FnOnce() -> IoErrorContext,
164 context: impl FnOnce() -> IoErrorContext,
161 ) -> Result<T, HgError> {
165 ) -> Result<T, HgError> {
162 self.map_err(|error| HgError::IoError {
166 self.map_err(|error| HgError::IoError {
163 error,
167 error,
164 context: context(),
168 context: context(),
165 })
169 })
166 }
170 }
167 }
171 }
168
172
169 pub trait HgResultExt<T> {
173 pub trait HgResultExt<T> {
170 /// Handle missing files separately from other I/O error cases.
174 /// Handle missing files separately from other I/O error cases.
171 ///
175 ///
172 /// Wraps the `Ok` type in an `Option`:
176 /// Wraps the `Ok` type in an `Option`:
173 ///
177 ///
174 /// * `Ok(x)` becomes `Ok(Some(x))`
178 /// * `Ok(x)` becomes `Ok(Some(x))`
175 /// * An I/O "not found" error becomes `Ok(None)`
179 /// * An I/O "not found" error becomes `Ok(None)`
176 /// * Other errors are unchanged
180 /// * Other errors are unchanged
177 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
181 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
178 }
182 }
179
183
180 impl<T> HgResultExt<T> for Result<T, HgError> {
184 impl<T> HgResultExt<T> for Result<T, HgError> {
181 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
185 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
182 match self {
186 match self {
183 Ok(x) => Ok(Some(x)),
187 Ok(x) => Ok(Some(x)),
184 Err(HgError::IoError { error, .. })
188 Err(HgError::IoError { error, .. })
185 if error.kind() == std::io::ErrorKind::NotFound =>
189 if error.kind() == std::io::ErrorKind::NotFound =>
186 {
190 {
187 Ok(None)
191 Ok(None)
188 }
192 }
189 Err(other_error) => Err(other_error),
193 Err(other_error) => Err(other_error),
190 }
194 }
191 }
195 }
192 }
196 }
@@ -1,143 +1,143 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};
5 use hg::config::{ConfigError, ConfigParseError};
6 use hg::errors::HgError;
6 use hg::errors::HgError;
7 use hg::repo::RepoError;
7 use hg::repo::RepoError;
8 use hg::revlog::revlog::RevlogError;
8 use hg::revlog::revlog::RevlogError;
9 use hg::utils::files::get_bytes_from_path;
9 use hg::utils::files::get_bytes_from_path;
10 use std::convert::From;
10 use std::convert::From;
11
11
12 /// The kind of command error
12 /// The kind of command error
13 #[derive(Debug)]
13 #[derive(Debug)]
14 pub enum CommandError {
14 pub enum CommandError {
15 /// Exit with an error message and "standard" failure exit code.
15 /// Exit with an error message and "standard" failure exit code.
16 Abort { message: Vec<u8> },
16 Abort { message: Vec<u8> },
17
17
18 /// Encountered something (such as a CLI argument, repository layout, …)
18 /// Encountered something (such as a CLI argument, repository layout, …)
19 /// not supported by this version of `rhg`. Depending on configuration
19 /// not supported by this version of `rhg`. Depending on configuration
20 /// `rhg` may attempt to silently fall back to Python-based `hg`, which
20 /// `rhg` may attempt to silently fall back to Python-based `hg`, which
21 /// may or may not support this feature.
21 /// may or may not support this feature.
22 UnsupportedFeature { message: Vec<u8> },
22 UnsupportedFeature { message: Vec<u8> },
23 }
23 }
24
24
25 impl CommandError {
25 impl CommandError {
26 pub fn abort(message: impl AsRef<str>) -> Self {
26 pub fn abort(message: impl AsRef<str>) -> Self {
27 CommandError::Abort {
27 CommandError::Abort {
28 // TODO: bytes-based (instead of Unicode-based) formatting
28 // TODO: bytes-based (instead of Unicode-based) formatting
29 // of error messages to handle non-UTF-8 filenames etc:
29 // of error messages to handle non-UTF-8 filenames etc:
30 // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output
30 // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output
31 message: utf8_to_local(message.as_ref()).into(),
31 message: utf8_to_local(message.as_ref()).into(),
32 }
32 }
33 }
33 }
34
34
35 pub fn unsupported(message: impl AsRef<str>) -> Self {
35 pub fn unsupported(message: impl AsRef<str>) -> Self {
36 CommandError::UnsupportedFeature {
36 CommandError::UnsupportedFeature {
37 message: utf8_to_local(message.as_ref()).into(),
37 message: utf8_to_local(message.as_ref()).into(),
38 }
38 }
39 }
39 }
40 }
40 }
41
41
42 /// For now we don’t differenciate between invalid CLI args and valid for `hg`
42 /// For now we don’t differenciate between invalid CLI args and valid for `hg`
43 /// but not supported yet by `rhg`.
43 /// but not supported yet by `rhg`.
44 impl From<clap::Error> for CommandError {
44 impl From<clap::Error> for CommandError {
45 fn from(error: clap::Error) -> Self {
45 fn from(error: clap::Error) -> Self {
46 CommandError::unsupported(error.to_string())
46 CommandError::unsupported(error.to_string())
47 }
47 }
48 }
48 }
49
49
50 impl From<HgError> for CommandError {
50 impl From<HgError> for CommandError {
51 fn from(error: HgError) -> Self {
51 fn from(error: HgError) -> Self {
52 match error {
52 match error {
53 HgError::UnsupportedFeature(message) => {
53 HgError::UnsupportedFeature(message) => {
54 CommandError::unsupported(message)
54 CommandError::unsupported(message)
55 }
55 }
56 _ => CommandError::abort(error.to_string()),
56 _ => CommandError::abort(error.to_string()),
57 }
57 }
58 }
58 }
59 }
59 }
60
60
61 impl From<UiError> for CommandError {
61 impl From<UiError> for CommandError {
62 fn from(_error: UiError) -> Self {
62 fn from(_error: UiError) -> Self {
63 // If we already failed writing to stdout or stderr,
63 // If we already failed writing to stdout or stderr,
64 // writing an error message to stderr about it would be likely to fail
64 // writing an error message to stderr about it would be likely to fail
65 // too.
65 // too.
66 CommandError::abort("")
66 CommandError::abort("")
67 }
67 }
68 }
68 }
69
69
70 impl From<RepoError> for CommandError {
70 impl From<RepoError> for CommandError {
71 fn from(error: RepoError) -> Self {
71 fn from(error: RepoError) -> Self {
72 match error {
72 match error {
73 RepoError::NotFound { at } => CommandError::Abort {
73 RepoError::NotFound { at } => CommandError::Abort {
74 message: format_bytes!(
74 message: format_bytes!(
75 b"repository {} not found",
75 b"repository {} not found",
76 get_bytes_from_path(at)
76 get_bytes_from_path(at)
77 ),
77 ),
78 },
78 },
79 RepoError::ConfigParseError(error) => error.into(),
79 RepoError::ConfigParseError(error) => error.into(),
80 RepoError::Other(error) => error.into(),
80 RepoError::Other(error) => error.into(),
81 }
81 }
82 }
82 }
83 }
83 }
84
84
85 impl<'a> From<&'a NoRepoInCwdError> for CommandError {
85 impl<'a> From<&'a NoRepoInCwdError> for CommandError {
86 fn from(error: &'a NoRepoInCwdError) -> Self {
86 fn from(error: &'a NoRepoInCwdError) -> Self {
87 let NoRepoInCwdError { cwd } = error;
87 let NoRepoInCwdError { cwd } = error;
88 CommandError::Abort {
88 CommandError::Abort {
89 message: format_bytes!(
89 message: format_bytes!(
90 b"no repository found in '{}' (.hg not found)!",
90 b"abort: no repository found in '{}' (.hg not found)!",
91 get_bytes_from_path(cwd)
91 get_bytes_from_path(cwd)
92 ),
92 ),
93 }
93 }
94 }
94 }
95 }
95 }
96
96
97 impl From<ConfigError> for CommandError {
97 impl From<ConfigError> for CommandError {
98 fn from(error: ConfigError) -> Self {
98 fn from(error: ConfigError) -> Self {
99 match error {
99 match error {
100 ConfigError::Parse(error) => error.into(),
100 ConfigError::Parse(error) => error.into(),
101 ConfigError::Other(error) => error.into(),
101 ConfigError::Other(error) => error.into(),
102 }
102 }
103 }
103 }
104 }
104 }
105
105
106 impl From<ConfigParseError> for CommandError {
106 impl From<ConfigParseError> for CommandError {
107 fn from(error: ConfigParseError) -> Self {
107 fn from(error: ConfigParseError) -> Self {
108 let ConfigParseError {
108 let ConfigParseError {
109 origin,
109 origin,
110 line,
110 line,
111 bytes,
111 message,
112 } = error;
112 } = error;
113 let line_message = if let Some(line_number) = line {
113 let line_message = if let Some(line_number) = line {
114 format_bytes!(b" at line {}", line_number.to_string().into_bytes())
114 format_bytes!(b":{}", line_number.to_string().into_bytes())
115 } else {
115 } else {
116 Vec::new()
116 Vec::new()
117 };
117 };
118 CommandError::Abort {
118 CommandError::Abort {
119 message: format_bytes!(
119 message: format_bytes!(
120 b"config parse error in {}{}: '{}'",
120 b"config error at {}{}: {}",
121 origin,
121 origin,
122 line_message,
122 line_message,
123 bytes
123 message
124 ),
124 ),
125 }
125 }
126 }
126 }
127 }
127 }
128
128
129 impl From<(RevlogError, &str)> for CommandError {
129 impl From<(RevlogError, &str)> for CommandError {
130 fn from((err, rev): (RevlogError, &str)) -> CommandError {
130 fn from((err, rev): (RevlogError, &str)) -> CommandError {
131 match err {
131 match err {
132 RevlogError::InvalidRevision => CommandError::abort(format!(
132 RevlogError::InvalidRevision => CommandError::abort(format!(
133 "invalid revision identifier {}",
133 "abort: invalid revision identifier: {}",
134 rev
134 rev
135 )),
135 )),
136 RevlogError::AmbiguousPrefix => CommandError::abort(format!(
136 RevlogError::AmbiguousPrefix => CommandError::abort(format!(
137 "ambiguous revision identifier {}",
137 "abort: ambiguous revision identifier: {}",
138 rev
138 rev
139 )),
139 )),
140 RevlogError::Other(error) => error.into(),
140 RevlogError::Other(error) => error.into(),
141 }
141 }
142 }
142 }
143 }
143 }
@@ -1,355 +1,354 b''
1 extern crate log;
1 extern crate log;
2 use crate::ui::Ui;
2 use crate::ui::Ui;
3 use clap::App;
3 use clap::App;
4 use clap::AppSettings;
4 use clap::AppSettings;
5 use clap::Arg;
5 use clap::Arg;
6 use clap::ArgMatches;
6 use clap::ArgMatches;
7 use format_bytes::format_bytes;
7 use format_bytes::format_bytes;
8 use hg::config::Config;
8 use hg::config::Config;
9 use hg::repo::{Repo, RepoError};
9 use hg::repo::{Repo, RepoError};
10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
11 use hg::utils::SliceExt;
11 use hg::utils::SliceExt;
12 use std::ffi::OsString;
12 use std::ffi::OsString;
13 use std::path::PathBuf;
13 use std::path::PathBuf;
14 use std::process::Command;
14 use std::process::Command;
15
15
16 mod blackbox;
16 mod blackbox;
17 mod error;
17 mod error;
18 mod exitcode;
18 mod exitcode;
19 mod ui;
19 mod ui;
20 use error::CommandError;
20 use error::CommandError;
21
21
22 fn main_with_result(
22 fn main_with_result(
23 process_start_time: &blackbox::ProcessStartTime,
23 process_start_time: &blackbox::ProcessStartTime,
24 ui: &ui::Ui,
24 ui: &ui::Ui,
25 repo: Result<&Repo, &NoRepoInCwdError>,
25 repo: Result<&Repo, &NoRepoInCwdError>,
26 config: &Config,
26 config: &Config,
27 ) -> Result<(), CommandError> {
27 ) -> Result<(), CommandError> {
28 let app = App::new("rhg")
28 let app = App::new("rhg")
29 .global_setting(AppSettings::AllowInvalidUtf8)
29 .global_setting(AppSettings::AllowInvalidUtf8)
30 .setting(AppSettings::SubcommandRequired)
30 .setting(AppSettings::SubcommandRequired)
31 .setting(AppSettings::VersionlessSubcommands)
31 .setting(AppSettings::VersionlessSubcommands)
32 .arg(
32 .arg(
33 Arg::with_name("repository")
33 Arg::with_name("repository")
34 .help("repository root directory")
34 .help("repository root directory")
35 .short("-R")
35 .short("-R")
36 .long("--repository")
36 .long("--repository")
37 .value_name("REPO")
37 .value_name("REPO")
38 .takes_value(true)
38 .takes_value(true)
39 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
39 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
40 .global(true),
40 .global(true),
41 )
41 )
42 .arg(
42 .arg(
43 Arg::with_name("config")
43 Arg::with_name("config")
44 .help("set/override config option (use 'section.name=value')")
44 .help("set/override config option (use 'section.name=value')")
45 .long("--config")
45 .long("--config")
46 .value_name("CONFIG")
46 .value_name("CONFIG")
47 .takes_value(true)
47 .takes_value(true)
48 .global(true)
48 .global(true)
49 // Ok: `--config section.key1=val --config section.key2=val2`
49 // Ok: `--config section.key1=val --config section.key2=val2`
50 .multiple(true)
50 .multiple(true)
51 // Not ok: `--config section.key1=val section.key2=val2`
51 // Not ok: `--config section.key1=val section.key2=val2`
52 .number_of_values(1),
52 .number_of_values(1),
53 )
53 )
54 .version("0.0.1");
54 .version("0.0.1");
55 let app = add_subcommand_args(app);
55 let app = add_subcommand_args(app);
56
56
57 let matches = app.clone().get_matches_safe()?;
57 let matches = app.clone().get_matches_safe()?;
58
58
59 let (subcommand_name, subcommand_matches) = matches.subcommand();
59 let (subcommand_name, subcommand_matches) = matches.subcommand();
60 let run = subcommand_run_fn(subcommand_name)
60 let run = subcommand_run_fn(subcommand_name)
61 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
61 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
62 let subcommand_args = subcommand_matches
62 let subcommand_args = subcommand_matches
63 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
63 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
64
64
65 let invocation = CliInvocation {
65 let invocation = CliInvocation {
66 ui,
66 ui,
67 subcommand_args,
67 subcommand_args,
68 config,
68 config,
69 repo,
69 repo,
70 };
70 };
71 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
71 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
72 blackbox.log_command_start();
72 blackbox.log_command_start();
73 let result = run(&invocation);
73 let result = run(&invocation);
74 blackbox.log_command_end(exit_code(&result));
74 blackbox.log_command_end(exit_code(&result));
75 result
75 result
76 }
76 }
77
77
78 fn main() {
78 fn main() {
79 // Run this first, before we find out if the blackbox extension is even
79 // Run this first, before we find out if the blackbox extension is even
80 // enabled, in order to include everything in-between in the duration
80 // enabled, in order to include everything in-between in the duration
81 // measurements. Reading config files can be slow if they’re on NFS.
81 // measurements. Reading config files can be slow if they’re on NFS.
82 let process_start_time = blackbox::ProcessStartTime::now();
82 let process_start_time = blackbox::ProcessStartTime::now();
83
83
84 env_logger::init();
84 env_logger::init();
85 let ui = ui::Ui::new();
85 let ui = ui::Ui::new();
86
86
87 let early_args = EarlyArgs::parse(std::env::args_os());
87 let early_args = EarlyArgs::parse(std::env::args_os());
88 let non_repo_config =
88 let non_repo_config =
89 Config::load(early_args.config).unwrap_or_else(|error| {
89 Config::load(early_args.config).unwrap_or_else(|error| {
90 // Normally this is decided based on config, but we don’t have that
90 // Normally this is decided based on config, but we don’t have that
91 // available. As of this writing config loading never returns an
91 // available. As of this writing config loading never returns an
92 // "unsupported" error but that is not enforced by the type system.
92 // "unsupported" error but that is not enforced by the type system.
93 let on_unsupported = OnUnsupported::Abort;
93 let on_unsupported = OnUnsupported::Abort;
94
94
95 exit(&ui, on_unsupported, Err(error.into()))
95 exit(&ui, on_unsupported, Err(error.into()))
96 });
96 });
97
97
98 if let Some(repo_path_bytes) = &early_args.repo {
98 if let Some(repo_path_bytes) = &early_args.repo {
99 lazy_static::lazy_static! {
99 lazy_static::lazy_static! {
100 static ref SCHEME_RE: regex::bytes::Regex =
100 static ref SCHEME_RE: regex::bytes::Regex =
101 // Same as `_matchscheme` in `mercurial/util.py`
101 // Same as `_matchscheme` in `mercurial/util.py`
102 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
102 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
103 }
103 }
104 if SCHEME_RE.is_match(&repo_path_bytes) {
104 if SCHEME_RE.is_match(&repo_path_bytes) {
105 exit(
105 exit(
106 &ui,
106 &ui,
107 OnUnsupported::from_config(&non_repo_config),
107 OnUnsupported::from_config(&non_repo_config),
108 Err(CommandError::UnsupportedFeature {
108 Err(CommandError::UnsupportedFeature {
109 message: format_bytes!(
109 message: format_bytes!(
110 b"URL-like --repository {}",
110 b"URL-like --repository {}",
111 repo_path_bytes
111 repo_path_bytes
112 ),
112 ),
113 }),
113 }),
114 )
114 )
115 }
115 }
116 }
116 }
117 let repo_path = early_args.repo.as_deref().map(get_path_from_bytes);
117 let repo_path = early_args.repo.as_deref().map(get_path_from_bytes);
118 let repo_result = match Repo::find(&non_repo_config, repo_path) {
118 let repo_result = match Repo::find(&non_repo_config, repo_path) {
119 Ok(repo) => Ok(repo),
119 Ok(repo) => Ok(repo),
120 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
120 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
121 // Not finding a repo is not fatal yet, if `-R` was not given
121 // Not finding a repo is not fatal yet, if `-R` was not given
122 Err(NoRepoInCwdError { cwd: at })
122 Err(NoRepoInCwdError { cwd: at })
123 }
123 }
124 Err(error) => exit(
124 Err(error) => exit(
125 &ui,
125 &ui,
126 OnUnsupported::from_config(&non_repo_config),
126 OnUnsupported::from_config(&non_repo_config),
127 Err(error.into()),
127 Err(error.into()),
128 ),
128 ),
129 };
129 };
130
130
131 let config = if let Ok(repo) = &repo_result {
131 let config = if let Ok(repo) = &repo_result {
132 repo.config()
132 repo.config()
133 } else {
133 } else {
134 &non_repo_config
134 &non_repo_config
135 };
135 };
136
136
137 let result = main_with_result(
137 let result = main_with_result(
138 &process_start_time,
138 &process_start_time,
139 &ui,
139 &ui,
140 repo_result.as_ref(),
140 repo_result.as_ref(),
141 config,
141 config,
142 );
142 );
143 exit(&ui, OnUnsupported::from_config(config), result)
143 exit(&ui, OnUnsupported::from_config(config), result)
144 }
144 }
145
145
146 fn exit_code(result: &Result<(), CommandError>) -> i32 {
146 fn exit_code(result: &Result<(), CommandError>) -> i32 {
147 match result {
147 match result {
148 Ok(()) => exitcode::OK,
148 Ok(()) => exitcode::OK,
149 Err(CommandError::Abort { .. }) => exitcode::ABORT,
149 Err(CommandError::Abort { .. }) => exitcode::ABORT,
150
150
151 // Exit with a specific code and no error message to let a potential
151 // Exit with a specific code and no error message to let a potential
152 // wrapper script fallback to Python-based Mercurial.
152 // wrapper script fallback to Python-based Mercurial.
153 Err(CommandError::UnsupportedFeature { .. }) => {
153 Err(CommandError::UnsupportedFeature { .. }) => {
154 exitcode::UNIMPLEMENTED
154 exitcode::UNIMPLEMENTED
155 }
155 }
156 }
156 }
157 }
157 }
158
158
159 fn exit(
159 fn exit(
160 ui: &Ui,
160 ui: &Ui,
161 mut on_unsupported: OnUnsupported,
161 mut on_unsupported: OnUnsupported,
162 result: Result<(), CommandError>,
162 result: Result<(), CommandError>,
163 ) -> ! {
163 ) -> ! {
164 if let (
164 if let (
165 OnUnsupported::Fallback { executable },
165 OnUnsupported::Fallback { executable },
166 Err(CommandError::UnsupportedFeature { .. }),
166 Err(CommandError::UnsupportedFeature { .. }),
167 ) = (&on_unsupported, &result)
167 ) = (&on_unsupported, &result)
168 {
168 {
169 let mut args = std::env::args_os();
169 let mut args = std::env::args_os();
170 let executable_path = get_path_from_bytes(&executable);
170 let executable_path = get_path_from_bytes(&executable);
171 let this_executable = args.next().expect("exepcted argv[0] to exist");
171 let this_executable = args.next().expect("exepcted argv[0] to exist");
172 if executable_path == &PathBuf::from(this_executable) {
172 if executable_path == &PathBuf::from(this_executable) {
173 // Avoid spawning infinitely many processes until resource
173 // Avoid spawning infinitely many processes until resource
174 // exhaustion.
174 // exhaustion.
175 let _ = ui.write_stderr(&format_bytes!(
175 let _ = ui.write_stderr(&format_bytes!(
176 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
176 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
177 points to `rhg` itself.\n",
177 points to `rhg` itself.\n",
178 executable
178 executable
179 ));
179 ));
180 on_unsupported = OnUnsupported::Abort
180 on_unsupported = OnUnsupported::Abort
181 } else {
181 } else {
182 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
182 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
183 let result = Command::new(executable_path).args(args).status();
183 let result = Command::new(executable_path).args(args).status();
184 match result {
184 match result {
185 Ok(status) => std::process::exit(
185 Ok(status) => std::process::exit(
186 status.code().unwrap_or(exitcode::ABORT),
186 status.code().unwrap_or(exitcode::ABORT),
187 ),
187 ),
188 Err(error) => {
188 Err(error) => {
189 let _ = ui.write_stderr(&format_bytes!(
189 let _ = ui.write_stderr(&format_bytes!(
190 b"tried to fall back to a '{}' sub-process but got error {}\n",
190 b"tried to fall back to a '{}' sub-process but got error {}\n",
191 executable, format_bytes::Utf8(error)
191 executable, format_bytes::Utf8(error)
192 ));
192 ));
193 on_unsupported = OnUnsupported::Abort
193 on_unsupported = OnUnsupported::Abort
194 }
194 }
195 }
195 }
196 }
196 }
197 }
197 }
198 match &result {
198 match &result {
199 Ok(_) => {}
199 Ok(_) => {}
200 Err(CommandError::Abort { message }) => {
200 Err(CommandError::Abort { message }) => {
201 if !message.is_empty() {
201 if !message.is_empty() {
202 // Ignore errors when writing to stderr, we’re already exiting
202 // Ignore errors when writing to stderr, we’re already exiting
203 // with failure code so there’s not much more we can do.
203 // with failure code so there’s not much more we can do.
204 let _ =
204 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
205 ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
206 }
205 }
207 }
206 }
208 Err(CommandError::UnsupportedFeature { message }) => {
207 Err(CommandError::UnsupportedFeature { message }) => {
209 match on_unsupported {
208 match on_unsupported {
210 OnUnsupported::Abort => {
209 OnUnsupported::Abort => {
211 let _ = ui.write_stderr(&format_bytes!(
210 let _ = ui.write_stderr(&format_bytes!(
212 b"unsupported feature: {}\n",
211 b"unsupported feature: {}\n",
213 message
212 message
214 ));
213 ));
215 }
214 }
216 OnUnsupported::AbortSilent => {}
215 OnUnsupported::AbortSilent => {}
217 OnUnsupported::Fallback { .. } => unreachable!(),
216 OnUnsupported::Fallback { .. } => unreachable!(),
218 }
217 }
219 }
218 }
220 }
219 }
221 std::process::exit(exit_code(&result))
220 std::process::exit(exit_code(&result))
222 }
221 }
223
222
224 macro_rules! subcommands {
223 macro_rules! subcommands {
225 ($( $command: ident )+) => {
224 ($( $command: ident )+) => {
226 mod commands {
225 mod commands {
227 $(
226 $(
228 pub mod $command;
227 pub mod $command;
229 )+
228 )+
230 }
229 }
231
230
232 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
231 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
233 app
232 app
234 $(
233 $(
235 .subcommand(commands::$command::args())
234 .subcommand(commands::$command::args())
236 )+
235 )+
237 }
236 }
238
237
239 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
238 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
240
239
241 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
240 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
242 match name {
241 match name {
243 $(
242 $(
244 stringify!($command) => Some(commands::$command::run),
243 stringify!($command) => Some(commands::$command::run),
245 )+
244 )+
246 _ => None,
245 _ => None,
247 }
246 }
248 }
247 }
249 };
248 };
250 }
249 }
251
250
252 subcommands! {
251 subcommands! {
253 cat
252 cat
254 debugdata
253 debugdata
255 debugrequirements
254 debugrequirements
256 files
255 files
257 root
256 root
258 config
257 config
259 }
258 }
260 pub struct CliInvocation<'a> {
259 pub struct CliInvocation<'a> {
261 ui: &'a Ui,
260 ui: &'a Ui,
262 subcommand_args: &'a ArgMatches<'a>,
261 subcommand_args: &'a ArgMatches<'a>,
263 config: &'a Config,
262 config: &'a Config,
264 /// References inside `Result` is a bit peculiar but allow
263 /// References inside `Result` is a bit peculiar but allow
265 /// `invocation.repo?` to work out with `&CliInvocation` since this
264 /// `invocation.repo?` to work out with `&CliInvocation` since this
266 /// `Result` type is `Copy`.
265 /// `Result` type is `Copy`.
267 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
266 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
268 }
267 }
269
268
270 struct NoRepoInCwdError {
269 struct NoRepoInCwdError {
271 cwd: PathBuf,
270 cwd: PathBuf,
272 }
271 }
273
272
274 /// CLI arguments to be parsed "early" in order to be able to read
273 /// CLI arguments to be parsed "early" in order to be able to read
275 /// configuration before using Clap. Ideally we would also use Clap for this,
274 /// configuration before using Clap. Ideally we would also use Clap for this,
276 /// see <https://github.com/clap-rs/clap/discussions/2366>.
275 /// see <https://github.com/clap-rs/clap/discussions/2366>.
277 ///
276 ///
278 /// These arguments are still declared when we do use Clap later, so that Clap
277 /// These arguments are still declared when we do use Clap later, so that Clap
279 /// does not return an error for their presence.
278 /// does not return an error for their presence.
280 struct EarlyArgs {
279 struct EarlyArgs {
281 /// Values of all `--config` arguments. (Possibly none)
280 /// Values of all `--config` arguments. (Possibly none)
282 config: Vec<Vec<u8>>,
281 config: Vec<Vec<u8>>,
283 /// Value of the `-R` or `--repository` argument, if any.
282 /// Value of the `-R` or `--repository` argument, if any.
284 repo: Option<Vec<u8>>,
283 repo: Option<Vec<u8>>,
285 }
284 }
286
285
287 impl EarlyArgs {
286 impl EarlyArgs {
288 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
287 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
289 let mut args = args.into_iter().map(get_bytes_from_os_str);
288 let mut args = args.into_iter().map(get_bytes_from_os_str);
290 let mut config = Vec::new();
289 let mut config = Vec::new();
291 let mut repo = None;
290 let mut repo = None;
292 // Use `while let` instead of `for` so that we can also call
291 // Use `while let` instead of `for` so that we can also call
293 // `args.next()` inside the loop.
292 // `args.next()` inside the loop.
294 while let Some(arg) = args.next() {
293 while let Some(arg) = args.next() {
295 if arg == b"--config" {
294 if arg == b"--config" {
296 if let Some(value) = args.next() {
295 if let Some(value) = args.next() {
297 config.push(value)
296 config.push(value)
298 }
297 }
299 } else if let Some(value) = arg.drop_prefix(b"--config=") {
298 } else if let Some(value) = arg.drop_prefix(b"--config=") {
300 config.push(value.to_owned())
299 config.push(value.to_owned())
301 }
300 }
302
301
303 if arg == b"--repository" || arg == b"-R" {
302 if arg == b"--repository" || arg == b"-R" {
304 if let Some(value) = args.next() {
303 if let Some(value) = args.next() {
305 repo = Some(value)
304 repo = Some(value)
306 }
305 }
307 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
306 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
308 repo = Some(value.to_owned())
307 repo = Some(value.to_owned())
309 } else if let Some(value) = arg.drop_prefix(b"-R") {
308 } else if let Some(value) = arg.drop_prefix(b"-R") {
310 repo = Some(value.to_owned())
309 repo = Some(value.to_owned())
311 }
310 }
312 }
311 }
313 Self { config, repo }
312 Self { config, repo }
314 }
313 }
315 }
314 }
316
315
317 /// What to do when encountering some unsupported feature.
316 /// What to do when encountering some unsupported feature.
318 ///
317 ///
319 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
318 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
320 enum OnUnsupported {
319 enum OnUnsupported {
321 /// Print an error message describing what feature is not supported,
320 /// Print an error message describing what feature is not supported,
322 /// and exit with code 252.
321 /// and exit with code 252.
323 Abort,
322 Abort,
324 /// Silently exit with code 252.
323 /// Silently exit with code 252.
325 AbortSilent,
324 AbortSilent,
326 /// Try running a Python implementation
325 /// Try running a Python implementation
327 Fallback { executable: Vec<u8> },
326 Fallback { executable: Vec<u8> },
328 }
327 }
329
328
330 impl OnUnsupported {
329 impl OnUnsupported {
331 const DEFAULT: Self = OnUnsupported::Abort;
330 const DEFAULT: Self = OnUnsupported::Abort;
332 const DEFAULT_FALLBACK_EXECUTABLE: &'static [u8] = b"hg";
331 const DEFAULT_FALLBACK_EXECUTABLE: &'static [u8] = b"hg";
333
332
334 fn from_config(config: &Config) -> Self {
333 fn from_config(config: &Config) -> Self {
335 match config
334 match config
336 .get(b"rhg", b"on-unsupported")
335 .get(b"rhg", b"on-unsupported")
337 .map(|value| value.to_ascii_lowercase())
336 .map(|value| value.to_ascii_lowercase())
338 .as_deref()
337 .as_deref()
339 {
338 {
340 Some(b"abort") => OnUnsupported::Abort,
339 Some(b"abort") => OnUnsupported::Abort,
341 Some(b"abort-silent") => OnUnsupported::AbortSilent,
340 Some(b"abort-silent") => OnUnsupported::AbortSilent,
342 Some(b"fallback") => OnUnsupported::Fallback {
341 Some(b"fallback") => OnUnsupported::Fallback {
343 executable: config
342 executable: config
344 .get(b"rhg", b"fallback-executable")
343 .get(b"rhg", b"fallback-executable")
345 .unwrap_or(Self::DEFAULT_FALLBACK_EXECUTABLE)
344 .unwrap_or(Self::DEFAULT_FALLBACK_EXECUTABLE)
346 .to_owned(),
345 .to_owned(),
347 },
346 },
348 None => Self::DEFAULT,
347 None => Self::DEFAULT,
349 Some(_) => {
348 Some(_) => {
350 // TODO: warn about unknown config value
349 // TODO: warn about unknown config value
351 Self::DEFAULT
350 Self::DEFAULT
352 }
351 }
353 }
352 }
354 }
353 }
355 }
354 }
@@ -1,299 +1,299 b''
1 #require rhg
1 #require rhg
2
2
3 $ NO_FALLBACK="env RHG_ON_UNSUPPORTED=abort"
3 $ NO_FALLBACK="env RHG_ON_UNSUPPORTED=abort"
4
4
5 Unimplemented command
5 Unimplemented command
6 $ $NO_FALLBACK rhg unimplemented-command
6 $ $NO_FALLBACK rhg unimplemented-command
7 unsupported feature: error: Found argument 'unimplemented-command' which wasn't expected, or isn't valid in this context
7 unsupported feature: error: Found argument 'unimplemented-command' which wasn't expected, or isn't valid in this context
8
8
9 USAGE:
9 USAGE:
10 rhg [OPTIONS] <SUBCOMMAND>
10 rhg [OPTIONS] <SUBCOMMAND>
11
11
12 For more information try --help
12 For more information try --help
13
13
14 [252]
14 [252]
15 $ rhg unimplemented-command --config rhg.on-unsupported=abort-silent
15 $ rhg unimplemented-command --config rhg.on-unsupported=abort-silent
16 [252]
16 [252]
17
17
18 Finding root
18 Finding root
19 $ $NO_FALLBACK rhg root
19 $ $NO_FALLBACK rhg root
20 abort: no repository found in '$TESTTMP' (.hg not found)!
20 abort: no repository found in '$TESTTMP' (.hg not found)!
21 [255]
21 [255]
22
22
23 $ hg init repository
23 $ hg init repository
24 $ cd repository
24 $ cd repository
25 $ $NO_FALLBACK rhg root
25 $ $NO_FALLBACK rhg root
26 $TESTTMP/repository
26 $TESTTMP/repository
27
27
28 Reading and setting configuration
28 Reading and setting configuration
29 $ echo "[ui]" >> $HGRCPATH
29 $ echo "[ui]" >> $HGRCPATH
30 $ echo "username = user1" >> $HGRCPATH
30 $ echo "username = user1" >> $HGRCPATH
31 $ $NO_FALLBACK rhg config ui.username
31 $ $NO_FALLBACK rhg config ui.username
32 user1
32 user1
33 $ echo "[ui]" >> .hg/hgrc
33 $ echo "[ui]" >> .hg/hgrc
34 $ echo "username = user2" >> .hg/hgrc
34 $ echo "username = user2" >> .hg/hgrc
35 $ $NO_FALLBACK rhg config ui.username
35 $ $NO_FALLBACK rhg config ui.username
36 user2
36 user2
37 $ $NO_FALLBACK rhg --config ui.username=user3 config ui.username
37 $ $NO_FALLBACK rhg --config ui.username=user3 config ui.username
38 user3
38 user3
39
39
40 Unwritable file descriptor
40 Unwritable file descriptor
41 $ $NO_FALLBACK rhg root > /dev/full
41 $ $NO_FALLBACK rhg root > /dev/full
42 abort: No space left on device (os error 28)
42 abort: No space left on device (os error 28)
43 [255]
43 [255]
44
44
45 Deleted repository
45 Deleted repository
46 $ rm -rf `pwd`
46 $ rm -rf `pwd`
47 $ $NO_FALLBACK rhg root
47 $ $NO_FALLBACK rhg root
48 abort: $ENOENT$: current directory
48 abort: error getting current working directory: $ENOENT$
49 [255]
49 [255]
50
50
51 Listing tracked files
51 Listing tracked files
52 $ cd $TESTTMP
52 $ cd $TESTTMP
53 $ hg init repository
53 $ hg init repository
54 $ cd repository
54 $ cd repository
55 $ for i in 1 2 3; do
55 $ for i in 1 2 3; do
56 > echo $i >> file$i
56 > echo $i >> file$i
57 > hg add file$i
57 > hg add file$i
58 > done
58 > done
59 > hg commit -m "commit $i" -q
59 > hg commit -m "commit $i" -q
60
60
61 Listing tracked files from root
61 Listing tracked files from root
62 $ $NO_FALLBACK rhg files
62 $ $NO_FALLBACK rhg files
63 file1
63 file1
64 file2
64 file2
65 file3
65 file3
66
66
67 Listing tracked files from subdirectory
67 Listing tracked files from subdirectory
68 $ mkdir -p path/to/directory
68 $ mkdir -p path/to/directory
69 $ cd path/to/directory
69 $ cd path/to/directory
70 $ $NO_FALLBACK rhg files
70 $ $NO_FALLBACK rhg files
71 ../../../file1
71 ../../../file1
72 ../../../file2
72 ../../../file2
73 ../../../file3
73 ../../../file3
74
74
75 Listing tracked files through broken pipe
75 Listing tracked files through broken pipe
76 $ $NO_FALLBACK rhg files | head -n 1
76 $ $NO_FALLBACK rhg files | head -n 1
77 ../../../file1
77 ../../../file1
78
78
79 Debuging data in inline index
79 Debuging data in inline index
80 $ cd $TESTTMP
80 $ cd $TESTTMP
81 $ rm -rf repository
81 $ rm -rf repository
82 $ hg init repository
82 $ hg init repository
83 $ cd repository
83 $ cd repository
84 $ for i in 1 2 3 4 5 6; do
84 $ for i in 1 2 3 4 5 6; do
85 > echo $i >> file-$i
85 > echo $i >> file-$i
86 > hg add file-$i
86 > hg add file-$i
87 > hg commit -m "Commit $i" -q
87 > hg commit -m "Commit $i" -q
88 > done
88 > done
89 $ $NO_FALLBACK rhg debugdata -c 2
89 $ $NO_FALLBACK rhg debugdata -c 2
90 8d0267cb034247ebfa5ee58ce59e22e57a492297
90 8d0267cb034247ebfa5ee58ce59e22e57a492297
91 test
91 test
92 0 0
92 0 0
93 file-3
93 file-3
94
94
95 Commit 3 (no-eol)
95 Commit 3 (no-eol)
96 $ $NO_FALLBACK rhg debugdata -m 2
96 $ $NO_FALLBACK rhg debugdata -m 2
97 file-1\x00b8e02f6433738021a065f94175c7cd23db5f05be (esc)
97 file-1\x00b8e02f6433738021a065f94175c7cd23db5f05be (esc)
98 file-2\x005d9299349fc01ddd25d0070d149b124d8f10411e (esc)
98 file-2\x005d9299349fc01ddd25d0070d149b124d8f10411e (esc)
99 file-3\x002661d26c649684b482d10f91960cc3db683c38b4 (esc)
99 file-3\x002661d26c649684b482d10f91960cc3db683c38b4 (esc)
100
100
101 Debuging with full node id
101 Debuging with full node id
102 $ $NO_FALLBACK rhg debugdata -c `hg log -r 0 -T '{node}'`
102 $ $NO_FALLBACK rhg debugdata -c `hg log -r 0 -T '{node}'`
103 d1d1c679d3053e8926061b6f45ca52009f011e3f
103 d1d1c679d3053e8926061b6f45ca52009f011e3f
104 test
104 test
105 0 0
105 0 0
106 file-1
106 file-1
107
107
108 Commit 1 (no-eol)
108 Commit 1 (no-eol)
109
109
110 Specifying revisions by changeset ID
110 Specifying revisions by changeset ID
111 $ hg log -T '{node}\n'
111 $ hg log -T '{node}\n'
112 c6ad58c44207b6ff8a4fbbca7045a5edaa7e908b
112 c6ad58c44207b6ff8a4fbbca7045a5edaa7e908b
113 d654274993d0149eecc3cc03214f598320211900
113 d654274993d0149eecc3cc03214f598320211900
114 f646af7e96481d3a5470b695cf30ad8e3ab6c575
114 f646af7e96481d3a5470b695cf30ad8e3ab6c575
115 cf8b83f14ead62b374b6e91a0e9303b85dfd9ed7
115 cf8b83f14ead62b374b6e91a0e9303b85dfd9ed7
116 91c6f6e73e39318534dc415ea4e8a09c99cd74d6
116 91c6f6e73e39318534dc415ea4e8a09c99cd74d6
117 6ae9681c6d30389694d8701faf24b583cf3ccafe
117 6ae9681c6d30389694d8701faf24b583cf3ccafe
118 $ $NO_FALLBACK rhg files -r cf8b83
118 $ $NO_FALLBACK rhg files -r cf8b83
119 file-1
119 file-1
120 file-2
120 file-2
121 file-3
121 file-3
122 $ $NO_FALLBACK rhg cat -r cf8b83 file-2
122 $ $NO_FALLBACK rhg cat -r cf8b83 file-2
123 2
123 2
124 $ $NO_FALLBACK rhg cat -r c file-2
124 $ $NO_FALLBACK rhg cat -r c file-2
125 abort: ambiguous revision identifier c
125 abort: ambiguous revision identifier: c
126 [255]
126 [255]
127 $ $NO_FALLBACK rhg cat -r d file-2
127 $ $NO_FALLBACK rhg cat -r d file-2
128 2
128 2
129
129
130 Cat files
130 Cat files
131 $ cd $TESTTMP
131 $ cd $TESTTMP
132 $ rm -rf repository
132 $ rm -rf repository
133 $ hg init repository
133 $ hg init repository
134 $ cd repository
134 $ cd repository
135 $ echo "original content" > original
135 $ echo "original content" > original
136 $ hg add original
136 $ hg add original
137 $ hg commit -m "add original" original
137 $ hg commit -m "add original" original
138 $ $NO_FALLBACK rhg cat -r 0 original
138 $ $NO_FALLBACK rhg cat -r 0 original
139 original content
139 original content
140 Cat copied file should not display copy metadata
140 Cat copied file should not display copy metadata
141 $ hg copy original copy_of_original
141 $ hg copy original copy_of_original
142 $ hg commit -m "add copy of original"
142 $ hg commit -m "add copy of original"
143 $ $NO_FALLBACK rhg cat -r 1 copy_of_original
143 $ $NO_FALLBACK rhg cat -r 1 copy_of_original
144 original content
144 original content
145
145
146 Fallback to Python
146 Fallback to Python
147 $ $NO_FALLBACK rhg cat original
147 $ $NO_FALLBACK rhg cat original
148 unsupported feature: `rhg cat` without `--rev` / `-r`
148 unsupported feature: `rhg cat` without `--rev` / `-r`
149 [252]
149 [252]
150 $ rhg cat original
150 $ rhg cat original
151 original content
151 original content
152
152
153 $ rhg cat original --config rhg.fallback-executable=false
153 $ rhg cat original --config rhg.fallback-executable=false
154 [1]
154 [1]
155
155
156 $ rhg cat original --config rhg.fallback-executable=hg-non-existent
156 $ rhg cat original --config rhg.fallback-executable=hg-non-existent
157 tried to fall back to a 'hg-non-existent' sub-process but got error $ENOENT$
157 tried to fall back to a 'hg-non-existent' sub-process but got error $ENOENT$
158 unsupported feature: `rhg cat` without `--rev` / `-r`
158 unsupported feature: `rhg cat` without `--rev` / `-r`
159 [252]
159 [252]
160
160
161 $ rhg cat original --config rhg.fallback-executable=rhg
161 $ rhg cat original --config rhg.fallback-executable=rhg
162 Blocking recursive fallback. The 'rhg.fallback-executable = rhg' config points to `rhg` itself.
162 Blocking recursive fallback. The 'rhg.fallback-executable = rhg' config points to `rhg` itself.
163 unsupported feature: `rhg cat` without `--rev` / `-r`
163 unsupported feature: `rhg cat` without `--rev` / `-r`
164 [252]
164 [252]
165
165
166 Requirements
166 Requirements
167 $ $NO_FALLBACK rhg debugrequirements
167 $ $NO_FALLBACK rhg debugrequirements
168 dotencode
168 dotencode
169 fncache
169 fncache
170 generaldelta
170 generaldelta
171 revlogv1
171 revlogv1
172 sparserevlog
172 sparserevlog
173 store
173 store
174
174
175 $ echo indoor-pool >> .hg/requires
175 $ echo indoor-pool >> .hg/requires
176 $ $NO_FALLBACK rhg files
176 $ $NO_FALLBACK rhg files
177 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
177 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
178 [252]
178 [252]
179
179
180 $ $NO_FALLBACK rhg cat -r 1 copy_of_original
180 $ $NO_FALLBACK rhg cat -r 1 copy_of_original
181 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
181 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
182 [252]
182 [252]
183
183
184 $ $NO_FALLBACK rhg debugrequirements
184 $ $NO_FALLBACK rhg debugrequirements
185 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
185 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
186 [252]
186 [252]
187
187
188 $ echo -e '\xFF' >> .hg/requires
188 $ echo -e '\xFF' >> .hg/requires
189 $ $NO_FALLBACK rhg debugrequirements
189 $ $NO_FALLBACK rhg debugrequirements
190 abort: corrupted repository: parse error in 'requires' file
190 abort: corrupted repository: parse error in 'requires' file
191 [255]
191 [255]
192
192
193 Persistent nodemap
193 Persistent nodemap
194 $ cd $TESTTMP
194 $ cd $TESTTMP
195 $ rm -rf repository
195 $ rm -rf repository
196 $ hg init repository
196 $ hg init repository
197 $ cd repository
197 $ cd repository
198 $ $NO_FALLBACK rhg debugrequirements | grep nodemap
198 $ $NO_FALLBACK rhg debugrequirements | grep nodemap
199 [1]
199 [1]
200 $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn"
200 $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn"
201 $ hg id -r tip
201 $ hg id -r tip
202 c3ae8dec9fad tip
202 c3ae8dec9fad tip
203 $ ls .hg/store/00changelog*
203 $ ls .hg/store/00changelog*
204 .hg/store/00changelog.d
204 .hg/store/00changelog.d
205 .hg/store/00changelog.i
205 .hg/store/00changelog.i
206 $ $NO_FALLBACK rhg files -r c3ae8dec9fad
206 $ $NO_FALLBACK rhg files -r c3ae8dec9fad
207 of
207 of
208
208
209 $ cd $TESTTMP
209 $ cd $TESTTMP
210 $ rm -rf repository
210 $ rm -rf repository
211 $ hg --config format.use-persistent-nodemap=True init repository
211 $ hg --config format.use-persistent-nodemap=True init repository
212 $ cd repository
212 $ cd repository
213 $ $NO_FALLBACK rhg debugrequirements | grep nodemap
213 $ $NO_FALLBACK rhg debugrequirements | grep nodemap
214 persistent-nodemap
214 persistent-nodemap
215 $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn"
215 $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn"
216 $ hg id -r tip
216 $ hg id -r tip
217 c3ae8dec9fad tip
217 c3ae8dec9fad tip
218 $ ls .hg/store/00changelog*
218 $ ls .hg/store/00changelog*
219 .hg/store/00changelog-*.nd (glob)
219 .hg/store/00changelog-*.nd (glob)
220 .hg/store/00changelog.d
220 .hg/store/00changelog.d
221 .hg/store/00changelog.i
221 .hg/store/00changelog.i
222 .hg/store/00changelog.n
222 .hg/store/00changelog.n
223
223
224 Specifying revisions by changeset ID
224 Specifying revisions by changeset ID
225 $ $NO_FALLBACK rhg files -r c3ae8dec9fad
225 $ $NO_FALLBACK rhg files -r c3ae8dec9fad
226 of
226 of
227 $ $NO_FALLBACK rhg cat -r c3ae8dec9fad of
227 $ $NO_FALLBACK rhg cat -r c3ae8dec9fad of
228 r5000
228 r5000
229
229
230 Crate a shared repository
230 Crate a shared repository
231
231
232 $ echo "[extensions]" >> $HGRCPATH
232 $ echo "[extensions]" >> $HGRCPATH
233 $ echo "share = " >> $HGRCPATH
233 $ echo "share = " >> $HGRCPATH
234
234
235 $ cd $TESTTMP
235 $ cd $TESTTMP
236 $ hg init repo1
236 $ hg init repo1
237 $ echo a > repo1/a
237 $ echo a > repo1/a
238 $ hg -R repo1 commit -A -m'init'
238 $ hg -R repo1 commit -A -m'init'
239 adding a
239 adding a
240
240
241 $ hg share repo1 repo2
241 $ hg share repo1 repo2
242 updating working directory
242 updating working directory
243 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
243 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
244
244
245 And check that basic rhg commands work with sharing
245 And check that basic rhg commands work with sharing
246
246
247 $ $NO_FALLBACK rhg files -R repo2
247 $ $NO_FALLBACK rhg files -R repo2
248 repo2/a
248 repo2/a
249 $ $NO_FALLBACK rhg -R repo2 cat -r 0 repo2/a
249 $ $NO_FALLBACK rhg -R repo2 cat -r 0 repo2/a
250 a
250 a
251
251
252 Same with relative sharing
252 Same with relative sharing
253
253
254 $ hg share repo2 repo3 --relative
254 $ hg share repo2 repo3 --relative
255 updating working directory
255 updating working directory
256 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
256 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
257
257
258 $ $NO_FALLBACK rhg files -R repo3
258 $ $NO_FALLBACK rhg files -R repo3
259 repo3/a
259 repo3/a
260 $ $NO_FALLBACK rhg -R repo3 cat -r 0 repo3/a
260 $ $NO_FALLBACK rhg -R repo3 cat -r 0 repo3/a
261 a
261 a
262
262
263 Same with share-safe
263 Same with share-safe
264
264
265 $ echo "[format]" >> $HGRCPATH
265 $ echo "[format]" >> $HGRCPATH
266 $ echo "use-share-safe = True" >> $HGRCPATH
266 $ echo "use-share-safe = True" >> $HGRCPATH
267
267
268 $ cd $TESTTMP
268 $ cd $TESTTMP
269 $ hg init repo4
269 $ hg init repo4
270 $ cd repo4
270 $ cd repo4
271 $ echo a > a
271 $ echo a > a
272 $ hg commit -A -m'init'
272 $ hg commit -A -m'init'
273 adding a
273 adding a
274
274
275 $ cd ..
275 $ cd ..
276 $ hg share repo4 repo5
276 $ hg share repo4 repo5
277 updating working directory
277 updating working directory
278 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
278 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
279
279
280 And check that basic rhg commands work with sharing
280 And check that basic rhg commands work with sharing
281
281
282 $ cd repo5
282 $ cd repo5
283 $ $NO_FALLBACK rhg files
283 $ $NO_FALLBACK rhg files
284 a
284 a
285 $ $NO_FALLBACK rhg cat -r 0 a
285 $ $NO_FALLBACK rhg cat -r 0 a
286 a
286 a
287
287
288 The blackbox extension is supported
288 The blackbox extension is supported
289
289
290 $ echo "[extensions]" >> $HGRCPATH
290 $ echo "[extensions]" >> $HGRCPATH
291 $ echo "blackbox =" >> $HGRCPATH
291 $ echo "blackbox =" >> $HGRCPATH
292 $ echo "[blackbox]" >> $HGRCPATH
292 $ echo "[blackbox]" >> $HGRCPATH
293 $ echo "maxsize = 1" >> $HGRCPATH
293 $ echo "maxsize = 1" >> $HGRCPATH
294 $ $NO_FALLBACK rhg files > /dev/null
294 $ $NO_FALLBACK rhg files > /dev/null
295 $ cat .hg/blackbox.log
295 $ cat .hg/blackbox.log
296 ????/??/?? ??:??:??.??? * @d3873e73d99ef67873dac33fbcc66268d5d2b6f4 (*)> (rust) files exited 0 after 0.??? seconds (glob)
296 ????/??/?? ??:??:??.??? * @d3873e73d99ef67873dac33fbcc66268d5d2b6f4 (*)> (rust) files exited 0 after 0.??? seconds (glob)
297 $ cat .hg/blackbox.log.1
297 $ cat .hg/blackbox.log.1
298 ????/??/?? ??:??:??.??? * @d3873e73d99ef67873dac33fbcc66268d5d2b6f4 (*)> (rust) files (glob)
298 ????/??/?? ??:??:??.??? * @d3873e73d99ef67873dac33fbcc66268d5d2b6f4 (*)> (rust) files (glob)
299
299
General Comments 0
You need to be logged in to leave comments. Login now