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