##// END OF EJS Templates
rhg: support rhg status --rev --rev
Arseniy Alekseyev -
r52048:ac3859a8 default
parent child Browse files
Show More
@@ -0,0 +1,89 b''
1 use crate::errors::HgError;
2 use crate::matchers::Matcher;
3 use crate::repo::Repo;
4 use crate::revlog::manifest::Manifest;
5 use crate::utils::filter_map_results;
6 use crate::utils::hg_path::HgPath;
7 use crate::utils::merge_join_results_by;
8
9 use crate::Revision;
10
11 use itertools::EitherOrBoth;
12
13 #[derive(Debug, Copy, Clone)]
14 pub enum DiffStatus {
15 Removed,
16 Added,
17 Matching,
18 Modified,
19 }
20
21 pub struct StatusRevRev {
22 manifest1: Manifest,
23 manifest2: Manifest,
24 narrow_matcher: Box<dyn Matcher>,
25 }
26
27 fn manifest_for_rev(repo: &Repo, rev: Revision) -> Result<Manifest, HgError> {
28 repo.manifest_for_rev(rev.into()).map_err(|e| {
29 HgError::corrupted(format!(
30 "manifest lookup failed for revision {}: {}",
31 rev, e
32 ))
33 })
34 }
35
36 pub fn status_rev_rev_no_copies(
37 repo: &Repo,
38 rev1: Revision,
39 rev2: Revision,
40 narrow_matcher: Box<dyn Matcher>,
41 ) -> Result<StatusRevRev, HgError> {
42 let manifest1 = manifest_for_rev(repo, rev1)?;
43 let manifest2 = manifest_for_rev(repo, rev2)?;
44 Ok(StatusRevRev {
45 manifest1,
46 manifest2,
47 narrow_matcher,
48 })
49 }
50
51 impl StatusRevRev {
52 pub fn iter(
53 &self,
54 ) -> impl Iterator<Item = Result<(&HgPath, DiffStatus), HgError>> {
55 let iter1 = self.manifest1.iter();
56 let iter2 = self.manifest2.iter();
57
58 let merged =
59 merge_join_results_by(iter1, iter2, |i1, i2| i1.path.cmp(i2.path));
60
61 filter_map_results(merged, |entry| {
62 let (path, status) = match entry {
63 EitherOrBoth::Left(entry) => {
64 let path = entry.path;
65 (path, DiffStatus::Removed)
66 }
67 EitherOrBoth::Right(entry) => {
68 let path = entry.path;
69 (path, DiffStatus::Added)
70 }
71 EitherOrBoth::Both(entry1, entry2) => {
72 let path = entry1.path;
73 if entry1.node_id().unwrap() == entry2.node_id().unwrap()
74 && entry1.flags == entry2.flags
75 {
76 (path, DiffStatus::Matching)
77 } else {
78 (path, DiffStatus::Modified)
79 }
80 }
81 };
82 Ok(if self.narrow_matcher.matches(path) {
83 Some((path, status))
84 } else {
85 None
86 })
87 })
88 }
89 }
@@ -1,10 +1,12 b''
1 //! A distinction is made between operations and commands.
1 //! A distinction is made between operations and commands.
2 //! An operation is what can be done whereas a command is what is exposed by
2 //! An operation is what can be done whereas a command is what is exposed by
3 //! the cli. A single command can use several operations to achieve its goal.
3 //! the cli. A single command can use several operations to achieve its goal.
4
4
5 mod cat;
5 mod cat;
6 mod debugdata;
6 mod debugdata;
7 mod list_tracked_files;
7 mod list_tracked_files;
8 mod status_rev_rev;
8 pub use cat::{cat, CatOutput};
9 pub use cat::{cat, CatOutput};
9 pub use debugdata::{debug_data, DebugDataKind};
10 pub use debugdata::{debug_data, DebugDataKind};
10 pub use list_tracked_files::{list_rev_tracked_files, FilesForRev};
11 pub use list_tracked_files::{list_rev_tracked_files, FilesForRev};
12 pub use status_rev_rev::{status_rev_rev_no_copies, DiffStatus, StatusRevRev};
@@ -1,728 +1,815 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
3 // Copyright 2020, Georges Racinet <georges.racinets@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 use crate::error::CommandError;
8 use crate::error::CommandError;
9 use crate::ui::{
9 use crate::ui::{
10 format_pattern_file_warning, print_narrow_sparse_warnings, relative_paths,
10 format_pattern_file_warning, print_narrow_sparse_warnings, relative_paths,
11 RelativePaths, Ui,
11 RelativePaths, Ui,
12 };
12 };
13 use crate::utils::path_utils::RelativizePaths;
13 use crate::utils::path_utils::RelativizePaths;
14 use clap::Arg;
14 use clap::Arg;
15 use format_bytes::format_bytes;
15 use format_bytes::format_bytes;
16 use hg::config::Config;
16 use hg::config::Config;
17 use hg::dirstate::has_exec_bit;
17 use hg::dirstate::has_exec_bit;
18 use hg::dirstate::status::StatusPath;
18 use hg::dirstate::status::StatusPath;
19 use hg::dirstate::TruncatedTimestamp;
19 use hg::dirstate::TruncatedTimestamp;
20 use hg::errors::{HgError, IoResultExt};
20 use hg::errors::{HgError, IoResultExt};
21 use hg::filepatterns::parse_pattern_args;
21 use hg::filepatterns::parse_pattern_args;
22 use hg::lock::LockError;
22 use hg::lock::LockError;
23 use hg::manifest::Manifest;
23 use hg::manifest::Manifest;
24 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
24 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
25 use hg::repo::Repo;
25 use hg::repo::Repo;
26 use hg::utils::debug::debug_wait_for_file;
26 use hg::utils::debug::debug_wait_for_file;
27 use hg::utils::files::{
27 use hg::utils::files::{
28 get_bytes_from_os_str, get_bytes_from_os_string, get_path_from_bytes,
28 get_bytes_from_os_str, get_bytes_from_os_string, get_path_from_bytes,
29 };
29 };
30 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
30 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
31 use hg::DirstateStatus;
31 use hg::DirstateStatus;
32 use hg::PatternFileWarning;
32 use hg::PatternFileWarning;
33 use hg::Revision;
33 use hg::StatusError;
34 use hg::StatusError;
34 use hg::StatusOptions;
35 use hg::StatusOptions;
35 use hg::{self, narrow, sparse};
36 use hg::{self, narrow, sparse};
36 use log::info;
37 use log::info;
37 use rayon::prelude::*;
38 use rayon::prelude::*;
39 use std::borrow::Cow;
38 use std::io;
40 use std::io;
39 use std::mem::take;
41 use std::mem::take;
40 use std::path::PathBuf;
42 use std::path::PathBuf;
41
43
42 pub const HELP_TEXT: &str = "
44 pub const HELP_TEXT: &str = "
43 Show changed files in the working directory
45 Show changed files in the working directory
44
46
45 This is a pure Rust version of `hg status`.
47 This is a pure Rust version of `hg status`.
46
48
47 Some options might be missing, check the list below.
49 Some options might be missing, check the list below.
48 ";
50 ";
49
51
50 pub fn args() -> clap::Command {
52 pub fn args() -> clap::Command {
51 clap::command!("status")
53 clap::command!("status")
52 .alias("st")
54 .alias("st")
53 .about(HELP_TEXT)
55 .about(HELP_TEXT)
54 .arg(
56 .arg(
55 Arg::new("file")
57 Arg::new("file")
56 .value_parser(clap::value_parser!(std::ffi::OsString))
58 .value_parser(clap::value_parser!(std::ffi::OsString))
57 .help("show only these files")
59 .help("show only these files")
58 .action(clap::ArgAction::Append),
60 .action(clap::ArgAction::Append),
59 )
61 )
60 .arg(
62 .arg(
61 Arg::new("all")
63 Arg::new("all")
62 .help("show status of all files")
64 .help("show status of all files")
63 .short('A')
65 .short('A')
64 .action(clap::ArgAction::SetTrue)
66 .action(clap::ArgAction::SetTrue)
65 .long("all"),
67 .long("all"),
66 )
68 )
67 .arg(
69 .arg(
68 Arg::new("modified")
70 Arg::new("modified")
69 .help("show only modified files")
71 .help("show only modified files")
70 .short('m')
72 .short('m')
71 .action(clap::ArgAction::SetTrue)
73 .action(clap::ArgAction::SetTrue)
72 .long("modified"),
74 .long("modified"),
73 )
75 )
74 .arg(
76 .arg(
75 Arg::new("added")
77 Arg::new("added")
76 .help("show only added files")
78 .help("show only added files")
77 .short('a')
79 .short('a')
78 .action(clap::ArgAction::SetTrue)
80 .action(clap::ArgAction::SetTrue)
79 .long("added"),
81 .long("added"),
80 )
82 )
81 .arg(
83 .arg(
82 Arg::new("removed")
84 Arg::new("removed")
83 .help("show only removed files")
85 .help("show only removed files")
84 .short('r')
86 .short('r')
85 .action(clap::ArgAction::SetTrue)
87 .action(clap::ArgAction::SetTrue)
86 .long("removed"),
88 .long("removed"),
87 )
89 )
88 .arg(
90 .arg(
89 Arg::new("clean")
91 Arg::new("clean")
90 .help("show only clean files")
92 .help("show only clean files")
91 .short('c')
93 .short('c')
92 .action(clap::ArgAction::SetTrue)
94 .action(clap::ArgAction::SetTrue)
93 .long("clean"),
95 .long("clean"),
94 )
96 )
95 .arg(
97 .arg(
96 Arg::new("deleted")
98 Arg::new("deleted")
97 .help("show only deleted files")
99 .help("show only deleted files")
98 .short('d')
100 .short('d')
99 .action(clap::ArgAction::SetTrue)
101 .action(clap::ArgAction::SetTrue)
100 .long("deleted"),
102 .long("deleted"),
101 )
103 )
102 .arg(
104 .arg(
103 Arg::new("unknown")
105 Arg::new("unknown")
104 .help("show only unknown (not tracked) files")
106 .help("show only unknown (not tracked) files")
105 .short('u')
107 .short('u')
106 .action(clap::ArgAction::SetTrue)
108 .action(clap::ArgAction::SetTrue)
107 .long("unknown"),
109 .long("unknown"),
108 )
110 )
109 .arg(
111 .arg(
110 Arg::new("ignored")
112 Arg::new("ignored")
111 .help("show only ignored files")
113 .help("show only ignored files")
112 .short('i')
114 .short('i')
113 .action(clap::ArgAction::SetTrue)
115 .action(clap::ArgAction::SetTrue)
114 .long("ignored"),
116 .long("ignored"),
115 )
117 )
116 .arg(
118 .arg(
117 Arg::new("copies")
119 Arg::new("copies")
118 .help("show source of copied files (DEFAULT: ui.statuscopies)")
120 .help("show source of copied files (DEFAULT: ui.statuscopies)")
119 .short('C')
121 .short('C')
120 .action(clap::ArgAction::SetTrue)
122 .action(clap::ArgAction::SetTrue)
121 .long("copies"),
123 .long("copies"),
122 )
124 )
123 .arg(
125 .arg(
124 Arg::new("print0")
126 Arg::new("print0")
125 .help("end filenames with NUL, for use with xargs")
127 .help("end filenames with NUL, for use with xargs")
126 .short('0')
128 .short('0')
127 .action(clap::ArgAction::SetTrue)
129 .action(clap::ArgAction::SetTrue)
128 .long("print0"),
130 .long("print0"),
129 )
131 )
130 .arg(
132 .arg(
131 Arg::new("no-status")
133 Arg::new("no-status")
132 .help("hide status prefix")
134 .help("hide status prefix")
133 .short('n')
135 .short('n')
134 .action(clap::ArgAction::SetTrue)
136 .action(clap::ArgAction::SetTrue)
135 .long("no-status"),
137 .long("no-status"),
136 )
138 )
137 .arg(
139 .arg(
138 Arg::new("verbose")
140 Arg::new("verbose")
139 .help("enable additional output")
141 .help("enable additional output")
140 .short('v')
142 .short('v')
141 .action(clap::ArgAction::SetTrue)
143 .action(clap::ArgAction::SetTrue)
142 .long("verbose"),
144 .long("verbose"),
143 )
145 )
146 .arg(
147 Arg::new("rev")
148 .help("show difference from/to revision")
149 .long("rev")
150 .num_args(1)
151 .action(clap::ArgAction::Append)
152 .value_name("REV"),
153 )
154 }
155
156 fn parse_revpair(
157 repo: &Repo,
158 revs: Option<Vec<String>>,
159 ) -> Result<Option<(Revision, Revision)>, CommandError> {
160 let revs = match revs {
161 None => return Ok(None),
162 Some(revs) => revs,
163 };
164 if revs.is_empty() {
165 return Ok(None);
166 }
167 if revs.len() != 2 {
168 return Err(CommandError::unsupported("expected 0 or 2 --rev flags"));
169 }
170
171 let rev1 = &revs[0];
172 let rev2 = &revs[1];
173 let rev1 = hg::revset::resolve_single(rev1, repo)
174 .map_err(|e| (e, rev1.as_str()))?;
175 let rev2 = hg::revset::resolve_single(rev2, repo)
176 .map_err(|e| (e, rev2.as_str()))?;
177 Ok(Some((rev1, rev2)))
144 }
178 }
145
179
146 /// Pure data type allowing the caller to specify file states to display
180 /// Pure data type allowing the caller to specify file states to display
147 #[derive(Copy, Clone, Debug)]
181 #[derive(Copy, Clone, Debug)]
148 pub struct DisplayStates {
182 pub struct DisplayStates {
149 pub modified: bool,
183 pub modified: bool,
150 pub added: bool,
184 pub added: bool,
151 pub removed: bool,
185 pub removed: bool,
152 pub clean: bool,
186 pub clean: bool,
153 pub deleted: bool,
187 pub deleted: bool,
154 pub unknown: bool,
188 pub unknown: bool,
155 pub ignored: bool,
189 pub ignored: bool,
156 }
190 }
157
191
158 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
192 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
159 modified: true,
193 modified: true,
160 added: true,
194 added: true,
161 removed: true,
195 removed: true,
162 clean: false,
196 clean: false,
163 deleted: true,
197 deleted: true,
164 unknown: true,
198 unknown: true,
165 ignored: false,
199 ignored: false,
166 };
200 };
167
201
168 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
202 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
169 modified: true,
203 modified: true,
170 added: true,
204 added: true,
171 removed: true,
205 removed: true,
172 clean: true,
206 clean: true,
173 deleted: true,
207 deleted: true,
174 unknown: true,
208 unknown: true,
175 ignored: true,
209 ignored: true,
176 };
210 };
177
211
178 impl DisplayStates {
212 impl DisplayStates {
179 pub fn is_empty(&self) -> bool {
213 pub fn is_empty(&self) -> bool {
180 !(self.modified
214 !(self.modified
181 || self.added
215 || self.added
182 || self.removed
216 || self.removed
183 || self.clean
217 || self.clean
184 || self.deleted
218 || self.deleted
185 || self.unknown
219 || self.unknown
186 || self.ignored)
220 || self.ignored)
187 }
221 }
188 }
222 }
189
223
190 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
224 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
191 Ok(repo.dirstate_parents()?.is_merge())
225 Ok(repo.dirstate_parents()?.is_merge())
192 }
226 }
193
227
194 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
228 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
195 // These are all the known values for the [fname] argument of
229 // These are all the known values for the [fname] argument of
196 // [addunfinished] function in [state.py]
230 // [addunfinished] function in [state.py]
197 let known_state_files: &[&str] = &[
231 let known_state_files: &[&str] = &[
198 "bisect.state",
232 "bisect.state",
199 "graftstate",
233 "graftstate",
200 "histedit-state",
234 "histedit-state",
201 "rebasestate",
235 "rebasestate",
202 "shelvedstate",
236 "shelvedstate",
203 "transplant/journal",
237 "transplant/journal",
204 "updatestate",
238 "updatestate",
205 ];
239 ];
206 if has_unfinished_merge(repo)? {
240 if has_unfinished_merge(repo)? {
207 return Ok(true);
241 return Ok(true);
208 };
242 };
209 for f in known_state_files {
243 for f in known_state_files {
210 if repo.hg_vfs().join(f).exists() {
244 if repo.hg_vfs().join(f).exists() {
211 return Ok(true);
245 return Ok(true);
212 }
246 }
213 }
247 }
214 Ok(false)
248 Ok(false)
215 }
249 }
216
250
217 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
251 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
218 // TODO: lift these limitations
252 // TODO: lift these limitations
219 if invocation
253 if invocation
220 .config
254 .config
221 .get(b"commands", b"status.terse")
255 .get(b"commands", b"status.terse")
222 .is_some()
256 .is_some()
223 {
257 {
224 return Err(CommandError::unsupported(
258 return Err(CommandError::unsupported(
225 "status.terse is not yet supported with rhg status",
259 "status.terse is not yet supported with rhg status",
226 ));
260 ));
227 }
261 }
228
262
229 let ui = invocation.ui;
263 let ui = invocation.ui;
230 let config = invocation.config;
264 let config = invocation.config;
231 let args = invocation.subcommand_args;
265 let args = invocation.subcommand_args;
232
266
267 let revs = args.get_many::<String>("rev");
233 let print0 = args.get_flag("print0");
268 let print0 = args.get_flag("print0");
234 let verbose = args.get_flag("verbose")
269 let verbose = args.get_flag("verbose")
235 || config.get_bool(b"ui", b"verbose")?
270 || config.get_bool(b"ui", b"verbose")?
236 || config.get_bool(b"commands", b"status.verbose")?;
271 || config.get_bool(b"commands", b"status.verbose")?;
237 let verbose = verbose && !print0;
272 let verbose = verbose && !print0;
238
273
239 let all = args.get_flag("all");
274 let all = args.get_flag("all");
240 let display_states = if all {
275 let display_states = if all {
241 // TODO when implementing `--quiet`: it excludes clean files
276 // TODO when implementing `--quiet`: it excludes clean files
242 // from `--all`
277 // from `--all`
243 ALL_DISPLAY_STATES
278 ALL_DISPLAY_STATES
244 } else {
279 } else {
245 let requested = DisplayStates {
280 let requested = DisplayStates {
246 modified: args.get_flag("modified"),
281 modified: args.get_flag("modified"),
247 added: args.get_flag("added"),
282 added: args.get_flag("added"),
248 removed: args.get_flag("removed"),
283 removed: args.get_flag("removed"),
249 clean: args.get_flag("clean"),
284 clean: args.get_flag("clean"),
250 deleted: args.get_flag("deleted"),
285 deleted: args.get_flag("deleted"),
251 unknown: args.get_flag("unknown"),
286 unknown: args.get_flag("unknown"),
252 ignored: args.get_flag("ignored"),
287 ignored: args.get_flag("ignored"),
253 };
288 };
254 if requested.is_empty() {
289 if requested.is_empty() {
255 DEFAULT_DISPLAY_STATES
290 DEFAULT_DISPLAY_STATES
256 } else {
291 } else {
257 requested
292 requested
258 }
293 }
259 };
294 };
260 let no_status = args.get_flag("no-status");
295 let no_status = args.get_flag("no-status");
261 let list_copies = all
296 let list_copies = all
262 || args.get_flag("copies")
297 || args.get_flag("copies")
263 || config.get_bool(b"ui", b"statuscopies")?;
298 || config.get_bool(b"ui", b"statuscopies")?;
264
299
265 let repo = invocation.repo?;
300 let repo = invocation.repo?;
301 let revpair = parse_revpair(repo, revs.map(|i| i.cloned().collect()))?;
266
302
267 if verbose && has_unfinished_state(repo)? {
303 if verbose && has_unfinished_state(repo)? {
268 return Err(CommandError::unsupported(
304 return Err(CommandError::unsupported(
269 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
305 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
270 ));
306 ));
271 }
307 }
272
308
273 let mut dmap = repo.dirstate_map_mut()?;
309 let mut dmap = repo.dirstate_map_mut()?;
274
310
275 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
311 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
276
312
277 let options = StatusOptions {
313 let options = StatusOptions {
278 check_exec,
314 check_exec,
279 list_clean: display_states.clean,
315 list_clean: display_states.clean,
280 list_unknown: display_states.unknown,
316 list_unknown: display_states.unknown,
281 list_ignored: display_states.ignored,
317 list_ignored: display_states.ignored,
282 list_copies,
318 list_copies,
283 collect_traversed_dirs: false,
319 collect_traversed_dirs: false,
284 };
320 };
285
321
286 type StatusResult<'a> =
322 type StatusResult<'a> =
287 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
323 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
288
324
289 let relative_status = config
325 let relative_status = config
290 .get_option(b"commands", b"status.relative")?
326 .get_option(b"commands", b"status.relative")?
291 .expect("commands.status.relative should have a default value");
327 .expect("commands.status.relative should have a default value");
292
328
293 let relativize_paths = relative_status || {
329 let relativize_paths = relative_status || {
294 // See in Python code with `getuipathfn` usage in `commands.py`.
330 // See in Python code with `getuipathfn` usage in `commands.py`.
295 let legacy_relative_behavior = args.contains_id("file");
331 let legacy_relative_behavior = args.contains_id("file");
296 match relative_paths(invocation.config)? {
332 match relative_paths(invocation.config)? {
297 RelativePaths::Legacy => legacy_relative_behavior,
333 RelativePaths::Legacy => legacy_relative_behavior,
298 RelativePaths::Bool(v) => v,
334 RelativePaths::Bool(v) => v,
299 }
335 }
300 };
336 };
301
337
302 let mut output = DisplayStatusPaths {
338 let mut output = DisplayStatusPaths {
303 ui,
339 ui,
304 no_status,
340 no_status,
305 relativize: if relativize_paths {
341 relativize: if relativize_paths {
306 Some(RelativizePaths::new(repo)?)
342 Some(RelativizePaths::new(repo)?)
307 } else {
343 } else {
308 None
344 None
309 },
345 },
310 print0,
346 print0,
311 };
347 };
312
348
313 let after_status = |res: StatusResult| -> Result<_, CommandError> {
349 let after_status = |res: StatusResult| -> Result<_, CommandError> {
314 let (mut ds_status, pattern_warnings) = res?;
350 let (mut ds_status, pattern_warnings) = res?;
315 for warning in pattern_warnings {
351 for warning in pattern_warnings {
316 ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
352 ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
317 }
353 }
318
354
319 for (path, error) in take(&mut ds_status.bad) {
355 for (path, error) in take(&mut ds_status.bad) {
320 let error = match error {
356 let error = match error {
321 hg::BadMatch::OsError(code) => {
357 hg::BadMatch::OsError(code) => {
322 std::io::Error::from_raw_os_error(code).to_string()
358 std::io::Error::from_raw_os_error(code).to_string()
323 }
359 }
324 hg::BadMatch::BadType(ty) => {
360 hg::BadMatch::BadType(ty) => {
325 format!("unsupported file type (type is {})", ty)
361 format!("unsupported file type (type is {})", ty)
326 }
362 }
327 };
363 };
328 ui.write_stderr(&format_bytes!(
364 ui.write_stderr(&format_bytes!(
329 b"{}: {}\n",
365 b"{}: {}\n",
330 path.as_bytes(),
366 path.as_bytes(),
331 error.as_bytes()
367 error.as_bytes()
332 ))?
368 ))?
333 }
369 }
334 if !ds_status.unsure.is_empty() {
370 if !ds_status.unsure.is_empty() {
335 info!(
371 info!(
336 "Files to be rechecked by retrieval from filelog: {:?}",
372 "Files to be rechecked by retrieval from filelog: {:?}",
337 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
373 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
338 );
374 );
339 }
375 }
340 let mut fixup = Vec::new();
376 let mut fixup = Vec::new();
341 if !ds_status.unsure.is_empty()
377 if !ds_status.unsure.is_empty()
342 && (display_states.modified || display_states.clean)
378 && (display_states.modified || display_states.clean)
343 {
379 {
344 let p1 = repo.dirstate_parents()?.p1;
380 let p1 = repo.dirstate_parents()?.p1;
345 let manifest = repo.manifest_for_node(p1).map_err(|e| {
381 let manifest = repo.manifest_for_node(p1).map_err(|e| {
346 CommandError::from((e, &*format!("{:x}", p1.short())))
382 CommandError::from((e, &*format!("{:x}", p1.short())))
347 })?;
383 })?;
348 let working_directory_vfs = repo.working_directory_vfs();
384 let working_directory_vfs = repo.working_directory_vfs();
349 let store_vfs = repo.store_vfs();
385 let store_vfs = repo.store_vfs();
350 let res: Vec<_> = take(&mut ds_status.unsure)
386 let res: Vec<_> = take(&mut ds_status.unsure)
351 .into_par_iter()
387 .into_par_iter()
352 .map(|to_check| {
388 .map(|to_check| {
353 // The compiler seems to get a bit confused with complex
389 // The compiler seems to get a bit confused with complex
354 // inference when using a parallel iterator + map
390 // inference when using a parallel iterator + map
355 // + map_err + collect, so let's just inline some of the
391 // + map_err + collect, so let's just inline some of the
356 // logic.
392 // logic.
357 match unsure_is_modified(
393 match unsure_is_modified(
358 working_directory_vfs,
394 working_directory_vfs,
359 store_vfs,
395 store_vfs,
360 check_exec,
396 check_exec,
361 &manifest,
397 &manifest,
362 &to_check.path,
398 &to_check.path,
363 ) {
399 ) {
364 Err(HgError::IoError { .. }) => {
400 Err(HgError::IoError { .. }) => {
365 // IO errors most likely stem from the file being
401 // IO errors most likely stem from the file being
366 // deleted even though we know it's in the
402 // deleted even though we know it's in the
367 // dirstate.
403 // dirstate.
368 Ok((to_check, UnsureOutcome::Deleted))
404 Ok((to_check, UnsureOutcome::Deleted))
369 }
405 }
370 Ok(outcome) => Ok((to_check, outcome)),
406 Ok(outcome) => Ok((to_check, outcome)),
371 Err(e) => Err(e),
407 Err(e) => Err(e),
372 }
408 }
373 })
409 })
374 .collect::<Result<_, _>>()?;
410 .collect::<Result<_, _>>()?;
375 for (status_path, outcome) in res.into_iter() {
411 for (status_path, outcome) in res.into_iter() {
376 match outcome {
412 match outcome {
377 UnsureOutcome::Clean => {
413 UnsureOutcome::Clean => {
378 if display_states.clean {
414 if display_states.clean {
379 ds_status.clean.push(status_path.clone());
415 ds_status.clean.push(status_path.clone());
380 }
416 }
381 fixup.push(status_path.path.into_owned())
417 fixup.push(status_path.path.into_owned())
382 }
418 }
383 UnsureOutcome::Modified => {
419 UnsureOutcome::Modified => {
384 if display_states.modified {
420 if display_states.modified {
385 ds_status.modified.push(status_path);
421 ds_status.modified.push(status_path);
386 }
422 }
387 }
423 }
388 UnsureOutcome::Deleted => {
424 UnsureOutcome::Deleted => {
389 if display_states.deleted {
425 if display_states.deleted {
390 ds_status.deleted.push(status_path);
426 ds_status.deleted.push(status_path);
391 }
427 }
392 }
428 }
393 }
429 }
394 }
430 }
395 }
431 }
396
432
397 let dirstate_write_needed = ds_status.dirty;
433 let dirstate_write_needed = ds_status.dirty;
398 let filesystem_time_at_status_start =
434 let filesystem_time_at_status_start =
399 ds_status.filesystem_time_at_status_start;
435 ds_status.filesystem_time_at_status_start;
400
436
401 output.output(display_states, ds_status)?;
437 output.output(display_states, ds_status)?;
402
438
403 Ok((
439 Ok((
404 fixup,
440 fixup,
405 dirstate_write_needed,
441 dirstate_write_needed,
406 filesystem_time_at_status_start,
442 filesystem_time_at_status_start,
407 ))
443 ))
408 };
444 };
409 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
445 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
446
447 match revpair {
448 Some((rev1, rev2)) => {
449 let mut ds_status = DirstateStatus::default();
450 if list_copies {
451 return Err(CommandError::unsupported(
452 "status --rev --rev with copy information is not implemented yet",
453 ));
454 }
455
456 let stat = hg::operations::status_rev_rev_no_copies(
457 repo,
458 rev1,
459 rev2,
460 narrow_matcher,
461 )?;
462 for entry in stat.iter() {
463 let (path, status) = entry?;
464 let path = StatusPath {
465 path: Cow::Borrowed(path),
466 copy_source: None,
467 };
468 match status {
469 hg::operations::DiffStatus::Removed => {
470 if display_states.removed {
471 ds_status.removed.push(path)
472 }
473 }
474 hg::operations::DiffStatus::Added => {
475 if display_states.added {
476 ds_status.added.push(path)
477 }
478 }
479 hg::operations::DiffStatus::Modified => {
480 if display_states.modified {
481 ds_status.modified.push(path)
482 }
483 }
484 hg::operations::DiffStatus::Matching => {
485 if display_states.clean {
486 ds_status.clean.push(path)
487 }
488 }
489 }
490 }
491 output.output(display_states, ds_status)?;
492 return Ok(());
493 }
494 None => (),
495 }
496
410 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
497 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
411 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
498 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
412 (true, true) => {
499 (true, true) => {
413 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
500 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
414 }
501 }
415 (true, false) => narrow_matcher,
502 (true, false) => narrow_matcher,
416 (false, true) => sparse_matcher,
503 (false, true) => sparse_matcher,
417 (false, false) => Box::new(AlwaysMatcher),
504 (false, false) => Box::new(AlwaysMatcher),
418 };
505 };
419 let matcher = match args.get_many::<std::ffi::OsString>("file") {
506 let matcher = match args.get_many::<std::ffi::OsString>("file") {
420 None => matcher,
507 None => matcher,
421 Some(files) => {
508 Some(files) => {
422 let patterns: Vec<Vec<u8>> = files
509 let patterns: Vec<Vec<u8>> = files
423 .filter(|s| !s.is_empty())
510 .filter(|s| !s.is_empty())
424 .map(get_bytes_from_os_str)
511 .map(get_bytes_from_os_str)
425 .collect();
512 .collect();
426 for file in &patterns {
513 for file in &patterns {
427 if file.starts_with(b"set:") {
514 if file.starts_with(b"set:") {
428 return Err(CommandError::unsupported("fileset"));
515 return Err(CommandError::unsupported("fileset"));
429 }
516 }
430 }
517 }
431 let cwd = hg::utils::current_dir()?;
518 let cwd = hg::utils::current_dir()?;
432 let root = repo.working_directory_path();
519 let root = repo.working_directory_path();
433 let ignore_patterns = parse_pattern_args(patterns, &cwd, root)?;
520 let ignore_patterns = parse_pattern_args(patterns, &cwd, root)?;
434 let files_matcher =
521 let files_matcher =
435 hg::matchers::PatternMatcher::new(ignore_patterns)?;
522 hg::matchers::PatternMatcher::new(ignore_patterns)?;
436 Box::new(IntersectionMatcher::new(
523 Box::new(IntersectionMatcher::new(
437 Box::new(files_matcher),
524 Box::new(files_matcher),
438 matcher,
525 matcher,
439 ))
526 ))
440 }
527 }
441 };
528 };
442
529
443 print_narrow_sparse_warnings(
530 print_narrow_sparse_warnings(
444 &narrow_warnings,
531 &narrow_warnings,
445 &sparse_warnings,
532 &sparse_warnings,
446 ui,
533 ui,
447 repo,
534 repo,
448 )?;
535 )?;
449 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
536 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
450 dmap.with_status(
537 dmap.with_status(
451 matcher.as_ref(),
538 matcher.as_ref(),
452 repo.working_directory_path().to_owned(),
539 repo.working_directory_path().to_owned(),
453 ignore_files(repo, config),
540 ignore_files(repo, config),
454 options,
541 options,
455 after_status,
542 after_status,
456 )?;
543 )?;
457
544
458 // Development config option to test write races
545 // Development config option to test write races
459 if let Err(e) =
546 if let Err(e) =
460 debug_wait_for_file(config, "status.pre-dirstate-write-file")
547 debug_wait_for_file(config, "status.pre-dirstate-write-file")
461 {
548 {
462 ui.write_stderr(e.as_bytes()).ok();
549 ui.write_stderr(e.as_bytes()).ok();
463 }
550 }
464
551
465 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
552 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
466 && !dirstate_write_needed
553 && !dirstate_write_needed
467 {
554 {
468 // Nothing to update
555 // Nothing to update
469 return Ok(());
556 return Ok(());
470 }
557 }
471
558
472 // Update the dirstate on disk if we can
559 // Update the dirstate on disk if we can
473 let with_lock_result =
560 let with_lock_result =
474 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
561 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
475 if let Some(mtime_boundary) = filesystem_time_at_status_start {
562 if let Some(mtime_boundary) = filesystem_time_at_status_start {
476 for hg_path in fixup {
563 for hg_path in fixup {
477 use std::os::unix::fs::MetadataExt;
564 use std::os::unix::fs::MetadataExt;
478 let fs_path = hg_path_to_path_buf(&hg_path)
565 let fs_path = hg_path_to_path_buf(&hg_path)
479 .expect("HgPath conversion");
566 .expect("HgPath conversion");
480 // Specifically do not reuse `fs_metadata` from
567 // Specifically do not reuse `fs_metadata` from
481 // `unsure_is_clean` which was needed before reading
568 // `unsure_is_clean` which was needed before reading
482 // contents. Here we access metadata again after reading
569 // contents. Here we access metadata again after reading
483 // content, in case it changed in the meantime.
570 // content, in case it changed in the meantime.
484 let metadata_res = repo
571 let metadata_res = repo
485 .working_directory_vfs()
572 .working_directory_vfs()
486 .symlink_metadata(&fs_path);
573 .symlink_metadata(&fs_path);
487 let fs_metadata = match metadata_res {
574 let fs_metadata = match metadata_res {
488 Ok(meta) => meta,
575 Ok(meta) => meta,
489 Err(err) => match err {
576 Err(err) => match err {
490 HgError::IoError { .. } => {
577 HgError::IoError { .. } => {
491 // The file has probably been deleted. In any
578 // The file has probably been deleted. In any
492 // case, it was in the dirstate before, so
579 // case, it was in the dirstate before, so
493 // let's ignore the error.
580 // let's ignore the error.
494 continue;
581 continue;
495 }
582 }
496 _ => return Err(err.into()),
583 _ => return Err(err.into()),
497 },
584 },
498 };
585 };
499 if let Some(mtime) =
586 if let Some(mtime) =
500 TruncatedTimestamp::for_reliable_mtime_of(
587 TruncatedTimestamp::for_reliable_mtime_of(
501 &fs_metadata,
588 &fs_metadata,
502 &mtime_boundary,
589 &mtime_boundary,
503 )
590 )
504 .when_reading_file(&fs_path)?
591 .when_reading_file(&fs_path)?
505 {
592 {
506 let mode = fs_metadata.mode();
593 let mode = fs_metadata.mode();
507 let size = fs_metadata.len();
594 let size = fs_metadata.len();
508 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
595 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
509 dirstate_write_needed = true
596 dirstate_write_needed = true
510 }
597 }
511 }
598 }
512 }
599 }
513 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
600 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
514 if dirstate_write_needed {
601 if dirstate_write_needed {
515 repo.write_dirstate()?
602 repo.write_dirstate()?
516 }
603 }
517 Ok(())
604 Ok(())
518 });
605 });
519 match with_lock_result {
606 match with_lock_result {
520 Ok(closure_result) => closure_result?,
607 Ok(closure_result) => closure_result?,
521 Err(LockError::AlreadyHeld) => {
608 Err(LockError::AlreadyHeld) => {
522 // Not updating the dirstate is not ideal but not critical:
609 // Not updating the dirstate is not ideal but not critical:
523 // don’t keep our caller waiting until some other Mercurial
610 // don’t keep our caller waiting until some other Mercurial
524 // process releases the lock.
611 // process releases the lock.
525 log::info!("not writing dirstate from `status`: lock is held")
612 log::info!("not writing dirstate from `status`: lock is held")
526 }
613 }
527 Err(LockError::Other(HgError::IoError { error, .. }))
614 Err(LockError::Other(HgError::IoError { error, .. }))
528 if error.kind() == io::ErrorKind::PermissionDenied =>
615 if error.kind() == io::ErrorKind::PermissionDenied =>
529 {
616 {
530 // `hg status` on a read-only repository is fine
617 // `hg status` on a read-only repository is fine
531 }
618 }
532 Err(LockError::Other(error)) => {
619 Err(LockError::Other(error)) => {
533 // Report other I/O errors
620 // Report other I/O errors
534 Err(error)?
621 Err(error)?
535 }
622 }
536 }
623 }
537 Ok(())
624 Ok(())
538 }
625 }
539
626
540 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
627 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
541 let mut ignore_files = Vec::new();
628 let mut ignore_files = Vec::new();
542 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
629 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
543 if repo_ignore.exists() {
630 if repo_ignore.exists() {
544 ignore_files.push(repo_ignore)
631 ignore_files.push(repo_ignore)
545 }
632 }
546 for (key, value) in config.iter_section(b"ui") {
633 for (key, value) in config.iter_section(b"ui") {
547 if key == b"ignore" || key.starts_with(b"ignore.") {
634 if key == b"ignore" || key.starts_with(b"ignore.") {
548 let path = get_path_from_bytes(value);
635 let path = get_path_from_bytes(value);
549 // TODO:Β expand "~/" and environment variable here, like Python
636 // TODO:Β expand "~/" and environment variable here, like Python
550 // does with `os.path.expanduser` and `os.path.expandvars`
637 // does with `os.path.expanduser` and `os.path.expandvars`
551
638
552 let joined = repo.working_directory_path().join(path);
639 let joined = repo.working_directory_path().join(path);
553 ignore_files.push(joined);
640 ignore_files.push(joined);
554 }
641 }
555 }
642 }
556 ignore_files
643 ignore_files
557 }
644 }
558
645
559 struct DisplayStatusPaths<'a> {
646 struct DisplayStatusPaths<'a> {
560 ui: &'a Ui,
647 ui: &'a Ui,
561 no_status: bool,
648 no_status: bool,
562 relativize: Option<RelativizePaths>,
649 relativize: Option<RelativizePaths>,
563 print0: bool,
650 print0: bool,
564 }
651 }
565
652
566 impl DisplayStatusPaths<'_> {
653 impl DisplayStatusPaths<'_> {
567 // Probably more elegant to use a Deref or Borrow trait rather than
654 // Probably more elegant to use a Deref or Borrow trait rather than
568 // harcode HgPathBuf, but probably not really useful at this point
655 // harcode HgPathBuf, but probably not really useful at this point
569 fn display(
656 fn display(
570 &self,
657 &self,
571 status_prefix: &[u8],
658 status_prefix: &[u8],
572 label: &'static str,
659 label: &'static str,
573 mut paths: Vec<StatusPath<'_>>,
660 mut paths: Vec<StatusPath<'_>>,
574 ) -> Result<(), CommandError> {
661 ) -> Result<(), CommandError> {
575 paths.sort_unstable();
662 paths.sort_unstable();
576 // TODO: get the stdout lock once for the whole loop
663 // TODO: get the stdout lock once for the whole loop
577 // instead of in each write
664 // instead of in each write
578 for StatusPath { path, copy_source } in paths {
665 for StatusPath { path, copy_source } in paths {
579 let relative_path;
666 let relative_path;
580 let relative_source;
667 let relative_source;
581 let (path, copy_source) = if let Some(relativize) =
668 let (path, copy_source) = if let Some(relativize) =
582 &self.relativize
669 &self.relativize
583 {
670 {
584 relative_path = relativize.relativize(&path);
671 relative_path = relativize.relativize(&path);
585 relative_source =
672 relative_source =
586 copy_source.as_ref().map(|s| relativize.relativize(s));
673 copy_source.as_ref().map(|s| relativize.relativize(s));
587 (&*relative_path, relative_source.as_deref())
674 (&*relative_path, relative_source.as_deref())
588 } else {
675 } else {
589 (path.as_bytes(), copy_source.as_ref().map(|s| s.as_bytes()))
676 (path.as_bytes(), copy_source.as_ref().map(|s| s.as_bytes()))
590 };
677 };
591 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
678 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
592 // in order to stream to stdout instead of allocating an
679 // in order to stream to stdout instead of allocating an
593 // itermediate `Vec<u8>`.
680 // itermediate `Vec<u8>`.
594 if !self.no_status {
681 if !self.no_status {
595 self.ui.write_stdout_labelled(status_prefix, label)?
682 self.ui.write_stdout_labelled(status_prefix, label)?
596 }
683 }
597 let linebreak = if self.print0 { b"\x00" } else { b"\n" };
684 let linebreak = if self.print0 { b"\x00" } else { b"\n" };
598 self.ui.write_stdout_labelled(
685 self.ui.write_stdout_labelled(
599 &format_bytes!(b"{}{}", path, linebreak),
686 &format_bytes!(b"{}{}", path, linebreak),
600 label,
687 label,
601 )?;
688 )?;
602 if let Some(source) = copy_source.filter(|_| !self.no_status) {
689 if let Some(source) = copy_source.filter(|_| !self.no_status) {
603 let label = "status.copied";
690 let label = "status.copied";
604 self.ui.write_stdout_labelled(
691 self.ui.write_stdout_labelled(
605 &format_bytes!(b" {}{}", source, linebreak),
692 &format_bytes!(b" {}{}", source, linebreak),
606 label,
693 label,
607 )?
694 )?
608 }
695 }
609 }
696 }
610 Ok(())
697 Ok(())
611 }
698 }
612
699
613 fn output(
700 fn output(
614 &mut self,
701 &mut self,
615 display_states: DisplayStates,
702 display_states: DisplayStates,
616 ds_status: DirstateStatus,
703 ds_status: DirstateStatus,
617 ) -> Result<(), CommandError> {
704 ) -> Result<(), CommandError> {
618 if display_states.modified {
705 if display_states.modified {
619 self.display(b"M ", "status.modified", ds_status.modified)?;
706 self.display(b"M ", "status.modified", ds_status.modified)?;
620 }
707 }
621 if display_states.added {
708 if display_states.added {
622 self.display(b"A ", "status.added", ds_status.added)?;
709 self.display(b"A ", "status.added", ds_status.added)?;
623 }
710 }
624 if display_states.removed {
711 if display_states.removed {
625 self.display(b"R ", "status.removed", ds_status.removed)?;
712 self.display(b"R ", "status.removed", ds_status.removed)?;
626 }
713 }
627 if display_states.deleted {
714 if display_states.deleted {
628 self.display(b"! ", "status.deleted", ds_status.deleted)?;
715 self.display(b"! ", "status.deleted", ds_status.deleted)?;
629 }
716 }
630 if display_states.unknown {
717 if display_states.unknown {
631 self.display(b"? ", "status.unknown", ds_status.unknown)?;
718 self.display(b"? ", "status.unknown", ds_status.unknown)?;
632 }
719 }
633 if display_states.ignored {
720 if display_states.ignored {
634 self.display(b"I ", "status.ignored", ds_status.ignored)?;
721 self.display(b"I ", "status.ignored", ds_status.ignored)?;
635 }
722 }
636 if display_states.clean {
723 if display_states.clean {
637 self.display(b"C ", "status.clean", ds_status.clean)?;
724 self.display(b"C ", "status.clean", ds_status.clean)?;
638 }
725 }
639 Ok(())
726 Ok(())
640 }
727 }
641 }
728 }
642
729
643 /// Outcome of the additional check for an ambiguous tracked file
730 /// Outcome of the additional check for an ambiguous tracked file
644 enum UnsureOutcome {
731 enum UnsureOutcome {
645 /// The file is actually clean
732 /// The file is actually clean
646 Clean,
733 Clean,
647 /// The file has been modified
734 /// The file has been modified
648 Modified,
735 Modified,
649 /// The file was deleted on disk (or became another type of fs entry)
736 /// The file was deleted on disk (or became another type of fs entry)
650 Deleted,
737 Deleted,
651 }
738 }
652
739
653 /// Check if a file is modified by comparing actual repo store and file system.
740 /// Check if a file is modified by comparing actual repo store and file system.
654 ///
741 ///
655 /// This meant to be used for those that the dirstate cannot resolve, due
742 /// This meant to be used for those that the dirstate cannot resolve, due
656 /// to time resolution limits.
743 /// to time resolution limits.
657 fn unsure_is_modified(
744 fn unsure_is_modified(
658 working_directory_vfs: hg::vfs::Vfs,
745 working_directory_vfs: hg::vfs::Vfs,
659 store_vfs: hg::vfs::Vfs,
746 store_vfs: hg::vfs::Vfs,
660 check_exec: bool,
747 check_exec: bool,
661 manifest: &Manifest,
748 manifest: &Manifest,
662 hg_path: &HgPath,
749 hg_path: &HgPath,
663 ) -> Result<UnsureOutcome, HgError> {
750 ) -> Result<UnsureOutcome, HgError> {
664 let vfs = working_directory_vfs;
751 let vfs = working_directory_vfs;
665 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
752 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
666 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
753 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
667 let is_symlink = fs_metadata.file_type().is_symlink();
754 let is_symlink = fs_metadata.file_type().is_symlink();
668
755
669 let entry = manifest
756 let entry = manifest
670 .find_by_path(hg_path)?
757 .find_by_path(hg_path)?
671 .expect("ambgious file not in p1");
758 .expect("ambgious file not in p1");
672
759
673 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
760 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
674 // dirstate
761 // dirstate
675 let fs_flags = if is_symlink {
762 let fs_flags = if is_symlink {
676 Some(b'l')
763 Some(b'l')
677 } else if check_exec && has_exec_bit(&fs_metadata) {
764 } else if check_exec && has_exec_bit(&fs_metadata) {
678 Some(b'x')
765 Some(b'x')
679 } else {
766 } else {
680 None
767 None
681 };
768 };
682
769
683 let entry_flags = if check_exec {
770 let entry_flags = if check_exec {
684 entry.flags
771 entry.flags
685 } else if entry.flags == Some(b'x') {
772 } else if entry.flags == Some(b'x') {
686 None
773 None
687 } else {
774 } else {
688 entry.flags
775 entry.flags
689 };
776 };
690
777
691 if entry_flags != fs_flags {
778 if entry_flags != fs_flags {
692 return Ok(UnsureOutcome::Modified);
779 return Ok(UnsureOutcome::Modified);
693 }
780 }
694 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
781 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
695 let fs_len = fs_metadata.len();
782 let fs_len = fs_metadata.len();
696 let file_node = entry.node_id()?;
783 let file_node = entry.node_id()?;
697 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
784 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
698 HgError::corrupted(format!(
785 HgError::corrupted(format!(
699 "filelog {:?} missing node {:?} from manifest",
786 "filelog {:?} missing node {:?} from manifest",
700 hg_path, file_node
787 hg_path, file_node
701 ))
788 ))
702 })?;
789 })?;
703 if filelog_entry.file_data_len_not_equal_to(fs_len) {
790 if filelog_entry.file_data_len_not_equal_to(fs_len) {
704 // No need to read file contents:
791 // No need to read file contents:
705 // it cannot be equal if it has a different length.
792 // it cannot be equal if it has a different length.
706 return Ok(UnsureOutcome::Modified);
793 return Ok(UnsureOutcome::Modified);
707 }
794 }
708
795
709 let p1_filelog_data = filelog_entry.data()?;
796 let p1_filelog_data = filelog_entry.data()?;
710 let p1_contents = p1_filelog_data.file_data()?;
797 let p1_contents = p1_filelog_data.file_data()?;
711 if p1_contents.len() as u64 != fs_len {
798 if p1_contents.len() as u64 != fs_len {
712 // No need to read file contents:
799 // No need to read file contents:
713 // it cannot be equal if it has a different length.
800 // it cannot be equal if it has a different length.
714 return Ok(UnsureOutcome::Modified);
801 return Ok(UnsureOutcome::Modified);
715 }
802 }
716
803
717 let fs_contents = if is_symlink {
804 let fs_contents = if is_symlink {
718 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
805 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
719 } else {
806 } else {
720 vfs.read(fs_path)?
807 vfs.read(fs_path)?
721 };
808 };
722
809
723 Ok(if p1_contents != &*fs_contents {
810 Ok(if p1_contents != &*fs_contents {
724 UnsureOutcome::Modified
811 UnsureOutcome::Modified
725 } else {
812 } else {
726 UnsureOutcome::Clean
813 UnsureOutcome::Clean
727 })
814 })
728 }
815 }
@@ -1,842 +1,849 b''
1 extern crate log;
1 extern crate log;
2 use crate::error::CommandError;
2 use crate::error::CommandError;
3 use crate::ui::{local_to_utf8, Ui};
3 use crate::ui::{local_to_utf8, Ui};
4 use clap::{command, Arg, ArgMatches};
4 use clap::{command, Arg, ArgMatches};
5 use format_bytes::{format_bytes, join};
5 use format_bytes::{format_bytes, join};
6 use hg::config::{Config, ConfigSource, PlainInfo};
6 use hg::config::{Config, ConfigSource, PlainInfo};
7 use hg::repo::{Repo, RepoError};
7 use hg::repo::{Repo, RepoError};
8 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
8 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
9 use hg::utils::SliceExt;
9 use hg::utils::SliceExt;
10 use hg::{exit_codes, requirements};
10 use hg::{exit_codes, requirements};
11 use std::borrow::Cow;
11 use std::borrow::Cow;
12 use std::collections::HashSet;
12 use std::collections::HashSet;
13 use std::ffi::OsString;
13 use std::ffi::OsString;
14 use std::os::unix::prelude::CommandExt;
14 use std::os::unix::prelude::CommandExt;
15 use std::path::PathBuf;
15 use std::path::PathBuf;
16 use std::process::Command;
16 use std::process::Command;
17
17
18 mod blackbox;
18 mod blackbox;
19 mod color;
19 mod color;
20 mod error;
20 mod error;
21 mod ui;
21 mod ui;
22 pub mod utils {
22 pub mod utils {
23 pub mod path_utils;
23 pub mod path_utils;
24 }
24 }
25
25
26 fn main_with_result(
26 fn main_with_result(
27 argv: Vec<OsString>,
27 argv: Vec<OsString>,
28 process_start_time: &blackbox::ProcessStartTime,
28 process_start_time: &blackbox::ProcessStartTime,
29 ui: &ui::Ui,
29 ui: &ui::Ui,
30 repo: Result<&Repo, &NoRepoInCwdError>,
30 repo: Result<&Repo, &NoRepoInCwdError>,
31 config: &Config,
31 config: &Config,
32 ) -> Result<(), CommandError> {
32 ) -> Result<(), CommandError> {
33 check_unsupported(config, repo)?;
33 check_unsupported(config, repo)?;
34
34
35 let app = command!()
35 let app = command!()
36 .subcommand_required(true)
36 .subcommand_required(true)
37 .arg(
37 .arg(
38 Arg::new("repository")
38 Arg::new("repository")
39 .help("repository root directory")
39 .help("repository root directory")
40 .short('R')
40 .short('R')
41 .value_name("REPO")
41 .value_name("REPO")
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::new("config")
46 Arg::new("config")
47 .help("set/override config option (use 'section.name=value')")
47 .help("set/override config option (use 'section.name=value')")
48 .value_name("CONFIG")
48 .value_name("CONFIG")
49 .global(true)
49 .global(true)
50 .long("config")
50 .long("config")
51 // Ok: `--config section.key1=val --config section.key2=val2`
51 // Ok: `--config section.key1=val --config section.key2=val2`
52 // Not ok: `--config section.key1=val section.key2=val2`
52 // Not ok: `--config section.key1=val section.key2=val2`
53 .action(clap::ArgAction::Append),
53 .action(clap::ArgAction::Append),
54 )
54 )
55 .arg(
55 .arg(
56 Arg::new("cwd")
56 Arg::new("cwd")
57 .help("change working directory")
57 .help("change working directory")
58 .value_name("DIR")
58 .value_name("DIR")
59 .long("cwd")
59 .long("cwd")
60 .global(true),
60 .global(true),
61 )
61 )
62 .arg(
62 .arg(
63 Arg::new("color")
63 Arg::new("color")
64 .help("when to colorize (boolean, always, auto, never, or debug)")
64 .help("when to colorize (boolean, always, auto, never, or debug)")
65 .value_name("TYPE")
65 .value_name("TYPE")
66 .long("color")
66 .long("color")
67 .global(true),
67 .global(true),
68 )
68 )
69 .version("0.0.1");
69 .version("0.0.1");
70 let app = add_subcommand_args(app);
70 let app = add_subcommand_args(app);
71
71
72 let matches = app.try_get_matches_from(argv.iter())?;
72 let matches = app.try_get_matches_from(argv.iter())?;
73
73
74 let (subcommand_name, subcommand_args) =
74 let (subcommand_name, subcommand_args) =
75 matches.subcommand().expect("subcommand required");
75 matches.subcommand().expect("subcommand required");
76
76
77 // Mercurial allows users to define "defaults" for commands, fallback
77 // Mercurial allows users to define "defaults" for commands, fallback
78 // if a default is detected for the current command
78 // if a default is detected for the current command
79 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes())?;
79 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes())?;
80 match defaults {
80 match defaults {
81 // Programmatic usage might set defaults to an empty string to unset
81 // Programmatic usage might set defaults to an empty string to unset
82 // it; allow that
82 // it; allow that
83 None | Some("") => {}
83 None | Some("") => {}
84 Some(_) => {
84 Some(_) => {
85 let msg = "`defaults` config set";
85 let msg = "`defaults` config set";
86 return Err(CommandError::unsupported(msg));
86 return Err(CommandError::unsupported(msg));
87 }
87 }
88 }
88 }
89
89
90 for prefix in ["pre", "post", "fail"].iter() {
90 for prefix in ["pre", "post", "fail"].iter() {
91 // Mercurial allows users to define generic hooks for commands,
91 // Mercurial allows users to define generic hooks for commands,
92 // fallback if any are detected
92 // fallback if any are detected
93 let item = format!("{}-{}", prefix, subcommand_name);
93 let item = format!("{}-{}", prefix, subcommand_name);
94 let hook_for_command =
94 let hook_for_command =
95 config.get_str_no_default(b"hooks", item.as_bytes())?;
95 config.get_str_no_default(b"hooks", item.as_bytes())?;
96 if hook_for_command.is_some() {
96 if hook_for_command.is_some() {
97 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
97 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
98 return Err(CommandError::unsupported(msg));
98 return Err(CommandError::unsupported(msg));
99 }
99 }
100 }
100 }
101 let run = subcommand_run_fn(subcommand_name)
101 let run = subcommand_run_fn(subcommand_name)
102 .expect("unknown subcommand name from clap despite Command::subcommand_required");
102 .expect("unknown subcommand name from clap despite Command::subcommand_required");
103
103
104 let invocation = CliInvocation {
104 let invocation = CliInvocation {
105 ui,
105 ui,
106 subcommand_args,
106 subcommand_args,
107 config,
107 config,
108 repo,
108 repo,
109 };
109 };
110
110
111 if let Ok(repo) = repo {
111 if let Ok(repo) = repo {
112 // We don't support subrepos, fallback if the subrepos file is present
112 // We don't support subrepos, fallback if the subrepos file is present
113 if repo.working_directory_vfs().join(".hgsub").exists() {
113 if repo.working_directory_vfs().join(".hgsub").exists() {
114 let msg = "subrepos (.hgsub is present)";
114 let msg = "subrepos (.hgsub is present)";
115 return Err(CommandError::unsupported(msg));
115 return Err(CommandError::unsupported(msg));
116 }
116 }
117 }
117 }
118
118
119 if config.is_extension_enabled(b"blackbox") {
119 if config.is_extension_enabled(b"blackbox") {
120 let blackbox =
120 let blackbox =
121 blackbox::Blackbox::new(&invocation, process_start_time)?;
121 blackbox::Blackbox::new(&invocation, process_start_time)?;
122 blackbox.log_command_start(argv.iter());
122 blackbox.log_command_start(argv.iter());
123 let result = run(&invocation);
123 let result = run(&invocation);
124 blackbox.log_command_end(
124 blackbox.log_command_end(
125 argv.iter(),
125 argv.iter(),
126 exit_code(
126 exit_code(
127 &result,
127 &result,
128 // TODO: show a warning or combine with original error if
128 // TODO: show a warning or combine with original error if
129 // `get_bool` returns an error
129 // `get_bool` returns an error
130 config
130 config
131 .get_bool(b"ui", b"detailed-exit-code")
131 .get_bool(b"ui", b"detailed-exit-code")
132 .unwrap_or(false),
132 .unwrap_or(false),
133 ),
133 ),
134 );
134 );
135 result
135 result
136 } else {
136 } else {
137 run(&invocation)
137 run(&invocation)
138 }
138 }
139 }
139 }
140
140
141 fn rhg_main(argv: Vec<OsString>) -> ! {
141 fn rhg_main(argv: Vec<OsString>) -> ! {
142 // Run this first, before we find out if the blackbox extension is even
142 // Run this first, before we find out if the blackbox extension is even
143 // enabled, in order to include everything in-between in the duration
143 // enabled, in order to include everything in-between in the duration
144 // measurements. Reading config files can be slow if they’re on NFS.
144 // measurements. Reading config files can be slow if they’re on NFS.
145 let process_start_time = blackbox::ProcessStartTime::now();
145 let process_start_time = blackbox::ProcessStartTime::now();
146
146
147 env_logger::init();
147 env_logger::init();
148
148
149 // Make sure nothing in a future version of `rhg` sets the global
149 // Make sure nothing in a future version of `rhg` sets the global
150 // threadpool before we can cap default threads. (This is also called
150 // threadpool before we can cap default threads. (This is also called
151 // in core because Python uses the same code path, we're adding a
151 // in core because Python uses the same code path, we're adding a
152 // redundant check.)
152 // redundant check.)
153 hg::utils::cap_default_rayon_threads()
153 hg::utils::cap_default_rayon_threads()
154 .expect("Rayon threadpool already initialized");
154 .expect("Rayon threadpool already initialized");
155
155
156 let early_args = EarlyArgs::parse(&argv);
156 let early_args = EarlyArgs::parse(&argv);
157
157
158 let initial_current_dir = early_args.cwd.map(|cwd| {
158 let initial_current_dir = early_args.cwd.map(|cwd| {
159 let cwd = get_path_from_bytes(&cwd);
159 let cwd = get_path_from_bytes(&cwd);
160 std::env::current_dir()
160 std::env::current_dir()
161 .and_then(|initial| {
161 .and_then(|initial| {
162 std::env::set_current_dir(cwd)?;
162 std::env::set_current_dir(cwd)?;
163 Ok(initial)
163 Ok(initial)
164 })
164 })
165 .unwrap_or_else(|error| {
165 .unwrap_or_else(|error| {
166 exit(
166 exit(
167 &argv,
167 &argv,
168 &None,
168 &None,
169 &Ui::new_infallible(&Config::empty()),
169 &Ui::new_infallible(&Config::empty()),
170 OnUnsupported::Abort,
170 OnUnsupported::Abort,
171 Err(CommandError::abort(format!(
171 Err(CommandError::abort(format!(
172 "abort: {}: '{}'",
172 "abort: {}: '{}'",
173 error,
173 error,
174 cwd.display()
174 cwd.display()
175 ))),
175 ))),
176 false,
176 false,
177 )
177 )
178 })
178 })
179 });
179 });
180
180
181 let mut non_repo_config =
181 let mut non_repo_config =
182 Config::load_non_repo().unwrap_or_else(|error| {
182 Config::load_non_repo().unwrap_or_else(|error| {
183 // Normally this is decided based on config, but we don’t have that
183 // Normally this is decided based on config, but we don’t have that
184 // available. As of this writing config loading never returns an
184 // available. As of this writing config loading never returns an
185 // "unsupported" error but that is not enforced by the type system.
185 // "unsupported" error but that is not enforced by the type system.
186 let on_unsupported = OnUnsupported::Abort;
186 let on_unsupported = OnUnsupported::Abort;
187
187
188 exit(
188 exit(
189 &argv,
189 &argv,
190 &initial_current_dir,
190 &initial_current_dir,
191 &Ui::new_infallible(&Config::empty()),
191 &Ui::new_infallible(&Config::empty()),
192 on_unsupported,
192 on_unsupported,
193 Err(error.into()),
193 Err(error.into()),
194 false,
194 false,
195 )
195 )
196 });
196 });
197
197
198 non_repo_config
198 non_repo_config
199 .load_cli_args(early_args.config, early_args.color)
199 .load_cli_args(early_args.config, early_args.color)
200 .unwrap_or_else(|error| {
200 .unwrap_or_else(|error| {
201 exit(
201 exit(
202 &argv,
202 &argv,
203 &initial_current_dir,
203 &initial_current_dir,
204 &Ui::new_infallible(&non_repo_config),
204 &Ui::new_infallible(&non_repo_config),
205 OnUnsupported::from_config(&non_repo_config),
205 OnUnsupported::from_config(&non_repo_config),
206 Err(error.into()),
206 Err(error.into()),
207 non_repo_config
207 non_repo_config
208 .get_bool(b"ui", b"detailed-exit-code")
208 .get_bool(b"ui", b"detailed-exit-code")
209 .unwrap_or(false),
209 .unwrap_or(false),
210 )
210 )
211 });
211 });
212
212
213 if let Some(repo_path_bytes) = &early_args.repo {
213 if let Some(repo_path_bytes) = &early_args.repo {
214 lazy_static::lazy_static! {
214 lazy_static::lazy_static! {
215 static ref SCHEME_RE: regex::bytes::Regex =
215 static ref SCHEME_RE: regex::bytes::Regex =
216 // Same as `_matchscheme` in `mercurial/util.py`
216 // Same as `_matchscheme` in `mercurial/util.py`
217 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
217 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
218 }
218 }
219 if SCHEME_RE.is_match(repo_path_bytes) {
219 if SCHEME_RE.is_match(repo_path_bytes) {
220 exit(
220 exit(
221 &argv,
221 &argv,
222 &initial_current_dir,
222 &initial_current_dir,
223 &Ui::new_infallible(&non_repo_config),
223 &Ui::new_infallible(&non_repo_config),
224 OnUnsupported::from_config(&non_repo_config),
224 OnUnsupported::from_config(&non_repo_config),
225 Err(CommandError::UnsupportedFeature {
225 Err(CommandError::UnsupportedFeature {
226 message: format_bytes!(
226 message: format_bytes!(
227 b"URL-like --repository {}",
227 b"URL-like --repository {}",
228 repo_path_bytes
228 repo_path_bytes
229 ),
229 ),
230 }),
230 }),
231 // TODO: show a warning or combine with original error if
231 // TODO: show a warning or combine with original error if
232 // `get_bool` returns an error
232 // `get_bool` returns an error
233 non_repo_config
233 non_repo_config
234 .get_bool(b"ui", b"detailed-exit-code")
234 .get_bool(b"ui", b"detailed-exit-code")
235 .unwrap_or(false),
235 .unwrap_or(false),
236 )
236 )
237 }
237 }
238 }
238 }
239 let repo_arg = early_args.repo.unwrap_or_default();
239 let repo_arg = early_args.repo.unwrap_or_default();
240 let repo_path: Option<PathBuf> = {
240 let repo_path: Option<PathBuf> = {
241 if repo_arg.is_empty() {
241 if repo_arg.is_empty() {
242 None
242 None
243 } else {
243 } else {
244 let local_config = {
244 let local_config = {
245 if std::env::var_os("HGRCSKIPREPO").is_none() {
245 if std::env::var_os("HGRCSKIPREPO").is_none() {
246 // TODO: handle errors from find_repo_root
246 // TODO: handle errors from find_repo_root
247 if let Ok(current_dir_path) = Repo::find_repo_root() {
247 if let Ok(current_dir_path) = Repo::find_repo_root() {
248 let config_files = vec![
248 let config_files = vec![
249 ConfigSource::AbsPath(
249 ConfigSource::AbsPath(
250 current_dir_path.join(".hg/hgrc"),
250 current_dir_path.join(".hg/hgrc"),
251 ),
251 ),
252 ConfigSource::AbsPath(
252 ConfigSource::AbsPath(
253 current_dir_path.join(".hg/hgrc-not-shared"),
253 current_dir_path.join(".hg/hgrc-not-shared"),
254 ),
254 ),
255 ];
255 ];
256 // TODO: handle errors from
256 // TODO: handle errors from
257 // `load_from_explicit_sources`
257 // `load_from_explicit_sources`
258 Config::load_from_explicit_sources(config_files).ok()
258 Config::load_from_explicit_sources(config_files).ok()
259 } else {
259 } else {
260 None
260 None
261 }
261 }
262 } else {
262 } else {
263 None
263 None
264 }
264 }
265 };
265 };
266
266
267 let non_repo_config_val = {
267 let non_repo_config_val = {
268 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
268 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
269 match &non_repo_val {
269 match &non_repo_val {
270 Some(val) if !val.is_empty() => home::home_dir()
270 Some(val) if !val.is_empty() => home::home_dir()
271 .unwrap_or_else(|| PathBuf::from("~"))
271 .unwrap_or_else(|| PathBuf::from("~"))
272 .join(get_path_from_bytes(val))
272 .join(get_path_from_bytes(val))
273 .canonicalize()
273 .canonicalize()
274 // TODO: handle error and make it similar to python
274 // TODO: handle error and make it similar to python
275 // implementation maybe?
275 // implementation maybe?
276 .ok(),
276 .ok(),
277 _ => None,
277 _ => None,
278 }
278 }
279 };
279 };
280
280
281 let config_val = match &local_config {
281 let config_val = match &local_config {
282 None => non_repo_config_val,
282 None => non_repo_config_val,
283 Some(val) => {
283 Some(val) => {
284 let local_config_val = val.get(b"paths", &repo_arg);
284 let local_config_val = val.get(b"paths", &repo_arg);
285 match &local_config_val {
285 match &local_config_val {
286 Some(val) if !val.is_empty() => {
286 Some(val) if !val.is_empty() => {
287 // presence of a local_config assures that
287 // presence of a local_config assures that
288 // current_dir
288 // current_dir
289 // wont result in an Error
289 // wont result in an Error
290 let canpath = hg::utils::current_dir()
290 let canpath = hg::utils::current_dir()
291 .unwrap()
291 .unwrap()
292 .join(get_path_from_bytes(val))
292 .join(get_path_from_bytes(val))
293 .canonicalize();
293 .canonicalize();
294 canpath.ok().or(non_repo_config_val)
294 canpath.ok().or(non_repo_config_val)
295 }
295 }
296 _ => non_repo_config_val,
296 _ => non_repo_config_val,
297 }
297 }
298 }
298 }
299 };
299 };
300 config_val
300 config_val
301 .or_else(|| Some(get_path_from_bytes(&repo_arg).to_path_buf()))
301 .or_else(|| Some(get_path_from_bytes(&repo_arg).to_path_buf()))
302 }
302 }
303 };
303 };
304
304
305 let simple_exit =
305 let simple_exit =
306 |ui: &Ui, config: &Config, result: Result<(), CommandError>| -> ! {
306 |ui: &Ui, config: &Config, result: Result<(), CommandError>| -> ! {
307 exit(
307 exit(
308 &argv,
308 &argv,
309 &initial_current_dir,
309 &initial_current_dir,
310 ui,
310 ui,
311 OnUnsupported::from_config(config),
311 OnUnsupported::from_config(config),
312 result,
312 result,
313 // TODO: show a warning or combine with original error if
313 // TODO: show a warning or combine with original error if
314 // `get_bool` returns an error
314 // `get_bool` returns an error
315 non_repo_config
315 non_repo_config
316 .get_bool(b"ui", b"detailed-exit-code")
316 .get_bool(b"ui", b"detailed-exit-code")
317 .unwrap_or(false),
317 .unwrap_or(false),
318 )
318 )
319 };
319 };
320 let early_exit = |config: &Config, error: CommandError| -> ! {
320 let early_exit = |config: &Config, error: CommandError| -> ! {
321 simple_exit(&Ui::new_infallible(config), config, Err(error))
321 simple_exit(&Ui::new_infallible(config), config, Err(error))
322 };
322 };
323 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
323 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
324 {
324 {
325 Ok(repo) => Ok(repo),
325 Ok(repo) => Ok(repo),
326 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
326 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
327 // Not finding a repo is not fatal yet, if `-R` was not given
327 // Not finding a repo is not fatal yet, if `-R` was not given
328 Err(NoRepoInCwdError { cwd: at })
328 Err(NoRepoInCwdError { cwd: at })
329 }
329 }
330 Err(error) => early_exit(&non_repo_config, error.into()),
330 Err(error) => early_exit(&non_repo_config, error.into()),
331 };
331 };
332
332
333 let config = if let Ok(repo) = &repo_result {
333 let config = if let Ok(repo) = &repo_result {
334 repo.config()
334 repo.config()
335 } else {
335 } else {
336 &non_repo_config
336 &non_repo_config
337 };
337 };
338
338
339 let mut config_cow = Cow::Borrowed(config);
339 let mut config_cow = Cow::Borrowed(config);
340 config_cow.to_mut().apply_plain(PlainInfo::from_env());
340 config_cow.to_mut().apply_plain(PlainInfo::from_env());
341 if !ui::plain(Some("tweakdefaults"))
341 if !ui::plain(Some("tweakdefaults"))
342 && config_cow
342 && config_cow
343 .as_ref()
343 .as_ref()
344 .get_bool(b"ui", b"tweakdefaults")
344 .get_bool(b"ui", b"tweakdefaults")
345 .unwrap_or_else(|error| early_exit(config, error.into()))
345 .unwrap_or_else(|error| early_exit(config, error.into()))
346 {
346 {
347 config_cow.to_mut().tweakdefaults()
347 config_cow.to_mut().tweakdefaults()
348 };
348 };
349 let config = config_cow.as_ref();
349 let config = config_cow.as_ref();
350 let ui = Ui::new(config)
350 let ui = Ui::new(config)
351 .unwrap_or_else(|error| early_exit(config, error.into()));
351 .unwrap_or_else(|error| early_exit(config, error.into()));
352
352
353 if let Ok(true) = config.get_bool(b"rhg", b"fallback-immediately") {
353 if let Ok(true) = config.get_bool(b"rhg", b"fallback-immediately") {
354 exit(
354 exit(
355 &argv,
355 &argv,
356 &initial_current_dir,
356 &initial_current_dir,
357 &ui,
357 &ui,
358 OnUnsupported::fallback(config),
358 OnUnsupported::fallback(config),
359 Err(CommandError::unsupported(
359 Err(CommandError::unsupported(
360 "`rhg.fallback-immediately is true`",
360 "`rhg.fallback-immediately is true`",
361 )),
361 )),
362 false,
362 false,
363 )
363 )
364 }
364 }
365
365
366 let result = main_with_result(
366 let result = main_with_result(
367 argv.iter().map(|s| s.to_owned()).collect(),
367 argv.iter().map(|s| s.to_owned()).collect(),
368 &process_start_time,
368 &process_start_time,
369 &ui,
369 &ui,
370 repo_result.as_ref(),
370 repo_result.as_ref(),
371 config,
371 config,
372 );
372 );
373 simple_exit(&ui, config, result)
373 simple_exit(&ui, config, result)
374 }
374 }
375
375
376 fn main() -> ! {
376 fn main() -> ! {
377 rhg_main(std::env::args_os().collect())
377 rhg_main(std::env::args_os().collect())
378 }
378 }
379
379
380 fn exit_code(
380 fn exit_code(
381 result: &Result<(), CommandError>,
381 result: &Result<(), CommandError>,
382 use_detailed_exit_code: bool,
382 use_detailed_exit_code: bool,
383 ) -> i32 {
383 ) -> i32 {
384 match result {
384 match result {
385 Ok(()) => exit_codes::OK,
385 Ok(()) => exit_codes::OK,
386 Err(CommandError::Abort {
386 Err(CommandError::Abort {
387 detailed_exit_code, ..
387 detailed_exit_code, ..
388 }) => {
388 }) => {
389 if use_detailed_exit_code {
389 if use_detailed_exit_code {
390 *detailed_exit_code
390 *detailed_exit_code
391 } else {
391 } else {
392 exit_codes::ABORT
392 exit_codes::ABORT
393 }
393 }
394 }
394 }
395 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
395 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
396 // Exit with a specific code and no error message to let a potential
396 // Exit with a specific code and no error message to let a potential
397 // wrapper script fallback to Python-based Mercurial.
397 // wrapper script fallback to Python-based Mercurial.
398 Err(CommandError::UnsupportedFeature { .. }) => {
398 Err(CommandError::UnsupportedFeature { .. }) => {
399 exit_codes::UNIMPLEMENTED
399 exit_codes::UNIMPLEMENTED
400 }
400 }
401 Err(CommandError::InvalidFallback { .. }) => {
401 Err(CommandError::InvalidFallback { .. }) => {
402 exit_codes::INVALID_FALLBACK
402 exit_codes::INVALID_FALLBACK
403 }
403 }
404 }
404 }
405 }
405 }
406
406
407 fn exit(
407 fn exit(
408 original_args: &[OsString],
408 original_args: &[OsString],
409 initial_current_dir: &Option<PathBuf>,
409 initial_current_dir: &Option<PathBuf>,
410 ui: &Ui,
410 ui: &Ui,
411 mut on_unsupported: OnUnsupported,
411 mut on_unsupported: OnUnsupported,
412 result: Result<(), CommandError>,
412 result: Result<(), CommandError>,
413 use_detailed_exit_code: bool,
413 use_detailed_exit_code: bool,
414 ) -> ! {
414 ) -> ! {
415 if let (
415 if let (
416 OnUnsupported::Fallback { executable },
416 OnUnsupported::Fallback { executable },
417 Err(CommandError::UnsupportedFeature { message }),
417 Err(CommandError::UnsupportedFeature { message }),
418 ) = (&on_unsupported, &result)
418 ) = (&on_unsupported, &result)
419 {
419 {
420 let mut args = original_args.iter();
420 let mut args = original_args.iter();
421 let executable = match executable {
421 let executable = match executable {
422 None => {
422 None => {
423 exit_no_fallback(
423 exit_no_fallback(
424 ui,
424 ui,
425 OnUnsupported::Abort,
425 OnUnsupported::Abort,
426 Err(CommandError::abort(
426 Err(CommandError::abort(
427 "abort: 'rhg.on-unsupported=fallback' without \
427 "abort: 'rhg.on-unsupported=fallback' without \
428 'rhg.fallback-executable' set.",
428 'rhg.fallback-executable' set.",
429 )),
429 )),
430 false,
430 false,
431 );
431 );
432 }
432 }
433 Some(executable) => executable,
433 Some(executable) => executable,
434 };
434 };
435 let executable_path = get_path_from_bytes(executable);
435 let executable_path = get_path_from_bytes(executable);
436 let this_executable = args.next().expect("exepcted argv[0] to exist");
436 let this_executable = args.next().expect("exepcted argv[0] to exist");
437 if executable_path == *this_executable {
437 if executable_path == *this_executable {
438 // Avoid spawning infinitely many processes until resource
438 // Avoid spawning infinitely many processes until resource
439 // exhaustion.
439 // exhaustion.
440 let _ = ui.write_stderr(&format_bytes!(
440 let _ = ui.write_stderr(&format_bytes!(
441 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
441 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
442 points to `rhg` itself.\n",
442 points to `rhg` itself.\n",
443 executable
443 executable
444 ));
444 ));
445 on_unsupported = OnUnsupported::Abort
445 on_unsupported = OnUnsupported::Abort
446 } else {
446 } else {
447 log::debug!("falling back (see trace-level log)");
447 log::debug!("falling back (see trace-level log)");
448 log::trace!("{}", local_to_utf8(message));
448 log::trace!("{}", local_to_utf8(message));
449 if let Err(err) = which::which(executable_path) {
449 if let Err(err) = which::which(executable_path) {
450 exit_no_fallback(
450 exit_no_fallback(
451 ui,
451 ui,
452 OnUnsupported::Abort,
452 OnUnsupported::Abort,
453 Err(CommandError::InvalidFallback {
453 Err(CommandError::InvalidFallback {
454 path: executable.to_owned(),
454 path: executable.to_owned(),
455 err: err.to_string(),
455 err: err.to_string(),
456 }),
456 }),
457 use_detailed_exit_code,
457 use_detailed_exit_code,
458 )
458 )
459 }
459 }
460 // `args` is now `argv[1..]` since we’ve already consumed
460 // `args` is now `argv[1..]` since we’ve already consumed
461 // `argv[0]`
461 // `argv[0]`
462 let mut command = Command::new(executable_path);
462 let mut command = Command::new(executable_path);
463 command.args(args);
463 command.args(args);
464 if let Some(initial) = initial_current_dir {
464 if let Some(initial) = initial_current_dir {
465 command.current_dir(initial);
465 command.current_dir(initial);
466 }
466 }
467 // We don't use subprocess because proper signal handling is harder
467 // We don't use subprocess because proper signal handling is harder
468 // and we don't want to keep `rhg` around after a fallback anyway.
468 // and we don't want to keep `rhg` around after a fallback anyway.
469 // For example, if `rhg` is run in the background and falls back to
469 // For example, if `rhg` is run in the background and falls back to
470 // `hg` which, in turn, waits for a signal, we'll get stuck if
470 // `hg` which, in turn, waits for a signal, we'll get stuck if
471 // we're doing plain subprocess.
471 // we're doing plain subprocess.
472 //
472 //
473 // If `exec` returns, we can only assume our process is very broken
473 // If `exec` returns, we can only assume our process is very broken
474 // (see its documentation), so only try to forward the error code
474 // (see its documentation), so only try to forward the error code
475 // when exiting.
475 // when exiting.
476 let err = command.exec();
476 let err = command.exec();
477 std::process::exit(
477 std::process::exit(
478 err.raw_os_error().unwrap_or(exit_codes::ABORT),
478 err.raw_os_error().unwrap_or(exit_codes::ABORT),
479 );
479 );
480 }
480 }
481 }
481 }
482 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
482 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
483 }
483 }
484
484
485 fn exit_no_fallback(
485 fn exit_no_fallback(
486 ui: &Ui,
486 ui: &Ui,
487 on_unsupported: OnUnsupported,
487 on_unsupported: OnUnsupported,
488 result: Result<(), CommandError>,
488 result: Result<(), CommandError>,
489 use_detailed_exit_code: bool,
489 use_detailed_exit_code: bool,
490 ) -> ! {
490 ) -> ! {
491 match &result {
491 match &result {
492 Ok(_) => {}
492 Ok(_) => {}
493 Err(CommandError::Unsuccessful) => {}
493 Err(CommandError::Unsuccessful) => {}
494 Err(CommandError::Abort { message, hint, .. }) => {
494 Err(CommandError::Abort { message, hint, .. }) => {
495 // Ignore errors when writing to stderr, we’re already exiting
495 // Ignore errors when writing to stderr, we’re already exiting
496 // with failure code so there’s not much more we can do.
496 // with failure code so there’s not much more we can do.
497 if !message.is_empty() {
497 if !message.is_empty() {
498 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
498 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
499 }
499 }
500 if let Some(hint) = hint {
500 if let Some(hint) = hint {
501 let _ = ui.write_stderr(&format_bytes!(b"({})\n", hint));
501 let _ = ui.write_stderr(&format_bytes!(b"({})\n", hint));
502 }
502 }
503 }
503 }
504 Err(CommandError::UnsupportedFeature { message }) => {
504 Err(CommandError::UnsupportedFeature { message }) => {
505 match on_unsupported {
505 match on_unsupported {
506 OnUnsupported::Abort => {
506 OnUnsupported::Abort => {
507 let _ = ui.write_stderr(&format_bytes!(
507 let _ = ui.write_stderr(&format_bytes!(
508 b"unsupported feature: {}\n",
508 b"unsupported feature: {}\n",
509 message
509 message
510 ));
510 ));
511 }
511 }
512 OnUnsupported::AbortSilent => {}
512 OnUnsupported::AbortSilent => {}
513 OnUnsupported::Fallback { .. } => unreachable!(),
513 OnUnsupported::Fallback { .. } => unreachable!(),
514 }
514 }
515 }
515 }
516 Err(CommandError::InvalidFallback { path, err }) => {
516 Err(CommandError::InvalidFallback { path, err }) => {
517 let _ = ui.write_stderr(&format_bytes!(
517 let _ = ui.write_stderr(&format_bytes!(
518 b"abort: invalid fallback '{}': {}\n",
518 b"abort: invalid fallback '{}': {}\n",
519 path,
519 path,
520 err.as_bytes(),
520 err.as_bytes(),
521 ));
521 ));
522 }
522 }
523 }
523 }
524 std::process::exit(exit_code(&result, use_detailed_exit_code))
524 std::process::exit(exit_code(&result, use_detailed_exit_code))
525 }
525 }
526
526
527 mod commands {
528 pub mod cat;
529 pub mod config;
530 pub mod debugdata;
531 pub mod debugignorerhg;
532 pub mod debugrequirements;
533 pub mod debugrhgsparse;
534 pub mod files;
535 pub mod root;
536 pub mod status;
537 }
538
527 macro_rules! subcommands {
539 macro_rules! subcommands {
528 ($( $command: ident )+) => {
540 ($( $command: ident )+) => {
529 mod commands {
530 $(
531 pub mod $command;
532 )+
533 }
534
541
535 fn add_subcommand_args(app: clap::Command) -> clap::Command {
542 fn add_subcommand_args(app: clap::Command) -> clap::Command {
536 app
543 app
537 $(
544 $(
538 .subcommand(commands::$command::args())
545 .subcommand(commands::$command::args())
539 )+
546 )+
540 }
547 }
541
548
542 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
549 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
543
550
544 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
551 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
545 match name {
552 match name {
546 $(
553 $(
547 stringify!($command) => Some(commands::$command::run),
554 stringify!($command) => Some(commands::$command::run),
548 )+
555 )+
549 _ => None,
556 _ => None,
550 }
557 }
551 }
558 }
552 };
559 };
553 }
560 }
554
561
555 subcommands! {
562 subcommands! {
556 cat
563 cat
557 debugdata
564 debugdata
558 debugrequirements
565 debugrequirements
559 debugignorerhg
566 debugignorerhg
560 debugrhgsparse
567 debugrhgsparse
561 files
568 files
562 root
569 root
563 config
570 config
564 status
571 status
565 }
572 }
566
573
567 pub struct CliInvocation<'a> {
574 pub struct CliInvocation<'a> {
568 ui: &'a Ui,
575 ui: &'a Ui,
569 subcommand_args: &'a ArgMatches,
576 subcommand_args: &'a ArgMatches,
570 config: &'a Config,
577 config: &'a Config,
571 /// References inside `Result` is a bit peculiar but allow
578 /// References inside `Result` is a bit peculiar but allow
572 /// `invocation.repo?` to work out with `&CliInvocation` since this
579 /// `invocation.repo?` to work out with `&CliInvocation` since this
573 /// `Result` type is `Copy`.
580 /// `Result` type is `Copy`.
574 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
581 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
575 }
582 }
576
583
577 struct NoRepoInCwdError {
584 struct NoRepoInCwdError {
578 cwd: PathBuf,
585 cwd: PathBuf,
579 }
586 }
580
587
581 /// CLI arguments to be parsed "early" in order to be able to read
588 /// CLI arguments to be parsed "early" in order to be able to read
582 /// configuration before using Clap. Ideally we would also use Clap for this,
589 /// configuration before using Clap. Ideally we would also use Clap for this,
583 /// see <https://github.com/clap-rs/clap/discussions/2366>.
590 /// see <https://github.com/clap-rs/clap/discussions/2366>.
584 ///
591 ///
585 /// These arguments are still declared when we do use Clap later, so that Clap
592 /// These arguments are still declared when we do use Clap later, so that Clap
586 /// does not return an error for their presence.
593 /// does not return an error for their presence.
587 struct EarlyArgs {
594 struct EarlyArgs {
588 /// Values of all `--config` arguments. (Possibly none)
595 /// Values of all `--config` arguments. (Possibly none)
589 config: Vec<Vec<u8>>,
596 config: Vec<Vec<u8>>,
590 /// Value of all the `--color` argument, if any.
597 /// Value of all the `--color` argument, if any.
591 color: Option<Vec<u8>>,
598 color: Option<Vec<u8>>,
592 /// Value of the `-R` or `--repository` argument, if any.
599 /// Value of the `-R` or `--repository` argument, if any.
593 repo: Option<Vec<u8>>,
600 repo: Option<Vec<u8>>,
594 /// Value of the `--cwd` argument, if any.
601 /// Value of the `--cwd` argument, if any.
595 cwd: Option<Vec<u8>>,
602 cwd: Option<Vec<u8>>,
596 }
603 }
597
604
598 impl EarlyArgs {
605 impl EarlyArgs {
599 fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
606 fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
600 let mut args = args.into_iter().map(get_bytes_from_os_str);
607 let mut args = args.into_iter().map(get_bytes_from_os_str);
601 let mut config = Vec::new();
608 let mut config = Vec::new();
602 let mut color = None;
609 let mut color = None;
603 let mut repo = None;
610 let mut repo = None;
604 let mut cwd = None;
611 let mut cwd = None;
605 // Use `while let` instead of `for` so that we can also call
612 // Use `while let` instead of `for` so that we can also call
606 // `args.next()` inside the loop.
613 // `args.next()` inside the loop.
607 while let Some(arg) = args.next() {
614 while let Some(arg) = args.next() {
608 if arg == b"--config" {
615 if arg == b"--config" {
609 if let Some(value) = args.next() {
616 if let Some(value) = args.next() {
610 config.push(value)
617 config.push(value)
611 }
618 }
612 } else if let Some(value) = arg.drop_prefix(b"--config=") {
619 } else if let Some(value) = arg.drop_prefix(b"--config=") {
613 config.push(value.to_owned())
620 config.push(value.to_owned())
614 }
621 }
615
622
616 if arg == b"--color" {
623 if arg == b"--color" {
617 if let Some(value) = args.next() {
624 if let Some(value) = args.next() {
618 color = Some(value)
625 color = Some(value)
619 }
626 }
620 } else if let Some(value) = arg.drop_prefix(b"--color=") {
627 } else if let Some(value) = arg.drop_prefix(b"--color=") {
621 color = Some(value.to_owned())
628 color = Some(value.to_owned())
622 }
629 }
623
630
624 if arg == b"--cwd" {
631 if arg == b"--cwd" {
625 if let Some(value) = args.next() {
632 if let Some(value) = args.next() {
626 cwd = Some(value)
633 cwd = Some(value)
627 }
634 }
628 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
635 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
629 cwd = Some(value.to_owned())
636 cwd = Some(value.to_owned())
630 }
637 }
631
638
632 if arg == b"--repository" || arg == b"-R" {
639 if arg == b"--repository" || arg == b"-R" {
633 if let Some(value) = args.next() {
640 if let Some(value) = args.next() {
634 repo = Some(value)
641 repo = Some(value)
635 }
642 }
636 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
643 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
637 repo = Some(value.to_owned())
644 repo = Some(value.to_owned())
638 } else if let Some(value) = arg.drop_prefix(b"-R") {
645 } else if let Some(value) = arg.drop_prefix(b"-R") {
639 repo = Some(value.to_owned())
646 repo = Some(value.to_owned())
640 }
647 }
641 }
648 }
642 Self {
649 Self {
643 config,
650 config,
644 color,
651 color,
645 repo,
652 repo,
646 cwd,
653 cwd,
647 }
654 }
648 }
655 }
649 }
656 }
650
657
651 /// What to do when encountering some unsupported feature.
658 /// What to do when encountering some unsupported feature.
652 ///
659 ///
653 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
660 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
654 enum OnUnsupported {
661 enum OnUnsupported {
655 /// Print an error message describing what feature is not supported,
662 /// Print an error message describing what feature is not supported,
656 /// and exit with code 252.
663 /// and exit with code 252.
657 Abort,
664 Abort,
658 /// Silently exit with code 252.
665 /// Silently exit with code 252.
659 AbortSilent,
666 AbortSilent,
660 /// Try running a Python implementation
667 /// Try running a Python implementation
661 Fallback { executable: Option<Vec<u8>> },
668 Fallback { executable: Option<Vec<u8>> },
662 }
669 }
663
670
664 impl OnUnsupported {
671 impl OnUnsupported {
665 const DEFAULT: Self = OnUnsupported::Abort;
672 const DEFAULT: Self = OnUnsupported::Abort;
666
673
667 fn fallback_executable(config: &Config) -> Option<Vec<u8>> {
674 fn fallback_executable(config: &Config) -> Option<Vec<u8>> {
668 config
675 config
669 .get(b"rhg", b"fallback-executable")
676 .get(b"rhg", b"fallback-executable")
670 .map(|x| x.to_owned())
677 .map(|x| x.to_owned())
671 }
678 }
672
679
673 fn fallback(config: &Config) -> Self {
680 fn fallback(config: &Config) -> Self {
674 OnUnsupported::Fallback {
681 OnUnsupported::Fallback {
675 executable: Self::fallback_executable(config),
682 executable: Self::fallback_executable(config),
676 }
683 }
677 }
684 }
678
685
679 fn from_config(config: &Config) -> Self {
686 fn from_config(config: &Config) -> Self {
680 match config
687 match config
681 .get(b"rhg", b"on-unsupported")
688 .get(b"rhg", b"on-unsupported")
682 .map(|value| value.to_ascii_lowercase())
689 .map(|value| value.to_ascii_lowercase())
683 .as_deref()
690 .as_deref()
684 {
691 {
685 Some(b"abort") => OnUnsupported::Abort,
692 Some(b"abort") => OnUnsupported::Abort,
686 Some(b"abort-silent") => OnUnsupported::AbortSilent,
693 Some(b"abort-silent") => OnUnsupported::AbortSilent,
687 Some(b"fallback") => Self::fallback(config),
694 Some(b"fallback") => Self::fallback(config),
688 None => Self::DEFAULT,
695 None => Self::DEFAULT,
689 Some(_) => {
696 Some(_) => {
690 // TODO: warn about unknown config value
697 // TODO: warn about unknown config value
691 Self::DEFAULT
698 Self::DEFAULT
692 }
699 }
693 }
700 }
694 }
701 }
695 }
702 }
696
703
697 /// The `*` extension is an edge-case for config sub-options that apply to all
704 /// The `*` extension is an edge-case for config sub-options that apply to all
698 /// extensions. For now, only `:required` exists, but that may change in the
705 /// extensions. For now, only `:required` exists, but that may change in the
699 /// future.
706 /// future.
700 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[
707 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[
701 b"blackbox",
708 b"blackbox",
702 b"share",
709 b"share",
703 b"sparse",
710 b"sparse",
704 b"narrow",
711 b"narrow",
705 b"*",
712 b"*",
706 b"strip",
713 b"strip",
707 b"rebase",
714 b"rebase",
708 ];
715 ];
709
716
710 fn check_extensions(config: &Config) -> Result<(), CommandError> {
717 fn check_extensions(config: &Config) -> Result<(), CommandError> {
711 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
718 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
712 // All extensions are to be ignored, nothing to do here
719 // All extensions are to be ignored, nothing to do here
713 return Ok(());
720 return Ok(());
714 }
721 }
715
722
716 let enabled: HashSet<&[u8]> = config
723 let enabled: HashSet<&[u8]> = config
717 .iter_section(b"extensions")
724 .iter_section(b"extensions")
718 .filter_map(|(extension, value)| {
725 .filter_map(|(extension, value)| {
719 if value == b"!" {
726 if value == b"!" {
720 // Filter out disabled extensions
727 // Filter out disabled extensions
721 return None;
728 return None;
722 }
729 }
723 // Ignore extension suboptions. Only `required` exists for now.
730 // Ignore extension suboptions. Only `required` exists for now.
724 // `rhg` either supports an extension or doesn't, so it doesn't
731 // `rhg` either supports an extension or doesn't, so it doesn't
725 // make sense to consider the loading of an extension.
732 // make sense to consider the loading of an extension.
726 let actual_extension =
733 let actual_extension =
727 extension.split_2(b':').unwrap_or((extension, b"")).0;
734 extension.split_2(b':').unwrap_or((extension, b"")).0;
728 Some(actual_extension)
735 Some(actual_extension)
729 })
736 })
730 .collect();
737 .collect();
731
738
732 let mut unsupported = enabled;
739 let mut unsupported = enabled;
733 for supported in SUPPORTED_EXTENSIONS {
740 for supported in SUPPORTED_EXTENSIONS {
734 unsupported.remove(supported);
741 unsupported.remove(supported);
735 }
742 }
736
743
737 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
744 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
738 {
745 {
739 for ignored in ignored_list {
746 for ignored in ignored_list {
740 unsupported.remove(ignored.as_slice());
747 unsupported.remove(ignored.as_slice());
741 }
748 }
742 }
749 }
743
750
744 if unsupported.is_empty() {
751 if unsupported.is_empty() {
745 Ok(())
752 Ok(())
746 } else {
753 } else {
747 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
754 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
748 // Sort the extensions to get a stable output
755 // Sort the extensions to get a stable output
749 unsupported.sort();
756 unsupported.sort();
750 Err(CommandError::UnsupportedFeature {
757 Err(CommandError::UnsupportedFeature {
751 message: format_bytes!(
758 message: format_bytes!(
752 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
759 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
753 join(unsupported, b", ")
760 join(unsupported, b", ")
754 ),
761 ),
755 })
762 })
756 }
763 }
757 }
764 }
758
765
759 /// Array of tuples of (auto upgrade conf, feature conf, local requirement)
766 /// Array of tuples of (auto upgrade conf, feature conf, local requirement)
760 #[allow(clippy::type_complexity)]
767 #[allow(clippy::type_complexity)]
761 const AUTO_UPGRADES: &[((&str, &str), (&str, &str), &str)] = &[
768 const AUTO_UPGRADES: &[((&str, &str), (&str, &str), &str)] = &[
762 (
769 (
763 ("format", "use-share-safe.automatic-upgrade-of-mismatching-repositories"),
770 ("format", "use-share-safe.automatic-upgrade-of-mismatching-repositories"),
764 ("format", "use-share-safe"),
771 ("format", "use-share-safe"),
765 requirements::SHARESAFE_REQUIREMENT,
772 requirements::SHARESAFE_REQUIREMENT,
766 ),
773 ),
767 (
774 (
768 ("format", "use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories"),
775 ("format", "use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories"),
769 ("format", "use-dirstate-tracked-hint"),
776 ("format", "use-dirstate-tracked-hint"),
770 requirements::DIRSTATE_TRACKED_HINT_V1,
777 requirements::DIRSTATE_TRACKED_HINT_V1,
771 ),
778 ),
772 (
779 (
773 ("format", "use-dirstate-v2.automatic-upgrade-of-mismatching-repositories"),
780 ("format", "use-dirstate-v2.automatic-upgrade-of-mismatching-repositories"),
774 ("format", "use-dirstate-v2"),
781 ("format", "use-dirstate-v2"),
775 requirements::DIRSTATE_V2_REQUIREMENT,
782 requirements::DIRSTATE_V2_REQUIREMENT,
776 ),
783 ),
777 ];
784 ];
778
785
779 /// Mercurial allows users to automatically upgrade their repository.
786 /// Mercurial allows users to automatically upgrade their repository.
780 /// `rhg` does not have the ability to upgrade yet, so fallback if an upgrade
787 /// `rhg` does not have the ability to upgrade yet, so fallback if an upgrade
781 /// is needed.
788 /// is needed.
782 fn check_auto_upgrade(
789 fn check_auto_upgrade(
783 config: &Config,
790 config: &Config,
784 reqs: &HashSet<String>,
791 reqs: &HashSet<String>,
785 ) -> Result<(), CommandError> {
792 ) -> Result<(), CommandError> {
786 for (upgrade_conf, feature_conf, local_req) in AUTO_UPGRADES.iter() {
793 for (upgrade_conf, feature_conf, local_req) in AUTO_UPGRADES.iter() {
787 let auto_upgrade = config
794 let auto_upgrade = config
788 .get_bool(upgrade_conf.0.as_bytes(), upgrade_conf.1.as_bytes())?;
795 .get_bool(upgrade_conf.0.as_bytes(), upgrade_conf.1.as_bytes())?;
789
796
790 if auto_upgrade {
797 if auto_upgrade {
791 let want_it = config.get_bool(
798 let want_it = config.get_bool(
792 feature_conf.0.as_bytes(),
799 feature_conf.0.as_bytes(),
793 feature_conf.1.as_bytes(),
800 feature_conf.1.as_bytes(),
794 )?;
801 )?;
795 let have_it = reqs.contains(*local_req);
802 let have_it = reqs.contains(*local_req);
796
803
797 let action = match (want_it, have_it) {
804 let action = match (want_it, have_it) {
798 (true, false) => Some("upgrade"),
805 (true, false) => Some("upgrade"),
799 (false, true) => Some("downgrade"),
806 (false, true) => Some("downgrade"),
800 _ => None,
807 _ => None,
801 };
808 };
802 if let Some(action) = action {
809 if let Some(action) = action {
803 let message = format!(
810 let message = format!(
804 "automatic {} {}.{}",
811 "automatic {} {}.{}",
805 action, upgrade_conf.0, upgrade_conf.1
812 action, upgrade_conf.0, upgrade_conf.1
806 );
813 );
807 return Err(CommandError::unsupported(message));
814 return Err(CommandError::unsupported(message));
808 }
815 }
809 }
816 }
810 }
817 }
811 Ok(())
818 Ok(())
812 }
819 }
813
820
814 fn check_unsupported(
821 fn check_unsupported(
815 config: &Config,
822 config: &Config,
816 repo: Result<&Repo, &NoRepoInCwdError>,
823 repo: Result<&Repo, &NoRepoInCwdError>,
817 ) -> Result<(), CommandError> {
824 ) -> Result<(), CommandError> {
818 check_extensions(config)?;
825 check_extensions(config)?;
819
826
820 if std::env::var_os("HG_PENDING").is_some() {
827 if std::env::var_os("HG_PENDING").is_some() {
821 // TODO: only if the value is `== repo.working_directory`?
828 // TODO: only if the value is `== repo.working_directory`?
822 // What about relative v.s. absolute paths?
829 // What about relative v.s. absolute paths?
823 Err(CommandError::unsupported("$HG_PENDING"))?
830 Err(CommandError::unsupported("$HG_PENDING"))?
824 }
831 }
825
832
826 if let Ok(repo) = repo {
833 if let Ok(repo) = repo {
827 if repo.has_subrepos()? {
834 if repo.has_subrepos()? {
828 Err(CommandError::unsupported("sub-repositories"))?
835 Err(CommandError::unsupported("sub-repositories"))?
829 }
836 }
830 check_auto_upgrade(config, repo.requirements())?;
837 check_auto_upgrade(config, repo.requirements())?;
831 }
838 }
832
839
833 if config.has_non_empty_section(b"encode") {
840 if config.has_non_empty_section(b"encode") {
834 Err(CommandError::unsupported("[encode] config"))?
841 Err(CommandError::unsupported("[encode] config"))?
835 }
842 }
836
843
837 if config.has_non_empty_section(b"decode") {
844 if config.has_non_empty_section(b"decode") {
838 Err(CommandError::unsupported("[decode] config"))?
845 Err(CommandError::unsupported("[decode] config"))?
839 }
846 }
840
847
841 Ok(())
848 Ok(())
842 }
849 }
@@ -1,159 +1,186 b''
1 Tests of 'hg status --rev <rev>' to make sure status between <rev> and '.' get
1 Tests of 'hg status --rev <rev>' to make sure status between <rev> and '.' get
2 combined correctly with the dirstate status.
2 combined correctly with the dirstate status.
3
3
4 $ hg init repo
4 $ hg init repo
5 $ cd repo
5 $ cd repo
6
6
7 First commit
7 First commit
8
8
9 $ "$PYTHON" $TESTDIR/generate-working-copy-states.py state 2 1
9 $ "$PYTHON" $TESTDIR/generate-working-copy-states.py state 2 1
10 $ hg addremove --similarity 0
10 $ hg addremove --similarity 0
11 adding content1_content1_content1-tracked
11 adding content1_content1_content1-tracked
12 adding content1_content1_content1-untracked
12 adding content1_content1_content1-untracked
13 adding content1_content1_content3-tracked
13 adding content1_content1_content3-tracked
14 adding content1_content1_content3-untracked
14 adding content1_content1_content3-untracked
15 adding content1_content1_missing-tracked
15 adding content1_content1_missing-tracked
16 adding content1_content1_missing-untracked
16 adding content1_content1_missing-untracked
17 adding content1_content2_content1-tracked
17 adding content1_content2_content1-tracked
18 adding content1_content2_content1-untracked
18 adding content1_content2_content1-untracked
19 adding content1_content2_content2-tracked
19 adding content1_content2_content2-tracked
20 adding content1_content2_content2-untracked
20 adding content1_content2_content2-untracked
21 adding content1_content2_content3-tracked
21 adding content1_content2_content3-tracked
22 adding content1_content2_content3-untracked
22 adding content1_content2_content3-untracked
23 adding content1_content2_missing-tracked
23 adding content1_content2_missing-tracked
24 adding content1_content2_missing-untracked
24 adding content1_content2_missing-untracked
25 adding content1_missing_content1-tracked
25 adding content1_missing_content1-tracked
26 adding content1_missing_content1-untracked
26 adding content1_missing_content1-untracked
27 adding content1_missing_content3-tracked
27 adding content1_missing_content3-tracked
28 adding content1_missing_content3-untracked
28 adding content1_missing_content3-untracked
29 adding content1_missing_missing-tracked
29 adding content1_missing_missing-tracked
30 adding content1_missing_missing-untracked
30 adding content1_missing_missing-untracked
31 $ hg commit -m first
31 $ hg commit -m first
32
32
33 Second commit
33 Second commit
34
34
35 $ "$PYTHON" $TESTDIR/generate-working-copy-states.py state 2 2
35 $ "$PYTHON" $TESTDIR/generate-working-copy-states.py state 2 2
36 $ hg addremove --similarity 0
36 $ hg addremove --similarity 0
37 removing content1_missing_content1-tracked
37 removing content1_missing_content1-tracked
38 removing content1_missing_content1-untracked
38 removing content1_missing_content1-untracked
39 removing content1_missing_content3-tracked
39 removing content1_missing_content3-tracked
40 removing content1_missing_content3-untracked
40 removing content1_missing_content3-untracked
41 removing content1_missing_missing-tracked
41 removing content1_missing_missing-tracked
42 removing content1_missing_missing-untracked
42 removing content1_missing_missing-untracked
43 adding missing_content2_content2-tracked
43 adding missing_content2_content2-tracked
44 adding missing_content2_content2-untracked
44 adding missing_content2_content2-untracked
45 adding missing_content2_content3-tracked
45 adding missing_content2_content3-tracked
46 adding missing_content2_content3-untracked
46 adding missing_content2_content3-untracked
47 adding missing_content2_missing-tracked
47 adding missing_content2_missing-tracked
48 adding missing_content2_missing-untracked
48 adding missing_content2_missing-untracked
49 $ hg commit -m second
49 $ hg commit -m second
50
50
51 Working copy
51 Working copy
52
52
53 $ "$PYTHON" $TESTDIR/generate-working-copy-states.py state 2 wc
53 $ "$PYTHON" $TESTDIR/generate-working-copy-states.py state 2 wc
54 $ hg addremove --similarity 0
54 $ hg addremove --similarity 0
55 adding content1_missing_content1-tracked
55 adding content1_missing_content1-tracked
56 adding content1_missing_content1-untracked
56 adding content1_missing_content1-untracked
57 adding content1_missing_content3-tracked
57 adding content1_missing_content3-tracked
58 adding content1_missing_content3-untracked
58 adding content1_missing_content3-untracked
59 adding content1_missing_missing-tracked
59 adding content1_missing_missing-tracked
60 adding content1_missing_missing-untracked
60 adding content1_missing_missing-untracked
61 adding missing_missing_content3-tracked
61 adding missing_missing_content3-tracked
62 adding missing_missing_content3-untracked
62 adding missing_missing_content3-untracked
63 adding missing_missing_missing-tracked
63 adding missing_missing_missing-tracked
64 adding missing_missing_missing-untracked
64 adding missing_missing_missing-untracked
65 $ hg forget *_*_*-untracked
65 $ hg forget *_*_*-untracked
66 $ rm *_*_missing-*
66 $ rm *_*_missing-*
67
67
68 Status compared to parent of the working copy, i.e. the dirstate status
68 Status compared to parent of the working copy, i.e. the dirstate status
69
69
70 $ hg status -A --rev 1 'glob:missing_content2_content3-tracked'
70 $ hg status -A --rev 1 'glob:missing_content2_content3-tracked'
71 M missing_content2_content3-tracked
71 M missing_content2_content3-tracked
72 $ hg status -A --rev 1 'glob:missing_content2_content2-tracked'
72 $ hg status -A --rev 1 'glob:missing_content2_content2-tracked'
73 C missing_content2_content2-tracked
73 C missing_content2_content2-tracked
74 $ hg status -A --rev 1 'glob:missing_missing_content3-tracked'
74 $ hg status -A --rev 1 'glob:missing_missing_content3-tracked'
75 A missing_missing_content3-tracked
75 A missing_missing_content3-tracked
76 $ hg status -A --rev 1 'glob:missing_missing_content3-untracked'
76 $ hg status -A --rev 1 'glob:missing_missing_content3-untracked'
77 ? missing_missing_content3-untracked
77 ? missing_missing_content3-untracked
78 $ hg status -A --rev 1 'glob:missing_content2_*-untracked'
78 $ hg status -A --rev 1 'glob:missing_content2_*-untracked'
79 R missing_content2_content2-untracked
79 R missing_content2_content2-untracked
80 R missing_content2_content3-untracked
80 R missing_content2_content3-untracked
81 R missing_content2_missing-untracked
81 R missing_content2_missing-untracked
82 $ hg status -A --rev 1 'glob:missing_*_missing-tracked'
82 $ hg status -A --rev 1 'glob:missing_*_missing-tracked'
83 ! missing_content2_missing-tracked
83 ! missing_content2_missing-tracked
84 ! missing_missing_missing-tracked
84 ! missing_missing_missing-tracked
85
85
86 $ hg status -A --rev 1 'glob:missing_missing_missing-untracked'
86 $ hg status -A --rev 1 'glob:missing_missing_missing-untracked'
87 missing_missing_missing-untracked: $ENOENT$
87 missing_missing_missing-untracked: $ENOENT$
88
88
89 Status between first and second commit. Should ignore dirstate status.
89 Status between first and second commit. Should ignore dirstate status.
90
90
91 $ hg status -marc --rev 0 --rev 1 --config rhg.on-unsupported=abort
92 M content1_content2_content1-tracked
93 M content1_content2_content1-untracked
94 M content1_content2_content2-tracked
95 M content1_content2_content2-untracked
96 M content1_content2_content3-tracked
97 M content1_content2_content3-untracked
98 M content1_content2_missing-tracked
99 M content1_content2_missing-untracked
100 A missing_content2_content2-tracked
101 A missing_content2_content2-untracked
102 A missing_content2_content3-tracked
103 A missing_content2_content3-untracked
104 A missing_content2_missing-tracked
105 A missing_content2_missing-untracked
106 R content1_missing_content1-tracked
107 R content1_missing_content1-untracked
108 R content1_missing_content3-tracked
109 R content1_missing_content3-untracked
110 R content1_missing_missing-tracked
111 R content1_missing_missing-untracked
112 C content1_content1_content1-tracked
113 C content1_content1_content1-untracked
114 C content1_content1_content3-tracked
115 C content1_content1_content3-untracked
116 C content1_content1_missing-tracked
117 C content1_content1_missing-untracked
91 $ hg status -A --rev 0:1 'glob:content1_content2_*'
118 $ hg status -A --rev 0:1 'glob:content1_content2_*'
92 M content1_content2_content1-tracked
119 M content1_content2_content1-tracked
93 M content1_content2_content1-untracked
120 M content1_content2_content1-untracked
94 M content1_content2_content2-tracked
121 M content1_content2_content2-tracked
95 M content1_content2_content2-untracked
122 M content1_content2_content2-untracked
96 M content1_content2_content3-tracked
123 M content1_content2_content3-tracked
97 M content1_content2_content3-untracked
124 M content1_content2_content3-untracked
98 M content1_content2_missing-tracked
125 M content1_content2_missing-tracked
99 M content1_content2_missing-untracked
126 M content1_content2_missing-untracked
100 $ hg status -A --rev 0:1 'glob:content1_content1_*'
127 $ hg status -A --rev 0:1 'glob:content1_content1_*'
101 C content1_content1_content1-tracked
128 C content1_content1_content1-tracked
102 C content1_content1_content1-untracked
129 C content1_content1_content1-untracked
103 C content1_content1_content3-tracked
130 C content1_content1_content3-tracked
104 C content1_content1_content3-untracked
131 C content1_content1_content3-untracked
105 C content1_content1_missing-tracked
132 C content1_content1_missing-tracked
106 C content1_content1_missing-untracked
133 C content1_content1_missing-untracked
107 $ hg status -A --rev 0:1 'glob:missing_content2_*'
134 $ hg status -A --rev 0:1 'glob:missing_content2_*'
108 A missing_content2_content2-tracked
135 A missing_content2_content2-tracked
109 A missing_content2_content2-untracked
136 A missing_content2_content2-untracked
110 A missing_content2_content3-tracked
137 A missing_content2_content3-tracked
111 A missing_content2_content3-untracked
138 A missing_content2_content3-untracked
112 A missing_content2_missing-tracked
139 A missing_content2_missing-tracked
113 A missing_content2_missing-untracked
140 A missing_content2_missing-untracked
114 $ hg status -A --rev 0:1 'glob:content1_missing_*'
141 $ hg status -A --rev 0:1 'glob:content1_missing_*'
115 R content1_missing_content1-tracked
142 R content1_missing_content1-tracked
116 R content1_missing_content1-untracked
143 R content1_missing_content1-untracked
117 R content1_missing_content3-tracked
144 R content1_missing_content3-tracked
118 R content1_missing_content3-untracked
145 R content1_missing_content3-untracked
119 R content1_missing_missing-tracked
146 R content1_missing_missing-tracked
120 R content1_missing_missing-untracked
147 R content1_missing_missing-untracked
121 $ hg status -A --rev 0:1 'glob:missing_missing_*'
148 $ hg status -A --rev 0:1 'glob:missing_missing_*'
122
149
123 Status compared to one revision back, checking that the dirstate status
150 Status compared to one revision back, checking that the dirstate status
124 is correctly combined with the inter-revision status
151 is correctly combined with the inter-revision status
125
152
126 $ hg status -A --rev 0 'glob:content1_*_content[23]-tracked'
153 $ hg status -A --rev 0 'glob:content1_*_content[23]-tracked'
127 M content1_content1_content3-tracked
154 M content1_content1_content3-tracked
128 M content1_content2_content2-tracked
155 M content1_content2_content2-tracked
129 M content1_content2_content3-tracked
156 M content1_content2_content3-tracked
130 M content1_missing_content3-tracked
157 M content1_missing_content3-tracked
131 $ hg status -A --rev 0 'glob:content1_*_content1-tracked'
158 $ hg status -A --rev 0 'glob:content1_*_content1-tracked'
132 C content1_content1_content1-tracked
159 C content1_content1_content1-tracked
133 C content1_content2_content1-tracked
160 C content1_content2_content1-tracked
134 C content1_missing_content1-tracked
161 C content1_missing_content1-tracked
135 $ hg status -A --rev 0 'glob:missing_*_content?-tracked'
162 $ hg status -A --rev 0 'glob:missing_*_content?-tracked'
136 A missing_content2_content2-tracked
163 A missing_content2_content2-tracked
137 A missing_content2_content3-tracked
164 A missing_content2_content3-tracked
138 A missing_missing_content3-tracked
165 A missing_missing_content3-tracked
139 BROKEN: missing_content2_content[23]-untracked exist, so should be listed
166 BROKEN: missing_content2_content[23]-untracked exist, so should be listed
140 $ hg status -A --rev 0 'glob:missing_*_content?-untracked'
167 $ hg status -A --rev 0 'glob:missing_*_content?-untracked'
141 ? missing_missing_content3-untracked
168 ? missing_missing_content3-untracked
142 $ hg status -A --rev 0 'glob:content1_*_*-untracked'
169 $ hg status -A --rev 0 'glob:content1_*_*-untracked'
143 R content1_content1_content1-untracked
170 R content1_content1_content1-untracked
144 R content1_content1_content3-untracked
171 R content1_content1_content3-untracked
145 R content1_content1_missing-untracked
172 R content1_content1_missing-untracked
146 R content1_content2_content1-untracked
173 R content1_content2_content1-untracked
147 R content1_content2_content2-untracked
174 R content1_content2_content2-untracked
148 R content1_content2_content3-untracked
175 R content1_content2_content3-untracked
149 R content1_content2_missing-untracked
176 R content1_content2_missing-untracked
150 R content1_missing_content1-untracked
177 R content1_missing_content1-untracked
151 R content1_missing_content3-untracked
178 R content1_missing_content3-untracked
152 R content1_missing_missing-untracked
179 R content1_missing_missing-untracked
153 $ hg status -A --rev 0 'glob:*_*_missing-tracked'
180 $ hg status -A --rev 0 'glob:*_*_missing-tracked'
154 ! content1_content1_missing-tracked
181 ! content1_content1_missing-tracked
155 ! content1_content2_missing-tracked
182 ! content1_content2_missing-tracked
156 ! content1_missing_missing-tracked
183 ! content1_missing_missing-tracked
157 ! missing_content2_missing-tracked
184 ! missing_content2_missing-tracked
158 ! missing_missing_missing-tracked
185 ! missing_missing_missing-tracked
159 $ hg status -A --rev 0 'glob:missing_*_missing-untracked'
186 $ hg status -A --rev 0 'glob:missing_*_missing-untracked'
General Comments 0
You need to be logged in to leave comments. Login now