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_ |
|
|
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