OXYGENHUB
# b9bf7eec1f2d oxygen / src / cli / sync.rs
20.8 KiB · Rust · last changed Git import: shared graphs, deterministic node ids; quieter status 1 month 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>,
17
) -> Result<i32> {
fce44ae9 Andy McCall ↶ 18
    let dest = dest.unwrap_or_else(|| {
75167672 Andy McCall 19
        let base = url
20
            .trim_end_matches('/')
21
            .rsplit('/')
22
            .next()
23
            .unwrap_or("repo");
fce44ae9 Andy McCall ↶ 24
        PathBuf::from(base)
25
    });
b3c5a59a Andy McCall ↶ 26
    let max_blob = max_blob.map(oxy_core::repo::parse_size).transpose()?;
135212de Andy McCall ↶ 27
    let sp = ui::spinner(&format!("Cloning {url}…"));
2863456a Andy McCall ↶ 28
    let repo = match depth {
29
        Some(d) => Repo::clone_shallow(url, &dest, d, max_blob, sparse)?,
30
        None => Repo::clone_with(url, &dest, max_blob, sparse)?,
31
    };
fce44ae9 Andy McCall ↶ 32
    let heads = repo.bookmark_heads()?;
75167672 Andy McCall 33
    sp.done(&format!(
34
        "{} into {} {}",
35
        ui::done("Cloned"),
36
        ui::path(repo.root().display()),
37
        ui::dim(format!(
38
            "({url}, {} with {})",
39
            ui::bookmark(&repo.config.bookmark),
40
            plural(heads.len(), "head")
41
        ))
42
    ));
fce44ae9 Andy McCall ↶ 43
    Ok(0)
44
}
45
46
pub fn remote_add(name: &str, url: &str) -> Result<i32> {
47
    let mut repo = open()?;
48
    repo.config.set(&format!("remote.{name}.url"), url)?;
49
    repo.save_config()?;
75167672 Andy McCall 50
    println!(
51
        "{} remote {} {} {url}",
52
        ui::done("Added"),
53
        ui::bookmark(name),
54
        ui::dim(ui::glyph("→", "->"))
55
    );
fce44ae9 Andy McCall ↶ 56
    Ok(0)
57
}
58
59
pub fn remote_remove(name: &str) -> Result<i32> {
60
    let mut repo = open()?;
61
    repo.config.set(&format!("remote.{name}.url"), "")?;
62
    repo.save_config()?;
94084087 Andy McCall ↶ 63
    println!("{} remote {}", ui::done("Removed"), ui::bookmark(name));
fce44ae9 Andy McCall ↶ 64
    Ok(0)
65
}
66
c6aa5bd9 Andy McCall ↶ 67
pub fn remote_list(as_json: bool) -> Result<i32> {
fce44ae9 Andy McCall ↶ 68
    let repo = open()?;
69
    let view = repo.view()?;
c6aa5bd9 Andy McCall ↶ 70
    if as_json {
71
        let items = repo.config.remotes.iter().map(|(name, url)| {
72
            let bms = view.remotes.get(name).map(|b| {
73
                json::arr(b.iter().map(|(bm, st)| {
75167672 Andy McCall 74
                    let heads = repo
75
                        .store()
76
                        .get_state(st)
77
                        .map(|s| s.heads)
78
                        .unwrap_or_default();
c6aa5bd9 Andy McCall ↶ 79
                    json::obj([("name", json::s(bm)), ("heads", json::hashes(&heads))])
80
                }))
81
            });
75167672 Andy McCall 82
            json::obj([
83
                ("name", json::s(name)),
84
                ("url", json::s(url)),
dfb23a3a Andy McCall ↶ 85
                // The CA file `remote.NAME.ca` names, for tools that talk to
86
                // the same hub (an editor's forge view, `oh`).
87
                (
88
                    "ca",
89
                    json::opt(repo.config.remote_cas.get(name), |c| json::s(c)),
90
                ),
75167672 Andy McCall 91
                ("bookmarks", bms.unwrap_or_else(|| "[]".into())),
92
            ])
c6aa5bd9 Andy McCall ↶ 93
        });
94
        println!("{}", json::arr(items));
95
        return Ok(0);
96
    }
94084087 Andy McCall ↶ 97
    if repo.config.remotes.is_empty() {
75167672 Andy McCall 98
        println!(
99
            "{}",
100
            ui::dim("No remotes; add one with oxy remote add <name> <url>.")
101
        );
94084087 Andy McCall ↶ 102
    }
fce44ae9 Andy McCall ↶ 103
    for (name, url) in &repo.config.remotes {
75167672 Andy McCall 104
        println!(
105
            "{} {}{}{}",
106
            ui::ok(ui::glyph("⇄", "<>")),
107
            ui::bookmark(name),
108
            ui::sep(),
109
            ui::dim(url)
110
        );
fce44ae9 Andy McCall ↶ 111
        if let Some(bms) = view.remotes.get(name) {
112
            for (bm, st) in bms {
75167672 Andy McCall 113
                let heads = repo
114
                    .store()
115
                    .get_state(st)
116
                    .map(|s| s.heads.len())
117
                    .unwrap_or(0);
118
                println!(
119
                    "    {} {}{}{}",
120
                    ui::dim(ui::glyph("↳", "->")),
121
                    ui::id(format!("{name}/{bm}")),
122
                    ui::sep(),
123
                    ui::dim(plural(heads, "head"))
124
                );
fce44ae9 Andy McCall ↶ 125
            }
126
        }
127
    }
128
    Ok(0)
129
}
130
131
fn remote_name(remote: Option<&str>) -> String {
132
    remote.unwrap_or("origin").to_string()
133
}
134
c6aa5bd9 Andy McCall ↶ 135
pub fn fetch(remote: Option<&str>, as_json: bool) -> Result<i32> {
fce44ae9 Andy McCall ↶ 136
    let repo = open()?;
137
    let remote = remote_name(remote);
135212de Andy McCall ↶ 138
    let sp = ui::spinner(&format!("Fetching from {remote}…"));
83e0677b Andy McCall ↶ 139
    let report = repo.fetch(&remote)?;
135212de Andy McCall ↶ 140
    sp.clear();
c6aa5bd9 Andy McCall ↶ 141
    if as_json {
142
        println!(
143
            "{}",
144
            json::obj([
145
                ("remote", json::s(&remote)),
75167672 Andy McCall 146
                (
147
                    "refs",
148
                    json::arr(report.refs.iter().map(|(name, heads, n)| json::obj([
149
                        ("name", json::s(name)),
150
                        ("heads", json::hashes(heads)),
151
                        ("new", json::n(*n))
152
                    ])))
153
                ),
c6aa5bd9 Andy McCall ↶ 154
                ("new_tags", json::n(report.new_tags)),
155
                ("unverified", json::b(report.unverified)),
156
            ])
157
        );
158
        return Ok(0);
159
    }
94084087 Andy McCall ↶ 160
    if report.refs.is_empty() {
75167672 Andy McCall 161
        println!(
162
            "{} {}",
163
            ui::done("Fetched"),
164
            ui::dim(format!("nothing new from {remote}"))
165
        );
94084087 Andy McCall ↶ 166
    }
83e0677b Andy McCall ↶ 167
    for (name, heads, n) in &report.refs {
75167672 Andy McCall 168
        let what = if *n == 0 {
169
            ui::dim("up to date")
170
        } else {
171
            ui::ok(plural(*n, "new change"))
172
        };
173
        println!(
174
            "{} {}{}{}{}{what}",
175
            ui::done("Fetched"),
176
            ui::id(format!("{remote}/{name}")),
177
            ui::sep(),
178
            ui::dim(plural(heads.len(), "head")),
179
            ui::sep()
180
        );
83e0677b Andy McCall ↶ 181
    }
132b884b Andy McCall ↶ 182
    if report.new_tags > 0 {
75167672 Andy McCall 183
        println!(
184
            "  {} {}",
185
            ui::note(),
186
            ui::dim(format!("{} adopted", plural(report.new_tags, "tag")))
187
        );
132b884b Andy McCall ↶ 188
    }
83e0677b Andy McCall ↶ 189
    if report.unverified {
75167672 Andy McCall 190
        eprintln!(
191
            "{} {remote} has no trust policy; nothing fetched from it was verified",
192
            ui::caution()
193
        );
fce44ae9 Andy McCall ↶ 194
    }
195
    Ok(0)
196
}
197
c6aa5bd9 Andy McCall ↶ 198
pub fn pull(remote: Option<&str>, as_json: bool) -> Result<i32> {
fce44ae9 Andy McCall ↶ 199
    let repo = open()?;
200
    let remote = remote_name(remote);
135212de Andy McCall ↶ 201
    let sp = ui::spinner(&format!("Pulling from {remote}…"));
fce44ae9 Andy McCall ↶ 202
    let r = repo.pull(&remote)?;
c6aa5bd9 Andy McCall ↶ 203
    if as_json {
204
        sp.clear();
cc7a0056 Andy McCall ↶ 205
        let conflicted = repo
206
            .snapshot_status("oxy pull")?
207
            .base_snapshot
208
            .is_conflicted();
75167672 Andy McCall 209
        println!(
210
            "{}",
211
            json::obj([
212
                ("remote", json::s(&remote)),
213
                ("heads", json::hashes(&r.heads)),
214
                ("changed", json::b(r.changed)),
215
                ("checked_out", json::b(r.checked_out)),
216
                ("conflicted", json::b(conflicted))
217
            ])
218
        );
c6aa5bd9 Andy McCall ↶ 219
        return Ok(0);
220
    }
fce44ae9 Andy McCall ↶ 221
    if !r.changed {
75167672 Andy McCall 222
        sp.done(&format!(
223
            "{} {}",
224
            ui::ok(ui::glyph("✔", "*")),
225
            ui::dim("Already up to date.")
226
        ));
fce44ae9 Andy McCall ↶ 227
        return Ok(0);
228
    }
135212de Andy McCall ↶ 229
    sp.done(&format!(
230
        "{} {} now has {}{}",
94084087 Andy McCall ↶ 231
        ui::done("Pulled:"),
232
        ui::bookmark(&repo.config.bookmark),
fce44ae9 Andy McCall ↶ 233
        plural(r.heads.len(), "head"),
75167672 Andy McCall 234
        ui::dim(if r.checked_out {
235
            "; working copy updated"
236
        } else {
237
            "; run oxy checkout to update the working copy"
238
        })
135212de Andy McCall ↶ 239
    ));
fce44ae9 Andy McCall ↶ 240
    let snap = repo.snapshot("oxy pull")?;
241
    if snap.base_snapshot.is_conflicted() {
75167672 Andy McCall 242
        println!(
243
            "  {} {}",
244
            ui::caution(),
245
            ui::warn("the result has conflicts; see oxy status")
246
        );
fce44ae9 Andy McCall ↶ 247
    }
248
    Ok(0)
249
}
250
f8c69576 Andy McCall ↶ 251
/// `oxy push [REMOTE] --delete NAME`.
252
pub fn push_delete(remote: Option<&str>, name: &str, as_json: bool) -> Result<i32> {
253
    let repo = open()?;
254
    let remote = remote_name(remote);
255
    let sp = ui::spinner(&format!("Deleting {remote}/{name}…"));
256
    let results = repo.push_delete(&remote, name)?;
257
    sp.clear();
258
    if as_json {
259
        println!(
260
            "{}",
261
            json::arr(results.iter().map(|r| {
262
                json::obj([
263
                    ("name", json::s(&r.name)),
264
                    ("ok", json::b(r.ok)),
265
                    ("reason", json::s(&r.reason)),
266
                    ("state", json::s("")),
267
                ])
268
            }))
269
        );
270
        return Ok(if results.iter().all(|r| r.ok) { 0 } else { 1 });
271
    }
272
    let mut code = 0;
273
    for r in results {
274
        if r.ok {
275
            println!(
276
                "{} {}",
277
                ui::done("Deleted"),
278
                ui::id(format!("{remote}/{}", r.name))
279
            );
280
        } else {
281
            println!(
282
                "{} {} {}{}{}",
283
                ui::err(ui::glyph("✖", "x")),
284
                ui::err("Rejected"),
285
                ui::id(format!("{remote}/{}", r.name)),
286
                ui::sep(),
287
                r.reason
288
            );
289
            code = 1;
290
        }
291
    }
292
    Ok(code)
293
}
294
75167672 Andy McCall 295
pub fn push(
296
    remote: Option<&str>,
297
    force: bool,
298
    bookmark: Option<&str>,
299
    as_json: bool,
300
) -> Result<i32> {
fce44ae9 Andy McCall ↶ 301
    let repo = open()?;
302
    let remote = remote_name(remote);
135212de Andy McCall ↶ 303
    let sp = ui::spinner(&format!("Pushing to {remote}…"));
7b9809e6 Andy McCall ↶ 304
    let results = match bookmark {
305
        Some(b) => repo.push_bookmark(&remote, b, force)?,
306
        None => repo.push(&remote, force)?,
307
    };
135212de Andy McCall ↶ 308
    sp.clear();
c6aa5bd9 Andy McCall ↶ 309
    if as_json {
75167672 Andy McCall 310
        println!(
311
            "{}",
312
            json::arr(results.iter().map(|r| json::obj([
313
                ("name", json::s(&r.name)),
314
                ("ok", json::b(r.ok)),
315
                ("reason", json::s(&r.reason))
316
            ])))
317
        );
c6aa5bd9 Andy McCall ↶ 318
        return Ok(if results.iter().all(|r| r.ok) { 0 } else { 1 });
319
    }
fce44ae9 Andy McCall ↶ 320
    let mut code = 0;
135212de Andy McCall ↶ 321
    for r in results {
fce44ae9 Andy McCall ↶ 322
        if r.ok {
75167672 Andy McCall 323
            println!(
324
                "{} {}{}{}",
325
                ui::done("Pushed"),
326
                ui::id(format!("{remote}/{}", r.name)),
327
                ui::sep(),
328
                ui::dim(if r.reason.is_empty() {
329
                    "updated"
330
                } else {
331
                    r.reason.as_str()
332
                })
333
            );
fce44ae9 Andy McCall ↶ 334
        } else {
75167672 Andy McCall 335
            println!(
336
                "{} {} {}{}{}",
337
                ui::err(ui::glyph("✖", "x")),
338
                ui::err("Rejected"),
339
                ui::id(format!("{remote}/{}", r.name)),
340
                ui::sep(),
341
                r.reason
342
            );
fce44ae9 Andy McCall ↶ 343
            code = 1;
344
        }
345
    }
346
    Ok(code)
347
}
348
75167672 Andy McCall 349
pub fn serve(
350
    root: PathBuf,
351
    bind: &str,
352
    tokens: Vec<String>,
353
    tls: Option<(PathBuf, PathBuf)>,
354
) -> Result<i32> {
fce44ae9 Andy McCall ↶ 355
    let listener = std::net::TcpListener::bind(bind).with_context(|| format!("binding {bind}"))?;
356
    let root = root.canonicalize()?;
2b5ee371 Andy McCall ↶ 357
    let tls = tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key });
