diff options
| author | Mel <einebeere@gmail.com> | 2021-09-20 22:25:55 +0200 |
|---|---|---|
| committer | Mel <einebeere@gmail.com> | 2021-09-20 22:25:55 +0200 |
| commit | c25d22681f9bc6ddba512895182af0ce0252731b (patch) | |
| tree | eadcdec7d0b4d26d59e77169bd89b8db13e22fb6 | |
| parent | 8bcbf51cf4f7924600eae39db67ff653b48ad95d (diff) | |
| download | ka-c25d22681f9bc6ddba512895182af0ce0252731b.tar.zst ka-c25d22681f9bc6ddba512895182af0ce0252731b.zip | |
Split out common logic
| -rw-r--r-- | cli/src/main.rs | 11 | ||||
| -rw-r--r-- | src/actions/create.rs | 20 | ||||
| -rw-r--r-- | src/actions/mod.rs | 30 | ||||
| -rw-r--r-- | src/actions/shift.rs | 55 | ||||
| -rw-r--r-- | src/actions/update.rs | 80 | ||||
| -rw-r--r-- | src/files.rs | 182 | ||||
| -rw-r--r-- | src/history.rs | 89 | ||||
| -rw-r--r-- | src/lib.rs | 260 | ||||
| -rw-r--r-- | src/text_diff.rs | 52 |
9 files changed, 519 insertions, 260 deletions
diff --git a/cli/src/main.rs b/cli/src/main.rs index 945da3a..3105f9a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,23 +1,26 @@ use std::env; -use ka::{create_command, shift_command, update_command}; +use ka::actions::{create, shift, update, ActionOptions}; 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."); + match command { "create" => { - create_command().unwrap(); + create(options).unwrap(); } "update" => { - update_command().unwrap(); + update(options).unwrap(); } "shift" => { let file_path = args[2].as_str(); let new_cursor: usize = args[3].as_str().parse().expect("Invalid cursor."); - shift_command(file_path, new_cursor); + shift(options, file_path, new_cursor).unwrap(); } _ => println!("Unknown command: {}", command), } diff --git a/src/actions/create.rs b/src/actions/create.rs new file mode 100644 index 0000000..26d20c8 --- /dev/null +++ b/src/actions/create.rs @@ -0,0 +1,20 @@ +use std::{fs, io}; + +use crate::{actions::update, files::Locations}; + +use super::ActionOptions; + +pub fn create(command_options: ActionOptions) -> io::Result<()> { + let locations = Locations::from(&command_options); + + 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)?; + + update(command_options)?; + + Ok(()) +} diff --git a/src/actions/mod.rs b/src/actions/mod.rs new file mode 100644 index 0000000..588ce8f --- /dev/null +++ b/src/actions/mod.rs @@ -0,0 +1,30 @@ +mod create; +mod shift; +mod update; + +use std::path::{Path, PathBuf}; + +pub use create::create; +pub use shift::shift; +pub use update::update; + +pub struct ActionOptions { + pub repository_path: PathBuf, +} + +impl ActionOptions { + pub fn from_path(path: &str) -> Self { + ActionOptions { + repository_path: Path::new(path).to_path_buf(), + } + } + + pub fn from_pwd() -> Result<Self, ()> { + let current_path = std::env::current_dir(); + if let Ok(repository_path) = current_path { + Ok(ActionOptions { repository_path }) + } else { + Err(()) + } + } +} diff --git a/src/actions/shift.rs b/src/actions/shift.rs new file mode 100644 index 0000000..c02f0ed --- /dev/null +++ b/src/actions/shift.rs @@ -0,0 +1,55 @@ +use std::{ + fs, + io::{self, Seek, Write}, + path::Path, +}; + +use crate::{ + files::{Locations, RepositoryPaths}, + history::FileHistory, +}; + +use super::ActionOptions; + +pub fn shift(command_options: ActionOptions, path: &str, new_cursor: usize) -> io::Result<()> { + let locations = Locations::from(&command_options); + + match RepositoryPaths::from_tracked(&locations, Path::new(path)) { + RepositoryPaths::Tracked(tracked) => { + let mut history_file = tracked.load_history_file()?; + + let mut file_history = FileHistory::from_file(&mut history_file).unwrap(); + file_history.set_cursor(new_cursor); + + if file_history.file_is_deleted() { + fs::remove_file(path)?; + } else { + let new_content = file_history.get_content(); + let mut tracked_file = tracked.create_tracked_file()?; + + tracked_file.rewind()?; + tracked_file.set_len(0)?; + + tracked_file.write_all(new_content.as_bytes())?; + } + + file_history.write_to_file(&mut history_file)?; + } + RepositoryPaths::Deleted(deleted) => { + let mut history_file = deleted.load_history_file()?; + + let mut file_history = FileHistory::from_file(&mut history_file).unwrap(); + file_history.set_cursor(new_cursor); + + if !file_history.file_is_deleted() { + let mut new_tracked_file = deleted.create_tracked_file(&locations)?; + let new_content = file_history.get_content(); + + new_tracked_file.write_all(new_content.as_bytes())?; + } + } + RepositoryPaths::Untracked(_) => panic!("File is not tracked with Ka."), + } + + Ok(()) +} diff --git a/src/actions/update.rs b/src/actions/update.rs new file mode 100644 index 0000000..a2bc08d --- /dev/null +++ b/src/actions/update.rs @@ -0,0 +1,80 @@ +use std::io::{self, Read}; + +use crate::{ + files::{Locations, RepositoryPaths}, + history::{FileChange, FileHistory}, + text_diff::TextChange, +}; + +use super::ActionOptions; + +pub fn update(command_options: ActionOptions) -> io::Result<()> { + let locations = Locations::from(&command_options); + + let entries = locations + .get_repository_paths() + .expect("Could not traverse files."); + + for element in entries { + update_file(element, &locations)?; + } + + Ok(()) +} + +fn update_file(paths: RepositoryPaths, locations: &Locations) -> io::Result<()> { + let new_state = match paths { + RepositoryPaths::Deleted(deleted) => { + let mut history_file = deleted.load_history_file()?; + let file_history = FileHistory::from_file(&mut history_file).unwrap(); + if !file_history.file_is_deleted() { + let mut new_history = file_history; + new_history.add_change(FileChange::Deleted); + Some((history_file, new_history)) + } else { + None + } + } + RepositoryPaths::Untracked(untracked) => { + let mut file = untracked.load_file()?; + + let mut file_content = String::new(); + file.read_to_string(&mut file_content)?; + + let change = FileChange::Updated(vec![TextChange::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)) + } + RepositoryPaths::Tracked(tracked) => { + let mut history_file = tracked.load_history_file()?; + let mut tracked_file = tracked.load_tracked_file()?; + + let file_history = FileHistory::from_file(&mut history_file).unwrap(); + + let mut new_content = String::new(); + tracked_file.read_to_string(&mut new_content)?; + + let old_content = file_history.get_content(); + + let changes = TextChange::diff(&old_content, &new_content); + + let mut new_history = file_history; + new_history.add_change(FileChange::Updated(changes)); + new_history.set_cursor(new_history.cursor() + 1); + + 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 new file mode 100644 index 0000000..2512fa5 --- /dev/null +++ b/src/files.rs @@ -0,0 +1,182 @@ +use std::{ + fs::{self, DirEntry, File, OpenOptions}, + io, + path::{Path, PathBuf}, +}; + +use crate::actions::ActionOptions; + +pub struct Locations { + pub repository_path: PathBuf, + pub ka_path: PathBuf, + pub ka_files_path: PathBuf, +} + +impl Locations { + pub fn get_repository_paths(&self) -> Result<Vec<RepositoryPaths>, ()> { + let repository_entries = fs::read_dir(&self.repository_path).map_err(|_| ())?; + let history_entries = fs::read_dir(&self.ka_files_path).map_err(|_| ())?; + + let tracked_paths = repository_entries + .map(Result::unwrap) + .filter(|entry| entry.path() != self.ka_path) + .flat_map(Self::flatten_directories) + .map(|entry| { + let path = entry.path(); + RepositoryPaths::from_tracked(&self, &path) + }); + + let deleted_paths = history_entries + .map(Result::unwrap) + .flat_map(Self::flatten_directories) + .filter_map(|entry| { + let path = entry.path(); + let file = RepositoryPaths::from_history(&self, &path); + match file { + RepositoryPaths::Deleted { .. } => Some(file), + RepositoryPaths::Tracked { .. } => None, + _ => unreachable!(), + } + }); + + let all_paths = tracked_paths.chain(deleted_paths); + + Ok(all_paths.collect()) + } + + pub fn tracked_from_history(&self, history_file_path: &Path) -> PathBuf { + self.repository_path + .join(history_file_path.strip_prefix(&self.ka_files_path).unwrap()) + } + + pub fn history_from_tracked(&self, tracked_file_path: &Path) -> PathBuf { + self.ka_files_path.join( + tracked_file_path + .strip_prefix(&self.repository_path) + .unwrap(), + ) + } + + fn flatten_directories(entry: DirEntry) -> Vec<DirEntry> { + let file_type = entry.file_type().expect("Could not read a file type."); + if file_type.is_dir() { + fs::read_dir(entry.path()) + .expect("Could not read nested directory.") + .map(Result::unwrap) + .flat_map(Self::flatten_directories) + .collect() + } else { + vec![entry] + } + } +} + +impl From<&ActionOptions> for Locations { + fn from(options: &ActionOptions) -> Self { + let ka_path = options.repository_path.join(".ka"); + let ka_files_path = ka_path.join("files"); + + Self { + repository_path: options.repository_path.clone(), + ka_path, + ka_files_path, + } + } +} + +pub enum RepositoryPaths { + Deleted(FileDeleted), + Untracked(FileUntracked), + Tracked(FileTracked), +} + +impl RepositoryPaths { + pub fn from_history(locations: &Locations, history_file_path: &Path) -> Self { + let tracked_path = locations.tracked_from_history(history_file_path); + if !tracked_path.exists() { + RepositoryPaths::Deleted(FileDeleted { + history_path: history_file_path.to_path_buf(), + }) + } else { + RepositoryPaths::Tracked(FileTracked { + history_path: history_file_path.to_path_buf(), + tracked_path, + }) + } + } + + pub fn from_tracked(locations: &Locations, tracked_file_path: &Path) -> Self { + let history_path = locations.history_from_tracked(tracked_file_path); + if !history_path.exists() { + RepositoryPaths::Untracked(FileUntracked { + path: tracked_file_path.to_path_buf(), + }) + } else { + RepositoryPaths::Tracked(FileTracked { + history_path, + tracked_path: tracked_file_path.to_path_buf(), + }) + } + } +} + +pub struct FileDeleted { + history_path: PathBuf, +} + +impl FileDeleted { + pub fn load_history_file(&self) -> io::Result<File> { + OpenOptions::new() + .write(true) + .read(true) + .open(&self.history_path) + } + + pub fn create_tracked_file(&self, locations: &Locations) -> io::Result<File> { + File::create(locations.tracked_from_history(&self.history_path)) + } +} + +pub struct FileUntracked { + path: PathBuf, +} + +impl FileUntracked { + pub fn load_file(&self) -> io::Result<File> { + OpenOptions::new().read(true).open(&self.path) + } + + pub fn create_history_file(&self, locations: &Locations) -> io::Result<File> { + let history_path = locations.history_from_tracked(&self.path); + + history_path.parent().map(|dir_path| { + if !dir_path.exists() { + fs::create_dir_all(dir_path).unwrap(); + } + }); + + File::create(history_path) + } +} + +pub struct FileTracked { + history_path: PathBuf, + tracked_path: PathBuf, +} + +impl FileTracked { + pub fn load_history_file(&self) -> io::Result<File> { + OpenOptions::new() + .write(true) + .read(true) + .open(&self.history_path) + } + + pub fn load_tracked_file(&self) -> io::Result<File> { + File::open(&self.tracked_path) + } + + pub fn create_tracked_file(&self) -> io::Result<File> { + File::create(&self.tracked_path) + } +} diff --git a/src/history.rs b/src/history.rs new file mode 100644 index 0000000..25a9555 --- /dev/null +++ b/src/history.rs @@ -0,0 +1,89 @@ +use std::{fs::File, io::{self, Read, Seek, Write}}; + +use serde::{Deserialize, Serialize}; + +use crate::text_diff::TextChange; + +#[derive(Serialize, Deserialize, Debug)] +pub struct FileHistory { + cursor: usize, + changes: Vec<FileChange>, +} + +impl FileHistory { + pub fn from_file(file: &mut File) -> Result<FileHistory, ()> { + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer) + .expect("Could not read file history,"); + + let file_history = serde_json::from_slice::<FileHistory>(&buffer); + Ok(file_history.expect("Corrupted file history.")) + } + + 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, + }, + None => false, + } + } + + pub fn get_content(&self) -> String { + let mut buffer = String::new(); + for file_change in self.changes.iter().take(self.cursor) { + if let FileChange::Updated(ref updated) = file_change { + for change in updated.iter() { + change.apply(&mut buffer) + } + } else { + buffer = String::new(); + } + } + buffer + } + + pub fn write_to_file(&self, file: &mut File) -> io::Result<()> { + let encoded: Vec<u8> = serde_json::to_vec(self).unwrap(); + file.rewind()?; + file.set_len(0)?; + file.write_all(encoded.as_ref())?; + Ok(()) + } + + 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() + ); + } + } + + pub fn add_change(&mut self, change: FileChange) { + self.changes.push(change); + } +} + +impl Default for FileHistory { + fn default() -> Self { + Self { + cursor: 0, + changes: Vec::new(), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum FileChange { + Updated(Vec<TextChange>), + Deleted, +} diff --git a/src/lib.rs b/src/lib.rs index 3a05db5..467ffe7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,257 +1,5 @@ -use difference::{Changeset, Difference}; -use serde::{Deserialize, Serialize}; -use std::{ - fs::{self, DirEntry, File, OpenOptions}, - io::{self, Read, Seek, Write}, - path::{Path, PathBuf}, - vec, -}; +pub mod actions; -pub fn create_command() -> io::Result<()> { - let repository_directory = Path::new("./repository"); - - let ka_directory = repository_directory.join(".ka"); - let ka_files_directory = ka_directory.join("files"); - - if ka_directory.exists() { - fs::remove_dir_all(ka_directory.as_path())?; - } - - fs::create_dir(ka_directory)?; - fs::create_dir(ka_files_directory)?; - - update_command()?; - - Ok(()) -} - -pub fn update_command() -> io::Result<()> { - let repository_directory = Path::new("./repository"); - - let ka_directory = repository_directory.join(".ka"); - - let entries: Vec<DirEntry> = fs::read_dir(repository_directory)? - .map(Result::unwrap) - .filter(|entry| entry.path() != ka_directory) - .flat_map(flatten_directories) - .collect(); - - for element in entries.iter() { - update_file(element.path())?; - } - - Ok(()) -} - -pub fn shift_command(path: &str, new_cursor: usize) { - let repository_directory = Path::new("./repository"); - - let ka_directory = repository_directory.join(".ka"); - let ka_files_directory = ka_directory.join("files"); - - let file_path = Path::new(path); - - let mut tracked_file = File::create(file_path).expect("Could not find file."); - - let history_file_path = - ka_files_directory.join(file_path.strip_prefix(repository_directory).unwrap()); - let mut history_file = OpenOptions::new() - .write(true) - .read(true) - .open(history_file_path) - .expect("Could not find corresponding history file"); - - let mut current_history = - read_history_from_file(&mut history_file).expect("Failed reading history from file."); - let state_at_cursor = get_state_from_history_for_cursor(¤t_history, new_cursor); - - println!("{}", state_at_cursor); - println!("{:?}", current_history); - - tracked_file - .write_all(state_at_cursor.as_bytes()) - .expect("Failed writing new state"); - - current_history.cursor = new_cursor; - write_history_to_file(&mut history_file, ¤t_history) - .expect("Could not write new cursor to history file."); -} - -fn flatten_directories(entry: DirEntry) -> Vec<DirEntry> { - let file_type = entry.file_type().expect("Could not read a file type."); - if file_type.is_dir() { - fs::read_dir(entry.path()) - .expect("Could not read nested directory.") - .map(Result::unwrap) - .flat_map(flatten_directories) - .collect() - } else { - vec![entry] - } -} - -fn update_file(file_path: PathBuf) -> io::Result<()> { - let repository_directory = Path::new("./repository"); - - let ka_directory = repository_directory.join(".ka"); - let ka_files_directory = ka_directory.join("files"); - - let mut tracked_file = File::open(file_path.as_path()).expect("Could not find tracked file."); - - let history_file_path = - ka_files_directory.join(file_path.strip_prefix(repository_directory).unwrap()); - - let (mut history_file, current_file_history) = if history_file_path.exists() { - let mut history_file = OpenOptions::new() - .read(true) - .write(true) - .open(history_file_path) - .expect("Could not open history file."); - - let file_history = read_history_from_file(&mut history_file).unwrap(); - - history_file - .set_len(0) - .expect("Failed truncating old history file."); - - history_file - .rewind() - .expect("Failed rewinding history file."); - - (history_file, file_history) - } else { - history_file_path.parent().map(|dir_path| { - if !dir_path.exists() { - fs::create_dir_all(dir_path).unwrap(); - } - }); - - let file = File::create(history_file_path).expect("Could not create history file."); - - let empty_file_history = FileHistory { - cursor: 0, - changes: Vec::new(), - }; - - (file, empty_file_history) - }; - - let mut new_content = String::new(); - tracked_file - .read_to_string(&mut new_content) - .expect("Could not read tracked file."); - - let old_content = - get_state_from_history_for_cursor(¤t_file_history, current_file_history.cursor); - - let change_set = Changeset::new(&old_content, &new_content, ""); - - let mut changes = Vec::new(); - let mut at: usize = 0; - for diff in change_set.diffs.iter() { - match diff { - Difference::Add(new_content) => { - let change = TextChange::Inserted(TextInserted { - at, - new_content: new_content.to_owned(), - }); - at += new_content.len(); - changes.push(change); - } - Difference::Rem(removed_content) => { - changes.push(TextChange::Deleted(TextDeleted { - at, - upto: removed_content.len(), - })); - } - Difference::Same(same_content) => { - at += same_content.len(); - } - } - } - - let mut file_changes = current_file_history.changes; - println!("{:?}", file_changes); - file_changes.push(FileChange::Updated(FileUpdated { changes })); - - let new_file_history = FileHistory { - changes: file_changes, - cursor: current_file_history.cursor + 1, - }; - - write_history_to_file(&mut history_file, &new_file_history) - .expect("Failed writing new history."); - - Ok(()) -} - -fn write_history_to_file(file: &mut File, history: &FileHistory) -> io::Result<()> { - let encoded: Vec<u8> = serde_json::to_vec(history).unwrap(); - file.write_all(encoded.as_ref())?; - Ok(()) -} - -fn read_history_from_file(file: &mut File) -> Result<FileHistory, ()> { - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer) - .expect("Could not read file history,"); - - let file_history = serde_json::from_slice::<FileHistory>(&buffer); - Ok(file_history.expect("Corrupted file history.")) -} - -fn get_state_from_history_for_cursor(history: &FileHistory, cursor: usize) -> String { - let mut buffer = String::new(); - for file_change in history.changes.iter().take(cursor) { - if let FileChange::Updated(ref updated) = file_change { - for change in updated.changes.iter() { - match change { - TextChange::Deleted(deletion) => { - buffer.replace_range(deletion.at..deletion.upto, ""); - } - TextChange::Inserted(insertion) => { - buffer.insert_str(insertion.at, insertion.new_content.as_str()); - } - } - } - } else { - buffer = String::new(); - } - } - buffer -} - -#[derive(Serialize, Deserialize, Debug)] -struct FileHistory { - cursor: usize, - changes: Vec<FileChange>, -} - -#[derive(Serialize, Deserialize, Debug)] -enum FileChange { - Updated(FileUpdated), - Deleted, -} - -#[derive(Serialize, Deserialize, Debug)] -struct FileUpdated { - changes: Vec<TextChange>, -} - -#[derive(Serialize, Deserialize, Debug)] -enum TextChange { - Inserted(TextInserted), - Deleted(TextDeleted), -} - -#[derive(Serialize, Deserialize, Debug)] -struct TextInserted { - at: usize, - new_content: String, -} - -#[derive(Serialize, Deserialize, Debug)] -struct TextDeleted { - at: usize, - upto: usize, -} +mod history; +mod text_diff; +mod files; diff --git a/src/text_diff.rs b/src/text_diff.rs new file mode 100644 index 0000000..bab8799 --- /dev/null +++ b/src/text_diff.rs @@ -0,0 +1,52 @@ +use difference::{Changeset, Difference}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub enum TextChange { + Inserted { at: usize, new_content: String }, + Deleted { at: usize, upto: usize }, +} + +impl TextChange { + pub fn diff(old: &str, new: &str) -> Vec<Self> { + let change_set = Changeset::new(old, new, ""); + + let mut changes = Vec::new(); + let mut at: usize = 0; + + for diff in change_set.diffs.iter() { + match diff { + Difference::Add(new_content) => { + let change = TextChange::Inserted { + at, + new_content: new_content.to_owned(), + }; + at += new_content.len(); + changes.push(change); + } + Difference::Rem(removed_content) => { + changes.push(TextChange::Deleted { + at, + upto: removed_content.len(), + }); + } + Difference::Same(same_content) => { + at += same_content.len(); + } + } + } + + changes + } + + pub fn apply(&self, buffer: &mut String) { + match self { + TextChange::Deleted { at, upto } => { + buffer.replace_range(at..upto, ""); + } + TextChange::Inserted { at, new_content } => { + buffer.insert_str(*at, new_content); + } + } + } +} |
