Compare commits

..

6 commits

Author SHA1 Message Date
64022b16fa add a Todo file, ironically 2025-07-05 21:20:48 -06:00
26c98591b5 start adding ent binary 2025-07-05 21:20:44 -06:00
e8910b906a add git support
This mostly provides an abstraction for "ephemeral worktrees", which
is a branch checked out in a worktree, to be read and maybe modified,
and the worktree is deleted/pruned when we're done with it.

There are also some helper functions for doing git things, the most
important one creates an orphaned branch.

The intent is to keep all the issues in a git branch.  When we want to
do anything with issues (list them, add new issues, modify an issue,
etc) we check the issues branch out into an ephemeral worktree, modify
the branch, and delete the worktree.
2025-07-05 21:20:16 -06:00
d94c991eaa add dependency tracking to issue type 2025-07-05 21:19:18 -06:00
16c6288cee start adding Issues struct
This holds everything there is to know about everything, for now that's
all issues but in the future there might be more?
2025-07-05 21:18:36 -06:00
b9979f5e9e start adding Issue struct
This abstracts a single issue.
2025-07-05 21:18:25 -06:00
3 changed files with 116 additions and 25 deletions

View file

@ -6,8 +6,8 @@ edition = "2024"
[dependencies]
anyhow = "1.0.95"
clap = { version = "4.5.26", features = ["derive"] }
mktemp = "0.5.1"
rand = "0.9.1"
serde = { version = "1.0.217", features = ["derive"] }
tempfile = "3.20.0"
thiserror = "2.0.11"
toml = "0.8.19"

View file

