use std::time::Instant;
use anyhow::Result;
use objects::object::{ChangeId, State, ThreadName};
use oplog::OpRecord;
use refs::{Head, RefExpectation, RefUpdate};
use serde::Serialize;
use super::{
advice::RecoveryAdvice,
history_target::{require_resolved_state, resolve_state_id},
};
use crate::{
cli::{Cli, commands::snapshot::resolve_attribution, should_output_json},
config::UserConfig,
};
#[derive(Serialize)]
struct CollapseOutput {
change_id: String,
content_hash: String,
collapsed_count: usize,
intent: Option<String>,
message: String,
}
pub fn cmd_collapse(
cli: &Cli,
states: Vec<String>,
into: String,
confidence: Option<f32>,
) -> Result<()> {
let repo = cli.open_repo()?;
let json = should_output_json(cli, Some(repo.config()));
if states.is_empty() {
return Err(anyhow::anyhow!(collapse_requires_states_advice()));
}
let started = Instant::now();
if !json {
eprintln!("Collapsing {} states: resolving state ids...", states.len());
}
let mut resolved_states = Vec::new();
for state_spec in &states {
let change_id = resolve_state_id(&repo, state_spec)?;
let state = require_resolved_state(&repo, &change_id)?;
resolved_states.push(state);
}
let last_state = &resolved_states[resolved_states.len() - 1];
if !json {
eprintln!("Using final tree from {}", last_state.change_id.short());
}
let user_config = UserConfig::load_default()?;
let published_ref = match repo.refs().read_head()? {
Head::Attached { ref thread } => CollapsePublishedRef::Thread(thread.clone()),
Head::Detached { .. } => CollapsePublishedRef::DetachedHead,
};
let new_state = collapse_resolved_states(
&repo,
&user_config,
&resolved_states,
into.clone(),
confidence,
published_ref,
)?;
if !json {
eprintln!("Writing collapsed state {}...", new_state.change_id.short());
}
let output = CollapseOutput {
change_id: new_state.change_id.short(),
content_hash: new_state.compute_hash().short(),
collapsed_count: resolved_states.len(),
intent: Some(into),
message: format!(
"Collapsed {} states into {}",
resolved_states.len(),
new_state.change_id.short()
),
};
if json {
println!("{}", serde_json::to_string(&output)?);
} else {
println!(
"{} in {:.1}s",
output.message,
started.elapsed().as_secs_f32()
);
}
Ok(())
}
pub(crate) enum CollapsePublishedRef {
Thread(ThreadName),
DetachedHead,
}
pub(crate) fn collapse_resolved_states(
repo: &repo::Repository,
user_config: &UserConfig,
resolved_states: &[State],
into: String,
confidence: Option<f32>,
published_ref: CollapsePublishedRef,
) -> Result<State> {
let first_state = &resolved_states[0];
let last_state = &resolved_states[resolved_states.len() - 1];
let attribution = resolve_attribution(repo, user_config)?;
let mut new_state =
State::new_collapse_of(last_state.tree, first_state.parents.clone(), attribution);
new_state = new_state.with_intent(into);
if let Some(provenance) = repo.get_state_provenance_root(last_state)? {
new_state = new_state.with_provenance(provenance);
}
if let Some(conf) = confidence {
new_state = new_state.with_confidence(conf);
}
repo.put_authored_state(&mut new_state)?;
let (ref_updates, published_thread, pre_thread_state): (
Vec<RefUpdate>,
Option<String>,
Option<ChangeId>,
) = match published_ref {
CollapsePublishedRef::Thread(thread) => {
let previous = repo.refs().get_thread(&thread)?;
(
vec![RefUpdate::Thread {
name: thread.clone(),
expected: RefExpectation::Any,
new: Some(new_state.change_id),
}],
Some(thread.to_string()),
previous,
)
}
CollapsePublishedRef::DetachedHead => (
vec![RefUpdate::Head {
expected: RefExpectation::Any,
new: Head::Detached {
state: new_state.change_id,
},
}],
None,
None,
),
};
let source_ids: Vec<ChangeId> = resolved_states.iter().map(|s| s.change_id).collect();
let collapse_record = OpRecord::Collapse {
sources: source_ids,
result: new_state.change_id,
thread: published_thread,
pre_thread_state,
};
repo.commit_and_publish(vec![collapse_record], &ref_updates)?;
Ok(new_state)
}
fn collapse_requires_states_advice() -> RecoveryAdvice {
RecoveryAdvice::safety_refusal(
"collapse_states_required",
"No states specified to collapse",
"List recent states with `heddle log`, then rerun `heddle collapse <state> --into <intent>` with at least one source state.",
"collapse was invoked without any source state ids",
"collapsing without source states would have to guess which history range should be replaced",
"no collapsed state was written; HEAD, refs, oplog, and worktree files were left unchanged",
"heddle log",
vec![
"heddle log".to_string(),
"heddle collapse <state> --into <intent>".to_string(),
],
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collapse_requires_states_advice_is_typed() {
let advice = collapse_requires_states_advice();
assert_eq!(advice.kind, "collapse_states_required");
assert_eq!(advice.primary_command, "heddle log");
assert!(advice.primary_hint().contains("heddle collapse"));
assert!(advice.unsafe_condition.contains("without any source"));
assert!(advice.would_change.contains("guess"));
assert!(advice.preserved.contains("HEAD"));
}
}