##// END OF EJS Templates
rhg: Rename cat_file_is_modified...
Simon Sapin -
r49167:b6d8eea9 default
parent child Browse files
Show More
@@ -1,325 +1,325 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, UiError};
9 use crate::ui::{Ui, UiError};
10 use crate::utils::path_utils::relativize_paths;
10 use crate::utils::path_utils::relativize_paths;
11 use clap::{Arg, SubCommand};
11 use clap::{Arg, SubCommand};
12 use hg;
12 use hg;
13 use hg::config::Config;
13 use hg::config::Config;
14 use hg::dirstate::TruncatedTimestamp;
14 use hg::dirstate::TruncatedTimestamp;
15 use hg::errors::HgError;
15 use hg::errors::HgError;
16 use hg::manifest::Manifest;
16 use hg::manifest::Manifest;
17 use hg::matchers::AlwaysMatcher;
17 use hg::matchers::AlwaysMatcher;
18 use hg::repo::Repo;
18 use hg::repo::Repo;
19 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
19 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
20 use hg::{HgPathCow, StatusOptions};
20 use hg::{HgPathCow, StatusOptions};
21 use log::{info, warn};
21 use log::{info, warn};
22 use std::borrow::Cow;
22 use std::borrow::Cow;
23
23
24 pub const HELP_TEXT: &str = "
24 pub const HELP_TEXT: &str = "
25 Show changed files in the working directory
25 Show changed files in the working directory
26
26
27 This is a pure Rust version of `hg status`.
27 This is a pure Rust version of `hg status`.
28
28
29 Some options might be missing, check the list below.
29 Some options might be missing, check the list below.
30 ";
30 ";
31
31
32 pub fn args() -> clap::App<'static, 'static> {
32 pub fn args() -> clap::App<'static, 'static> {
33 SubCommand::with_name("status")
33 SubCommand::with_name("status")
34 .alias("st")
34 .alias("st")
35 .about(HELP_TEXT)
35 .about(HELP_TEXT)
36 .arg(
36 .arg(
37 Arg::with_name("all")
37 Arg::with_name("all")
38 .help("show status of all files")
38 .help("show status of all files")
39 .short("-A")
39 .short("-A")
40 .long("--all"),
40 .long("--all"),
41 )
41 )
42 .arg(
42 .arg(
43 Arg::with_name("modified")
43 Arg::with_name("modified")
44 .help("show only modified files")
44 .help("show only modified files")
45 .short("-m")
45 .short("-m")
46 .long("--modified"),
46 .long("--modified"),
47 )
47 )
48 .arg(
48 .arg(
49 Arg::with_name("added")
49 Arg::with_name("added")
50 .help("show only added files")
50 .help("show only added files")
51 .short("-a")
51 .short("-a")
52 .long("--added"),
52 .long("--added"),
53 )
53 )
54 .arg(
54 .arg(
55 Arg::with_name("removed")
55 Arg::with_name("removed")
56 .help("show only removed files")
56 .help("show only removed files")
57 .short("-r")
57 .short("-r")
58 .long("--removed"),
58 .long("--removed"),
59 )
59 )
60 .arg(
60 .arg(
61 Arg::with_name("clean")
61 Arg::with_name("clean")
62 .help("show only clean files")
62 .help("show only clean files")
63 .short("-c")
63 .short("-c")
64 .long("--clean"),
64 .long("--clean"),
65 )
65 )
66 .arg(
66 .arg(
67 Arg::with_name("deleted")
67 Arg::with_name("deleted")
68 .help("show only deleted files")
68 .help("show only deleted files")
69 .short("-d")
69 .short("-d")
70 .long("--deleted"),
70 .long("--deleted"),
71 )
71 )
72 .arg(
72 .arg(
73 Arg::with_name("unknown")
73 Arg::with_name("unknown")
74 .help("show only unknown (not tracked) files")
74 .help("show only unknown (not tracked) files")
75 .short("-u")
75 .short("-u")
76 .long("--unknown"),
76 .long("--unknown"),
77 )
77 )
78 .arg(
78 .arg(
79 Arg::with_name("ignored")
79 Arg::with_name("ignored")
80 .help("show only ignored files")
80 .help("show only ignored files")
81 .short("-i")
81 .short("-i")
82 .long("--ignored"),
82 .long("--ignored"),
83 )
83 )
84 }
84 }
85
85
86 /// Pure data type allowing the caller to specify file states to display
86 /// Pure data type allowing the caller to specify file states to display
87 #[derive(Copy, Clone, Debug)]
87 #[derive(Copy, Clone, Debug)]
88 pub struct DisplayStates {
88 pub struct DisplayStates {
89 pub modified: bool,
89 pub modified: bool,
90 pub added: bool,
90 pub added: bool,
91 pub removed: bool,
91 pub removed: bool,
92 pub clean: bool,
92 pub clean: bool,
93 pub deleted: bool,
93 pub deleted: bool,
94 pub unknown: bool,
94 pub unknown: bool,
95 pub ignored: bool,
95 pub ignored: bool,
96 }
96 }
97
97
98 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
98 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
99 modified: true,
99 modified: true,
100 added: true,
100 added: true,
101 removed: true,
101 removed: true,
102 clean: false,
102 clean: false,
103 deleted: true,
103 deleted: true,
104 unknown: true,
104 unknown: true,
105 ignored: false,
105 ignored: false,
106 };
106 };
107
107
108 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
108 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
109 modified: true,
109 modified: true,
110 added: true,
110 added: true,
111 removed: true,
111 removed: true,
112 clean: true,
112 clean: true,
113 deleted: true,
113 deleted: true,
114 unknown: true,
114 unknown: true,
115 ignored: true,
115 ignored: true,
116 };
116 };
117
117
118 impl DisplayStates {
118 impl DisplayStates {
119 pub fn is_empty(&self) -> bool {
119 pub fn is_empty(&self) -> bool {
120 !(self.modified
120 !(self.modified
121 || self.added
121 || self.added
122 || self.removed
122 || self.removed
123 || self.clean
123 || self.clean
124 || self.deleted
124 || self.deleted
125 || self.unknown
125 || self.unknown
126 || self.ignored)
126 || self.ignored)
127 }
127 }
128 }
128 }
129
129
130 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
130 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
131 let status_enabled_default = false;
131 let status_enabled_default = false;
132 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
132 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
133 if !status_enabled.unwrap_or(status_enabled_default) {
133 if !status_enabled.unwrap_or(status_enabled_default) {
134 return Err(CommandError::unsupported(
134 return Err(CommandError::unsupported(
135 "status is experimental in rhg (enable it with 'rhg.status = true' \
135 "status is experimental in rhg (enable it with 'rhg.status = true' \
136 or enable fallback with 'rhg.on-unsupported = fallback')"
136 or enable fallback with 'rhg.on-unsupported = fallback')"
137 ));
137 ));
138 }
138 }
139
139
140 // TODO: lift these limitations
140 // TODO: lift these limitations
141 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
141 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
142 return Err(CommandError::unsupported(
142 return Err(CommandError::unsupported(
143 "ui.tweakdefaults is not yet supported with rhg status",
143 "ui.tweakdefaults is not yet supported with rhg status",
144 ));
144 ));
145 }
145 }
146 if invocation.config.get_bool(b"ui", b"statuscopies")? {
146 if invocation.config.get_bool(b"ui", b"statuscopies")? {
147 return Err(CommandError::unsupported(
147 return Err(CommandError::unsupported(
148 "ui.statuscopies is not yet supported with rhg status",
148 "ui.statuscopies is not yet supported with rhg status",
149 ));
149 ));
150 }
150 }
151 if invocation
151 if invocation
152 .config
152 .config
153 .get(b"commands", b"status.terse")
153 .get(b"commands", b"status.terse")
154 .is_some()
154 .is_some()
155 {
155 {
156 return Err(CommandError::unsupported(
156 return Err(CommandError::unsupported(
157 "status.terse is not yet supported with rhg status",
157 "status.terse is not yet supported with rhg status",
158 ));
158 ));
159 }
159 }
160
160
161 let ui = invocation.ui;
161 let ui = invocation.ui;
162 let config = invocation.config;
162 let config = invocation.config;
163 let args = invocation.subcommand_args;
163 let args = invocation.subcommand_args;
164 let display_states = if args.is_present("all") {
164 let display_states = if args.is_present("all") {
165 // TODO when implementing `--quiet`: it excludes clean files
165 // TODO when implementing `--quiet`: it excludes clean files
166 // from `--all`
166 // from `--all`
167 ALL_DISPLAY_STATES
167 ALL_DISPLAY_STATES
168 } else {
168 } else {
169 let requested = DisplayStates {
169 let requested = DisplayStates {
170 modified: args.is_present("modified"),
170 modified: args.is_present("modified"),
171 added: args.is_present("added"),
171 added: args.is_present("added"),
172 removed: args.is_present("removed"),
172 removed: args.is_present("removed"),
173 clean: args.is_present("clean"),
173 clean: args.is_present("clean"),
174 deleted: args.is_present("deleted"),
174 deleted: args.is_present("deleted"),
175 unknown: args.is_present("unknown"),
175 unknown: args.is_present("unknown"),
176 ignored: args.is_present("ignored"),
176 ignored: args.is_present("ignored"),
177 };
177 };
178 if requested.is_empty() {
178 if requested.is_empty() {
179 DEFAULT_DISPLAY_STATES
179 DEFAULT_DISPLAY_STATES
180 } else {
180 } else {
181 requested
181 requested
182 }
182 }
183 };
183 };
184
184
185 let repo = invocation.repo?;
185 let repo = invocation.repo?;
186 let mut dmap = repo.dirstate_map_mut()?;
186 let mut dmap = repo.dirstate_map_mut()?;
187
187
188 let options = StatusOptions {
188 let options = StatusOptions {
189 // TODO should be provided by the dirstate parsing and
189 // TODO should be provided by the dirstate parsing and
190 // hence be stored on dmap. Using a value that assumes we aren't
190 // hence be stored on dmap. Using a value that assumes we aren't
191 // below the time resolution granularity of the FS and the
191 // below the time resolution granularity of the FS and the
192 // dirstate.
192 // dirstate.
193 last_normal_time: TruncatedTimestamp::new_truncate(0, 0),
193 last_normal_time: TruncatedTimestamp::new_truncate(0, 0),
194 // we're currently supporting file systems with exec flags only
194 // we're currently supporting file systems with exec flags only
195 // anyway
195 // anyway
196 check_exec: true,
196 check_exec: true,
197 list_clean: display_states.clean,
197 list_clean: display_states.clean,
198 list_unknown: display_states.unknown,
198 list_unknown: display_states.unknown,
199 list_ignored: display_states.ignored,
199 list_ignored: display_states.ignored,
200 collect_traversed_dirs: false,
200 collect_traversed_dirs: false,
201 };
201 };
202 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
202 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
203 let (mut ds_status, pattern_warnings) = dmap.status(
203 let (mut ds_status, pattern_warnings) = dmap.status(
204 &AlwaysMatcher,
204 &AlwaysMatcher,
205 repo.working_directory_path().to_owned(),
205 repo.working_directory_path().to_owned(),
206 vec![ignore_file],
206 vec![ignore_file],
207 options,
207 options,
208 )?;
208 )?;
209 if !pattern_warnings.is_empty() {
209 if !pattern_warnings.is_empty() {
210 warn!("Pattern warnings: {:?}", &pattern_warnings);
210 warn!("Pattern warnings: {:?}", &pattern_warnings);
211 }
211 }
212
212
213 if !ds_status.bad.is_empty() {
213 if !ds_status.bad.is_empty() {
214 warn!("Bad matches {:?}", &(ds_status.bad))
214 warn!("Bad matches {:?}", &(ds_status.bad))
215 }
215 }
216 if !ds_status.unsure.is_empty() {
216 if !ds_status.unsure.is_empty() {
217 info!(
217 info!(
218 "Files to be rechecked by retrieval from filelog: {:?}",
218 "Files to be rechecked by retrieval from filelog: {:?}",
219 &ds_status.unsure
219 &ds_status.unsure
220 );
220 );
221 }
221 }
222 if !ds_status.unsure.is_empty()
222 if !ds_status.unsure.is_empty()
223 && (display_states.modified || display_states.clean)
223 && (display_states.modified || display_states.clean)
224 {
224 {
225 let p1 = repo.dirstate_parents()?.p1;
225 let p1 = repo.dirstate_parents()?.p1;
226 let manifest = repo.manifest_for_node(p1).map_err(|e| {
226 let manifest = repo.manifest_for_node(p1).map_err(|e| {
227 CommandError::from((e, &*format!("{:x}", p1.short())))
227 CommandError::from((e, &*format!("{:x}", p1.short())))
228 })?;
228 })?;
229 for to_check in ds_status.unsure {
229 for to_check in ds_status.unsure {
230 if cat_file_is_modified(repo, &manifest, &to_check)? {
230 if unsure_is_modified(repo, &manifest, &to_check)? {
231 if display_states.modified {
231 if display_states.modified {
232 ds_status.modified.push(to_check);
232 ds_status.modified.push(to_check);
233 }
233 }
234 } else {
234 } else {
235 if display_states.clean {
235 if display_states.clean {
236 ds_status.clean.push(to_check);
236 ds_status.clean.push(to_check);
237 }
237 }
238 }
238 }
239 }
239 }
240 }
240 }
241 if display_states.modified {
241 if display_states.modified {
242 display_status_paths(ui, repo, config, &mut ds_status.modified, b"M")?;
242 display_status_paths(ui, repo, config, &mut ds_status.modified, b"M")?;
243 }
243 }
244 if display_states.added {
244 if display_states.added {
245 display_status_paths(ui, repo, config, &mut ds_status.added, b"A")?;
245 display_status_paths(ui, repo, config, &mut ds_status.added, b"A")?;
246 }
246 }
247 if display_states.removed {
247 if display_states.removed {
248 display_status_paths(ui, repo, config, &mut ds_status.removed, b"R")?;
248 display_status_paths(ui, repo, config, &mut ds_status.removed, b"R")?;
249 }
249 }
250 if display_states.deleted {
250 if display_states.deleted {
251 display_status_paths(ui, repo, config, &mut ds_status.deleted, b"!")?;
251 display_status_paths(ui, repo, config, &mut ds_status.deleted, b"!")?;
252 }
252 }
253 if display_states.unknown {
253 if display_states.unknown {
254 display_status_paths(ui, repo, config, &mut ds_status.unknown, b"?")?;
254 display_status_paths(ui, repo, config, &mut ds_status.unknown, b"?")?;
255 }
255 }
256 if display_states.ignored {
256 if display_states.ignored {
257 display_status_paths(ui, repo, config, &mut ds_status.ignored, b"I")?;
257 display_status_paths(ui, repo, config, &mut ds_status.ignored, b"I")?;
258 }
258 }
259 if display_states.clean {
259 if display_states.clean {
260 display_status_paths(ui, repo, config, &mut ds_status.clean, b"C")?;
260 display_status_paths(ui, repo, config, &mut ds_status.clean, b"C")?;
261 }
261 }
262 Ok(())
262 Ok(())
263 }
263 }
264
264
265 // Probably more elegant to use a Deref or Borrow trait rather than
265 // Probably more elegant to use a Deref or Borrow trait rather than
266 // harcode HgPathBuf, but probably not really useful at this point
266 // harcode HgPathBuf, but probably not really useful at this point
267 fn display_status_paths(
267 fn display_status_paths(
268 ui: &Ui,
268 ui: &Ui,
269 repo: &Repo,
269 repo: &Repo,
270 config: &Config,
270 config: &Config,
271 paths: &mut [HgPathCow],
271 paths: &mut [HgPathCow],
272 status_prefix: &[u8],
272 status_prefix: &[u8],
273 ) -> Result<(), CommandError> {
273 ) -> Result<(), CommandError> {
274 paths.sort_unstable();
274 paths.sort_unstable();
275 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
275 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
276 relative = config
276 relative = config
277 .get_option(b"commands", b"status.relative")?
277 .get_option(b"commands", b"status.relative")?
278 .unwrap_or(relative);
278 .unwrap_or(relative);
279 if relative && !ui.plain() {
279 if relative && !ui.plain() {
280 relativize_paths(
280 relativize_paths(
281 repo,
281 repo,
282 paths.iter().map(Ok),
282 paths.iter().map(Ok),
283 |path: Cow<[u8]>| -> Result<(), UiError> {
283 |path: Cow<[u8]>| -> Result<(), UiError> {
284 ui.write_stdout(
284 ui.write_stdout(
285 &[status_prefix, b" ", path.as_ref(), b"\n"].concat(),
285 &[status_prefix, b" ", path.as_ref(), b"\n"].concat(),
286 )
286 )
287 },
287 },
288 )?;
288 )?;
289 } else {
289 } else {
290 for path in paths {
290 for path in paths {
291 // Same TODO as in commands::root
291 // Same TODO as in commands::root
292 let bytes: &[u8] = path.as_bytes();
292 let bytes: &[u8] = path.as_bytes();
293 // TODO optim, probably lots of unneeded copies here, especially
293 // TODO optim, probably lots of unneeded copies here, especially
294 // if out stream is buffered
294 // if out stream is buffered
295 ui.write_stdout(&[status_prefix, b" ", bytes, b"\n"].concat())?;
295 ui.write_stdout(&[status_prefix, b" ", bytes, b"\n"].concat())?;
296 }
296 }
297 }
297 }
298 Ok(())
298 Ok(())
299 }
299 }
300
300
301 /// Check if a file is modified by comparing actual repo store and file system.
301 /// Check if a file is modified by comparing actual repo store and file system.
302 ///
302 ///
303 /// This meant to be used for those that the dirstate cannot resolve, due
303 /// This meant to be used for those that the dirstate cannot resolve, due
304 /// to time resolution limits.
304 /// to time resolution limits.
305 ///
305 ///
306 /// TODO: detect permission bits and similar metadata modifications
306 /// TODO: detect permission bits and similar metadata modifications
307 fn cat_file_is_modified(
307 fn unsure_is_modified(
308 repo: &Repo,
308 repo: &Repo,
309 manifest: &Manifest,
309 manifest: &Manifest,
310 hg_path: &HgPath,
310 hg_path: &HgPath,
311 ) -> Result<bool, HgError> {
311 ) -> Result<bool, HgError> {
312 let entry = manifest
312 let entry = manifest
313 .find_file(hg_path)?
313 .find_file(hg_path)?
314 .expect("ambgious file not in p1");
314 .expect("ambgious file not in p1");
315 let filelog = repo.filelog(hg_path)?;
315 let filelog = repo.filelog(hg_path)?;
316 let filelog_entry =
316 let filelog_entry =
317 filelog.data_for_node(entry.node_id()?).map_err(|_| {
317 filelog.data_for_node(entry.node_id()?).map_err(|_| {
318 HgError::corrupted("filelog missing node from manifest")
318 HgError::corrupted("filelog missing node from manifest")
319 })?;
319 })?;
320 let contents_in_p1 = filelog_entry.data()?;
320 let contents_in_p1 = filelog_entry.data()?;
321
321
322 let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
322 let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
323 let fs_contents = repo.working_directory_vfs().read(fs_path)?;
323 let fs_contents = repo.working_directory_vfs().read(fs_path)?;
324 return Ok(contents_in_p1 != &*fs_contents);
324 return Ok(contents_in_p1 != &*fs_contents);
325 }
325 }
General Comments 0
You need to be logged in to leave comments. Login now