OXYGENHUB
◆ v0.6.0 oxygen / src / cli / sync.rs
39.1 KiB · Rust · last changed A tag can be withdrawn, by a signed withdrawal (oxygenhub#301) 11 days ago source blame history raw
fce44ae9 Andy McCall ↶ 1
//! Commands that talk to remotes, and the reference server.
2
3
use super::format::plural;
75167672 Andy McCall 4
use super::json;
fce44ae9 Andy McCall ↶ 5
use super::open;
135212de Andy McCall ↶ 6
use super::ui;
fce44ae9 Andy McCall ↶ 7
use anyhow::{Context, Result};
8
use oxy_core::repo::Repo;
9
use std::path::PathBuf;
10
75167672 Andy McCall 11
pub fn clone(
12
    url: &str,
13
    dest: Option<PathBuf>,
14
    max_blob: Option<&str>,
15
    sparse: &[String],
16
    depth: Option<u32>,
14edbaaf Andy McCall ↶ 17
    recursive: bool,
75167672 Andy McCall 18
) -> Result<i32> {
fce44ae9 Andy McCall ↶ 19
    let dest = dest.unwrap_or_else(|| {
75167672 Andy McCall 20
        let base = url
21
            .trim_end_matches('/')
22
            .rsplit('/')
23
            .next()
24
            .unwrap_or("repo");
fce44ae9 Andy McCall ↶ 25
        PathBuf::from(base)
26
    });
b3c5a59a Andy McCall ↶ 27
    let max_blob = max_blob.map(oxy_core::repo::parse_size).transpose()?;
135212de Andy McCall ↶ 28
    let sp = ui::spinner(&format!("Cloning {url}…"));
d36ba2f7 Andy McCall ↶ 29
    let (repo, skipped) = Repo::clone_reporting(url, &dest, max_blob, sparse, depth)?;
fce44ae9 Andy McCall ↶ 30
    let heads = repo.bookmark_heads()?;
75167672 Andy McCall 31
    sp.done(&format!(
32
        "{} into {} {}",
33
        ui::done("Cloned"),
34
        ui::path(repo.root().display()),
35
        ui::dim(format!(
36
            "({url}, {} with {})",
37
            ui::bookmark(&repo.config.bookmark),
38
            plural(heads.len(), "head")
39
        ))
40
    ));
d36ba2f7 Andy McCall ↶ 41
    warn_skipped(&skipped);
14edbaaf Andy McCall ↶ 42
    let others = repo.remote_only_bookmarks()?.len();
43
    if others > 0 {
44
        println!(
45
            "  {} {}",
46
            ui::note(),
47
            ui::dim(format!(
48
                "origin has {} more: oxy bookmark list --all, then oxy bookmark switch NAME",
49
                plural(others, "bookmark")
50
            ))
51
        );
52
    }
53
    let subrepos = repo.subrepos()?.len();
54
    if subrepos == 0 {
55
        return Ok(0);
56
    }
57
    if recursive {
58
        let sp = ui::spinner("Cloning subrepos…");
59
        let results = repo.submodule_update(true, &[])?;
60
        sp.clear();
61
        return Ok(print_subrepo_results(results));
62
    }
63
    println!(
64
        "  {} {}",
65
        ui::note(),
66
        ui::dim(format!(
67
            "{} not cloned: oxy submodule update --init (or clone with --recursive)",
68
            plural(subrepos, "subrepo")
69
        ))
70
    );
fce44ae9 Andy McCall ↶ 71
    Ok(0)
72
}
73
14edbaaf Andy McCall ↶ 74
/// After a switch or pull: say when subrepos are not at their pins.
75
pub fn subrepos_behind(repo: &Repo) {
76
    let Ok(subs) = repo.subrepos() else {
77
        return;
78
    };
79
    let stale = subs
80
        .iter()
81
        .filter(|s| s.committed && s.actual.as_ref() != Some(&s.pin))
82
        .count();
83
    if stale > 0 {
84
        println!(
85
            "  {} {}",
86
            ui::note(),
87
            ui::dim(format!(
88
                "{} not at {} pin: oxy submodule update --init",
89
                plural(stale, "subrepo"),
90
                if stale == 1 { "its" } else { "their" }
91
            ))
92
        );
93
    }
94
}
95
9aa39904 Andy McCall ↶ 96
/// Whether a URL names a store (a bucket or a directory in the store
97
/// layout) rather than a repository.
98
fn is_store_url(url: &str) -> bool {
99
    ["s3://", "gs://", "az://", "dir://"]
100
        .iter()
101
        .any(|p| url.starts_with(p))
102
}
103
104
/// Percent-encode what would break a query parameter.
105
fn encode_param(v: &str) -> String {
106
    let mut out = String::with_capacity(v.len());
107
    for b in v.bytes() {
108
        match b {
109
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' | b':' => {
110
                out.push(b as char)
111
            }
112
            _ => out.push_str(&format!("%{b:02X}")),
113
        }
114
    }
115
    out
116
}
117
118
/// A store URL with settings added as query parameters, which is where a
119
/// store remote keeps them (`s3://b/p?profile=work&region=eu-west-2`).
120
fn url_with_settings(url: &str, settings: &[(&str, Option<String>)]) -> Result<String> {
121
    let given: Vec<(&str, &str)> = settings
122
        .iter()
123
        .filter_map(|(k, v)| v.as_deref().map(|v| (*k, v)))
124
        .collect();
125
    if given.is_empty() {
126
        return Ok(url.to_string());
127
    }
128
    if !is_store_url(url) || url.starts_with("dir://") {
129
        let names: Vec<String> = given.iter().map(|(k, _)| format!("--{k}")).collect();
130
        anyhow::bail!(
131
            "{} apply to a store remote (s3://, gs://, az://), not to {url}",
132
            names.join(", ")
133
        );
134
    }
135
    let mut out = url.to_string();
136
    for (k, v) in given {
137
        out.push(if out.contains('?') { '&' } else { '?' });
138
        out.push_str(k);
139
        out.push('=');
140
        out.push_str(&encode_param(v));
141
    }
142
    Ok(out)
143
}
144
145
pub fn remote_add(name: &str, url: &str, settings: &[(&str, Option<String>)]) -> Result<i32> {
fce44ae9 Andy McCall ↶ 146
    let mut repo = open()?;
9aa39904 Andy McCall ↶ 147
    let url = url_with_settings(url, settings)?;
148
    let url = url.as_str();
827281ee Andy McCall ↶ 149
    repo.config.set(
150
        &format!("remote.{name}.url"),
151
        &oxy_core::protocol::stored_url(url),
152
    )?;
fce44ae9 Andy McCall ↶ 153
    repo.save_config()?;
75167672 Andy McCall 154
    println!(
155
        "{} remote {} {} {url}",
156
        ui::done("Added"),
157
        ui::bookmark(name),
158
        ui::dim(ui::glyph("→", "->"))
159
    );
fce44ae9 Andy McCall ↶ 160
    Ok(0)
9aa39904 Andy McCall ↶ 161
}
162
163
/// `oxy remote check NAME`: reach the remote with the credentials in
164
/// hand, say what it holds, and, for a store, put it through the
165
/// conformance check under a probe prefix (left empty afterwards).
166
pub fn remote_check(name: &str) -> Result<i32> {
167
    let repo = open()?;
168
    let Some(url) = repo.config.remotes.get(name).cloned() else {
169
        anyhow::bail!("no remote {name:?}; oxy remote list shows the remotes there are");
170
    };
171
    let options = repo.options_for_url(&url);
172
    let mine = repo.id()?;
173
    let started = std::time::Instant::now();
174
    let sp = ui::spinner(&format!("Checking {name}…"));
175
    let Some(store) = oxy_core::protocol::open_store(&url, &options) else {
176
        // A hub or another repository: reach it and read its references.
177
        let (_, remote) = repo.open_remote_at(&url)?;
178
        let id = remote.repo_id()?;
179
        let refs = remote.refs()?;
180
        sp.done(&format!(
181
            "{} {} {}",
182
            ui::done("Reachable"),
183
            ui::bookmark(name),
184
            ui::dim(format!("({url}, {} ms)", started.elapsed().as_millis()))
185
        ));
186
        describe_holding(id, mine, refs.refs.len(), refs.tags.len(), None);
187
        return Ok(0);
188
    };
189
    let store = store.with_context(|| format!("opening the store of {name}"))?;
190
    // What it holds, in one listing and two reads.
191
    let marker = match store.get("oxygen.toml") {
192
        Ok(b) => Some(b),
193
        Err(oxy_core::strata::Error::NotFound(_)) => None,
194
        Err(e) => {
195
            sp.clear();
196
            return Err(anyhow::anyhow!("{e}"))
197
                .with_context(|| format!("reaching the store of {name}"));
198
        }
199
    };
200
    let reached = started.elapsed();
201
    let bundles = store.list("bundles/")?.len();
202
    let record = store.read_ref("refs")?;
203
    sp.done(&format!(
204
        "{} {} {}",
205
        ui::done("Reachable"),
206
        ui::bookmark(name),
207
        ui::dim(format!("({url}, {} ms)", reached.as_millis()))
208
    ));
209
    match marker {
210
        None => println!(
211
            "  {} {}",
212
            ui::note(),
213
            ui::dim("holds no repository yet: oxy push NAME binds it to this one")
214
        ),
215
        Some(_) => {
216
            let (id, bookmarks, tags) = match record {
217
                Some((bytes, _)) => {
218
                    let (id, resp) = oxy_core::protocol::wire::decode_refs(&bytes)?;
219
                    (id, resp.refs.len(), resp.tags.len())
220
                }
221
                None => (mine, 0, 0),
222
            };
223
            describe_holding(id, mine, bookmarks, tags, Some(bundles));
224
        }
225
    }
226
    // Then the behaviour a repository depends on, under a prefix of its own.
227
    let sp = ui::spinner("Checking that the store behaves…");
228
    let probe = format!("oxy-check-{}", std::process::id());
229
    let began = std::time::Instant::now();
230
    match oxy_core::strata::conformance(store.as_ref(), &probe) {
231
        Ok(passed) => {
232
            sp.clear();
233
            for p in &passed {
234
                println!("  {} {}", ui::ok(ui::glyph("✔", "*")), p);
235
            }
236
            println!(
237
                "{} {}",
238
                ui::done("Behaves"),
239
                ui::dim(format!(
240
                    "({} checks in {} ms; nothing left behind)",
241
                    passed.len(),
242
                    began.elapsed().as_millis()
243
                ))
244
            );
245
            Ok(0)
246
        }
247
        Err(e) => {
248
            sp.clear();
249
            // Leave nothing behind, even after a failed check.
250
            if let Ok(left) = store.list(&probe) {
251
                for entry in left {
252
                    let _ = store.delete(&entry.key);
253
                }
254
            }
255
            println!("{} {}", ui::err(ui::glyph("✘", "x")), e);
256
            println!(
257
                "  {} {}",
258
                ui::note(),
259
                ui::dim("a store that fails this cannot keep a repository safely; check the service and the credentials' permissions")
260
            );
261
            Ok(1)
262
        }
263
    }
264
}
265
266
fn describe_holding(
267
    id: [u8; 16],
268
    mine: [u8; 16],
269
    bookmarks: usize,
270
    tags: usize,
271
    bundles: Option<usize>,
272
) {
273
    let short: String = id[..6].iter().map(|b| format!("{b:02x}")).collect();
274
    let mut what = format!(
275
        "repository {short}: {} and {}",
276
        plural(bookmarks, "bookmark"),
277
        plural(tags, "tag")
278
    );
279
    if let Some(b) = bundles {
280
        what.push_str(&format!(" in {}", plural(b, "bundle")));
281
    }
282
    if id == mine {
283
        println!("  {} {}", ui::note(), ui::dim(format!("holds this {what}")));
284
    } else if id == oxy_core::protocol::UNBOUND_ID {
285
        println!("  {} {}", ui::note(), ui::dim("holds no repository yet"));
286
    } else {
287
        println!(
288
            "  {} {}",
289
            ui::caution(),
290
            ui::warn(format!("holds another {what}; a push would be refused"))
291
        );
292
    }
fce44ae9 Andy McCall ↶ 293
}
294
295
pub fn remote_remove(name: &str) -> Result<i32> {
296
    let mut repo = open()?;
34fad4a9 Andy McCall ↶ 297
    repo.remove_remote(name)?;
94084087 Andy McCall ↶ 298
    println!("{} remote {}", ui::done("Removed"), ui::bookmark(name));
fce44ae9 Andy McCall ↶ 299
    Ok(0)
300
}
301
c6aa5bd9 Andy McCall ↶ 302
pub fn remote_list(as_json: bool) -> Result<i32> {
fce44ae9 Andy McCall ↶ 303
    let repo = open()?;
304
    let view = repo.view()?;
c6aa5bd9 Andy McCall ↶ 305
    if as_json {
306
        let items = repo.config.remotes.iter().map(|(name, url)| {
307
            let bms = view.remotes.get(name).map(|b| {
308
                json::arr(b.iter().map(|(bm, st)| {
75167672 Andy McCall 309
                    let heads = repo
310
                        .store()
311
                        .get_state(st)
312
                        .map(|s| s.heads)
313
                        .unwrap_or_default();
c6aa5bd9 Andy McCall ↶ 314
                    json::obj([("name", json::s(bm)), ("heads", json::hashes(&heads))])
315
                }))
316
            });
75167672 Andy McCall 317
            json::obj([
318
                ("name", json::s(name)),
319
                ("url", json::s(url)),
dfb23a3a Andy McCall ↶ 320
                // The CA file `remote.NAME.ca` names, for tools that talk to
321
                // the same hub (an editor's forge view, `oh`).
322
                (
323
                    "ca",
324
                    json::opt(repo.config.remote_cas.get(name), |c| json::s(c)),
325
                ),
75167672 Andy McCall 326
                ("bookmarks", bms.unwrap_or_else(|| "[]".into())),
327
            ])
c6aa5bd9 Andy McCall ↶ 328
        });
329
        println!("{}", json::arr(items));
330
        return Ok(0);
331
    }
94084087 Andy McCall ↶ 332
    if repo.config.remotes.is_empty() {
75167672 Andy McCall 333
        println!(
334
            "{}",
335
            ui::dim("No remotes; add one with oxy remote add <name> <url>.")
336
        );
94084087 Andy McCall ↶ 337
    }
fce44ae9 Andy McCall ↶ 338
    for (name, url) in &repo.config.remotes {
75167672 Andy McCall 339
        println!(
340
            "{} {}{}{}",
341
            ui::ok(ui::glyph("⇄", "<>")),
342
            ui::bookmark(name),
343
            ui::sep(),
344
            ui::dim(url)
345
        );
fce44ae9 Andy McCall ↶ 346
        if let Some(bms) = view.remotes.get(name) {
347
            for (bm, st) in bms {
75167672 Andy McCall 348
                let heads = repo
349
                    .store()
350
                    .get_state(st)
351
                    .map(|s| s.heads.len())
352
                    .unwrap_or(0);
353
                println!(
354
                    "    {} {}{}{}",
355
                    ui::dim(ui::glyph("↳", "->")),
356
                    ui::id(format!("{name}/{bm}")),
357
                    ui::sep(),
358
                    ui::dim(plural(heads, "head"))
359
                );
fce44ae9 Andy McCall ↶ 360
            }
361
        }
362
    }
363
    Ok(0)
364
}
365
b35df36b Andy McCall ↶ 366
/// The remote named, else `origin`, else the only remote there is.
367
fn remote_name(repo: &oxy_core::repo::Repo, remote: Option<&str>) -> String {
368
    remote.map_or_else(|| repo.default_remote(), str::to_string)
fce44ae9 Andy McCall ↶ 369
}
370
c6aa5bd9 Andy McCall ↶ 371
pub fn fetch(remote: Option<&str>, as_json: bool) -> Result<i32> {
fce44ae9 Andy McCall ↶ 372
    let repo = open()?;
b35df36b Andy McCall ↶ 373
    let remote = remote_name(&repo, remote);
135212de Andy McCall ↶ 374
    let sp = ui::spinner(&format!("Fetching from {remote}…"));
83e0677b Andy McCall ↶ 375
    let report = repo.fetch(&remote)?;
135212de Andy McCall ↶ 376
    sp.clear();
c6aa5bd9 Andy McCall ↶ 377
    if as_json {
378
        println!(
379
            "{}",
380
            json::obj([
381
                ("remote", json::s(&remote)),
75167672 Andy McCall 382
                (
383
                    "refs",
b35df36b Andy McCall ↶ 384
                    json::arr(report.refs.iter().map(|(name, heads, n)| {
385
                        let t = repo.tracking(&remote, name).ok().flatten();
386
                        json::obj([
387
                            ("name", json::s(name)),
388
                            ("heads", json::hashes(heads)),
389
                            ("new", json::n(*n)),
390
                            (
391
                                "ahead",
392
                                t.map_or_else(|| "null".into(), |(a, _)| json::n(a)),
393
                            ),
394
                            (
395
                                "behind",
396
                                t.map_or_else(|| "null".into(), |(_, b)| json::n(b)),
397
                            ),
398
                        ])
399
                    }))
75167672 Andy McCall 400
                ),
c6aa5bd9 Andy McCall ↶ 401
                ("new_tags", json::n(report.new_tags)),
b63fc7a7 Andy McCall ↶ 402
                (
403
                    "withdrawn_tags",
404
                    json::arr(report.withdrawn_tags.iter().map(|n| json::s(n))),
405
                ),
c6aa5bd9 Andy McCall ↶ 406
                ("unverified", json::b(report.unverified)),
4b9d0677 Andy McCall ↶ 407
                (
408
                    "log_notes",
409
                    json::arr(report.log_notes.iter().map(|n| json::s(n))),
410
                ),
d36ba2f7 Andy McCall ↶ 411
                ("skipped", skipped_json(&report.skipped)),
c6aa5bd9 Andy McCall ↶ 412
            ])
413
        );
414
        return Ok(0);
415
    }
94084087 Andy McCall ↶ 416
    if report.refs.is_empty() {
75167672 Andy McCall 417
        println!(
418
            "{} {}",
419
            ui::done("Fetched"),
420
            ui::dim(format!("nothing new from {remote}"))
421
        );
94084087 Andy McCall ↶ 422
    }
83e0677b Andy McCall ↶ 423
    for (name, heads, n) in &report.refs {
b35df36b Andy McCall ↶ 424
        // What was fetched, then how the local bookmark stands against it:
425
        // "up to date" only when the two are the same (oxygen#56).
426
        let fetched =
427
            (*n > 0).then(|| format!("{}{}", ui::ok(plural(*n, "new change")), ui::sep()));
428
        let them = |k: usize| if k == 1 { "it" } else { "them" };
429
        let standing = match repo.tracking(&remote, name).ok().flatten() {
430
            None => ui::dim(format!("no local bookmark {name}")),
431
            Some((0, 0)) => ui::dim(format!("{name} up to date")),
432
            Some((0, b)) if *name == repo.config.bookmark => ui::warn(format!(
433
                "{name} is {} behind: oxy pull to take {}",
434
                plural(b, "change"),
435
                them(b)
436
            )),
437
            Some((0, b)) => ui::warn(format!(
438
                "{name} is {} behind: oxy bookmark switch {name} then oxy pull to take {}",
439
                plural(b, "change"),
440
                them(b)
441
            )),
442
            Some((a, 0)) => ui::dim(format!(
443
                "{name} is {} ahead: oxy push to send {}",
444
                plural(a, "change"),
445
                them(a)
446
            )),
447
            Some((a, b)) => ui::warn(format!(
448
                "{name} and {remote}/{name} have diverged ({a} here, {b} there): oxy pull to merge them"
449
            )),
75167672 Andy McCall 450
        };
b35df36b Andy McCall ↶ 451
        let what = format!("{}{standing}", fetched.unwrap_or_default());
75167672 Andy McCall 452
        println!(
453
            "{} {}{}{}{}{what}",
454
            ui::done("Fetched"),
455
            ui::id(format!("{remote}/{name}")),
456
            ui::sep(),
457
            ui::dim(plural(heads.len(), "head")),
458
            ui::sep()
459
        );
83e0677b Andy McCall ↶ 460
    }
132b884b Andy McCall ↶ 461
    if report.new_tags > 0 {
75167672 Andy McCall 462
        println!(
463
            "  {} {}",
464
            ui::note(),
465
            ui::dim(format!("{} adopted", plural(report.new_tags, "tag")))
466
        );
132b884b Andy McCall ↶ 467
    }
b63fc7a7 Andy McCall ↶ 468
    for t in &report.withdrawn_tags {
469
        println!(
470
            "  {} {}",
471
            ui::note(),
472
            ui::dim(format!("tag {t} was withdrawn on {remote}, and here"))
473
        );
474
    }
83e0677b Andy McCall ↶ 475
    if report.unverified {
75167672 Andy McCall 476
        eprintln!(
477
            "{} {remote} has no trust policy; nothing fetched from it was verified",
478
            ui::caution()
479
        );
4b9d0677 Andy McCall ↶ 480
    }
481
    for n in &report.log_notes {
482
        eprintln!("{} {n}", ui::caution());
fce44ae9 Andy McCall ↶ 483
    }
d36ba2f7 Andy McCall ↶ 484
    warn_skipped(&report.skipped);
fce44ae9 Andy McCall ↶ 485
    Ok(0)
d36ba2f7 Andy McCall ↶ 486
}
487
488
/// Bookmarks a fetch left as they were, their attestation unverifiable
489
/// (oxygenhub#266): one warning each, naming it and why.
490
fn warn_skipped(skipped: &[(String, String)]) {
491
    for (name, why) in skipped {
492
        eprintln!(
493
            "{} {} {}",
494
            ui::caution(),
495
            ui::warn(format!("{name} not updated:")),
496
            ui::dim(why)
497
        );
498
    }
499
}
500
501
fn skipped_json(skipped: &[(String, String)]) -> String {
502
    json::arr(
503
        skipped
504
            .iter()
505
            .map(|(name, why)| json::obj([("name", json::s(name)), ("reason", json::s(why))])),
506
    )
fce44ae9 Andy McCall ↶ 507
}
508
7d9dab13 Andy McCall ↶ 509
/// Pull acts on the current bookmark only: name the others the fetch
510
/// found behind their remote, which is easy to take for the current one
511
/// (oxygen#29).
512
fn others_behind(repo: &oxy_core::repo::Repo, remote: &str) {
513
    let Ok(bookmarks) = repo.bookmarks() else {
514
        return;
515
    };
516
    for b in bookmarks {
517
        let Some(n) = b.behind.filter(|n| *n > 0) else {
518
            continue;
519
        };
520
        if b.current {
521
            continue;
522
        }
523
        println!(
524
            "  {} {}",
525
            ui::note(),
526
            ui::dim(format!(
527
                "{} is {} behind {remote}/{}: oxy bookmark switch {} then oxy pull to take {}",
528
                b.name,
529
                plural(n, "change"),
530
                b.name,
531
                b.name,
532
                if n == 1 { "it" } else { "them" }
533
            ))
534
        );
535
    }
536
}
537
c6aa5bd9 Andy McCall ↶ 538
pub fn pull(remote: Option<&str>, as_json: bool) -> Result<i32> {
fce44ae9 Andy McCall ↶ 539
    let repo = open()?;
b35df36b Andy McCall ↶ 540
    let remote = remote_name(&repo, remote);
135212de Andy McCall ↶ 541
    let sp = ui::spinner(&format!("Pulling from {remote}…"));
fce44ae9 Andy McCall ↶ 542
    let r = repo.pull(&remote)?;
c6aa5bd9 Andy McCall ↶ 543
    if as_json {
544
        sp.clear();
cc7a0056 Andy McCall ↶ 545
        let conflicted = repo
546
            .snapshot_status("oxy pull")?
547
            .base_snapshot
548
            .is_conflicted();
75167672 Andy McCall 549
        println!(
550
            "{}",
551
            json::obj([
552
                ("remote", json::s(&remote)),
7d9dab13 Andy McCall ↶ 553
                ("bookmark", json::s(&repo.config.bookmark)),
75167672 Andy McCall 554
                ("heads", json::hashes(&r.heads)),
555
                ("changed", json::b(r.changed)),
556
                ("checked_out", json::b(r.checked_out)),
14edbaaf Andy McCall ↶ 557
                ("conflicted", json::b(conflicted)),
d36ba2f7 Andy McCall ↶ 558
                ("others", json::arr(r.others.iter().map(|o| json::s(o)))),
559
                ("skipped", skipped_json(&r.skipped))
75167672 Andy McCall 560
            ])
561
        );
c6aa5bd9 Andy McCall ↶ 562
        return Ok(0);
563
    }
fce44ae9 Andy McCall ↶ 564
    if !r.changed {
75167672 Andy McCall 565
        sp.done(&format!(
f6dccc45 Andy McCall ↶ 566
            "{} {}{}",
75167672 Andy McCall 567
            ui::ok(ui::glyph("✔", "*")),
7d9dab13 Andy McCall ↶ 568
            ui::dim(format!(
569
                "{} is up to date with {remote}.",
570
                repo.config.bookmark
f6dccc45 Andy McCall ↶ 571
            )),
572
            if r.checked_out {
573
                ui::dim(" The working copy had fallen behind it and is updated.")
574
            } else {
575
                String::new()
576
            }
75167672 Andy McCall 577
        ));
14edbaaf Andy McCall ↶ 578
        others_pulled(&r.others);
7d9dab13 Andy McCall ↶ 579
        others_behind(&repo, &remote);
d36ba2f7 Andy McCall ↶ 580
        warn_skipped(&r.skipped);
f6dccc45 Andy McCall ↶ 581
        if r.checked_out {
582
            super::workspace::where_now(&repo);
583
        }
fce44ae9 Andy McCall ↶ 584
        return Ok(0);
585
    }
135212de Andy McCall ↶ 586
    sp.done(&format!(
587
        "{} {} now has {}{}",
94084087 Andy McCall ↶ 588
        ui::done("Pulled:"),
589
        ui::bookmark(&repo.config.bookmark),
fce44ae9 Andy McCall ↶ 590
        plural(r.heads.len(), "head"),
75167672 Andy McCall 591
        ui::dim(if r.checked_out {
592
            "; working copy updated"
593
        } else {
594
            "; run oxy checkout to update the working copy"
595
        })
135212de Andy McCall ↶ 596
    ));
fce44ae9 Andy McCall ↶ 597
    let snap = repo.snapshot("oxy pull")?;
598
    if snap.base_snapshot.is_conflicted() {
75167672 Andy McCall 599
        println!(
600
            "  {} {}",
601
            ui::caution(),
602
            ui::warn("the result has conflicts; see oxy status")
603
        );
fce44ae9 Andy McCall ↶ 604
    }
14edbaaf Andy McCall ↶ 605
    others_pulled(&r.others);
7d9dab13 Andy McCall ↶ 606
    others_behind(&repo, &remote);
d36ba2f7 Andy McCall ↶ 607
    warn_skipped(&r.skipped);
14edbaaf Andy McCall ↶ 608
    subrepos_behind(&repo);
98a1620a Andy McCall ↶ 609
    super::workspace::where_now(&repo);
fce44ae9 Andy McCall ↶ 610
    Ok(0)
611
}
612
14edbaaf Andy McCall ↶ 613
/// The other bookmarks a pull moved.
614
fn others_pulled(others: &[String]) {
615
    if others.is_empty() {
616
        return;
617
    }
618
    let shown: Vec<String> = others.iter().take(8).map(ui::bookmark).collect();
619
    let more = others.len().saturating_sub(8);
620
    println!(
621
        "  {} {}{}",
622
        ui::done("Also updated"),
623
        shown.join(", "),
624
        if more > 0 {
625
            ui::dim(format!(" and {more} more"))
626
        } else {
627
            String::new()
628
        }
629
    );
630
}
631
f8c69576 Andy McCall ↶ 632
/// `oxy push [REMOTE] --delete NAME`.
633
pub fn push_delete(remote: Option<&str>, name: &str, as_json: bool) -> Result<i32> {
634
    let repo = open()?;
b35df36b Andy McCall ↶ 635
    let remote = remote_name(&repo, remote);
f8c69576 Andy McCall ↶ 636
    let sp = ui::spinner(&format!("Deleting {remote}/{name}…"));
637
    let results = repo.push_delete(&remote, name)?;
638
    sp.clear();
639
    if as_json {
640
        println!(
641
            "{}",
642
            json::arr(results.iter().map(|r| {
643
                json::obj([
644
                    ("name", json::s(&r.name)),
645
                    ("ok", json::b(r.ok)),
646
                    ("reason", json::s(&r.reason)),
647
                    ("state", json::s("")),
648
                ])
649
            }))
650
        );
651
        return Ok(if results.iter().all(|r| r.ok) { 0 } else { 1 });
652
    }
653
    let mut code = 0;
654
    for r in results {
655
        if r.ok {
b63fc7a7 Andy McCall ↶ 656
            let done = if r.name.starts_with("tags/") {
657
                "Withdrew"
658
            } else {
659
                "Deleted"
660
            };
f8c69576 Andy McCall ↶ 661
            println!(
662
                "{} {}",
b63fc7a7 Andy McCall ↶ 663
                ui::done(done),
f8c69576 Andy McCall ↶ 664
                ui::id(format!("{remote}/{}", r.name))
665
            );
666
        } else {
667
            println!(
668
                "{} {} {}{}{}",
669
                ui::err(ui::glyph("✖", "x")),
670
                ui::err("Rejected"),
671
                ui::id(format!("{remote}/{}", r.name)),
672
                ui::sep(),
673
                r.reason
674
            );
675
            code = 1;
676
        }
677
    }
678
    Ok(code)
679
}
680
75167672 Andy McCall 681
pub fn push(
682
    remote: Option<&str>,
683
    force: bool,
684
    bookmark: Option<&str>,
685
    as_json: bool,
686
) -> Result<i32> {
fce44ae9 Andy McCall ↶ 687
    let repo = open()?;
b35df36b Andy McCall ↶ 688
    let remote = remote_name(&repo, remote);
135212de Andy McCall ↶ 689
    let sp = ui::spinner(&format!("Pushing to {remote}…"));
cb8b8082 Andy McCall ↶ 690
    let bookmark_name = bookmark.map_or_else(|| repo.config.bookmark.clone(), String::from);
691
    // A long push goes in parts: the spinner says which.
692
    let results = repo.push_bookmark_with(&remote, &bookmark_name, force, &mut |m| {
693
        sp.set(&format!("Pushing to {remote}… {m}"))
694
    })?;
135212de Andy McCall ↶ 695
    sp.clear();
c6aa5bd9 Andy McCall ↶ 696
    if as_json {
60a1223c Andy McCall ↶ 697
        // The state each accepted bookmark now holds on the remote (its local
698
        // state object), so a caller can look up what the push started, such
699
        // as the checks a forge runs on it.
700
        let view = repo.view()?;
75167672 Andy McCall 701
        println!(
702
            "{}",
60a1223c Andy McCall ↶ 703
            json::arr(results.iter().map(|r| {
704
                let state = view
705
                    .bookmarks
706
                    .get(&r.name)
707
                    .filter(|_| r.ok)
708
                    .map(|st| st.to_hex())
709
                    .unwrap_or_default();
710
                json::obj([
711
                    ("name", json::s(&r.name)),
712
                    ("ok", json::b(r.ok)),
713
                    ("reason", json::s(&r.reason)),
714
                    ("state", json::s(&state)),
715
                ])
716
            }))
75167672 Andy McCall 717
        );
c6aa5bd9 Andy McCall ↶ 718
        return Ok(if results.iter().all(|r| r.ok) { 0 } else { 1 });
719
    }
fce44ae9 Andy McCall ↶ 720
    let mut code = 0;
135212de Andy McCall ↶ 721
    for r in results {
fce44ae9 Andy McCall ↶ 722
        if r.ok {
75167672 Andy McCall 723
            println!(
724
                "{} {}{}{}",
725
                ui::done("Pushed"),
726
                ui::id(format!("{remote}/{}", r.name)),
727
                ui::sep(),
728
                ui::dim(if r.reason.is_empty() {
729
                    "updated"
730
                } else {
731
                    r.reason.as_str()
732
                })
733
            );
fce44ae9 Andy McCall ↶ 734
        } else {
75167672 Andy McCall 735
            println!(
736
                "{} {} {}{}{}",
737
                ui::err(ui::glyph("✖", "x")),
738
                ui::err("Rejected"),
739
                ui::id(format!("{remote}/{}", r.name)),
740
                ui::sep(),
741
                r.reason
742
            );
fce44ae9 Andy McCall ↶ 743
            code = 1;
744
        }
745
    }
746
    Ok(code)
747
}
748
75167672 Andy McCall 749
pub fn serve(
750
    root: PathBuf,
751
    bind: &str,
752
    tokens: Vec<String>,
753
    tls: Option<(PathBuf, PathBuf)>,
754
) -> Result<i32> {
fce44ae9 Andy McCall ↶ 755
    let listener = std::net::TcpListener::bind(bind).with_context(|| format!("binding {bind}"))?;
756
    let root = root.canonicalize()?;
2b5ee371 Andy McCall ↶ 757
    let tls = tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key });
