OXYGENHUB
# 94084087a876 oxygen / src / cli / workspace.rs
18.1 KiB · Rust · last changed History editing: orphaned references on rewrite are reified by re-anchoring (nearest surviving line of the chain, else the chain's anchors; vanished nodes drop their edits) and reported as notes instead of refused (spec 0.6m) 1 month ago source blame history raw
fce44ae9 Andy McCall ↶ 1
//! Commands that act on the local repository and working copy.
2
94084087 Andy McCall ↶ 3
use super::format::{author_name, deps_line, deps_of, plural, short_id, short_meta_id, signature_line, signature_mark, subject_of, when};
fce44ae9 Andy McCall ↶ 4
use super::open;
135212de Andy McCall ↶ 5
use super::ui;
fce44ae9 Andy McCall ↶ 6
use anyhow::Result;
94084087 Andy McCall ↶ 7
use oxy_core::objects::ChangeKind;
fce44ae9 Andy McCall ↶ 8
use oxy_core::repo::{render_change, ChangeStatus, Repo};
9
use std::path::PathBuf;
10
11
pub fn init(path: Option<PathBuf>) -> Result<i32> {
12
    let path = path.unwrap_or(std::env::current_dir()?);
13
    let repo = Repo::init(&path)?;
94084087 Andy McCall ↶ 14
    println!("{} oxy repository in {}", ui::done("Initialized"), ui::path(repo.root().display()));
15
    println!("{}", ui::kv("bookmark", ui::bookmark(&repo.config.bookmark)));
16
    println!("{}", ui::kv("author", format!("{} <{}>", ui::author(&repo.config.author_name), repo.config.author_email)));
fce44ae9 Andy McCall ↶ 17
    Ok(0)
18
}
19
20
fn status_tag(s: ChangeStatus) -> &'static str {
21
    match s {
22
        ChangeStatus::Added => "A",
23
        ChangeStatus::Modified => "M",
24
        ChangeStatus::Removed => "D",
25
        ChangeStatus::ModeChanged => "X",
26
        ChangeStatus::ConflictEdited => "C!",
27
        ChangeStatus::Resolved => "R",
28
    }
29
}
30
31
pub fn status() -> Result<i32> {
32
    let repo = open()?;
135212de Andy McCall ↶ 33
    let sp = ui::spinner("Snapshotting the working copy…");
fce44ae9 Andy McCall ↶ 34
    let snap = repo.snapshot("oxy status")?;
35
    let heads = repo.bookmark_heads()?;
135212de Andy McCall ↶ 36
    sp.clear();
37
    println!(
94084087 Andy McCall ↶ 38
        "{} {}{}working copy {}{}{}",
39
        ui::dim("On"),
40
        ui::bookmark(&repo.config.bookmark),
41
        ui::sep(),
135212de Andy McCall ↶ 42
        ui::id(snap.wc.change_id.short(8)),
94084087 Andy McCall ↶ 43
        ui::sep(),
44
        ui::dim(plural(heads.len(), "head"))
135212de Andy McCall ↶ 45
    );
fce44ae9 Andy McCall ↶ 46
    if snap.statuses.is_empty() {
94084087 Andy McCall ↶ 47
        println!("  {} {}", ui::ok(ui::glyph("✔", "*")), ui::dim("nothing to commit, working copy clean"));
48
    } else {
49
        println!();
50
        for s in &snap.statuses {
51
            println!("  {} {}", ui::status_mark(status_tag(s.status)), ui::path(&s.path));
52
        }
fce44ae9 Andy McCall ↶ 53
    }
54
    if !snap.base_snapshot.is_conflicted() {
55
        return Ok(0);
56
    }
135212de Andy McCall ↶ 57
    println!();
94084087 Andy McCall ↶ 58
    println!("{} {}", ui::section(ui::err("Conflicts")), ui::dim("edit the marked files, or the .oxy-conflict-N candidates, then commit"));
fce44ae9 Andy McCall ↶ 59
    for c in &snap.base_snapshot.tree.conflicts {
94084087 Andy McCall ↶ 60
        println!("  {} tree: {c:?}", ui::err(ui::glyph("✱", "!")));
fce44ae9 Andy McCall ↶ 61
    }
62
    for (p, f) in &snap.base_snapshot.files {
63
        if f.is_conflicted() {
64
            let state = match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) {
94084087 Andy McCall ↶ 65
                Some(ChangeStatus::Resolved) => ui::ok(format!("{} resolved in working copy", ui::glyph("✔", "*"))),
66
                Some(ChangeStatus::ConflictEdited) => ui::warn(format!("{} markers remain", ui::glyph("‼", "!"))),
67
                _ => ui::err(format!("{} unresolved", ui::glyph("✱", "!"))),
fce44ae9 Andy McCall ↶ 68
            };
135212de Andy McCall ↶ 69
            println!("  {}  {state}", ui::path(p));
fce44ae9 Andy McCall ↶ 70
        }
71
    }
