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