##// END OF EJS Templates
rhg: support tweakdefaults
Arseniy Alekseyev -
r50409:e37416d4 default
parent child Browse files
Show More
@@ -1,612 +1,654 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::config::plain_info::PlainInfo;
15 use crate::config::plain_info::PlainInfo;
16 use crate::utils::files::get_bytes_from_os_str;
16 use crate::utils::files::get_bytes_from_os_str;
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::fmt;
20 use std::fmt;
21 use std::path::{Path, PathBuf};
21 use std::path::{Path, PathBuf};
22 use std::str;
22 use std::str;
23
23
24 use crate::errors::{HgResultExt, IoResultExt};
24 use crate::errors::{HgResultExt, IoResultExt};
25
25
26 /// Holds the config values for the current repository
26 /// Holds the config values for the current repository
27 /// TODO update this docstring once we support more sources
27 /// TODO update this docstring once we support more sources
28 #[derive(Clone)]
28 #[derive(Clone)]
29 pub struct Config {
29 pub struct Config {
30 layers: Vec<layer::ConfigLayer>,
30 layers: Vec<layer::ConfigLayer>,
31 plain: PlainInfo,
31 plain: PlainInfo,
32 }
32 }
33
33
34 impl DisplayBytes for Config {
34 impl DisplayBytes for Config {
35 fn display_bytes(
35 fn display_bytes(
36 &self,
36 &self,
37 out: &mut dyn std::io::Write,
37 out: &mut dyn std::io::Write,
38 ) -> std::io::Result<()> {
38 ) -> std::io::Result<()> {
39 for (index, layer) in self.layers.iter().rev().enumerate() {
39 for (index, layer) in self.layers.iter().rev().enumerate() {
40 write_bytes!(
40 write_bytes!(
41 out,
41 out,
42 b"==== Layer {} (trusted: {}) ====\n{}",
42 b"==== Layer {} (trusted: {}) ====\n{}",
43 index,
43 index,
44 if layer.trusted {
44 if layer.trusted {
45 &b"yes"[..]
45 &b"yes"[..]
46 } else {
46 } else {
47 &b"no"[..]
47 &b"no"[..]
48 },
48 },
49 layer
49 layer
50 )?;
50 )?;
51 }
51 }
52 Ok(())
52 Ok(())
53 }
53 }
54 }
54 }
55
55
56 pub enum ConfigSource {
56 pub enum ConfigSource {
57 /// Absolute path to a config file
57 /// Absolute path to a config file
58 AbsPath(PathBuf),
58 AbsPath(PathBuf),
59 /// Already parsed (from the CLI, env, Python resources, etc.)
59 /// Already parsed (from the CLI, env, Python resources, etc.)
60 Parsed(layer::ConfigLayer),
60 Parsed(layer::ConfigLayer),
61 }
61 }
62
62
63 #[derive(Debug)]
63 #[derive(Debug)]
64 pub struct ConfigValueParseError {
64 pub struct ConfigValueParseError {
65 pub origin: ConfigOrigin,
65 pub origin: ConfigOrigin,
66 pub line: Option<usize>,
66 pub line: Option<usize>,
67 pub section: Vec<u8>,
67 pub section: Vec<u8>,
68 pub item: Vec<u8>,
68 pub item: Vec<u8>,
69 pub value: Vec<u8>,
69 pub value: Vec<u8>,
70 pub expected_type: &'static str,
70 pub expected_type: &'static str,
71 }
71 }
72
72
73 impl fmt::Display for ConfigValueParseError {
73 impl fmt::Display for ConfigValueParseError {
74 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75 // TODO: add origin and line number information, here and in
75 // TODO: add origin and line number information, here and in
76 // corresponding python code
76 // corresponding python code
77 write!(
77 write!(
78 f,
78 f,
79 "config error: {}.{} is not a {} ('{}')",
79 "config error: {}.{} is not a {} ('{}')",
80 String::from_utf8_lossy(&self.section),
80 String::from_utf8_lossy(&self.section),
81 String::from_utf8_lossy(&self.item),
81 String::from_utf8_lossy(&self.item),
82 self.expected_type,
82 self.expected_type,
83 String::from_utf8_lossy(&self.value)
83 String::from_utf8_lossy(&self.value)
84 )
84 )
85 }
85 }
86 }
86 }
87
87
88 /// Returns true if the config item is disabled by PLAIN or PLAINEXCEPT
88 /// Returns true if the config item is disabled by PLAIN or PLAINEXCEPT
89 fn should_ignore(plain: &PlainInfo, section: &[u8], item: &[u8]) -> bool {
89 fn should_ignore(plain: &PlainInfo, section: &[u8], item: &[u8]) -> bool {
90 // duplication with [_applyconfig] in [ui.py],
90 // duplication with [_applyconfig] in [ui.py],
91 if !plain.is_plain() {
91 if !plain.is_plain() {
92 return false;
92 return false;
93 }
93 }
94 if section == b"alias" {
94 if section == b"alias" {
95 return plain.plainalias();
95 return plain.plainalias();
96 }
96 }
97 if section == b"revsetalias" {
97 if section == b"revsetalias" {
98 return plain.plainrevsetalias();
98 return plain.plainrevsetalias();
99 }
99 }
100 if section == b"templatealias" {
100 if section == b"templatealias" {
101 return plain.plaintemplatealias();
101 return plain.plaintemplatealias();
102 }
102 }
103 if section == b"ui" {
103 if section == b"ui" {
104 let to_delete: &[&[u8]] = &[
104 let to_delete: &[&[u8]] = &[
105 b"debug",
105 b"debug",
106 b"fallbackencoding",
106 b"fallbackencoding",
107 b"quiet",
107 b"quiet",
108 b"slash",
108 b"slash",
109 b"logtemplate",
109 b"logtemplate",
110 b"message-output",
110 b"message-output",
111 b"statuscopies",
111 b"statuscopies",
112 b"style",
112 b"style",
113 b"traceback",
113 b"traceback",
114 b"verbose",
114 b"verbose",
115 ];
115 ];
116 return to_delete.contains(&item);
116 return to_delete.contains(&item);
117 }
117 }
118 let sections_to_delete: &[&[u8]] =
118 let sections_to_delete: &[&[u8]] =
119 &[b"defaults", b"commands", b"command-templates"];
119 &[b"defaults", b"commands", b"command-templates"];
120 return sections_to_delete.contains(&section);
120 return sections_to_delete.contains(&section);
121 }
121 }
122
122
123 impl Config {
123 impl Config {
124 /// The configuration to use when printing configuration-loading errors
124 /// The configuration to use when printing configuration-loading errors
125 pub fn empty() -> Self {
125 pub fn empty() -> Self {
126 Self {
126 Self {
127 layers: Vec::new(),
127 layers: Vec::new(),
128 plain: PlainInfo::empty(),
128 plain: PlainInfo::empty(),
129 }
129 }
130 }
130 }
131
131
132 /// Load system and user configuration from various files.
132 /// Load system and user configuration from various files.
133 ///
133 ///
134 /// This is also affected by some environment variables.
134 /// This is also affected by some environment variables.
135 pub fn load_non_repo() -> Result<Self, ConfigError> {
135 pub fn load_non_repo() -> Result<Self, ConfigError> {
136 let mut config = Self::empty();
136 let mut config = Self::empty();
137 let opt_rc_path = env::var_os("HGRCPATH");
137 let opt_rc_path = env::var_os("HGRCPATH");
138 // HGRCPATH replaces system config
138 // HGRCPATH replaces system config
139 if opt_rc_path.is_none() {
139 if opt_rc_path.is_none() {
140 config.add_system_config()?
140 config.add_system_config()?
141 }
141 }
142
142
143 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
143 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
144 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
144 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
145 config.add_for_environment_variable("PAGER", b"pager", b"pager");
145 config.add_for_environment_variable("PAGER", b"pager", b"pager");
146
146
147 // These are set by `run-tests.py --rhg` to enable fallback for the
147 // These are set by `run-tests.py --rhg` to enable fallback for the
148 // entire test suite. Alternatives would be setting configuration
148 // entire test suite. Alternatives would be setting configuration
149 // through `$HGRCPATH` but some tests override that, or changing the
149 // through `$HGRCPATH` but some tests override that, or changing the
150 // `hg` shell alias to include `--config` but that disrupts tests that
150 // `hg` shell alias to include `--config` but that disrupts tests that
151 // print command lines and check expected output.
151 // print command lines and check expected output.
152 config.add_for_environment_variable(
152 config.add_for_environment_variable(
153 "RHG_ON_UNSUPPORTED",
153 "RHG_ON_UNSUPPORTED",
154 b"rhg",
154 b"rhg",
155 b"on-unsupported",
155 b"on-unsupported",
156 );
156 );
157 config.add_for_environment_variable(
157 config.add_for_environment_variable(
158 "RHG_FALLBACK_EXECUTABLE",
158 "RHG_FALLBACK_EXECUTABLE",
159 b"rhg",
159 b"rhg",
160 b"fallback-executable",
160 b"fallback-executable",
161 );
161 );
162
162
163 // HGRCPATH replaces user config
163 // HGRCPATH replaces user config
164 if opt_rc_path.is_none() {
164 if opt_rc_path.is_none() {
165 config.add_user_config()?
165 config.add_user_config()?
166 }
166 }
167 if let Some(rc_path) = &opt_rc_path {
167 if let Some(rc_path) = &opt_rc_path {
168 for path in env::split_paths(rc_path) {
168 for path in env::split_paths(rc_path) {
169 if !path.as_os_str().is_empty() {
169 if !path.as_os_str().is_empty() {
170 if path.is_dir() {
170 if path.is_dir() {
171 config.add_trusted_dir(&path)?
171 config.add_trusted_dir(&path)?
172 } else {
172 } else {
173 config.add_trusted_file(&path)?
173 config.add_trusted_file(&path)?
174 }
174 }
175 }
175 }
176 }
176 }
177 }
177 }
178 Ok(config)
178 Ok(config)
179 }
179 }
180
180
181 pub fn load_cli_args(
181 pub fn load_cli_args(
182 &mut self,
182 &mut self,
183 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
183 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
184 color_arg: Option<Vec<u8>>,
184 color_arg: Option<Vec<u8>>,
185 ) -> Result<(), ConfigError> {
185 ) -> Result<(), ConfigError> {
186 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
186 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
187 self.layers.push(layer)
187 self.layers.push(layer)
188 }
188 }
189 if let Some(arg) = color_arg {
189 if let Some(arg) = color_arg {
190 let mut layer = ConfigLayer::new(ConfigOrigin::CommandLineColor);
190 let mut layer = ConfigLayer::new(ConfigOrigin::CommandLineColor);
191 layer.add(b"ui"[..].into(), b"color"[..].into(), arg, None);
191 layer.add(b"ui"[..].into(), b"color"[..].into(), arg, None);
192 self.layers.push(layer)
192 self.layers.push(layer)
193 }
193 }
194 Ok(())
194 Ok(())
195 }
195 }
196
196
197 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
197 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
198 if let Some(entries) = std::fs::read_dir(path)
198 if let Some(entries) = std::fs::read_dir(path)
199 .when_reading_file(path)
199 .when_reading_file(path)
200 .io_not_found_as_none()?
200 .io_not_found_as_none()?
201 {
201 {
202 let mut file_paths = entries
202 let mut file_paths = entries
203 .map(|result| {
203 .map(|result| {
204 result.when_reading_file(path).map(|entry| entry.path())
204 result.when_reading_file(path).map(|entry| entry.path())
205 })
205 })
206 .collect::<Result<Vec<_>, _>>()?;
206 .collect::<Result<Vec<_>, _>>()?;
207 file_paths.sort();
207 file_paths.sort();
208 for file_path in &file_paths {
208 for file_path in &file_paths {
209 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
209 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
210 self.add_trusted_file(&file_path)?
210 self.add_trusted_file(&file_path)?
211 }
211 }
212 }
212 }
213 }
213 }
214 Ok(())
214 Ok(())
215 }
215 }
216
216
217 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
217 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
218 if let Some(data) = std::fs::read(path)
218 if let Some(data) = std::fs::read(path)
219 .when_reading_file(path)
219 .when_reading_file(path)
220 .io_not_found_as_none()?
220 .io_not_found_as_none()?
221 {
221 {
222 self.layers.extend(ConfigLayer::parse(path, &data)?)
222 self.layers.extend(ConfigLayer::parse(path, &data)?)
223 }
223 }
224 Ok(())
224 Ok(())
225 }
225 }
226
226
227 fn add_for_environment_variable(
227 fn add_for_environment_variable(
228 &mut self,
228 &mut self,
229 var: &str,
229 var: &str,
230 section: &[u8],
230 section: &[u8],
231 key: &[u8],
231 key: &[u8],
232 ) {
232 ) {
233 if let Some(value) = env::var_os(var) {
233 if let Some(value) = env::var_os(var) {
234 let origin = layer::ConfigOrigin::Environment(var.into());
234 let origin = layer::ConfigOrigin::Environment(var.into());
235 let mut layer = ConfigLayer::new(origin);
235 let mut layer = ConfigLayer::new(origin);
236 layer.add(
236 layer.add(
237 section.to_owned(),
237 section.to_owned(),
238 key.to_owned(),
238 key.to_owned(),
239 get_bytes_from_os_str(value),
239 get_bytes_from_os_str(value),
240 None,
240 None,
241 );
241 );
242 self.layers.push(layer)
242 self.layers.push(layer)
243 }
243 }
244 }
244 }
245
245
246 #[cfg(unix)] // TODO: other platforms
246 #[cfg(unix)] // TODO: other platforms
247 fn add_system_config(&mut self) -> Result<(), ConfigError> {
247 fn add_system_config(&mut self) -> Result<(), ConfigError> {
248 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
248 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
249 let etc = prefix.join("etc").join("mercurial");
249 let etc = prefix.join("etc").join("mercurial");
250 self.add_trusted_file(&etc.join("hgrc"))?;
250 self.add_trusted_file(&etc.join("hgrc"))?;
251 self.add_trusted_dir(&etc.join("hgrc.d"))
251 self.add_trusted_dir(&etc.join("hgrc.d"))
252 };
252 };
253 let root = Path::new("/");
253 let root = Path::new("/");
254 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
254 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
255 // instead? TODO: can this be a relative path?
255 // instead? TODO: can this be a relative path?
256 let hg = crate::utils::current_exe()?;
256 let hg = crate::utils::current_exe()?;
257 // TODO: this order (per-installation then per-system) matches
257 // TODO: this order (per-installation then per-system) matches
258 // `systemrcpath()` in `mercurial/scmposix.py`, but
258 // `systemrcpath()` in `mercurial/scmposix.py`, but
259 // `mercurial/helptext/config.txt` suggests it should be reversed
259 // `mercurial/helptext/config.txt` suggests it should be reversed
260 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
260 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
261 if installation_prefix != root {
261 if installation_prefix != root {
262 add_for_prefix(&installation_prefix)?
262 add_for_prefix(&installation_prefix)?
263 }
263 }
264 }
264 }
265 add_for_prefix(root)?;
265 add_for_prefix(root)?;
266 Ok(())
266 Ok(())
267 }
267 }
268
268
269 #[cfg(unix)] // TODO: other plateforms
269 #[cfg(unix)] // TODO: other plateforms
270 fn add_user_config(&mut self) -> Result<(), ConfigError> {
270 fn add_user_config(&mut self) -> Result<(), ConfigError> {
271 let opt_home = home::home_dir();
271 let opt_home = home::home_dir();
272 if let Some(home) = &opt_home {
272 if let Some(home) = &opt_home {
273 self.add_trusted_file(&home.join(".hgrc"))?
273 self.add_trusted_file(&home.join(".hgrc"))?
274 }
274 }
275 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
275 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
276 if !darwin {
276 if !darwin {
277 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
277 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
278 .map(PathBuf::from)
278 .map(PathBuf::from)
279 .or_else(|| opt_home.map(|home| home.join(".config")))
279 .or_else(|| opt_home.map(|home| home.join(".config")))
280 {
280 {
281 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
281 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
282 }
282 }
283 }
283 }
284 Ok(())
284 Ok(())
285 }
285 }
286
286
287 /// Loads in order, which means that the precedence is the same
287 /// Loads in order, which means that the precedence is the same
288 /// as the order of `sources`.
288 /// as the order of `sources`.
289 pub fn load_from_explicit_sources(
289 pub fn load_from_explicit_sources(
290 sources: Vec<ConfigSource>,
290 sources: Vec<ConfigSource>,
291 ) -> Result<Self, ConfigError> {
291 ) -> Result<Self, ConfigError> {
292 let mut layers = vec![];
292 let mut layers = vec![];
293
293
294 for source in sources.into_iter() {
294 for source in sources.into_iter() {
295 match source {
295 match source {
296 ConfigSource::Parsed(c) => layers.push(c),
296 ConfigSource::Parsed(c) => layers.push(c),
297 ConfigSource::AbsPath(c) => {
297 ConfigSource::AbsPath(c) => {
298 // TODO check if it should be trusted
298 // TODO check if it should be trusted
299 // mercurial/ui.py:427
299 // mercurial/ui.py:427
300 let data = match std::fs::read(&c) {
300 let data = match std::fs::read(&c) {
301 Err(_) => continue, // same as the python code
301 Err(_) => continue, // same as the python code
302 Ok(data) => data,
302 Ok(data) => data,
303 };
303 };
304 layers.extend(ConfigLayer::parse(&c, &data)?)
304 layers.extend(ConfigLayer::parse(&c, &data)?)
305 }
305 }
306 }
306 }
307 }
307 }
308
308
309 Ok(Config {
309 Ok(Config {
310 layers,
310 layers,
311 plain: PlainInfo::empty(),
311 plain: PlainInfo::empty(),
312 })
312 })
313 }
313 }
314
314
315 /// Loads the per-repository config into a new `Config` which is combined
315 /// Loads the per-repository config into a new `Config` which is combined
316 /// with `self`.
316 /// with `self`.
317 pub(crate) fn combine_with_repo(
317 pub(crate) fn combine_with_repo(
318 &self,
318 &self,
319 repo_config_files: &[PathBuf],
319 repo_config_files: &[PathBuf],
320 ) -> Result<Self, ConfigError> {
320 ) -> Result<Self, ConfigError> {
321 let (cli_layers, other_layers) = self
321 let (cli_layers, other_layers) = self
322 .layers
322 .layers
323 .iter()
323 .iter()
324 .cloned()
324 .cloned()
325 .partition(ConfigLayer::is_from_command_line);
325 .partition(ConfigLayer::is_from_command_line);
326
326
327 let mut repo_config = Self {
327 let mut repo_config = Self {
328 layers: other_layers,
328 layers: other_layers,
329 plain: PlainInfo::empty(),
329 plain: PlainInfo::empty(),
330 };
330 };
331 for path in repo_config_files {
331 for path in repo_config_files {
332 // TODO: check if this file should be trusted:
332 // TODO: check if this file should be trusted:
333 // `mercurial/ui.py:427`
333 // `mercurial/ui.py:427`
334 repo_config.add_trusted_file(path)?;
334 repo_config.add_trusted_file(path)?;
335 }
335 }
336 repo_config.layers.extend(cli_layers);
336 repo_config.layers.extend(cli_layers);
337 Ok(repo_config)
337 Ok(repo_config)
338 }
338 }
339
339
340 pub fn apply_plain(&mut self, plain: PlainInfo) {
340 pub fn apply_plain(&mut self, plain: PlainInfo) {
341 self.plain = plain;
341 self.plain = plain;
342 }
342 }
343
343
344 fn get_parse<'config, T: 'config>(
344 fn get_parse<'config, T: 'config>(
345 &'config self,
345 &'config self,
346 section: &[u8],
346 section: &[u8],
347 item: &[u8],
347 item: &[u8],
348 expected_type: &'static str,
348 expected_type: &'static str,
349 parse: impl Fn(&'config [u8]) -> Option<T>,
349 parse: impl Fn(&'config [u8]) -> Option<T>,
350 ) -> Result<Option<T>, ConfigValueParseError> {
350 ) -> Result<Option<T>, ConfigValueParseError> {
351 match self.get_inner(&section, &item) {
351 match self.get_inner(&section, &item) {
352 Some((layer, v)) => match parse(&v.bytes) {
352 Some((layer, v)) => match parse(&v.bytes) {
353 Some(b) => Ok(Some(b)),
353 Some(b) => Ok(Some(b)),
354 None => Err(ConfigValueParseError {
354 None => Err(ConfigValueParseError {
355 origin: layer.origin.to_owned(),
355 origin: layer.origin.to_owned(),
356 line: v.line,
356 line: v.line,
357 value: v.bytes.to_owned(),
357 value: v.bytes.to_owned(),
358 section: section.to_owned(),
358 section: section.to_owned(),
359 item: item.to_owned(),
359 item: item.to_owned(),
360 expected_type,
360 expected_type,
361 }),
361 }),
362 },
362 },
363 None => Ok(None),
363 None => Ok(None),
364 }
364 }
365 }
365 }
366
366
367 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
367 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
368 /// Otherwise, returns an `Ok(value)` if found, or `None`.
368 /// Otherwise, returns an `Ok(value)` if found, or `None`.
369 pub fn get_str(
369 pub fn get_str(
370 &self,
370 &self,
371 section: &[u8],
371 section: &[u8],
372 item: &[u8],
372 item: &[u8],
373 ) -> Result<Option<&str>, ConfigValueParseError> {
373 ) -> Result<Option<&str>, ConfigValueParseError> {
374 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
374 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
375 str::from_utf8(value).ok()
375 str::from_utf8(value).ok()
376 })
376 })
377 }
377 }
378
378
379 /// Returns an `Err` if the first value found is not a valid unsigned
379 /// Returns an `Err` if the first value found is not a valid unsigned
380 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
380 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
381 pub fn get_u32(
381 pub fn get_u32(
382 &self,
382 &self,
383 section: &[u8],
383 section: &[u8],
384 item: &[u8],
384 item: &[u8],
385 ) -> Result<Option<u32>, ConfigValueParseError> {
385 ) -> Result<Option<u32>, ConfigValueParseError> {
386 self.get_parse(section, item, "valid integer", |value| {
386 self.get_parse(section, item, "valid integer", |value| {
387 str::from_utf8(value).ok()?.parse().ok()
387 str::from_utf8(value).ok()?.parse().ok()
388 })
388 })
389 }
389 }
390
390
391 /// Returns an `Err` if the first value found is not a valid file size
391 /// Returns an `Err` if the first value found is not a valid file size
392 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
392 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
393 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
393 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
394 pub fn get_byte_size(
394 pub fn get_byte_size(
395 &self,
395 &self,
396 section: &[u8],
396 section: &[u8],
397 item: &[u8],
397 item: &[u8],
398 ) -> Result<Option<u64>, ConfigValueParseError> {
398 ) -> Result<Option<u64>, ConfigValueParseError> {
399 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
399 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
400 }
400 }
401
401
402 /// Returns an `Err` if the first value found is not a valid boolean.
402 /// Returns an `Err` if the first value found is not a valid boolean.
403 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
403 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
404 /// found, or `None`.
404 /// found, or `None`.
405 pub fn get_option(
405 pub fn get_option(
406 &self,
406 &self,
407 section: &[u8],
407 section: &[u8],
408 item: &[u8],
408 item: &[u8],
409 ) -> Result<Option<bool>, ConfigValueParseError> {
409 ) -> Result<Option<bool>, ConfigValueParseError> {
410 self.get_parse(section, item, "boolean", values::parse_bool)
410 self.get_parse(section, item, "boolean", values::parse_bool)
411 }
411 }
412
412
413 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
413 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
414 /// if the value is not found, an `Err` if it's not a valid boolean.
414 /// if the value is not found, an `Err` if it's not a valid boolean.
415 pub fn get_bool(
415 pub fn get_bool(
416 &self,
416 &self,
417 section: &[u8],
417 section: &[u8],
418 item: &[u8],
418 item: &[u8],
419 ) -> Result<bool, ConfigValueParseError> {
419 ) -> Result<bool, ConfigValueParseError> {
420 Ok(self.get_option(section, item)?.unwrap_or(false))
420 Ok(self.get_option(section, item)?.unwrap_or(false))
421 }
421 }
422
422
423 /// Returns `true` if the extension is enabled, `false` otherwise
423 /// Returns `true` if the extension is enabled, `false` otherwise
424 pub fn is_extension_enabled(&self, extension: &[u8]) -> bool {
424 pub fn is_extension_enabled(&self, extension: &[u8]) -> bool {
425 let value = self.get(b"extensions", extension);
425 let value = self.get(b"extensions", extension);
426 match value {
426 match value {
427 Some(c) => !c.starts_with(b"!"),
427 Some(c) => !c.starts_with(b"!"),
428 None => false,
428 None => false,
429 }
429 }
430 }
430 }
431
431
432 /// If there is an `item` value in `section`, parse and return a list of
432 /// If there is an `item` value in `section`, parse and return a list of
433 /// byte strings.
433 /// byte strings.
434 pub fn get_list(
434 pub fn get_list(
435 &self,
435 &self,
436 section: &[u8],
436 section: &[u8],
437 item: &[u8],
437 item: &[u8],
438 ) -> Option<Vec<Vec<u8>>> {
438 ) -> Option<Vec<Vec<u8>>> {
439 self.get(section, item).map(values::parse_list)
439 self.get(section, item).map(values::parse_list)
440 }
440 }
441
441
442 /// Returns the raw value bytes of the first one found, or `None`.
442 /// Returns the raw value bytes of the first one found, or `None`.
443 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
443 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
444 self.get_inner(section, item)
444 self.get_inner(section, item)
445 .map(|(_, value)| value.bytes.as_ref())
445 .map(|(_, value)| value.bytes.as_ref())
446 }
446 }
447
447
448 /// Returns the raw value bytes of the first one found, or `None`.
448 /// Returns the raw value bytes of the first one found, or `None`.
449 pub fn get_with_origin(
449 pub fn get_with_origin(
450 &self,
450 &self,
451 section: &[u8],
451 section: &[u8],
452 item: &[u8],
452 item: &[u8],
453 ) -> Option<(&[u8], &ConfigOrigin)> {
453 ) -> Option<(&[u8], &ConfigOrigin)> {
454 self.get_inner(section, item)
454 self.get_inner(section, item)
455 .map(|(layer, value)| (value.bytes.as_ref(), &layer.origin))
455 .map(|(layer, value)| (value.bytes.as_ref(), &layer.origin))
456 }
456 }
457
457
458 /// Returns the layer and the value of the first one found, or `None`.
458 /// Returns the layer and the value of the first one found, or `None`.
459 fn get_inner(
459 fn get_inner(
460 &self,
460 &self,
461 section: &[u8],
461 section: &[u8],
462 item: &[u8],
462 item: &[u8],
463 ) -> Option<(&ConfigLayer, &ConfigValue)> {
463 ) -> Option<(&ConfigLayer, &ConfigValue)> {
464 // Filter out the config items that are hidden by [PLAIN].
464 // Filter out the config items that are hidden by [PLAIN].
465 // This differs from python hg where we delete them from the config.
465 // This differs from python hg where we delete them from the config.
466 if should_ignore(&self.plain, &section, &item) {
466 let should_ignore = should_ignore(&self.plain, &section, &item);
467 return None;
468 }
469 for layer in self.layers.iter().rev() {
467 for layer in self.layers.iter().rev() {
470 if !layer.trusted {
468 if !layer.trusted {
471 continue;
469 continue;
472 }
470 }
471 //The [PLAIN] config should not affect the defaults.
472 //
473 // However, PLAIN should also affect the "tweaked" defaults (unless
474 // "tweakdefault" is part of "HGPLAINEXCEPT").
475 //
476 // In practice the tweak-default layer is only added when it is
477 // relevant, so we can safely always take it into
478 // account here.
479 if should_ignore && !(layer.origin == ConfigOrigin::Tweakdefaults)
480 {
481 continue;
482 }
473 if let Some(v) = layer.get(&section, &item) {
483 if let Some(v) = layer.get(&section, &item) {
474 return Some((&layer, v));
484 return Some((&layer, v));
475 }
485 }
476 }
486 }
477 None
487 None
478 }
488 }
479
489
480 /// Return all keys defined for the given section
490 /// Return all keys defined for the given section
481 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
491 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
482 self.layers
492 self.layers
483 .iter()
493 .iter()
484 .flat_map(|layer| layer.iter_keys(section))
494 .flat_map(|layer| layer.iter_keys(section))
485 .collect()
495 .collect()
486 }
496 }
487
497
488 /// Returns whether any key is defined in the given section
498 /// Returns whether any key is defined in the given section
489 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
499 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
490 self.layers
500 self.layers
491 .iter()
501 .iter()
492 .any(|layer| layer.has_non_empty_section(section))
502 .any(|layer| layer.has_non_empty_section(section))
493 }
503 }
494
504
495 /// Yields (key, value) pairs for everything in the given section
505 /// Yields (key, value) pairs for everything in the given section
496 pub fn iter_section<'a>(
506 pub fn iter_section<'a>(
497 &'a self,
507 &'a self,
498 section: &'a [u8],
508 section: &'a [u8],
499 ) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
509 ) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
500 // TODO: Use `Iterator`’s `.peekable()` when its `peek_mut` is
510 // TODO: Use `Iterator`’s `.peekable()` when its `peek_mut` is
501 // available:
511 // available:
502 // https://doc.rust-lang.org/nightly/std/iter/struct.Peekable.html#method.peek_mut
512 // https://doc.rust-lang.org/nightly/std/iter/struct.Peekable.html#method.peek_mut
503 struct Peekable<I: Iterator> {
513 struct Peekable<I: Iterator> {
504 iter: I,
514 iter: I,
505 /// Remember a peeked value, even if it was None.
515 /// Remember a peeked value, even if it was None.
506 peeked: Option<Option<I::Item>>,
516 peeked: Option<Option<I::Item>>,
507 }
517 }
508
518
509 impl<I: Iterator> Peekable<I> {
519 impl<I: Iterator> Peekable<I> {
510 fn new(iter: I) -> Self {
520 fn new(iter: I) -> Self {
511 Self { iter, peeked: None }
521 Self { iter, peeked: None }
512 }
522 }
513
523
514 fn next(&mut self) {
524 fn next(&mut self) {
515 self.peeked = None
525 self.peeked = None
516 }
526 }
517
527
518 fn peek_mut(&mut self) -> Option<&mut I::Item> {
528 fn peek_mut(&mut self) -> Option<&mut I::Item> {
519 let iter = &mut self.iter;
529 let iter = &mut self.iter;
520 self.peeked.get_or_insert_with(|| iter.next()).as_mut()
530 self.peeked.get_or_insert_with(|| iter.next()).as_mut()
521 }
531 }
522 }
532 }
523
533
524 // Deduplicate keys redefined in multiple layers
534 // Deduplicate keys redefined in multiple layers
525 let mut keys_already_seen = HashSet::new();
535 let mut keys_already_seen = HashSet::new();
526 let mut key_is_new =
536 let mut key_is_new =
527 move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
537 move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
528 keys_already_seen.insert(key)
538 keys_already_seen.insert(key)
529 };
539 };
530 // This is similar to `flat_map` + `filter_map`, except with a single
540 // This is similar to `flat_map` + `filter_map`, except with a single
531 // closure that owns `key_is_new` (and therefore the
541 // closure that owns `key_is_new` (and therefore the
532 // `keys_already_seen` set):
542 // `keys_already_seen` set):
533 let mut layer_iters = Peekable::new(
543 let mut layer_iters = Peekable::new(
534 self.layers
544 self.layers
535 .iter()
545 .iter()
536 .rev()
546 .rev()
537 .map(move |layer| layer.iter_section(section)),
547 .map(move |layer| layer.iter_section(section)),
538 );
548 );
539 std::iter::from_fn(move || loop {
549 std::iter::from_fn(move || loop {
540 if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
550 if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
541 return Some(pair);
551 return Some(pair);
542 } else {
552 } else {
543 layer_iters.next();
553 layer_iters.next();
544 }
554 }
545 })
555 })
546 }
556 }
547
557
548 /// Get raw values bytes from all layers (even untrusted ones) in order
558 /// Get raw values bytes from all layers (even untrusted ones) in order
549 /// of precedence.
559 /// of precedence.
550 #[cfg(test)]
560 #[cfg(test)]
551 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
561 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
552 let mut res = vec![];
562 let mut res = vec![];
553 for layer in self.layers.iter().rev() {
563 for layer in self.layers.iter().rev() {
554 if let Some(v) = layer.get(&section, &item) {
564 if let Some(v) = layer.get(&section, &item) {
555 res.push(v.bytes.as_ref());
565 res.push(v.bytes.as_ref());
556 }
566 }
557 }
567 }
558 res
568 res
559 }
569 }
570
571 // a config layer that's introduced by ui.tweakdefaults
572 fn tweakdefaults_layer() -> ConfigLayer {
573 let mut layer = ConfigLayer::new(ConfigOrigin::Tweakdefaults);
574
575 let mut add = |section: &[u8], item: &[u8], value: &[u8]| {
576 layer.add(
577 section[..].into(),
578 item[..].into(),
579 value[..].into(),
580 None,
581 );
582 };
583 // duplication of [tweakrc] from [ui.py]
584 add(b"ui", b"rollback", b"False");
585 add(b"ui", b"statuscopies", b"yes");
586 add(b"ui", b"interface", b"curses");
587 add(b"ui", b"relative-paths", b"yes");
588 add(b"commands", b"grep.all-files", b"True");
589 add(b"commands", b"update.check", b"noconflict");
590 add(b"commands", b"status.verbose", b"True");
591 add(b"commands", b"resolve.explicit-re-merge", b"True");
592 add(b"git", b"git", b"1");
593 add(b"git", b"showfunc", b"1");
594 add(b"git", b"word-diff", b"1");
595 return layer;
596 }
597
598 // introduce the tweaked defaults as implied by ui.tweakdefaults
599 pub fn tweakdefaults<'a>(&mut self) -> () {
600 self.layers.insert(0, Config::tweakdefaults_layer());
601 }
560 }
602 }
561
603
562 #[cfg(test)]
604 #[cfg(test)]
563 mod tests {
605 mod tests {
564 use super::*;
606 use super::*;
565 use pretty_assertions::assert_eq;
607 use pretty_assertions::assert_eq;
566 use std::fs::File;
608 use std::fs::File;
567 use std::io::Write;
609 use std::io::Write;
568
610
569 #[test]
611 #[test]
570 fn test_include_layer_ordering() {
612 fn test_include_layer_ordering() {
571 let tmpdir = tempfile::tempdir().unwrap();
613 let tmpdir = tempfile::tempdir().unwrap();
572 let tmpdir_path = tmpdir.path();
614 let tmpdir_path = tmpdir.path();
573 let mut included_file =
615 let mut included_file =
574 File::create(&tmpdir_path.join("included.rc")).unwrap();
616 File::create(&tmpdir_path.join("included.rc")).unwrap();
575
617
576 included_file.write_all(b"[section]\nitem=value1").unwrap();
618 included_file.write_all(b"[section]\nitem=value1").unwrap();
577 let base_config_path = tmpdir_path.join("base.rc");
619 let base_config_path = tmpdir_path.join("base.rc");
578 let mut config_file = File::create(&base_config_path).unwrap();
620 let mut config_file = File::create(&base_config_path).unwrap();
579 let data =
621 let data =
580 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
622 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
581 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
623 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
582 config_file.write_all(data).unwrap();
624 config_file.write_all(data).unwrap();
583
625
584 let sources = vec![ConfigSource::AbsPath(base_config_path)];
626 let sources = vec![ConfigSource::AbsPath(base_config_path)];
585 let config = Config::load_from_explicit_sources(sources)
627 let config = Config::load_from_explicit_sources(sources)
586 .expect("expected valid config");
628 .expect("expected valid config");
587
629
588 let (_, value) = config.get_inner(b"section", b"item").unwrap();
630 let (_, value) = config.get_inner(b"section", b"item").unwrap();
589 assert_eq!(
631 assert_eq!(
590 value,
632 value,
591 &ConfigValue {
633 &ConfigValue {
592 bytes: b"value2".to_vec(),
634 bytes: b"value2".to_vec(),
593 line: Some(4)
635 line: Some(4)
594 }
636 }
595 );
637 );
596
638
597 let value = config.get(b"section", b"item").unwrap();
639 let value = config.get(b"section", b"item").unwrap();
598 assert_eq!(value, b"value2",);
640 assert_eq!(value, b"value2",);
599 assert_eq!(
641 assert_eq!(
600 config.get_all(b"section", b"item"),
642 config.get_all(b"section", b"item"),
601 [b"value2", b"value1", b"value0"]
643 [b"value2", b"value1", b"value0"]
602 );
644 );
603
645
604 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
646 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
605 assert_eq!(
647 assert_eq!(
606 config.get_byte_size(b"section2", b"size").unwrap(),
648 config.get_byte_size(b"section2", b"size").unwrap(),
607 Some(1024 + 512)
649 Some(1024 + 512)
608 );
650 );
609 assert!(config.get_u32(b"section2", b"not-count").is_err());
651 assert!(config.get_u32(b"section2", b"not-count").is_err());
610 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
652 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
611 }
653 }
612 }
654 }
@@ -1,344 +1,349 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;
10 use crate::errors::HgError;
11 use crate::exit_codes::CONFIG_PARSE_ERROR_ABORT;
11 use crate::exit_codes::CONFIG_PARSE_ERROR_ABORT;
12 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
12 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
13 use format_bytes::{format_bytes, write_bytes, DisplayBytes};
13 use format_bytes::{format_bytes, write_bytes, DisplayBytes};
14 use lazy_static::lazy_static;
14 use lazy_static::lazy_static;
15 use regex::bytes::Regex;
15 use regex::bytes::Regex;
16 use std::collections::HashMap;
16 use std::collections::HashMap;
17 use std::path::{Path, PathBuf};
17 use std::path::{Path, PathBuf};
18
18
19 lazy_static! {
19 lazy_static! {
20 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
20 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
21 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
21 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
22 /// Continuation whitespace
22 /// Continuation whitespace
23 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
23 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
24 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
24 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
25 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
25 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
26 /// A directive that allows for removing previous entries
26 /// A directive that allows for removing previous entries
27 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
27 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
28 /// A directive that allows for including other config files
28 /// A directive that allows for including other config files
29 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
29 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
30 }
30 }
31
31
32 /// All config values separated by layers of precedence.
32 /// All config values separated by layers of precedence.
33 /// Each config source may be split in multiple layers if `%include` directives
33 /// Each config source may be split in multiple layers if `%include` directives
34 /// are used.
34 /// are used.
35 /// TODO detail the general precedence
35 /// TODO detail the general precedence
36 #[derive(Clone)]
36 #[derive(Clone)]
37 pub struct ConfigLayer {
37 pub struct ConfigLayer {
38 /// Mapping of the sections to their items
38 /// Mapping of the sections to their items
39 sections: HashMap<Vec<u8>, ConfigItem>,
39 sections: HashMap<Vec<u8>, ConfigItem>,
40 /// All sections (and their items/values) in a layer share the same origin
40 /// All sections (and their items/values) in a layer share the same origin
41 pub origin: ConfigOrigin,
41 pub origin: ConfigOrigin,
42 /// Whether this layer comes from a trusted user or group
42 /// Whether this layer comes from a trusted user or group
43 pub trusted: bool,
43 pub trusted: bool,
44 }
44 }
45
45
46 impl ConfigLayer {
46 impl ConfigLayer {
47 pub fn new(origin: ConfigOrigin) -> Self {
47 pub fn new(origin: ConfigOrigin) -> Self {
48 ConfigLayer {
48 ConfigLayer {
49 sections: HashMap::new(),
49 sections: HashMap::new(),
50 trusted: true, // TODO check
50 trusted: true, // TODO check
51 origin,
51 origin,
52 }
52 }
53 }
53 }
54
54
55 /// Parse `--config` CLI arguments and return a layer if there’s any
55 /// Parse `--config` CLI arguments and return a layer if there’s any
56 pub(crate) fn parse_cli_args(
56 pub(crate) fn parse_cli_args(
57 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
57 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
58 ) -> Result<Option<Self>, ConfigError> {
58 ) -> Result<Option<Self>, ConfigError> {
59 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
59 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
60 use crate::utils::SliceExt;
60 use crate::utils::SliceExt;
61
61
62 let (section_and_item, value) = arg.split_2(b'=')?;
62 let (section_and_item, value) = arg.split_2(b'=')?;
63 let (section, item) = section_and_item.trim().split_2(b'.')?;
63 let (section, item) = section_and_item.trim().split_2(b'.')?;
64 Some((
64 Some((
65 section.to_owned(),
65 section.to_owned(),
66 item.to_owned(),
66 item.to_owned(),
67 value.trim().to_owned(),
67 value.trim().to_owned(),
68 ))
68 ))
69 }
69 }
70
70
71 let mut layer = Self::new(ConfigOrigin::CommandLine);
71 let mut layer = Self::new(ConfigOrigin::CommandLine);
72 for arg in cli_config_args {
72 for arg in cli_config_args {
73 let arg = arg.as_ref();
73 let arg = arg.as_ref();
74 if let Some((section, item, value)) = parse_one(arg) {
74 if let Some((section, item, value)) = parse_one(arg) {
75 layer.add(section, item, value, None);
75 layer.add(section, item, value, None);
76 } else {
76 } else {
77 Err(HgError::abort(
77 Err(HgError::abort(
78 format!(
78 format!(
79 "abort: malformed --config option: '{}' \
79 "abort: malformed --config option: '{}' \
80 (use --config section.name=value)",
80 (use --config section.name=value)",
81 String::from_utf8_lossy(arg),
81 String::from_utf8_lossy(arg),
82 ),
82 ),
83 CONFIG_PARSE_ERROR_ABORT,
83 CONFIG_PARSE_ERROR_ABORT,
84 None,
84 None,
85 ))?
85 ))?
86 }
86 }
87 }
87 }
88 if layer.sections.is_empty() {
88 if layer.sections.is_empty() {
89 Ok(None)
89 Ok(None)
90 } else {
90 } else {
91 Ok(Some(layer))
91 Ok(Some(layer))
92 }
92 }
93 }
93 }
94
94
95 /// Returns whether this layer comes from `--config` CLI arguments
95 /// Returns whether this layer comes from `--config` CLI arguments
96 pub(crate) fn is_from_command_line(&self) -> bool {
96 pub(crate) fn is_from_command_line(&self) -> bool {
97 if let ConfigOrigin::CommandLine = self.origin {
97 if let ConfigOrigin::CommandLine = self.origin {
98 true
98 true
99 } else {
99 } else {
100 false
100 false
101 }
101 }
102 }
102 }
103
103
104 /// Add an entry to the config, overwriting the old one if already present.
104 /// Add an entry to the config, overwriting the old one if already present.
105 pub fn add(
105 pub fn add(
106 &mut self,
106 &mut self,
107 section: Vec<u8>,
107 section: Vec<u8>,
108 item: Vec<u8>,
108 item: Vec<u8>,
109 value: Vec<u8>,
109 value: Vec<u8>,
110 line: Option<usize>,
110 line: Option<usize>,
111 ) {
111 ) {
112 self.sections
112 self.sections
113 .entry(section)
113 .entry(section)
114 .or_insert_with(|| HashMap::new())
114 .or_insert_with(|| HashMap::new())
115 .insert(item, ConfigValue { bytes: value, line });
115 .insert(item, ConfigValue { bytes: value, line });
116 }
116 }
117
117
118 /// Returns the config value in `<section>.<item>` if it exists
118 /// Returns the config value in `<section>.<item>` if it exists
119 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
119 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
120 Some(self.sections.get(section)?.get(item)?)
120 Some(self.sections.get(section)?.get(item)?)
121 }
121 }
122
122
123 /// Returns the keys defined in the given section
123 /// Returns the keys defined in the given section
124 pub fn iter_keys(&self, section: &[u8]) -> impl Iterator<Item = &[u8]> {
124 pub fn iter_keys(&self, section: &[u8]) -> impl Iterator<Item = &[u8]> {
125 self.sections
125 self.sections
126 .get(section)
126 .get(section)
127 .into_iter()
127 .into_iter()
128 .flat_map(|section| section.keys().map(|vec| &**vec))
128 .flat_map(|section| section.keys().map(|vec| &**vec))
129 }
129 }
130
130
131 /// Returns the (key, value) pairs defined in the given section
131 /// Returns the (key, value) pairs defined in the given section
132 pub fn iter_section<'layer>(
132 pub fn iter_section<'layer>(
133 &'layer self,
133 &'layer self,
134 section: &[u8],
134 section: &[u8],
135 ) -> impl Iterator<Item = (&'layer [u8], &'layer [u8])> {
135 ) -> impl Iterator<Item = (&'layer [u8], &'layer [u8])> {
136 self.sections
136 self.sections
137 .get(section)
137 .get(section)
138 .into_iter()
138 .into_iter()
139 .flat_map(|section| section.iter().map(|(k, v)| (&**k, &*v.bytes)))
139 .flat_map(|section| section.iter().map(|(k, v)| (&**k, &*v.bytes)))
140 }
140 }
141
141
142 /// Returns whether any key is defined in the given section
142 /// Returns whether any key is defined in the given section
143 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
143 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
144 self.sections
144 self.sections
145 .get(section)
145 .get(section)
146 .map_or(false, |section| !section.is_empty())
146 .map_or(false, |section| !section.is_empty())
147 }
147 }
148
148
149 pub fn is_empty(&self) -> bool {
149 pub fn is_empty(&self) -> bool {
150 self.sections.is_empty()
150 self.sections.is_empty()
151 }
151 }
152
152
153 /// Returns a `Vec` of layers in order of precedence (so, in read order),
153 /// Returns a `Vec` of layers in order of precedence (so, in read order),
154 /// recursively parsing the `%include` directives if any.
154 /// recursively parsing the `%include` directives if any.
155 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
155 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
156 let mut layers = vec![];
156 let mut layers = vec![];
157
157
158 // Discard byte order mark if any
158 // Discard byte order mark if any
159 let data = if data.starts_with(b"\xef\xbb\xbf") {
159 let data = if data.starts_with(b"\xef\xbb\xbf") {
160 &data[3..]
160 &data[3..]
161 } else {
161 } else {
162 data
162 data
163 };
163 };
164
164
165 // TODO check if it's trusted
165 // TODO check if it's trusted
166 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
166 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
167
167
168 let mut lines_iter =
168 let mut lines_iter =
169 data.split(|b| *b == b'\n').enumerate().peekable();
169 data.split(|b| *b == b'\n').enumerate().peekable();
170 let mut section = b"".to_vec();
170 let mut section = b"".to_vec();
171
171
172 while let Some((index, bytes)) = lines_iter.next() {
172 while let Some((index, bytes)) = lines_iter.next() {
173 let line = Some(index + 1);
173 let line = Some(index + 1);
174 if let Some(m) = INCLUDE_RE.captures(&bytes) {
174 if let Some(m) = INCLUDE_RE.captures(&bytes) {
175 let filename_bytes = &m[1];
175 let filename_bytes = &m[1];
176 let filename_bytes = crate::utils::expand_vars(filename_bytes);
176 let filename_bytes = crate::utils::expand_vars(filename_bytes);
177 // `Path::parent` only fails for the root directory,
177 // `Path::parent` only fails for the root directory,
178 // which `src` can’t be since we’ve managed to open it as a
178 // which `src` can’t be since we’ve managed to open it as a
179 // file.
179 // file.
180 let dir = src
180 let dir = src
181 .parent()
181 .parent()
182 .expect("Path::parent fail on a file we’ve read");
182 .expect("Path::parent fail on a file we’ve read");
183 // `Path::join` with an absolute argument correctly ignores the
183 // `Path::join` with an absolute argument correctly ignores the
184 // base path
184 // base path
185 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
185 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
186 match std::fs::read(&filename) {
186 match std::fs::read(&filename) {
187 Ok(data) => {
187 Ok(data) => {
188 layers.push(current_layer);
188 layers.push(current_layer);
189 layers.extend(Self::parse(&filename, &data)?);
189 layers.extend(Self::parse(&filename, &data)?);
190 current_layer =
190 current_layer =
191 Self::new(ConfigOrigin::File(src.to_owned()));
191 Self::new(ConfigOrigin::File(src.to_owned()));
192 }
192 }
193 Err(error) => {
193 Err(error) => {
194 if error.kind() != std::io::ErrorKind::NotFound {
194 if error.kind() != std::io::ErrorKind::NotFound {
195 return Err(ConfigParseError {
195 return Err(ConfigParseError {
196 origin: ConfigOrigin::File(src.to_owned()),
196 origin: ConfigOrigin::File(src.to_owned()),
197 line,
197 line,
198 message: format_bytes!(
198 message: format_bytes!(
199 b"cannot include {} ({})",
199 b"cannot include {} ({})",
200 filename_bytes,
200 filename_bytes,
201 format_bytes::Utf8(error)
201 format_bytes::Utf8(error)
202 ),
202 ),
203 }
203 }
204 .into());
204 .into());
205 }
205 }
206 }
206 }
207 }
207 }
208 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
208 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
209 } else if let Some(m) = SECTION_RE.captures(&bytes) {
209 } else if let Some(m) = SECTION_RE.captures(&bytes) {
210 section = m[1].to_vec();
210 section = m[1].to_vec();
211 } else if let Some(m) = ITEM_RE.captures(&bytes) {
211 } else if let Some(m) = ITEM_RE.captures(&bytes) {
212 let item = m[1].to_vec();
212 let item = m[1].to_vec();
213 let mut value = m[2].to_vec();
213 let mut value = m[2].to_vec();
214 loop {
214 loop {
215 match lines_iter.peek() {
215 match lines_iter.peek() {
216 None => break,
216 None => break,
217 Some((_, v)) => {
217 Some((_, v)) => {
218 if let Some(_) = COMMENT_RE.captures(&v) {
218 if let Some(_) = COMMENT_RE.captures(&v) {
219 } else if let Some(_) = CONT_RE.captures(&v) {
219 } else if let Some(_) = CONT_RE.captures(&v) {
220 value.extend(b"\n");
220 value.extend(b"\n");
221 value.extend(&m[1]);
221 value.extend(&m[1]);
222 } else {
222 } else {
223 break;
223 break;
224 }
224 }
225 }
225 }
226 };
226 };
227 lines_iter.next();
227 lines_iter.next();
228 }
228 }
229 current_layer.add(section.clone(), item, value, line);
229 current_layer.add(section.clone(), item, value, line);
230 } else if let Some(m) = UNSET_RE.captures(&bytes) {
230 } else if let Some(m) = UNSET_RE.captures(&bytes) {
231 if let Some(map) = current_layer.sections.get_mut(&section) {
231 if let Some(map) = current_layer.sections.get_mut(&section) {
232 map.remove(&m[1]);
232 map.remove(&m[1]);
233 }
233 }
234 } else {
234 } else {
235 let message = if bytes.starts_with(b" ") {
235 let message = if bytes.starts_with(b" ") {
236 format_bytes!(b"unexpected leading whitespace: {}", bytes)
236 format_bytes!(b"unexpected leading whitespace: {}", bytes)
237 } else {
237 } else {
238 bytes.to_owned()
238 bytes.to_owned()
239 };
239 };
240 return Err(ConfigParseError {
240 return Err(ConfigParseError {
241 origin: ConfigOrigin::File(src.to_owned()),
241 origin: ConfigOrigin::File(src.to_owned()),
242 line,
242 line,
243 message,
243 message,
244 }
244 }
245 .into());
245 .into());
246 }
246 }
247 }
247 }
248 if !current_layer.is_empty() {
248 if !current_layer.is_empty() {
249 layers.push(current_layer);
249 layers.push(current_layer);
250 }
250 }
251 Ok(layers)
251 Ok(layers)
252 }
252 }
253 }
253 }
254
254
255 impl DisplayBytes for ConfigLayer {
255 impl DisplayBytes for ConfigLayer {
256 fn display_bytes(
256 fn display_bytes(
257 &self,
257 &self,
258 out: &mut dyn std::io::Write,
258 out: &mut dyn std::io::Write,
259 ) -> std::io::Result<()> {
259 ) -> std::io::Result<()> {
260 let mut sections: Vec<_> = self.sections.iter().collect();
260 let mut sections: Vec<_> = self.sections.iter().collect();
261 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
261 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
262
262
263 for (section, items) in sections.into_iter() {
263 for (section, items) in sections.into_iter() {
264 let mut items: Vec<_> = items.into_iter().collect();
264 let mut items: Vec<_> = items.into_iter().collect();
265 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
265 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
266
266
267 for (item, config_entry) in items {
267 for (item, config_entry) in items {
268 write_bytes!(
268 write_bytes!(
269 out,
269 out,
270 b"{}.{}={} # {}\n",
270 b"{}.{}={} # {}\n",
271 section,
271 section,
272 item,
272 item,
273 &config_entry.bytes,
273 &config_entry.bytes,
274 &self.origin,
274 &self.origin,
275 )?
275 )?
276 }
276 }
277 }
277 }
278 Ok(())
278 Ok(())
279 }
279 }
280 }
280 }
281
281
282 /// Mapping of section item to value.
282 /// Mapping of section item to value.
283 /// In the following:
283 /// In the following:
284 /// ```text
284 /// ```text
285 /// [ui]
285 /// [ui]
286 /// paginate=no
286 /// paginate=no
287 /// ```
287 /// ```
288 /// "paginate" is the section item and "no" the value.
288 /// "paginate" is the section item and "no" the value.
289 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
289 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
290
290
291 #[derive(Clone, Debug, PartialEq)]
291 #[derive(Clone, Debug, PartialEq)]
292 pub struct ConfigValue {
292 pub struct ConfigValue {
293 /// The raw bytes of the value (be it from the CLI, env or from a file)
293 /// The raw bytes of the value (be it from the CLI, env or from a file)
294 pub bytes: Vec<u8>,
294 pub bytes: Vec<u8>,
295 /// Only present if the value comes from a file, 1-indexed.
295 /// Only present if the value comes from a file, 1-indexed.
296 pub line: Option<usize>,
296 pub line: Option<usize>,
297 }
297 }
298
298
299 #[derive(Clone, Debug, PartialEq, Eq)]
299 #[derive(Clone, Debug, PartialEq, Eq)]
300 pub enum ConfigOrigin {
300 pub enum ConfigOrigin {
301 /// From a configuration file
301 /// From a configuration file
302 File(PathBuf),
302 File(PathBuf),
303 /// From [ui.tweakdefaults]
304 Tweakdefaults,
303 /// From a `--config` CLI argument
305 /// From a `--config` CLI argument
304 CommandLine,
306 CommandLine,
305 /// From a `--color` CLI argument
307 /// From a `--color` CLI argument
306 CommandLineColor,
308 CommandLineColor,
307 /// From environment variables like `$PAGER` or `$EDITOR`
309 /// From environment variables like `$PAGER` or `$EDITOR`
308 Environment(Vec<u8>),
310 Environment(Vec<u8>),
309 /* TODO defaults (configitems.py)
311 /* TODO defaults (configitems.py)
310 * TODO extensions
312 * TODO extensions
311 * TODO Python resources?
313 * TODO Python resources?
312 * Others? */
314 * Others? */
313 }
315 }
314
316
315 impl DisplayBytes for ConfigOrigin {
317 impl DisplayBytes for ConfigOrigin {
316 fn display_bytes(
318 fn display_bytes(
317 &self,
319 &self,
318 out: &mut dyn std::io::Write,
320 out: &mut dyn std::io::Write,
319 ) -> std::io::Result<()> {
321 ) -> std::io::Result<()> {
320 match self {
322 match self {
321 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
323 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
322 ConfigOrigin::CommandLine => out.write_all(b"--config"),
324 ConfigOrigin::CommandLine => out.write_all(b"--config"),
323 ConfigOrigin::CommandLineColor => out.write_all(b"--color"),
325 ConfigOrigin::CommandLineColor => out.write_all(b"--color"),
324 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
326 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
327 ConfigOrigin::Tweakdefaults => {
328 write_bytes!(out, b"ui.tweakdefaults")
329 }
325 }
330 }
326 }
331 }
327 }
332 }
328
333
329 #[derive(Debug)]
334 #[derive(Debug)]
330 pub struct ConfigParseError {
335 pub struct ConfigParseError {
331 pub origin: ConfigOrigin,
336 pub origin: ConfigOrigin,
332 pub line: Option<usize>,
337 pub line: Option<usize>,
333 pub message: Vec<u8>,
338 pub message: Vec<u8>,
334 }
339 }
335
340
336 #[derive(Debug, derive_more::From)]
341 #[derive(Debug, derive_more::From)]
337 pub enum ConfigError {
342 pub enum ConfigError {
338 Parse(ConfigParseError),
343 Parse(ConfigParseError),
339 Other(HgError),
344 Other(HgError),
340 }
345 }
341
346
342 fn make_regex(pattern: &'static str) -> Regex {
347 fn make_regex(pattern: &'static str) -> Regex {
343 Regex::new(pattern).expect("expected a valid regex")
348 Regex::new(pattern).expect("expected a valid regex")
344 }
349 }
@@ -1,618 +1,613 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 use crate::error::CommandError;
8 use crate::error::CommandError;
9 use crate::ui::Ui;
9 use crate::ui::Ui;
10 use crate::utils::path_utils::RelativizePaths;
10 use crate::utils::path_utils::RelativizePaths;
11 use clap::{Arg, SubCommand};
11 use clap::{Arg, SubCommand};
12 use format_bytes::format_bytes;
12 use format_bytes::format_bytes;
13 use hg::config::Config;
13 use hg::config::Config;
14 use hg::dirstate::has_exec_bit;
14 use hg::dirstate::has_exec_bit;
15 use hg::dirstate::status::StatusPath;
15 use hg::dirstate::status::StatusPath;
16 use hg::dirstate::TruncatedTimestamp;
16 use hg::dirstate::TruncatedTimestamp;
17 use hg::errors::{HgError, IoResultExt};
17 use hg::errors::{HgError, IoResultExt};
18 use hg::lock::LockError;
18 use hg::lock::LockError;
19 use hg::manifest::Manifest;
19 use hg::manifest::Manifest;
20 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
20 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
21 use hg::repo::Repo;
21 use hg::repo::Repo;
22 use hg::utils::files::get_bytes_from_os_string;
22 use hg::utils::files::get_bytes_from_os_string;
23 use hg::utils::files::get_bytes_from_path;
23 use hg::utils::files::get_bytes_from_path;
24 use hg::utils::files::get_path_from_bytes;
24 use hg::utils::files::get_path_from_bytes;
25 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
25 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
26 use hg::DirstateStatus;
26 use hg::DirstateStatus;
27 use hg::PatternFileWarning;
27 use hg::PatternFileWarning;
28 use hg::StatusError;
28 use hg::StatusError;
29 use hg::StatusOptions;
29 use hg::StatusOptions;
30 use hg::{self, narrow, sparse};
30 use hg::{self, narrow, sparse};
31 use log::info;
31 use log::info;
32 use std::io;
32 use std::io;
33 use std::path::PathBuf;
33 use std::path::PathBuf;
34
34
35 pub const HELP_TEXT: &str = "
35 pub const HELP_TEXT: &str = "
36 Show changed files in the working directory
36 Show changed files in the working directory
37
37
38 This is a pure Rust version of `hg status`.
38 This is a pure Rust version of `hg status`.
39
39
40 Some options might be missing, check the list below.
40 Some options might be missing, check the list below.
41 ";
41 ";
42
42
43 pub fn args() -> clap::App<'static, 'static> {
43 pub fn args() -> clap::App<'static, 'static> {
44 SubCommand::with_name("status")
44 SubCommand::with_name("status")
45 .alias("st")
45 .alias("st")
46 .about(HELP_TEXT)
46 .about(HELP_TEXT)
47 .arg(
47 .arg(
48 Arg::with_name("all")
48 Arg::with_name("all")
49 .help("show status of all files")
49 .help("show status of all files")
50 .short("-A")
50 .short("-A")
51 .long("--all"),
51 .long("--all"),
52 )
52 )
53 .arg(
53 .arg(
54 Arg::with_name("modified")
54 Arg::with_name("modified")
55 .help("show only modified files")
55 .help("show only modified files")
56 .short("-m")
56 .short("-m")
57 .long("--modified"),
57 .long("--modified"),
58 )
58 )
59 .arg(
59 .arg(
60 Arg::with_name("added")
60 Arg::with_name("added")
61 .help("show only added files")
61 .help("show only added files")
62 .short("-a")
62 .short("-a")
63 .long("--added"),
63 .long("--added"),
64 )
64 )
65 .arg(
65 .arg(
66 Arg::with_name("removed")
66 Arg::with_name("removed")
67 .help("show only removed files")
67 .help("show only removed files")
68 .short("-r")
68 .short("-r")
69 .long("--removed"),
69 .long("--removed"),
70 )
70 )
71 .arg(
71 .arg(
72 Arg::with_name("clean")
72 Arg::with_name("clean")
73 .help("show only clean files")
73 .help("show only clean files")
74 .short("-c")
74 .short("-c")
75 .long("--clean"),
75 .long("--clean"),
76 )
76 )
77 .arg(
77 .arg(
78 Arg::with_name("deleted")
78 Arg::with_name("deleted")
79 .help("show only deleted files")
79 .help("show only deleted files")
80 .short("-d")
80 .short("-d")
81 .long("--deleted"),
81 .long("--deleted"),
82 )
82 )
83 .arg(
83 .arg(
84 Arg::with_name("unknown")
84 Arg::with_name("unknown")
85 .help("show only unknown (not tracked) files")
85 .help("show only unknown (not tracked) files")
86 .short("-u")
86 .short("-u")
87 .long("--unknown"),
87 .long("--unknown"),
88 )
88 )
89 .arg(
89 .arg(
90 Arg::with_name("ignored")
90 Arg::with_name("ignored")
91 .help("show only ignored files")
91 .help("show only ignored files")
92 .short("-i")
92 .short("-i")
93 .long("--ignored"),
93 .long("--ignored"),
94 )
94 )
95 .arg(
95 .arg(
96 Arg::with_name("copies")
96 Arg::with_name("copies")
97 .help("show source of copied files (DEFAULT: ui.statuscopies)")
97 .help("show source of copied files (DEFAULT: ui.statuscopies)")
98 .short("-C")
98 .short("-C")
99 .long("--copies"),
99 .long("--copies"),
100 )
100 )
101 .arg(
101 .arg(
102 Arg::with_name("no-status")
102 Arg::with_name("no-status")
103 .help("hide status prefix")
103 .help("hide status prefix")
104 .short("-n")
104 .short("-n")
105 .long("--no-status"),
105 .long("--no-status"),
106 )
106 )
107 .arg(
107 .arg(
108 Arg::with_name("verbose")
108 Arg::with_name("verbose")
109 .help("enable additional output")
109 .help("enable additional output")
110 .short("-v")
110 .short("-v")
111 .long("--verbose"),
111 .long("--verbose"),
112 )
112 )
113 }
113 }
114
114
115 /// Pure data type allowing the caller to specify file states to display
115 /// Pure data type allowing the caller to specify file states to display
116 #[derive(Copy, Clone, Debug)]
116 #[derive(Copy, Clone, Debug)]
117 pub struct DisplayStates {
117 pub struct DisplayStates {
118 pub modified: bool,
118 pub modified: bool,
119 pub added: bool,
119 pub added: bool,
120 pub removed: bool,
120 pub removed: bool,
121 pub clean: bool,
121 pub clean: bool,
122 pub deleted: bool,
122 pub deleted: bool,
123 pub unknown: bool,
123 pub unknown: bool,
124 pub ignored: bool,
124 pub ignored: bool,
125 }
125 }
126
126
127 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
127 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
128 modified: true,
128 modified: true,
129 added: true,
129 added: true,
130 removed: true,
130 removed: true,
131 clean: false,
131 clean: false,
132 deleted: true,
132 deleted: true,
133 unknown: true,
133 unknown: true,
134 ignored: false,
134 ignored: false,
135 };
135 };
136
136
137 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
137 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
138 modified: true,
138 modified: true,
139 added: true,
139 added: true,
140 removed: true,
140 removed: true,
141 clean: true,
141 clean: true,
142 deleted: true,
142 deleted: true,
143 unknown: true,
143 unknown: true,
144 ignored: true,
144 ignored: true,
145 };
145 };
146
146
147 impl DisplayStates {
147 impl DisplayStates {
148 pub fn is_empty(&self) -> bool {
148 pub fn is_empty(&self) -> bool {
149 !(self.modified
149 !(self.modified
150 || self.added
150 || self.added
151 || self.removed
151 || self.removed
152 || self.clean
152 || self.clean
153 || self.deleted
153 || self.deleted
154 || self.unknown
154 || self.unknown
155 || self.ignored)
155 || self.ignored)
156 }
156 }
157 }
157 }
158
158
159 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
159 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
160 return Ok(repo.dirstate_parents()?.is_merge());
160 return Ok(repo.dirstate_parents()?.is_merge());
161 }
161 }
162
162
163 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
163 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
164 // These are all the known values for the [fname] argument of
164 // These are all the known values for the [fname] argument of
165 // [addunfinished] function in [state.py]
165 // [addunfinished] function in [state.py]
166 let known_state_files: &[&str] = &[
166 let known_state_files: &[&str] = &[
167 "bisect.state",
167 "bisect.state",
168 "graftstate",
168 "graftstate",
169 "histedit-state",
169 "histedit-state",
170 "rebasestate",
170 "rebasestate",
171 "shelvedstate",
171 "shelvedstate",
172 "transplant/journal",
172 "transplant/journal",
173 "updatestate",
173 "updatestate",
174 ];
174 ];
175 if has_unfinished_merge(repo)? {
175 if has_unfinished_merge(repo)? {
176 return Ok(true);
176 return Ok(true);
177 };
177 };
178 for f in known_state_files {
178 for f in known_state_files {
179 if repo.hg_vfs().join(f).exists() {
179 if repo.hg_vfs().join(f).exists() {
180 return Ok(true);
180 return Ok(true);
181 }
181 }
182 }
182 }
183 return Ok(false);
183 return Ok(false);
184 }
184 }
185
185
186 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
186 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
187 // TODO: lift these limitations
187 // TODO: lift these limitations
188 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
189 return Err(CommandError::unsupported(
190 "ui.tweakdefaults is not yet supported with rhg status",
191 ));
192 }
193 if invocation.config.get_bool(b"ui", b"statuscopies")? {
188 if invocation.config.get_bool(b"ui", b"statuscopies")? {
194 return Err(CommandError::unsupported(
189 return Err(CommandError::unsupported(
195 "ui.statuscopies is not yet supported with rhg status",
190 "ui.statuscopies is not yet supported with rhg status",
196 ));
191 ));
197 }
192 }
198 if invocation
193 if invocation
199 .config
194 .config
200 .get(b"commands", b"status.terse")
195 .get(b"commands", b"status.terse")
201 .is_some()
196 .is_some()
202 {
197 {
203 return Err(CommandError::unsupported(
198 return Err(CommandError::unsupported(
204 "status.terse is not yet supported with rhg status",
199 "status.terse is not yet supported with rhg status",
205 ));
200 ));
206 }
201 }
207
202
208 let ui = invocation.ui;
203 let ui = invocation.ui;
209 let config = invocation.config;
204 let config = invocation.config;
210 let args = invocation.subcommand_args;
205 let args = invocation.subcommand_args;
211
206
212 let verbose = !args.is_present("print0")
207 let verbose = !args.is_present("print0")
213 && (args.is_present("verbose")
208 && (args.is_present("verbose")
214 || config.get_bool(b"ui", b"verbose")?
209 || config.get_bool(b"ui", b"verbose")?
215 || config.get_bool(b"commands", b"status.verbose")?);
210 || config.get_bool(b"commands", b"status.verbose")?);
216
211
217 let all = args.is_present("all");
212 let all = args.is_present("all");
218 let display_states = if all {
213 let display_states = if all {
219 // TODO when implementing `--quiet`: it excludes clean files
214 // TODO when implementing `--quiet`: it excludes clean files
220 // from `--all`
215 // from `--all`
221 ALL_DISPLAY_STATES
216 ALL_DISPLAY_STATES
222 } else {
217 } else {
223 let requested = DisplayStates {
218 let requested = DisplayStates {
224 modified: args.is_present("modified"),
219 modified: args.is_present("modified"),
225 added: args.is_present("added"),
220 added: args.is_present("added"),
226 removed: args.is_present("removed"),
221 removed: args.is_present("removed"),
227 clean: args.is_present("clean"),
222 clean: args.is_present("clean"),
228 deleted: args.is_present("deleted"),
223 deleted: args.is_present("deleted"),
229 unknown: args.is_present("unknown"),
224 unknown: args.is_present("unknown"),
230 ignored: args.is_present("ignored"),
225 ignored: args.is_present("ignored"),
231 };
226 };
232 if requested.is_empty() {
227 if requested.is_empty() {
233 DEFAULT_DISPLAY_STATES
228 DEFAULT_DISPLAY_STATES
234 } else {
229 } else {
235 requested
230 requested
236 }
231 }
237 };
232 };
238 let no_status = args.is_present("no-status");
233 let no_status = args.is_present("no-status");
239 let list_copies = all
234 let list_copies = all
240 || args.is_present("copies")
235 || args.is_present("copies")
241 || config.get_bool(b"ui", b"statuscopies")?;
236 || config.get_bool(b"ui", b"statuscopies")?;
242
237
243 let repo = invocation.repo?;
238 let repo = invocation.repo?;
244
239
245 if verbose {
240 if verbose {
246 if has_unfinished_state(repo)? {
241 if has_unfinished_state(repo)? {
247 return Err(CommandError::unsupported(
242 return Err(CommandError::unsupported(
248 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
243 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
249 ));
244 ));
250 };
245 };
251 }
246 }
252
247
253 let mut dmap = repo.dirstate_map_mut()?;
248 let mut dmap = repo.dirstate_map_mut()?;
254
249
255 let options = StatusOptions {
250 let options = StatusOptions {
256 // we're currently supporting file systems with exec flags only
251 // we're currently supporting file systems with exec flags only
257 // anyway
252 // anyway
258 check_exec: true,
253 check_exec: true,
259 list_clean: display_states.clean,
254 list_clean: display_states.clean,
260 list_unknown: display_states.unknown,
255 list_unknown: display_states.unknown,
261 list_ignored: display_states.ignored,
256 list_ignored: display_states.ignored,
262 list_copies,
257 list_copies,
263 collect_traversed_dirs: false,
258 collect_traversed_dirs: false,
264 };
259 };
265
260
266 type StatusResult<'a> =
261 type StatusResult<'a> =
267 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
262 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
268
263
269 let after_status = |res: StatusResult| -> Result<_, CommandError> {
264 let after_status = |res: StatusResult| -> Result<_, CommandError> {
270 let (mut ds_status, pattern_warnings) = res?;
265 let (mut ds_status, pattern_warnings) = res?;
271 for warning in pattern_warnings {
266 for warning in pattern_warnings {
272 ui.write_stderr(&print_pattern_file_warning(&warning, &repo))?;
267 ui.write_stderr(&print_pattern_file_warning(&warning, &repo))?;
273 }
268 }
274
269
275 for (path, error) in ds_status.bad {
270 for (path, error) in ds_status.bad {
276 let error = match error {
271 let error = match error {
277 hg::BadMatch::OsError(code) => {
272 hg::BadMatch::OsError(code) => {
278 std::io::Error::from_raw_os_error(code).to_string()
273 std::io::Error::from_raw_os_error(code).to_string()
279 }
274 }
280 hg::BadMatch::BadType(ty) => {
275 hg::BadMatch::BadType(ty) => {
281 format!("unsupported file type (type is {})", ty)
276 format!("unsupported file type (type is {})", ty)
282 }
277 }
283 };
278 };
284 ui.write_stderr(&format_bytes!(
279 ui.write_stderr(&format_bytes!(
285 b"{}: {}\n",
280 b"{}: {}\n",
286 path.as_bytes(),
281 path.as_bytes(),
287 error.as_bytes()
282 error.as_bytes()
288 ))?
283 ))?
289 }
284 }
290 if !ds_status.unsure.is_empty() {
285 if !ds_status.unsure.is_empty() {
291 info!(
286 info!(
292 "Files to be rechecked by retrieval from filelog: {:?}",
287 "Files to be rechecked by retrieval from filelog: {:?}",
293 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
288 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
294 );
289 );
295 }
290 }
296 let mut fixup = Vec::new();
291 let mut fixup = Vec::new();
297 if !ds_status.unsure.is_empty()
292 if !ds_status.unsure.is_empty()
298 && (display_states.modified || display_states.clean)
293 && (display_states.modified || display_states.clean)
299 {
294 {
300 let p1 = repo.dirstate_parents()?.p1;
295 let p1 = repo.dirstate_parents()?.p1;
301 let manifest = repo.manifest_for_node(p1).map_err(|e| {
296 let manifest = repo.manifest_for_node(p1).map_err(|e| {
302 CommandError::from((e, &*format!("{:x}", p1.short())))
297 CommandError::from((e, &*format!("{:x}", p1.short())))
303 })?;
298 })?;
304 for to_check in ds_status.unsure {
299 for to_check in ds_status.unsure {
305 if unsure_is_modified(repo, &manifest, &to_check.path)? {
300 if unsure_is_modified(repo, &manifest, &to_check.path)? {
306 if display_states.modified {
301 if display_states.modified {
307 ds_status.modified.push(to_check);
302 ds_status.modified.push(to_check);
308 }
303 }
309 } else {
304 } else {
310 if display_states.clean {
305 if display_states.clean {
311 ds_status.clean.push(to_check.clone());
306 ds_status.clean.push(to_check.clone());
312 }
307 }
313 fixup.push(to_check.path.into_owned())
308 fixup.push(to_check.path.into_owned())
314 }
309 }
315 }
310 }
316 }
311 }
317 let relative_paths = config
312 let relative_paths = config
318 .get_option(b"commands", b"status.relative")?
313 .get_option(b"commands", b"status.relative")?
319 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
314 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
320 let output = DisplayStatusPaths {
315 let output = DisplayStatusPaths {
321 ui,
316 ui,
322 no_status,
317 no_status,
323 relativize: if relative_paths {
318 relativize: if relative_paths {
324 Some(RelativizePaths::new(repo)?)
319 Some(RelativizePaths::new(repo)?)
325 } else {
320 } else {
326 None
321 None
327 },
322 },
328 };
323 };
329 if display_states.modified {
324 if display_states.modified {
330 output.display(b"M ", "status.modified", ds_status.modified)?;
325 output.display(b"M ", "status.modified", ds_status.modified)?;
331 }
326 }
332 if display_states.added {
327 if display_states.added {
333 output.display(b"A ", "status.added", ds_status.added)?;
328 output.display(b"A ", "status.added", ds_status.added)?;
334 }
329 }
335 if display_states.removed {
330 if display_states.removed {
336 output.display(b"R ", "status.removed", ds_status.removed)?;
331 output.display(b"R ", "status.removed", ds_status.removed)?;
337 }
332 }
338 if display_states.deleted {
333 if display_states.deleted {
339 output.display(b"! ", "status.deleted", ds_status.deleted)?;
334 output.display(b"! ", "status.deleted", ds_status.deleted)?;
340 }
335 }
341 if display_states.unknown {
336 if display_states.unknown {
342 output.display(b"? ", "status.unknown", ds_status.unknown)?;
337 output.display(b"? ", "status.unknown", ds_status.unknown)?;
343 }
338 }
344 if display_states.ignored {
339 if display_states.ignored {
345 output.display(b"I ", "status.ignored", ds_status.ignored)?;
340 output.display(b"I ", "status.ignored", ds_status.ignored)?;
346 }
341 }
347 if display_states.clean {
342 if display_states.clean {
348 output.display(b"C ", "status.clean", ds_status.clean)?;
343 output.display(b"C ", "status.clean", ds_status.clean)?;
349 }
344 }
350
345
351 let dirstate_write_needed = ds_status.dirty;
346 let dirstate_write_needed = ds_status.dirty;
352 let filesystem_time_at_status_start =
347 let filesystem_time_at_status_start =
353 ds_status.filesystem_time_at_status_start;
348 ds_status.filesystem_time_at_status_start;
354
349
355 Ok((
350 Ok((
356 fixup,
351 fixup,
357 dirstate_write_needed,
352 dirstate_write_needed,
358 filesystem_time_at_status_start,
353 filesystem_time_at_status_start,
359 ))
354 ))
360 };
355 };
361 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
356 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
362 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
357 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
363 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
358 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
364 (true, true) => {
359 (true, true) => {
365 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
360 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
366 }
361 }
367 (true, false) => narrow_matcher,
362 (true, false) => narrow_matcher,
368 (false, true) => sparse_matcher,
363 (false, true) => sparse_matcher,
369 (false, false) => Box::new(AlwaysMatcher),
364 (false, false) => Box::new(AlwaysMatcher),
370 };
365 };
371
366
372 for warning in narrow_warnings.into_iter().chain(sparse_warnings) {
367 for warning in narrow_warnings.into_iter().chain(sparse_warnings) {
373 match &warning {
368 match &warning {
374 sparse::SparseWarning::RootWarning { context, line } => {
369 sparse::SparseWarning::RootWarning { context, line } => {
375 let msg = format_bytes!(
370 let msg = format_bytes!(
376 b"warning: {} profile cannot use paths \"
371 b"warning: {} profile cannot use paths \"
377 starting with /, ignoring {}\n",
372 starting with /, ignoring {}\n",
378 context,
373 context,
379 line
374 line
380 );
375 );
381 ui.write_stderr(&msg)?;
376 ui.write_stderr(&msg)?;
382 }
377 }
383 sparse::SparseWarning::ProfileNotFound { profile, rev } => {
378 sparse::SparseWarning::ProfileNotFound { profile, rev } => {
384 let msg = format_bytes!(
379 let msg = format_bytes!(
385 b"warning: sparse profile '{}' not found \"
380 b"warning: sparse profile '{}' not found \"
386 in rev {} - ignoring it\n",
381 in rev {} - ignoring it\n",
387 profile,
382 profile,
388 rev
383 rev
389 );
384 );
390 ui.write_stderr(&msg)?;
385 ui.write_stderr(&msg)?;
391 }
386 }
392 sparse::SparseWarning::Pattern(e) => {
387 sparse::SparseWarning::Pattern(e) => {
393 ui.write_stderr(&print_pattern_file_warning(e, &repo))?;
388 ui.write_stderr(&print_pattern_file_warning(e, &repo))?;
394 }
389 }
395 }
390 }
396 }
391 }
397 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
392 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
398 dmap.with_status(
393 dmap.with_status(
399 matcher.as_ref(),
394 matcher.as_ref(),
400 repo.working_directory_path().to_owned(),
395 repo.working_directory_path().to_owned(),
401 ignore_files(repo, config),
396 ignore_files(repo, config),
402 options,
397 options,
403 after_status,
398 after_status,
404 )?;
399 )?;
405
400
406 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
401 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
407 && !dirstate_write_needed
402 && !dirstate_write_needed
408 {
403 {
409 // Nothing to update
404 // Nothing to update
410 return Ok(());
405 return Ok(());
411 }
406 }
412
407
413 // Update the dirstate on disk if we can
408 // Update the dirstate on disk if we can
414 let with_lock_result =
409 let with_lock_result =
415 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
410 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
416 if let Some(mtime_boundary) = filesystem_time_at_status_start {
411 if let Some(mtime_boundary) = filesystem_time_at_status_start {
417 for hg_path in fixup {
412 for hg_path in fixup {
418 use std::os::unix::fs::MetadataExt;
413 use std::os::unix::fs::MetadataExt;
419 let fs_path = hg_path_to_path_buf(&hg_path)
414 let fs_path = hg_path_to_path_buf(&hg_path)
420 .expect("HgPath conversion");
415 .expect("HgPath conversion");
421 // Specifically do not reuse `fs_metadata` from
416 // Specifically do not reuse `fs_metadata` from
422 // `unsure_is_clean` which was needed before reading
417 // `unsure_is_clean` which was needed before reading
423 // contents. Here we access metadata again after reading
418 // contents. Here we access metadata again after reading
424 // content, in case it changed in the meantime.
419 // content, in case it changed in the meantime.
425 let fs_metadata = repo
420 let fs_metadata = repo
426 .working_directory_vfs()
421 .working_directory_vfs()
427 .symlink_metadata(&fs_path)?;
422 .symlink_metadata(&fs_path)?;
428 if let Some(mtime) =
423 if let Some(mtime) =
429 TruncatedTimestamp::for_reliable_mtime_of(
424 TruncatedTimestamp::for_reliable_mtime_of(
430 &fs_metadata,
425 &fs_metadata,
431 &mtime_boundary,
426 &mtime_boundary,
432 )
427 )
433 .when_reading_file(&fs_path)?
428 .when_reading_file(&fs_path)?
434 {
429 {
435 let mode = fs_metadata.mode();
430 let mode = fs_metadata.mode();
436 let size = fs_metadata.len();
431 let size = fs_metadata.len();
437 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
432 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
438 dirstate_write_needed = true
433 dirstate_write_needed = true
439 }
434 }
440 }
435 }
441 }
436 }
442 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
437 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
443 if dirstate_write_needed {
438 if dirstate_write_needed {
444 repo.write_dirstate()?
439 repo.write_dirstate()?
445 }
440 }
446 Ok(())
441 Ok(())
447 });
442 });
448 match with_lock_result {
443 match with_lock_result {
449 Ok(closure_result) => closure_result?,
444 Ok(closure_result) => closure_result?,
450 Err(LockError::AlreadyHeld) => {
445 Err(LockError::AlreadyHeld) => {
451 // Not updating the dirstate is not ideal but not critical:
446 // Not updating the dirstate is not ideal but not critical:
452 // don’t keep our caller waiting until some other Mercurial
447 // don’t keep our caller waiting until some other Mercurial
453 // process releases the lock.
448 // process releases the lock.
454 }
449 }
455 Err(LockError::Other(HgError::IoError { error, .. }))
450 Err(LockError::Other(HgError::IoError { error, .. }))
456 if error.kind() == io::ErrorKind::PermissionDenied =>
451 if error.kind() == io::ErrorKind::PermissionDenied =>
457 {
452 {
458 // `hg status` on a read-only repository is fine
453 // `hg status` on a read-only repository is fine
459 }
454 }
460 Err(LockError::Other(error)) => {
455 Err(LockError::Other(error)) => {
461 // Report other I/O errors
456 // Report other I/O errors
462 Err(error)?
457 Err(error)?
463 }
458 }
464 }
459 }
465 Ok(())
460 Ok(())
466 }
461 }
467
462
468 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
463 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
469 let mut ignore_files = Vec::new();
464 let mut ignore_files = Vec::new();
470 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
465 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
471 if repo_ignore.exists() {
466 if repo_ignore.exists() {
472 ignore_files.push(repo_ignore)
467 ignore_files.push(repo_ignore)
473 }
468 }
474 for (key, value) in config.iter_section(b"ui") {
469 for (key, value) in config.iter_section(b"ui") {
475 if key == b"ignore" || key.starts_with(b"ignore.") {
470 if key == b"ignore" || key.starts_with(b"ignore.") {
476 let path = get_path_from_bytes(value);
471 let path = get_path_from_bytes(value);
477 // TODO:Β expand "~/" and environment variable here, like Python
472 // TODO:Β expand "~/" and environment variable here, like Python
478 // does with `os.path.expanduser` and `os.path.expandvars`
473 // does with `os.path.expanduser` and `os.path.expandvars`
479
474
480 let joined = repo.working_directory_path().join(path);
475 let joined = repo.working_directory_path().join(path);
481 ignore_files.push(joined);
476 ignore_files.push(joined);
482 }
477 }
483 }
478 }
484 ignore_files
479 ignore_files
485 }
480 }
486
481
487 struct DisplayStatusPaths<'a> {
482 struct DisplayStatusPaths<'a> {
488 ui: &'a Ui,
483 ui: &'a Ui,
489 no_status: bool,
484 no_status: bool,
490 relativize: Option<RelativizePaths>,
485 relativize: Option<RelativizePaths>,
491 }
486 }
492
487
493 impl DisplayStatusPaths<'_> {
488 impl DisplayStatusPaths<'_> {
494 // Probably more elegant to use a Deref or Borrow trait rather than
489 // Probably more elegant to use a Deref or Borrow trait rather than
495 // harcode HgPathBuf, but probably not really useful at this point
490 // harcode HgPathBuf, but probably not really useful at this point
496 fn display(
491 fn display(
497 &self,
492 &self,
498 status_prefix: &[u8],
493 status_prefix: &[u8],
499 label: &'static str,
494 label: &'static str,
500 mut paths: Vec<StatusPath<'_>>,
495 mut paths: Vec<StatusPath<'_>>,
501 ) -> Result<(), CommandError> {
496 ) -> Result<(), CommandError> {
502 paths.sort_unstable();
497 paths.sort_unstable();
503 // TODO: get the stdout lock once for the whole loop
498 // TODO: get the stdout lock once for the whole loop
504 // instead of in each write
499 // instead of in each write
505 for StatusPath { path, copy_source } in paths {
500 for StatusPath { path, copy_source } in paths {
506 let relative;
501 let relative;
507 let path = if let Some(relativize) = &self.relativize {
502 let path = if let Some(relativize) = &self.relativize {
508 relative = relativize.relativize(&path);
503 relative = relativize.relativize(&path);
509 &*relative
504 &*relative
510 } else {
505 } else {
511 path.as_bytes()
506 path.as_bytes()
512 };
507 };
513 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
508 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
514 // in order to stream to stdout instead of allocating an
509 // in order to stream to stdout instead of allocating an
515 // itermediate `Vec<u8>`.
510 // itermediate `Vec<u8>`.
516 if !self.no_status {
511 if !self.no_status {
517 self.ui.write_stdout_labelled(status_prefix, label)?
512 self.ui.write_stdout_labelled(status_prefix, label)?
518 }
513 }
519 self.ui
514 self.ui
520 .write_stdout_labelled(&format_bytes!(b"{}\n", path), label)?;
515 .write_stdout_labelled(&format_bytes!(b"{}\n", path), label)?;
521 if let Some(source) = copy_source {
516 if let Some(source) = copy_source {
522 let label = "status.copied";
517 let label = "status.copied";
523 self.ui.write_stdout_labelled(
518 self.ui.write_stdout_labelled(
524 &format_bytes!(b" {}\n", source.as_bytes()),
519 &format_bytes!(b" {}\n", source.as_bytes()),
525 label,
520 label,
526 )?
521 )?
527 }
522 }
528 }
523 }
529 Ok(())
524 Ok(())
530 }
525 }
531 }
526 }
532
527
533 /// Check if a file is modified by comparing actual repo store and file system.
528 /// Check if a file is modified by comparing actual repo store and file system.
534 ///
529 ///
535 /// This meant to be used for those that the dirstate cannot resolve, due
530 /// This meant to be used for those that the dirstate cannot resolve, due
536 /// to time resolution limits.
531 /// to time resolution limits.
537 fn unsure_is_modified(
532 fn unsure_is_modified(
538 repo: &Repo,
533 repo: &Repo,
539 manifest: &Manifest,
534 manifest: &Manifest,
540 hg_path: &HgPath,
535 hg_path: &HgPath,
541 ) -> Result<bool, HgError> {
536 ) -> Result<bool, HgError> {
542 let vfs = repo.working_directory_vfs();
537 let vfs = repo.working_directory_vfs();
543 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
538 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
544 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
539 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
545 let is_symlink = fs_metadata.file_type().is_symlink();
540 let is_symlink = fs_metadata.file_type().is_symlink();
546 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
541 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
547 // dirstate
542 // dirstate
548 let fs_flags = if is_symlink {
543 let fs_flags = if is_symlink {
549 Some(b'l')
544 Some(b'l')
550 } else if has_exec_bit(&fs_metadata) {
545 } else if has_exec_bit(&fs_metadata) {
551 Some(b'x')
546 Some(b'x')
552 } else {
547 } else {
553 None
548 None
554 };
549 };
555
550
556 let entry = manifest
551 let entry = manifest
557 .find_by_path(hg_path)?
552 .find_by_path(hg_path)?
558 .expect("ambgious file not in p1");
553 .expect("ambgious file not in p1");
559 if entry.flags != fs_flags {
554 if entry.flags != fs_flags {
560 return Ok(true);
555 return Ok(true);
561 }
556 }
562 let filelog = repo.filelog(hg_path)?;
557 let filelog = repo.filelog(hg_path)?;
563 let fs_len = fs_metadata.len();
558 let fs_len = fs_metadata.len();
564 let file_node = entry.node_id()?;
559 let file_node = entry.node_id()?;
565 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
560 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
566 HgError::corrupted(format!(
561 HgError::corrupted(format!(
567 "filelog missing node {:?} from manifest",
562 "filelog missing node {:?} from manifest",
568 file_node
563 file_node
569 ))
564 ))
570 })?;
565 })?;
571 if filelog_entry.file_data_len_not_equal_to(fs_len) {
566 if filelog_entry.file_data_len_not_equal_to(fs_len) {
572 // No need to read file contents:
567 // No need to read file contents:
573 // it cannot be equal if it has a different length.
568 // it cannot be equal if it has a different length.
574 return Ok(true);
569 return Ok(true);
575 }
570 }
576
571
577 let p1_filelog_data = filelog_entry.data()?;
572 let p1_filelog_data = filelog_entry.data()?;
578 let p1_contents = p1_filelog_data.file_data()?;
573 let p1_contents = p1_filelog_data.file_data()?;
579 if p1_contents.len() as u64 != fs_len {
574 if p1_contents.len() as u64 != fs_len {
580 // No need to read file contents:
575 // No need to read file contents:
581 // it cannot be equal if it has a different length.
576 // it cannot be equal if it has a different length.
582 return Ok(true);
577 return Ok(true);
583 }
578 }
584
579
585 let fs_contents = if is_symlink {
580 let fs_contents = if is_symlink {
586 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
581 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
587 } else {
582 } else {
588 vfs.read(fs_path)?
583 vfs.read(fs_path)?
589 };
584 };
590 Ok(p1_contents != &*fs_contents)
585 Ok(p1_contents != &*fs_contents)
591 }
586 }
592
587
593 fn print_pattern_file_warning(
588 fn print_pattern_file_warning(
594 warning: &PatternFileWarning,
589 warning: &PatternFileWarning,
595 repo: &Repo,
590 repo: &Repo,
596 ) -> Vec<u8> {
591 ) -> Vec<u8> {
597 match warning {
592 match warning {
598 PatternFileWarning::InvalidSyntax(path, syntax) => format_bytes!(
593 PatternFileWarning::InvalidSyntax(path, syntax) => format_bytes!(
599 b"{}: ignoring invalid syntax '{}'\n",
594 b"{}: ignoring invalid syntax '{}'\n",
600 get_bytes_from_path(path),
595 get_bytes_from_path(path),
601 &*syntax
596 &*syntax
602 ),
597 ),
603 PatternFileWarning::NoSuchFile(path) => {
598 PatternFileWarning::NoSuchFile(path) => {
604 let path = if let Ok(relative) =
599 let path = if let Ok(relative) =
605 path.strip_prefix(repo.working_directory_path())
600 path.strip_prefix(repo.working_directory_path())
606 {
601 {
607 relative
602 relative
608 } else {
603 } else {
609 &*path
604 &*path
610 };
605 };
611 format_bytes!(
606 format_bytes!(
612 b"skipping unreadable pattern file '{}': \
607 b"skipping unreadable pattern file '{}': \
613 No such file or directory\n",
608 No such file or directory\n",
614 get_bytes_from_path(path),
609 get_bytes_from_path(path),
615 )
610 )
616 }
611 }
617 }
612 }
618 }
613 }
@@ -1,827 +1,845 b''
1 extern crate log;
1 extern crate log;
2 use crate::error::CommandError;
2 use crate::error::CommandError;
3 use crate::ui::{local_to_utf8, Ui};
3 use crate::ui::{local_to_utf8, Ui};
4 use clap::App;
4 use clap::App;
5 use clap::AppSettings;
5 use clap::AppSettings;
6 use clap::Arg;
6 use clap::Arg;
7 use clap::ArgMatches;
7 use clap::ArgMatches;
8 use format_bytes::{format_bytes, join};
8 use format_bytes::{format_bytes, join};
9 use hg::config::{Config, ConfigSource, PlainInfo};
9 use hg::config::{Config, ConfigSource, PlainInfo};
10 use hg::repo::{Repo, RepoError};
10 use hg::repo::{Repo, RepoError};
11 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
11 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
12 use hg::utils::SliceExt;
12 use hg::utils::SliceExt;
13 use hg::{exit_codes, requirements};
13 use hg::{exit_codes, requirements};
14 use std::borrow::Cow;
14 use std::borrow::Cow;
15 use std::collections::HashSet;
15 use std::collections::HashSet;
16 use std::ffi::OsString;
16 use std::ffi::OsString;
17 use std::os::unix::prelude::CommandExt;
17 use std::os::unix::prelude::CommandExt;
18 use std::path::PathBuf;
18 use std::path::PathBuf;
19 use std::process::Command;
19 use std::process::Command;
20
20
21 mod blackbox;
21 mod blackbox;
22 mod color;
22 mod color;
23 mod error;
23 mod error;
24 mod ui;
24 mod ui;
25 pub mod utils {
25 pub mod utils {
26 pub mod path_utils;
26 pub mod path_utils;
27 }
27 }
28
28
29 fn main_with_result(
29 fn main_with_result(
30 argv: Vec<OsString>,
30 argv: Vec<OsString>,
31 process_start_time: &blackbox::ProcessStartTime,
31 process_start_time: &blackbox::ProcessStartTime,
32 ui: &ui::Ui,
32 ui: &ui::Ui,
33 repo: Result<&Repo, &NoRepoInCwdError>,
33 repo: Result<&Repo, &NoRepoInCwdError>,
34 config: &Config,
34 config: &Config,
35 ) -> Result<(), CommandError> {
35 ) -> Result<(), CommandError> {
36 check_unsupported(config, repo)?;
36 check_unsupported(config, repo)?;
37
37
38 let app = App::new("rhg")
38 let app = App::new("rhg")
39 .global_setting(AppSettings::AllowInvalidUtf8)
39 .global_setting(AppSettings::AllowInvalidUtf8)
40 .global_setting(AppSettings::DisableVersion)
40 .global_setting(AppSettings::DisableVersion)
41 .setting(AppSettings::SubcommandRequired)
41 .setting(AppSettings::SubcommandRequired)
42 .setting(AppSettings::VersionlessSubcommands)
42 .setting(AppSettings::VersionlessSubcommands)
43 .arg(
43 .arg(
44 Arg::with_name("repository")
44 Arg::with_name("repository")
45 .help("repository root directory")
45 .help("repository root directory")
46 .short("-R")
46 .short("-R")
47 .long("--repository")
47 .long("--repository")
48 .value_name("REPO")
48 .value_name("REPO")
49 .takes_value(true)
49 .takes_value(true)
50 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
50 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
51 .global(true),
51 .global(true),
52 )
52 )
53 .arg(
53 .arg(
54 Arg::with_name("config")
54 Arg::with_name("config")
55 .help("set/override config option (use 'section.name=value')")
55 .help("set/override config option (use 'section.name=value')")
56 .long("--config")
56 .long("--config")
57 .value_name("CONFIG")
57 .value_name("CONFIG")
58 .takes_value(true)
58 .takes_value(true)
59 .global(true)
59 .global(true)
60 // Ok: `--config section.key1=val --config section.key2=val2`
60 // Ok: `--config section.key1=val --config section.key2=val2`
61 .multiple(true)
61 .multiple(true)
62 // Not ok: `--config section.key1=val section.key2=val2`
62 // Not ok: `--config section.key1=val section.key2=val2`
63 .number_of_values(1),
63 .number_of_values(1),
64 )
64 )
65 .arg(
65 .arg(
66 Arg::with_name("cwd")
66 Arg::with_name("cwd")
67 .help("change working directory")
67 .help("change working directory")
68 .long("--cwd")
68 .long("--cwd")
69 .value_name("DIR")
69 .value_name("DIR")
70 .takes_value(true)
70 .takes_value(true)
71 .global(true),
71 .global(true),
72 )
72 )
73 .arg(
73 .arg(
74 Arg::with_name("color")
74 Arg::with_name("color")
75 .help("when to colorize (boolean, always, auto, never, or debug)")
75 .help("when to colorize (boolean, always, auto, never, or debug)")
76 .long("--color")
76 .long("--color")
77 .value_name("TYPE")
77 .value_name("TYPE")
78 .takes_value(true)
78 .takes_value(true)
79 .global(true),
79 .global(true),
80 )
80 )
81 .version("0.0.1");
81 .version("0.0.1");
82 let app = add_subcommand_args(app);
82 let app = add_subcommand_args(app);
83
83
84 let matches = app.clone().get_matches_from_safe(argv.iter())?;
84 let matches = app.clone().get_matches_from_safe(argv.iter())?;
85
85
86 let (subcommand_name, subcommand_matches) = matches.subcommand();
86 let (subcommand_name, subcommand_matches) = matches.subcommand();
87
87
88 // Mercurial allows users to define "defaults" for commands, fallback
88 // Mercurial allows users to define "defaults" for commands, fallback
89 // if a default is detected for the current command
89 // if a default is detected for the current command
90 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
90 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
91 if defaults?.is_some() {
91 if defaults?.is_some() {
92 let msg = "`defaults` config set";
92 let msg = "`defaults` config set";
93 return Err(CommandError::unsupported(msg));
93 return Err(CommandError::unsupported(msg));
94 }
94 }
95
95
96 for prefix in ["pre", "post", "fail"].iter() {
96 for prefix in ["pre", "post", "fail"].iter() {
97 // Mercurial allows users to define generic hooks for commands,
97 // Mercurial allows users to define generic hooks for commands,
98 // fallback if any are detected
98 // fallback if any are detected
99 let item = format!("{}-{}", prefix, subcommand_name);
99 let item = format!("{}-{}", prefix, subcommand_name);
100 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
100 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
101 if hook_for_command.is_some() {
101 if hook_for_command.is_some() {
102 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
102 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
103 return Err(CommandError::unsupported(msg));
103 return Err(CommandError::unsupported(msg));
104 }
104 }
105 }
105 }
106 let run = subcommand_run_fn(subcommand_name)
106 let run = subcommand_run_fn(subcommand_name)
107 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
107 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
108 let subcommand_args = subcommand_matches
108 let subcommand_args = subcommand_matches
109 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
109 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
110
110
111 let invocation = CliInvocation {
111 let invocation = CliInvocation {
112 ui,
112 ui,
113 subcommand_args,
113 subcommand_args,
114 config,
114 config,
115 repo,
115 repo,
116 };
116 };
117
117
118 if let Ok(repo) = repo {
118 if let Ok(repo) = repo {
119 // We don't support subrepos, fallback if the subrepos file is present
119 // We don't support subrepos, fallback if the subrepos file is present
120 if repo.working_directory_vfs().join(".hgsub").exists() {
120 if repo.working_directory_vfs().join(".hgsub").exists() {
121 let msg = "subrepos (.hgsub is present)";
121 let msg = "subrepos (.hgsub is present)";
122 return Err(CommandError::unsupported(msg));
122 return Err(CommandError::unsupported(msg));
123 }
123 }
124 }
124 }
125
125
126 if config.is_extension_enabled(b"blackbox") {
126 if config.is_extension_enabled(b"blackbox") {
127 let blackbox =
127 let blackbox =
128 blackbox::Blackbox::new(&invocation, process_start_time)?;
128 blackbox::Blackbox::new(&invocation, process_start_time)?;
129 blackbox.log_command_start(argv.iter());
129 blackbox.log_command_start(argv.iter());
130 let result = run(&invocation);
130 let result = run(&invocation);
131 blackbox.log_command_end(
131 blackbox.log_command_end(
132 argv.iter(),
132 argv.iter(),
133 exit_code(
133 exit_code(
134 &result,
134 &result,
135 // TODO: show a warning or combine with original error if
135 // TODO: show a warning or combine with original error if
136 // `get_bool` returns an error
136 // `get_bool` returns an error
137 config
137 config
138 .get_bool(b"ui", b"detailed-exit-code")
138 .get_bool(b"ui", b"detailed-exit-code")
139 .unwrap_or(false),
139 .unwrap_or(false),
140 ),
140 ),
141 );
141 );
142 result
142 result
143 } else {
143 } else {
144 run(&invocation)
144 run(&invocation)
145 }
145 }
146 }
146 }
147
147
148 fn rhg_main(argv: Vec<OsString>) -> ! {
148 fn rhg_main(argv: Vec<OsString>) -> ! {
149 // Run this first, before we find out if the blackbox extension is even
149 // Run this first, before we find out if the blackbox extension is even
150 // enabled, in order to include everything in-between in the duration
150 // enabled, in order to include everything in-between in the duration
151 // measurements. Reading config files can be slow if they’re on NFS.
151 // measurements. Reading config files can be slow if they’re on NFS.
152 let process_start_time = blackbox::ProcessStartTime::now();
152 let process_start_time = blackbox::ProcessStartTime::now();
153
153
154 env_logger::init();
154 env_logger::init();
155
155
156 let early_args = EarlyArgs::parse(&argv);
156 let early_args = EarlyArgs::parse(&argv);
157
157
158 let initial_current_dir = early_args.cwd.map(|cwd| {
158 let initial_current_dir = early_args.cwd.map(|cwd| {
159 let cwd = get_path_from_bytes(&cwd);
159 let cwd = get_path_from_bytes(&cwd);
160 std::env::current_dir()
160 std::env::current_dir()
161 .and_then(|initial| {
161 .and_then(|initial| {
162 std::env::set_current_dir(cwd)?;
162 std::env::set_current_dir(cwd)?;
163 Ok(initial)
163 Ok(initial)
164 })
164 })
165 .unwrap_or_else(|error| {
165 .unwrap_or_else(|error| {
166 exit(
166 exit(
167 &argv,
167 &argv,
168 &None,
168 &None,
169 &Ui::new_infallible(&Config::empty()),
169 &Ui::new_infallible(&Config::empty()),
170 OnUnsupported::Abort,
170 OnUnsupported::Abort,
171 Err(CommandError::abort(format!(
171 Err(CommandError::abort(format!(
172 "abort: {}: '{}'",
172 "abort: {}: '{}'",
173 error,
173 error,
174 cwd.display()
174 cwd.display()
175 ))),
175 ))),
176 false,
176 false,
177 )
177 )
178 })
178 })
179 });
179 });
180
180
181 let mut non_repo_config =
181 let mut non_repo_config =
182 Config::load_non_repo().unwrap_or_else(|error| {
182 Config::load_non_repo().unwrap_or_else(|error| {
183 // Normally this is decided based on config, but we don’t have that
183 // Normally this is decided based on config, but we don’t have that
184 // available. As of this writing config loading never returns an
184 // available. As of this writing config loading never returns an
185 // "unsupported" error but that is not enforced by the type system.
185 // "unsupported" error but that is not enforced by the type system.
186 let on_unsupported = OnUnsupported::Abort;
186 let on_unsupported = OnUnsupported::Abort;
187
187
188 exit(
188 exit(
189 &argv,
189 &argv,
190 &initial_current_dir,
190 &initial_current_dir,
191 &Ui::new_infallible(&Config::empty()),
191 &Ui::new_infallible(&Config::empty()),
192 on_unsupported,
192 on_unsupported,
193 Err(error.into()),
193 Err(error.into()),
194 false,
194 false,
195 )
195 )
196 });
196 });
197
197
198 non_repo_config
198 non_repo_config
199 .load_cli_args(early_args.config, early_args.color)
199 .load_cli_args(early_args.config, early_args.color)
200 .unwrap_or_else(|error| {
200 .unwrap_or_else(|error| {
201 exit(
201 exit(
202 &argv,
202 &argv,
203 &initial_current_dir,
203 &initial_current_dir,
204 &Ui::new_infallible(&non_repo_config),
204 &Ui::new_infallible(&non_repo_config),
205 OnUnsupported::from_config(&non_repo_config),
205 OnUnsupported::from_config(&non_repo_config),
206 Err(error.into()),
206 Err(error.into()),
207 non_repo_config
207 non_repo_config
208 .get_bool(b"ui", b"detailed-exit-code")
208 .get_bool(b"ui", b"detailed-exit-code")
209 .unwrap_or(false),
209 .unwrap_or(false),
210 )
210 )
211 });
211 });
212
212
213 if let Some(repo_path_bytes) = &early_args.repo {
213 if let Some(repo_path_bytes) = &early_args.repo {
214 lazy_static::lazy_static! {
214 lazy_static::lazy_static! {
215 static ref SCHEME_RE: regex::bytes::Regex =
215 static ref SCHEME_RE: regex::bytes::Regex =
216 // Same as `_matchscheme` in `mercurial/util.py`
216 // Same as `_matchscheme` in `mercurial/util.py`
217 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
217 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
218 }
218 }
219 if SCHEME_RE.is_match(&repo_path_bytes) {
219 if SCHEME_RE.is_match(&repo_path_bytes) {
220 exit(
220 exit(
221 &argv,
221 &argv,
222 &initial_current_dir,
222 &initial_current_dir,
223 &Ui::new_infallible(&non_repo_config),
223 &Ui::new_infallible(&non_repo_config),
224 OnUnsupported::from_config(&non_repo_config),
224 OnUnsupported::from_config(&non_repo_config),
225 Err(CommandError::UnsupportedFeature {
225 Err(CommandError::UnsupportedFeature {
226 message: format_bytes!(
226 message: format_bytes!(
227 b"URL-like --repository {}",
227 b"URL-like --repository {}",
228 repo_path_bytes
228 repo_path_bytes
229 ),
229 ),
230 }),
230 }),
231 // TODO: show a warning or combine with original error if
231 // TODO: show a warning or combine with original error if
232 // `get_bool` returns an error
232 // `get_bool` returns an error
233 non_repo_config
233 non_repo_config
234 .get_bool(b"ui", b"detailed-exit-code")
234 .get_bool(b"ui", b"detailed-exit-code")
235 .unwrap_or(false),
235 .unwrap_or(false),
236 )
236 )
237 }
237 }
238 }
238 }
239 let repo_arg = early_args.repo.unwrap_or(Vec::new());
239 let repo_arg = early_args.repo.unwrap_or(Vec::new());
240 let repo_path: Option<PathBuf> = {
240 let repo_path: Option<PathBuf> = {
241 if repo_arg.is_empty() {
241 if repo_arg.is_empty() {
242 None
242 None
243 } else {
243 } else {
244 let local_config = {
244 let local_config = {
245 if std::env::var_os("HGRCSKIPREPO").is_none() {
245 if std::env::var_os("HGRCSKIPREPO").is_none() {
246 // TODO: handle errors from find_repo_root
246 // TODO: handle errors from find_repo_root
247 if let Ok(current_dir_path) = Repo::find_repo_root() {
247 if let Ok(current_dir_path) = Repo::find_repo_root() {
248 let config_files = vec![
248 let config_files = vec![
249 ConfigSource::AbsPath(
249 ConfigSource::AbsPath(
250 current_dir_path.join(".hg/hgrc"),
250 current_dir_path.join(".hg/hgrc"),
251 ),
251 ),
252 ConfigSource::AbsPath(
252 ConfigSource::AbsPath(
253 current_dir_path.join(".hg/hgrc-not-shared"),
253 current_dir_path.join(".hg/hgrc-not-shared"),
254 ),
254 ),
255 ];
255 ];
256 // TODO: handle errors from
256 // TODO: handle errors from
257 // `load_from_explicit_sources`
257 // `load_from_explicit_sources`
258 Config::load_from_explicit_sources(config_files).ok()
258 Config::load_from_explicit_sources(config_files).ok()
259 } else {
259 } else {
260 None
260 None
261 }
261 }
262 } else {
262 } else {
263 None
263 None
264 }
264 }
265 };
265 };
266
266
267 let non_repo_config_val = {
267 let non_repo_config_val = {
268 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
268 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
269 match &non_repo_val {
269 match &non_repo_val {
270 Some(val) if val.len() > 0 => home::home_dir()
270 Some(val) if val.len() > 0 => home::home_dir()
271 .unwrap_or_else(|| PathBuf::from("~"))
271 .unwrap_or_else(|| PathBuf::from("~"))
272 .join(get_path_from_bytes(val))
272 .join(get_path_from_bytes(val))
273 .canonicalize()
273 .canonicalize()
274 // TODO: handle error and make it similar to python
274 // TODO: handle error and make it similar to python
275 // implementation maybe?
275 // implementation maybe?
276 .ok(),
276 .ok(),
277 _ => None,
277 _ => None,
278 }
278 }
279 };
279 };
280
280
281 let config_val = match &local_config {
281 let config_val = match &local_config {
282 None => non_repo_config_val,
282 None => non_repo_config_val,
283 Some(val) => {
283 Some(val) => {
284 let local_config_val = val.get(b"paths", &repo_arg);
284 let local_config_val = val.get(b"paths", &repo_arg);
285 match &local_config_val {
285 match &local_config_val {
286 Some(val) if val.len() > 0 => {
286 Some(val) if val.len() > 0 => {
287 // presence of a local_config assures that
287 // presence of a local_config assures that
288 // current_dir
288 // current_dir
289 // wont result in an Error
289 // wont result in an Error
290 let canpath = hg::utils::current_dir()
290 let canpath = hg::utils::current_dir()
291 .unwrap()
291 .unwrap()
292 .join(get_path_from_bytes(val))
292 .join(get_path_from_bytes(val))
293 .canonicalize();
293 .canonicalize();
294 canpath.ok().or(non_repo_config_val)
294 canpath.ok().or(non_repo_config_val)
295 }
295 }
296 _ => non_repo_config_val,
296 _ => non_repo_config_val,
297 }
297 }
298 }
298 }
299 };
299 };
300 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
300 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
301 }
301 }
302 };
302 };
303
303
304 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
304 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
305 {
305 {
306 Ok(repo) => Ok(repo),
306 Ok(repo) => Ok(repo),
307 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
307 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
308 // Not finding a repo is not fatal yet, if `-R` was not given
308 // Not finding a repo is not fatal yet, if `-R` was not given
309 Err(NoRepoInCwdError { cwd: at })
309 Err(NoRepoInCwdError { cwd: at })
310 }
310 }
311 Err(error) => exit(
311 Err(error) => exit(
312 &argv,
312 &argv,
313 &initial_current_dir,
313 &initial_current_dir,
314 &Ui::new_infallible(&non_repo_config),
314 &Ui::new_infallible(&non_repo_config),
315 OnUnsupported::from_config(&non_repo_config),
315 OnUnsupported::from_config(&non_repo_config),
316 Err(error.into()),
316 Err(error.into()),
317 // TODO: show a warning or combine with original error if
317 // TODO: show a warning or combine with original error if
318 // `get_bool` returns an error
318 // `get_bool` returns an error
319 non_repo_config
319 non_repo_config
320 .get_bool(b"ui", b"detailed-exit-code")
320 .get_bool(b"ui", b"detailed-exit-code")
321 .unwrap_or(false),
321 .unwrap_or(false),
322 ),
322 ),
323 };
323 };
324
324
325 let config = if let Ok(repo) = &repo_result {
325 let config = if let Ok(repo) = &repo_result {
326 repo.config()
326 repo.config()
327 } else {
327 } else {
328 &non_repo_config
328 &non_repo_config
329 };
329 };
330
330
331 let mut config_cow = Cow::Borrowed(config);
331 let mut config_cow = Cow::Borrowed(config);
332 config_cow.to_mut().apply_plain(PlainInfo::from_env());
332 config_cow.to_mut().apply_plain(PlainInfo::from_env());
333 if !ui::plain(Some("tweakdefaults"))
334 && config_cow
335 .as_ref()
336 .get_bool(b"ui", b"tweakdefaults")
337 .unwrap_or_else(|error| {
338 exit(
339 &argv,
340 &initial_current_dir,
341 &Ui::new_infallible(&config),
342 OnUnsupported::from_config(&config),
343 Err(error.into()),
344 config
345 .get_bool(b"ui", b"detailed-exit-code")
346 .unwrap_or(false),
347 )
348 })
349 {
350 config_cow.to_mut().tweakdefaults()
351 };
333 let config = config_cow.as_ref();
352 let config = config_cow.as_ref();
334
335 let ui = Ui::new(&config).unwrap_or_else(|error| {
353 let ui = Ui::new(&config).unwrap_or_else(|error| {
336 exit(
354 exit(
337 &argv,
355 &argv,
338 &initial_current_dir,
356 &initial_current_dir,
339 &Ui::new_infallible(&config),
357 &Ui::new_infallible(&config),
340 OnUnsupported::from_config(&config),
358 OnUnsupported::from_config(&config),
341 Err(error.into()),
359 Err(error.into()),
342 config
360 config
343 .get_bool(b"ui", b"detailed-exit-code")
361 .get_bool(b"ui", b"detailed-exit-code")
344 .unwrap_or(false),
362 .unwrap_or(false),
345 )
363 )
346 });
364 });
347 let on_unsupported = OnUnsupported::from_config(config);
365 let on_unsupported = OnUnsupported::from_config(config);
348
366
349 let result = main_with_result(
367 let result = main_with_result(
350 argv.iter().map(|s| s.to_owned()).collect(),
368 argv.iter().map(|s| s.to_owned()).collect(),
351 &process_start_time,
369 &process_start_time,
352 &ui,
370 &ui,
353 repo_result.as_ref(),
371 repo_result.as_ref(),
354 config,
372 config,
355 );
373 );
356 exit(
374 exit(
357 &argv,
375 &argv,
358 &initial_current_dir,
376 &initial_current_dir,
359 &ui,
377 &ui,
360 on_unsupported,
378 on_unsupported,
361 result,
379 result,
362 // TODO: show a warning or combine with original error if `get_bool`
380 // TODO: show a warning or combine with original error if `get_bool`
363 // returns an error
381 // returns an error
364 config
382 config
365 .get_bool(b"ui", b"detailed-exit-code")
383 .get_bool(b"ui", b"detailed-exit-code")
366 .unwrap_or(false),
384 .unwrap_or(false),
367 )
385 )
368 }
386 }
369
387
370 fn main() -> ! {
388 fn main() -> ! {
371 rhg_main(std::env::args_os().collect())
389 rhg_main(std::env::args_os().collect())
372 }
390 }
373
391
374 fn exit_code(
392 fn exit_code(
375 result: &Result<(), CommandError>,
393 result: &Result<(), CommandError>,
376 use_detailed_exit_code: bool,
394 use_detailed_exit_code: bool,
377 ) -> i32 {
395 ) -> i32 {
378 match result {
396 match result {
379 Ok(()) => exit_codes::OK,
397 Ok(()) => exit_codes::OK,
380 Err(CommandError::Abort {
398 Err(CommandError::Abort {
381 detailed_exit_code, ..
399 detailed_exit_code, ..
382 }) => {
400 }) => {
383 if use_detailed_exit_code {
401 if use_detailed_exit_code {
384 *detailed_exit_code
402 *detailed_exit_code
385 } else {
403 } else {
386 exit_codes::ABORT
404 exit_codes::ABORT
387 }
405 }
388 }
406 }
389 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
407 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
390 // Exit with a specific code and no error message to let a potential
408 // Exit with a specific code and no error message to let a potential
391 // wrapper script fallback to Python-based Mercurial.
409 // wrapper script fallback to Python-based Mercurial.
392 Err(CommandError::UnsupportedFeature { .. }) => {
410 Err(CommandError::UnsupportedFeature { .. }) => {
393 exit_codes::UNIMPLEMENTED
411 exit_codes::UNIMPLEMENTED
394 }
412 }
395 Err(CommandError::InvalidFallback { .. }) => {
413 Err(CommandError::InvalidFallback { .. }) => {
396 exit_codes::INVALID_FALLBACK
414 exit_codes::INVALID_FALLBACK
397 }
415 }
398 }
416 }
399 }
417 }
400
418
401 fn exit<'a>(
419 fn exit<'a>(
402 original_args: &'a [OsString],
420 original_args: &'a [OsString],
403 initial_current_dir: &Option<PathBuf>,
421 initial_current_dir: &Option<PathBuf>,
404 ui: &Ui,
422 ui: &Ui,
405 mut on_unsupported: OnUnsupported,
423 mut on_unsupported: OnUnsupported,
406 result: Result<(), CommandError>,
424 result: Result<(), CommandError>,
407 use_detailed_exit_code: bool,
425 use_detailed_exit_code: bool,
408 ) -> ! {
426 ) -> ! {
409 if let (
427 if let (
410 OnUnsupported::Fallback { executable },
428 OnUnsupported::Fallback { executable },
411 Err(CommandError::UnsupportedFeature { message }),
429 Err(CommandError::UnsupportedFeature { message }),
412 ) = (&on_unsupported, &result)
430 ) = (&on_unsupported, &result)
413 {
431 {
414 let mut args = original_args.iter();
432 let mut args = original_args.iter();
415 let executable = match executable {
433 let executable = match executable {
416 None => {
434 None => {
417 exit_no_fallback(
435 exit_no_fallback(
418 ui,
436 ui,
419 OnUnsupported::Abort,
437 OnUnsupported::Abort,
420 Err(CommandError::abort(
438 Err(CommandError::abort(
421 "abort: 'rhg.on-unsupported=fallback' without \
439 "abort: 'rhg.on-unsupported=fallback' without \
422 'rhg.fallback-executable' set.",
440 'rhg.fallback-executable' set.",
423 )),
441 )),
424 false,
442 false,
425 );
443 );
426 }
444 }
427 Some(executable) => executable,
445 Some(executable) => executable,
428 };
446 };
429 let executable_path = get_path_from_bytes(&executable);
447 let executable_path = get_path_from_bytes(&executable);
430 let this_executable = args.next().expect("exepcted argv[0] to exist");
448 let this_executable = args.next().expect("exepcted argv[0] to exist");
431 if executable_path == &PathBuf::from(this_executable) {
449 if executable_path == &PathBuf::from(this_executable) {
432 // Avoid spawning infinitely many processes until resource
450 // Avoid spawning infinitely many processes until resource
433 // exhaustion.
451 // exhaustion.
434 let _ = ui.write_stderr(&format_bytes!(
452 let _ = ui.write_stderr(&format_bytes!(
435 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
453 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
436 points to `rhg` itself.\n",
454 points to `rhg` itself.\n",
437 executable
455 executable
438 ));
456 ));
439 on_unsupported = OnUnsupported::Abort
457 on_unsupported = OnUnsupported::Abort
440 } else {
458 } else {
441 log::debug!("falling back (see trace-level log)");
459 log::debug!("falling back (see trace-level log)");
442 log::trace!("{}", local_to_utf8(message));
460 log::trace!("{}", local_to_utf8(message));
443 if let Err(err) = which::which(executable_path) {
461 if let Err(err) = which::which(executable_path) {
444 exit_no_fallback(
462 exit_no_fallback(
445 ui,
463 ui,
446 OnUnsupported::Abort,
464 OnUnsupported::Abort,
447 Err(CommandError::InvalidFallback {
465 Err(CommandError::InvalidFallback {
448 path: executable.to_owned(),
466 path: executable.to_owned(),
449 err: err.to_string(),
467 err: err.to_string(),
450 }),
468 }),
451 use_detailed_exit_code,
469 use_detailed_exit_code,
452 )
470 )
453 }
471 }
454 // `args` is now `argv[1..]` since we’ve already consumed
472 // `args` is now `argv[1..]` since we’ve already consumed
455 // `argv[0]`
473 // `argv[0]`
456 let mut command = Command::new(executable_path);
474 let mut command = Command::new(executable_path);
457 command.args(args);
475 command.args(args);
458 if let Some(initial) = initial_current_dir {
476 if let Some(initial) = initial_current_dir {
459 command.current_dir(initial);
477 command.current_dir(initial);
460 }
478 }
461 // We don't use subprocess because proper signal handling is harder
479 // We don't use subprocess because proper signal handling is harder
462 // and we don't want to keep `rhg` around after a fallback anyway.
480 // and we don't want to keep `rhg` around after a fallback anyway.
463 // For example, if `rhg` is run in the background and falls back to
481 // For example, if `rhg` is run in the background and falls back to
464 // `hg` which, in turn, waits for a signal, we'll get stuck if
482 // `hg` which, in turn, waits for a signal, we'll get stuck if
465 // we're doing plain subprocess.
483 // we're doing plain subprocess.
466 //
484 //
467 // If `exec` returns, we can only assume our process is very broken
485 // If `exec` returns, we can only assume our process is very broken
468 // (see its documentation), so only try to forward the error code
486 // (see its documentation), so only try to forward the error code
469 // when exiting.
487 // when exiting.
470 let err = command.exec();
488 let err = command.exec();
471 std::process::exit(
489 std::process::exit(
472 err.raw_os_error().unwrap_or(exit_codes::ABORT),
490 err.raw_os_error().unwrap_or(exit_codes::ABORT),
473 );
491 );
474 }
492 }
475 }
493 }
476 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
494 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
477 }
495 }
478
496
479 fn exit_no_fallback(
497 fn exit_no_fallback(
480 ui: &Ui,
498 ui: &Ui,
481 on_unsupported: OnUnsupported,
499 on_unsupported: OnUnsupported,
482 result: Result<(), CommandError>,
500 result: Result<(), CommandError>,
483 use_detailed_exit_code: bool,
501 use_detailed_exit_code: bool,
484 ) -> ! {
502 ) -> ! {
485 match &result {
503 match &result {
486 Ok(_) => {}
504 Ok(_) => {}
487 Err(CommandError::Unsuccessful) => {}
505 Err(CommandError::Unsuccessful) => {}
488 Err(CommandError::Abort { message, hint, .. }) => {
506 Err(CommandError::Abort { message, hint, .. }) => {
489 // Ignore errors when writing to stderr, we’re already exiting
507 // Ignore errors when writing to stderr, we’re already exiting
490 // with failure code so there’s not much more we can do.
508 // with failure code so there’s not much more we can do.
491 if !message.is_empty() {
509 if !message.is_empty() {
492 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
510 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
493 }
511 }
494 if let Some(hint) = hint {
512 if let Some(hint) = hint {
495 let _ = ui.write_stderr(&format_bytes!(b"({})\n", hint));
513 let _ = ui.write_stderr(&format_bytes!(b"({})\n", hint));
496 }
514 }
497 }
515 }
498 Err(CommandError::UnsupportedFeature { message }) => {
516 Err(CommandError::UnsupportedFeature { message }) => {
499 match on_unsupported {
517 match on_unsupported {
500 OnUnsupported::Abort => {
518 OnUnsupported::Abort => {
501 let _ = ui.write_stderr(&format_bytes!(
519 let _ = ui.write_stderr(&format_bytes!(
502 b"unsupported feature: {}\n",
520 b"unsupported feature: {}\n",
503 message
521 message
504 ));
522 ));
505 }
523 }
506 OnUnsupported::AbortSilent => {}
524 OnUnsupported::AbortSilent => {}
507 OnUnsupported::Fallback { .. } => unreachable!(),
525 OnUnsupported::Fallback { .. } => unreachable!(),
508 }
526 }
509 }
527 }
510 Err(CommandError::InvalidFallback { path, err }) => {
528 Err(CommandError::InvalidFallback { path, err }) => {
511 let _ = ui.write_stderr(&format_bytes!(
529 let _ = ui.write_stderr(&format_bytes!(
512 b"abort: invalid fallback '{}': {}\n",
530 b"abort: invalid fallback '{}': {}\n",
513 path,
531 path,
514 err.as_bytes(),
532 err.as_bytes(),
515 ));
533 ));
516 }
534 }
517 }
535 }
518 std::process::exit(exit_code(&result, use_detailed_exit_code))
536 std::process::exit(exit_code(&result, use_detailed_exit_code))
519 }
537 }
520
538
521 macro_rules! subcommands {
539 macro_rules! subcommands {
522 ($( $command: ident )+) => {
540 ($( $command: ident )+) => {
523 mod commands {
541 mod commands {
524 $(
542 $(
525 pub mod $command;
543 pub mod $command;
526 )+
544 )+
527 }
545 }
528
546
529 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
547 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
530 app
548 app
531 $(
549 $(
532 .subcommand(commands::$command::args())
550 .subcommand(commands::$command::args())
533 )+
551 )+
534 }
552 }
535
553
536 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
554 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
537
555
538 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
556 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
539 match name {
557 match name {
540 $(
558 $(
541 stringify!($command) => Some(commands::$command::run),
559 stringify!($command) => Some(commands::$command::run),
542 )+
560 )+
543 _ => None,
561 _ => None,
544 }
562 }
545 }
563 }
546 };
564 };
547 }
565 }
548
566
549 subcommands! {
567 subcommands! {
550 cat
568 cat
551 debugdata
569 debugdata
552 debugrequirements
570 debugrequirements
553 debugignorerhg
571 debugignorerhg
554 debugrhgsparse
572 debugrhgsparse
555 files
573 files
556 root
574 root
557 config
575 config
558 status
576 status
559 }
577 }
560
578
561 pub struct CliInvocation<'a> {
579 pub struct CliInvocation<'a> {
562 ui: &'a Ui,
580 ui: &'a Ui,
563 subcommand_args: &'a ArgMatches<'a>,
581 subcommand_args: &'a ArgMatches<'a>,
564 config: &'a Config,
582 config: &'a Config,
565 /// References inside `Result` is a bit peculiar but allow
583 /// References inside `Result` is a bit peculiar but allow
566 /// `invocation.repo?` to work out with `&CliInvocation` since this
584 /// `invocation.repo?` to work out with `&CliInvocation` since this
567 /// `Result` type is `Copy`.
585 /// `Result` type is `Copy`.
568 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
586 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
569 }
587 }
570
588
571 struct NoRepoInCwdError {
589 struct NoRepoInCwdError {
572 cwd: PathBuf,
590 cwd: PathBuf,
573 }
591 }
574
592
575 /// CLI arguments to be parsed "early" in order to be able to read
593 /// CLI arguments to be parsed "early" in order to be able to read
576 /// configuration before using Clap. Ideally we would also use Clap for this,
594 /// configuration before using Clap. Ideally we would also use Clap for this,
577 /// see <https://github.com/clap-rs/clap/discussions/2366>.
595 /// see <https://github.com/clap-rs/clap/discussions/2366>.
578 ///
596 ///
579 /// These arguments are still declared when we do use Clap later, so that Clap
597 /// These arguments are still declared when we do use Clap later, so that Clap
580 /// does not return an error for their presence.
598 /// does not return an error for their presence.
581 struct EarlyArgs {
599 struct EarlyArgs {
582 /// Values of all `--config` arguments. (Possibly none)
600 /// Values of all `--config` arguments. (Possibly none)
583 config: Vec<Vec<u8>>,
601 config: Vec<Vec<u8>>,
584 /// Value of all the `--color` argument, if any.
602 /// Value of all the `--color` argument, if any.
585 color: Option<Vec<u8>>,
603 color: Option<Vec<u8>>,
586 /// Value of the `-R` or `--repository` argument, if any.
604 /// Value of the `-R` or `--repository` argument, if any.
587 repo: Option<Vec<u8>>,
605 repo: Option<Vec<u8>>,
588 /// Value of the `--cwd` argument, if any.
606 /// Value of the `--cwd` argument, if any.
589 cwd: Option<Vec<u8>>,
607 cwd: Option<Vec<u8>>,
590 }
608 }
591
609
592 impl EarlyArgs {
610 impl EarlyArgs {
593 fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
611 fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
594 let mut args = args.into_iter().map(get_bytes_from_os_str);
612 let mut args = args.into_iter().map(get_bytes_from_os_str);
595 let mut config = Vec::new();
613 let mut config = Vec::new();
596 let mut color = None;
614 let mut color = None;
597 let mut repo = None;
615 let mut repo = None;
598 let mut cwd = None;
616 let mut cwd = None;
599 // Use `while let` instead of `for` so that we can also call
617 // Use `while let` instead of `for` so that we can also call
600 // `args.next()` inside the loop.
618 // `args.next()` inside the loop.
601 while let Some(arg) = args.next() {
619 while let Some(arg) = args.next() {
602 if arg == b"--config" {
620 if arg == b"--config" {
603 if let Some(value) = args.next() {
621 if let Some(value) = args.next() {
604 config.push(value)
622 config.push(value)
605 }
623 }
606 } else if let Some(value) = arg.drop_prefix(b"--config=") {
624 } else if let Some(value) = arg.drop_prefix(b"--config=") {
607 config.push(value.to_owned())
625 config.push(value.to_owned())
608 }
626 }
609
627
610 if arg == b"--color" {
628 if arg == b"--color" {
611 if let Some(value) = args.next() {
629 if let Some(value) = args.next() {
612 color = Some(value)
630 color = Some(value)
613 }
631 }
614 } else if let Some(value) = arg.drop_prefix(b"--color=") {
632 } else if let Some(value) = arg.drop_prefix(b"--color=") {
615 color = Some(value.to_owned())
633 color = Some(value.to_owned())
616 }
634 }
617
635
618 if arg == b"--cwd" {
636 if arg == b"--cwd" {
619 if let Some(value) = args.next() {
637 if let Some(value) = args.next() {
620 cwd = Some(value)
638 cwd = Some(value)
621 }
639 }
622 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
640 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
623 cwd = Some(value.to_owned())
641 cwd = Some(value.to_owned())
624 }
642 }
625
643
626 if arg == b"--repository" || arg == b"-R" {
644 if arg == b"--repository" || arg == b"-R" {
627 if let Some(value) = args.next() {
645 if let Some(value) = args.next() {
628 repo = Some(value)
646 repo = Some(value)
629 }
647 }
630 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
648 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
631 repo = Some(value.to_owned())
649 repo = Some(value.to_owned())
632 } else if let Some(value) = arg.drop_prefix(b"-R") {
650 } else if let Some(value) = arg.drop_prefix(b"-R") {
633 repo = Some(value.to_owned())
651 repo = Some(value.to_owned())
634 }
652 }
635 }
653 }
636 Self {
654 Self {
637 config,
655 config,
638 color,
656 color,
639 repo,
657 repo,
640 cwd,
658 cwd,
641 }
659 }
642 }
660 }
643 }
661 }
644
662
645 /// What to do when encountering some unsupported feature.
663 /// What to do when encountering some unsupported feature.
646 ///
664 ///
647 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
665 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
648 enum OnUnsupported {
666 enum OnUnsupported {
649 /// Print an error message describing what feature is not supported,
667 /// Print an error message describing what feature is not supported,
650 /// and exit with code 252.
668 /// and exit with code 252.
651 Abort,
669 Abort,
652 /// Silently exit with code 252.
670 /// Silently exit with code 252.
653 AbortSilent,
671 AbortSilent,
654 /// Try running a Python implementation
672 /// Try running a Python implementation
655 Fallback { executable: Option<Vec<u8>> },
673 Fallback { executable: Option<Vec<u8>> },
656 }
674 }
657
675
658 impl OnUnsupported {
676 impl OnUnsupported {
659 const DEFAULT: Self = OnUnsupported::Abort;
677 const DEFAULT: Self = OnUnsupported::Abort;
660
678
661 fn from_config(config: &Config) -> Self {
679 fn from_config(config: &Config) -> Self {
662 match config
680 match config
663 .get(b"rhg", b"on-unsupported")
681 .get(b"rhg", b"on-unsupported")
664 .map(|value| value.to_ascii_lowercase())
682 .map(|value| value.to_ascii_lowercase())
665 .as_deref()
683 .as_deref()
666 {
684 {
667 Some(b"abort") => OnUnsupported::Abort,
685 Some(b"abort") => OnUnsupported::Abort,
668 Some(b"abort-silent") => OnUnsupported::AbortSilent,
686 Some(b"abort-silent") => OnUnsupported::AbortSilent,
669 Some(b"fallback") => OnUnsupported::Fallback {
687 Some(b"fallback") => OnUnsupported::Fallback {
670 executable: config
688 executable: config
671 .get(b"rhg", b"fallback-executable")
689 .get(b"rhg", b"fallback-executable")
672 .map(|x| x.to_owned()),
690 .map(|x| x.to_owned()),
673 },
691 },
674 None => Self::DEFAULT,
692 None => Self::DEFAULT,
675 Some(_) => {
693 Some(_) => {
676 // TODO: warn about unknown config value
694 // TODO: warn about unknown config value
677 Self::DEFAULT
695 Self::DEFAULT
678 }
696 }
679 }
697 }
680 }
698 }
681 }
699 }
682
700
683 /// The `*` extension is an edge-case for config sub-options that apply to all
701 /// The `*` extension is an edge-case for config sub-options that apply to all
684 /// extensions. For now, only `:required` exists, but that may change in the
702 /// extensions. For now, only `:required` exists, but that may change in the
685 /// future.
703 /// future.
686 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[
704 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[
687 b"blackbox",
705 b"blackbox",
688 b"share",
706 b"share",
689 b"sparse",
707 b"sparse",
690 b"narrow",
708 b"narrow",
691 b"*",
709 b"*",
692 b"strip",
710 b"strip",
693 b"rebase",
711 b"rebase",
694 ];
712 ];
695
713
696 fn check_extensions(config: &Config) -> Result<(), CommandError> {
714 fn check_extensions(config: &Config) -> Result<(), CommandError> {
697 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
715 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
698 // All extensions are to be ignored, nothing to do here
716 // All extensions are to be ignored, nothing to do here
699 return Ok(());
717 return Ok(());
700 }
718 }
701
719
702 let enabled: HashSet<&[u8]> = config
720 let enabled: HashSet<&[u8]> = config
703 .iter_section(b"extensions")
721 .iter_section(b"extensions")
704 .filter_map(|(extension, value)| {
722 .filter_map(|(extension, value)| {
705 if value == b"!" {
723 if value == b"!" {
706 // Filter out disabled extensions
724 // Filter out disabled extensions
707 return None;
725 return None;
708 }
726 }
709 // Ignore extension suboptions. Only `required` exists for now.
727 // Ignore extension suboptions. Only `required` exists for now.
710 // `rhg` either supports an extension or doesn't, so it doesn't
728 // `rhg` either supports an extension or doesn't, so it doesn't
711 // make sense to consider the loading of an extension.
729 // make sense to consider the loading of an extension.
712 let actual_extension =
730 let actual_extension =
713 extension.split_2(b':').unwrap_or((extension, b"")).0;
731 extension.split_2(b':').unwrap_or((extension, b"")).0;
714 Some(actual_extension)
732 Some(actual_extension)
715 })
733 })
716 .collect();
734 .collect();
717
735
718 let mut unsupported = enabled;
736 let mut unsupported = enabled;
719 for supported in SUPPORTED_EXTENSIONS {
737 for supported in SUPPORTED_EXTENSIONS {
720 unsupported.remove(supported);
738 unsupported.remove(supported);
721 }
739 }
722
740
723 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
741 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
724 {
742 {
725 for ignored in ignored_list {
743 for ignored in ignored_list {
726 unsupported.remove(ignored.as_slice());
744 unsupported.remove(ignored.as_slice());
727 }
745 }
728 }
746 }
729
747
730 if unsupported.is_empty() {
748 if unsupported.is_empty() {
731 Ok(())
749 Ok(())
732 } else {
750 } else {
733 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
751 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
734 // Sort the extensions to get a stable output
752 // Sort the extensions to get a stable output
735 unsupported.sort();
753 unsupported.sort();
736 Err(CommandError::UnsupportedFeature {
754 Err(CommandError::UnsupportedFeature {
737 message: format_bytes!(
755 message: format_bytes!(
738 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
756 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
739 join(unsupported, b", ")
757 join(unsupported, b", ")
740 ),
758 ),
741 })
759 })
742 }
760 }
743 }
761 }
744
762
745 /// Array of tuples of (auto upgrade conf, feature conf, local requirement)
763 /// Array of tuples of (auto upgrade conf, feature conf, local requirement)
746 const AUTO_UPGRADES: &[((&str, &str), (&str, &str), &str)] = &[
764 const AUTO_UPGRADES: &[((&str, &str), (&str, &str), &str)] = &[
747 (
765 (
748 ("format", "use-share-safe.automatic-upgrade-of-mismatching-repositories"),
766 ("format", "use-share-safe.automatic-upgrade-of-mismatching-repositories"),
749 ("format", "use-share-safe"),
767 ("format", "use-share-safe"),
750 requirements::SHARESAFE_REQUIREMENT,
768 requirements::SHARESAFE_REQUIREMENT,
751 ),
769 ),
752 (
770 (
753 ("format", "use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories"),
771 ("format", "use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories"),
754 ("format", "use-dirstate-tracked-hint"),
772 ("format", "use-dirstate-tracked-hint"),
755 requirements::DIRSTATE_TRACKED_HINT_V1,
773 requirements::DIRSTATE_TRACKED_HINT_V1,
756 ),
774 ),
757 (
775 (
758 ("format", "use-dirstate-v2.automatic-upgrade-of-mismatching-repositories"),
776 ("format", "use-dirstate-v2.automatic-upgrade-of-mismatching-repositories"),
759 ("format", "use-dirstate-v2"),
777 ("format", "use-dirstate-v2"),
760 requirements::DIRSTATE_V2_REQUIREMENT,
778 requirements::DIRSTATE_V2_REQUIREMENT,
761 ),
779 ),
762 ];
780 ];
763
781
764 /// Mercurial allows users to automatically upgrade their repository.
782 /// Mercurial allows users to automatically upgrade their repository.
765 /// `rhg` does not have the ability to upgrade yet, so fallback if an upgrade
783 /// `rhg` does not have the ability to upgrade yet, so fallback if an upgrade
766 /// is needed.
784 /// is needed.
767 fn check_auto_upgrade(
785 fn check_auto_upgrade(
768 config: &Config,
786 config: &Config,
769 reqs: &HashSet<String>,
787 reqs: &HashSet<String>,
770 ) -> Result<(), CommandError> {
788 ) -> Result<(), CommandError> {
771 for (upgrade_conf, feature_conf, local_req) in AUTO_UPGRADES.iter() {
789 for (upgrade_conf, feature_conf, local_req) in AUTO_UPGRADES.iter() {
772 let auto_upgrade = config
790 let auto_upgrade = config
773 .get_bool(upgrade_conf.0.as_bytes(), upgrade_conf.1.as_bytes())?;
791 .get_bool(upgrade_conf.0.as_bytes(), upgrade_conf.1.as_bytes())?;
774
792
775 if auto_upgrade {
793 if auto_upgrade {
776 let want_it = config.get_bool(
794 let want_it = config.get_bool(
777 feature_conf.0.as_bytes(),
795 feature_conf.0.as_bytes(),
778 feature_conf.1.as_bytes(),
796 feature_conf.1.as_bytes(),
779 )?;
797 )?;
780 let have_it = reqs.contains(*local_req);
798 let have_it = reqs.contains(*local_req);
781
799
782 let action = match (want_it, have_it) {
800 let action = match (want_it, have_it) {
783 (true, false) => Some("upgrade"),
801 (true, false) => Some("upgrade"),
784 (false, true) => Some("downgrade"),
802 (false, true) => Some("downgrade"),
785 _ => None,
803 _ => None,
786 };
804 };
787 if let Some(action) = action {
805 if let Some(action) = action {
788 let message = format!(
806 let message = format!(
789 "automatic {} {}.{}",
807 "automatic {} {}.{}",
790 action, upgrade_conf.0, upgrade_conf.1
808 action, upgrade_conf.0, upgrade_conf.1
791 );
809 );
792 return Err(CommandError::unsupported(message));
810 return Err(CommandError::unsupported(message));
793 }
811 }
794 }
812 }
795 }
813 }
796 Ok(())
814 Ok(())
797 }
815 }
798
816
799 fn check_unsupported(
817 fn check_unsupported(
800 config: &Config,
818 config: &Config,
801 repo: Result<&Repo, &NoRepoInCwdError>,
819 repo: Result<&Repo, &NoRepoInCwdError>,
802 ) -> Result<(), CommandError> {
820 ) -> Result<(), CommandError> {
803 check_extensions(config)?;
821 check_extensions(config)?;
804
822
805 if std::env::var_os("HG_PENDING").is_some() {
823 if std::env::var_os("HG_PENDING").is_some() {
806 // TODO: only if the value is `== repo.working_directory`?
824 // TODO: only if the value is `== repo.working_directory`?
807 // What about relative v.s. absolute paths?
825 // What about relative v.s. absolute paths?
808 Err(CommandError::unsupported("$HG_PENDING"))?
826 Err(CommandError::unsupported("$HG_PENDING"))?
809 }
827 }
810
828
811 if let Ok(repo) = repo {
829 if let Ok(repo) = repo {
812 if repo.has_subrepos()? {
830 if repo.has_subrepos()? {
813 Err(CommandError::unsupported("sub-repositories"))?
831 Err(CommandError::unsupported("sub-repositories"))?
814 }
832 }
815 check_auto_upgrade(config, repo.requirements())?;
833 check_auto_upgrade(config, repo.requirements())?;
816 }
834 }
817
835
818 if config.has_non_empty_section(b"encode") {
836 if config.has_non_empty_section(b"encode") {
819 Err(CommandError::unsupported("[encode] config"))?
837 Err(CommandError::unsupported("[encode] config"))?
820 }
838 }
821
839
822 if config.has_non_empty_section(b"decode") {
840 if config.has_non_empty_section(b"decode") {
823 Err(CommandError::unsupported("[decode] config"))?
841 Err(CommandError::unsupported("[decode] config"))?
824 }
842 }
825
843
826 Ok(())
844 Ok(())
827 }
845 }
General Comments 0
You need to be logged in to leave comments. Login now