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