use std::collections::HashMap;
use std::fs;
use std::path::Path;
use git2::{Oid, Repository};
use crate::error::Result;
use crate::storage::{FileRefStore, RefStore};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorageVersion {
V1FileSystem,
V2GitNative,
Mixed,
}
#[derive(Debug)]
pub struct MigrationResult {
pub objects_migrated: usize,
pub refs_migrated: usize,
pub version: StorageVersion,
}
pub fn detect_version(agit_dir: &Path, repo: &Repository) -> StorageVersion {
let has_git_refs = repo
.references_glob("refs/agit/*")
.map(|mut r| r.next().is_some())
.unwrap_or(false);
let refs_dir = agit_dir.join("refs").join("heads");
let has_file_refs = refs_dir.exists()
&& refs_dir
.read_dir()
.map(|mut d| d.next().is_some())
.unwrap_or(false);
let objects_dir = agit_dir.join("objects");
let has_file_objects = objects_dir.exists()
&& objects_dir
.read_dir()
.map(|mut d| d.next().is_some())
.unwrap_or(false);
match (has_git_refs, has_file_refs || has_file_objects) {
(true, false) => StorageVersion::V2GitNative,
(false, true) => StorageVersion::V1FileSystem,
(true, true) => StorageVersion::Mixed,
(false, false) => StorageVersion::V2GitNative, }
}
pub fn migrate_v1_to_v2(agit_dir: &Path, repo: &Repository) -> Result<MigrationResult> {
let file_refs = FileRefStore::new(agit_dir);
let mut hash_map: HashMap<String, String> = HashMap::new();
let mut objects_migrated = 0;
let objects_dir = agit_dir.join("objects");
if objects_dir.exists() {
for prefix_entry in fs::read_dir(&objects_dir)?.flatten() {
if !prefix_entry.file_type()?.is_dir() {
continue;
}
let prefix = prefix_entry.file_name();
for obj_entry in fs::read_dir(prefix_entry.path())?.flatten() {
let content = fs::read(obj_entry.path())?;
let oid = repo.blob(&content)?;
let suffix = obj_entry.file_name();
let old_hash = format!("{}{}", prefix.to_string_lossy(), suffix.to_string_lossy());
hash_map.insert(old_hash, oid.to_string());
objects_migrated += 1;
}
}
}
let mut refs_migrated = 0;
for branch in file_refs.list()? {
if let Some(old_hash) = file_refs.get(&branch)? {
if let Some(new_oid_str) = hash_map.get(&old_hash) {
let ref_name = format!("refs/agit/heads/{}", branch);
let oid = Oid::from_str(new_oid_str)?;
repo.reference(&ref_name, oid, true, &format!("agit migration: {}", branch))?;
refs_migrated += 1;
} else {
if let Ok(oid) = Oid::from_str(&old_hash) {
let ref_name = format!("refs/agit/heads/{}", branch);
repo.reference(&ref_name, oid, true, &format!("agit migration: {}", branch))?;
refs_migrated += 1;
}
}
}
}
Ok(MigrationResult {
objects_migrated,
refs_migrated,
version: StorageVersion::V2GitNative,
})
}
pub fn cleanup_v1_storage(agit_dir: &Path) -> Result<()> {
let objects_dir = agit_dir.join("objects");
if objects_dir.exists() {
fs::remove_dir_all(&objects_dir)?;
}
let refs_dir = agit_dir.join("refs");
if refs_dir.exists() {
fs::remove_dir_all(&refs_dir)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::FileObjectStore;
use tempfile::TempDir;
fn setup() -> (TempDir, Repository) {
let temp = TempDir::new().unwrap();
let repo = Repository::init(temp.path()).unwrap();
(temp, repo)
}
#[test]
fn test_detect_version_empty() {
let (temp, repo) = setup();
let agit_dir = temp.path().join(".agit");
fs::create_dir_all(&agit_dir).unwrap();
let version = detect_version(&agit_dir, &repo);
assert_eq!(version, StorageVersion::V2GitNative);
}
#[test]
fn test_detect_version_v1() {
let (temp, repo) = setup();
let agit_dir = temp.path().join(".agit");
fs::create_dir_all(agit_dir.join("objects").join("ab")).unwrap();
fs::write(agit_dir.join("objects").join("ab").join("cdef"), "content").unwrap();
let version = detect_version(&agit_dir, &repo);
assert_eq!(version, StorageVersion::V1FileSystem);
}
#[test]
fn test_detect_version_v2() {
let (temp, repo) = setup();
let agit_dir = temp.path().join(".agit");
fs::create_dir_all(&agit_dir).unwrap();
let oid = repo.blob(b"test content").unwrap();
repo.reference("refs/agit/heads/main", oid, true, "test")
.unwrap();
let version = detect_version(&agit_dir, &repo);
assert_eq!(version, StorageVersion::V2GitNative);
}
#[test]
fn test_migrate_v1_to_v2() {
let (temp, repo) = setup();
let agit_dir = temp.path().join(".agit");
let objects_dir = agit_dir.join("objects").join("ab");
fs::create_dir_all(&objects_dir).unwrap();
let test_content = b"{\"test\": \"content\"}";
let hash = FileObjectStore::hash_content(test_content);
let (prefix, rest) = hash.split_at(2);
fs::create_dir_all(agit_dir.join("objects").join(prefix)).unwrap();
fs::write(
agit_dir.join("objects").join(prefix).join(rest),
test_content,
)
.unwrap();
let refs_dir = agit_dir.join("refs").join("heads");
fs::create_dir_all(&refs_dir).unwrap();
fs::write(refs_dir.join("main"), &hash).unwrap();
let result = migrate_v1_to_v2(&agit_dir, &repo).unwrap();
assert_eq!(result.objects_migrated, 1);
assert_eq!(result.refs_migrated, 1);
let v2_ref = repo.find_reference("refs/agit/heads/main").unwrap();
assert!(v2_ref.target().is_some());
}
#[test]
fn test_cleanup_v1_storage() {
let (temp, _repo) = setup();
let agit_dir = temp.path().join(".agit");
fs::create_dir_all(agit_dir.join("objects").join("ab")).unwrap();
fs::create_dir_all(agit_dir.join("refs").join("heads")).unwrap();
assert!(agit_dir.join("objects").exists());
assert!(agit_dir.join("refs").exists());
cleanup_v1_storage(&agit_dir).unwrap();
assert!(!agit_dir.join("objects").exists());
assert!(!agit_dir.join("refs").exists());
}
}