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.
This commit is contained in:
parent
d94c991eaa
commit
e8910b906a
3 changed files with 203 additions and 0 deletions
|
|
@ -4,6 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
rand = "0.9.1"
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
|
tempfile = "3.20.0"
|
||||||
thiserror = "2.0.11"
|
thiserror = "2.0.11"
|
||||||
toml = "0.8.19"
|
toml = "0.8.19"
|
||||||
|
|
|
||||||
200
src/git.rs
Normal file
200
src/git.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum GitError {
|
||||||
|
#[error(transparent)]
|
||||||
|
StdIoError(#[from] std::io::Error),
|
||||||
|
#[error("Oops, something went wrong")]
|
||||||
|
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,
|
||||||
|
) -> Result<(), GitError> {
|
||||||
|
let result = std::process::Command::new("git")
|
||||||
|
.args(["worktree", "add", &worktree_dir.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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn git_worktree_prune() -> Result<(), GitError> {
|
||||||
|
let result = std::process::Command::new("git")
|
||||||
|
.args(["worktree", "prune"])
|
||||||
|
.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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn git_remove_branch(branch: &str) -> Result<(), GitError> {
|
||||||
|
let result = std::process::Command::new("git")
|
||||||
|
.args(["branch", "-D", 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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = tempfile::tempdir().unwrap();
|
||||||
|
create_orphan_branch_at_path(branch, tmp_worktree.path())?;
|
||||||
|
}
|
||||||
|
// The temp dir is now removed / cleaned up.
|
||||||
|
|
||||||
|
let result = std::process::Command::new("git")
|
||||||
|
.args(["worktree", "prune"])
|
||||||
|
.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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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!(
|
||||||
|
readme,
|
||||||
|
"This branch is used by entomologist to track issues."
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let result = std::process::Command::new("git")
|
||||||
|
.args(["add", "README.md"])
|
||||||
|
.current_dir(worktree_path)
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = std::process::Command::new("git")
|
||||||
|
.args(["commit", "-m", "create entomologist issue branch"])
|
||||||
|
.current_dir(worktree_path)
|
||||||
|
.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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_worktree() {
|
||||||
|
let mut p = std::path::PathBuf::new();
|
||||||
|
{
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
// The temporary worktree directory is removed when the Temp variable is dropped.
|
||||||
|
assert!(!p.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_orphan_branch() {
|
||||||
|
let rnd: u128 = rand::random();
|
||||||
|
let mut branch = std::string::String::from("entomologist-test-branch-");
|
||||||
|
branch.push_str(&format!("{:0x}", rnd));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
pub mod git;
|
||||||
pub mod issue;
|
pub mod issue;
|
||||||
pub mod issues;
|
pub mod issues;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue