##// END OF EJS Templates
rhg: Reuse manifest when checking status of multiple ambiguous files...
Simon Sapin -
r48778:796206e7 default
parent child Browse files
Show More
@@ -1,71 +1,71 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 crate::repo::Repo;
9 9 use crate::revlog::revlog::RevlogError;
10 10 use crate::revlog::Node;
11 11
12 12 use crate::utils::hg_path::HgPathBuf;
13 13
14 14 pub struct CatOutput {
15 15 /// Whether any file in the manifest matched the paths given as CLI
16 16 /// arguments
17 17 pub found_any: bool,
18 18 /// The contents of matching files, in manifest order
19 19 pub concatenated: Vec<u8>,
20 20 /// Which of the CLI arguments did not match any manifest file
21 21 pub missing: Vec<HgPathBuf>,
22 22 /// The node ID that the given revset was resolved to
23 23 pub node: Node,
24 24 }
25 25
26 26 /// Output the given revision of files
27 27 ///
28 28 /// * `root`: Repository root
29 29 /// * `rev`: The revision to cat the files from.
30 30 /// * `files`: The files to output.
31 31 pub fn cat<'a>(
32 32 repo: &Repo,
33 33 revset: &str,
34 34 files: &'a [HgPathBuf],
35 35 ) -> Result<CatOutput, RevlogError> {
36 36 let rev = crate::revset::resolve_single(revset, repo)?;
37 let manifest = repo.manifest(rev)?;
37 let manifest = repo.manifest_for_rev(rev)?;
38 38 let node = *repo
39 39 .changelog()?
40 40 .node_from_rev(rev)
41 41 .expect("should succeed when repo.manifest did");
42 42 let mut bytes = vec![];
43 43 let mut matched = vec![false; files.len()];
44 44 let mut found_any = false;
45 45
46 46 for (manifest_file, node_bytes) in manifest.files_with_nodes() {
47 47 for (cat_file, is_matched) in files.iter().zip(&mut matched) {
48 48 if cat_file.as_bytes() == manifest_file.as_bytes() {
49 49 *is_matched = true;
50 50 found_any = true;
51 51 let file_log = repo.filelog(manifest_file)?;
52 52 let file_node = Node::from_hex_for_repo(node_bytes)?;
53 53 let entry = file_log.get_node(file_node)?;
54 54 bytes.extend(entry.data()?)
55 55 }
56 56 }
57 57 }
58 58
59 59 let missing: Vec<_> = files
60 60 .iter()
61 61 .zip(&matched)
62 62 .filter(|pair| !*pair.1)
63 63 .map(|pair| pair.0.clone())
64 64 .collect();
65 65 Ok(CatOutput {
66 66 found_any,
67 67 concatenated: bytes,
68 68 missing,
69 69 node,
70 70 })
71 71 }
@@ -1,82 +1,82 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 crate::dirstate::parsers::parse_dirstate_entries;
9 9 use crate::dirstate_tree::on_disk::{for_each_tracked_path, read_docket};
10 10 use crate::errors::HgError;
11 11 use crate::repo::Repo;
12 12 use crate::revlog::manifest::Manifest;
13 13 use crate::revlog::revlog::RevlogError;
14 14 use crate::utils::hg_path::HgPath;
15 15 use crate::DirstateError;
16 16 use rayon::prelude::*;
17 17
18 18 /// List files under Mercurial control in the working directory
19 19 /// by reading the dirstate
20 20 pub struct Dirstate {
21 21 /// The `dirstate` content.
22 22 content: Vec<u8>,
23 23 v2_metadata: Option<Vec<u8>>,
24 24 }
25 25
26 26 impl Dirstate {
27 27 pub fn new(repo: &Repo) -> Result<Self, HgError> {
28 28 let mut content = repo.hg_vfs().read("dirstate")?;
29 29 let v2_metadata = if repo.has_dirstate_v2() {
30 30 let docket = read_docket(&content)?;
31 31 let meta = docket.tree_metadata().to_vec();
32 32 content = repo.hg_vfs().read(docket.data_filename())?;
33 33 Some(meta)
34 34 } else {
35 35 None
36 36 };
37 37 Ok(Self {
38 38 content,
39 39 v2_metadata,
40 40 })
41 41 }
42 42
43 43 pub fn tracked_files(&self) -> Result<Vec<&HgPath>, DirstateError> {
44 44 let mut files = Vec::new();
45 45 if !self.content.is_empty() {
46 46 if let Some(meta) = &self.v2_metadata {
47 47 for_each_tracked_path(&self.content, meta, |path| {
48 48 files.push(path)
49 49 })?
50 50 } else {
51 51 let _parents = parse_dirstate_entries(
52 52 &self.content,
53 53 |path, entry, _copy_source| {
54 54 if entry.state.is_tracked() {
55 55 files.push(path)
56 56 }
57 57 Ok(())
58 58 },
59 59 )?;
60 60 }
61 61 }
62 62 files.par_sort_unstable();
63 63 Ok(files)
64 64 }
65 65 }
66 66
67 67 /// List files under Mercurial control at a given revision.
68 68 pub fn list_rev_tracked_files(
69 69 repo: &Repo,
70 70 revset: &str,
71 71 ) -> Result<FilesForRev, RevlogError> {
72 72 let rev = crate::revset::resolve_single(revset, repo)?;
73 Ok(FilesForRev(repo.manifest(rev)?))
73 Ok(FilesForRev(repo.manifest_for_rev(rev)?))
74 74 }
75 75
76 76 pub struct FilesForRev(Manifest);
77 77
78 78 impl FilesForRev {
79 79 pub fn iter(&self) -> impl Iterator<Item = &HgPath> {
80 80 self.0.files()
81 81 }
82 82 }
@@ -1,409 +1,419 b''
1 1 use crate::changelog::Changelog;
2 2 use crate::config::{Config, ConfigError, ConfigParseError};
3 3 use crate::dirstate::DirstateParents;
4 4 use crate::dirstate_tree::dirstate_map::DirstateMap;
5 5 use crate::dirstate_tree::owning::OwningDirstateMap;
6 6 use crate::errors::HgError;
7 7 use crate::errors::HgResultExt;
8 use crate::exit_codes;
8 9 use crate::manifest::{Manifest, Manifestlog};
9 use crate::requirements;
10 10 use crate::revlog::filelog::Filelog;
11 11 use crate::revlog::revlog::RevlogError;
12 12 use crate::utils::files::get_path_from_bytes;
13 13 use crate::utils::hg_path::HgPath;
14 14 use crate::utils::SliceExt;
15 15 use crate::vfs::{is_dir, is_file, Vfs};
16 use crate::{exit_codes, Node};
16 use crate::{requirements, NodePrefix};
17 17 use crate::{DirstateError, Revision};
18 18 use std::cell::{Cell, Ref, RefCell, RefMut};
19 19 use std::collections::HashSet;
20 20 use std::path::{Path, PathBuf};
21 21
22 22 /// A repository on disk
23 23 pub struct Repo {
24 24 working_directory: PathBuf,
25 25 dot_hg: PathBuf,
26 26 store: PathBuf,
27 27 requirements: HashSet<String>,
28 28 config: Config,
29 29 // None means not known/initialized yet
30 30 dirstate_parents: Cell<Option<DirstateParents>>,
31 31 dirstate_map: LazyCell<OwningDirstateMap, DirstateError>,
32 32 changelog: LazyCell<Changelog, HgError>,
33 33 manifestlog: LazyCell<Manifestlog, HgError>,
34 34 }
35 35
36 36 #[derive(Debug, derive_more::From)]
37 37 pub enum RepoError {
38 38 NotFound {
39 39 at: PathBuf,
40 40 },
41 41 #[from]
42 42 ConfigParseError(ConfigParseError),
43 43 #[from]
44 44 Other(HgError),
45 45 }
46 46
47 47 impl From<ConfigError> for RepoError {
48 48 fn from(error: ConfigError) -> Self {
49 49 match error {
50 50 ConfigError::Parse(error) => error.into(),
51 51 ConfigError::Other(error) => error.into(),
52 52 }
53 53 }
54 54 }
55 55
56 56 impl Repo {
57 57 /// tries to find nearest repository root in current working directory or
58 58 /// its ancestors
59 59 pub fn find_repo_root() -> Result<PathBuf, RepoError> {
60 60 let current_directory = crate::utils::current_dir()?;
61 61 // ancestors() is inclusive: it first yields `current_directory`
62 62 // as-is.
63 63 for ancestor in current_directory.ancestors() {
64 64 if is_dir(ancestor.join(".hg"))? {
65 65 return Ok(ancestor.to_path_buf());
66 66 }
67 67 }
68 68 return Err(RepoError::NotFound {
69 69 at: current_directory,
70 70 });
71 71 }
72 72
73 73 /// Find a repository, either at the given path (which must contain a `.hg`
74 74 /// sub-directory) or by searching the current directory and its
75 75 /// ancestors.
76 76 ///
77 77 /// A method with two very different "modes" like this usually a code smell
78 78 /// to make two methods instead, but in this case an `Option` is what rhg
79 79 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
80 80 /// Having two methods would just move that `if` to almost all callers.
81 81 pub fn find(
82 82 config: &Config,
83 83 explicit_path: Option<PathBuf>,
84 84 ) -> Result<Self, RepoError> {
85 85 if let Some(root) = explicit_path {
86 86 if is_dir(root.join(".hg"))? {
87 87 Self::new_at_path(root.to_owned(), config)
88 88 } else if is_file(&root)? {
89 89 Err(HgError::unsupported("bundle repository").into())
90 90 } else {
91 91 Err(RepoError::NotFound {
92 92 at: root.to_owned(),
93 93 })
94 94 }
95 95 } else {
96 96 let root = Self::find_repo_root()?;
97 97 Self::new_at_path(root, config)
98 98 }
99 99 }
100 100
101 101 /// To be called after checking that `.hg` is a sub-directory
102 102 fn new_at_path(
103 103 working_directory: PathBuf,
104 104 config: &Config,
105 105 ) -> Result<Self, RepoError> {
106 106 let dot_hg = working_directory.join(".hg");
107 107
108 108 let mut repo_config_files = Vec::new();
109 109 repo_config_files.push(dot_hg.join("hgrc"));
110 110 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
111 111
112 112 let hg_vfs = Vfs { base: &dot_hg };
113 113 let mut reqs = requirements::load_if_exists(hg_vfs)?;
114 114 let relative =
115 115 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
116 116 let shared =
117 117 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
118 118
119 119 // From `mercurial/localrepo.py`:
120 120 //
121 121 // if .hg/requires contains the sharesafe requirement, it means
122 122 // there exists a `.hg/store/requires` too and we should read it
123 123 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
124 124 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
125 125 // is not present, refer checkrequirementscompat() for that
126 126 //
127 127 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
128 128 // repository was shared the old way. We check the share source
129 129 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
130 130 // current repository needs to be reshared
131 131 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
132 132
133 133 let store_path;
134 134 if !shared {
135 135 store_path = dot_hg.join("store");
136 136 } else {
137 137 let bytes = hg_vfs.read("sharedpath")?;
138 138 let mut shared_path =
139 139 get_path_from_bytes(bytes.trim_end_matches(|b| b == b'\n'))
140 140 .to_owned();
141 141 if relative {
142 142 shared_path = dot_hg.join(shared_path)
143 143 }
144 144 if !is_dir(&shared_path)? {
145 145 return Err(HgError::corrupted(format!(
146 146 ".hg/sharedpath points to nonexistent directory {}",
147 147 shared_path.display()
148 148 ))
149 149 .into());
150 150 }
151 151
152 152 store_path = shared_path.join("store");
153 153
154 154 let source_is_share_safe =
155 155 requirements::load(Vfs { base: &shared_path })?
156 156 .contains(requirements::SHARESAFE_REQUIREMENT);
157 157
158 158 if share_safe && !source_is_share_safe {
159 159 return Err(match config
160 160 .get(b"share", b"safe-mismatch.source-not-safe")
161 161 {
162 162 Some(b"abort") | None => HgError::abort(
163 163 "abort: share source does not support share-safe requirement\n\
164 164 (see `hg help config.format.use-share-safe` for more information)",
165 165 exit_codes::ABORT,
166 166 ),
167 167 _ => HgError::unsupported("share-safe downgrade"),
168 168 }
169 169 .into());
170 170 } else if source_is_share_safe && !share_safe {
171 171 return Err(
172 172 match config.get(b"share", b"safe-mismatch.source-safe") {
173 173 Some(b"abort") | None => HgError::abort(
174 174 "abort: version mismatch: source uses share-safe \
175 175 functionality while the current share does not\n\
176 176 (see `hg help config.format.use-share-safe` for more information)",
177 177 exit_codes::ABORT,
178 178 ),
179 179 _ => HgError::unsupported("share-safe upgrade"),
180 180 }
181 181 .into(),
182 182 );
183 183 }
184 184
185 185 if share_safe {
186 186 repo_config_files.insert(0, shared_path.join("hgrc"))
187 187 }
188 188 }
189 189 if share_safe {
190 190 reqs.extend(requirements::load(Vfs { base: &store_path })?);
191 191 }
192 192
193 193 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
194 194 config.combine_with_repo(&repo_config_files)?
195 195 } else {
196 196 config.clone()
197 197 };
198 198
199 199 let repo = Self {
200 200 requirements: reqs,
201 201 working_directory,
202 202 store: store_path,
203 203 dot_hg,
204 204 config: repo_config,
205 205 dirstate_parents: Cell::new(None),
206 206 dirstate_map: LazyCell::new(Self::new_dirstate_map),
207 207 changelog: LazyCell::new(Changelog::open),
208 208 manifestlog: LazyCell::new(Manifestlog::open),
209 209 };
210 210
211 211 requirements::check(&repo)?;
212 212
213 213 Ok(repo)
214 214 }
215 215
216 216 pub fn working_directory_path(&self) -> &Path {
217 217 &self.working_directory
218 218 }
219 219
220 220 pub fn requirements(&self) -> &HashSet<String> {
221 221 &self.requirements
222 222 }
223 223
224 224 pub fn config(&self) -> &Config {
225 225 &self.config
226 226 }
227 227
228 228 /// For accessing repository files (in `.hg`), except for the store
229 229 /// (`.hg/store`).
230 230 pub fn hg_vfs(&self) -> Vfs<'_> {
231 231 Vfs { base: &self.dot_hg }
232 232 }
233 233
234 234 /// For accessing repository store files (in `.hg/store`)
235 235 pub fn store_vfs(&self) -> Vfs<'_> {
236 236 Vfs { base: &self.store }
237 237 }
238 238
239 239 /// For accessing the working copy
240 240 pub fn working_directory_vfs(&self) -> Vfs<'_> {
241 241 Vfs {
242 242 base: &self.working_directory,
243 243 }
244 244 }
245 245
246 246 pub fn has_dirstate_v2(&self) -> bool {
247 247 self.requirements
248 248 .contains(requirements::DIRSTATE_V2_REQUIREMENT)
249 249 }
250 250
251 251 fn dirstate_file_contents(&self) -> Result<Vec<u8>, HgError> {
252 252 Ok(self
253 253 .hg_vfs()
254 254 .read("dirstate")
255 255 .io_not_found_as_none()?
256 256 .unwrap_or(Vec::new()))
257 257 }
258 258
259 259 pub fn dirstate_parents(&self) -> Result<DirstateParents, HgError> {
260 260 if let Some(parents) = self.dirstate_parents.get() {
261 261 return Ok(parents);
262 262 }
263 263 let dirstate = self.dirstate_file_contents()?;
264 264 let parents = if dirstate.is_empty() {
265 265 DirstateParents::NULL
266 266 } else if self.has_dirstate_v2() {
267 267 crate::dirstate_tree::on_disk::read_docket(&dirstate)?.parents()
268 268 } else {
269 269 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?
270 270 .clone()
271 271 };
272 272 self.dirstate_parents.set(Some(parents));
273 273 Ok(parents)
274 274 }
275 275
276 276 fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> {
277 277 let dirstate_file_contents = self.dirstate_file_contents()?;
278 278 if dirstate_file_contents.is_empty() {
279 279 self.dirstate_parents.set(Some(DirstateParents::NULL));
280 280 Ok(OwningDirstateMap::new_empty(Vec::new()))
281 281 } else if self.has_dirstate_v2() {
282 282 let docket = crate::dirstate_tree::on_disk::read_docket(
283 283 &dirstate_file_contents,
284 284 )?;
285 285 self.dirstate_parents.set(Some(docket.parents()));
286 286 let data_size = docket.data_size();
287 287 let metadata = docket.tree_metadata();
288 288 let mut map = if let Some(data_mmap) = self
289 289 .hg_vfs()
290 290 .mmap_open(docket.data_filename())
291 291 .io_not_found_as_none()?
292 292 {
293 293 OwningDirstateMap::new_empty(MmapWrapper(data_mmap))
294 294 } else {
295 295 OwningDirstateMap::new_empty(Vec::new())
296 296 };
297 297 let (on_disk, placeholder) = map.get_mut_pair();
298 298 *placeholder = DirstateMap::new_v2(on_disk, data_size, metadata)?;
299 299 Ok(map)
300 300 } else {
301 301 let mut map = OwningDirstateMap::new_empty(dirstate_file_contents);
302 302 let (on_disk, placeholder) = map.get_mut_pair();
303 303 let (inner, parents) = DirstateMap::new_v1(on_disk)?;
304 304 self.dirstate_parents
305 305 .set(Some(parents.unwrap_or(DirstateParents::NULL)));
306 306 *placeholder = inner;
307 307 Ok(map)
308 308 }
309 309 }
310 310
311 311 pub fn dirstate_map(
312 312 &self,
313 313 ) -> Result<Ref<OwningDirstateMap>, DirstateError> {
314 314 self.dirstate_map.get_or_init(self)
315 315 }
316 316
317 317 pub fn dirstate_map_mut(
318 318 &self,
319 319 ) -> Result<RefMut<OwningDirstateMap>, DirstateError> {
320 320 self.dirstate_map.get_mut_or_init(self)
321 321 }
322 322
323 323 pub fn changelog(&self) -> Result<Ref<Changelog>, HgError> {
324 324 self.changelog.get_or_init(self)
325 325 }
326 326
327 327 pub fn changelog_mut(&self) -> Result<RefMut<Changelog>, HgError> {
328 328 self.changelog.get_mut_or_init(self)
329 329 }
330 330
331 331 pub fn manifestlog(&self) -> Result<Ref<Manifestlog>, HgError> {
332 332 self.manifestlog.get_or_init(self)
333 333 }
334 334
335 335 pub fn manifestlog_mut(&self) -> Result<RefMut<Manifestlog>, HgError> {
336 336 self.manifestlog.get_mut_or_init(self)
337 337 }
338 338
339 /// Returns the manifest of the given node ID
340 pub fn manifest_for_node(
341 &self,
342 node: impl Into<NodePrefix>,
343 ) -> Result<Manifest, RevlogError> {
344 self.manifestlog()?.get_node(
345 self.changelog()?
346 .get_node(node.into())?
347 .manifest_node()?
348 .into(),
349 )
350 }
351
339 352 /// Returns the manifest of the given revision
340 pub fn manifest(
353 pub fn manifest_for_rev(
341 354 &self,
342 355 revision: Revision,
343 356 ) -> Result<Manifest, RevlogError> {
344 let changelog = self.changelog()?;
345 let manifest = self.manifestlog()?;
346 let changelog_entry = changelog.get_rev(revision)?;
347 let manifest_node =
348 Node::from_hex_for_repo(&changelog_entry.manifest_node()?)?;
349 manifest.get_node(manifest_node.into())
357 self.manifestlog()?.get_node(
358 self.changelog()?.get_rev(revision)?.manifest_node()?.into(),
359 )
350 360 }
351 361
352 362 pub fn filelog(&self, path: &HgPath) -> Result<Filelog, HgError> {
353 363 Filelog::open(self, path)
354 364 }
355 365 }
356 366
357 367 /// Lazily-initialized component of `Repo` with interior mutability
358 368 ///
359 369 /// This differs from `OnceCell` in that the value can still be "deinitialized"
360 370 /// later by setting its inner `Option` to `None`.
361 371 struct LazyCell<T, E> {
362 372 value: RefCell<Option<T>>,
363 373 // `Fn`s that don’t capture environment are zero-size, so this box does
364 374 // not allocate:
365 375 init: Box<dyn Fn(&Repo) -> Result<T, E>>,
366 376 }
367 377
368 378 impl<T, E> LazyCell<T, E> {
369 379 fn new(init: impl Fn(&Repo) -> Result<T, E> + 'static) -> Self {
370 380 Self {
371 381 value: RefCell::new(None),
372 382 init: Box::new(init),
373 383 }
374 384 }
375 385
376 386 fn get_or_init(&self, repo: &Repo) -> Result<Ref<T>, E> {
377 387 let mut borrowed = self.value.borrow();
378 388 if borrowed.is_none() {
379 389 drop(borrowed);
380 390 // Only use `borrow_mut` if it is really needed to avoid panic in
381 391 // case there is another outstanding borrow but mutation is not
382 392 // needed.
383 393 *self.value.borrow_mut() = Some((self.init)(repo)?);
384 394 borrowed = self.value.borrow()
385 395 }
386 396 Ok(Ref::map(borrowed, |option| option.as_ref().unwrap()))
387 397 }
388 398
389 399 pub fn get_mut_or_init(&self, repo: &Repo) -> Result<RefMut<T>, E> {
390 400 let mut borrowed = self.value.borrow_mut();
391 401 if borrowed.is_none() {
392 402 *borrowed = Some((self.init)(repo)?);
393 403 }
394 404 Ok(RefMut::map(borrowed, |option| option.as_mut().unwrap()))
395 405 }
396 406 }
397 407
398 408 // TODO: remove this when https://github.com/RazrFalcon/memmap2-rs/pull/22 is on crates.io
399 409 struct MmapWrapper(memmap2::Mmap);
400 410
401 411 impl std::ops::Deref for MmapWrapper {
402 412 type Target = [u8];
403 413
404 414 fn deref(&self) -> &[u8] {
405 415 self.0.deref()
406 416 }
407 417 }
408 418
409 419 unsafe impl stable_deref_trait::StableDeref for MmapWrapper {}
@@ -1,65 +1,67 b''
1 1 use crate::errors::HgError;
2 2 use crate::repo::Repo;
3 3 use crate::revlog::revlog::{Revlog, RevlogError};
4 4 use crate::revlog::Revision;
5 5 use crate::revlog::{Node, NodePrefix};
6 6
7 7 /// A specialized `Revlog` to work with `changelog` data format.
8 8 pub struct Changelog {
9 9 /// The generic `revlog` format.
10 10 pub(crate) revlog: Revlog,
11 11 }
12 12
13 13 impl Changelog {
14 14 /// Open the `changelog` of a repository given by its root.
15 15 pub fn open(repo: &Repo) -> Result<Self, HgError> {
16 16 let revlog = Revlog::open(repo, "00changelog.i", None)?;
17 17 Ok(Self { revlog })
18 18 }
19 19
20 20 /// Return the `ChangelogEntry` a given node id.
21 21 pub fn get_node(
22 22 &self,
23 23 node: NodePrefix,
24 24 ) -> Result<ChangelogEntry, RevlogError> {
25 25 let rev = self.revlog.get_node_rev(node)?;
26 26 self.get_rev(rev)
27 27 }
28 28
29 29 /// Return the `ChangelogEntry` of a given node revision.
30 30 pub fn get_rev(
31 31 &self,
32 32 rev: Revision,
33 33 ) -> Result<ChangelogEntry, RevlogError> {
34 34 let bytes = self.revlog.get_rev_data(rev)?;
35 35 Ok(ChangelogEntry { bytes })
36 36 }
37 37
38 38 pub fn node_from_rev(&self, rev: Revision) -> Option<&Node> {
39 39 Some(self.revlog.index.get_entry(rev)?.hash())
40 40 }
41 41 }
42 42
43 43 /// `Changelog` entry which knows how to interpret the `changelog` data bytes.
44 44 #[derive(Debug)]
45 45 pub struct ChangelogEntry {
46 46 /// The data bytes of the `changelog` entry.
47 47 bytes: Vec<u8>,
48 48 }
49 49
50 50 impl ChangelogEntry {
51 51 /// Return an iterator over the lines of the entry.
52 52 pub fn lines(&self) -> impl Iterator<Item = &[u8]> {
53 53 self.bytes
54 54 .split(|b| b == &b'\n')
55 55 .filter(|line| !line.is_empty())
56 56 }
57 57
58 58 /// Return the node id of the `manifest` referenced by this `changelog`
59 59 /// entry.
60 pub fn manifest_node(&self) -> Result<&[u8], RevlogError> {
60 pub fn manifest_node(&self) -> Result<Node, HgError> {
61 Node::from_hex_for_repo(
61 62 self.lines()
62 63 .next()
63 .ok_or_else(|| HgError::corrupted("empty changelog entry").into())
64 .ok_or_else(|| HgError::corrupted("empty changelog entry"))?,
65 )
64 66 }
65 67 }
@@ -1,71 +1,84 b''
1 1 use crate::errors::HgError;
2 2 use crate::repo::Repo;
3 3 use crate::revlog::revlog::{Revlog, RevlogError};
4 use crate::revlog::NodePrefix;
5 4 use crate::revlog::Revision;
5 use crate::revlog::{Node, NodePrefix};
6 6 use crate::utils::hg_path::HgPath;
7 7
8 8 /// A specialized `Revlog` to work with `manifest` data format.
9 9 pub struct Manifestlog {
10 10 /// The generic `revlog` format.
11 11 revlog: Revlog,
12 12 }
13 13
14 14 impl Manifestlog {
15 15 /// Open the `manifest` of a repository given by its root.
16 16 pub fn open(repo: &Repo) -> Result<Self, HgError> {
17 17 let revlog = Revlog::open(repo, "00manifest.i", None)?;
18 18 Ok(Self { revlog })
19 19 }
20 20
21 21 /// Return the `ManifestEntry` of a given node id.
22 22 pub fn get_node(&self, node: NodePrefix) -> Result<Manifest, RevlogError> {
23 23 let rev = self.revlog.get_node_rev(node)?;
24 24 self.get_rev(rev)
25 25 }
26 26
27 27 /// Return the `ManifestEntry` of a given node revision.
28 28 pub fn get_rev(&self, rev: Revision) -> Result<Manifest, RevlogError> {
29 29 let bytes = self.revlog.get_rev_data(rev)?;
30 30 Ok(Manifest { bytes })
31 31 }
32 32 }
33 33
34 34 /// `Manifestlog` entry which knows how to interpret the `manifest` data bytes.
35 35 #[derive(Debug)]
36 36 pub struct Manifest {
37 37 bytes: Vec<u8>,
38 38 }
39 39
40 40 impl Manifest {
41 41 /// Return an iterator over the lines of the entry.
42 42 pub fn lines(&self) -> impl Iterator<Item = &[u8]> {
43 43 self.bytes
44 44 .split(|b| b == &b'\n')
45 45 .filter(|line| !line.is_empty())
46 46 }
47 47
48 48 /// Return an iterator over the files of the entry.
49 49 pub fn files(&self) -> impl Iterator<Item = &HgPath> {
50 50 self.lines().filter(|line| !line.is_empty()).map(|line| {
51 51 let pos = line
52 52 .iter()
53 53 .position(|x| x == &b'\0')
54 54 .expect("manifest line should contain \\0");
55 55 HgPath::new(&line[..pos])
56 56 })
57 57 }
58 58
59 59 /// Return an iterator over the files of the entry.
60 60 pub fn files_with_nodes(&self) -> impl Iterator<Item = (&HgPath, &[u8])> {
61 61 self.lines().filter(|line| !line.is_empty()).map(|line| {
62 62 let pos = line
63 63 .iter()
64 64 .position(|x| x == &b'\0')
65 65 .expect("manifest line should contain \\0");
66 66 let hash_start = pos + 1;
67 67 let hash_end = hash_start + 40;
68 68 (HgPath::new(&line[..pos]), &line[hash_start..hash_end])
69 69 })
70 70 }
71
72 /// If the given path is in this manifest, return its filelog node ID
73 pub fn find_file(&self, path: &HgPath) -> Result<Option<Node>, HgError> {
74 // TODO: use binary search instead of linear scan. This may involve
75 // building (and caching) an index of the byte indicex of each manifest
76 // line.
77 for (manifest_path, node) in self.files_with_nodes() {
78 if manifest_path == path {
79 return Ok(Some(Node::from_hex_for_repo(node)?));
71 80 }
81 }
82 Ok(None)
83 }
84 }
@@ -1,305 +1,304 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 12 use hg::dirstate_tree::dispatch::DirstateMapMethods;
13 use hg::errors::IoResultExt;
13 use hg::errors::{HgError, IoResultExt};
14 use hg::manifest::Manifest;
14 15 use hg::matchers::AlwaysMatcher;
15 use hg::operations::cat;
16 16 use hg::repo::Repo;
17 use hg::revlog::node::Node;
18 17 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
19 use hg::StatusError;
20 18 use hg::{HgPathCow, StatusOptions};
21 19 use log::{info, warn};
22 20 use std::convert::TryInto;
23 21 use std::fs;
24 22 use std::io::BufReader;
25 23 use std::io::Read;
26 24
27 25 pub const HELP_TEXT: &str = "
28 26 Show changed files in the working directory
29 27
30 28 This is a pure Rust version of `hg status`.
31 29
32 30 Some options might be missing, check the list below.
33 31 ";
34 32
35 33 pub fn args() -> clap::App<'static, 'static> {
36 34 SubCommand::with_name("status")
37 35 .alias("st")
38 36 .about(HELP_TEXT)
39 37 .arg(
40 38 Arg::with_name("all")
41 39 .help("show status of all files")
42 40 .short("-A")
43 41 .long("--all"),
44 42 )
45 43 .arg(
46 44 Arg::with_name("modified")
47 45 .help("show only modified files")
48 46 .short("-m")
49 47 .long("--modified"),
50 48 )
51 49 .arg(
52 50 Arg::with_name("added")
53 51 .help("show only added files")
54 52 .short("-a")
55 53 .long("--added"),
56 54 )
57 55 .arg(
58 56 Arg::with_name("removed")
59 57 .help("show only removed files")
60 58 .short("-r")
61 59 .long("--removed"),
62 60 )
63 61 .arg(
64 62 Arg::with_name("clean")
65 63 .help("show only clean files")
66 64 .short("-c")
67 65 .long("--clean"),
68 66 )
69 67 .arg(
70 68 Arg::with_name("deleted")
71 69 .help("show only deleted files")
72 70 .short("-d")
73 71 .long("--deleted"),
74 72 )
75 73 .arg(
76 74 Arg::with_name("unknown")
77 75 .help("show only unknown (not tracked) files")
78 76 .short("-u")
79 77 .long("--unknown"),
80 78 )
81 79 .arg(
82 80 Arg::with_name("ignored")
83 81 .help("show only ignored files")
84 82 .short("-i")
85 83 .long("--ignored"),
86 84 )
87 85 }
88 86
89 87 /// Pure data type allowing the caller to specify file states to display
90 88 #[derive(Copy, Clone, Debug)]
91 89 pub struct DisplayStates {
92 90 pub modified: bool,
93 91 pub added: bool,
94 92 pub removed: bool,
95 93 pub clean: bool,
96 94 pub deleted: bool,
97 95 pub unknown: bool,
98 96 pub ignored: bool,
99 97 }
100 98
101 99 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
102 100 modified: true,
103 101 added: true,
104 102 removed: true,
105 103 clean: false,
106 104 deleted: true,
107 105 unknown: true,
108 106 ignored: false,
109 107 };
110 108
111 109 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
112 110 modified: true,
113 111 added: true,
114 112 removed: true,
115 113 clean: true,
116 114 deleted: true,
117 115 unknown: true,
118 116 ignored: true,
119 117 };
120 118
121 119 impl DisplayStates {
122 120 pub fn is_empty(&self) -> bool {
123 121 !(self.modified
124 122 || self.added
125 123 || self.removed
126 124 || self.clean
127 125 || self.deleted
128 126 || self.unknown
129 127 || self.ignored)
130 128 }
131 129 }
132 130
133 131 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
134 132 let status_enabled_default = false;
135 133 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
136 134 if !status_enabled.unwrap_or(status_enabled_default) {
137 135 return Err(CommandError::unsupported(
138 136 "status is experimental in rhg (enable it with 'rhg.status = true' \
139 137 or enable fallback with 'rhg.on-unsupported = fallback')"
140 138 ));
141 139 }
142 140
143 141 let ui = invocation.ui;
144 142 let args = invocation.subcommand_args;
145 143 let display_states = if args.is_present("all") {
146 144 // TODO when implementing `--quiet`: it excludes clean files
147 145 // from `--all`
148 146 ALL_DISPLAY_STATES
149 147 } else {
150 148 let requested = DisplayStates {
151 149 modified: args.is_present("modified"),
152 150 added: args.is_present("added"),
153 151 removed: args.is_present("removed"),
154 152 clean: args.is_present("clean"),
155 153 deleted: args.is_present("deleted"),
156 154 unknown: args.is_present("unknown"),
157 155 ignored: args.is_present("ignored"),
158 156 };
159 157 if requested.is_empty() {
160 158 DEFAULT_DISPLAY_STATES
161 159 } else {
162 160 requested
163 161 }
164 162 };
165 163
166 164 let repo = invocation.repo?;
167 165 let mut dmap = repo.dirstate_map_mut()?;
168 166
169 167 let options = StatusOptions {
170 168 // TODO should be provided by the dirstate parsing and
171 169 // hence be stored on dmap. Using a value that assumes we aren't
172 170 // below the time resolution granularity of the FS and the
173 171 // dirstate.
174 172 last_normal_time: 0,
175 173 // we're currently supporting file systems with exec flags only
176 174 // anyway
177 175 check_exec: true,
178 176 list_clean: display_states.clean,
179 177 list_unknown: display_states.unknown,
180 178 list_ignored: display_states.ignored,
181 179 collect_traversed_dirs: false,
182 180 };
183 181 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
184 182 let (mut ds_status, pattern_warnings) = dmap.status(
185 183 &AlwaysMatcher,
186 184 repo.working_directory_path().to_owned(),
187 185 vec![ignore_file],
188 186 options,
189 187 )?;
190 188 if !pattern_warnings.is_empty() {
191 189 warn!("Pattern warnings: {:?}", &pattern_warnings);
192 190 }
193 191
194 192 if !ds_status.bad.is_empty() {
195 193 warn!("Bad matches {:?}", &(ds_status.bad))
196 194 }
197 195 if !ds_status.unsure.is_empty() {
198 196 info!(
199 197 "Files to be rechecked by retrieval from filelog: {:?}",
200 198 &ds_status.unsure
201 199 );
202 200 }
203 201 if !ds_status.unsure.is_empty()
204 202 && (display_states.modified || display_states.clean)
205 203 {
206 let p1: Node = repo.dirstate_parents()?.p1.into();
207 let p1_hex = format!("{:x}", p1);
204 let p1 = repo.dirstate_parents()?.p1;
205 let manifest = repo.manifest_for_node(p1).map_err(|e| {
206 CommandError::from((e, &*format!("{:x}", p1.short())))
207 })?;
208 208 for to_check in ds_status.unsure {
209 if cat_file_is_modified(repo, &to_check, &p1_hex)? {
209 if cat_file_is_modified(repo, &manifest, &to_check)? {
210 210 if display_states.modified {
211 211 ds_status.modified.push(to_check);
212 212 }
213 213 } else {
214 214 if display_states.clean {
215 215 ds_status.clean.push(to_check);
216 216 }
217 217 }
218 218 }
219 219 }
220 220 if display_states.modified {
221 221 display_status_paths(ui, &mut ds_status.modified, b"M")?;
222 222 }
223 223 if display_states.added {
224 224 display_status_paths(ui, &mut ds_status.added, b"A")?;
225 225 }
226 226 if display_states.removed {
227 227 display_status_paths(ui, &mut ds_status.removed, b"R")?;
228 228 }
229 229 if display_states.deleted {
230 230 display_status_paths(ui, &mut ds_status.deleted, b"!")?;
231 231 }
232 232 if display_states.unknown {
233 233 display_status_paths(ui, &mut ds_status.unknown, b"?")?;
234 234 }
235 235 if display_states.ignored {
236 236 display_status_paths(ui, &mut ds_status.ignored, b"I")?;
237 237 }
238 238 if display_states.clean {
239 239 display_status_paths(ui, &mut ds_status.clean, b"C")?;
240 240 }
241 241 Ok(())
242 242 }
243 243
244 244 // Probably more elegant to use a Deref or Borrow trait rather than
245 245 // harcode HgPathBuf, but probably not really useful at this point
246 246 fn display_status_paths(
247 247 ui: &Ui,
248 248 paths: &mut [HgPathCow],
249 249 status_prefix: &[u8],
250 250 ) -> Result<(), CommandError> {
251 251 paths.sort_unstable();
252 252 for path in paths {
253 253 // Same TODO as in commands::root
254 254 let bytes: &[u8] = path.as_bytes();
255 255 // TODO optim, probably lots of unneeded copies here, especially
256 256 // if out stream is buffered
257 257 ui.write_stdout(&[status_prefix, b" ", bytes, b"\n"].concat())?;
258 258 }
259 259 Ok(())
260 260 }
261 261
262 262 /// Check if a file is modified by comparing actual repo store and file system.
263 263 ///
264 264 /// This meant to be used for those that the dirstate cannot resolve, due
265 265 /// to time resolution limits.
266 266 ///
267 267 /// TODO: detect permission bits and similar metadata modifications
268 268 fn cat_file_is_modified(
269 269 repo: &Repo,
270 manifest: &Manifest,
270 271 hg_path: &HgPath,
271 rev: &str,
272 ) -> Result<bool, CommandError> {
273 // TODO CatRev expects &[HgPathBuf], something like
274 // &[impl Deref<HgPath>] would be nicer and should avoid the copy
275 let path_bufs = [hg_path.into()];
276 // TODO IIUC CatRev returns a simple Vec<u8> for all files
277 // being able to tell them apart as (path, bytes) would be nicer
278 // and OPTIM would allow manifest resolution just once.
279 let output = cat(repo, rev, &path_bufs).map_err(|e| (e, rev))?;
272 ) -> Result<bool, HgError> {
273 let file_node = manifest
274 .find_file(hg_path)?
275 .expect("ambgious file not in p1");
276 let filelog = repo.filelog(hg_path)?;
277 let filelog_entry = filelog.get_node(file_node).map_err(|_| {
278 HgError::corrupted("filelog missing node from manifest")
279 })?;
280 let contents_in_p1 = filelog_entry.data()?;
280 281
281 282 let fs_path = repo
282 283 .working_directory_vfs()
283 284 .join(hg_path_to_os_string(hg_path).expect("HgPath conversion"));
284 let hg_data_len: u64 = match output.concatenated.len().try_into() {
285 let hg_data_len: u64 = match contents_in_p1.len().try_into() {
285 286 Ok(v) => v,
286 287 Err(_) => {
287 288 // conversion of data length to u64 failed,
288 289 // good luck for any file to have this content
289 290 return Ok(true);
290 291 }
291 292 };
292 293 let fobj = fs::File::open(&fs_path).when_reading_file(&fs_path)?;
293 if fobj.metadata().map_err(|e| StatusError::from(e))?.len() != hg_data_len
294 {
294 if fobj.metadata().when_reading_file(&fs_path)?.len() != hg_data_len {
295 295 return Ok(true);
296 296 }
297 for (fs_byte, hg_byte) in
298 BufReader::new(fobj).bytes().zip(output.concatenated)
297 for (fs_byte, &hg_byte) in BufReader::new(fobj).bytes().zip(contents_in_p1)
299 298 {
300 if fs_byte.map_err(|e| StatusError::from(e))? != hg_byte {
299 if fs_byte.when_reading_file(&fs_path)? != hg_byte {
301 300 return Ok(true);
302 301 }
303 302 }
304 303 Ok(false)
305 304 }
General Comments 0
You need to be logged in to leave comments. Login now