use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::VcsError;
use crate::hash::ObjectId;
use crate::object::{CommitObject, Object};
use crate::store::{self, ReflogEntry, Store};
#[derive(Clone, Debug)]
pub struct StashEntry {
pub index: usize,
pub commit_id: ObjectId,
pub message: String,
pub timestamp: u64,
}
pub fn stash_push(
store: &mut dyn Store,
schema_id: ObjectId,
author: &str,
message: Option<&str>,
) -> Result<ObjectId, VcsError> {
let head_id = store::resolve_head(store)?.ok_or_else(|| VcsError::RefNotFound {
name: "HEAD".to_owned(),
})?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let msg = message.unwrap_or("WIP on stash").to_owned();
let stash_commit = CommitObject::builder(schema_id, "", author, msg.clone())
.parents(vec![head_id])
.timestamp(timestamp)
.build();
let stash_id = store.put(&Object::Commit(stash_commit))?;
let old_stash = store.get_ref("refs/stash")?;
store.set_ref("refs/stash", stash_id)?;
store.append_reflog(
"refs/stash",
ReflogEntry {
old_id: old_stash,
new_id: stash_id,
author: author.to_owned(),
timestamp,
message: msg,
},
)?;
Ok(stash_id)
}
pub fn stash_pop(store: &mut dyn Store) -> Result<ObjectId, VcsError> {
let stash_id = store
.get_ref("refs/stash")?
.ok_or_else(|| VcsError::RefNotFound {
name: "refs/stash".to_owned(),
})?;
let stash_commit = match store.get(&stash_id)? {
Object::Commit(c) => c,
other => {
return Err(VcsError::WrongObjectType {
expected: "commit",
found: other.type_name(),
});
}
};
let schema_id = stash_commit.schema_id;
let reflog = store.read_reflog("refs/stash", Some(2))?;
if reflog.len() > 1 {
store.set_ref("refs/stash", reflog[1].new_id)?;
} else {
let _ = store.delete_ref("refs/stash");
}
Ok(schema_id)
}
pub fn stash_list(store: &dyn Store) -> Result<Vec<StashEntry>, VcsError> {
let reflog = store.read_reflog("refs/stash", None)?;
Ok(reflog
.into_iter()
.enumerate()
.map(|(i, entry)| StashEntry {
index: i,
commit_id: entry.new_id,
message: entry.message,
timestamp: entry.timestamp,
})
.collect())
}
pub fn stash_apply(store: &dyn Store, index: usize) -> Result<ObjectId, VcsError> {
let entries = stash_list(store)?;
let entry = entries.get(index).ok_or_else(|| VcsError::RefNotFound {
name: format!("stash@{{{index}}}"),
})?;
let stash_commit = match store.get(&entry.commit_id)? {
Object::Commit(c) => c,
other => {
return Err(VcsError::WrongObjectType {
expected: "commit",
found: other.type_name(),
});
}
};
Ok(stash_commit.schema_id)
}
pub fn stash_show(store: &dyn Store, index: usize) -> Result<ObjectId, VcsError> {
stash_apply(store, index)
}
pub fn stash_clear(store: &mut dyn Store) -> Result<(), VcsError> {
if store.get_ref("refs/stash")?.is_some() {
let _ = store.delete_ref("refs/stash");
}
Ok(())
}
pub fn stash_drop(store: &mut dyn Store, index: usize) -> Result<(), VcsError> {
if index != 0 {
return Err(VcsError::RefNotFound {
name: format!("stash@{{{index}}} (only stash@{{0}} can be dropped)"),
});
}
stash_pop(store)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MemStore;
use crate::error::VcsError;
#[test]
fn stash_push_and_pop() -> Result<(), VcsError> {
let mut store = MemStore::new();
let head_commit =
CommitObject::builder(ObjectId::from_bytes([0; 32]), "test", "test", "initial")
.timestamp(100)
.build();
let head_id = store.put(&Object::Commit(head_commit))?;
store.set_ref("refs/heads/main", head_id)?;
let stashed_schema_id = ObjectId::from_bytes([42; 32]);
let _stash_id = stash_push(&mut store, stashed_schema_id, "alice", Some("my stash"))?;
assert!(store.get_ref("refs/stash")?.is_some());
let stashes = stash_list(&store)?;
assert_eq!(stashes.len(), 1);
assert_eq!(stashes[0].message, "my stash");
let popped = stash_pop(&mut store)?;
assert_eq!(popped, stashed_schema_id);
assert!(store.get_ref("refs/stash")?.is_none());
Ok(())
}
#[test]
fn stash_multiple() -> Result<(), VcsError> {
let mut store = MemStore::new();
let head_commit =
CommitObject::builder(ObjectId::from_bytes([0; 32]), "test", "test", "initial")
.timestamp(100)
.build();
let head_id = store.put(&Object::Commit(head_commit))?;
store.set_ref("refs/heads/main", head_id)?;
stash_push(
&mut store,
ObjectId::from_bytes([1; 32]),
"alice",
Some("first"),
)?;
stash_push(
&mut store,
ObjectId::from_bytes([2; 32]),
"alice",
Some("second"),
)?;
let stashes = stash_list(&store)?;
assert_eq!(stashes.len(), 2);
assert_eq!(stashes[0].message, "second");
assert_eq!(stashes[1].message, "first");
let popped = stash_pop(&mut store)?;
assert_eq!(popped, ObjectId::from_bytes([2; 32]));
Ok(())
}
#[test]
fn stash_pop_empty_fails() {
let mut store = MemStore::new();
assert!(stash_pop(&mut store).is_err());
}
}