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