##// END OF EJS Templates
rhg: Initial support for the 'status' command...
Georges Racinet -
r47578:c71e8d9e default
parent child Browse files
Show More
@@ -0,0 +1,315 b''
1 // status.rs
2 //
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
4 //
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.
7
8 use crate::error::CommandError;
9 use crate::ui::Ui;
10 use clap::{Arg, SubCommand};
11 use hg;
12 use hg::errors::IoResultExt;
13 use hg::matchers::AlwaysMatcher;
14 use hg::operations::cat;
15 use hg::repo::Repo;
16 use hg::revlog::node::Node;
17 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
18 use hg::{DirstateMap, StatusError};
19 use hg::{HgPathCow, StatusOptions};
20 use log::{info, warn};
21 use std::convert::TryInto;
22 use std::fs;
23 use std::io::BufReader;
24 use std::io::Read;
25
26 pub const HELP_TEXT: &str = "
27 Show changed files in the working directory
28
29 This is a pure Rust version of `hg status`.
30
31 Some options might be missing, check the list below.
32 ";
33
34 pub fn args() -> clap::App<'static, 'static> {
35 SubCommand::with_name("status")
36 .alias("st")
37 .about(HELP_TEXT)
38 .arg(
39 Arg::with_name("all")
40 .help("show status of all files")
41 .short("-A")
42 .long("--all"),
43 )
44 .arg(
45 Arg::with_name("modified")
46 .help("show only modified files")
47 .short("-m")
48 .long("--modified"),
49 )
50 .arg(
51 Arg::with_name("added")
52 .help("show only added files")
53 .short("-a")
54 .long("--added"),
55 )
56 .arg(
57 Arg::with_name("removed")
58 .help("show only removed files")
59 .short("-r")
60 .long("--removed"),
61 )
62 .arg(
63 Arg::with_name("clean")
64 .help("show only clean files")
65 .short("-c")
66 .long("--clean"),
67 )
68 .arg(
69 Arg::with_name("deleted")
70 .help("show only deleted files")
71 .short("-d")
72 .long("--deleted"),
73 )
74 .arg(
75 Arg::with_name("unknown")
76 .help("show only unknown (not tracked) files")
77 .short("-u")
78 .long("--unknown"),
79 )
80 .arg(
81 Arg::with_name("ignored")
82 .help("show only ignored files")
83 .short("-i")
84 .long("--ignored"),
85 )
86 }
87
88 /// Pure data type allowing the caller to specify file states to display
89 #[derive(Copy, Clone, Debug)]
90 pub struct DisplayStates {
91 pub modified: bool,
92 pub added: bool,
93 pub removed: bool,
94 pub clean: bool,
95 pub deleted: bool,
96 pub unknown: bool,
97 pub ignored: bool,
98 }
99
100 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
101 modified: true,
102 added: true,
103 removed: true,
104 clean: false,
105 deleted: true,
106 unknown: true,
107 ignored: false,
108 };
109
110 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
111 modified: true,
112 added: true,
113 removed: true,
114 clean: true,
115 deleted: true,
116 unknown: true,
117 ignored: true,
118 };
119
120 impl DisplayStates {
121 pub fn is_empty(&self) -> bool {
122 !(self.modified
123 || self.added
124 || self.removed
125 || self.clean
126 || self.deleted
127 || self.unknown
128 || self.ignored)
129 }
130 }
131
132 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
133 let status_enabled_default = false;
134 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
135 if !status_enabled.unwrap_or(status_enabled_default) {
136 return Err(CommandError::unsupported(
137 "status is experimental in rhg (enable it with 'rhg.status = true' \
138 or enable fallback with 'rhg.on-unsupported = fallback')"
139 ));
140 }
141
142 let ui = invocation.ui;
143 let args = invocation.subcommand_args;
144 let display_states = if args.is_present("all") {
145 // TODO when implementing `--quiet`: it excludes clean files
146 // from `--all`
147 ALL_DISPLAY_STATES
148 } else {
149 let requested = DisplayStates {
150 modified: args.is_present("modified"),
151 added: args.is_present("added"),
152 removed: args.is_present("removed"),
153 clean: args.is_present("clean"),
154 deleted: args.is_present("deleted"),
155 unknown: args.is_present("unknown"),
156 ignored: args.is_present("ignored"),
157 };
158 if requested.is_empty() {
159 DEFAULT_DISPLAY_STATES
160 } else {
161 requested
162 }
163 };
164
165 let repo = invocation.repo?;
166 let mut dmap = DirstateMap::new();
167 let dirstate_data = repo.hg_vfs().mmap_open("dirstate")?;
168 let parents = dmap.read(&dirstate_data)?;
169 let options = StatusOptions {
170 // TODO should be provided by the dirstate parsing and
171 // hence be stored on dmap. Using a value that assumes we aren't
172 // below the time resolution granularity of the FS and the
173 // dirstate.
174 last_normal_time: 0,
175 // we're currently supporting file systems with exec flags only
176 // anyway
177 check_exec: true,
178 list_clean: display_states.clean,
179 list_unknown: display_states.unknown,
180 list_ignored: display_states.ignored,
181 collect_traversed_dirs: false,
182 };
183 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
184 let ((lookup, ds_status), pattern_warnings) = hg::status(
185 &dmap,
186 &AlwaysMatcher,
187 repo.working_directory_path().to_owned(),
188 vec![ignore_file],
189 options,
190 )?;
191 if !pattern_warnings.is_empty() {
192 warn!("Pattern warnings: {:?}", &pattern_warnings);
193 }
194
195 if !ds_status.bad.is_empty() {
196 warn!("Bad matches {:?}", &(ds_status.bad))
197 }
198 if !lookup.is_empty() {
199 info!(
200 "Files to be rechecked by retrieval from filelog: {:?}",
201 &lookup
202 );
203 }
204 // TODO check ordering to match `hg status` output.
205 // (this is as in `hg help status`)
206 if display_states.modified {
207 display_status_paths(ui, &(ds_status.modified), b"M")?;
208 }
209 if !lookup.is_empty() {
210 let p1: Node = parents
211 .expect(
212 "Dirstate with no parents should not list any file to
213 be rechecked for modifications",
214 )
215 .p1
216 .into();
217 let p1_hex = format!("{:x}", p1);
218 let mut rechecked_modified: Vec<HgPathCow> = Vec::new();
219 let mut rechecked_clean: Vec<HgPathCow> = Vec::new();
220 for to_check in lookup {
221 if cat_file_is_modified(repo, &to_check, &p1_hex)? {
222 rechecked_modified.push(to_check);
223 } else {
224 rechecked_clean.push(to_check);
225 }
226 }
227 if display_states.modified {
228 display_status_paths(ui, &rechecked_modified, b"M")?;
229 }
230 if display_states.clean {
231 display_status_paths(ui, &rechecked_clean, b"C")?;
232 }
233 }
234 if display_states.added {
235 display_status_paths(ui, &(ds_status.added), b"A")?;
236 }
237 if display_states.clean {
238 display_status_paths(ui, &(ds_status.clean), b"C")?;
239 }
240 if display_states.removed {
241 display_status_paths(ui, &(ds_status.removed), b"R")?;
242 }
243 if display_states.deleted {
244 display_status_paths(ui, &(ds_status.deleted), b"!")?;
245 }
246 if display_states.unknown {
247 display_status_paths(ui, &(ds_status.unknown), b"?")?;
248 }
249 if display_states.ignored {
250 display_status_paths(ui, &(ds_status.ignored), b"I")?;
251 }
252 Ok(())
253 }
254
255 // Probably more elegant to use a Deref or Borrow trait rather than
256 // harcode HgPathBuf, but probably not really useful at this point
257 fn display_status_paths(
258 ui: &Ui,
259 paths: &[HgPathCow],
260 status_prefix: &[u8],
261 ) -> Result<(), CommandError> {
262 for path in paths {
263 // Same TODO as in commands::root
264 let bytes: &[u8] = path.as_bytes();
265 // TODO optim, probably lots of unneeded copies here, especially
266 // if out stream is buffered
267 ui.write_stdout(&[status_prefix, b" ", bytes, b"\n"].concat())?;
268 }
269 Ok(())
270 }
271
272 /// Check if a file is modified by comparing actual repo store and file system.
273 ///
274 /// This meant to be used for those that the dirstate cannot resolve, due
275 /// to time resolution limits.
276 ///
277 /// TODO: detect permission bits and similar metadata modifications
278 fn cat_file_is_modified(
279 repo: &Repo,
280 hg_path: &HgPath,
281 rev: &str,
282 ) -> Result<bool, CommandError> {
283 // TODO CatRev expects &[HgPathBuf], something like
284 // &[impl Deref<HgPath>] would be nicer and should avoid the copy
285 let path_bufs = [hg_path.into()];
286 // TODO IIUC CatRev returns a simple Vec<u8> for all files
287 // being able to tell them apart as (path, bytes) would be nicer
288 // and OPTIM would allow manifest resolution just once.
289 let output = cat(repo, rev, &path_bufs).map_err(|e| (e, rev))?;
290
291 let fs_path = repo
292 .working_directory_vfs()
293 .join(hg_path_to_os_string(hg_path).expect("HgPath conversion"));
294 let hg_data_len: u64 = match output.concatenated.len().try_into() {
295 Ok(v) => v,
296 Err(_) => {
297 // conversion of data length to u64 failed,
298 // good luck for any file to have this content
299 return Ok(true);
300 }
301 };
302 let fobj = fs::File::open(&fs_path).when_reading_file(&fs_path)?;
303 if fobj.metadata().map_err(|e| StatusError::from(e))?.len() != hg_data_len
304 {
305 return Ok(true);
306 }
307 for (fs_byte, hg_byte) in
308 BufReader::new(fobj).bytes().zip(output.concatenated)
309 {
310 if fs_byte.map_err(|e| StatusError::from(e))? != hg_byte {
311 return Ok(true);
312 }
313 }
314 Ok(false)
315 }
@@ -1,269 +1,266 b''
1 use crate::config::{Config, ConfigError, ConfigParseError};
1 use crate::config::{Config, ConfigError, ConfigParseError};
2 use crate::errors::{HgError, IoErrorContext, IoResultExt};
2 use crate::errors::{HgError, IoErrorContext, IoResultExt};
3 use crate::requirements;
3 use crate::requirements;
4 use crate::utils::files::get_path_from_bytes;
4 use crate::utils::files::get_path_from_bytes;
5 use crate::utils::SliceExt;
5 use crate::utils::SliceExt;
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 struct Vfs<'a> {
41 pub struct Vfs<'a> {
42 pub(crate) base: &'a Path,
42 pub(crate) base: &'a Path,
43 }
43 }
44
44
45 impl Repo {
45 impl Repo {
46 /// Find a repository, either at the given path (which must contain a `.hg`
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
47 /// sub-directory) or by searching the current directory and its
48 /// ancestors.
48 /// ancestors.
49 ///
49 ///
50 /// A method with two very different "modes" like this usually a code smell
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
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.
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.
53 /// Having two methods would just move that `if` to almost all callers.
54 pub fn find(
54 pub fn find(
55 config: &Config,
55 config: &Config,
56 explicit_path: Option<&Path>,
56 explicit_path: Option<&Path>,
57 ) -> Result<Self, RepoError> {
57 ) -> Result<Self, RepoError> {
58 if let Some(root) = explicit_path {
58 if let Some(root) = explicit_path {
59 if root.join(".hg").is_dir() {
59 if root.join(".hg").is_dir() {
60 Self::new_at_path(root.to_owned(), config)
60 Self::new_at_path(root.to_owned(), config)
61 } else if root.is_file() {
61 } else if root.is_file() {
62 Err(HgError::unsupported("bundle repository").into())
62 Err(HgError::unsupported("bundle repository").into())
63 } else {
63 } else {
64 Err(RepoError::NotFound {
64 Err(RepoError::NotFound {
65 at: root.to_owned(),
65 at: root.to_owned(),
66 })
66 })
67 }
67 }
68 } else {
68 } else {
69 let current_directory = crate::utils::current_dir()?;
69 let current_directory = crate::utils::current_dir()?;
70 // ancestors() is inclusive: it first yields `current_directory`
70 // ancestors() is inclusive: it first yields `current_directory`
71 // as-is.
71 // as-is.
72 for ancestor in current_directory.ancestors() {
72 for ancestor in current_directory.ancestors() {
73 if ancestor.join(".hg").is_dir() {
73 if ancestor.join(".hg").is_dir() {
74 return Self::new_at_path(ancestor.to_owned(), config);
74 return Self::new_at_path(ancestor.to_owned(), config);
75 }
75 }
76 }
76 }
77 Err(RepoError::NotFound {
77 Err(RepoError::NotFound {
78 at: current_directory,
78 at: current_directory,
79 })
79 })
80 }
80 }
81 }
81 }
82
82
83 /// To be called after checking that `.hg` is a sub-directory
83 /// To be called after checking that `.hg` is a sub-directory
84 fn new_at_path(
84 fn new_at_path(
85 working_directory: PathBuf,
85 working_directory: PathBuf,
86 config: &Config,
86 config: &Config,
87 ) -> Result<Self, RepoError> {
87 ) -> Result<Self, RepoError> {
88 let dot_hg = working_directory.join(".hg");
88 let dot_hg = working_directory.join(".hg");
89
89
90 let mut repo_config_files = Vec::new();
90 let mut repo_config_files = Vec::new();
91 repo_config_files.push(dot_hg.join("hgrc"));
91 repo_config_files.push(dot_hg.join("hgrc"));
92 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
92 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
93
93
94 let hg_vfs = Vfs { base: &dot_hg };
94 let hg_vfs = Vfs { base: &dot_hg };
95 let mut reqs = requirements::load_if_exists(hg_vfs)?;
95 let mut reqs = requirements::load_if_exists(hg_vfs)?;
96 let relative =
96 let relative =
97 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
97 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
98 let shared =
98 let shared =
99 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
99 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
100
100
101 // From `mercurial/localrepo.py`:
101 // From `mercurial/localrepo.py`:
102 //
102 //
103 // if .hg/requires contains the sharesafe requirement, it means
103 // if .hg/requires contains the sharesafe requirement, it means
104 // there exists a `.hg/store/requires` too and we should read it
104 // there exists a `.hg/store/requires` too and we should read it
105 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
105 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
106 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
106 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
107 // is not present, refer checkrequirementscompat() for that
107 // is not present, refer checkrequirementscompat() for that
108 //
108 //
109 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
109 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
110 // repository was shared the old way. We check the share source
110 // repository was shared the old way. We check the share source
111 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
111 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
112 // current repository needs to be reshared
112 // current repository needs to be reshared
113 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
113 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
114
114
115 let store_path;
115 let store_path;
116 if !shared {
116 if !shared {
117 store_path = dot_hg.join("store");
117 store_path = dot_hg.join("store");
118 } else {
118 } else {
119 let bytes = hg_vfs.read("sharedpath")?;
119 let bytes = hg_vfs.read("sharedpath")?;
120 let mut shared_path =
120 let mut shared_path =
121 get_path_from_bytes(bytes.trim_end_newlines()).to_owned();
121 get_path_from_bytes(bytes.trim_end_newlines()).to_owned();
122 if relative {
122 if relative {
123 shared_path = dot_hg.join(shared_path)
123 shared_path = dot_hg.join(shared_path)
124 }
124 }
125 if !shared_path.is_dir() {
125 if !shared_path.is_dir() {
126 return Err(HgError::corrupted(format!(
126 return Err(HgError::corrupted(format!(
127 ".hg/sharedpath points to nonexistent directory {}",
127 ".hg/sharedpath points to nonexistent directory {}",
128 shared_path.display()
128 shared_path.display()
129 ))
129 ))
130 .into());
130 .into());
131 }
131 }
132
132
133 store_path = shared_path.join("store");
133 store_path = shared_path.join("store");
134
134
135 let source_is_share_safe =
135 let source_is_share_safe =
136 requirements::load(Vfs { base: &shared_path })?
136 requirements::load(Vfs { base: &shared_path })?
137 .contains(requirements::SHARESAFE_REQUIREMENT);
137 .contains(requirements::SHARESAFE_REQUIREMENT);
138
138
139 if share_safe && !source_is_share_safe {
139 if share_safe && !source_is_share_safe {
140 return Err(match config
140 return Err(match config
141 .get(b"share", b"safe-mismatch.source-not-safe")
141 .get(b"share", b"safe-mismatch.source-not-safe")
142 {
142 {
143 Some(b"abort") | None => HgError::abort(
143 Some(b"abort") | None => HgError::abort(
144 "abort: share source does not support share-safe requirement\n\
144 "abort: share source does not support share-safe requirement\n\
145 (see `hg help config.format.use-share-safe` for more information)",
145 (see `hg help config.format.use-share-safe` for more information)",
146 ),
146 ),
147 _ => HgError::unsupported("share-safe downgrade"),
147 _ => HgError::unsupported("share-safe downgrade"),
148 }
148 }
149 .into());
149 .into());
150 } else if source_is_share_safe && !share_safe {
150 } else if source_is_share_safe && !share_safe {
151 return Err(
151 return Err(
152 match config.get(b"share", b"safe-mismatch.source-safe") {
152 match config.get(b"share", b"safe-mismatch.source-safe") {
153 Some(b"abort") | None => HgError::abort(
153 Some(b"abort") | None => HgError::abort(
154 "abort: version mismatch: source uses share-safe \
154 "abort: version mismatch: source uses share-safe \
155 functionality while the current share does not\n\
155 functionality while the current share does not\n\
156 (see `hg help config.format.use-share-safe` for more information)",
156 (see `hg help config.format.use-share-safe` for more information)",
157 ),
157 ),
158 _ => HgError::unsupported("share-safe upgrade"),
158 _ => HgError::unsupported("share-safe upgrade"),
159 }
159 }
160 .into(),
160 .into(),
161 );
161 );
162 }
162 }
163
163
164 if share_safe {
164 if share_safe {
165 repo_config_files.insert(0, shared_path.join("hgrc"))
165 repo_config_files.insert(0, shared_path.join("hgrc"))
166 }
166 }
167 }
167 }
168 if share_safe {
168 if share_safe {
169 reqs.extend(requirements::load(Vfs { base: &store_path })?);
169 reqs.extend(requirements::load(Vfs { base: &store_path })?);
170 }
170 }
171
171
172 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
172 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
173 config.combine_with_repo(&repo_config_files)?
173 config.combine_with_repo(&repo_config_files)?
174 } else {
174 } else {
175 config.clone()
175 config.clone()
176 };
176 };
177
177
178 let repo = Self {
178 let repo = Self {
179 requirements: reqs,
179 requirements: reqs,
180 working_directory,
180 working_directory,
181 store: store_path,
181 store: store_path,
182 dot_hg,
182 dot_hg,
183 config: repo_config,
183 config: repo_config,
184 };
184 };
185
185
186 requirements::check(&repo)?;
186 requirements::check(&repo)?;
187
187
188 Ok(repo)
188 Ok(repo)
189 }
189 }
190
190
191 pub fn working_directory_path(&self) -> &Path {
191 pub fn working_directory_path(&self) -> &Path {
192 &self.working_directory
192 &self.working_directory
193 }
193 }
194
194
195 pub fn requirements(&self) -> &HashSet<String> {
195 pub fn requirements(&self) -> &HashSet<String> {
196 &self.requirements
196 &self.requirements
197 }
197 }
198
198
199 pub fn config(&self) -> &Config {
199 pub fn config(&self) -> &Config {
200 &self.config
200 &self.config
201 }
201 }
202
202
203 /// For accessing repository files (in `.hg`), except for the store
203 /// For accessing repository files (in `.hg`), except for the store
204 /// (`.hg/store`).
204 /// (`.hg/store`).
205 pub fn hg_vfs(&self) -> Vfs<'_> {
205 pub fn hg_vfs(&self) -> Vfs<'_> {
206 Vfs { base: &self.dot_hg }
206 Vfs { base: &self.dot_hg }
207 }
207 }
208
208
209 /// For accessing repository store files (in `.hg/store`)
209 /// For accessing repository store files (in `.hg/store`)
210 pub fn store_vfs(&self) -> Vfs<'_> {
210 pub fn store_vfs(&self) -> Vfs<'_> {
211 Vfs { base: &self.store }
211 Vfs { base: &self.store }
212 }
212 }
213
213
214 /// For accessing the working copy
214 /// For accessing the working copy
215
215 pub fn working_directory_vfs(&self) -> Vfs<'_> {
216 // The undescore prefix silences the "never used" warning. Remove before
217 // using.
218 pub fn _working_directory_vfs(&self) -> Vfs<'_> {
219 Vfs {
216 Vfs {
220 base: &self.working_directory,
217 base: &self.working_directory,
221 }
218 }
222 }
219 }
223
220
224 pub fn dirstate_parents(
221 pub fn dirstate_parents(
225 &self,
222 &self,
226 ) -> Result<crate::dirstate::DirstateParents, HgError> {
223 ) -> Result<crate::dirstate::DirstateParents, HgError> {
227 let dirstate = self.hg_vfs().mmap_open("dirstate")?;
224 let dirstate = self.hg_vfs().mmap_open("dirstate")?;
228 let parents =
225 let parents =
229 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?;
226 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?;
230 Ok(parents.clone())
227 Ok(parents.clone())
231 }
228 }
232 }
229 }
233
230
234 impl Vfs<'_> {
231 impl Vfs<'_> {
235 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
232 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
236 self.base.join(relative_path)
233 self.base.join(relative_path)
237 }
234 }
238
235
239 pub fn read(
236 pub fn read(
240 &self,
237 &self,
241 relative_path: impl AsRef<Path>,
238 relative_path: impl AsRef<Path>,
242 ) -> Result<Vec<u8>, HgError> {
239 ) -> Result<Vec<u8>, HgError> {
243 let path = self.join(relative_path);
240 let path = self.join(relative_path);
244 std::fs::read(&path).when_reading_file(&path)
241 std::fs::read(&path).when_reading_file(&path)
245 }
242 }
246
243
247 pub fn mmap_open(
244 pub fn mmap_open(
248 &self,
245 &self,
249 relative_path: impl AsRef<Path>,
246 relative_path: impl AsRef<Path>,
250 ) -> Result<Mmap, HgError> {
247 ) -> Result<Mmap, HgError> {
251 let path = self.base.join(relative_path);
248 let path = self.base.join(relative_path);
252 let file = std::fs::File::open(&path).when_reading_file(&path)?;
249 let file = std::fs::File::open(&path).when_reading_file(&path)?;
253 // TODO: what are the safety requirements here?
250 // TODO: what are the safety requirements here?
254 let mmap = unsafe { MmapOptions::new().map(&file) }
251 let mmap = unsafe { MmapOptions::new().map(&file) }
255 .when_reading_file(&path)?;
252 .when_reading_file(&path)?;
256 Ok(mmap)
253 Ok(mmap)
257 }
254 }
258
255
259 pub fn rename(
256 pub fn rename(
260 &self,
257 &self,
261 relative_from: impl AsRef<Path>,
258 relative_from: impl AsRef<Path>,
262 relative_to: impl AsRef<Path>,
259 relative_to: impl AsRef<Path>,
263 ) -> Result<(), HgError> {
260 ) -> Result<(), HgError> {
264 let from = self.join(relative_from);
261 let from = self.join(relative_from);
265 let to = self.join(relative_to);
262 let to = self.join(relative_to);
266 std::fs::rename(&from, &to)
263 std::fs::rename(&from, &to)
267 .with_context(|| IoErrorContext::RenamingFile { from, to })
264 .with_context(|| IoErrorContext::RenamingFile { from, to })
268 }
265 }
269 }
266 }
@@ -1,507 +1,509 b''
1 extern crate log;
1 extern crate log;
2 use crate::ui::Ui;
2 use crate::ui::Ui;
3 use clap::App;
3 use clap::App;
4 use clap::AppSettings;
4 use clap::AppSettings;
5 use clap::Arg;
5 use clap::Arg;
6 use clap::ArgMatches;
6 use clap::ArgMatches;
7 use format_bytes::{format_bytes, join};
7 use format_bytes::{format_bytes, join};
8 use hg::config::Config;
8 use hg::config::Config;
9 use hg::repo::{Repo, RepoError};
9 use hg::repo::{Repo, RepoError};
10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
11 use hg::utils::SliceExt;
11 use hg::utils::SliceExt;
12 use std::ffi::OsString;
12 use std::ffi::OsString;
13 use std::path::PathBuf;
13 use std::path::PathBuf;
14 use std::process::Command;
14 use std::process::Command;
15
15
16 mod blackbox;
16 mod blackbox;
17 mod error;
17 mod error;
18 mod exitcode;
18 mod exitcode;
19 mod ui;
19 mod ui;
20 use error::CommandError;
20 use error::CommandError;
21
21
22 fn main_with_result(
22 fn main_with_result(
23 process_start_time: &blackbox::ProcessStartTime,
23 process_start_time: &blackbox::ProcessStartTime,
24 ui: &ui::Ui,
24 ui: &ui::Ui,
25 repo: Result<&Repo, &NoRepoInCwdError>,
25 repo: Result<&Repo, &NoRepoInCwdError>,
26 config: &Config,
26 config: &Config,
27 ) -> Result<(), CommandError> {
27 ) -> Result<(), CommandError> {
28 check_extensions(config)?;
28 check_extensions(config)?;
29
29
30 let app = App::new("rhg")
30 let app = App::new("rhg")
31 .global_setting(AppSettings::AllowInvalidUtf8)
31 .global_setting(AppSettings::AllowInvalidUtf8)
32 .global_setting(AppSettings::DisableVersion)
32 .global_setting(AppSettings::DisableVersion)
33 .setting(AppSettings::SubcommandRequired)
33 .setting(AppSettings::SubcommandRequired)
34 .setting(AppSettings::VersionlessSubcommands)
34 .setting(AppSettings::VersionlessSubcommands)
35 .arg(
35 .arg(
36 Arg::with_name("repository")
36 Arg::with_name("repository")
37 .help("repository root directory")
37 .help("repository root directory")
38 .short("-R")
38 .short("-R")
39 .long("--repository")
39 .long("--repository")
40 .value_name("REPO")
40 .value_name("REPO")
41 .takes_value(true)
41 .takes_value(true)
42 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
42 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
43 .global(true),
43 .global(true),
44 )
44 )
45 .arg(
45 .arg(
46 Arg::with_name("config")
46 Arg::with_name("config")
47 .help("set/override config option (use 'section.name=value')")
47 .help("set/override config option (use 'section.name=value')")
48 .long("--config")
48 .long("--config")
49 .value_name("CONFIG")
49 .value_name("CONFIG")
50 .takes_value(true)
50 .takes_value(true)
51 .global(true)
51 .global(true)
52 // Ok: `--config section.key1=val --config section.key2=val2`
52 // Ok: `--config section.key1=val --config section.key2=val2`
53 .multiple(true)
53 .multiple(true)
54 // Not ok: `--config section.key1=val section.key2=val2`
54 // Not ok: `--config section.key1=val section.key2=val2`
55 .number_of_values(1),
55 .number_of_values(1),
56 )
56 )
57 .arg(
57 .arg(
58 Arg::with_name("cwd")
58 Arg::with_name("cwd")
59 .help("change working directory")
59 .help("change working directory")
60 .long("--cwd")
60 .long("--cwd")
61 .value_name("DIR")
61 .value_name("DIR")
62 .takes_value(true)
62 .takes_value(true)
63 .global(true),
63 .global(true),
64 )
64 )
65 .version("0.0.1");
65 .version("0.0.1");
66 let app = add_subcommand_args(app);
66 let app = add_subcommand_args(app);
67
67
68 let matches = app.clone().get_matches_safe()?;
68 let matches = app.clone().get_matches_safe()?;
69
69
70 let (subcommand_name, subcommand_matches) = matches.subcommand();
70 let (subcommand_name, subcommand_matches) = matches.subcommand();
71 let run = subcommand_run_fn(subcommand_name)
71 let run = subcommand_run_fn(subcommand_name)
72 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
72 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
73 let subcommand_args = subcommand_matches
73 let subcommand_args = subcommand_matches
74 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
74 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
75
75
76 let invocation = CliInvocation {
76 let invocation = CliInvocation {
77 ui,
77 ui,
78 subcommand_args,
78 subcommand_args,
79 config,
79 config,
80 repo,
80 repo,
81 };
81 };
82 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
82 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
83 blackbox.log_command_start();
83 blackbox.log_command_start();
84 let result = run(&invocation);
84 let result = run(&invocation);
85 blackbox.log_command_end(exit_code(
85 blackbox.log_command_end(exit_code(
86 &result,
86 &result,
87 // TODO: show a warning or combine with original error if `get_bool`
87 // TODO: show a warning or combine with original error if `get_bool`
88 // returns an error
88 // returns an error
89 config
89 config
90 .get_bool(b"ui", b"detailed-exit-code")
90 .get_bool(b"ui", b"detailed-exit-code")
91 .unwrap_or(false),
91 .unwrap_or(false),
92 ));
92 ));
93 result
93 result
94 }
94 }
95
95
96 fn main() {
96 fn main() {
97 // Run this first, before we find out if the blackbox extension is even
97 // Run this first, before we find out if the blackbox extension is even
98 // enabled, in order to include everything in-between in the duration
98 // enabled, in order to include everything in-between in the duration
99 // measurements. Reading config files can be slow if they’re on NFS.
99 // measurements. Reading config files can be slow if they’re on NFS.
100 let process_start_time = blackbox::ProcessStartTime::now();
100 let process_start_time = blackbox::ProcessStartTime::now();
101
101
102 env_logger::init();
102 env_logger::init();
103 let ui = ui::Ui::new();
103 let ui = ui::Ui::new();
104
104
105 let early_args = EarlyArgs::parse(std::env::args_os());
105 let early_args = EarlyArgs::parse(std::env::args_os());
106
106
107 let initial_current_dir = early_args.cwd.map(|cwd| {
107 let initial_current_dir = early_args.cwd.map(|cwd| {
108 let cwd = get_path_from_bytes(&cwd);
108 let cwd = get_path_from_bytes(&cwd);
109 std::env::current_dir()
109 std::env::current_dir()
110 .and_then(|initial| {
110 .and_then(|initial| {
111 std::env::set_current_dir(cwd)?;
111 std::env::set_current_dir(cwd)?;
112 Ok(initial)
112 Ok(initial)
113 })
113 })
114 .unwrap_or_else(|error| {
114 .unwrap_or_else(|error| {
115 exit(
115 exit(
116 &None,
116 &None,
117 &ui,
117 &ui,
118 OnUnsupported::Abort,
118 OnUnsupported::Abort,
119 Err(CommandError::abort(format!(
119 Err(CommandError::abort(format!(
120 "abort: {}: '{}'",
120 "abort: {}: '{}'",
121 error,
121 error,
122 cwd.display()
122 cwd.display()
123 ))),
123 ))),
124 false,
124 false,
125 )
125 )
126 })
126 })
127 });
127 });
128
128
129 let non_repo_config =
129 let non_repo_config =
130 Config::load(early_args.config).unwrap_or_else(|error| {
130 Config::load(early_args.config).unwrap_or_else(|error| {
131 // Normally this is decided based on config, but we don’t have that
131 // Normally this is decided based on config, but we don’t have that
132 // available. As of this writing config loading never returns an
132 // available. As of this writing config loading never returns an
133 // "unsupported" error but that is not enforced by the type system.
133 // "unsupported" error but that is not enforced by the type system.
134 let on_unsupported = OnUnsupported::Abort;
134 let on_unsupported = OnUnsupported::Abort;
135
135
136 exit(
136 exit(
137 &initial_current_dir,
137 &initial_current_dir,
138 &ui,
138 &ui,
139 on_unsupported,
139 on_unsupported,
140 Err(error.into()),
140 Err(error.into()),
141 false,
141 false,
142 )
142 )
143 });
143 });
144
144
145 if let Some(repo_path_bytes) = &early_args.repo {
145 if let Some(repo_path_bytes) = &early_args.repo {
146 lazy_static::lazy_static! {
146 lazy_static::lazy_static! {
147 static ref SCHEME_RE: regex::bytes::Regex =
147 static ref SCHEME_RE: regex::bytes::Regex =
148 // Same as `_matchscheme` in `mercurial/util.py`
148 // Same as `_matchscheme` in `mercurial/util.py`
149 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
149 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
150 }
150 }
151 if SCHEME_RE.is_match(&repo_path_bytes) {
151 if SCHEME_RE.is_match(&repo_path_bytes) {
152 exit(
152 exit(
153 &initial_current_dir,
153 &initial_current_dir,
154 &ui,
154 &ui,
155 OnUnsupported::from_config(&ui, &non_repo_config),
155 OnUnsupported::from_config(&ui, &non_repo_config),
156 Err(CommandError::UnsupportedFeature {
156 Err(CommandError::UnsupportedFeature {
157 message: format_bytes!(
157 message: format_bytes!(
158 b"URL-like --repository {}",
158 b"URL-like --repository {}",
159 repo_path_bytes
159 repo_path_bytes
160 ),
160 ),
161 }),
161 }),
162 // TODO: show a warning or combine with original error if
162 // TODO: show a warning or combine with original error if
163 // `get_bool` returns an error
163 // `get_bool` returns an error
164 non_repo_config
164 non_repo_config
165 .get_bool(b"ui", b"detailed-exit-code")
165 .get_bool(b"ui", b"detailed-exit-code")
166 .unwrap_or(false),
166 .unwrap_or(false),
167 )
167 )
168 }
168 }
169 }
169 }
170 let repo_path = early_args.repo.as_deref().map(get_path_from_bytes);
170 let repo_path = early_args.repo.as_deref().map(get_path_from_bytes);
171 let repo_result = match Repo::find(&non_repo_config, repo_path) {
171 let repo_result = match Repo::find(&non_repo_config, repo_path) {
172 Ok(repo) => Ok(repo),
172 Ok(repo) => Ok(repo),
173 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
173 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
174 // Not finding a repo is not fatal yet, if `-R` was not given
174 // Not finding a repo is not fatal yet, if `-R` was not given
175 Err(NoRepoInCwdError { cwd: at })
175 Err(NoRepoInCwdError { cwd: at })
176 }
176 }
177 Err(error) => exit(
177 Err(error) => exit(
178 &initial_current_dir,
178 &initial_current_dir,
179 &ui,
179 &ui,
180 OnUnsupported::from_config(&ui, &non_repo_config),
180 OnUnsupported::from_config(&ui, &non_repo_config),
181 Err(error.into()),
181 Err(error.into()),
182 // TODO: show a warning or combine with original error if
182 // TODO: show a warning or combine with original error if
183 // `get_bool` returns an error
183 // `get_bool` returns an error
184 non_repo_config
184 non_repo_config
185 .get_bool(b"ui", b"detailed-exit-code")
185 .get_bool(b"ui", b"detailed-exit-code")
186 .unwrap_or(false),
186 .unwrap_or(false),
187 ),
187 ),
188 };
188 };
189
189
190 let config = if let Ok(repo) = &repo_result {
190 let config = if let Ok(repo) = &repo_result {
191 repo.config()
191 repo.config()
192 } else {
192 } else {
193 &non_repo_config
193 &non_repo_config
194 };
194 };
195 let on_unsupported = OnUnsupported::from_config(&ui, config);
195 let on_unsupported = OnUnsupported::from_config(&ui, config);
196
196
197 let result = main_with_result(
197 let result = main_with_result(
198 &process_start_time,
198 &process_start_time,
199 &ui,
199 &ui,
200 repo_result.as_ref(),
200 repo_result.as_ref(),
201 config,
201 config,
202 );
202 );
203 exit(
203 exit(
204 &initial_current_dir,
204 &initial_current_dir,
205 &ui,
205 &ui,
206 on_unsupported,
206 on_unsupported,
207 result,
207 result,
208 // TODO: show a warning or combine with original error if `get_bool`
208 // TODO: show a warning or combine with original error if `get_bool`
209 // returns an error
209 // returns an error
210 config
210 config
211 .get_bool(b"ui", b"detailed-exit-code")
211 .get_bool(b"ui", b"detailed-exit-code")
212 .unwrap_or(false),
212 .unwrap_or(false),
213 )
213 )
214 }
214 }
215
215
216 fn exit_code(
216 fn exit_code(
217 result: &Result<(), CommandError>,
217 result: &Result<(), CommandError>,
218 use_detailed_exit_code: bool,
218 use_detailed_exit_code: bool,
219 ) -> i32 {
219 ) -> i32 {
220 match result {
220 match result {
221 Ok(()) => exitcode::OK,
221 Ok(()) => exitcode::OK,
222 Err(CommandError::Abort {
222 Err(CommandError::Abort {
223 message: _,
223 message: _,
224 detailed_exit_code,
224 detailed_exit_code,
225 }) => {
225 }) => {
226 if use_detailed_exit_code {
226 if use_detailed_exit_code {
227 *detailed_exit_code
227 *detailed_exit_code
228 } else {
228 } else {
229 exitcode::ABORT
229 exitcode::ABORT
230 }
230 }
231 }
231 }
232 Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL,
232 Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL,
233
233
234 // Exit with a specific code and no error message to let a potential
234 // Exit with a specific code and no error message to let a potential
235 // wrapper script fallback to Python-based Mercurial.
235 // wrapper script fallback to Python-based Mercurial.
236 Err(CommandError::UnsupportedFeature { .. }) => {
236 Err(CommandError::UnsupportedFeature { .. }) => {
237 exitcode::UNIMPLEMENTED
237 exitcode::UNIMPLEMENTED
238 }
238 }
239 }
239 }
240 }
240 }
241
241
242 fn exit(
242 fn exit(
243 initial_current_dir: &Option<PathBuf>,
243 initial_current_dir: &Option<PathBuf>,
244 ui: &Ui,
244 ui: &Ui,
245 mut on_unsupported: OnUnsupported,
245 mut on_unsupported: OnUnsupported,
246 result: Result<(), CommandError>,
246 result: Result<(), CommandError>,
247 use_detailed_exit_code: bool,
247 use_detailed_exit_code: bool,
248 ) -> ! {
248 ) -> ! {
249 if let (
249 if let (
250 OnUnsupported::Fallback { executable },
250 OnUnsupported::Fallback { executable },
251 Err(CommandError::UnsupportedFeature { .. }),
251 Err(CommandError::UnsupportedFeature { .. }),
252 ) = (&on_unsupported, &result)
252 ) = (&on_unsupported, &result)
253 {
253 {
254 let mut args = std::env::args_os();
254 let mut args = std::env::args_os();
255 let executable_path = get_path_from_bytes(&executable);
255 let executable_path = get_path_from_bytes(&executable);
256 let this_executable = args.next().expect("exepcted argv[0] to exist");
256 let this_executable = args.next().expect("exepcted argv[0] to exist");
257 if executable_path == &PathBuf::from(this_executable) {
257 if executable_path == &PathBuf::from(this_executable) {
258 // Avoid spawning infinitely many processes until resource
258 // Avoid spawning infinitely many processes until resource
259 // exhaustion.
259 // exhaustion.
260 let _ = ui.write_stderr(&format_bytes!(
260 let _ = ui.write_stderr(&format_bytes!(
261 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
261 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
262 points to `rhg` itself.\n",
262 points to `rhg` itself.\n",
263 executable
263 executable
264 ));
264 ));
265 on_unsupported = OnUnsupported::Abort
265 on_unsupported = OnUnsupported::Abort
266 } else {
266 } else {
267 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
267 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
268 let mut command = Command::new(executable_path);
268 let mut command = Command::new(executable_path);
269 command.args(args);
269 command.args(args);
270 if let Some(initial) = initial_current_dir {
270 if let Some(initial) = initial_current_dir {
271 command.current_dir(initial);
271 command.current_dir(initial);
272 }
272 }
273 let result = command.status();
273 let result = command.status();
274 match result {
274 match result {
275 Ok(status) => std::process::exit(
275 Ok(status) => std::process::exit(
276 status.code().unwrap_or(exitcode::ABORT),
276 status.code().unwrap_or(exitcode::ABORT),
277 ),
277 ),
278 Err(error) => {
278 Err(error) => {
279 let _ = ui.write_stderr(&format_bytes!(
279 let _ = ui.write_stderr(&format_bytes!(
280 b"tried to fall back to a '{}' sub-process but got error {}\n",
280 b"tried to fall back to a '{}' sub-process but got error {}\n",
281 executable, format_bytes::Utf8(error)
281 executable, format_bytes::Utf8(error)
282 ));
282 ));
283 on_unsupported = OnUnsupported::Abort
283 on_unsupported = OnUnsupported::Abort
284 }
284 }
285 }
285 }
286 }
286 }
287 }
287 }
288 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
288 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
289 }
289 }
290
290
291 fn exit_no_fallback(
291 fn exit_no_fallback(
292 ui: &Ui,
292 ui: &Ui,
293 on_unsupported: OnUnsupported,
293 on_unsupported: OnUnsupported,
294 result: Result<(), CommandError>,
294 result: Result<(), CommandError>,
295 use_detailed_exit_code: bool,
295 use_detailed_exit_code: bool,
296 ) -> ! {
296 ) -> ! {
297 match &result {
297 match &result {
298 Ok(_) => {}
298 Ok(_) => {}
299 Err(CommandError::Unsuccessful) => {}
299 Err(CommandError::Unsuccessful) => {}
300 Err(CommandError::Abort {
300 Err(CommandError::Abort {
301 message,
301 message,
302 detailed_exit_code: _,
302 detailed_exit_code: _,
303 }) => {
303 }) => {
304 if !message.is_empty() {
304 if !message.is_empty() {
305 // Ignore errors when writing to stderr, we’re already exiting
305 // Ignore errors when writing to stderr, we’re already exiting
306 // with failure code so there’s not much more we can do.
306 // with failure code so there’s not much more we can do.
307 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
307 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
308 }
308 }
309 }
309 }
310 Err(CommandError::UnsupportedFeature { message }) => {
310 Err(CommandError::UnsupportedFeature { message }) => {
311 match on_unsupported {
311 match on_unsupported {
312 OnUnsupported::Abort => {
312 OnUnsupported::Abort => {
313 let _ = ui.write_stderr(&format_bytes!(
313 let _ = ui.write_stderr(&format_bytes!(
314 b"unsupported feature: {}\n",
314 b"unsupported feature: {}\n",
315 message
315 message
316 ));
316 ));
317 }
317 }
318 OnUnsupported::AbortSilent => {}
318 OnUnsupported::AbortSilent => {}
319 OnUnsupported::Fallback { .. } => unreachable!(),
319 OnUnsupported::Fallback { .. } => unreachable!(),
320 }
320 }
321 }
321 }
322 }
322 }
323 std::process::exit(exit_code(&result, use_detailed_exit_code))
323 std::process::exit(exit_code(&result, use_detailed_exit_code))
324 }
324 }
325
325
326 macro_rules! subcommands {
326 macro_rules! subcommands {
327 ($( $command: ident )+) => {
327 ($( $command: ident )+) => {
328 mod commands {
328 mod commands {
329 $(
329 $(
330 pub mod $command;
330 pub mod $command;
331 )+
331 )+
332 }
332 }
333
333
334 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
334 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
335 app
335 app
336 $(
336 $(
337 .subcommand(commands::$command::args())
337 .subcommand(commands::$command::args())
338 )+
338 )+
339 }
339 }
340
340
341 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
341 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
342
342
343 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
343 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
344 match name {
344 match name {
345 $(
345 $(
346 stringify!($command) => Some(commands::$command::run),
346 stringify!($command) => Some(commands::$command::run),
347 )+
347 )+
348 _ => None,
348 _ => None,
349 }
349 }
350 }
350 }
351 };
351 };
352 }
352 }
353
353
354 subcommands! {
354 subcommands! {
355 cat
355 cat
356 debugdata
356 debugdata
357 debugrequirements
357 debugrequirements
358 files
358 files
359 root
359 root
360 config
360 config
361 status
361 }
362 }
363
362 pub struct CliInvocation<'a> {
364 pub struct CliInvocation<'a> {
363 ui: &'a Ui,
365 ui: &'a Ui,
364 subcommand_args: &'a ArgMatches<'a>,
366 subcommand_args: &'a ArgMatches<'a>,
365 config: &'a Config,
367 config: &'a Config,
366 /// References inside `Result` is a bit peculiar but allow
368 /// References inside `Result` is a bit peculiar but allow
367 /// `invocation.repo?` to work out with `&CliInvocation` since this
369 /// `invocation.repo?` to work out with `&CliInvocation` since this
368 /// `Result` type is `Copy`.
370 /// `Result` type is `Copy`.
369 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
371 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
370 }
372 }
371
373
372 struct NoRepoInCwdError {
374 struct NoRepoInCwdError {
373 cwd: PathBuf,
375 cwd: PathBuf,
374 }
376 }
375
377
376 /// CLI arguments to be parsed "early" in order to be able to read
378 /// CLI arguments to be parsed "early" in order to be able to read
377 /// configuration before using Clap. Ideally we would also use Clap for this,
379 /// configuration before using Clap. Ideally we would also use Clap for this,
378 /// see <https://github.com/clap-rs/clap/discussions/2366>.
380 /// see <https://github.com/clap-rs/clap/discussions/2366>.
379 ///
381 ///
380 /// These arguments are still declared when we do use Clap later, so that Clap
382 /// These arguments are still declared when we do use Clap later, so that Clap
381 /// does not return an error for their presence.
383 /// does not return an error for their presence.
382 struct EarlyArgs {
384 struct EarlyArgs {
383 /// Values of all `--config` arguments. (Possibly none)
385 /// Values of all `--config` arguments. (Possibly none)
384 config: Vec<Vec<u8>>,
386 config: Vec<Vec<u8>>,
385 /// Value of the `-R` or `--repository` argument, if any.
387 /// Value of the `-R` or `--repository` argument, if any.
386 repo: Option<Vec<u8>>,
388 repo: Option<Vec<u8>>,
387 /// Value of the `--cwd` argument, if any.
389 /// Value of the `--cwd` argument, if any.
388 cwd: Option<Vec<u8>>,
390 cwd: Option<Vec<u8>>,
389 }
391 }
390
392
391 impl EarlyArgs {
393 impl EarlyArgs {
392 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
394 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
393 let mut args = args.into_iter().map(get_bytes_from_os_str);
395 let mut args = args.into_iter().map(get_bytes_from_os_str);
394 let mut config = Vec::new();
396 let mut config = Vec::new();
395 let mut repo = None;
397 let mut repo = None;
396 let mut cwd = None;
398 let mut cwd = None;
397 // Use `while let` instead of `for` so that we can also call
399 // Use `while let` instead of `for` so that we can also call
398 // `args.next()` inside the loop.
400 // `args.next()` inside the loop.
399 while let Some(arg) = args.next() {
401 while let Some(arg) = args.next() {
400 if arg == b"--config" {
402 if arg == b"--config" {
401 if let Some(value) = args.next() {
403 if let Some(value) = args.next() {
402 config.push(value)
404 config.push(value)
403 }
405 }
404 } else if let Some(value) = arg.drop_prefix(b"--config=") {
406 } else if let Some(value) = arg.drop_prefix(b"--config=") {
405 config.push(value.to_owned())
407 config.push(value.to_owned())
406 }
408 }
407
409
408 if arg == b"--cwd" {
410 if arg == b"--cwd" {
409 if let Some(value) = args.next() {
411 if let Some(value) = args.next() {
410 cwd = Some(value)
412 cwd = Some(value)
411 }
413 }
412 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
414 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
413 cwd = Some(value.to_owned())
415 cwd = Some(value.to_owned())
414 }
416 }
415
417
416 if arg == b"--repository" || arg == b"-R" {
418 if arg == b"--repository" || arg == b"-R" {
417 if let Some(value) = args.next() {
419 if let Some(value) = args.next() {
418 repo = Some(value)
420 repo = Some(value)
419 }
421 }
420 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
422 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
421 repo = Some(value.to_owned())
423 repo = Some(value.to_owned())
422 } else if let Some(value) = arg.drop_prefix(b"-R") {
424 } else if let Some(value) = arg.drop_prefix(b"-R") {
423 repo = Some(value.to_owned())
425 repo = Some(value.to_owned())
424 }
426 }
425 }
427 }
426 Self { config, repo, cwd }
428 Self { config, repo, cwd }
427 }
429 }
428 }
430 }
429
431
430 /// What to do when encountering some unsupported feature.
432 /// What to do when encountering some unsupported feature.
431 ///
433 ///
432 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
434 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
433 enum OnUnsupported {
435 enum OnUnsupported {
434 /// Print an error message describing what feature is not supported,
436 /// Print an error message describing what feature is not supported,
435 /// and exit with code 252.
437 /// and exit with code 252.
436 Abort,
438 Abort,
437 /// Silently exit with code 252.
439 /// Silently exit with code 252.
438 AbortSilent,
440 AbortSilent,
439 /// Try running a Python implementation
441 /// Try running a Python implementation
440 Fallback { executable: Vec<u8> },
442 Fallback { executable: Vec<u8> },
441 }
443 }
442
444
443 impl OnUnsupported {
445 impl OnUnsupported {
444 const DEFAULT: Self = OnUnsupported::Abort;
446 const DEFAULT: Self = OnUnsupported::Abort;
445
447
446 fn from_config(ui: &Ui, config: &Config) -> Self {
448 fn from_config(ui: &Ui, config: &Config) -> Self {
447 match config
449 match config
448 .get(b"rhg", b"on-unsupported")
450 .get(b"rhg", b"on-unsupported")
449 .map(|value| value.to_ascii_lowercase())
451 .map(|value| value.to_ascii_lowercase())
450 .as_deref()
452 .as_deref()
451 {
453 {
452 Some(b"abort") => OnUnsupported::Abort,
454 Some(b"abort") => OnUnsupported::Abort,
453 Some(b"abort-silent") => OnUnsupported::AbortSilent,
455 Some(b"abort-silent") => OnUnsupported::AbortSilent,
454 Some(b"fallback") => OnUnsupported::Fallback {
456 Some(b"fallback") => OnUnsupported::Fallback {
455 executable: config
457 executable: config
456 .get(b"rhg", b"fallback-executable")
458 .get(b"rhg", b"fallback-executable")
457 .unwrap_or_else(|| {
459 .unwrap_or_else(|| {
458 exit_no_fallback(
460 exit_no_fallback(
459 ui,
461 ui,
460 Self::Abort,
462 Self::Abort,
461 Err(CommandError::abort(
463 Err(CommandError::abort(
462 "abort: 'rhg.on-unsupported=fallback' without \
464 "abort: 'rhg.on-unsupported=fallback' without \
463 'rhg.fallback-executable' set."
465 'rhg.fallback-executable' set."
464 )),
466 )),
465 false,
467 false,
466 )
468 )
467 })
469 })
468 .to_owned(),
470 .to_owned(),
469 },
471 },
470 None => Self::DEFAULT,
472 None => Self::DEFAULT,
471 Some(_) => {
473 Some(_) => {
472 // TODO: warn about unknown config value
474 // TODO: warn about unknown config value
473 Self::DEFAULT
475 Self::DEFAULT
474 }
476 }
475 }
477 }
476 }
478 }
477 }
479 }
478
480
479 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"];
481 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"];
480
482
481 fn check_extensions(config: &Config) -> Result<(), CommandError> {
483 fn check_extensions(config: &Config) -> Result<(), CommandError> {
482 let enabled = config.get_section_keys(b"extensions");
484 let enabled = config.get_section_keys(b"extensions");
483
485
484 let mut unsupported = enabled;
486 let mut unsupported = enabled;
485 for supported in SUPPORTED_EXTENSIONS {
487 for supported in SUPPORTED_EXTENSIONS {
486 unsupported.remove(supported);
488 unsupported.remove(supported);
487 }
489 }
488
490
489 if let Some(ignored_list) =
491 if let Some(ignored_list) =
490 config.get_simple_list(b"rhg", b"ignored-extensions")
492 config.get_simple_list(b"rhg", b"ignored-extensions")
491 {
493 {
492 for ignored in ignored_list {
494 for ignored in ignored_list {
493 unsupported.remove(ignored);
495 unsupported.remove(ignored);
494 }
496 }
495 }
497 }
496
498
497 if unsupported.is_empty() {
499 if unsupported.is_empty() {
498 Ok(())
500 Ok(())
499 } else {
501 } else {
500 Err(CommandError::UnsupportedFeature {
502 Err(CommandError::UnsupportedFeature {
501 message: format_bytes!(
503 message: format_bytes!(
502 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
504 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
503 join(unsupported, b", ")
505 join(unsupported, b", ")
504 ),
506 ),
505 })
507 })
506 }
508 }
507 }
509 }
General Comments 0
You need to be logged in to leave comments. Login now