about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMel <einebeere@gmail.com>2021-10-01 00:34:16 +0200
committerMel <einebeere@gmail.com>2021-10-01 00:34:16 +0200
commit3f5c5026909af023c8a3885a4a3d97c669302c1b (patch)
tree44c7e3e81d3b71ad6977afe4ac1596033b5834a6
parent8f700551fd3f98c7b9846af647a7318c171152a6 (diff)
downloadka-3f5c5026909af023c8a3885a4a3d97c669302c1b.tar.zst
ka-3f5c5026909af023c8a3885a4a3d97c669302c1b.zip
Global repository cursor
-rw-r--r--cli/src/main.rs9
-rw-r--r--src/actions/create.rs12
-rw-r--r--src/actions/shift.rs67
-rw-r--r--src/actions/update.rs94
-rw-r--r--src/files.rs20
-rw-r--r--src/history.rs112
6 files changed, 214 insertions, 100 deletions
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 3b0203d..751871c 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -6,7 +6,8 @@ fn main() {
     let args: Vec<String> = env::args().collect();
     let command = args[1].as_str();
 
-    let options = ActionOptions::from_pwd().expect("Could not get current path.");
+    let options = ActionOptions::from_path("./repo");
+    //let options = ActionOptions::from_pwd().expect("Could not get current path.");
 
     match command {
         "create" => {
@@ -16,11 +17,9 @@ fn main() {
             update(options).expect("Failed executing Update action.");
         }
         "shift" => {
-            let file_path = args[2].as_str();
+            let new_cursor: usize = args[2].as_str().parse().expect("Invalid cursor.");
 
-            let new_cursor: usize = args[3].as_str().parse().expect("Invalid cursor.");
-
-            shift(options, file_path, new_cursor).expect("Failed executing Shift actions.");
+            shift(options, new_cursor).expect("Failed executing Shift actions.");
         }
         _ => panic!("Unknown command: {}", command),
     }
diff --git a/src/actions/create.rs b/src/actions/create.rs
index f00e95d..472cbe3 100644
--- a/src/actions/create.rs
+++ b/src/actions/create.rs
@@ -1,8 +1,8 @@
-use std::fs;
+use std::fs::{self, File};
 
 use anyhow::Result;
 
-use crate::{actions::update, files::Locations};
+use crate::{actions::update, files::Locations, history::RepositoryHistory};
 
 use super::ActionOptions;
 
@@ -13,8 +13,12 @@ pub fn create(command_options: ActionOptions) -> Result<()> {
         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 empty_history = RepositoryHistory::default();
+    empty_history.write_to_file(&mut index_file)?;
 
     update(command_options)?;
 
diff --git a/src/actions/shift.rs b/src/actions/shift.rs
index 6880c20..0fbc233 100644
--- a/src/actions/shift.rs
+++ b/src/actions/shift.rs
@@ -1,56 +1,63 @@
 use std::{
-    fs,
+    fs::{self, OpenOptions},
     io::{Seek, Write},
-    path::Path,
 };
 
 use anyhow::Result;
 
 use crate::{
-    files::{Locations, FileState},
-    history::FileHistory,
+    files::{FileState, Locations},
+    history::{FileHistory, RepositoryHistory},
 };
 
 use super::ActionOptions;
 
-pub fn shift(command_options: ActionOptions, path: &str, new_cursor: usize) -> Result<()> {
+pub fn shift(command_options: ActionOptions, new_cursor: usize) -> Result<()> {
     let locations = Locations::from(&command_options);
 
-    match FileState::from_working(&locations, Path::new(path))? {
-        FileState::Tracked(tracked) => {
-            let mut history_file = tracked.load_history_file()?;
+    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 mut file_history = FileHistory::from_file(&mut history_file)?;
-            file_history.set_cursor(new_cursor);
+    repository_history.cursor = new_cursor;
+    repository_history.write_to_file(&mut repository_index_file)?;
 
-            if file_history.file_is_deleted() {
-                fs::remove_file(path)?;
-            } else {
-                let new_content = file_history.get_content();
-                let mut working_file = tracked.create_working_file()?;
+    // TOOD: Only shift files which would be affected by cursor change
 
-                working_file.rewind()?;
-                working_file.set_len(0)?;
+    for state in locations.get_repository_files()? {
+        match state {
+            FileState::Tracked(tracked) => {
+                let mut history_file = tracked.load_history_file()?;
 
-                working_file.write_all(&new_content)?;
-            }
+                let file_history = FileHistory::from_file(&mut history_file)?;
 
-            file_history.write_to_file(&mut history_file)?;
-        }
-        FileState::Deleted(deleted) => {
-            let mut history_file = deleted.load_history_file()?;
+                if file_history.is_file_deleted(new_cursor) {
+                    fs::remove_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)?;
+                }
+            }
+            FileState::Deleted(deleted) => {
+                let mut history_file = deleted.load_history_file()?;
 
-            let mut file_history = FileHistory::from_file(&mut history_file)?;
-            file_history.set_cursor(new_cursor);
+                let file_history = FileHistory::from_file(&mut history_file)?;
 
-            if !file_history.file_is_deleted() {
-                let mut new_working_file = deleted.create_working_file(&locations)?;
-                let new_content = file_history.get_content();
+                if !file_history.is_file_deleted(new_cursor) {
+                    let mut new_working_file = deleted.create_working_file(&locations)?;
+                    let new_content = file_history.get_content(new_cursor);
 
-                new_working_file.write_all(&new_content)?;
+                    new_working_file.write_all(&new_content)?;
+                }
             }
+            // TODO: What do we do with untracked files on a shift? Delete them?
+            _ => (),
         }
-        FileState::Untracked(_) => panic!("File is not tracked with Ka."),
     }
 
     Ok(())
diff --git a/src/actions/update.rs b/src/actions/update.rs
index aa9c3a4..a9bc0d2 100644
--- a/src/actions/update.rs
+++ b/src/actions/update.rs
@@ -1,40 +1,78 @@
-use std::io::Read;
+use std::{
+    fs::{File, OpenOptions},
+    io::Read,
+    time::SystemTime,
+};
 
 use anyhow::{Context, Result};
 
 use crate::{
-    files::{Locations, FileState},
-    history::{FileChange, FileHistory},
     diff::ContentChange,
+    files::{FileState, Locations},
+    history::{FileChange, FileChangeVariant, FileHistory, RepositoryChange, RepositoryHistory},
 };
 
 use super::ActionOptions;
 
 pub fn update(command_options: ActionOptions) -> 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 entries = locations
         .get_repository_files()
         .context("Could not traverse files.")?;
 
-    for element in entries {
-        update_file(element, &locations)?;
+    let mut affected_files = Vec::new();
+
+    for state in entries {
+        let changed_file = get_new_history_for_file(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)?;
+            affected_files.push(state.get_working_path(&locations)?);
+        }
+    }
+
+    if affected_files.len() > 0 {
+        repository_history.add_change(RepositoryChange {
+            affected_files,
+            timestamp,
+        });
+        repository_history.cursor += 1;
+
+        repository_history.write_to_file(&mut repository_index_file)?;
     }
 
     Ok(())
 }
 
-fn update_file(file_state: FileState, locations: &Locations) -> Result<()> {
-    let new_state = match file_state {
+fn get_new_history_for_file(
+    cursor: usize,
+    file_state: &FileState,
+    locations: &Locations,
+) -> Result<Option<(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)?;
-            if !file_history.file_is_deleted() {
+            if !file_history.is_file_deleted(cursor) {
                 let mut new_history = file_history;
-                new_history.add_change(FileChange::Deleted);
-                Some((history_file, new_history))
+                new_history.add_change(FileChange {
+                    change_index: cursor + 1,
+                    variant: FileChangeVariant::Deleted,
+                });
+                Ok(Some((history_file, new_history)))
             } else {
-                None
+                Ok(None)
             }
         }
         FileState::Untracked(untracked) => {
@@ -43,15 +81,21 @@ fn update_file(file_state: FileState, locations: &Locations) -> Result<()> {
             let mut file_content = Vec::new();
             file.read_to_end(&mut file_content)?;
 
-            let change = FileChange::Updated(vec![ContentChange::Inserted {
-                at: 0,
-                new_content: file_content,
-            }]);
+            let change = FileChange {
+                change_index: cursor + 1,
+                variant: FileChangeVariant::Updated(vec![ContentChange::Inserted {
+                    at: 0,
+                    new_content: file_content,
+                }]),
+            };
 
             let mut new_history = FileHistory::default();
             new_history.add_change(change);
 
-            Some((untracked.create_history_file(locations)?, new_history))
+            Ok(Some((
+                untracked.create_history_file(locations)?,
+                new_history,
+            )))
         }
         FileState::Tracked(tracked) => {
             let mut history_file = tracked.load_history_file()?;
@@ -62,21 +106,19 @@ fn update_file(file_state: FileState, locations: &Locations) -> Result<()> {
             let mut new_content = Vec::new();
             working_file.read_to_end(&mut new_content)?;
 
-            let old_content = file_history.get_content();
+            let old_content = file_history.get_content(cursor);
 
             let changes = ContentChange::diff(&old_content, &new_content);
 
+            // TODO: Check if file was changed.
+
             let mut new_history = file_history;
-            new_history.add_change(FileChange::Updated(changes));
-            new_history.set_cursor(new_history.cursor() + 1);
+            new_history.add_change(FileChange {
+                change_index: cursor + 1,
+                variant: FileChangeVariant::Updated(changes),
+            });
 
-            Some((history_file, new_history))
+            Ok(Some((history_file, new_history)))
         }
-    };
-
-    if let Some((mut history_file, new_file_history)) = new_state {
-        new_file_history.write_to_file(&mut history_file)?;
     }
-
-    Ok(())
 }
diff --git a/src/files.rs b/src/files.rs
index 41dbad1..a2b1db4 100644
--- a/src/files.rs
+++ b/src/files.rs
@@ -15,6 +15,10 @@ pub struct Locations {
 }
 
 impl Locations {
+    pub fn get_repository_index(&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)
             .context("Failed reading working file entries.")?
@@ -124,10 +128,18 @@ impl FileState {
             })
         })
     }
+
+    pub fn get_working_path(&self, locations: &Locations) -> Result<PathBuf> {
+        match self {
+            FileState::Deleted(deleted) => locations.working_from_history(&deleted.history_path),
+            FileState::Untracked(untracked) => Ok(untracked.path.clone()),
+            FileState::Tracked(tracked) => Ok(tracked.working_path.clone()),
+        }
+    }
 }
 
 pub struct FileDeleted {
-    history_path: PathBuf,
+    pub history_path: PathBuf,
 }
 
 impl FileDeleted {
@@ -146,7 +158,7 @@ impl FileDeleted {
 }
 
 pub struct FileUntracked {
-    path: PathBuf,
+    pub path: PathBuf,
 }
 
 impl FileUntracked {
@@ -168,8 +180,8 @@ impl FileUntracked {
 }
 
 pub struct FileTracked {
-    history_path: PathBuf,
-    working_path: PathBuf,
+    pub history_path: PathBuf,
+    pub working_path: PathBuf,
 }
 
 impl FileTracked {
diff --git a/src/history.rs b/src/history.rs
index 3f5e43f..c474143 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,6 +1,7 @@
 use std::{
     fs::File,
     io::{Read, Seek, Write},
+    path::PathBuf,
 };
 
 use serde::{Deserialize, Serialize};
@@ -10,8 +11,51 @@ use anyhow::{Context, Result};
 use crate::diff::ContentChange;
 
 #[derive(Serialize, Deserialize, Debug)]
+pub struct RepositoryHistory {
+    pub cursor: usize,
+    changes: Vec<RepositoryChange>,
+}
+
+impl RepositoryHistory {
+    pub fn from_file(file: &mut File) -> Result<RepositoryHistory> {
+        let mut buffer = Vec::new();
+        file.read_to_end(&mut buffer)
+            .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<()> {
+        let encoded: Vec<u8> = serde_json::to_vec(self)?;
+        file.rewind()?;
+        file.set_len(0)?;
+        file.write_all(encoded.as_ref())?;
+        Ok(())
+    }
+
+    pub fn add_change(&mut self, change: RepositoryChange) {
+        self.changes.push(change);
+    }
+}
+
+impl Default for RepositoryHistory {
+    fn default() -> Self {
+        Self {
+            cursor: 0,
+            changes: Vec::new(),
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct RepositoryChange {
+    pub affected_files: Vec<PathBuf>,
+    pub timestamp: u64,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
 pub struct FileHistory {
-    cursor: usize,
     changes: Vec<FileChange>,
 }
 
@@ -33,24 +77,30 @@ impl FileHistory {
         Ok(())
     }
 
-    pub fn cursor(&self) -> usize {
-        self.cursor
-    }
-
-    pub fn file_is_deleted(&self) -> bool {
-        match self.changes.last() {
-            Some(change) => match change {
-                FileChange::Deleted => true,
-                FileChange::Updated(_) => false,
+    pub fn is_file_deleted(&self, at_cursor: usize) -> bool {
+        match self
+            .changes
+            .iter()
+            .take_while(|c| c.change_index <= at_cursor)
+            .last()
+        {
+            Some(change) => match change.variant {
+                FileChangeVariant::Deleted => true,
+                FileChangeVariant::Updated(_) => false,
             },
             None => false,
         }
     }
 
-    pub fn get_content(&self) -> Vec<u8> {
+    pub fn get_content(&self, at_cursor: usize) -> Vec<u8> {
         let mut buffer = Vec::new();
-        for file_change in self.changes.iter().take(self.cursor + 1) {
-            if let FileChange::Updated(ref updated) = file_change {
+
+        for file_change in self
+            .changes
+            .iter()
+            .take_while(|change| change.change_index <= at_cursor)
+        {
+            if let FileChangeVariant::Updated(ref updated) = file_change.variant {
                 for change in updated.iter() {
                     change.apply(&mut buffer)
                 }
@@ -61,18 +111,6 @@ impl FileHistory {
         buffer
     }
 
-    pub fn set_cursor(&mut self, new_cursor: usize) {
-        if new_cursor < self.changes.len() {
-            self.cursor = new_cursor;
-        } else {
-            panic!(
-                "Out-of-bounds cursor for file history: {}, can be at most {}",
-                new_cursor,
-                self.changes.len() - 1
-            );
-        }
-    }
-
     pub fn add_change(&mut self, change: FileChange) {
         self.changes.push(change);
     }
@@ -81,14 +119,19 @@ impl FileHistory {
 impl Default for FileHistory {
     fn default() -> Self {
         Self {
-            cursor: 0,
             changes: Vec::new(),
         }
     }
 }
 
 #[derive(Serialize, Deserialize, Debug)]
-pub enum FileChange {
+pub struct FileChange {
+    pub change_index: usize,
+    pub variant: FileChangeVariant,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum FileChangeVariant {
     Updated(Vec<ContentChange>),
     Deleted,
 }
@@ -107,19 +150,26 @@ mod tests {
         ];
 
         let mut history = FileHistory::default();
-        history.add_change(FileChange::Updated(Vec::new()));
+
+        history.add_change(FileChange {
+            change_index: 0,
+            variant: FileChangeVariant::Updated(Vec::new()),
+        });
 
         for old_index in 0..stages.len() - 1 {
             let old = stages[old_index].as_bytes();
             let new = stages[old_index + 1].as_bytes();
 
             let stage_difference = ContentChange::diff(old, new);
-            history.add_change(FileChange::Updated(stage_difference));
+
+            history.add_change(FileChange {
+                change_index: old_index + 1,
+                variant: FileChangeVariant::Updated(stage_difference),
+            });
         }
 
         for index in 0..stages.len() {
-            history.set_cursor(index);
-            assert_eq!(stages[index].as_bytes(), history.get_content());
+            assert_eq!(stages[index].as_bytes(), history.get_content(index));
         }
     }
 }