fce44ae9 Andy McCall ↶ 358
    println!(
2b5ee371 Andy McCall ↶ 359
        "Serving repositories under {} on {}://{bind}/<repo>/oxy/v1/ ({})",
fce44ae9 Andy McCall ↶ 360
        root.display(),
2b5ee371 Andy McCall ↶ 361
        if tls.is_some() { "https" } else { "http" },
75167672 Andy McCall 362
        if tokens.is_empty() {
363
            "open: no push tokens configured"
364
        } else {
365
            "push requires a token"
366
        }
fce44ae9 Andy McCall ↶ 367
    );
75167672 Andy McCall 368
    if tls.is_none()
369
        && !bind.starts_with("127.")
370
        && !bind.starts_with("localhost")
371
        && !bind.starts_with("[::1]")
372
    {
2b5ee371 Andy McCall ↶ 373
        eprintln!("warning: serving plain HTTP on a non-loopback address; put a TLS terminator in front or pass --tls-cert/--tls-key");
374
    }
375
    oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens, tls })?;
fce44ae9 Andy McCall ↶ 376
    Ok(0)
377
}
79ab8528 Andy McCall ↶ 378
30ea9c1e Andy McCall ↶ 379
pub fn git_export(git_dir: &std::path::Path, branch: Option<&str>) -> Result<i32> {
79ab8528 Andy McCall ↶ 380
    let repo = open()?;
135212de Andy McCall ↶ 381
    let sp = ui::spinner("Exporting to Git…");
30ea9c1e Andy McCall ↶ 382
    let r = match branch {
383
        Some(b) => repo.git_export_as(git_dir, b)?,
384
        None => repo.git_export(git_dir)?,
385
    };
135212de Andy McCall ↶ 386
    sp.clear();
79ab8528 Andy McCall ↶ 387
    println!(
94084087 Andy McCall ↶ 388
        "{} {} to {} as branch {} {}",
389
        ui::done("Exported"),
e8b4e156 Andy McCall ↶ 390
        plural(r.exported, "new state"),
94084087 Andy McCall ↶ 391
        ui::path(git_dir.display()),
392
        ui::bookmark(&r.branch),
75167672 Andy McCall 393
        ui::dim(format!(
394
            "(currently {}; {} already exported)",
395
            plural(r.heads, "head"),
396
            r.skipped
397
        ))
79ab8528 Andy McCall ↶ 398
    );
399
    Ok(0)
400
}
c3069537 Andy McCall ↶ 401
30ea9c1e Andy McCall ↶ 402
pub fn git_fetch(url: &str, branch: &str) -> Result<i32> {
403
    let repo = open()?;
404
    let sp = ui::spinner(&format!("Fetching {url} {branch}…"));
405
    let r = repo.git_fetch(url, branch)?;
406
    sp.clear();
407
    if r.commits == 0 {
75167672 Andy McCall 408
        println!(
409
            "{} {}",
410
            ui::done("Fetched"),
411
            ui::dim(format!("nothing new from {url} {branch}"))
412
        );
30ea9c1e Andy McCall ↶ 413
    } else {
414
        println!(
415
            "{} {} ({} merges) from {} {}{}{} now has {}",
416
            ui::done("Fetched"),
417
            plural(r.commits, "new commit"),
418
            r.merges,
419
            ui::path(url),
420
            ui::bookmark(branch),
421
            ui::sep(),
422
            ui::bookmark(&repo.config.bookmark),
423
            plural(r.heads, "head")
424
        );
425
        if !repo.snapshot("oxy git fetch")?.changed {
426
            println!("  {} {}", ui::note(), ui::dim("working copy updated"));
427
        } else {
75167672 Andy McCall 428
            println!(
429
                "  {} {}",
430
                ui::note(),
431
                ui::dim("run oxy checkout to update the working copy")
432
            );
30ea9c1e Andy McCall ↶ 433
        }
434
    }
4dee277c Andy McCall ↶ 435
    if r.submodules > 0 {
75167672 Andy McCall 436
        println!(
437
            "  {} {}",
438
            ui::note(),
439
            ui::dim(format!(
440
                "{} submodule {} imported as subrepos with Git pins",
441
                r.submodules,
442
                if r.submodules == 1 {
443
                    "entry"
444
                } else {
445
                    "entries"
446
                }
447
            ))
448
        );
30ea9c1e Andy McCall ↶ 449
    }
450
    Ok(0)
451
}
452
453
pub fn git_push(url: &str, branch: &str) -> Result<i32> {
454
    let repo = open()?;
455
    let sp = ui::spinner(&format!("Pushing to {url} {branch}…"));
456
    let r = repo.git_push(url, branch)?;
457
    sp.clear();
458
    println!(
459
        "{} {} to {} {} {}",
460
        ui::done("Pushed"),
461
        plural(r.exported, "new commit"),
462
        ui::path(url),
463
        ui::bookmark(&r.branch),
464
        ui::dim(format!("({} already there)", r.skipped))
465
    );
466
    Ok(0)
467
}
468
62794ecc Andy McCall ↶ 469
/// `oxy git verify`: exit 1 if any commit's tree is not reproduced.
470
pub fn git_verify(git_dir: &std::path::Path, branch: &str) -> Result<i32> {
471
    let repo = open()?;
472
    let started = std::time::Instant::now();
473
    let sp = ui::spinner(&format!(
474
        "Rebuilding every commit of {} {branch}…",
475
        git_dir.display()
476
    ));
477
    let r = repo.verify_git_import(git_dir, branch)?;
478
    sp.clear();
479
    for (_, what) in &r.mismatches {
480
        println!("{} {what}", ui::err(ui::glyph("✖", "x")));
481
    }
482
    if r.mismatches.is_empty() {
483
        println!(
484
            "{}",
485
            ui::done(format!(
486
                "Every commit reproduced: {} commits, {} files compared, in {:.1}s",
487
                r.commits,
488
                r.files_compared,
489
                started.elapsed().as_secs_f64()
490
            ))
491
        );
492
        Ok(0)
493
    } else {
494
        println!(
495
            "{} differences in {} commits checked",
496
            r.mismatches.len(),
497
            r.commits
498
        );
499
        Ok(1)
500
    }
501
}
502
c3069537 Andy McCall ↶ 503
pub fn git_import(git_dir: &std::path::Path, dest: &std::path::Path, branch: &str) -> Result<i32> {
504
    let repo = Repo::init(dest)?;
505
    let started = std::time::Instant::now();
135212de Andy McCall ↶ 506
    let sp = ui::spinner(&format!("Importing {} {branch}…", git_dir.display()));
c3069537 Andy McCall ↶ 507
    let r = repo.import_git(git_dir, branch)?;
135212de Andy McCall ↶ 508
    sp.clear();
c3069537 Andy McCall ↶ 509
    println!(
94084087 Andy McCall ↶ 510
        "{} {} ({} merges) from {} {} into {} {}",
511
        ui::done("Imported"),
c3069537 Andy McCall ↶ 512
        plural(r.commits, "commit"),
513
        r.merges,
94084087 Andy McCall ↶ 514
        ui::path(git_dir.display()),
515
        ui::bookmark(branch),
516
        ui::path(repo.root().display()),
75167672 Andy McCall 517
        ui::dim(format!(
518
            "in {:.1?}; {} now has {}",
519
            started.elapsed(),
520
            repo.config.bookmark,
521
            plural(r.heads, "head")
522
        ))
c3069537 Andy McCall ↶ 523
    );
4dee277c Andy McCall ↶ 524
    if r.submodules > 0 {
75167672 Andy McCall 525
        println!(
526
            "  {} {}",
527
            ui::note(),
528
            ui::dim(format!(
529
                "{} submodule {} imported as subrepos with Git pins",
530
                r.submodules,
531
                if r.submodules == 1 {
532
                    "entry"
533
                } else {
534
                    "entries"
535
                }
536
            ))
537
        );
4dee277c Andy McCall ↶ 538
    }
539
    Ok(0)
540
}
541
542
pub fn submodule_add(url: &str, path: &str, bookmark: Option<&str>) -> Result<i32> {
543
    let repo = open()?;
544
    let sp = ui::spinner(&format!("Cloning {url} into {path}…"));
545
    let pin = repo.submodule_add(url, path, bookmark)?;
75167672 Andy McCall 546
    sp.done(&format!(
547
        "{} subrepo {} {} {}",
548
        ui::done("Added"),
549
        ui::path(path),
550
        ui::dim(ui::glyph("→", "->")),
551
        ui::dim(pin.summary())
552
    ));
553
    println!(
554
        "  {} {}",
555
        ui::note(),
556
        ui::dim("recorded in .oxymodules; commit to pin it")
557
    );
4dee277c Andy McCall ↶ 558
    Ok(0)
559
}
560
561
pub fn submodule_update(init: bool, paths: &[String]) -> Result<i32> {
562
    let repo = open()?;
563
    let sp = ui::spinner("Updating subrepos…");
564
    let results = repo.submodule_update(init, paths)?;
565
    sp.clear();
566
    if results.is_empty() {
567
        println!("{}", ui::dim("No subrepos."));
568
    }
569
    let mut code = 0;
570
    for (path, r) in results {
571
        use oxy_core::repo::SubrepoSync::*;
572
        let line = match r {
573
            Updated => ui::done("Updated"),
574
            UpToDate => ui::ok(format!("{} up to date", ui::glyph("✔", "*"))),
575
            Missing => {
576
                code = 1;
75167672 Andy McCall 577
                ui::warn(format!(
578
                    "{} missing (pass --init to clone)",
579
                    ui::glyph("⚠", "!")
580
                ))
4dee277c Andy McCall ↶ 581
            }
582
            Dirty => {
583
                code = 1;
75167672 Andy McCall 584
                ui::warn(format!(
585
                    "{} has uncommitted changes; left as is",
586
                    ui::glyph("⚠", "!")
587
                ))
4dee277c Andy McCall ↶ 588
            }
589
            Failed(e) => {
590
                code = 1;
591
                ui::err(format!("{} {e}", ui::glyph("✖", "x")))
592
            }
593
        };
594
        println!("{line} {}", ui::path(path));
595
    }
596
    Ok(code)
597
}
598
c6aa5bd9 Andy McCall ↶ 599
fn pin_json(p: &oxy_core::subrepo::Pin) -> String {
600
    match p {
75167672 Andy McCall 601
        oxy_core::subrepo::Pin::Oxy {
602
            repo,
603
            heads,
604
            bookmark,
605
        } => json::obj([
606
            ("kind", json::s("oxy")),
607
            (
608
                "repo",
609
                json::s(&repo.iter().map(|b| format!("{b:02x}")).collect::<String>()),
610
            ),
611
            ("heads", json::hashes(heads)),
612
            ("bookmark", json::s(bookmark)),
613
        ]),
614
        oxy_core::subrepo::Pin::Git { sha, branch } => json::obj([
615
            ("kind", json::s("git")),
616
            ("sha", json::s(sha)),
617
            ("branch", json::s(branch)),
618
        ]),
c6aa5bd9 Andy McCall ↶ 619
    }
620
}
621
622
pub fn submodule_status(as_json: bool) -> Result<i32> {
4dee277c Andy McCall ↶ 623
    let repo = open()?;
624
    let subs = repo.subrepos()?;
c6aa5bd9 Andy McCall ↶ 625
    if as_json {
626
        println!(
627
            "{}",
628
            json::arr(subs.iter().map(|s| {
629
                json::obj([
630
                    ("path", json::s(&s.path)),
631
                    ("pin", pin_json(&s.pin)),
632
                    ("actual", json::opt(s.actual.as_ref(), pin_json)),
633
                    ("at_pin", json::b(s.actual.as_ref() == Some(&s.pin))),
634
                    ("dirty", json::b(s.dirty)),
e7ac6c2e Andy McCall ↶ 635
                    ("committed", json::b(s.committed)),
c6aa5bd9 Andy McCall ↶ 636
                    ("url", json::opt(s.url.as_deref(), json::s)),
637
                ])
638
            }))
639
        );
640
        return Ok(0);
641
    }
4dee277c Andy McCall ↶ 642
    if subs.is_empty() {
643
        println!("{}", ui::dim("No subrepos."));
94084087 Andy McCall ↶ 644
    }
4dee277c Andy McCall ↶ 645
    for s in subs {
646
        let state = match (&s.actual, s.dirty) {
e7ac6c2e Andy McCall ↶ 647
            _ if !s.committed => {
648
                ui::warn(format!("{} added, not committed yet", ui::glyph("+", "+")))
649
            }
4dee277c Andy McCall ↶ 650
            (None, _) => ui::warn(format!("{} missing", ui::glyph("⚠", "!"))),
651
            (Some(a), dirty) if *a == s.pin => {
75167672 Andy McCall 652
                if dirty {
653
                    ui::warn(format!(
654
                        "{} at pin, uncommitted changes",
655
                        ui::glyph("✎", "~")
656
                    ))
657
                } else {
658
                    ui::ok(format!("{} at pin", ui::glyph("✔", "*")))
659
                }
4dee277c Andy McCall ↶ 660
            }
661
            (Some(a), _) => ui::warn(format!("{} at {}", ui::glyph("↕", "<>"), a.summary())),
662
        };
75167672 Andy McCall 663
        println!(
664
            "{} {}{}{}{}{state}",
665
            ui::ok(ui::glyph("⧉", "#")),
666
            ui::path(&s.path),
667
            ui::sep(),
668
            ui::dim(s.pin.summary()),
669
            ui::sep()
670
        );
4dee277c Andy McCall ↶ 671
        if let Some(u) = s.url {
672
            println!("    {} {}", ui::dim(ui::glyph("↳", "->")), ui::dim(u));
673
        }
674
    }
675
    Ok(0)
676
}
677
678
pub fn submodule_remove(path: &str) -> Result<i32> {
679
    let repo = open()?;
680
    repo.submodule_remove(path)?;
75167672 Andy McCall 681
    println!(
682
        "{} subrepo {} {}",
683
        ui::done("Removed"),
684
        ui::path(path),
685
        ui::dim("(directory left in place; commit to record the removal)")
686
    );
c3069537 Andy McCall ↶ 687
    Ok(0)
688
}
2863456a Andy McCall ↶ 689
690
pub fn deepen(remote: &str, depth: u32) -> Result<i32> {
691
    let repo = open()?;
692
    if !repo.is_shallow() {
75167672 Andy McCall 693
        println!(
694
            "{}",
695
            ui::dim("This repository is not shallow; nothing to deepen.")
696
        );
2863456a Andy McCall ↶ 697
        return Ok(0);
698
    }
699
    let sp = ui::spinner(&format!("Fetching {depth} more levels from {remote}…"));
700
    let n = repo.deepen(remote, depth)?;
75167672 Andy McCall 701
    sp.done(&format!(
702
        "{} {}; {}",
703
        ui::ok("Deepened by"),
704
        plural(n, "change"),
705
        if repo.is_shallow() {
706
            "still shallow".to_string()
707
        } else {
708
            ui::ok("history is now complete")
709
        }
710
    ));
2863456a Andy McCall ↶ 711
    Ok(0)
712
}