OXYGENHUB
# c177d1752eaa oxygen / src / cli / workspace.rs
9.9 KiB · Rust · last changed Pack merging above 16 packs; modern CLI presentation (colour, spinners, --plain, NO_COLOR); oxy prompt with manifest-based dirty check and docs/prompt.md 1 month ago source blame history raw
fce44ae9 Andy McCall ↶ 1
//! Commands that act on the local repository and working copy.
2
3
use super::format::{deps_line, plural, short_id, signature_line, when};
4
use super::open;
135212de Andy McCall ↶ 5
use super::ui;
fce44ae9 Andy McCall ↶ 6
use anyhow::Result;
7
use oxy_core::repo::{render_change, ChangeStatus, Repo};
8
use std::path::PathBuf;
9
10
pub fn init(path: Option<PathBuf>) -> Result<i32> {
11
    let path = path.unwrap_or(std::env::current_dir()?);
12
    let repo = Repo::init(&path)?;
135212de Andy McCall ↶ 13
    println!("{} oxy repository in {}", ui::ok("Initialized"), ui::path(repo.root().display()));
14
    println!("  bookmark {}   author {} <{}>", ui::id(&repo.config.bookmark), repo.config.author_name, repo.config.author_email);
fce44ae9 Andy McCall ↶ 15
    Ok(0)
16
}
17
18
fn status_tag(s: ChangeStatus) -> &'static str {
19
    match s {
20
        ChangeStatus::Added => "A",
21
        ChangeStatus::Modified => "M",
22
        ChangeStatus::Removed => "D",
23
        ChangeStatus::ModeChanged => "X",
24
        ChangeStatus::ConflictEdited => "C!",
25
        ChangeStatus::Resolved => "R",
26
    }
27
}
28
29
pub fn status() -> Result<i32> {
30
    let repo = open()?;
135212de Andy McCall ↶ 31
    let sp = ui::spinner("Snapshotting the working copy…");
fce44ae9 Andy McCall ↶ 32
    let snap = repo.snapshot("oxy status")?;
33
    let heads = repo.bookmark_heads()?;
135212de Andy McCall ↶ 34
    sp.clear();
35
    println!(
36
        "{} {} on {} {}",
37
        ui::heading("Working copy"),
38
        ui::id(snap.wc.change_id.short(8)),
39
        ui::id(&repo.config.bookmark),
40
        ui::dim(format!("({})", plural(heads.len(), "head")))
41
    );
fce44ae9 Andy McCall ↶ 42
    if snap.statuses.is_empty() {
135212de Andy McCall ↶ 43
        println!("  {}", ui::dim("clean"));
fce44ae9 Andy McCall ↶ 44
    }
45
    for s in &snap.statuses {
135212de Andy McCall ↶ 46
        println!("  {:2} {}", ui::tag(status_tag(s.status)), ui::path(&s.path));
fce44ae9 Andy McCall ↶ 47
    }
48
    if !snap.base_snapshot.is_conflicted() {
49
        return Ok(0);
50
    }
135212de Andy McCall ↶ 51
    println!();
52
    println!("{} {}", ui::err("Conflicts"), ui::dim("edit the marked files, or the .oxy-conflict-N candidates, then commit"));
fce44ae9 Andy McCall ↶ 53
    for c in &snap.base_snapshot.tree.conflicts {
54
        println!("  tree: {c:?}");
55
    }
56
    for (p, f) in &snap.base_snapshot.files {
57
        if f.is_conflicted() {
58
            let state = match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) {
135212de Andy McCall ↶ 59
                Some(ChangeStatus::Resolved) => ui::ok("resolved in working copy"),
60
                Some(ChangeStatus::ConflictEdited) => ui::warn("markers remain"),
61
                _ => ui::err("unresolved"),
fce44ae9 Andy McCall ↶ 62
            };
135212de Andy McCall ↶ 63
            println!("  {}  {state}", ui::path(p));
fce44ae9 Andy McCall ↶ 64
        }
65
    }
