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