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