//! Commands that act on the local repository and working copy. use super::format::{deps_line, plural, short_id, signature_line, when}; use super::open; use anyhow::Result; use oxy_core::repo::{render_change, ChangeStatus, Repo}; use std::path::PathBuf; pub fn init(path: Option) -> Result { let path = path.unwrap_or(std::env::current_dir()?); let repo = Repo::init(&path)?; println!("Initialized oxy repository in {}", repo.root().display()); println!("Bookmark: {} Author: {} <{}>", repo.config.bookmark, repo.config.author_name, repo.config.author_email); Ok(0) } fn status_tag(s: ChangeStatus) -> &'static str { match s { ChangeStatus::Added => "A", ChangeStatus::Modified => "M", ChangeStatus::Removed => "D", ChangeStatus::ModeChanged => "X", ChangeStatus::ConflictEdited => "C!", ChangeStatus::Resolved => "R", } } pub fn status() -> Result { let repo = open()?; let snap = repo.snapshot("oxy status")?; let heads = repo.bookmark_heads()?; println!("Working copy: {} on {} ({})", snap.wc.change_id.short(8), repo.config.bookmark, plural(heads.len(), "head")); if snap.statuses.is_empty() { println!("The working copy is clean."); } for s in &snap.statuses { println!("{:2} {}", status_tag(s.status), s.path); } if !snap.base_snapshot.is_conflicted() { return Ok(0); } println!("\nConflicts in the base state (edit the marked files, or the .oxy-conflict-N candidates, then commit):"); for c in &snap.base_snapshot.tree.conflicts { println!(" tree: {c:?}"); } for (p, f) in &snap.base_snapshot.files { if f.is_conflicted() { let state = match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) { Some(ChangeStatus::Resolved) => "resolved in working copy", Some(ChangeStatus::ConflictEdited) => "markers remain", _ => "unresolved", }; println!(" {p} ({state})"); } } Ok(2) } pub fn commit(message: &str, depends_on: &[String]) -> Result { let repo = open()?; let mut deps = Vec::new(); for d in depends_on { deps.push(repo.resolve_change(d)?); } let h = repo.commit(message, &deps)?; let c = repo.store().get_change(&h)?; println!("Committed {} #{}: {}", short_id(&c), h.short(12), message.lines().next().unwrap_or("")); Ok(0) } pub fn log() -> Result { let repo = open()?; repo.snapshot("oxy log")?; for e in repo.log()? { let c = &e.change; let mut flags = Vec::new(); if e.is_working_copy { flags.push("working copy"); } if e.is_head { flags.push("head"); } let flags = if flags.is_empty() { String::new() } else { format!(" ({})", flags.join(", ")) }; println!("{} #{}{}", short_id(c), e.hash.short(12), flags); println!(" {}", when(c.hlc.wall_ms)); if !c.deps.is_empty() { println!(" deps: {}", deps_line(c)); } let msg = if c.message.is_empty() { "(no description)" } else { &c.message }; for line in msg.lines() { println!(" {line}"); } println!(); } Ok(0) } pub fn diff() -> Result { let repo = open()?; let snap = repo.snapshot("oxy diff")?; print!("{}", render_change(&snap.wc, &snap.base, &snap.base_snapshot)); Ok(0) } pub fn show(change: &str) -> Result { let repo = open()?; repo.snapshot("oxy show")?; let h = repo.resolve_change(change)?; let c = repo.store().get_change(&h)?; println!("change {} #{}", short_id(&c), h); println!("kind: {:?}", c.kind); println!("date: {}", when(c.hlc.wall_ms)); println!("deps: {}", deps_line(&c)); println!("sig: {}", signature_line(&repo, &h)?); println!("\n{}\n", if c.message.is_empty() { "(no description)" } else { &c.message }); print!("{}", repo.describe_change(&h)?); Ok(0) } pub fn checkout(changes: &[String], discard: bool) -> Result { let repo = open()?; let heads = if changes.is_empty() { repo.bookmark_heads()? } else { changes.iter().map(|c| repo.resolve_change(c)).collect::, _>>()? }; let spec = if changes.is_empty() { repo.config.bookmark.clone() } else { changes.join(" ") }; let r = repo.checkout(&heads, discard, &format!("oxy checkout {spec}"))?; println!("Working copy now on {spec}: {} written, {} removed", plural(r.written, "file"), r.removed); if r.conflicted { println!("This state has conflicts; see oxy status."); } Ok(0) } pub fn resolve() -> Result { let repo = open()?; let snap = repo.snapshot("oxy resolve")?; if !snap.base_snapshot.is_conflicted() { println!("The working copy's base has no conflicts."); return Ok(0); } let mut remaining = 0; for (p, f) in &snap.base_snapshot.files { if !f.is_conflicted() { continue; } match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) { Some(ChangeStatus::Resolved) => println!("resolved {p}"), Some(ChangeStatus::ConflictEdited) => { remaining += 1; println!("markers {p}"); } _ => { remaining += 1; println!("unresolved {p}"); } } } for c in &snap.base_snapshot.tree.conflicts { remaining += 1; println!("tree {c:?}"); } if remaining == 0 { println!("All conflicts are resolved; `oxy commit` will record the resolution."); Ok(0) } else { Ok(2) } } pub fn undo() -> Result { let repo = open()?; let op = repo.undo()?; println!("Undid the last operation; now at: {}", op.command); Ok(0) } pub fn op_log() -> Result { let repo = open()?; for (i, (r, op)) in repo.op_log()?.into_iter().enumerate() { println!("{}{} {} {}", if i == 0 { "@ " } else { " " }, r.short(12), when(op.hlc.wall_ms), op.command); } Ok(0) } pub fn gc(now: bool) -> Result { let repo = open()?; let r = repo.gc(now)?; if r.packed.objects > 0 { println!( "Packed {}: {} bytes -> {} bytes ({:.0}%).", plural(r.packed.objects, "loose object"), r.packed.raw_bytes, r.packed.packed_bytes, 100.0 * r.packed.packed_bytes as f64 / r.packed.raw_bytes.max(1) as f64 ); } println!("{} reachable; pruned {} ({} bytes){}; {} present.", plural(r.reachable, "object"), plural(r.pruned.objects, "unreachable object"), r.pruned.bytes, if r.pruned.deferred > 0 { format!("; {} deferred by the grace period (use --now)", plural(r.pruned.deferred, "object")) } else { String::new() }, plural(repo.store().pack_count(), "pack") ); Ok(0) } /// The single head the working copy sits on, for commands that default to it. fn current_head(repo: &Repo) -> Result { let snap = repo.snapshot("oxy")?; let base = snap.base.maximal(&snap.wc.deps); match base.as_slice() { [h] => Ok(*h), _ => anyhow::bail!("the working copy sits on {} heads; name the change explicitly", base.len()), } } pub fn describe(message: &str, change: Option<&str>) -> Result { let repo = open()?; let h = match change { Some(c) => repo.resolve_change(c)?, None => current_head(&repo)?, }; let new = repo.describe(&h, message)?; let c = repo.store().get_change(&new)?; println!("Described {} #{}: {}", short_id(&c), new.short(12), message.lines().next().unwrap_or("")); Ok(0) } pub fn amend(message: Option<&str>) -> Result { let repo = open()?; let new = repo.amend(message)?; let c = repo.store().get_change(&new)?; println!("Amended {} #{}: {}", short_id(&c), new.short(12), c.message.lines().next().unwrap_or("")); Ok(0) } pub fn abandon(change: &str) -> Result { let repo = open()?; let h = repo.resolve_change(change)?; let c = repo.store().get_change(&h)?; repo.abandon(&h)?; println!("Abandoned {} #{}; its content remains on disk as uncommitted work if the working copy sat on it.", short_id(&c), h.short(12)); Ok(0) } pub fn checkpoint() -> Result { let repo = open()?; let (r, changes, bytes) = repo.checkpoint()?; println!("Checkpoint #{} of {} ({} bytes) written for {}.", r.short(12), plural(changes, "change"), bytes, repo.config.bookmark); Ok(0) }