72
    Ok(2)
73
}
74
75
pub fn commit(message: &str, depends_on: &[String]) -> Result<i32> {
76
    let repo = open()?;
77
    let mut deps = Vec::new();
78
    for d in depends_on {
79
        deps.push(repo.resolve_change(d)?);
80
    }
135212de Andy McCall ↶ 81
    let sp = ui::spinner("Committing…");
fce44ae9 Andy McCall ↶ 82
    let h = repo.commit(message, &deps)?;
83
    let c = repo.store().get_change(&h)?;
94084087 Andy McCall ↶ 84
    sp.done(&format!("{} {} {}  {}", ui::done("Committed"), ui::id(short_id(&c)), ui::hash(format!("#{}", h.short(12))), ui::subject(subject_of(message))));
fce44ae9 Andy McCall ↶ 85
    Ok(0)
86
}
87
94084087 Andy McCall ↶ 88
/// The log: one entry per change, newest first, joined by a rail. The
89
/// default view is the subject and a metadata line; `verbose` adds the full
90
/// message, dependencies, and the authenticating key.
91
pub fn log(limit: Option<usize>, verbose: bool) -> Result<i32> {
fce44ae9 Andy McCall ↶ 92
    let repo = open()?;
94084087 Andy McCall ↶ 93
    let snap = repo.snapshot("oxy log")?;
94
    let conflicted = snap.base_snapshot.is_conflicted();
95
    let entries = repo.log()?;
96
    let total = entries.len();
97
    let shown = limit.unwrap_or(total).min(total);
98
    let rail = ui::rail();
99
    for (i, e) in entries.iter().take(shown).enumerate() {
fce44ae9 Andy McCall ↶ 100
        let c = &e.change;
94084087 Andy McCall ↶ 101
        let bullet = ui::log_bullet(e.is_working_copy, e.is_head, e.is_working_copy && conflicted);
102
        let mut labels = Vec::new();
fce44ae9 Andy McCall ↶ 103
        if e.is_working_copy {
94084087 Andy McCall ↶ 104
            labels.push(ui::warn("working copy"));
fce44ae9 Andy McCall ↶ 105
        }
94084087 Andy McCall ↶ 106
        if c.kind == ChangeKind::Resolution {
107
            labels.push(ui::warn("resolution"));
fce44ae9 Andy McCall ↶ 108
        }
94084087 Andy McCall ↶ 109
        let labels = if labels.is_empty() { String::new() } else { format!(" {}{}{}", ui::dim("("), labels.join(ui::dim(", ").as_str()), ui::dim(")")) };
110
        let subject = if e.is_working_copy && c.message.is_empty() {
111
            ui::dim(if snap.statuses.is_empty() { "(clean)" } else { "(uncommitted changes)" })
112
        } else {
113
            ui::subject(subject_of(&c.message))
114
        };
115
        println!("{bullet} {}{labels}  {subject}", ui::id(short_meta_id(c)));
116
        let meta = format!(
117
            "{}{}{}{}{}{}{}",
118
            ui::hash(format!("#{}", e.hash.short(12))),
119
            ui::sep(),
120
            ui::author(author_name(&repo, c)),
121
            ui::sep(),
122
            ui::dim(ui::ago(c.hlc.wall_ms)),
123
            ui::sep(),
124
            if e.is_working_copy { ui::dim("not yet committed") } else { signature_mark(&repo, &e.hash)? }
125
        );
126
        println!("{rail} {meta}");
127
        if verbose {
128
            if !e.is_working_copy {
129
                println!("{rail} {}", ui::dim(format!("{} {}", when(c.hlc.wall_ms), signature_line(&repo, &e.hash)?)));
130
            }
131
            if !c.deps.is_empty() {
132
                println!("{rail} {} {}", ui::dim(ui::glyph("↳", "->")), ui::dim(deps_of(&c.deps)));
133
            }
134
            let body: Vec<&str> = c.message.lines().skip(1).skip_while(|l| l.trim().is_empty()).collect();
135
            if !body.is_empty() {
136
                println!("{rail}");
137
                for line in body {
138
                    println!("{rail}   {line}");
139
                }
140
            }
fce44ae9 Andy McCall ↶ 141
        }
94084087 Andy McCall ↶ 142
        if i + 1 < shown {
143
            println!("{rail}");
fce44ae9 Andy McCall ↶ 144
        }
145
    }
94084087 Andy McCall ↶ 146
    if shown < total {
147
        println!("{}", ui::dim(format!("{} {} more; pass -n to see them", ui::glyph("…", "..."), total - shown)));
148
    }
fce44ae9 Andy McCall ↶ 149
    Ok(0)
150
}
151
152
pub fn diff() -> Result<i32> {
153
    let repo = open()?;
154
    let snap = repo.snapshot("oxy diff")?;
155
    print!("{}", render_change(&snap.wc, &snap.base, &snap.base_snapshot));
156
    Ok(0)
157
}
158
159
pub fn show(change: &str) -> Result<i32> {
160
    let repo = open()?;
161
    repo.snapshot("oxy show")?;
162
    let h = repo.resolve_change(change)?;
163
    let c = repo.store().get_change(&h)?;
94084087 Andy McCall ↶ 164
    println!("{} {} {}", ui::section("change"), ui::id(short_id(&c)), ui::hash(format!("#{h}")));
165
    println!("{}", ui::kv("kind", format!("{:?}", c.kind).to_lowercase()));
166
    println!("{}", ui::kv("author", ui::author(author_name(&repo, &oxy_core::state::ChangeMeta::of(&c)))));
167
    println!("{}", ui::kv("date", format!("{} {}", when(c.hlc.wall_ms), ui::dim(format!("({})", ui::ago(c.hlc.wall_ms))))));
168
    println!("{}", ui::kv("deps", ui::dim(deps_line(&c))));
169
    println!("{}", ui::kv("sig", signature_line(&repo, &h)?));
170
    println!();
171
    for line in (if c.message.is_empty() { "(no description)" } else { &c.message }).lines() {
172
        println!("    {line}");
173
    }
174
    println!();
fce44ae9 Andy McCall ↶ 175
    print!("{}", repo.describe_change(&h)?);
176
    Ok(0)
177
}
178
179
pub fn checkout(changes: &[String], discard: bool) -> Result<i32> {
180
    let repo = open()?;
181
    let heads = if changes.is_empty() {
182
        repo.bookmark_heads()?
183
    } else {
132b884b Andy McCall ↶ 184
        let mut heads = Vec::new();
185
        for c in changes {
186
            heads.extend(repo.resolve_heads(c)?);
187
        }
188
        heads
fce44ae9 Andy McCall ↶ 189
    };
190
    let spec = if changes.is_empty() { repo.config.bookmark.clone() } else { changes.join(" ") };
135212de Andy McCall ↶ 191
    let sp = ui::spinner(&format!("Checking out {spec}…"));
fce44ae9 Andy McCall ↶ 192
    let r = repo.checkout(&heads, discard, &format!("oxy checkout {spec}"))?;
94084087 Andy McCall ↶ 193
    sp.done(&format!("{} on {}: {} written, {} removed", ui::done("Working copy"), ui::bookmark(&spec), plural(r.written, "file"), r.removed));
fce44ae9 Andy McCall ↶ 194
    if r.conflicted {
94084087 Andy McCall ↶ 195
        println!("  {} {}", ui::caution(), ui::warn("this state has conflicts; see oxy status"));
fce44ae9 Andy McCall ↶ 196
    }
197
    Ok(0)
198
}
199
200
pub fn resolve() -> Result<i32> {
201
    let repo = open()?;
202
    let snap = repo.snapshot("oxy resolve")?;
203
    if !snap.base_snapshot.is_conflicted() {
204
        println!("The working copy's base has no conflicts.");
205
        return Ok(0);
206
    }
207
    let mut remaining = 0;
208
    for (p, f) in &snap.base_snapshot.files {
209
        if !f.is_conflicted() {
210
            continue;
211
        }
212
        match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) {
213
            Some(ChangeStatus::Resolved) => println!("resolved   {p}"),
214
            Some(ChangeStatus::ConflictEdited) => {
215
                remaining += 1;
216
                println!("markers    {p}");
217
            }
218
            _ => {
219
                remaining += 1;
220
                println!("unresolved {p}");
221
            }
222
        }
223
    }
