From 2c5ff584db07ae185c655cc9ff28f2ab98f7a5dd Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 30 Apr 2025 00:49:04 +0200 Subject: [PATCH] Add repo add and repo show commands --- gdn-cli/src/commands.rs | 6 ++++ gdn-cli/src/commands/repo.rs | 22 ++++++++++++ gdn-cli/src/commands/repo/add.rs | 18 ++++++++++ gdn-cli/src/commands/repo/show.rs | 30 ++++++++++++++++ gdn/src/data.rs | 4 ++- gdn/src/data/v1.rs | 34 ++++++++++++++++-- gdn/src/lib.rs | 1 + gdn/src/repo.rs | 59 +++++++++++++++++++++++++++++++ gdn/src/repo/v0.rs | 20 +++++++++++ gdn/src/repo/v1.rs | 29 +++++++++++++++ 10 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 gdn-cli/src/commands/repo.rs create mode 100644 gdn-cli/src/commands/repo/add.rs create mode 100644 gdn-cli/src/commands/repo/show.rs create mode 100644 gdn/src/repo.rs create mode 100644 gdn/src/repo/v0.rs create mode 100644 gdn/src/repo/v1.rs diff --git a/gdn-cli/src/commands.rs b/gdn-cli/src/commands.rs index 873bef0..27d3af5 100644 --- a/gdn-cli/src/commands.rs +++ b/gdn-cli/src/commands.rs @@ -2,6 +2,7 @@ use clap::Parser; use crate::Environment; +mod repo; mod status; mod tidy; @@ -9,6 +10,10 @@ mod tidy; pub enum Command { Status(status::Command), Tidy(tidy::Command), + Repo { + #[command(subcommand)] + command: repo::Command, + }, } impl Command { @@ -16,6 +21,7 @@ impl Command { match self { Self::Status(command) => command.run(env), Self::Tidy(command) => command.run(env), + Self::Repo { command } => command.run(env), } } } diff --git a/gdn-cli/src/commands/repo.rs b/gdn-cli/src/commands/repo.rs new file mode 100644 index 0000000..fb05c5f --- /dev/null +++ b/gdn-cli/src/commands/repo.rs @@ -0,0 +1,22 @@ +mod add; +mod show; + +use clap::Parser; + +use crate::Environment; + +/// Perform repo operations. +#[derive(Debug, Parser)] +pub enum Command { + Show(show::Command), + Add(add::Command), +} + +impl Command { + pub fn run(self, env: &Environment) -> anyhow::Result<()> { + match self { + Self::Show(command) => command.run(env), + Self::Add(command) => command.run(env), + } + } +} diff --git a/gdn-cli/src/commands/repo/add.rs b/gdn-cli/src/commands/repo/add.rs new file mode 100644 index 0000000..1e177af --- /dev/null +++ b/gdn-cli/src/commands/repo/add.rs @@ -0,0 +1,18 @@ +use clap::Parser; + +use crate::Environment; + +/// Add a new repository. +#[derive(Debug, Parser)] +pub struct Command { + name: String, +} + +impl Command { + pub fn run(self, env: &Environment) -> anyhow::Result<()> { + let data = gdn::data::open_and_migrate(env.data_dir.clone())?; + let id = gdn::data::add_repo(&data, self.name.clone())?; + println!("Added repo {} ({id}).", self.name); + Ok(()) + } +} diff --git a/gdn-cli/src/commands/repo/show.rs b/gdn-cli/src/commands/repo/show.rs new file mode 100644 index 0000000..71c6ec1 --- /dev/null +++ b/gdn-cli/src/commands/repo/show.rs @@ -0,0 +1,30 @@ +use clap::Parser; +use gdn::data::REPO_VERSION; + +use crate::Environment; + +/// Show info about a repository. +#[derive(Debug, Parser)] +pub struct Command { + repo: String, +} + +impl Command { + pub fn run(self, env: &Environment) -> anyhow::Result<()> { + let data = gdn::data::open(env.data_dir.clone())?; + let state = gdn::data::load_state(&data)?; + + let Some(id) = state.resolve_repo_identifier(&self.repo) else { + println!("No repo found for identifier {}.", self.repo); + return Ok(()); + }; + + let version = gdn::data::load_repo_version(&data, id)?; + let repo = gdn::data::load_repo(&data, id)?; + + println!("Repo version: {version} (latest: {REPO_VERSION})",); + println!("Number of notes: {}", repo.notes.len()); + + Ok(()) + } +} diff --git a/gdn/src/data.rs b/gdn/src/data.rs index b47ca10..6df707b 100644 --- a/gdn/src/data.rs +++ b/gdn/src/data.rs @@ -8,9 +8,11 @@ mod lockfile; mod v0; mod v1; +pub use crate::repo::VERSION as REPO_VERSION; + pub use self::{ datadir::{LockedDataDir, UnlockedDataDir}, - v1::{State, VERSION, load_state, tidy}, + v1::{State, VERSION, add_repo, load_repo, load_repo_version, load_state, tidy}, }; fn migrate(dir: &LockedDataDir) -> anyhow::Result<()> { diff --git a/gdn/src/data/v1.rs b/gdn/src/data/v1.rs index 0932fea..ed79305 100644 --- a/gdn/src/data/v1.rs +++ b/gdn/src/data/v1.rs @@ -3,7 +3,10 @@ use std::{collections::HashMap, fs, path::PathBuf}; use anyhow::anyhow; use serde::{Deserialize, Serialize}; -use crate::ids::RepoId; +use crate::{ + ids::RepoId, + repo::{self, Repo}, +}; use super::{LockedDataDir, UnlockedDataDir}; @@ -23,6 +26,23 @@ impl State { } } } + + pub fn resolve_repo_identifier(&self, identifier: &str) -> Option { + // If the identifier is a valid repo id, always interpret it as such. + // There must always be an unambiguous way to refer to repos. + if let Ok(id) = identifier.parse::() { + if self.repos.contains_key(&id) { + return Some(id); + } + } + + // Otherwise, interpret the identifier as a repo name and find the + // corresponding id. + self.repos + .iter() + .find(|(_, name)| *name == identifier) + .map(|(id, _)| *id) + } } pub fn state_file(dir: &UnlockedDataDir) -> PathBuf { @@ -46,6 +66,14 @@ pub fn save_state(dir: &LockedDataDir, mut state: State) -> anyhow::Result<()> { dir.write_json(&state_file(dir), &state) } +pub fn load_repo_version(dir: &UnlockedDataDir, id: RepoId) -> anyhow::Result { + repo::load_version(&repo_dir(dir, id)) +} + +pub fn load_repo(dir: &UnlockedDataDir, id: RepoId) -> anyhow::Result { + repo::load(&repo_dir(dir, id)) +} + pub fn add_repo(dir: &LockedDataDir, name: String) -> anyhow::Result { let id = RepoId::new(); @@ -53,8 +81,8 @@ pub fn add_repo(dir: &LockedDataDir, name: String) -> anyhow::Result { state.repos.insert(id, name); save_state(dir, state)?; - fs::create_dir_all(repos_dir(dir))?; - // TODO Initialize bare repo + repo::init(&repo_dir(dir, id))?; + Ok(id) } diff --git a/gdn/src/lib.rs b/gdn/src/lib.rs index 574ce1d..23ee8d5 100644 --- a/gdn/src/lib.rs +++ b/gdn/src/lib.rs @@ -1,5 +1,6 @@ pub mod data; pub mod ids; +mod repo; pub const PROPER_NAME: &str = "GedächtNAS"; pub const TECHNICAL_NAME: &str = "gedaechtnas"; diff --git a/gdn/src/repo.rs b/gdn/src/repo.rs new file mode 100644 index 0000000..996df0e --- /dev/null +++ b/gdn/src/repo.rs @@ -0,0 +1,59 @@ +mod v0; +mod v1; + +use std::path::Path; + +use anyhow::{anyhow, bail}; +use git2::{ErrorCode, Repository}; + +pub use self::v1::{Note, Repo, VERSION}; + +const VERSION_FILE: &str = "VERSION"; + +pub fn init(path: &Path) -> anyhow::Result<()> { + Repository::init_bare(path)?; + Ok(()) +} + +fn read_version(repo: &Repository) -> anyhow::Result { + let head = match repo.head() { + Ok(head) => head, + Err(error) if error.code() == ErrorCode::UnbornBranch => return Ok(0), + Err(error) => Err(error)?, + }; + + let object = head + .peel_to_commit()? + .tree()? + .get_path(VERSION_FILE.as_ref())? + .to_object(repo)?; + + let blob = object + .as_blob() + .ok_or(anyhow!("Failed to read file {VERSION_FILE}"))?; + + let content = blob.content().to_vec(); + let version = String::from_utf8(content)?.trim().parse::()?; + + Ok(version) +} + +pub fn load_version(path: &Path) -> anyhow::Result { + let repo = Repository::open_bare(path)?; + let version = read_version(&repo)?; + Ok(version) +} + +pub fn load(path: &Path) -> anyhow::Result { + let repository = Repository::open_bare(path)?; + let version = read_version(&repository)?; + + #[expect(unused_qualifications)] + let repo = match version { + 0 => v0::Repo::load().migrate(), + 1 => v1::Repo::load(&repository)?.migrate(), + n => bail!("invalid repo version {n}"), + }; + + Ok(repo) +} diff --git a/gdn/src/repo/v0.rs b/gdn/src/repo/v0.rs new file mode 100644 index 0000000..14b2e9c --- /dev/null +++ b/gdn/src/repo/v0.rs @@ -0,0 +1,20 @@ +use std::collections::HashMap; + +use super::v1; + +pub const VERSION: u32 = 0; + +pub struct Repo; + +impl Repo { + pub fn load() -> Self { + Self + } + + pub fn migrate(self) -> super::Repo { + v1::Repo { + notes: HashMap::new(), + } + .migrate() + } +} diff --git a/gdn/src/repo/v1.rs b/gdn/src/repo/v1.rs new file mode 100644 index 0000000..f43ab55 --- /dev/null +++ b/gdn/src/repo/v1.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; + +use git2::Repository; +use serde::{Deserialize, Serialize}; + +use crate::ids::NoteId; + +pub const VERSION: u32 = 1; + +#[derive(Serialize, Deserialize)] +pub struct Note { + pub text: String, + pub children: Vec, +} + +#[derive(Default)] +pub struct Repo { + pub notes: HashMap, +} + +impl Repo { + pub fn load(repository: &Repository) -> anyhow::Result { + todo!() + } + + pub fn migrate(self) -> super::Repo { + self + } +}