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