use std::collections::VecDeque;
use std::fs;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::stack::Stack;
#[derive(Debug)]
pub struct State {
rung_dir: PathBuf,
}
impl State {
const STACK_FILE: &'static str = "stack.json";
const CONFIG_FILE: &'static str = "config.toml";
const SYNC_STATE_FILE: &'static str = "sync_state";
const RESTACK_STATE_FILE: &'static str = "restack_state";
const SPLIT_STATE_FILE: &'static str = "split_state";
const FOLD_STATE_FILE: &'static str = "fold_state";
const REFS_DIR: &'static str = "refs";
pub fn new(repo_path: impl AsRef<Path>) -> Result<Self> {
let git_dir = repo_path.as_ref().join(".git");
if !git_dir.exists() {
return Err(Error::NotARepository);
}
Ok(Self {
rung_dir: git_dir.join("rung"),
})
}
pub fn init(&self) -> Result<()> {
fs::create_dir_all(&self.rung_dir)?;
fs::create_dir_all(self.rung_dir.join(Self::REFS_DIR))?;
if !self.stack_path().exists() {
self.save_stack(&Stack::new())?;
}
Ok(())
}
#[must_use]
pub fn is_initialized(&self) -> bool {
self.rung_dir.exists() && self.stack_path().exists()
}
#[must_use]
pub fn rung_dir(&self) -> &Path {
&self.rung_dir
}
fn stack_path(&self) -> PathBuf {
self.rung_dir.join(Self::STACK_FILE)
}
pub fn load_stack(&self) -> Result<Stack> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
let content = fs::read_to_string(self.stack_path())?;
let stack: Stack = serde_json::from_str(&content)?;
Ok(stack)
}
pub fn save_stack(&self, stack: &Stack) -> Result<()> {
let content = serde_json::to_string_pretty(stack)?;
fs::write(self.stack_path(), content)?;
Ok(())
}
fn config_path(&self) -> PathBuf {
self.rung_dir.join(Self::CONFIG_FILE)
}
pub fn load_config(&self) -> Result<crate::config::Config> {
crate::config::Config::load(self.config_path())
}
pub fn save_config(&self, config: &crate::config::Config) -> Result<()> {
config.save(self.config_path())
}
pub fn default_branch(&self) -> Result<String> {
let config = self.load_config()?;
Ok(config
.general
.default_branch
.unwrap_or_else(|| "main".into()))
}
fn sync_state_path(&self) -> PathBuf {
self.rung_dir.join(Self::SYNC_STATE_FILE)
}
#[must_use]
pub fn is_sync_in_progress(&self) -> bool {
self.sync_state_path().exists()
}
pub fn load_sync_state(&self) -> Result<SyncState> {
if !self.is_sync_in_progress() {
return Err(Error::NoBackupFound);
}
let content = fs::read_to_string(self.sync_state_path())?;
let state: SyncState = serde_json::from_str(&content)?;
Ok(state)
}
pub fn save_sync_state(&self, state: &SyncState) -> Result<()> {
let content = serde_json::to_string_pretty(state)?;
fs::write(self.sync_state_path(), content)?;
Ok(())
}
pub fn clear_sync_state(&self) -> Result<()> {
let path = self.sync_state_path();
if path.exists() {
fs::remove_file(path)?;
}
Ok(())
}
fn restack_state_path(&self) -> PathBuf {
self.rung_dir.join(Self::RESTACK_STATE_FILE)
}
#[must_use]
pub fn is_restack_in_progress(&self) -> bool {
self.restack_state_path().exists()
}
pub fn load_restack_state(&self) -> Result<RestackState> {
if !self.is_restack_in_progress() {
return Err(Error::NoBackupFound);
}
let content = fs::read_to_string(self.restack_state_path())?;
let state: RestackState = serde_json::from_str(&content)?;
Ok(state)
}
pub fn save_restack_state(&self, state: &RestackState) -> Result<()> {
let content = serde_json::to_string_pretty(state)?;
fs::write(self.restack_state_path(), content)?;
Ok(())
}
pub fn clear_restack_state(&self) -> Result<()> {
let path = self.restack_state_path();
if path.exists() {
fs::remove_file(path)?;
}
Ok(())
}
fn split_state_path(&self) -> PathBuf {
self.rung_dir.join(Self::SPLIT_STATE_FILE)
}
#[must_use]
pub fn is_split_in_progress(&self) -> bool {
self.split_state_path().exists()
}
pub fn load_split_state(&self) -> Result<SplitState> {
if !self.is_split_in_progress() {
return Err(Error::NoBackupFound);
}
let content = fs::read_to_string(self.split_state_path())?;
let state: SplitState = serde_json::from_str(&content)?;
Ok(state)
}
pub fn save_split_state(&self, state: &SplitState) -> Result<()> {
let content = serde_json::to_string_pretty(state)?;
fs::write(self.split_state_path(), content)?;
Ok(())
}
pub fn clear_split_state(&self) -> Result<()> {
let path = self.split_state_path();
if path.exists() {
fs::remove_file(path)?;
}
Ok(())
}
fn fold_state_path(&self) -> PathBuf {
self.rung_dir.join(Self::FOLD_STATE_FILE)
}
#[must_use]
pub fn is_fold_in_progress(&self) -> bool {
self.fold_state_path().exists()
}
pub fn load_fold_state(&self) -> Result<FoldState> {
if !self.is_fold_in_progress() {
return Err(Error::NoBackupFound);
}
let content = fs::read_to_string(self.fold_state_path())?;
let state: FoldState = serde_json::from_str(&content)?;
Ok(state)
}
pub fn save_fold_state(&self, state: &FoldState) -> Result<()> {
let content = serde_json::to_string_pretty(state)?;
fs::write(self.fold_state_path(), content)?;
Ok(())
}
pub fn clear_fold_state(&self) -> Result<()> {
let path = self.fold_state_path();
if path.exists() {
fs::remove_file(path)?;
}
Ok(())
}
fn refs_dir(&self) -> PathBuf {
self.rung_dir.join(Self::REFS_DIR)
}
pub fn create_backup(&self, branches: &[(&str, &str)]) -> Result<String> {
let backup_id = Utc::now().timestamp().to_string();
let backup_dir = self.refs_dir().join(&backup_id);
fs::create_dir_all(&backup_dir)?;
for (branch_name, commit_sha) in branches {
let safe_name = branch_name.replace('/', "-");
fs::write(backup_dir.join(safe_name), commit_sha)?;
}
Ok(backup_id)
}
pub fn latest_backup(&self) -> Result<String> {
let refs_dir = self.refs_dir();
if !refs_dir.exists() {
return Err(Error::NoBackupFound);
}
let mut backups: Vec<_> = fs::read_dir(&refs_dir)?
.filter_map(std::result::Result::ok)
.filter(|e| e.path().is_dir())
.filter_map(|e| e.file_name().to_str().map(String::from))
.filter_map(|name| name.parse::<i64>().ok().map(|ts| (ts, name)))
.collect();
backups.sort_by_key(|(ts, _)| std::cmp::Reverse(*ts));
backups
.into_iter()
.next()
.map(|(_, name)| name)
.ok_or(Error::NoBackupFound)
}
pub fn load_backup(&self, backup_id: &str) -> Result<Vec<(String, String)>> {
let backup_dir = self.refs_dir().join(backup_id);
if !backup_dir.exists() {
return Err(Error::NoBackupFound);
}
let mut refs = vec![];
for entry in fs::read_dir(&backup_dir)? {
let entry = entry?;
if entry.path().is_file() {
let name = entry
.file_name()
.to_str()
.ok_or_else(|| Error::StateParseError {
file: entry.path(),
message: "invalid filename".into(),
})?
.replace('-', "/");
let sha = fs::read_to_string(entry.path())?.trim().to_string();
refs.push((name, sha));
}
}
Ok(refs)
}
pub fn delete_backup(&self, backup_id: &str) -> Result<()> {
let backup_dir = self.refs_dir().join(backup_id);
if backup_dir.exists() {
fs::remove_dir_all(backup_dir)?;
}
Ok(())
}
pub fn cleanup_backups(&self, keep: usize) -> Result<()> {
let refs_dir = self.refs_dir();
if !refs_dir.exists() {
return Ok(());
}
let mut backups: Vec<_> = fs::read_dir(&refs_dir)?
.filter_map(std::result::Result::ok)
.filter(|e| e.path().is_dir())
.filter_map(|e| {
e.file_name()
.to_str()
.and_then(|s| s.parse::<i64>().ok())
.map(|ts| (ts, e.path()))
})
.collect();
backups.sort_by_key(|(ts, _)| std::cmp::Reverse(*ts));
for (_, path) in backups.into_iter().skip(keep) {
fs::remove_dir_all(path)?;
}
Ok(())
}
}
use crate::traits::StateStore;
impl StateStore for State {
fn is_initialized(&self) -> bool {
Self::is_initialized(self)
}
fn init(&self) -> Result<()> {
Self::init(self)
}
fn rung_dir(&self) -> &Path {
Self::rung_dir(self)
}
fn load_stack(&self) -> Result<Stack> {
Self::load_stack(self)
}
fn save_stack(&self, stack: &Stack) -> Result<()> {
Self::save_stack(self, stack)
}
fn load_config(&self) -> Result<crate::config::Config> {
Self::load_config(self)
}
fn save_config(&self, config: &crate::config::Config) -> Result<()> {
Self::save_config(self, config)
}
fn default_branch(&self) -> Result<String> {
Self::default_branch(self)
}
fn is_sync_in_progress(&self) -> bool {
Self::is_sync_in_progress(self)
}
fn load_sync_state(&self) -> Result<SyncState> {
Self::load_sync_state(self)
}
fn save_sync_state(&self, state: &SyncState) -> Result<()> {
Self::save_sync_state(self, state)
}
fn clear_sync_state(&self) -> Result<()> {
Self::clear_sync_state(self)
}
fn is_restack_in_progress(&self) -> bool {
Self::is_restack_in_progress(self)
}
fn load_restack_state(&self) -> Result<RestackState> {
Self::load_restack_state(self)
}
fn save_restack_state(&self, state: &RestackState) -> Result<()> {
Self::save_restack_state(self, state)
}
fn clear_restack_state(&self) -> Result<()> {
Self::clear_restack_state(self)
}
fn is_split_in_progress(&self) -> bool {
Self::is_split_in_progress(self)
}
fn load_split_state(&self) -> Result<SplitState> {
Self::load_split_state(self)
}
fn save_split_state(&self, state: &SplitState) -> Result<()> {
Self::save_split_state(self, state)
}
fn clear_split_state(&self) -> Result<()> {
Self::clear_split_state(self)
}
fn is_fold_in_progress(&self) -> bool {
Self::is_fold_in_progress(self)
}
fn load_fold_state(&self) -> Result<FoldState> {
Self::load_fold_state(self)
}
fn save_fold_state(&self, state: &FoldState) -> Result<()> {
Self::save_fold_state(self, state)
}
fn clear_fold_state(&self) -> Result<()> {
Self::clear_fold_state(self)
}
fn create_backup(&self, branches: &[(&str, &str)]) -> Result<String> {
Self::create_backup(self, branches)
}
fn latest_backup(&self) -> Result<String> {
Self::latest_backup(self)
}
fn load_backup(&self, backup_id: &str) -> Result<Vec<(String, String)>> {
Self::load_backup(self, backup_id)
}
fn delete_backup(&self, backup_id: &str) -> Result<()> {
Self::delete_backup(self, backup_id)
}
fn cleanup_backups(&self, keep: usize) -> Result<()> {
Self::cleanup_backups(self, keep)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncState {
pub started_at: DateTime<Utc>,
pub backup_id: String,
pub current_branch: String,
pub completed: Vec<String>,
pub remaining: VecDeque<String>,
}
impl SyncState {
#[must_use]
pub fn new(backup_id: String, branches: Vec<String>) -> Self {
let current = branches.first().cloned().unwrap_or_default();
let remaining: VecDeque<String> = branches.into_iter().skip(1).collect();
Self {
started_at: Utc::now(),
backup_id,
current_branch: current,
completed: vec![],
remaining,
}
}
pub fn advance(&mut self) {
if !self.current_branch.is_empty() {
self.completed
.push(std::mem::take(&mut self.current_branch));
}
self.current_branch = self.remaining.pop_front().unwrap_or_default();
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.current_branch.is_empty() && self.remaining.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DivergenceRecord {
pub branch: String,
pub ahead: usize,
pub behind: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestackState {
pub started_at: DateTime<Utc>,
pub backup_id: String,
pub target_branch: String,
pub new_parent: String,
pub old_parent: Option<String>,
pub original_branch: String,
pub current_branch: String,
pub completed: Vec<String>,
pub remaining: VecDeque<String>,
pub stack_updated: bool,
#[serde(default)]
pub diverged_branches: Vec<DivergenceRecord>,
}
impl RestackState {
#[must_use]
pub fn new(
backup_id: String,
target_branch: String,
new_parent: String,
old_parent: Option<String>,
original_branch: String,
branches_to_rebase: Vec<String>,
diverged_branches: Vec<DivergenceRecord>,
) -> Self {
let current = branches_to_rebase.first().cloned().unwrap_or_default();
let remaining: VecDeque<String> = branches_to_rebase.into_iter().skip(1).collect();
Self {
started_at: Utc::now(),
backup_id,
target_branch,
new_parent,
old_parent,
original_branch,
current_branch: current,
completed: vec![],
remaining,
stack_updated: false,
diverged_branches,
}
}
pub fn advance(&mut self) {
if !self.current_branch.is_empty() {
self.completed
.push(std::mem::take(&mut self.current_branch));
}
self.current_branch = self.remaining.pop_front().unwrap_or_default();
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.current_branch.is_empty() && self.remaining.is_empty()
}
pub const fn mark_stack_updated(&mut self) {
self.stack_updated = true;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SplitPoint {
pub commit_sha: String,
pub message: String,
pub branch_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SplitState {
pub started_at: DateTime<Utc>,
pub backup_id: String,
pub source_branch: String,
pub parent_branch: String,
pub original_branch: String,
pub split_points: Vec<SplitPoint>,
pub current_index: usize,
pub completed: Vec<String>,
pub stack_updated: bool,
}
impl SplitState {
#[must_use]
pub fn new(
backup_id: String,
source_branch: String,
parent_branch: String,
original_branch: String,
split_points: Vec<SplitPoint>,
) -> Self {
Self {
started_at: Utc::now(),
backup_id,
source_branch,
parent_branch,
original_branch,
split_points,
current_index: 0,
completed: vec![],
stack_updated: false,
}
}
#[must_use]
pub fn current_split_point(&self) -> Option<&SplitPoint> {
self.split_points.get(self.current_index)
}
pub fn advance(&mut self) {
if self.current_index >= self.split_points.len() {
return;
}
self.completed
.push(self.split_points[self.current_index].branch_name.clone());
self.current_index += 1;
}
#[must_use]
pub const fn is_complete(&self) -> bool {
self.current_index >= self.split_points.len()
}
pub const fn mark_stack_updated(&mut self) {
self.stack_updated = true;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FoldState {
pub started_at: DateTime<Utc>,
pub backup_id: String,
pub target_branch: String,
pub branches_to_fold: Vec<String>,
pub new_parent: String,
pub original_branch: String,
pub prs_to_close: Vec<u64>,
pub completed: Vec<String>,
pub stack_updated: bool,
#[serde(default)]
pub original_stack_json: Option<String>,
}
impl FoldState {
#[must_use]
pub fn new(
backup_id: String,
target_branch: String,
branches_to_fold: Vec<String>,
new_parent: String,
original_branch: String,
prs_to_close: Vec<u64>,
) -> Self {
Self {
started_at: Utc::now(),
backup_id,
target_branch,
branches_to_fold,
new_parent,
original_branch,
prs_to_close,
completed: vec![],
stack_updated: false,
original_stack_json: None,
}
}
pub fn set_original_stack(&mut self, stack_json: String) {
self.original_stack_json = Some(stack_json);
}
pub const fn mark_stack_updated(&mut self) {
self.stack_updated = true;
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use tempfile::TempDir;
fn setup_test_repo() -> (TempDir, State) {
let temp = TempDir::new().unwrap();
fs::create_dir(temp.path().join(".git")).unwrap();
let state = State::new(temp.path()).unwrap();
(temp, state)
}
#[test]
fn test_init_and_check() {
let (_temp, state) = setup_test_repo();
assert!(!state.is_initialized());
state.init().unwrap();
assert!(state.is_initialized());
}
#[test]
fn test_stack_persistence() {
let (_temp, state) = setup_test_repo();
state.init().unwrap();
let mut stack = Stack::new();
stack.add_branch(crate::stack::StackBranch::try_new("feature/test", Some("main")).unwrap());
state.save_stack(&stack).unwrap();
let loaded = state.load_stack().unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded.branches[0].name, "feature/test");
}
#[test]
fn test_backup_operations() {
let (_temp, state) = setup_test_repo();
state.init().unwrap();
let branches = vec![("feature/a", "abc123"), ("feature/b", "def456")];
let backup_id = state.create_backup(&branches).unwrap();
let loaded = state.load_backup(&backup_id).unwrap();
assert_eq!(loaded.len(), 2);
let latest = state.latest_backup().unwrap();
assert_eq!(latest, backup_id);
state.delete_backup(&backup_id).unwrap();
assert!(state.latest_backup().is_err());
}
}