Skip to content

Commit 81357d8

Browse files
authored
Implement reflink copy with automatic fallback for faster tree copying (#557)
This might be faster or use less disk space when source and tmp are on the same filesystem and the filesystem supports reflinks.
2 parents d9cc669 + 65b85d4 commit 81357d8

File tree

4 files changed

+49
-8
lines changed

4 files changed

+49
-8
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ num_cpus = "1.16"
4747
patch = "0.7"
4848
path-slash = "0.2"
4949
quote = "1.0.35"
50+
reflink = "0.1"
5051
regex = "1.10"
5152
schemars = "0.9"
5253
serde_json = "1.0.128"

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Changed: Tree copying now attempts to use reflinks (copy-on-write) for faster copying on supported filesystems (Btrfs, XFS, APFS), with automatic fallback to regular copying.
6+
57
- Book: Recommend using the `-Zunstable-options --fail-fast` argument to test targets to speed up mutation testing, on recent nightly toolchains.
68

79
- Fixed: Don't error if the `--in-diff` patch file contains non-UTF-8 data in non-Rust files.

src/copy_tree.rs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
//! Copy a source tree, with some exclusions, to a new temporary directory.
44
5+
use std::fs;
6+
use std::path::Path;
7+
use std::sync::atomic::{AtomicBool, Ordering};
8+
59
use anyhow::Context;
610
use camino::{Utf8Path, Utf8PathBuf};
711
use ignore::WalkBuilder;
8-
use path_slash::PathExt;
912
use tempfile::TempDir;
1013
use tracing::{debug, warn};
1114

@@ -24,6 +27,32 @@ use windows::copy_symlink;
2427

2528
static VCS_DIRS: &[&str] = &[".git", ".hg", ".bzr", ".svn", "_darcs", ".jj", ".pijul"];
2629

30+
/// Copy a file, attempting to use reflink if supported.
31+
/// Returns the number of bytes copied.
32+
fn copy_file(src: &Path, dest: &Path, reflink_supported: &AtomicBool) -> Result<u64> {
33+
// Try reflink first if we haven't determined it's not supported
34+
if reflink_supported.load(Ordering::Relaxed) {
35+
match reflink::reflink(src, dest) {
36+
Ok(()) => {
37+
// Reflink succeeded, get file size for progress tracking
38+
let metadata = fs::metadata(dest)
39+
.with_context(|| format!("Failed to get metadata for {}", dest.display()))?;
40+
return Ok(metadata.len());
41+
}
42+
Err(e) => {
43+
// On Windows, reflink can fail without returning ErrorKind::Unsupported,
44+
// so we give up on reflinks after any error to avoid repeated failures.
45+
reflink_supported.store(false, Ordering::Relaxed);
46+
debug!("Reflink failed: {}, falling back to regular copy", e);
47+
}
48+
}
49+
}
50+
51+
// Fall back to regular copy
52+
fs::copy(src, dest)
53+
.with_context(|| format!("Failed to copy {} to {}", src.display(), dest.display()))
54+
}
55+
2756
/// Copy a source tree, with some exclusions, to a new temporary directory.
2857
pub fn copy_tree(
2958
from_path: &Utf8Path,
@@ -33,6 +62,7 @@ pub fn copy_tree(
3362
) -> Result<TempDir> {
3463
let mut total_bytes = 0;
3564
let mut total_files = 0;
65+
let reflink_supported = AtomicBool::new(true);
3666
let temp_dir = tempfile::Builder::new()
3767
.prefix(name_base)
3868
.suffix(".tmp")
@@ -86,12 +116,8 @@ pub fn copy_tree(
86116
)
87117
})?;
88118
if ft.is_file() {
89-
let bytes_copied = std::fs::copy(entry.path(), &dest_path).with_context(|| {
90-
format!(
91-
"Failed to copy {:?} to {dest_path:?}",
92-
entry.path().to_slash_lossy(),
93-
)
94-
})?;
119+
let bytes_copied =
120+
copy_file(entry.path(), dest_path.as_std_path(), &reflink_supported)?;
95121
total_bytes += bytes_copied;
96122
total_files += 1;
97123
console.copy_progress(dest, total_bytes);
@@ -112,7 +138,8 @@ pub fn copy_tree(
112138
}
113139
}
114140
console.finish_copy(dest);
115-
debug!(?total_bytes, ?total_files, temp_dir = ?temp_dir.path(), "Copied source tree");
141+
let reflink_used = reflink_supported.load(Ordering::Relaxed);
142+
debug!(?total_bytes, ?total_files, ?reflink_used, temp_dir = ?temp_dir.path(), "Copied source tree");
116143
Ok(temp_dir)
117144
}
118145

0 commit comments

Comments
 (0)