//! Commands that act on the local repository and working copy. use super::format::{deps_line, deps_of, plural, short_id, short_meta_id, signature_line, when}; use super::open; use super::ui; 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!("{} oxy repository in {}", ui::ok("Initialized"), ui::path(repo.root().display())); println!(" bookmark {} author {} <{}>", ui::id(&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 sp = ui::spinner("Snapshotting the working copy…"); let snap = repo.snapshot("oxy status")?; let heads = repo.bookmark_heads()?; sp.clear(); println!( "{} {} on {} {}", ui::heading("Working copy"), ui::id(snap.wc.change_id.short(8)), ui::id(&repo.config.bookmark), ui::dim(format!("({})", plural(heads.len(), "head"))) ); if snap.statuses.is_empty() { println!(" {}", ui::dim("clean")); } for s in &snap.statuses { println!(" {:2} {}", ui::tag(status_tag(s.status)), ui::path(&s.path)); } if !snap.base_snapshot.is_conflicted() { return Ok(0); } println!(); println!("{} {}", ui::err("Conflicts"), ui::dim("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) => ui::ok("resolved in working copy"), Some(ChangeStatus::ConflictEdited) => ui::warn("markers remain"), _ => ui::err("unresolved"), }; println!(" {} {state}", ui::path(p)); } } 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 sp = ui::spinner("Committing…"); let h = repo.commit(message, &deps)?; let c = repo.store().get_change(&h)?; sp.done(&format!("{} {} {} {}", ui::ok("Committed"), ui::id(short_id(&c)), ui::hash(format!("#{}", 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 { ui::warn(format!(" ({})", flags.join(", "))) }; println!("{} {}{}", ui::id(short_meta_id(c)), ui::hash(format!("#{}", e.hash.short(12))), flags); println!(" {}", ui::dim(when(c.hlc.wall_ms))); if !c.deps.is_empty() { println!(" {}", ui::dim(format!("deps: {}", deps_of(&c.deps)))); } 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!("{} {} {}", ui::heading("change"), ui::id(short_id(&c)), ui::hash(format!("#{h}"))); println!(" kind {:?}", c.kind); println!(" date {}", when(c.hlc.wall_ms)); println!(" deps {}", ui::dim(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 sp = ui::spinner(&format!("Checking out {spec}…")); let r = repo.checkout(&heads, discard, &format!("oxy checkout {spec}"))?; sp.done(&format!("{} on {}: {} written, {} removed", ui::ok("Working copy"), ui::id(&spec), plural(r.written, "file"), r.removed)); if r.conflicted { println!(" {}", ui::warn("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!("{} now at: {}", ui::ok("Undone;"), ui::dim(&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 { ui::ok("@ ") } else { " ".into() }, ui::hash(r.short(12)), ui::dim(when(op.hlc.wall_ms)), op.command); } Ok(0) } pub fn gc(now: bool) -> Result { let repo = open()?; let sp = ui::spinner("Packing and pruning…"); let r = repo.gc(now)?; sp.clear(); 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() }, if r.merged_packs > 0 { format!("; merged {} into one", plural(r.merged_packs, "pack")) } 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!("{} {} {} {}", ui::ok("Described"), ui::id(short_id(&c)), ui::hash(format!("#{}", 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!("{} {} {} {}", ui::ok("Amended"), ui::id(short_id(&c)), ui::hash(format!("#{}", 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 sp = ui::spinner("Writing checkpoint…"); let (r, changes, bytes) = repo.checkpoint()?; sp.done(&format!("{} {} of {} ({} bytes) for {}", ui::ok("Checkpoint"), ui::hash(format!("#{}", r.short(12))), plural(changes, "change"), bytes, ui::id(&repo.config.bookmark))); Ok(0) } /// For shell prompts: silent (exit 1) outside a repository, otherwise one line. pub fn prompt(format: &str) -> Result { let Ok(repo) = Repo::open(&std::env::current_dir()?) else { return Ok(1) }; let info = repo.quick_status()?; println!("{}", info.format(format)); Ok(0) } pub fn sparse(paths: &[String]) -> Result { let mut repo = open()?; if paths.is_empty() { if repo.config.sparse_paths.is_empty() { println!("No sparse filter: the whole tree is materialized."); } else { println!("{}", ui::heading("Sparse filter")); for p in &repo.config.sparse_paths { println!(" {}", ui::path(p)); } } return Ok(0); } let snap = repo.snapshot("oxy sparse")?; if !snap.wc.tree_ops.is_empty() || !snap.wc.edits.is_empty() { anyhow::bail!("the working copy has changes; commit them before changing the sparse filter"); } repo.config.set("filter.paths", &paths.join(","))?; repo.save_config()?; let heads = repo.bookmark_heads()?; let sp = ui::spinner("Re-checking out with the new filter…"); let r = repo.checkout(&heads, true, "oxy sparse (checkout)")?; sp.done(&format!("{} {}: {} written, {} removed", ui::ok("Sparse filter set to"), ui::path(paths.join(",")), plural(r.written, "file"), r.removed)); Ok(0) }