use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use serde_json;
use url::percent_encoding::{utf8_percent_encode, PATH_SEGMENT_ENCODE_SET};
#[cfg(not(target_arch = "wasm32"))]
use rand;
#[cfg(not(target_arch = "wasm32"))]
use libpijul;
#[cfg(not(target_arch = "wasm32"))]
use libpijul::{
fs_representation::{RepoPath, RepoRoot},
graph::LineBuffer,
patch::Record,
DiffAlgorithm, Key, PatchId, RecordState, Transaction, Value,
};
#[cfg(not(target_arch = "wasm32"))]
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use url::percent_encoding::percent_decode;
mod msg;
mod entry_contents;
pub use self::msg::Msg;
pub use self::entry_contents::*;
#[derive(Clone, Serialize, Deserialize)]
pub struct State {
path: String,
entry_path: PathBuf,
patches: Vec<PatchHeader>,
selected_patch: Option<String>,
dir_contents: Option<Vec<DirEntry>>,
file_contents: Option<String>,
render_md: bool,
diff_mode: bool,
}
impl State {
pub fn new() -> State {
State {
path: "/".to_string(),
entry_path: PathBuf::from("./"),
patches: vec![],
selected_patch: None,
dir_contents: None,
file_contents: None,
render_md: true,
diff_mode: false,
}
}
pub fn from_json(state_json: &str) -> State {
serde_json::from_str(state_json).expect("Unable to deserialize State json")
}
pub fn to_json(&self) -> String {
serde_json::to_string(&self).expect("Unable to serialize state")
}
}
impl State {
pub fn msg(&mut self, msg: &Msg) {
match msg {
Msg::SetPath(path) => self.set_path(path.to_string()),
Msg::SetSelectedPatch(selected_patch) => {
self.selected_patch = selected_patch.to_owned();
}
#[cfg(not(target_arch = "wasm32"))]
Msg::SetEntryContents(encoded_path) => {
self.set_entry_path(encoded_path.to_string());
self.set_entry_contents()
}
Msg::SetEntryContentsJson(json) => {
let entry: EntryContents = json
.into_serde()
.expect("Unable to deserialize EntryContents json");
self.entry_path = PathBuf::from(entry.path);
self.dir_contents = entry.dir_contents;
self.file_contents = entry.file_contents;
self.patches = entry.patches;
}
Msg::ToggleRenderMd => self.toggle_render_md(),
};
}
pub fn path(&self) -> &str {
&self.path
}
pub fn entry_path(&self) -> &Path {
self.entry_path.as_path()
}
pub fn dir_contents(&self) -> &Option<Vec<DirEntry>> {
&self.dir_contents
}
pub fn file_contents(&self) -> &Option<String> {
&self.file_contents
}
pub fn patches(&self) -> &Vec<PatchHeader> {
&self.patches
}
pub fn render_md(&self) -> &bool {
&self.render_md
}
pub fn selected_patch(&self) -> &Option<String> {
&self.selected_patch
}
pub fn entry_contents(&self) -> EntryContents {
EntryContents {
path: self.entry_path.clone(),
patches: self.patches.clone(),
selected_patch: self.selected_patch.clone(),
dir_contents: self.dir_contents.clone(),
file_contents: self.file_contents.clone(),
}
}
pub fn is_dir(&self) -> bool {
self.entry_path.to_str().unwrap().ends_with("/")
}
pub fn encoded_path(&self) -> String {
let entry_path = self.entry_path.to_str().unwrap()[2..].to_string();
utf8_percent_encode(&entry_path, PATH_SEGMENT_ENCODE_SET).to_string()
}
}
impl State {
fn set_path(&mut self, path: String) {
self.path = path;
}
fn toggle_render_md(&mut self) {
self.render_md = !self.render_md
}
}
#[cfg(not(target_arch = "wasm32"))]
impl State {
fn set_entry_path(&mut self, encoded_path: String) {
let mut entry_path = "./".to_string();
let decoded_path = percent_decode(encoded_path.as_bytes())
.decode_utf8()
.expect("unable to decode path as utf8");
entry_path.push_str(&decoded_path);
self.entry_path = PathBuf::from(&entry_path);
if !encoded_path.is_empty() && self.entry_path.is_dir() {
entry_path.push('/');
self.entry_path = PathBuf::from(&entry_path);
}
}
fn set_entry_contents(&mut self) {
if self.is_dir() {
self.set_dir_contents();
let readme_path = self.entry_path().join("README.md");
self.set_file_contents(readme_path);
} else {
self.dir_contents = None;
self.set_file_contents(self.entry_path().to_owned());
}
self.set_patches();
}
fn set_patches(&mut self) {
let repo_root = RepoRoot {
repo_root: Path::new("./"),
};
let repo = repo_root.open_repo(None).unwrap();
let txn = repo.txn_begin().unwrap();
let branch = txn.get_branch("master").unwrap();
let repo_path = repo_root.relativize(&self.entry_path).unwrap().to_owned();
for (_applied, patchid) in txn.rev_iter_applied(&branch, None) {
let hash = txn.get_external(patchid).unwrap();
if self.entry_path != PathBuf::from("./") {
let inode = if let Ok(inode) = txn.find_inode(&repo_path) {
inode
} else {
continue;
};
match txn.get_inodes(inode) {
Some(file_header) => {
if self.is_dir() {
let descendants = txn.list_files(inode).unwrap();
for descendant in descendants {
let d_inode = txn.find_inode(&repo_path.join(&descendant.as_path())).unwrap();
let d_file_header = txn.get_inodes(d_inode).unwrap();
if txn.get_touched(d_file_header.key, patchid) {
self.patches.push(PatchHeader::from_hash(hash));
break;
}
}
continue;
} else if !txn.get_touched(file_header.key, patchid) {
continue;
}
}
None => {
self.patches.push(PatchHeader::current_state());
break;
}
}
}
self.patches.push(PatchHeader::from_hash(hash));
}
}
fn set_dir_contents(&mut self) {
let repo_root = RepoRoot {
repo_root: Path::new("./"),
};
let repo = repo_root.open_repo(None).unwrap();
let mut txn = repo.mut_txn_begin(rand::thread_rng()).unwrap();
let branch = txn.open_branch("master").unwrap();
let dir_path = repo_root.relativize(&self.entry_path).unwrap();
let mut dir_contents_map = HashMap::new();
let entries = txn.list_files(txn.find_inode(&dir_path).unwrap()).unwrap();
let mut changed = false;
for repo_path in entries {
let name = repo_path
.components()
.next()
.unwrap()
.as_os_str()
.to_str()
.unwrap();
if !dir_contents_map.contains_key(name) {
let mut record = RecordState::new();
txn.record(
DiffAlgorithm::default(),
&mut record,
&branch,
&repo_root,
&dir_path.join(&repo_path.as_path()),
)
.unwrap();
let (changes, _) = record.finish();
let (status, latest_patch) = if changes.is_empty() {
let mut latest_patch = PatchHeader::current_state();
for (_applied, patchid) in txn.rev_iter_applied(&branch, None) {
let hash_ext = txn.get_external(patchid).unwrap();
let patch = repo_root.read_patch_nochanges(hash_ext).unwrap();
let inode = if let Ok(inode) = txn.find_inode(&repo_path) {
inode
} else {
continue;
};
match txn.get_inodes(inode) {
Some(file_header) => {
if !txn.get_touched(file_header.key, patchid) {
continue;
}
}
None => {
break;
}
}
latest_patch = PatchHeader {
hash: hash_ext.to_base58(),
name: patch.name,
authors: patch.authors,
timestamp: patch.timestamp,
};
break;
}
(None, latest_patch)
} else {
if !changed {
changed = true;
self.patches.push(PatchHeader::current_state());
}
let status = Some(
if repo_path.parent() == None
|| repo_path.parent() == Some(RepoPath(Path::new("")))
{
match changes[0] {
Record::Change { .. } => Status::Modified,
Record::FileAdd { .. } => Status::Added,
Record::FileDel { .. } => Status::Removed,
Record::FileMove { .. } => Status::Moved,
}
} else {
Status::Modified
},
);
(status, PatchHeader::current_state())
};
dir_contents_map.insert(name.to_string(), (status, latest_patch));
}
}
let mut dir_contents_vec: Vec<DirEntry> = dir_contents_map
.iter()
.map(|(name, (status, latest_patch))| DirEntry {
name: name.to_string(),
is_dir: dir_path.as_path().join(&name).is_dir(),
status: status.to_owned(),
latest_patch: latest_patch.to_owned(),
})
.collect();
dir_contents_vec.sort_by_key(|entry| entry.name.to_string());
dir_contents_vec.sort_by_key(|entry| !entry.is_dir);
self.dir_contents = Some(dir_contents_vec);
}
fn set_file_contents(&mut self, file_path: PathBuf) {
let repo_root = RepoRoot {
repo_root: Path::new("./"),
};
let repo = repo_root.open_repo(None).unwrap();
let txn = repo.txn_begin().unwrap();
let entry_path = repo_root.relativize(&file_path).unwrap();
let inode = txn.find_inode(&entry_path);
if inode.is_ok() {
let file_header = txn.get_inodes(inode.unwrap()).unwrap();
let branch = txn.get_branch("master").unwrap();
let mut graph = txn.retrieve(&branch, file_header.key);
let mut buf = OutBuffer {
lines: String::new(),
};
txn.output_file(&branch, &mut buf, &mut graph, &mut Vec::new())
.unwrap();
self.file_contents = Some(buf.lines);
} else {
self.file_contents = None
}
}
}
#[cfg(not(target_arch = "wasm32"))]
struct OutBuffer {
lines: String,
}
#[cfg(not(target_arch = "wasm32"))]
impl<'a, T: 'a + Transaction> LineBuffer<'a, T> for OutBuffer {
fn output_line(
&mut self,
_key: &Key<PatchId>,
contents: Value<'a, T>,
) -> Result<(), libpijul::Error> {
for chunk in contents {
self.lines.push_str(&String::from_utf8_lossy(&chunk));
}
Ok(())
}
fn output_conflict_marker(&mut self, s: &'a str) -> Result<(), libpijul::Error> {
let conflict_output = format!("Conflict: {}", s);
self.lines.push_str(&conflict_output);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serialize_deserialize() {
let state_json =
r#"{"path":"/","entry_path":"./","patches":[],"dir_contents":null,"file_contents":null,"render_md":true,"diff_mode":false}"#;
let state = State::from_json(state_json);
assert_eq!(state.to_json(), state_json);
}
}