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