Show More
@@ -1,344 +1,343 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 |
use crate::utils::files::get_bytes_from_ |
|
|
14 | use crate::utils::files::get_bytes_from_os_str; | |
|
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 | 68 | pub fn load( |
|
69 | 69 | cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>, |
|
70 | 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 | 95 | if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? { |
|
96 | 96 | config.layers.push(layer) |
|
97 | 97 | } |
|
98 | 98 | Ok(config) |
|
99 | 99 | } |
|
100 | 100 | |
|
101 | 101 | fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> { |
|
102 | 102 | if let Some(entries) = std::fs::read_dir(path) |
|
103 | 103 | .for_file(path) |
|
104 | 104 | .io_not_found_as_none()? |
|
105 | 105 | { |
|
106 | 106 | for entry in entries { |
|
107 | 107 | let file_path = entry.for_file(path)?.path(); |
|
108 | 108 | if file_path.extension() == Some(std::ffi::OsStr::new("rc")) { |
|
109 | 109 | self.add_trusted_file(&file_path)? |
|
110 | 110 | } |
|
111 | 111 | } |
|
112 | 112 | } |
|
113 | 113 | Ok(()) |
|
114 | 114 | } |
|
115 | 115 | |
|
116 | 116 | fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> { |
|
117 | 117 | if let Some(data) = |
|
118 | 118 | std::fs::read(path).for_file(path).io_not_found_as_none()? |
|
119 | 119 | { |
|
120 | 120 | self.layers.extend(ConfigLayer::parse(path, &data)?) |
|
121 | 121 | } |
|
122 | 122 | Ok(()) |
|
123 | 123 | } |
|
124 | 124 | |
|
125 | 125 | fn add_for_environment_variable( |
|
126 | 126 | &mut self, |
|
127 | 127 | var: &str, |
|
128 | 128 | section: &[u8], |
|
129 | 129 | key: &[u8], |
|
130 | 130 | ) { |
|
131 | 131 | if let Some(value) = env::var_os(var) { |
|
132 | 132 | let origin = layer::ConfigOrigin::Environment(var.into()); |
|
133 | 133 | let mut layer = ConfigLayer::new(origin); |
|
134 | 134 | layer.add( |
|
135 | 135 | section.to_owned(), |
|
136 | 136 | key.to_owned(), |
|
137 | // `value` is not a path but this works for any `OsStr`: | |
|
138 | get_bytes_from_path(value), | |
|
137 | get_bytes_from_os_str(value), | |
|
139 | 138 | None, |
|
140 | 139 | ); |
|
141 | 140 | self.layers.push(layer) |
|
142 | 141 | } |
|
143 | 142 | } |
|
144 | 143 | |
|
145 | 144 | #[cfg(unix)] // TODO: other platforms |
|
146 | 145 | fn add_system_config(&mut self) -> Result<(), ConfigError> { |
|
147 | 146 | let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> { |
|
148 | 147 | let etc = prefix.join("etc").join("mercurial"); |
|
149 | 148 | self.add_trusted_file(&etc.join("hgrc"))?; |
|
150 | 149 | self.add_trusted_dir(&etc.join("hgrc.d")) |
|
151 | 150 | }; |
|
152 | 151 | let root = Path::new("/"); |
|
153 | 152 | // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0] |
|
154 | 153 | // instead? TODO: can this be a relative path? |
|
155 | 154 | let hg = crate::utils::current_exe()?; |
|
156 | 155 | // TODO: this order (per-installation then per-system) matches |
|
157 | 156 | // `systemrcpath()` in `mercurial/scmposix.py`, but |
|
158 | 157 | // `mercurial/helptext/config.txt` suggests it should be reversed |
|
159 | 158 | if let Some(installation_prefix) = hg.parent().and_then(Path::parent) { |
|
160 | 159 | if installation_prefix != root { |
|
161 | 160 | add_for_prefix(&installation_prefix)? |
|
162 | 161 | } |
|
163 | 162 | } |
|
164 | 163 | add_for_prefix(root)?; |
|
165 | 164 | Ok(()) |
|
166 | 165 | } |
|
167 | 166 | |
|
168 | 167 | #[cfg(unix)] // TODO: other plateforms |
|
169 | 168 | fn add_user_config(&mut self) -> Result<(), ConfigError> { |
|
170 | 169 | let opt_home = home::home_dir(); |
|
171 | 170 | if let Some(home) = &opt_home { |
|
172 | 171 | self.add_trusted_file(&home.join(".hgrc"))? |
|
173 | 172 | } |
|
174 | 173 | let darwin = cfg!(any(target_os = "macos", target_os = "ios")); |
|
175 | 174 | if !darwin { |
|
176 | 175 | if let Some(config_home) = env::var_os("XDG_CONFIG_HOME") |
|
177 | 176 | .map(PathBuf::from) |
|
178 | 177 | .or_else(|| opt_home.map(|home| home.join(".config"))) |
|
179 | 178 | { |
|
180 | 179 | self.add_trusted_file(&config_home.join("hg").join("hgrc"))? |
|
181 | 180 | } |
|
182 | 181 | } |
|
183 | 182 | Ok(()) |
|
184 | 183 | } |
|
185 | 184 | |
|
186 | 185 | /// Loads in order, which means that the precedence is the same |
|
187 | 186 | /// as the order of `sources`. |
|
188 | 187 | pub fn load_from_explicit_sources( |
|
189 | 188 | sources: Vec<ConfigSource>, |
|
190 | 189 | ) -> Result<Self, ConfigError> { |
|
191 | 190 | let mut layers = vec![]; |
|
192 | 191 | |
|
193 | 192 | for source in sources.into_iter() { |
|
194 | 193 | match source { |
|
195 | 194 | ConfigSource::Parsed(c) => layers.push(c), |
|
196 | 195 | ConfigSource::AbsPath(c) => { |
|
197 | 196 | // TODO check if it should be trusted |
|
198 | 197 | // mercurial/ui.py:427 |
|
199 | 198 | let data = match std::fs::read(&c) { |
|
200 | 199 | Err(_) => continue, // same as the python code |
|
201 | 200 | Ok(data) => data, |
|
202 | 201 | }; |
|
203 | 202 | layers.extend(ConfigLayer::parse(&c, &data)?) |
|
204 | 203 | } |
|
205 | 204 | } |
|
206 | 205 | } |
|
207 | 206 | |
|
208 | 207 | Ok(Config { layers }) |
|
209 | 208 | } |
|
210 | 209 | |
|
211 | 210 | /// Loads the per-repository config into a new `Config` which is combined |
|
212 | 211 | /// with `self`. |
|
213 | 212 | pub(crate) fn combine_with_repo( |
|
214 | 213 | &self, |
|
215 | 214 | repo_config_files: &[PathBuf], |
|
216 | 215 | ) -> Result<Self, ConfigError> { |
|
217 | 216 | let (cli_layers, other_layers) = self |
|
218 | 217 | .layers |
|
219 | 218 | .iter() |
|
220 | 219 | .cloned() |
|
221 | 220 | .partition(ConfigLayer::is_from_command_line); |
|
222 | 221 | |
|
223 | 222 | let mut repo_config = Self { |
|
224 | 223 | layers: other_layers, |
|
225 | 224 | }; |
|
226 | 225 | for path in repo_config_files { |
|
227 | 226 | // TODO: check if this file should be trusted: |
|
228 | 227 | // `mercurial/ui.py:427` |
|
229 | 228 | repo_config.add_trusted_file(path)?; |
|
230 | 229 | } |
|
231 | 230 | repo_config.layers.extend(cli_layers); |
|
232 | 231 | Ok(repo_config) |
|
233 | 232 | } |
|
234 | 233 | |
|
235 | 234 | /// Returns an `Err` if the first value found is not a valid boolean. |
|
236 | 235 | /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if |
|
237 | 236 | /// found, or `None`. |
|
238 | 237 | pub fn get_option( |
|
239 | 238 | &self, |
|
240 | 239 | section: &[u8], |
|
241 | 240 | item: &[u8], |
|
242 | 241 | ) -> Result<Option<bool>, ConfigParseError> { |
|
243 | 242 | match self.get_inner(§ion, &item) { |
|
244 | 243 | Some((layer, v)) => match parse_bool(&v.bytes) { |
|
245 | 244 | Some(b) => Ok(Some(b)), |
|
246 | 245 | None => Err(ConfigParseError { |
|
247 | 246 | origin: layer.origin.to_owned(), |
|
248 | 247 | line: v.line, |
|
249 | 248 | bytes: v.bytes.to_owned(), |
|
250 | 249 | }), |
|
251 | 250 | }, |
|
252 | 251 | None => Ok(None), |
|
253 | 252 | } |
|
254 | 253 | } |
|
255 | 254 | |
|
256 | 255 | /// Returns the corresponding boolean in the config. Returns `Ok(false)` |
|
257 | 256 | /// if the value is not found, an `Err` if it's not a valid boolean. |
|
258 | 257 | pub fn get_bool( |
|
259 | 258 | &self, |
|
260 | 259 | section: &[u8], |
|
261 | 260 | item: &[u8], |
|
262 | 261 | ) -> Result<bool, ConfigError> { |
|
263 | 262 | Ok(self.get_option(section, item)?.unwrap_or(false)) |
|
264 | 263 | } |
|
265 | 264 | |
|
266 | 265 | /// Returns the raw value bytes of the first one found, or `None`. |
|
267 | 266 | pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> { |
|
268 | 267 | self.get_inner(section, item) |
|
269 | 268 | .map(|(_, value)| value.bytes.as_ref()) |
|
270 | 269 | } |
|
271 | 270 | |
|
272 | 271 | /// Returns the layer and the value of the first one found, or `None`. |
|
273 | 272 | fn get_inner( |
|
274 | 273 | &self, |
|
275 | 274 | section: &[u8], |
|
276 | 275 | item: &[u8], |
|
277 | 276 | ) -> Option<(&ConfigLayer, &ConfigValue)> { |
|
278 | 277 | for layer in self.layers.iter().rev() { |
|
279 | 278 | if !layer.trusted { |
|
280 | 279 | continue; |
|
281 | 280 | } |
|
282 | 281 | if let Some(v) = layer.get(§ion, &item) { |
|
283 | 282 | return Some((&layer, v)); |
|
284 | 283 | } |
|
285 | 284 | } |
|
286 | 285 | None |
|
287 | 286 | } |
|
288 | 287 | |
|
289 | 288 | /// Get raw values bytes from all layers (even untrusted ones) in order |
|
290 | 289 | /// of precedence. |
|
291 | 290 | #[cfg(test)] |
|
292 | 291 | fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> { |
|
293 | 292 | let mut res = vec![]; |
|
294 | 293 | for layer in self.layers.iter().rev() { |
|
295 | 294 | if let Some(v) = layer.get(§ion, &item) { |
|
296 | 295 | res.push(v.bytes.as_ref()); |
|
297 | 296 | } |
|
298 | 297 | } |
|
299 | 298 | res |
|
300 | 299 | } |
|
301 | 300 | } |
|
302 | 301 | |
|
303 | 302 | #[cfg(test)] |
|
304 | 303 | mod tests { |
|
305 | 304 | use super::*; |
|
306 | 305 | use pretty_assertions::assert_eq; |
|
307 | 306 | use std::fs::File; |
|
308 | 307 | use std::io::Write; |
|
309 | 308 | |
|
310 | 309 | #[test] |
|
311 | 310 | fn test_include_layer_ordering() { |
|
312 | 311 | let tmpdir = tempfile::tempdir().unwrap(); |
|
313 | 312 | let tmpdir_path = tmpdir.path(); |
|
314 | 313 | let mut included_file = |
|
315 | 314 | File::create(&tmpdir_path.join("included.rc")).unwrap(); |
|
316 | 315 | |
|
317 | 316 | included_file.write_all(b"[section]\nitem=value1").unwrap(); |
|
318 | 317 | let base_config_path = tmpdir_path.join("base.rc"); |
|
319 | 318 | let mut config_file = File::create(&base_config_path).unwrap(); |
|
320 | 319 | let data = |
|
321 | 320 | b"[section]\nitem=value0\n%include included.rc\nitem=value2"; |
|
322 | 321 | config_file.write_all(data).unwrap(); |
|
323 | 322 | |
|
324 | 323 | let sources = vec![ConfigSource::AbsPath(base_config_path)]; |
|
325 | 324 | let config = Config::load_from_explicit_sources(sources) |
|
326 | 325 | .expect("expected valid config"); |
|
327 | 326 | |
|
328 | 327 | let (_, value) = config.get_inner(b"section", b"item").unwrap(); |
|
329 | 328 | assert_eq!( |
|
330 | 329 | value, |
|
331 | 330 | &ConfigValue { |
|
332 | 331 | bytes: b"value2".to_vec(), |
|
333 | 332 | line: Some(4) |
|
334 | 333 | } |
|
335 | 334 | ); |
|
336 | 335 | |
|
337 | 336 | let value = config.get(b"section", b"item").unwrap(); |
|
338 | 337 | assert_eq!(value, b"value2",); |
|
339 | 338 | assert_eq!( |
|
340 | 339 | config.get_all(b"section", b"item"), |
|
341 | 340 | [b"value2", b"value1", b"value0"] |
|
342 | 341 | ); |
|
343 | 342 | } |
|
344 | 343 | } |
@@ -1,442 +1,448 b'' | |||
|
1 | 1 | // files.rs |
|
2 | 2 | // |
|
3 | 3 | // Copyright 2019 |
|
4 | 4 | // Raphaël Gomès <rgomes@octobus.net>, |
|
5 | 5 | // Yuya Nishihara <yuya@tcha.org> |
|
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 | //! Functions for fiddling with files. |
|
11 | 11 | |
|
12 | 12 | use crate::utils::{ |
|
13 | 13 | hg_path::{path_to_hg_path_buf, HgPath, HgPathBuf, HgPathError}, |
|
14 | 14 | path_auditor::PathAuditor, |
|
15 | 15 | replace_slice, |
|
16 | 16 | }; |
|
17 | 17 | use lazy_static::lazy_static; |
|
18 | 18 | use same_file::is_same_file; |
|
19 | 19 | use std::borrow::{Cow, ToOwned}; |
|
20 | use std::ffi::OsStr; | |
|
20 | 21 | use std::fs::Metadata; |
|
21 | 22 | use std::iter::FusedIterator; |
|
22 | 23 | use std::ops::Deref; |
|
23 | 24 | use std::path::{Path, PathBuf}; |
|
24 | 25 | |
|
25 | 26 | pub fn get_path_from_bytes(bytes: &[u8]) -> &Path { |
|
26 | 27 | let os_str; |
|
27 | 28 | #[cfg(unix)] |
|
28 | 29 | { |
|
29 | 30 | use std::os::unix::ffi::OsStrExt; |
|
30 | 31 | os_str = std::ffi::OsStr::from_bytes(bytes); |
|
31 | 32 | } |
|
32 | 33 | // TODO Handle other platforms |
|
33 | 34 | // TODO: convert from WTF8 to Windows MBCS (ANSI encoding). |
|
34 | 35 | // Perhaps, the return type would have to be Result<PathBuf>. |
|
35 | 36 | |
|
36 | 37 | Path::new(os_str) |
|
37 | 38 | } |
|
38 | 39 | |
|
39 | 40 | // TODO: need to convert from WTF8 to MBCS bytes on Windows. |
|
40 | 41 | // that's why Vec<u8> is returned. |
|
41 | 42 | #[cfg(unix)] |
|
42 | 43 | pub fn get_bytes_from_path(path: impl AsRef<Path>) -> Vec<u8> { |
|
44 | get_bytes_from_os_str(path.as_ref()) | |
|
45 | } | |
|
46 | ||
|
47 | #[cfg(unix)] | |
|
48 | pub fn get_bytes_from_os_str(str: impl AsRef<OsStr>) -> Vec<u8> { | |
|
43 | 49 | use std::os::unix::ffi::OsStrExt; |
|
44 |
|
|
|
50 | str.as_ref().as_bytes().to_vec() | |
|
45 | 51 | } |
|
46 | 52 | |
|
47 | 53 | /// An iterator over repository path yielding itself and its ancestors. |
|
48 | 54 | #[derive(Copy, Clone, Debug)] |
|
49 | 55 | pub struct Ancestors<'a> { |
|
50 | 56 | next: Option<&'a HgPath>, |
|
51 | 57 | } |
|
52 | 58 | |
|
53 | 59 | impl<'a> Iterator for Ancestors<'a> { |
|
54 | 60 | type Item = &'a HgPath; |
|
55 | 61 | |
|
56 | 62 | fn next(&mut self) -> Option<Self::Item> { |
|
57 | 63 | let next = self.next; |
|
58 | 64 | self.next = match self.next { |
|
59 | 65 | Some(s) if s.is_empty() => None, |
|
60 | 66 | Some(s) => { |
|
61 | 67 | let p = s.bytes().rposition(|c| *c == b'/').unwrap_or(0); |
|
62 | 68 | Some(HgPath::new(&s.as_bytes()[..p])) |
|
63 | 69 | } |
|
64 | 70 | None => None, |
|
65 | 71 | }; |
|
66 | 72 | next |
|
67 | 73 | } |
|
68 | 74 | } |
|
69 | 75 | |
|
70 | 76 | impl<'a> FusedIterator for Ancestors<'a> {} |
|
71 | 77 | |
|
72 | 78 | /// An iterator over repository path yielding itself and its ancestors. |
|
73 | 79 | #[derive(Copy, Clone, Debug)] |
|
74 | 80 | pub(crate) struct AncestorsWithBase<'a> { |
|
75 | 81 | next: Option<(&'a HgPath, &'a HgPath)>, |
|
76 | 82 | } |
|
77 | 83 | |
|
78 | 84 | impl<'a> Iterator for AncestorsWithBase<'a> { |
|
79 | 85 | type Item = (&'a HgPath, &'a HgPath); |
|
80 | 86 | |
|
81 | 87 | fn next(&mut self) -> Option<Self::Item> { |
|
82 | 88 | let next = self.next; |
|
83 | 89 | self.next = match self.next { |
|
84 | 90 | Some((s, _)) if s.is_empty() => None, |
|
85 | 91 | Some((s, _)) => Some(s.split_filename()), |
|
86 | 92 | None => None, |
|
87 | 93 | }; |
|
88 | 94 | next |
|
89 | 95 | } |
|
90 | 96 | } |
|
91 | 97 | |
|
92 | 98 | impl<'a> FusedIterator for AncestorsWithBase<'a> {} |
|
93 | 99 | |
|
94 | 100 | /// Returns an iterator yielding ancestor directories of the given repository |
|
95 | 101 | /// path. |
|
96 | 102 | /// |
|
97 | 103 | /// The path is separated by '/', and must not start with '/'. |
|
98 | 104 | /// |
|
99 | 105 | /// The path itself isn't included unless it is b"" (meaning the root |
|
100 | 106 | /// directory.) |
|
101 | 107 | pub fn find_dirs(path: &HgPath) -> Ancestors { |
|
102 | 108 | let mut dirs = Ancestors { next: Some(path) }; |
|
103 | 109 | if !path.is_empty() { |
|
104 | 110 | dirs.next(); // skip itself |
|
105 | 111 | } |
|
106 | 112 | dirs |
|
107 | 113 | } |
|
108 | 114 | |
|
109 | 115 | /// Returns an iterator yielding ancestor directories of the given repository |
|
110 | 116 | /// path. |
|
111 | 117 | /// |
|
112 | 118 | /// The path is separated by '/', and must not start with '/'. |
|
113 | 119 | /// |
|
114 | 120 | /// The path itself isn't included unless it is b"" (meaning the root |
|
115 | 121 | /// directory.) |
|
116 | 122 | pub(crate) fn find_dirs_with_base(path: &HgPath) -> AncestorsWithBase { |
|
117 | 123 | let mut dirs = AncestorsWithBase { |
|
118 | 124 | next: Some((path, HgPath::new(b""))), |
|
119 | 125 | }; |
|
120 | 126 | if !path.is_empty() { |
|
121 | 127 | dirs.next(); // skip itself |
|
122 | 128 | } |
|
123 | 129 | dirs |
|
124 | 130 | } |
|
125 | 131 | |
|
126 | 132 | /// TODO more than ASCII? |
|
127 | 133 | pub fn normalize_case(path: &HgPath) -> HgPathBuf { |
|
128 | 134 | #[cfg(windows)] // NTFS compares via upper() |
|
129 | 135 | return path.to_ascii_uppercase(); |
|
130 | 136 | #[cfg(unix)] |
|
131 | 137 | path.to_ascii_lowercase() |
|
132 | 138 | } |
|
133 | 139 | |
|
134 | 140 | lazy_static! { |
|
135 | 141 | static ref IGNORED_CHARS: Vec<Vec<u8>> = { |
|
136 | 142 | [ |
|
137 | 143 | 0x200c, 0x200d, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d, |
|
138 | 144 | 0x202e, 0x206a, 0x206b, 0x206c, 0x206d, 0x206e, 0x206f, 0xfeff, |
|
139 | 145 | ] |
|
140 | 146 | .iter() |
|
141 | 147 | .map(|code| { |
|
142 | 148 | std::char::from_u32(*code) |
|
143 | 149 | .unwrap() |
|
144 | 150 | .encode_utf8(&mut [0; 3]) |
|
145 | 151 | .bytes() |
|
146 | 152 | .collect() |
|
147 | 153 | }) |
|
148 | 154 | .collect() |
|
149 | 155 | }; |
|
150 | 156 | } |
|
151 | 157 | |
|
152 | 158 | fn hfs_ignore_clean(bytes: &[u8]) -> Vec<u8> { |
|
153 | 159 | let mut buf = bytes.to_owned(); |
|
154 | 160 | let needs_escaping = bytes.iter().any(|b| *b == b'\xe2' || *b == b'\xef'); |
|
155 | 161 | if needs_escaping { |
|
156 | 162 | for forbidden in IGNORED_CHARS.iter() { |
|
157 | 163 | replace_slice(&mut buf, forbidden, &[]) |
|
158 | 164 | } |
|
159 | 165 | buf |
|
160 | 166 | } else { |
|
161 | 167 | buf |
|
162 | 168 | } |
|
163 | 169 | } |
|
164 | 170 | |
|
165 | 171 | pub fn lower_clean(bytes: &[u8]) -> Vec<u8> { |
|
166 | 172 | hfs_ignore_clean(&bytes.to_ascii_lowercase()) |
|
167 | 173 | } |
|
168 | 174 | |
|
169 | 175 | #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] |
|
170 | 176 | pub struct HgMetadata { |
|
171 | 177 | pub st_dev: u64, |
|
172 | 178 | pub st_mode: u32, |
|
173 | 179 | pub st_nlink: u64, |
|
174 | 180 | pub st_size: u64, |
|
175 | 181 | pub st_mtime: i64, |
|
176 | 182 | pub st_ctime: i64, |
|
177 | 183 | } |
|
178 | 184 | |
|
179 | 185 | // TODO support other plaforms |
|
180 | 186 | #[cfg(unix)] |
|
181 | 187 | impl HgMetadata { |
|
182 | 188 | pub fn from_metadata(metadata: Metadata) -> Self { |
|
183 | 189 | use std::os::unix::fs::MetadataExt; |
|
184 | 190 | Self { |
|
185 | 191 | st_dev: metadata.dev(), |
|
186 | 192 | st_mode: metadata.mode(), |
|
187 | 193 | st_nlink: metadata.nlink(), |
|
188 | 194 | st_size: metadata.size(), |
|
189 | 195 | st_mtime: metadata.mtime(), |
|
190 | 196 | st_ctime: metadata.ctime(), |
|
191 | 197 | } |
|
192 | 198 | } |
|
193 | 199 | } |
|
194 | 200 | |
|
195 | 201 | /// Returns the canonical path of `name`, given `cwd` and `root` |
|
196 | 202 | pub fn canonical_path( |
|
197 | 203 | root: impl AsRef<Path>, |
|
198 | 204 | cwd: impl AsRef<Path>, |
|
199 | 205 | name: impl AsRef<Path>, |
|
200 | 206 | ) -> Result<PathBuf, HgPathError> { |
|
201 | 207 | // TODO add missing normalization for other platforms |
|
202 | 208 | let root = root.as_ref(); |
|
203 | 209 | let cwd = cwd.as_ref(); |
|
204 | 210 | let name = name.as_ref(); |
|
205 | 211 | |
|
206 | 212 | let name = if !name.is_absolute() { |
|
207 | 213 | root.join(&cwd).join(&name) |
|
208 | 214 | } else { |
|
209 | 215 | name.to_owned() |
|
210 | 216 | }; |
|
211 | 217 | let auditor = PathAuditor::new(&root); |
|
212 | 218 | if name != root && name.starts_with(&root) { |
|
213 | 219 | let name = name.strip_prefix(&root).unwrap(); |
|
214 | 220 | auditor.audit_path(path_to_hg_path_buf(name)?)?; |
|
215 | 221 | Ok(name.to_owned()) |
|
216 | 222 | } else if name == root { |
|
217 | 223 | Ok("".into()) |
|
218 | 224 | } else { |
|
219 | 225 | // Determine whether `name' is in the hierarchy at or beneath `root', |
|
220 | 226 | // by iterating name=name.parent() until it returns `None` (can't |
|
221 | 227 | // check name == '/', because that doesn't work on windows). |
|
222 | 228 | let mut name = name.deref(); |
|
223 | 229 | let original_name = name.to_owned(); |
|
224 | 230 | loop { |
|
225 | 231 | let same = is_same_file(&name, &root).unwrap_or(false); |
|
226 | 232 | if same { |
|
227 | 233 | if name == original_name { |
|
228 | 234 | // `name` was actually the same as root (maybe a symlink) |
|
229 | 235 | return Ok("".into()); |
|
230 | 236 | } |
|
231 | 237 | // `name` is a symlink to root, so `original_name` is under |
|
232 | 238 | // root |
|
233 | 239 | let rel_path = original_name.strip_prefix(&name).unwrap(); |
|
234 | 240 | auditor.audit_path(path_to_hg_path_buf(&rel_path)?)?; |
|
235 | 241 | return Ok(rel_path.to_owned()); |
|
236 | 242 | } |
|
237 | 243 | name = match name.parent() { |
|
238 | 244 | None => break, |
|
239 | 245 | Some(p) => p, |
|
240 | 246 | }; |
|
241 | 247 | } |
|
242 | 248 | // TODO hint to the user about using --cwd |
|
243 | 249 | // Bubble up the responsibility to Python for now |
|
244 | 250 | Err(HgPathError::NotUnderRoot { |
|
245 | 251 | path: original_name.to_owned(), |
|
246 | 252 | root: root.to_owned(), |
|
247 | 253 | }) |
|
248 | 254 | } |
|
249 | 255 | } |
|
250 | 256 | |
|
251 | 257 | /// Returns the representation of the path relative to the current working |
|
252 | 258 | /// directory for display purposes. |
|
253 | 259 | /// |
|
254 | 260 | /// `cwd` is a `HgPath`, so it is considered relative to the root directory |
|
255 | 261 | /// of the repository. |
|
256 | 262 | /// |
|
257 | 263 | /// # Examples |
|
258 | 264 | /// |
|
259 | 265 | /// ``` |
|
260 | 266 | /// use hg::utils::hg_path::HgPath; |
|
261 | 267 | /// use hg::utils::files::relativize_path; |
|
262 | 268 | /// use std::borrow::Cow; |
|
263 | 269 | /// |
|
264 | 270 | /// let file = HgPath::new(b"nested/file"); |
|
265 | 271 | /// let cwd = HgPath::new(b""); |
|
266 | 272 | /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"nested/file")); |
|
267 | 273 | /// |
|
268 | 274 | /// let cwd = HgPath::new(b"nested"); |
|
269 | 275 | /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"file")); |
|
270 | 276 | /// |
|
271 | 277 | /// let cwd = HgPath::new(b"other"); |
|
272 | 278 | /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"../nested/file")); |
|
273 | 279 | /// ``` |
|
274 | 280 | pub fn relativize_path(path: &HgPath, cwd: impl AsRef<HgPath>) -> Cow<[u8]> { |
|
275 | 281 | if cwd.as_ref().is_empty() { |
|
276 | 282 | Cow::Borrowed(path.as_bytes()) |
|
277 | 283 | } else { |
|
278 | 284 | let mut res: Vec<u8> = Vec::new(); |
|
279 | 285 | let mut path_iter = path.as_bytes().split(|b| *b == b'/').peekable(); |
|
280 | 286 | let mut cwd_iter = |
|
281 | 287 | cwd.as_ref().as_bytes().split(|b| *b == b'/').peekable(); |
|
282 | 288 | loop { |
|
283 | 289 | match (path_iter.peek(), cwd_iter.peek()) { |
|
284 | 290 | (Some(a), Some(b)) if a == b => (), |
|
285 | 291 | _ => break, |
|
286 | 292 | } |
|
287 | 293 | path_iter.next(); |
|
288 | 294 | cwd_iter.next(); |
|
289 | 295 | } |
|
290 | 296 | let mut need_sep = false; |
|
291 | 297 | for _ in cwd_iter { |
|
292 | 298 | if need_sep { |
|
293 | 299 | res.extend(b"/") |
|
294 | 300 | } else { |
|
295 | 301 | need_sep = true |
|
296 | 302 | }; |
|
297 | 303 | res.extend(b".."); |
|
298 | 304 | } |
|
299 | 305 | for c in path_iter { |
|
300 | 306 | if need_sep { |
|
301 | 307 | res.extend(b"/") |
|
302 | 308 | } else { |
|
303 | 309 | need_sep = true |
|
304 | 310 | }; |
|
305 | 311 | res.extend(c); |
|
306 | 312 | } |
|
307 | 313 | Cow::Owned(res) |
|
308 | 314 | } |
|
309 | 315 | } |
|
310 | 316 | |
|
311 | 317 | #[cfg(test)] |
|
312 | 318 | mod tests { |
|
313 | 319 | use super::*; |
|
314 | 320 | use pretty_assertions::assert_eq; |
|
315 | 321 | |
|
316 | 322 | #[test] |
|
317 | 323 | fn find_dirs_some() { |
|
318 | 324 | let mut dirs = super::find_dirs(HgPath::new(b"foo/bar/baz")); |
|
319 | 325 | assert_eq!(dirs.next(), Some(HgPath::new(b"foo/bar"))); |
|
320 | 326 | assert_eq!(dirs.next(), Some(HgPath::new(b"foo"))); |
|
321 | 327 | assert_eq!(dirs.next(), Some(HgPath::new(b""))); |
|
322 | 328 | assert_eq!(dirs.next(), None); |
|
323 | 329 | assert_eq!(dirs.next(), None); |
|
324 | 330 | } |
|
325 | 331 | |
|
326 | 332 | #[test] |
|
327 | 333 | fn find_dirs_empty() { |
|
328 | 334 | // looks weird, but mercurial.pathutil.finddirs(b"") yields b"" |
|
329 | 335 | let mut dirs = super::find_dirs(HgPath::new(b"")); |
|
330 | 336 | assert_eq!(dirs.next(), Some(HgPath::new(b""))); |
|
331 | 337 | assert_eq!(dirs.next(), None); |
|
332 | 338 | assert_eq!(dirs.next(), None); |
|
333 | 339 | } |
|
334 | 340 | |
|
335 | 341 | #[test] |
|
336 | 342 | fn test_find_dirs_with_base_some() { |
|
337 | 343 | let mut dirs = super::find_dirs_with_base(HgPath::new(b"foo/bar/baz")); |
|
338 | 344 | assert_eq!( |
|
339 | 345 | dirs.next(), |
|
340 | 346 | Some((HgPath::new(b"foo/bar"), HgPath::new(b"baz"))) |
|
341 | 347 | ); |
|
342 | 348 | assert_eq!( |
|
343 | 349 | dirs.next(), |
|
344 | 350 | Some((HgPath::new(b"foo"), HgPath::new(b"bar"))) |
|
345 | 351 | ); |
|
346 | 352 | assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b"foo")))); |
|
347 | 353 | assert_eq!(dirs.next(), None); |
|
348 | 354 | assert_eq!(dirs.next(), None); |
|
349 | 355 | } |
|
350 | 356 | |
|
351 | 357 | #[test] |
|
352 | 358 | fn test_find_dirs_with_base_empty() { |
|
353 | 359 | let mut dirs = super::find_dirs_with_base(HgPath::new(b"")); |
|
354 | 360 | assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b"")))); |
|
355 | 361 | assert_eq!(dirs.next(), None); |
|
356 | 362 | assert_eq!(dirs.next(), None); |
|
357 | 363 | } |
|
358 | 364 | |
|
359 | 365 | #[test] |
|
360 | 366 | fn test_canonical_path() { |
|
361 | 367 | let root = Path::new("/repo"); |
|
362 | 368 | let cwd = Path::new("/dir"); |
|
363 | 369 | let name = Path::new("filename"); |
|
364 | 370 | assert_eq!( |
|
365 | 371 | canonical_path(root, cwd, name), |
|
366 | 372 | Err(HgPathError::NotUnderRoot { |
|
367 | 373 | path: PathBuf::from("/dir/filename"), |
|
368 | 374 | root: root.to_path_buf() |
|
369 | 375 | }) |
|
370 | 376 | ); |
|
371 | 377 | |
|
372 | 378 | let root = Path::new("/repo"); |
|
373 | 379 | let cwd = Path::new("/"); |
|
374 | 380 | let name = Path::new("filename"); |
|
375 | 381 | assert_eq!( |
|
376 | 382 | canonical_path(root, cwd, name), |
|
377 | 383 | Err(HgPathError::NotUnderRoot { |
|
378 | 384 | path: PathBuf::from("/filename"), |
|
379 | 385 | root: root.to_path_buf() |
|
380 | 386 | }) |
|
381 | 387 | ); |
|
382 | 388 | |
|
383 | 389 | let root = Path::new("/repo"); |
|
384 | 390 | let cwd = Path::new("/"); |
|
385 | 391 | let name = Path::new("repo/filename"); |
|
386 | 392 | assert_eq!( |
|
387 | 393 | canonical_path(root, cwd, name), |
|
388 | 394 | Ok(PathBuf::from("filename")) |
|
389 | 395 | ); |
|
390 | 396 | |
|
391 | 397 | let root = Path::new("/repo"); |
|
392 | 398 | let cwd = Path::new("/repo"); |
|
393 | 399 | let name = Path::new("filename"); |
|
394 | 400 | assert_eq!( |
|
395 | 401 | canonical_path(root, cwd, name), |
|
396 | 402 | Ok(PathBuf::from("filename")) |
|
397 | 403 | ); |
|
398 | 404 | |
|
399 | 405 | let root = Path::new("/repo"); |
|
400 | 406 | let cwd = Path::new("/repo/subdir"); |
|
401 | 407 | let name = Path::new("filename"); |
|
402 | 408 | assert_eq!( |
|
403 | 409 | canonical_path(root, cwd, name), |
|
404 | 410 | Ok(PathBuf::from("subdir/filename")) |
|
405 | 411 | ); |
|
406 | 412 | } |
|
407 | 413 | |
|
408 | 414 | #[test] |
|
409 | 415 | fn test_canonical_path_not_rooted() { |
|
410 | 416 | use std::fs::create_dir; |
|
411 | 417 | use tempfile::tempdir; |
|
412 | 418 | |
|
413 | 419 | let base_dir = tempdir().unwrap(); |
|
414 | 420 | let base_dir_path = base_dir.path(); |
|
415 | 421 | let beneath_repo = base_dir_path.join("a"); |
|
416 | 422 | let root = base_dir_path.join("a/b"); |
|
417 | 423 | let out_of_repo = base_dir_path.join("c"); |
|
418 | 424 | let under_repo_symlink = out_of_repo.join("d"); |
|
419 | 425 | |
|
420 | 426 | create_dir(&beneath_repo).unwrap(); |
|
421 | 427 | create_dir(&root).unwrap(); |
|
422 | 428 | |
|
423 | 429 | // TODO make portable |
|
424 | 430 | std::os::unix::fs::symlink(&root, &out_of_repo).unwrap(); |
|
425 | 431 | |
|
426 | 432 | assert_eq!( |
|
427 | 433 | canonical_path(&root, Path::new(""), out_of_repo), |
|
428 | 434 | Ok(PathBuf::from("")) |
|
429 | 435 | ); |
|
430 | 436 | assert_eq!( |
|
431 | 437 | canonical_path(&root, Path::new(""), &beneath_repo), |
|
432 | 438 | Err(HgPathError::NotUnderRoot { |
|
433 | 439 | path: beneath_repo.to_owned(), |
|
434 | 440 | root: root.to_owned() |
|
435 | 441 | }) |
|
436 | 442 | ); |
|
437 | 443 | assert_eq!( |
|
438 | 444 | canonical_path(&root, Path::new(""), &under_repo_symlink), |
|
439 | 445 | Ok(PathBuf::from("d")) |
|
440 | 446 | ); |
|
441 | 447 | } |
|
442 | 448 | } |
@@ -1,176 +1,175 b'' | |||
|
1 | 1 | extern crate log; |
|
2 | 2 | use crate::ui::Ui; |
|
3 | 3 | use clap::App; |
|
4 | 4 | use clap::AppSettings; |
|
5 | 5 | use clap::Arg; |
|
6 | 6 | use clap::ArgMatches; |
|
7 | 7 | use format_bytes::format_bytes; |
|
8 | 8 | use hg::config::Config; |
|
9 | 9 | use hg::repo::{Repo, RepoError}; |
|
10 | 10 | use std::path::{Path, PathBuf}; |
|
11 | 11 | |
|
12 | 12 | mod error; |
|
13 | 13 | mod exitcode; |
|
14 | 14 | mod ui; |
|
15 | 15 | use error::CommandError; |
|
16 | 16 | |
|
17 | 17 | fn add_global_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { |
|
18 | 18 | app.arg( |
|
19 | 19 | Arg::with_name("repository") |
|
20 | 20 | .help("repository root directory") |
|
21 | 21 | .short("-R") |
|
22 | 22 | .long("--repository") |
|
23 | 23 | .value_name("REPO") |
|
24 | 24 | .takes_value(true), |
|
25 | 25 | ) |
|
26 | 26 | .arg( |
|
27 | 27 | Arg::with_name("config") |
|
28 | 28 | .help("set/override config option (use 'section.name=value')") |
|
29 | 29 | .long("--config") |
|
30 | 30 | .value_name("CONFIG") |
|
31 | 31 | .takes_value(true) |
|
32 | 32 | // Ok: `--config section.key1=val --config section.key2=val2` |
|
33 | 33 | .multiple(true) |
|
34 | 34 | // Not ok: `--config section.key1=val section.key2=val2` |
|
35 | 35 | .number_of_values(1), |
|
36 | 36 | ) |
|
37 | 37 | } |
|
38 | 38 | |
|
39 | 39 | fn main_with_result(ui: &ui::Ui) -> Result<(), CommandError> { |
|
40 | 40 | env_logger::init(); |
|
41 | 41 | let app = App::new("rhg") |
|
42 | 42 | .setting(AppSettings::AllowInvalidUtf8) |
|
43 | 43 | .setting(AppSettings::SubcommandRequired) |
|
44 | 44 | .setting(AppSettings::VersionlessSubcommands) |
|
45 | 45 | .version("0.0.1"); |
|
46 | 46 | let app = add_global_args(app); |
|
47 | 47 | let app = add_subcommand_args(app); |
|
48 | 48 | |
|
49 | 49 | let matches = app.clone().get_matches_safe()?; |
|
50 | 50 | |
|
51 | 51 | let (subcommand_name, subcommand_matches) = matches.subcommand(); |
|
52 | 52 | let run = subcommand_run_fn(subcommand_name) |
|
53 | 53 | .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired"); |
|
54 | 54 | let subcommand_args = subcommand_matches |
|
55 | 55 | .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired"); |
|
56 | 56 | |
|
57 | 57 | // Global arguments can be in either based on e.g. `hg -R ./foo log` v.s. |
|
58 | 58 | // `hg log -R ./foo` |
|
59 | 59 | let value_of_global_arg = |name| { |
|
60 | 60 | subcommand_args |
|
61 | 61 | .value_of_os(name) |
|
62 | 62 | .or_else(|| matches.value_of_os(name)) |
|
63 | 63 | }; |
|
64 | 64 | // For arguments where multiple occurences are allowed, return a |
|
65 | 65 | // possibly-iterator of all values. |
|
66 | 66 | let values_of_global_arg = |name: &str| { |
|
67 | 67 | let a = matches.values_of_os(name).into_iter().flatten(); |
|
68 | 68 | let b = subcommand_args.values_of_os(name).into_iter().flatten(); |
|
69 | 69 | a.chain(b) |
|
70 | 70 | }; |
|
71 | 71 | |
|
72 | 72 | let config_args = values_of_global_arg("config") |
|
73 | // `get_bytes_from_path` works for OsStr the same as for Path | |
|
74 | .map(hg::utils::files::get_bytes_from_path); | |
|
73 | .map(hg::utils::files::get_bytes_from_os_str); | |
|
75 | 74 | let non_repo_config = &hg::config::Config::load(config_args)?; |
|
76 | 75 | |
|
77 | 76 | let repo_path = value_of_global_arg("repository").map(Path::new); |
|
78 | 77 | let repo = match Repo::find(non_repo_config, repo_path) { |
|
79 | 78 | Ok(repo) => Ok(repo), |
|
80 | 79 | Err(RepoError::NotFound { at }) if repo_path.is_none() => { |
|
81 | 80 | // Not finding a repo is not fatal yet, if `-R` was not given |
|
82 | 81 | Err(NoRepoInCwdError { cwd: at }) |
|
83 | 82 | } |
|
84 | 83 | Err(error) => return Err(error.into()), |
|
85 | 84 | }; |
|
86 | 85 | |
|
87 | 86 | run(&CliInvocation { |
|
88 | 87 | ui, |
|
89 | 88 | subcommand_args, |
|
90 | 89 | non_repo_config, |
|
91 | 90 | repo: repo.as_ref(), |
|
92 | 91 | }) |
|
93 | 92 | } |
|
94 | 93 | |
|
95 | 94 | fn main() { |
|
96 | 95 | let ui = ui::Ui::new(); |
|
97 | 96 | |
|
98 | 97 | let exit_code = match main_with_result(&ui) { |
|
99 | 98 | Ok(()) => exitcode::OK, |
|
100 | 99 | |
|
101 | 100 | // Exit with a specific code and no error message to let a potential |
|
102 | 101 | // wrapper script fallback to Python-based Mercurial. |
|
103 | 102 | Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED, |
|
104 | 103 | |
|
105 | 104 | Err(CommandError::Abort { message }) => { |
|
106 | 105 | if !message.is_empty() { |
|
107 | 106 | // Ignore errors when writing to stderr, we’re already exiting |
|
108 | 107 | // with failure code so there’s not much more we can do. |
|
109 | 108 | let _ = |
|
110 | 109 | ui.write_stderr(&format_bytes!(b"abort: {}\n", message)); |
|
111 | 110 | } |
|
112 | 111 | exitcode::ABORT |
|
113 | 112 | } |
|
114 | 113 | }; |
|
115 | 114 | std::process::exit(exit_code) |
|
116 | 115 | } |
|
117 | 116 | |
|
118 | 117 | macro_rules! subcommands { |
|
119 | 118 | ($( $command: ident )+) => { |
|
120 | 119 | mod commands { |
|
121 | 120 | $( |
|
122 | 121 | pub mod $command; |
|
123 | 122 | )+ |
|
124 | 123 | } |
|
125 | 124 | |
|
126 | 125 | fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { |
|
127 | 126 | app |
|
128 | 127 | $( |
|
129 | 128 | .subcommand(add_global_args(commands::$command::args())) |
|
130 | 129 | )+ |
|
131 | 130 | } |
|
132 | 131 | |
|
133 | 132 | pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>; |
|
134 | 133 | |
|
135 | 134 | fn subcommand_run_fn(name: &str) -> Option<RunFn> { |
|
136 | 135 | match name { |
|
137 | 136 | $( |
|
138 | 137 | stringify!($command) => Some(commands::$command::run), |
|
139 | 138 | )+ |
|
140 | 139 | _ => None, |
|
141 | 140 | } |
|
142 | 141 | } |
|
143 | 142 | }; |
|
144 | 143 | } |
|
145 | 144 | |
|
146 | 145 | subcommands! { |
|
147 | 146 | cat |
|
148 | 147 | debugdata |
|
149 | 148 | debugrequirements |
|
150 | 149 | files |
|
151 | 150 | root |
|
152 | 151 | config |
|
153 | 152 | } |
|
154 | 153 | pub struct CliInvocation<'a> { |
|
155 | 154 | ui: &'a Ui, |
|
156 | 155 | subcommand_args: &'a ArgMatches<'a>, |
|
157 | 156 | non_repo_config: &'a Config, |
|
158 | 157 | /// References inside `Result` is a bit peculiar but allow |
|
159 | 158 | /// `invocation.repo?` to work out with `&CliInvocation` since this |
|
160 | 159 | /// `Result` type is `Copy`. |
|
161 | 160 | repo: Result<&'a Repo, &'a NoRepoInCwdError>, |
|
162 | 161 | } |
|
163 | 162 | |
|
164 | 163 | struct NoRepoInCwdError { |
|
165 | 164 | cwd: PathBuf, |
|
166 | 165 | } |
|
167 | 166 | |
|
168 | 167 | impl CliInvocation<'_> { |
|
169 | 168 | fn config(&self) -> &Config { |
|
170 | 169 | if let Ok(repo) = self.repo { |
|
171 | 170 | repo.config() |
|
172 | 171 | } else { |
|
173 | 172 | self.non_repo_config |
|
174 | 173 | } |
|
175 | 174 | } |
|
176 | 175 | } |
General Comments 0
You need to be logged in to leave comments.
Login now