use crate::cli::UI;
use crate::ops::refstore;
use crate::ops::utils::short_oid;
use anyhow::{bail, Result};
use git2::Repository;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackEntry {
pub branch: String,
pub parent_branch: String,
pub stack_name: String,
pub position: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackMetadata {
pub name: String,
pub base_branch: String,
pub branches: Vec<String>,
pub created_at: String,
}
fn stack_meta_ref(name: &str) -> String {
format!("refs/stack-metadata/__stack__{}", name)
}
fn branch_meta_ref(branch: &str) -> String {
format!("refs/stack-metadata/{}", branch)
}
pub fn new_stack(path: &Path, name: &str, ui: &UI) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let head = repo.head()?;
let base_branch = head
.shorthand()
.ok_or_else(|| anyhow::anyhow!("Cannot create stack: HEAD is detached"))?
.to_string();
let meta_ref = stack_meta_ref(name);
if refstore::read_json_ref::<StackMetadata>(&repo, &meta_ref)?.is_some() {
bail!("Stack '{}' already exists.", name);
}
let metadata = StackMetadata {
name: name.to_string(),
base_branch: base_branch.clone(),
branches: Vec::new(),
created_at: crate::ops::oplog::now_iso8601_pub(),
};
refstore::write_json_ref(&repo, &meta_ref, &metadata)?;
ui.success(format!(
"Created stack '{}' based on '{}'",
name, base_branch
));
Ok(())
}
pub fn push_branch(path: &Path, branch_name: Option<&str>, ui: &UI) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let current_branch = repo
.head()?
.shorthand()
.map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("HEAD is detached"))?;
let (stack_name, mut metadata) = find_stack_for_branch(&repo, ¤t_branch)?;
let new_branch = if let Some(name) = branch_name {
name.to_string()
} else {
format!("{}/{}", stack_name, metadata.branches.len() + 1)
};
let head_commit = repo.head()?.peel_to_commit()?;
repo.branch(&new_branch, &head_commit, false)?;
let entry = StackEntry {
branch: new_branch.clone(),
parent_branch: current_branch.clone(),
stack_name: stack_name.clone(),
position: metadata.branches.len() as u32,
};
refstore::write_json_ref(&repo, &branch_meta_ref(&new_branch), &entry)?;
metadata.branches.push(new_branch.clone());
refstore::write_json_ref(&repo, &stack_meta_ref(&stack_name), &metadata)?;
let obj = repo
.find_branch(&new_branch, git2::BranchType::Local)?
.get()
.peel(git2::ObjectType::Commit)?;
repo.checkout_tree(&obj, None)?;
repo.set_head(&format!("refs/heads/{}", new_branch))?;
ui.success(format!(
"Pushed '{}' onto stack '{}'",
new_branch, stack_name
));
Ok(())
}
pub fn status(path: &Path, ui: &UI) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let current_branch = repo
.head()?
.shorthand()
.map(|s| s.to_string())
.unwrap_or_default();
let (stack_name, metadata) = match find_stack_for_branch(&repo, ¤t_branch) {
Ok(result) => result,
Err(_) => {
ui.info("Not in a stack. Use 'securegit stack new <name>' to create one.");
return Ok(());
}
};
ui.section(&format!("Stack: {}", stack_name));
ui.field("base", &metadata.base_branch);
ui.blank();
let branch_count = metadata.branches.len();
for (i, branch) in metadata.branches.iter().enumerate() {
let is_last = i == branch_count - 1;
let entry = refstore::read_json_ref::<StackEntry>(&repo, &branch_meta_ref(branch))?;
let ahead = if let Some(entry) = &entry {
count_ahead(&repo, &entry.parent_branch, branch).unwrap_or(0)
} else {
0
};
let is_current = branch == ¤t_branch;
let branch_display = if is_current {
format!("{} *", branch)
} else {
branch.to_string()
};
let plural = if ahead == 1 { "" } else { "s" };
ui.tree_item(
is_last,
format!("{}. {} ({} commit{})", i + 1, branch_display, ahead, plural),
);
}
Ok(())
}
pub fn rebase_stack(path: &Path, ui: &UI) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let current_branch = repo
.head()?
.shorthand()
.map(|s| s.to_string())
.unwrap_or_default();
let (stack_name, metadata) = find_stack_for_branch(&repo, ¤t_branch)?;
ui.info(format!(
"Rebasing stack '{}' ({} branches)...",
stack_name,
metadata.branches.len()
));
let mut prev_branch = metadata.base_branch.clone();
for branch in &metadata.branches {
ui.info(format!("Rebasing '{}' onto '{}'", branch, prev_branch));
let branch_ref = repo.find_branch(branch, git2::BranchType::Local)?;
let obj = branch_ref.get().peel(git2::ObjectType::Commit)?;
repo.checkout_tree(&obj, None)?;
repo.set_head(&format!("refs/heads/{}", branch))?;
let upstream_obj = repo.revparse_single(&prev_branch)?;
let upstream_commit = upstream_obj.peel_to_commit()?;
let upstream_annotated = repo.find_annotated_commit(upstream_commit.id())?;
let mut rebase = repo.rebase(None, Some(&upstream_annotated), None, None)?;
let sig = repo.signature()?;
while rebase.next().is_some() {
let index = repo.index()?;
if index.has_conflicts() {
ui.warning(format!(
"Conflict in '{}'. Resolve and run 'securegit stack rebase' again.",
branch
));
rebase.abort()?;
let obj = repo
.find_branch(¤t_branch, git2::BranchType::Local)?
.get()
.peel(git2::ObjectType::Commit)?;
repo.checkout_tree(&obj, None)?;
repo.set_head(&format!("refs/heads/{}", current_branch))?;
bail!("Stack rebase stopped due to conflicts in '{}'", branch);
}
rebase.commit(None, &sig, None)?;
}
rebase.finish(None)?;
ui.success("Done");
prev_branch = branch.clone();
}
let obj = repo
.find_branch(¤t_branch, git2::BranchType::Local)?
.get()
.peel(git2::ObjectType::Commit)?;
repo.checkout_tree(&obj, None)?;
repo.set_head(&format!("refs/heads/{}", current_branch))?;
ui.success(format!("Stack '{}' rebased successfully", stack_name));
Ok(())
}
pub fn pop_branch(path: &Path, ui: &UI) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let current_branch = repo
.head()?
.shorthand()
.map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("HEAD is detached"))?;
let (stack_name, mut metadata) = find_stack_for_branch(&repo, ¤t_branch)?;
if metadata.branches.last().map(|s| s.as_str()) != Some(¤t_branch) {
bail!(
"Can only pop the top of the stack. '{}' is not the top branch.",
current_branch
);
}
let entry = refstore::read_json_ref::<StackEntry>(&repo, &branch_meta_ref(¤t_branch))?
.ok_or_else(|| anyhow::anyhow!("No stack entry for '{}'", current_branch))?;
let parent = entry.parent_branch.clone();
metadata.branches.retain(|b| b != ¤t_branch);
refstore::write_json_ref(&repo, &stack_meta_ref(&stack_name), &metadata)?;
refstore::delete_ref(&repo, &branch_meta_ref(¤t_branch))?;
let obj = repo
.find_branch(&parent, git2::BranchType::Local)?
.get()
.peel(git2::ObjectType::Commit)?;
repo.checkout_tree(&obj, None)?;
repo.set_head(&format!("refs/heads/{}", parent))?;
ui.success(format!(
"Popped '{}' from stack '{}'. Now on '{}'.",
current_branch, stack_name, parent
));
Ok(())
}
pub fn log_stack(path: &Path, ui: &UI) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let current_branch = repo
.head()?
.shorthand()
.map(|s| s.to_string())
.unwrap_or_default();
let (_stack_name, metadata) = find_stack_for_branch(&repo, ¤t_branch)?;
let mut prev_branch = metadata.base_branch.clone();
for branch in &metadata.branches {
ui.section(branch);
if let Ok(Some(branch_oid)) = repo
.find_branch(branch, git2::BranchType::Local)
.map(|b| b.get().target())
{
if let Ok(Some(parent_oid)) = repo
.find_branch(&prev_branch, git2::BranchType::Local)
.map(|b| b.get().target())
{
let mut revwalk = repo.revwalk()?;
revwalk.push(branch_oid)?;
revwalk.hide(parent_oid)?;
for oid in revwalk.flatten() {
if let Ok(commit) = repo.find_commit(oid) {
ui.log_oneline(&short_oid(&oid), commit.summary().unwrap_or(""));
}
}
}
}
prev_branch = branch.clone();
}
Ok(())
}
fn find_stack_for_branch(repo: &Repository, branch_name: &str) -> Result<(String, StackMetadata)> {
if let Some(entry) = refstore::read_json_ref::<StackEntry>(repo, &branch_meta_ref(branch_name))?
{
let metadata =
refstore::read_json_ref::<StackMetadata>(repo, &stack_meta_ref(&entry.stack_name))?
.ok_or_else(|| {
anyhow::anyhow!("Stack '{}' metadata not found", entry.stack_name)
})?;
return Ok((entry.stack_name, metadata));
}
let refs = refstore::list_refs_with_prefix(repo, "refs/stack-metadata/__stack__")?;
for ref_name in &refs {
if let Some(metadata) = refstore::read_json_ref::<StackMetadata>(repo, ref_name)? {
if metadata.base_branch == branch_name {
return Ok((metadata.name.clone(), metadata));
}
}
}
bail!("Branch '{}' is not part of any stack.", branch_name)
}
fn count_ahead(repo: &Repository, base: &str, branch: &str) -> Result<usize> {
let base_oid = repo
.find_branch(base, git2::BranchType::Local)?
.get()
.target()
.ok_or_else(|| anyhow::anyhow!("No OID for '{}'", base))?;
let branch_oid = repo
.find_branch(branch, git2::BranchType::Local)?
.get()
.target()
.ok_or_else(|| anyhow::anyhow!("No OID for '{}'", branch))?;
let (ahead, _) = repo.graph_ahead_behind(branch_oid, base_oid)?;
Ok(ahead)
}