move entomologist database to new library file #21

Merged
seb merged 2 commits from 03/issues-db-into-library into main 2025-07-13 21:52:26 -06:00
3 changed files with 115 additions and 99 deletions

View file

@ -86,92 +86,13 @@ enum Commands {
}, },
} }
/// The main function looks at the command-line arguments and determines
/// from there where to get the Issues Database to operate on.
///
/// * If the user specified `--issues-dir` we use that.
///
/// * If the user specified `--issues-branch` we make sure the branch
/// exists, then use that.
///
/// * If the user specified neither, we use the default branch
/// `entomologist-data` (after ensuring that it exists).
///
/// * If the user specified both, it's an operator error and we abort.
///
/// The result of that code populates an IssuesDatabaseSource object,
/// that gets used later to access the database.
enum IssuesDatabaseSource<'a> {
Dir(&'a std::path::Path),
Branch(&'a str),
}
/// The IssuesDatabase type is a "fat path". It holds a PathBuf pointing
/// at the issues database directory, and optionally a Worktree object
/// corresponding to that path.
///
/// The worktree field itself is never read: we put its path in `dir`
/// and that's all that the calling code cares about.
///
/// The Worktree object is included here *when* the IssuesDatabaseSource
/// is a branch. In this case a git worktree is created to hold the
/// checkout of the branch. When the IssueDatabase object is dropped,
/// the contained/owned Worktree object is dropped, which deletes the
/// worktree directory from the filesystem and prunes the worktree from
/// git's worktree list.
struct IssuesDatabase {
dir: std::path::PathBuf,
#[allow(dead_code)]
worktree: Option<entomologist::git::Worktree>,
}
enum IssuesDatabaseAccess {
ReadOnly,
ReadWrite,
}
fn make_issues_database(
issues_database_source: &IssuesDatabaseSource,
access_type: IssuesDatabaseAccess,
) -> anyhow::Result<IssuesDatabase> {
match issues_database_source {
IssuesDatabaseSource::Dir(dir) => Ok(IssuesDatabase {
dir: std::path::PathBuf::from(dir),
worktree: None,
}),
IssuesDatabaseSource::Branch(branch) => {
let worktree = match access_type {
IssuesDatabaseAccess::ReadOnly => {
entomologist::git::Worktree::new_detached(branch)?
}
IssuesDatabaseAccess::ReadWrite => entomologist::git::Worktree::new(branch)?,
};
Ok(IssuesDatabase {
dir: std::path::PathBuf::from(worktree.path()),
worktree: Some(worktree),
})
}
}
}
fn read_issues_database(
issues_database_source: &IssuesDatabaseSource,
) -> anyhow::Result<entomologist::issues::Issues> {
let issues_database =
make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadOnly)?;
Ok(entomologist::issues::Issues::new_from_dir(
&issues_database.dir,
)?)
}
fn handle_command( fn handle_command(
args: &Args, args: &Args,
issues_database_source: &IssuesDatabaseSource, issues_database_source: &entomologist::database::IssuesDatabaseSource,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
match &args.command { match &args.command {
Commands::List { filter } => { Commands::List { filter } => {
let issues = read_issues_database(issues_database_source)?; let issues = entomologist::database::read_issues_database(issues_database_source)?;
let filter = entomologist::Filter::new_from_str(filter)?; let filter = entomologist::Filter::new_from_str(filter)?;
let mut uuids_by_state = std::collections::HashMap::< let mut uuids_by_state = std::collections::HashMap::<
entomologist::issue::State, entomologist::issue::State,
@ -271,7 +192,7 @@ fn handle_command(
Commands::New { description } => { Commands::New { description } => {
let issues_database = let issues_database =
make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadWrite)?; entomologist::database::make_issues_database(issues_database_source, entomologist::database::IssuesDatabaseAccess::ReadWrite)?;
match entomologist::issue::Issue::new(&issues_database.dir, description) { match entomologist::issue::Issue::new(&issues_database.dir, description) {
Err(entomologist::issue::IssueError::EmptyDescription) => { Err(entomologist::issue::IssueError::EmptyDescription) => {
println!("no new issue created"); println!("no new issue created");
@ -289,7 +210,7 @@ fn handle_command(
Commands::Edit { uuid } => { Commands::Edit { uuid } => {
let issues_database = let issues_database =
make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadWrite)?; entomologist::database::make_issues_database(issues_database_source, entomologist::database::IssuesDatabaseAccess::ReadWrite)?;
let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?;
if let Some(issue) = issues.get_mut_issue(uuid) { if let Some(issue) = issues.get_mut_issue(uuid) {
match issue.edit_description() { match issue.edit_description() {
@ -323,7 +244,7 @@ fn handle_command(
} }
Commands::Show { issue_id } => { Commands::Show { issue_id } => {
let issues = read_issues_database(issues_database_source)?; let issues = entomologist::database::read_issues_database(issues_database_source)?;
match issues.get_issue(issue_id) { match issues.get_issue(issue_id) {
Some(issue) => { Some(issue) => {
println!("issue {}", issue_id); println!("issue {}", issue_id);
@ -359,7 +280,7 @@ fn handle_command(
} => match new_state { } => match new_state {
Some(new_state) => { Some(new_state) => {
let issues_database = let issues_database =
make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadWrite)?; entomologist::database::make_issues_database(issues_database_source, entomologist::database::IssuesDatabaseAccess::ReadWrite)?;
let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?;
match issues.issues.get_mut(issue_id) { match issues.issues.get_mut(issue_id) {
Some(issue) => { Some(issue) => {
@ -374,7 +295,7 @@ fn handle_command(
} }
} }
None => { None => {
let issues = read_issues_database(issues_database_source)?; let issues = entomologist::database::read_issues_database(issues_database_source)?;
match issues.issues.get(issue_id) { match issues.issues.get(issue_id) {
Some(issue) => { Some(issue) => {
println!("issue: {}", issue_id); println!("issue: {}", issue_id);
@ -392,7 +313,7 @@ fn handle_command(
description, description,
} => { } => {
let issues_database = let issues_database =
make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadWrite)?; entomologist::database::make_issues_database(issues_database_source, entomologist::database::IssuesDatabaseAccess::ReadWrite)?;
let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?;
let Some(issue) = issues.get_mut_issue(issue_id) else { let Some(issue) = issues.get_mut_issue(issue_id) else {
return Err(anyhow::anyhow!("issue {} not found", issue_id)); return Err(anyhow::anyhow!("issue {} not found", issue_id));
@ -417,9 +338,9 @@ fn handle_command(
} }
Commands::Sync { remote } => { Commands::Sync { remote } => {
if let IssuesDatabaseSource::Branch(branch) = issues_database_source { if let entomologist::database::IssuesDatabaseSource::Branch(branch) = issues_database_source {
let issues_database = let issues_database =
make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadWrite)?; entomologist::database::make_issues_database(issues_database_source, entomologist::database::IssuesDatabaseAccess::ReadWrite)?;
entomologist::git::sync(&issues_database.dir, remote, branch)?; entomologist::git::sync(&issues_database.dir, remote, branch)?;
println!("synced {:?} with {:?}", branch, remote); println!("synced {:?} with {:?}", branch, remote);
} else { } else {
@ -433,7 +354,7 @@ fn handle_command(
issue_id, issue_id,
new_assignee, new_assignee,
} => { } => {
let issues = read_issues_database(issues_database_source)?; let issues = entomologist::database::read_issues_database(issues_database_source)?;
let Some(original_issue) = issues.issues.get(issue_id) else { let Some(original_issue) = issues.issues.get(issue_id) else {
return Err(anyhow::anyhow!("issue {} not found", issue_id)); return Err(anyhow::anyhow!("issue {} not found", issue_id));
}; };
@ -444,9 +365,9 @@ fn handle_command(
println!("issue: {}", issue_id); println!("issue: {}", issue_id);
match new_assignee { match new_assignee {
Some(new_assignee) => { Some(new_assignee) => {
let issues_database = make_issues_database( let issues_database = entomologist::database::make_issues_database(
issues_database_source, issues_database_source,
IssuesDatabaseAccess::ReadWrite, entomologist::database::IssuesDatabaseAccess::ReadWrite,
)?; )?;
let mut issues = let mut issues =
entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; entomologist::issues::Issues::new_from_dir(&issues_database.dir)?;
@ -463,16 +384,16 @@ fn handle_command(
} }
Commands::Tag { issue_id, tag } => { Commands::Tag { issue_id, tag } => {
let issues = read_issues_database(issues_database_source)?; let issues = entomologist::database::read_issues_database(issues_database_source)?;
let Some(issue) = issues.issues.get(issue_id) else { let Some(issue) = issues.issues.get(issue_id) else {
return Err(anyhow::anyhow!("issue {} not found", issue_id)); return Err(anyhow::anyhow!("issue {} not found", issue_id));
}; };
match tag { match tag {
Some(tag) => { Some(tag) => {
// Add or remove tag. // Add or remove tag.
let issues_database = make_issues_database( let issues_database = entomologist::database::make_issues_database(
issues_database_source, issues_database_source,
IssuesDatabaseAccess::ReadWrite, entomologist::database::IssuesDatabaseAccess::ReadWrite,
)?; )?;
let mut issues = let mut issues =
entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; entomologist::issues::Issues::new_from_dir(&issues_database.dir)?;
@ -519,9 +440,9 @@ fn main() -> anyhow::Result<()> {
// println!("{:?}", args); // println!("{:?}", args);
let issues_database_source = match (&args.issues_dir, &args.issues_branch) { let issues_database_source = match (&args.issues_dir, &args.issues_branch) {
(Some(dir), None) => IssuesDatabaseSource::Dir(std::path::Path::new(dir)), (Some(dir), None) => entomologist::database::IssuesDatabaseSource::Dir(std::path::Path::new(dir)),
(None, Some(branch)) => IssuesDatabaseSource::Branch(branch), (None, Some(branch)) => entomologist::database::IssuesDatabaseSource::Branch(branch),
(None, None) => IssuesDatabaseSource::Branch("entomologist-data"), (None, None) => entomologist::database::IssuesDatabaseSource::Branch("entomologist-data"),
(Some(_), Some(_)) => { (Some(_), Some(_)) => {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"don't specify both `--issues-dir` and `--issues-branch`" "don't specify both `--issues-dir` and `--issues-branch`"
@ -529,7 +450,7 @@ fn main() -> anyhow::Result<()> {
} }
}; };
if let IssuesDatabaseSource::Branch(branch) = &issues_database_source { if let entomologist::database::IssuesDatabaseSource::Branch(branch) = &issues_database_source {
if !entomologist::git::git_branch_exists(branch)? { if !entomologist::git::git_branch_exists(branch)? {
entomologist::git::create_orphan_branch(branch)?; entomologist::git::create_orphan_branch(branch)?;
} }

94
src/database.rs Normal file
View file

@ -0,0 +1,94 @@
use thiserror::Error;
use crate::{git::GitError, issues::ReadIssuesError};
/// Errors that the DB can emit:
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
IssuesError(#[from] ReadIssuesError),
#[error(transparent)]
GitError(#[from] GitError),
}
/// The main function looks at the command-line arguments and determines
/// from there where to get the Issues Database to operate on.
///
/// * If the user specified `--issues-dir` we use that.
///
/// * If the user specified `--issues-branch` we make sure the branch
/// exists, then use that.
///
/// * If the user specified neither, we use the default branch
/// `entomologist-data` (after ensuring that it exists).
///
/// * If the user specified both, it's an operator error and we abort.
///
/// The result of that code populates an IssuesDatabaseSource object,
/// that gets used later to access the database.
pub enum IssuesDatabaseSource<'a> {
Dir(&'a std::path::Path),
Branch(&'a str),
}
/// The IssuesDatabase type is a "fat path". It holds a PathBuf pointing
/// at the issues database directory, and optionally a Worktree object
/// corresponding to that path.
///
/// The worktree field itself is never read: we put its path in `dir`
/// and that's all that the calling code cares about.
///
/// The Worktree object is included here *when* the IssuesDatabaseSource
/// is a branch. In this case a git worktree is created to hold the
/// checkout of the branch. When the IssueDatabase object is dropped,
/// the contained/owned Worktree object is dropped, which deletes the
/// worktree directory from the filesystem and prunes the worktree from
/// git's worktree list.
pub struct IssuesDatabase {
pub dir: std::path::PathBuf,
#[allow(dead_code)]
pub worktree: Option<crate::git::Worktree>,
}
pub enum IssuesDatabaseAccess {
ReadOnly,
ReadWrite,
}
pub fn make_issues_database(
issues_database_source: &IssuesDatabaseSource,
access_type: IssuesDatabaseAccess,
) -> Result<IssuesDatabase, Error> {
match issues_database_source {
IssuesDatabaseSource::Dir(dir) => Ok(IssuesDatabase {
dir: std::path::PathBuf::from(dir),
worktree: None,
}),
IssuesDatabaseSource::Branch(branch) => {
let worktree = match access_type {
IssuesDatabaseAccess::ReadOnly => {
crate::git::Worktree::new_detached(branch)?
}
IssuesDatabaseAccess::ReadWrite => crate::git::Worktree::new(branch)?,
};
Ok(IssuesDatabase {
dir: std::path::PathBuf::from(worktree.path()),
worktree: Some(worktree),
})
}
}
}
pub fn read_issues_database(
issues_database_source: &IssuesDatabaseSource,
) -> Result<crate::issues::Issues, Error> {
let issues_database =
make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadOnly)?;
Ok(crate::issues::Issues::new_from_dir(
&issues_database.dir,
)?)
}

View file

@ -4,6 +4,7 @@ pub mod comment;
pub mod git; pub mod git;
pub mod issue; pub mod issue;
pub mod issues; pub mod issues;
pub mod database;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ParseFilterError { pub enum ParseFilterError {