use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use git2::{Repository, Signature};
pub const BEADS_DIR: &str = ".beads";
pub const DB_FILE: &str = "beads.db";
pub const JSONL_FILE: &str = "issues.jsonl";
pub struct GitRepo {
repo: Repository,
}
impl GitRepo {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let repo = Repository::open(path.as_ref())
.with_context(|| format!("Failed to open git repository at {:?}", path.as_ref()))?;
Ok(Self { repo })
}
pub fn discover() -> Result<Self> {
let repo = Repository::discover(".")
.context("Failed to discover git repository")?;
Ok(Self { repo })
}
pub fn workdir(&self) -> Option<&Path> {
self.repo.workdir()
}
pub fn find_beads_dir(&self) -> Option<PathBuf> {
let workdir = self.workdir()?;
let beads_path = workdir.join(BEADS_DIR);
if beads_path.exists() {
Some(beads_path)
} else {
None
}
}
pub fn database_path(&self) -> Option<PathBuf> {
self.find_beads_dir().map(|p| p.join(DB_FILE))
}
pub fn jsonl_path(&self) -> Option<PathBuf> {
self.find_beads_dir().map(|p| p.join(JSONL_FILE))
}
pub fn current_branch(&self) -> Result<String> {
let head = self.repo.head()
.context("Failed to get HEAD")?;
let name = head.shorthand()
.unwrap_or("HEAD")
.to_string();
Ok(name)
}
pub fn has_beads_changes(&self) -> Result<bool> {
let statuses = self.repo.statuses(None)
.context("Failed to get repository status")?;
for status in statuses.iter() {
if let Some(path) = status.path() {
if path.starts_with(BEADS_DIR) {
return Ok(true);
}
}
}
Ok(false)
}
pub fn stage_beads_files(&self) -> Result<()> {
let mut index = self.repo.index()
.context("Failed to get index")?;
index.add_all([BEADS_DIR], git2::IndexAddOption::DEFAULT, None)
.context("Failed to stage .beads files")?;
index.write()
.context("Failed to write index")?;
Ok(())
}
pub fn commit(&self, message: &str, author_name: &str, author_email: &str) -> Result<git2::Oid> {
let mut index = self.repo.index()
.context("Failed to get index")?;
let tree_id = index.write_tree()
.context("Failed to write tree")?;
let tree = self.repo.find_tree(tree_id)
.context("Failed to find tree")?;
let signature = Signature::now(author_name, author_email)
.context("Failed to create signature")?;
let head = self.repo.head()
.context("Failed to get HEAD")?;
let parent = self.repo.find_commit(head.target().unwrap())
.context("Failed to find parent commit")?;
let oid = self.repo.commit(
Some("HEAD"),
&signature,
&signature,
message,
&tree,
&[&parent],
).context("Failed to create commit")?;
Ok(oid)
}
pub fn commit_beads_changes(&self, message: &str, actor: &str) -> Result<git2::Oid> {
self.stage_beads_files()?;
let email = self.repo.config()
.ok()
.and_then(|cfg| cfg.get_string("user.email").ok())
.unwrap_or_else(|| format!("{}@beads", actor));
self.commit(message, actor, &email)
}
pub fn get_user_name(&self) -> Option<String> {
self.repo.config()
.ok()
.and_then(|cfg| cfg.get_string("user.name").ok())
}
pub fn get_user_email(&self) -> Option<String> {
self.repo.config()
.ok()
.and_then(|cfg| cfg.get_string("user.email").ok())
}
}
pub fn find_beads_dir() -> Option<PathBuf> {
let mut current = std::env::current_dir().ok()?;
loop {
let beads_path = current.join(BEADS_DIR);
if beads_path.exists() && beads_path.is_dir() {
return Some(beads_path);
}
if !current.pop() {
return None;
}
}
}
pub fn find_database_path() -> Option<PathBuf> {
find_beads_dir().map(|p| p.join(DB_FILE))
}
pub fn find_jsonl_path() -> Option<PathBuf> {
find_beads_dir().map(|p| p.join(JSONL_FILE))
}
pub fn init_beads_dir(path: impl AsRef<Path>) -> Result<PathBuf> {
let beads_path = path.as_ref().join(BEADS_DIR);
std::fs::create_dir_all(&beads_path)
.with_context(|| format!("Failed to create .beads directory at {:?}", beads_path))?;
Ok(beads_path)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_find_beads_dir_in_current() {
let temp = TempDir::new().unwrap();
let beads_path = temp.path().join(BEADS_DIR);
std::fs::create_dir(&beads_path).unwrap();
let old_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(temp.path()).unwrap();
let found = find_beads_dir();
assert!(found.is_some());
assert!(found.unwrap().ends_with(BEADS_DIR));
std::env::set_current_dir(old_dir).unwrap();
}
#[test]
fn test_init_beads_dir() {
let temp = TempDir::new().unwrap();
let result = init_beads_dir(temp.path());
assert!(result.is_ok());
assert!(temp.path().join(BEADS_DIR).exists());
}
}