##// END OF EJS Templates
rhg: add exit code to HgError::Abort()...
Pulkit Goyal -
r48199:6e49769b default
parent child Browse files
Show More
@@ -0,0 +1,19
1 pub type ExitCode = i32;
2
3 /// Successful exit
4 pub const OK: ExitCode = 0;
5
6 /// Generic abort
7 pub const ABORT: ExitCode = 255;
8
9 // Abort when there is a config related error
10 pub const CONFIG_ERROR_ABORT: ExitCode = 30;
11
12 // Abort when there is an error while parsing config
13 pub const CONFIG_PARSE_ERROR_ABORT: ExitCode = 10;
14
15 /// Generic something completed but did not succeed
16 pub const UNSUCCESSFUL: ExitCode = 1;
17
18 /// Command or feature not implemented by rhg
19 pub const UNIMPLEMENTED: ExitCode = 252;
@@ -1,319 +1,323
1 1 // layer.rs
2 2 //
3 3 // Copyright 2020
4 4 // Valentin Gatien-Baron,
5 5 // Raphaël Gomès <rgomes@octobus.net>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 use crate::errors::HgError;
11 use crate::exit_codes::CONFIG_PARSE_ERROR_ABORT;
11 12 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
12 13 use format_bytes::{format_bytes, write_bytes, DisplayBytes};
13 14 use lazy_static::lazy_static;
14 15 use regex::bytes::Regex;
15 16 use std::collections::HashMap;
16 17 use std::path::{Path, PathBuf};
17 18
18 19 lazy_static! {
19 20 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
20 21 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
21 22 /// Continuation whitespace
22 23 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
23 24 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
24 25 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
25 26 /// A directive that allows for removing previous entries
26 27 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
27 28 /// A directive that allows for including other config files
28 29 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
29 30 }
30 31
31 32 /// All config values separated by layers of precedence.
32 33 /// Each config source may be split in multiple layers if `%include` directives
33 34 /// are used.
34 35 /// TODO detail the general precedence
35 36 #[derive(Clone)]
36 37 pub struct ConfigLayer {
37 38 /// Mapping of the sections to their items
38 39 sections: HashMap<Vec<u8>, ConfigItem>,
39 40 /// All sections (and their items/values) in a layer share the same origin
40 41 pub origin: ConfigOrigin,
41 42 /// Whether this layer comes from a trusted user or group
42 43 pub trusted: bool,
43 44 }
44 45
45 46 impl ConfigLayer {
46 47 pub fn new(origin: ConfigOrigin) -> Self {
47 48 ConfigLayer {
48 49 sections: HashMap::new(),
49 50 trusted: true, // TODO check
50 51 origin,
51 52 }
52 53 }
53 54
54 55 /// Parse `--config` CLI arguments and return a layer if there’s any
55 56 pub(crate) fn parse_cli_args(
56 57 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
57 58 ) -> Result<Option<Self>, ConfigError> {
58 59 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
59 60 use crate::utils::SliceExt;
60 61
61 62 let (section_and_item, value) = arg.split_2(b'=')?;
62 63 let (section, item) = section_and_item.trim().split_2(b'.')?;
63 64 Some((
64 65 section.to_owned(),
65 66 item.to_owned(),
66 67 value.trim().to_owned(),
67 68 ))
68 69 }
69 70
70 71 let mut layer = Self::new(ConfigOrigin::CommandLine);
71 72 for arg in cli_config_args {
72 73 let arg = arg.as_ref();
73 74 if let Some((section, item, value)) = parse_one(arg) {
74 75 layer.add(section, item, value, None);
75 76 } else {
76 Err(HgError::abort(format!(
77 "abort: malformed --config option: '{}' \
77 Err(HgError::abort(
78 format!(
79 "abort: malformed --config option: '{}' \
78 80 (use --config section.name=value)",
79 String::from_utf8_lossy(arg),
80 )))?
81 String::from_utf8_lossy(arg),
82 ),
83 CONFIG_PARSE_ERROR_ABORT,
84 ))?
81 85 }
82 86 }
83 87 if layer.sections.is_empty() {
84 88 Ok(None)
85 89 } else {
86 90 Ok(Some(layer))
87 91 }
88 92 }
89 93
90 94 /// Returns whether this layer comes from `--config` CLI arguments
91 95 pub(crate) fn is_from_command_line(&self) -> bool {
92 96 if let ConfigOrigin::CommandLine = self.origin {
93 97 true
94 98 } else {
95 99 false
96 100 }
97 101 }
98 102
99 103 /// Add an entry to the config, overwriting the old one if already present.
100 104 pub fn add(
101 105 &mut self,
102 106 section: Vec<u8>,
103 107 item: Vec<u8>,
104 108 value: Vec<u8>,
105 109 line: Option<usize>,
106 110 ) {
107 111 self.sections
108 112 .entry(section)
109 113 .or_insert_with(|| HashMap::new())
110 114 .insert(item, ConfigValue { bytes: value, line });
111 115 }
112 116
113 117 /// Returns the config value in `<section>.<item>` if it exists
114 118 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
115 119 Some(self.sections.get(section)?.get(item)?)
116 120 }
117 121
118 122 /// Returns the keys defined in the given section
119 123 pub fn iter_keys(&self, section: &[u8]) -> impl Iterator<Item = &[u8]> {
120 124 self.sections
121 125 .get(section)
122 126 .into_iter()
123 127 .flat_map(|section| section.keys().map(|vec| &**vec))
124 128 }
125 129
126 130 pub fn is_empty(&self) -> bool {
127 131 self.sections.is_empty()
128 132 }
129 133
130 134 /// Returns a `Vec` of layers in order of precedence (so, in read order),
131 135 /// recursively parsing the `%include` directives if any.
132 136 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
133 137 let mut layers = vec![];
134 138
135 139 // Discard byte order mark if any
136 140 let data = if data.starts_with(b"\xef\xbb\xbf") {
137 141 &data[3..]
138 142 } else {
139 143 data
140 144 };
141 145
142 146 // TODO check if it's trusted
143 147 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
144 148
145 149 let mut lines_iter =
146 150 data.split(|b| *b == b'\n').enumerate().peekable();
147 151 let mut section = b"".to_vec();
148 152
149 153 while let Some((index, bytes)) = lines_iter.next() {
150 154 let line = Some(index + 1);
151 155 if let Some(m) = INCLUDE_RE.captures(&bytes) {
152 156 let filename_bytes = &m[1];
153 157 let filename_bytes = crate::utils::expand_vars(filename_bytes);
154 158 // `Path::parent` only fails for the root directory,
155 159 // which `src` can’t be since we’ve managed to open it as a
156 160 // file.
157 161 let dir = src
158 162 .parent()
159 163 .expect("Path::parent fail on a file we’ve read");
160 164 // `Path::join` with an absolute argument correctly ignores the
161 165 // base path
162 166 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
163 167 match std::fs::read(&filename) {
164 168 Ok(data) => {
165 169 layers.push(current_layer);
166 170 layers.extend(Self::parse(&filename, &data)?);
167 171 current_layer =
168 172 Self::new(ConfigOrigin::File(src.to_owned()));
169 173 }
170 174 Err(error) => {
171 175 if error.kind() != std::io::ErrorKind::NotFound {
172 176 return Err(ConfigParseError {
173 177 origin: ConfigOrigin::File(src.to_owned()),
174 178 line,
175 179 message: format_bytes!(
176 180 b"cannot include {} ({})",
177 181 filename_bytes,
178 182 format_bytes::Utf8(error)
179 183 ),
180 184 }
181 185 .into());
182 186 }
183 187 }
184 188 }
185 189 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
186 190 } else if let Some(m) = SECTION_RE.captures(&bytes) {
187 191 section = m[1].to_vec();
188 192 } else if let Some(m) = ITEM_RE.captures(&bytes) {
189 193 let item = m[1].to_vec();
190 194 let mut value = m[2].to_vec();
191 195 loop {
192 196 match lines_iter.peek() {
193 197 None => break,
194 198 Some((_, v)) => {
195 199 if let Some(_) = COMMENT_RE.captures(&v) {
196 200 } else if let Some(_) = CONT_RE.captures(&v) {
197 201 value.extend(b"\n");
198 202 value.extend(&m[1]);
199 203 } else {
200 204 break;
201 205 }
202 206 }
203 207 };
204 208 lines_iter.next();
205 209 }
206 210 current_layer.add(section.clone(), item, value, line);
207 211 } else if let Some(m) = UNSET_RE.captures(&bytes) {
208 212 if let Some(map) = current_layer.sections.get_mut(&section) {
209 213 map.remove(&m[1]);
210 214 }
211 215 } else {
212 216 let message = if bytes.starts_with(b" ") {
213 217 format_bytes!(b"unexpected leading whitespace: {}", bytes)
214 218 } else {
215 219 bytes.to_owned()
216 220 };
217 221 return Err(ConfigParseError {
218 222 origin: ConfigOrigin::File(src.to_owned()),
219 223 line,
220 224 message,
221 225 }
222 226 .into());
223 227 }
224 228 }
225 229 if !current_layer.is_empty() {
226 230 layers.push(current_layer);
227 231 }
228 232 Ok(layers)
229 233 }
230 234 }
231 235
232 236 impl DisplayBytes for ConfigLayer {
233 237 fn display_bytes(
234 238 &self,
235 239 out: &mut dyn std::io::Write,
236 240 ) -> std::io::Result<()> {
237 241 let mut sections: Vec<_> = self.sections.iter().collect();
238 242 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
239 243
240 244 for (section, items) in sections.into_iter() {
241 245 let mut items: Vec<_> = items.into_iter().collect();
242 246 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
243 247
244 248 for (item, config_entry) in items {
245 249 write_bytes!(
246 250 out,
247 251 b"{}.{}={} # {}\n",
248 252 section,
249 253 item,
250 254 &config_entry.bytes,
251 255 &self.origin,
252 256 )?
253 257 }
254 258 }
255 259 Ok(())
256 260 }
257 261 }
258 262
259 263 /// Mapping of section item to value.
260 264 /// In the following:
261 265 /// ```text
262 266 /// [ui]
263 267 /// paginate=no
264 268 /// ```
265 269 /// "paginate" is the section item and "no" the value.
266 270 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
267 271
268 272 #[derive(Clone, Debug, PartialEq)]
269 273 pub struct ConfigValue {
270 274 /// The raw bytes of the value (be it from the CLI, env or from a file)
271 275 pub bytes: Vec<u8>,
272 276 /// Only present if the value comes from a file, 1-indexed.
273 277 pub line: Option<usize>,
274 278 }
275 279
276 280 #[derive(Clone, Debug)]
277 281 pub enum ConfigOrigin {
278 282 /// From a configuration file
279 283 File(PathBuf),
280 284 /// From a `--config` CLI argument
281 285 CommandLine,
282 286 /// From environment variables like `$PAGER` or `$EDITOR`
283 287 Environment(Vec<u8>),
284 288 /* TODO cli
285 289 * TODO defaults (configitems.py)
286 290 * TODO extensions
287 291 * TODO Python resources?
288 292 * Others? */
289 293 }
290 294
291 295 impl DisplayBytes for ConfigOrigin {
292 296 fn display_bytes(
293 297 &self,
294 298 out: &mut dyn std::io::Write,
295 299 ) -> std::io::Result<()> {
296 300 match self {
297 301 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
298 302 ConfigOrigin::CommandLine => out.write_all(b"--config"),
299 303 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
300 304 }
301 305 }
302 306 }
303 307
304 308 #[derive(Debug)]
305 309 pub struct ConfigParseError {
306 310 pub origin: ConfigOrigin,
307 311 pub line: Option<usize>,
308 312 pub message: Vec<u8>,
309 313 }
310 314
311 315 #[derive(Debug, derive_more::From)]
312 316 pub enum ConfigError {
313 317 Parse(ConfigParseError),
314 318 Other(HgError),
315 319 }
316 320
317 321 fn make_regex(pattern: &'static str) -> Regex {
318 322 Regex::new(pattern).expect("expected a valid regex")
319 323 }
@@ -1,183 +1,194
1 1 use crate::config::ConfigValueParseError;
2 use crate::exit_codes;
2 3 use std::fmt;
3 4
4 5 /// Common error cases that can happen in many different APIs
5 6 #[derive(Debug, derive_more::From)]
6 7 pub enum HgError {
7 8 IoError {
8 9 error: std::io::Error,
9 10 context: IoErrorContext,
10 11 },
11 12
12 13 /// A file under `.hg/` normally only written by Mercurial is not in the
13 14 /// expected format. This indicates a bug in Mercurial, filesystem
14 15 /// corruption, or hardware failure.
15 16 ///
16 17 /// The given string is a short explanation for users, not intended to be
17 18 /// machine-readable.
18 19 CorruptedRepository(String),
19 20
20 21 /// The respository or requested operation involves a feature not
21 22 /// supported by the Rust implementation. Falling back to the Python
22 23 /// implementation may or may not work.
23 24 ///
24 25 /// The given string is a short explanation for users, not intended to be
25 26 /// machine-readable.
26 27 UnsupportedFeature(String),
27 28
28 29 /// Operation cannot proceed for some other reason.
29 30 ///
30 /// The given string is a short explanation for users, not intended to be
31 /// The message is a short explanation for users, not intended to be
31 32 /// machine-readable.
32 Abort(String),
33 Abort {
34 message: String,
35 detailed_exit_code: exit_codes::ExitCode,
36 },
33 37
34 38 /// A configuration value is not in the expected syntax.
35 39 ///
36 40 /// These errors can happen in many places in the code because values are
37 41 /// parsed lazily as the file-level parser does not know the expected type
38 42 /// and syntax of each value.
39 43 #[from]
40 44 ConfigValueParseError(ConfigValueParseError),
41 45 }
42 46
43 47 /// Details about where an I/O error happened
44 48 #[derive(Debug)]
45 49 pub enum IoErrorContext {
46 50 ReadingFile(std::path::PathBuf),
47 51 WritingFile(std::path::PathBuf),
48 52 RemovingFile(std::path::PathBuf),
49 53 RenamingFile {
50 54 from: std::path::PathBuf,
51 55 to: std::path::PathBuf,
52 56 },
53 57 /// `std::fs::canonicalize`
54 58 CanonicalizingPath(std::path::PathBuf),
55 59 /// `std::env::current_dir`
56 60 CurrentDir,
57 61 /// `std::env::current_exe`
58 62 CurrentExe,
59 63 }
60 64
61 65 impl HgError {
62 66 pub fn corrupted(explanation: impl Into<String>) -> Self {
63 67 // TODO: capture a backtrace here and keep it in the error value
64 68 // to aid debugging?
65 69 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
66 70 HgError::CorruptedRepository(explanation.into())
67 71 }
68 72
69 73 pub fn unsupported(explanation: impl Into<String>) -> Self {
70 74 HgError::UnsupportedFeature(explanation.into())
71 75 }
72 pub fn abort(explanation: impl Into<String>) -> Self {
73 HgError::Abort(explanation.into())
76
77 pub fn abort(
78 explanation: impl Into<String>,
79 exit_code: exit_codes::ExitCode,
80 ) -> Self {
81 HgError::Abort {
82 message: explanation.into(),
83 detailed_exit_code: exit_code,
84 }
74 85 }
75 86 }
76 87
77 88 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
78 89 impl fmt::Display for HgError {
79 90 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80 91 match self {
81 HgError::Abort(explanation) => write!(f, "{}", explanation),
92 HgError::Abort { message, .. } => write!(f, "{}", message),
82 93 HgError::IoError { error, context } => {
83 94 write!(f, "abort: {}: {}", context, error)
84 95 }
85 96 HgError::CorruptedRepository(explanation) => {
86 97 write!(f, "abort: {}", explanation)
87 98 }
88 99 HgError::UnsupportedFeature(explanation) => {
89 100 write!(f, "unsupported feature: {}", explanation)
90 101 }
91 102 HgError::ConfigValueParseError(error) => error.fmt(f),
92 103 }
93 104 }
94 105 }
95 106
96 107 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
97 108 impl fmt::Display for IoErrorContext {
98 109 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99 110 match self {
100 111 IoErrorContext::ReadingFile(path) => {
101 112 write!(f, "when reading {}", path.display())
102 113 }
103 114 IoErrorContext::WritingFile(path) => {
104 115 write!(f, "when writing {}", path.display())
105 116 }
106 117 IoErrorContext::RemovingFile(path) => {
107 118 write!(f, "when removing {}", path.display())
108 119 }
109 120 IoErrorContext::RenamingFile { from, to } => write!(
110 121 f,
111 122 "when renaming {} to {}",
112 123 from.display(),
113 124 to.display()
114 125 ),
115 126 IoErrorContext::CanonicalizingPath(path) => {
116 127 write!(f, "when canonicalizing {}", path.display())
117 128 }
118 129 IoErrorContext::CurrentDir => {
119 130 write!(f, "error getting current working directory")
120 131 }
121 132 IoErrorContext::CurrentExe => {
122 133 write!(f, "error getting current executable")
123 134 }
124 135 }
125 136 }
126 137 }
127 138
128 139 pub trait IoResultExt<T> {
129 140 /// Annotate a possible I/O error as related to a reading a file at the
130 141 /// given path.
131 142 ///
132 143 /// This allows printing something like “File not found when reading
133 144 /// example.txt” instead of just “File not found”.
134 145 ///
135 146 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
136 147 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError>;
137 148
138 149 fn with_context(
139 150 self,
140 151 context: impl FnOnce() -> IoErrorContext,
141 152 ) -> Result<T, HgError>;
142 153 }
143 154
144 155 impl<T> IoResultExt<T> for std::io::Result<T> {
145 156 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError> {
146 157 self.with_context(|| IoErrorContext::ReadingFile(path.to_owned()))
147 158 }
148 159
149 160 fn with_context(
150 161 self,
151 162 context: impl FnOnce() -> IoErrorContext,
152 163 ) -> Result<T, HgError> {
153 164 self.map_err(|error| HgError::IoError {
154 165 error,
155 166 context: context(),
156 167 })
157 168 }
158 169 }
159 170
160 171 pub trait HgResultExt<T> {
161 172 /// Handle missing files separately from other I/O error cases.
162 173 ///
163 174 /// Wraps the `Ok` type in an `Option`:
164 175 ///
165 176 /// * `Ok(x)` becomes `Ok(Some(x))`
166 177 /// * An I/O "not found" error becomes `Ok(None)`
167 178 /// * Other errors are unchanged
168 179 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
169 180 }
170 181
171 182 impl<T> HgResultExt<T> for Result<T, HgError> {
172 183 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
173 184 match self {
174 185 Ok(x) => Ok(Some(x)),
175 186 Err(HgError::IoError { error, .. })
176 187 if error.kind() == std::io::ErrorKind::NotFound =>
177 188 {
178 189 Ok(None)
179 190 }
180 191 Err(other_error) => Err(other_error),
181 192 }
182 193 }
183 194 }
@@ -1,132 +1,133
1 1 // Copyright 2018-2020 Georges Racinet <georges.racinet@octobus.net>
2 2 // and Mercurial contributors
3 3 //
4 4 // This software may be used and distributed according to the terms of the
5 5 // GNU General Public License version 2 or any later version.
6 6
7 7 mod ancestors;
8 8 pub mod dagops;
9 9 pub mod errors;
10 10 pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors};
11 11 pub mod dirstate;
12 12 pub mod dirstate_tree;
13 13 pub mod discovery;
14 pub mod exit_codes;
14 15 pub mod requirements;
15 16 pub mod testing; // unconditionally built, for use from integration tests
16 17 pub use dirstate::{
17 18 dirs_multiset::{DirsMultiset, DirsMultisetIter},
18 19 dirstate_map::DirstateMap,
19 20 parsers::{pack_dirstate, parse_dirstate, PARENT_SIZE},
20 21 status::{
21 22 status, BadMatch, BadType, DirstateStatus, HgPathCow, StatusError,
22 23 StatusOptions,
23 24 },
24 25 CopyMap, CopyMapIter, DirstateEntry, DirstateParents, EntryState,
25 26 StateMap, StateMapIter,
26 27 };
27 28 pub mod copy_tracing;
28 29 mod filepatterns;
29 30 pub mod matchers;
30 31 pub mod repo;
31 32 pub mod revlog;
32 33 pub use revlog::*;
33 34 pub mod config;
34 35 pub mod logging;
35 36 pub mod operations;
36 37 pub mod revset;
37 38 pub mod utils;
38 39
39 40 use crate::utils::hg_path::{HgPathBuf, HgPathError};
40 41 pub use filepatterns::{
41 42 parse_pattern_syntax, read_pattern_file, IgnorePattern,
42 43 PatternFileWarning, PatternSyntax,
43 44 };
44 45 use std::collections::HashMap;
45 46 use std::fmt;
46 47 use twox_hash::RandomXxHashBuilder64;
47 48
48 49 /// This is a contract between the `micro-timer` crate and us, to expose
49 50 /// the `log` crate as `crate::log`.
50 51 use log;
51 52
52 53 pub type LineNumber = usize;
53 54
54 55 /// Rust's default hasher is too slow because it tries to prevent collision
55 56 /// attacks. We are not concerned about those: if an ill-minded person has
56 57 /// write access to your repository, you have other issues.
57 58 pub type FastHashMap<K, V> = HashMap<K, V, RandomXxHashBuilder64>;
58 59
59 60 #[derive(Debug, PartialEq)]
60 61 pub enum DirstateMapError {
61 62 PathNotFound(HgPathBuf),
62 63 EmptyPath,
63 64 InvalidPath(HgPathError),
64 65 }
65 66
66 67 impl fmt::Display for DirstateMapError {
67 68 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
68 69 match self {
69 70 DirstateMapError::PathNotFound(_) => {
70 71 f.write_str("expected a value, found none")
71 72 }
72 73 DirstateMapError::EmptyPath => {
73 74 f.write_str("Overflow in dirstate.")
74 75 }
75 76 DirstateMapError::InvalidPath(path_error) => path_error.fmt(f),
76 77 }
77 78 }
78 79 }
79 80
80 81 #[derive(Debug, derive_more::From)]
81 82 pub enum DirstateError {
82 83 Map(DirstateMapError),
83 84 Common(errors::HgError),
84 85 }
85 86
86 87 impl fmt::Display for DirstateError {
87 88 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
88 89 match self {
89 90 DirstateError::Map(error) => error.fmt(f),
90 91 DirstateError::Common(error) => error.fmt(f),
91 92 }
92 93 }
93 94 }
94 95
95 96 #[derive(Debug, derive_more::From)]
96 97 pub enum PatternError {
97 98 #[from]
98 99 Path(HgPathError),
99 100 UnsupportedSyntax(String),
100 101 UnsupportedSyntaxInFile(String, String, usize),
101 102 TooLong(usize),
102 103 #[from]
103 104 IO(std::io::Error),
104 105 /// Needed a pattern that can be turned into a regex but got one that
105 106 /// can't. This should only happen through programmer error.
106 107 NonRegexPattern(IgnorePattern),
107 108 }
108 109
109 110 impl fmt::Display for PatternError {
110 111 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
111 112 match self {
112 113 PatternError::UnsupportedSyntax(syntax) => {
113 114 write!(f, "Unsupported syntax {}", syntax)
114 115 }
115 116 PatternError::UnsupportedSyntaxInFile(syntax, file_path, line) => {
116 117 write!(
117 118 f,
118 119 "{}:{}: unsupported syntax {}",
119 120 file_path, line, syntax
120 121 )
121 122 }
122 123 PatternError::TooLong(size) => {
123 124 write!(f, "matcher pattern is too long ({} bytes)", size)
124 125 }
125 126 PatternError::IO(error) => error.fmt(f),
126 127 PatternError::Path(error) => error.fmt(f),
127 128 PatternError::NonRegexPattern(pattern) => {
128 129 write!(f, "'{:?}' cannot be turned into a regex", pattern)
129 130 }
130 131 }
131 132 }
132 133 }
@@ -1,284 +1,287
1 1 use crate::config::{Config, ConfigError, ConfigParseError};
2 2 use crate::errors::{HgError, IoErrorContext, IoResultExt};
3 use crate::exit_codes;
3 4 use crate::requirements;
4 5 use crate::utils::files::get_path_from_bytes;
5 6 use crate::utils::SliceExt;
6 7 use memmap::{Mmap, MmapOptions};
7 8 use std::collections::HashSet;
8 9 use std::path::{Path, PathBuf};
9 10
10 11 /// A repository on disk
11 12 pub struct Repo {
12 13 working_directory: PathBuf,
13 14 dot_hg: PathBuf,
14 15 store: PathBuf,
15 16 requirements: HashSet<String>,
16 17 config: Config,
17 18 }
18 19
19 20 #[derive(Debug, derive_more::From)]
20 21 pub enum RepoError {
21 22 NotFound {
22 23 at: PathBuf,
23 24 },
24 25 #[from]
25 26 ConfigParseError(ConfigParseError),
26 27 #[from]
27 28 Other(HgError),
28 29 }
29 30
30 31 impl From<ConfigError> for RepoError {
31 32 fn from(error: ConfigError) -> Self {
32 33 match error {
33 34 ConfigError::Parse(error) => error.into(),
34 35 ConfigError::Other(error) => error.into(),
35 36 }
36 37 }
37 38 }
38 39
39 40 /// Filesystem access abstraction for the contents of a given "base" diretory
40 41 #[derive(Clone, Copy)]
41 42 pub struct Vfs<'a> {
42 43 pub(crate) base: &'a Path,
43 44 }
44 45
45 46 impl Repo {
46 47 /// tries to find nearest repository root in current working directory or
47 48 /// its ancestors
48 49 pub fn find_repo_root() -> Result<PathBuf, RepoError> {
49 50 let current_directory = crate::utils::current_dir()?;
50 51 // ancestors() is inclusive: it first yields `current_directory`
51 52 // as-is.
52 53 for ancestor in current_directory.ancestors() {
53 54 if ancestor.join(".hg").is_dir() {
54 55 return Ok(ancestor.to_path_buf());
55 56 }
56 57 }
57 58 return Err(RepoError::NotFound {
58 59 at: current_directory,
59 60 });
60 61 }
61 62
62 63 /// Find a repository, either at the given path (which must contain a `.hg`
63 64 /// sub-directory) or by searching the current directory and its
64 65 /// ancestors.
65 66 ///
66 67 /// A method with two very different "modes" like this usually a code smell
67 68 /// to make two methods instead, but in this case an `Option` is what rhg
68 69 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
69 70 /// Having two methods would just move that `if` to almost all callers.
70 71 pub fn find(
71 72 config: &Config,
72 73 explicit_path: Option<PathBuf>,
73 74 ) -> Result<Self, RepoError> {
74 75 if let Some(root) = explicit_path {
75 76 if root.join(".hg").is_dir() {
76 77 Self::new_at_path(root.to_owned(), config)
77 78 } else if root.is_file() {
78 79 Err(HgError::unsupported("bundle repository").into())
79 80 } else {
80 81 Err(RepoError::NotFound {
81 82 at: root.to_owned(),
82 83 })
83 84 }
84 85 } else {
85 86 let root = Self::find_repo_root()?;
86 87 Self::new_at_path(root, config)
87 88 }
88 89 }
89 90
90 91 /// To be called after checking that `.hg` is a sub-directory
91 92 fn new_at_path(
92 93 working_directory: PathBuf,
93 94 config: &Config,
94 95 ) -> Result<Self, RepoError> {
95 96 let dot_hg = working_directory.join(".hg");
96 97
97 98 let mut repo_config_files = Vec::new();
98 99 repo_config_files.push(dot_hg.join("hgrc"));
99 100 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
100 101
101 102 let hg_vfs = Vfs { base: &dot_hg };
102 103 let mut reqs = requirements::load_if_exists(hg_vfs)?;
103 104 let relative =
104 105 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
105 106 let shared =
106 107 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
107 108
108 109 // From `mercurial/localrepo.py`:
109 110 //
110 111 // if .hg/requires contains the sharesafe requirement, it means
111 112 // there exists a `.hg/store/requires` too and we should read it
112 113 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
113 114 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
114 115 // is not present, refer checkrequirementscompat() for that
115 116 //
116 117 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
117 118 // repository was shared the old way. We check the share source
118 119 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
119 120 // current repository needs to be reshared
120 121 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
121 122
122 123 let store_path;
123 124 if !shared {
124 125 store_path = dot_hg.join("store");
125 126 } else {
126 127 let bytes = hg_vfs.read("sharedpath")?;
127 128 let mut shared_path =
128 129 get_path_from_bytes(bytes.trim_end_newlines()).to_owned();
129 130 if relative {
130 131 shared_path = dot_hg.join(shared_path)
131 132 }
132 133 if !shared_path.is_dir() {
133 134 return Err(HgError::corrupted(format!(
134 135 ".hg/sharedpath points to nonexistent directory {}",
135 136 shared_path.display()
136 137 ))
137 138 .into());
138 139 }
139 140
140 141 store_path = shared_path.join("store");
141 142
142 143 let source_is_share_safe =
143 144 requirements::load(Vfs { base: &shared_path })?
144 145 .contains(requirements::SHARESAFE_REQUIREMENT);
145 146
146 147 if share_safe && !source_is_share_safe {
147 148 return Err(match config
148 149 .get(b"share", b"safe-mismatch.source-not-safe")
149 150 {
150 151 Some(b"abort") | None => HgError::abort(
151 152 "abort: share source does not support share-safe requirement\n\
152 153 (see `hg help config.format.use-share-safe` for more information)",
154 exit_codes::ABORT,
153 155 ),
154 156 _ => HgError::unsupported("share-safe downgrade"),
155 157 }
156 158 .into());
157 159 } else if source_is_share_safe && !share_safe {
158 160 return Err(
159 161 match config.get(b"share", b"safe-mismatch.source-safe") {
160 162 Some(b"abort") | None => HgError::abort(
161 163 "abort: version mismatch: source uses share-safe \
162 164 functionality while the current share does not\n\
163 165 (see `hg help config.format.use-share-safe` for more information)",
166 exit_codes::ABORT,
164 167 ),
165 168 _ => HgError::unsupported("share-safe upgrade"),
166 169 }
167 170 .into(),
168 171 );
169 172 }
170 173
171 174 if share_safe {
172 175 repo_config_files.insert(0, shared_path.join("hgrc"))
173 176 }
174 177 }
175 178 if share_safe {
176 179 reqs.extend(requirements::load(Vfs { base: &store_path })?);
177 180 }
178 181
179 182 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
180 183 config.combine_with_repo(&repo_config_files)?
181 184 } else {
182 185 config.clone()
183 186 };
184 187
185 188 let repo = Self {
186 189 requirements: reqs,
187 190 working_directory,
188 191 store: store_path,
189 192 dot_hg,
190 193 config: repo_config,
191 194 };
192 195
193 196 requirements::check(&repo)?;
194 197
195 198 Ok(repo)
196 199 }
197 200
198 201 pub fn working_directory_path(&self) -> &Path {
199 202 &self.working_directory
200 203 }
201 204
202 205 pub fn requirements(&self) -> &HashSet<String> {
203 206 &self.requirements
204 207 }
205 208
206 209 pub fn config(&self) -> &Config {
207 210 &self.config
208 211 }
209 212
210 213 /// For accessing repository files (in `.hg`), except for the store
211 214 /// (`.hg/store`).
212 215 pub fn hg_vfs(&self) -> Vfs<'_> {
213 216 Vfs { base: &self.dot_hg }
214 217 }
215 218
216 219 /// For accessing repository store files (in `.hg/store`)
217 220 pub fn store_vfs(&self) -> Vfs<'_> {
218 221 Vfs { base: &self.store }
219 222 }
220 223
221 224 /// For accessing the working copy
222 225 pub fn working_directory_vfs(&self) -> Vfs<'_> {
223 226 Vfs {
224 227 base: &self.working_directory,
225 228 }
226 229 }
227 230
228 231 pub fn has_dirstate_v2(&self) -> bool {
229 232 self.requirements
230 233 .contains(requirements::DIRSTATE_V2_REQUIREMENT)
231 234 }
232 235
233 236 pub fn dirstate_parents(
234 237 &self,
235 238 ) -> Result<crate::dirstate::DirstateParents, HgError> {
236 239 let dirstate = self.hg_vfs().mmap_open("dirstate")?;
237 240 if dirstate.is_empty() {
238 241 return Ok(crate::dirstate::DirstateParents::NULL);
239 242 }
240 243 let parents = if self.has_dirstate_v2() {
241 244 crate::dirstate_tree::on_disk::parse_dirstate_parents(&dirstate)?
242 245 } else {
243 246 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?
244 247 };
245 248 Ok(parents.clone())
246 249 }
247 250 }
248 251
249 252 impl Vfs<'_> {
250 253 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
251 254 self.base.join(relative_path)
252 255 }
253 256
254 257 pub fn read(
255 258 &self,
256 259 relative_path: impl AsRef<Path>,
257 260 ) -> Result<Vec<u8>, HgError> {
258 261 let path = self.join(relative_path);
259 262 std::fs::read(&path).when_reading_file(&path)
260 263 }
261 264
262 265 pub fn mmap_open(
263 266 &self,
264 267 relative_path: impl AsRef<Path>,
265 268 ) -> Result<Mmap, HgError> {
266 269 let path = self.base.join(relative_path);
267 270 let file = std::fs::File::open(&path).when_reading_file(&path)?;
268 271 // TODO: what are the safety requirements here?
269 272 let mmap = unsafe { MmapOptions::new().map(&file) }
270 273 .when_reading_file(&path)?;
271 274 Ok(mmap)
272 275 }
273 276
274 277 pub fn rename(
275 278 &self,
276 279 relative_from: impl AsRef<Path>,
277 280 relative_to: impl AsRef<Path>,
278 281 ) -> Result<(), HgError> {
279 282 let from = self.join(relative_from);
280 283 let to = self.join(relative_to);
281 284 std::fs::rename(&from, &to)
282 285 .with_context(|| IoErrorContext::RenamingFile { from, to })
283 286 }
284 287 }
@@ -1,195 +1,195
1 use crate::exitcode;
2 1 use crate::ui::utf8_to_local;
3 2 use crate::ui::UiError;
4 3 use crate::NoRepoInCwdError;
5 4 use format_bytes::format_bytes;
6 5 use hg::config::{ConfigError, ConfigParseError, ConfigValueParseError};
7 6 use hg::errors::HgError;
7 use hg::exit_codes;
8 8 use hg::repo::RepoError;
9 9 use hg::revlog::revlog::RevlogError;
10 10 use hg::utils::files::get_bytes_from_path;
11 11 use hg::{DirstateError, DirstateMapError, StatusError};
12 12 use std::convert::From;
13 13
14 14 /// The kind of command error
15 15 #[derive(Debug)]
16 16 pub enum CommandError {
17 17 /// Exit with an error message and "standard" failure exit code.
18 18 Abort {
19 19 message: Vec<u8>,
20 detailed_exit_code: exitcode::ExitCode,
20 detailed_exit_code: exit_codes::ExitCode,
21 21 },
22 22
23 23 /// Exit with a failure exit code but no message.
24 24 Unsuccessful,
25 25
26 26 /// Encountered something (such as a CLI argument, repository layout, …)
27 27 /// not supported by this version of `rhg`. Depending on configuration
28 28 /// `rhg` may attempt to silently fall back to Python-based `hg`, which
29 29 /// may or may not support this feature.
30 30 UnsupportedFeature { message: Vec<u8> },
31 31 }
32 32
33 33 impl CommandError {
34 34 pub fn abort(message: impl AsRef<str>) -> Self {
35 CommandError::abort_with_exit_code(message, exitcode::ABORT)
35 CommandError::abort_with_exit_code(message, exit_codes::ABORT)
36 36 }
37 37
38 38 pub fn abort_with_exit_code(
39 39 message: impl AsRef<str>,
40 detailed_exit_code: exitcode::ExitCode,
40 detailed_exit_code: exit_codes::ExitCode,
41 41 ) -> Self {
42 42 CommandError::Abort {
43 43 // TODO: bytes-based (instead of Unicode-based) formatting
44 44 // of error messages to handle non-UTF-8 filenames etc:
45 45 // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output
46 46 message: utf8_to_local(message.as_ref()).into(),
47 47 detailed_exit_code: detailed_exit_code,
48 48 }
49 49 }
50 50
51 51 pub fn unsupported(message: impl AsRef<str>) -> Self {
52 52 CommandError::UnsupportedFeature {
53 53 message: utf8_to_local(message.as_ref()).into(),
54 54 }
55 55 }
56 56 }
57 57
58 58 /// For now we don’t differenciate between invalid CLI args and valid for `hg`
59 59 /// but not supported yet by `rhg`.
60 60 impl From<clap::Error> for CommandError {
61 61 fn from(error: clap::Error) -> Self {
62 62 CommandError::unsupported(error.to_string())
63 63 }
64 64 }
65 65
66 66 impl From<HgError> for CommandError {
67 67 fn from(error: HgError) -> Self {
68 68 match error {
69 69 HgError::UnsupportedFeature(message) => {
70 70 CommandError::unsupported(message)
71 71 }
72 72 _ => CommandError::abort(error.to_string()),
73 73 }
74 74 }
75 75 }
76 76
77 77 impl From<ConfigValueParseError> for CommandError {
78 78 fn from(error: ConfigValueParseError) -> Self {
79 79 CommandError::abort_with_exit_code(
80 80 error.to_string(),
81 exitcode::CONFIG_ERROR_ABORT,
81 exit_codes::CONFIG_ERROR_ABORT,
82 82 )
83 83 }
84 84 }
85 85
86 86 impl From<UiError> for CommandError {
87 87 fn from(_error: UiError) -> Self {
88 88 // If we already failed writing to stdout or stderr,
89 89 // writing an error message to stderr about it would be likely to fail
90 90 // too.
91 91 CommandError::abort("")
92 92 }
93 93 }
94 94
95 95 impl From<RepoError> for CommandError {
96 96 fn from(error: RepoError) -> Self {
97 97 match error {
98 98 RepoError::NotFound { at } => CommandError::Abort {
99 99 message: format_bytes!(
100 100 b"abort: repository {} not found",
101 101 get_bytes_from_path(at)
102 102 ),
103 detailed_exit_code: exitcode::ABORT,
103 detailed_exit_code: exit_codes::ABORT,
104 104 },
105 105 RepoError::ConfigParseError(error) => error.into(),
106 106 RepoError::Other(error) => error.into(),
107 107 }
108 108 }
109 109 }
110 110
111 111 impl<'a> From<&'a NoRepoInCwdError> for CommandError {
112 112 fn from(error: &'a NoRepoInCwdError) -> Self {
113 113 let NoRepoInCwdError { cwd } = error;
114 114 CommandError::Abort {
115 115 message: format_bytes!(
116 116 b"abort: no repository found in '{}' (.hg not found)!",
117 117 get_bytes_from_path(cwd)
118 118 ),
119 detailed_exit_code: exitcode::ABORT,
119 detailed_exit_code: exit_codes::ABORT,
120 120 }
121 121 }
122 122 }
123 123
124 124 impl From<ConfigError> for CommandError {
125 125 fn from(error: ConfigError) -> Self {
126 126 match error {
127 127 ConfigError::Parse(error) => error.into(),
128 128 ConfigError::Other(error) => error.into(),
129 129 }
130 130 }
131 131 }
132 132
133 133 impl From<ConfigParseError> for CommandError {
134 134 fn from(error: ConfigParseError) -> Self {
135 135 let ConfigParseError {
136 136 origin,
137 137 line,
138 138 message,
139 139 } = error;
140 140 let line_message = if let Some(line_number) = line {
141 141 format_bytes!(b":{}", line_number.to_string().into_bytes())
142 142 } else {
143 143 Vec::new()
144 144 };
145 145 CommandError::Abort {
146 146 message: format_bytes!(
147 147 b"config error at {}{}: {}",
148 148 origin,
149 149 line_message,
150 150 message
151 151 ),
152 detailed_exit_code: exitcode::CONFIG_ERROR_ABORT,
152 detailed_exit_code: exit_codes::CONFIG_ERROR_ABORT,
153 153 }
154 154 }
155 155 }
156 156
157 157 impl From<(RevlogError, &str)> for CommandError {
158 158 fn from((err, rev): (RevlogError, &str)) -> CommandError {
159 159 match err {
160 160 RevlogError::WDirUnsupported => CommandError::abort(
161 161 "abort: working directory revision cannot be specified",
162 162 ),
163 163 RevlogError::InvalidRevision => CommandError::abort(format!(
164 164 "abort: invalid revision identifier: {}",
165 165 rev
166 166 )),
167 167 RevlogError::AmbiguousPrefix => CommandError::abort(format!(
168 168 "abort: ambiguous revision identifier: {}",
169 169 rev
170 170 )),
171 171 RevlogError::Other(error) => error.into(),
172 172 }
173 173 }
174 174 }
175 175
176 176 impl From<StatusError> for CommandError {
177 177 fn from(error: StatusError) -> Self {
178 178 CommandError::abort(format!("{}", error))
179 179 }
180 180 }
181 181
182 182 impl From<DirstateMapError> for CommandError {
183 183 fn from(error: DirstateMapError) -> Self {
184 184 CommandError::abort(format!("{}", error))
185 185 }
186 186 }
187 187
188 188 impl From<DirstateError> for CommandError {
189 189 fn from(error: DirstateError) -> Self {
190 190 match error {
191 191 DirstateError::Common(error) => error.into(),
192 192 DirstateError::Map(error) => error.into(),
193 193 }
194 194 }
195 195 }
@@ -1,588 +1,588
1 1 extern crate log;
2 2 use crate::ui::Ui;
3 3 use clap::App;
4 4 use clap::AppSettings;
5 5 use clap::Arg;
6 6 use clap::ArgMatches;
7 7 use format_bytes::{format_bytes, join};
8 8 use hg::config::{Config, ConfigSource};
9 use hg::exit_codes;
9 10 use hg::repo::{Repo, RepoError};
10 11 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
11 12 use hg::utils::SliceExt;
12 13 use std::ffi::OsString;
13 14 use std::path::PathBuf;
14 15 use std::process::Command;
15 16
16 17 mod blackbox;
17 18 mod error;
18 mod exitcode;
19 19 mod ui;
20 20 use error::CommandError;
21 21
22 22 fn main_with_result(
23 23 process_start_time: &blackbox::ProcessStartTime,
24 24 ui: &ui::Ui,
25 25 repo: Result<&Repo, &NoRepoInCwdError>,
26 26 config: &Config,
27 27 ) -> Result<(), CommandError> {
28 28 check_extensions(config)?;
29 29
30 30 let app = App::new("rhg")
31 31 .global_setting(AppSettings::AllowInvalidUtf8)
32 32 .global_setting(AppSettings::DisableVersion)
33 33 .setting(AppSettings::SubcommandRequired)
34 34 .setting(AppSettings::VersionlessSubcommands)
35 35 .arg(
36 36 Arg::with_name("repository")
37 37 .help("repository root directory")
38 38 .short("-R")
39 39 .long("--repository")
40 40 .value_name("REPO")
41 41 .takes_value(true)
42 42 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
43 43 .global(true),
44 44 )
45 45 .arg(
46 46 Arg::with_name("config")
47 47 .help("set/override config option (use 'section.name=value')")
48 48 .long("--config")
49 49 .value_name("CONFIG")
50 50 .takes_value(true)
51 51 .global(true)
52 52 // Ok: `--config section.key1=val --config section.key2=val2`
53 53 .multiple(true)
54 54 // Not ok: `--config section.key1=val section.key2=val2`
55 55 .number_of_values(1),
56 56 )
57 57 .arg(
58 58 Arg::with_name("cwd")
59 59 .help("change working directory")
60 60 .long("--cwd")
61 61 .value_name("DIR")
62 62 .takes_value(true)
63 63 .global(true),
64 64 )
65 65 .version("0.0.1");
66 66 let app = add_subcommand_args(app);
67 67
68 68 let matches = app.clone().get_matches_safe()?;
69 69
70 70 let (subcommand_name, subcommand_matches) = matches.subcommand();
71 71 let run = subcommand_run_fn(subcommand_name)
72 72 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
73 73 let subcommand_args = subcommand_matches
74 74 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
75 75
76 76 let invocation = CliInvocation {
77 77 ui,
78 78 subcommand_args,
79 79 config,
80 80 repo,
81 81 };
82 82 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
83 83 blackbox.log_command_start();
84 84 let result = run(&invocation);
85 85 blackbox.log_command_end(exit_code(
86 86 &result,
87 87 // TODO: show a warning or combine with original error if `get_bool`
88 88 // returns an error
89 89 config
90 90 .get_bool(b"ui", b"detailed-exit-code")
91 91 .unwrap_or(false),
92 92 ));
93 93 result
94 94 }
95 95
96 96 fn main() {
97 97 // Run this first, before we find out if the blackbox extension is even
98 98 // enabled, in order to include everything in-between in the duration
99 99 // measurements. Reading config files can be slow if they’re on NFS.
100 100 let process_start_time = blackbox::ProcessStartTime::now();
101 101
102 102 env_logger::init();
103 103 let ui = ui::Ui::new();
104 104
105 105 let early_args = EarlyArgs::parse(std::env::args_os());
106 106
107 107 let initial_current_dir = early_args.cwd.map(|cwd| {
108 108 let cwd = get_path_from_bytes(&cwd);
109 109 std::env::current_dir()
110 110 .and_then(|initial| {
111 111 std::env::set_current_dir(cwd)?;
112 112 Ok(initial)
113 113 })
114 114 .unwrap_or_else(|error| {
115 115 exit(
116 116 &None,
117 117 &ui,
118 118 OnUnsupported::Abort,
119 119 Err(CommandError::abort(format!(
120 120 "abort: {}: '{}'",
121 121 error,
122 122 cwd.display()
123 123 ))),
124 124 false,
125 125 )
126 126 })
127 127 });
128 128
129 129 let mut non_repo_config =
130 130 Config::load_non_repo().unwrap_or_else(|error| {
131 131 // Normally this is decided based on config, but we don’t have that
132 132 // available. As of this writing config loading never returns an
133 133 // "unsupported" error but that is not enforced by the type system.
134 134 let on_unsupported = OnUnsupported::Abort;
135 135
136 136 exit(
137 137 &initial_current_dir,
138 138 &ui,
139 139 on_unsupported,
140 140 Err(error.into()),
141 141 false,
142 142 )
143 143 });
144 144
145 145 non_repo_config
146 146 .load_cli_args_config(early_args.config)
147 147 .unwrap_or_else(|error| {
148 148 exit(
149 149 &initial_current_dir,
150 150 &ui,
151 151 OnUnsupported::from_config(&ui, &non_repo_config),
152 152 Err(error.into()),
153 153 non_repo_config
154 154 .get_bool(b"ui", b"detailed-exit-code")
155 155 .unwrap_or(false),
156 156 )
157 157 });
158 158
159 159 if let Some(repo_path_bytes) = &early_args.repo {
160 160 lazy_static::lazy_static! {
161 161 static ref SCHEME_RE: regex::bytes::Regex =
162 162 // Same as `_matchscheme` in `mercurial/util.py`
163 163 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
164 164 }
165 165 if SCHEME_RE.is_match(&repo_path_bytes) {
166 166 exit(
167 167 &initial_current_dir,
168 168 &ui,
169 169 OnUnsupported::from_config(&ui, &non_repo_config),
170 170 Err(CommandError::UnsupportedFeature {
171 171 message: format_bytes!(
172 172 b"URL-like --repository {}",
173 173 repo_path_bytes
174 174 ),
175 175 }),
176 176 // TODO: show a warning or combine with original error if
177 177 // `get_bool` returns an error
178 178 non_repo_config
179 179 .get_bool(b"ui", b"detailed-exit-code")
180 180 .unwrap_or(false),
181 181 )
182 182 }
183 183 }
184 184 let repo_arg = early_args.repo.unwrap_or(Vec::new());
185 185 let repo_path: Option<PathBuf> = {
186 186 if repo_arg.is_empty() {
187 187 None
188 188 } else {
189 189 let local_config = {
190 190 if std::env::var_os("HGRCSKIPREPO").is_none() {
191 191 // TODO: handle errors from find_repo_root
192 192 if let Ok(current_dir_path) = Repo::find_repo_root() {
193 193 let config_files = vec![
194 194 ConfigSource::AbsPath(
195 195 current_dir_path.join(".hg/hgrc"),
196 196 ),
197 197 ConfigSource::AbsPath(
198 198 current_dir_path.join(".hg/hgrc-not-shared"),
199 199 ),
200 200 ];
201 201 // TODO: handle errors from
202 202 // `load_from_explicit_sources`
203 203 Config::load_from_explicit_sources(config_files).ok()
204 204 } else {
205 205 None
206 206 }
207 207 } else {
208 208 None
209 209 }
210 210 };
211 211
212 212 let non_repo_config_val = {
213 213 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
214 214 match &non_repo_val {
215 215 Some(val) if val.len() > 0 => home::home_dir()
216 216 .unwrap_or_else(|| PathBuf::from("~"))
217 217 .join(get_path_from_bytes(val))
218 218 .canonicalize()
219 219 // TODO: handle error and make it similar to python
220 220 // implementation maybe?
221 221 .ok(),
222 222 _ => None,
223 223 }
224 224 };
225 225
226 226 let config_val = match &local_config {
227 227 None => non_repo_config_val,
228 228 Some(val) => {
229 229 let local_config_val = val.get(b"paths", &repo_arg);
230 230 match &local_config_val {
231 231 Some(val) if val.len() > 0 => {
232 232 // presence of a local_config assures that
233 233 // current_dir
234 234 // wont result in an Error
235 235 let canpath = hg::utils::current_dir()
236 236 .unwrap()
237 237 .join(get_path_from_bytes(val))
238 238 .canonicalize();
239 239 canpath.ok().or(non_repo_config_val)
240 240 }
241 241 _ => non_repo_config_val,
242 242 }
243 243 }
244 244 };
245 245 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
246 246 }
247 247 };
248 248
249 249 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
250 250 {
251 251 Ok(repo) => Ok(repo),
252 252 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
253 253 // Not finding a repo is not fatal yet, if `-R` was not given
254 254 Err(NoRepoInCwdError { cwd: at })
255 255 }
256 256 Err(error) => exit(
257 257 &initial_current_dir,
258 258 &ui,
259 259 OnUnsupported::from_config(&ui, &non_repo_config),
260 260 Err(error.into()),
261 261 // TODO: show a warning or combine with original error if
262 262 // `get_bool` returns an error
263 263 non_repo_config
264 264 .get_bool(b"ui", b"detailed-exit-code")
265 265 .unwrap_or(false),
266 266 ),
267 267 };
268 268
269 269 let config = if let Ok(repo) = &repo_result {
270 270 repo.config()
271 271 } else {
272 272 &non_repo_config
273 273 };
274 274 let on_unsupported = OnUnsupported::from_config(&ui, config);
275 275
276 276 let result = main_with_result(
277 277 &process_start_time,
278 278 &ui,
279 279 repo_result.as_ref(),
280 280 config,
281 281 );
282 282 exit(
283 283 &initial_current_dir,
284 284 &ui,
285 285 on_unsupported,
286 286 result,
287 287 // TODO: show a warning or combine with original error if `get_bool`
288 288 // returns an error
289 289 config
290 290 .get_bool(b"ui", b"detailed-exit-code")
291 291 .unwrap_or(false),
292 292 )
293 293 }
294 294
295 295 fn exit_code(
296 296 result: &Result<(), CommandError>,
297 297 use_detailed_exit_code: bool,
298 298 ) -> i32 {
299 299 match result {
300 Ok(()) => exitcode::OK,
300 Ok(()) => exit_codes::OK,
301 301 Err(CommandError::Abort {
302 302 message: _,
303 303 detailed_exit_code,
304 304 }) => {
305 305 if use_detailed_exit_code {
306 306 *detailed_exit_code
307 307 } else {
308 exitcode::ABORT
308 exit_codes::ABORT
309 309 }
310 310 }
311 Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL,
311 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
312 312
313 313 // Exit with a specific code and no error message to let a potential
314 314 // wrapper script fallback to Python-based Mercurial.
315 315 Err(CommandError::UnsupportedFeature { .. }) => {
316 exitcode::UNIMPLEMENTED
316 exit_codes::UNIMPLEMENTED
317 317 }
318 318 }
319 319 }
320 320
321 321 fn exit(
322 322 initial_current_dir: &Option<PathBuf>,
323 323 ui: &Ui,
324 324 mut on_unsupported: OnUnsupported,
325 325 result: Result<(), CommandError>,
326 326 use_detailed_exit_code: bool,
327 327 ) -> ! {
328 328 if let (
329 329 OnUnsupported::Fallback { executable },
330 330 Err(CommandError::UnsupportedFeature { .. }),
331 331 ) = (&on_unsupported, &result)
332 332 {
333 333 let mut args = std::env::args_os();
334 334 let executable_path = get_path_from_bytes(&executable);
335 335 let this_executable = args.next().expect("exepcted argv[0] to exist");
336 336 if executable_path == &PathBuf::from(this_executable) {
337 337 // Avoid spawning infinitely many processes until resource
338 338 // exhaustion.
339 339 let _ = ui.write_stderr(&format_bytes!(
340 340 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
341 341 points to `rhg` itself.\n",
342 342 executable
343 343 ));
344 344 on_unsupported = OnUnsupported::Abort
345 345 } else {
346 346 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
347 347 let mut command = Command::new(executable_path);
348 348 command.args(args);
349 349 if let Some(initial) = initial_current_dir {
350 350 command.current_dir(initial);
351 351 }
352 352 let result = command.status();
353 353 match result {
354 354 Ok(status) => std::process::exit(
355 status.code().unwrap_or(exitcode::ABORT),
355 status.code().unwrap_or(exit_codes::ABORT),
356 356 ),
357 357 Err(error) => {
358 358 let _ = ui.write_stderr(&format_bytes!(
359 359 b"tried to fall back to a '{}' sub-process but got error {}\n",
360 360 executable, format_bytes::Utf8(error)
361 361 ));
362 362 on_unsupported = OnUnsupported::Abort
363 363 }
364 364 }
365 365 }
366 366 }
367 367 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
368 368 }
369 369
370 370 fn exit_no_fallback(
371 371 ui: &Ui,
372 372 on_unsupported: OnUnsupported,
373 373 result: Result<(), CommandError>,
374 374 use_detailed_exit_code: bool,
375 375 ) -> ! {
376 376 match &result {
377 377 Ok(_) => {}
378 378 Err(CommandError::Unsuccessful) => {}
379 379 Err(CommandError::Abort {
380 380 message,
381 381 detailed_exit_code: _,
382 382 }) => {
383 383 if !message.is_empty() {
384 384 // Ignore errors when writing to stderr, we’re already exiting
385 385 // with failure code so there’s not much more we can do.
386 386 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
387 387 }
388 388 }
389 389 Err(CommandError::UnsupportedFeature { message }) => {
390 390 match on_unsupported {
391 391 OnUnsupported::Abort => {
392 392 let _ = ui.write_stderr(&format_bytes!(
393 393 b"unsupported feature: {}\n",
394 394 message
395 395 ));
396 396 }
397 397 OnUnsupported::AbortSilent => {}
398 398 OnUnsupported::Fallback { .. } => unreachable!(),
399 399 }
400 400 }
401 401 }
402 402 std::process::exit(exit_code(&result, use_detailed_exit_code))
403 403 }
404 404
405 405 macro_rules! subcommands {
406 406 ($( $command: ident )+) => {
407 407 mod commands {
408 408 $(
409 409 pub mod $command;
410 410 )+
411 411 }
412 412
413 413 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
414 414 app
415 415 $(
416 416 .subcommand(commands::$command::args())
417 417 )+
418 418 }
419 419
420 420 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
421 421
422 422 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
423 423 match name {
424 424 $(
425 425 stringify!($command) => Some(commands::$command::run),
426 426 )+
427 427 _ => None,
428 428 }
429 429 }
430 430 };
431 431 }
432 432
433 433 subcommands! {
434 434 cat
435 435 debugdata
436 436 debugrequirements
437 437 files
438 438 root
439 439 config
440 440 status
441 441 }
442 442
443 443 pub struct CliInvocation<'a> {
444 444 ui: &'a Ui,
445 445 subcommand_args: &'a ArgMatches<'a>,
446 446 config: &'a Config,
447 447 /// References inside `Result` is a bit peculiar but allow
448 448 /// `invocation.repo?` to work out with `&CliInvocation` since this
449 449 /// `Result` type is `Copy`.
450 450 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
451 451 }
452 452
453 453 struct NoRepoInCwdError {
454 454 cwd: PathBuf,
455 455 }
456 456
457 457 /// CLI arguments to be parsed "early" in order to be able to read
458 458 /// configuration before using Clap. Ideally we would also use Clap for this,
459 459 /// see <https://github.com/clap-rs/clap/discussions/2366>.
460 460 ///
461 461 /// These arguments are still declared when we do use Clap later, so that Clap
462 462 /// does not return an error for their presence.
463 463 struct EarlyArgs {
464 464 /// Values of all `--config` arguments. (Possibly none)
465 465 config: Vec<Vec<u8>>,
466 466 /// Value of the `-R` or `--repository` argument, if any.
467 467 repo: Option<Vec<u8>>,
468 468 /// Value of the `--cwd` argument, if any.
469 469 cwd: Option<Vec<u8>>,
470 470 }
471 471
472 472 impl EarlyArgs {
473 473 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
474 474 let mut args = args.into_iter().map(get_bytes_from_os_str);
475 475 let mut config = Vec::new();
476 476 let mut repo = None;
477 477 let mut cwd = None;
478 478 // Use `while let` instead of `for` so that we can also call
479 479 // `args.next()` inside the loop.
480 480 while let Some(arg) = args.next() {
481 481 if arg == b"--config" {
482 482 if let Some(value) = args.next() {
483 483 config.push(value)
484 484 }
485 485 } else if let Some(value) = arg.drop_prefix(b"--config=") {
486 486 config.push(value.to_owned())
487 487 }
488 488
489 489 if arg == b"--cwd" {
490 490 if let Some(value) = args.next() {
491 491 cwd = Some(value)
492 492 }
493 493 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
494 494 cwd = Some(value.to_owned())
495 495 }
496 496
497 497 if arg == b"--repository" || arg == b"-R" {
498 498 if let Some(value) = args.next() {
499 499 repo = Some(value)
500 500 }
501 501 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
502 502 repo = Some(value.to_owned())
503 503 } else if let Some(value) = arg.drop_prefix(b"-R") {
504 504 repo = Some(value.to_owned())
505 505 }
506 506 }
507 507 Self { config, repo, cwd }
508 508 }
509 509 }
510 510
511 511 /// What to do when encountering some unsupported feature.
512 512 ///
513 513 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
514 514 enum OnUnsupported {
515 515 /// Print an error message describing what feature is not supported,
516 516 /// and exit with code 252.
517 517 Abort,
518 518 /// Silently exit with code 252.
519 519 AbortSilent,
520 520 /// Try running a Python implementation
521 521 Fallback { executable: Vec<u8> },
522 522 }
523 523
524 524 impl OnUnsupported {
525 525 const DEFAULT: Self = OnUnsupported::Abort;
526 526
527 527 fn from_config(ui: &Ui, config: &Config) -> Self {
528 528 match config
529 529 .get(b"rhg", b"on-unsupported")
530 530 .map(|value| value.to_ascii_lowercase())
531 531 .as_deref()
532 532 {
533 533 Some(b"abort") => OnUnsupported::Abort,
534 534 Some(b"abort-silent") => OnUnsupported::AbortSilent,
535 535 Some(b"fallback") => OnUnsupported::Fallback {
536 536 executable: config
537 537 .get(b"rhg", b"fallback-executable")
538 538 .unwrap_or_else(|| {
539 539 exit_no_fallback(
540 540 ui,
541 541 Self::Abort,
542 542 Err(CommandError::abort(
543 543 "abort: 'rhg.on-unsupported=fallback' without \
544 544 'rhg.fallback-executable' set."
545 545 )),
546 546 false,
547 547 )
548 548 })
549 549 .to_owned(),
550 550 },
551 551 None => Self::DEFAULT,
552 552 Some(_) => {
553 553 // TODO: warn about unknown config value
554 554 Self::DEFAULT
555 555 }
556 556 }
557 557 }
558 558 }
559 559
560 560 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"];
561 561
562 562 fn check_extensions(config: &Config) -> Result<(), CommandError> {
563 563 let enabled = config.get_section_keys(b"extensions");
564 564
565 565 let mut unsupported = enabled;
566 566 for supported in SUPPORTED_EXTENSIONS {
567 567 unsupported.remove(supported);
568 568 }
569 569
570 570 if let Some(ignored_list) =
571 571 config.get_simple_list(b"rhg", b"ignored-extensions")
572 572 {
573 573 for ignored in ignored_list {
574 574 unsupported.remove(ignored);
575 575 }
576 576 }
577 577
578 578 if unsupported.is_empty() {
579 579 Ok(())
580 580 } else {
581 581 Err(CommandError::UnsupportedFeature {
582 582 message: format_bytes!(
583 583 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
584 584 join(unsupported, b", ")
585 585 ),
586 586 })
587 587 }
588 588 }
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now