#![feature(map_try_insert)]
use std::borrow::Cow;
use camino::{Utf8Path, Utf8PathBuf};
use libpijul::change::{Atom, BaseHunk, Change, ChangeHeader, TextSerError};
use libpijul::changestore::ChangeStore;
use libpijul::pristine::TxnErr;
use libpijul::pristine::sanakirja::{MutTxn, Pristine, SanakirjaError};
use libpijul::working_copy::WorkingCopy;
use libpijul::working_copy::{self, WorkingCopyRead};
use libpijul::{ArcTxn, ChangeId, ChannelRef, GraphTxnT, TxnT, Vertex};
use libpijul::{TxnTExt, changestore};
use crate::author::{AuthorSource, Authors, GetAuthorsError};
use crate::file_system::changes::{
ActiveHunk, CreditSource, FileCredits, FileCreditsError, HunkDiff,
};
use crate::file_system::open_file::contents::FileContents;
use crate::file_system::open_file::{OpenFile, OpenFileError};
use crate::file_system::working_copy::{
EditorWorkingCopy, UpdateOpenFileContentsError, UpdateOpenFileCreditsError,
};
use crate::path_state::{PathState, PathStates, PathStatesError};
pub mod author;
pub mod file_system;
pub mod path_state;
#[derive(Debug, thiserror::Error)]
pub enum RepositoryError<C: std::error::Error + 'static, W: std::error::Error + 'static> {
#[error("unable to open pristine: {0:#?}")]
Pristine(#[from] SanakirjaError),
#[error("unable to begin transaction: {0:#?}")]
BeginTransaction(#[from] BeginTransactionError),
#[error("unable to get untracked paths: {0:#?}")]
Changes(#[from] GetAuthorsError<C>),
#[error("unable to get path states: {0:#?}")]
PathStates(#[from] PathStatesError<C, W>),
}
#[derive(Debug, thiserror::Error)]
pub enum BeginTransactionError {
#[error("unable to begin transaction: {0:#?}")]
Transaction(#[from] SanakirjaError),
#[error("unable to open channel: {0:#?}")]
Channel(#[from] TxnErr<SanakirjaError>),
}
#[derive(Debug, thiserror::Error)]
pub enum GetChangeError<C: std::error::Error + 'static> {
#[error("Unable to begin transaction: {0}")]
Transaction(#[from] SanakirjaError),
#[error("Unable to get external hash: {0}")]
Hash(#[from] TxnErr<SanakirjaError>),
#[error("Failed to get change: {0}")]
Change(C),
#[error("No matching change for {change_id:?}")]
NoMatchingChange { change_id: ChangeId },
}
#[derive(Debug, thiserror::Error)]
pub enum FindVertexHunkError<C: std::error::Error + 'static> {
#[error("Unable to get change: {0}")]
Change(#[from] GetChangeError<C>),
#[error("No hunk found for vertex: {vertex:?}")]
NoHunkForVertex { vertex: Vertex<ChangeId> },
}
#[derive(Debug, thiserror::Error)]
pub enum CreditError {
#[error("error while checking if `{path}` is tracked: {error:#?}")]
GetIsTracked {
path: Utf8PathBuf,
error: SanakirjaError,
},
#[error("missing open file for {0}")]
MissingOpenFile(Utf8PathBuf),
}
#[derive(Debug, thiserror::Error)]
pub enum UpdatePathStateError<C: std::error::Error + 'static, W: std::error::Error + 'static> {
#[error("Unable to begin transaction: {0}")]
BeginTransaction(#[from] BeginTransactionError),
#[error(transparent)]
PathStates(#[from] PathStatesError<C, W>),
}
#[derive(Debug, thiserror::Error)]
pub enum ActiveHunkError<C: std::error::Error + 'static> {
#[error("File is not tracked by Pijul")]
Untracked,
#[error("Missing file contents for hunk: {hunk:#?}")]
MissingFileContents {
hunk: Box<BaseHunk<Atom<Option<libpijul::Hash>>, libpijul::change::Local>>,
},
#[error("Failed to get change for {change_id:?}: {error}")]
Change {
change_id: ChangeId,
error: GetChangeError<C>,
},
#[error("Unable to find hunk for vertex: {0}")]
FindVertexHunk(#[from] FindVertexHunkError<C>),
#[error("Failed to get change contents: {0}")]
ChangeContents(#[from] TextSerError<C>),
#[error("Hunk not yet implemented: {hunk:#?}")]
UnimplementedHunk {
hunk: Box<BaseHunk<Atom<Option<libpijul::Hash>>, libpijul::change::Local>>,
},
}
#[derive(Debug, thiserror::Error)]
pub enum CreateOpenFileError<C: std::error::Error + 'static, W: std::error::Error + 'static> {
#[error("unable to get metadata for file: {0:?}")]
FileMetadata(W),
#[error(transparent)]
OpenFile(#[from] OpenFileError<C, W>),
#[error("File already open at {0}")]
AlreadyOpen(Utf8PathBuf),
#[error(transparent)]
FileCredits(#[from] FileCreditsError<C, W>),
}
#[derive(Debug, thiserror::Error)]
pub enum UpdateOpenFileError<C: std::error::Error + 'static, W: std::error::Error + 'static> {
#[error("Unable to begin transaction: {0}")]
BeginTransaction(#[from] BeginTransactionError),
#[error(transparent)]
UpdateContents(#[from] UpdateOpenFileContentsError),
#[error(transparent)]
UpdateCredits(#[from] UpdateOpenFileCreditsError<C, W>),
}
pub type FileSystemRepository =
Repository<changestore::filesystem::FileSystem, working_copy::FileSystem>;
pub struct Repository<C, W>
where
C: ChangeStore + Clone + Send + 'static,
W: WorkingCopy + Clone + Send + Sync + 'static,
{
pub authors: Authors,
pub working_copy: EditorWorkingCopy<W>,
pub path_states: PathStates,
pub change_store: C,
pub pristine: Pristine,
}
impl FileSystemRepository {
pub fn new(
root: &Utf8Path,
) -> Result<Self, RepositoryError<changestore::filesystem::Error, std::io::Error>> {
let dot_directory = root.join(libpijul::DOT_DIR);
let change_store = changestore::filesystem::FileSystem::from_root(root.as_str(), 256);
let pristine = Pristine::new(dot_directory.join("pristine").join("db").as_str())?;
let file_system = working_copy::FileSystem::from_root(root.as_str());
let (transaction, channel) = begin_transaction(&pristine)?;
let authors = Authors::new(&dot_directory, &transaction, &channel, &change_store)?;
let path_states =
PathStates::new(root, &transaction, &channel, &file_system, &change_store)?;
let working_copy = EditorWorkingCopy::new(file_system);
Ok(Self {
authors,
working_copy,
path_states,
change_store,
pristine,
})
}
}
impl<C, W> Repository<C, W>
where
C: ChangeStore + Clone + Send + 'static,
W: WorkingCopy + Clone + Send + Sync + 'static,
{
pub fn get_change(&self, change_id: ChangeId) -> Result<Change, GetChangeError<C::Error>> {
let transaction = self.pristine.arc_txn_begin()?;
let read_transaction = transaction.read();
let external_hash = read_transaction
.get_external(&change_id)?
.ok_or(GetChangeError::NoMatchingChange { change_id })?;
self.change_store
.get_change(&external_hash.into())
.map_err(GetChangeError::Change)
}
pub fn find_vertex_hunk(
&self,
vertex: Vertex<ChangeId>,
) -> Result<
(
usize,
BaseHunk<Atom<Option<libpijul::Hash>>, libpijul::change::Local>,
),
FindVertexHunkError<C::Error>,
> {
let change = self.get_change(vertex.change)?;
change
.hashed
.changes
.into_iter()
.enumerate()
.find_map(|(index, hunk)| {
hunk.iter()
.any(|atom| match atom {
Atom::NewVertex(new_vertex) => {
new_vertex.start <= vertex.start && new_vertex.end >= vertex.end
}
Atom::EdgeMap(_edge_map) => false,
})
.then_some((index + 1, hunk))
})
.ok_or(FindVertexHunkError::NoHunkForVertex { vertex })
}
pub fn get_active_hunk(
&self,
open_file: &OpenFile,
credit_source: CreditSource,
) -> Result<ActiveHunk, ActiveHunkError<C::Error>> {
let Some(credits) = open_file.credits.as_ref() else {
return Err(ActiveHunkError::Untracked);
};
let change_contents = match credit_source {
CreditSource::Tracked { vertex } => {
let change =
self.get_change(vertex.change)
.map_err(|error| ActiveHunkError::Change {
change_id: vertex.change,
error,
})?;
Cow::Owned(change.contents)
}
CreditSource::Untracked { hunk_index: _ } => {
Cow::Borrowed(&credits.unrecorded_state.change_contents)
}
};
let (hunk_index, hunk) = match credit_source {
CreditSource::Tracked { vertex } => {
let (index, hunk) = self.find_vertex_hunk(vertex)?;
(index, Cow::Owned(hunk))
}
CreditSource::Untracked { hunk_index } => {
(
hunk_index + 1,
Cow::Borrowed(&credits.unrecorded_state.hunks[hunk_index]),
)
}
};
let hunk_diff = match hunk.as_ref() {
BaseHunk::FileAdd {
contents,
add_name: _,
add_inode: _,
path: _,
encoding,
} => {
let Some(contents) = contents else {
return Err(ActiveHunkError::MissingFileContents {
hunk: Box::new(hunk.into_owned()),
});
};
let file_contents = libpijul::change::get_change_contents(
&self.change_store,
contents,
&change_contents,
)?;
let file_text = String::from_utf8(file_contents).unwrap();
HunkDiff::TextChange {
lines_added: file_text.lines().map(str::to_string).collect(),
lines_removed: Vec::new(),
}
}
BaseHunk::Replacement {
change,
replacement,
local: _,
encoding,
} => {
let old_contents = libpijul::change::get_change_contents(
&self.change_store,
change,
&change_contents,
)?;
let new_contents = libpijul::change::get_change_contents(
&self.change_store,
replacement,
&change_contents,
)?;
let old_text = String::from_utf8(old_contents).unwrap();
let new_text = String::from_utf8(new_contents).unwrap();
HunkDiff::TextChange {
lines_added: new_text.lines().map(str::to_string).collect(),
lines_removed: old_text.lines().map(str::to_string).collect(),
}
}
BaseHunk::Edit {
change,
local: _,
encoding,
} => {
let new_contents = libpijul::change::get_change_contents(
&self.change_store,
change,
&change_contents,
)?;
let text = String::from_utf8(new_contents).unwrap();
if let Atom::EdgeMap(edge) = change
&& (edge.edges.is_empty() || edge.edges[0].flag.is_deleted())
{
HunkDiff::TextChange {
lines_added: Vec::new(),
lines_removed: text.lines().map(str::to_string).collect(),
}
} else {
HunkDiff::TextChange {
lines_added: text.lines().map(str::to_string).collect(),
lines_removed: Vec::new(),
}
}
}
_ => {
return Err(ActiveHunkError::UnimplementedHunk {
hunk: Box::new(hunk.into_owned()),
});
}
};
Ok(ActiveHunk {
index: hunk_index,
diff: hunk_diff,
})
}
pub fn get_path_state(&self, path: &Utf8Path) -> Option<PathState> {
self.path_states.get_path_state(path)
}
pub fn iter_path_states(&self) -> impl Iterator<Item = (Utf8PathBuf, PathState)> {
self.path_states.iter_path_states()
}
pub fn update_path_state(
&mut self,
path: Utf8PathBuf,
) -> Result<(), UpdatePathStateError<C::Error, W::Error>> {
let (transaction, channel) = begin_transaction(&self.pristine)?;
self.path_states.update_path_state(
path,
&transaction,
&channel,
&self.working_copy.working_copy,
&self.change_store,
)?;
Ok(())
}
pub fn authors_for_change(&self, change_header: &ChangeHeader) -> Vec<&AuthorSource> {
self.authors.authors_for_change(change_header)
}
pub fn create_open_file(
&mut self,
path: Utf8PathBuf,
file_contents: String,
) -> Result<(), CreateOpenFileError<C::Error, W::Error>> {
let metadata = self
.working_copy
.file_metadata(path.as_str())
.map_err(CreateOpenFileError::FileMetadata)?;
let (transaction, channel) = begin_transaction(&self.pristine).unwrap();
let open_file = OpenFile {
contents: FileContents::new(path.clone(), metadata, file_contents, &self.working_copy),
credits: None,
};
self.working_copy
.open_files
.try_insert(path.clone(), open_file)
.map_err(|_error| CreateOpenFileError::AlreadyOpen(path.clone()))?;
let is_tracked = transaction
.read()
.is_tracked(path.as_str())
.map_err(|error| OpenFileError::CheckIfTracked {
path: path.clone(),
error,
})?;
let file_credits = match is_tracked {
true => Some(FileCredits::new(
&path,
&self.working_copy.open_files.get(&path).unwrap().contents,
&transaction,
&channel,
&self.change_store,
&self.working_copy,
)?),
false => None,
};
self.working_copy.open_files.get_mut(&path).unwrap().credits = file_credits;
Ok(())
}
pub fn update_open_file(
&mut self,
path: &Utf8Path,
character_offset: usize,
characters_replaced: usize,
replacement_text: &str,
) -> Result<(), UpdateOpenFileError<C::Error, W::Error>> {
let (transaction, channel) = begin_transaction(&self.pristine)?;
self.working_copy.update_open_file_contents(
path,
character_offset,
characters_replaced,
replacement_text,
)?;
self.working_copy.update_open_file_credits(
path,
&transaction,
&channel,
&self.change_store,
)?;
Ok(())
}
pub fn get_open_file(&self, path: &Utf8Path) -> Option<&OpenFile> {
self.working_copy.open_files.get(path)
}
}
fn begin_transaction(
pristine: &Pristine,
) -> Result<(ArcTxn<MutTxn<()>>, ChannelRef<MutTxn<()>>), BeginTransactionError> {
let transaction = pristine.arc_txn_begin()?;
let channel = transaction
.read()
.load_channel(
transaction
.read()
.current_channel()
.unwrap_or(libpijul::DEFAULT_CHANNEL),
)?
.unwrap();
Ok((transaction, channel))
}