fce44ae9 Andy McCall ↶ 758
    println!(
2b5ee371 Andy McCall ↶ 759
        "Serving repositories under {} on {}://{bind}/<repo>/oxy/v1/ ({})",
fce44ae9 Andy McCall ↶ 760
        root.display(),
2b5ee371 Andy McCall ↶ 761
        if tls.is_some() { "https" } else { "http" },
75167672 Andy McCall 762
        if tokens.is_empty() {
763
            "open: no push tokens configured"
764
        } else {
765
            "push requires a token"
766
        }
fce44ae9 Andy McCall ↶ 767
    );
75167672 Andy McCall 768
    if tls.is_none()
769
        && !bind.starts_with("127.")
770
        && !bind.starts_with("localhost")
771
        && !bind.starts_with("[::1]")
772
    {
2b5ee371 Andy McCall ↶ 773
        eprintln!("warning: serving plain HTTP on a non-loopback address; put a TLS terminator in front or pass --tls-cert/--tls-key");
774
    }
775
    oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens, tls })?;
fce44ae9 Andy McCall ↶ 776
    Ok(0)
777
}
79ab8528 Andy McCall ↶ 778
30ea9c1e Andy McCall ↶ 779
pub fn git_export(git_dir: &std::path::Path, branch: Option<&str>) -> Result<i32> {
79ab8528 Andy McCall ↶ 780
    let repo = open()?;
135212de Andy McCall ↶ 781
    let sp = ui::spinner("Exporting to Git…");
30ea9c1e Andy McCall ↶ 782
    let r = match branch {
783
        Some(b) => repo.git_export_as(git_dir, b)?,
784
        None => repo.git_export(git_dir)?,
785
    };
135212de Andy McCall ↶ 786
    sp.clear();
79ab8528 Andy McCall ↶ 787
    println!(
94084087 Andy McCall ↶ 788
        "{} {} to {} as branch {} {}",
789
        ui::done("Exported"),
e8b4e156 Andy McCall ↶ 790
        plural(r.exported, "new state"),
94084087 Andy McCall ↶ 791
        ui::path(git_dir.display()),
792
        ui::bookmark(&r.branch),
75167672 Andy McCall 793
        ui::dim(format!(
794
            "(currently {}; {} already exported)",
795
            plural(r.heads, "head"),
796
            r.skipped
797
        ))
79ab8528 Andy McCall ↶ 798
    );
799
    Ok(0)
800
}
c3069537 Andy McCall ↶ 801
54e2e630 Andy McCall ↶ 802
pub fn git_recover() -> Result<i32> {
803
    let repo = open()?;
804
    let sp = ui::spinner("Reading the store…");
805
    let (changes, heads) = repo.recover_git_import(&|m| sp.set(m))?;
806
    sp.done(&format!(
807
        "{} {} from imported Git commits: {} now has {}",
808
        ui::done("Recovered"),
809
        plural(changes, "change"),
810
        ui::bookmark(&repo.config.bookmark),
811
        plural(heads, "head")
812
    ));
813
    println!(
814
        "  {} {}",
815
        ui::note(),
816
        ui::dim("carry on with oxy git fetch GIT_DIR --branch BRANCH")
817
    );
818
    Ok(0)
819
}
820
d6e0d5b8 Andy McCall ↶ 821
/// `--claim` (oxygen#97): record `git.claim`, so this import and every
822
/// later fetch author the identity's own commits with its key.
823
fn claim_commits(repo: &mut Repo) -> Result<()> {
824
    repo.config.set("git.claim", "true")?;
825
    repo.save_config()?;
826
    Ok(())
827
}
828
829
pub fn git_fetch(url: &str, branch: &str, claim: bool) -> Result<i32> {
830
    let mut repo = open()?;
831
    if claim {
832
        claim_commits(&mut repo)?;
833
    }
30ea9c1e Andy McCall ↶ 834
    let sp = ui::spinner(&format!("Fetching {url} {branch}…"));
835
    let r = repo.git_fetch(url, branch)?;
836
    sp.clear();
837
    if r.commits == 0 {
75167672 Andy McCall 838
        println!(
839
            "{} {}",
840
            ui::done("Fetched"),
841
            ui::dim(format!("nothing new from {url} {branch}"))
842
        );
30ea9c1e Andy McCall ↶ 843
    } else {
844
        println!(
845
            "{} {} ({} merges) from {} {}{}{} now has {}",
846
            ui::done("Fetched"),
847
            plural(r.commits, "new commit"),
848
            r.merges,
849
            ui::path(url),
850
            ui::bookmark(branch),
851
            ui::sep(),
852
            ui::bookmark(&repo.config.bookmark),
853
            plural(r.heads, "head")
854
        );
855
        if !repo.snapshot("oxy git fetch")?.changed {
856
            println!("  {} {}", ui::note(), ui::dim("working copy updated"));
857
        } else {
75167672 Andy McCall 858
            println!(
859
                "  {} {}",
860
                ui::note(),
861
                ui::dim("run oxy checkout to update the working copy")
862
            );
30ea9c1e Andy McCall ↶ 863
        }
864
    }
4dee277c Andy McCall ↶ 865
    if r.submodules > 0 {
75167672 Andy McCall 866
        println!(
867
            "  {} {}",
868
            ui::note(),
869
            ui::dim(format!(
870
                "{} submodule {} imported as subrepos with Git pins",
871
                r.submodules,
872
                if r.submodules == 1 {
873
                    "entry"
874
                } else {
875
                    "entries"
876
                }
877
            ))
878
        );
30ea9c1e Andy McCall ↶ 879
    }
880
    Ok(0)
881
}
882
883
pub fn git_push(url: &str, branch: &str) -> Result<i32> {
884
    let repo = open()?;
885
    let sp = ui::spinner(&format!("Pushing to {url} {branch}…"));
886
    let r = repo.git_push(url, branch)?;
887
    sp.clear();
888
    println!(
889
        "{} {} to {} {} {}",
890
        ui::done("Pushed"),
891
        plural(r.exported, "new commit"),
892
        ui::path(url),
893
        ui::bookmark(&r.branch),
894
        ui::dim(format!("({} already there)", r.skipped))
895
    );
896
    Ok(0)
897
}
898
62794ecc Andy McCall ↶ 899
/// `oxy git verify`: exit 1 if any commit's tree is not reproduced.
8b7986dd Andy McCall ↶ 900
pub fn git_verify(
901
    git_dir: &std::path::Path,
902
    branch: &str,
903
    last: Option<usize>,
904
    max: usize,
905
) -> Result<i32> {
62794ecc Andy McCall ↶ 906
    let repo = open()?;
907
    let started = std::time::Instant::now();
908
    let sp = ui::spinner(&format!(
909
        "Rebuilding every commit of {} {branch}…",
910
        git_dir.display()
911
    ));
8b7986dd Andy McCall ↶ 912
    let r = repo.verify_git_import_last(git_dir, branch, last, max)?;
62794ecc Andy McCall ↶ 913
    sp.clear();
914
    for (_, what) in &r.mismatches {
915
        println!("{} {what}", ui::err(ui::glyph("✖", "x")));
916
    }
f0406c3b Andy McCall ↶ 917
    if r.not_yet > 0 {
918
        println!(
919
            "  {} {}",
920
            ui::note(),
921
            ui::dim(format!(
922
                "{} not imported yet, after the last imported commit: not checked",
923
                plural(r.not_yet, "commit")
924
            ))
925
        );
926
    }
62794ecc Andy McCall ↶ 927
    if r.mismatches.is_empty() {
928
        println!(
929
            "{}",
7eac74ab Andy McCall ↶ 930
            ui::done(if r.skipped > 0 {
931
                format!(
932
                    "The last {} commits reproduced ({} followed), {} files compared, in {:.1}s",
933
                    r.commits - r.skipped,
934
                    r.skipped,
935
                    r.files_compared,
936
                    started.elapsed().as_secs_f64()
937
                )
938
            } else {
939
                format!(
940
                    "Every commit reproduced: {} commits, {} files compared, in {:.1}s",
941
                    r.commits,
942
                    r.files_compared,
943
                    started.elapsed().as_secs_f64()
944
                )
945
            })
62794ecc Andy McCall ↶ 946
        );
947
        Ok(0)
948
    } else {
8b7986dd Andy McCall ↶ 949
        // What differs, by kind, the commit taken off: one difference
950
        // carried through history reads as one line.
951
        let mut kinds: std::collections::BTreeMap<String, usize> = Default::default();
952
        for (_, what) in &r.mismatches {
953
            let kind = what.split_once(": ").map_or(what.as_str(), |(_, k)| k);
954
            *kinds.entry(kind.to_string()).or_default() += 1;
955
        }
956
        let mut kinds: Vec<_> = kinds.into_iter().collect();
957
        kinds.sort_by_key(|k| std::cmp::Reverse(k.1));
958
        for (kind, n) in kinds.iter().take(10) {
959
            println!("  {} {}", ui::dim(format!("{n:>6} ×")), kind);
960
        }
62794ecc Andy McCall ↶ 961
        println!(
8b7986dd Andy McCall ↶ 962
            "{} differences in {} commits checked{}",
62794ecc Andy McCall ↶ 963
            r.mismatches.len(),
8b7986dd Andy McCall ↶ 964
            r.commits,
965
            if max > 0 && r.mismatches.len() >= max {
966
                format!(" (stopped at {max}; --max-differences 0 checks every commit)")
967
            } else {
968
                String::new()
969
            }
62794ecc Andy McCall ↶ 970
        );
971
        Ok(1)
972
    }
973
}
974
d6e0d5b8 Andy McCall ↶ 975
pub fn git_import(
976
    git_dir: &std::path::Path,
977
    dest: &std::path::Path,
978
    branch: &str,
979
    claim: bool,
980
) -> Result<i32> {
981
    let mut repo = Repo::init(dest)?;
982
    if claim {
983
        claim_commits(&mut repo)?;
984
    }
c3069537 Andy McCall ↶ 985
    let started = std::time::Instant::now();
135212de Andy McCall ↶ 986
    let sp = ui::spinner(&format!("Importing {} {branch}…", git_dir.display()));
58dfe0e1 Andy McCall ↶ 987
    let r = repo.import_git_with(git_dir, branch, &mut |where_| sp.set(where_))?;
135212de Andy McCall ↶ 988
    sp.clear();
c3069537 Andy McCall ↶ 989
    println!(
94084087 Andy McCall ↶ 990
        "{} {} ({} merges) from {} {} into {} {}",
991
        ui::done("Imported"),
c3069537 Andy McCall ↶ 992
        plural(r.commits, "commit"),
993
        r.merges,
94084087 Andy McCall ↶ 994
        ui::path(git_dir.display()),
995
        ui::bookmark(branch),
996
        ui::path(repo.root().display()),
75167672 Andy McCall 997
        ui::dim(format!(
998
            "in {:.1?}; {} now has {}",
999
            started.elapsed(),
1000
            repo.config.bookmark,
1001
            plural(r.heads, "head")
1002
        ))
c3069537 Andy McCall ↶ 1003
    );
4dee277c Andy McCall ↶ 1004
    if r.submodules > 0 {
75167672 Andy McCall 1005
        println!(
1006
            "  {} {}",
1007
            ui::note(),
1008
            ui::dim(format!(
1009
                "{} submodule {} imported as subrepos with Git pins",
1010
                r.submodules,
1011
                if r.submodules == 1 {
1012
                    "entry"
1013
                } else {
1014
                    "entries"
1015
                }
1016
            ))
1017
        );
4dee277c Andy McCall ↶ 1018
    }
1019
    Ok(0)
1020
}
1021
1022
pub fn submodule_add(url: &str, path: &str, bookmark: Option<&str>) -> Result<i32> {
1023
    let repo = open()?;
1024
    let sp = ui::spinner(&format!("Cloning {url} into {path}…"));
1025
    let pin = repo.submodule_add(url, path, bookmark)?;
75167672 Andy McCall 1026
    sp.done(&format!(
1027
        "{} subrepo {} {} {}",
1028
        ui::done("Added"),
1029
        ui::path(path),
1030
        ui::dim(ui::glyph("→", "->")),
1031
        ui::dim(pin.summary())
1032
    ));
1033
    println!(
1034
        "  {} {}",
1035
        ui::note(),
1036
        ui::dim("recorded in .oxymodules; commit to pin it")
1037
    );
4dee277c Andy McCall ↶ 1038
    Ok(0)
1039
}
1040
1041
pub fn submodule_update(init: bool, paths: &[String]) -> Result<i32> {
1042
    let repo = open()?;
1043
    let sp = ui::spinner("Updating subrepos…");
1044
    let results = repo.submodule_update(init, paths)?;
7993d465 Andy McCall ↶ 1045
    let unpinned: Vec<_> = repo
1046
        .unpinned_modules()?
1047
        .into_iter()
1048
        .filter(|(p, _)| paths.is_empty() || paths.iter().any(|q| q.trim_end_matches('/') == p))
1049
        .collect();
4dee277c Andy McCall ↶ 1050
    sp.clear();
7993d465 Andy McCall ↶ 1051
    if results.is_empty() && unpinned.is_empty() {
1052
        println!("{}", ui::dim("No subrepos."));
1053
    }
1054
    let code = print_subrepo_results(results);
1055
    print_unpinned(&unpinned);
1056
    Ok(if unpinned.is_empty() { code } else { 1 })
14edbaaf Andy McCall ↶ 1057
}
1058
7993d465 Andy McCall ↶ 1059
/// `.oxymodules` entries never added: nothing to clone, so say how to add
1060
/// them (oxygen#72).
1061
fn print_unpinned(unpinned: &[(String, oxy_core::subrepo::Module)]) {
1062
    for (path, m) in unpinned {
1063
        let url = if m.url.is_empty() { "<url>" } else { &m.url };
1064
        println!(
1065
            "{} {}",
1066
            ui::warn(format!(
1067
                "{} in .oxymodules but never added, so no pin",
1068
                ui::glyph("⚠", "!")
1069
            )),
1070
            ui::path(path)
1071
        );
1072
        println!(
1073
            "  {}",
1074
            ui::dim(format!("add it with: oxy submodule add {url} {path}"))
1075
        );
1076
    }
1077
}
1078
14edbaaf Andy McCall ↶ 1079
fn print_subrepo_results(results: Vec<(String, oxy_core::repo::SubrepoSync)>) -> i32 {
4dee277c Andy McCall ↶ 1080
    let mut code = 0;
1081
    for (path, r) in results {
1082
        use oxy_core::repo::SubrepoSync::*;
1083
        let line = match r {
1084
            Updated => ui::done("Updated"),
1085
            UpToDate => ui::ok(format!("{} up to date", ui::glyph("✔", "*"))),
1086
            Missing => {
1087
                code = 1;
75167672 Andy McCall 1088
                ui::warn(format!(
1089
                    "{} missing (pass --init to clone)",
1090
                    ui::glyph("⚠", "!")
1091
                ))
4dee277c Andy McCall ↶ 1092
            }
1093
            Dirty => {
1094
                code = 1;
75167672 Andy McCall 1095
                ui::warn(format!(
1096
                    "{} has uncommitted changes; left as is",
1097
                    ui::glyph("⚠", "!")
1098
                ))
4dee277c Andy McCall ↶ 1099
            }
1a3b6203 Andy McCall ↶ 1100
            KeptBookmark { bookmark, newer } => ui::warn(format!(
1101
                "{} checked out at the pin; its bookmark {bookmark} keeps {} the pin lacks",
1102
                ui::glyph("⚠", "!"),
1103
                plural(newer, "newer change")
1104
            )),
4dee277c Andy McCall ↶ 1105
            Failed(e) => {
1106
                code = 1;
1107
                ui::err(format!("{} {e}", ui::glyph("✖", "x")))
1108
            }
1109
        };
1110
        println!("{line} {}", ui::path(path));
1111
    }
14edbaaf Andy McCall ↶ 1112
    code
4dee277c Andy McCall ↶ 1113
}
1114
c6aa5bd9 Andy McCall ↶ 1115
fn pin_json(p: &oxy_core::subrepo::Pin) -> String {
1116
    match p {
75167672 Andy McCall 1117
        oxy_core::subrepo::Pin::Oxy {
1118
            repo,
1119
            heads,
1120
            bookmark,
1121
        } => json::obj([
1122
            ("kind", json::s("oxy")),
1123
            (
1124
                "repo",
1125
                json::s(&repo.iter().map(|b| format!("{b:02x}")).collect::<String>()),
1126
            ),
1127
            ("heads", json::hashes(heads)),
1128
            ("bookmark", json::s(bookmark)),
1129
        ]),
1130
        oxy_core::subrepo::Pin::Git { sha, branch } => json::obj([
1131
            ("kind", json::s("git")),
1132
            ("sha", json::s(sha)),
1133
            ("branch", json::s(branch)),
1134
        ]),
c6aa5bd9 Andy McCall ↶ 1135
    }
1136
}
1137
1138
pub fn submodule_status(as_json: bool) -> Result<i32> {
4dee277c Andy McCall ↶ 1139
    let repo = open()?;
1140
    let subs = repo.subrepos()?;
7993d465 Andy McCall ↶ 1141
    let unpinned = repo.unpinned_modules()?;
c6aa5bd9 Andy McCall ↶ 1142
    if as_json {
7993d465 Andy McCall ↶ 1143
        let pinned = subs.iter().map(|s| {
1144
            json::obj([
1145
                ("path", json::s(&s.path)),
1146
                ("pin", pin_json(&s.pin)),
1147
                ("actual", json::opt(s.actual.as_ref(), pin_json)),
1148
                ("at_pin", json::b(s.actual.as_ref() == Some(&s.pin))),
1149
                ("dirty", json::b(s.dirty)),
1150
                ("committed", json::b(s.committed)),
1151
                ("url", json::opt(s.url.as_deref(), json::s)),
1152
            ])
1153
        });
1154
        // Listed in .oxymodules but never added: no pin at all.
1155
        let unpinned_json = unpinned.iter().map(|(path, m)| {
1156
            json::obj([
1157
                ("path", json::s(path)),
1158
                ("pin", "null".to_string()),
1159
                ("actual", "null".to_string()),
1160
                ("at_pin", json::b(false)),
1161
                ("dirty", json::b(false)),
1162
                ("committed", json::b(false)),
1163
                ("url", json::s(&m.url)),
1164
            ])
1165
        });
1166
        println!("{}", json::arr(pinned.chain(unpinned_json)));
c6aa5bd9 Andy McCall ↶ 1167
        return Ok(0);
1168
    }
7993d465 Andy McCall ↶ 1169
    if subs.is_empty() && unpinned.is_empty() {
4dee277c Andy McCall ↶ 1170
        println!("{}", ui::dim("No subrepos."));
94084087 Andy McCall ↶ 1171
    }
4dee277c Andy McCall ↶ 1172
    for s in subs {
1173
        let state = match (&s.actual, s.dirty) {
e7ac6c2e Andy McCall ↶ 1174
            _ if !s.committed => {
1175
                ui::warn(format!("{} added, not committed yet", ui::glyph("+", "+")))
1176
            }
4dee277c Andy McCall ↶ 1177
            (None, _) => ui::warn(format!("{} missing", ui::glyph("⚠", "!"))),
1178
            (Some(a), dirty) if *a == s.pin => {
75167672 Andy McCall 1179
                if dirty {
1180
                    ui::warn(format!(
1181
                        "{} at pin, uncommitted changes",
1182
                        ui::glyph("✎", "~")
1183
                    ))
1184
                } else {
1185
                    ui::ok(format!("{} at pin", ui::glyph("✔", "*")))
1186
                }
4dee277c Andy McCall ↶ 1187
            }
1188
            (Some(a), _) => ui::warn(format!("{} at {}", ui::glyph("↕", "<>"), a.summary())),
1189
        };
75167672 Andy McCall 1190
        println!(
1191
            "{} {}{}{}{}{state}",
1192
            ui::ok(ui::glyph("⧉", "#")),
1193
            ui::path(&s.path),
1194
            ui::sep(),
1195
            ui::dim(s.pin.summary()),
1196
            ui::sep()
1197
        );
4dee277c Andy McCall ↶ 1198
        if let Some(u) = s.url {
1199
            println!("    {} {}", ui::dim(ui::glyph("↳", "->")), ui::dim(u));
1200
        }
1201
    }
7993d465 Andy McCall ↶ 1202
    print_unpinned(&unpinned);
4dee277c Andy McCall ↶ 1203
    Ok(0)
1204
}
1205
1206
pub fn submodule_remove(path: &str) -> Result<i32> {
1207
    let repo = open()?;
1208
    repo.submodule_remove(path)?;
75167672 Andy McCall 1209
    println!(
1210
        "{} subrepo {} {}",
1211
        ui::done("Removed"),
1212
        ui::path(path),
1213
        ui::dim("(directory left in place; commit to record the removal)")
1214
    );
c3069537 Andy McCall ↶ 1215
    Ok(0)
1216
}
2863456a Andy McCall ↶ 1217
1218
pub fn deepen(remote: &str, depth: u32) -> Result<i32> {
1219
    let repo = open()?;
1220
    if !repo.is_shallow() {
75167672 Andy McCall 1221
        println!(
1222
            "{}",
1223
            ui::dim("This repository is not shallow; nothing to deepen.")
1224
        );
2863456a Andy McCall ↶ 1225
        return Ok(0);
1226
    }
1227
    let sp = ui::spinner(&format!("Fetching {depth} more levels from {remote}…"));
1228
    let n = repo.deepen(remote, depth)?;
75167672 Andy McCall 1229
    sp.done(&format!(
1230
        "{} {}; {}",
c07406bc Andy McCall ↶ 1231
        ui::done("Deepened by"),
75167672 Andy McCall 1232
        plural(n, "change"),
1233
        if repo.is_shallow() {
1234
            "still shallow".to_string()
1235
        } else {
1236
            ui::ok("history is now complete")
1237
        }
1238
    ));
2863456a Andy McCall ↶ 1239
    Ok(0)
1240
}