use std::borrow::Cow;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::error::{Error, Result};
use crate::odb::Odb;
static QUARANTINE_SEQ: AtomicU64 = AtomicU64::new(0);
pub struct ReceiveQuarantine {
path: PathBuf,
main_objects: PathBuf,
config_git_dir: Option<PathBuf>,
migrated: bool,
}
impl ReceiveQuarantine {
pub fn create(source: &Odb, git_dir: &Path) -> Result<Self> {
let main_objects = source
.files_objects_dir()
.ok_or(Error::UnsupportedObjectStore {
operation: "receive quarantine",
})?
.to_path_buf();
let seq = QUARANTINE_SEQ.fetch_add(1, Ordering::Relaxed);
let path = main_objects.join(format!(
"tmp_objdir-incoming-{}-{}",
std::process::id(),
seq
));
fs::create_dir_all(path.join("pack")).map_err(Error::Io)?;
Ok(Self {
path,
main_objects,
config_git_dir: Some(git_dir.to_path_buf()),
migrated: false,
})
}
#[must_use]
pub fn odb(&self) -> Odb {
let mut odb = Odb::new(&self.path).with_env_alternate_dirs(vec![self.main_objects.clone()]);
if let Some(git_dir) = &self.config_git_dir {
odb = odb.with_config_git_dir(git_dir.clone());
}
odb
}
#[must_use]
pub fn hook_env(&self) -> Vec<(String, String)> {
let quarantine = abs_or_path(&self.path);
let main = format_alternate_object_directories(&abs_or_path(&self.main_objects));
vec![
("GIT_OBJECT_DIRECTORY".to_owned(), quarantine.clone()),
("GIT_ALTERNATE_OBJECT_DIRECTORIES".to_owned(), main),
("GIT_QUARANTINE_PATH".to_owned(), quarantine),
]
}
pub fn migrate(&mut self) -> Result<()> {
migrate_tree(&self.path, &self.main_objects)?;
self.migrated = true;
remove_quarantine_dir(&self.path)?;
Ok(())
}
}
impl Drop for ReceiveQuarantine {
fn drop(&mut self) {
if !self.migrated {
let _ = remove_quarantine_dir(&self.path);
}
}
}
fn abs_or_path(path: &Path) -> String {
path.canonicalize()
.unwrap_or_else(|_| path.to_path_buf())
.to_string_lossy()
.into_owned()
}
fn format_alternate_object_directories(path: &str) -> String {
match quote_git_env_path(path) {
Cow::Borrowed(p) => p.to_owned(),
Cow::Owned(p) => p,
}
}
fn quote_git_env_path(path: &str) -> Cow<'_, str> {
if !path.contains(':') && !path.contains('"') {
return Cow::Borrowed(path);
}
let mut out = String::from('"');
for ch in path.chars() {
match ch {
'"' | '\\' => {
out.push('\\');
out.push(ch);
}
c if c.is_control() => {
let b = c as u32;
out.push_str(&format!("\\{b:03o}"));
}
c => out.push(c),
}
}
out.push('"');
Cow::Owned(out)
}
fn remove_quarantine_dir(path: &Path) -> Result<()> {
if path.is_dir() {
fs::remove_dir_all(path).map_err(Error::Io)?;
}
Ok(())
}
fn migrate_tree(src: &Path, dst: &Path) -> Result<()> {
if !src.is_dir() {
return Ok(());
}
fs::create_dir_all(dst).map_err(Error::Io)?;
let mut names: Vec<String> = fs::read_dir(src)
.map_err(Error::Io)?
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
names.sort_by_key(|name| pack_copy_priority(name));
for name in names {
let from = src.join(&name);
let to = dst.join(&name);
let meta = fs::symlink_metadata(&from).map_err(Error::Io)?;
if meta.is_dir() {
migrate_tree(&from, &to)?;
} else {
migrate_file(&from, &to)?;
}
}
Ok(())
}
fn pack_copy_priority(name: &str) -> u8 {
if !name.starts_with("pack-") && name != "pack" {
return 0;
}
if name.ends_with(".keep") {
1
} else if name.ends_with(".pack") {
2
} else if name.ends_with(".rev") {
3
} else if name.ends_with(".idx") {
4
} else {
5
}
}
fn migrate_file(from: &Path, to: &Path) -> Result<()> {
if let Some(parent) = to.parent() {
fs::create_dir_all(parent).map_err(Error::Io)?;
}
if to.exists() {
return Ok(());
}
if fs::rename(from, to).is_err() {
fs::copy(from, to).map_err(Error::Io)?;
fs::remove_file(from).map_err(Error::Io)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::objects::ObjectKind;
#[test]
fn quarantine_ingest_and_migrate_makes_objects_visible_in_main() {
let tmp = tempfile::tempdir().expect("tempdir");
let main = tmp.path().join("objects");
fs::create_dir_all(&main).unwrap();
let main_odb = Odb::new(&main);
let git_dir = tmp.path().join(".git");
std::fs::create_dir_all(&git_dir).unwrap();
let mut q = ReceiveQuarantine::create(&main_odb, &git_dir).expect("quarantine");
let oid = q
.odb()
.write(ObjectKind::Blob, b"quarantined")
.expect("write");
assert!(q.odb().exists(&oid));
assert!(!Odb::new(&main).exists(&oid));
q.migrate().expect("migrate");
assert!(Odb::new(&main).exists(&oid));
}
#[test]
fn quarantine_odb_matches_repository_hash_algorithm() {
let tmp = tempfile::tempdir().expect("tempdir");
let git_dir = tmp.path().join("repo.git");
std::fs::create_dir_all(&git_dir).unwrap();
let init = std::process::Command::new("git")
.current_dir(&git_dir)
.args(["init", "-q", "--bare", "--object-format=sha256", "."])
.status();
if init.map(|s| !s.success()).unwrap_or(true) {
return;
}
let objects = git_dir.join("objects");
let source = Odb::new(&objects).with_config_git_dir(git_dir.clone());
assert_eq!(source.hash_algo(), crate::objects::HashAlgo::Sha256);
let q = ReceiveQuarantine::create(&source, &git_dir).expect("quarantine");
assert_eq!(q.odb().hash_algo(), crate::objects::HashAlgo::Sha256);
}
#[test]
fn alternate_env_quotes_paths_containing_colons() {
let tmp = tempfile::tempdir().expect("tempdir");
let main = tmp.path().join("obj:ects");
std::fs::create_dir_all(&main).unwrap();
let git_dir = tmp.path().join("repo.git");
std::fs::create_dir_all(&git_dir).unwrap();
let source = Odb::new(&main).with_config_git_dir(git_dir.clone());
let q = ReceiveQuarantine::create(&source, &git_dir).expect("quarantine");
let alt = q
.hook_env()
.into_iter()
.find(|(k, _)| k == "GIT_ALTERNATE_OBJECT_DIRECTORIES")
.map(|(_, v)| v)
.expect("alt env");
assert!(
alt.starts_with('"') && alt.ends_with('"'),
"colon in path must be quoted: {alt}"
);
}
#[test]
fn drop_removes_quarantine_without_migrating() {
let tmp = tempfile::tempdir().expect("tempdir");
let main = tmp.path().join("objects");
fs::create_dir_all(&main).unwrap();
let main_odb = Odb::new(&main);
let git_dir = tmp.path().join(".git");
std::fs::create_dir_all(&git_dir).unwrap();
let quarantine_path = {
let q = ReceiveQuarantine::create(&main_odb, &git_dir).expect("quarantine");
let oid = q.odb().write(ObjectKind::Blob, b"x").expect("write");
let path = q
.hook_env()
.into_iter()
.find(|(k, _)| k == "GIT_QUARANTINE_PATH")
.map(|(_, v)| PathBuf::from(v))
.expect("quarantine path");
drop(q);
assert!(!path.exists());
assert!(!Odb::new(&main).exists(&oid));
path
};
let _ = quarantine_path;
}
}