##// END OF EJS Templates
rhg: Add support for the HGRCSKIPREPO environment variable...
Simon Sapin -
r47475:25e3dac5 default
parent child Browse files
Show More
@@ -1,463 +1,464 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 super::values;
11 use super::values;
12 use crate::config::layer::{
12 use crate::config::layer::{
13 ConfigError, ConfigLayer, ConfigOrigin, ConfigValue,
13 ConfigError, ConfigLayer, ConfigOrigin, ConfigValue,
14 };
14 };
15 use crate::utils::files::get_bytes_from_os_str;
15 use crate::utils::files::get_bytes_from_os_str;
16 use crate::utils::SliceExt;
16 use crate::utils::SliceExt;
17 use format_bytes::{write_bytes, DisplayBytes};
17 use format_bytes::{write_bytes, DisplayBytes};
18 use std::collections::HashSet;
18 use std::collections::HashSet;
19 use std::env;
19 use std::env;
20 use std::path::{Path, PathBuf};
20 use std::path::{Path, PathBuf};
21 use std::str;
21 use std::str;
22
22
23 use crate::errors::{HgResultExt, IoResultExt};
23 use crate::errors::{HgResultExt, IoResultExt};
24
24
25 /// Holds the config values for the current repository
25 /// Holds the config values for the current repository
26 /// TODO update this docstring once we support more sources
26 /// TODO update this docstring once we support more sources
27 #[derive(Clone)]
27 pub struct Config {
28 pub struct Config {
28 layers: Vec<layer::ConfigLayer>,
29 layers: Vec<layer::ConfigLayer>,
29 }
30 }
30
31
31 impl DisplayBytes for Config {
32 impl DisplayBytes for Config {
32 fn display_bytes(
33 fn display_bytes(
33 &self,
34 &self,
34 out: &mut dyn std::io::Write,
35 out: &mut dyn std::io::Write,
35 ) -> std::io::Result<()> {
36 ) -> std::io::Result<()> {
36 for (index, layer) in self.layers.iter().rev().enumerate() {
37 for (index, layer) in self.layers.iter().rev().enumerate() {
37 write_bytes!(
38 write_bytes!(
38 out,
39 out,
39 b"==== Layer {} (trusted: {}) ====\n{}",
40 b"==== Layer {} (trusted: {}) ====\n{}",
40 index,
41 index,
41 if layer.trusted {
42 if layer.trusted {
42 &b"yes"[..]
43 &b"yes"[..]
43 } else {
44 } else {
44 &b"no"[..]
45 &b"no"[..]
45 },
46 },
46 layer
47 layer
47 )?;
48 )?;
48 }
49 }
49 Ok(())
50 Ok(())
50 }
51 }
51 }
52 }
52
53
53 pub enum ConfigSource {
54 pub enum ConfigSource {
54 /// Absolute path to a config file
55 /// Absolute path to a config file
55 AbsPath(PathBuf),
56 AbsPath(PathBuf),
56 /// Already parsed (from the CLI, env, Python resources, etc.)
57 /// Already parsed (from the CLI, env, Python resources, etc.)
57 Parsed(layer::ConfigLayer),
58 Parsed(layer::ConfigLayer),
58 }
59 }
59
60
60 #[derive(Debug)]
61 #[derive(Debug)]
61 pub struct ConfigValueParseError {
62 pub struct ConfigValueParseError {
62 pub origin: ConfigOrigin,
63 pub origin: ConfigOrigin,
63 pub line: Option<usize>,
64 pub line: Option<usize>,
64 pub section: Vec<u8>,
65 pub section: Vec<u8>,
65 pub item: Vec<u8>,
66 pub item: Vec<u8>,
66 pub value: Vec<u8>,
67 pub value: Vec<u8>,
67 pub expected_type: &'static str,
68 pub expected_type: &'static str,
68 }
69 }
69
70
70 impl Config {
71 impl Config {
71 /// Load system and user configuration from various files.
72 /// Load system and user configuration from various files.
72 ///
73 ///
73 /// This is also affected by some environment variables.
74 /// This is also affected by some environment variables.
74 pub fn load(
75 pub fn load(
75 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
76 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
76 ) -> Result<Self, ConfigError> {
77 ) -> Result<Self, ConfigError> {
77 let mut config = Self { layers: Vec::new() };
78 let mut config = Self { layers: Vec::new() };
78 let opt_rc_path = env::var_os("HGRCPATH");
79 let opt_rc_path = env::var_os("HGRCPATH");
79 // HGRCPATH replaces system config
80 // HGRCPATH replaces system config
80 if opt_rc_path.is_none() {
81 if opt_rc_path.is_none() {
81 config.add_system_config()?
82 config.add_system_config()?
82 }
83 }
83
84
84 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
85 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
85 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
86 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
86 config.add_for_environment_variable("PAGER", b"pager", b"pager");
87 config.add_for_environment_variable("PAGER", b"pager", b"pager");
87
88
88 // These are set by `run-tests.py --rhg` to enable fallback for the
89 // These are set by `run-tests.py --rhg` to enable fallback for the
89 // entire test suite. Alternatives would be setting configuration
90 // entire test suite. Alternatives would be setting configuration
90 // through `$HGRCPATH` but some tests override that, or changing the
91 // through `$HGRCPATH` but some tests override that, or changing the
91 // `hg` shell alias to include `--config` but that disrupts tests that
92 // `hg` shell alias to include `--config` but that disrupts tests that
92 // print command lines and check expected output.
93 // print command lines and check expected output.
93 config.add_for_environment_variable(
94 config.add_for_environment_variable(
94 "RHG_ON_UNSUPPORTED",
95 "RHG_ON_UNSUPPORTED",
95 b"rhg",
96 b"rhg",
96 b"on-unsupported",
97 b"on-unsupported",
97 );
98 );
98 config.add_for_environment_variable(
99 config.add_for_environment_variable(
99 "RHG_FALLBACK_EXECUTABLE",
100 "RHG_FALLBACK_EXECUTABLE",
100 b"rhg",
101 b"rhg",
101 b"fallback-executable",
102 b"fallback-executable",
102 );
103 );
103
104
104 // HGRCPATH replaces user config
105 // HGRCPATH replaces user config
105 if opt_rc_path.is_none() {
106 if opt_rc_path.is_none() {
106 config.add_user_config()?
107 config.add_user_config()?
107 }
108 }
108 if let Some(rc_path) = &opt_rc_path {
109 if let Some(rc_path) = &opt_rc_path {
109 for path in env::split_paths(rc_path) {
110 for path in env::split_paths(rc_path) {
110 if !path.as_os_str().is_empty() {
111 if !path.as_os_str().is_empty() {
111 if path.is_dir() {
112 if path.is_dir() {
112 config.add_trusted_dir(&path)?
113 config.add_trusted_dir(&path)?
113 } else {
114 } else {
114 config.add_trusted_file(&path)?
115 config.add_trusted_file(&path)?
115 }
116 }
116 }
117 }
117 }
118 }
118 }
119 }
119 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
120 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
120 config.layers.push(layer)
121 config.layers.push(layer)
121 }
122 }
122 Ok(config)
123 Ok(config)
123 }
124 }
124
125
125 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
126 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
126 if let Some(entries) = std::fs::read_dir(path)
127 if let Some(entries) = std::fs::read_dir(path)
127 .when_reading_file(path)
128 .when_reading_file(path)
128 .io_not_found_as_none()?
129 .io_not_found_as_none()?
129 {
130 {
130 let mut file_paths = entries
131 let mut file_paths = entries
131 .map(|result| {
132 .map(|result| {
132 result.when_reading_file(path).map(|entry| entry.path())
133 result.when_reading_file(path).map(|entry| entry.path())
133 })
134 })
134 .collect::<Result<Vec<_>, _>>()?;
135 .collect::<Result<Vec<_>, _>>()?;
135 file_paths.sort();
136 file_paths.sort();
136 for file_path in &file_paths {
137 for file_path in &file_paths {
137 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
138 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
138 self.add_trusted_file(&file_path)?
139 self.add_trusted_file(&file_path)?
139 }
140 }
140 }
141 }
141 }
142 }
142 Ok(())
143 Ok(())
143 }
144 }
144
145
145 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
146 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
146 if let Some(data) = std::fs::read(path)
147 if let Some(data) = std::fs::read(path)
147 .when_reading_file(path)
148 .when_reading_file(path)
148 .io_not_found_as_none()?
149 .io_not_found_as_none()?
149 {
150 {
150 self.layers.extend(ConfigLayer::parse(path, &data)?)
151 self.layers.extend(ConfigLayer::parse(path, &data)?)
151 }
152 }
152 Ok(())
153 Ok(())
153 }
154 }
154
155
155 fn add_for_environment_variable(
156 fn add_for_environment_variable(
156 &mut self,
157 &mut self,
157 var: &str,
158 var: &str,
158 section: &[u8],
159 section: &[u8],
159 key: &[u8],
160 key: &[u8],
160 ) {
161 ) {
161 if let Some(value) = env::var_os(var) {
162 if let Some(value) = env::var_os(var) {
162 let origin = layer::ConfigOrigin::Environment(var.into());
163 let origin = layer::ConfigOrigin::Environment(var.into());
163 let mut layer = ConfigLayer::new(origin);
164 let mut layer = ConfigLayer::new(origin);
164 layer.add(
165 layer.add(
165 section.to_owned(),
166 section.to_owned(),
166 key.to_owned(),
167 key.to_owned(),
167 get_bytes_from_os_str(value),
168 get_bytes_from_os_str(value),
168 None,
169 None,
169 );
170 );
170 self.layers.push(layer)
171 self.layers.push(layer)
171 }
172 }
172 }
173 }
173
174
174 #[cfg(unix)] // TODO: other platforms
175 #[cfg(unix)] // TODO: other platforms
175 fn add_system_config(&mut self) -> Result<(), ConfigError> {
176 fn add_system_config(&mut self) -> Result<(), ConfigError> {
176 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
177 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
177 let etc = prefix.join("etc").join("mercurial");
178 let etc = prefix.join("etc").join("mercurial");
178 self.add_trusted_file(&etc.join("hgrc"))?;
179 self.add_trusted_file(&etc.join("hgrc"))?;
179 self.add_trusted_dir(&etc.join("hgrc.d"))
180 self.add_trusted_dir(&etc.join("hgrc.d"))
180 };
181 };
181 let root = Path::new("/");
182 let root = Path::new("/");
182 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
183 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
183 // instead? TODO: can this be a relative path?
184 // instead? TODO: can this be a relative path?
184 let hg = crate::utils::current_exe()?;
185 let hg = crate::utils::current_exe()?;
185 // TODO: this order (per-installation then per-system) matches
186 // TODO: this order (per-installation then per-system) matches
186 // `systemrcpath()` in `mercurial/scmposix.py`, but
187 // `systemrcpath()` in `mercurial/scmposix.py`, but
187 // `mercurial/helptext/config.txt` suggests it should be reversed
188 // `mercurial/helptext/config.txt` suggests it should be reversed
188 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
189 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
189 if installation_prefix != root {
190 if installation_prefix != root {
190 add_for_prefix(&installation_prefix)?
191 add_for_prefix(&installation_prefix)?
191 }
192 }
192 }
193 }
193 add_for_prefix(root)?;
194 add_for_prefix(root)?;
194 Ok(())
195 Ok(())
195 }
196 }
196
197
197 #[cfg(unix)] // TODO: other plateforms
198 #[cfg(unix)] // TODO: other plateforms
198 fn add_user_config(&mut self) -> Result<(), ConfigError> {
199 fn add_user_config(&mut self) -> Result<(), ConfigError> {
199 let opt_home = home::home_dir();
200 let opt_home = home::home_dir();
200 if let Some(home) = &opt_home {
201 if let Some(home) = &opt_home {
201 self.add_trusted_file(&home.join(".hgrc"))?
202 self.add_trusted_file(&home.join(".hgrc"))?
202 }
203 }
203 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
204 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
204 if !darwin {
205 if !darwin {
205 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
206 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
206 .map(PathBuf::from)
207 .map(PathBuf::from)
207 .or_else(|| opt_home.map(|home| home.join(".config")))
208 .or_else(|| opt_home.map(|home| home.join(".config")))
208 {
209 {
209 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
210 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
210 }
211 }
211 }
212 }
212 Ok(())
213 Ok(())
213 }
214 }
214
215
215 /// Loads in order, which means that the precedence is the same
216 /// Loads in order, which means that the precedence is the same
216 /// as the order of `sources`.
217 /// as the order of `sources`.
217 pub fn load_from_explicit_sources(
218 pub fn load_from_explicit_sources(
218 sources: Vec<ConfigSource>,
219 sources: Vec<ConfigSource>,
219 ) -> Result<Self, ConfigError> {
220 ) -> Result<Self, ConfigError> {
220 let mut layers = vec![];
221 let mut layers = vec![];
221
222
222 for source in sources.into_iter() {
223 for source in sources.into_iter() {
223 match source {
224 match source {
224 ConfigSource::Parsed(c) => layers.push(c),
225 ConfigSource::Parsed(c) => layers.push(c),
225 ConfigSource::AbsPath(c) => {
226 ConfigSource::AbsPath(c) => {
226 // TODO check if it should be trusted
227 // TODO check if it should be trusted
227 // mercurial/ui.py:427
228 // mercurial/ui.py:427
228 let data = match std::fs::read(&c) {
229 let data = match std::fs::read(&c) {
229 Err(_) => continue, // same as the python code
230 Err(_) => continue, // same as the python code
230 Ok(data) => data,
231 Ok(data) => data,
231 };
232 };
232 layers.extend(ConfigLayer::parse(&c, &data)?)
233 layers.extend(ConfigLayer::parse(&c, &data)?)
233 }
234 }
234 }
235 }
235 }
236 }
236
237
237 Ok(Config { layers })
238 Ok(Config { layers })
238 }
239 }
239
240
240 /// Loads the per-repository config into a new `Config` which is combined
241 /// Loads the per-repository config into a new `Config` which is combined
241 /// with `self`.
242 /// with `self`.
242 pub(crate) fn combine_with_repo(
243 pub(crate) fn combine_with_repo(
243 &self,
244 &self,
244 repo_config_files: &[PathBuf],
245 repo_config_files: &[PathBuf],
245 ) -> Result<Self, ConfigError> {
246 ) -> Result<Self, ConfigError> {
246 let (cli_layers, other_layers) = self
247 let (cli_layers, other_layers) = self
247 .layers
248 .layers
248 .iter()
249 .iter()
249 .cloned()
250 .cloned()
250 .partition(ConfigLayer::is_from_command_line);
251 .partition(ConfigLayer::is_from_command_line);
251
252
252 let mut repo_config = Self {
253 let mut repo_config = Self {
253 layers: other_layers,
254 layers: other_layers,
254 };
255 };
255 for path in repo_config_files {
256 for path in repo_config_files {
256 // TODO: check if this file should be trusted:
257 // TODO: check if this file should be trusted:
257 // `mercurial/ui.py:427`
258 // `mercurial/ui.py:427`
258 repo_config.add_trusted_file(path)?;
259 repo_config.add_trusted_file(path)?;
259 }
260 }
260 repo_config.layers.extend(cli_layers);
261 repo_config.layers.extend(cli_layers);
261 Ok(repo_config)
262 Ok(repo_config)
262 }
263 }
263
264
264 fn get_parse<'config, T: 'config>(
265 fn get_parse<'config, T: 'config>(
265 &'config self,
266 &'config self,
266 section: &[u8],
267 section: &[u8],
267 item: &[u8],
268 item: &[u8],
268 expected_type: &'static str,
269 expected_type: &'static str,
269 parse: impl Fn(&'config [u8]) -> Option<T>,
270 parse: impl Fn(&'config [u8]) -> Option<T>,
270 ) -> Result<Option<T>, ConfigValueParseError> {
271 ) -> Result<Option<T>, ConfigValueParseError> {
271 match self.get_inner(&section, &item) {
272 match self.get_inner(&section, &item) {
272 Some((layer, v)) => match parse(&v.bytes) {
273 Some((layer, v)) => match parse(&v.bytes) {
273 Some(b) => Ok(Some(b)),
274 Some(b) => Ok(Some(b)),
274 None => Err(ConfigValueParseError {
275 None => Err(ConfigValueParseError {
275 origin: layer.origin.to_owned(),
276 origin: layer.origin.to_owned(),
276 line: v.line,
277 line: v.line,
277 value: v.bytes.to_owned(),
278 value: v.bytes.to_owned(),
278 section: section.to_owned(),
279 section: section.to_owned(),
279 item: item.to_owned(),
280 item: item.to_owned(),
280 expected_type,
281 expected_type,
281 }),
282 }),
282 },
283 },
283 None => Ok(None),
284 None => Ok(None),
284 }
285 }
285 }
286 }
286
287
287 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
288 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
288 /// Otherwise, returns an `Ok(value)` if found, or `None`.
289 /// Otherwise, returns an `Ok(value)` if found, or `None`.
289 pub fn get_str(
290 pub fn get_str(
290 &self,
291 &self,
291 section: &[u8],
292 section: &[u8],
292 item: &[u8],
293 item: &[u8],
293 ) -> Result<Option<&str>, ConfigValueParseError> {
294 ) -> Result<Option<&str>, ConfigValueParseError> {
294 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
295 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
295 str::from_utf8(value).ok()
296 str::from_utf8(value).ok()
296 })
297 })
297 }
298 }
298
299
299 /// Returns an `Err` if the first value found is not a valid unsigned
300 /// Returns an `Err` if the first value found is not a valid unsigned
300 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
301 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
301 pub fn get_u32(
302 pub fn get_u32(
302 &self,
303 &self,
303 section: &[u8],
304 section: &[u8],
304 item: &[u8],
305 item: &[u8],
305 ) -> Result<Option<u32>, ConfigValueParseError> {
306 ) -> Result<Option<u32>, ConfigValueParseError> {
306 self.get_parse(section, item, "valid integer", |value| {
307 self.get_parse(section, item, "valid integer", |value| {
307 str::from_utf8(value).ok()?.parse().ok()
308 str::from_utf8(value).ok()?.parse().ok()
308 })
309 })
309 }
310 }
310
311
311 /// Returns an `Err` if the first value found is not a valid file size
312 /// Returns an `Err` if the first value found is not a valid file size
312 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
313 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
313 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
314 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
314 pub fn get_byte_size(
315 pub fn get_byte_size(
315 &self,
316 &self,
316 section: &[u8],
317 section: &[u8],
317 item: &[u8],
318 item: &[u8],
318 ) -> Result<Option<u64>, ConfigValueParseError> {
319 ) -> Result<Option<u64>, ConfigValueParseError> {
319 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
320 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
320 }
321 }
321
322
322 /// Returns an `Err` if the first value found is not a valid boolean.
323 /// Returns an `Err` if the first value found is not a valid boolean.
323 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
324 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
324 /// found, or `None`.
325 /// found, or `None`.
325 pub fn get_option(
326 pub fn get_option(
326 &self,
327 &self,
327 section: &[u8],
328 section: &[u8],
328 item: &[u8],
329 item: &[u8],
329 ) -> Result<Option<bool>, ConfigValueParseError> {
330 ) -> Result<Option<bool>, ConfigValueParseError> {
330 self.get_parse(section, item, "boolean", values::parse_bool)
331 self.get_parse(section, item, "boolean", values::parse_bool)
331 }
332 }
332
333
333 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
334 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
334 /// if the value is not found, an `Err` if it's not a valid boolean.
335 /// if the value is not found, an `Err` if it's not a valid boolean.
335 pub fn get_bool(
336 pub fn get_bool(
336 &self,
337 &self,
337 section: &[u8],
338 section: &[u8],
338 item: &[u8],
339 item: &[u8],
339 ) -> Result<bool, ConfigValueParseError> {
340 ) -> Result<bool, ConfigValueParseError> {
340 Ok(self.get_option(section, item)?.unwrap_or(false))
341 Ok(self.get_option(section, item)?.unwrap_or(false))
341 }
342 }
342
343
343 /// Returns the corresponding list-value in the config if found, or `None`.
344 /// Returns the corresponding list-value in the config if found, or `None`.
344 ///
345 ///
345 /// This is appropriate for new configuration keys. The value syntax is
346 /// This is appropriate for new configuration keys. The value syntax is
346 /// **not** the same as most existing list-valued config, which has Python
347 /// **not** the same as most existing list-valued config, which has Python
347 /// parsing implemented in `parselist()` in `mercurial/config.py`.
348 /// parsing implemented in `parselist()` in `mercurial/config.py`.
348 /// Faithfully porting that parsing algorithm to Rust (including behavior
349 /// Faithfully porting that parsing algorithm to Rust (including behavior
349 /// that are arguably bugs) turned out to be non-trivial and hasn’t been
350 /// that are arguably bugs) turned out to be non-trivial and hasn’t been
350 /// completed as of this writing.
351 /// completed as of this writing.
351 ///
352 ///
352 /// Instead, the "simple" syntax is: split on comma, then trim leading and
353 /// Instead, the "simple" syntax is: split on comma, then trim leading and
353 /// trailing whitespace of each component. Quotes or backslashes are not
354 /// trailing whitespace of each component. Quotes or backslashes are not
354 /// interpreted in any way. Commas are mandatory between values. Values
355 /// interpreted in any way. Commas are mandatory between values. Values
355 /// that contain a comma are not supported.
356 /// that contain a comma are not supported.
356 pub fn get_simple_list(
357 pub fn get_simple_list(
357 &self,
358 &self,
358 section: &[u8],
359 section: &[u8],
359 item: &[u8],
360 item: &[u8],
360 ) -> Option<impl Iterator<Item = &[u8]>> {
361 ) -> Option<impl Iterator<Item = &[u8]>> {
361 self.get(section, item).map(|value| {
362 self.get(section, item).map(|value| {
362 value
363 value
363 .split(|&byte| byte == b',')
364 .split(|&byte| byte == b',')
364 .map(|component| component.trim())
365 .map(|component| component.trim())
365 })
366 })
366 }
367 }
367
368
368 /// Returns the raw value bytes of the first one found, or `None`.
369 /// Returns the raw value bytes of the first one found, or `None`.
369 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
370 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
370 self.get_inner(section, item)
371 self.get_inner(section, item)
371 .map(|(_, value)| value.bytes.as_ref())
372 .map(|(_, value)| value.bytes.as_ref())
372 }
373 }
373
374
374 /// Returns the layer and the value of the first one found, or `None`.
375 /// Returns the layer and the value of the first one found, or `None`.
375 fn get_inner(
376 fn get_inner(
376 &self,
377 &self,
377 section: &[u8],
378 section: &[u8],
378 item: &[u8],
379 item: &[u8],
379 ) -> Option<(&ConfigLayer, &ConfigValue)> {
380 ) -> Option<(&ConfigLayer, &ConfigValue)> {
380 for layer in self.layers.iter().rev() {
381 for layer in self.layers.iter().rev() {
381 if !layer.trusted {
382 if !layer.trusted {
382 continue;
383 continue;
383 }
384 }
384 if let Some(v) = layer.get(&section, &item) {
385 if let Some(v) = layer.get(&section, &item) {
385 return Some((&layer, v));
386 return Some((&layer, v));
386 }
387 }
387 }
388 }
388 None
389 None
389 }
390 }
390
391
391 /// Return all keys defined for the given section
392 /// Return all keys defined for the given section
392 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
393 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
393 self.layers
394 self.layers
394 .iter()
395 .iter()
395 .flat_map(|layer| layer.iter_keys(section))
396 .flat_map(|layer| layer.iter_keys(section))
396 .collect()
397 .collect()
397 }
398 }
398
399
399 /// Get raw values bytes from all layers (even untrusted ones) in order
400 /// Get raw values bytes from all layers (even untrusted ones) in order
400 /// of precedence.
401 /// of precedence.
401 #[cfg(test)]
402 #[cfg(test)]
402 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
403 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
403 let mut res = vec![];
404 let mut res = vec![];
404 for layer in self.layers.iter().rev() {
405 for layer in self.layers.iter().rev() {
405 if let Some(v) = layer.get(&section, &item) {
406 if let Some(v) = layer.get(&section, &item) {
406 res.push(v.bytes.as_ref());
407 res.push(v.bytes.as_ref());
407 }
408 }
408 }
409 }
409 res
410 res
410 }
411 }
411 }
412 }
412
413
413 #[cfg(test)]
414 #[cfg(test)]
414 mod tests {
415 mod tests {
415 use super::*;
416 use super::*;
416 use pretty_assertions::assert_eq;
417 use pretty_assertions::assert_eq;
417 use std::fs::File;
418 use std::fs::File;
418 use std::io::Write;
419 use std::io::Write;
419
420
420 #[test]
421 #[test]
421 fn test_include_layer_ordering() {
422 fn test_include_layer_ordering() {
422 let tmpdir = tempfile::tempdir().unwrap();
423 let tmpdir = tempfile::tempdir().unwrap();
423 let tmpdir_path = tmpdir.path();
424 let tmpdir_path = tmpdir.path();
424 let mut included_file =
425 let mut included_file =
425 File::create(&tmpdir_path.join("included.rc")).unwrap();
426 File::create(&tmpdir_path.join("included.rc")).unwrap();
426
427
427 included_file.write_all(b"[section]\nitem=value1").unwrap();
428 included_file.write_all(b"[section]\nitem=value1").unwrap();
428 let base_config_path = tmpdir_path.join("base.rc");
429 let base_config_path = tmpdir_path.join("base.rc");
429 let mut config_file = File::create(&base_config_path).unwrap();
430 let mut config_file = File::create(&base_config_path).unwrap();
430 let data =
431 let data =
431 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
432 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
432 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
433 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
433 config_file.write_all(data).unwrap();
434 config_file.write_all(data).unwrap();
434
435
435 let sources = vec![ConfigSource::AbsPath(base_config_path)];
436 let sources = vec![ConfigSource::AbsPath(base_config_path)];
436 let config = Config::load_from_explicit_sources(sources)
437 let config = Config::load_from_explicit_sources(sources)
437 .expect("expected valid config");
438 .expect("expected valid config");
438
439
439 let (_, value) = config.get_inner(b"section", b"item").unwrap();
440 let (_, value) = config.get_inner(b"section", b"item").unwrap();
440 assert_eq!(
441 assert_eq!(
441 value,
442 value,
442 &ConfigValue {
443 &ConfigValue {
443 bytes: b"value2".to_vec(),
444 bytes: b"value2".to_vec(),
444 line: Some(4)
445 line: Some(4)
445 }
446 }
446 );
447 );
447
448
448 let value = config.get(b"section", b"item").unwrap();
449 let value = config.get(b"section", b"item").unwrap();
449 assert_eq!(value, b"value2",);
450 assert_eq!(value, b"value2",);
450 assert_eq!(
451 assert_eq!(
451 config.get_all(b"section", b"item"),
452 config.get_all(b"section", b"item"),
452 [b"value2", b"value1", b"value0"]
453 [b"value2", b"value1", b"value0"]
453 );
454 );
454
455
455 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
456 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
456 assert_eq!(
457 assert_eq!(
457 config.get_byte_size(b"section2", b"size").unwrap(),
458 config.get_byte_size(b"section2", b"size").unwrap(),
458 Some(1024 + 512)
459 Some(1024 + 512)
459 );
460 );
460 assert!(config.get_u32(b"section2", b"not-count").is_err());
461 assert!(config.get_u32(b"section2", b"not-count").is_err());
461 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
462 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
462 }
463 }
463 }
464 }
@@ -1,265 +1,269 b''
1 use crate::config::{Config, ConfigError, ConfigParseError};
1 use crate::config::{Config, ConfigError, ConfigParseError};
2 use crate::errors::{HgError, IoErrorContext, IoResultExt};
2 use crate::errors::{HgError, IoErrorContext, IoResultExt};
3 use crate::requirements;
3 use crate::requirements;
4 use crate::utils::files::get_path_from_bytes;
4 use crate::utils::files::get_path_from_bytes;
5 use crate::utils::SliceExt;
5 use crate::utils::SliceExt;
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 struct Vfs<'a> {
41 pub struct Vfs<'a> {
42 pub(crate) 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 if root.join(".hg").is_dir() {
59 if root.join(".hg").is_dir() {
60 Self::new_at_path(root.to_owned(), config)
60 Self::new_at_path(root.to_owned(), config)
61 } else if root.is_file() {
61 } else if root.is_file() {
62 Err(HgError::unsupported("bundle repository").into())
62 Err(HgError::unsupported("bundle repository").into())
63 } else {
63 } else {
64 Err(RepoError::NotFound {
64 Err(RepoError::NotFound {
65 at: root.to_owned(),
65 at: root.to_owned(),
66 })
66 })
67 }
67 }
68 } else {
68 } else {
69 let current_directory = crate::utils::current_dir()?;
69 let current_directory = crate::utils::current_dir()?;
70 // ancestors() is inclusive: it first yields `current_directory`
70 // ancestors() is inclusive: it first yields `current_directory`
71 // as-is.
71 // as-is.
72 for ancestor in current_directory.ancestors() {
72 for ancestor in current_directory.ancestors() {
73 if ancestor.join(".hg").is_dir() {
73 if ancestor.join(".hg").is_dir() {
74 return Self::new_at_path(ancestor.to_owned(), config);
74 return Self::new_at_path(ancestor.to_owned(), config);
75 }
75 }
76 }
76 }
77 Err(RepoError::NotFound {
77 Err(RepoError::NotFound {
78 at: current_directory,
78 at: current_directory,
79 })
79 })
80 }
80 }
81 }
81 }
82
82
83 /// To be called after checking that `.hg` is a sub-directory
83 /// To be called after checking that `.hg` is a sub-directory
84 fn new_at_path(
84 fn new_at_path(
85 working_directory: PathBuf,
85 working_directory: PathBuf,
86 config: &Config,
86 config: &Config,
87 ) -> Result<Self, RepoError> {
87 ) -> Result<Self, RepoError> {
88 let dot_hg = working_directory.join(".hg");
88 let dot_hg = working_directory.join(".hg");
89
89
90 let mut repo_config_files = Vec::new();
90 let mut repo_config_files = Vec::new();
91 repo_config_files.push(dot_hg.join("hgrc"));
91 repo_config_files.push(dot_hg.join("hgrc"));
92 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
92 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
93
93
94 let hg_vfs = Vfs { base: &dot_hg };
94 let hg_vfs = Vfs { base: &dot_hg };
95 let mut reqs = requirements::load_if_exists(hg_vfs)?;
95 let mut reqs = requirements::load_if_exists(hg_vfs)?;
96 let relative =
96 let relative =
97 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
97 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
98 let shared =
98 let shared =
99 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
99 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
100
100
101 // From `mercurial/localrepo.py`:
101 // From `mercurial/localrepo.py`:
102 //
102 //
103 // if .hg/requires contains the sharesafe requirement, it means
103 // if .hg/requires contains the sharesafe requirement, it means
104 // there exists a `.hg/store/requires` too and we should read it
104 // there exists a `.hg/store/requires` too and we should read it
105 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
105 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
106 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
106 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
107 // is not present, refer checkrequirementscompat() for that
107 // is not present, refer checkrequirementscompat() for that
108 //
108 //
109 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
109 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
110 // repository was shared the old way. We check the share source
110 // repository was shared the old way. We check the share source
111 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
111 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
112 // current repository needs to be reshared
112 // current repository needs to be reshared
113 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
113 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
114
114
115 let store_path;
115 let store_path;
116 if !shared {
116 if !shared {
117 store_path = dot_hg.join("store");
117 store_path = dot_hg.join("store");
118 } else {
118 } else {
119 let bytes = hg_vfs.read("sharedpath")?;
119 let bytes = hg_vfs.read("sharedpath")?;
120 let mut shared_path =
120 let mut shared_path =
121 get_path_from_bytes(bytes.trim_end_newlines()).to_owned();
121 get_path_from_bytes(bytes.trim_end_newlines()).to_owned();
122 if relative {
122 if relative {
123 shared_path = dot_hg.join(shared_path)
123 shared_path = dot_hg.join(shared_path)
124 }
124 }
125 if !shared_path.is_dir() {
125 if !shared_path.is_dir() {
126 return Err(HgError::corrupted(format!(
126 return Err(HgError::corrupted(format!(
127 ".hg/sharedpath points to nonexistent directory {}",
127 ".hg/sharedpath points to nonexistent directory {}",
128 shared_path.display()
128 shared_path.display()
129 ))
129 ))
130 .into());
130 .into());
131 }
131 }
132
132
133 store_path = shared_path.join("store");
133 store_path = shared_path.join("store");
134
134
135 let source_is_share_safe =
135 let source_is_share_safe =
136 requirements::load(Vfs { base: &shared_path })?
136 requirements::load(Vfs { base: &shared_path })?
137 .contains(requirements::SHARESAFE_REQUIREMENT);
137 .contains(requirements::SHARESAFE_REQUIREMENT);
138
138
139 if share_safe && !source_is_share_safe {
139 if share_safe && !source_is_share_safe {
140 return Err(match config
140 return Err(match config
141 .get(b"share", b"safe-mismatch.source-not-safe")
141 .get(b"share", b"safe-mismatch.source-not-safe")
142 {
142 {
143 Some(b"abort") | None => HgError::abort(
143 Some(b"abort") | None => HgError::abort(
144 "abort: share source does not support share-safe requirement\n\
144 "abort: share source does not support share-safe requirement\n\
145 (see `hg help config.format.use-share-safe` for more information)",
145 (see `hg help config.format.use-share-safe` for more information)",
146 ),
146 ),
147 _ => HgError::unsupported("share-safe downgrade"),
147 _ => HgError::unsupported("share-safe downgrade"),
148 }
148 }
149 .into());
149 .into());
150 } else if source_is_share_safe && !share_safe {
150 } else if source_is_share_safe && !share_safe {
151 return Err(
151 return Err(
152 match config.get(b"share", b"safe-mismatch.source-safe") {
152 match config.get(b"share", b"safe-mismatch.source-safe") {
153 Some(b"abort") | None => HgError::abort(
153 Some(b"abort") | None => HgError::abort(
154 "abort: version mismatch: source uses share-safe \
154 "abort: version mismatch: source uses share-safe \
155 functionality while the current share does not\n\
155 functionality while the current share does not\n\
156 (see `hg help config.format.use-share-safe` for more information)",
156 (see `hg help config.format.use-share-safe` for more information)",
157 ),
157 ),
158 _ => HgError::unsupported("share-safe upgrade"),
158 _ => HgError::unsupported("share-safe upgrade"),
159 }
159 }
160 .into(),
160 .into(),
161 );
161 );
162 }
162 }
163
163
164 if share_safe {
164 if share_safe {
165 repo_config_files.insert(0, shared_path.join("hgrc"))
165 repo_config_files.insert(0, shared_path.join("hgrc"))
166 }
166 }
167 }
167 }
168 if share_safe {
168 if share_safe {
169 reqs.extend(requirements::load(Vfs { base: &store_path })?);
169 reqs.extend(requirements::load(Vfs { base: &store_path })?);
170 }
170 }
171
171
172 let repo_config = config.combine_with_repo(&repo_config_files)?;
172 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
173 config.combine_with_repo(&repo_config_files)?
174 } else {
175 config.clone()
176 };
173
177
174 let repo = Self {
178 let repo = Self {
175 requirements: reqs,
179 requirements: reqs,
176 working_directory,
180 working_directory,
177 store: store_path,
181 store: store_path,
178 dot_hg,
182 dot_hg,
179 config: repo_config,
183 config: repo_config,
180 };
184 };
181
185
182 requirements::check(&repo)?;
186 requirements::check(&repo)?;
183
187
184 Ok(repo)
188 Ok(repo)
185 }
189 }
186
190
187 pub fn working_directory_path(&self) -> &Path {
191 pub fn working_directory_path(&self) -> &Path {
188 &self.working_directory
192 &self.working_directory
189 }
193 }
190
194
191 pub fn requirements(&self) -> &HashSet<String> {
195 pub fn requirements(&self) -> &HashSet<String> {
192 &self.requirements
196 &self.requirements
193 }
197 }
194
198
195 pub fn config(&self) -> &Config {
199 pub fn config(&self) -> &Config {
196 &self.config
200 &self.config
197 }
201 }
198
202
199 /// For accessing repository files (in `.hg`), except for the store
203 /// For accessing repository files (in `.hg`), except for the store
200 /// (`.hg/store`).
204 /// (`.hg/store`).
201 pub fn hg_vfs(&self) -> Vfs<'_> {
205 pub fn hg_vfs(&self) -> Vfs<'_> {
202 Vfs { base: &self.dot_hg }
206 Vfs { base: &self.dot_hg }
203 }
207 }
204
208
205 /// For accessing repository store files (in `.hg/store`)
209 /// For accessing repository store files (in `.hg/store`)
206 pub fn store_vfs(&self) -> Vfs<'_> {
210 pub fn store_vfs(&self) -> Vfs<'_> {
207 Vfs { base: &self.store }
211 Vfs { base: &self.store }
208 }
212 }
209
213
210 /// For accessing the working copy
214 /// For accessing the working copy
211
215
212 // The undescore prefix silences the "never used" warning. Remove before
216 // The undescore prefix silences the "never used" warning. Remove before
213 // using.
217 // using.
214 pub fn _working_directory_vfs(&self) -> Vfs<'_> {
218 pub fn _working_directory_vfs(&self) -> Vfs<'_> {
215 Vfs {
219 Vfs {
216 base: &self.working_directory,
220 base: &self.working_directory,
217 }
221 }
218 }
222 }
219
223
220 pub fn dirstate_parents(
224 pub fn dirstate_parents(
221 &self,
225 &self,
222 ) -> Result<crate::dirstate::DirstateParents, HgError> {
226 ) -> Result<crate::dirstate::DirstateParents, HgError> {
223 let dirstate = self.hg_vfs().mmap_open("dirstate")?;
227 let dirstate = self.hg_vfs().mmap_open("dirstate")?;
224 let parents =
228 let parents =
225 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?;
229 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?;
226 Ok(parents.clone())
230 Ok(parents.clone())
227 }
231 }
228 }
232 }
229
233
230 impl Vfs<'_> {
234 impl Vfs<'_> {
231 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
235 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
232 self.base.join(relative_path)
236 self.base.join(relative_path)
233 }
237 }
234
238
235 pub fn read(
239 pub fn read(
236 &self,
240 &self,
237 relative_path: impl AsRef<Path>,
241 relative_path: impl AsRef<Path>,
238 ) -> Result<Vec<u8>, HgError> {
242 ) -> Result<Vec<u8>, HgError> {
239 let path = self.join(relative_path);
243 let path = self.join(relative_path);
240 std::fs::read(&path).when_reading_file(&path)
244 std::fs::read(&path).when_reading_file(&path)
241 }
245 }
242
246
243 pub fn mmap_open(
247 pub fn mmap_open(
244 &self,
248 &self,
245 relative_path: impl AsRef<Path>,
249 relative_path: impl AsRef<Path>,
246 ) -> Result<Mmap, HgError> {
250 ) -> Result<Mmap, HgError> {
247 let path = self.base.join(relative_path);
251 let path = self.base.join(relative_path);
248 let file = std::fs::File::open(&path).when_reading_file(&path)?;
252 let file = std::fs::File::open(&path).when_reading_file(&path)?;
249 // TODO: what are the safety requirements here?
253 // TODO: what are the safety requirements here?
250 let mmap = unsafe { MmapOptions::new().map(&file) }
254 let mmap = unsafe { MmapOptions::new().map(&file) }
251 .when_reading_file(&path)?;
255 .when_reading_file(&path)?;
252 Ok(mmap)
256 Ok(mmap)
253 }
257 }
254
258
255 pub fn rename(
259 pub fn rename(
256 &self,
260 &self,
257 relative_from: impl AsRef<Path>,
261 relative_from: impl AsRef<Path>,
258 relative_to: impl AsRef<Path>,
262 relative_to: impl AsRef<Path>,
259 ) -> Result<(), HgError> {
263 ) -> Result<(), HgError> {
260 let from = self.join(relative_from);
264 let from = self.join(relative_from);
261 let to = self.join(relative_to);
265 let to = self.join(relative_to);
262 std::fs::rename(&from, &to)
266 std::fs::rename(&from, &to)
263 .with_context(|| IoErrorContext::RenamingFile { from, to })
267 .with_context(|| IoErrorContext::RenamingFile { from, to })
264 }
268 }
265 }
269 }
General Comments 0
You need to be logged in to leave comments. Login now