Add repo add and repo show commands

This commit is contained in:
Joscha 2025-04-30 00:49:04 +02:00
parent b922af9283
commit 2c5ff584db
10 changed files with 219 additions and 4 deletions

View file

@ -2,6 +2,7 @@ use clap::Parser;
use crate::Environment; use crate::Environment;
mod repo;
mod status; mod status;
mod tidy; mod tidy;
@ -9,6 +10,10 @@ mod tidy;
pub enum Command { pub enum Command {
Status(status::Command), Status(status::Command),
Tidy(tidy::Command), Tidy(tidy::Command),
Repo {
#[command(subcommand)]
command: repo::Command,
},
} }
impl Command { impl Command {
@ -16,6 +21,7 @@ impl Command {
match self { match self {
Self::Status(command) => command.run(env), Self::Status(command) => command.run(env),
Self::Tidy(command) => command.run(env), Self::Tidy(command) => command.run(env),
Self::Repo { command } => command.run(env),
} }
} }
} }

View file

@ -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),
}
}
}

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -8,9 +8,11 @@ mod lockfile;
mod v0; mod v0;
mod v1; mod v1;
pub use crate::repo::VERSION as REPO_VERSION;
pub use self::{ pub use self::{
datadir::{LockedDataDir, UnlockedDataDir}, 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<()> { fn migrate(dir: &LockedDataDir) -> anyhow::Result<()> {

View file

@ -3,7 +3,10 @@ use std::{collections::HashMap, fs, path::PathBuf};
use anyhow::anyhow; use anyhow::anyhow;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::ids::RepoId; use crate::{
ids::RepoId,
repo::{self, Repo},
};
use super::{LockedDataDir, UnlockedDataDir}; use super::{LockedDataDir, UnlockedDataDir};
@ -23,6 +26,23 @@ impl State {
} }
} }
} }
pub fn resolve_repo_identifier(&self, identifier: &str) -> Option<RepoId> {
// 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::<RepoId>() {
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 { 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) dir.write_json(&state_file(dir), &state)
} }
pub fn load_repo_version(dir: &UnlockedDataDir, id: RepoId) -> anyhow::Result<u32> {
repo::load_version(&repo_dir(dir, id))
}
pub fn load_repo(dir: &UnlockedDataDir, id: RepoId) -> anyhow::Result<Repo> {
repo::load(&repo_dir(dir, id))
}
pub fn add_repo(dir: &LockedDataDir, name: String) -> anyhow::Result<RepoId> { pub fn add_repo(dir: &LockedDataDir, name: String) -> anyhow::Result<RepoId> {
let id = RepoId::new(); let id = RepoId::new();
@ -53,8 +81,8 @@ pub fn add_repo(dir: &LockedDataDir, name: String) -> anyhow::Result<RepoId> {
state.repos.insert(id, name); state.repos.insert(id, name);
save_state(dir, state)?; save_state(dir, state)?;
fs::create_dir_all(repos_dir(dir))?; repo::init(&repo_dir(dir, id))?;
// TODO Initialize bare repo
Ok(id) Ok(id)
} }

View file

@ -1,5 +1,6 @@
pub mod data; pub mod data;
pub mod ids; pub mod ids;
mod repo;
pub const PROPER_NAME: &str = "GedächtNAS"; pub const PROPER_NAME: &str = "GedächtNAS";
pub const TECHNICAL_NAME: &str = "gedaechtnas"; pub const TECHNICAL_NAME: &str = "gedaechtnas";

59
gdn/src/repo.rs Normal file
View file

@ -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<u32> {
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::<u32>()?;
Ok(version)
}
pub fn load_version(path: &Path) -> anyhow::Result<u32> {
let repo = Repository::open_bare(path)?;
let version = read_version(&repo)?;
Ok(version)
}
pub fn load(path: &Path) -> anyhow::Result<Repo> {
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)
}

20
gdn/src/repo/v0.rs Normal file
View file

@ -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()
}
}

29
gdn/src/repo/v1.rs Normal file
View file

@ -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<NoteId>,
}
#[derive(Default)]
pub struct Repo {
pub notes: HashMap<NoteId, Note>,
}
impl Repo {
pub fn load(repository: &Repository) -> anyhow::Result<Self> {
todo!()
}
pub fn migrate(self) -> super::Repo {
self
}
}