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