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