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