##// END OF EJS Templates
rhg: [encode] and [decode] config sections are not supported...
Simon Sapin -
r49162:a2e278b5 default
parent child Browse files
Show More
@@ -1,470 +1,477 b''
1 // config.rs
1 // config.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 super::layer;
10 use super::layer;
11 use super::values;
11 use super::values;
12 use crate::config::layer::{
12 use crate::config::layer::{
13 ConfigError, ConfigLayer, ConfigOrigin, ConfigValue,
13 ConfigError, ConfigLayer, ConfigOrigin, ConfigValue,
14 };
14 };
15 use crate::utils::files::get_bytes_from_os_str;
15 use crate::utils::files::get_bytes_from_os_str;
16 use format_bytes::{write_bytes, DisplayBytes};
16 use format_bytes::{write_bytes, DisplayBytes};
17 use std::collections::HashSet;
17 use std::collections::HashSet;
18 use std::env;
18 use std::env;
19 use std::fmt;
19 use std::fmt;
20 use std::path::{Path, PathBuf};
20 use std::path::{Path, PathBuf};
21 use std::str;
21 use std::str;
22
22
23 use crate::errors::{HgResultExt, IoResultExt};
23 use crate::errors::{HgResultExt, IoResultExt};
24
24
25 /// Holds the config values for the current repository
25 /// Holds the config values for the current repository
26 /// TODO update this docstring once we support more sources
26 /// TODO update this docstring once we support more sources
27 #[derive(Clone)]
27 #[derive(Clone)]
28 pub struct Config {
28 pub struct Config {
29 layers: Vec<layer::ConfigLayer>,
29 layers: Vec<layer::ConfigLayer>,
30 }
30 }
31
31
32 impl DisplayBytes for Config {
32 impl DisplayBytes for Config {
33 fn display_bytes(
33 fn display_bytes(
34 &self,
34 &self,
35 out: &mut dyn std::io::Write,
35 out: &mut dyn std::io::Write,
36 ) -> std::io::Result<()> {
36 ) -> std::io::Result<()> {
37 for (index, layer) in self.layers.iter().rev().enumerate() {
37 for (index, layer) in self.layers.iter().rev().enumerate() {
38 write_bytes!(
38 write_bytes!(
39 out,
39 out,
40 b"==== Layer {} (trusted: {}) ====\n{}",
40 b"==== Layer {} (trusted: {}) ====\n{}",
41 index,
41 index,
42 if layer.trusted {
42 if layer.trusted {
43 &b"yes"[..]
43 &b"yes"[..]
44 } else {
44 } else {
45 &b"no"[..]
45 &b"no"[..]
46 },
46 },
47 layer
47 layer
48 )?;
48 )?;
49 }
49 }
50 Ok(())
50 Ok(())
51 }
51 }
52 }
52 }
53
53
54 pub enum ConfigSource {
54 pub enum ConfigSource {
55 /// Absolute path to a config file
55 /// Absolute path to a config file
56 AbsPath(PathBuf),
56 AbsPath(PathBuf),
57 /// Already parsed (from the CLI, env, Python resources, etc.)
57 /// Already parsed (from the CLI, env, Python resources, etc.)
58 Parsed(layer::ConfigLayer),
58 Parsed(layer::ConfigLayer),
59 }
59 }
60
60
61 #[derive(Debug)]
61 #[derive(Debug)]
62 pub struct ConfigValueParseError {
62 pub struct ConfigValueParseError {
63 pub origin: ConfigOrigin,
63 pub origin: ConfigOrigin,
64 pub line: Option<usize>,
64 pub line: Option<usize>,
65 pub section: Vec<u8>,
65 pub section: Vec<u8>,
66 pub item: Vec<u8>,
66 pub item: Vec<u8>,
67 pub value: Vec<u8>,
67 pub value: Vec<u8>,
68 pub expected_type: &'static str,
68 pub expected_type: &'static str,
69 }
69 }
70
70
71 impl fmt::Display for ConfigValueParseError {
71 impl fmt::Display for ConfigValueParseError {
72 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
72 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73 // TODO: add origin and line number information, here and in
73 // TODO: add origin and line number information, here and in
74 // corresponding python code
74 // corresponding python code
75 write!(
75 write!(
76 f,
76 f,
77 "config error: {}.{} is not a {} ('{}')",
77 "config error: {}.{} is not a {} ('{}')",
78 String::from_utf8_lossy(&self.section),
78 String::from_utf8_lossy(&self.section),
79 String::from_utf8_lossy(&self.item),
79 String::from_utf8_lossy(&self.item),
80 self.expected_type,
80 self.expected_type,
81 String::from_utf8_lossy(&self.value)
81 String::from_utf8_lossy(&self.value)
82 )
82 )
83 }
83 }
84 }
84 }
85
85
86 impl Config {
86 impl Config {
87 /// Load system and user configuration from various files.
87 /// Load system and user configuration from various files.
88 ///
88 ///
89 /// This is also affected by some environment variables.
89 /// This is also affected by some environment variables.
90 pub fn load_non_repo() -> Result<Self, ConfigError> {
90 pub fn load_non_repo() -> Result<Self, ConfigError> {
91 let mut config = Self { layers: Vec::new() };
91 let mut config = Self { layers: Vec::new() };
92 let opt_rc_path = env::var_os("HGRCPATH");
92 let opt_rc_path = env::var_os("HGRCPATH");
93 // HGRCPATH replaces system config
93 // HGRCPATH replaces system config
94 if opt_rc_path.is_none() {
94 if opt_rc_path.is_none() {
95 config.add_system_config()?
95 config.add_system_config()?
96 }
96 }
97
97
98 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
98 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
99 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
99 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
100 config.add_for_environment_variable("PAGER", b"pager", b"pager");
100 config.add_for_environment_variable("PAGER", b"pager", b"pager");
101
101
102 // These are set by `run-tests.py --rhg` to enable fallback for the
102 // These are set by `run-tests.py --rhg` to enable fallback for the
103 // entire test suite. Alternatives would be setting configuration
103 // entire test suite. Alternatives would be setting configuration
104 // through `$HGRCPATH` but some tests override that, or changing the
104 // through `$HGRCPATH` but some tests override that, or changing the
105 // `hg` shell alias to include `--config` but that disrupts tests that
105 // `hg` shell alias to include `--config` but that disrupts tests that
106 // print command lines and check expected output.
106 // print command lines and check expected output.
107 config.add_for_environment_variable(
107 config.add_for_environment_variable(
108 "RHG_ON_UNSUPPORTED",
108 "RHG_ON_UNSUPPORTED",
109 b"rhg",
109 b"rhg",
110 b"on-unsupported",
110 b"on-unsupported",
111 );
111 );
112 config.add_for_environment_variable(
112 config.add_for_environment_variable(
113 "RHG_FALLBACK_EXECUTABLE",
113 "RHG_FALLBACK_EXECUTABLE",
114 b"rhg",
114 b"rhg",
115 b"fallback-executable",
115 b"fallback-executable",
116 );
116 );
117 config.add_for_environment_variable("RHG_STATUS", b"rhg", b"status");
117 config.add_for_environment_variable("RHG_STATUS", b"rhg", b"status");
118
118
119 // HGRCPATH replaces user config
119 // HGRCPATH replaces user config
120 if opt_rc_path.is_none() {
120 if opt_rc_path.is_none() {
121 config.add_user_config()?
121 config.add_user_config()?
122 }
122 }
123 if let Some(rc_path) = &opt_rc_path {
123 if let Some(rc_path) = &opt_rc_path {
124 for path in env::split_paths(rc_path) {
124 for path in env::split_paths(rc_path) {
125 if !path.as_os_str().is_empty() {
125 if !path.as_os_str().is_empty() {
126 if path.is_dir() {
126 if path.is_dir() {
127 config.add_trusted_dir(&path)?
127 config.add_trusted_dir(&path)?
128 } else {
128 } else {
129 config.add_trusted_file(&path)?
129 config.add_trusted_file(&path)?
130 }
130 }
131 }
131 }
132 }
132 }
133 }
133 }
134 Ok(config)
134 Ok(config)
135 }
135 }
136
136
137 pub fn load_cli_args_config(
137 pub fn load_cli_args_config(
138 &mut self,
138 &mut self,
139 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
139 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
140 ) -> Result<(), ConfigError> {
140 ) -> Result<(), ConfigError> {
141 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
141 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
142 self.layers.push(layer)
142 self.layers.push(layer)
143 }
143 }
144 Ok(())
144 Ok(())
145 }
145 }
146
146
147 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
147 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
148 if let Some(entries) = std::fs::read_dir(path)
148 if let Some(entries) = std::fs::read_dir(path)
149 .when_reading_file(path)
149 .when_reading_file(path)
150 .io_not_found_as_none()?
150 .io_not_found_as_none()?
151 {
151 {
152 let mut file_paths = entries
152 let mut file_paths = entries
153 .map(|result| {
153 .map(|result| {
154 result.when_reading_file(path).map(|entry| entry.path())
154 result.when_reading_file(path).map(|entry| entry.path())
155 })
155 })
156 .collect::<Result<Vec<_>, _>>()?;
156 .collect::<Result<Vec<_>, _>>()?;
157 file_paths.sort();
157 file_paths.sort();
158 for file_path in &file_paths {
158 for file_path in &file_paths {
159 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
159 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
160 self.add_trusted_file(&file_path)?
160 self.add_trusted_file(&file_path)?
161 }
161 }
162 }
162 }
163 }
163 }
164 Ok(())
164 Ok(())
165 }
165 }
166
166
167 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
167 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
168 if let Some(data) = std::fs::read(path)
168 if let Some(data) = std::fs::read(path)
169 .when_reading_file(path)
169 .when_reading_file(path)
170 .io_not_found_as_none()?
170 .io_not_found_as_none()?
171 {
171 {
172 self.layers.extend(ConfigLayer::parse(path, &data)?)
172 self.layers.extend(ConfigLayer::parse(path, &data)?)
173 }
173 }
174 Ok(())
174 Ok(())
175 }
175 }
176
176
177 fn add_for_environment_variable(
177 fn add_for_environment_variable(
178 &mut self,
178 &mut self,
179 var: &str,
179 var: &str,
180 section: &[u8],
180 section: &[u8],
181 key: &[u8],
181 key: &[u8],
182 ) {
182 ) {
183 if let Some(value) = env::var_os(var) {
183 if let Some(value) = env::var_os(var) {
184 let origin = layer::ConfigOrigin::Environment(var.into());
184 let origin = layer::ConfigOrigin::Environment(var.into());
185 let mut layer = ConfigLayer::new(origin);
185 let mut layer = ConfigLayer::new(origin);
186 layer.add(
186 layer.add(
187 section.to_owned(),
187 section.to_owned(),
188 key.to_owned(),
188 key.to_owned(),
189 get_bytes_from_os_str(value),
189 get_bytes_from_os_str(value),
190 None,
190 None,
191 );
191 );
192 self.layers.push(layer)
192 self.layers.push(layer)
193 }
193 }
194 }
194 }
195
195
196 #[cfg(unix)] // TODO: other platforms
196 #[cfg(unix)] // TODO: other platforms
197 fn add_system_config(&mut self) -> Result<(), ConfigError> {
197 fn add_system_config(&mut self) -> Result<(), ConfigError> {
198 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
198 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
199 let etc = prefix.join("etc").join("mercurial");
199 let etc = prefix.join("etc").join("mercurial");
200 self.add_trusted_file(&etc.join("hgrc"))?;
200 self.add_trusted_file(&etc.join("hgrc"))?;
201 self.add_trusted_dir(&etc.join("hgrc.d"))
201 self.add_trusted_dir(&etc.join("hgrc.d"))
202 };
202 };
203 let root = Path::new("/");
203 let root = Path::new("/");
204 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
204 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
205 // instead? TODO: can this be a relative path?
205 // instead? TODO: can this be a relative path?
206 let hg = crate::utils::current_exe()?;
206 let hg = crate::utils::current_exe()?;
207 // TODO: this order (per-installation then per-system) matches
207 // TODO: this order (per-installation then per-system) matches
208 // `systemrcpath()` in `mercurial/scmposix.py`, but
208 // `systemrcpath()` in `mercurial/scmposix.py`, but
209 // `mercurial/helptext/config.txt` suggests it should be reversed
209 // `mercurial/helptext/config.txt` suggests it should be reversed
210 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
210 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
211 if installation_prefix != root {
211 if installation_prefix != root {
212 add_for_prefix(&installation_prefix)?
212 add_for_prefix(&installation_prefix)?
213 }
213 }
214 }
214 }
215 add_for_prefix(root)?;
215 add_for_prefix(root)?;
216 Ok(())
216 Ok(())
217 }
217 }
218
218
219 #[cfg(unix)] // TODO: other plateforms
219 #[cfg(unix)] // TODO: other plateforms
220 fn add_user_config(&mut self) -> Result<(), ConfigError> {
220 fn add_user_config(&mut self) -> Result<(), ConfigError> {
221 let opt_home = home::home_dir();
221 let opt_home = home::home_dir();
222 if let Some(home) = &opt_home {
222 if let Some(home) = &opt_home {
223 self.add_trusted_file(&home.join(".hgrc"))?
223 self.add_trusted_file(&home.join(".hgrc"))?
224 }
224 }
225 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
225 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
226 if !darwin {
226 if !darwin {
227 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
227 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
228 .map(PathBuf::from)
228 .map(PathBuf::from)
229 .or_else(|| opt_home.map(|home| home.join(".config")))
229 .or_else(|| opt_home.map(|home| home.join(".config")))
230 {
230 {
231 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
231 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
232 }
232 }
233 }
233 }
234 Ok(())
234 Ok(())
235 }
235 }
236
236
237 /// Loads in order, which means that the precedence is the same
237 /// Loads in order, which means that the precedence is the same
238 /// as the order of `sources`.
238 /// as the order of `sources`.
239 pub fn load_from_explicit_sources(
239 pub fn load_from_explicit_sources(
240 sources: Vec<ConfigSource>,
240 sources: Vec<ConfigSource>,
241 ) -> Result<Self, ConfigError> {
241 ) -> Result<Self, ConfigError> {
242 let mut layers = vec![];
242 let mut layers = vec![];
243
243
244 for source in sources.into_iter() {
244 for source in sources.into_iter() {
245 match source {
245 match source {
246 ConfigSource::Parsed(c) => layers.push(c),
246 ConfigSource::Parsed(c) => layers.push(c),
247 ConfigSource::AbsPath(c) => {
247 ConfigSource::AbsPath(c) => {
248 // TODO check if it should be trusted
248 // TODO check if it should be trusted
249 // mercurial/ui.py:427
249 // mercurial/ui.py:427
250 let data = match std::fs::read(&c) {
250 let data = match std::fs::read(&c) {
251 Err(_) => continue, // same as the python code
251 Err(_) => continue, // same as the python code
252 Ok(data) => data,
252 Ok(data) => data,
253 };
253 };
254 layers.extend(ConfigLayer::parse(&c, &data)?)
254 layers.extend(ConfigLayer::parse(&c, &data)?)
255 }
255 }
256 }
256 }
257 }
257 }
258
258
259 Ok(Config { layers })
259 Ok(Config { layers })
260 }
260 }
261
261
262 /// Loads the per-repository config into a new `Config` which is combined
262 /// Loads the per-repository config into a new `Config` which is combined
263 /// with `self`.
263 /// with `self`.
264 pub(crate) fn combine_with_repo(
264 pub(crate) fn combine_with_repo(
265 &self,
265 &self,
266 repo_config_files: &[PathBuf],
266 repo_config_files: &[PathBuf],
267 ) -> Result<Self, ConfigError> {
267 ) -> Result<Self, ConfigError> {
268 let (cli_layers, other_layers) = self
268 let (cli_layers, other_layers) = self
269 .layers
269 .layers
270 .iter()
270 .iter()
271 .cloned()
271 .cloned()
272 .partition(ConfigLayer::is_from_command_line);
272 .partition(ConfigLayer::is_from_command_line);
273
273
274 let mut repo_config = Self {
274 let mut repo_config = Self {
275 layers: other_layers,
275 layers: other_layers,
276 };
276 };
277 for path in repo_config_files {
277 for path in repo_config_files {
278 // TODO: check if this file should be trusted:
278 // TODO: check if this file should be trusted:
279 // `mercurial/ui.py:427`
279 // `mercurial/ui.py:427`
280 repo_config.add_trusted_file(path)?;
280 repo_config.add_trusted_file(path)?;
281 }
281 }
282 repo_config.layers.extend(cli_layers);
282 repo_config.layers.extend(cli_layers);
283 Ok(repo_config)
283 Ok(repo_config)
284 }
284 }
285
285
286 fn get_parse<'config, T: 'config>(
286 fn get_parse<'config, T: 'config>(
287 &'config self,
287 &'config self,
288 section: &[u8],
288 section: &[u8],
289 item: &[u8],
289 item: &[u8],
290 expected_type: &'static str,
290 expected_type: &'static str,
291 parse: impl Fn(&'config [u8]) -> Option<T>,
291 parse: impl Fn(&'config [u8]) -> Option<T>,
292 ) -> Result<Option<T>, ConfigValueParseError> {
292 ) -> Result<Option<T>, ConfigValueParseError> {
293 match self.get_inner(&section, &item) {
293 match self.get_inner(&section, &item) {
294 Some((layer, v)) => match parse(&v.bytes) {
294 Some((layer, v)) => match parse(&v.bytes) {
295 Some(b) => Ok(Some(b)),
295 Some(b) => Ok(Some(b)),
296 None => Err(ConfigValueParseError {
296 None => Err(ConfigValueParseError {
297 origin: layer.origin.to_owned(),
297 origin: layer.origin.to_owned(),
298 line: v.line,
298 line: v.line,
299 value: v.bytes.to_owned(),
299 value: v.bytes.to_owned(),
300 section: section.to_owned(),
300 section: section.to_owned(),
301 item: item.to_owned(),
301 item: item.to_owned(),
302 expected_type,
302 expected_type,
303 }),
303 }),
304 },
304 },
305 None => Ok(None),
305 None => Ok(None),
306 }
306 }
307 }
307 }
308
308
309 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
309 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
310 /// Otherwise, returns an `Ok(value)` if found, or `None`.
310 /// Otherwise, returns an `Ok(value)` if found, or `None`.
311 pub fn get_str(
311 pub fn get_str(
312 &self,
312 &self,
313 section: &[u8],
313 section: &[u8],
314 item: &[u8],
314 item: &[u8],
315 ) -> Result<Option<&str>, ConfigValueParseError> {
315 ) -> Result<Option<&str>, ConfigValueParseError> {
316 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
316 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
317 str::from_utf8(value).ok()
317 str::from_utf8(value).ok()
318 })
318 })
319 }
319 }
320
320
321 /// Returns an `Err` if the first value found is not a valid unsigned
321 /// Returns an `Err` if the first value found is not a valid unsigned
322 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
322 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
323 pub fn get_u32(
323 pub fn get_u32(
324 &self,
324 &self,
325 section: &[u8],
325 section: &[u8],
326 item: &[u8],
326 item: &[u8],
327 ) -> Result<Option<u32>, ConfigValueParseError> {
327 ) -> Result<Option<u32>, ConfigValueParseError> {
328 self.get_parse(section, item, "valid integer", |value| {
328 self.get_parse(section, item, "valid integer", |value| {
329 str::from_utf8(value).ok()?.parse().ok()
329 str::from_utf8(value).ok()?.parse().ok()
330 })
330 })
331 }
331 }
332
332
333 /// Returns an `Err` if the first value found is not a valid file size
333 /// Returns an `Err` if the first value found is not a valid file size
334 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
334 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
335 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
335 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
336 pub fn get_byte_size(
336 pub fn get_byte_size(
337 &self,
337 &self,
338 section: &[u8],
338 section: &[u8],
339 item: &[u8],
339 item: &[u8],
340 ) -> Result<Option<u64>, ConfigValueParseError> {
340 ) -> Result<Option<u64>, ConfigValueParseError> {
341 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
341 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
342 }
342 }
343
343
344 /// Returns an `Err` if the first value found is not a valid boolean.
344 /// Returns an `Err` if the first value found is not a valid boolean.
345 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
345 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
346 /// found, or `None`.
346 /// found, or `None`.
347 pub fn get_option(
347 pub fn get_option(
348 &self,
348 &self,
349 section: &[u8],
349 section: &[u8],
350 item: &[u8],
350 item: &[u8],
351 ) -> Result<Option<bool>, ConfigValueParseError> {
351 ) -> Result<Option<bool>, ConfigValueParseError> {
352 self.get_parse(section, item, "boolean", values::parse_bool)
352 self.get_parse(section, item, "boolean", values::parse_bool)
353 }
353 }
354
354
355 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
355 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
356 /// if the value is not found, an `Err` if it's not a valid boolean.
356 /// if the value is not found, an `Err` if it's not a valid boolean.
357 pub fn get_bool(
357 pub fn get_bool(
358 &self,
358 &self,
359 section: &[u8],
359 section: &[u8],
360 item: &[u8],
360 item: &[u8],
361 ) -> Result<bool, ConfigValueParseError> {
361 ) -> Result<bool, ConfigValueParseError> {
362 Ok(self.get_option(section, item)?.unwrap_or(false))
362 Ok(self.get_option(section, item)?.unwrap_or(false))
363 }
363 }
364
364
365 /// If there is an `item` value in `section`, parse and return a list of
365 /// If there is an `item` value in `section`, parse and return a list of
366 /// byte strings.
366 /// byte strings.
367 pub fn get_list(
367 pub fn get_list(
368 &self,
368 &self,
369 section: &[u8],
369 section: &[u8],
370 item: &[u8],
370 item: &[u8],
371 ) -> Option<Vec<Vec<u8>>> {
371 ) -> Option<Vec<Vec<u8>>> {
372 self.get(section, item).map(values::parse_list)
372 self.get(section, item).map(values::parse_list)
373 }
373 }
374
374
375 /// Returns the raw value bytes of the first one found, or `None`.
375 /// Returns the raw value bytes of the first one found, or `None`.
376 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
376 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
377 self.get_inner(section, item)
377 self.get_inner(section, item)
378 .map(|(_, value)| value.bytes.as_ref())
378 .map(|(_, value)| value.bytes.as_ref())
379 }
379 }
380
380
381 /// Returns the layer and the value of the first one found, or `None`.
381 /// Returns the layer and the value of the first one found, or `None`.
382 fn get_inner(
382 fn get_inner(
383 &self,
383 &self,
384 section: &[u8],
384 section: &[u8],
385 item: &[u8],
385 item: &[u8],
386 ) -> Option<(&ConfigLayer, &ConfigValue)> {
386 ) -> Option<(&ConfigLayer, &ConfigValue)> {
387 for layer in self.layers.iter().rev() {
387 for layer in self.layers.iter().rev() {
388 if !layer.trusted {
388 if !layer.trusted {
389 continue;
389 continue;
390 }
390 }
391 if let Some(v) = layer.get(&section, &item) {
391 if let Some(v) = layer.get(&section, &item) {
392 return Some((&layer, v));
392 return Some((&layer, v));
393 }
393 }
394 }
394 }
395 None
395 None
396 }
396 }
397
397
398 /// Return all keys defined for the given section
398 /// Return all keys defined for the given section
399 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
399 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
400 self.layers
400 self.layers
401 .iter()
401 .iter()
402 .flat_map(|layer| layer.iter_keys(section))
402 .flat_map(|layer| layer.iter_keys(section))
403 .collect()
403 .collect()
404 }
404 }
405
405
406 /// Returns whether any key is defined in the given section
407 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
408 self.layers
409 .iter()
410 .any(|layer| layer.has_non_empty_section(section))
411 }
412
406 /// Get raw values bytes from all layers (even untrusted ones) in order
413 /// Get raw values bytes from all layers (even untrusted ones) in order
407 /// of precedence.
414 /// of precedence.
408 #[cfg(test)]
415 #[cfg(test)]
409 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
416 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
410 let mut res = vec![];
417 let mut res = vec![];
411 for layer in self.layers.iter().rev() {
418 for layer in self.layers.iter().rev() {
412 if let Some(v) = layer.get(&section, &item) {
419 if let Some(v) = layer.get(&section, &item) {
413 res.push(v.bytes.as_ref());
420 res.push(v.bytes.as_ref());
414 }
421 }
415 }
422 }
416 res
423 res
417 }
424 }
418 }
425 }
419
426
420 #[cfg(test)]
427 #[cfg(test)]
421 mod tests {
428 mod tests {
422 use super::*;
429 use super::*;
423 use pretty_assertions::assert_eq;
430 use pretty_assertions::assert_eq;
424 use std::fs::File;
431 use std::fs::File;
425 use std::io::Write;
432 use std::io::Write;
426
433
427 #[test]
434 #[test]
428 fn test_include_layer_ordering() {
435 fn test_include_layer_ordering() {
429 let tmpdir = tempfile::tempdir().unwrap();
436 let tmpdir = tempfile::tempdir().unwrap();
430 let tmpdir_path = tmpdir.path();
437 let tmpdir_path = tmpdir.path();
431 let mut included_file =
438 let mut included_file =
432 File::create(&tmpdir_path.join("included.rc")).unwrap();
439 File::create(&tmpdir_path.join("included.rc")).unwrap();
433
440
434 included_file.write_all(b"[section]\nitem=value1").unwrap();
441 included_file.write_all(b"[section]\nitem=value1").unwrap();
435 let base_config_path = tmpdir_path.join("base.rc");
442 let base_config_path = tmpdir_path.join("base.rc");
436 let mut config_file = File::create(&base_config_path).unwrap();
443 let mut config_file = File::create(&base_config_path).unwrap();
437 let data =
444 let data =
438 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
445 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
439 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
446 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
440 config_file.write_all(data).unwrap();
447 config_file.write_all(data).unwrap();
441
448
442 let sources = vec![ConfigSource::AbsPath(base_config_path)];
449 let sources = vec![ConfigSource::AbsPath(base_config_path)];
443 let config = Config::load_from_explicit_sources(sources)
450 let config = Config::load_from_explicit_sources(sources)
444 .expect("expected valid config");
451 .expect("expected valid config");
445
452
446 let (_, value) = config.get_inner(b"section", b"item").unwrap();
453 let (_, value) = config.get_inner(b"section", b"item").unwrap();
447 assert_eq!(
454 assert_eq!(
448 value,
455 value,
449 &ConfigValue {
456 &ConfigValue {
450 bytes: b"value2".to_vec(),
457 bytes: b"value2".to_vec(),
451 line: Some(4)
458 line: Some(4)
452 }
459 }
453 );
460 );
454
461
455 let value = config.get(b"section", b"item").unwrap();
462 let value = config.get(b"section", b"item").unwrap();
456 assert_eq!(value, b"value2",);
463 assert_eq!(value, b"value2",);
457 assert_eq!(
464 assert_eq!(
458 config.get_all(b"section", b"item"),
465 config.get_all(b"section", b"item"),
459 [b"value2", b"value1", b"value0"]
466 [b"value2", b"value1", b"value0"]
460 );
467 );
461
468
462 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
469 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
463 assert_eq!(
470 assert_eq!(
464 config.get_byte_size(b"section2", b"size").unwrap(),
471 config.get_byte_size(b"section2", b"size").unwrap(),
465 Some(1024 + 512)
472 Some(1024 + 512)
466 );
473 );
467 assert!(config.get_u32(b"section2", b"not-count").is_err());
474 assert!(config.get_u32(b"section2", b"not-count").is_err());
468 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
475 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
469 }
476 }
470 }
477 }
@@ -1,323 +1,330 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 ))?
84 ))?
85 }
85 }
86 }
86 }
87 if layer.sections.is_empty() {
87 if layer.sections.is_empty() {
88 Ok(None)
88 Ok(None)
89 } else {
89 } else {
90 Ok(Some(layer))
90 Ok(Some(layer))
91 }
91 }
92 }
92 }
93
93
94 /// Returns whether this layer comes from `--config` CLI arguments
94 /// Returns whether this layer comes from `--config` CLI arguments
95 pub(crate) fn is_from_command_line(&self) -> bool {
95 pub(crate) fn is_from_command_line(&self) -> bool {
96 if let ConfigOrigin::CommandLine = self.origin {
96 if let ConfigOrigin::CommandLine = self.origin {
97 true
97 true
98 } else {
98 } else {
99 false
99 false
100 }
100 }
101 }
101 }
102
102
103 /// Add an entry to the config, overwriting the old one if already present.
103 /// Add an entry to the config, overwriting the old one if already present.
104 pub fn add(
104 pub fn add(
105 &mut self,
105 &mut self,
106 section: Vec<u8>,
106 section: Vec<u8>,
107 item: Vec<u8>,
107 item: Vec<u8>,
108 value: Vec<u8>,
108 value: Vec<u8>,
109 line: Option<usize>,
109 line: Option<usize>,
110 ) {
110 ) {
111 self.sections
111 self.sections
112 .entry(section)
112 .entry(section)
113 .or_insert_with(|| HashMap::new())
113 .or_insert_with(|| HashMap::new())
114 .insert(item, ConfigValue { bytes: value, line });
114 .insert(item, ConfigValue { bytes: value, line });
115 }
115 }
116
116
117 /// Returns the config value in `<section>.<item>` if it exists
117 /// Returns the config value in `<section>.<item>` if it exists
118 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
118 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
119 Some(self.sections.get(section)?.get(item)?)
119 Some(self.sections.get(section)?.get(item)?)
120 }
120 }
121
121
122 /// Returns the keys defined in the given section
122 /// Returns the keys defined in the given section
123 pub fn iter_keys(&self, section: &[u8]) -> impl Iterator<Item = &[u8]> {
123 pub fn iter_keys(&self, section: &[u8]) -> impl Iterator<Item = &[u8]> {
124 self.sections
124 self.sections
125 .get(section)
125 .get(section)
126 .into_iter()
126 .into_iter()
127 .flat_map(|section| section.keys().map(|vec| &**vec))
127 .flat_map(|section| section.keys().map(|vec| &**vec))
128 }
128 }
129
129
130 /// Returns whether any key is defined in the given section
131 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
132 self.sections
133 .get(section)
134 .map_or(false, |section| !section.is_empty())
135 }
136
130 pub fn is_empty(&self) -> bool {
137 pub fn is_empty(&self) -> bool {
131 self.sections.is_empty()
138 self.sections.is_empty()
132 }
139 }
133
140
134 /// Returns a `Vec` of layers in order of precedence (so, in read order),
141 /// Returns a `Vec` of layers in order of precedence (so, in read order),
135 /// recursively parsing the `%include` directives if any.
142 /// recursively parsing the `%include` directives if any.
136 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
143 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
137 let mut layers = vec![];
144 let mut layers = vec![];
138
145
139 // Discard byte order mark if any
146 // Discard byte order mark if any
140 let data = if data.starts_with(b"\xef\xbb\xbf") {
147 let data = if data.starts_with(b"\xef\xbb\xbf") {
141 &data[3..]
148 &data[3..]
142 } else {
149 } else {
143 data
150 data
144 };
151 };
145
152
146 // TODO check if it's trusted
153 // TODO check if it's trusted
147 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
154 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
148
155
149 let mut lines_iter =
156 let mut lines_iter =
150 data.split(|b| *b == b'\n').enumerate().peekable();
157 data.split(|b| *b == b'\n').enumerate().peekable();
151 let mut section = b"".to_vec();
158 let mut section = b"".to_vec();
152
159
153 while let Some((index, bytes)) = lines_iter.next() {
160 while let Some((index, bytes)) = lines_iter.next() {
154 let line = Some(index + 1);
161 let line = Some(index + 1);
155 if let Some(m) = INCLUDE_RE.captures(&bytes) {
162 if let Some(m) = INCLUDE_RE.captures(&bytes) {
156 let filename_bytes = &m[1];
163 let filename_bytes = &m[1];
157 let filename_bytes = crate::utils::expand_vars(filename_bytes);
164 let filename_bytes = crate::utils::expand_vars(filename_bytes);
158 // `Path::parent` only fails for the root directory,
165 // `Path::parent` only fails for the root directory,
159 // which `src` can’t be since we’ve managed to open it as a
166 // which `src` can’t be since we’ve managed to open it as a
160 // file.
167 // file.
161 let dir = src
168 let dir = src
162 .parent()
169 .parent()
163 .expect("Path::parent fail on a file we’ve read");
170 .expect("Path::parent fail on a file we’ve read");
164 // `Path::join` with an absolute argument correctly ignores the
171 // `Path::join` with an absolute argument correctly ignores the
165 // base path
172 // base path
166 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
173 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
167 match std::fs::read(&filename) {
174 match std::fs::read(&filename) {
168 Ok(data) => {
175 Ok(data) => {
169 layers.push(current_layer);
176 layers.push(current_layer);
170 layers.extend(Self::parse(&filename, &data)?);
177 layers.extend(Self::parse(&filename, &data)?);
171 current_layer =
178 current_layer =
172 Self::new(ConfigOrigin::File(src.to_owned()));
179 Self::new(ConfigOrigin::File(src.to_owned()));
173 }
180 }
174 Err(error) => {
181 Err(error) => {
175 if error.kind() != std::io::ErrorKind::NotFound {
182 if error.kind() != std::io::ErrorKind::NotFound {
176 return Err(ConfigParseError {
183 return Err(ConfigParseError {
177 origin: ConfigOrigin::File(src.to_owned()),
184 origin: ConfigOrigin::File(src.to_owned()),
178 line,
185 line,
179 message: format_bytes!(
186 message: format_bytes!(
180 b"cannot include {} ({})",
187 b"cannot include {} ({})",
181 filename_bytes,
188 filename_bytes,
182 format_bytes::Utf8(error)
189 format_bytes::Utf8(error)
183 ),
190 ),
184 }
191 }
185 .into());
192 .into());
186 }
193 }
187 }
194 }
188 }
195 }
189 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
196 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
190 } else if let Some(m) = SECTION_RE.captures(&bytes) {
197 } else if let Some(m) = SECTION_RE.captures(&bytes) {
191 section = m[1].to_vec();
198 section = m[1].to_vec();
192 } else if let Some(m) = ITEM_RE.captures(&bytes) {
199 } else if let Some(m) = ITEM_RE.captures(&bytes) {
193 let item = m[1].to_vec();
200 let item = m[1].to_vec();
194 let mut value = m[2].to_vec();
201 let mut value = m[2].to_vec();
195 loop {
202 loop {
196 match lines_iter.peek() {
203 match lines_iter.peek() {
197 None => break,
204 None => break,
198 Some((_, v)) => {
205 Some((_, v)) => {
199 if let Some(_) = COMMENT_RE.captures(&v) {
206 if let Some(_) = COMMENT_RE.captures(&v) {
200 } else if let Some(_) = CONT_RE.captures(&v) {
207 } else if let Some(_) = CONT_RE.captures(&v) {
201 value.extend(b"\n");
208 value.extend(b"\n");
202 value.extend(&m[1]);
209 value.extend(&m[1]);
203 } else {
210 } else {
204 break;
211 break;
205 }
212 }
206 }
213 }
207 };
214 };
208 lines_iter.next();
215 lines_iter.next();
209 }
216 }
210 current_layer.add(section.clone(), item, value, line);
217 current_layer.add(section.clone(), item, value, line);
211 } else if let Some(m) = UNSET_RE.captures(&bytes) {
218 } else if let Some(m) = UNSET_RE.captures(&bytes) {
212 if let Some(map) = current_layer.sections.get_mut(&section) {
219 if let Some(map) = current_layer.sections.get_mut(&section) {
213 map.remove(&m[1]);
220 map.remove(&m[1]);
214 }
221 }
215 } else {
222 } else {
216 let message = if bytes.starts_with(b" ") {
223 let message = if bytes.starts_with(b" ") {
217 format_bytes!(b"unexpected leading whitespace: {}", bytes)
224 format_bytes!(b"unexpected leading whitespace: {}", bytes)
218 } else {
225 } else {
219 bytes.to_owned()
226 bytes.to_owned()
220 };
227 };
221 return Err(ConfigParseError {
228 return Err(ConfigParseError {
222 origin: ConfigOrigin::File(src.to_owned()),
229 origin: ConfigOrigin::File(src.to_owned()),
223 line,
230 line,
224 message,
231 message,
225 }
232 }
226 .into());
233 .into());
227 }
234 }
228 }
235 }
229 if !current_layer.is_empty() {
236 if !current_layer.is_empty() {
230 layers.push(current_layer);
237 layers.push(current_layer);
231 }
238 }
232 Ok(layers)
239 Ok(layers)
233 }
240 }
234 }
241 }
235
242
236 impl DisplayBytes for ConfigLayer {
243 impl DisplayBytes for ConfigLayer {
237 fn display_bytes(
244 fn display_bytes(
238 &self,
245 &self,
239 out: &mut dyn std::io::Write,
246 out: &mut dyn std::io::Write,
240 ) -> std::io::Result<()> {
247 ) -> std::io::Result<()> {
241 let mut sections: Vec<_> = self.sections.iter().collect();
248 let mut sections: Vec<_> = self.sections.iter().collect();
242 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
249 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
243
250
244 for (section, items) in sections.into_iter() {
251 for (section, items) in sections.into_iter() {
245 let mut items: Vec<_> = items.into_iter().collect();
252 let mut items: Vec<_> = items.into_iter().collect();
246 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
253 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
247
254
248 for (item, config_entry) in items {
255 for (item, config_entry) in items {
249 write_bytes!(
256 write_bytes!(
250 out,
257 out,
251 b"{}.{}={} # {}\n",
258 b"{}.{}={} # {}\n",
252 section,
259 section,
253 item,
260 item,
254 &config_entry.bytes,
261 &config_entry.bytes,
255 &self.origin,
262 &self.origin,
256 )?
263 )?
257 }
264 }
258 }
265 }
259 Ok(())
266 Ok(())
260 }
267 }
261 }
268 }
262
269
263 /// Mapping of section item to value.
270 /// Mapping of section item to value.
264 /// In the following:
271 /// In the following:
265 /// ```text
272 /// ```text
266 /// [ui]
273 /// [ui]
267 /// paginate=no
274 /// paginate=no
268 /// ```
275 /// ```
269 /// "paginate" is the section item and "no" the value.
276 /// "paginate" is the section item and "no" the value.
270 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
277 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
271
278
272 #[derive(Clone, Debug, PartialEq)]
279 #[derive(Clone, Debug, PartialEq)]
273 pub struct ConfigValue {
280 pub struct ConfigValue {
274 /// The raw bytes of the value (be it from the CLI, env or from a file)
281 /// The raw bytes of the value (be it from the CLI, env or from a file)
275 pub bytes: Vec<u8>,
282 pub bytes: Vec<u8>,
276 /// Only present if the value comes from a file, 1-indexed.
283 /// Only present if the value comes from a file, 1-indexed.
277 pub line: Option<usize>,
284 pub line: Option<usize>,
278 }
285 }
279
286
280 #[derive(Clone, Debug)]
287 #[derive(Clone, Debug)]
281 pub enum ConfigOrigin {
288 pub enum ConfigOrigin {
282 /// From a configuration file
289 /// From a configuration file
283 File(PathBuf),
290 File(PathBuf),
284 /// From a `--config` CLI argument
291 /// From a `--config` CLI argument
285 CommandLine,
292 CommandLine,
286 /// From environment variables like `$PAGER` or `$EDITOR`
293 /// From environment variables like `$PAGER` or `$EDITOR`
287 Environment(Vec<u8>),
294 Environment(Vec<u8>),
288 /* TODO cli
295 /* TODO cli
289 * TODO defaults (configitems.py)
296 * TODO defaults (configitems.py)
290 * TODO extensions
297 * TODO extensions
291 * TODO Python resources?
298 * TODO Python resources?
292 * Others? */
299 * Others? */
293 }
300 }
294
301
295 impl DisplayBytes for ConfigOrigin {
302 impl DisplayBytes for ConfigOrigin {
296 fn display_bytes(
303 fn display_bytes(
297 &self,
304 &self,
298 out: &mut dyn std::io::Write,
305 out: &mut dyn std::io::Write,
299 ) -> std::io::Result<()> {
306 ) -> std::io::Result<()> {
300 match self {
307 match self {
301 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
308 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
302 ConfigOrigin::CommandLine => out.write_all(b"--config"),
309 ConfigOrigin::CommandLine => out.write_all(b"--config"),
303 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
310 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
304 }
311 }
305 }
312 }
306 }
313 }
307
314
308 #[derive(Debug)]
315 #[derive(Debug)]
309 pub struct ConfigParseError {
316 pub struct ConfigParseError {
310 pub origin: ConfigOrigin,
317 pub origin: ConfigOrigin,
311 pub line: Option<usize>,
318 pub line: Option<usize>,
312 pub message: Vec<u8>,
319 pub message: Vec<u8>,
313 }
320 }
314
321
315 #[derive(Debug, derive_more::From)]
322 #[derive(Debug, derive_more::From)]
316 pub enum ConfigError {
323 pub enum ConfigError {
317 Parse(ConfigParseError),
324 Parse(ConfigParseError),
318 Other(HgError),
325 Other(HgError),
319 }
326 }
320
327
321 fn make_regex(pattern: &'static str) -> Regex {
328 fn make_regex(pattern: &'static str) -> Regex {
322 Regex::new(pattern).expect("expected a valid regex")
329 Regex::new(pattern).expect("expected a valid regex")
323 }
330 }
@@ -1,630 +1,638 b''
1 extern crate log;
1 extern crate log;
2 use crate::error::CommandError;
2 use crate::error::CommandError;
3 use crate::ui::Ui;
3 use crate::ui::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::exit_codes;
10 use hg::exit_codes;
11 use hg::repo::{Repo, RepoError};
11 use hg::repo::{Repo, RepoError};
12 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
12 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
13 use hg::utils::SliceExt;
13 use hg::utils::SliceExt;
14 use std::ffi::OsString;
14 use std::ffi::OsString;
15 use std::path::PathBuf;
15 use std::path::PathBuf;
16 use std::process::Command;
16 use std::process::Command;
17
17
18 mod blackbox;
18 mod blackbox;
19 mod error;
19 mod error;
20 mod ui;
20 mod ui;
21 pub mod utils {
21 pub mod utils {
22 pub mod path_utils;
22 pub mod path_utils;
23 }
23 }
24
24
25 fn main_with_result(
25 fn main_with_result(
26 process_start_time: &blackbox::ProcessStartTime,
26 process_start_time: &blackbox::ProcessStartTime,
27 ui: &ui::Ui,
27 ui: &ui::Ui,
28 repo: Result<&Repo, &NoRepoInCwdError>,
28 repo: Result<&Repo, &NoRepoInCwdError>,
29 config: &Config,
29 config: &Config,
30 ) -> Result<(), CommandError> {
30 ) -> Result<(), CommandError> {
31 check_unsupported(config)?;
31 check_unsupported(config)?;
32
32
33 let app = App::new("rhg")
33 let app = App::new("rhg")
34 .global_setting(AppSettings::AllowInvalidUtf8)
34 .global_setting(AppSettings::AllowInvalidUtf8)
35 .global_setting(AppSettings::DisableVersion)
35 .global_setting(AppSettings::DisableVersion)
36 .setting(AppSettings::SubcommandRequired)
36 .setting(AppSettings::SubcommandRequired)
37 .setting(AppSettings::VersionlessSubcommands)
37 .setting(AppSettings::VersionlessSubcommands)
38 .arg(
38 .arg(
39 Arg::with_name("repository")
39 Arg::with_name("repository")
40 .help("repository root directory")
40 .help("repository root directory")
41 .short("-R")
41 .short("-R")
42 .long("--repository")
42 .long("--repository")
43 .value_name("REPO")
43 .value_name("REPO")
44 .takes_value(true)
44 .takes_value(true)
45 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
45 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
46 .global(true),
46 .global(true),
47 )
47 )
48 .arg(
48 .arg(
49 Arg::with_name("config")
49 Arg::with_name("config")
50 .help("set/override config option (use 'section.name=value')")
50 .help("set/override config option (use 'section.name=value')")
51 .long("--config")
51 .long("--config")
52 .value_name("CONFIG")
52 .value_name("CONFIG")
53 .takes_value(true)
53 .takes_value(true)
54 .global(true)
54 .global(true)
55 // Ok: `--config section.key1=val --config section.key2=val2`
55 // Ok: `--config section.key1=val --config section.key2=val2`
56 .multiple(true)
56 .multiple(true)
57 // Not ok: `--config section.key1=val section.key2=val2`
57 // Not ok: `--config section.key1=val section.key2=val2`
58 .number_of_values(1),
58 .number_of_values(1),
59 )
59 )
60 .arg(
60 .arg(
61 Arg::with_name("cwd")
61 Arg::with_name("cwd")
62 .help("change working directory")
62 .help("change working directory")
63 .long("--cwd")
63 .long("--cwd")
64 .value_name("DIR")
64 .value_name("DIR")
65 .takes_value(true)
65 .takes_value(true)
66 .global(true),
66 .global(true),
67 )
67 )
68 .version("0.0.1");
68 .version("0.0.1");
69 let app = add_subcommand_args(app);
69 let app = add_subcommand_args(app);
70
70
71 let matches = app.clone().get_matches_safe()?;
71 let matches = app.clone().get_matches_safe()?;
72
72
73 let (subcommand_name, subcommand_matches) = matches.subcommand();
73 let (subcommand_name, subcommand_matches) = matches.subcommand();
74
74
75 // Mercurial allows users to define "defaults" for commands, fallback
75 // Mercurial allows users to define "defaults" for commands, fallback
76 // if a default is detected for the current command
76 // if a default is detected for the current command
77 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
77 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
78 if defaults?.is_some() {
78 if defaults?.is_some() {
79 let msg = "`defaults` config set";
79 let msg = "`defaults` config set";
80 return Err(CommandError::unsupported(msg));
80 return Err(CommandError::unsupported(msg));
81 }
81 }
82
82
83 for prefix in ["pre", "post", "fail"].iter() {
83 for prefix in ["pre", "post", "fail"].iter() {
84 // Mercurial allows users to define generic hooks for commands,
84 // Mercurial allows users to define generic hooks for commands,
85 // fallback if any are detected
85 // fallback if any are detected
86 let item = format!("{}-{}", prefix, subcommand_name);
86 let item = format!("{}-{}", prefix, subcommand_name);
87 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
87 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
88 if hook_for_command.is_some() {
88 if hook_for_command.is_some() {
89 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
89 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
90 return Err(CommandError::unsupported(msg));
90 return Err(CommandError::unsupported(msg));
91 }
91 }
92 }
92 }
93 let run = subcommand_run_fn(subcommand_name)
93 let run = subcommand_run_fn(subcommand_name)
94 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
94 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
95 let subcommand_args = subcommand_matches
95 let subcommand_args = subcommand_matches
96 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
96 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
97
97
98 let invocation = CliInvocation {
98 let invocation = CliInvocation {
99 ui,
99 ui,
100 subcommand_args,
100 subcommand_args,
101 config,
101 config,
102 repo,
102 repo,
103 };
103 };
104
104
105 if let Ok(repo) = repo {
105 if let Ok(repo) = repo {
106 // We don't support subrepos, fallback if the subrepos file is present
106 // We don't support subrepos, fallback if the subrepos file is present
107 if repo.working_directory_vfs().join(".hgsub").exists() {
107 if repo.working_directory_vfs().join(".hgsub").exists() {
108 let msg = "subrepos (.hgsub is present)";
108 let msg = "subrepos (.hgsub is present)";
109 return Err(CommandError::unsupported(msg));
109 return Err(CommandError::unsupported(msg));
110 }
110 }
111 }
111 }
112
112
113 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
113 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
114 blackbox.log_command_start();
114 blackbox.log_command_start();
115 let result = run(&invocation);
115 let result = run(&invocation);
116 blackbox.log_command_end(exit_code(
116 blackbox.log_command_end(exit_code(
117 &result,
117 &result,
118 // TODO: show a warning or combine with original error if `get_bool`
118 // TODO: show a warning or combine with original error if `get_bool`
119 // returns an error
119 // returns an error
120 config
120 config
121 .get_bool(b"ui", b"detailed-exit-code")
121 .get_bool(b"ui", b"detailed-exit-code")
122 .unwrap_or(false),
122 .unwrap_or(false),
123 ));
123 ));
124 result
124 result
125 }
125 }
126
126
127 fn main() {
127 fn main() {
128 // Run this first, before we find out if the blackbox extension is even
128 // Run this first, before we find out if the blackbox extension is even
129 // enabled, in order to include everything in-between in the duration
129 // enabled, in order to include everything in-between in the duration
130 // measurements. Reading config files can be slow if they’re on NFS.
130 // measurements. Reading config files can be slow if they’re on NFS.
131 let process_start_time = blackbox::ProcessStartTime::now();
131 let process_start_time = blackbox::ProcessStartTime::now();
132
132
133 env_logger::init();
133 env_logger::init();
134 let ui = ui::Ui::new();
134 let ui = ui::Ui::new();
135
135
136 let early_args = EarlyArgs::parse(std::env::args_os());
136 let early_args = EarlyArgs::parse(std::env::args_os());
137
137
138 let initial_current_dir = early_args.cwd.map(|cwd| {
138 let initial_current_dir = early_args.cwd.map(|cwd| {
139 let cwd = get_path_from_bytes(&cwd);
139 let cwd = get_path_from_bytes(&cwd);
140 std::env::current_dir()
140 std::env::current_dir()
141 .and_then(|initial| {
141 .and_then(|initial| {
142 std::env::set_current_dir(cwd)?;
142 std::env::set_current_dir(cwd)?;
143 Ok(initial)
143 Ok(initial)
144 })
144 })
145 .unwrap_or_else(|error| {
145 .unwrap_or_else(|error| {
146 exit(
146 exit(
147 &None,
147 &None,
148 &ui,
148 &ui,
149 OnUnsupported::Abort,
149 OnUnsupported::Abort,
150 Err(CommandError::abort(format!(
150 Err(CommandError::abort(format!(
151 "abort: {}: '{}'",
151 "abort: {}: '{}'",
152 error,
152 error,
153 cwd.display()
153 cwd.display()
154 ))),
154 ))),
155 false,
155 false,
156 )
156 )
157 })
157 })
158 });
158 });
159
159
160 let mut non_repo_config =
160 let mut non_repo_config =
161 Config::load_non_repo().unwrap_or_else(|error| {
161 Config::load_non_repo().unwrap_or_else(|error| {
162 // Normally this is decided based on config, but we don’t have that
162 // Normally this is decided based on config, but we don’t have that
163 // available. As of this writing config loading never returns an
163 // available. As of this writing config loading never returns an
164 // "unsupported" error but that is not enforced by the type system.
164 // "unsupported" error but that is not enforced by the type system.
165 let on_unsupported = OnUnsupported::Abort;
165 let on_unsupported = OnUnsupported::Abort;
166
166
167 exit(
167 exit(
168 &initial_current_dir,
168 &initial_current_dir,
169 &ui,
169 &ui,
170 on_unsupported,
170 on_unsupported,
171 Err(error.into()),
171 Err(error.into()),
172 false,
172 false,
173 )
173 )
174 });
174 });
175
175
176 non_repo_config
176 non_repo_config
177 .load_cli_args_config(early_args.config)
177 .load_cli_args_config(early_args.config)
178 .unwrap_or_else(|error| {
178 .unwrap_or_else(|error| {
179 exit(
179 exit(
180 &initial_current_dir,
180 &initial_current_dir,
181 &ui,
181 &ui,
182 OnUnsupported::from_config(&ui, &non_repo_config),
182 OnUnsupported::from_config(&ui, &non_repo_config),
183 Err(error.into()),
183 Err(error.into()),
184 non_repo_config
184 non_repo_config
185 .get_bool(b"ui", b"detailed-exit-code")
185 .get_bool(b"ui", b"detailed-exit-code")
186 .unwrap_or(false),
186 .unwrap_or(false),
187 )
187 )
188 });
188 });
189
189
190 if let Some(repo_path_bytes) = &early_args.repo {
190 if let Some(repo_path_bytes) = &early_args.repo {
191 lazy_static::lazy_static! {
191 lazy_static::lazy_static! {
192 static ref SCHEME_RE: regex::bytes::Regex =
192 static ref SCHEME_RE: regex::bytes::Regex =
193 // Same as `_matchscheme` in `mercurial/util.py`
193 // Same as `_matchscheme` in `mercurial/util.py`
194 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
194 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
195 }
195 }
196 if SCHEME_RE.is_match(&repo_path_bytes) {
196 if SCHEME_RE.is_match(&repo_path_bytes) {
197 exit(
197 exit(
198 &initial_current_dir,
198 &initial_current_dir,
199 &ui,
199 &ui,
200 OnUnsupported::from_config(&ui, &non_repo_config),
200 OnUnsupported::from_config(&ui, &non_repo_config),
201 Err(CommandError::UnsupportedFeature {
201 Err(CommandError::UnsupportedFeature {
202 message: format_bytes!(
202 message: format_bytes!(
203 b"URL-like --repository {}",
203 b"URL-like --repository {}",
204 repo_path_bytes
204 repo_path_bytes
205 ),
205 ),
206 }),
206 }),
207 // TODO: show a warning or combine with original error if
207 // TODO: show a warning or combine with original error if
208 // `get_bool` returns an error
208 // `get_bool` returns an error
209 non_repo_config
209 non_repo_config
210 .get_bool(b"ui", b"detailed-exit-code")
210 .get_bool(b"ui", b"detailed-exit-code")
211 .unwrap_or(false),
211 .unwrap_or(false),
212 )
212 )
213 }
213 }
214 }
214 }
215 let repo_arg = early_args.repo.unwrap_or(Vec::new());
215 let repo_arg = early_args.repo.unwrap_or(Vec::new());
216 let repo_path: Option<PathBuf> = {
216 let repo_path: Option<PathBuf> = {
217 if repo_arg.is_empty() {
217 if repo_arg.is_empty() {
218 None
218 None
219 } else {
219 } else {
220 let local_config = {
220 let local_config = {
221 if std::env::var_os("HGRCSKIPREPO").is_none() {
221 if std::env::var_os("HGRCSKIPREPO").is_none() {
222 // TODO: handle errors from find_repo_root
222 // TODO: handle errors from find_repo_root
223 if let Ok(current_dir_path) = Repo::find_repo_root() {
223 if let Ok(current_dir_path) = Repo::find_repo_root() {
224 let config_files = vec![
224 let config_files = vec![
225 ConfigSource::AbsPath(
225 ConfigSource::AbsPath(
226 current_dir_path.join(".hg/hgrc"),
226 current_dir_path.join(".hg/hgrc"),
227 ),
227 ),
228 ConfigSource::AbsPath(
228 ConfigSource::AbsPath(
229 current_dir_path.join(".hg/hgrc-not-shared"),
229 current_dir_path.join(".hg/hgrc-not-shared"),
230 ),
230 ),
231 ];
231 ];
232 // TODO: handle errors from
232 // TODO: handle errors from
233 // `load_from_explicit_sources`
233 // `load_from_explicit_sources`
234 Config::load_from_explicit_sources(config_files).ok()
234 Config::load_from_explicit_sources(config_files).ok()
235 } else {
235 } else {
236 None
236 None
237 }
237 }
238 } else {
238 } else {
239 None
239 None
240 }
240 }
241 };
241 };
242
242
243 let non_repo_config_val = {
243 let non_repo_config_val = {
244 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
244 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
245 match &non_repo_val {
245 match &non_repo_val {
246 Some(val) if val.len() > 0 => home::home_dir()
246 Some(val) if val.len() > 0 => home::home_dir()
247 .unwrap_or_else(|| PathBuf::from("~"))
247 .unwrap_or_else(|| PathBuf::from("~"))
248 .join(get_path_from_bytes(val))
248 .join(get_path_from_bytes(val))
249 .canonicalize()
249 .canonicalize()
250 // TODO: handle error and make it similar to python
250 // TODO: handle error and make it similar to python
251 // implementation maybe?
251 // implementation maybe?
252 .ok(),
252 .ok(),
253 _ => None,
253 _ => None,
254 }
254 }
255 };
255 };
256
256
257 let config_val = match &local_config {
257 let config_val = match &local_config {
258 None => non_repo_config_val,
258 None => non_repo_config_val,
259 Some(val) => {
259 Some(val) => {
260 let local_config_val = val.get(b"paths", &repo_arg);
260 let local_config_val = val.get(b"paths", &repo_arg);
261 match &local_config_val {
261 match &local_config_val {
262 Some(val) if val.len() > 0 => {
262 Some(val) if val.len() > 0 => {
263 // presence of a local_config assures that
263 // presence of a local_config assures that
264 // current_dir
264 // current_dir
265 // wont result in an Error
265 // wont result in an Error
266 let canpath = hg::utils::current_dir()
266 let canpath = hg::utils::current_dir()
267 .unwrap()
267 .unwrap()
268 .join(get_path_from_bytes(val))
268 .join(get_path_from_bytes(val))
269 .canonicalize();
269 .canonicalize();
270 canpath.ok().or(non_repo_config_val)
270 canpath.ok().or(non_repo_config_val)
271 }
271 }
272 _ => non_repo_config_val,
272 _ => non_repo_config_val,
273 }
273 }
274 }
274 }
275 };
275 };
276 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
276 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
277 }
277 }
278 };
278 };
279
279
280 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
280 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
281 {
281 {
282 Ok(repo) => Ok(repo),
282 Ok(repo) => Ok(repo),
283 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
283 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
284 // Not finding a repo is not fatal yet, if `-R` was not given
284 // Not finding a repo is not fatal yet, if `-R` was not given
285 Err(NoRepoInCwdError { cwd: at })
285 Err(NoRepoInCwdError { cwd: at })
286 }
286 }
287 Err(error) => exit(
287 Err(error) => exit(
288 &initial_current_dir,
288 &initial_current_dir,
289 &ui,
289 &ui,
290 OnUnsupported::from_config(&ui, &non_repo_config),
290 OnUnsupported::from_config(&ui, &non_repo_config),
291 Err(error.into()),
291 Err(error.into()),
292 // TODO: show a warning or combine with original error if
292 // TODO: show a warning or combine with original error if
293 // `get_bool` returns an error
293 // `get_bool` returns an error
294 non_repo_config
294 non_repo_config
295 .get_bool(b"ui", b"detailed-exit-code")
295 .get_bool(b"ui", b"detailed-exit-code")
296 .unwrap_or(false),
296 .unwrap_or(false),
297 ),
297 ),
298 };
298 };
299
299
300 let config = if let Ok(repo) = &repo_result {
300 let config = if let Ok(repo) = &repo_result {
301 repo.config()
301 repo.config()
302 } else {
302 } else {
303 &non_repo_config
303 &non_repo_config
304 };
304 };
305 let on_unsupported = OnUnsupported::from_config(&ui, config);
305 let on_unsupported = OnUnsupported::from_config(&ui, config);
306
306
307 let result = main_with_result(
307 let result = main_with_result(
308 &process_start_time,
308 &process_start_time,
309 &ui,
309 &ui,
310 repo_result.as_ref(),
310 repo_result.as_ref(),
311 config,
311 config,
312 );
312 );
313 exit(
313 exit(
314 &initial_current_dir,
314 &initial_current_dir,
315 &ui,
315 &ui,
316 on_unsupported,
316 on_unsupported,
317 result,
317 result,
318 // TODO: show a warning or combine with original error if `get_bool`
318 // TODO: show a warning or combine with original error if `get_bool`
319 // returns an error
319 // returns an error
320 config
320 config
321 .get_bool(b"ui", b"detailed-exit-code")
321 .get_bool(b"ui", b"detailed-exit-code")
322 .unwrap_or(false),
322 .unwrap_or(false),
323 )
323 )
324 }
324 }
325
325
326 fn exit_code(
326 fn exit_code(
327 result: &Result<(), CommandError>,
327 result: &Result<(), CommandError>,
328 use_detailed_exit_code: bool,
328 use_detailed_exit_code: bool,
329 ) -> i32 {
329 ) -> i32 {
330 match result {
330 match result {
331 Ok(()) => exit_codes::OK,
331 Ok(()) => exit_codes::OK,
332 Err(CommandError::Abort {
332 Err(CommandError::Abort {
333 message: _,
333 message: _,
334 detailed_exit_code,
334 detailed_exit_code,
335 }) => {
335 }) => {
336 if use_detailed_exit_code {
336 if use_detailed_exit_code {
337 *detailed_exit_code
337 *detailed_exit_code
338 } else {
338 } else {
339 exit_codes::ABORT
339 exit_codes::ABORT
340 }
340 }
341 }
341 }
342 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
342 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
343
343
344 // Exit with a specific code and no error message to let a potential
344 // Exit with a specific code and no error message to let a potential
345 // wrapper script fallback to Python-based Mercurial.
345 // wrapper script fallback to Python-based Mercurial.
346 Err(CommandError::UnsupportedFeature { .. }) => {
346 Err(CommandError::UnsupportedFeature { .. }) => {
347 exit_codes::UNIMPLEMENTED
347 exit_codes::UNIMPLEMENTED
348 }
348 }
349 }
349 }
350 }
350 }
351
351
352 fn exit(
352 fn exit(
353 initial_current_dir: &Option<PathBuf>,
353 initial_current_dir: &Option<PathBuf>,
354 ui: &Ui,
354 ui: &Ui,
355 mut on_unsupported: OnUnsupported,
355 mut on_unsupported: OnUnsupported,
356 result: Result<(), CommandError>,
356 result: Result<(), CommandError>,
357 use_detailed_exit_code: bool,
357 use_detailed_exit_code: bool,
358 ) -> ! {
358 ) -> ! {
359 if let (
359 if let (
360 OnUnsupported::Fallback { executable },
360 OnUnsupported::Fallback { executable },
361 Err(CommandError::UnsupportedFeature { .. }),
361 Err(CommandError::UnsupportedFeature { .. }),
362 ) = (&on_unsupported, &result)
362 ) = (&on_unsupported, &result)
363 {
363 {
364 let mut args = std::env::args_os();
364 let mut args = std::env::args_os();
365 let executable_path = get_path_from_bytes(&executable);
365 let executable_path = get_path_from_bytes(&executable);
366 let this_executable = args.next().expect("exepcted argv[0] to exist");
366 let this_executable = args.next().expect("exepcted argv[0] to exist");
367 if executable_path == &PathBuf::from(this_executable) {
367 if executable_path == &PathBuf::from(this_executable) {
368 // Avoid spawning infinitely many processes until resource
368 // Avoid spawning infinitely many processes until resource
369 // exhaustion.
369 // exhaustion.
370 let _ = ui.write_stderr(&format_bytes!(
370 let _ = ui.write_stderr(&format_bytes!(
371 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
371 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
372 points to `rhg` itself.\n",
372 points to `rhg` itself.\n",
373 executable
373 executable
374 ));
374 ));
375 on_unsupported = OnUnsupported::Abort
375 on_unsupported = OnUnsupported::Abort
376 } else {
376 } else {
377 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
377 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
378 let mut command = Command::new(executable_path);
378 let mut command = Command::new(executable_path);
379 command.args(args);
379 command.args(args);
380 if let Some(initial) = initial_current_dir {
380 if let Some(initial) = initial_current_dir {
381 command.current_dir(initial);
381 command.current_dir(initial);
382 }
382 }
383 let result = command.status();
383 let result = command.status();
384 match result {
384 match result {
385 Ok(status) => std::process::exit(
385 Ok(status) => std::process::exit(
386 status.code().unwrap_or(exit_codes::ABORT),
386 status.code().unwrap_or(exit_codes::ABORT),
387 ),
387 ),
388 Err(error) => {
388 Err(error) => {
389 let _ = ui.write_stderr(&format_bytes!(
389 let _ = ui.write_stderr(&format_bytes!(
390 b"tried to fall back to a '{}' sub-process but got error {}\n",
390 b"tried to fall back to a '{}' sub-process but got error {}\n",
391 executable, format_bytes::Utf8(error)
391 executable, format_bytes::Utf8(error)
392 ));
392 ));
393 on_unsupported = OnUnsupported::Abort
393 on_unsupported = OnUnsupported::Abort
394 }
394 }
395 }
395 }
396 }
396 }
397 }
397 }
398 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
398 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
399 }
399 }
400
400
401 fn exit_no_fallback(
401 fn exit_no_fallback(
402 ui: &Ui,
402 ui: &Ui,
403 on_unsupported: OnUnsupported,
403 on_unsupported: OnUnsupported,
404 result: Result<(), CommandError>,
404 result: Result<(), CommandError>,
405 use_detailed_exit_code: bool,
405 use_detailed_exit_code: bool,
406 ) -> ! {
406 ) -> ! {
407 match &result {
407 match &result {
408 Ok(_) => {}
408 Ok(_) => {}
409 Err(CommandError::Unsuccessful) => {}
409 Err(CommandError::Unsuccessful) => {}
410 Err(CommandError::Abort {
410 Err(CommandError::Abort {
411 message,
411 message,
412 detailed_exit_code: _,
412 detailed_exit_code: _,
413 }) => {
413 }) => {
414 if !message.is_empty() {
414 if !message.is_empty() {
415 // Ignore errors when writing to stderr, we’re already exiting
415 // Ignore errors when writing to stderr, we’re already exiting
416 // with failure code so there’s not much more we can do.
416 // with failure code so there’s not much more we can do.
417 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
417 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
418 }
418 }
419 }
419 }
420 Err(CommandError::UnsupportedFeature { message }) => {
420 Err(CommandError::UnsupportedFeature { message }) => {
421 match on_unsupported {
421 match on_unsupported {
422 OnUnsupported::Abort => {
422 OnUnsupported::Abort => {
423 let _ = ui.write_stderr(&format_bytes!(
423 let _ = ui.write_stderr(&format_bytes!(
424 b"unsupported feature: {}\n",
424 b"unsupported feature: {}\n",
425 message
425 message
426 ));
426 ));
427 }
427 }
428 OnUnsupported::AbortSilent => {}
428 OnUnsupported::AbortSilent => {}
429 OnUnsupported::Fallback { .. } => unreachable!(),
429 OnUnsupported::Fallback { .. } => unreachable!(),
430 }
430 }
431 }
431 }
432 }
432 }
433 std::process::exit(exit_code(&result, use_detailed_exit_code))
433 std::process::exit(exit_code(&result, use_detailed_exit_code))
434 }
434 }
435
435
436 macro_rules! subcommands {
436 macro_rules! subcommands {
437 ($( $command: ident )+) => {
437 ($( $command: ident )+) => {
438 mod commands {
438 mod commands {
439 $(
439 $(
440 pub mod $command;
440 pub mod $command;
441 )+
441 )+
442 }
442 }
443
443
444 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
444 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
445 app
445 app
446 $(
446 $(
447 .subcommand(commands::$command::args())
447 .subcommand(commands::$command::args())
448 )+
448 )+
449 }
449 }
450
450
451 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
451 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
452
452
453 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
453 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
454 match name {
454 match name {
455 $(
455 $(
456 stringify!($command) => Some(commands::$command::run),
456 stringify!($command) => Some(commands::$command::run),
457 )+
457 )+
458 _ => None,
458 _ => None,
459 }
459 }
460 }
460 }
461 };
461 };
462 }
462 }
463
463
464 subcommands! {
464 subcommands! {
465 cat
465 cat
466 debugdata
466 debugdata
467 debugrequirements
467 debugrequirements
468 files
468 files
469 root
469 root
470 config
470 config
471 status
471 status
472 }
472 }
473
473
474 pub struct CliInvocation<'a> {
474 pub struct CliInvocation<'a> {
475 ui: &'a Ui,
475 ui: &'a Ui,
476 subcommand_args: &'a ArgMatches<'a>,
476 subcommand_args: &'a ArgMatches<'a>,
477 config: &'a Config,
477 config: &'a Config,
478 /// References inside `Result` is a bit peculiar but allow
478 /// References inside `Result` is a bit peculiar but allow
479 /// `invocation.repo?` to work out with `&CliInvocation` since this
479 /// `invocation.repo?` to work out with `&CliInvocation` since this
480 /// `Result` type is `Copy`.
480 /// `Result` type is `Copy`.
481 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
481 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
482 }
482 }
483
483
484 struct NoRepoInCwdError {
484 struct NoRepoInCwdError {
485 cwd: PathBuf,
485 cwd: PathBuf,
486 }
486 }
487
487
488 /// CLI arguments to be parsed "early" in order to be able to read
488 /// CLI arguments to be parsed "early" in order to be able to read
489 /// configuration before using Clap. Ideally we would also use Clap for this,
489 /// configuration before using Clap. Ideally we would also use Clap for this,
490 /// see <https://github.com/clap-rs/clap/discussions/2366>.
490 /// see <https://github.com/clap-rs/clap/discussions/2366>.
491 ///
491 ///
492 /// These arguments are still declared when we do use Clap later, so that Clap
492 /// These arguments are still declared when we do use Clap later, so that Clap
493 /// does not return an error for their presence.
493 /// does not return an error for their presence.
494 struct EarlyArgs {
494 struct EarlyArgs {
495 /// Values of all `--config` arguments. (Possibly none)
495 /// Values of all `--config` arguments. (Possibly none)
496 config: Vec<Vec<u8>>,
496 config: Vec<Vec<u8>>,
497 /// Value of the `-R` or `--repository` argument, if any.
497 /// Value of the `-R` or `--repository` argument, if any.
498 repo: Option<Vec<u8>>,
498 repo: Option<Vec<u8>>,
499 /// Value of the `--cwd` argument, if any.
499 /// Value of the `--cwd` argument, if any.
500 cwd: Option<Vec<u8>>,
500 cwd: Option<Vec<u8>>,
501 }
501 }
502
502
503 impl EarlyArgs {
503 impl EarlyArgs {
504 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
504 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
505 let mut args = args.into_iter().map(get_bytes_from_os_str);
505 let mut args = args.into_iter().map(get_bytes_from_os_str);
506 let mut config = Vec::new();
506 let mut config = Vec::new();
507 let mut repo = None;
507 let mut repo = None;
508 let mut cwd = None;
508 let mut cwd = None;
509 // Use `while let` instead of `for` so that we can also call
509 // Use `while let` instead of `for` so that we can also call
510 // `args.next()` inside the loop.
510 // `args.next()` inside the loop.
511 while let Some(arg) = args.next() {
511 while let Some(arg) = args.next() {
512 if arg == b"--config" {
512 if arg == b"--config" {
513 if let Some(value) = args.next() {
513 if let Some(value) = args.next() {
514 config.push(value)
514 config.push(value)
515 }
515 }
516 } else if let Some(value) = arg.drop_prefix(b"--config=") {
516 } else if let Some(value) = arg.drop_prefix(b"--config=") {
517 config.push(value.to_owned())
517 config.push(value.to_owned())
518 }
518 }
519
519
520 if arg == b"--cwd" {
520 if arg == b"--cwd" {
521 if let Some(value) = args.next() {
521 if let Some(value) = args.next() {
522 cwd = Some(value)
522 cwd = Some(value)
523 }
523 }
524 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
524 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
525 cwd = Some(value.to_owned())
525 cwd = Some(value.to_owned())
526 }
526 }
527
527
528 if arg == b"--repository" || arg == b"-R" {
528 if arg == b"--repository" || arg == b"-R" {
529 if let Some(value) = args.next() {
529 if let Some(value) = args.next() {
530 repo = Some(value)
530 repo = Some(value)
531 }
531 }
532 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
532 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
533 repo = Some(value.to_owned())
533 repo = Some(value.to_owned())
534 } else if let Some(value) = arg.drop_prefix(b"-R") {
534 } else if let Some(value) = arg.drop_prefix(b"-R") {
535 repo = Some(value.to_owned())
535 repo = Some(value.to_owned())
536 }
536 }
537 }
537 }
538 Self { config, repo, cwd }
538 Self { config, repo, cwd }
539 }
539 }
540 }
540 }
541
541
542 /// What to do when encountering some unsupported feature.
542 /// What to do when encountering some unsupported feature.
543 ///
543 ///
544 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
544 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
545 enum OnUnsupported {
545 enum OnUnsupported {
546 /// Print an error message describing what feature is not supported,
546 /// Print an error message describing what feature is not supported,
547 /// and exit with code 252.
547 /// and exit with code 252.
548 Abort,
548 Abort,
549 /// Silently exit with code 252.
549 /// Silently exit with code 252.
550 AbortSilent,
550 AbortSilent,
551 /// Try running a Python implementation
551 /// Try running a Python implementation
552 Fallback { executable: Vec<u8> },
552 Fallback { executable: Vec<u8> },
553 }
553 }
554
554
555 impl OnUnsupported {
555 impl OnUnsupported {
556 const DEFAULT: Self = OnUnsupported::Abort;
556 const DEFAULT: Self = OnUnsupported::Abort;
557
557
558 fn from_config(ui: &Ui, config: &Config) -> Self {
558 fn from_config(ui: &Ui, config: &Config) -> Self {
559 match config
559 match config
560 .get(b"rhg", b"on-unsupported")
560 .get(b"rhg", b"on-unsupported")
561 .map(|value| value.to_ascii_lowercase())
561 .map(|value| value.to_ascii_lowercase())
562 .as_deref()
562 .as_deref()
563 {
563 {
564 Some(b"abort") => OnUnsupported::Abort,
564 Some(b"abort") => OnUnsupported::Abort,
565 Some(b"abort-silent") => OnUnsupported::AbortSilent,
565 Some(b"abort-silent") => OnUnsupported::AbortSilent,
566 Some(b"fallback") => OnUnsupported::Fallback {
566 Some(b"fallback") => OnUnsupported::Fallback {
567 executable: config
567 executable: config
568 .get(b"rhg", b"fallback-executable")
568 .get(b"rhg", b"fallback-executable")
569 .unwrap_or_else(|| {
569 .unwrap_or_else(|| {
570 exit_no_fallback(
570 exit_no_fallback(
571 ui,
571 ui,
572 Self::Abort,
572 Self::Abort,
573 Err(CommandError::abort(
573 Err(CommandError::abort(
574 "abort: 'rhg.on-unsupported=fallback' without \
574 "abort: 'rhg.on-unsupported=fallback' without \
575 'rhg.fallback-executable' set."
575 'rhg.fallback-executable' set."
576 )),
576 )),
577 false,
577 false,
578 )
578 )
579 })
579 })
580 .to_owned(),
580 .to_owned(),
581 },
581 },
582 None => Self::DEFAULT,
582 None => Self::DEFAULT,
583 Some(_) => {
583 Some(_) => {
584 // TODO: warn about unknown config value
584 // TODO: warn about unknown config value
585 Self::DEFAULT
585 Self::DEFAULT
586 }
586 }
587 }
587 }
588 }
588 }
589 }
589 }
590
590
591 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"];
591 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"];
592
592
593 fn check_extensions(config: &Config) -> Result<(), CommandError> {
593 fn check_extensions(config: &Config) -> Result<(), CommandError> {
594 let enabled = config.get_section_keys(b"extensions");
594 let enabled = config.get_section_keys(b"extensions");
595
595
596 let mut unsupported = enabled;
596 let mut unsupported = enabled;
597 for supported in SUPPORTED_EXTENSIONS {
597 for supported in SUPPORTED_EXTENSIONS {
598 unsupported.remove(supported);
598 unsupported.remove(supported);
599 }
599 }
600
600
601 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
601 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
602 {
602 {
603 for ignored in ignored_list {
603 for ignored in ignored_list {
604 unsupported.remove(ignored.as_slice());
604 unsupported.remove(ignored.as_slice());
605 }
605 }
606 }
606 }
607
607
608 if unsupported.is_empty() {
608 if unsupported.is_empty() {
609 Ok(())
609 Ok(())
610 } else {
610 } else {
611 Err(CommandError::UnsupportedFeature {
611 Err(CommandError::UnsupportedFeature {
612 message: format_bytes!(
612 message: format_bytes!(
613 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
613 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
614 join(unsupported, b", ")
614 join(unsupported, b", ")
615 ),
615 ),
616 })
616 })
617 }
617 }
618 }
618 }
619
619
620 fn check_unsupported(config: &Config) -> Result<(), CommandError> {
620 fn check_unsupported(config: &Config) -> Result<(), CommandError> {
621 check_extensions(config)?;
621 check_extensions(config)?;
622
622
623 if std::env::var_os("HG_PENDING").is_some() {
623 if std::env::var_os("HG_PENDING").is_some() {
624 // TODO: only if the value is `== repo.working_directory`?
624 // TODO: only if the value is `== repo.working_directory`?
625 // What about relative v.s. absolute paths?
625 // What about relative v.s. absolute paths?
626 Err(CommandError::unsupported("$HG_PENDING"))?
626 Err(CommandError::unsupported("$HG_PENDING"))?
627 }
627 }
628
628
629 if config.has_non_empty_section(b"encode") {
630 Err(CommandError::unsupported("[encode] config"))?
631 }
632
633 if config.has_non_empty_section(b"decode") {
634 Err(CommandError::unsupported("[decode] config"))?
635 }
636
629 Ok(())
637 Ok(())
630 }
638 }
@@ -1,76 +1,72 b''
1 TODO: fix rhg bugs that make this test fail when status is enabled
2 $ unset RHG_STATUS
3
4
5 Test encode/decode filters
1 Test encode/decode filters
6
2
7 $ hg init
3 $ hg init
8 $ cat > .hg/hgrc <<EOF
4 $ cat > .hg/hgrc <<EOF
9 > [encode]
5 > [encode]
10 > not.gz = tr [:lower:] [:upper:]
6 > not.gz = tr [:lower:] [:upper:]
11 > *.gz = gzip -d
7 > *.gz = gzip -d
12 > [decode]
8 > [decode]
13 > not.gz = tr [:upper:] [:lower:]
9 > not.gz = tr [:upper:] [:lower:]
14 > *.gz = gzip
10 > *.gz = gzip
15 > EOF
11 > EOF
16 $ echo "this is a test" | gzip > a.gz
12 $ echo "this is a test" | gzip > a.gz
17 $ echo "this is a test" > not.gz
13 $ echo "this is a test" > not.gz
18 $ hg add *
14 $ hg add *
19 $ hg ci -m "test"
15 $ hg ci -m "test"
20
16
21 no changes
17 no changes
22
18
23 $ hg status
19 $ hg status
24 $ touch *
20 $ touch *
25
21
26 no changes
22 no changes
27
23
28 $ hg status
24 $ hg status
29
25
30 check contents in repo are encoded
26 check contents in repo are encoded
31
27
32 $ hg debugdata a.gz 0
28 $ hg debugdata a.gz 0
33 this is a test
29 this is a test
34 $ hg debugdata not.gz 0
30 $ hg debugdata not.gz 0
35 THIS IS A TEST
31 THIS IS A TEST
36
32
37 check committed content was decoded
33 check committed content was decoded
38
34
39 $ gunzip < a.gz
35 $ gunzip < a.gz
40 this is a test
36 this is a test
41 $ cat not.gz
37 $ cat not.gz
42 this is a test
38 this is a test
43 $ rm *
39 $ rm *
44 $ hg co -C
40 $ hg co -C
45 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
41 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
46
42
47 check decoding of our new working dir copy
43 check decoding of our new working dir copy
48
44
49 $ gunzip < a.gz
45 $ gunzip < a.gz
50 this is a test
46 this is a test
51 $ cat not.gz
47 $ cat not.gz
52 this is a test
48 this is a test
53
49
54 check hg cat operation
50 check hg cat operation
55
51
56 $ hg cat a.gz
52 $ hg cat a.gz
57 this is a test
53 this is a test
58 $ hg cat --decode a.gz | gunzip
54 $ hg cat --decode a.gz | gunzip
59 this is a test
55 this is a test
60 $ mkdir subdir
56 $ mkdir subdir
61 $ cd subdir
57 $ cd subdir
62 $ hg -R .. cat ../a.gz
58 $ hg -R .. cat ../a.gz
63 this is a test
59 this is a test
64 $ hg -R .. cat --decode ../a.gz | gunzip
60 $ hg -R .. cat --decode ../a.gz | gunzip
65 this is a test
61 this is a test
66 $ cd ..
62 $ cd ..
67
63
68 check tempfile filter
64 check tempfile filter
69
65
70 $ hg cat a.gz --decode --config 'decode.*.gz=tempfile:gzip -c INFILE > OUTFILE' | gunzip
66 $ hg cat a.gz --decode --config 'decode.*.gz=tempfile:gzip -c INFILE > OUTFILE' | gunzip
71 this is a test
67 this is a test
72 $ hg cat a.gz --decode --config 'decode.*.gz=tempfile:sh -c "exit 1"'
68 $ hg cat a.gz --decode --config 'decode.*.gz=tempfile:sh -c "exit 1"'
73 abort: command '*' failed: exited with status 1 (glob)
69 abort: command '*' failed: exited with status 1 (glob)
74 [255]
70 [255]
75
71
76 $ cd ..
72 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now