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