use anyhow::{Result, anyhow};
use objects::object::ContentHash;
use repo::{DiffKind, Repository};
use serde::Serialize;
use super::stash_ops::{apply_stash, build_worktree_tree, restore_worktree};
use crate::cli::{Cli, StashCommands, should_output_json, worktree_status_options};
#[derive(Serialize)]
struct StashOutput {
message: String,
stash_index: Option<usize>,
}
#[derive(Serialize)]
struct StashListEntry {
index: usize,
message: Option<String>,
created_at: String,
}
#[derive(Serialize)]
struct StashListOutput {
stashes: Vec<StashListEntry>,
}
#[derive(Serialize)]
struct StashShowOutput {
modified: Vec<String>,
added: Vec<String>,
deleted: Vec<String>,
}
pub fn cmd_stash(cli: &Cli, command: StashCommands) -> Result<()> {
let repo = Repository::open(cli.repo.as_ref().unwrap_or(&std::env::current_dir()?))?;
match command {
StashCommands::Push { message } => cmd_stash_push(cli, &repo, message),
StashCommands::List => cmd_stash_list(cli, &repo),
StashCommands::Pop => cmd_stash_pop(cli, &repo),
StashCommands::Apply => cmd_stash_apply(cli, &repo),
StashCommands::Drop => cmd_stash_drop(cli, &repo),
StashCommands::Clear => cmd_stash_clear(cli, &repo),
StashCommands::Show => cmd_stash_show(cli, &repo),
}
}
fn cmd_stash_push(cli: &Cli, repo: &Repository, message: Option<String>) -> Result<()> {
let current_state = repo.current_state()?;
let current_tree = current_state
.as_ref()
.map(|s| repo.store().get_tree(&s.tree))
.transpose()?
.flatten()
.unwrap_or_default();
let status = repo.compare_worktree_cached_with_options(
¤t_tree,
&worktree_status_options(Some(repo.config())),
)?;
if status.is_clean() {
return Err(anyhow!("No changes to stash"));
}
let stash_manager = repo.stash_manager();
stash_manager.init()?;
let parent_tree_hash = current_tree.hash().to_string();
let worktree_tree = build_worktree_tree(repo, &status)?;
let tree_hash = repo.store().put_tree(&worktree_tree)?;
let entry = stash_manager.push(tree_hash, parent_tree_hash, message)?;
restore_worktree(repo, ¤t_tree, &status)?;
if should_output_json(cli, Some(repo.config())) {
println!(
"{}",
serde_json::to_string(&StashOutput {
message: format!("Saved stash@{{{}}}", entry.index),
stash_index: Some(entry.index),
})?
);
} else {
println!("Saved stash@{{{}}}", entry.index);
}
Ok(())
}
fn cmd_stash_list(cli: &Cli, repo: &Repository) -> Result<()> {
let stash_manager = repo.stash_manager();
let stashes = stash_manager.list()?;
if should_output_json(cli, Some(repo.config())) {
let entries: Vec<StashListEntry> = stashes
.iter()
.map(|s| StashListEntry {
index: s.index,
message: s.message.clone(),
created_at: s.created_at.to_rfc3339(),
})
.collect();
println!(
"{}",
serde_json::to_string(&StashListOutput { stashes: entries })?
);
} else if stashes.is_empty() {
println!("No stashes.");
} else {
for stash in stashes {
let msg = stash.message.as_deref().unwrap_or("WIP on main");
println!("stash@{{{}}}: {}", stash.index, msg);
}
}
Ok(())
}
fn cmd_stash_pop(cli: &Cli, repo: &Repository) -> Result<()> {
let stash_manager = repo.stash_manager();
let stash = stash_manager
.pop_with(|stash| {
apply_stash(repo, stash).map_err(|err| std::io::Error::other(err.to_string()).into())
})?
.ok_or_else(|| anyhow!("No stash found"))?;
if should_output_json(cli, Some(repo.config())) {
println!(
"{}",
serde_json::to_string(&StashOutput {
message: "Applied and dropped stash".to_string(),
stash_index: None,
})?
);
} else {
println!("Applied and dropped stash@{{{}}}", stash.index);
}
Ok(())
}
fn cmd_stash_apply(cli: &Cli, repo: &Repository) -> Result<()> {
let stash_manager = repo.stash_manager();
let stash = stash_manager
.top()?
.ok_or_else(|| anyhow!("No stash found"))?;
apply_stash(repo, &stash)?;
if should_output_json(cli, Some(repo.config())) {
println!(
"{}",
serde_json::to_string(&StashOutput {
message: format!("Applied stash@{{{}}}", stash.index),
stash_index: Some(stash.index),
})?
);
} else {
println!("Applied stash@{{{}}}", stash.index);
}
Ok(())
}
fn cmd_stash_drop(cli: &Cli, repo: &Repository) -> Result<()> {
let stash_manager = repo.stash_manager();
let stash = stash_manager.drop()?;
match stash {
Some(s) => {
if should_output_json(cli, Some(repo.config())) {
println!(
"{}",
serde_json::to_string(&StashOutput {
message: format!("Dropped stash@{{{}}}", s.index),
stash_index: None,
})?
);
} else {
println!("Dropped stash@{{{}}}", s.index);
}
}
None => {
return Err(anyhow!("No stash to drop"));
}
}
Ok(())
}
fn cmd_stash_clear(cli: &Cli, repo: &Repository) -> Result<()> {
let stash_manager = repo.stash_manager();
let count = stash_manager.clear()?;
if should_output_json(cli, Some(repo.config())) {
println!(
"{}",
serde_json::to_string(&StashOutput {
message: format!("Cleared {} stash(es)", count),
stash_index: None,
})?
);
} else if count == 0 {
println!("No stashes to clear");
} else {
println!("Cleared {} stash(es)", count);
}
Ok(())
}
fn cmd_stash_show(cli: &Cli, repo: &Repository) -> Result<()> {
let stash_manager = repo.stash_manager();
let stash = stash_manager
.top()?
.ok_or_else(|| anyhow!("No stash found"))?;
let parent_tree_hash = ContentHash::from_hex(&stash.parent_tree_hash)
.map_err(|e| anyhow!("Invalid parent tree hash: {}", e))?;
let _parent_tree = repo
.store()
.get_tree(&parent_tree_hash)?
.unwrap_or_default();
let stash_tree_hash = ContentHash::from_hex(&stash.tree_hash)
.map_err(|e| anyhow!("Invalid stash tree hash: {}", e))?;
let _stash_tree = repo.store().get_tree(&stash_tree_hash)?.unwrap_or_default();
let changes = repo.diff_trees(&parent_tree_hash, &stash_tree_hash)?;
if should_output_json(cli, Some(repo.config())) {
let mut modified = Vec::new();
let mut added = Vec::new();
let mut deleted = Vec::new();
for change in &changes {
match change.kind {
DiffKind::Modified => modified.push(change.path.clone()),
DiffKind::Added => added.push(change.path.clone()),
DiffKind::Deleted => deleted.push(change.path.clone()),
DiffKind::Unchanged => {}
}
}
println!(
"{}",
serde_json::to_string(&StashShowOutput {
modified,
added,
deleted,
})?
);
} else if changes.is_empty() {
println!("Empty stash");
} else {
for change in &changes {
let prefix = match change.kind {
DiffKind::Modified => "M",
DiffKind::Added => "A",
DiffKind::Deleted => "D",
DiffKind::Unchanged => continue,
};
println!("{} {}", prefix, change.path);
}
}
Ok(())
}