use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{CopyError, Result};
type SkipCallback = Box<dyn FnMut(&'static str, &Path)>;
pub struct CopyOptions<'a> {
pub patterns: &'a [String],
pub excludes: &'a [String],
pub force: bool,
pub include_ignored: bool,
pub on_copied: Box<dyn FnMut(&Path)>,
pub on_skipped: SkipCallback,
}
impl Default for CopyOptions<'_> {
fn default() -> Self {
Self {
patterns: &[],
excludes: &[],
force: false,
include_ignored: false,
on_copied: Box::new(|_| {}),
on_skipped: Box::new(|_, _| {}),
}
}
}
pub fn copy_untracked(
from_path: &Path,
to_path: &Path,
options: CopyOptions<'_>,
) -> Result<Vec<PathBuf>> {
let CopyOptions {
patterns,
excludes,
force,
include_ignored,
mut on_copied,
mut on_skipped,
} = options;
let repo = git2::Repository::open(from_path).map_err(|source| CopyError::RepoOpen {
path: from_path.to_path_buf(),
source,
})?;
let mut index = repo.index().map_err(|source| CopyError::RepoOpen {
path: from_path.to_path_buf(),
source,
})?;
index.read(false).map_err(|source| CopyError::RepoOpen {
path: from_path.to_path_buf(),
source,
})?;
let include_patterns: Vec<glob::Pattern> = patterns
.iter()
.map(|p| {
glob::Pattern::new(p).map_err(|e| CopyError::InvalidGlobPattern {
pattern: p.clone(),
source: e,
})
})
.collect::<std::result::Result<Vec<_>, CopyError>>()?;
let exclude_patterns: Vec<glob::Pattern> = excludes
.iter()
.map(|p| {
glob::Pattern::new(p).map_err(|e| CopyError::InvalidGlobPattern {
pattern: p.clone(),
source: e,
})
})
.collect::<std::result::Result<Vec<_>, CopyError>>()?;
let match_opts = glob::MatchOptions {
case_sensitive: true,
require_literal_separator: false,
require_literal_leading_dot: false,
};
let mut builder = ignore::WalkBuilder::new(from_path);
builder.hidden(false);
if include_ignored {
builder
.git_ignore(false)
.git_global(false)
.git_exclude(false);
}
let mut copied_files = Vec::new();
for entry in builder.build() {
let entry = match entry {
Ok(e) => e,
Err(e) => {
log::debug!("Walk error: {}", e);
continue;
}
};
if entry.file_type().is_none_or(|ft| ft.is_dir()) {
continue;
}
let path = entry.path();
let rel_path = match path.strip_prefix(from_path) {
Ok(p) => p.to_path_buf(),
Err(_) => continue,
};
let rel_path_str = match rel_path.to_str() {
Some(s) => s,
None => continue,
};
if index.get_path(&rel_path, 0).is_some() {
on_skipped("tracked", &rel_path);
continue;
}
if !include_patterns.is_empty()
&& !include_patterns
.iter()
.any(|p| p.matches_with(rel_path_str, match_opts))
{
continue;
}
if exclude_patterns
.iter()
.any(|p| p.matches_with(rel_path_str, match_opts))
{
continue;
}
let dest_file = to_path.join(&rel_path);
if dest_file.exists() && !force {
on_skipped("exists", &rel_path);
continue;
}
if let Some(parent) = dest_file.parent() {
fs::create_dir_all(parent)?;
}
copy_file_platform(path, &dest_file)?;
on_copied(&rel_path);
copied_files.push(rel_path);
}
Ok(copied_files)
}
#[cfg(target_os = "macos")]
fn copy_file_platform(src: &Path, dest: &Path) -> Result<()> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let src_c = CString::new(src.as_os_str().as_bytes()).map_err(|_| CopyError::CopyFailed {
src: src.to_path_buf(),
dest: dest.to_path_buf(),
source: std::io::Error::from(std::io::ErrorKind::InvalidInput),
})?;
let dest_c = CString::new(dest.as_os_str().as_bytes()).map_err(|_| CopyError::CopyFailed {
src: src.to_path_buf(),
dest: dest.to_path_buf(),
source: std::io::Error::from(std::io::ErrorKind::InvalidInput),
})?;
if unsafe { libc::clonefile(src_c.as_ptr(), dest_c.as_ptr(), 0) } == 0 {
return Ok(());
}
fs::copy(src, dest)
.map(|_| ())
.map_err(|e| CopyError::CopyFailed {
src: src.to_path_buf(),
dest: dest.to_path_buf(),
source: e,
})
.map_err(Into::into)
}
#[cfg(target_os = "linux")]
fn copy_file_platform(src: &Path, dest: &Path) -> Result<()> {
use std::fs::{File, OpenOptions};
use std::os::unix::io::AsRawFd;
const FICLONE: libc::c_ulong = 0x40049409;
if let (Ok(src_file), Ok(dest_file)) = (
File::open(src),
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(dest),
) {
if unsafe { libc::ioctl(dest_file.as_raw_fd(), FICLONE, src_file.as_raw_fd()) } == 0 {
return Ok(());
}
drop(dest_file);
}
fs::copy(src, dest)
.map(|_| ())
.map_err(|e| CopyError::CopyFailed {
src: src.to_path_buf(),
dest: dest.to_path_buf(),
source: e,
})
.map_err(Into::into)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn copy_file_platform(src: &Path, dest: &Path) -> Result<()> {
fs::copy(src, dest)
.map(|_| ())
.map_err(|e| CopyError::CopyFailed {
src: src.to_path_buf(),
dest: dest.to_path_buf(),
source: e,
})
.map_err(Into::into)
}