66
    Ok(2)
67
}
68
69
pub fn commit(message: &str, depends_on: &[String]) -> Result<i32> {
70
    let repo = open()?;
71
    let mut deps = Vec::new();
72
    for d in depends_on {
73
        deps.push(repo.resolve_change(d)?);
74
    }
135212de Andy McCall ↶ 75
    let sp = ui::spinner("Committing…");
fce44ae9 Andy McCall ↶ 76
    let h = repo.commit(message, &deps)?;
77
    let c = repo.store().get_change(&h)?;
135212de Andy McCall ↶ 78
    sp.done(&format!("{} {} {} {}", ui::ok("Committed"), ui::id(short_id(&c)), ui::hash(format!("#{}", h.short(12))), message.lines().next().unwrap_or("")));
fce44ae9 Andy McCall ↶ 79
    Ok(0)
80
}
81
82
pub fn log() -> Result<i32> {
83
    let repo = open()?;
84
    repo.snapshot("oxy log")?;
85
    for e in repo.log()? {
86
        let c = &e.change;
87
        let mut flags = Vec::new();
88
        if e.is_working_copy {
89
            flags.push("working copy");
90
        }
91
        if e.is_head {
92
            flags.push("head");
93
        }
135212de Andy McCall ↶ 94
        let flags = if flags.is_empty() { String::new() } else { ui::warn(format!(" ({})", flags.join(", "))) };
95
        println!("{} {}{}", ui::id(short_id(c)), ui::hash(format!("#{}", e.hash.short(12))), flags);
96
        println!("    {}", ui::dim(when(c.hlc.wall_ms)));
fce44ae9 Andy McCall ↶ 97
        if !c.deps.is_empty() {
135212de Andy McCall ↶ 98
            println!("    {}", ui::dim(format!("deps: {}", deps_line(c))));
fce44ae9 Andy McCall ↶ 99
        }
100
        let msg = if c.message.is_empty() { "(no description)" } else { &c.message };
101
        for line in msg.lines() {
102
            println!("    {line}");
103
        }
104
        println!();
105
    }
106
    Ok(0)
107
}
108
109
pub fn diff() -> Result<i32> {
110
    let repo = open()?;
111
    let snap = repo.snapshot("oxy diff")?;
112
    print!("{}", render_change(&snap.wc, &snap.base, &snap.base_snapshot));
113
    Ok(0)
114
}
115
116
pub fn show(change: &str) -> Result<i32> {
117
    let repo = open()?;
118
    repo.snapshot("oxy show")?;
119
    let h = repo.resolve_change(change)?;
120
    let c = repo.store().get_change(&h)?;
135212de Andy McCall ↶ 121
    println!("{} {} {}", ui::heading("change"), ui::id(short_id(&c)), ui::hash(format!("#{h}")));
122
    println!("  kind  {:?}", c.kind);
123
    println!("  date  {}", when(c.hlc.wall_ms));
124
    println!("  deps  {}", ui::dim(deps_line(&c)));
125
    println!("  sig   {}", signature_line(&repo, &h)?);
fce44ae9 Andy McCall ↶ 126
    println!("\n{}\n", if c.message.is_empty() { "(no description)" } else { &c.message });
127
    print!("{}", repo.describe_change(&h)?);
128
    Ok(0)
129
}
130
131
pub fn checkout(changes: &[String], discard: bool) -> Result<i32> {
132
    let repo = open()?;
133
    let heads = if changes.is_empty() {
134
        repo.bookmark_heads()?
135
    } else {
136
        changes.iter().map(|c| repo.resolve_change(c)).collect::<Result<Vec<_>, _>>()?
137
    };
138
    let spec = if changes.is_empty() { repo.config.bookmark.clone() } else { changes.join(" ") };
135212de Andy McCall ↶ 139
    let sp = ui::spinner(&format!("Checking out {spec}…"));
fce44ae9 Andy McCall ↶ 140
    let r = repo.checkout(&heads, discard, &format!("oxy checkout {spec}"))?;
135212de Andy McCall ↶ 141
    sp.done(&format!("{} on {}: {} written, {} removed", ui::ok("Working copy"), ui::id(&spec), plural(r.written, "file"), r.removed));
fce44ae9 Andy McCall ↶ 142
    if r.conflicted {
135212de Andy McCall ↶ 143
        println!("  {}", ui::warn("this state has conflicts; see oxy status"));
fce44ae9 Andy McCall ↶ 144
    }
145
    Ok(0)
146
}
147
148
pub fn resolve() -> Result<i32> {
149
    let repo = open()?;
150
    let snap = repo.snapshot("oxy resolve")?;
151
    if !snap.base_snapshot.is_conflicted() {
152
        println!("The working copy's base has no conflicts.");
153
        return Ok(0);
154
    }
155
    let mut remaining = 0;
156
    for (p, f) in &snap.base_snapshot.files {
157
        if !f.is_conflicted() {
158
            continue;
159
        }
160
        match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) {
161
            Some(ChangeStatus::Resolved) => println!("resolved   {p}"),
162
            Some(ChangeStatus::ConflictEdited) => {
163
                remaining += 1;
164
                println!("markers    {p}");
165
            }
166
            _ => {
167
                remaining += 1;
168
                println!("unresolved {p}");
169
            }
170
        }
171
    }
