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