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