##// END OF EJS Templates
rust: Introduce a get_bytes_from_os_str utility function...
Simon Sapin -
r47338:d2e61f00 default
parent child Browse files
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_path;
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(&section, &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(&section, &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(&section, &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 path.as_ref().as_os_str().as_bytes().to_vec()
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