about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--cli/src/main.rs13
-rw-r--r--src/actions/create.rs25
-rw-r--r--src/actions/shift.rs40
-rw-r--r--src/actions/update.rs53
-rw-r--r--src/files.rs111
-rw-r--r--src/filesystem.rs141
-rw-r--r--src/history.rs32
-rw-r--r--src/lib.rs4
8 files changed, 255 insertions, 164 deletions
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 751871c..be52e60 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -1,6 +1,9 @@
 use std::env;
 
-use ka::actions::{create, shift, update, ActionOptions};
+use ka::{
+    actions::{create, shift, update, ActionOptions},
+    filesystem::FsImpl,
+};
 
 fn main() {
     let args: Vec<String> = env::args().collect();
@@ -9,17 +12,19 @@ fn main() {
     let options = ActionOptions::from_path("./repo");
     //let options = ActionOptions::from_pwd().expect("Could not get current path.");
 
+    let mut filesystem = FsImpl {};
+
     match command {
         "create" => {
-            create(options).expect("Failed executing Create action.");
+            create(options, &mut filesystem).expect("Failed executing Create action.");
         }
         "update" => {
-            update(options).expect("Failed executing Update action.");
+            update(options, &mut filesystem).expect("Failed executing Update action.");
         }
         "shift" => {
             let new_cursor: usize = args[2].as_str().parse().expect("Invalid cursor.");
 
-            shift(options, new_cursor).expect("Failed executing Shift actions.");
+            shift(options, &mut filesystem, new_cursor).expect("Failed executing Shift actions.");
         }
         _ => panic!("Unknown command: {}", command),
     }
diff --git a/src/actions/create.rs b/src/actions/create.rs
index 472cbe3..20dbff7 100644
--- a/src/actions/create.rs
+++ b/src/actions/create.rs
@@ -1,26 +1,23 @@
-use std::fs::{self, File};
-
+use crate::{actions::update, files::Locations, filesystem::Fs, history::RepositoryHistory};
 use anyhow::Result;
 
-use crate::{actions::update, files::Locations, history::RepositoryHistory};
-
 use super::ActionOptions;
 
-pub fn create(command_options: ActionOptions) -> Result<()> {
+pub fn create(command_options: ActionOptions, fs: &mut impl Fs) -> Result<()> {
     let locations = Locations::from(&command_options);
+    // FIXME: Re-add old repository deletion.
+    // if locations.ka_path.exists() {
+    //     fs::remove_dir_all(locations.ka_path.as_path())?;
+    // }
 
-    if locations.ka_path.exists() {
-        fs::remove_dir_all(locations.ka_path.as_path())?;
-    }
-
-    fs::create_dir(&locations.ka_path)?;
-    fs::create_dir(&locations.ka_files_path)?;
+    // fs::create_dir(&locations.ka_path)?;
+    // fs::create_dir(&locations.ka_files_path)?;
 
-    let mut index_file = File::create(locations.get_repository_index())?;
+    let mut index_file = fs.create_file(&locations.get_repository_index_path())?;
     let empty_history = RepositoryHistory::default();
-    empty_history.write_to_file(&mut index_file)?;
+    empty_history.write_to_file(fs, &mut index_file)?;
 
-    update(command_options)?;
+    update(command_options, fs)?;
 
     Ok(())
 }
diff --git a/src/actions/shift.rs b/src/actions/shift.rs
index 94b8914..8f37874 100644
--- a/src/actions/shift.rs
+++ b/src/actions/shift.rs
@@ -1,29 +1,26 @@
-use std::{
-    collections::HashSet,
-    fs::{self, OpenOptions},
-    io::{Seek, Write},
-};
+use std::collections::HashSet;
 
 use anyhow::Result;
 
 use crate::{
     files::{FileState, Locations},
+    filesystem::Fs,
     history::{FileHistory, RepositoryHistory},
 };
 
 use super::ActionOptions;
 
-pub fn shift(command_options: ActionOptions, new_cursor: usize) -> Result<()> {
+pub fn shift(command_options: ActionOptions, fs: &mut impl Fs, new_cursor: usize) -> Result<()> {
     let locations = Locations::from(&command_options);
 
-    let repository_index_path = locations.get_repository_index();
-    let mut repository_index_file = OpenOptions::new().write(true).open(repository_index_path)?;
-    let mut repository_history = RepositoryHistory::from_file(&mut repository_index_file)?;
+    let repository_index_path = locations.get_repository_index_path();
+    let mut repository_index_file = fs.open_writable_file(&repository_index_path)?;
+    let mut repository_history = RepositoryHistory::from_file(fs, &mut repository_index_file)?;
 
     let old_cursor = repository_history.cursor;
 
     repository_history.cursor = new_cursor;
-    repository_history.write_to_file(&mut repository_index_file)?;
+    repository_history.write_to_file(fs, &mut repository_index_file)?;
 
     let changes_between_cursors = if old_cursor < new_cursor {
         old_cursor..new_cursor
@@ -47,32 +44,27 @@ pub fn shift(command_options: ActionOptions, new_cursor: usize) -> Result<()> {
     for state in affected_files_by_shift? {
         match state {
             FileState::Tracked(tracked) => {
-                let mut history_file = tracked.load_history_file()?;
+                let mut history_file = tracked.load_history_file(fs)?;
 
-                let file_history = FileHistory::from_file(&mut history_file)?;
+                let file_history = FileHistory::from_file(fs, &mut history_file)?;
 
                 if file_history.is_file_deleted(new_cursor) {
-                    fs::remove_file(tracked.working_path)?;
+                    fs.delete_file(&tracked.working_path)?;
                 } else {
                     let new_content = file_history.get_content(new_cursor);
-                    let mut working_file = tracked.create_working_file()?;
-
-                    working_file.rewind()?;
-                    working_file.set_len(0)?;
-
-                    working_file.write_all(&new_content)?;
+                    let mut working_file = tracked.create_working_file(fs)?;
+                    fs.write_to_file(&mut working_file, new_content)?;
                 }
             }
             FileState::Deleted(deleted) => {
-                let mut history_file = deleted.load_history_file()?;
+                let mut history_file = deleted.load_history_file(fs)?;
 
-                let file_history = FileHistory::from_file(&mut history_file)?;
+                let file_history = FileHistory::from_file(fs, &mut history_file)?;
 
                 if !file_history.is_file_deleted(new_cursor) {
-                    let mut new_working_file = deleted.create_working_file(&locations)?;
+                    let mut new_working_file = deleted.create_working_file(fs, &locations)?;
                     let new_content = file_history.get_content(new_cursor);
-
-                    new_working_file.write_all(&new_content)?;
+                    fs.write_to_file(&mut new_working_file, new_content)?;
                 }
             }
             // TODO: What do we do with untracked files on a shift? Delete them?
diff --git a/src/actions/update.rs b/src/actions/update.rs
index a9bc0d2..8201862 100644
--- a/src/actions/update.rs
+++ b/src/actions/update.rs
@@ -1,43 +1,38 @@
-use std::{
-    fs::{File, OpenOptions},
-    io::Read,
-    time::SystemTime,
-};
+use std::time::SystemTime;
 
 use anyhow::{Context, Result};
 
 use crate::{
     diff::ContentChange,
     files::{FileState, Locations},
+    filesystem::Fs,
     history::{FileChange, FileChangeVariant, FileHistory, RepositoryChange, RepositoryHistory},
 };
 
 use super::ActionOptions;
 
-pub fn update(command_options: ActionOptions) -> Result<()> {
+pub fn update(command_options: ActionOptions, fs: &mut impl Fs) -> Result<()> {
     let timestamp = SystemTime::now()
         .duration_since(SystemTime::UNIX_EPOCH)?
         .as_secs();
 
     let locations = Locations::from(&command_options);
 
-    let repository_index_path = locations.get_repository_index();
-    let mut repository_index_file = OpenOptions::new()
-        .read(true)
-        .write(true)
-        .open(repository_index_path)?;
-    let mut repository_history = RepositoryHistory::from_file(&mut repository_index_file)?;
+    let repository_index_path = locations.get_repository_index_path();
+    let mut repository_index_file = fs.open_writable_file(&repository_index_path)?;
+    let mut repository_history = RepositoryHistory::from_file(fs, &mut repository_index_file)?;
 
     let entries = locations
-        .get_repository_files()
+        .get_repository_files(fs)
         .context("Could not traverse files.")?;
 
     let mut affected_files = Vec::new();
 
     for state in entries {
-        let changed_file = get_new_history_for_file(repository_history.cursor, &state, &locations)?;
+        let changed_file =
+            get_new_history_for_file(fs, repository_history.cursor, &state, &locations)?;
         if let Some((mut history_file, new_file_history)) = changed_file {
-            new_file_history.write_to_file(&mut history_file)?;
+            new_file_history.write_to_file(fs, &mut history_file)?;
             affected_files.push(state.get_working_path(&locations)?);
         }
     }
@@ -49,21 +44,22 @@ pub fn update(command_options: ActionOptions) -> Result<()> {
         });
         repository_history.cursor += 1;
 
-        repository_history.write_to_file(&mut repository_index_file)?;
+        repository_history.write_to_file(fs, &mut repository_index_file)?;
     }
 
     Ok(())
 }
 
-fn get_new_history_for_file(
+fn get_new_history_for_file<FS: Fs>(
+    fs: &mut FS,
     cursor: usize,
     file_state: &FileState,
     locations: &Locations,
-) -> Result<Option<(File, FileHistory)>> {
+) -> Result<Option<(FS::File, FileHistory)>> {
     match file_state {
         FileState::Deleted(deleted) => {
-            let mut history_file = deleted.load_history_file()?;
-            let file_history = FileHistory::from_file(&mut history_file)?;
+            let mut history_file = deleted.load_history_file(fs)?;
+            let file_history = FileHistory::from_file(fs, &mut history_file)?;
             if !file_history.is_file_deleted(cursor) {
                 let mut new_history = file_history;
                 new_history.add_change(FileChange {
@@ -76,10 +72,9 @@ fn get_new_history_for_file(
             }
         }
         FileState::Untracked(untracked) => {
-            let mut file = untracked.load_file()?;
+            let mut file = untracked.load_file(fs)?;
 
-            let mut file_content = Vec::new();
-            file.read_to_end(&mut file_content)?;
+            let file_content = fs.read_from_file(&mut file)?;
 
             let change = FileChange {
                 change_index: cursor + 1,
@@ -93,19 +88,17 @@ fn get_new_history_for_file(
             new_history.add_change(change);
 
             Ok(Some((
-                untracked.create_history_file(locations)?,
+                untracked.create_history_file(fs, locations)?,
                 new_history,
             )))
         }
         FileState::Tracked(tracked) => {
-            let mut history_file = tracked.load_history_file()?;
-            let mut working_file = tracked.load_working_file()?;
-
-            let file_history = FileHistory::from_file(&mut history_file)?;
+            let mut history_file = tracked.load_history_file(fs)?;
+            let mut working_file = tracked.load_working_file(fs)?;
 
-            let mut new_content = Vec::new();
-            working_file.read_to_end(&mut new_content)?;
+            let file_history = FileHistory::from_file(fs, &mut history_file)?;
 
+            let new_content = fs.read_from_file(&mut working_file)?;
             let old_content = file_history.get_content(cursor);
 
             let changes = ContentChange::diff(&old_content, &new_content);
diff --git a/src/files.rs b/src/files.rs
index a2b1db4..aceb684 100644
--- a/src/files.rs
+++ b/src/files.rs
@@ -1,12 +1,11 @@
-use std::{
-    fs::{self, DirEntry, File, OpenOptions},
-    io,
-    path::{Path, PathBuf},
-};
+use std::path::{Path, PathBuf};
 
 use anyhow::{Context, Error, Result};
 
-use crate::actions::ActionOptions;
+use crate::{
+    actions::ActionOptions,
+    filesystem::{Fs, FsEntry},
+};
 
 pub struct Locations {
     pub repository_path: PathBuf,
@@ -15,25 +14,26 @@ pub struct Locations {
 }
 
 impl Locations {
-    pub fn get_repository_index(&self) -> PathBuf {
-        return self.ka_path.join("index")
+    pub fn get_repository_index_path(&self) -> PathBuf {
+        return self.ka_path.join("index");
     }
 
-    pub fn get_repository_files(&self) -> Result<Vec<FileState>, Error> {
-        let working_entries = fs::read_dir(&self.repository_path)
+    pub fn get_repository_files<FS: Fs>(&self, fs: &mut FS) -> Result<Vec<FileState>, Error> {
+        let working_entries = fs
+            .read_directory(&self.repository_path)
             .context("Failed reading working file entries.")?
-            .filter(|res| {
-                res.as_ref()
-                    .map_or(true, |entry| entry.path() != self.ka_path)
-            });
-        let history_entries =
-            fs::read_dir(&self.ka_files_path).context("Failed reading history file entries.")?;
-
-        let working_files = Self::walk_directory(working_entries, &|entry| {
+            .into_iter()
+            .filter(|e| e.path() == self.ka_path)
+            .collect();
+        let history_entries = fs
+            .read_directory(&self.ka_files_path)
+            .context("Failed reading history file entries.")?;
+
+        let working_files = Self::walk_directory(fs, working_entries, &|entry| {
             FileState::from_working(self, &entry.path()).ok()
         })?;
 
-        let deleted_files = Self::walk_directory(history_entries, &|entry| {
+        let deleted_files = Self::walk_directory(fs, history_entries, &|entry| {
             let file_path = entry.path();
             let file = FileState::from_history(self, &file_path).ok()?;
             match file {
@@ -59,20 +59,19 @@ impl Locations {
         Ok(self.ka_files_path.join(raw_path))
     }
 
-    fn walk_directory(
-        directory: impl Iterator<Item = io::Result<DirEntry>>,
-        filter_map: &dyn Fn(DirEntry) -> Option<FileState>,
+    fn walk_directory<FS: Fs>(
+        fs: &mut FS,
+        directory: Vec<FS::Entry>,
+        filter_map: &dyn Fn(&FS::Entry) -> Option<FileState>,
     ) -> Result<Vec<FileState>> {
         let mut entries = Vec::new();
 
-        for res in directory {
-            let entry = res?;
-            let file_type = entry.file_type()?;
-            if file_type.is_dir() {
-                let nested_directory = fs::read_dir(entry.path())?;
-                let nested_files = Self::walk_directory(nested_directory, filter_map)?;
+        for entry in directory {
+            if entry.is_directory()? {
+                let nested_directory = fs.read_directory(&entry.path())?;
+                let nested_files = Self::walk_directory(fs, nested_directory, filter_map)?;
                 entries.extend(nested_files);
-            } else if let Some(states) = filter_map(entry) {
+            } else if let Some(states) = filter_map(&entry) {
                 entries.push(states);
             }
         }
@@ -117,6 +116,8 @@ impl FileState {
 
     pub fn from_working(locations: &Locations, working_file_path: &Path) -> Result<Self> {
         let history_path = locations.history_from_working(working_file_path)?;
+        // FIXME: Path::exists wouldn't work with Fs abstraction.
+        // TODO: Think whether abstracting Path would be needed for Fs abstraction.
         Ok(if !history_path.exists() {
             FileState::Untracked(FileUntracked {
                 path: working_file_path.to_path_buf(),
@@ -143,17 +144,17 @@ pub struct FileDeleted {
 }
 
 impl FileDeleted {
-    pub fn load_history_file(&self) -> io::Result<File> {
-        OpenOptions::new()
-            .write(true)
-            .read(true)
-            .open(&self.history_path)
+    pub fn load_history_file<FS: Fs>(&self, fs: &mut FS) -> Result<FS::File> {
+        fs.open_writable_file(&self.history_path)
     }
 
-    pub fn create_working_file(&self, locations: &Locations) -> Result<File> {
-        Ok(File::create(
-            locations.working_from_history(&self.history_path)?,
-        )?)
+    pub fn create_working_file<FS: Fs>(
+        &self,
+        fs: &mut FS,
+        locations: &Locations,
+    ) -> Result<FS::File> {
+        let working_path = locations.working_from_history(&self.history_path)?;
+        fs.create_file(&working_path)
     }
 }
 
@@ -162,20 +163,17 @@ pub struct FileUntracked {
 }
 
 impl FileUntracked {
-    pub fn load_file(&self) -> io::Result<File> {
-        OpenOptions::new().read(true).open(&self.path)
+    pub fn load_file<FS: Fs>(&self, fs: &mut FS) -> Result<FS::File> {
+        fs.open_readable_file(&self.path)
     }
 
-    pub fn create_history_file(&self, locations: &Locations) -> Result<File> {
+    pub fn create_history_file<FS: Fs>(
+        &self,
+        fs: &mut FS,
+        locations: &Locations,
+    ) -> Result<FS::File> {
         let history_path = locations.history_from_working(&self.path)?;
-
-        if let Some(parent_path) = history_path.parent() {
-            if !parent_path.exists() {
-                fs::create_dir_all(parent_path)?;
-            }
-        }
-
-        Ok(File::create(history_path)?)
+        Ok(fs.create_file(&history_path)?)
     }
 }
 
@@ -185,18 +183,15 @@ pub struct FileTracked {
 }
 
 impl FileTracked {
-    pub fn load_history_file(&self) -> io::Result<File> {
-        OpenOptions::new()
-            .write(true)
-            .read(true)
-            .open(&self.history_path)
+    pub fn load_history_file<FS: Fs>(&self, fs: &mut FS) -> Result<FS::File> {
+        fs.open_writable_file(&self.history_path)
     }
 
-    pub fn load_working_file(&self) -> io::Result<File> {
-        File::open(&self.working_path)
+    pub fn load_working_file<FS: Fs>(&self, fs: &mut FS) -> Result<FS::File> {
+        fs.open_readable_file(&self.working_path)
     }
 
-    pub fn create_working_file(&self) -> io::Result<File> {
-        File::create(&self.working_path)
+    pub fn create_working_file<FS: Fs>(&self, fs: &mut FS) -> Result<FS::File> {
+        fs.create_file(&self.working_path)
     }
 }
diff --git a/src/filesystem.rs b/src/filesystem.rs
index ab5bf66..d8c96a1 100644
--- a/src/filesystem.rs
+++ b/src/filesystem.rs
@@ -1,27 +1,45 @@
 use anyhow::{Context, Result};
 use std::{
-    fs::{File, OpenOptions},
-    io::{Read, Seek, Write},
-    path::Path,
+    fs::{self, DirEntry, File, OpenOptions},
+    io::{self, Read, Seek, Write},
+    path::{Path, PathBuf},
 };
 
 pub trait Fs {
     type File;
+    type Entry: FsEntry;
 
     fn create_file(&mut self, path: &Path) -> Result<Self::File>;
+    fn delete_file(&mut self, path: &Path) -> Result<()>;
     fn open_readable_file(&mut self, path: &Path) -> Result<Self::File>;
     fn open_writable_file(&mut self, path: &Path) -> Result<Self::File>;
 
+    fn read_directory(&mut self, path: &Path) -> Result<Vec<Self::Entry>>;
+
     fn write_to_file(&mut self, file: &mut Self::File, buffer: Vec<u8>) -> Result<()>;
     fn read_from_file(&mut self, file: &mut Self::File) -> Result<Vec<u8>>;
+
+    fn path_exists(&mut self, path: &Path) -> bool;
+}
+
+pub trait FsEntry {
+    fn path(&self) -> PathBuf;
+    fn is_directory(&self) -> Result<bool>;
 }
 
 pub struct FsImpl {}
 
 impl Fs for FsImpl {
     type File = File;
+    type Entry = DirEntry;
 
     fn create_file(&mut self, path: &Path) -> Result<Self::File> {
+        if let Some(parent_path) = path.parent() {
+            if !parent_path.exists() {
+                fs::create_dir_all(parent_path)?;
+            }
+        }
+
         OpenOptions::new()
             .create(true)
             .read(true)
@@ -30,6 +48,11 @@ impl Fs for FsImpl {
             .with_context(|| format!("Failed creating '{}'.", path.display()))
     }
 
+    fn delete_file(&mut self, path: &Path) -> Result<()> {
+        fs::remove_file(path)?;
+        Ok(())
+    }
+
     fn open_readable_file(&mut self, path: &Path) -> Result<Self::File> {
         File::open(path)
             .with_context(|| format!("Failed opening '{}' for reading.", path.display()))
@@ -48,6 +71,11 @@ impl Fs for FsImpl {
             })
     }
 
+    fn read_directory(&mut self, path: &Path) -> Result<Vec<Self::Entry>> {
+        let result: io::Result<_> = fs::read_dir(path)?.collect();
+        result.with_context(|| format!("Failed reading directory {}", path.display()))
+    }
+
     fn write_to_file(&mut self, file: &mut Self::File, buffer: Vec<u8>) -> Result<()> {
         file.rewind()?;
         file.set_len(0)?;
@@ -60,14 +88,31 @@ impl Fs for FsImpl {
         file.read_to_end(&mut buffer)?;
         Ok(buffer)
     }
+
+    fn path_exists(&mut self, path: &Path) -> bool {
+        path.exists()
+    }
 }
 
+impl FsEntry for DirEntry {
+    fn path(&self) -> PathBuf {
+        self.path()
+    }
+
+    fn is_directory(&self) -> Result<bool> {
+        let file_type = self.file_type()?;
+        Ok(file_type.is_dir())
+    }
+}
+
+// TODO: This will be used for tests. Write them.
+#[allow(dead_code)]
 #[cfg(test)]
 pub mod mock {
-    use std::path::{Path, PathBuf};
     use anyhow::{Error, Result};
+    use std::path::{Path, PathBuf};
 
-    use super::Fs;
+    use super::{Fs, FsEntry};
 
     pub struct FsMock {
         expected_calls: Vec<ExpectedCall>,
@@ -85,10 +130,11 @@ pub mod mock {
 
     impl Fs for FsMock {
         type File = FileMock;
+        type Entry = EntryMock;
 
         fn create_file(&mut self, path: &Path) -> Result<Self::File> {
             let call = ReceivedCall {
-                affected_file: path.to_path_buf(),
+                affected_path: path.to_path_buf(),
                 variant: ReceivedCallVariant::FileCreated,
             };
 
@@ -100,9 +146,20 @@ pub mod mock {
             })
         }
 
+        fn delete_file(&mut self, path: &Path) -> Result<()> {
+            let call = ReceivedCall {
+                affected_path: path.to_path_buf(),
+                variant: ReceivedCallVariant::FileCreated,
+            };
+
+            self.received_calls.push(call);
+
+            Ok(())
+        }
+
         fn open_readable_file(&mut self, path: &Path) -> Result<Self::File> {
             let call = ReceivedCall {
-                affected_file: path.to_path_buf(),
+                affected_path: path.to_path_buf(),
                 variant: ReceivedCallVariant::ReadableFileOpened,
             };
 
@@ -116,7 +173,7 @@ pub mod mock {
 
         fn open_writable_file(&mut self, path: &Path) -> Result<Self::File> {
             let call = ReceivedCall {
-                affected_file: path.to_path_buf(),
+                affected_path: path.to_path_buf(),
                 variant: ReceivedCallVariant::WritableFileOpened,
             };
 
@@ -128,9 +185,28 @@ pub mod mock {
             })
         }
 
+        fn read_directory(&mut self, path: &Path) -> Result<Vec<Self::Entry>> {
+            let call = ReceivedCall {
+                affected_path: path.to_path_buf(),
+                variant: ReceivedCallVariant::DirectoryRead,
+            };
+
+            self.received_calls.push(call);
+
+            let expected_call_option = self.expected_calls.get(self.received_calls.len() - 1);
+
+            if let Some(expected_call) = expected_call_option {
+                if let ExpectedCallVariant::ReadDirectory { entries } = &expected_call.variant {
+                    return Ok(entries.to_vec());
+                }
+            }
+
+            Err(Error::msg("No content to read."))
+        }
+
         fn write_to_file(&mut self, file: &mut Self::File, buffer: Vec<u8>) -> Result<()> {
             let call = ReceivedCall {
-                affected_file: file.path.clone(),
+                affected_path: file.path.clone(),
                 variant: ReceivedCallVariant::FileWritten {
                     written_content: buffer,
                 },
@@ -145,7 +221,7 @@ pub mod mock {
 
         fn read_from_file(&mut self, file: &mut Self::File) -> Result<Vec<u8>> {
             let call = ReceivedCall {
-                affected_file: file.path.clone(),
+                affected_path: file.path.clone(),
                 variant: ReceivedCallVariant::FileRead,
             };
 
@@ -161,6 +237,41 @@ pub mod mock {
 
             Err(Error::msg("No content to read."))
         }
+
+        fn path_exists(&mut self, path: &Path) -> bool {
+            let call = ReceivedCall {
+                affected_path: path.to_path_buf(),
+                variant: ReceivedCallVariant::DoesPathExist,
+            };
+
+            self.received_calls.push(call);
+
+            let expected_call_option = self.expected_calls.get(self.received_calls.len() - 1);
+
+            if let Some(expected_call) = expected_call_option {
+                if let ExpectedCallVariant::PathExists(answer) = &expected_call.variant {
+                    return *answer;
+                }
+            }
+
+            panic!("No mocked return value given for call.");
+        }
+    }
+
+    #[derive(Clone)]
+    pub struct EntryMock {
+        path: PathBuf,
+        is_directory: bool,
+    }
+
+    impl FsEntry for EntryMock {
+        fn path(&self) -> PathBuf {
+            self.path.clone()
+        }
+
+        fn is_directory(&self) -> Result<bool> {
+            Ok(self.is_directory)
+        }
     }
 
     pub struct FileMock {
@@ -170,28 +281,34 @@ pub mod mock {
 
     // TODO: Do we need ways to return errors?
     pub struct ExpectedCall {
-        pub affected_file: PathBuf,
+        pub affected_path: PathBuf,
         pub variant: ExpectedCallVariant,
     }
 
     pub enum ExpectedCallVariant {
         CreateFile,
+        DeleteFile,
         OpenReadableFile,
         OpenWritableFile,
+        ReadDirectory { entries: Vec<EntryMock> },
         ReadFile { read_content: Vec<u8> },
         WriteToFile,
+        PathExists(bool),
     }
 
     pub struct ReceivedCall {
-        pub affected_file: PathBuf,
+        pub affected_path: PathBuf,
         pub variant: ReceivedCallVariant,
     }
 
     pub enum ReceivedCallVariant {
         FileCreated,
+        FileDeleted,
         ReadableFileOpened,
         WritableFileOpened,
+        DirectoryRead,
         FileRead,
         FileWritten { written_content: Vec<u8> },
+        DoesPathExist,
     }
 }
diff --git a/src/history.rs b/src/history.rs
index 72640b6..4f23790 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,14 +1,10 @@
-use std::{
-    fs::File,
-    io::{Read, Seek, Write},
-    path::PathBuf,
-};
+use std::path::PathBuf;
 
 use serde::{Deserialize, Serialize};
 
 use anyhow::{Context, Result};
 
-use crate::diff::ContentChange;
+use crate::{diff::ContentChange, filesystem::Fs};
 
 #[derive(Serialize, Deserialize, Debug)]
 pub struct RepositoryHistory {
@@ -17,20 +13,18 @@ pub struct RepositoryHistory {
 }
 
 impl RepositoryHistory {
-    pub fn from_file(file: &mut File) -> Result<RepositoryHistory> {
-        let mut buffer = Vec::new();
-        file.read_to_end(&mut buffer)
+    pub fn from_file<FS: Fs>(fs: &mut FS, file: &mut FS::File) -> Result<RepositoryHistory> {
+        let buffer = fs
+            .read_from_file(file)
             .context("Failed reading repository history.")?;
 
         let repository_history = serde_json::from_slice::<RepositoryHistory>(&buffer);
         repository_history.context("Corrupted repository history.")
     }
 
-    pub fn write_to_file(&self, file: &mut File) -> anyhow::Result<()> {
+    pub fn write_to_file<FS: Fs>(&self, fs: &mut FS, file: &mut FS::File) -> anyhow::Result<()> {
         let encoded: Vec<u8> = serde_json::to_vec(self)?;
-        file.rewind()?;
-        file.set_len(0)?;
-        file.write_all(encoded.as_ref())?;
+        fs.write_to_file(file, encoded)?;
         Ok(())
     }
 
@@ -64,20 +58,18 @@ pub struct FileHistory {
 }
 
 impl FileHistory {
-    pub fn from_file(file: &mut File) -> Result<FileHistory> {
-        let mut buffer = Vec::new();
-        file.read_to_end(&mut buffer)
+    pub fn from_file<FS: Fs>(fs: &mut FS, file: &mut FS::File) -> Result<FileHistory> {
+        let buffer = fs
+            .read_from_file(file)
             .context("Failed reading file history.")?;
 
         let file_history = serde_json::from_slice::<FileHistory>(&buffer);
         file_history.context("Corrupted file history.")
     }
 
-    pub fn write_to_file(&self, file: &mut File) -> anyhow::Result<()> {
+    pub fn write_to_file<FS: Fs>(&self, fs: &mut FS, file: &mut FS::File) -> Result<()> {
         let encoded: Vec<u8> = serde_json::to_vec(self)?;
-        file.rewind()?;
-        file.set_len(0)?;
-        file.write_all(encoded.as_ref())?;
+        fs.write_to_file(file, encoded)?;
         Ok(())
     }
 
diff --git a/src/lib.rs b/src/lib.rs
index 3c489c5..34937ba 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,6 +1,6 @@
 pub mod actions;
+pub mod filesystem;
 
-mod history;
 mod diff;
 mod files;
-mod filesystem;
+mod history;