224
    for c in &snap.base_snapshot.tree.conflicts {
225
        remaining += 1;
226
        println!("tree       {c:?}");
227
    }
228
    if remaining == 0 {
229
        println!("All conflicts are resolved; `oxy commit` will record the resolution.");
230
        Ok(0)
231
    } else {
232
        Ok(2)
233
    }
234
}
235
236
pub fn undo() -> Result<i32> {
237
    let repo = open()?;
238
    let op = repo.undo()?;
94084087 Andy McCall ↶ 239
    println!("{} now at: {}", ui::done("Undone;"), ui::dim(&op.command));
fce44ae9 Andy McCall ↶ 240
    Ok(0)
241
}
242
243
pub fn op_log() -> Result<i32> {
244
    let repo = open()?;
245
    for (i, (r, op)) in repo.op_log()?.into_iter().enumerate() {
94084087 Andy McCall ↶ 246
        let bullet = if i == 0 { ui::ok(ui::glyph("●", "@")) } else { ui::dim(ui::glyph("○", "o")) };
247
        let cmd = subject_of(&op.command).split("\\n").next().unwrap_or("");
248
        println!("{bullet} {}{}{}  {}", ui::hash(r.short(12)), ui::sep(), ui::dim(format!("{:<16}", ui::ago(op.hlc.wall_ms))), if i == 0 { ui::subject(cmd) } else { cmd.to_string() });
fce44ae9 Andy McCall ↶ 249
    }
250
    Ok(0)
251
}
d02e545d Andy McCall ↶ 252
ba160f60 Andy McCall ↶ 253
pub fn gc(now: bool) -> Result<i32> {
d02e545d Andy McCall ↶ 254
    let repo = open()?;
135212de Andy McCall ↶ 255
    let sp = ui::spinner("Packing and pruning…");
ba160f60 Andy McCall ↶ 256
    let r = repo.gc(now)?;
135212de Andy McCall ↶ 257
    sp.clear();
ba160f60 Andy McCall ↶ 258
    if r.packed.objects > 0 {
d02e545d Andy McCall ↶ 259
        println!(
ba160f60 Andy McCall ↶ 260
            "Packed {}: {} bytes -> {} bytes ({:.0}%).",
261
            plural(r.packed.objects, "loose object"),
262
            r.packed.raw_bytes,
263
            r.packed.packed_bytes,
264
            100.0 * r.packed.packed_bytes as f64 / r.packed.raw_bytes.max(1) as f64
d02e545d Andy McCall ↶ 265
        );
266
    }
135212de Andy McCall ↶ 267
    println!("{} reachable; pruned {} ({} bytes){}{}; {} present.",
ba160f60 Andy McCall ↶ 268
        plural(r.reachable, "object"),
269
        plural(r.pruned.objects, "unreachable object"),
270
        r.pruned.bytes,
271
        if r.pruned.deferred > 0 { format!("; {} deferred by the grace period (use --now)", plural(r.pruned.deferred, "object")) } else { String::new() },
135212de Andy McCall ↶ 272
        if r.merged_packs > 0 { format!("; merged {} into one", plural(r.merged_packs, "pack")) } else { String::new() },
ba160f60 Andy McCall ↶ 273
        plural(repo.store().pack_count(), "pack")
274
    );
d02e545d Andy McCall ↶ 275
    Ok(0)
276
}
614541c3 Andy McCall ↶ 277
278
/// The single head the working copy sits on, for commands that default to it.
279
fn current_head(repo: &Repo) -> Result<oxy_core::hash::ChangeHash> {
280
    let snap = repo.snapshot("oxy")?;
281
    let base = snap.base.maximal(&snap.wc.deps);
282
    match base.as_slice() {
283
        [h] => Ok(*h),
284
        _ => anyhow::bail!("the working copy sits on {} heads; name the change explicitly", base.len()),
285
    }
286
}
287
288
pub fn describe(message: &str, change: Option<&str>) -> Result<i32> {
289
    let repo = open()?;
290
    let h = match change {
291
        Some(c) => repo.resolve_change(c)?,
292
        None => current_head(&repo)?,
293
    };
294
    let new = repo.describe(&h, message)?;
295
    let c = repo.store().get_change(&new)?;
94084087 Andy McCall ↶ 296
    println!("{} {} {}  {}", ui::done("Described"), ui::id(short_id(&c)), ui::hash(format!("#{}", new.short(12))), ui::subject(subject_of(message)));
614541c3 Andy McCall ↶ 297
    Ok(0)
298
}
299
23b217d7 Andy McCall ↶ 300
pub fn amend(message: Option<&str>, into: Option<&str>) -> Result<i32> {
614541c3 Andy McCall ↶ 301
    let repo = open()?;
23b217d7 Andy McCall ↶ 302
    let new = match into {
303
        Some(spec) => {
304
            let target = repo.resolve_change(spec)?;
305
            repo.amend_into(&target, message)?
306
        }
307
        None => repo.amend(message)?,
308
    };
614541c3 Andy McCall ↶ 309
    let c = repo.store().get_change(&new)?;
94084087 Andy McCall ↶ 310
    println!("{} {} {}  {}", ui::done("Amended"), ui::id(short_id(&c)), ui::hash(format!("#{}", new.short(12))), ui::subject(subject_of(&c.message)));
614541c3 Andy McCall ↶ 311
    Ok(0)
312
}
313
314
pub fn abandon(change: &str) -> Result<i32> {
315
    let repo = open()?;
316
    let h = repo.resolve_change(change)?;
317
    let c = repo.store().get_change(&h)?;
6706bdcf Andy McCall ↶ 318
    let notes = repo.abandon(&h)?;
94084087 Andy McCall ↶ 319
    println!("{} {} {}  {}", ui::done("Abandoned"), ui::id(short_id(&c)), ui::hash(format!("#{}", h.short(12))), ui::dim("its content remains on disk as uncommitted work if the working copy sat on it"));
6706bdcf Andy McCall ↶ 320
    for n in notes {
94084087 Andy McCall ↶ 321
        println!("  {} {n}", ui::note());
6706bdcf Andy McCall ↶ 322
    }
614541c3 Andy McCall ↶ 323
    Ok(0)
324
}
252e8cb2 Andy McCall ↶ 325
326
pub fn checkpoint() -> Result<i32> {
327
    let repo = open()?;
135212de Andy McCall ↶ 328
    let sp = ui::spinner("Writing checkpoint…");
252e8cb2 Andy McCall ↶ 329
    let (r, changes, bytes) = repo.checkpoint()?;
94084087 Andy McCall ↶ 330
    sp.done(&format!("{} {} of {} ({} bytes) for {}", ui::done("Checkpoint"), ui::hash(format!("#{}", r.short(12))), plural(changes, "change"), bytes, ui::id(&repo.config.bookmark)));
135212de Andy McCall ↶ 331
    Ok(0)
332
}
333
334
/// For shell prompts: silent (exit 1) outside a repository, otherwise one line.
335
pub fn prompt(format: &str) -> Result<i32> {
336
    let Ok(repo) = Repo::open(&std::env::current_dir()?) else { return Ok(1) };
337
    let info = repo.quick_status()?;
338
    println!("{}", info.format(format));
252e8cb2 Andy McCall ↶ 339
    Ok(0)
340
}
7421309f Andy McCall ↶ 341
342
pub fn sparse(paths: &[String]) -> Result<i32> {
343
    let mut repo = open()?;
344
    if paths.is_empty() {
345
        if repo.config.sparse_paths.is_empty() {
346
            println!("No sparse filter: the whole tree is materialized.");
347
        } else {
348
            println!("{}", ui::heading("Sparse filter"));
349
            for p in &repo.config.sparse_paths {
350
                println!("  {}", ui::path(p));
351
            }
352
        }
353
        return Ok(0);
354
    }
355
    let snap = repo.snapshot("oxy sparse")?;
356
    if !snap.wc.tree_ops.is_empty() || !snap.wc.edits.is_empty() {
357
        anyhow::bail!("the working copy has changes; commit them before changing the sparse filter");
358
    }
359
    repo.config.set("filter.paths", &paths.join(","))?;
360
    repo.save_config()?;
361
    let heads = repo.bookmark_heads()?;
362
    let sp = ui::spinner("Re-checking out with the new filter…");
363
    let r = repo.checkout(&heads, true, "oxy sparse (checkout)")?;
364
    sp.done(&format!("{} {}: {} written, {} removed", ui::ok("Sparse filter set to"), ui::path(paths.join(",")), plural(r.written, "file"), r.removed));
365
    Ok(0)
366
}
132b884b Andy McCall ↶ 367
368
pub fn tag_create(name: &str, changes: &[String], message: &str) -> Result<i32> {
369
    let repo = open()?;
370
    repo.snapshot("oxy tag")?;
371
    let heads = if changes.is_empty() {
372
        repo.bookmark_heads()?
373
    } else {
374
        let mut heads = Vec::new();
375
        for c in changes {
376
            heads.extend(repo.resolve_heads(c)?);
377
        }
378
        heads
379
    };
380
    let t = repo.tag_create(name, &heads, message)?;
381
    println!(
94084087 Andy McCall ↶ 382
        "{} {} {} {} ({}){}",
383
        ui::done("Tagged"),
384
        ui::bookmark(&t.name),
385
        ui::dim(ui::glyph("→", "->")),
132b884b Andy McCall ↶ 386
        ui::hash(format!("#{}", t.state.short(12))),
387
        plural(t.heads.len(), "head"),
388
        t.signer.map(|s| format!(", signed by {s}")).unwrap_or_else(|| ui::dim(", unsigned"))
389
    );
390
    Ok(0)
391
}
392
393
pub fn tag_list() -> Result<i32> {
394
    let repo = open()?;
395
    let tags = repo.tags()?;
396
    if tags.is_empty() {
397
        println!("{}", ui::dim("No tags."));
398
    }
399
    for t in tags {
400
        println!(
94084087 Andy McCall ↶ 401
            "{} {} {}{}{}{}",
402
            ui::ok(ui::glyph("⚑", "#")),
403
            ui::bookmark(&t.name),
132b884b Andy McCall ↶ 404
            ui::hash(format!("#{}", t.state.short(12))),
94084087 Andy McCall ↶ 405
            ui::sep() + &ui::dim(plural(t.heads.len(), "head")),
406
            if t.message.is_empty() { String::new() } else { format!("  {}", ui::subject(subject_of(&t.message))) },
407
            t.signer.map(|s| ui::sep() + &ui::ok(format!("{} {s}", ui::glyph("✔", "v")))).unwrap_or_default()
132b884b Andy McCall ↶ 408
        );
409
    }
410
    Ok(0)
411
}
4e88e774 Andy McCall ↶ 412
413
pub fn workspace_add(path: &std::path::Path, changes: &[String]) -> Result<i32> {
414
    let repo = open()?;
415
    let heads = if changes.is_empty() {
416
        None
417
    } else {
418
        let mut heads = Vec::new();
419
        for c in changes {
420
            heads.extend(repo.resolve_heads(c)?);
421
        }
422
        Some(heads)
423
    };
424
    let sp = ui::spinner(&format!("Creating workspace at {}…", path.display()));
425
    let ws = repo.workspace_add(path, heads.as_deref())?;
94084087 Andy McCall ↶ 426
    sp.done(&format!("{} {} (workspace {})", ui::done("Workspace created at"), ui::path(ws.root().display()), ui::id(ws.workspace().short(8))));
4e88e774 Andy McCall ↶ 427
    Ok(0)
428
}
429
430
pub fn workspace_list() -> Result<i32> {
431
    let repo = open()?;
432
    for w in repo.workspaces()? {
433
        println!(
434
            "{} {} {}{}",
94084087 Andy McCall ↶ 435
            if w.current { ui::ok(ui::glyph("●", "*")) } else { ui::dim(ui::glyph("○", "o")) },
4e88e774 Andy McCall ↶ 436
            ui::id(w.id.short(8)),
437
            ui::path(w.path.display()),
94084087 Andy McCall ↶ 438
            w.working_copy.map(|h| ui::sep() + &ui::dim(format!("working copy #{}", h.short(12)))).unwrap_or_else(|| ui::sep() + &ui::warn("not in view"))
4e88e774 Andy McCall ↶ 439
        );
440
    }
441
    Ok(0)
442
}
443
444
pub fn workspace_forget(spec: &str) -> Result<i32> {
445
    let repo = open()?;
446
    let all = repo.workspaces()?;
447
    let target = std::fs::canonicalize(spec).ok();
448
    let matches: Vec<_> = all.iter().filter(|w| w.id.starts_with_hex(spec) || target.as_ref().is_some_and(|t| *t == w.path)).collect();
449
    let w = match matches.as_slice() {
450
        [w] => *w,
451
        [] => anyhow::bail!("no workspace matches {spec:?}"),
452
        _ => anyhow::bail!("{spec:?} is ambiguous"),
453
    };
454
    repo.workspace_forget(&w.id)?;
94084087 Andy McCall ↶ 455
    println!("{} workspace {} at {}; its files were left in place.", ui::done("Forgot"), ui::id(w.id.short(8)), ui::path(w.path.display()));
4e88e774 Andy McCall ↶ 456
    Ok(0)
457
}
cdb5beba Andy McCall ↶ 458
459
pub fn lock(path: &str) -> Result<i32> {
460
    let repo = open()?;
461
    repo.lock_path(path)?;
94084087 Andy McCall ↶ 462
    println!("{} {} {}", ui::done("Locked"), ui::path(path), ui::dim("(advisory, local)"));
cdb5beba Andy McCall ↶ 463
    Ok(0)
464
}
465
466
pub fn unlock(path: &str) -> Result<i32> {
467
    let repo = open()?;
468
    repo.unlock_path(path)?;
94084087 Andy McCall ↶ 469
    println!("{} {}", ui::done("Unlocked"), ui::path(path));
cdb5beba Andy McCall ↶ 470
    Ok(0)
471
}
23b217d7 Andy McCall ↶ 472
473
pub fn split(change: &str, paths: &[String]) -> Result<i32> {
474
    let repo = open()?;
475
    let h = repo.resolve_change(change)?;
476
    let (h1, h2) = repo.split(&h, paths)?;
477
    let c1 = repo.store().get_change(&h1)?;
478
    let c2 = repo.store().get_change(&h2)?;
94084087 Andy McCall ↶ 479
    println!("{} into {} {} and {} {}", ui::done("Split"), ui::id(short_id(&c1)), ui::hash(format!("#{}", h1.short(12))), ui::id(short_id(&c2)), ui::hash(format!("#{}", h2.short(12))));
23b217d7 Andy McCall ↶ 480
    Ok(0)
481
}