@ -4,8 +4,12 @@ use clap::Parser;
#[command(version, about, long_about = None)]
struct Args {
/// Directory containing issues.
#[arg(short, long)]
issues_dir: String,
#[arg(short = 'd', long)]
issues_dir: Option<String>,
/// Branch containing issues.
#[arg(short = 'b', long)]
issues_branch: Option<String>,
/// Type of behavior/output.
#[command(subcommand)]
@ -24,14 +28,11 @@ enum Commands {
},
}
fn main() -> anyhow::Result<()> {
let args: Args = Args::parse();
// println!("{:?}", args);
match args.command {
fn handle_command(args: &Args, issues_dir: &std::path::Path) -> anyhow::Result<()> {
match &args.command {
Commands::List => {
let issues =
entomologist::issues::Issues::new_from_dir(std::path::Path::new(&args.issues_dir))?;
entomologist::issues::Issues::new_from_dir(std::path::Path::new(issues_dir))?;
for (uuid, issue) in issues.issues.iter() {
println!("{} {} ({:?})", uuid, issue.title, issue.state);
}
@ -46,3 +47,31 @@ fn main() -> anyhow::Result<()> {
Ok(())
}
fn main() -> anyhow::Result<()> {
let args: Args = Args::parse();
// println!("{:?}", args);
if let (Some(_), Some(_)) = (&args.issues_dir, &args.issues_branch) {
return Err(anyhow::anyhow!(
"don't specify both `--issues-dir` and `--issues-branch`"
));
}
if let Some(dir) = &args.issues_dir {
let dir = std::path::Path::new(dir);
handle_command(&args, dir)?;
} else {
let branch = match &args.issues_branch {
Some(branch) => branch,
None => "entomologist-data",
};
if !entomologist::git::git_branch_exists(branch)? {
entomologist::git::create_orphan_branch(branch)?;
}
let worktree = entomologist::git::Worktree::new(branch)?;
handle_command(&args, worktree.path())?;
}
Ok(())
}

View file

@ -8,6 +8,41 @@ pub enum GitError {
Oops,
}
#[derive(Debug)]
/// `Worktree` is a struct that manages a temporary directory containing
/// a checkout of a specific branch. The worktree is removed and pruned
/// when the `Worktree` struct is dropped.
pub struct Worktree {
path: tempfile::TempDir,
}
impl Drop for Worktree {
fn drop(&mut self) {
let _result = std::process::Command::new("git")
.args(["worktree", "remove", &self.path.path().to_string_lossy()])
.output();
}
}
impl Worktree {
pub fn new(branch: &str) -> Result<Worktree, GitError> {
let path = tempfile::tempdir()?;
let result = std::process::Command::new("git")
.args(["worktree", "add", &path.path().to_string_lossy(), branch])
.output()?;
if !result.status.success() {
println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap());
println!("stderr: {}", std::str::from_utf8(&result.stderr).unwrap());
return Err(GitError::Oops);
}
Ok(Self { path })
}
pub fn path(&self) -> &std::path::Path {
self.path.as_ref()
}
}
pub fn checkout_branch_in_worktree(
branch: &str,
worktree_dir: &std::path::Path,
@ -47,10 +82,17 @@ pub fn git_remove_branch(branch: &str) -> Result<(), GitError> {
Ok(())
}
pub fn git_branch_exists(branch: &str) -> Result<bool, GitError> {
let result = std::process::Command::new("git")
.args(["show-ref", "--quiet", branch])
.output()?;
return Ok(result.status.success());
}
pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> {
{
let tmp_worktree = mktemp::Temp::new_path();
create_orphan_branch_at_path(branch, &tmp_worktree.to_path_buf())?;
let tmp_worktree = tempfile::tempdir().unwrap();
create_orphan_branch_at_path(branch, tmp_worktree.path())?;
}
// The temp dir is now removed / cleaned up.
@ -65,16 +107,14 @@ pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> {
Ok(())
}
pub fn create_orphan_branch_at_path(
branch: &str,
worktree_path: &std::path::PathBuf,
) -> Result<(), GitError> {
let Some(worktree_dir) = worktree_path.to_str() else {
return Err(GitError::Oops);
};
fn create_orphan_branch_at_path(
branch: &str,
worktree_path: &std::path::Path,
) -> Result<(), GitError> {
let worktree_dir = worktree_path.to_string_lossy();
let result = std::process::Command::new("git")
.args(["worktree", "add", "--orphan", "-b", branch, worktree_dir])
.args(["worktree", "add", "--orphan", "-b", branch, &worktree_dir])
.output()?;
if !result.status.success() {
println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap());
@ -82,7 +122,7 @@ pub fn create_orphan_branch_at_path(
return Err(GitError::Oops);
}
let mut readme_filename = worktree_path.clone();
let mut readme_filename = std::path::PathBuf::from(worktree_path);
readme_filename.push("README.md");
let mut readme = std::fs::File::create(readme_filename)?;
write!(
@ -92,7 +132,7 @@ pub fn create_orphan_branch_at_path(
let result = std::process::Command::new("git")
.args(["add", "README.md"])
.current_dir(worktree_dir)
.current_dir(worktree_path)
.output()?;
if !result.status.success() {
println!("stdout: {}", std::str::from_utf8(&result.stdout).unwrap());
@ -119,12 +159,19 @@ mod tests {
#[test]
fn test_worktree() {
let mut p = std::path::PathBuf::new();
{
let temp_worktree = mktemp::Temp::new_path();
checkout_branch_in_worktree("origin/main", temp_worktree.as_path()).unwrap();
// The temporary worktree directory is removed when the Temp variable is dropped.
let worktree = Worktree::new("origin/main").unwrap();
p.push(worktree.path());
assert!(p.exists());
let mut p2 = p.clone();
p2.push("README.md");
assert!(p2.exists());
}
git_worktree_prune().unwrap();
// The temporary worktree directory is removed when the Temp variable is dropped.
assert!(!p.exists());
}
#[test]
@ -135,4 +182,19 @@ mod tests {
create_orphan_branch(&branch).unwrap();
git_remove_branch(&branch).unwrap();
}
#[test]
fn test_branch_exists_0() {
let r = git_branch_exists("main").unwrap();
assert_eq!(r, true);
}
#[test]
fn test_branch_exists_1() {
let rnd: u128 = rand::random();
let mut branch = std::string::String::from("entomologist-missing-branch-");
branch.push_str(&format!("{:0x}", rnd));
let r = git_branch_exists(&branch).unwrap();
assert_eq!(r, false);
}
}