use crate::config::Config;
use crate::metalog::{MetaEvent, MetaLog};
use crate::shadow::ShadowFile;
use crate::utils;
use crate::wal::{WalOperation, WriteAheadLog};
use anyhow::{anyhow, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq)]
pub enum FileStatus {
Tracked,
Modified,
Untracked,
}
pub struct FileStatusInfo {
pub path: PathBuf,
pub status: FileStatus,
}
pub struct Repository {
root: PathBuf,
wal: WriteAheadLog,
}
impl Repository {
pub fn init(path: &Path) -> Result<Self> {
let root = path.canonicalize().context("Failed to canonicalize path")?;
let wlk_dir = root.join(".wlk");
if !wlk_dir.exists() {
fs::create_dir_all(&wlk_dir).context("Failed to create .wlk directory")?;
}
let config_path = wlk_dir.join("config");
if !config_path.exists() {
let default_config = Config::default();
default_config
.save(&wlk_dir)
.context("Failed to create config file")?;
}
let mut wal = WriteAheadLog::new(&wlk_dir)?;
wal.recover()?;
let mut metalog = MetaLog::new(&root.to_string_lossy());
metalog.record_event(MetaEvent::DirectoryCreated);
metalog.save(&MetaLog::metalog_path(&root)?)?;
Ok(Repository { root, wal })
}
pub fn open(path: &Path) -> Result<Self> {
let root = path.canonicalize().context("Failed to canonicalize path")?;
let wlk_dir = root.join(".wlk");
if !wlk_dir.exists() {
return Err(anyhow!(
"Not a wlk repository (no .wlk directory found at {:?})",
root
));
}
let mut wal = WriteAheadLog::new(&wlk_dir)?;
wal.recover()?;
Ok(Repository { root, wal })
}
pub fn track_file(&mut self, file_path: &Path) -> Result<()> {
let absolute_path = if file_path.is_absolute() {
file_path.to_path_buf()
} else {
self.root.join(file_path)
};
if !absolute_path.exists() {
return Err(anyhow!("File does not exist: {:?}", absolute_path));
}
if !absolute_path.is_file() {
return Err(anyhow!("Path is not a file: {:?}", absolute_path));
}
let content = fs::read_to_string(&absolute_path).context("Failed to read file content")?;
let filename = absolute_path
.file_name()
.ok_or_else(|| anyhow!("Invalid filename"))?
.to_string_lossy()
.to_string();
let shadow = ShadowFile::new(&filename, content);
let shadow_path = ShadowFile::shadow_path(&absolute_path)?;
if let Some(parent) = shadow_path.parent() {
fs::create_dir_all(parent).context("Failed to create .wlk directory")?;
}
self.wal.begin()?;
let shadow_json =
serde_json::to_string_pretty(&shadow).context("Failed to serialize shadow file")?;
self.wal.append(WalOperation::WriteShadowFile {
path: shadow_path.clone(),
content: shadow_json,
})?;
let dir_path = absolute_path
.parent()
.ok_or_else(|| anyhow!("File has no parent directory"))?;
let mut metalog = MetaLog::get_or_create(dir_path)?;
metalog.record_event(MetaEvent::FileCreated { file: filename });
let metalog_path = MetaLog::metalog_path(dir_path)?;
let metalog_json =
serde_json::to_string_pretty(&metalog).context("Failed to serialize metalog")?;
self.wal.append(WalOperation::WriteMetaLog {
path: metalog_path,
content: metalog_json,
})?;
self.wal.commit()?;
Ok(())
}
pub fn untrack_file(&mut self, file_path: &Path) -> Result<()> {
let absolute_path = if file_path.is_absolute() {
file_path.to_path_buf()
} else {
self.root.join(file_path)
};
let shadow_path = ShadowFile::shadow_path(&absolute_path)?;
if !shadow_path.exists() {
return Err(anyhow!("File is not tracked: {:?}", absolute_path));
}
self.wal.begin()?;
self.wal
.append(WalOperation::DeleteFile { path: shadow_path })?;
let dir_path = absolute_path
.parent()
.ok_or_else(|| anyhow!("File has no parent directory"))?;
let filename = absolute_path
.file_name()
.ok_or_else(|| anyhow!("Invalid filename"))?
.to_string_lossy()
.to_string();
let mut metalog = MetaLog::get_or_create(dir_path)?;
metalog.record_event(MetaEvent::FileDeleted { file: filename });
let metalog_path = MetaLog::metalog_path(dir_path)?;
let metalog_json =
serde_json::to_string_pretty(&metalog).context("Failed to serialize metalog")?;
self.wal.append(WalOperation::WriteMetaLog {
path: metalog_path,
content: metalog_json,
})?;
self.wal.commit()?;
Ok(())
}
pub fn is_tracked(&self, file_path: &Path) -> bool {
let absolute_path = if file_path.is_absolute() {
file_path.to_path_buf()
} else {
self.root.join(file_path)
};
if let Ok(shadow_path) = ShadowFile::shadow_path(&absolute_path) {
shadow_path.exists()
} else {
false
}
}
pub fn list_tracked_files(&self) -> Result<Vec<PathBuf>> {
let mut tracked_files = vec![];
self.walk_directory(&self.root, &mut tracked_files)?;
Ok(tracked_files)
}
fn walk_directory(&self, dir: &Path, tracked_files: &mut Vec<PathBuf>) -> Result<()> {
for entry in fs::read_dir(dir).context("Failed to read directory")? {
let entry = entry.context("Failed to read directory entry")?;
let path = entry.path();
if path.is_dir() {
if path.file_name() != Some(std::ffi::OsStr::new(".wlk")) {
self.walk_directory(&path, tracked_files)?;
}
} else if path.is_file() && self.is_tracked(&path) {
tracked_files.push(path);
}
}
Ok(())
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn file_has_changes(&self, file_path: &Path) -> Result<bool> {
let absolute_path = if file_path.is_absolute() {
file_path.to_path_buf()
} else {
self.root.join(file_path)
};
if !absolute_path.exists() {
return Ok(true); }
let shadow_path = ShadowFile::shadow_path(&absolute_path)?;
if !shadow_path.exists() {
return Ok(false); }
let shadow = ShadowFile::load(&shadow_path)?;
let current_content = fs::read_to_string(&absolute_path).context("Failed to read file")?;
let head_content = shadow.current_content()?;
Ok(utils::hash_file_content(¤t_content) != utils::hash_file_content(&head_content))
}
pub fn get_status(&self) -> Result<Vec<FileStatusInfo>> {
let mut status_list = vec![];
let tracked_files = self.list_tracked_files()?;
for file in &tracked_files {
let has_changes = self.file_has_changes(file)?;
let status = if has_changes {
FileStatus::Modified
} else {
FileStatus::Tracked
};
status_list.push(FileStatusInfo {
path: file.clone(),
status,
});
}
let untracked = self.find_untracked_files()?;
for file in untracked {
status_list.push(FileStatusInfo {
path: file,
status: FileStatus::Untracked,
});
}
Ok(status_list)
}
fn find_untracked_files(&self) -> Result<Vec<PathBuf>> {
let mut untracked = vec![];
self.find_untracked_recursive(&self.root, &mut untracked)?;
Ok(untracked)
}
fn find_untracked_recursive(&self, dir: &Path, untracked: &mut Vec<PathBuf>) -> Result<()> {
for entry in fs::read_dir(dir).context("Failed to read directory")? {
let entry = entry.context("Failed to read directory entry")?;
let path = entry.path();
if path.is_dir() {
if path.file_name() != Some(std::ffi::OsStr::new(".wlk")) {
self.find_untracked_recursive(&path, untracked)?;
}
} else if path.is_file() && !self.is_tracked(&path) {
untracked.push(path);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_init_repository() {
let temp_dir = TempDir::new().unwrap();
let _repo = Repository::init(temp_dir.path()).unwrap();
assert!(temp_dir.path().join(".wlk").exists());
assert!(temp_dir.path().join(".wlk/config").exists());
assert!(temp_dir.path().join(".wlk/metalog").exists());
}
#[test]
fn test_track_file() {
let temp_dir = TempDir::new().unwrap();
let mut repo = Repository::init(temp_dir.path()).unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "initial content").unwrap();
repo.track_file(&test_file).unwrap();
assert!(repo.is_tracked(&test_file));
let shadow_path = ShadowFile::shadow_path(&test_file).unwrap();
assert!(shadow_path.exists());
}
#[test]
fn test_untrack_file() {
let temp_dir = TempDir::new().unwrap();
let mut repo = Repository::init(temp_dir.path()).unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "content").unwrap();
repo.track_file(&test_file).unwrap();
assert!(repo.is_tracked(&test_file));
repo.untrack_file(&test_file).unwrap();
assert!(!repo.is_tracked(&test_file));
}
#[test]
fn test_list_tracked_files() {
let temp_dir = TempDir::new().unwrap();
let mut repo = Repository::init(temp_dir.path()).unwrap();
let file1 = temp_dir.path().join("file1.txt");
let file2 = temp_dir.path().join("file2.txt");
fs::write(&file1, "content 1").unwrap();
fs::write(&file2, "content 2").unwrap();
repo.track_file(&file1).unwrap();
repo.track_file(&file2).unwrap();
let tracked = repo.list_tracked_files().unwrap();
assert_eq!(tracked.len(), 2);
}
}