172
    for c in &snap.base_snapshot.tree.conflicts {
173
        remaining += 1;
174
        println!("tree       {c:?}");
175
    }
176
    if remaining == 0 {
177
        println!("All conflicts are resolved; `oxy commit` will record the resolution.");
178
        Ok(0)
179
    } else {
180
        Ok(2)
181
    }
182
}
183
184
pub fn undo() -> Result<i32> {
185
    let repo = open()?;
186
    let op = repo.undo()?;
135212de Andy McCall ↶ 187
    println!("{} now at: {}", ui::ok("Undone;"), ui::dim(&op.command));
fce44ae9 Andy McCall ↶ 188
    Ok(0)
189
}
190
191
pub fn op_log() -> Result<i32> {
192
    let repo = open()?;
193
    for (i, (r, op)) in repo.op_log()?.into_iter().enumerate() {
135212de Andy McCall ↶ 194
        println!("{}{} {}  {}", if i == 0 { ui::ok("@ ") } else { "  ".into() }, ui::hash(r.short(12)), ui::dim(when(op.hlc.wall_ms)), op.command);
fce44ae9 Andy McCall ↶ 195
    }
196
    Ok(0)
197
}
d02e545d Andy McCall ↶ 198
ba160f60 Andy McCall ↶ 199
pub fn gc(now: bool) -> Result<i32> {
d02e545d Andy McCall ↶ 200
    let repo = open()?;
135212de Andy McCall ↶ 201
    let sp = ui::spinner("Packing and pruning…");
ba160f60 Andy McCall ↶ 202
    let r = repo.gc(now)?;
135212de Andy McCall ↶ 203
    sp.clear();
ba160f60 Andy McCall ↶ 204
    if r.packed.objects > 0 {
d02e545d Andy McCall ↶ 205
        println!(
ba160f60 Andy McCall ↶ 206
            "Packed {}: {} bytes -> {} bytes ({:.0}%).",
207
            plural(r.packed.objects, "loose object"),
208
            r.packed.raw_bytes,
209
            r.packed.packed_bytes,
210
            100.0 * r.packed.packed_bytes as f64 / r.packed.raw_bytes.max(1) as f64
d02e545d Andy McCall ↶ 211
        );
212
    }
135212de Andy McCall ↶ 213
    println!("{} reachable; pruned {} ({} bytes){}{}; {} present.",
ba160f60 Andy McCall ↶ 214
        plural(r.reachable, "object"),
215
        plural(r.pruned.objects, "unreachable object"),
216
        r.pruned.bytes,
217
        if r.pruned.deferred > 0 { format!("; {} deferred by the grace period (use --now)", plural(r.pruned.deferred, "object")) } else { String::new() },
135212de Andy McCall ↶ 218
        if r.merged_packs > 0 { format!("; merged {} into one", plural(r.merged_packs, "pack")) } else { String::new() },
ba160f60 Andy McCall ↶ 219
        plural(repo.store().pack_count(), "pack")
220
    );
d02e545d Andy McCall ↶ 221
    Ok(0)
222
}
614541c3 Andy McCall ↶ 223
224
/// The single head the working copy sits on, for commands that default to it.
225
fn current_head(repo: &Repo) -> Result<oxy_core::hash::ChangeHash> {
226
    let snap = repo.snapshot("oxy")?;
227
    let base = snap.base.maximal(&snap.wc.deps);
228
    match base.as_slice() {
229
        [h] => Ok(*h),
230
        _ => anyhow::bail!("the working copy sits on {} heads; name the change explicitly", base.len()),
231
    }
232
}
233
234
pub fn describe(message: &str, change: Option<&str>) -> Result<i32> {
235
    let repo = open()?;
236
    let h = match change {
237
        Some(c) => repo.resolve_change(c)?,
238
        None => current_head(&repo)?,
239
    };
240
    let new = repo.describe(&h, message)?;
241
    let c = repo.store().get_change(&new)?;
135212de Andy McCall ↶ 242
    println!("{} {} {} {}", ui::ok("Described"), ui::id(short_id(&c)), ui::hash(format!("#{}", new.short(12))), message.lines().next().unwrap_or(""));
614541c3 Andy McCall ↶ 243
    Ok(0)
244
}
245
246
pub fn amend(message: Option<&str>) -> Result<i32> {
247
    let repo = open()?;
248
    let new = repo.amend(message)?;
249
    let c = repo.store().get_change(&new)?;
135212de Andy McCall ↶ 250
    println!("{} {} {} {}", ui::ok("Amended"), ui::id(short_id(&c)), ui::hash(format!("#{}", new.short(12))), c.message.lines().next().unwrap_or(""));
614541c3 Andy McCall ↶ 251
    Ok(0)
252
}
253
254
pub fn abandon(change: &str) -> Result<i32> {
255
    let repo = open()?;
256
    let h = repo.resolve_change(change)?;
257
    let c = repo.store().get_change(&h)?;
258
    repo.abandon(&h)?;
259
    println!("Abandoned {} #{}; its content remains on disk as uncommitted work if the working copy sat on it.", short_id(&c), h.short(12));
260
    Ok(0)
261
}
252e8cb2 Andy McCall ↶ 262
263
pub fn checkpoint() -> Result<i32> {
264
    let repo = open()?;
135212de Andy McCall ↶ 265
    let sp = ui::spinner("Writing checkpoint…");
252e8cb2 Andy McCall ↶ 266
    let (r, changes, bytes) = repo.checkpoint()?;
135212de Andy McCall ↶ 267
    sp.done(&format!("{} {} of {} ({} bytes) for {}", ui::ok("Checkpoint"), ui::hash(format!("#{}", r.short(12))), plural(changes, "change"), bytes, ui::id(&repo.config.bookmark)));
268
    Ok(0)
269
}
270
271
/// For shell prompts: silent (exit 1) outside a repository, otherwise one line.
272
pub fn prompt(format: &str) -> Result<i32> {
273
    let Ok(repo) = Repo::open(&std::env::current_dir()?) else { return Ok(1) };
274
    let info = repo.quick_status()?;
275
    println!("{}", info.format(format));
252e8cb2 Andy McCall ↶ 276
    Ok(0)
277
}