##// END OF EJS Templates
rhg: Align config file parse error formatting with Python...
Simon Sapin -
r47465:3d692e72 default
parent child Browse files
Show More
@@ -1,292 +1,297 b''
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, IoResultExt};
11 11 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
12 use format_bytes::{write_bytes, DisplayBytes};
12 use format_bytes::{format_bytes, write_bytes, DisplayBytes};
13 13 use lazy_static::lazy_static;
14 14 use regex::bytes::Regex;
15 15 use std::collections::HashMap;
16 16 use std::path::{Path, PathBuf};
17 17
18 18 lazy_static! {
19 19 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
20 20 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
21 21 /// Continuation whitespace
22 22 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
23 23 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
24 24 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
25 25 /// A directive that allows for removing previous entries
26 26 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
27 27 /// A directive that allows for including other config files
28 28 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
29 29 }
30 30
31 31 /// All config values separated by layers of precedence.
32 32 /// Each config source may be split in multiple layers if `%include` directives
33 33 /// are used.
34 34 /// TODO detail the general precedence
35 35 #[derive(Clone)]
36 36 pub struct ConfigLayer {
37 37 /// Mapping of the sections to their items
38 38 sections: HashMap<Vec<u8>, ConfigItem>,
39 39 /// All sections (and their items/values) in a layer share the same origin
40 40 pub origin: ConfigOrigin,
41 41 /// Whether this layer comes from a trusted user or group
42 42 pub trusted: bool,
43 43 }
44 44
45 45 impl ConfigLayer {
46 46 pub fn new(origin: ConfigOrigin) -> Self {
47 47 ConfigLayer {
48 48 sections: HashMap::new(),
49 49 trusted: true, // TODO check
50 50 origin,
51 51 }
52 52 }
53 53
54 54 /// Parse `--config` CLI arguments and return a layer if there’s any
55 55 pub(crate) fn parse_cli_args(
56 56 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
57 57 ) -> Result<Option<Self>, ConfigError> {
58 58 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
59 59 use crate::utils::SliceExt;
60 60
61 61 let (section_and_item, value) = arg.split_2(b'=')?;
62 62 let (section, item) = section_and_item.trim().split_2(b'.')?;
63 63 Some((
64 64 section.to_owned(),
65 65 item.to_owned(),
66 66 value.trim().to_owned(),
67 67 ))
68 68 }
69 69
70 70 let mut layer = Self::new(ConfigOrigin::CommandLine);
71 71 for arg in cli_config_args {
72 72 let arg = arg.as_ref();
73 73 if let Some((section, item, value)) = parse_one(arg) {
74 74 layer.add(section, item, value, None);
75 75 } else {
76 76 Err(HgError::abort(format!(
77 77 "malformed --config option: '{}' \
78 78 (use --config section.name=value)",
79 79 String::from_utf8_lossy(arg),
80 80 )))?
81 81 }
82 82 }
83 83 if layer.sections.is_empty() {
84 84 Ok(None)
85 85 } else {
86 86 Ok(Some(layer))
87 87 }
88 88 }
89 89
90 90 /// Returns whether this layer comes from `--config` CLI arguments
91 91 pub(crate) fn is_from_command_line(&self) -> bool {
92 92 if let ConfigOrigin::CommandLine = self.origin {
93 93 true
94 94 } else {
95 95 false
96 96 }
97 97 }
98 98
99 99 /// Add an entry to the config, overwriting the old one if already present.
100 100 pub fn add(
101 101 &mut self,
102 102 section: Vec<u8>,
103 103 item: Vec<u8>,
104 104 value: Vec<u8>,
105 105 line: Option<usize>,
106 106 ) {
107 107 self.sections
108 108 .entry(section)
109 109 .or_insert_with(|| HashMap::new())
110 110 .insert(item, ConfigValue { bytes: value, line });
111 111 }
112 112
113 113 /// Returns the config value in `<section>.<item>` if it exists
114 114 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
115 115 Some(self.sections.get(section)?.get(item)?)
116 116 }
117 117
118 118 pub fn is_empty(&self) -> bool {
119 119 self.sections.is_empty()
120 120 }
121 121
122 122 /// Returns a `Vec` of layers in order of precedence (so, in read order),
123 123 /// recursively parsing the `%include` directives if any.
124 124 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
125 125 let mut layers = vec![];
126 126
127 127 // Discard byte order mark if any
128 128 let data = if data.starts_with(b"\xef\xbb\xbf") {
129 129 &data[3..]
130 130 } else {
131 131 data
132 132 };
133 133
134 134 // TODO check if it's trusted
135 135 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
136 136
137 137 let mut lines_iter =
138 138 data.split(|b| *b == b'\n').enumerate().peekable();
139 139 let mut section = b"".to_vec();
140 140
141 141 while let Some((index, bytes)) = lines_iter.next() {
142 142 if let Some(m) = INCLUDE_RE.captures(&bytes) {
143 143 let filename_bytes = &m[1];
144 144 // `Path::parent` only fails for the root directory,
145 145 // which `src` can’t be since we’ve managed to open it as a
146 146 // file.
147 147 let dir = src
148 148 .parent()
149 149 .expect("Path::parent fail on a file we’ve read");
150 150 // `Path::join` with an absolute argument correctly ignores the
151 151 // base path
152 152 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
153 153 let data =
154 154 std::fs::read(&filename).when_reading_file(&filename)?;
155 155 layers.push(current_layer);
156 156 layers.extend(Self::parse(&filename, &data)?);
157 157 current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
158 158 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
159 159 } else if let Some(m) = SECTION_RE.captures(&bytes) {
160 160 section = m[1].to_vec();
161 161 } else if let Some(m) = ITEM_RE.captures(&bytes) {
162 162 let item = m[1].to_vec();
163 163 let mut value = m[2].to_vec();
164 164 loop {
165 165 match lines_iter.peek() {
166 166 None => break,
167 167 Some((_, v)) => {
168 168 if let Some(_) = COMMENT_RE.captures(&v) {
169 169 } else if let Some(_) = CONT_RE.captures(&v) {
170 170 value.extend(b"\n");
171 171 value.extend(&m[1]);
172 172 } else {
173 173 break;
174 174 }
175 175 }
176 176 };
177 177 lines_iter.next();
178 178 }
179 179 current_layer.add(
180 180 section.clone(),
181 181 item,
182 182 value,
183 183 Some(index + 1),
184 184 );
185 185 } else if let Some(m) = UNSET_RE.captures(&bytes) {
186 186 if let Some(map) = current_layer.sections.get_mut(&section) {
187 187 map.remove(&m[1]);
188 188 }
189 189 } else {
190 let message = if bytes.starts_with(b" ") {
191 format_bytes!(b"unexpected leading whitespace: {}", bytes)
192 } else {
193 bytes.to_owned()
194 };
190 195 return Err(ConfigParseError {
191 196 origin: ConfigOrigin::File(src.to_owned()),
192 197 line: Some(index + 1),
193 bytes: bytes.to_owned(),
198 message,
194 199 }
195 200 .into());
196 201 }
197 202 }
198 203 if !current_layer.is_empty() {
199 204 layers.push(current_layer);
200 205 }
201 206 Ok(layers)
202 207 }
203 208 }
204 209
205 210 impl DisplayBytes for ConfigLayer {
206 211 fn display_bytes(
207 212 &self,
208 213 out: &mut dyn std::io::Write,
209 214 ) -> std::io::Result<()> {
210 215 let mut sections: Vec<_> = self.sections.iter().collect();
211 216 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
212 217
213 218 for (section, items) in sections.into_iter() {
214 219 let mut items: Vec<_> = items.into_iter().collect();
215 220 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
216 221
217 222 for (item, config_entry) in items {
218 223 write_bytes!(
219 224 out,
220 225 b"{}.{}={} # {}\n",
221 226 section,
222 227 item,
223 228 &config_entry.bytes,
224 229 &self.origin,
225 230 )?
226 231 }
227 232 }
228 233 Ok(())
229 234 }
230 235 }
231 236
232 237 /// Mapping of section item to value.
233 238 /// In the following:
234 239 /// ```text
235 240 /// [ui]
236 241 /// paginate=no
237 242 /// ```
238 243 /// "paginate" is the section item and "no" the value.
239 244 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
240 245
241 246 #[derive(Clone, Debug, PartialEq)]
242 247 pub struct ConfigValue {
243 248 /// The raw bytes of the value (be it from the CLI, env or from a file)
244 249 pub bytes: Vec<u8>,
245 250 /// Only present if the value comes from a file, 1-indexed.
246 251 pub line: Option<usize>,
247 252 }
248 253
249 254 #[derive(Clone, Debug)]
250 255 pub enum ConfigOrigin {
251 256 /// From a configuration file
252 257 File(PathBuf),
253 258 /// From a `--config` CLI argument
254 259 CommandLine,
255 260 /// From environment variables like `$PAGER` or `$EDITOR`
256 261 Environment(Vec<u8>),
257 262 /* TODO cli
258 263 * TODO defaults (configitems.py)
259 264 * TODO extensions
260 265 * TODO Python resources?
261 266 * Others? */
262 267 }
263 268
264 269 impl DisplayBytes for ConfigOrigin {
265 270 fn display_bytes(
266 271 &self,
267 272 out: &mut dyn std::io::Write,
268 273 ) -> std::io::Result<()> {
269 274 match self {
270 275 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
271 276 ConfigOrigin::CommandLine => out.write_all(b"--config"),
272 277 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
273 278 }
274 279 }
275 280 }
276 281
277 282 #[derive(Debug)]
278 283 pub struct ConfigParseError {
279 284 pub origin: ConfigOrigin,
280 285 pub line: Option<usize>,
281 pub bytes: Vec<u8>,
286 pub message: Vec<u8>,
282 287 }
283 288
284 289 #[derive(Debug, derive_more::From)]
285 290 pub enum ConfigError {
286 291 Parse(ConfigParseError),
287 292 Other(HgError),
288 293 }
289 294
290 295 fn make_regex(pattern: &'static str) -> Regex {
291 296 Regex::new(pattern).expect("expected a valid regex")
292 297 }
@@ -1,192 +1,196 b''
1 1 use crate::config::ConfigValueParseError;
2 2 use std::fmt;
3 3
4 4 /// Common error cases that can happen in many different APIs
5 5 #[derive(Debug, derive_more::From)]
6 6 pub enum HgError {
7 7 IoError {
8 8 error: std::io::Error,
9 9 context: IoErrorContext,
10 10 },
11 11
12 12 /// A file under `.hg/` normally only written by Mercurial is not in the
13 13 /// expected format. This indicates a bug in Mercurial, filesystem
14 14 /// corruption, or hardware failure.
15 15 ///
16 16 /// The given string is a short explanation for users, not intended to be
17 17 /// machine-readable.
18 18 CorruptedRepository(String),
19 19
20 20 /// The respository or requested operation involves a feature not
21 21 /// supported by the Rust implementation. Falling back to the Python
22 22 /// implementation may or may not work.
23 23 ///
24 24 /// The given string is a short explanation for users, not intended to be
25 25 /// machine-readable.
26 26 UnsupportedFeature(String),
27 27
28 28 /// Operation cannot proceed for some other reason.
29 29 ///
30 30 /// The given string is a short explanation for users, not intended to be
31 31 /// machine-readable.
32 32 Abort(String),
33 33
34 34 /// A configuration value is not in the expected syntax.
35 35 ///
36 36 /// These errors can happen in many places in the code because values are
37 37 /// parsed lazily as the file-level parser does not know the expected type
38 38 /// and syntax of each value.
39 39 #[from]
40 40 ConfigValueParseError(ConfigValueParseError),
41 41 }
42 42
43 43 /// Details about where an I/O error happened
44 44 #[derive(Debug)]
45 45 pub enum IoErrorContext {
46 46 ReadingFile(std::path::PathBuf),
47 47 WritingFile(std::path::PathBuf),
48 48 RemovingFile(std::path::PathBuf),
49 49 RenamingFile {
50 50 from: std::path::PathBuf,
51 51 to: std::path::PathBuf,
52 52 },
53 53 /// `std::env::current_dir`
54 54 CurrentDir,
55 55 /// `std::env::current_exe`
56 56 CurrentExe,
57 57 }
58 58
59 59 impl HgError {
60 60 pub fn corrupted(explanation: impl Into<String>) -> Self {
61 61 // TODO: capture a backtrace here and keep it in the error value
62 62 // to aid debugging?
63 63 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
64 64 HgError::CorruptedRepository(explanation.into())
65 65 }
66 66
67 67 pub fn unsupported(explanation: impl Into<String>) -> Self {
68 68 HgError::UnsupportedFeature(explanation.into())
69 69 }
70 70 pub fn abort(explanation: impl Into<String>) -> Self {
71 71 HgError::Abort(explanation.into())
72 72 }
73 73 }
74 74
75 75 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
76 76 impl fmt::Display for HgError {
77 77 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78 78 match self {
79 79 HgError::Abort(explanation) => write!(f, "{}", explanation),
80 80 HgError::IoError { error, context } => {
81 write!(f, "{}: {}", error, context)
81 write!(f, "abort: {}: {}", context, error)
82 82 }
83 83 HgError::CorruptedRepository(explanation) => {
84 write!(f, "corrupted repository: {}", explanation)
84 write!(f, "abort: corrupted repository: {}", explanation)
85 85 }
86 86 HgError::UnsupportedFeature(explanation) => {
87 87 write!(f, "unsupported feature: {}", explanation)
88 88 }
89 89 HgError::ConfigValueParseError(ConfigValueParseError {
90 90 origin: _,
91 91 line: _,
92 92 section,
93 93 item,
94 94 value,
95 95 expected_type,
96 96 }) => {
97 97 // TODO: add origin and line number information, here and in
98 98 // corresponding python code
99 99 write!(
100 100 f,
101 101 "config error: {}.{} is not a {} ('{}')",
102 102 String::from_utf8_lossy(section),
103 103 String::from_utf8_lossy(item),
104 104 expected_type,
105 105 String::from_utf8_lossy(value)
106 106 )
107 107 }
108 108 }
109 109 }
110 110 }
111 111
112 112 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
113 113 impl fmt::Display for IoErrorContext {
114 114 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
115 115 match self {
116 116 IoErrorContext::ReadingFile(path) => {
117 117 write!(f, "when reading {}", path.display())
118 118 }
119 119 IoErrorContext::WritingFile(path) => {
120 120 write!(f, "when writing {}", path.display())
121 121 }
122 122 IoErrorContext::RemovingFile(path) => {
123 123 write!(f, "when removing {}", path.display())
124 124 }
125 125 IoErrorContext::RenamingFile { from, to } => write!(
126 126 f,
127 127 "when renaming {} to {}",
128 128 from.display(),
129 129 to.display()
130 130 ),
131 IoErrorContext::CurrentDir => write!(f, "current directory"),
132 IoErrorContext::CurrentExe => write!(f, "current executable"),
131 IoErrorContext::CurrentDir => {
132 write!(f, "error getting current working directory")
133 }
134 IoErrorContext::CurrentExe => {
135 write!(f, "error getting current executable")
136 }
133 137 }
134 138 }
135 139 }
136 140
137 141 pub trait IoResultExt<T> {
138 142 /// Annotate a possible I/O error as related to a reading a file at the
139 143 /// given path.
140 144 ///
141 145 /// This allows printing something like β€œFile not found when reading
142 146 /// example.txt” instead of just β€œFile not found”.
143 147 ///
144 148 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
145 149 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError>;
146 150
147 151 fn with_context(
148 152 self,
149 153 context: impl FnOnce() -> IoErrorContext,
150 154 ) -> Result<T, HgError>;
151 155 }
152 156
153 157 impl<T> IoResultExt<T> for std::io::Result<T> {
154 158 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError> {
155 159 self.with_context(|| IoErrorContext::ReadingFile(path.to_owned()))
156 160 }
157 161
158 162 fn with_context(
159 163 self,
160 164 context: impl FnOnce() -> IoErrorContext,
161 165 ) -> Result<T, HgError> {
162 166 self.map_err(|error| HgError::IoError {
163 167 error,
164 168 context: context(),
165 169 })
166 170 }
167 171 }
168 172
169 173 pub trait HgResultExt<T> {
170 174 /// Handle missing files separately from other I/O error cases.
171 175 ///
172 176 /// Wraps the `Ok` type in an `Option`:
173 177 ///
174 178 /// * `Ok(x)` becomes `Ok(Some(x))`
175 179 /// * An I/O "not found" error becomes `Ok(None)`
176 180 /// * Other errors are unchanged
177 181 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
178 182 }
179 183
180 184 impl<T> HgResultExt<T> for Result<T, HgError> {
181 185 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
182 186 match self {
183 187 Ok(x) => Ok(Some(x)),
184 188 Err(HgError::IoError { error, .. })
185 189 if error.kind() == std::io::ErrorKind::NotFound =>
186 190 {
187 191 Ok(None)
188 192 }
189 193 Err(other_error) => Err(other_error),
190 194 }
191 195 }
192 196 }
@@ -1,143 +1,143 b''
1 1 use crate::ui::utf8_to_local;
2 2 use crate::ui::UiError;
3 3 use crate::NoRepoInCwdError;
4 4 use format_bytes::format_bytes;
5 5 use hg::config::{ConfigError, ConfigParseError};
6 6 use hg::errors::HgError;
7 7 use hg::repo::RepoError;
8 8 use hg::revlog::revlog::RevlogError;
9 9 use hg::utils::files::get_bytes_from_path;
10 10 use std::convert::From;
11 11
12 12 /// The kind of command error
13 13 #[derive(Debug)]
14 14 pub enum CommandError {
15 15 /// Exit with an error message and "standard" failure exit code.
16 16 Abort { message: Vec<u8> },
17 17
18 18 /// Encountered something (such as a CLI argument, repository layout, …)
19 19 /// not supported by this version of `rhg`. Depending on configuration
20 20 /// `rhg` may attempt to silently fall back to Python-based `hg`, which
21 21 /// may or may not support this feature.
22 22 UnsupportedFeature { message: Vec<u8> },
23 23 }
24 24
25 25 impl CommandError {
26 26 pub fn abort(message: impl AsRef<str>) -> Self {
27 27 CommandError::Abort {
28 28 // TODO: bytes-based (instead of Unicode-based) formatting
29 29 // of error messages to handle non-UTF-8 filenames etc:
30 30 // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output
31 31 message: utf8_to_local(message.as_ref()).into(),
32 32 }
33 33 }
34 34
35 35 pub fn unsupported(message: impl AsRef<str>) -> Self {
36 36 CommandError::UnsupportedFeature {
37 37 message: utf8_to_local(message.as_ref()).into(),
38 38 }
39 39 }
40 40 }
41 41
42 42 /// For now we don’t differenciate between invalid CLI args and valid for `hg`
43 43 /// but not supported yet by `rhg`.
44 44 impl From<clap::Error> for CommandError {
45 45 fn from(error: clap::Error) -> Self {
46 46 CommandError::unsupported(error.to_string())
47 47 }
48 48 }
49 49
50 50 impl From<HgError> for CommandError {
51 51 fn from(error: HgError) -> Self {
52 52 match error {
53 53 HgError::UnsupportedFeature(message) => {
54 54 CommandError::unsupported(message)
55 55 }
56 56 _ => CommandError::abort(error.to_string()),
57 57 }
58 58 }
59 59 }
60 60
61 61 impl From<UiError> for CommandError {
62 62 fn from(_error: UiError) -> Self {
63 63 // If we already failed writing to stdout or stderr,
64 64 // writing an error message to stderr about it would be likely to fail
65 65 // too.
66 66 CommandError::abort("")
67 67 }
68 68 }
69 69
70 70 impl From<RepoError> for CommandError {
71 71 fn from(error: RepoError) -> Self {
72 72 match error {
73 73 RepoError::NotFound { at } => CommandError::Abort {
74 74 message: format_bytes!(
75 75 b"repository {} not found",
76 76 get_bytes_from_path(at)
77 77 ),
78 78 },
79 79 RepoError::ConfigParseError(error) => error.into(),
80 80 RepoError::Other(error) => error.into(),
81 81 }
82 82 }
83 83 }
84 84
85 85 impl<'a> From<&'a NoRepoInCwdError> for CommandError {
86 86 fn from(error: &'a NoRepoInCwdError) -> Self {
87 87 let NoRepoInCwdError { cwd } = error;
88 88 CommandError::Abort {
89 89 message: format_bytes!(
90 b"no repository found in '{}' (.hg not found)!",
90 b"abort: no repository found in '{}' (.hg not found)!",
91 91 get_bytes_from_path(cwd)
92 92 ),
93 93 }
94 94 }
95 95 }
96 96
97 97 impl From<ConfigError> for CommandError {
98 98 fn from(error: ConfigError) -> Self {
99 99 match error {
100 100 ConfigError::Parse(error) => error.into(),
101 101 ConfigError::Other(error) => error.into(),
102 102 }
103 103 }
104 104 }
105 105
106 106 impl From<ConfigParseError> for CommandError {
107 107 fn from(error: ConfigParseError) -> Self {
108 108 let ConfigParseError {
109 109 origin,
110 110 line,
111 bytes,
111 message,
112 112 } = error;
113 113 let line_message = if let Some(line_number) = line {
114 format_bytes!(b" at line {}", line_number.to_string().into_bytes())
114 format_bytes!(b":{}", line_number.to_string().into_bytes())
115 115 } else {
116 116 Vec::new()
117 117 };
118 118 CommandError::Abort {
119 119 message: format_bytes!(
120 b"config parse error in {}{}: '{}'",
120 b"config error at {}{}: {}",
121 121 origin,
122 122 line_message,
123 bytes
123 message
124 124 ),
125 125 }
126 126 }
127 127 }
128 128
129 129 impl From<(RevlogError, &str)> for CommandError {
130 130 fn from((err, rev): (RevlogError, &str)) -> CommandError {
131 131 match err {
132 132 RevlogError::InvalidRevision => CommandError::abort(format!(
133 "invalid revision identifier {}",
133 "abort: invalid revision identifier: {}",
134 134 rev
135 135 )),
136 136 RevlogError::AmbiguousPrefix => CommandError::abort(format!(
137 "ambiguous revision identifier {}",
137 "abort: ambiguous revision identifier: {}",
138 138 rev
139 139 )),
140 140 RevlogError::Other(error) => error.into(),
141 141 }
142 142 }
143 143 }
@@ -1,355 +1,354 b''
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;
8 8 use hg::config::Config;
9 9 use hg::repo::{Repo, RepoError};
10 10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
11 11 use hg::utils::SliceExt;
12 12 use std::ffi::OsString;
13 13 use std::path::PathBuf;
14 14 use std::process::Command;
15 15
16 16 mod blackbox;
17 17 mod error;
18 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 let app = App::new("rhg")
29 29 .global_setting(AppSettings::AllowInvalidUtf8)
30 30 .setting(AppSettings::SubcommandRequired)
31 31 .setting(AppSettings::VersionlessSubcommands)
32 32 .arg(
33 33 Arg::with_name("repository")
34 34 .help("repository root directory")
35 35 .short("-R")
36 36 .long("--repository")
37 37 .value_name("REPO")
38 38 .takes_value(true)
39 39 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
40 40 .global(true),
41 41 )
42 42 .arg(
43 43 Arg::with_name("config")
44 44 .help("set/override config option (use 'section.name=value')")
45 45 .long("--config")
46 46 .value_name("CONFIG")
47 47 .takes_value(true)
48 48 .global(true)
49 49 // Ok: `--config section.key1=val --config section.key2=val2`
50 50 .multiple(true)
51 51 // Not ok: `--config section.key1=val section.key2=val2`
52 52 .number_of_values(1),
53 53 )
54 54 .version("0.0.1");
55 55 let app = add_subcommand_args(app);
56 56
57 57 let matches = app.clone().get_matches_safe()?;
58 58
59 59 let (subcommand_name, subcommand_matches) = matches.subcommand();
60 60 let run = subcommand_run_fn(subcommand_name)
61 61 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
62 62 let subcommand_args = subcommand_matches
63 63 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
64 64
65 65 let invocation = CliInvocation {
66 66 ui,
67 67 subcommand_args,
68 68 config,
69 69 repo,
70 70 };
71 71 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
72 72 blackbox.log_command_start();
73 73 let result = run(&invocation);
74 74 blackbox.log_command_end(exit_code(&result));
75 75 result
76 76 }
77 77
78 78 fn main() {
79 79 // Run this first, before we find out if the blackbox extension is even
80 80 // enabled, in order to include everything in-between in the duration
81 81 // measurements. Reading config files can be slow if they’re on NFS.
82 82 let process_start_time = blackbox::ProcessStartTime::now();
83 83
84 84 env_logger::init();
85 85 let ui = ui::Ui::new();
86 86
87 87 let early_args = EarlyArgs::parse(std::env::args_os());
88 88 let non_repo_config =
89 89 Config::load(early_args.config).unwrap_or_else(|error| {
90 90 // Normally this is decided based on config, but we don’t have that
91 91 // available. As of this writing config loading never returns an
92 92 // "unsupported" error but that is not enforced by the type system.
93 93 let on_unsupported = OnUnsupported::Abort;
94 94
95 95 exit(&ui, on_unsupported, Err(error.into()))
96 96 });
97 97
98 98 if let Some(repo_path_bytes) = &early_args.repo {
99 99 lazy_static::lazy_static! {
100 100 static ref SCHEME_RE: regex::bytes::Regex =
101 101 // Same as `_matchscheme` in `mercurial/util.py`
102 102 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
103 103 }
104 104 if SCHEME_RE.is_match(&repo_path_bytes) {
105 105 exit(
106 106 &ui,
107 107 OnUnsupported::from_config(&non_repo_config),
108 108 Err(CommandError::UnsupportedFeature {
109 109 message: format_bytes!(
110 110 b"URL-like --repository {}",
111 111 repo_path_bytes
112 112 ),
113 113 }),
114 114 )
115 115 }
116 116 }
117 117 let repo_path = early_args.repo.as_deref().map(get_path_from_bytes);
118 118 let repo_result = match Repo::find(&non_repo_config, repo_path) {
119 119 Ok(repo) => Ok(repo),
120 120 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
121 121 // Not finding a repo is not fatal yet, if `-R` was not given
122 122 Err(NoRepoInCwdError { cwd: at })
123 123 }
124 124 Err(error) => exit(
125 125 &ui,
126 126 OnUnsupported::from_config(&non_repo_config),
127 127 Err(error.into()),
128 128 ),
129 129 };
130 130
131 131 let config = if let Ok(repo) = &repo_result {
132 132 repo.config()
133 133 } else {
134 134 &non_repo_config
135 135 };
136 136
137 137 let result = main_with_result(
138 138 &process_start_time,
139 139 &ui,
140 140 repo_result.as_ref(),
141 141 config,
142 142 );
143 143 exit(&ui, OnUnsupported::from_config(config), result)
144 144 }
145 145
146 146 fn exit_code(result: &Result<(), CommandError>) -> i32 {
147 147 match result {
148 148 Ok(()) => exitcode::OK,
149 149 Err(CommandError::Abort { .. }) => exitcode::ABORT,
150 150
151 151 // Exit with a specific code and no error message to let a potential
152 152 // wrapper script fallback to Python-based Mercurial.
153 153 Err(CommandError::UnsupportedFeature { .. }) => {
154 154 exitcode::UNIMPLEMENTED
155 155 }
156 156 }
157 157 }
158 158
159 159 fn exit(
160 160 ui: &Ui,
161 161 mut on_unsupported: OnUnsupported,
162 162 result: Result<(), CommandError>,
163 163 ) -> ! {
164 164 if let (
165 165 OnUnsupported::Fallback { executable },
166 166 Err(CommandError::UnsupportedFeature { .. }),
167 167 ) = (&on_unsupported, &result)
168 168 {
169 169 let mut args = std::env::args_os();
170 170 let executable_path = get_path_from_bytes(&executable);
171 171 let this_executable = args.next().expect("exepcted argv[0] to exist");
172 172 if executable_path == &PathBuf::from(this_executable) {
173 173 // Avoid spawning infinitely many processes until resource
174 174 // exhaustion.
175 175 let _ = ui.write_stderr(&format_bytes!(
176 176 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
177 177 points to `rhg` itself.\n",
178 178 executable
179 179 ));
180 180 on_unsupported = OnUnsupported::Abort
181 181 } else {
182 182 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
183 183 let result = Command::new(executable_path).args(args).status();
184 184 match result {
185 185 Ok(status) => std::process::exit(
186 186 status.code().unwrap_or(exitcode::ABORT),
187 187 ),
188 188 Err(error) => {
189 189 let _ = ui.write_stderr(&format_bytes!(
190 190 b"tried to fall back to a '{}' sub-process but got error {}\n",
191 191 executable, format_bytes::Utf8(error)
192 192 ));
193 193 on_unsupported = OnUnsupported::Abort
194 194 }
195 195 }
196 196 }
197 197 }
198 198 match &result {
199 199 Ok(_) => {}
200 200 Err(CommandError::Abort { message }) => {
201 201 if !message.is_empty() {
202 202 // Ignore errors when writing to stderr, we’re already exiting
203 203 // with failure code so there’s not much more we can do.
204 let _ =
205 ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
204 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
206 205 }
207 206 }
208 207 Err(CommandError::UnsupportedFeature { message }) => {
209 208 match on_unsupported {
210 209 OnUnsupported::Abort => {
211 210 let _ = ui.write_stderr(&format_bytes!(
212 211 b"unsupported feature: {}\n",
213 212 message
214 213 ));
215 214 }
216 215 OnUnsupported::AbortSilent => {}
217 216 OnUnsupported::Fallback { .. } => unreachable!(),
218 217 }
219 218 }
220 219 }
221 220 std::process::exit(exit_code(&result))
222 221 }
223 222
224 223 macro_rules! subcommands {
225 224 ($( $command: ident )+) => {
226 225 mod commands {
227 226 $(
228 227 pub mod $command;
229 228 )+
230 229 }
231 230
232 231 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
233 232 app
234 233 $(
235 234 .subcommand(commands::$command::args())
236 235 )+
237 236 }
238 237
239 238 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
240 239
241 240 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
242 241 match name {
243 242 $(
244 243 stringify!($command) => Some(commands::$command::run),
245 244 )+
246 245 _ => None,
247 246 }
248 247 }
249 248 };
250 249 }
251 250
252 251 subcommands! {
253 252 cat
254 253 debugdata
255 254 debugrequirements
256 255 files
257 256 root
258 257 config
259 258 }
260 259 pub struct CliInvocation<'a> {
261 260 ui: &'a Ui,
262 261 subcommand_args: &'a ArgMatches<'a>,
263 262 config: &'a Config,
264 263 /// References inside `Result` is a bit peculiar but allow
265 264 /// `invocation.repo?` to work out with `&CliInvocation` since this
266 265 /// `Result` type is `Copy`.
267 266 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
268 267 }
269 268
270 269 struct NoRepoInCwdError {
271 270 cwd: PathBuf,
272 271 }
273 272
274 273 /// CLI arguments to be parsed "early" in order to be able to read
275 274 /// configuration before using Clap. Ideally we would also use Clap for this,
276 275 /// see <https://github.com/clap-rs/clap/discussions/2366>.
277 276 ///
278 277 /// These arguments are still declared when we do use Clap later, so that Clap
279 278 /// does not return an error for their presence.
280 279 struct EarlyArgs {
281 280 /// Values of all `--config` arguments. (Possibly none)
282 281 config: Vec<Vec<u8>>,
283 282 /// Value of the `-R` or `--repository` argument, if any.
284 283 repo: Option<Vec<u8>>,
285 284 }
286 285
287 286 impl EarlyArgs {
288 287 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
289 288 let mut args = args.into_iter().map(get_bytes_from_os_str);
290 289 let mut config = Vec::new();
291 290 let mut repo = None;
292 291 // Use `while let` instead of `for` so that we can also call
293 292 // `args.next()` inside the loop.
294 293 while let Some(arg) = args.next() {
295 294 if arg == b"--config" {
296 295 if let Some(value) = args.next() {
297 296 config.push(value)
298 297 }
299 298 } else if let Some(value) = arg.drop_prefix(b"--config=") {
300 299 config.push(value.to_owned())
301 300 }
302 301
303 302 if arg == b"--repository" || arg == b"-R" {
304 303 if let Some(value) = args.next() {
305 304 repo = Some(value)
306 305 }
307 306 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
308 307 repo = Some(value.to_owned())
309 308 } else if let Some(value) = arg.drop_prefix(b"-R") {
310 309 repo = Some(value.to_owned())
311 310 }
312 311 }
313 312 Self { config, repo }
314 313 }
315 314 }
316 315
317 316 /// What to do when encountering some unsupported feature.
318 317 ///
319 318 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
320 319 enum OnUnsupported {
321 320 /// Print an error message describing what feature is not supported,
322 321 /// and exit with code 252.
323 322 Abort,
324 323 /// Silently exit with code 252.
325 324 AbortSilent,
326 325 /// Try running a Python implementation
327 326 Fallback { executable: Vec<u8> },
328 327 }
329 328
330 329 impl OnUnsupported {
331 330 const DEFAULT: Self = OnUnsupported::Abort;
332 331 const DEFAULT_FALLBACK_EXECUTABLE: &'static [u8] = b"hg";
333 332
334 333 fn from_config(config: &Config) -> Self {
335 334 match config
336 335 .get(b"rhg", b"on-unsupported")
337 336 .map(|value| value.to_ascii_lowercase())
338 337 .as_deref()
339 338 {
340 339 Some(b"abort") => OnUnsupported::Abort,
341 340 Some(b"abort-silent") => OnUnsupported::AbortSilent,
342 341 Some(b"fallback") => OnUnsupported::Fallback {
343 342 executable: config
344 343 .get(b"rhg", b"fallback-executable")
345 344 .unwrap_or(Self::DEFAULT_FALLBACK_EXECUTABLE)
346 345 .to_owned(),
347 346 },
348 347 None => Self::DEFAULT,
349 348 Some(_) => {
350 349 // TODO: warn about unknown config value
351 350 Self::DEFAULT
352 351 }
353 352 }
354 353 }
355 354 }
@@ -1,299 +1,299 b''
1 1 #require rhg
2 2
3 3 $ NO_FALLBACK="env RHG_ON_UNSUPPORTED=abort"
4 4
5 5 Unimplemented command
6 6 $ $NO_FALLBACK rhg unimplemented-command
7 7 unsupported feature: error: Found argument 'unimplemented-command' which wasn't expected, or isn't valid in this context
8 8
9 9 USAGE:
10 10 rhg [OPTIONS] <SUBCOMMAND>
11 11
12 12 For more information try --help
13 13
14 14 [252]
15 15 $ rhg unimplemented-command --config rhg.on-unsupported=abort-silent
16 16 [252]
17 17
18 18 Finding root
19 19 $ $NO_FALLBACK rhg root
20 20 abort: no repository found in '$TESTTMP' (.hg not found)!
21 21 [255]
22 22
23 23 $ hg init repository
24 24 $ cd repository
25 25 $ $NO_FALLBACK rhg root
26 26 $TESTTMP/repository
27 27
28 28 Reading and setting configuration
29 29 $ echo "[ui]" >> $HGRCPATH
30 30 $ echo "username = user1" >> $HGRCPATH
31 31 $ $NO_FALLBACK rhg config ui.username
32 32 user1
33 33 $ echo "[ui]" >> .hg/hgrc
34 34 $ echo "username = user2" >> .hg/hgrc
35 35 $ $NO_FALLBACK rhg config ui.username
36 36 user2
37 37 $ $NO_FALLBACK rhg --config ui.username=user3 config ui.username
38 38 user3
39 39
40 40 Unwritable file descriptor
41 41 $ $NO_FALLBACK rhg root > /dev/full
42 42 abort: No space left on device (os error 28)
43 43 [255]
44 44
45 45 Deleted repository
46 46 $ rm -rf `pwd`
47 47 $ $NO_FALLBACK rhg root
48 abort: $ENOENT$: current directory
48 abort: error getting current working directory: $ENOENT$
49 49 [255]
50 50
51 51 Listing tracked files
52 52 $ cd $TESTTMP
53 53 $ hg init repository
54 54 $ cd repository
55 55 $ for i in 1 2 3; do
56 56 > echo $i >> file$i
57 57 > hg add file$i
58 58 > done
59 59 > hg commit -m "commit $i" -q
60 60
61 61 Listing tracked files from root
62 62 $ $NO_FALLBACK rhg files
63 63 file1
64 64 file2
65 65 file3
66 66
67 67 Listing tracked files from subdirectory
68 68 $ mkdir -p path/to/directory
69 69 $ cd path/to/directory
70 70 $ $NO_FALLBACK rhg files
71 71 ../../../file1
72 72 ../../../file2
73 73 ../../../file3
74 74
75 75 Listing tracked files through broken pipe
76 76 $ $NO_FALLBACK rhg files | head -n 1
77 77 ../../../file1
78 78
79 79 Debuging data in inline index
80 80 $ cd $TESTTMP
81 81 $ rm -rf repository
82 82 $ hg init repository
83 83 $ cd repository
84 84 $ for i in 1 2 3 4 5 6; do
85 85 > echo $i >> file-$i
86 86 > hg add file-$i
87 87 > hg commit -m "Commit $i" -q
88 88 > done
89 89 $ $NO_FALLBACK rhg debugdata -c 2
90 90 8d0267cb034247ebfa5ee58ce59e22e57a492297
91 91 test
92 92 0 0
93 93 file-3
94 94
95 95 Commit 3 (no-eol)
96 96 $ $NO_FALLBACK rhg debugdata -m 2
97 97 file-1\x00b8e02f6433738021a065f94175c7cd23db5f05be (esc)
98 98 file-2\x005d9299349fc01ddd25d0070d149b124d8f10411e (esc)
99 99 file-3\x002661d26c649684b482d10f91960cc3db683c38b4 (esc)
100 100
101 101 Debuging with full node id
102 102 $ $NO_FALLBACK rhg debugdata -c `hg log -r 0 -T '{node}'`
103 103 d1d1c679d3053e8926061b6f45ca52009f011e3f
104 104 test
105 105 0 0
106 106 file-1
107 107
108 108 Commit 1 (no-eol)
109 109
110 110 Specifying revisions by changeset ID
111 111 $ hg log -T '{node}\n'
112 112 c6ad58c44207b6ff8a4fbbca7045a5edaa7e908b
113 113 d654274993d0149eecc3cc03214f598320211900
114 114 f646af7e96481d3a5470b695cf30ad8e3ab6c575
115 115 cf8b83f14ead62b374b6e91a0e9303b85dfd9ed7
116 116 91c6f6e73e39318534dc415ea4e8a09c99cd74d6
117 117 6ae9681c6d30389694d8701faf24b583cf3ccafe
118 118 $ $NO_FALLBACK rhg files -r cf8b83
119 119 file-1
120 120 file-2
121 121 file-3
122 122 $ $NO_FALLBACK rhg cat -r cf8b83 file-2
123 123 2
124 124 $ $NO_FALLBACK rhg cat -r c file-2
125 abort: ambiguous revision identifier c
125 abort: ambiguous revision identifier: c
126 126 [255]
127 127 $ $NO_FALLBACK rhg cat -r d file-2
128 128 2
129 129
130 130 Cat files
131 131 $ cd $TESTTMP
132 132 $ rm -rf repository
133 133 $ hg init repository
134 134 $ cd repository
135 135 $ echo "original content" > original
136 136 $ hg add original
137 137 $ hg commit -m "add original" original
138 138 $ $NO_FALLBACK rhg cat -r 0 original
139 139 original content
140 140 Cat copied file should not display copy metadata
141 141 $ hg copy original copy_of_original
142 142 $ hg commit -m "add copy of original"
143 143 $ $NO_FALLBACK rhg cat -r 1 copy_of_original
144 144 original content
145 145
146 146 Fallback to Python
147 147 $ $NO_FALLBACK rhg cat original
148 148 unsupported feature: `rhg cat` without `--rev` / `-r`
149 149 [252]
150 150 $ rhg cat original
151 151 original content
152 152
153 153 $ rhg cat original --config rhg.fallback-executable=false
154 154 [1]
155 155
156 156 $ rhg cat original --config rhg.fallback-executable=hg-non-existent
157 157 tried to fall back to a 'hg-non-existent' sub-process but got error $ENOENT$
158 158 unsupported feature: `rhg cat` without `--rev` / `-r`
159 159 [252]
160 160
161 161 $ rhg cat original --config rhg.fallback-executable=rhg
162 162 Blocking recursive fallback. The 'rhg.fallback-executable = rhg' config points to `rhg` itself.
163 163 unsupported feature: `rhg cat` without `--rev` / `-r`
164 164 [252]
165 165
166 166 Requirements
167 167 $ $NO_FALLBACK rhg debugrequirements
168 168 dotencode
169 169 fncache
170 170 generaldelta
171 171 revlogv1
172 172 sparserevlog
173 173 store
174 174
175 175 $ echo indoor-pool >> .hg/requires
176 176 $ $NO_FALLBACK rhg files
177 177 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
178 178 [252]
179 179
180 180 $ $NO_FALLBACK rhg cat -r 1 copy_of_original
181 181 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
182 182 [252]
183 183
184 184 $ $NO_FALLBACK rhg debugrequirements
185 185 unsupported feature: repository requires feature unknown to this Mercurial: indoor-pool
186 186 [252]
187 187
188 188 $ echo -e '\xFF' >> .hg/requires
189 189 $ $NO_FALLBACK rhg debugrequirements
190 190 abort: corrupted repository: parse error in 'requires' file
191 191 [255]
192 192
193 193 Persistent nodemap
194 194 $ cd $TESTTMP
195 195 $ rm -rf repository
196 196 $ hg init repository
197 197 $ cd repository
198 198 $ $NO_FALLBACK rhg debugrequirements | grep nodemap
199 199 [1]
200 200 $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn"
201 201 $ hg id -r tip
202 202 c3ae8dec9fad tip
203 203 $ ls .hg/store/00changelog*
204 204 .hg/store/00changelog.d
205 205 .hg/store/00changelog.i
206 206 $ $NO_FALLBACK rhg files -r c3ae8dec9fad
207 207 of
208 208
209 209 $ cd $TESTTMP
210 210 $ rm -rf repository
211 211 $ hg --config format.use-persistent-nodemap=True init repository
212 212 $ cd repository
213 213 $ $NO_FALLBACK rhg debugrequirements | grep nodemap
214 214 persistent-nodemap
215 215 $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn"
216 216 $ hg id -r tip
217 217 c3ae8dec9fad tip
218 218 $ ls .hg/store/00changelog*
219 219 .hg/store/00changelog-*.nd (glob)
220 220 .hg/store/00changelog.d
221 221 .hg/store/00changelog.i
222 222 .hg/store/00changelog.n
223 223
224 224 Specifying revisions by changeset ID
225 225 $ $NO_FALLBACK rhg files -r c3ae8dec9fad
226 226 of
227 227 $ $NO_FALLBACK rhg cat -r c3ae8dec9fad of
228 228 r5000
229 229
230 230 Crate a shared repository
231 231
232 232 $ echo "[extensions]" >> $HGRCPATH
233 233 $ echo "share = " >> $HGRCPATH
234 234
235 235 $ cd $TESTTMP
236 236 $ hg init repo1
237 237 $ echo a > repo1/a
238 238 $ hg -R repo1 commit -A -m'init'
239 239 adding a
240 240
241 241 $ hg share repo1 repo2
242 242 updating working directory
243 243 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
244 244
245 245 And check that basic rhg commands work with sharing
246 246
247 247 $ $NO_FALLBACK rhg files -R repo2
248 248 repo2/a
249 249 $ $NO_FALLBACK rhg -R repo2 cat -r 0 repo2/a
250 250 a
251 251
252 252 Same with relative sharing
253 253
254 254 $ hg share repo2 repo3 --relative
255 255 updating working directory
256 256 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
257 257
258 258 $ $NO_FALLBACK rhg files -R repo3
259 259 repo3/a
260 260 $ $NO_FALLBACK rhg -R repo3 cat -r 0 repo3/a
261 261 a
262 262
263 263 Same with share-safe
264 264
265 265 $ echo "[format]" >> $HGRCPATH
266 266 $ echo "use-share-safe = True" >> $HGRCPATH
267 267
268 268 $ cd $TESTTMP
269 269 $ hg init repo4
270 270 $ cd repo4
271 271 $ echo a > a
272 272 $ hg commit -A -m'init'
273 273 adding a
274 274
275 275 $ cd ..
276 276 $ hg share repo4 repo5
277 277 updating working directory
278 278 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
279 279
280 280 And check that basic rhg commands work with sharing
281 281
282 282 $ cd repo5
283 283 $ $NO_FALLBACK rhg files
284 284 a
285 285 $ $NO_FALLBACK rhg cat -r 0 a
286 286 a
287 287
288 288 The blackbox extension is supported
289 289
290 290 $ echo "[extensions]" >> $HGRCPATH
291 291 $ echo "blackbox =" >> $HGRCPATH
292 292 $ echo "[blackbox]" >> $HGRCPATH
293 293 $ echo "maxsize = 1" >> $HGRCPATH
294 294 $ $NO_FALLBACK rhg files > /dev/null
295 295 $ cat .hg/blackbox.log
296 296 ????/??/?? ??:??:??.??? * @d3873e73d99ef67873dac33fbcc66268d5d2b6f4 (*)> (rust) files exited 0 after 0.??? seconds (glob)
297 297 $ cat .hg/blackbox.log.1
298 298 ????/??/?? ??:??:??.??? * @d3873e73d99ef67873dac33fbcc66268d5d2b6f4 (*)> (rust) files (glob)
299 299
General Comments 0
You need to be logged in to leave comments. Login now