Save repos
This commit is contained in:
parent
d08922e753
commit
357de970ee
10 changed files with 252 additions and 47 deletions
|
|
@ -7,6 +7,7 @@ edition = { workspace = true }
|
|||
anyhow = { workspace = true }
|
||||
directories = { workspace = true }
|
||||
git2 = { workspace = true }
|
||||
jiff = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub use crate::repo::VERSION as REPO_VERSION;
|
|||
pub use self::{
|
||||
datadir::{LockedDataDir, UnlockedDataDir},
|
||||
v1::{
|
||||
State, VERSION, add_repo, load_repo, load_repo_version, load_state, remove_repo,
|
||||
State, VERSION, add_repo, load_repo, load_repo_version, load_state, remove_repo, save_repo,
|
||||
select_repo, tidy,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::{collections::HashMap, fs, path::PathBuf};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use git2::Oid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -82,6 +83,10 @@ pub fn load_repo(dir: &UnlockedDataDir, id: RepoId) -> anyhow::Result<Repo> {
|
|||
repo::load(&repo_dir(dir, id))
|
||||
}
|
||||
|
||||
pub fn save_repo(dir: &LockedDataDir, id: RepoId, repo: Repo) -> anyhow::Result<Oid> {
|
||||
repo::save(&repo_dir(dir, id), repo)
|
||||
}
|
||||
|
||||
pub fn add_repo(dir: &LockedDataDir, name: String) -> anyhow::Result<RepoId> {
|
||||
let id = RepoId::new();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::{fmt, ops::Shl, str::FromStr, time::SystemTime};
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use jiff::{Timestamp, Zoned, tz::TimeZone};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
/// A timestamp- and randomness-based id.
|
||||
|
|
@ -13,19 +14,18 @@ struct TimestampId(u64);
|
|||
|
||||
impl TimestampId {
|
||||
fn new() -> Self {
|
||||
let secs = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("duration is positive")
|
||||
.as_secs();
|
||||
let secs: u64 = Timestamp::now()
|
||||
.as_second()
|
||||
.try_into()
|
||||
.expect("timestamp out of range");
|
||||
assert!(secs < 0x000000FF_FFFFFFFF_u64, "timestamp out of range");
|
||||
let random = rand::random::<u64>() & 0x00000000_00FFFFFF_u64;
|
||||
Self(secs << (3 * 8) | random)
|
||||
}
|
||||
|
||||
let random = rand::random::<u64>();
|
||||
|
||||
// Zeroing the last three bytes just in case. They should already be
|
||||
// zero under normal circumstances.
|
||||
let first_part = secs.shl(3 * 8) & 0xFFFFFFFF_FF000000_u64;
|
||||
let second_part = random & 0x00000000_00FFFFFF_u64;
|
||||
|
||||
Self(first_part | second_part)
|
||||
fn timestamp(self) -> Timestamp {
|
||||
let secs = self.0 >> (3 * 8);
|
||||
Timestamp::from_second(secs as i64).expect("timestamp out of range")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +62,14 @@ impl NoteId {
|
|||
pub fn new() -> Self {
|
||||
Self(TimestampId::new())
|
||||
}
|
||||
|
||||
pub fn timestamp(self) -> Timestamp {
|
||||
self.0.timestamp()
|
||||
}
|
||||
|
||||
pub fn time_utc(self) -> Zoned {
|
||||
self.timestamp().to_zoned(TimeZone::UTC)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for NoteId {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ mod v1;
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use git2::{ErrorCode, Repository};
|
||||
use git2::{Commit, ErrorCode, Oid, Reference, Repository};
|
||||
use jiff::Zoned;
|
||||
|
||||
pub use self::v1::{Repo, VERSION};
|
||||
|
||||
|
|
@ -15,18 +16,19 @@ pub fn init(path: &Path) -> anyhow::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn read_version(repo: &Repository) -> anyhow::Result<u32> {
|
||||
let head = match repo.head() {
|
||||
Ok(head) => head,
|
||||
Err(error) if error.code() == ErrorCode::UnbornBranch => return Ok(0),
|
||||
fn read_head(repository: &Repository) -> anyhow::Result<Option<Reference<'_>>> {
|
||||
match repository.head() {
|
||||
Ok(head) => Ok(Some(head)),
|
||||
Err(error) if error.code() == ErrorCode::UnbornBranch => Ok(None),
|
||||
Err(error) => Err(error)?,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let object = head
|
||||
.peel_to_commit()?
|
||||
fn read_version(repository: &Repository, commit: &Commit<'_>) -> anyhow::Result<u32> {
|
||||
let object = commit
|
||||
.tree()?
|
||||
.get_path(VERSION_FILE.as_ref())?
|
||||
.to_object(repo)?;
|
||||
.to_object(repository)?;
|
||||
|
||||
let blob = object
|
||||
.as_blob()
|
||||
|
|
@ -39,21 +41,59 @@ fn read_version(repo: &Repository) -> anyhow::Result<u32> {
|
|||
}
|
||||
|
||||
pub fn load_version(path: &Path) -> anyhow::Result<u32> {
|
||||
let repo = Repository::open_bare(path)?;
|
||||
let version = read_version(&repo)?;
|
||||
let repository = Repository::open_bare(path)?;
|
||||
let Some(head) = read_head(&repository)? else {
|
||||
return Ok(v0::VERSION);
|
||||
};
|
||||
let commit = head.peel_to_commit()?;
|
||||
let version = read_version(&repository, &commit)?;
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
pub fn load(path: &Path) -> anyhow::Result<Repo> {
|
||||
let repository = Repository::open_bare(path)?;
|
||||
let version = read_version(&repository)?;
|
||||
let Some(head) = read_head(&repository)? else {
|
||||
return Ok(v0::Repo::load().migrate());
|
||||
};
|
||||
let commit = head.peel_to_commit()?;
|
||||
let version = read_version(&repository, &commit)?;
|
||||
let tree = commit.tree()?;
|
||||
|
||||
#[expect(unused_qualifications)]
|
||||
let repo = match version {
|
||||
v0::VERSION => v0::Repo::load().migrate(),
|
||||
v1::VERSION => v1::Repo::load(&repository)?.migrate(),
|
||||
v1::VERSION => v1::Repo::load_from_tree(&repository, &tree)?.migrate(),
|
||||
n => bail!("invalid repo version {n}"),
|
||||
};
|
||||
|
||||
Ok(repo)
|
||||
}
|
||||
|
||||
pub fn save(path: &Path, repo: Repo) -> anyhow::Result<Oid> {
|
||||
let repository = Repository::open_bare(path)?;
|
||||
|
||||
let tree = repo.save_to_tree(&repository)?;
|
||||
|
||||
let signature = repository.signature()?;
|
||||
let message = Zoned::now().to_string();
|
||||
|
||||
// TODO Check that the repo is actually based on this commit.
|
||||
let parent = match read_head(&repository)? {
|
||||
None => None,
|
||||
Some(parent) => Some(parent.peel_to_commit()?),
|
||||
};
|
||||
let parents = match &parent {
|
||||
None => vec![],
|
||||
Some(parent) => vec![parent],
|
||||
};
|
||||
|
||||
let oid = repository.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
&message,
|
||||
&tree,
|
||||
&parents,
|
||||
)?;
|
||||
|
||||
Ok(oid)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use super::v1;
|
||||
|
||||
pub const VERSION: u32 = 0;
|
||||
|
|
@ -12,9 +10,6 @@ impl Repo {
|
|||
}
|
||||
|
||||
pub fn migrate(self) -> super::Repo {
|
||||
v1::Repo {
|
||||
notes: HashMap::new(),
|
||||
}
|
||||
.migrate()
|
||||
v1::Repo { notes: vec![] }.migrate()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use git2::Repository;
|
||||
use anyhow::anyhow;
|
||||
use git2::{FileMode, Repository, Tree, TreeBuilder, TreeEntry, TreeWalkMode, TreeWalkResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ids::NoteId;
|
||||
|
|
@ -9,18 +8,118 @@ pub const VERSION: u32 = 1;
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Note {
|
||||
pub id: NoteId,
|
||||
pub text: String,
|
||||
pub children: Vec<NoteId>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Repo {
|
||||
pub notes: HashMap<NoteId, Note>,
|
||||
pub notes: Vec<Note>,
|
||||
}
|
||||
|
||||
fn add_note_to_tree(
|
||||
repository: &Repository,
|
||||
target: &mut TreeBuilder<'_>,
|
||||
note: &Note,
|
||||
) -> anyhow::Result<()> {
|
||||
let filename = format!("{}.json", note.id);
|
||||
let oid = repository.blob(&serde_json::to_vec(note)?)?;
|
||||
target.insert(filename, oid, FileMode::Blob.into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_tree_to_tree(
|
||||
target: &mut TreeBuilder<'_>,
|
||||
tree: &TreeBuilder<'_>,
|
||||
filename: String,
|
||||
) -> anyhow::Result<()> {
|
||||
if tree.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let oid = tree.write()?;
|
||||
target.insert(filename, oid, FileMode::Tree.into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_note(
|
||||
repository: &Repository,
|
||||
entry: &TreeEntry<'_>,
|
||||
notes: &mut Vec<Note>,
|
||||
) -> anyhow::Result<()> {
|
||||
let object = entry.to_object(repository)?;
|
||||
let content = object
|
||||
.as_blob()
|
||||
.ok_or(anyhow!("json file is not a blob!?"))?
|
||||
.content();
|
||||
let note = serde_json::from_slice(content)?;
|
||||
notes.push(note);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Repo {
|
||||
pub fn load(repository: &Repository) -> anyhow::Result<Self> {
|
||||
todo!()
|
||||
pub fn load_from_tree(repository: &Repository, tree: &Tree<'_>) -> anyhow::Result<Self> {
|
||||
let mut notes = vec![];
|
||||
let mut error: Option<anyhow::Error> = None;
|
||||
|
||||
tree.walk(TreeWalkMode::PreOrder, |name, entry| {
|
||||
if name.ends_with(".json") {
|
||||
if let Err(err) = load_note(repository, entry, &mut notes) {
|
||||
error = Some(err);
|
||||
return TreeWalkResult::Abort;
|
||||
}
|
||||
}
|
||||
TreeWalkResult::Ok
|
||||
})?;
|
||||
|
||||
if let Some(err) = error {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(Self { notes })
|
||||
}
|
||||
|
||||
pub fn save_to_tree(mut self, repository: &Repository) -> anyhow::Result<Tree<'_>> {
|
||||
self.notes.sort_unstable_by_key(|it| it.id);
|
||||
|
||||
let mut root_tree = repository.treebuilder(None)?;
|
||||
let mut year = 0;
|
||||
let mut year_tree = repository.treebuilder(None)?;
|
||||
let mut month = 0;
|
||||
let mut month_tree = repository.treebuilder(None)?;
|
||||
let mut day = 0;
|
||||
let mut day_tree = repository.treebuilder(None)?;
|
||||
|
||||
for note in self.notes {
|
||||
let time = note.id.time_utc();
|
||||
|
||||
if day != time.day() || month != time.month() || year != time.year() {
|
||||
add_tree_to_tree(&mut month_tree, &day_tree, format!("{day:02}"))?;
|
||||
day_tree.clear()?;
|
||||
day = time.day();
|
||||
}
|
||||
|
||||
if month != time.month() || year != time.year() {
|
||||
add_tree_to_tree(&mut year_tree, &month_tree, format!("{month:02}"))?;
|
||||
month_tree.clear()?;
|
||||
month = time.month();
|
||||
}
|
||||
|
||||
if year != time.year() {
|
||||
add_tree_to_tree(&mut root_tree, &year_tree, format!("{year:04}"))?;
|
||||
year_tree.clear()?;
|
||||
year = time.year();
|
||||
}
|
||||
|
||||
add_note_to_tree(repository, &mut day_tree, ¬e)?;
|
||||
}
|
||||
|
||||
add_tree_to_tree(&mut month_tree, &day_tree, format!("{day:02}"))?;
|
||||
add_tree_to_tree(&mut year_tree, &month_tree, format!("{month:02}"))?;
|
||||
add_tree_to_tree(&mut root_tree, &year_tree, format!("{year:04}"))?;
|
||||
|
||||
let tree = repository.find_tree(root_tree.write()?)?;
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
pub fn migrate(self) -> Self {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue