##// END OF EJS Templates
rhg: refactor hg status, make the display code usable for non-dirstate status
Arseniy Alekseyev -
r52046:976403c9 default
parent child Browse files
Show More
@@ -1,718 +1,728
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::StatusError;
33 use hg::StatusError;
34 use hg::StatusOptions;
34 use hg::StatusOptions;
35 use hg::{self, narrow, sparse};
35 use hg::{self, narrow, sparse};
36 use log::info;
36 use log::info;
37 use rayon::prelude::*;
37 use rayon::prelude::*;
38 use std::io;
38 use std::io;
39 use std::mem::take;
39 use std::path::PathBuf;
40 use std::path::PathBuf;
40
41
41 pub const HELP_TEXT: &str = "
42 pub const HELP_TEXT: &str = "
42 Show changed files in the working directory
43 Show changed files in the working directory
43
44
44 This is a pure Rust version of `hg status`.
45 This is a pure Rust version of `hg status`.
45
46
46 Some options might be missing, check the list below.
47 Some options might be missing, check the list below.
47 ";
48 ";
48
49
49 pub fn args() -> clap::Command {
50 pub fn args() -> clap::Command {
50 clap::command!("status")
51 clap::command!("status")
51 .alias("st")
52 .alias("st")
52 .about(HELP_TEXT)
53 .about(HELP_TEXT)
53 .arg(
54 .arg(
54 Arg::new("file")
55 Arg::new("file")
55 .value_parser(clap::value_parser!(std::ffi::OsString))
56 .value_parser(clap::value_parser!(std::ffi::OsString))
56 .help("show only these files")
57 .help("show only these files")
57 .action(clap::ArgAction::Append),
58 .action(clap::ArgAction::Append),
58 )
59 )
59 .arg(
60 .arg(
60 Arg::new("all")
61 Arg::new("all")
61 .help("show status of all files")
62 .help("show status of all files")
62 .short('A')
63 .short('A')
63 .action(clap::ArgAction::SetTrue)
64 .action(clap::ArgAction::SetTrue)
64 .long("all"),
65 .long("all"),
65 )
66 )
66 .arg(
67 .arg(
67 Arg::new("modified")
68 Arg::new("modified")
68 .help("show only modified files")
69 .help("show only modified files")
69 .short('m')
70 .short('m')
70 .action(clap::ArgAction::SetTrue)
71 .action(clap::ArgAction::SetTrue)
71 .long("modified"),
72 .long("modified"),
72 )
73 )
73 .arg(
74 .arg(
74 Arg::new("added")
75 Arg::new("added")
75 .help("show only added files")
76 .help("show only added files")
76 .short('a')
77 .short('a')
77 .action(clap::ArgAction::SetTrue)
78 .action(clap::ArgAction::SetTrue)
78 .long("added"),
79 .long("added"),
79 )
80 )
80 .arg(
81 .arg(
81 Arg::new("removed")
82 Arg::new("removed")
82 .help("show only removed files")
83 .help("show only removed files")
83 .short('r')
84 .short('r')
84 .action(clap::ArgAction::SetTrue)
85 .action(clap::ArgAction::SetTrue)
85 .long("removed"),
86 .long("removed"),
86 )
87 )
87 .arg(
88 .arg(
88 Arg::new("clean")
89 Arg::new("clean")
89 .help("show only clean files")
90 .help("show only clean files")
90 .short('c')
91 .short('c')
91 .action(clap::ArgAction::SetTrue)
92 .action(clap::ArgAction::SetTrue)
92 .long("clean"),
93 .long("clean"),
93 )
94 )
94 .arg(
95 .arg(
95 Arg::new("deleted")
96 Arg::new("deleted")
96 .help("show only deleted files")
97 .help("show only deleted files")
97 .short('d')
98 .short('d')
98 .action(clap::ArgAction::SetTrue)
99 .action(clap::ArgAction::SetTrue)
99 .long("deleted"),
100 .long("deleted"),
100 )
101 )
101 .arg(
102 .arg(
102 Arg::new("unknown")
103 Arg::new("unknown")
103 .help("show only unknown (not tracked) files")
104 .help("show only unknown (not tracked) files")
104 .short('u')
105 .short('u')
105 .action(clap::ArgAction::SetTrue)
106 .action(clap::ArgAction::SetTrue)
106 .long("unknown"),
107 .long("unknown"),
107 )
108 )
108 .arg(
109 .arg(
109 Arg::new("ignored")
110 Arg::new("ignored")
110 .help("show only ignored files")
111 .help("show only ignored files")
111 .short('i')
112 .short('i')
112 .action(clap::ArgAction::SetTrue)
113 .action(clap::ArgAction::SetTrue)
113 .long("ignored"),
114 .long("ignored"),
114 )
115 )
115 .arg(
116 .arg(
116 Arg::new("copies")
117 Arg::new("copies")
117 .help("show source of copied files (DEFAULT: ui.statuscopies)")
118 .help("show source of copied files (DEFAULT: ui.statuscopies)")
118 .short('C')
119 .short('C')
119 .action(clap::ArgAction::SetTrue)
120 .action(clap::ArgAction::SetTrue)
120 .long("copies"),
121 .long("copies"),
121 )
122 )
122 .arg(
123 .arg(
123 Arg::new("print0")
124 Arg::new("print0")
124 .help("end filenames with NUL, for use with xargs")
125 .help("end filenames with NUL, for use with xargs")
125 .short('0')
126 .short('0')
126 .action(clap::ArgAction::SetTrue)
127 .action(clap::ArgAction::SetTrue)
127 .long("print0"),
128 .long("print0"),
128 )
129 )
129 .arg(
130 .arg(
130 Arg::new("no-status")
131 Arg::new("no-status")
131 .help("hide status prefix")
132 .help("hide status prefix")
132 .short('n')
133 .short('n')
133 .action(clap::ArgAction::SetTrue)
134 .action(clap::ArgAction::SetTrue)
134 .long("no-status"),
135 .long("no-status"),
135 )
136 )
136 .arg(
137 .arg(
137 Arg::new("verbose")
138 Arg::new("verbose")
138 .help("enable additional output")
139 .help("enable additional output")
139 .short('v')
140 .short('v')
140 .action(clap::ArgAction::SetTrue)
141 .action(clap::ArgAction::SetTrue)
141 .long("verbose"),
142 .long("verbose"),
142 )
143 )
143 }
144 }
144
145
145 /// Pure data type allowing the caller to specify file states to display
146 /// Pure data type allowing the caller to specify file states to display
146 #[derive(Copy, Clone, Debug)]
147 #[derive(Copy, Clone, Debug)]
147 pub struct DisplayStates {
148 pub struct DisplayStates {
148 pub modified: bool,
149 pub modified: bool,
149 pub added: bool,
150 pub added: bool,
150 pub removed: bool,
151 pub removed: bool,
151 pub clean: bool,
152 pub clean: bool,
152 pub deleted: bool,
153 pub deleted: bool,
153 pub unknown: bool,
154 pub unknown: bool,
154 pub ignored: bool,
155 pub ignored: bool,
155 }
156 }
156
157
157 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
158 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
158 modified: true,
159 modified: true,
159 added: true,
160 added: true,
160 removed: true,
161 removed: true,
161 clean: false,
162 clean: false,
162 deleted: true,
163 deleted: true,
163 unknown: true,
164 unknown: true,
164 ignored: false,
165 ignored: false,
165 };
166 };
166
167
167 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
168 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
168 modified: true,
169 modified: true,
169 added: true,
170 added: true,
170 removed: true,
171 removed: true,
171 clean: true,
172 clean: true,
172 deleted: true,
173 deleted: true,
173 unknown: true,
174 unknown: true,
174 ignored: true,
175 ignored: true,
175 };
176 };
176
177
177 impl DisplayStates {
178 impl DisplayStates {
178 pub fn is_empty(&self) -> bool {
179 pub fn is_empty(&self) -> bool {
179 !(self.modified
180 !(self.modified
180 || self.added
181 || self.added
181 || self.removed
182 || self.removed
182 || self.clean
183 || self.clean
183 || self.deleted
184 || self.deleted
184 || self.unknown
185 || self.unknown
185 || self.ignored)
186 || self.ignored)
186 }
187 }
187 }
188 }
188
189
189 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
190 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
190 Ok(repo.dirstate_parents()?.is_merge())
191 Ok(repo.dirstate_parents()?.is_merge())
191 }
192 }
192
193
193 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
194 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
194 // These are all the known values for the [fname] argument of
195 // These are all the known values for the [fname] argument of
195 // [addunfinished] function in [state.py]
196 // [addunfinished] function in [state.py]
196 let known_state_files: &[&str] = &[
197 let known_state_files: &[&str] = &[
197 "bisect.state",
198 "bisect.state",
198 "graftstate",
199 "graftstate",
199 "histedit-state",
200 "histedit-state",
200 "rebasestate",
201 "rebasestate",
201 "shelvedstate",
202 "shelvedstate",
202 "transplant/journal",
203 "transplant/journal",
203 "updatestate",
204 "updatestate",
204 ];
205 ];
205 if has_unfinished_merge(repo)? {
206 if has_unfinished_merge(repo)? {
206 return Ok(true);
207 return Ok(true);
207 };
208 };
208 for f in known_state_files {
209 for f in known_state_files {
209 if repo.hg_vfs().join(f).exists() {
210 if repo.hg_vfs().join(f).exists() {
210 return Ok(true);
211 return Ok(true);
211 }
212 }
212 }
213 }
213 Ok(false)
214 Ok(false)
214 }
215 }
215
216
216 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
217 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
217 // TODO: lift these limitations
218 // TODO: lift these limitations
218 if invocation
219 if invocation
219 .config
220 .config
220 .get(b"commands", b"status.terse")
221 .get(b"commands", b"status.terse")
221 .is_some()
222 .is_some()
222 {
223 {
223 return Err(CommandError::unsupported(
224 return Err(CommandError::unsupported(
224 "status.terse is not yet supported with rhg status",
225 "status.terse is not yet supported with rhg status",
225 ));
226 ));
226 }
227 }
227
228
228 let ui = invocation.ui;
229 let ui = invocation.ui;
229 let config = invocation.config;
230 let config = invocation.config;
230 let args = invocation.subcommand_args;
231 let args = invocation.subcommand_args;
231
232
232 let print0 = args.get_flag("print0");
233 let print0 = args.get_flag("print0");
233 let verbose = args.get_flag("verbose")
234 let verbose = args.get_flag("verbose")
234 || config.get_bool(b"ui", b"verbose")?
235 || config.get_bool(b"ui", b"verbose")?
235 || config.get_bool(b"commands", b"status.verbose")?;
236 || config.get_bool(b"commands", b"status.verbose")?;
236 let verbose = verbose && !print0;
237 let verbose = verbose && !print0;
237
238
238 let all = args.get_flag("all");
239 let all = args.get_flag("all");
239 let display_states = if all {
240 let display_states = if all {
240 // TODO when implementing `--quiet`: it excludes clean files
241 // TODO when implementing `--quiet`: it excludes clean files
241 // from `--all`
242 // from `--all`
242 ALL_DISPLAY_STATES
243 ALL_DISPLAY_STATES
243 } else {
244 } else {
244 let requested = DisplayStates {
245 let requested = DisplayStates {
245 modified: args.get_flag("modified"),
246 modified: args.get_flag("modified"),
246 added: args.get_flag("added"),
247 added: args.get_flag("added"),
247 removed: args.get_flag("removed"),
248 removed: args.get_flag("removed"),
248 clean: args.get_flag("clean"),
249 clean: args.get_flag("clean"),
249 deleted: args.get_flag("deleted"),
250 deleted: args.get_flag("deleted"),
250 unknown: args.get_flag("unknown"),
251 unknown: args.get_flag("unknown"),
251 ignored: args.get_flag("ignored"),
252 ignored: args.get_flag("ignored"),
252 };
253 };
253 if requested.is_empty() {
254 if requested.is_empty() {
254 DEFAULT_DISPLAY_STATES
255 DEFAULT_DISPLAY_STATES
255 } else {
256 } else {
256 requested
257 requested
257 }
258 }
258 };
259 };
259 let no_status = args.get_flag("no-status");
260 let no_status = args.get_flag("no-status");
260 let list_copies = all
261 let list_copies = all
261 || args.get_flag("copies")
262 || args.get_flag("copies")
262 || config.get_bool(b"ui", b"statuscopies")?;
263 || config.get_bool(b"ui", b"statuscopies")?;
263
264
264 let repo = invocation.repo?;
265 let repo = invocation.repo?;
265
266
266 if verbose && has_unfinished_state(repo)? {
267 if verbose && has_unfinished_state(repo)? {
267 return Err(CommandError::unsupported(
268 return Err(CommandError::unsupported(
268 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
269 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
269 ));
270 ));
270 }
271 }
271
272
272 let mut dmap = repo.dirstate_map_mut()?;
273 let mut dmap = repo.dirstate_map_mut()?;
273
274
274 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
275 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
275
276
276 let options = StatusOptions {
277 let options = StatusOptions {
277 check_exec,
278 check_exec,
278 list_clean: display_states.clean,
279 list_clean: display_states.clean,
279 list_unknown: display_states.unknown,
280 list_unknown: display_states.unknown,
280 list_ignored: display_states.ignored,
281 list_ignored: display_states.ignored,
281 list_copies,
282 list_copies,
282 collect_traversed_dirs: false,
283 collect_traversed_dirs: false,
283 };
284 };
284
285
285 type StatusResult<'a> =
286 type StatusResult<'a> =
286 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
287 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
287
288
289 let relative_status = config
290 .get_option(b"commands", b"status.relative")?
291 .expect("commands.status.relative should have a default value");
292
293 let relativize_paths = relative_status || {
294 // See in Python code with `getuipathfn` usage in `commands.py`.
295 let legacy_relative_behavior = args.contains_id("file");
296 match relative_paths(invocation.config)? {
297 RelativePaths::Legacy => legacy_relative_behavior,
298 RelativePaths::Bool(v) => v,
299 }
300 };
301
302 let mut output = DisplayStatusPaths {
303 ui,
304 no_status,
305 relativize: if relativize_paths {
306 Some(RelativizePaths::new(repo)?)
307 } else {
308 None
309 },
310 print0,
311 };
312
288 let after_status = |res: StatusResult| -> Result<_, CommandError> {
313 let after_status = |res: StatusResult| -> Result<_, CommandError> {
289 let (mut ds_status, pattern_warnings) = res?;
314 let (mut ds_status, pattern_warnings) = res?;
290 for warning in pattern_warnings {
315 for warning in pattern_warnings {
291 ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
316 ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
292 }
317 }
293
318
294 for (path, error) in ds_status.bad {
319 for (path, error) in take(&mut ds_status.bad) {
295 let error = match error {
320 let error = match error {
296 hg::BadMatch::OsError(code) => {
321 hg::BadMatch::OsError(code) => {
297 std::io::Error::from_raw_os_error(code).to_string()
322 std::io::Error::from_raw_os_error(code).to_string()
298 }
323 }
299 hg::BadMatch::BadType(ty) => {
324 hg::BadMatch::BadType(ty) => {
300 format!("unsupported file type (type is {})", ty)
325 format!("unsupported file type (type is {})", ty)
301 }
326 }
302 };
327 };
303 ui.write_stderr(&format_bytes!(
328 ui.write_stderr(&format_bytes!(
304 b"{}: {}\n",
329 b"{}: {}\n",
305 path.as_bytes(),
330 path.as_bytes(),
306 error.as_bytes()
331 error.as_bytes()
307 ))?
332 ))?
308 }
333 }
309 if !ds_status.unsure.is_empty() {
334 if !ds_status.unsure.is_empty() {
310 info!(
335 info!(
311 "Files to be rechecked by retrieval from filelog: {:?}",
336 "Files to be rechecked by retrieval from filelog: {:?}",
312 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
337 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
313 );
338 );
314 }
339 }
315 let mut fixup = Vec::new();
340 let mut fixup = Vec::new();
316 if !ds_status.unsure.is_empty()
341 if !ds_status.unsure.is_empty()
317 && (display_states.modified || display_states.clean)
342 && (display_states.modified || display_states.clean)
318 {
343 {
319 let p1 = repo.dirstate_parents()?.p1;
344 let p1 = repo.dirstate_parents()?.p1;
320 let manifest = repo.manifest_for_node(p1).map_err(|e| {
345 let manifest = repo.manifest_for_node(p1).map_err(|e| {
321 CommandError::from((e, &*format!("{:x}", p1.short())))
346 CommandError::from((e, &*format!("{:x}", p1.short())))
322 })?;
347 })?;
323 let working_directory_vfs = repo.working_directory_vfs();
348 let working_directory_vfs = repo.working_directory_vfs();
324 let store_vfs = repo.store_vfs();
349 let store_vfs = repo.store_vfs();
325 let res: Vec<_> = ds_status
350 let res: Vec<_> = take(&mut ds_status.unsure)
326 .unsure
327 .into_par_iter()
351 .into_par_iter()
328 .map(|to_check| {
352 .map(|to_check| {
329 // The compiler seems to get a bit confused with complex
353 // The compiler seems to get a bit confused with complex
330 // inference when using a parallel iterator + map
354 // inference when using a parallel iterator + map
331 // + map_err + collect, so let's just inline some of the
355 // + map_err + collect, so let's just inline some of the
332 // logic.
356 // logic.
333 match unsure_is_modified(
357 match unsure_is_modified(
334 working_directory_vfs,
358 working_directory_vfs,
335 store_vfs,
359 store_vfs,
336 check_exec,
360 check_exec,
337 &manifest,
361 &manifest,
338 &to_check.path,
362 &to_check.path,
339 ) {
363 ) {
340 Err(HgError::IoError { .. }) => {
364 Err(HgError::IoError { .. }) => {
341 // IO errors most likely stem from the file being
365 // IO errors most likely stem from the file being
342 // deleted even though we know it's in the
366 // deleted even though we know it's in the
343 // dirstate.
367 // dirstate.
344 Ok((to_check, UnsureOutcome::Deleted))
368 Ok((to_check, UnsureOutcome::Deleted))
345 }
369 }
346 Ok(outcome) => Ok((to_check, outcome)),
370 Ok(outcome) => Ok((to_check, outcome)),
347 Err(e) => Err(e),
371 Err(e) => Err(e),
348 }
372 }
349 })
373 })
350 .collect::<Result<_, _>>()?;
374 .collect::<Result<_, _>>()?;
351 for (status_path, outcome) in res.into_iter() {
375 for (status_path, outcome) in res.into_iter() {
352 match outcome {
376 match outcome {
353 UnsureOutcome::Clean => {
377 UnsureOutcome::Clean => {
354 if display_states.clean {
378 if display_states.clean {
355 ds_status.clean.push(status_path.clone());
379 ds_status.clean.push(status_path.clone());
356 }
380 }
357 fixup.push(status_path.path.into_owned())
381 fixup.push(status_path.path.into_owned())
358 }
382 }
359 UnsureOutcome::Modified => {
383 UnsureOutcome::Modified => {
360 if display_states.modified {
384 if display_states.modified {
361 ds_status.modified.push(status_path);
385 ds_status.modified.push(status_path);
362 }
386 }
363 }
387 }
364 UnsureOutcome::Deleted => {
388 UnsureOutcome::Deleted => {
365 if display_states.deleted {
389 if display_states.deleted {
366 ds_status.deleted.push(status_path);
390 ds_status.deleted.push(status_path);
367 }
391 }
368 }
392 }
369 }
393 }
370 }
394 }
371 }
395 }
372
396
373 let relative_status = config
374 .get_option(b"commands", b"status.relative")?
375 .expect("commands.status.relative should have a default value");
376
377 let relativize_paths = relative_status || {
378 // See in Python code with `getuipathfn` usage in `commands.py`.
379 let legacy_relative_behavior = args.contains_id("file");
380 match relative_paths(invocation.config)? {
381 RelativePaths::Legacy => legacy_relative_behavior,
382 RelativePaths::Bool(v) => v,
383 }
384 };
385
386 let output = DisplayStatusPaths {
387 ui,
388 no_status,
389 relativize: if relativize_paths {
390 Some(RelativizePaths::new(repo)?)
391 } else {
392 None
393 },
394 print0,
395 };
396 if display_states.modified {
397 output.display(b"M ", "status.modified", ds_status.modified)?;
398 }
399 if display_states.added {
400 output.display(b"A ", "status.added", ds_status.added)?;
401 }
402 if display_states.removed {
403 output.display(b"R ", "status.removed", ds_status.removed)?;
404 }
405 if display_states.deleted {
406 output.display(b"! ", "status.deleted", ds_status.deleted)?;
407 }
408 if display_states.unknown {
409 output.display(b"? ", "status.unknown", ds_status.unknown)?;
410 }
411 if display_states.ignored {
412 output.display(b"I ", "status.ignored", ds_status.ignored)?;
413 }
414 if display_states.clean {
415 output.display(b"C ", "status.clean", ds_status.clean)?;
416 }
417
418 let dirstate_write_needed = ds_status.dirty;
397 let dirstate_write_needed = ds_status.dirty;
419 let filesystem_time_at_status_start =
398 let filesystem_time_at_status_start =
420 ds_status.filesystem_time_at_status_start;
399 ds_status.filesystem_time_at_status_start;
421
400
401 output.output(display_states, ds_status)?;
402
422 Ok((
403 Ok((
423 fixup,
404 fixup,
424 dirstate_write_needed,
405 dirstate_write_needed,
425 filesystem_time_at_status_start,
406 filesystem_time_at_status_start,
426 ))
407 ))
427 };
408 };
428 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
409 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
429 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
410 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
430 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
411 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
431 (true, true) => {
412 (true, true) => {
432 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
413 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
433 }
414 }
434 (true, false) => narrow_matcher,
415 (true, false) => narrow_matcher,
435 (false, true) => sparse_matcher,
416 (false, true) => sparse_matcher,
436 (false, false) => Box::new(AlwaysMatcher),
417 (false, false) => Box::new(AlwaysMatcher),
437 };
418 };
438 let matcher = match args.get_many::<std::ffi::OsString>("file") {
419 let matcher = match args.get_many::<std::ffi::OsString>("file") {
439 None => matcher,
420 None => matcher,
440 Some(files) => {
421 Some(files) => {
441 let patterns: Vec<Vec<u8>> = files
422 let patterns: Vec<Vec<u8>> = files
442 .filter(|s| !s.is_empty())
423 .filter(|s| !s.is_empty())
443 .map(get_bytes_from_os_str)
424 .map(get_bytes_from_os_str)
444 .collect();
425 .collect();
445 for file in &patterns {
426 for file in &patterns {
446 if file.starts_with(b"set:") {
427 if file.starts_with(b"set:") {
447 return Err(CommandError::unsupported("fileset"));
428 return Err(CommandError::unsupported("fileset"));
448 }
429 }
449 }
430 }
450 let cwd = hg::utils::current_dir()?;
431 let cwd = hg::utils::current_dir()?;
451 let root = repo.working_directory_path();
432 let root = repo.working_directory_path();
452 let ignore_patterns = parse_pattern_args(patterns, &cwd, root)?;
433 let ignore_patterns = parse_pattern_args(patterns, &cwd, root)?;
453 let files_matcher =
434 let files_matcher =
454 hg::matchers::PatternMatcher::new(ignore_patterns)?;
435 hg::matchers::PatternMatcher::new(ignore_patterns)?;
455 Box::new(IntersectionMatcher::new(
436 Box::new(IntersectionMatcher::new(
456 Box::new(files_matcher),
437 Box::new(files_matcher),
457 matcher,
438 matcher,
458 ))
439 ))
459 }
440 }
460 };
441 };
461
442
462 print_narrow_sparse_warnings(
443 print_narrow_sparse_warnings(
463 &narrow_warnings,
444 &narrow_warnings,
464 &sparse_warnings,
445 &sparse_warnings,
465 ui,
446 ui,
466 repo,
447 repo,
467 )?;
448 )?;
468 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
449 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
469 dmap.with_status(
450 dmap.with_status(
470 matcher.as_ref(),
451 matcher.as_ref(),
471 repo.working_directory_path().to_owned(),
452 repo.working_directory_path().to_owned(),
472 ignore_files(repo, config),
453 ignore_files(repo, config),
473 options,
454 options,
474 after_status,
455 after_status,
475 )?;
456 )?;
476
457
477 // Development config option to test write races
458 // Development config option to test write races
478 if let Err(e) =
459 if let Err(e) =
479 debug_wait_for_file(config, "status.pre-dirstate-write-file")
460 debug_wait_for_file(config, "status.pre-dirstate-write-file")
480 {
461 {
481 ui.write_stderr(e.as_bytes()).ok();
462 ui.write_stderr(e.as_bytes()).ok();
482 }
463 }
483
464
484 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
465 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
485 && !dirstate_write_needed
466 && !dirstate_write_needed
486 {
467 {
487 // Nothing to update
468 // Nothing to update
488 return Ok(());
469 return Ok(());
489 }
470 }
490
471
491 // Update the dirstate on disk if we can
472 // Update the dirstate on disk if we can
492 let with_lock_result =
473 let with_lock_result =
493 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
474 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
494 if let Some(mtime_boundary) = filesystem_time_at_status_start {
475 if let Some(mtime_boundary) = filesystem_time_at_status_start {
495 for hg_path in fixup {
476 for hg_path in fixup {
496 use std::os::unix::fs::MetadataExt;
477 use std::os::unix::fs::MetadataExt;
497 let fs_path = hg_path_to_path_buf(&hg_path)
478 let fs_path = hg_path_to_path_buf(&hg_path)
498 .expect("HgPath conversion");
479 .expect("HgPath conversion");
499 // Specifically do not reuse `fs_metadata` from
480 // Specifically do not reuse `fs_metadata` from
500 // `unsure_is_clean` which was needed before reading
481 // `unsure_is_clean` which was needed before reading
501 // contents. Here we access metadata again after reading
482 // contents. Here we access metadata again after reading
502 // content, in case it changed in the meantime.
483 // content, in case it changed in the meantime.
503 let metadata_res = repo
484 let metadata_res = repo
504 .working_directory_vfs()
485 .working_directory_vfs()
505 .symlink_metadata(&fs_path);
486 .symlink_metadata(&fs_path);
506 let fs_metadata = match metadata_res {
487 let fs_metadata = match metadata_res {
507 Ok(meta) => meta,
488 Ok(meta) => meta,
508 Err(err) => match err {
489 Err(err) => match err {
509 HgError::IoError { .. } => {
490 HgError::IoError { .. } => {
510 // The file has probably been deleted. In any
491 // The file has probably been deleted. In any
511 // case, it was in the dirstate before, so
492 // case, it was in the dirstate before, so
512 // let's ignore the error.
493 // let's ignore the error.
513 continue;
494 continue;
514 }
495 }
515 _ => return Err(err.into()),
496 _ => return Err(err.into()),
516 },
497 },
517 };
498 };
518 if let Some(mtime) =
499 if let Some(mtime) =
519 TruncatedTimestamp::for_reliable_mtime_of(
500 TruncatedTimestamp::for_reliable_mtime_of(
520 &fs_metadata,
501 &fs_metadata,
521 &mtime_boundary,
502 &mtime_boundary,
522 )
503 )
523 .when_reading_file(&fs_path)?
504 .when_reading_file(&fs_path)?
524 {
505 {
525 let mode = fs_metadata.mode();
506 let mode = fs_metadata.mode();
526 let size = fs_metadata.len();
507 let size = fs_metadata.len();
527 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
508 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
528 dirstate_write_needed = true
509 dirstate_write_needed = true
529 }
510 }
530 }
511 }
531 }
512 }
532 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
513 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
533 if dirstate_write_needed {
514 if dirstate_write_needed {
534 repo.write_dirstate()?
515 repo.write_dirstate()?
535 }
516 }
536 Ok(())
517 Ok(())
537 });
518 });
538 match with_lock_result {
519 match with_lock_result {
539 Ok(closure_result) => closure_result?,
520 Ok(closure_result) => closure_result?,
540 Err(LockError::AlreadyHeld) => {
521 Err(LockError::AlreadyHeld) => {
541 // Not updating the dirstate is not ideal but not critical:
522 // Not updating the dirstate is not ideal but not critical:
542 // don’t keep our caller waiting until some other Mercurial
523 // don’t keep our caller waiting until some other Mercurial
543 // process releases the lock.
524 // process releases the lock.
544 log::info!("not writing dirstate from `status`: lock is held")
525 log::info!("not writing dirstate from `status`: lock is held")
545 }
526 }
546 Err(LockError::Other(HgError::IoError { error, .. }))
527 Err(LockError::Other(HgError::IoError { error, .. }))
547 if error.kind() == io::ErrorKind::PermissionDenied =>
528 if error.kind() == io::ErrorKind::PermissionDenied =>
548 {
529 {
549 // `hg status` on a read-only repository is fine
530 // `hg status` on a read-only repository is fine
550 }
531 }
551 Err(LockError::Other(error)) => {
532 Err(LockError::Other(error)) => {
552 // Report other I/O errors
533 // Report other I/O errors
553 Err(error)?
534 Err(error)?
554 }
535 }
555 }
536 }
556 Ok(())
537 Ok(())
557 }
538 }
558
539
559 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
540 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
560 let mut ignore_files = Vec::new();
541 let mut ignore_files = Vec::new();
561 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
542 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
562 if repo_ignore.exists() {
543 if repo_ignore.exists() {
563 ignore_files.push(repo_ignore)
544 ignore_files.push(repo_ignore)
564 }
545 }
565 for (key, value) in config.iter_section(b"ui") {
546 for (key, value) in config.iter_section(b"ui") {
566 if key == b"ignore" || key.starts_with(b"ignore.") {
547 if key == b"ignore" || key.starts_with(b"ignore.") {
567 let path = get_path_from_bytes(value);
548 let path = get_path_from_bytes(value);
568 // TODO:Β expand "~/" and environment variable here, like Python
549 // TODO:Β expand "~/" and environment variable here, like Python
569 // does with `os.path.expanduser` and `os.path.expandvars`
550 // does with `os.path.expanduser` and `os.path.expandvars`
570
551
571 let joined = repo.working_directory_path().join(path);
552 let joined = repo.working_directory_path().join(path);
572 ignore_files.push(joined);
553 ignore_files.push(joined);
573 }
554 }
574 }
555 }
575 ignore_files
556 ignore_files
576 }
557 }
577
558
578 struct DisplayStatusPaths<'a> {
559 struct DisplayStatusPaths<'a> {
579 ui: &'a Ui,
560 ui: &'a Ui,
580 no_status: bool,
561 no_status: bool,
581 relativize: Option<RelativizePaths>,
562 relativize: Option<RelativizePaths>,
582 print0: bool,
563 print0: bool,
583 }
564 }
584
565
585 impl DisplayStatusPaths<'_> {
566 impl DisplayStatusPaths<'_> {
586 // Probably more elegant to use a Deref or Borrow trait rather than
567 // Probably more elegant to use a Deref or Borrow trait rather than
587 // harcode HgPathBuf, but probably not really useful at this point
568 // harcode HgPathBuf, but probably not really useful at this point
588 fn display(
569 fn display(
589 &self,
570 &self,
590 status_prefix: &[u8],
571 status_prefix: &[u8],
591 label: &'static str,
572 label: &'static str,
592 mut paths: Vec<StatusPath<'_>>,
573 mut paths: Vec<StatusPath<'_>>,
593 ) -> Result<(), CommandError> {
574 ) -> Result<(), CommandError> {
594 paths.sort_unstable();
575 paths.sort_unstable();
595 // TODO: get the stdout lock once for the whole loop
576 // TODO: get the stdout lock once for the whole loop
596 // instead of in each write
577 // instead of in each write
597 for StatusPath { path, copy_source } in paths {
578 for StatusPath { path, copy_source } in paths {
598 let relative_path;
579 let relative_path;
599 let relative_source;
580 let relative_source;
600 let (path, copy_source) = if let Some(relativize) =
581 let (path, copy_source) = if let Some(relativize) =
601 &self.relativize
582 &self.relativize
602 {
583 {
603 relative_path = relativize.relativize(&path);
584 relative_path = relativize.relativize(&path);
604 relative_source =
585 relative_source =
605 copy_source.as_ref().map(|s| relativize.relativize(s));
586 copy_source.as_ref().map(|s| relativize.relativize(s));
606 (&*relative_path, relative_source.as_deref())
587 (&*relative_path, relative_source.as_deref())
607 } else {
588 } else {
608 (path.as_bytes(), copy_source.as_ref().map(|s| s.as_bytes()))
589 (path.as_bytes(), copy_source.as_ref().map(|s| s.as_bytes()))
609 };
590 };
610 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
591 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
611 // in order to stream to stdout instead of allocating an
592 // in order to stream to stdout instead of allocating an
612 // itermediate `Vec<u8>`.
593 // itermediate `Vec<u8>`.
613 if !self.no_status {
594 if !self.no_status {
614 self.ui.write_stdout_labelled(status_prefix, label)?
595 self.ui.write_stdout_labelled(status_prefix, label)?
615 }
596 }
616 let linebreak = if self.print0 { b"\x00" } else { b"\n" };
597 let linebreak = if self.print0 { b"\x00" } else { b"\n" };
617 self.ui.write_stdout_labelled(
598 self.ui.write_stdout_labelled(
618 &format_bytes!(b"{}{}", path, linebreak),
599 &format_bytes!(b"{}{}", path, linebreak),
619 label,
600 label,
620 )?;
601 )?;
621 if let Some(source) = copy_source.filter(|_| !self.no_status) {
602 if let Some(source) = copy_source.filter(|_| !self.no_status) {
622 let label = "status.copied";
603 let label = "status.copied";
623 self.ui.write_stdout_labelled(
604 self.ui.write_stdout_labelled(
624 &format_bytes!(b" {}{}", source, linebreak),
605 &format_bytes!(b" {}{}", source, linebreak),
625 label,
606 label,
626 )?
607 )?
627 }
608 }
628 }
609 }
629 Ok(())
610 Ok(())
630 }
611 }
612
613 fn output(
614 &mut self,
615 display_states: DisplayStates,
616 ds_status: DirstateStatus,
617 ) -> Result<(), CommandError> {
618 if display_states.modified {
619 self.display(b"M ", "status.modified", ds_status.modified)?;
620 }
621 if display_states.added {
622 self.display(b"A ", "status.added", ds_status.added)?;
623 }
624 if display_states.removed {
625 self.display(b"R ", "status.removed", ds_status.removed)?;
626 }
627 if display_states.deleted {
628 self.display(b"! ", "status.deleted", ds_status.deleted)?;
629 }
630 if display_states.unknown {
631 self.display(b"? ", "status.unknown", ds_status.unknown)?;
632 }
633 if display_states.ignored {
634 self.display(b"I ", "status.ignored", ds_status.ignored)?;
635 }
636 if display_states.clean {
637 self.display(b"C ", "status.clean", ds_status.clean)?;
638 }
639 Ok(())
640 }
631 }
641 }
632
642
633 /// Outcome of the additional check for an ambiguous tracked file
643 /// Outcome of the additional check for an ambiguous tracked file
634 enum UnsureOutcome {
644 enum UnsureOutcome {
635 /// The file is actually clean
645 /// The file is actually clean
636 Clean,
646 Clean,
637 /// The file has been modified
647 /// The file has been modified
638 Modified,
648 Modified,
639 /// The file was deleted on disk (or became another type of fs entry)
649 /// The file was deleted on disk (or became another type of fs entry)
640 Deleted,
650 Deleted,
641 }
651 }
642
652
643 /// Check if a file is modified by comparing actual repo store and file system.
653 /// Check if a file is modified by comparing actual repo store and file system.
644 ///
654 ///
645 /// This meant to be used for those that the dirstate cannot resolve, due
655 /// This meant to be used for those that the dirstate cannot resolve, due
646 /// to time resolution limits.
656 /// to time resolution limits.
647 fn unsure_is_modified(
657 fn unsure_is_modified(
648 working_directory_vfs: hg::vfs::Vfs,
658 working_directory_vfs: hg::vfs::Vfs,
649 store_vfs: hg::vfs::Vfs,
659 store_vfs: hg::vfs::Vfs,
650 check_exec: bool,
660 check_exec: bool,
651 manifest: &Manifest,
661 manifest: &Manifest,
652 hg_path: &HgPath,
662 hg_path: &HgPath,
653 ) -> Result<UnsureOutcome, HgError> {
663 ) -> Result<UnsureOutcome, HgError> {
654 let vfs = working_directory_vfs;
664 let vfs = working_directory_vfs;
655 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
665 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
656 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
666 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
657 let is_symlink = fs_metadata.file_type().is_symlink();
667 let is_symlink = fs_metadata.file_type().is_symlink();
658
668
659 let entry = manifest
669 let entry = manifest
660 .find_by_path(hg_path)?
670 .find_by_path(hg_path)?
661 .expect("ambgious file not in p1");
671 .expect("ambgious file not in p1");
662
672
663 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
673 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
664 // dirstate
674 // dirstate
665 let fs_flags = if is_symlink {
675 let fs_flags = if is_symlink {
666 Some(b'l')
676 Some(b'l')
667 } else if check_exec && has_exec_bit(&fs_metadata) {
677 } else if check_exec && has_exec_bit(&fs_metadata) {
668 Some(b'x')
678 Some(b'x')
669 } else {
679 } else {
670 None
680 None
671 };
681 };
672
682
673 let entry_flags = if check_exec {
683 let entry_flags = if check_exec {
674 entry.flags
684 entry.flags
675 } else if entry.flags == Some(b'x') {
685 } else if entry.flags == Some(b'x') {
676 None
686 None
677 } else {
687 } else {
678 entry.flags
688 entry.flags
679 };
689 };
680
690
681 if entry_flags != fs_flags {
691 if entry_flags != fs_flags {
682 return Ok(UnsureOutcome::Modified);
692 return Ok(UnsureOutcome::Modified);
683 }
693 }
684 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
694 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
685 let fs_len = fs_metadata.len();
695 let fs_len = fs_metadata.len();
686 let file_node = entry.node_id()?;
696 let file_node = entry.node_id()?;
687 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
697 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
688 HgError::corrupted(format!(
698 HgError::corrupted(format!(
689 "filelog {:?} missing node {:?} from manifest",
699 "filelog {:?} missing node {:?} from manifest",
690 hg_path, file_node
700 hg_path, file_node
691 ))
701 ))
692 })?;
702 })?;
693 if filelog_entry.file_data_len_not_equal_to(fs_len) {
703 if filelog_entry.file_data_len_not_equal_to(fs_len) {
694 // No need to read file contents:
704 // No need to read file contents:
695 // it cannot be equal if it has a different length.
705 // it cannot be equal if it has a different length.
696 return Ok(UnsureOutcome::Modified);
706 return Ok(UnsureOutcome::Modified);
697 }
707 }
698
708
699 let p1_filelog_data = filelog_entry.data()?;
709 let p1_filelog_data = filelog_entry.data()?;
700 let p1_contents = p1_filelog_data.file_data()?;
710 let p1_contents = p1_filelog_data.file_data()?;
701 if p1_contents.len() as u64 != fs_len {
711 if p1_contents.len() as u64 != fs_len {
702 // No need to read file contents:
712 // No need to read file contents:
703 // it cannot be equal if it has a different length.
713 // it cannot be equal if it has a different length.
704 return Ok(UnsureOutcome::Modified);
714 return Ok(UnsureOutcome::Modified);
705 }
715 }
706
716
707 let fs_contents = if is_symlink {
717 let fs_contents = if is_symlink {
708 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
718 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
709 } else {
719 } else {
710 vfs.read(fs_path)?
720 vfs.read(fs_path)?
711 };
721 };
712
722
713 Ok(if p1_contents != &*fs_contents {
723 Ok(if p1_contents != &*fs_contents {
714 UnsureOutcome::Modified
724 UnsureOutcome::Modified
715 } else {
725 } else {
716 UnsureOutcome::Clean
726 UnsureOutcome::Clean
717 })
727 })
718 }
728 }
General Comments 0
You need to be logged in to leave comments. Login now