use anyhow::{Context, Result};
use std::process::Command;
const WORK_BRANCH: &str = "neti-work";
fn in_git_repo() -> bool {
Command::new("git")
.args(["rev-parse", "--git-dir"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn current_branch() -> Result<String> {
let output = Command::new("git")
.args(["branch", "--show-current"])
.output()
.context("Failed to run git")?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn branch_exists(name: &str) -> bool {
Command::new("git")
.args(["rev-parse", "--verify", name])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn has_uncommitted_changes() -> bool {
Command::new("git")
.args(["status", "--porcelain"])
.output()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false)
}
#[must_use]
pub fn count_modified_files() -> usize {
let output = Command::new("git").args(["status", "--porcelain"]).output();
match output {
Ok(o) => String::from_utf8_lossy(&o.stdout).lines().count(),
Err(_) => 0,
}
}
pub fn init_branch(force: bool) -> Result<BranchResult> {
if !in_git_repo() {
anyhow::bail!("Not a git repository. Run 'git init' first.");
}
let on_work_branch = current_branch()? == WORK_BRANCH;
if branch_exists(WORK_BRANCH) && !on_work_branch {
if force {
run_git(&["branch", "-D", WORK_BRANCH])?;
} else {
anyhow::bail!(
"Branch '{WORK_BRANCH}' already exists. Use --force to reset it.",
);
}
}
if on_work_branch {
if force {
run_git(&["checkout", "main"])?;
run_git(&["branch", "-D", WORK_BRANCH])?;
run_git(&["checkout", "-b", WORK_BRANCH])?;
return Ok(BranchResult::Reset);
}
return Ok(BranchResult::AlreadyOnBranch);
}
run_git(&["checkout", "-b", WORK_BRANCH])?;
Ok(BranchResult::Created)
}
pub fn promote(dry_run: bool, custom_msg: Option<String>) -> Result<PromoteResult> {
if !in_git_repo() {
anyhow::bail!("Not a git repository.");
}
let current = current_branch()?;
if current != WORK_BRANCH {
anyhow::bail!(
"Not on work branch. Currently on '{current}'. Run 'neti branch' first.",
);
}
if has_uncommitted_changes() {
anyhow::bail!("Uncommitted changes. Commit or stash before promoting.");
}
if dry_run {
return Ok(PromoteResult::DryRun);
}
let msg = custom_msg.unwrap_or_else(|| "chore: promote neti-work".to_string());
run_git(&["checkout", "main"])?;
run_git(&["merge", "--squash", WORK_BRANCH])?;
run_git(&["commit", "-m", &msg])?;
run_git(&["branch", "-D", WORK_BRANCH])?;
Ok(PromoteResult::Merged)
}
pub fn abort() -> Result<()> {
if !in_git_repo() {
anyhow::bail!("Not a git repository.");
}
let current = current_branch()?;
if current == WORK_BRANCH {
run_git(&["checkout", "main"])?;
}
if branch_exists(WORK_BRANCH) {
run_git(&["branch", "-D", WORK_BRANCH])?;
}
Ok(())
}
#[must_use]
pub fn work_branch_name() -> &'static str {
WORK_BRANCH
}
#[must_use]
pub fn on_work_branch() -> bool {
current_branch().map(|b| b == WORK_BRANCH).unwrap_or(false)
}
fn run_git(args: &[&str]) -> Result<()> {
let output = Command::new("git")
.args(args)
.output()
.with_context(|| format!("Failed to run: git {}", args.join(" ")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git {} failed: {stderr}", args.join(" "));
}
Ok(())
}
#[derive(Debug)]
pub enum BranchResult {
Created,
Reset,
AlreadyOnBranch,
}
#[derive(Debug)]
pub enum PromoteResult {
Merged,
DryRun,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_work_branch_name() {
assert_eq!(work_branch_name(), "neti-work");
}
}