##// END OF EJS Templates
rust: fix darwin build
Dan Villiom Podlaski Christiansen -
r53207:33d8cb64 default
parent child Browse files
Show More
@@ -1,421 +1,421
1 1 //! Helpers for the revlog config and opening options
2 2
3 3 use std::collections::HashSet;
4 4
5 5 use crate::{
6 6 config::{Config, ResourceProfileValue},
7 7 errors::HgError,
8 8 requirements::{
9 9 CHANGELOGV2_REQUIREMENT, GENERALDELTA_REQUIREMENT, NARROW_REQUIREMENT,
10 10 NODEMAP_REQUIREMENT, REVLOGV1_REQUIREMENT, REVLOGV2_REQUIREMENT,
11 11 SPARSEREVLOG_REQUIREMENT,
12 12 },
13 13 };
14 14
15 15 use super::{compression::CompressionConfig, RevlogType};
16 16
17 17 const DEFAULT_CHUNK_CACHE_SIZE: u64 = 65536;
18 18 const DEFAULT_SPARSE_READ_DENSITY_THRESHOLD: f64 = 0.50;
19 19 const DEFAULT_SPARSE_READ_MIN_GAP_SIZE: u64 = 262144;
20 20
21 21 /// The known revlog versions and their options
22 22 #[derive(Debug, Copy, Clone, PartialEq)]
23 23 pub enum RevlogVersionOptions {
24 24 V0,
25 25 V1 { general_delta: bool, inline: bool },
26 26 V2,
27 27 ChangelogV2 { compute_rank: bool },
28 28 }
29 29
30 30 /// Options to govern how a revlog should be opened, usually from the
31 31 /// repository configuration or requirements.
32 32 #[derive(Debug, Copy, Clone)]
33 33 pub struct RevlogOpenOptions {
34 34 /// The revlog version, along with any option specific to this version
35 35 pub version: RevlogVersionOptions,
36 36 /// Whether the revlog uses a persistent nodemap.
37 37 pub use_nodemap: bool,
38 38 pub delta_config: RevlogDeltaConfig,
39 39 pub data_config: RevlogDataConfig,
40 40 pub feature_config: RevlogFeatureConfig,
41 41 }
42 42
43 43 #[cfg(test)]
44 44 impl Default for RevlogOpenOptions {
45 45 fn default() -> Self {
46 46 Self {
47 47 version: RevlogVersionOptions::V1 {
48 48 general_delta: true,
49 49 inline: false,
50 50 },
51 51 use_nodemap: true,
52 52 data_config: Default::default(),
53 53 delta_config: Default::default(),
54 54 feature_config: Default::default(),
55 55 }
56 56 }
57 57 }
58 58
59 59 impl RevlogOpenOptions {
60 60 pub fn new(
61 61 inline: bool,
62 62 data_config: RevlogDataConfig,
63 63 delta_config: RevlogDeltaConfig,
64 64 feature_config: RevlogFeatureConfig,
65 65 ) -> Self {
66 66 Self {
67 67 version: RevlogVersionOptions::V1 {
68 68 general_delta: data_config.general_delta,
69 69 inline,
70 70 },
71 71 use_nodemap: false,
72 72 data_config,
73 73 delta_config,
74 74 feature_config,
75 75 }
76 76 }
77 77
78 78 pub fn index_header(&self) -> super::index::IndexHeader {
79 79 super::index::IndexHeader {
80 80 header_bytes: match self.version {
81 81 RevlogVersionOptions::V0 => [0, 0, 0, 0],
82 82 RevlogVersionOptions::V1 {
83 83 general_delta,
84 84 inline,
85 85 } => [
86 86 0,
87 87 if general_delta && inline {
88 88 3
89 89 } else if general_delta {
90 90 2
91 91 } else {
92 92 u8::from(inline)
93 93 },
94 94 0,
95 95 1,
96 96 ],
97 97 RevlogVersionOptions::V2 => 0xDEADu32.to_be_bytes(),
98 98 RevlogVersionOptions::ChangelogV2 { compute_rank: _ } => {
99 99 0xD34Du32.to_be_bytes()
100 100 }
101 101 },
102 102 }
103 103 }
104 104 }
105 105
106 106 /// Technically only Linux 2.5.46+ has `MAP_POPULATE` and only `2.6.23` on
107 107 /// private mappings, but if you're using such ancient Linux, you have other
108 108 /// problems.
109 109 #[cfg(target_os = "linux")]
110 110 const fn can_populate_mmap() -> bool {
111 111 true
112 112 }
113 113
114 114 /// There is a of populating mmaps for Windows, but it would need testing.
115 115 #[cfg(not(target_os = "linux"))]
116 const fn can_populate_mmap() {
116 const fn can_populate_mmap() -> bool {
117 117 false
118 118 }
119 119
120 120 #[derive(Debug, Clone, Copy, PartialEq)]
121 121 /// Holds configuration values about how the revlog data is read
122 122 pub struct RevlogDataConfig {
123 123 /// Should we try to open the "pending" version of the revlog
124 124 pub try_pending: bool,
125 125 /// Should we try to open the "split" version of the revlog
126 126 pub try_split: bool,
127 127 /// When True, `indexfile` should be opened with `checkambig=True` at
128 128 /// writing time, to avoid file stat ambiguity
129 129 pub check_ambig: bool,
130 130 /// If true, use mmap instead of reading to deal with large indexes
131 131 pub mmap_large_index: bool,
132 132 /// How much data is considered large
133 133 pub mmap_index_threshold: Option<u64>,
134 134 /// How much data to read and cache into the raw revlog data cache
135 135 pub chunk_cache_size: u64,
136 136 /// The size of the uncompressed cache compared to the largest revision
137 137 /// seen
138 138 pub uncompressed_cache_factor: Option<f64>,
139 139 /// The number of chunks cached
140 140 pub uncompressed_cache_count: Option<u64>,
141 141 /// Allow sparse reading of the revlog data
142 142 pub with_sparse_read: bool,
143 143 /// Minimal density of a sparse read chunk
144 144 pub sr_density_threshold: f64,
145 145 /// Minimal size of the data we skip when performing sparse reads
146 146 pub sr_min_gap_size: u64,
147 147 /// Whether deltas are encoded against arbitrary bases
148 148 pub general_delta: bool,
149 149 }
150 150
151 151 impl RevlogDataConfig {
152 152 pub fn new(
153 153 config: &Config,
154 154 requirements: &HashSet<String>,
155 155 ) -> Result<Self, HgError> {
156 156 let mut data_config = Self::default();
157 157 if let Some(chunk_cache_size) =
158 158 config.get_byte_size(b"format", b"chunkcachesize")?
159 159 {
160 160 data_config.chunk_cache_size = chunk_cache_size;
161 161 }
162 162
163 163 let memory_profile = config.get_resource_profile(Some("memory"));
164 164 if memory_profile.value >= ResourceProfileValue::Medium {
165 165 data_config.uncompressed_cache_count = Some(10_000);
166 166 data_config.uncompressed_cache_factor = Some(4.0);
167 167 if memory_profile.value >= ResourceProfileValue::High {
168 168 data_config.uncompressed_cache_factor = Some(10.0)
169 169 }
170 170 }
171 171
172 172 // Use mmap if requested, or by default if we can fully populate it
173 173 let mmap_index = config
174 174 .get_option_no_default(b"storage", b"revlog.mmap.index")?
175 175 .unwrap_or(can_populate_mmap());
176 176 if mmap_index {
177 177 if let Some(mmap_index_threshold) = config.get_byte_size(
178 178 b"storage",
179 179 b"revlog.mmap.index:size-threshold",
180 180 )? {
181 181 // Only mmap if above the requested size threshold
182 182 data_config.mmap_index_threshold = Some(mmap_index_threshold);
183 183 }
184 184 }
185 185
186 186 if let Some(mmap_index_threshold) = config
187 187 .get_byte_size(b"storage", b"revlog.mmap.index:size-threshold")?
188 188 {
189 189 data_config.mmap_index_threshold = Some(mmap_index_threshold);
190 190 }
191 191
192 192 let with_sparse_read =
193 193 config.get_bool(b"experimental", b"sparse-read")?;
194 194 if let Some(sr_density_threshold) = config
195 195 .get_f64(b"experimental", b"sparse-read.density-threshold")?
196 196 {
197 197 data_config.sr_density_threshold = sr_density_threshold;
198 198 }
199 199 data_config.with_sparse_read = with_sparse_read;
200 200 if let Some(sr_min_gap_size) = config
201 201 .get_byte_size(b"experimental", b"sparse-read.min-gap-size")?
202 202 {
203 203 data_config.sr_min_gap_size = sr_min_gap_size;
204 204 }
205 205
206 206 data_config.with_sparse_read =
207 207 requirements.contains(SPARSEREVLOG_REQUIREMENT);
208 208
209 209 Ok(data_config)
210 210 }
211 211 }
212 212
213 213 impl Default for RevlogDataConfig {
214 214 fn default() -> Self {
215 215 Self {
216 216 chunk_cache_size: DEFAULT_CHUNK_CACHE_SIZE,
217 217 sr_density_threshold: DEFAULT_SPARSE_READ_DENSITY_THRESHOLD,
218 218 sr_min_gap_size: DEFAULT_SPARSE_READ_MIN_GAP_SIZE,
219 219 try_pending: Default::default(),
220 220 try_split: Default::default(),
221 221 check_ambig: Default::default(),
222 222 mmap_large_index: Default::default(),
223 223 mmap_index_threshold: Default::default(),
224 224 uncompressed_cache_factor: Default::default(),
225 225 uncompressed_cache_count: Default::default(),
226 226 with_sparse_read: Default::default(),
227 227 general_delta: Default::default(),
228 228 }
229 229 }
230 230 }
231 231
232 232 #[derive(Debug, Clone, Copy, PartialEq)]
233 233 /// Holds configuration values about how new deltas are computed.
234 234 ///
235 235 /// Some attributes are duplicated from [`RevlogDataConfig`] to help having
236 236 /// each object self contained.
237 237 pub struct RevlogDeltaConfig {
238 238 /// Whether deltas can be encoded against arbitrary bases
239 239 pub general_delta: bool,
240 240 /// Allow sparse writing of the revlog data
241 241 pub sparse_revlog: bool,
242 242 /// Maximum length of a delta chain
243 243 pub max_chain_len: Option<u64>,
244 244 /// Maximum distance between a delta chain's start and end
245 245 pub max_deltachain_span: Option<u64>,
246 246 /// If `upper_bound_comp` is not None, this is the expected maximal
247 247 /// gain from compression for the data content
248 248 pub upper_bound_comp: Option<f64>,
249 249 /// Should we try a delta against both parents
250 250 pub delta_both_parents: bool,
251 251 /// Test delta base candidate groups by chunks of this maximal size
252 252 pub candidate_group_chunk_size: u64,
253 253 /// Should we display debug information about delta computation
254 254 pub debug_delta: bool,
255 255 /// Trust incoming deltas by default
256 256 pub lazy_delta: bool,
257 257 /// Trust the base of incoming deltas by default
258 258 pub lazy_delta_base: bool,
259 259 }
260 260
261 261 impl RevlogDeltaConfig {
262 262 pub fn new(
263 263 config: &Config,
264 264 requirements: &HashSet<String>,
265 265 revlog_type: RevlogType,
266 266 ) -> Result<Self, HgError> {
267 267 let mut delta_config = Self {
268 268 delta_both_parents: config
269 269 .get_option_no_default(
270 270 b"storage",
271 271 b"revlog.optimize-delta-parent-choice",
272 272 )?
273 273 .unwrap_or(true),
274 274 candidate_group_chunk_size: config
275 275 .get_u64(
276 276 b"storage",
277 277 b"revlog.delta-parent-search.candidate-group-chunk-size",
278 278 )?
279 279 .unwrap_or_default(),
280 280 ..Default::default()
281 281 };
282 282
283 283 delta_config.debug_delta =
284 284 config.get_bool(b"debug", b"revlog.debug-delta")?;
285 285
286 286 delta_config.general_delta =
287 287 requirements.contains(GENERALDELTA_REQUIREMENT);
288 288
289 289 let lazy_delta =
290 290 config.get_bool(b"storage", b"revlog.reuse-external-delta")?;
291 291
292 292 if revlog_type == RevlogType::Manifestlog {
293 293 // upper bound of what we expect from compression
294 294 // (real life value seems to be 3)
295 295 delta_config.upper_bound_comp = Some(3.0)
296 296 }
297 297
298 298 let mut lazy_delta_base = false;
299 299 if lazy_delta {
300 300 lazy_delta_base = match config.get_option_no_default(
301 301 b"storage",
302 302 b"revlog.reuse-external-delta-parent",
303 303 )? {
304 304 Some(base) => base,
305 305 None => config.get_bool(b"format", b"generaldelta")?,
306 306 };
307 307 }
308 308 delta_config.lazy_delta = lazy_delta;
309 309 delta_config.lazy_delta_base = lazy_delta_base;
310 310
311 311 delta_config.max_deltachain_span =
312 312 match config.get_i64(b"experimental", b"maxdeltachainspan")? {
313 313 Some(span) => {
314 314 if span < 0 {
315 315 None
316 316 } else {
317 317 Some(span as u64)
318 318 }
319 319 }
320 320 None => None,
321 321 };
322 322
323 323 delta_config.sparse_revlog =
324 324 requirements.contains(SPARSEREVLOG_REQUIREMENT);
325 325
326 326 delta_config.max_chain_len =
327 327 config.get_byte_size_no_default(b"format", b"maxchainlen")?;
328 328
329 329 Ok(delta_config)
330 330 }
331 331 }
332 332
333 333 impl Default for RevlogDeltaConfig {
334 334 fn default() -> Self {
335 335 Self {
336 336 delta_both_parents: true,
337 337 lazy_delta: true,
338 338 general_delta: Default::default(),
339 339 sparse_revlog: Default::default(),
340 340 max_chain_len: Default::default(),
341 341 max_deltachain_span: Default::default(),
342 342 upper_bound_comp: Default::default(),
343 343 candidate_group_chunk_size: Default::default(),
344 344 debug_delta: Default::default(),
345 345 lazy_delta_base: Default::default(),
346 346 }
347 347 }
348 348 }
349 349
350 350 #[derive(Debug, Default, Clone, Copy, PartialEq)]
351 351 /// Holds configuration values about the available revlog features
352 352 pub struct RevlogFeatureConfig {
353 353 /// The compression engine and its options
354 354 pub compression_engine: CompressionConfig,
355 355 /// Can we use censor on this revlog
356 356 pub censorable: bool,
357 357 /// Does this revlog use the "side data" feature
358 358 pub has_side_data: bool,
359 359 /// Might remove this configuration once the rank computation has no
360 360 /// impact
361 361 pub compute_rank: bool,
362 362 /// Parent order is supposed to be semantically irrelevant, so we
363 363 /// normally re-sort parents to ensure that the first parent is non-null,
364 364 /// if there is a non-null parent at all.
365 365 /// filelog abuses the parent order as a flag to mark some instances of
366 366 /// meta-encoded files, so allow it to disable this behavior.
367 367 pub canonical_parent_order: bool,
368 368 /// Can ellipsis commit be used
369 369 pub enable_ellipsis: bool,
370 370 }
371 371
372 372 impl RevlogFeatureConfig {
373 373 pub fn new(
374 374 config: &Config,
375 375 requirements: &HashSet<String>,
376 376 ) -> Result<Self, HgError> {
377 377 Ok(Self {
378 378 compression_engine: CompressionConfig::new(config, requirements)?,
379 379 enable_ellipsis: requirements.contains(NARROW_REQUIREMENT),
380 380 ..Default::default()
381 381 })
382 382 }
383 383 }
384 384
385 385 /// Return the default options for a revlog of `revlog_type` according to the
386 386 /// current config and requirements.
387 387 pub fn default_revlog_options(
388 388 config: &Config,
389 389 requirements: &HashSet<String>,
390 390 revlog_type: RevlogType,
391 391 ) -> Result<RevlogOpenOptions, HgError> {
392 392 let is_changelog = revlog_type == RevlogType::Changelog;
393 393 let version =
394 394 if is_changelog && requirements.contains(CHANGELOGV2_REQUIREMENT) {
395 395 let compute_rank = config
396 396 .get_bool(b"experimental", b"changelog-v2.compute-rank")?;
397 397 RevlogVersionOptions::ChangelogV2 { compute_rank }
398 398 } else if requirements.contains(REVLOGV2_REQUIREMENT) {
399 399 RevlogVersionOptions::V2
400 400 } else if requirements.contains(REVLOGV1_REQUIREMENT) {
401 401 RevlogVersionOptions::V1 {
402 402 general_delta: requirements.contains(GENERALDELTA_REQUIREMENT),
403 403 inline: !is_changelog,
404 404 }
405 405 } else {
406 406 RevlogVersionOptions::V0
407 407 };
408 408 Ok(RevlogOpenOptions {
409 409 version,
410 410 // We don't need to dance around the slow path like in the Python
411 411 // implementation since we know we have access to the fast code.
412 412 use_nodemap: requirements.contains(NODEMAP_REQUIREMENT),
413 413 delta_config: RevlogDeltaConfig::new(
414 414 config,
415 415 requirements,
416 416 revlog_type,
417 417 )?,
418 418 data_config: RevlogDataConfig::new(config, requirements)?,
419 419 feature_config: RevlogFeatureConfig::new(config, requirements)?,
420 420 })
421 421 }
@@ -1,1130 +1,1131
1 1 use crate::errors::{HgError, HgResultExt, IoErrorContext, IoResultExt};
2 2 use crate::exit_codes;
3 3 use crate::fncache::FnCache;
4 4 use crate::revlog::path_encode::path_encode;
5 5 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
6 6 use dyn_clone::DynClone;
7 7 use format_bytes::format_bytes;
8 8 use memmap2::{Mmap, MmapOptions};
9 9 use rand::distributions::{Alphanumeric, DistString};
10 10 use std::fs::{File, Metadata, OpenOptions};
11 11 use std::io::{ErrorKind, Read, Seek, Write};
12 12 use std::os::fd::AsRawFd;
13 13 use std::os::unix::fs::{MetadataExt, PermissionsExt};
14 14 use std::path::{Path, PathBuf};
15 15 #[cfg(test)]
16 16 use std::sync::atomic::AtomicUsize;
17 17 #[cfg(test)]
18 18 use std::sync::atomic::Ordering;
19 19 use std::sync::OnceLock;
20 20
21 21 /// Filesystem access abstraction for the contents of a given "base" diretory
22 22 #[derive(Clone)]
23 23 pub struct VfsImpl {
24 24 pub(crate) base: PathBuf,
25 25 pub readonly: bool,
26 26 pub mode: Option<u32>,
27 27 }
28 28
29 29 struct FileNotFound(std::io::Error, PathBuf);
30 30
31 31 /// Store the umask for the whole process since it's expensive to get.
32 32 static UMASK: OnceLock<u32> = OnceLock::new();
33 33
34 34 fn get_umask() -> u32 {
35 35 *UMASK.get_or_init(|| unsafe {
36 36 // TODO is there any way of getting the umask without temporarily
37 37 // setting it? Doesn't this affect all threads in this tiny window?
38 38 let mask = libc::umask(0);
39 39 libc::umask(mask);
40 mask & 0o777
40 #[allow(clippy::useless_conversion)]
41 (mask & 0o777).into()
41 42 })
42 43 }
43 44
44 45 /// Return the (unix) mode with which we will create/fix files
45 46 fn get_mode(base: impl AsRef<Path>) -> Option<u32> {
46 47 match base.as_ref().metadata() {
47 48 Ok(meta) => {
48 49 // files in .hg/ will be created using this mode
49 50 let mode = meta.mode();
50 51 // avoid some useless chmods
51 52 if (0o777 & !get_umask()) == (0o777 & mode) {
52 53 None
53 54 } else {
54 55 Some(mode)
55 56 }
56 57 }
57 58 Err(_) => None,
58 59 }
59 60 }
60 61
61 62 impl VfsImpl {
62 63 pub fn new(base: PathBuf, readonly: bool) -> Self {
63 64 let mode = get_mode(&base);
64 65 Self {
65 66 base,
66 67 readonly,
67 68 mode,
68 69 }
69 70 }
70 71
71 72 // XXX these methods are probably redundant with VFS trait?
72 73
73 74 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
74 75 self.base.join(relative_path)
75 76 }
76 77
77 78 pub fn symlink_metadata(
78 79 &self,
79 80 relative_path: impl AsRef<Path>,
80 81 ) -> Result<std::fs::Metadata, HgError> {
81 82 let path = self.join(relative_path);
82 83 std::fs::symlink_metadata(&path).when_reading_file(&path)
83 84 }
84 85
85 86 pub fn read_link(
86 87 &self,
87 88 relative_path: impl AsRef<Path>,
88 89 ) -> Result<PathBuf, HgError> {
89 90 let path = self.join(relative_path);
90 91 std::fs::read_link(&path).when_reading_file(&path)
91 92 }
92 93
93 94 pub fn read(
94 95 &self,
95 96 relative_path: impl AsRef<Path>,
96 97 ) -> Result<Vec<u8>, HgError> {
97 98 let path = self.join(relative_path);
98 99 std::fs::read(&path).when_reading_file(&path)
99 100 }
100 101
101 102 /// Returns `Ok(None)` if the file does not exist.
102 103 pub fn try_read(
103 104 &self,
104 105 relative_path: impl AsRef<Path>,
105 106 ) -> Result<Option<Vec<u8>>, HgError> {
106 107 match self.read(relative_path) {
107 108 Err(e) => match &e {
108 109 HgError::IoError { error, .. } => match error.kind() {
109 110 ErrorKind::NotFound => Ok(None),
110 111 _ => Err(e),
111 112 },
112 113 _ => Err(e),
113 114 },
114 115 Ok(v) => Ok(Some(v)),
115 116 }
116 117 }
117 118
118 119 fn mmap_open_gen(
119 120 &self,
120 121 relative_path: impl AsRef<Path>,
121 122 ) -> Result<Result<Mmap, FileNotFound>, HgError> {
122 123 let path = self.join(relative_path);
123 124 let file = match std::fs::File::open(&path) {
124 125 Err(err) => {
125 126 if let ErrorKind::NotFound = err.kind() {
126 127 return Ok(Err(FileNotFound(err, path)));
127 128 };
128 129 return (Err(err)).when_reading_file(&path);
129 130 }
130 131 Ok(file) => file,
131 132 };
132 133 // Safety is "enforced" by locks and assuming other processes are
133 134 // well-behaved. If any misbehaving or malicious process does touch
134 135 // the index, it could lead to corruption. This is inherent
135 136 // to file-based `mmap`, though some platforms have some ways of
136 137 // mitigating.
137 138 // TODO linux: set the immutable flag with `chattr(1)`?
138 139 let mmap = unsafe { MmapOptions::new().map(&file) }
139 140 .when_reading_file(&path)?;
140 141 Ok(Ok(mmap))
141 142 }
142 143
143 144 pub fn mmap_open_opt(
144 145 &self,
145 146 relative_path: impl AsRef<Path>,
146 147 ) -> Result<Option<Mmap>, HgError> {
147 148 self.mmap_open_gen(relative_path).map(|res| res.ok())
148 149 }
149 150
150 151 pub fn mmap_open(
151 152 &self,
152 153 relative_path: impl AsRef<Path>,
153 154 ) -> Result<Mmap, HgError> {
154 155 match self.mmap_open_gen(relative_path)? {
155 156 Err(FileNotFound(err, path)) => Err(err).when_reading_file(&path),
156 157 Ok(res) => Ok(res),
157 158 }
158 159 }
159 160
160 161 #[cfg(unix)]
161 162 pub fn create_symlink(
162 163 &self,
163 164 relative_link_path: impl AsRef<Path>,
164 165 target_path: impl AsRef<Path>,
165 166 ) -> Result<(), HgError> {
166 167 let link_path = self.join(relative_link_path);
167 168 std::os::unix::fs::symlink(target_path, &link_path)
168 169 .when_writing_file(&link_path)
169 170 }
170 171
171 172 /// Write `contents` into a temporary file, then rename to `relative_path`.
172 173 /// This makes writing to a file "atomic": a reader opening that path will
173 174 /// see either the previous contents of the file or the complete new
174 175 /// content, never a partial write.
175 176 pub fn atomic_write(
176 177 &self,
177 178 relative_path: impl AsRef<Path>,
178 179 contents: &[u8],
179 180 ) -> Result<(), HgError> {
180 181 let mut tmp = tempfile::NamedTempFile::new_in(&self.base)
181 182 .when_writing_file(&self.base)?;
182 183 tmp.write_all(contents)
183 184 .and_then(|()| tmp.flush())
184 185 .when_writing_file(tmp.path())?;
185 186 let path = self.join(relative_path);
186 187 tmp.persist(&path)
187 188 .map_err(|e| e.error)
188 189 .when_writing_file(&path)?;
189 190 Ok(())
190 191 }
191 192 }
192 193
193 194 fn fs_metadata(
194 195 path: impl AsRef<Path>,
195 196 ) -> Result<Option<std::fs::Metadata>, HgError> {
196 197 let path = path.as_ref();
197 198 match path.metadata() {
198 199 Ok(meta) => Ok(Some(meta)),
199 200 Err(error) => match error.kind() {
200 201 // TODO: when we require a Rust version where `NotADirectory` is
201 202 // stable, invert this logic and return None for it and `NotFound`
202 203 // and propagate any other error.
203 204 ErrorKind::PermissionDenied => Err(error).with_context(|| {
204 205 IoErrorContext::ReadingMetadata(path.to_owned())
205 206 }),
206 207 _ => Ok(None),
207 208 },
208 209 }
209 210 }
210 211
211 212 /// Abstraction over the files handled by a [`Vfs`].
212 213 #[derive(Debug)]
213 214 pub enum VfsFile {
214 215 Atomic(AtomicFile),
215 216
216 217 Normal {
217 218 file: File,
218 219 path: PathBuf,
219 220 /// If `Some`, check (and maybe fix) this file's timestamp ambiguity.
220 221 /// See [`is_filetime_ambiguous`].
221 222 check_ambig: Option<Metadata>,
222 223 },
223 224 }
224 225
225 226 impl VfsFile {
226 227 pub fn normal(file: File, path: PathBuf) -> Self {
227 228 Self::Normal {
228 229 file,
229 230 check_ambig: None,
230 231 path,
231 232 }
232 233 }
233 234 pub fn normal_check_ambig(
234 235 file: File,
235 236 path: PathBuf,
236 237 ) -> Result<Self, HgError> {
237 238 Ok(Self::Normal {
238 239 file,
239 240 check_ambig: Some(path.metadata().when_reading_file(&path)?),
240 241 path,
241 242 })
242 243 }
243 244 pub fn try_clone(&self) -> Result<VfsFile, HgError> {
244 245 Ok(match self {
245 246 VfsFile::Atomic(AtomicFile {
246 247 fp,
247 248 temp_path,
248 249 check_ambig,
249 250 target_name,
250 251 is_open,
251 252 }) => Self::Atomic(AtomicFile {
252 253 fp: fp.try_clone().when_reading_file(temp_path)?,
253 254 temp_path: temp_path.clone(),
254 255 check_ambig: *check_ambig,
255 256 target_name: target_name.clone(),
256 257 is_open: *is_open,
257 258 }),
258 259 VfsFile::Normal {
259 260 file,
260 261 check_ambig,
261 262 path,
262 263 } => Self::Normal {
263 264 file: file.try_clone().when_reading_file(path)?,
264 265 check_ambig: check_ambig.clone(),
265 266 path: path.to_owned(),
266 267 },
267 268 })
268 269 }
269 270 pub fn set_len(&self, len: u64) -> Result<(), std::io::Error> {
270 271 match self {
271 272 VfsFile::Atomic(atomic_file) => atomic_file.fp.set_len(len),
272 273 VfsFile::Normal { file, .. } => file.set_len(len),
273 274 }
274 275 }
275 276
276 277 pub fn metadata(&self) -> Result<std::fs::Metadata, std::io::Error> {
277 278 match self {
278 279 VfsFile::Atomic(atomic_file) => atomic_file.fp.metadata(),
279 280 VfsFile::Normal { file, .. } => file.metadata(),
280 281 }
281 282 }
282 283 }
283 284
284 285 impl AsRawFd for VfsFile {
285 286 fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
286 287 match self {
287 288 VfsFile::Atomic(atomic_file) => atomic_file.fp.as_raw_fd(),
288 289 VfsFile::Normal { file, .. } => file.as_raw_fd(),
289 290 }
290 291 }
291 292 }
292 293
293 294 impl Seek for VfsFile {
294 295 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
295 296 match self {
296 297 VfsFile::Atomic(atomic_file) => atomic_file.seek(pos),
297 298 VfsFile::Normal { file, .. } => file.seek(pos),
298 299 }
299 300 }
300 301 }
301 302
302 303 impl Read for VfsFile {
303 304 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
304 305 match self {
305 306 VfsFile::Atomic(atomic_file) => atomic_file.fp.read(buf),
306 307 VfsFile::Normal { file, .. } => file.read(buf),
307 308 }
308 309 }
309 310 }
310 311
311 312 impl Write for VfsFile {
312 313 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
313 314 match self {
314 315 VfsFile::Atomic(atomic_file) => atomic_file.fp.write(buf),
315 316 VfsFile::Normal { file, .. } => file.write(buf),
316 317 }
317 318 }
318 319
319 320 fn flush(&mut self) -> std::io::Result<()> {
320 321 match self {
321 322 VfsFile::Atomic(atomic_file) => atomic_file.fp.flush(),
322 323 VfsFile::Normal { file, .. } => file.flush(),
323 324 }
324 325 }
325 326 }
326 327
327 328 impl Drop for VfsFile {
328 329 fn drop(&mut self) {
329 330 if let VfsFile::Normal {
330 331 path,
331 332 check_ambig: Some(old),
332 333 ..
333 334 } = self
334 335 {
335 336 avoid_timestamp_ambiguity(path, old)
336 337 }
337 338 }
338 339 }
339 340
340 341 /// Records the number of times we've fixed a timestamp ambiguity, only
341 342 /// applicable for tests.
342 343 #[cfg(test)]
343 344 static TIMESTAMP_FIXES_CALLS: AtomicUsize = AtomicUsize::new(0);
344 345
345 346 fn avoid_timestamp_ambiguity(path: &Path, old: &Metadata) {
346 347 if let Ok(new) = path.metadata() {
347 348 let is_ambiguous = is_filetime_ambiguous(&new, old);
348 349 if is_ambiguous {
349 350 let advanced =
350 351 filetime::FileTime::from_unix_time(old.mtime() + 1, 0);
351 352 if filetime::set_file_times(path, advanced, advanced).is_ok() {
352 353 #[cfg(test)]
353 354 {
354 355 TIMESTAMP_FIXES_CALLS.fetch_add(1, Ordering::Relaxed);
355 356 }
356 357 }
357 358 }
358 359 }
359 360 }
360 361
361 362 /// Examine whether new stat is ambiguous against old one
362 363 ///
363 364 /// `S[N]` below means stat of a file at `N`-th change:
364 365 ///
365 366 /// - `S[n-1].ctime < S[n].ctime`: can detect change of a file
366 367 /// - `S[n-1].ctime == S[n].ctime`
367 368 /// - `S[n-1].ctime < S[n].mtime`: means natural advancing (*1)
368 369 /// - `S[n-1].ctime == S[n].mtime`: is ambiguous (*2)
369 370 /// - `S[n-1].ctime > S[n].mtime`: never occurs naturally (don't care)
370 371 /// - `S[n-1].ctime > S[n].ctime`: never occurs naturally (don't care)
371 372 ///
372 373 /// Case (*2) above means that a file was changed twice or more at
373 374 /// same time in sec (= `S[n-1].ctime`), and comparison of timestamp
374 375 /// is ambiguous.
375 376 ///
376 377 /// Base idea to avoid such ambiguity is "advance mtime 1 sec, if
377 378 /// timestamp is ambiguous".
378 379 ///
379 380 /// But advancing mtime only in case (*2) doesn't work as
380 381 /// expected, because naturally advanced `S[n].mtime` in case (*1)
381 382 /// might be equal to manually advanced `S[n-1 or earlier].mtime`.
382 383 ///
383 384 /// Therefore, all `S[n-1].ctime == S[n].ctime` cases should be
384 385 /// treated as ambiguous regardless of mtime, to avoid overlooking
385 386 /// by confliction between such mtime.
386 387 ///
387 388 /// Advancing mtime `if isambig(new, old)` ensures `S[n-1].mtime !=
388 389 /// S[n].mtime`, even if size of a file isn't changed.
389 390 pub fn is_filetime_ambiguous(new: &Metadata, old: &Metadata) -> bool {
390 391 new.ctime() == old.ctime()
391 392 }
392 393
393 394 /// Writable file object that atomically updates a file
394 395 ///
395 396 /// All writes will go to a temporary copy of the original file. Call
396 397 /// [`Self::close`] when you are done writing, and [`Self`] will rename
397 398 /// the temporary copy to the original name, making the changes
398 399 /// visible. If the object is destroyed without being closed, all your
399 400 /// writes are discarded.
400 401 #[derive(Debug)]
401 402 pub struct AtomicFile {
402 403 /// The temporary file to write to
403 404 fp: std::fs::File,
404 405 /// Path of the temp file
405 406 temp_path: PathBuf,
406 407 /// Used when stat'ing the file, is useful only if the target file is
407 408 /// guarded by any lock (e.g. repo.lock or repo.wlock).
408 409 check_ambig: bool,
409 410 /// Path of the target file
410 411 target_name: PathBuf,
411 412 /// Whether the file is open or not
412 413 is_open: bool,
413 414 }
414 415
415 416 impl AtomicFile {
416 417 pub fn new(
417 418 target_path: impl AsRef<Path>,
418 419 empty: bool,
419 420 check_ambig: bool,
420 421 ) -> Result<Self, HgError> {
421 422 let target_path = target_path.as_ref().to_owned();
422 423
423 424 let random_id =
424 425 Alphanumeric.sample_string(&mut rand::thread_rng(), 12);
425 426 let filename =
426 427 target_path.file_name().expect("target has no filename");
427 428 let filename = get_bytes_from_path(filename);
428 429 let temp_filename =
429 430 format_bytes!(b".{}-{}~", filename, random_id.as_bytes());
430 431 let temp_path =
431 432 target_path.with_file_name(get_path_from_bytes(&temp_filename));
432 433
433 434 if !empty {
434 435 std::fs::copy(&target_path, &temp_path)
435 436 .with_context(|| IoErrorContext::CopyingFile {
436 437 from: target_path.to_owned(),
437 438 to: temp_path.to_owned(),
438 439 })
439 440 // If it doesn't exist, create it on open
440 441 .io_not_found_as_none()?;
441 442 }
442 443 let fp = std::fs::OpenOptions::new()
443 444 .write(true)
444 445 .create(true)
445 446 .truncate(empty)
446 447 .open(&temp_path)
447 448 .when_writing_file(&temp_path)?;
448 449
449 450 Ok(Self {
450 451 fp,
451 452 temp_path,
452 453 check_ambig,
453 454 target_name: target_path,
454 455 is_open: true,
455 456 })
456 457 }
457 458
458 459 pub fn from_file(
459 460 fp: std::fs::File,
460 461 check_ambig: bool,
461 462 temp_name: PathBuf,
462 463 target_name: PathBuf,
463 464 ) -> Self {
464 465 Self {
465 466 fp,
466 467 check_ambig,
467 468 temp_path: temp_name,
468 469 target_name,
469 470 is_open: true,
470 471 }
471 472 }
472 473
473 474 /// Write `buf` to the temporary file
474 475 pub fn write_all(&mut self, buf: &[u8]) -> Result<(), std::io::Error> {
475 476 self.fp.write_all(buf)
476 477 }
477 478
478 479 fn target(&self) -> PathBuf {
479 480 self.temp_path
480 481 .parent()
481 482 .expect("should not be at the filesystem root")
482 483 .join(&self.target_name)
483 484 }
484 485
485 486 /// Close the temporary file and rename to the target
486 487 pub fn close(mut self) -> Result<(), std::io::Error> {
487 488 self.fp.flush()?;
488 489 let target = self.target();
489 490 if self.check_ambig {
490 491 if let Ok(stat) = target.metadata() {
491 492 std::fs::rename(&self.temp_path, &target)?;
492 493 avoid_timestamp_ambiguity(&target, &stat);
493 494 } else {
494 495 std::fs::rename(&self.temp_path, target)?;
495 496 }
496 497 } else {
497 498 std::fs::rename(&self.temp_path, target)?;
498 499 }
499 500 self.is_open = false;
500 501 Ok(())
501 502 }
502 503 }
503 504
504 505 impl Seek for AtomicFile {
505 506 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
506 507 self.fp.seek(pos)
507 508 }
508 509 }
509 510
510 511 impl Drop for AtomicFile {
511 512 fn drop(&mut self) {
512 513 if self.is_open {
513 514 std::fs::remove_file(&self.temp_path).ok();
514 515 }
515 516 }
516 517 }
517 518
518 519 /// Abstracts over the VFS to allow for different implementations of the
519 520 /// filesystem layer (like passing one from Python).
520 521 pub trait Vfs: Sync + Send + DynClone {
521 522 /// Open a [`VfsFile::Normal`] for reading the file at `filename`,
522 523 /// relative to this VFS's root.
523 524 fn open(&self, filename: &Path) -> Result<VfsFile, HgError>;
524 525 /// Open a [`VfsFile::Normal`] for writing and reading the file at
525 526 /// `filename`, relative to this VFS's root.
526 527 fn open_write(&self, filename: &Path) -> Result<VfsFile, HgError>;
527 528 /// Open a [`VfsFile::Normal`] for reading and writing the file at
528 529 /// `filename`, relative to this VFS's root. This file will be checked
529 530 /// for an ambiguous mtime on [`drop`]. See [`is_filetime_ambiguous`].
530 531 fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError>;
531 532 /// Create a [`VfsFile::Normal`] for reading and writing the file at
532 533 /// `filename`, relative to this VFS's root. If the file already exists,
533 534 /// it will be truncated to 0 bytes.
534 535 fn create(
535 536 &self,
536 537 filename: &Path,
537 538 check_ambig: bool,
538 539 ) -> Result<VfsFile, HgError>;
539 540 /// Create a [`VfsFile::Atomic`] for reading and writing the file at
540 541 /// `filename`, relative to this VFS's root. If the file already exists,
541 542 /// it will be truncated to 0 bytes.
542 543 fn create_atomic(
543 544 &self,
544 545 filename: &Path,
545 546 check_ambig: bool,
546 547 ) -> Result<VfsFile, HgError>;
547 548 /// Return the total file size in bytes of the open `file`. Errors are
548 549 /// usual IO errors (invalid file handle, permissions, etc.)
549 550 fn file_size(&self, file: &VfsFile) -> Result<u64, HgError>;
550 551 /// Return `true` if `filename` exists relative to this VFS's root. Errors
551 552 /// will coerce to `false`, to this also returns `false` if there are
552 553 /// IO problems. This is fine because any operation that actually tries
553 554 /// to do anything with this path will get the same error.
554 555 fn exists(&self, filename: &Path) -> bool;
555 556 /// Remove the file at `filename` relative to this VFS's root. Errors
556 557 /// are the usual IO errors (lacking permission, file does not exist, etc.)
557 558 fn unlink(&self, filename: &Path) -> Result<(), HgError>;
558 559 /// Rename the file `from` to `to`, both relative to this VFS's root.
559 560 /// Errors are the usual IO errors (lacking permission, file does not
560 561 /// exist, etc.). If `check_ambig` is `true`, the VFS will check for an
561 562 /// ambiguous mtime on rename. See [`is_filetime_ambiguous`].
562 563 fn rename(
563 564 &self,
564 565 from: &Path,
565 566 to: &Path,
566 567 check_ambig: bool,
567 568 ) -> Result<(), HgError>;
568 569 /// Rename the file `from` to `to`, both relative to this VFS's root.
569 570 /// Errors are the usual IO errors (lacking permission, file does not
570 571 /// exist, etc.). If `check_ambig` is passed, the VFS will check for an
571 572 /// ambiguous mtime on rename. See [`is_filetime_ambiguous`].
572 573 fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError>;
573 574 /// Returns the absolute root path of this VFS, relative to which all
574 575 /// operations are done.
575 576 fn base(&self) -> &Path;
576 577 }
577 578
578 579 /// These methods will need to be implemented once `rhg` (and other) non-Python
579 580 /// users of `hg-core` start doing more on their own, like writing to files.
580 581 impl Vfs for VfsImpl {
581 582 fn open(&self, filename: &Path) -> Result<VfsFile, HgError> {
582 583 // TODO auditpath
583 584 let path = self.base.join(filename);
584 585 Ok(VfsFile::normal(
585 586 std::fs::File::open(&path).when_reading_file(&path)?,
586 587 filename.to_owned(),
587 588 ))
588 589 }
589 590
590 591 fn open_write(&self, filename: &Path) -> Result<VfsFile, HgError> {
591 592 if self.readonly {
592 593 return Err(HgError::abort(
593 594 "write access in a readonly vfs",
594 595 exit_codes::ABORT,
595 596 None,
596 597 ));
597 598 }
598 599 // TODO auditpath
599 600 let path = self.base.join(filename);
600 601 copy_in_place_if_hardlink(&path)?;
601 602
602 603 Ok(VfsFile::normal(
603 604 OpenOptions::new()
604 605 .create(false)
605 606 .create_new(false)
606 607 .write(true)
607 608 .read(true)
608 609 .open(&path)
609 610 .when_writing_file(&path)?,
610 611 path.to_owned(),
611 612 ))
612 613 }
613 614
614 615 fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError> {
615 616 if self.readonly {
616 617 return Err(HgError::abort(
617 618 "write access in a readonly vfs",
618 619 exit_codes::ABORT,
619 620 None,
620 621 ));
621 622 }
622 623
623 624 let path = self.base.join(filename);
624 625 copy_in_place_if_hardlink(&path)?;
625 626
626 627 // TODO auditpath
627 628 VfsFile::normal_check_ambig(
628 629 OpenOptions::new()
629 630 .write(true)
630 631 .read(true) // Can be used for reading to save on `open` calls
631 632 .create(false)
632 633 .open(&path)
633 634 .when_reading_file(&path)?,
634 635 path.to_owned(),
635 636 )
636 637 }
637 638
638 639 fn create(
639 640 &self,
640 641 filename: &Path,
641 642 check_ambig: bool,
642 643 ) -> Result<VfsFile, HgError> {
643 644 if self.readonly {
644 645 return Err(HgError::abort(
645 646 "write access in a readonly vfs",
646 647 exit_codes::ABORT,
647 648 None,
648 649 ));
649 650 }
650 651 // TODO auditpath
651 652 let path = self.base.join(filename);
652 653 let parent = path.parent().expect("file at root");
653 654 std::fs::create_dir_all(parent).when_writing_file(parent)?;
654 655
655 656 let file = OpenOptions::new()
656 657 .create(true)
657 658 .truncate(true)
658 659 .write(true)
659 660 .read(true)
660 661 .open(&path)
661 662 .when_writing_file(&path)?;
662 663
663 664 if let Some(mode) = self.mode {
664 665 // Creating the file with the right permission (with `.mode()`)
665 666 // may not work since umask takes effect for file creation.
666 667 // So we need to fix the permission after creating the file.
667 668 fix_directory_permissions(&self.base, &path, mode)?;
668 669 let perm = std::fs::Permissions::from_mode(mode & 0o666);
669 670 std::fs::set_permissions(&path, perm).when_writing_file(&path)?;
670 671 }
671 672
672 673 Ok(VfsFile::Normal {
673 674 file,
674 675 check_ambig: if check_ambig {
675 676 Some(path.metadata().when_reading_file(&path)?)
676 677 } else {
677 678 None
678 679 },
679 680 path: path.to_owned(),
680 681 })
681 682 }
682 683
683 684 fn create_atomic(
684 685 &self,
685 686 _filename: &Path,
686 687 _check_ambig: bool,
687 688 ) -> Result<VfsFile, HgError> {
688 689 todo!()
689 690 }
690 691
691 692 fn file_size(&self, file: &VfsFile) -> Result<u64, HgError> {
692 693 Ok(file
693 694 .metadata()
694 695 .map_err(|e| {
695 696 HgError::abort(
696 697 format!("Could not get file metadata: {}", e),
697 698 exit_codes::ABORT,
698 699 None,
699 700 )
700 701 })?
701 702 .size())
702 703 }
703 704
704 705 fn exists(&self, filename: &Path) -> bool {
705 706 self.base.join(filename).exists()
706 707 }
707 708
708 709 fn unlink(&self, filename: &Path) -> Result<(), HgError> {
709 710 if self.readonly {
710 711 return Err(HgError::abort(
711 712 "write access in a readonly vfs",
712 713 exit_codes::ABORT,
713 714 None,
714 715 ));
715 716 }
716 717 let path = self.base.join(filename);
717 718 std::fs::remove_file(&path)
718 719 .with_context(|| IoErrorContext::RemovingFile(path))
719 720 }
720 721
721 722 fn rename(
722 723 &self,
723 724 from: &Path,
724 725 to: &Path,
725 726 check_ambig: bool,
726 727 ) -> Result<(), HgError> {
727 728 if self.readonly {
728 729 return Err(HgError::abort(
729 730 "write access in a readonly vfs",
730 731 exit_codes::ABORT,
731 732 None,
732 733 ));
733 734 }
734 735 let old_stat = if check_ambig {
735 736 Some(
736 737 from.metadata()
737 738 .when_reading_file(from)
738 739 .io_not_found_as_none()?,
739 740 )
740 741 } else {
741 742 None
742 743 };
743 744 let from = self.base.join(from);
744 745 let to = self.base.join(to);
745 746 std::fs::rename(&from, &to).with_context(|| {
746 747 IoErrorContext::RenamingFile {
747 748 from,
748 749 to: to.to_owned(),
749 750 }
750 751 })?;
751 752 if let Some(Some(old)) = old_stat {
752 753 avoid_timestamp_ambiguity(&to, &old);
753 754 }
754 755 Ok(())
755 756 }
756 757
757 758 fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError> {
758 759 let from = self.base.join(from);
759 760 let to = self.base.join(to);
760 761 std::fs::copy(&from, &to)
761 762 .with_context(|| IoErrorContext::CopyingFile { from, to })
762 763 .map(|_| ())
763 764 }
764 765
765 766 fn base(&self) -> &Path {
766 767 &self.base
767 768 }
768 769 }
769 770
770 771 fn fix_directory_permissions(
771 772 base: &Path,
772 773 path: &Path,
773 774 mode: u32,
774 775 ) -> Result<(), HgError> {
775 776 let mut ancestors = path.ancestors();
776 777 ancestors.next(); // yields the path itself
777 778
778 779 for ancestor in ancestors {
779 780 if ancestor == base {
780 781 break;
781 782 }
782 783 let perm = std::fs::Permissions::from_mode(mode);
783 784 std::fs::set_permissions(ancestor, perm)
784 785 .when_writing_file(ancestor)?;
785 786 }
786 787 Ok(())
787 788 }
788 789
789 790 /// A VFS that understands the `fncache` store layout (file encoding), and
790 791 /// adds new entries to the `fncache`.
791 792 /// TODO Only works when using from Python for now.
792 793 pub struct FnCacheVfs {
793 794 inner: VfsImpl,
794 795 fncache: Box<dyn FnCache>,
795 796 }
796 797
797 798 impl Clone for FnCacheVfs {
798 799 fn clone(&self) -> Self {
799 800 Self {
800 801 inner: self.inner.clone(),
801 802 fncache: dyn_clone::clone_box(&*self.fncache),
802 803 }
803 804 }
804 805 }
805 806
806 807 impl FnCacheVfs {
807 808 pub fn new(
808 809 base: PathBuf,
809 810 readonly: bool,
810 811 fncache: Box<dyn FnCache>,
811 812 ) -> Self {
812 813 let inner = VfsImpl::new(base, readonly);
813 814 Self { inner, fncache }
814 815 }
815 816
816 817 fn maybe_add_to_fncache(
817 818 &self,
818 819 filename: &Path,
819 820 encoded_path: &Path,
820 821 ) -> Result<(), HgError> {
821 822 let relevant_file = (filename.starts_with("data/")
822 823 || filename.starts_with("meta/"))
823 824 && is_revlog_file(filename);
824 825 if relevant_file {
825 826 let not_load = !self.fncache.is_loaded()
826 827 && (self.exists(filename)
827 828 && self
828 829 .inner
829 830 .join(encoded_path)
830 831 .metadata()
831 832 .when_reading_file(encoded_path)?
832 833 .size()
833 834 != 0);
834 835 if !not_load {
835 836 self.fncache.add(filename);
836 837 }
837 838 };
838 839 Ok(())
839 840 }
840 841 }
841 842
842 843 impl Vfs for FnCacheVfs {
843 844 fn open(&self, filename: &Path) -> Result<VfsFile, HgError> {
844 845 let encoded = path_encode(&get_bytes_from_path(filename));
845 846 let filename = get_path_from_bytes(&encoded);
846 847 self.inner.open(filename)
847 848 }
848 849
849 850 fn open_write(&self, filename: &Path) -> Result<VfsFile, HgError> {
850 851 let encoded = path_encode(&get_bytes_from_path(filename));
851 852 let encoded_path = get_path_from_bytes(&encoded);
852 853 self.maybe_add_to_fncache(filename, encoded_path)?;
853 854 self.inner.open_write(encoded_path)
854 855 }
855 856
856 857 fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError> {
857 858 let encoded = path_encode(&get_bytes_from_path(filename));
858 859 let filename = get_path_from_bytes(&encoded);
859 860 self.inner.open_check_ambig(filename)
860 861 }
861 862
862 863 fn create(
863 864 &self,
864 865 filename: &Path,
865 866 check_ambig: bool,
866 867 ) -> Result<VfsFile, HgError> {
867 868 let encoded = path_encode(&get_bytes_from_path(filename));
868 869 let encoded_path = get_path_from_bytes(&encoded);
869 870 self.maybe_add_to_fncache(filename, encoded_path)?;
870 871 self.inner.create(encoded_path, check_ambig)
871 872 }
872 873
873 874 fn create_atomic(
874 875 &self,
875 876 filename: &Path,
876 877 check_ambig: bool,
877 878 ) -> Result<VfsFile, HgError> {
878 879 let encoded = path_encode(&get_bytes_from_path(filename));
879 880 let filename = get_path_from_bytes(&encoded);
880 881 self.inner.create_atomic(filename, check_ambig)
881 882 }
882 883
883 884 fn file_size(&self, file: &VfsFile) -> Result<u64, HgError> {
884 885 self.inner.file_size(file)
885 886 }
886 887
887 888 fn exists(&self, filename: &Path) -> bool {
888 889 let encoded = path_encode(&get_bytes_from_path(filename));
889 890 let filename = get_path_from_bytes(&encoded);
890 891 self.inner.exists(filename)
891 892 }
892 893
893 894 fn unlink(&self, filename: &Path) -> Result<(), HgError> {
894 895 let encoded = path_encode(&get_bytes_from_path(filename));
895 896 let filename = get_path_from_bytes(&encoded);
896 897 self.inner.unlink(filename)
897 898 }
898 899
899 900 fn rename(
900 901 &self,
901 902 from: &Path,
902 903 to: &Path,
903 904 check_ambig: bool,
904 905 ) -> Result<(), HgError> {
905 906 let encoded = path_encode(&get_bytes_from_path(from));
906 907 let from = get_path_from_bytes(&encoded);
907 908 let encoded = path_encode(&get_bytes_from_path(to));
908 909 let to = get_path_from_bytes(&encoded);
909 910 self.inner.rename(from, to, check_ambig)
910 911 }
911 912
912 913 fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError> {
913 914 let encoded = path_encode(&get_bytes_from_path(from));
914 915 let from = get_path_from_bytes(&encoded);
915 916 let encoded = path_encode(&get_bytes_from_path(to));
916 917 let to = get_path_from_bytes(&encoded);
917 918 self.inner.copy(from, to)
918 919 }
919 920 fn base(&self) -> &Path {
920 921 self.inner.base()
921 922 }
922 923 }
923 924
924 925 /// Detects whether `path` is a hardlink and does a tmp copy + rename erase
925 926 /// to turn it into its own file. Revlogs are usually hardlinked when doing
926 927 /// a local clone, and we don't want to modify the original repo.
927 928 fn copy_in_place_if_hardlink(path: &Path) -> Result<(), HgError> {
928 929 let metadata = path.metadata().when_writing_file(path)?;
929 930 if metadata.nlink() > 0 {
930 931 // If it's hardlinked, copy it and rename it back before changing it.
931 932 let tmpdir = path.parent().expect("file at root");
932 933 let name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
933 934 let tmpfile = tmpdir.join(name);
934 935 std::fs::create_dir_all(tmpfile.parent().expect("file at root"))
935 936 .with_context(|| IoErrorContext::CopyingFile {
936 937 from: path.to_owned(),
937 938 to: tmpfile.to_owned(),
938 939 })?;
939 940 std::fs::copy(path, &tmpfile).with_context(|| {
940 941 IoErrorContext::CopyingFile {
941 942 from: path.to_owned(),
942 943 to: tmpfile.to_owned(),
943 944 }
944 945 })?;
945 946 std::fs::rename(&tmpfile, path).with_context(|| {
946 947 IoErrorContext::RenamingFile {
947 948 from: tmpfile,
948 949 to: path.to_owned(),
949 950 }
950 951 })?;
951 952 }
952 953 Ok(())
953 954 }
954 955
955 956 pub fn is_revlog_file(path: impl AsRef<Path>) -> bool {
956 957 path.as_ref()
957 958 .extension()
958 959 .map(|ext| {
959 960 ["i", "idx", "d", "dat", "n", "nd", "sda"]
960 961 .contains(&ext.to_string_lossy().as_ref())
961 962 })
962 963 .unwrap_or(false)
963 964 }
964 965
965 966 pub(crate) fn is_dir(path: impl AsRef<Path>) -> Result<bool, HgError> {
966 967 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir()))
967 968 }
968 969
969 970 pub(crate) fn is_file(path: impl AsRef<Path>) -> Result<bool, HgError> {
970 971 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_file()))
971 972 }
972 973
973 974 /// Returns whether the given `path` is on a network file system.
974 975 /// Taken from `cargo`'s codebase.
975 976 #[cfg(target_os = "linux")]
976 977 pub(crate) fn is_on_nfs_mount(path: impl AsRef<Path>) -> bool {
977 978 use std::ffi::CString;
978 979 use std::mem;
979 980 use std::os::unix::prelude::*;
980 981
981 982 let path = match CString::new(path.as_ref().as_os_str().as_bytes()) {
982 983 Ok(path) => path,
983 984 Err(_) => return false,
984 985 };
985 986
986 987 unsafe {
987 988 let mut buf: libc::statfs = mem::zeroed();
988 989 let r = libc::statfs(path.as_ptr(), &mut buf);
989 990
990 991 r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
991 992 }
992 993 }
993 994
994 995 /// Similar to what Cargo does; although detecting NFS (or non-local
995 996 /// file systems) _should_ be possible on other operating systems,
996 997 /// we'll just assume that mmap() works there, for now; after all,
997 998 /// _some_ functionality is better than a compile error, i.e. none at
998 999 /// all
999 1000 #[cfg(not(target_os = "linux"))]
1000 1001 pub(crate) fn is_on_nfs_mount(_path: impl AsRef<Path>) -> bool {
1001 1002 false
1002 1003 }
1003 1004
1004 1005 #[cfg(test)]
1005 1006 mod tests {
1006 1007 use super::*;
1007 1008
1008 1009 #[test]
1009 1010 fn test_atomic_file() {
1010 1011 let dir = tempfile::tempdir().unwrap().into_path();
1011 1012 let target_path = dir.join("sometargetname");
1012 1013
1013 1014 for empty in [true, false] {
1014 1015 let file = AtomicFile::new(&target_path, empty, false).unwrap();
1015 1016 assert!(file.is_open);
1016 1017 let filename =
1017 1018 file.temp_path.file_name().unwrap().to_str().unwrap();
1018 1019 // Make sure we have a coherent temp name
1019 1020 assert_eq!(filename.len(), 29, "{}", filename);
1020 1021 assert!(filename.contains("sometargetname"));
1021 1022
1022 1023 // Make sure the temp file is created in the same folder
1023 1024 assert_eq!(target_path.parent(), file.temp_path.parent());
1024 1025 }
1025 1026
1026 1027 assert!(!target_path.exists());
1027 1028 std::fs::write(&target_path, "version 1").unwrap();
1028 1029 let mut file = AtomicFile::new(&target_path, false, false).unwrap();
1029 1030 file.write_all(b"version 2!").unwrap();
1030 1031 assert_eq!(
1031 1032 std::fs::read(&target_path).unwrap(),
1032 1033 b"version 1".to_vec()
1033 1034 );
1034 1035 let temp_path = file.temp_path.to_owned();
1035 1036 // test that dropping the file should discard the temp file and not
1036 1037 // affect the target path.
1037 1038 drop(file);
1038 1039 assert_eq!(
1039 1040 std::fs::read(&target_path).unwrap(),
1040 1041 b"version 1".to_vec()
1041 1042 );
1042 1043 assert!(!temp_path.exists());
1043 1044
1044 1045 let mut file = AtomicFile::new(&target_path, false, false).unwrap();
1045 1046 file.write_all(b"version 2!").unwrap();
1046 1047 assert_eq!(
1047 1048 std::fs::read(&target_path).unwrap(),
1048 1049 b"version 1".to_vec()
1049 1050 );
1050 1051 file.close().unwrap();
1051 1052 assert_eq!(
1052 1053 std::fs::read(&target_path).unwrap(),
1053 1054 b"version 2!".to_vec(),
1054 1055 "{}",
1055 1056 std::fs::read_to_string(&target_path).unwrap()
1056 1057 );
1057 1058 assert!(target_path.exists());
1058 1059 assert!(!temp_path.exists());
1059 1060 }
1060 1061
1061 1062 #[test]
1062 1063 fn test_vfs_file_check_ambig() {
1063 1064 let dir = tempfile::tempdir().unwrap().into_path();
1064 1065 let file_path = dir.join("file");
1065 1066
1066 1067 fn vfs_file_write(file_path: &Path, check_ambig: bool) {
1067 1068 let file = std::fs::OpenOptions::new()
1068 1069 .write(true)
1069 1070 .open(file_path)
1070 1071 .unwrap();
1071 1072 let old_stat = if check_ambig {
1072 1073 Some(file.metadata().unwrap())
1073 1074 } else {
1074 1075 None
1075 1076 };
1076 1077
1077 1078 let mut vfs_file = VfsFile::Normal {
1078 1079 file,
1079 1080 path: file_path.to_owned(),
1080 1081 check_ambig: old_stat,
1081 1082 };
1082 1083 vfs_file.write_all(b"contents").unwrap();
1083 1084 }
1084 1085
1085 1086 std::fs::OpenOptions::new()
1086 1087 .write(true)
1087 1088 .create(true)
1088 1089 .truncate(false)
1089 1090 .open(&file_path)
1090 1091 .unwrap();
1091 1092
1092 1093 let number_of_writes = 3;
1093 1094
1094 1095 // Try multiple times, because reproduction of an ambiguity depends
1095 1096 // on "filesystem time"
1096 1097 for _ in 0..5 {
1097 1098 TIMESTAMP_FIXES_CALLS.store(0, Ordering::Relaxed);
1098 1099 vfs_file_write(&file_path, false);
1099 1100 let old_stat = file_path.metadata().unwrap();
1100 1101 if old_stat.ctime() != old_stat.mtime() {
1101 1102 // subsequent changing never causes ambiguity
1102 1103 continue;
1103 1104 }
1104 1105
1105 1106 // Repeat atomic write with `check_ambig == true`, to examine
1106 1107 // whether the mtime is advanced multiple times as expected
1107 1108 for _ in 0..number_of_writes {
1108 1109 vfs_file_write(&file_path, true);
1109 1110 }
1110 1111 let new_stat = file_path.metadata().unwrap();
1111 1112 if !is_filetime_ambiguous(&new_stat, &old_stat) {
1112 1113 // timestamp ambiguity was naturally avoided while repetition
1113 1114 continue;
1114 1115 }
1115 1116
1116 1117 assert_eq!(
1117 1118 TIMESTAMP_FIXES_CALLS.load(Ordering::Relaxed),
1118 1119 number_of_writes
1119 1120 );
1120 1121 assert_eq!(
1121 1122 old_stat.mtime() + number_of_writes as i64,
1122 1123 file_path.metadata().unwrap().mtime()
1123 1124 );
1124 1125 break;
1125 1126 }
1126 1127 // If we've arrived here without breaking, we might not have
1127 1128 // tested anything because the platform is too slow. This test will
1128 1129 // still work on fast platforms.
1129 1130 }
1130 1131 }
General Comments 0
You need to be logged in to leave comments. Login now