##// END OF EJS Templates
rhg: Add support for --config CLI arguments...
Simon Sapin -
r47232:87fd7f47 default draft
parent child Browse files
Show More
@@ -1,341 +1,344 b''
1 1 // config.rs
2 2 //
3 3 // Copyright 2020
4 4 // Valentin Gatien-Baron,
5 5 // Raphaël Gomès <rgomes@octobus.net>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 use super::layer;
11 11 use crate::config::layer::{
12 12 ConfigError, ConfigLayer, ConfigParseError, ConfigValue,
13 13 };
14 14 use crate::utils::files::get_bytes_from_path;
15 15 use format_bytes::{write_bytes, DisplayBytes};
16 16 use std::env;
17 17 use std::path::{Path, PathBuf};
18 18
19 19 use crate::errors::{HgResultExt, IoResultExt};
20 20
21 21 /// Holds the config values for the current repository
22 22 /// TODO update this docstring once we support more sources
23 23 pub struct Config {
24 24 layers: Vec<layer::ConfigLayer>,
25 25 }
26 26
27 27 impl DisplayBytes for Config {
28 28 fn display_bytes(
29 29 &self,
30 30 out: &mut dyn std::io::Write,
31 31 ) -> std::io::Result<()> {
32 32 for (index, layer) in self.layers.iter().rev().enumerate() {
33 33 write_bytes!(
34 34 out,
35 35 b"==== Layer {} (trusted: {}) ====\n{}",
36 36 index,
37 37 if layer.trusted {
38 38 &b"yes"[..]
39 39 } else {
40 40 &b"no"[..]
41 41 },
42 42 layer
43 43 )?;
44 44 }
45 45 Ok(())
46 46 }
47 47 }
48 48
49 49 pub enum ConfigSource {
50 50 /// Absolute path to a config file
51 51 AbsPath(PathBuf),
52 52 /// Already parsed (from the CLI, env, Python resources, etc.)
53 53 Parsed(layer::ConfigLayer),
54 54 }
55 55
56 56 pub fn parse_bool(v: &[u8]) -> Option<bool> {
57 57 match v.to_ascii_lowercase().as_slice() {
58 58 b"1" | b"yes" | b"true" | b"on" | b"always" => Some(true),
59 59 b"0" | b"no" | b"false" | b"off" | b"never" => Some(false),
60 60 _ => None,
61 61 }
62 62 }
63 63
64 64 impl Config {
65 65 /// Load system and user configuration from various files.
66 66 ///
67 67 /// This is also affected by some environment variables.
68 ///
69 /// TODO: add a parameter for `--config` CLI arguments
70 pub fn load() -> Result<Self, ConfigError> {
68 pub fn load(
69 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
70 ) -> Result<Self, ConfigError> {
71 71 let mut config = Self { layers: Vec::new() };
72 72 let opt_rc_path = env::var_os("HGRCPATH");
73 73 // HGRCPATH replaces system config
74 74 if opt_rc_path.is_none() {
75 75 config.add_system_config()?
76 76 }
77 77 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
78 78 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
79 79 config.add_for_environment_variable("PAGER", b"pager", b"pager");
80 80 // HGRCPATH replaces user config
81 81 if opt_rc_path.is_none() {
82 82 config.add_user_config()?
83 83 }
84 84 if let Some(rc_path) = &opt_rc_path {
85 85 for path in env::split_paths(rc_path) {
86 86 if !path.as_os_str().is_empty() {
87 87 if path.is_dir() {
88 88 config.add_trusted_dir(&path)?
89 89 } else {
90 90 config.add_trusted_file(&path)?
91 91 }
92 92 }
93 93 }
94 94 }
95 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
96 config.layers.push(layer)
97 }
95 98 Ok(config)
96 99 }
97 100
98 101 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
99 102 if let Some(entries) = std::fs::read_dir(path)
100 103 .for_file(path)
101 104 .io_not_found_as_none()?
102 105 {
103 106 for entry in entries {
104 107 let file_path = entry.for_file(path)?.path();
105 108 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
106 109 self.add_trusted_file(&file_path)?
107 110 }
108 111 }
109 112 }
110 113 Ok(())
111 114 }
112 115
113 116 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
114 117 if let Some(data) =
115 118 std::fs::read(path).for_file(path).io_not_found_as_none()?
116 119 {
117 120 self.layers.extend(ConfigLayer::parse(path, &data)?)
118 121 }
119 122 Ok(())
120 123 }
121 124
122 125 fn add_for_environment_variable(
123 126 &mut self,
124 127 var: &str,
125 128 section: &[u8],
126 129 key: &[u8],
127 130 ) {
128 131 if let Some(value) = env::var_os(var) {
129 132 let origin = layer::ConfigOrigin::Environment(var.into());
130 133 let mut layer = ConfigLayer::new(origin);
131 134 layer.add(
132 135 section.to_owned(),
133 136 key.to_owned(),
134 137 // `value` is not a path but this works for any `OsStr`:
135 138 get_bytes_from_path(value),
136 139 None,
137 140 );
138 141 self.layers.push(layer)
139 142 }
140 143 }
141 144
142 145 #[cfg(unix)] // TODO: other platforms
143 146 fn add_system_config(&mut self) -> Result<(), ConfigError> {
144 147 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
145 148 let etc = prefix.join("etc").join("mercurial");
146 149 self.add_trusted_file(&etc.join("hgrc"))?;
147 150 self.add_trusted_dir(&etc.join("hgrc.d"))
148 151 };
149 152 let root = Path::new("/");
150 153 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
151 154 // instead? TODO: can this be a relative path?
152 155 let hg = crate::utils::current_exe()?;
153 156 // TODO: this order (per-installation then per-system) matches
154 157 // `systemrcpath()` in `mercurial/scmposix.py`, but
155 158 // `mercurial/helptext/config.txt` suggests it should be reversed
156 159 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
157 160 if installation_prefix != root {
158 161 add_for_prefix(&installation_prefix)?
159 162 }
160 163 }
161 164 add_for_prefix(root)?;
162 165 Ok(())
163 166 }
164 167
165 168 #[cfg(unix)] // TODO: other plateforms
166 169 fn add_user_config(&mut self) -> Result<(), ConfigError> {
167 170 let opt_home = home::home_dir();
168 171 if let Some(home) = &opt_home {
169 172 self.add_trusted_file(&home.join(".hgrc"))?
170 173 }
171 174 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
172 175 if !darwin {
173 176 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
174 177 .map(PathBuf::from)
175 178 .or_else(|| opt_home.map(|home| home.join(".config")))
176 179 {
177 180 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
178 181 }
179 182 }
180 183 Ok(())
181 184 }
182 185
183 186 /// Loads in order, which means that the precedence is the same
184 187 /// as the order of `sources`.
185 188 pub fn load_from_explicit_sources(
186 189 sources: Vec<ConfigSource>,
187 190 ) -> Result<Self, ConfigError> {
188 191 let mut layers = vec![];
189 192
190 193 for source in sources.into_iter() {
191 194 match source {
192 195 ConfigSource::Parsed(c) => layers.push(c),
193 196 ConfigSource::AbsPath(c) => {
194 197 // TODO check if it should be trusted
195 198 // mercurial/ui.py:427
196 199 let data = match std::fs::read(&c) {
197 200 Err(_) => continue, // same as the python code
198 201 Ok(data) => data,
199 202 };
200 203 layers.extend(ConfigLayer::parse(&c, &data)?)
201 204 }
202 205 }
203 206 }
204 207
205 208 Ok(Config { layers })
206 209 }
207 210
208 211 /// Loads the per-repository config into a new `Config` which is combined
209 212 /// with `self`.
210 213 pub(crate) fn combine_with_repo(
211 214 &self,
212 215 repo_config_files: &[PathBuf],
213 216 ) -> Result<Self, ConfigError> {
214 217 let (cli_layers, other_layers) = self
215 218 .layers
216 219 .iter()
217 220 .cloned()
218 221 .partition(ConfigLayer::is_from_command_line);
219 222
220 223 let mut repo_config = Self {
221 224 layers: other_layers,
222 225 };
223 226 for path in repo_config_files {
224 227 // TODO: check if this file should be trusted:
225 228 // `mercurial/ui.py:427`
226 229 repo_config.add_trusted_file(path)?;
227 230 }
228 231 repo_config.layers.extend(cli_layers);
229 232 Ok(repo_config)
230 233 }
231 234
232 235 /// Returns an `Err` if the first value found is not a valid boolean.
233 236 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
234 237 /// found, or `None`.
235 238 pub fn get_option(
236 239 &self,
237 240 section: &[u8],
238 241 item: &[u8],
239 242 ) -> Result<Option<bool>, ConfigParseError> {
240 243 match self.get_inner(&section, &item) {
241 244 Some((layer, v)) => match parse_bool(&v.bytes) {
242 245 Some(b) => Ok(Some(b)),
243 246 None => Err(ConfigParseError {
244 247 origin: layer.origin.to_owned(),
245 248 line: v.line,
246 249 bytes: v.bytes.to_owned(),
247 250 }),
248 251 },
249 252 None => Ok(None),
250 253 }
251 254 }
252 255
253 256 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
254 257 /// if the value is not found, an `Err` if it's not a valid boolean.
255 258 pub fn get_bool(
256 259 &self,
257 260 section: &[u8],
258 261 item: &[u8],
259 262 ) -> Result<bool, ConfigError> {
260 263 Ok(self.get_option(section, item)?.unwrap_or(false))
261 264 }
262 265
263 266 /// Returns the raw value bytes of the first one found, or `None`.
264 267 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
265 268 self.get_inner(section, item)
266 269 .map(|(_, value)| value.bytes.as_ref())
267 270 }
268 271
269 272 /// Returns the layer and the value of the first one found, or `None`.
270 273 fn get_inner(
271 274 &self,
272 275 section: &[u8],
273 276 item: &[u8],
274 277 ) -> Option<(&ConfigLayer, &ConfigValue)> {
275 278 for layer in self.layers.iter().rev() {
276 279 if !layer.trusted {
277 280 continue;
278 281 }
279 282 if let Some(v) = layer.get(&section, &item) {
280 283 return Some((&layer, v));
281 284 }
282 285 }
283 286 None
284 287 }
285 288
286 289 /// Get raw values bytes from all layers (even untrusted ones) in order
287 290 /// of precedence.
288 291 #[cfg(test)]
289 292 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
290 293 let mut res = vec![];
291 294 for layer in self.layers.iter().rev() {
292 295 if let Some(v) = layer.get(&section, &item) {
293 296 res.push(v.bytes.as_ref());
294 297 }
295 298 }
296 299 res
297 300 }
298 301 }
299 302
300 303 #[cfg(test)]
301 304 mod tests {
302 305 use super::*;
303 306 use pretty_assertions::assert_eq;
304 307 use std::fs::File;
305 308 use std::io::Write;
306 309
307 310 #[test]
308 311 fn test_include_layer_ordering() {
309 312 let tmpdir = tempfile::tempdir().unwrap();
310 313 let tmpdir_path = tmpdir.path();
311 314 let mut included_file =
312 315 File::create(&tmpdir_path.join("included.rc")).unwrap();
313 316
314 317 included_file.write_all(b"[section]\nitem=value1").unwrap();
315 318 let base_config_path = tmpdir_path.join("base.rc");
316 319 let mut config_file = File::create(&base_config_path).unwrap();
317 320 let data =
318 321 b"[section]\nitem=value0\n%include included.rc\nitem=value2";
319 322 config_file.write_all(data).unwrap();
320 323
321 324 let sources = vec![ConfigSource::AbsPath(base_config_path)];
322 325 let config = Config::load_from_explicit_sources(sources)
323 326 .expect("expected valid config");
324 327
325 328 let (_, value) = config.get_inner(b"section", b"item").unwrap();
326 329 assert_eq!(
327 330 value,
328 331 &ConfigValue {
329 332 bytes: b"value2".to_vec(),
330 333 line: Some(4)
331 334 }
332 335 );
333 336
334 337 let value = config.get(b"section", b"item").unwrap();
335 338 assert_eq!(value, b"value2",);
336 339 assert_eq!(
337 340 config.get_all(b"section", b"item"),
338 341 [b"value2", b"value1", b"value0"]
339 342 );
340 343 }
341 344 }
@@ -1,255 +1,298 b''
1 1 // layer.rs
2 2 //
3 3 // Copyright 2020
4 4 // Valentin Gatien-Baron,
5 5 // Raphaël Gomès <rgomes@octobus.net>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 use crate::errors::{HgError, IoResultExt};
11 11 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
12 12 use format_bytes::{write_bytes, DisplayBytes};
13 13 use lazy_static::lazy_static;
14 14 use regex::bytes::Regex;
15 15 use std::collections::HashMap;
16 16 use std::path::{Path, PathBuf};
17 17
18 18 lazy_static! {
19 19 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
20 20 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
21 21 /// Continuation whitespace
22 22 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
23 23 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
24 24 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
25 25 /// A directive that allows for removing previous entries
26 26 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
27 27 /// A directive that allows for including other config files
28 28 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
29 29 }
30 30
31 31 /// All config values separated by layers of precedence.
32 32 /// Each config source may be split in multiple layers if `%include` directives
33 33 /// are used.
34 34 /// TODO detail the general precedence
35 35 #[derive(Clone)]
36 36 pub struct ConfigLayer {
37 37 /// Mapping of the sections to their items
38 38 sections: HashMap<Vec<u8>, ConfigItem>,
39 39 /// All sections (and their items/values) in a layer share the same origin
40 40 pub origin: ConfigOrigin,
41 41 /// Whether this layer comes from a trusted user or group
42 42 pub trusted: bool,
43 43 }
44 44
45 45 impl ConfigLayer {
46 46 pub fn new(origin: ConfigOrigin) -> Self {
47 47 ConfigLayer {
48 48 sections: HashMap::new(),
49 49 trusted: true, // TODO check
50 50 origin,
51 51 }
52 52 }
53 53
54 /// Parse `--config` CLI arguments and return a layer if there’s any
55 pub(crate) fn parse_cli_args(
56 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
57 ) -> Result<Option<Self>, ConfigError> {
58 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
59 use crate::utils::SliceExt;
60
61 let (section_and_item, value) = split_2(arg, b'=')?;
62 let (section, item) = split_2(section_and_item.trim(), b'.')?;
63 Some((
64 section.to_owned(),
65 item.to_owned(),
66 value.trim().to_owned(),
67 ))
68 }
69
70 fn split_2(bytes: &[u8], separator: u8) -> Option<(&[u8], &[u8])> {
71 let mut iter = bytes.splitn(2, |&byte| byte == separator);
72 let a = iter.next()?;
73 let b = iter.next()?;
74 Some((a, b))
75 }
76
77 let mut layer = Self::new(ConfigOrigin::CommandLine);
78 for arg in cli_config_args {
79 let arg = arg.as_ref();
80 if let Some((section, item, value)) = parse_one(arg) {
81 layer.add(section, item, value, None);
82 } else {
83 Err(HgError::abort(format!(
84 "malformed --config option: \"{}\" \
85 (use --config section.name=value)",
86 String::from_utf8_lossy(arg),
87 )))?
88 }
89 }
90 if layer.sections.is_empty() {
91 Ok(None)
92 } else {
93 Ok(Some(layer))
94 }
95 }
96
54 97 /// Returns whether this layer comes from `--config` CLI arguments
55 98 pub(crate) fn is_from_command_line(&self) -> bool {
56 99 if let ConfigOrigin::CommandLine = self.origin {
57 100 true
58 101 } else {
59 102 false
60 103 }
61 104 }
62 105
63 106 /// Add an entry to the config, overwriting the old one if already present.
64 107 pub fn add(
65 108 &mut self,
66 109 section: Vec<u8>,
67 110 item: Vec<u8>,
68 111 value: Vec<u8>,
69 112 line: Option<usize>,
70 113 ) {
71 114 self.sections
72 115 .entry(section)
73 116 .or_insert_with(|| HashMap::new())
74 117 .insert(item, ConfigValue { bytes: value, line });
75 118 }
76 119
77 120 /// Returns the config value in `<section>.<item>` if it exists
78 121 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
79 122 Some(self.sections.get(section)?.get(item)?)
80 123 }
81 124
82 125 pub fn is_empty(&self) -> bool {
83 126 self.sections.is_empty()
84 127 }
85 128
86 129 /// Returns a `Vec` of layers in order of precedence (so, in read order),
87 130 /// recursively parsing the `%include` directives if any.
88 131 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
89 132 let mut layers = vec![];
90 133
91 134 // Discard byte order mark if any
92 135 let data = if data.starts_with(b"\xef\xbb\xbf") {
93 136 &data[3..]
94 137 } else {
95 138 data
96 139 };
97 140
98 141 // TODO check if it's trusted
99 142 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
100 143
101 144 let mut lines_iter =
102 145 data.split(|b| *b == b'\n').enumerate().peekable();
103 146 let mut section = b"".to_vec();
104 147
105 148 while let Some((index, bytes)) = lines_iter.next() {
106 149 if let Some(m) = INCLUDE_RE.captures(&bytes) {
107 150 let filename_bytes = &m[1];
108 151 // `Path::parent` only fails for the root directory,
109 152 // which `src` can’t be since we’ve managed to open it as a
110 153 // file.
111 154 let dir = src
112 155 .parent()
113 156 .expect("Path::parent fail on a file we’ve read");
114 157 // `Path::join` with an absolute argument correctly ignores the
115 158 // base path
116 159 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
117 160 let data = std::fs::read(&filename).for_file(&filename)?;
118 161 layers.push(current_layer);
119 162 layers.extend(Self::parse(&filename, &data)?);
120 163 current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
121 164 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
122 165 } else if let Some(m) = SECTION_RE.captures(&bytes) {
123 166 section = m[1].to_vec();
124 167 } else if let Some(m) = ITEM_RE.captures(&bytes) {
125 168 let item = m[1].to_vec();
126 169 let mut value = m[2].to_vec();
127 170 loop {
128 171 match lines_iter.peek() {
129 172 None => break,
130 173 Some((_, v)) => {
131 174 if let Some(_) = COMMENT_RE.captures(&v) {
132 175 } else if let Some(_) = CONT_RE.captures(&v) {
133 176 value.extend(b"\n");
134 177 value.extend(&m[1]);
135 178 } else {
136 179 break;
137 180 }
138 181 }
139 182 };
140 183 lines_iter.next();
141 184 }
142 185 current_layer.add(
143 186 section.clone(),
144 187 item,
145 188 value,
146 189 Some(index + 1),
147 190 );
148 191 } else if let Some(m) = UNSET_RE.captures(&bytes) {
149 192 if let Some(map) = current_layer.sections.get_mut(&section) {
150 193 map.remove(&m[1]);
151 194 }
152 195 } else {
153 196 return Err(ConfigParseError {
154 197 origin: ConfigOrigin::File(src.to_owned()),
155 198 line: Some(index + 1),
156 199 bytes: bytes.to_owned(),
157 200 }
158 201 .into());
159 202 }
160 203 }
161 204 if !current_layer.is_empty() {
162 205 layers.push(current_layer);
163 206 }
164 207 Ok(layers)
165 208 }
166 209 }
167 210
168 211 impl DisplayBytes for ConfigLayer {
169 212 fn display_bytes(
170 213 &self,
171 214 out: &mut dyn std::io::Write,
172 215 ) -> std::io::Result<()> {
173 216 let mut sections: Vec<_> = self.sections.iter().collect();
174 217 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
175 218
176 219 for (section, items) in sections.into_iter() {
177 220 let mut items: Vec<_> = items.into_iter().collect();
178 221 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
179 222
180 223 for (item, config_entry) in items {
181 224 write_bytes!(
182 225 out,
183 226 b"{}.{}={} # {}\n",
184 227 section,
185 228 item,
186 229 &config_entry.bytes,
187 230 &self.origin,
188 231 )?
189 232 }
190 233 }
191 234 Ok(())
192 235 }
193 236 }
194 237
195 238 /// Mapping of section item to value.
196 239 /// In the following:
197 240 /// ```text
198 241 /// [ui]
199 242 /// paginate=no
200 243 /// ```
201 244 /// "paginate" is the section item and "no" the value.
202 245 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
203 246
204 247 #[derive(Clone, Debug, PartialEq)]
205 248 pub struct ConfigValue {
206 249 /// The raw bytes of the value (be it from the CLI, env or from a file)
207 250 pub bytes: Vec<u8>,
208 251 /// Only present if the value comes from a file, 1-indexed.
209 252 pub line: Option<usize>,
210 253 }
211 254
212 255 #[derive(Clone, Debug)]
213 256 pub enum ConfigOrigin {
214 257 /// From a configuration file
215 258 File(PathBuf),
216 259 /// From a `--config` CLI argument
217 260 CommandLine,
218 261 /// From environment variables like `$PAGER` or `$EDITOR`
219 262 Environment(Vec<u8>),
220 263 /* TODO cli
221 264 * TODO defaults (configitems.py)
222 265 * TODO extensions
223 266 * TODO Python resources?
224 267 * Others? */
225 268 }
226 269
227 270 impl DisplayBytes for ConfigOrigin {
228 271 fn display_bytes(
229 272 &self,
230 273 out: &mut dyn std::io::Write,
231 274 ) -> std::io::Result<()> {
232 275 match self {
233 276 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
234 277 ConfigOrigin::CommandLine => out.write_all(b"--config"),
235 278 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
236 279 }
237 280 }
238 281 }
239 282
240 283 #[derive(Debug)]
241 284 pub struct ConfigParseError {
242 285 pub origin: ConfigOrigin,
243 286 pub line: Option<usize>,
244 287 pub bytes: Vec<u8>,
245 288 }
246 289
247 290 #[derive(Debug, derive_more::From)]
248 291 pub enum ConfigError {
249 292 Parse(ConfigParseError),
250 293 Other(HgError),
251 294 }
252 295
253 296 fn make_regex(pattern: &'static str) -> Regex {
254 297 Regex::new(pattern).expect("expected a valid regex")
255 298 }
@@ -1,116 +1,137 b''
1 1 extern crate log;
2 2 use clap::App;
3 3 use clap::AppSettings;
4 4 use clap::Arg;
5 5 use clap::ArgMatches;
6 6 use format_bytes::format_bytes;
7 7 use std::path::Path;
8 8
9 9 mod error;
10 10 mod exitcode;
11 11 mod ui;
12 12 use error::CommandError;
13 13
14 14 fn add_global_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
15 15 app.arg(
16 16 Arg::with_name("repository")
17 17 .help("repository root directory")
18 18 .short("-R")
19 19 .long("--repository")
20 20 .value_name("REPO")
21 21 .takes_value(true),
22 22 )
23 .arg(
24 Arg::with_name("config")
25 .help("set/override config option (use 'section.name=value')")
26 .long("--config")
27 .value_name("CONFIG")
28 .takes_value(true)
29 // Ok: `--config section.key1=val --config section.key2=val2`
30 .multiple(true)
31 // Not ok: `--config section.key1=val section.key2=val2`
32 .number_of_values(1),
33 )
23 34 }
24 35
25 36 fn main() {
26 37 env_logger::init();
27 38 let app = App::new("rhg")
28 39 .setting(AppSettings::AllowInvalidUtf8)
29 40 .setting(AppSettings::SubcommandRequired)
30 41 .setting(AppSettings::VersionlessSubcommands)
31 42 .version("0.0.1");
32 43 let app = add_global_args(app);
33 44 let app = add_subcommand_args(app);
34 45
35 46 let ui = ui::Ui::new();
36 47
37 48 let matches = app.clone().get_matches_safe().unwrap_or_else(|err| {
38 49 let _ = ui.writeln_stderr_str(&err.message);
39 50 std::process::exit(exitcode::UNIMPLEMENTED)
40 51 });
41 52
42 53 let (subcommand_name, subcommand_matches) = matches.subcommand();
43 54 let run = subcommand_run_fn(subcommand_name)
44 55 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
45 56 let args = subcommand_matches
46 57 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
47 58
48 59 // Global arguments can be in either based on e.g. `hg -R ./foo log` v.s.
49 60 // `hg log -R ./foo`
50 let global_arg =
61 let value_of_global_arg =
51 62 |name| args.value_of_os(name).or_else(|| matches.value_of_os(name));
63 // For arguments where multiple occurences are allowed, return a
64 // possibly-iterator of all values.
65 let values_of_global_arg = |name: &str| {
66 let a = matches.values_of_os(name).into_iter().flatten();
67 let b = args.values_of_os(name).into_iter().flatten();
68 a.chain(b)
69 };
52 70
53 let repo_path = global_arg("repository").map(Path::new);
71 let repo_path = value_of_global_arg("repository").map(Path::new);
54 72 let result = (|| -> Result<(), CommandError> {
55 let config = hg::config::Config::load()?;
73 let config_args = values_of_global_arg("config")
74 // `get_bytes_from_path` works for OsStr the same as for Path
75 .map(hg::utils::files::get_bytes_from_path);
76 let config = hg::config::Config::load(config_args)?;
56 77 run(&ui, &config, repo_path, args)
57 78 })();
58 79
59 80 let exit_code = match result {
60 81 Ok(_) => exitcode::OK,
61 82
62 83 // Exit with a specific code and no error message to let a potential
63 84 // wrapper script fallback to Python-based Mercurial.
64 85 Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED,
65 86
66 87 Err(CommandError::Abort { message }) => {
67 88 if !message.is_empty() {
68 89 // Ignore errors when writing to stderr, we’re already exiting
69 90 // with failure code so there’s not much more we can do.
70 91 let _ =
71 92 ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
72 93 }
73 94 exitcode::ABORT
74 95 }
75 96 };
76 97 std::process::exit(exit_code)
77 98 }
78 99
79 100 macro_rules! subcommands {
80 101 ($( $command: ident )+) => {
81 102 mod commands {
82 103 $(
83 104 pub mod $command;
84 105 )+
85 106 }
86 107
87 108 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
88 109 app
89 110 $(
90 111 .subcommand(add_global_args(commands::$command::args()))
91 112 )+
92 113 }
93 114
94 115 fn subcommand_run_fn(name: &str) -> Option<fn(
95 116 &ui::Ui,
96 117 &hg::config::Config,
97 118 Option<&Path>,
98 119 &ArgMatches,
99 120 ) -> Result<(), CommandError>> {
100 121 match name {
101 122 $(
102 123 stringify!($command) => Some(commands::$command::run),
103 124 )+
104 125 _ => None,
105 126 }
106 127 }
107 128 };
108 129 }
109 130
110 131 subcommands! {
111 132 cat
112 133 debugdata
113 134 debugrequirements
114 135 files
115 136 root
116 137 }
General Comments 0
You need to be logged in to leave comments. Login now