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