Show More
@@ -1,298 +1,291 | |||||
1 | // layer.rs |
|
1 | // layer.rs | |
2 | // |
|
2 | // | |
3 | // Copyright 2020 |
|
3 | // Copyright 2020 | |
4 | // Valentin Gatien-Baron, |
|
4 | // Valentin Gatien-Baron, | |
5 | // Raphaël Gomès <rgomes@octobus.net> |
|
5 | // Raphaël Gomès <rgomes@octobus.net> | |
6 | // |
|
6 | // | |
7 | // This software may be used and distributed according to the terms of the |
|
7 | // This software may be used and distributed according to the terms of the | |
8 | // GNU General Public License version 2 or any later version. |
|
8 | // GNU General Public License version 2 or any later version. | |
9 |
|
9 | |||
10 | use crate::errors::{HgError, IoResultExt}; |
|
10 | use crate::errors::{HgError, IoResultExt}; | |
11 | use crate::utils::files::{get_bytes_from_path, get_path_from_bytes}; |
|
11 | use crate::utils::files::{get_bytes_from_path, get_path_from_bytes}; | |
12 | use format_bytes::{write_bytes, DisplayBytes}; |
|
12 | use format_bytes::{write_bytes, DisplayBytes}; | |
13 | use lazy_static::lazy_static; |
|
13 | use lazy_static::lazy_static; | |
14 | use regex::bytes::Regex; |
|
14 | use regex::bytes::Regex; | |
15 | use std::collections::HashMap; |
|
15 | use std::collections::HashMap; | |
16 | use std::path::{Path, PathBuf}; |
|
16 | use std::path::{Path, PathBuf}; | |
17 |
|
17 | |||
18 | lazy_static! { |
|
18 | lazy_static! { | |
19 | static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]"); |
|
19 | static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]"); | |
20 | static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)"); |
|
20 | static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)"); | |
21 | /// Continuation whitespace |
|
21 | /// Continuation whitespace | |
22 | static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$"); |
|
22 | static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$"); | |
23 | static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)"); |
|
23 | static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)"); | |
24 | static ref COMMENT_RE: Regex = make_regex(r"^(;|#)"); |
|
24 | static ref COMMENT_RE: Regex = make_regex(r"^(;|#)"); | |
25 | /// A directive that allows for removing previous entries |
|
25 | /// A directive that allows for removing previous entries | |
26 | static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)"); |
|
26 | static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)"); | |
27 | /// A directive that allows for including other config files |
|
27 | /// A directive that allows for including other config files | |
28 | static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$"); |
|
28 | static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$"); | |
29 | } |
|
29 | } | |
30 |
|
30 | |||
31 | /// All config values separated by layers of precedence. |
|
31 | /// All config values separated by layers of precedence. | |
32 | /// Each config source may be split in multiple layers if `%include` directives |
|
32 | /// Each config source may be split in multiple layers if `%include` directives | |
33 | /// are used. |
|
33 | /// are used. | |
34 | /// TODO detail the general precedence |
|
34 | /// TODO detail the general precedence | |
35 | #[derive(Clone)] |
|
35 | #[derive(Clone)] | |
36 | pub struct ConfigLayer { |
|
36 | pub struct ConfigLayer { | |
37 | /// Mapping of the sections to their items |
|
37 | /// Mapping of the sections to their items | |
38 | sections: HashMap<Vec<u8>, ConfigItem>, |
|
38 | sections: HashMap<Vec<u8>, ConfigItem>, | |
39 | /// All sections (and their items/values) in a layer share the same origin |
|
39 | /// All sections (and their items/values) in a layer share the same origin | |
40 | pub origin: ConfigOrigin, |
|
40 | pub origin: ConfigOrigin, | |
41 | /// Whether this layer comes from a trusted user or group |
|
41 | /// Whether this layer comes from a trusted user or group | |
42 | pub trusted: bool, |
|
42 | pub trusted: bool, | |
43 | } |
|
43 | } | |
44 |
|
44 | |||
45 | impl ConfigLayer { |
|
45 | impl ConfigLayer { | |
46 | pub fn new(origin: ConfigOrigin) -> Self { |
|
46 | pub fn new(origin: ConfigOrigin) -> Self { | |
47 | ConfigLayer { |
|
47 | ConfigLayer { | |
48 | sections: HashMap::new(), |
|
48 | sections: HashMap::new(), | |
49 | trusted: true, // TODO check |
|
49 | trusted: true, // TODO check | |
50 | origin, |
|
50 | origin, | |
51 | } |
|
51 | } | |
52 | } |
|
52 | } | |
53 |
|
53 | |||
54 | /// Parse `--config` CLI arguments and return a layer if thereβs any |
|
54 | /// Parse `--config` CLI arguments and return a layer if thereβs any | |
55 | pub(crate) fn parse_cli_args( |
|
55 | pub(crate) fn parse_cli_args( | |
56 | cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>, |
|
56 | cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>, | |
57 | ) -> Result<Option<Self>, ConfigError> { |
|
57 | ) -> Result<Option<Self>, ConfigError> { | |
58 | fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> { |
|
58 | fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> { | |
59 | use crate::utils::SliceExt; |
|
59 | use crate::utils::SliceExt; | |
60 |
|
60 | |||
61 |
let (section_and_item, value) = split_2( |
|
61 | let (section_and_item, value) = arg.split_2(b'=')?; | |
62 |
let (section, item) = |
|
62 | let (section, item) = section_and_item.trim().split_2(b'.')?; | |
63 | Some(( |
|
63 | Some(( | |
64 | section.to_owned(), |
|
64 | section.to_owned(), | |
65 | item.to_owned(), |
|
65 | item.to_owned(), | |
66 | value.trim().to_owned(), |
|
66 | value.trim().to_owned(), | |
67 | )) |
|
67 | )) | |
68 | } |
|
68 | } | |
69 |
|
69 | |||
70 | fn split_2(bytes: &[u8], separator: u8) -> Option<(&[u8], &[u8])> { |
|
|||
71 | let mut iter = bytes.splitn(2, |&byte| byte == separator); |
|
|||
72 | let a = iter.next()?; |
|
|||
73 | let b = iter.next()?; |
|
|||
74 | Some((a, b)) |
|
|||
75 | } |
|
|||
76 |
|
||||
77 | let mut layer = Self::new(ConfigOrigin::CommandLine); |
|
70 | let mut layer = Self::new(ConfigOrigin::CommandLine); | |
78 | for arg in cli_config_args { |
|
71 | for arg in cli_config_args { | |
79 | let arg = arg.as_ref(); |
|
72 | let arg = arg.as_ref(); | |
80 | if let Some((section, item, value)) = parse_one(arg) { |
|
73 | if let Some((section, item, value)) = parse_one(arg) { | |
81 | layer.add(section, item, value, None); |
|
74 | layer.add(section, item, value, None); | |
82 | } else { |
|
75 | } else { | |
83 | Err(HgError::abort(format!( |
|
76 | Err(HgError::abort(format!( | |
84 | "malformed --config option: \"{}\" \ |
|
77 | "malformed --config option: \"{}\" \ | |
85 | (use --config section.name=value)", |
|
78 | (use --config section.name=value)", | |
86 | String::from_utf8_lossy(arg), |
|
79 | String::from_utf8_lossy(arg), | |
87 | )))? |
|
80 | )))? | |
88 | } |
|
81 | } | |
89 | } |
|
82 | } | |
90 | if layer.sections.is_empty() { |
|
83 | if layer.sections.is_empty() { | |
91 | Ok(None) |
|
84 | Ok(None) | |
92 | } else { |
|
85 | } else { | |
93 | Ok(Some(layer)) |
|
86 | Ok(Some(layer)) | |
94 | } |
|
87 | } | |
95 | } |
|
88 | } | |
96 |
|
89 | |||
97 | /// Returns whether this layer comes from `--config` CLI arguments |
|
90 | /// Returns whether this layer comes from `--config` CLI arguments | |
98 | pub(crate) fn is_from_command_line(&self) -> bool { |
|
91 | pub(crate) fn is_from_command_line(&self) -> bool { | |
99 | if let ConfigOrigin::CommandLine = self.origin { |
|
92 | if let ConfigOrigin::CommandLine = self.origin { | |
100 | true |
|
93 | true | |
101 | } else { |
|
94 | } else { | |
102 | false |
|
95 | false | |
103 | } |
|
96 | } | |
104 | } |
|
97 | } | |
105 |
|
98 | |||
106 | /// Add an entry to the config, overwriting the old one if already present. |
|
99 | /// Add an entry to the config, overwriting the old one if already present. | |
107 | pub fn add( |
|
100 | pub fn add( | |
108 | &mut self, |
|
101 | &mut self, | |
109 | section: Vec<u8>, |
|
102 | section: Vec<u8>, | |
110 | item: Vec<u8>, |
|
103 | item: Vec<u8>, | |
111 | value: Vec<u8>, |
|
104 | value: Vec<u8>, | |
112 | line: Option<usize>, |
|
105 | line: Option<usize>, | |
113 | ) { |
|
106 | ) { | |
114 | self.sections |
|
107 | self.sections | |
115 | .entry(section) |
|
108 | .entry(section) | |
116 | .or_insert_with(|| HashMap::new()) |
|
109 | .or_insert_with(|| HashMap::new()) | |
117 | .insert(item, ConfigValue { bytes: value, line }); |
|
110 | .insert(item, ConfigValue { bytes: value, line }); | |
118 | } |
|
111 | } | |
119 |
|
112 | |||
120 | /// Returns the config value in `<section>.<item>` if it exists |
|
113 | /// Returns the config value in `<section>.<item>` if it exists | |
121 | pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> { |
|
114 | pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> { | |
122 | Some(self.sections.get(section)?.get(item)?) |
|
115 | Some(self.sections.get(section)?.get(item)?) | |
123 | } |
|
116 | } | |
124 |
|
117 | |||
125 | pub fn is_empty(&self) -> bool { |
|
118 | pub fn is_empty(&self) -> bool { | |
126 | self.sections.is_empty() |
|
119 | self.sections.is_empty() | |
127 | } |
|
120 | } | |
128 |
|
121 | |||
129 | /// Returns a `Vec` of layers in order of precedence (so, in read order), |
|
122 | /// Returns a `Vec` of layers in order of precedence (so, in read order), | |
130 | /// recursively parsing the `%include` directives if any. |
|
123 | /// recursively parsing the `%include` directives if any. | |
131 | pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> { |
|
124 | pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> { | |
132 | let mut layers = vec![]; |
|
125 | let mut layers = vec![]; | |
133 |
|
126 | |||
134 | // Discard byte order mark if any |
|
127 | // Discard byte order mark if any | |
135 | let data = if data.starts_with(b"\xef\xbb\xbf") { |
|
128 | let data = if data.starts_with(b"\xef\xbb\xbf") { | |
136 | &data[3..] |
|
129 | &data[3..] | |
137 | } else { |
|
130 | } else { | |
138 | data |
|
131 | data | |
139 | }; |
|
132 | }; | |
140 |
|
133 | |||
141 | // TODO check if it's trusted |
|
134 | // TODO check if it's trusted | |
142 | let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned())); |
|
135 | let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned())); | |
143 |
|
136 | |||
144 | let mut lines_iter = |
|
137 | let mut lines_iter = | |
145 | data.split(|b| *b == b'\n').enumerate().peekable(); |
|
138 | data.split(|b| *b == b'\n').enumerate().peekable(); | |
146 | let mut section = b"".to_vec(); |
|
139 | let mut section = b"".to_vec(); | |
147 |
|
140 | |||
148 | while let Some((index, bytes)) = lines_iter.next() { |
|
141 | while let Some((index, bytes)) = lines_iter.next() { | |
149 | if let Some(m) = INCLUDE_RE.captures(&bytes) { |
|
142 | if let Some(m) = INCLUDE_RE.captures(&bytes) { | |
150 | let filename_bytes = &m[1]; |
|
143 | let filename_bytes = &m[1]; | |
151 | // `Path::parent` only fails for the root directory, |
|
144 | // `Path::parent` only fails for the root directory, | |
152 | // which `src` canβt be since weβve managed to open it as a |
|
145 | // which `src` canβt be since weβve managed to open it as a | |
153 | // file. |
|
146 | // file. | |
154 | let dir = src |
|
147 | let dir = src | |
155 | .parent() |
|
148 | .parent() | |
156 | .expect("Path::parent fail on a file weβve read"); |
|
149 | .expect("Path::parent fail on a file weβve read"); | |
157 | // `Path::join` with an absolute argument correctly ignores the |
|
150 | // `Path::join` with an absolute argument correctly ignores the | |
158 | // base path |
|
151 | // base path | |
159 | let filename = dir.join(&get_path_from_bytes(&filename_bytes)); |
|
152 | let filename = dir.join(&get_path_from_bytes(&filename_bytes)); | |
160 | let data = std::fs::read(&filename).for_file(&filename)?; |
|
153 | let data = std::fs::read(&filename).for_file(&filename)?; | |
161 | layers.push(current_layer); |
|
154 | layers.push(current_layer); | |
162 | layers.extend(Self::parse(&filename, &data)?); |
|
155 | layers.extend(Self::parse(&filename, &data)?); | |
163 | current_layer = Self::new(ConfigOrigin::File(src.to_owned())); |
|
156 | current_layer = Self::new(ConfigOrigin::File(src.to_owned())); | |
164 | } else if let Some(_) = EMPTY_RE.captures(&bytes) { |
|
157 | } else if let Some(_) = EMPTY_RE.captures(&bytes) { | |
165 | } else if let Some(m) = SECTION_RE.captures(&bytes) { |
|
158 | } else if let Some(m) = SECTION_RE.captures(&bytes) { | |
166 | section = m[1].to_vec(); |
|
159 | section = m[1].to_vec(); | |
167 | } else if let Some(m) = ITEM_RE.captures(&bytes) { |
|
160 | } else if let Some(m) = ITEM_RE.captures(&bytes) { | |
168 | let item = m[1].to_vec(); |
|
161 | let item = m[1].to_vec(); | |
169 | let mut value = m[2].to_vec(); |
|
162 | let mut value = m[2].to_vec(); | |
170 | loop { |
|
163 | loop { | |
171 | match lines_iter.peek() { |
|
164 | match lines_iter.peek() { | |
172 | None => break, |
|
165 | None => break, | |
173 | Some((_, v)) => { |
|
166 | Some((_, v)) => { | |
174 | if let Some(_) = COMMENT_RE.captures(&v) { |
|
167 | if let Some(_) = COMMENT_RE.captures(&v) { | |
175 | } else if let Some(_) = CONT_RE.captures(&v) { |
|
168 | } else if let Some(_) = CONT_RE.captures(&v) { | |
176 | value.extend(b"\n"); |
|
169 | value.extend(b"\n"); | |
177 | value.extend(&m[1]); |
|
170 | value.extend(&m[1]); | |
178 | } else { |
|
171 | } else { | |
179 | break; |
|
172 | break; | |
180 | } |
|
173 | } | |
181 | } |
|
174 | } | |
182 | }; |
|
175 | }; | |
183 | lines_iter.next(); |
|
176 | lines_iter.next(); | |
184 | } |
|
177 | } | |
185 | current_layer.add( |
|
178 | current_layer.add( | |
186 | section.clone(), |
|
179 | section.clone(), | |
187 | item, |
|
180 | item, | |
188 | value, |
|
181 | value, | |
189 | Some(index + 1), |
|
182 | Some(index + 1), | |
190 | ); |
|
183 | ); | |
191 | } else if let Some(m) = UNSET_RE.captures(&bytes) { |
|
184 | } else if let Some(m) = UNSET_RE.captures(&bytes) { | |
192 | if let Some(map) = current_layer.sections.get_mut(§ion) { |
|
185 | if let Some(map) = current_layer.sections.get_mut(§ion) { | |
193 | map.remove(&m[1]); |
|
186 | map.remove(&m[1]); | |
194 | } |
|
187 | } | |
195 | } else { |
|
188 | } else { | |
196 | return Err(ConfigParseError { |
|
189 | return Err(ConfigParseError { | |
197 | origin: ConfigOrigin::File(src.to_owned()), |
|
190 | origin: ConfigOrigin::File(src.to_owned()), | |
198 | line: Some(index + 1), |
|
191 | line: Some(index + 1), | |
199 | bytes: bytes.to_owned(), |
|
192 | bytes: bytes.to_owned(), | |
200 | } |
|
193 | } | |
201 | .into()); |
|
194 | .into()); | |
202 | } |
|
195 | } | |
203 | } |
|
196 | } | |
204 | if !current_layer.is_empty() { |
|
197 | if !current_layer.is_empty() { | |
205 | layers.push(current_layer); |
|
198 | layers.push(current_layer); | |
206 | } |
|
199 | } | |
207 | Ok(layers) |
|
200 | Ok(layers) | |
208 | } |
|
201 | } | |
209 | } |
|
202 | } | |
210 |
|
203 | |||
211 | impl DisplayBytes for ConfigLayer { |
|
204 | impl DisplayBytes for ConfigLayer { | |
212 | fn display_bytes( |
|
205 | fn display_bytes( | |
213 | &self, |
|
206 | &self, | |
214 | out: &mut dyn std::io::Write, |
|
207 | out: &mut dyn std::io::Write, | |
215 | ) -> std::io::Result<()> { |
|
208 | ) -> std::io::Result<()> { | |
216 | let mut sections: Vec<_> = self.sections.iter().collect(); |
|
209 | let mut sections: Vec<_> = self.sections.iter().collect(); | |
217 | sections.sort_by(|e0, e1| e0.0.cmp(e1.0)); |
|
210 | sections.sort_by(|e0, e1| e0.0.cmp(e1.0)); | |
218 |
|
211 | |||
219 | for (section, items) in sections.into_iter() { |
|
212 | for (section, items) in sections.into_iter() { | |
220 | let mut items: Vec<_> = items.into_iter().collect(); |
|
213 | let mut items: Vec<_> = items.into_iter().collect(); | |
221 | items.sort_by(|e0, e1| e0.0.cmp(e1.0)); |
|
214 | items.sort_by(|e0, e1| e0.0.cmp(e1.0)); | |
222 |
|
215 | |||
223 | for (item, config_entry) in items { |
|
216 | for (item, config_entry) in items { | |
224 | write_bytes!( |
|
217 | write_bytes!( | |
225 | out, |
|
218 | out, | |
226 | b"{}.{}={} # {}\n", |
|
219 | b"{}.{}={} # {}\n", | |
227 | section, |
|
220 | section, | |
228 | item, |
|
221 | item, | |
229 | &config_entry.bytes, |
|
222 | &config_entry.bytes, | |
230 | &self.origin, |
|
223 | &self.origin, | |
231 | )? |
|
224 | )? | |
232 | } |
|
225 | } | |
233 | } |
|
226 | } | |
234 | Ok(()) |
|
227 | Ok(()) | |
235 | } |
|
228 | } | |
236 | } |
|
229 | } | |
237 |
|
230 | |||
238 | /// Mapping of section item to value. |
|
231 | /// Mapping of section item to value. | |
239 | /// In the following: |
|
232 | /// In the following: | |
240 | /// ```text |
|
233 | /// ```text | |
241 | /// [ui] |
|
234 | /// [ui] | |
242 | /// paginate=no |
|
235 | /// paginate=no | |
243 | /// ``` |
|
236 | /// ``` | |
244 | /// "paginate" is the section item and "no" the value. |
|
237 | /// "paginate" is the section item and "no" the value. | |
245 | pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>; |
|
238 | pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>; | |
246 |
|
239 | |||
247 | #[derive(Clone, Debug, PartialEq)] |
|
240 | #[derive(Clone, Debug, PartialEq)] | |
248 | pub struct ConfigValue { |
|
241 | pub struct ConfigValue { | |
249 | /// The raw bytes of the value (be it from the CLI, env or from a file) |
|
242 | /// The raw bytes of the value (be it from the CLI, env or from a file) | |
250 | pub bytes: Vec<u8>, |
|
243 | pub bytes: Vec<u8>, | |
251 | /// Only present if the value comes from a file, 1-indexed. |
|
244 | /// Only present if the value comes from a file, 1-indexed. | |
252 | pub line: Option<usize>, |
|
245 | pub line: Option<usize>, | |
253 | } |
|
246 | } | |
254 |
|
247 | |||
255 | #[derive(Clone, Debug)] |
|
248 | #[derive(Clone, Debug)] | |
256 | pub enum ConfigOrigin { |
|
249 | pub enum ConfigOrigin { | |
257 | /// From a configuration file |
|
250 | /// From a configuration file | |
258 | File(PathBuf), |
|
251 | File(PathBuf), | |
259 | /// From a `--config` CLI argument |
|
252 | /// From a `--config` CLI argument | |
260 | CommandLine, |
|
253 | CommandLine, | |
261 | /// From environment variables like `$PAGER` or `$EDITOR` |
|
254 | /// From environment variables like `$PAGER` or `$EDITOR` | |
262 | Environment(Vec<u8>), |
|
255 | Environment(Vec<u8>), | |
263 | /* TODO cli |
|
256 | /* TODO cli | |
264 | * TODO defaults (configitems.py) |
|
257 | * TODO defaults (configitems.py) | |
265 | * TODO extensions |
|
258 | * TODO extensions | |
266 | * TODO Python resources? |
|
259 | * TODO Python resources? | |
267 | * Others? */ |
|
260 | * Others? */ | |
268 | } |
|
261 | } | |
269 |
|
262 | |||
270 | impl DisplayBytes for ConfigOrigin { |
|
263 | impl DisplayBytes for ConfigOrigin { | |
271 | fn display_bytes( |
|
264 | fn display_bytes( | |
272 | &self, |
|
265 | &self, | |
273 | out: &mut dyn std::io::Write, |
|
266 | out: &mut dyn std::io::Write, | |
274 | ) -> std::io::Result<()> { |
|
267 | ) -> std::io::Result<()> { | |
275 | match self { |
|
268 | match self { | |
276 | ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)), |
|
269 | ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)), | |
277 | ConfigOrigin::CommandLine => out.write_all(b"--config"), |
|
270 | ConfigOrigin::CommandLine => out.write_all(b"--config"), | |
278 | ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e), |
|
271 | ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e), | |
279 | } |
|
272 | } | |
280 | } |
|
273 | } | |
281 | } |
|
274 | } | |
282 |
|
275 | |||
283 | #[derive(Debug)] |
|
276 | #[derive(Debug)] | |
284 | pub struct ConfigParseError { |
|
277 | pub struct ConfigParseError { | |
285 | pub origin: ConfigOrigin, |
|
278 | pub origin: ConfigOrigin, | |
286 | pub line: Option<usize>, |
|
279 | pub line: Option<usize>, | |
287 | pub bytes: Vec<u8>, |
|
280 | pub bytes: Vec<u8>, | |
288 | } |
|
281 | } | |
289 |
|
282 | |||
290 | #[derive(Debug, derive_more::From)] |
|
283 | #[derive(Debug, derive_more::From)] | |
291 | pub enum ConfigError { |
|
284 | pub enum ConfigError { | |
292 | Parse(ConfigParseError), |
|
285 | Parse(ConfigParseError), | |
293 | Other(HgError), |
|
286 | Other(HgError), | |
294 | } |
|
287 | } | |
295 |
|
288 | |||
296 | fn make_regex(pattern: &'static str) -> Regex { |
|
289 | fn make_regex(pattern: &'static str) -> Regex { | |
297 | Regex::new(pattern).expect("expected a valid regex") |
|
290 | Regex::new(pattern).expect("expected a valid regex") | |
298 | } |
|
291 | } |
@@ -1,238 +1,264 | |||||
1 | use crate::config::{Config, ConfigError, ConfigParseError}; |
|
1 | use crate::config::{Config, ConfigError, ConfigParseError}; | |
2 | use crate::errors::{HgError, IoResultExt}; |
|
2 | use crate::errors::{HgError, IoResultExt}; | |
3 | use crate::requirements; |
|
3 | use crate::requirements; | |
4 | use crate::utils::current_dir; |
|
4 | use crate::utils::current_dir; | |
5 | use crate::utils::files::get_path_from_bytes; |
|
5 | use crate::utils::files::get_path_from_bytes; | |
6 | use memmap::{Mmap, MmapOptions}; |
|
6 | use memmap::{Mmap, MmapOptions}; | |
7 | use std::collections::HashSet; |
|
7 | use std::collections::HashSet; | |
8 | use std::path::{Path, PathBuf}; |
|
8 | use std::path::{Path, PathBuf}; | |
9 |
|
9 | |||
10 | /// A repository on disk |
|
10 | /// A repository on disk | |
11 | pub struct Repo { |
|
11 | pub struct Repo { | |
12 | working_directory: PathBuf, |
|
12 | working_directory: PathBuf, | |
13 | dot_hg: PathBuf, |
|
13 | dot_hg: PathBuf, | |
14 | store: PathBuf, |
|
14 | store: PathBuf, | |
15 | requirements: HashSet<String>, |
|
15 | requirements: HashSet<String>, | |
16 | config: Config, |
|
16 | config: Config, | |
17 | } |
|
17 | } | |
18 |
|
18 | |||
19 | #[derive(Debug, derive_more::From)] |
|
19 | #[derive(Debug, derive_more::From)] | |
20 | pub enum RepoError { |
|
20 | pub enum RepoError { | |
21 | NotFound { |
|
21 | NotFound { | |
22 | at: PathBuf, |
|
22 | at: PathBuf, | |
23 | }, |
|
23 | }, | |
24 | #[from] |
|
24 | #[from] | |
25 | ConfigParseError(ConfigParseError), |
|
25 | ConfigParseError(ConfigParseError), | |
26 | #[from] |
|
26 | #[from] | |
27 | Other(HgError), |
|
27 | Other(HgError), | |
28 | } |
|
28 | } | |
29 |
|
29 | |||
30 | impl From<ConfigError> for RepoError { |
|
30 | impl From<ConfigError> for RepoError { | |
31 | fn from(error: ConfigError) -> Self { |
|
31 | fn from(error: ConfigError) -> Self { | |
32 | match error { |
|
32 | match error { | |
33 | ConfigError::Parse(error) => error.into(), |
|
33 | ConfigError::Parse(error) => error.into(), | |
34 | ConfigError::Other(error) => error.into(), |
|
34 | ConfigError::Other(error) => error.into(), | |
35 | } |
|
35 | } | |
36 | } |
|
36 | } | |
37 | } |
|
37 | } | |
38 |
|
38 | |||
39 | /// Filesystem access abstraction for the contents of a given "base" diretory |
|
39 | /// Filesystem access abstraction for the contents of a given "base" diretory | |
40 | #[derive(Clone, Copy)] |
|
40 | #[derive(Clone, Copy)] | |
41 | pub(crate) struct Vfs<'a> { |
|
41 | pub(crate) struct Vfs<'a> { | |
42 | base: &'a Path, |
|
42 | base: &'a Path, | |
43 | } |
|
43 | } | |
44 |
|
44 | |||
45 | impl Repo { |
|
45 | impl Repo { | |
46 | /// Search the current directory and its ancestores for a repository: |
|
46 | /// Find a repository, either at the given path (which must contain a `.hg` | |
47 | /// a working directory that contains a `.hg` sub-directory. |
|
47 | /// sub-directory) or by searching the current directory and its | |
|
48 | /// ancestors. | |||
48 | /// |
|
49 | /// | |
49 | /// `explicit_path` is for `--repository` command-line arguments. |
|
50 | /// A method with two very different "modes" like this usually a code smell | |
|
51 | /// to make two methods instead, but in this case an `Option` is what rhg | |||
|
52 | /// sub-commands get from Clap for the `-R` / `--repository` CLI argument. | |||
|
53 | /// Having two methods would just move that `if` to almost all callers. | |||
50 | pub fn find( |
|
54 | pub fn find( | |
51 | config: &Config, |
|
55 | config: &Config, | |
52 | explicit_path: Option<&Path>, |
|
56 | explicit_path: Option<&Path>, | |
53 | ) -> Result<Self, RepoError> { |
|
57 | ) -> Result<Self, RepoError> { | |
54 | if let Some(root) = explicit_path { |
|
58 | if let Some(root) = explicit_path { | |
55 | // Having an absolute path isnβt necessary here but can help code |
|
59 | // Having an absolute path isnβt necessary here but can help code | |
56 | // elsewhere |
|
60 | // elsewhere | |
57 | let root = current_dir()?.join(root); |
|
61 | let root = current_dir()?.join(root); | |
58 | if root.join(".hg").is_dir() { |
|
62 | if root.join(".hg").is_dir() { | |
59 | Self::new_at_path(root, config) |
|
63 | Self::new_at_path(root, config) | |
60 | } else { |
|
64 | } else { | |
61 | Err(RepoError::NotFound { |
|
65 | Err(RepoError::NotFound { | |
62 | at: root.to_owned(), |
|
66 | at: root.to_owned(), | |
63 | }) |
|
67 | }) | |
64 | } |
|
68 | } | |
65 | } else { |
|
69 | } else { | |
66 | let current_directory = crate::utils::current_dir()?; |
|
70 | let current_directory = crate::utils::current_dir()?; | |
67 | // ancestors() is inclusive: it first yields `current_directory` |
|
71 | // ancestors() is inclusive: it first yields `current_directory` | |
68 | // as-is. |
|
72 | // as-is. | |
69 | for ancestor in current_directory.ancestors() { |
|
73 | for ancestor in current_directory.ancestors() { | |
70 | if ancestor.join(".hg").is_dir() { |
|
74 | if ancestor.join(".hg").is_dir() { | |
71 | return Self::new_at_path(ancestor.to_owned(), config); |
|
75 | return Self::new_at_path(ancestor.to_owned(), config); | |
72 | } |
|
76 | } | |
73 | } |
|
77 | } | |
74 | Err(RepoError::NotFound { |
|
78 | Err(RepoError::NotFound { | |
75 | at: current_directory, |
|
79 | at: current_directory, | |
76 | }) |
|
80 | }) | |
77 | } |
|
81 | } | |
78 | } |
|
82 | } | |
79 |
|
83 | |||
|
84 | /// Like `Repo::find`, but not finding a repository is not an error if no | |||
|
85 | /// explicit path is given. `Ok(None)` is returned in that case. | |||
|
86 | /// | |||
|
87 | /// If an explicit path *is* given, not finding a repository there is still | |||
|
88 | /// an error. | |||
|
89 | /// | |||
|
90 | /// For sub-commands that donβt need a repository, configuration should | |||
|
91 | /// still be affected by a repositoryβs `.hg/hgrc` file. This is the | |||
|
92 | /// constructor to use. | |||
|
93 | pub fn find_optional( | |||
|
94 | config: &Config, | |||
|
95 | explicit_path: Option<&Path>, | |||
|
96 | ) -> Result<Option<Self>, RepoError> { | |||
|
97 | match Self::find(config, explicit_path) { | |||
|
98 | Ok(repo) => Ok(Some(repo)), | |||
|
99 | Err(RepoError::NotFound { .. }) if explicit_path.is_none() => { | |||
|
100 | Ok(None) | |||
|
101 | } | |||
|
102 | Err(error) => Err(error), | |||
|
103 | } | |||
|
104 | } | |||
|
105 | ||||
80 | /// To be called after checking that `.hg` is a sub-directory |
|
106 | /// To be called after checking that `.hg` is a sub-directory | |
81 | fn new_at_path( |
|
107 | fn new_at_path( | |
82 | working_directory: PathBuf, |
|
108 | working_directory: PathBuf, | |
83 | config: &Config, |
|
109 | config: &Config, | |
84 | ) -> Result<Self, RepoError> { |
|
110 | ) -> Result<Self, RepoError> { | |
85 | let dot_hg = working_directory.join(".hg"); |
|
111 | let dot_hg = working_directory.join(".hg"); | |
86 |
|
112 | |||
87 | let mut repo_config_files = Vec::new(); |
|
113 | let mut repo_config_files = Vec::new(); | |
88 | repo_config_files.push(dot_hg.join("hgrc")); |
|
114 | repo_config_files.push(dot_hg.join("hgrc")); | |
89 | repo_config_files.push(dot_hg.join("hgrc-not-shared")); |
|
115 | repo_config_files.push(dot_hg.join("hgrc-not-shared")); | |
90 |
|
116 | |||
91 | let hg_vfs = Vfs { base: &dot_hg }; |
|
117 | let hg_vfs = Vfs { base: &dot_hg }; | |
92 | let mut reqs = requirements::load_if_exists(hg_vfs)?; |
|
118 | let mut reqs = requirements::load_if_exists(hg_vfs)?; | |
93 | let relative = |
|
119 | let relative = | |
94 | reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT); |
|
120 | reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT); | |
95 | let shared = |
|
121 | let shared = | |
96 | reqs.contains(requirements::SHARED_REQUIREMENT) || relative; |
|
122 | reqs.contains(requirements::SHARED_REQUIREMENT) || relative; | |
97 |
|
123 | |||
98 | // From `mercurial/localrepo.py`: |
|
124 | // From `mercurial/localrepo.py`: | |
99 | // |
|
125 | // | |
100 | // if .hg/requires contains the sharesafe requirement, it means |
|
126 | // if .hg/requires contains the sharesafe requirement, it means | |
101 | // there exists a `.hg/store/requires` too and we should read it |
|
127 | // there exists a `.hg/store/requires` too and we should read it | |
102 | // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement |
|
128 | // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement | |
103 | // is present. We never write SHARESAFE_REQUIREMENT for a repo if store |
|
129 | // is present. We never write SHARESAFE_REQUIREMENT for a repo if store | |
104 | // is not present, refer checkrequirementscompat() for that |
|
130 | // is not present, refer checkrequirementscompat() for that | |
105 | // |
|
131 | // | |
106 | // However, if SHARESAFE_REQUIREMENT is not present, it means that the |
|
132 | // However, if SHARESAFE_REQUIREMENT is not present, it means that the | |
107 | // repository was shared the old way. We check the share source |
|
133 | // repository was shared the old way. We check the share source | |
108 | // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the |
|
134 | // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the | |
109 | // current repository needs to be reshared |
|
135 | // current repository needs to be reshared | |
110 | let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT); |
|
136 | let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT); | |
111 |
|
137 | |||
112 | let store_path; |
|
138 | let store_path; | |
113 | if !shared { |
|
139 | if !shared { | |
114 | store_path = dot_hg.join("store"); |
|
140 | store_path = dot_hg.join("store"); | |
115 | if share_safe { |
|
141 | if share_safe { | |
116 | reqs.extend(requirements::load(Vfs { base: &store_path })?); |
|
142 | reqs.extend(requirements::load(Vfs { base: &store_path })?); | |
117 | } |
|
143 | } | |
118 | } else { |
|
144 | } else { | |
119 | let bytes = hg_vfs.read("sharedpath")?; |
|
145 | let bytes = hg_vfs.read("sharedpath")?; | |
120 | let mut shared_path = get_path_from_bytes(&bytes).to_owned(); |
|
146 | let mut shared_path = get_path_from_bytes(&bytes).to_owned(); | |
121 | if relative { |
|
147 | if relative { | |
122 | shared_path = dot_hg.join(shared_path) |
|
148 | shared_path = dot_hg.join(shared_path) | |
123 | } |
|
149 | } | |
124 | if !shared_path.is_dir() { |
|
150 | if !shared_path.is_dir() { | |
125 | return Err(HgError::corrupted(format!( |
|
151 | return Err(HgError::corrupted(format!( | |
126 | ".hg/sharedpath points to nonexistent directory {}", |
|
152 | ".hg/sharedpath points to nonexistent directory {}", | |
127 | shared_path.display() |
|
153 | shared_path.display() | |
128 | )) |
|
154 | )) | |
129 | .into()); |
|
155 | .into()); | |
130 | } |
|
156 | } | |
131 |
|
157 | |||
132 | store_path = shared_path.join("store"); |
|
158 | store_path = shared_path.join("store"); | |
133 |
|
159 | |||
134 | let source_is_share_safe = |
|
160 | let source_is_share_safe = | |
135 | requirements::load(Vfs { base: &shared_path })? |
|
161 | requirements::load(Vfs { base: &shared_path })? | |
136 | .contains(requirements::SHARESAFE_REQUIREMENT); |
|
162 | .contains(requirements::SHARESAFE_REQUIREMENT); | |
137 |
|
163 | |||
138 | if share_safe && !source_is_share_safe { |
|
164 | if share_safe && !source_is_share_safe { | |
139 | return Err(match config |
|
165 | return Err(match config | |
140 | .get(b"safe-mismatch", b"source-not-safe") |
|
166 | .get(b"safe-mismatch", b"source-not-safe") | |
141 | { |
|
167 | { | |
142 | Some(b"abort") | None => HgError::abort( |
|
168 | Some(b"abort") | None => HgError::abort( | |
143 | "share source does not support share-safe requirement", |
|
169 | "share source does not support share-safe requirement", | |
144 | ), |
|
170 | ), | |
145 | _ => HgError::unsupported("share-safe downgrade"), |
|
171 | _ => HgError::unsupported("share-safe downgrade"), | |
146 | } |
|
172 | } | |
147 | .into()); |
|
173 | .into()); | |
148 | } else if source_is_share_safe && !share_safe { |
|
174 | } else if source_is_share_safe && !share_safe { | |
149 | return Err( |
|
175 | return Err( | |
150 | match config.get(b"safe-mismatch", b"source-safe") { |
|
176 | match config.get(b"safe-mismatch", b"source-safe") { | |
151 | Some(b"abort") | None => HgError::abort( |
|
177 | Some(b"abort") | None => HgError::abort( | |
152 | "version mismatch: source uses share-safe \ |
|
178 | "version mismatch: source uses share-safe \ | |
153 | functionality while the current share does not", |
|
179 | functionality while the current share does not", | |
154 | ), |
|
180 | ), | |
155 | _ => HgError::unsupported("share-safe upgrade"), |
|
181 | _ => HgError::unsupported("share-safe upgrade"), | |
156 | } |
|
182 | } | |
157 | .into(), |
|
183 | .into(), | |
158 | ); |
|
184 | ); | |
159 | } |
|
185 | } | |
160 |
|
186 | |||
161 | if share_safe { |
|
187 | if share_safe { | |
162 | repo_config_files.insert(0, shared_path.join("hgrc")) |
|
188 | repo_config_files.insert(0, shared_path.join("hgrc")) | |
163 | } |
|
189 | } | |
164 | } |
|
190 | } | |
165 |
|
191 | |||
166 | let repo_config = config.combine_with_repo(&repo_config_files)?; |
|
192 | let repo_config = config.combine_with_repo(&repo_config_files)?; | |
167 |
|
193 | |||
168 | let repo = Self { |
|
194 | let repo = Self { | |
169 | requirements: reqs, |
|
195 | requirements: reqs, | |
170 | working_directory, |
|
196 | working_directory, | |
171 | store: store_path, |
|
197 | store: store_path, | |
172 | dot_hg, |
|
198 | dot_hg, | |
173 | config: repo_config, |
|
199 | config: repo_config, | |
174 | }; |
|
200 | }; | |
175 |
|
201 | |||
176 | requirements::check(&repo)?; |
|
202 | requirements::check(&repo)?; | |
177 |
|
203 | |||
178 | Ok(repo) |
|
204 | Ok(repo) | |
179 | } |
|
205 | } | |
180 |
|
206 | |||
181 | pub fn working_directory_path(&self) -> &Path { |
|
207 | pub fn working_directory_path(&self) -> &Path { | |
182 | &self.working_directory |
|
208 | &self.working_directory | |
183 | } |
|
209 | } | |
184 |
|
210 | |||
185 | pub fn requirements(&self) -> &HashSet<String> { |
|
211 | pub fn requirements(&self) -> &HashSet<String> { | |
186 | &self.requirements |
|
212 | &self.requirements | |
187 | } |
|
213 | } | |
188 |
|
214 | |||
189 | pub fn config(&self) -> &Config { |
|
215 | pub fn config(&self) -> &Config { | |
190 | &self.config |
|
216 | &self.config | |
191 | } |
|
217 | } | |
192 |
|
218 | |||
193 | /// For accessing repository files (in `.hg`), except for the store |
|
219 | /// For accessing repository files (in `.hg`), except for the store | |
194 | /// (`.hg/store`). |
|
220 | /// (`.hg/store`). | |
195 | pub(crate) fn hg_vfs(&self) -> Vfs<'_> { |
|
221 | pub(crate) fn hg_vfs(&self) -> Vfs<'_> { | |
196 | Vfs { base: &self.dot_hg } |
|
222 | Vfs { base: &self.dot_hg } | |
197 | } |
|
223 | } | |
198 |
|
224 | |||
199 | /// For accessing repository store files (in `.hg/store`) |
|
225 | /// For accessing repository store files (in `.hg/store`) | |
200 | pub(crate) fn store_vfs(&self) -> Vfs<'_> { |
|
226 | pub(crate) fn store_vfs(&self) -> Vfs<'_> { | |
201 | Vfs { base: &self.store } |
|
227 | Vfs { base: &self.store } | |
202 | } |
|
228 | } | |
203 |
|
229 | |||
204 | /// For accessing the working copy |
|
230 | /// For accessing the working copy | |
205 |
|
231 | |||
206 | // The undescore prefix silences the "never used" warning. Remove before |
|
232 | // The undescore prefix silences the "never used" warning. Remove before | |
207 | // using. |
|
233 | // using. | |
208 | pub(crate) fn _working_directory_vfs(&self) -> Vfs<'_> { |
|
234 | pub(crate) fn _working_directory_vfs(&self) -> Vfs<'_> { | |
209 | Vfs { |
|
235 | Vfs { | |
210 | base: &self.working_directory, |
|
236 | base: &self.working_directory, | |
211 | } |
|
237 | } | |
212 | } |
|
238 | } | |
213 | } |
|
239 | } | |
214 |
|
240 | |||
215 | impl Vfs<'_> { |
|
241 | impl Vfs<'_> { | |
216 | pub(crate) fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { |
|
242 | pub(crate) fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { | |
217 | self.base.join(relative_path) |
|
243 | self.base.join(relative_path) | |
218 | } |
|
244 | } | |
219 |
|
245 | |||
220 | pub(crate) fn read( |
|
246 | pub(crate) fn read( | |
221 | &self, |
|
247 | &self, | |
222 | relative_path: impl AsRef<Path>, |
|
248 | relative_path: impl AsRef<Path>, | |
223 | ) -> Result<Vec<u8>, HgError> { |
|
249 | ) -> Result<Vec<u8>, HgError> { | |
224 | let path = self.join(relative_path); |
|
250 | let path = self.join(relative_path); | |
225 | std::fs::read(&path).for_file(&path) |
|
251 | std::fs::read(&path).for_file(&path) | |
226 | } |
|
252 | } | |
227 |
|
253 | |||
228 | pub(crate) fn mmap_open( |
|
254 | pub(crate) fn mmap_open( | |
229 | &self, |
|
255 | &self, | |
230 | relative_path: impl AsRef<Path>, |
|
256 | relative_path: impl AsRef<Path>, | |
231 | ) -> Result<Mmap, HgError> { |
|
257 | ) -> Result<Mmap, HgError> { | |
232 | let path = self.base.join(relative_path); |
|
258 | let path = self.base.join(relative_path); | |
233 | let file = std::fs::File::open(&path).for_file(&path)?; |
|
259 | let file = std::fs::File::open(&path).for_file(&path)?; | |
234 | // TODO: what are the safety requirements here? |
|
260 | // TODO: what are the safety requirements here? | |
235 | let mmap = unsafe { MmapOptions::new().map(&file) }.for_file(&path)?; |
|
261 | let mmap = unsafe { MmapOptions::new().map(&file) }.for_file(&path)?; | |
236 | Ok(mmap) |
|
262 | Ok(mmap) | |
237 | } |
|
263 | } | |
238 | } |
|
264 | } |
@@ -1,193 +1,201 | |||||
1 | // utils module |
|
1 | // utils module | |
2 | // |
|
2 | // | |
3 | // Copyright 2019 Raphaël Gomès <rgomes@octobus.net> |
|
3 | // Copyright 2019 Raphaël Gomès <rgomes@octobus.net> | |
4 | // |
|
4 | // | |
5 | // This software may be used and distributed according to the terms of the |
|
5 | // This software may be used and distributed according to the terms of the | |
6 | // GNU General Public License version 2 or any later version. |
|
6 | // GNU General Public License version 2 or any later version. | |
7 |
|
7 | |||
8 | //! Contains useful functions, traits, structs, etc. for use in core. |
|
8 | //! Contains useful functions, traits, structs, etc. for use in core. | |
9 |
|
9 | |||
10 | use crate::errors::{HgError, IoErrorContext}; |
|
10 | use crate::errors::{HgError, IoErrorContext}; | |
11 | use crate::utils::hg_path::HgPath; |
|
11 | use crate::utils::hg_path::HgPath; | |
12 | use std::{io::Write, ops::Deref}; |
|
12 | use std::{io::Write, ops::Deref}; | |
13 |
|
13 | |||
14 | pub mod files; |
|
14 | pub mod files; | |
15 | pub mod hg_path; |
|
15 | pub mod hg_path; | |
16 | pub mod path_auditor; |
|
16 | pub mod path_auditor; | |
17 |
|
17 | |||
18 | /// Useful until rust/issues/56345 is stable |
|
18 | /// Useful until rust/issues/56345 is stable | |
19 | /// |
|
19 | /// | |
20 | /// # Examples |
|
20 | /// # Examples | |
21 | /// |
|
21 | /// | |
22 | /// ``` |
|
22 | /// ``` | |
23 | /// use crate::hg::utils::find_slice_in_slice; |
|
23 | /// use crate::hg::utils::find_slice_in_slice; | |
24 | /// |
|
24 | /// | |
25 | /// let haystack = b"This is the haystack".to_vec(); |
|
25 | /// let haystack = b"This is the haystack".to_vec(); | |
26 | /// assert_eq!(find_slice_in_slice(&haystack, b"the"), Some(8)); |
|
26 | /// assert_eq!(find_slice_in_slice(&haystack, b"the"), Some(8)); | |
27 | /// assert_eq!(find_slice_in_slice(&haystack, b"not here"), None); |
|
27 | /// assert_eq!(find_slice_in_slice(&haystack, b"not here"), None); | |
28 | /// ``` |
|
28 | /// ``` | |
29 | pub fn find_slice_in_slice<T>(slice: &[T], needle: &[T]) -> Option<usize> |
|
29 | pub fn find_slice_in_slice<T>(slice: &[T], needle: &[T]) -> Option<usize> | |
30 | where |
|
30 | where | |
31 | for<'a> &'a [T]: PartialEq, |
|
31 | for<'a> &'a [T]: PartialEq, | |
32 | { |
|
32 | { | |
33 | slice |
|
33 | slice | |
34 | .windows(needle.len()) |
|
34 | .windows(needle.len()) | |
35 | .position(|window| window == needle) |
|
35 | .position(|window| window == needle) | |
36 | } |
|
36 | } | |
37 |
|
37 | |||
38 | /// Replaces the `from` slice with the `to` slice inside the `buf` slice. |
|
38 | /// Replaces the `from` slice with the `to` slice inside the `buf` slice. | |
39 | /// |
|
39 | /// | |
40 | /// # Examples |
|
40 | /// # Examples | |
41 | /// |
|
41 | /// | |
42 | /// ``` |
|
42 | /// ``` | |
43 | /// use crate::hg::utils::replace_slice; |
|
43 | /// use crate::hg::utils::replace_slice; | |
44 | /// let mut line = b"I hate writing tests!".to_vec(); |
|
44 | /// let mut line = b"I hate writing tests!".to_vec(); | |
45 | /// replace_slice(&mut line, b"hate", b"love"); |
|
45 | /// replace_slice(&mut line, b"hate", b"love"); | |
46 | /// assert_eq!( |
|
46 | /// assert_eq!( | |
47 | /// line, |
|
47 | /// line, | |
48 | /// b"I love writing tests!".to_vec() |
|
48 | /// b"I love writing tests!".to_vec() | |
49 | /// ); |
|
49 | /// ); | |
50 | /// ``` |
|
50 | /// ``` | |
51 | pub fn replace_slice<T>(buf: &mut [T], from: &[T], to: &[T]) |
|
51 | pub fn replace_slice<T>(buf: &mut [T], from: &[T], to: &[T]) | |
52 | where |
|
52 | where | |
53 | T: Clone + PartialEq, |
|
53 | T: Clone + PartialEq, | |
54 | { |
|
54 | { | |
55 | if buf.len() < from.len() || from.len() != to.len() { |
|
55 | if buf.len() < from.len() || from.len() != to.len() { | |
56 | return; |
|
56 | return; | |
57 | } |
|
57 | } | |
58 | for i in 0..=buf.len() - from.len() { |
|
58 | for i in 0..=buf.len() - from.len() { | |
59 | if buf[i..].starts_with(from) { |
|
59 | if buf[i..].starts_with(from) { | |
60 | buf[i..(i + from.len())].clone_from_slice(to); |
|
60 | buf[i..(i + from.len())].clone_from_slice(to); | |
61 | } |
|
61 | } | |
62 | } |
|
62 | } | |
63 | } |
|
63 | } | |
64 |
|
64 | |||
65 | pub trait SliceExt { |
|
65 | pub trait SliceExt { | |
66 | fn trim_end(&self) -> &Self; |
|
66 | fn trim_end(&self) -> &Self; | |
67 | fn trim_start(&self) -> &Self; |
|
67 | fn trim_start(&self) -> &Self; | |
68 | fn trim(&self) -> &Self; |
|
68 | fn trim(&self) -> &Self; | |
69 | fn drop_prefix(&self, needle: &Self) -> Option<&Self>; |
|
69 | fn drop_prefix(&self, needle: &Self) -> Option<&Self>; | |
|
70 | fn split_2(&self, separator: u8) -> Option<(&[u8], &[u8])>; | |||
70 | } |
|
71 | } | |
71 |
|
72 | |||
72 | #[allow(clippy::trivially_copy_pass_by_ref)] |
|
73 | #[allow(clippy::trivially_copy_pass_by_ref)] | |
73 | fn is_not_whitespace(c: &u8) -> bool { |
|
74 | fn is_not_whitespace(c: &u8) -> bool { | |
74 | !(*c as char).is_whitespace() |
|
75 | !(*c as char).is_whitespace() | |
75 | } |
|
76 | } | |
76 |
|
77 | |||
77 | impl SliceExt for [u8] { |
|
78 | impl SliceExt for [u8] { | |
78 | fn trim_end(&self) -> &[u8] { |
|
79 | fn trim_end(&self) -> &[u8] { | |
79 | if let Some(last) = self.iter().rposition(is_not_whitespace) { |
|
80 | if let Some(last) = self.iter().rposition(is_not_whitespace) { | |
80 | &self[..=last] |
|
81 | &self[..=last] | |
81 | } else { |
|
82 | } else { | |
82 | &[] |
|
83 | &[] | |
83 | } |
|
84 | } | |
84 | } |
|
85 | } | |
85 | fn trim_start(&self) -> &[u8] { |
|
86 | fn trim_start(&self) -> &[u8] { | |
86 | if let Some(first) = self.iter().position(is_not_whitespace) { |
|
87 | if let Some(first) = self.iter().position(is_not_whitespace) { | |
87 | &self[first..] |
|
88 | &self[first..] | |
88 | } else { |
|
89 | } else { | |
89 | &[] |
|
90 | &[] | |
90 | } |
|
91 | } | |
91 | } |
|
92 | } | |
92 |
|
93 | |||
93 | /// ``` |
|
94 | /// ``` | |
94 | /// use hg::utils::SliceExt; |
|
95 | /// use hg::utils::SliceExt; | |
95 | /// assert_eq!( |
|
96 | /// assert_eq!( | |
96 | /// b" to trim ".trim(), |
|
97 | /// b" to trim ".trim(), | |
97 | /// b"to trim" |
|
98 | /// b"to trim" | |
98 | /// ); |
|
99 | /// ); | |
99 | /// assert_eq!( |
|
100 | /// assert_eq!( | |
100 | /// b"to trim ".trim(), |
|
101 | /// b"to trim ".trim(), | |
101 | /// b"to trim" |
|
102 | /// b"to trim" | |
102 | /// ); |
|
103 | /// ); | |
103 | /// assert_eq!( |
|
104 | /// assert_eq!( | |
104 | /// b" to trim".trim(), |
|
105 | /// b" to trim".trim(), | |
105 | /// b"to trim" |
|
106 | /// b"to trim" | |
106 | /// ); |
|
107 | /// ); | |
107 | /// ``` |
|
108 | /// ``` | |
108 | fn trim(&self) -> &[u8] { |
|
109 | fn trim(&self) -> &[u8] { | |
109 | self.trim_start().trim_end() |
|
110 | self.trim_start().trim_end() | |
110 | } |
|
111 | } | |
111 |
|
112 | |||
112 | fn drop_prefix(&self, needle: &Self) -> Option<&Self> { |
|
113 | fn drop_prefix(&self, needle: &Self) -> Option<&Self> { | |
113 | if self.starts_with(needle) { |
|
114 | if self.starts_with(needle) { | |
114 | Some(&self[needle.len()..]) |
|
115 | Some(&self[needle.len()..]) | |
115 | } else { |
|
116 | } else { | |
116 | None |
|
117 | None | |
117 | } |
|
118 | } | |
118 | } |
|
119 | } | |
|
120 | ||||
|
121 | fn split_2(&self, separator: u8) -> Option<(&[u8], &[u8])> { | |||
|
122 | let mut iter = self.splitn(2, |&byte| byte == separator); | |||
|
123 | let a = iter.next()?; | |||
|
124 | let b = iter.next()?; | |||
|
125 | Some((a, b)) | |||
|
126 | } | |||
119 | } |
|
127 | } | |
120 |
|
128 | |||
121 | pub trait Escaped { |
|
129 | pub trait Escaped { | |
122 | /// Return bytes escaped for display to the user |
|
130 | /// Return bytes escaped for display to the user | |
123 | fn escaped_bytes(&self) -> Vec<u8>; |
|
131 | fn escaped_bytes(&self) -> Vec<u8>; | |
124 | } |
|
132 | } | |
125 |
|
133 | |||
126 | impl Escaped for u8 { |
|
134 | impl Escaped for u8 { | |
127 | fn escaped_bytes(&self) -> Vec<u8> { |
|
135 | fn escaped_bytes(&self) -> Vec<u8> { | |
128 | let mut acc = vec![]; |
|
136 | let mut acc = vec![]; | |
129 | match self { |
|
137 | match self { | |
130 | c @ b'\'' | c @ b'\\' => { |
|
138 | c @ b'\'' | c @ b'\\' => { | |
131 | acc.push(b'\\'); |
|
139 | acc.push(b'\\'); | |
132 | acc.push(*c); |
|
140 | acc.push(*c); | |
133 | } |
|
141 | } | |
134 | b'\t' => { |
|
142 | b'\t' => { | |
135 | acc.extend(br"\\t"); |
|
143 | acc.extend(br"\\t"); | |
136 | } |
|
144 | } | |
137 | b'\n' => { |
|
145 | b'\n' => { | |
138 | acc.extend(br"\\n"); |
|
146 | acc.extend(br"\\n"); | |
139 | } |
|
147 | } | |
140 | b'\r' => { |
|
148 | b'\r' => { | |
141 | acc.extend(br"\\r"); |
|
149 | acc.extend(br"\\r"); | |
142 | } |
|
150 | } | |
143 | c if (*c < b' ' || *c >= 127) => { |
|
151 | c if (*c < b' ' || *c >= 127) => { | |
144 | write!(acc, "\\x{:x}", self).unwrap(); |
|
152 | write!(acc, "\\x{:x}", self).unwrap(); | |
145 | } |
|
153 | } | |
146 | c => { |
|
154 | c => { | |
147 | acc.push(*c); |
|
155 | acc.push(*c); | |
148 | } |
|
156 | } | |
149 | } |
|
157 | } | |
150 | acc |
|
158 | acc | |
151 | } |
|
159 | } | |
152 | } |
|
160 | } | |
153 |
|
161 | |||
154 | impl<'a, T: Escaped> Escaped for &'a [T] { |
|
162 | impl<'a, T: Escaped> Escaped for &'a [T] { | |
155 | fn escaped_bytes(&self) -> Vec<u8> { |
|
163 | fn escaped_bytes(&self) -> Vec<u8> { | |
156 | self.iter().flat_map(Escaped::escaped_bytes).collect() |
|
164 | self.iter().flat_map(Escaped::escaped_bytes).collect() | |
157 | } |
|
165 | } | |
158 | } |
|
166 | } | |
159 |
|
167 | |||
160 | impl<T: Escaped> Escaped for Vec<T> { |
|
168 | impl<T: Escaped> Escaped for Vec<T> { | |
161 | fn escaped_bytes(&self) -> Vec<u8> { |
|
169 | fn escaped_bytes(&self) -> Vec<u8> { | |
162 | self.deref().escaped_bytes() |
|
170 | self.deref().escaped_bytes() | |
163 | } |
|
171 | } | |
164 | } |
|
172 | } | |
165 |
|
173 | |||
166 | impl<'a> Escaped for &'a HgPath { |
|
174 | impl<'a> Escaped for &'a HgPath { | |
167 | fn escaped_bytes(&self) -> Vec<u8> { |
|
175 | fn escaped_bytes(&self) -> Vec<u8> { | |
168 | self.as_bytes().escaped_bytes() |
|
176 | self.as_bytes().escaped_bytes() | |
169 | } |
|
177 | } | |
170 | } |
|
178 | } | |
171 |
|
179 | |||
172 | // TODO: use the str method when we require Rust 1.45 |
|
180 | // TODO: use the str method when we require Rust 1.45 | |
173 | pub(crate) fn strip_suffix<'a>(s: &'a str, suffix: &str) -> Option<&'a str> { |
|
181 | pub(crate) fn strip_suffix<'a>(s: &'a str, suffix: &str) -> Option<&'a str> { | |
174 | if s.ends_with(suffix) { |
|
182 | if s.ends_with(suffix) { | |
175 | Some(&s[..s.len() - suffix.len()]) |
|
183 | Some(&s[..s.len() - suffix.len()]) | |
176 | } else { |
|
184 | } else { | |
177 | None |
|
185 | None | |
178 | } |
|
186 | } | |
179 | } |
|
187 | } | |
180 |
|
188 | |||
181 | pub fn current_dir() -> Result<std::path::PathBuf, HgError> { |
|
189 | pub fn current_dir() -> Result<std::path::PathBuf, HgError> { | |
182 | std::env::current_dir().map_err(|error| HgError::IoError { |
|
190 | std::env::current_dir().map_err(|error| HgError::IoError { | |
183 | error, |
|
191 | error, | |
184 | context: IoErrorContext::CurrentDir, |
|
192 | context: IoErrorContext::CurrentDir, | |
185 | }) |
|
193 | }) | |
186 | } |
|
194 | } | |
187 |
|
195 | |||
188 | pub fn current_exe() -> Result<std::path::PathBuf, HgError> { |
|
196 | pub fn current_exe() -> Result<std::path::PathBuf, HgError> { | |
189 | std::env::current_exe().map_err(|error| HgError::IoError { |
|
197 | std::env::current_exe().map_err(|error| HgError::IoError { | |
190 | error, |
|
198 | error, | |
191 | context: IoErrorContext::CurrentExe, |
|
199 | context: IoErrorContext::CurrentExe, | |
192 | }) |
|
200 | }) | |
193 | } |
|
201 | } |
@@ -1,30 +1,52 | |||||
1 | use crate::error::CommandError; |
|
1 | use crate::error::CommandError; | |
2 | use crate::ui::Ui; |
|
2 | use crate::ui::Ui; | |
|
3 | use clap::Arg; | |||
3 | use clap::ArgMatches; |
|
4 | use clap::ArgMatches; | |
4 | use format_bytes::format_bytes; |
|
5 | use format_bytes::format_bytes; | |
5 | use hg::config::Config; |
|
6 | use hg::config::Config; | |
|
7 | use hg::errors::HgError; | |||
6 | use hg::repo::Repo; |
|
8 | use hg::repo::Repo; | |
7 | use hg::utils::files::get_bytes_from_path; |
|
9 | use hg::utils::SliceExt; | |
8 | use std::path::Path; |
|
10 | use std::path::Path; | |
9 |
|
11 | |||
10 | pub const HELP_TEXT: &str = " |
|
12 | pub const HELP_TEXT: &str = " | |
11 | Print the root directory of the current repository. |
|
13 | With one argument of the form section.name, print just the value of that config item. | |
12 |
|
||||
13 | Returns 0 on success. |
|
|||
14 | "; |
|
14 | "; | |
15 |
|
15 | |||
16 | pub fn args() -> clap::App<'static, 'static> { |
|
16 | pub fn args() -> clap::App<'static, 'static> { | |
17 |
clap::SubCommand::with_name(" |
|
17 | clap::SubCommand::with_name("config") | |
|
18 | .arg( | |||
|
19 | Arg::with_name("name") | |||
|
20 | .help("the section.name to print") | |||
|
21 | .value_name("NAME") | |||
|
22 | .required(true) | |||
|
23 | .takes_value(true), | |||
|
24 | ) | |||
|
25 | .about(HELP_TEXT) | |||
18 | } |
|
26 | } | |
19 |
|
27 | |||
20 | pub fn run( |
|
28 | pub fn run( | |
21 | ui: &Ui, |
|
29 | ui: &Ui, | |
22 | config: &Config, |
|
30 | config: &Config, | |
23 | repo_path: Option<&Path>, |
|
31 | repo_path: Option<&Path>, | |
24 |
|
|
32 | args: &ArgMatches, | |
25 | ) -> Result<(), CommandError> { |
|
33 | ) -> Result<(), CommandError> { | |
26 | let repo = Repo::find(config, repo_path)?; |
|
34 | let opt_repo = Repo::find_optional(config, repo_path)?; | |
27 | let bytes = get_bytes_from_path(repo.working_directory_path()); |
|
35 | let config = if let Some(repo) = &opt_repo { | |
28 | ui.write_stdout(&format_bytes!(b"{}\n", bytes.as_slice()))?; |
|
36 | repo.config() | |
|
37 | } else { | |||
|
38 | config | |||
|
39 | }; | |||
|
40 | ||||
|
41 | let (section, name) = args | |||
|
42 | .value_of("name") | |||
|
43 | .expect("missing required CLI argument") | |||
|
44 | .as_bytes() | |||
|
45 | .split_2(b'.') | |||
|
46 | .ok_or_else(|| HgError::abort(""))?; | |||
|
47 | ||||
|
48 | let value = config.get(section, name).unwrap_or(b""); | |||
|
49 | ||||
|
50 | ui.write_stdout(&format_bytes!(b"{}\n", value))?; | |||
29 | Ok(()) |
|
51 | Ok(()) | |
30 | } |
|
52 | } |
@@ -1,137 +1,138 | |||||
1 | extern crate log; |
|
1 | extern crate log; | |
2 | use clap::App; |
|
2 | use clap::App; | |
3 | use clap::AppSettings; |
|
3 | use clap::AppSettings; | |
4 | use clap::Arg; |
|
4 | use clap::Arg; | |
5 | use clap::ArgMatches; |
|
5 | use clap::ArgMatches; | |
6 | use format_bytes::format_bytes; |
|
6 | use format_bytes::format_bytes; | |
7 | use std::path::Path; |
|
7 | use std::path::Path; | |
8 |
|
8 | |||
9 | mod error; |
|
9 | mod error; | |
10 | mod exitcode; |
|
10 | mod exitcode; | |
11 | mod ui; |
|
11 | mod ui; | |
12 | use error::CommandError; |
|
12 | use error::CommandError; | |
13 |
|
13 | |||
14 | fn add_global_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { |
|
14 | fn add_global_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { | |
15 | app.arg( |
|
15 | app.arg( | |
16 | Arg::with_name("repository") |
|
16 | Arg::with_name("repository") | |
17 | .help("repository root directory") |
|
17 | .help("repository root directory") | |
18 | .short("-R") |
|
18 | .short("-R") | |
19 | .long("--repository") |
|
19 | .long("--repository") | |
20 | .value_name("REPO") |
|
20 | .value_name("REPO") | |
21 | .takes_value(true), |
|
21 | .takes_value(true), | |
22 | ) |
|
22 | ) | |
23 | .arg( |
|
23 | .arg( | |
24 | Arg::with_name("config") |
|
24 | Arg::with_name("config") | |
25 | .help("set/override config option (use 'section.name=value')") |
|
25 | .help("set/override config option (use 'section.name=value')") | |
26 | .long("--config") |
|
26 | .long("--config") | |
27 | .value_name("CONFIG") |
|
27 | .value_name("CONFIG") | |
28 | .takes_value(true) |
|
28 | .takes_value(true) | |
29 | // Ok: `--config section.key1=val --config section.key2=val2` |
|
29 | // Ok: `--config section.key1=val --config section.key2=val2` | |
30 | .multiple(true) |
|
30 | .multiple(true) | |
31 | // Not ok: `--config section.key1=val section.key2=val2` |
|
31 | // Not ok: `--config section.key1=val section.key2=val2` | |
32 | .number_of_values(1), |
|
32 | .number_of_values(1), | |
33 | ) |
|
33 | ) | |
34 | } |
|
34 | } | |
35 |
|
35 | |||
36 | fn main() { |
|
36 | fn main() { | |
37 | env_logger::init(); |
|
37 | env_logger::init(); | |
38 | let app = App::new("rhg") |
|
38 | let app = App::new("rhg") | |
39 | .setting(AppSettings::AllowInvalidUtf8) |
|
39 | .setting(AppSettings::AllowInvalidUtf8) | |
40 | .setting(AppSettings::SubcommandRequired) |
|
40 | .setting(AppSettings::SubcommandRequired) | |
41 | .setting(AppSettings::VersionlessSubcommands) |
|
41 | .setting(AppSettings::VersionlessSubcommands) | |
42 | .version("0.0.1"); |
|
42 | .version("0.0.1"); | |
43 | let app = add_global_args(app); |
|
43 | let app = add_global_args(app); | |
44 | let app = add_subcommand_args(app); |
|
44 | let app = add_subcommand_args(app); | |
45 |
|
45 | |||
46 | let ui = ui::Ui::new(); |
|
46 | let ui = ui::Ui::new(); | |
47 |
|
47 | |||
48 | let matches = app.clone().get_matches_safe().unwrap_or_else(|err| { |
|
48 | let matches = app.clone().get_matches_safe().unwrap_or_else(|err| { | |
49 | let _ = ui.writeln_stderr_str(&err.message); |
|
49 | let _ = ui.writeln_stderr_str(&err.message); | |
50 | std::process::exit(exitcode::UNIMPLEMENTED) |
|
50 | std::process::exit(exitcode::UNIMPLEMENTED) | |
51 | }); |
|
51 | }); | |
52 |
|
52 | |||
53 | let (subcommand_name, subcommand_matches) = matches.subcommand(); |
|
53 | let (subcommand_name, subcommand_matches) = matches.subcommand(); | |
54 | let run = subcommand_run_fn(subcommand_name) |
|
54 | let run = subcommand_run_fn(subcommand_name) | |
55 | .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired"); |
|
55 | .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired"); | |
56 | let args = subcommand_matches |
|
56 | let args = subcommand_matches | |
57 | .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired"); |
|
57 | .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired"); | |
58 |
|
58 | |||
59 | // Global arguments can be in either based on e.g. `hg -R ./foo log` v.s. |
|
59 | // Global arguments can be in either based on e.g. `hg -R ./foo log` v.s. | |
60 | // `hg log -R ./foo` |
|
60 | // `hg log -R ./foo` | |
61 | let value_of_global_arg = |
|
61 | let value_of_global_arg = | |
62 | |name| args.value_of_os(name).or_else(|| matches.value_of_os(name)); |
|
62 | |name| args.value_of_os(name).or_else(|| matches.value_of_os(name)); | |
63 | // For arguments where multiple occurences are allowed, return a |
|
63 | // For arguments where multiple occurences are allowed, return a | |
64 | // possibly-iterator of all values. |
|
64 | // possibly-iterator of all values. | |
65 | let values_of_global_arg = |name: &str| { |
|
65 | let values_of_global_arg = |name: &str| { | |
66 | let a = matches.values_of_os(name).into_iter().flatten(); |
|
66 | let a = matches.values_of_os(name).into_iter().flatten(); | |
67 | let b = args.values_of_os(name).into_iter().flatten(); |
|
67 | let b = args.values_of_os(name).into_iter().flatten(); | |
68 | a.chain(b) |
|
68 | a.chain(b) | |
69 | }; |
|
69 | }; | |
70 |
|
70 | |||
71 | let repo_path = value_of_global_arg("repository").map(Path::new); |
|
71 | let repo_path = value_of_global_arg("repository").map(Path::new); | |
72 | let result = (|| -> Result<(), CommandError> { |
|
72 | let result = (|| -> Result<(), CommandError> { | |
73 | let config_args = values_of_global_arg("config") |
|
73 | let config_args = values_of_global_arg("config") | |
74 | // `get_bytes_from_path` works for OsStr the same as for Path |
|
74 | // `get_bytes_from_path` works for OsStr the same as for Path | |
75 | .map(hg::utils::files::get_bytes_from_path); |
|
75 | .map(hg::utils::files::get_bytes_from_path); | |
76 | let config = hg::config::Config::load(config_args)?; |
|
76 | let config = hg::config::Config::load(config_args)?; | |
77 | run(&ui, &config, repo_path, args) |
|
77 | run(&ui, &config, repo_path, args) | |
78 | })(); |
|
78 | })(); | |
79 |
|
79 | |||
80 | let exit_code = match result { |
|
80 | let exit_code = match result { | |
81 | Ok(_) => exitcode::OK, |
|
81 | Ok(_) => exitcode::OK, | |
82 |
|
82 | |||
83 | // Exit with a specific code and no error message to let a potential |
|
83 | // Exit with a specific code and no error message to let a potential | |
84 | // wrapper script fallback to Python-based Mercurial. |
|
84 | // wrapper script fallback to Python-based Mercurial. | |
85 | Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED, |
|
85 | Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED, | |
86 |
|
86 | |||
87 | Err(CommandError::Abort { message }) => { |
|
87 | Err(CommandError::Abort { message }) => { | |
88 | if !message.is_empty() { |
|
88 | if !message.is_empty() { | |
89 | // Ignore errors when writing to stderr, weβre already exiting |
|
89 | // Ignore errors when writing to stderr, weβre already exiting | |
90 | // with failure code so thereβs not much more we can do. |
|
90 | // with failure code so thereβs not much more we can do. | |
91 | let _ = |
|
91 | let _ = | |
92 | ui.write_stderr(&format_bytes!(b"abort: {}\n", message)); |
|
92 | ui.write_stderr(&format_bytes!(b"abort: {}\n", message)); | |
93 | } |
|
93 | } | |
94 | exitcode::ABORT |
|
94 | exitcode::ABORT | |
95 | } |
|
95 | } | |
96 | }; |
|
96 | }; | |
97 | std::process::exit(exit_code) |
|
97 | std::process::exit(exit_code) | |
98 | } |
|
98 | } | |
99 |
|
99 | |||
100 | macro_rules! subcommands { |
|
100 | macro_rules! subcommands { | |
101 | ($( $command: ident )+) => { |
|
101 | ($( $command: ident )+) => { | |
102 | mod commands { |
|
102 | mod commands { | |
103 | $( |
|
103 | $( | |
104 | pub mod $command; |
|
104 | pub mod $command; | |
105 | )+ |
|
105 | )+ | |
106 | } |
|
106 | } | |
107 |
|
107 | |||
108 | fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { |
|
108 | fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { | |
109 | app |
|
109 | app | |
110 | $( |
|
110 | $( | |
111 | .subcommand(add_global_args(commands::$command::args())) |
|
111 | .subcommand(add_global_args(commands::$command::args())) | |
112 | )+ |
|
112 | )+ | |
113 | } |
|
113 | } | |
114 |
|
114 | |||
115 | fn subcommand_run_fn(name: &str) -> Option<fn( |
|
115 | fn subcommand_run_fn(name: &str) -> Option<fn( | |
116 | &ui::Ui, |
|
116 | &ui::Ui, | |
117 | &hg::config::Config, |
|
117 | &hg::config::Config, | |
118 | Option<&Path>, |
|
118 | Option<&Path>, | |
119 | &ArgMatches, |
|
119 | &ArgMatches, | |
120 | ) -> Result<(), CommandError>> { |
|
120 | ) -> Result<(), CommandError>> { | |
121 | match name { |
|
121 | match name { | |
122 | $( |
|
122 | $( | |
123 | stringify!($command) => Some(commands::$command::run), |
|
123 | stringify!($command) => Some(commands::$command::run), | |
124 | )+ |
|
124 | )+ | |
125 | _ => None, |
|
125 | _ => None, | |
126 | } |
|
126 | } | |
127 | } |
|
127 | } | |
128 | }; |
|
128 | }; | |
129 | } |
|
129 | } | |
130 |
|
130 | |||
131 | subcommands! { |
|
131 | subcommands! { | |
132 | cat |
|
132 | cat | |
133 | debugdata |
|
133 | debugdata | |
134 | debugrequirements |
|
134 | debugrequirements | |
135 | files |
|
135 | files | |
136 | root |
|
136 | root | |
|
137 | config | |||
137 | } |
|
138 | } |
@@ -1,257 +1,269 | |||||
1 | #require rust |
|
1 | #require rust | |
2 |
|
2 | |||
3 | Define an rhg function that will only run if rhg exists |
|
3 | Define an rhg function that will only run if rhg exists | |
4 | $ rhg() { |
|
4 | $ rhg() { | |
5 | > if [ -f "$RUNTESTDIR/../rust/target/release/rhg" ]; then |
|
5 | > if [ -f "$RUNTESTDIR/../rust/target/release/rhg" ]; then | |
6 | > "$RUNTESTDIR/../rust/target/release/rhg" "$@" |
|
6 | > "$RUNTESTDIR/../rust/target/release/rhg" "$@" | |
7 | > else |
|
7 | > else | |
8 | > echo "skipped: Cannot find rhg. Try to run cargo build in rust/rhg." |
|
8 | > echo "skipped: Cannot find rhg. Try to run cargo build in rust/rhg." | |
9 | > exit 80 |
|
9 | > exit 80 | |
10 | > fi |
|
10 | > fi | |
11 | > } |
|
11 | > } | |
12 |
|
12 | |||
13 | Unimplemented command |
|
13 | Unimplemented command | |
14 | $ rhg unimplemented-command |
|
14 | $ rhg unimplemented-command | |
15 | error: Found argument 'unimplemented-command' which wasn't expected, or isn't valid in this context |
|
15 | error: Found argument 'unimplemented-command' which wasn't expected, or isn't valid in this context | |
16 |
|
16 | |||
17 | USAGE: |
|
17 | USAGE: | |
18 | rhg [OPTIONS] <SUBCOMMAND> |
|
18 | rhg [OPTIONS] <SUBCOMMAND> | |
19 |
|
19 | |||
20 | For more information try --help |
|
20 | For more information try --help | |
21 | [252] |
|
21 | [252] | |
22 |
|
22 | |||
23 | Finding root |
|
23 | Finding root | |
24 | $ rhg root |
|
24 | $ rhg root | |
25 | abort: no repository found in '$TESTTMP' (.hg not found)! |
|
25 | abort: no repository found in '$TESTTMP' (.hg not found)! | |
26 | [255] |
|
26 | [255] | |
27 |
|
27 | |||
28 | $ hg init repository |
|
28 | $ hg init repository | |
29 | $ cd repository |
|
29 | $ cd repository | |
30 | $ rhg root |
|
30 | $ rhg root | |
31 | $TESTTMP/repository |
|
31 | $TESTTMP/repository | |
32 |
|
32 | |||
|
33 | Reading and setting configuration | |||
|
34 | $ echo "[ui]" >> $HGRCPATH | |||
|
35 | $ echo "username = user1" >> $HGRCPATH | |||
|
36 | $ rhg config ui.username | |||
|
37 | user1 | |||
|
38 | $ echo "[ui]" >> .hg/hgrc | |||
|
39 | $ echo "username = user2" >> .hg/hgrc | |||
|
40 | $ rhg config ui.username | |||
|
41 | user2 | |||
|
42 | $ rhg --config ui.username=user3 config ui.username | |||
|
43 | user3 | |||
|
44 | ||||
33 | Unwritable file descriptor |
|
45 | Unwritable file descriptor | |
34 | $ rhg root > /dev/full |
|
46 | $ rhg root > /dev/full | |
35 | abort: No space left on device (os error 28) |
|
47 | abort: No space left on device (os error 28) | |
36 | [255] |
|
48 | [255] | |
37 |
|
49 | |||
38 | Deleted repository |
|
50 | Deleted repository | |
39 | $ rm -rf `pwd` |
|
51 | $ rm -rf `pwd` | |
40 | $ rhg root |
|
52 | $ rhg root | |
41 | abort: $ENOENT$: current directory |
|
53 | abort: $ENOENT$: current directory | |
42 | [255] |
|
54 | [255] | |
43 |
|
55 | |||
44 | Listing tracked files |
|
56 | Listing tracked files | |
45 | $ cd $TESTTMP |
|
57 | $ cd $TESTTMP | |
46 | $ hg init repository |
|
58 | $ hg init repository | |
47 | $ cd repository |
|
59 | $ cd repository | |
48 | $ for i in 1 2 3; do |
|
60 | $ for i in 1 2 3; do | |
49 | > echo $i >> file$i |
|
61 | > echo $i >> file$i | |
50 | > hg add file$i |
|
62 | > hg add file$i | |
51 | > done |
|
63 | > done | |
52 | > hg commit -m "commit $i" -q |
|
64 | > hg commit -m "commit $i" -q | |
53 |
|
65 | |||
54 | Listing tracked files from root |
|
66 | Listing tracked files from root | |
55 | $ rhg files |
|
67 | $ rhg files | |
56 | file1 |
|
68 | file1 | |
57 | file2 |
|
69 | file2 | |
58 | file3 |
|
70 | file3 | |
59 |
|
71 | |||
60 | Listing tracked files from subdirectory |
|
72 | Listing tracked files from subdirectory | |
61 | $ mkdir -p path/to/directory |
|
73 | $ mkdir -p path/to/directory | |
62 | $ cd path/to/directory |
|
74 | $ cd path/to/directory | |
63 | $ rhg files |
|
75 | $ rhg files | |
64 | ../../../file1 |
|
76 | ../../../file1 | |
65 | ../../../file2 |
|
77 | ../../../file2 | |
66 | ../../../file3 |
|
78 | ../../../file3 | |
67 |
|
79 | |||
68 | Listing tracked files through broken pipe |
|
80 | Listing tracked files through broken pipe | |
69 | $ rhg files | head -n 1 |
|
81 | $ rhg files | head -n 1 | |
70 | ../../../file1 |
|
82 | ../../../file1 | |
71 |
|
83 | |||
72 | Debuging data in inline index |
|
84 | Debuging data in inline index | |
73 | $ cd $TESTTMP |
|
85 | $ cd $TESTTMP | |
74 | $ rm -rf repository |
|
86 | $ rm -rf repository | |
75 | $ hg init repository |
|
87 | $ hg init repository | |
76 | $ cd repository |
|
88 | $ cd repository | |
77 | $ for i in 1 2 3 4 5 6; do |
|
89 | $ for i in 1 2 3 4 5 6; do | |
78 | > echo $i >> file-$i |
|
90 | > echo $i >> file-$i | |
79 | > hg add file-$i |
|
91 | > hg add file-$i | |
80 | > hg commit -m "Commit $i" -q |
|
92 | > hg commit -m "Commit $i" -q | |
81 | > done |
|
93 | > done | |
82 | $ rhg debugdata -c 2 |
|
94 | $ rhg debugdata -c 2 | |
83 | 8d0267cb034247ebfa5ee58ce59e22e57a492297 |
|
95 | 8d0267cb034247ebfa5ee58ce59e22e57a492297 | |
84 | test |
|
96 | test | |
85 | 0 0 |
|
97 | 0 0 | |
86 | file-3 |
|
98 | file-3 | |
87 |
|
99 | |||
88 | Commit 3 (no-eol) |
|
100 | Commit 3 (no-eol) | |
89 | $ rhg debugdata -m 2 |
|
101 | $ rhg debugdata -m 2 | |
90 | file-1\x00b8e02f6433738021a065f94175c7cd23db5f05be (esc) |
|
102 | file-1\x00b8e02f6433738021a065f94175c7cd23db5f05be (esc) | |
91 | file-2\x005d9299349fc01ddd25d0070d149b124d8f10411e (esc) |
|
103 | file-2\x005d9299349fc01ddd25d0070d149b124d8f10411e (esc) | |
92 | file-3\x002661d26c649684b482d10f91960cc3db683c38b4 (esc) |
|
104 | file-3\x002661d26c649684b482d10f91960cc3db683c38b4 (esc) | |
93 |
|
105 | |||
94 | Debuging with full node id |
|
106 | Debuging with full node id | |
95 | $ rhg debugdata -c `hg log -r 0 -T '{node}'` |
|
107 | $ rhg debugdata -c `hg log -r 0 -T '{node}'` | |
96 | d1d1c679d3053e8926061b6f45ca52009f011e3f |
|
108 | d1d1c679d3053e8926061b6f45ca52009f011e3f | |
97 | test |
|
109 | test | |
98 | 0 0 |
|
110 | 0 0 | |
99 | file-1 |
|
111 | file-1 | |
100 |
|
112 | |||
101 | Commit 1 (no-eol) |
|
113 | Commit 1 (no-eol) | |
102 |
|
114 | |||
103 | Specifying revisions by changeset ID |
|
115 | Specifying revisions by changeset ID | |
104 | $ hg log -T '{node}\n' |
|
116 | $ hg log -T '{node}\n' | |
105 | c6ad58c44207b6ff8a4fbbca7045a5edaa7e908b |
|
117 | c6ad58c44207b6ff8a4fbbca7045a5edaa7e908b | |
106 | d654274993d0149eecc3cc03214f598320211900 |
|
118 | d654274993d0149eecc3cc03214f598320211900 | |
107 | f646af7e96481d3a5470b695cf30ad8e3ab6c575 |
|
119 | f646af7e96481d3a5470b695cf30ad8e3ab6c575 | |
108 | cf8b83f14ead62b374b6e91a0e9303b85dfd9ed7 |
|
120 | cf8b83f14ead62b374b6e91a0e9303b85dfd9ed7 | |
109 | 91c6f6e73e39318534dc415ea4e8a09c99cd74d6 |
|
121 | 91c6f6e73e39318534dc415ea4e8a09c99cd74d6 | |
110 | 6ae9681c6d30389694d8701faf24b583cf3ccafe |
|
122 | 6ae9681c6d30389694d8701faf24b583cf3ccafe | |
111 | $ rhg files -r cf8b83 |
|
123 | $ rhg files -r cf8b83 | |
112 | file-1 |
|
124 | file-1 | |
113 | file-2 |
|
125 | file-2 | |
114 | file-3 |
|
126 | file-3 | |
115 | $ rhg cat -r cf8b83 file-2 |
|
127 | $ rhg cat -r cf8b83 file-2 | |
116 | 2 |
|
128 | 2 | |
117 | $ rhg cat -r c file-2 |
|
129 | $ rhg cat -r c file-2 | |
118 | abort: ambiguous revision identifier c |
|
130 | abort: ambiguous revision identifier c | |
119 | [255] |
|
131 | [255] | |
120 | $ rhg cat -r d file-2 |
|
132 | $ rhg cat -r d file-2 | |
121 | 2 |
|
133 | 2 | |
122 |
|
134 | |||
123 | Cat files |
|
135 | Cat files | |
124 | $ cd $TESTTMP |
|
136 | $ cd $TESTTMP | |
125 | $ rm -rf repository |
|
137 | $ rm -rf repository | |
126 | $ hg init repository |
|
138 | $ hg init repository | |
127 | $ cd repository |
|
139 | $ cd repository | |
128 | $ echo "original content" > original |
|
140 | $ echo "original content" > original | |
129 | $ hg add original |
|
141 | $ hg add original | |
130 | $ hg commit -m "add original" original |
|
142 | $ hg commit -m "add original" original | |
131 | $ rhg cat -r 0 original |
|
143 | $ rhg cat -r 0 original | |
132 | original content |
|
144 | original content | |
133 | Cat copied file should not display copy metadata |
|
145 | Cat copied file should not display copy metadata | |
134 | $ hg copy original copy_of_original |
|
146 | $ hg copy original copy_of_original | |
135 | $ hg commit -m "add copy of original" |
|
147 | $ hg commit -m "add copy of original" | |
136 | $ rhg cat -r 1 copy_of_original |
|
148 | $ rhg cat -r 1 copy_of_original | |
137 | original content |
|
149 | original content | |
138 |
|
150 | |||
139 | Requirements |
|
151 | Requirements | |
140 | $ rhg debugrequirements |
|
152 | $ rhg debugrequirements | |
141 | dotencode |
|
153 | dotencode | |
142 | fncache |
|
154 | fncache | |
143 | generaldelta |
|
155 | generaldelta | |
144 | revlogv1 |
|
156 | revlogv1 | |
145 | sparserevlog |
|
157 | sparserevlog | |
146 | store |
|
158 | store | |
147 |
|
159 | |||
148 | $ echo indoor-pool >> .hg/requires |
|
160 | $ echo indoor-pool >> .hg/requires | |
149 | $ rhg files |
|
161 | $ rhg files | |
150 | [252] |
|
162 | [252] | |
151 |
|
163 | |||
152 | $ rhg cat -r 1 copy_of_original |
|
164 | $ rhg cat -r 1 copy_of_original | |
153 | [252] |
|
165 | [252] | |
154 |
|
166 | |||
155 | $ rhg debugrequirements |
|
167 | $ rhg debugrequirements | |
156 | [252] |
|
168 | [252] | |
157 |
|
169 | |||
158 | $ echo -e '\xFF' >> .hg/requires |
|
170 | $ echo -e '\xFF' >> .hg/requires | |
159 | $ rhg debugrequirements |
|
171 | $ rhg debugrequirements | |
160 | abort: corrupted repository: parse error in 'requires' file |
|
172 | abort: corrupted repository: parse error in 'requires' file | |
161 | [255] |
|
173 | [255] | |
162 |
|
174 | |||
163 | Persistent nodemap |
|
175 | Persistent nodemap | |
164 | $ cd $TESTTMP |
|
176 | $ cd $TESTTMP | |
165 | $ rm -rf repository |
|
177 | $ rm -rf repository | |
166 | $ hg init repository |
|
178 | $ hg init repository | |
167 | $ cd repository |
|
179 | $ cd repository | |
168 | $ rhg debugrequirements | grep nodemap |
|
180 | $ rhg debugrequirements | grep nodemap | |
169 | [1] |
|
181 | [1] | |
170 | $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn" |
|
182 | $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn" | |
171 | $ hg id -r tip |
|
183 | $ hg id -r tip | |
172 | c3ae8dec9fad tip |
|
184 | c3ae8dec9fad tip | |
173 | $ ls .hg/store/00changelog* |
|
185 | $ ls .hg/store/00changelog* | |
174 | .hg/store/00changelog.d |
|
186 | .hg/store/00changelog.d | |
175 | .hg/store/00changelog.i |
|
187 | .hg/store/00changelog.i | |
176 | $ rhg files -r c3ae8dec9fad |
|
188 | $ rhg files -r c3ae8dec9fad | |
177 | of |
|
189 | of | |
178 |
|
190 | |||
179 | $ cd $TESTTMP |
|
191 | $ cd $TESTTMP | |
180 | $ rm -rf repository |
|
192 | $ rm -rf repository | |
181 | $ hg --config format.use-persistent-nodemap=True init repository |
|
193 | $ hg --config format.use-persistent-nodemap=True init repository | |
182 | $ cd repository |
|
194 | $ cd repository | |
183 | $ rhg debugrequirements | grep nodemap |
|
195 | $ rhg debugrequirements | grep nodemap | |
184 | persistent-nodemap |
|
196 | persistent-nodemap | |
185 | $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn" |
|
197 | $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn" | |
186 | $ hg id -r tip |
|
198 | $ hg id -r tip | |
187 | c3ae8dec9fad tip |
|
199 | c3ae8dec9fad tip | |
188 | $ ls .hg/store/00changelog* |
|
200 | $ ls .hg/store/00changelog* | |
189 | .hg/store/00changelog-*.nd (glob) |
|
201 | .hg/store/00changelog-*.nd (glob) | |
190 | .hg/store/00changelog.d |
|
202 | .hg/store/00changelog.d | |
191 | .hg/store/00changelog.i |
|
203 | .hg/store/00changelog.i | |
192 | .hg/store/00changelog.n |
|
204 | .hg/store/00changelog.n | |
193 |
|
205 | |||
194 | Specifying revisions by changeset ID |
|
206 | Specifying revisions by changeset ID | |
195 | $ rhg files -r c3ae8dec9fad |
|
207 | $ rhg files -r c3ae8dec9fad | |
196 | of |
|
208 | of | |
197 | $ rhg cat -r c3ae8dec9fad of |
|
209 | $ rhg cat -r c3ae8dec9fad of | |
198 | r5000 |
|
210 | r5000 | |
199 |
|
211 | |||
200 | Crate a shared repository |
|
212 | Crate a shared repository | |
201 |
|
213 | |||
202 | $ echo "[extensions]" >> $HGRCPATH |
|
214 | $ echo "[extensions]" >> $HGRCPATH | |
203 | $ echo "share = " >> $HGRCPATH |
|
215 | $ echo "share = " >> $HGRCPATH | |
204 |
|
216 | |||
205 | $ cd $TESTTMP |
|
217 | $ cd $TESTTMP | |
206 | $ hg init repo1 |
|
218 | $ hg init repo1 | |
207 | $ echo a > repo1/a |
|
219 | $ echo a > repo1/a | |
208 | $ hg -R repo1 commit -A -m'init' |
|
220 | $ hg -R repo1 commit -A -m'init' | |
209 | adding a |
|
221 | adding a | |
210 |
|
222 | |||
211 | $ hg share repo1 repo2 |
|
223 | $ hg share repo1 repo2 | |
212 | updating working directory |
|
224 | updating working directory | |
213 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
225 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | |
214 |
|
226 | |||
215 | And check that basic rhg commands work with sharing |
|
227 | And check that basic rhg commands work with sharing | |
216 |
|
228 | |||
217 | $ rhg files -R repo2 |
|
229 | $ rhg files -R repo2 | |
218 | repo2/a |
|
230 | repo2/a | |
219 | $ rhg -R repo2 cat -r 0 repo2/a |
|
231 | $ rhg -R repo2 cat -r 0 repo2/a | |
220 | a |
|
232 | a | |
221 |
|
233 | |||
222 | Same with relative sharing |
|
234 | Same with relative sharing | |
223 |
|
235 | |||
224 | $ hg share repo2 repo3 --relative |
|
236 | $ hg share repo2 repo3 --relative | |
225 | updating working directory |
|
237 | updating working directory | |
226 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
238 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | |
227 |
|
239 | |||
228 | $ rhg files -R repo3 |
|
240 | $ rhg files -R repo3 | |
229 | repo3/a |
|
241 | repo3/a | |
230 | $ rhg -R repo3 cat -r 0 repo3/a |
|
242 | $ rhg -R repo3 cat -r 0 repo3/a | |
231 | a |
|
243 | a | |
232 |
|
244 | |||
233 | Same with share-safe |
|
245 | Same with share-safe | |
234 |
|
246 | |||
235 | $ echo "[format]" >> $HGRCPATH |
|
247 | $ echo "[format]" >> $HGRCPATH | |
236 | $ echo "use-share-safe = True" >> $HGRCPATH |
|
248 | $ echo "use-share-safe = True" >> $HGRCPATH | |
237 |
|
249 | |||
238 | $ cd $TESTTMP |
|
250 | $ cd $TESTTMP | |
239 | $ hg init repo4 |
|
251 | $ hg init repo4 | |
240 | $ cd repo4 |
|
252 | $ cd repo4 | |
241 | $ echo a > a |
|
253 | $ echo a > a | |
242 | $ hg commit -A -m'init' |
|
254 | $ hg commit -A -m'init' | |
243 | adding a |
|
255 | adding a | |
244 |
|
256 | |||
245 | $ cd .. |
|
257 | $ cd .. | |
246 | $ hg share repo4 repo5 |
|
258 | $ hg share repo4 repo5 | |
247 | updating working directory |
|
259 | updating working directory | |
248 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
260 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | |
249 |
|
261 | |||
250 | And check that basic rhg commands work with sharing |
|
262 | And check that basic rhg commands work with sharing | |
251 |
|
263 | |||
252 | $ cd repo5 |
|
264 | $ cd repo5 | |
253 | $ rhg files |
|
265 | $ rhg files | |
254 | a |
|
266 | a | |
255 | $ rhg cat -r 0 a |
|
267 | $ rhg cat -r 0 a | |
256 | a |
|
268 | a | |
257 |
|
269 |
General Comments 0
You need to be logged in to leave comments.
Login now