##// END OF EJS Templates
rhg: make output of `files` relative to the current directory and the root...
Raphaël Gomès -
r46007:1b319704 default
parent child Browse files
Show More
@@ -1,85 +1,94 b''
1 1 // list_tracked_files.rs
2 2 //
3 3 // Copyright 2020 Antoine Cezar <antoine.cezar@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 super::find_root;
9 9 use crate::dirstate::parsers::parse_dirstate;
10 10 use crate::utils::hg_path::HgPath;
11 11 use crate::{DirstateParseError, EntryState};
12 12 use rayon::prelude::*;
13 13 use std::convert::From;
14 14 use std::fmt;
15 15 use std::fs;
16 16 use std::io;
17 use std::path::PathBuf;
17 use std::path::{Path, PathBuf};
18 18
19 19 /// Kind of error encoutered by ListTrackedFiles
20 20 #[derive(Debug)]
21 21 pub enum ListTrackedFilesErrorKind {
22 22 ParseError(DirstateParseError),
23 23 }
24 24
25 25 /// A ListTrackedFiles error
26 26 #[derive(Debug)]
27 27 pub struct ListTrackedFilesError {
28 28 /// Kind of error encoutered by ListTrackedFiles
29 29 pub kind: ListTrackedFilesErrorKind,
30 30 }
31 31
32 32 impl std::error::Error for ListTrackedFilesError {}
33 33
34 34 impl fmt::Display for ListTrackedFilesError {
35 35 fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 36 unimplemented!()
37 37 }
38 38 }
39 39
40 40 impl From<ListTrackedFilesErrorKind> for ListTrackedFilesError {
41 41 fn from(kind: ListTrackedFilesErrorKind) -> Self {
42 42 ListTrackedFilesError { kind }
43 43 }
44 44 }
45 45
46 46 /// List files under Mercurial control in the working directory
47 47 pub struct ListTrackedFiles {
48 48 root: PathBuf,
49 49 }
50 50
51 51 impl ListTrackedFiles {
52 52 pub fn new() -> Result<Self, find_root::FindRootError> {
53 53 let root = find_root::FindRoot::new().run()?;
54 54 Ok(ListTrackedFiles { root })
55 55 }
56 56
57 57 /// Load the tracked files data from disk
58 58 pub fn load(&self) -> Result<ListDirstateTrackedFiles, io::Error> {
59 59 let dirstate = &self.root.join(".hg/dirstate");
60 60 let content = fs::read(&dirstate)?;
61 61 Ok(ListDirstateTrackedFiles { content })
62 62 }
63
64 /// Returns the repository root directory
65 /// TODO I think this is a crutch that creates a dependency that should not
66 /// be there. Operations that need the root of the repository should get
67 /// it themselves, probably in a lazy fashion. But this would make the
68 /// current series even larger, so this is simplified for now.
69 pub fn get_root(&self) -> &Path {
70 &self.root
71 }
63 72 }
64 73
65 74 /// List files under Mercurial control in the working directory
66 75 /// by reading the dirstate
67 76 pub struct ListDirstateTrackedFiles {
68 77 content: Vec<u8>,
69 78 }
70 79
71 80 impl ListDirstateTrackedFiles {
72 81 pub fn run(&self) -> Result<Vec<&HgPath>, ListTrackedFilesError> {
73 82 let (_, entries, _) = parse_dirstate(&self.content)
74 83 .map_err(ListTrackedFilesErrorKind::ParseError)?;
75 84 let mut files: Vec<&HgPath> = entries
76 85 .into_iter()
77 86 .filter_map(|(path, entry)| match entry.state {
78 87 EntryState::Removed => None,
79 88 _ => Some(path),
80 89 })
81 90 .collect();
82 91 files.par_sort_unstable();
83 92 Ok(files)
84 93 }
85 94 }
@@ -1,382 +1,442 b''
1 1 // files.rs
2 2 //
3 3 // Copyright 2019
4 4 // Raphaël Gomès <rgomes@octobus.net>,
5 5 // Yuya Nishihara <yuya@tcha.org>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 //! Functions for fiddling with files.
11 11
12 12 use crate::utils::{
13 13 hg_path::{path_to_hg_path_buf, HgPath, HgPathBuf, HgPathError},
14 14 path_auditor::PathAuditor,
15 15 replace_slice,
16 16 };
17 17 use lazy_static::lazy_static;
18 18 use same_file::is_same_file;
19 use std::borrow::ToOwned;
19 use std::borrow::{Cow, ToOwned};
20 20 use std::fs::Metadata;
21 21 use std::iter::FusedIterator;
22 22 use std::ops::Deref;
23 23 use std::path::{Path, PathBuf};
24 24
25 25 pub fn get_path_from_bytes(bytes: &[u8]) -> &Path {
26 26 let os_str;
27 27 #[cfg(unix)]
28 28 {
29 29 use std::os::unix::ffi::OsStrExt;
30 30 os_str = std::ffi::OsStr::from_bytes(bytes);
31 31 }
32 32 // TODO Handle other platforms
33 33 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
34 34 // Perhaps, the return type would have to be Result<PathBuf>.
35 35
36 36 Path::new(os_str)
37 37 }
38 38
39 39 // TODO: need to convert from WTF8 to MBCS bytes on Windows.
40 40 // that's why Vec<u8> is returned.
41 41 #[cfg(unix)]
42 42 pub fn get_bytes_from_path(path: impl AsRef<Path>) -> Vec<u8> {
43 43 use std::os::unix::ffi::OsStrExt;
44 44 path.as_ref().as_os_str().as_bytes().to_vec()
45 45 }
46 46
47 47 /// An iterator over repository path yielding itself and its ancestors.
48 48 #[derive(Copy, Clone, Debug)]
49 49 pub struct Ancestors<'a> {
50 50 next: Option<&'a HgPath>,
51 51 }
52 52
53 53 impl<'a> Iterator for Ancestors<'a> {
54 54 type Item = &'a HgPath;
55 55
56 56 fn next(&mut self) -> Option<Self::Item> {
57 57 let next = self.next;
58 58 self.next = match self.next {
59 59 Some(s) if s.is_empty() => None,
60 60 Some(s) => {
61 61 let p = s.bytes().rposition(|c| *c == b'/').unwrap_or(0);
62 62 Some(HgPath::new(&s.as_bytes()[..p]))
63 63 }
64 64 None => None,
65 65 };
66 66 next
67 67 }
68 68 }
69 69
70 70 impl<'a> FusedIterator for Ancestors<'a> {}
71 71
72 72 /// An iterator over repository path yielding itself and its ancestors.
73 73 #[derive(Copy, Clone, Debug)]
74 74 pub(crate) struct AncestorsWithBase<'a> {
75 75 next: Option<(&'a HgPath, &'a HgPath)>,
76 76 }
77 77
78 78 impl<'a> Iterator for AncestorsWithBase<'a> {
79 79 type Item = (&'a HgPath, &'a HgPath);
80 80
81 81 fn next(&mut self) -> Option<Self::Item> {
82 82 let next = self.next;
83 83 self.next = match self.next {
84 84 Some((s, _)) if s.is_empty() => None,
85 85 Some((s, _)) => Some(s.split_filename()),
86 86 None => None,
87 87 };
88 88 next
89 89 }
90 90 }
91 91
92 92 impl<'a> FusedIterator for AncestorsWithBase<'a> {}
93 93
94 94 /// Returns an iterator yielding ancestor directories of the given repository
95 95 /// path.
96 96 ///
97 97 /// The path is separated by '/', and must not start with '/'.
98 98 ///
99 99 /// The path itself isn't included unless it is b"" (meaning the root
100 100 /// directory.)
101 101 pub fn find_dirs(path: &HgPath) -> Ancestors {
102 102 let mut dirs = Ancestors { next: Some(path) };
103 103 if !path.is_empty() {
104 104 dirs.next(); // skip itself
105 105 }
106 106 dirs
107 107 }
108 108
109 109 /// Returns an iterator yielding ancestor directories of the given repository
110 110 /// path.
111 111 ///
112 112 /// The path is separated by '/', and must not start with '/'.
113 113 ///
114 114 /// The path itself isn't included unless it is b"" (meaning the root
115 115 /// directory.)
116 116 pub(crate) fn find_dirs_with_base(path: &HgPath) -> AncestorsWithBase {
117 117 let mut dirs = AncestorsWithBase {
118 118 next: Some((path, HgPath::new(b""))),
119 119 };
120 120 if !path.is_empty() {
121 121 dirs.next(); // skip itself
122 122 }
123 123 dirs
124 124 }
125 125
126 126 /// TODO more than ASCII?
127 127 pub fn normalize_case(path: &HgPath) -> HgPathBuf {
128 128 #[cfg(windows)] // NTFS compares via upper()
129 129 return path.to_ascii_uppercase();
130 130 #[cfg(unix)]
131 131 path.to_ascii_lowercase()
132 132 }
133 133
134 134 lazy_static! {
135 135 static ref IGNORED_CHARS: Vec<Vec<u8>> = {
136 136 [
137 137 0x200c, 0x200d, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d,
138 138 0x202e, 0x206a, 0x206b, 0x206c, 0x206d, 0x206e, 0x206f, 0xfeff,
139 139 ]
140 140 .iter()
141 141 .map(|code| {
142 142 std::char::from_u32(*code)
143 143 .unwrap()
144 144 .encode_utf8(&mut [0; 3])
145 145 .bytes()
146 146 .collect()
147 147 })
148 148 .collect()
149 149 };
150 150 }
151 151
152 152 fn hfs_ignore_clean(bytes: &[u8]) -> Vec<u8> {
153 153 let mut buf = bytes.to_owned();
154 154 let needs_escaping = bytes.iter().any(|b| *b == b'\xe2' || *b == b'\xef');
155 155 if needs_escaping {
156 156 for forbidden in IGNORED_CHARS.iter() {
157 157 replace_slice(&mut buf, forbidden, &[])
158 158 }
159 159 buf
160 160 } else {
161 161 buf
162 162 }
163 163 }
164 164
165 165 pub fn lower_clean(bytes: &[u8]) -> Vec<u8> {
166 166 hfs_ignore_clean(&bytes.to_ascii_lowercase())
167 167 }
168 168
169 169 #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
170 170 pub struct HgMetadata {
171 171 pub st_dev: u64,
172 172 pub st_mode: u32,
173 173 pub st_nlink: u64,
174 174 pub st_size: u64,
175 175 pub st_mtime: i64,
176 176 pub st_ctime: i64,
177 177 }
178 178
179 179 // TODO support other plaforms
180 180 #[cfg(unix)]
181 181 impl HgMetadata {
182 182 pub fn from_metadata(metadata: Metadata) -> Self {
183 183 use std::os::unix::fs::MetadataExt;
184 184 Self {
185 185 st_dev: metadata.dev(),
186 186 st_mode: metadata.mode(),
187 187 st_nlink: metadata.nlink(),
188 188 st_size: metadata.size(),
189 189 st_mtime: metadata.mtime(),
190 190 st_ctime: metadata.ctime(),
191 191 }
192 192 }
193 193 }
194 194
195 195 /// Returns the canonical path of `name`, given `cwd` and `root`
196 196 pub fn canonical_path(
197 197 root: impl AsRef<Path>,
198 198 cwd: impl AsRef<Path>,
199 199 name: impl AsRef<Path>,
200 200 ) -> Result<PathBuf, HgPathError> {
201 201 // TODO add missing normalization for other platforms
202 202 let root = root.as_ref();
203 203 let cwd = cwd.as_ref();
204 204 let name = name.as_ref();
205 205
206 206 let name = if !name.is_absolute() {
207 207 root.join(&cwd).join(&name)
208 208 } else {
209 209 name.to_owned()
210 210 };
211 211 let auditor = PathAuditor::new(&root);
212 212 if name != root && name.starts_with(&root) {
213 213 let name = name.strip_prefix(&root).unwrap();
214 214 auditor.audit_path(path_to_hg_path_buf(name)?)?;
215 215 Ok(name.to_owned())
216 216 } else if name == root {
217 217 Ok("".into())
218 218 } else {
219 219 // Determine whether `name' is in the hierarchy at or beneath `root',
220 220 // by iterating name=name.parent() until it returns `None` (can't
221 221 // check name == '/', because that doesn't work on windows).
222 222 let mut name = name.deref();
223 223 let original_name = name.to_owned();
224 224 loop {
225 225 let same = is_same_file(&name, &root).unwrap_or(false);
226 226 if same {
227 227 if name == original_name {
228 228 // `name` was actually the same as root (maybe a symlink)
229 229 return Ok("".into());
230 230 }
231 231 // `name` is a symlink to root, so `original_name` is under
232 232 // root
233 233 let rel_path = original_name.strip_prefix(&name).unwrap();
234 234 auditor.audit_path(path_to_hg_path_buf(&rel_path)?)?;
235 235 return Ok(rel_path.to_owned());
236 236 }
237 237 name = match name.parent() {
238 238 None => break,
239 239 Some(p) => p,
240 240 };
241 241 }
242 242 // TODO hint to the user about using --cwd
243 243 // Bubble up the responsibility to Python for now
244 244 Err(HgPathError::NotUnderRoot {
245 245 path: original_name.to_owned(),
246 246 root: root.to_owned(),
247 247 })
248 248 }
249 249 }
250 250
251 /// Returns the representation of the path relative to the current working
252 /// directory for display purposes.
253 ///
254 /// `cwd` is a `HgPath`, so it is considered relative to the root directory
255 /// of the repository.
256 ///
257 /// # Examples
258 ///
259 /// ```
260 /// use hg::utils::hg_path::HgPath;
261 /// use hg::utils::files::relativize_path;
262 /// use std::borrow::Cow;
263 ///
264 /// let file = HgPath::new(b"nested/file");
265 /// let cwd = HgPath::new(b"");
266 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"nested/file"));
267 ///
268 /// let cwd = HgPath::new(b"nested");
269 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"file"));
270 ///
271 /// let cwd = HgPath::new(b"other");
272 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"../nested/file"));
273 /// ```
274 pub fn relativize_path(path: &HgPath, cwd: impl AsRef<HgPath>) -> Cow<[u8]> {
275 if cwd.as_ref().is_empty() {
276 Cow::Borrowed(path.as_bytes())
277 } else {
278 let mut res: Vec<u8> = Vec::new();
279 let mut path_iter = path.as_bytes().split(|b| *b == b'/').peekable();
280 let mut cwd_iter =
281 cwd.as_ref().as_bytes().split(|b| *b == b'/').peekable();
282 loop {
283 match (path_iter.peek(), cwd_iter.peek()) {
284 (Some(a), Some(b)) if a == b => (),
285 _ => break,
286 }
287 path_iter.next();
288 cwd_iter.next();
289 }
290 let mut need_sep = false;
291 for _ in cwd_iter {
292 if need_sep {
293 res.extend(b"/")
294 } else {
295 need_sep = true
296 };
297 res.extend(b"..");
298 }
299 for c in path_iter {
300 if need_sep {
301 res.extend(b"/")
302 } else {
303 need_sep = true
304 };
305 res.extend(c);
306 }
307 Cow::Owned(res)
308 }
309 }
310
251 311 #[cfg(test)]
252 312 mod tests {
253 313 use super::*;
254 314 use pretty_assertions::assert_eq;
255 315
256 316 #[test]
257 317 fn find_dirs_some() {
258 318 let mut dirs = super::find_dirs(HgPath::new(b"foo/bar/baz"));
259 319 assert_eq!(dirs.next(), Some(HgPath::new(b"foo/bar")));
260 320 assert_eq!(dirs.next(), Some(HgPath::new(b"foo")));
261 321 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
262 322 assert_eq!(dirs.next(), None);
263 323 assert_eq!(dirs.next(), None);
264 324 }
265 325
266 326 #[test]
267 327 fn find_dirs_empty() {
268 328 // looks weird, but mercurial.pathutil.finddirs(b"") yields b""
269 329 let mut dirs = super::find_dirs(HgPath::new(b""));
270 330 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
271 331 assert_eq!(dirs.next(), None);
272 332 assert_eq!(dirs.next(), None);
273 333 }
274 334
275 335 #[test]
276 336 fn test_find_dirs_with_base_some() {
277 337 let mut dirs = super::find_dirs_with_base(HgPath::new(b"foo/bar/baz"));
278 338 assert_eq!(
279 339 dirs.next(),
280 340 Some((HgPath::new(b"foo/bar"), HgPath::new(b"baz")))
281 341 );
282 342 assert_eq!(
283 343 dirs.next(),
284 344 Some((HgPath::new(b"foo"), HgPath::new(b"bar")))
285 345 );
286 346 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b"foo"))));
287 347 assert_eq!(dirs.next(), None);
288 348 assert_eq!(dirs.next(), None);
289 349 }
290 350
291 351 #[test]
292 352 fn test_find_dirs_with_base_empty() {
293 353 let mut dirs = super::find_dirs_with_base(HgPath::new(b""));
294 354 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b""))));
295 355 assert_eq!(dirs.next(), None);
296 356 assert_eq!(dirs.next(), None);
297 357 }
298 358
299 359 #[test]
300 360 fn test_canonical_path() {
301 361 let root = Path::new("/repo");
302 362 let cwd = Path::new("/dir");
303 363 let name = Path::new("filename");
304 364 assert_eq!(
305 365 canonical_path(root, cwd, name),
306 366 Err(HgPathError::NotUnderRoot {
307 367 path: PathBuf::from("/dir/filename"),
308 368 root: root.to_path_buf()
309 369 })
310 370 );
311 371
312 372 let root = Path::new("/repo");
313 373 let cwd = Path::new("/");
314 374 let name = Path::new("filename");
315 375 assert_eq!(
316 376 canonical_path(root, cwd, name),
317 377 Err(HgPathError::NotUnderRoot {
318 378 path: PathBuf::from("/filename"),
319 379 root: root.to_path_buf()
320 380 })
321 381 );
322 382
323 383 let root = Path::new("/repo");
324 384 let cwd = Path::new("/");
325 385 let name = Path::new("repo/filename");
326 386 assert_eq!(
327 387 canonical_path(root, cwd, name),
328 388 Ok(PathBuf::from("filename"))
329 389 );
330 390
331 391 let root = Path::new("/repo");
332 392 let cwd = Path::new("/repo");
333 393 let name = Path::new("filename");
334 394 assert_eq!(
335 395 canonical_path(root, cwd, name),
336 396 Ok(PathBuf::from("filename"))
337 397 );
338 398
339 399 let root = Path::new("/repo");
340 400 let cwd = Path::new("/repo/subdir");
341 401 let name = Path::new("filename");
342 402 assert_eq!(
343 403 canonical_path(root, cwd, name),
344 404 Ok(PathBuf::from("subdir/filename"))
345 405 );
346 406 }
347 407
348 408 #[test]
349 409 fn test_canonical_path_not_rooted() {
350 410 use std::fs::create_dir;
351 411 use tempfile::tempdir;
352 412
353 413 let base_dir = tempdir().unwrap();
354 414 let base_dir_path = base_dir.path();
355 415 let beneath_repo = base_dir_path.join("a");
356 416 let root = base_dir_path.join("a/b");
357 417 let out_of_repo = base_dir_path.join("c");
358 418 let under_repo_symlink = out_of_repo.join("d");
359 419
360 420 create_dir(&beneath_repo).unwrap();
361 421 create_dir(&root).unwrap();
362 422
363 423 // TODO make portable
364 424 std::os::unix::fs::symlink(&root, &out_of_repo).unwrap();
365 425
366 426 assert_eq!(
367 427 canonical_path(&root, Path::new(""), out_of_repo),
368 428 Ok(PathBuf::from(""))
369 429 );
370 430 assert_eq!(
371 431 canonical_path(&root, Path::new(""), &beneath_repo),
372 432 Err(HgPathError::NotUnderRoot {
373 433 path: beneath_repo.to_owned(),
374 434 root: root.to_owned()
375 435 })
376 436 );
377 437 assert_eq!(
378 438 canonical_path(&root, Path::new(""), &under_repo_symlink),
379 439 Ok(PathBuf::from("d"))
380 440 );
381 441 }
382 442 }
@@ -1,50 +1,60 b''
1 1 use crate::commands::Command;
2 2 use crate::error::{CommandError, CommandErrorKind};
3 3 use crate::ui::Ui;
4 4 use hg::operations::{ListTrackedFiles, ListTrackedFilesErrorKind};
5 use hg::utils::files::{get_bytes_from_path, relativize_path};
6 use hg::utils::hg_path::HgPathBuf;
5 7
6 8 pub const HELP_TEXT: &str = "
7 9 List tracked files.
8 10
9 11 Returns 0 on success.
10 12 ";
11 13
12 14 pub struct FilesCommand<'a> {
13 15 ui: &'a Ui,
14 16 }
15 17
16 18 impl<'a> FilesCommand<'a> {
17 19 pub fn new(ui: &'a Ui) -> Self {
18 20 FilesCommand { ui }
19 21 }
20 22 }
21 23
22 24 impl<'a> Command<'a> for FilesCommand<'a> {
23 25 fn run(&self) -> Result<(), CommandError> {
24 26 let operation_builder = ListTrackedFiles::new()?;
25 27 let operation = operation_builder.load().map_err(|err| {
26 28 CommandErrorKind::Abort(Some(
27 29 [b"abort: ", err.to_string().as_bytes(), b"\n"]
28 30 .concat()
29 31 .to_vec(),
30 32 ))
31 33 })?;
32 34 let files = operation.run().map_err(|err| match err.kind {
33 35 ListTrackedFilesErrorKind::ParseError(_) => {
34 36 CommandErrorKind::Abort(Some(
35 37 // TODO find a better error message
36 38 b"abort: parse error\n".to_vec(),
37 39 ))
38 40 }
39 41 })?;
40 42
43 let cwd = std::env::current_dir()
44 .or_else(|e| Err(CommandErrorKind::CurrentDirNotFound(e)))?;
45 let rooted_cwd = cwd
46 .strip_prefix(operation_builder.get_root())
47 .expect("cwd was already checked within the repository");
48 let rooted_cwd = HgPathBuf::from(get_bytes_from_path(rooted_cwd));
49
41 50 let mut stdout = self.ui.stdout_buffer();
51
42 52 for file in files {
43 stdout.write_all(file.as_bytes())?;
53 stdout.write_all(relativize_path(file, &rooted_cwd).as_ref())?;
44 54 stdout.write_all(b"\n")?;
45 55 }
46 56 stdout.flush()?;
47 57
48 58 Ok(())
49 59 }
50 60 }
General Comments 0
You need to be logged in to leave comments. Login now