##// END OF EJS Templates
rust: Add a log file rotation utility...
Simon Sapin -
r47341:1f55cd5b default
parent child Browse files
Show More
@@ -0,0 +1,101 b''
1 use crate::errors::{HgError, HgResultExt, IoErrorContext, IoResultExt};
2 use crate::repo::Vfs;
3 use std::io::Write;
4
5 /// An utility to append to a log file with the given name, and optionally
6 /// rotate it after it reaches a certain maximum size.
7 ///
8 /// Rotation works by renaming "example.log" to "example.log.1", after renaming
9 /// "example.log.1" to "example.log.2" etc up to the given maximum number of
10 /// files.
11 pub struct LogFile<'a> {
12 vfs: Vfs<'a>,
13 name: &'a str,
14 max_size: Option<u64>,
15 max_files: u32,
16 }
17
18 impl<'a> LogFile<'a> {
19 pub fn new(vfs: Vfs<'a>, name: &'a str) -> Self {
20 Self {
21 vfs,
22 name,
23 max_size: None,
24 max_files: 0,
25 }
26 }
27
28 /// Rotate before writing to a log file that was already larger than the
29 /// given size, in bytes. `None` disables rotation.
30 pub fn max_size(mut self, value: Option<u64>) -> Self {
31 self.max_size = value;
32 self
33 }
34
35 /// Keep this many rotated files `{name}.1` up to `{name}.{max}`, in
36 /// addition to the original `{name}` file.
37 pub fn max_files(mut self, value: u32) -> Self {
38 self.max_files = value;
39 self
40 }
41
42 /// Append the given `bytes` as-is to the log file, after rotating if
43 /// needed.
44 ///
45 /// No trailing newline is added. Make sure to include one in `bytes` if
46 /// desired.
47 pub fn write(&self, bytes: &[u8]) -> Result<(), HgError> {
48 let path = self.vfs.join(self.name);
49 let context = || IoErrorContext::WritingFile(path.clone());
50 let open = || {
51 std::fs::OpenOptions::new()
52 .create(true)
53 .append(true)
54 .open(&path)
55 .with_context(context)
56 };
57 let mut file = open()?;
58 if let Some(max_size) = self.max_size {
59 if file.metadata().with_context(context)?.len() >= max_size {
60 // For example with `max_files == 5`, the first iteration of
61 // this loop has `i == 4` and renames `{name}.4` to `{name}.5`.
62 // The last iteration renames `{name}.1` to
63 // `{name}.2`
64 for i in (1..self.max_files).rev() {
65 self.vfs
66 .rename(
67 format!("{}.{}", self.name, i),
68 format!("{}.{}", self.name, i + 1),
69 )
70 .io_not_found_as_none()?;
71 }
72 // Then rename `{name}` to `{name}.1`. This is the
73 // previously-opened `file`.
74 self.vfs
75 .rename(self.name, format!("{}.1", self.name))
76 .io_not_found_as_none()?;
77 // Finally, create a new `{name}` file and replace our `file`
78 // handle.
79 file = open()?;
80 }
81 }
82 file.write_all(bytes).with_context(context)?;
83 file.sync_all().with_context(context)
84 }
85 }
86
87 #[test]
88 fn test_rotation() {
89 let temp = tempfile::tempdir().unwrap();
90 let vfs = Vfs { base: temp.path() };
91 let logger = LogFile::new(vfs, "log").max_size(Some(3)).max_files(2);
92 logger.write(b"one\n").unwrap();
93 logger.write(b"two\n").unwrap();
94 logger.write(b"3\n").unwrap();
95 logger.write(b"four\n").unwrap();
96 logger.write(b"five\n").unwrap();
97 assert_eq!(vfs.read("log").unwrap(), b"five\n");
98 assert_eq!(vfs.read("log.1").unwrap(), b"3\nfour\n");
99 assert_eq!(vfs.read("log.2").unwrap(), b"two\n");
100 assert!(vfs.read("log.3").io_not_found_as_none().unwrap().is_none());
101 }
@@ -1,437 +1,438 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, ConfigOrigin, ConfigValue,
13 13 };
14 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 use std::str;
19 19
20 20 use crate::errors::{HgResultExt, IoResultExt};
21 21
22 22 /// Holds the config values for the current repository
23 23 /// TODO update this docstring once we support more sources
24 24 pub struct Config {
25 25 layers: Vec<layer::ConfigLayer>,
26 26 }
27 27
28 28 impl DisplayBytes for Config {
29 29 fn display_bytes(
30 30 &self,
31 31 out: &mut dyn std::io::Write,
32 32 ) -> std::io::Result<()> {
33 33 for (index, layer) in self.layers.iter().rev().enumerate() {
34 34 write_bytes!(
35 35 out,
36 36 b"==== Layer {} (trusted: {}) ====\n{}",
37 37 index,
38 38 if layer.trusted {
39 39 &b"yes"[..]
40 40 } else {
41 41 &b"no"[..]
42 42 },
43 43 layer
44 44 )?;
45 45 }
46 46 Ok(())
47 47 }
48 48 }
49 49
50 50 pub enum ConfigSource {
51 51 /// Absolute path to a config file
52 52 AbsPath(PathBuf),
53 53 /// Already parsed (from the CLI, env, Python resources, etc.)
54 54 Parsed(layer::ConfigLayer),
55 55 }
56 56
57 57 #[derive(Debug)]
58 58 pub struct ConfigValueParseError {
59 59 pub origin: ConfigOrigin,
60 60 pub line: Option<usize>,
61 61 pub section: Vec<u8>,
62 62 pub item: Vec<u8>,
63 63 pub value: Vec<u8>,
64 64 pub expected_type: &'static str,
65 65 }
66 66
67 67 pub fn parse_bool(v: &[u8]) -> Option<bool> {
68 68 match v.to_ascii_lowercase().as_slice() {
69 69 b"1" | b"yes" | b"true" | b"on" | b"always" => Some(true),
70 70 b"0" | b"no" | b"false" | b"off" | b"never" => Some(false),
71 71 _ => None,
72 72 }
73 73 }
74 74
75 75 pub fn parse_byte_size(value: &[u8]) -> Option<u64> {
76 76 let value = str::from_utf8(value).ok()?.to_ascii_lowercase();
77 77 const UNITS: &[(&str, u64)] = &[
78 78 ("g", 1 << 30),
79 79 ("gb", 1 << 30),
80 80 ("m", 1 << 20),
81 81 ("mb", 1 << 20),
82 82 ("k", 1 << 10),
83 83 ("kb", 1 << 10),
84 84 ("b", 1 << 0), // Needs to be last
85 85 ];
86 86 for &(unit, multiplier) in UNITS {
87 87 // TODO: use `value.strip_suffix(unit)` when we require Rust 1.45+
88 88 if value.ends_with(unit) {
89 89 let value_before_unit = &value[..value.len() - unit.len()];
90 90 let float: f64 = value_before_unit.trim().parse().ok()?;
91 91 if float >= 0.0 {
92 92 return Some((float * multiplier as f64).round() as u64);
93 93 } else {
94 94 return None;
95 95 }
96 96 }
97 97 }
98 98 value.parse().ok()
99 99 }
100 100
101 101 impl Config {
102 102 /// Load system and user configuration from various files.
103 103 ///
104 104 /// This is also affected by some environment variables.
105 105 pub fn load(
106 106 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
107 107 ) -> Result<Self, ConfigError> {
108 108 let mut config = Self { layers: Vec::new() };
109 109 let opt_rc_path = env::var_os("HGRCPATH");
110 110 // HGRCPATH replaces system config
111 111 if opt_rc_path.is_none() {
112 112 config.add_system_config()?
113 113 }
114 114 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
115 115 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
116 116 config.add_for_environment_variable("PAGER", b"pager", b"pager");
117 117 // HGRCPATH replaces user config
118 118 if opt_rc_path.is_none() {
119 119 config.add_user_config()?
120 120 }
121 121 if let Some(rc_path) = &opt_rc_path {
122 122 for path in env::split_paths(rc_path) {
123 123 if !path.as_os_str().is_empty() {
124 124 if path.is_dir() {
125 125 config.add_trusted_dir(&path)?
126 126 } else {
127 127 config.add_trusted_file(&path)?
128 128 }
129 129 }
130 130 }
131 131 }
132 132 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
133 133 config.layers.push(layer)
134 134 }
135 135 Ok(config)
136 136 }
137 137
138 138 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
139 139 if let Some(entries) = std::fs::read_dir(path)
140 .for_file(path)
140 .when_reading_file(path)
141 141 .io_not_found_as_none()?
142 142 {
143 143 for entry in entries {
144 let file_path = entry.for_file(path)?.path();
144 let file_path = entry.when_reading_file(path)?.path();
145 145 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
146 146 self.add_trusted_file(&file_path)?
147 147 }
148 148 }
149 149 }
150 150 Ok(())
151 151 }
152 152
153 153 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
154 if let Some(data) =
155 std::fs::read(path).for_file(path).io_not_found_as_none()?
154 if let Some(data) = std::fs::read(path)
155 .when_reading_file(path)
156 .io_not_found_as_none()?
156 157 {
157 158 self.layers.extend(ConfigLayer::parse(path, &data)?)
158 159 }
159 160 Ok(())
160 161 }
161 162
162 163 fn add_for_environment_variable(
163 164 &mut self,
164 165 var: &str,
165 166 section: &[u8],
166 167 key: &[u8],
167 168 ) {
168 169 if let Some(value) = env::var_os(var) {
169 170 let origin = layer::ConfigOrigin::Environment(var.into());
170 171 let mut layer = ConfigLayer::new(origin);
171 172 layer.add(
172 173 section.to_owned(),
173 174 key.to_owned(),
174 175 get_bytes_from_os_str(value),
175 176 None,
176 177 );
177 178 self.layers.push(layer)
178 179 }
179 180 }
180 181
181 182 #[cfg(unix)] // TODO: other platforms
182 183 fn add_system_config(&mut self) -> Result<(), ConfigError> {
183 184 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
184 185 let etc = prefix.join("etc").join("mercurial");
185 186 self.add_trusted_file(&etc.join("hgrc"))?;
186 187 self.add_trusted_dir(&etc.join("hgrc.d"))
187 188 };
188 189 let root = Path::new("/");
189 190 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
190 191 // instead? TODO: can this be a relative path?
191 192 let hg = crate::utils::current_exe()?;
192 193 // TODO: this order (per-installation then per-system) matches
193 194 // `systemrcpath()` in `mercurial/scmposix.py`, but
194 195 // `mercurial/helptext/config.txt` suggests it should be reversed
195 196 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
196 197 if installation_prefix != root {
197 198 add_for_prefix(&installation_prefix)?
198 199 }
199 200 }
200 201 add_for_prefix(root)?;
201 202 Ok(())
202 203 }
203 204
204 205 #[cfg(unix)] // TODO: other plateforms
205 206 fn add_user_config(&mut self) -> Result<(), ConfigError> {
206 207 let opt_home = home::home_dir();
207 208 if let Some(home) = &opt_home {
208 209 self.add_trusted_file(&home.join(".hgrc"))?
209 210 }
210 211 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
211 212 if !darwin {
212 213 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
213 214 .map(PathBuf::from)
214 215 .or_else(|| opt_home.map(|home| home.join(".config")))
215 216 {
216 217 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
217 218 }
218 219 }
219 220 Ok(())
220 221 }
221 222
222 223 /// Loads in order, which means that the precedence is the same
223 224 /// as the order of `sources`.
224 225 pub fn load_from_explicit_sources(
225 226 sources: Vec<ConfigSource>,
226 227 ) -> Result<Self, ConfigError> {
227 228 let mut layers = vec![];
228 229
229 230 for source in sources.into_iter() {
230 231 match source {
231 232 ConfigSource::Parsed(c) => layers.push(c),
232 233 ConfigSource::AbsPath(c) => {
233 234 // TODO check if it should be trusted
234 235 // mercurial/ui.py:427
235 236 let data = match std::fs::read(&c) {
236 237 Err(_) => continue, // same as the python code
237 238 Ok(data) => data,
238 239 };
239 240 layers.extend(ConfigLayer::parse(&c, &data)?)
240 241 }
241 242 }
242 243 }
243 244
244 245 Ok(Config { layers })
245 246 }
246 247
247 248 /// Loads the per-repository config into a new `Config` which is combined
248 249 /// with `self`.
249 250 pub(crate) fn combine_with_repo(
250 251 &self,
251 252 repo_config_files: &[PathBuf],
252 253 ) -> Result<Self, ConfigError> {
253 254 let (cli_layers, other_layers) = self
254 255 .layers
255 256 .iter()
256 257 .cloned()
257 258 .partition(ConfigLayer::is_from_command_line);
258 259
259 260 let mut repo_config = Self {
260 261 layers: other_layers,
261 262 };
262 263 for path in repo_config_files {
263 264 // TODO: check if this file should be trusted:
264 265 // `mercurial/ui.py:427`
265 266 repo_config.add_trusted_file(path)?;
266 267 }
267 268 repo_config.layers.extend(cli_layers);
268 269 Ok(repo_config)
269 270 }
270 271
271 272 fn get_parse<'config, T: 'config>(
272 273 &'config self,
273 274 section: &[u8],
274 275 item: &[u8],
275 276 expected_type: &'static str,
276 277 parse: impl Fn(&'config [u8]) -> Option<T>,
277 278 ) -> Result<Option<T>, ConfigValueParseError> {
278 279 match self.get_inner(&section, &item) {
279 280 Some((layer, v)) => match parse(&v.bytes) {
280 281 Some(b) => Ok(Some(b)),
281 282 None => Err(ConfigValueParseError {
282 283 origin: layer.origin.to_owned(),
283 284 line: v.line,
284 285 value: v.bytes.to_owned(),
285 286 section: section.to_owned(),
286 287 item: item.to_owned(),
287 288 expected_type,
288 289 }),
289 290 },
290 291 None => Ok(None),
291 292 }
292 293 }
293 294
294 295 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
295 296 /// Otherwise, returns an `Ok(value)` if found, or `None`.
296 297 pub fn get_str(
297 298 &self,
298 299 section: &[u8],
299 300 item: &[u8],
300 301 ) -> Result<Option<&str>, ConfigValueParseError> {
301 302 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
302 303 str::from_utf8(value).ok()
303 304 })
304 305 }
305 306
306 307 /// Returns an `Err` if the first value found is not a valid unsigned
307 308 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
308 309 pub fn get_u32(
309 310 &self,
310 311 section: &[u8],
311 312 item: &[u8],
312 313 ) -> Result<Option<u32>, ConfigValueParseError> {
313 314 self.get_parse(section, item, "valid integer", |value| {
314 315 str::from_utf8(value).ok()?.parse().ok()
315 316 })
316 317 }
317 318
318 319 /// Returns an `Err` if the first value found is not a valid file size
319 320 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
320 321 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
321 322 pub fn get_byte_size(
322 323 &self,
323 324 section: &[u8],
324 325 item: &[u8],
325 326 ) -> Result<Option<u64>, ConfigValueParseError> {
326 327 self.get_parse(section, item, "byte quantity", parse_byte_size)
327 328 }
328 329
329 330 /// Returns an `Err` if the first value found is not a valid boolean.
330 331 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
331 332 /// found, or `None`.
332 333 pub fn get_option(
333 334 &self,
334 335 section: &[u8],
335 336 item: &[u8],
336 337 ) -> Result<Option<bool>, ConfigValueParseError> {
337 338 self.get_parse(section, item, "boolean", parse_bool)
338 339 }
339 340
340 341 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
341 342 /// if the value is not found, an `Err` if it's not a valid boolean.
342 343 pub fn get_bool(
343 344 &self,
344 345 section: &[u8],
345 346 item: &[u8],
346 347 ) -> Result<bool, ConfigValueParseError> {
347 348 Ok(self.get_option(section, item)?.unwrap_or(false))
348 349 }
349 350
350 351 /// Returns the raw value bytes of the first one found, or `None`.
351 352 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
352 353 self.get_inner(section, item)
353 354 .map(|(_, value)| value.bytes.as_ref())
354 355 }
355 356
356 357 /// Returns the layer and the value of the first one found, or `None`.
357 358 fn get_inner(
358 359 &self,
359 360 section: &[u8],
360 361 item: &[u8],
361 362 ) -> Option<(&ConfigLayer, &ConfigValue)> {
362 363 for layer in self.layers.iter().rev() {
363 364 if !layer.trusted {
364 365 continue;
365 366 }
366 367 if let Some(v) = layer.get(&section, &item) {
367 368 return Some((&layer, v));
368 369 }
369 370 }
370 371 None
371 372 }
372 373
373 374 /// Get raw values bytes from all layers (even untrusted ones) in order
374 375 /// of precedence.
375 376 #[cfg(test)]
376 377 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
377 378 let mut res = vec![];
378 379 for layer in self.layers.iter().rev() {
379 380 if let Some(v) = layer.get(&section, &item) {
380 381 res.push(v.bytes.as_ref());
381 382 }
382 383 }
383 384 res
384 385 }
385 386 }
386 387
387 388 #[cfg(test)]
388 389 mod tests {
389 390 use super::*;
390 391 use pretty_assertions::assert_eq;
391 392 use std::fs::File;
392 393 use std::io::Write;
393 394
394 395 #[test]
395 396 fn test_include_layer_ordering() {
396 397 let tmpdir = tempfile::tempdir().unwrap();
397 398 let tmpdir_path = tmpdir.path();
398 399 let mut included_file =
399 400 File::create(&tmpdir_path.join("included.rc")).unwrap();
400 401
401 402 included_file.write_all(b"[section]\nitem=value1").unwrap();
402 403 let base_config_path = tmpdir_path.join("base.rc");
403 404 let mut config_file = File::create(&base_config_path).unwrap();
404 405 let data =
405 406 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
406 407 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
407 408 config_file.write_all(data).unwrap();
408 409
409 410 let sources = vec![ConfigSource::AbsPath(base_config_path)];
410 411 let config = Config::load_from_explicit_sources(sources)
411 412 .expect("expected valid config");
412 413
413 414 let (_, value) = config.get_inner(b"section", b"item").unwrap();
414 415 assert_eq!(
415 416 value,
416 417 &ConfigValue {
417 418 bytes: b"value2".to_vec(),
418 419 line: Some(4)
419 420 }
420 421 );
421 422
422 423 let value = config.get(b"section", b"item").unwrap();
423 424 assert_eq!(value, b"value2",);
424 425 assert_eq!(
425 426 config.get_all(b"section", b"item"),
426 427 [b"value2", b"value1", b"value0"]
427 428 );
428 429
429 430 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
430 431 assert_eq!(
431 432 config.get_byte_size(b"section2", b"size").unwrap(),
432 433 Some(1024 + 512)
433 434 );
434 435 assert!(config.get_u32(b"section2", b"not-count").is_err());
435 436 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
436 437 }
437 438 }
@@ -1,291 +1,292 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 54 /// Parse `--config` CLI arguments and return a layer if there’s any
55 55 pub(crate) fn parse_cli_args(
56 56 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
57 57 ) -> Result<Option<Self>, ConfigError> {
58 58 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
59 59 use crate::utils::SliceExt;
60 60
61 61 let (section_and_item, value) = arg.split_2(b'=')?;
62 62 let (section, item) = section_and_item.trim().split_2(b'.')?;
63 63 Some((
64 64 section.to_owned(),
65 65 item.to_owned(),
66 66 value.trim().to_owned(),
67 67 ))
68 68 }
69 69
70 70 let mut layer = Self::new(ConfigOrigin::CommandLine);
71 71 for arg in cli_config_args {
72 72 let arg = arg.as_ref();
73 73 if let Some((section, item, value)) = parse_one(arg) {
74 74 layer.add(section, item, value, None);
75 75 } else {
76 76 Err(HgError::abort(format!(
77 77 "malformed --config option: \"{}\" \
78 78 (use --config section.name=value)",
79 79 String::from_utf8_lossy(arg),
80 80 )))?
81 81 }
82 82 }
83 83 if layer.sections.is_empty() {
84 84 Ok(None)
85 85 } else {
86 86 Ok(Some(layer))
87 87 }
88 88 }
89 89
90 90 /// Returns whether this layer comes from `--config` CLI arguments
91 91 pub(crate) fn is_from_command_line(&self) -> bool {
92 92 if let ConfigOrigin::CommandLine = self.origin {
93 93 true
94 94 } else {
95 95 false
96 96 }
97 97 }
98 98
99 99 /// Add an entry to the config, overwriting the old one if already present.
100 100 pub fn add(
101 101 &mut self,
102 102 section: Vec<u8>,
103 103 item: Vec<u8>,
104 104 value: Vec<u8>,
105 105 line: Option<usize>,
106 106 ) {
107 107 self.sections
108 108 .entry(section)
109 109 .or_insert_with(|| HashMap::new())
110 110 .insert(item, ConfigValue { bytes: value, line });
111 111 }
112 112
113 113 /// Returns the config value in `<section>.<item>` if it exists
114 114 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
115 115 Some(self.sections.get(section)?.get(item)?)
116 116 }
117 117
118 118 pub fn is_empty(&self) -> bool {
119 119 self.sections.is_empty()
120 120 }
121 121
122 122 /// Returns a `Vec` of layers in order of precedence (so, in read order),
123 123 /// recursively parsing the `%include` directives if any.
124 124 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
125 125 let mut layers = vec![];
126 126
127 127 // Discard byte order mark if any
128 128 let data = if data.starts_with(b"\xef\xbb\xbf") {
129 129 &data[3..]
130 130 } else {
131 131 data
132 132 };
133 133
134 134 // TODO check if it's trusted
135 135 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
136 136
137 137 let mut lines_iter =
138 138 data.split(|b| *b == b'\n').enumerate().peekable();
139 139 let mut section = b"".to_vec();
140 140
141 141 while let Some((index, bytes)) = lines_iter.next() {
142 142 if let Some(m) = INCLUDE_RE.captures(&bytes) {
143 143 let filename_bytes = &m[1];
144 144 // `Path::parent` only fails for the root directory,
145 145 // which `src` can’t be since we’ve managed to open it as a
146 146 // file.
147 147 let dir = src
148 148 .parent()
149 149 .expect("Path::parent fail on a file we’ve read");
150 150 // `Path::join` with an absolute argument correctly ignores the
151 151 // base path
152 152 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
153 let data = std::fs::read(&filename).for_file(&filename)?;
153 let data =
154 std::fs::read(&filename).when_reading_file(&filename)?;
154 155 layers.push(current_layer);
155 156 layers.extend(Self::parse(&filename, &data)?);
156 157 current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
157 158 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
158 159 } else if let Some(m) = SECTION_RE.captures(&bytes) {
159 160 section = m[1].to_vec();
160 161 } else if let Some(m) = ITEM_RE.captures(&bytes) {
161 162 let item = m[1].to_vec();
162 163 let mut value = m[2].to_vec();
163 164 loop {
164 165 match lines_iter.peek() {
165 166 None => break,
166 167 Some((_, v)) => {
167 168 if let Some(_) = COMMENT_RE.captures(&v) {
168 169 } else if let Some(_) = CONT_RE.captures(&v) {
169 170 value.extend(b"\n");
170 171 value.extend(&m[1]);
171 172 } else {
172 173 break;
173 174 }
174 175 }
175 176 };
176 177 lines_iter.next();
177 178 }
178 179 current_layer.add(
179 180 section.clone(),
180 181 item,
181 182 value,
182 183 Some(index + 1),
183 184 );
184 185 } else if let Some(m) = UNSET_RE.captures(&bytes) {
185 186 if let Some(map) = current_layer.sections.get_mut(&section) {
186 187 map.remove(&m[1]);
187 188 }
188 189 } else {
189 190 return Err(ConfigParseError {
190 191 origin: ConfigOrigin::File(src.to_owned()),
191 192 line: Some(index + 1),
192 193 bytes: bytes.to_owned(),
193 194 }
194 195 .into());
195 196 }
196 197 }
197 198 if !current_layer.is_empty() {
198 199 layers.push(current_layer);
199 200 }
200 201 Ok(layers)
201 202 }
202 203 }
203 204
204 205 impl DisplayBytes for ConfigLayer {
205 206 fn display_bytes(
206 207 &self,
207 208 out: &mut dyn std::io::Write,
208 209 ) -> std::io::Result<()> {
209 210 let mut sections: Vec<_> = self.sections.iter().collect();
210 211 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
211 212
212 213 for (section, items) in sections.into_iter() {
213 214 let mut items: Vec<_> = items.into_iter().collect();
214 215 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
215 216
216 217 for (item, config_entry) in items {
217 218 write_bytes!(
218 219 out,
219 220 b"{}.{}={} # {}\n",
220 221 section,
221 222 item,
222 223 &config_entry.bytes,
223 224 &self.origin,
224 225 )?
225 226 }
226 227 }
227 228 Ok(())
228 229 }
229 230 }
230 231
231 232 /// Mapping of section item to value.
232 233 /// In the following:
233 234 /// ```text
234 235 /// [ui]
235 236 /// paginate=no
236 237 /// ```
237 238 /// "paginate" is the section item and "no" the value.
238 239 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
239 240
240 241 #[derive(Clone, Debug, PartialEq)]
241 242 pub struct ConfigValue {
242 243 /// The raw bytes of the value (be it from the CLI, env or from a file)
243 244 pub bytes: Vec<u8>,
244 245 /// Only present if the value comes from a file, 1-indexed.
245 246 pub line: Option<usize>,
246 247 }
247 248
248 249 #[derive(Clone, Debug)]
249 250 pub enum ConfigOrigin {
250 251 /// From a configuration file
251 252 File(PathBuf),
252 253 /// From a `--config` CLI argument
253 254 CommandLine,
254 255 /// From environment variables like `$PAGER` or `$EDITOR`
255 256 Environment(Vec<u8>),
256 257 /* TODO cli
257 258 * TODO defaults (configitems.py)
258 259 * TODO extensions
259 260 * TODO Python resources?
260 261 * Others? */
261 262 }
262 263
263 264 impl DisplayBytes for ConfigOrigin {
264 265 fn display_bytes(
265 266 &self,
266 267 out: &mut dyn std::io::Write,
267 268 ) -> std::io::Result<()> {
268 269 match self {
269 270 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
270 271 ConfigOrigin::CommandLine => out.write_all(b"--config"),
271 272 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
272 273 }
273 274 }
274 275 }
275 276
276 277 #[derive(Debug)]
277 278 pub struct ConfigParseError {
278 279 pub origin: ConfigOrigin,
279 280 pub line: Option<usize>,
280 281 pub bytes: Vec<u8>,
281 282 }
282 283
283 284 #[derive(Debug, derive_more::From)]
284 285 pub enum ConfigError {
285 286 Parse(ConfigParseError),
286 287 Other(HgError),
287 288 }
288 289
289 290 fn make_regex(pattern: &'static str) -> Regex {
290 291 Regex::new(pattern).expect("expected a valid regex")
291 292 }
@@ -1,161 +1,192 b''
1 1 use crate::config::ConfigValueParseError;
2 2 use std::fmt;
3 3
4 4 /// Common error cases that can happen in many different APIs
5 5 #[derive(Debug, derive_more::From)]
6 6 pub enum HgError {
7 7 IoError {
8 8 error: std::io::Error,
9 9 context: IoErrorContext,
10 10 },
11 11
12 12 /// A file under `.hg/` normally only written by Mercurial is not in the
13 13 /// expected format. This indicates a bug in Mercurial, filesystem
14 14 /// corruption, or hardware failure.
15 15 ///
16 16 /// The given string is a short explanation for users, not intended to be
17 17 /// machine-readable.
18 18 CorruptedRepository(String),
19 19
20 20 /// The respository or requested operation involves a feature not
21 21 /// supported by the Rust implementation. Falling back to the Python
22 22 /// implementation may or may not work.
23 23 ///
24 24 /// The given string is a short explanation for users, not intended to be
25 25 /// machine-readable.
26 26 UnsupportedFeature(String),
27 27
28 28 /// Operation cannot proceed for some other reason.
29 29 ///
30 30 /// The given string is a short explanation for users, not intended to be
31 31 /// machine-readable.
32 32 Abort(String),
33 33
34 34 /// A configuration value is not in the expected syntax.
35 35 ///
36 36 /// These errors can happen in many places in the code because values are
37 37 /// parsed lazily as the file-level parser does not know the expected type
38 38 /// and syntax of each value.
39 39 #[from]
40 40 ConfigValueParseError(ConfigValueParseError),
41 41 }
42 42
43 43 /// Details about where an I/O error happened
44 #[derive(Debug, derive_more::From)]
44 #[derive(Debug)]
45 45 pub enum IoErrorContext {
46 /// A filesystem operation for the given file
47 #[from]
48 File(std::path::PathBuf),
46 ReadingFile(std::path::PathBuf),
47 WritingFile(std::path::PathBuf),
48 RemovingFile(std::path::PathBuf),
49 RenamingFile {
50 from: std::path::PathBuf,
51 to: std::path::PathBuf,
52 },
49 53 /// `std::env::current_dir`
50 54 CurrentDir,
51 55 /// `std::env::current_exe`
52 56 CurrentExe,
53 57 }
54 58
55 59 impl HgError {
56 60 pub fn corrupted(explanation: impl Into<String>) -> Self {
57 61 // TODO: capture a backtrace here and keep it in the error value
58 62 // to aid debugging?
59 63 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
60 64 HgError::CorruptedRepository(explanation.into())
61 65 }
62 66
63 67 pub fn unsupported(explanation: impl Into<String>) -> Self {
64 68 HgError::UnsupportedFeature(explanation.into())
65 69 }
66 70 pub fn abort(explanation: impl Into<String>) -> Self {
67 71 HgError::Abort(explanation.into())
68 72 }
69 73 }
70 74
71 75 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
72 76 impl fmt::Display for HgError {
73 77 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74 78 match self {
75 79 HgError::Abort(explanation) => write!(f, "{}", explanation),
76 80 HgError::IoError { error, context } => {
77 81 write!(f, "{}: {}", error, context)
78 82 }
79 83 HgError::CorruptedRepository(explanation) => {
80 84 write!(f, "corrupted repository: {}", explanation)
81 85 }
82 86 HgError::UnsupportedFeature(explanation) => {
83 87 write!(f, "unsupported feature: {}", explanation)
84 88 }
85 89 HgError::ConfigValueParseError(ConfigValueParseError {
86 90 origin: _,
87 91 line: _,
88 92 section,
89 93 item,
90 94 value,
91 95 expected_type,
92 96 }) => {
93 97 // TODO: add origin and line number information, here and in
94 98 // corresponding python code
95 99 write!(
96 100 f,
97 101 "config error: {}.{} is not a {} ('{}')",
98 102 String::from_utf8_lossy(section),
99 103 String::from_utf8_lossy(item),
100 104 expected_type,
101 105 String::from_utf8_lossy(value)
102 106 )
103 107 }
104 108 }
105 109 }
106 110 }
107 111
108 112 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
109 113 impl fmt::Display for IoErrorContext {
110 114 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
111 115 match self {
112 IoErrorContext::File(path) => path.display().fmt(f),
113 IoErrorContext::CurrentDir => f.write_str("current directory"),
114 IoErrorContext::CurrentExe => f.write_str("current executable"),
116 IoErrorContext::ReadingFile(path) => {
117 write!(f, "when reading {}", path.display())
118 }
119 IoErrorContext::WritingFile(path) => {
120 write!(f, "when writing {}", path.display())
121 }
122 IoErrorContext::RemovingFile(path) => {
123 write!(f, "when removing {}", path.display())
124 }
125 IoErrorContext::RenamingFile { from, to } => write!(
126 f,
127 "when renaming {} to {}",
128 from.display(),
129 to.display()
130 ),
131 IoErrorContext::CurrentDir => write!(f, "current directory"),
132 IoErrorContext::CurrentExe => write!(f, "current executable"),
115 133 }
116 134 }
117 135 }
118 136
119 137 pub trait IoResultExt<T> {
120 /// Annotate a possible I/O error as related to a file at the given path.
138 /// Annotate a possible I/O error as related to a reading a file at the
139 /// given path.
121 140 ///
122 /// This allows printing something like β€œFile not found: example.txt”
123 /// instead of just β€œFile not found”.
141 /// This allows printing something like β€œFile not found when reading
142 /// example.txt” instead of just β€œFile not found”.
124 143 ///
125 144 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
126 fn for_file(self, path: &std::path::Path) -> Result<T, HgError>;
145 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError>;
146
147 fn with_context(
148 self,
149 context: impl FnOnce() -> IoErrorContext,
150 ) -> Result<T, HgError>;
127 151 }
128 152
129 153 impl<T> IoResultExt<T> for std::io::Result<T> {
130 fn for_file(self, path: &std::path::Path) -> Result<T, HgError> {
154 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError> {
155 self.with_context(|| IoErrorContext::ReadingFile(path.to_owned()))
156 }
157
158 fn with_context(
159 self,
160 context: impl FnOnce() -> IoErrorContext,
161 ) -> Result<T, HgError> {
131 162 self.map_err(|error| HgError::IoError {
132 163 error,
133 context: IoErrorContext::File(path.to_owned()),
164 context: context(),
134 165 })
135 166 }
136 167 }
137 168
138 169 pub trait HgResultExt<T> {
139 170 /// Handle missing files separately from other I/O error cases.
140 171 ///
141 172 /// Wraps the `Ok` type in an `Option`:
142 173 ///
143 174 /// * `Ok(x)` becomes `Ok(Some(x))`
144 175 /// * An I/O "not found" error becomes `Ok(None)`
145 176 /// * Other errors are unchanged
146 177 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
147 178 }
148 179
149 180 impl<T> HgResultExt<T> for Result<T, HgError> {
150 181 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
151 182 match self {
152 183 Ok(x) => Ok(Some(x)),
153 184 Err(HgError::IoError { error, .. })
154 185 if error.kind() == std::io::ErrorKind::NotFound =>
155 186 {
156 187 Ok(None)
157 188 }
158 189 Err(other_error) => Err(other_error),
159 190 }
160 191 }
161 192 }
@@ -1,120 +1,121 b''
1 1 // Copyright 2018-2020 Georges Racinet <georges.racinet@octobus.net>
2 2 // and Mercurial contributors
3 3 //
4 4 // This software may be used and distributed according to the terms of the
5 5 // GNU General Public License version 2 or any later version.
6 6
7 7 mod ancestors;
8 8 pub mod dagops;
9 9 pub mod errors;
10 10 pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors};
11 11 mod dirstate;
12 12 pub mod discovery;
13 13 pub mod requirements;
14 14 pub mod testing; // unconditionally built, for use from integration tests
15 15 pub use dirstate::{
16 16 dirs_multiset::{DirsMultiset, DirsMultisetIter},
17 17 dirstate_map::DirstateMap,
18 18 parsers::{pack_dirstate, parse_dirstate, PARENT_SIZE},
19 19 status::{
20 20 status, BadMatch, BadType, DirstateStatus, StatusError, StatusOptions,
21 21 },
22 22 CopyMap, CopyMapIter, DirstateEntry, DirstateParents, EntryState,
23 23 StateMap, StateMapIter,
24 24 };
25 25 pub mod copy_tracing;
26 26 mod filepatterns;
27 27 pub mod matchers;
28 28 pub mod repo;
29 29 pub mod revlog;
30 30 pub use revlog::*;
31 31 pub mod config;
32 pub mod logging;
32 33 pub mod operations;
33 34 pub mod revset;
34 35 pub mod utils;
35 36
36 37 use crate::utils::hg_path::{HgPathBuf, HgPathError};
37 38 pub use filepatterns::{
38 39 parse_pattern_syntax, read_pattern_file, IgnorePattern,
39 40 PatternFileWarning, PatternSyntax,
40 41 };
41 42 use std::collections::HashMap;
42 43 use std::fmt;
43 44 use twox_hash::RandomXxHashBuilder64;
44 45
45 46 /// This is a contract between the `micro-timer` crate and us, to expose
46 47 /// the `log` crate as `crate::log`.
47 48 use log;
48 49
49 50 pub type LineNumber = usize;
50 51
51 52 /// Rust's default hasher is too slow because it tries to prevent collision
52 53 /// attacks. We are not concerned about those: if an ill-minded person has
53 54 /// write access to your repository, you have other issues.
54 55 pub type FastHashMap<K, V> = HashMap<K, V, RandomXxHashBuilder64>;
55 56
56 57 #[derive(Debug, PartialEq)]
57 58 pub enum DirstateMapError {
58 59 PathNotFound(HgPathBuf),
59 60 EmptyPath,
60 61 InvalidPath(HgPathError),
61 62 }
62 63
63 64 impl fmt::Display for DirstateMapError {
64 65 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65 66 match self {
66 67 DirstateMapError::PathNotFound(_) => {
67 68 f.write_str("expected a value, found none")
68 69 }
69 70 DirstateMapError::EmptyPath => {
70 71 f.write_str("Overflow in dirstate.")
71 72 }
72 73 DirstateMapError::InvalidPath(path_error) => path_error.fmt(f),
73 74 }
74 75 }
75 76 }
76 77
77 78 #[derive(Debug, derive_more::From)]
78 79 pub enum DirstateError {
79 80 Map(DirstateMapError),
80 81 Common(errors::HgError),
81 82 }
82 83
83 84 #[derive(Debug, derive_more::From)]
84 85 pub enum PatternError {
85 86 #[from]
86 87 Path(HgPathError),
87 88 UnsupportedSyntax(String),
88 89 UnsupportedSyntaxInFile(String, String, usize),
89 90 TooLong(usize),
90 91 #[from]
91 92 IO(std::io::Error),
92 93 /// Needed a pattern that can be turned into a regex but got one that
93 94 /// can't. This should only happen through programmer error.
94 95 NonRegexPattern(IgnorePattern),
95 96 }
96 97
97 98 impl fmt::Display for PatternError {
98 99 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99 100 match self {
100 101 PatternError::UnsupportedSyntax(syntax) => {
101 102 write!(f, "Unsupported syntax {}", syntax)
102 103 }
103 104 PatternError::UnsupportedSyntaxInFile(syntax, file_path, line) => {
104 105 write!(
105 106 f,
106 107 "{}:{}: unsupported syntax {}",
107 108 file_path, line, syntax
108 109 )
109 110 }
110 111 PatternError::TooLong(size) => {
111 112 write!(f, "matcher pattern is too long ({} bytes)", size)
112 113 }
113 114 PatternError::IO(error) => error.fmt(f),
114 115 PatternError::Path(error) => error.fmt(f),
115 116 PatternError::NonRegexPattern(pattern) => {
116 117 write!(f, "'{:?}' cannot be turned into a regex", pattern)
117 118 }
118 119 }
119 120 }
120 121 }
@@ -1,242 +1,254 b''
1 1 use crate::config::{Config, ConfigError, ConfigParseError};
2 use crate::errors::{HgError, IoResultExt};
2 use crate::errors::{HgError, IoErrorContext, IoResultExt};
3 3 use crate::requirements;
4 4 use crate::utils::current_dir;
5 5 use crate::utils::files::get_path_from_bytes;
6 6 use memmap::{Mmap, MmapOptions};
7 7 use std::collections::HashSet;
8 8 use std::path::{Path, PathBuf};
9 9
10 10 /// A repository on disk
11 11 pub struct Repo {
12 12 working_directory: PathBuf,
13 13 dot_hg: PathBuf,
14 14 store: PathBuf,
15 15 requirements: HashSet<String>,
16 16 config: Config,
17 17 }
18 18
19 19 #[derive(Debug, derive_more::From)]
20 20 pub enum RepoError {
21 21 NotFound {
22 22 at: PathBuf,
23 23 },
24 24 #[from]
25 25 ConfigParseError(ConfigParseError),
26 26 #[from]
27 27 Other(HgError),
28 28 }
29 29
30 30 impl From<ConfigError> for RepoError {
31 31 fn from(error: ConfigError) -> Self {
32 32 match error {
33 33 ConfigError::Parse(error) => error.into(),
34 34 ConfigError::Other(error) => error.into(),
35 35 }
36 36 }
37 37 }
38 38
39 39 /// Filesystem access abstraction for the contents of a given "base" diretory
40 40 #[derive(Clone, Copy)]
41 pub(crate) struct Vfs<'a> {
42 base: &'a Path,
41 pub struct Vfs<'a> {
42 pub(crate) base: &'a Path,
43 43 }
44 44
45 45 impl Repo {
46 46 /// Find a repository, either at the given path (which must contain a `.hg`
47 47 /// sub-directory) or by searching the current directory and its
48 48 /// ancestors.
49 49 ///
50 50 /// A method with two very different "modes" like this usually a code smell
51 51 /// to make two methods instead, but in this case an `Option` is what rhg
52 52 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
53 53 /// Having two methods would just move that `if` to almost all callers.
54 54 pub fn find(
55 55 config: &Config,
56 56 explicit_path: Option<&Path>,
57 57 ) -> Result<Self, RepoError> {
58 58 if let Some(root) = explicit_path {
59 59 // Having an absolute path isn’t necessary here but can help code
60 60 // elsewhere
61 61 let root = current_dir()?.join(root);
62 62 if root.join(".hg").is_dir() {
63 63 Self::new_at_path(root, config)
64 64 } else {
65 65 Err(RepoError::NotFound {
66 66 at: root.to_owned(),
67 67 })
68 68 }
69 69 } else {
70 70 let current_directory = crate::utils::current_dir()?;
71 71 // ancestors() is inclusive: it first yields `current_directory`
72 72 // as-is.
73 73 for ancestor in current_directory.ancestors() {
74 74 if ancestor.join(".hg").is_dir() {
75 75 return Self::new_at_path(ancestor.to_owned(), config);
76 76 }
77 77 }
78 78 Err(RepoError::NotFound {
79 79 at: current_directory,
80 80 })
81 81 }
82 82 }
83 83
84 84 /// To be called after checking that `.hg` is a sub-directory
85 85 fn new_at_path(
86 86 working_directory: PathBuf,
87 87 config: &Config,
88 88 ) -> Result<Self, RepoError> {
89 89 let dot_hg = working_directory.join(".hg");
90 90
91 91 let mut repo_config_files = Vec::new();
92 92 repo_config_files.push(dot_hg.join("hgrc"));
93 93 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
94 94
95 95 let hg_vfs = Vfs { base: &dot_hg };
96 96 let mut reqs = requirements::load_if_exists(hg_vfs)?;
97 97 let relative =
98 98 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
99 99 let shared =
100 100 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
101 101
102 102 // From `mercurial/localrepo.py`:
103 103 //
104 104 // if .hg/requires contains the sharesafe requirement, it means
105 105 // there exists a `.hg/store/requires` too and we should read it
106 106 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
107 107 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
108 108 // is not present, refer checkrequirementscompat() for that
109 109 //
110 110 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
111 111 // repository was shared the old way. We check the share source
112 112 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
113 113 // current repository needs to be reshared
114 114 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
115 115
116 116 let store_path;
117 117 if !shared {
118 118 store_path = dot_hg.join("store");
119 119 if share_safe {
120 120 reqs.extend(requirements::load(Vfs { base: &store_path })?);
121 121 }
122 122 } else {
123 123 let bytes = hg_vfs.read("sharedpath")?;
124 124 let mut shared_path = get_path_from_bytes(&bytes).to_owned();
125 125 if relative {
126 126 shared_path = dot_hg.join(shared_path)
127 127 }
128 128 if !shared_path.is_dir() {
129 129 return Err(HgError::corrupted(format!(
130 130 ".hg/sharedpath points to nonexistent directory {}",
131 131 shared_path.display()
132 132 ))
133 133 .into());
134 134 }
135 135
136 136 store_path = shared_path.join("store");
137 137
138 138 let source_is_share_safe =
139 139 requirements::load(Vfs { base: &shared_path })?
140 140 .contains(requirements::SHARESAFE_REQUIREMENT);
141 141
142 142 if share_safe && !source_is_share_safe {
143 143 return Err(match config
144 144 .get(b"safe-mismatch", b"source-not-safe")
145 145 {
146 146 Some(b"abort") | None => HgError::abort(
147 147 "share source does not support share-safe requirement",
148 148 ),
149 149 _ => HgError::unsupported("share-safe downgrade"),
150 150 }
151 151 .into());
152 152 } else if source_is_share_safe && !share_safe {
153 153 return Err(
154 154 match config.get(b"safe-mismatch", b"source-safe") {
155 155 Some(b"abort") | None => HgError::abort(
156 156 "version mismatch: source uses share-safe \
157 157 functionality while the current share does not",
158 158 ),
159 159 _ => HgError::unsupported("share-safe upgrade"),
160 160 }
161 161 .into(),
162 162 );
163 163 }
164 164
165 165 if share_safe {
166 166 repo_config_files.insert(0, shared_path.join("hgrc"))
167 167 }
168 168 }
169 169
170 170 let repo_config = config.combine_with_repo(&repo_config_files)?;
171 171
172 172 let repo = Self {
173 173 requirements: reqs,
174 174 working_directory,
175 175 store: store_path,
176 176 dot_hg,
177 177 config: repo_config,
178 178 };
179 179
180 180 requirements::check(&repo)?;
181 181
182 182 Ok(repo)
183 183 }
184 184
185 185 pub fn working_directory_path(&self) -> &Path {
186 186 &self.working_directory
187 187 }
188 188
189 189 pub fn requirements(&self) -> &HashSet<String> {
190 190 &self.requirements
191 191 }
192 192
193 193 pub fn config(&self) -> &Config {
194 194 &self.config
195 195 }
196 196
197 197 /// For accessing repository files (in `.hg`), except for the store
198 198 /// (`.hg/store`).
199 pub(crate) fn hg_vfs(&self) -> Vfs<'_> {
199 pub fn hg_vfs(&self) -> Vfs<'_> {
200 200 Vfs { base: &self.dot_hg }
201 201 }
202 202
203 203 /// For accessing repository store files (in `.hg/store`)
204 pub(crate) fn store_vfs(&self) -> Vfs<'_> {
204 pub fn store_vfs(&self) -> Vfs<'_> {
205 205 Vfs { base: &self.store }
206 206 }
207 207
208 208 /// For accessing the working copy
209 209
210 210 // The undescore prefix silences the "never used" warning. Remove before
211 211 // using.
212 pub(crate) fn _working_directory_vfs(&self) -> Vfs<'_> {
212 pub fn _working_directory_vfs(&self) -> Vfs<'_> {
213 213 Vfs {
214 214 base: &self.working_directory,
215 215 }
216 216 }
217 217 }
218 218
219 219 impl Vfs<'_> {
220 pub(crate) fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
220 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
221 221 self.base.join(relative_path)
222 222 }
223 223
224 pub(crate) fn read(
224 pub fn read(
225 225 &self,
226 226 relative_path: impl AsRef<Path>,
227 227 ) -> Result<Vec<u8>, HgError> {
228 228 let path = self.join(relative_path);
229 std::fs::read(&path).for_file(&path)
229 std::fs::read(&path).when_reading_file(&path)
230 230 }
231 231
232 pub(crate) fn mmap_open(
232 pub fn mmap_open(
233 233 &self,
234 234 relative_path: impl AsRef<Path>,
235 235 ) -> Result<Mmap, HgError> {
236 236 let path = self.base.join(relative_path);
237 let file = std::fs::File::open(&path).for_file(&path)?;
237 let file = std::fs::File::open(&path).when_reading_file(&path)?;
238 238 // TODO: what are the safety requirements here?
239 let mmap = unsafe { MmapOptions::new().map(&file) }.for_file(&path)?;
239 let mmap = unsafe { MmapOptions::new().map(&file) }
240 .when_reading_file(&path)?;
240 241 Ok(mmap)
241 242 }
243
244 pub fn rename(
245 &self,
246 relative_from: impl AsRef<Path>,
247 relative_to: impl AsRef<Path>,
248 ) -> Result<(), HgError> {
249 let from = self.join(relative_from);
250 let to = self.join(relative_to);
251 std::fs::rename(&from, &to)
252 .with_context(|| IoErrorContext::RenamingFile { from, to })
242 253 }
254 }
General Comments 0
You need to be logged in to leave comments. Login now