use std::fs;
use std::path::PathBuf;
use crate::crlf::{ConversionConfig, FileAttrs};
use crate::diff::{mode_from_metadata, worktree_blob_content, WorktreeBlobContent};
use crate::error::{Error, Result};
use crate::filter_process::FilterProcessState;
use crate::hash::{
hash_object, try_par_hash_with, try_par_hash_with_force, ParallelHashError, Parallelism,
};
use crate::index::IndexEntry;
use crate::objects::{HashAlgo, ObjectId, ObjectKind};
use crate::odb::{self, Odb, WriteOptions};
use crate::worktree::{io_with_path, map_worktree_io, WorktreePathOperation};
pub(crate) struct WorktreeBlobReadInput {
pub abs: PathBuf,
pub index_relpath: String,
pub index_entry: Option<IndexEntry>,
pub file_attrs: FileAttrs,
pub cached_meta: Option<fs::Metadata>,
}
pub(crate) enum PreparedBlobPayload {
Zlib(Vec<u8>),
Raw(Vec<u8>),
Stream(PathBuf),
}
impl PreparedBlobPayload {
pub(crate) fn store(
&self,
odb: &Odb,
index_relpath: &str,
oid: &ObjectId,
options: WriteOptions,
) -> Result<()> {
match self {
Self::Zlib(zlib_store) => {
odb.write_loose_zlib_prehashed(oid, zlib_store, options)?;
}
Self::Raw(data) => {
let written = odb.write_with_options(ObjectKind::Blob, data, options)?;
debug_assert_eq!(written, *oid);
}
Self::Stream(path) => {
let mut file = fs::File::open(path).map_err(|source| {
map_worktree_io(index_relpath, WorktreePathOperation::Read, source)
})?;
let size = file.metadata()?.len();
let written = odb.write_stream(ObjectKind::Blob, size, &mut file, options)?;
if written != *oid {
return Err(Error::WorktreeFileChanged {
path: index_relpath.to_owned(),
});
}
}
}
Ok(())
}
}
pub(crate) struct WorktreeBlob {
pub index_relpath: String,
pub oid: ObjectId,
pub meta: fs::Metadata,
pub mode: u32,
}
impl WorktreeBlob {
fn new(item: &WorktreeBlobReadInput, oid: ObjectId, meta: fs::Metadata) -> Self {
Self {
index_relpath: item.index_relpath.clone(),
oid,
mode: mode_from_metadata(&meta),
meta,
}
}
}
pub(crate) struct PreparedWorktreeBlob {
pub blob: WorktreeBlob,
pub payload: PreparedBlobPayload,
}
struct PrepareBlobContext {
algo: HashAlgo,
compression: Option<flate2::Compression>,
}
impl PrepareBlobContext {
fn new(odb: &Odb) -> Result<Self> {
let compression = if odb.uses_files_primary() {
Some(odb.loose_compression()?)
} else {
None
};
Ok(Self {
algo: odb.hash_algo(),
compression,
})
}
fn read(
odb: &Odb,
item: &WorktreeBlobReadInput,
conv: &ConversionConfig,
filter_process: Option<&FilterProcessState>,
) -> Result<(fs::Metadata, WorktreeBlobContent)> {
let meta = match &item.cached_meta {
Some(m) => m.clone(),
None => io_with_path(
&item.index_relpath,
WorktreePathOperation::Metadata,
fs::symlink_metadata(&item.abs),
)?,
};
let content = worktree_blob_content(
odb,
&item.abs,
&meta,
conv,
&item.file_attrs,
&item.index_relpath,
item.index_entry.as_ref(),
filter_process,
)?;
Ok((meta, content))
}
fn encode(&self, data: Vec<u8>) -> Result<(ObjectId, PreparedBlobPayload)> {
let oid = hash_object(self.algo, ObjectKind::Blob, &data)?;
let payload = match self.compression {
Some(compression) => {
PreparedBlobPayload::Zlib(odb::zlib_compress_blob_loose(&data, compression)?)
}
None => PreparedBlobPayload::Raw(data),
};
Ok((oid, payload))
}
fn prepare(
&self,
odb: &Odb,
item: &WorktreeBlobReadInput,
conv: &ConversionConfig,
filter_process: Option<&FilterProcessState>,
) -> Result<PreparedWorktreeBlob> {
let (meta, content) = Self::read(odb, item, conv, filter_process)?;
let (oid, payload) = match content {
WorktreeBlobContent::Bytes(data) => self.encode(data)?,
stream @ WorktreeBlobContent::Stream { .. } => (
stream.hash(odb)?,
PreparedBlobPayload::Stream(item.abs.clone()),
),
};
Ok(PreparedWorktreeBlob {
blob: WorktreeBlob::new(item, oid, meta),
payload,
})
}
fn prepare_and_store(
&self,
odb: &Odb,
item: &WorktreeBlobReadInput,
conv: &ConversionConfig,
filter_process: Option<&FilterProcessState>,
write_opts: WriteOptions,
) -> Result<WorktreeBlob> {
let (meta, content) = Self::read(odb, item, conv, filter_process)?;
match content {
WorktreeBlobContent::Bytes(data) => {
let oid = hash_object(self.algo, ObjectKind::Blob, &data)?;
odb.write_blob_prehashed(&oid, &data, write_opts)
.map_err(|e| with_path_context(&item.index_relpath, e))?;
Ok(WorktreeBlob::new(item, oid, meta))
}
stream @ WorktreeBlobContent::Stream { .. } => {
let oid = stream
.store(odb, write_opts)
.map_err(|e| with_path_context(&item.index_relpath, e))?;
Ok(WorktreeBlob::new(item, oid, meta))
}
}
}
}
fn with_path_context(index_relpath: &str, err: Error) -> Error {
match err {
Error::Io(source) => Error::LooseObjectWrite {
path: index_relpath.to_owned(),
source,
},
Error::WorktreePathIo { .. } => err,
other => other,
}
}
pub(crate) fn prepare_worktree_blobs_parallel(
odb: &Odb,
items: &[WorktreeBlobReadInput],
conv: &ConversionConfig,
parallelism: Parallelism,
filter_process: Option<&FilterProcessState>,
) -> Result<Vec<PreparedWorktreeBlob>> {
if items.is_empty() {
return Ok(Vec::new());
}
let ctx = PrepareBlobContext::new(odb)?;
let total_bytes = items.len().saturating_mul(4096);
let threads = parallelism.threads();
try_par_hash_with(items, threads, total_bytes, |item| {
ctx.prepare(odb, item, conv, filter_process)
})
.map_err(|e: ParallelHashError<Error>| match e {
ParallelHashError::Task(err) => err,
})
}
pub(crate) fn prepare_and_store_worktree_blobs_parallel(
odb: &Odb,
items: &[WorktreeBlobReadInput],
conv: &ConversionConfig,
parallelism: Parallelism,
filter_process: Option<&FilterProcessState>,
write_opts: WriteOptions,
) -> Result<Vec<WorktreeBlob>> {
if items.is_empty() {
return Ok(Vec::new());
}
let ctx = PrepareBlobContext::new(odb)?;
let threads = parallelism.threads();
let map_item = |item: &WorktreeBlobReadInput| {
ctx.prepare_and_store(odb, item, conv, filter_process, write_opts)
};
if items.len() >= crate::hash::PAR_HASH_MIN_ITEMS {
try_par_hash_with_force(items, threads, map_item)
} else {
let total_bytes = items.len().saturating_mul(4096);
try_par_hash_with(items, threads, total_bytes, map_item)
}
.map_err(|e: ParallelHashError<Error>| match e {
ParallelHashError::Task(err) => err,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ConfigSet;
use crate::crlf::FileAttrs;
use std::fs;
use std::path::PathBuf;
fn shattered_prefix() -> Vec<u8> {
fs::read(
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/shattered-1-prefix.bin"),
)
.expect("fixture")
}
#[test]
fn worktree_add_accepts_shattered_file_like_git() {
let dir = tempfile::tempdir().expect("tempdir");
let file = dir.path().join("shattered.bin");
let data = shattered_prefix();
fs::write(&file, &data).expect("write");
let meta = fs::symlink_metadata(&file).expect("stat");
let odb = Odb::new(&dir.path().join("objects"));
fs::create_dir_all(odb.objects_dir()).expect("objects");
let conv = ConversionConfig::from_config(&ConfigSet::new());
let input = WorktreeBlobReadInput {
abs: file,
index_relpath: "shattered.bin".to_owned(),
index_entry: None,
file_attrs: FileAttrs::default(),
cached_meta: Some(meta),
};
let prepared = prepare_worktree_blobs_parallel(
&odb,
&[input],
&conv,
Parallelism::resolve(Some(1)),
None,
)
.expect("prepare shattered blob");
assert_eq!(prepared.len(), 1);
assert_eq!(
prepared[0].blob.oid,
hash_object(HashAlgo::Sha1, ObjectKind::Blob, &data).expect("blob id")
);
}
}