use std::io::{self, IsTerminal, Write};
use git2::Repository;
use crate::cli::args::CommitArgs;
use crate::core::{
check_conflicted_state, detect_version, ensure_sync, sanitize_entries, ChangeState,
CommitPipeline, EnsureSyncResult, GitNativeCommitPipeline, StorageVersion, SynthesizeSummary,
};
use crate::error::{AgitError, Result};
use crate::git::GitRepository;
use crate::storage::{FileHeadStore, FileIndexStore, FileObjectStore, FileRefStore, IndexStore};
fn is_interactive() -> bool {
io::stdin().is_terminal()
}
pub fn execute(args: CommitArgs) -> Result<()> {
let cwd = std::env::current_dir()?;
let agit_dir = cwd.join(".agit");
if !agit_dir.exists() {
return Err(AgitError::NotInitialized);
}
let git_repo = GitRepository::open(&cwd)?;
check_conflicted_state(&git_repo)?;
if let Some(result) = ensure_sync(&cwd, &agit_dir)? {
match &result {
EnsureSyncResult::ForkedToNew { new_branch, .. } => {
println!("Syncing Agit memory to new branch: '{}'", new_branch);
},
EnsureSyncResult::SwitchedToExisting { new_branch, .. } => {
println!("Syncing Agit memory to branch: '{}'", new_branch);
},
_ => {},
}
}
let index_store = FileIndexStore::new(&agit_dir);
let has_staged = index_store.has_staged()?;
let pending_entries = index_store.read_all()?;
let mut entries = if has_staged {
index_store.read_staged()?
} else {
pending_entries.clone()
};
if !args.journal && !entries.is_empty() {
let staged_files = git_repo.staged_files()?;
let sanitize_result = sanitize_entries(entries, &staged_files);
if !sanitize_result.pruned.is_empty() {
eprintln!();
for (pruned_entry, orphaned_paths) in &sanitize_result.pruned {
for path in orphaned_paths {
eprintln!(" Pruning orphaned memory for reverted file: {}", path);
}
let content_preview = if pruned_entry.content.len() > 60 {
format!("{}...", &pruned_entry.content[..60])
} else {
pruned_entry.content.clone()
};
eprintln!(
" \u{2514} {}: \"{}\"",
pruned_entry.category, content_preview
);
}
eprintln!();
eprintln!(
" {} memory entries pruned (files not staged)",
sanitize_result.pruned.len()
);
eprintln!();
}
entries = sanitize_result.kept;
if entries.is_empty() && staged_files.is_empty() {
return Err(AgitError::InvalidArgument(
"No effective changes detected. Memories for reverted files were pruned.\n\
Use `agit commit --journal` to force a context-only commit."
.to_string(),
));
}
}
if entries.is_empty() && !args.amend && !args.journal {
println!("No thoughts recorded in staging area.");
println!(
"Use 'agit record' to add thoughts, or the MCP server will log them automatically."
);
return Ok(());
}
let message = match args.message {
Some(msg) => msg,
None => {
return Err(AgitError::InvalidArgument(
"Commit message required. Use -m or --message".to_string(),
));
},
};
let version = {
let repo = Repository::discover(&cwd)?;
detect_version(&agit_dir, &repo)
};
let is_v2 = matches!(version, StorageVersion::V2GitNative);
let change_state = if is_v2 {
let pipeline = GitNativeCommitPipeline::new(agit_dir.clone(), GitRepository::open(&cwd)?)?;
pipeline.detect_change_state()?
} else {
let object_store = FileObjectStore::new(&agit_dir);
let ref_store = FileRefStore::new(&agit_dir);
let head_store = FileHeadStore::new(&agit_dir);
let pipeline = CommitPipeline::new(
agit_dir.clone(),
GitRepository::open(&cwd)?,
object_store,
ref_store,
head_store,
FileIndexStore::new(&agit_dir),
);
pipeline.detect_change_state()?
};
if change_state == ChangeState::MemoryOnly {
if args.journal || args.yes {
println!("[Agit] Creating Journal Entry (memory-only commit)...");
} else if is_interactive() {
println!();
println!("⚠️ No code changes detected. This will be saved as a 'Journal Entry'");
println!(" (Empty Commit) to preserve the decision history.");
println!();
print!("Are you sure? [y/N]: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
match input.trim().to_lowercase().as_str() {
"y" | "yes" => {
println!();
println!("[Agit] Creating Journal Entry (memory-only commit)...");
},
_ => {
println!();
println!("Commit cancelled.");
println!(
"Tip: Use 'agit commit --journal' to explicitly create a Journal Entry."
);
return Ok(());
},
}
} else {
return Err(AgitError::InvalidArgument(
"No code changes detected. To save a pure thought/decision, use `agit commit --journal`.".to_string()
));
}
}
if change_state == ChangeState::NoChanges {
if args.journal {
println!("[Agit] Creating empty Journal Entry (decision checkpoint)...");
} else {
return Err(AgitError::NothingToCommit);
}
}
let summary = SynthesizeSummary::synthesize(&entries);
let final_summary = if args.edit_summary {
println!("Summary: {}", summary);
summary
} else {
summary
};
let result = if is_v2 {
let mut pipeline =
GitNativeCommitPipeline::new(agit_dir.clone(), GitRepository::open(&cwd)?)?;
pipeline.execute(&message, &final_summary, args.force)?
} else {
let object_store = FileObjectStore::new(&agit_dir);
let ref_store = FileRefStore::new(&agit_dir);
let head_store = FileHeadStore::new(&agit_dir);
let mut pipeline = CommitPipeline::new(
agit_dir.clone(),
GitRepository::open(&cwd)?,
object_store,
ref_store,
head_store,
index_store.clone(),
);
pipeline.execute(&message, &final_summary, args.force)?
};
if result.is_memory_only {
println!("[Agit] Memory-only commit (no code changes)");
}
if result.git_commit_created {
println!("Created git commit: {}", &result.git_hash[..7]);
} else {
println!("Linked to git commit: {}", &result.git_hash[..7]);
}
println!("Created neural commit: {}", &result.neural_hash[..7]);
println!();
println!("Summary: {}", final_summary);
Ok(())
}