OXYGENHUB
◆ v0.2.0 oxygen / src / cli / sync.rs
25.3 KiB · Rust · last changed The operation log says what each operation did; op restore, op revert and --at-op; commands say where they left you (closes #51, closes #52, closes #53) 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>,
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
fce44ae9 Andy McCall ↶ 98
pub fn remote_add(name: &str, url: &str) -> Result<i32> {
99
    let mut repo = open()?;
100
    repo.config.set(&format!("remote.{name}.url"), url)?;
101
    repo.save_config()?;
75167672 Andy McCall 102
    println!(
103
        "{} remote {} {} {url}",
104
        ui::done("Added"),
105
        ui::bookmark(name),
106
        ui::dim(ui::glyph("→", "->"))
107
    );
fce44ae9 Andy McCall ↶ 108
    Ok(0)
109
}
110
111
pub fn remote_remove(name: &str) -> Result<i32> {
112
    let mut repo = open()?;
113
    repo.config.set(&format!("remote.{name}.url"), "")?;
114
    repo.save_config()?;
94084087 Andy McCall ↶ 115
    println!("{} remote {}", ui::done("Removed"), ui::bookmark(name));
fce44ae9 Andy McCall ↶ 116
    Ok(0)
117
}
118
c6aa5bd9 Andy McCall ↶ 119
pub fn remote_list(as_json: bool) -> Result<i32> {
fce44ae9 Andy McCall ↶ 120
    let repo = open()?;
121
    let view = repo.view()?;
c6aa5bd9 Andy McCall ↶ 122
    if as_json {
123
        let items = repo.config.remotes.iter().map(|(name, url)| {
124
            let bms = view.remotes.get(name).map(|b| {
125
                json::arr(b.iter().map(|(bm, st)| {
75167672 Andy McCall 126
                    let heads = repo
127
                        .store()
128
                        .get_state(st)
129
                        .map(|s| s.heads)
130
                        .unwrap_or_default();
c6aa5bd9 Andy McCall ↶ 131
                    json::obj([("name", json::s(bm)), ("heads", json::hashes(&heads))])
132
                }))
133
            });
75167672 Andy McCall 134
            json::obj([
135
                ("name", json::s(name)),
136
                ("url", json::s(url)),
dfb23a3a Andy McCall ↶ 137
                // The CA file `remote.NAME.ca` names, for tools that talk to
138
                // the same hub (an editor's forge view, `oh`).
139
                (
140
                    "ca",
141
                    json::opt(repo.config.remote_cas.get(name), |c| json::s(c)),
142
                ),
75167672 Andy McCall 143
                ("bookmarks", bms.unwrap_or_else(|| "[]".into())),
144
            ])
c6aa5bd9 Andy McCall ↶ 145
        });
146
        println!("{}", json::arr(items));
147
        return Ok(0);
148
    }
94084087 Andy McCall ↶ 149
    if repo.config.remotes.is_empty() {
75167672 Andy McCall 150
        println!(
151
            "{}",
152
            ui::dim("No remotes; add one with oxy remote add <name> <url>.")
153
        );
94084087 Andy McCall ↶ 154
    }
fce44ae9 Andy McCall ↶ 155
    for (name, url) in &repo.config.remotes {
75167672 Andy McCall 156
        println!(
157
            "{} {}{}{}",
158
            ui::ok(ui::glyph("⇄", "<>")),
159
            ui::bookmark(name),
160
            ui::sep(),
161
            ui::dim(url)
162
        );
fce44ae9 Andy McCall ↶ 163
        if let Some(bms) = view.remotes.get(name) {
164
            for (bm, st) in bms {
75167672 Andy McCall 165
                let heads = repo
166
                    .store()
167
                    .get_state(st)
168
                    .map(|s| s.heads.len())
169
                    .unwrap_or(0);
170
                println!(
171
                    "    {} {}{}{}",
172
                    ui::dim(ui::glyph("↳", "->")),
173
                    ui::id(format!("{name}/{bm}")),
174
                    ui::sep(),
175
                    ui::dim(plural(heads, "head"))
176
                );
fce44ae9 Andy McCall ↶ 177
            }
178
        }
179
    }
180
    Ok(0)
181
}
182
183
fn remote_name(remote: Option<&str>) -> String {
184
    remote.unwrap_or("origin").to_string()
185
}
186
c6aa5bd9 Andy McCall ↶ 187
pub fn fetch(remote: Option<&str>, as_json: bool) -> Result<i32> {
fce44ae9 Andy McCall ↶ 188
    let repo = open()?;
189
    let remote = remote_name(remote);
135212de Andy McCall ↶ 190
    let sp = ui::spinner(&format!("Fetching from {remote}…"));
83e0677b Andy McCall ↶ 191
    let report = repo.fetch(&remote)?;
135212de Andy McCall ↶ 192
    sp.clear();
c6aa5bd9 Andy McCall ↶ 193
    if as_json {
194
        println!(
195
            "{}",
196
            json::obj([
197
                ("remote", json::s(&remote)),
75167672 Andy McCall 198
                (
199
                    "refs",
200
                    json::arr(report.refs.iter().map(|(name, heads, n)| json::obj([
201
                        ("name", json::s(name)),
202
                        ("heads", json::hashes(heads)),
203
                        ("new", json::n(*n))
204
                    ])))
205
                ),
c6aa5bd9 Andy McCall ↶ 206
                ("new_tags", json::n(report.new_tags)),
207
                ("unverified", json::b(report.unverified)),
208
            ])
209
        );
210
        return Ok(0);
211
    }
94084087 Andy McCall ↶ 212
    if report.refs.is_empty() {
75167672 Andy McCall 213
        println!(
214
            "{} {}",
215
            ui::done("Fetched"),
216
            ui::dim(format!("nothing new from {remote}"))
217
        );
94084087 Andy McCall ↶ 218
    }
83e0677b Andy McCall ↶ 219
    for (name, heads, n) in &report.refs {
75167672 Andy McCall 220
        let what = if *n == 0 {
221
            ui::dim("up to date")
222
        } else {
223
            ui::ok(plural(*n, "new change"))
224
        };
225
        println!(
226
            "{} {}{}{}{}{what}",
227
            ui::done("Fetched"),
228
            ui::id(format!("{remote}/{name}")),
229
            ui::sep(),
230
            ui::dim(plural(heads.len(), "head")),
231
            ui::sep()
232
        );
83e0677b Andy McCall ↶ 233
    }
132b884b Andy McCall ↶ 234
    if report.new_tags > 0 {
75167672 Andy McCall 235
        println!(
236
            "  {} {}",
237
            ui::note(),
238
            ui::dim(format!("{} adopted", plural(report.new_tags, "tag")))
239
        );
132b884b Andy McCall ↶ 240
    }
83e0677b Andy McCall ↶ 241
    if report.unverified {
75167672 Andy McCall 242
        eprintln!(
243
            "{} {remote} has no trust policy; nothing fetched from it was verified",
244
            ui::caution()
245
        );
fce44ae9 Andy McCall ↶ 246
    }
247
    Ok(0)
248
}
249
7d9dab13 Andy McCall ↶ 250
/// Pull acts on the current bookmark only: name the others the fetch
251
/// found behind their remote, which is easy to take for the current one
252
/// (oxygen#29).
253
fn others_behind(repo: &oxy_core::repo::Repo, remote: &str) {
254
    let Ok(bookmarks) = repo.bookmarks() else {
255
        return;
256
    };
257
    for b in bookmarks {
258
        let Some(n) = b.behind.filter(|n| *n > 0) else {
259
            continue;
260
        };
261
        if b.current {
262
            continue;
263
        }
264
        println!(
265
            "  {} {}",
266
            ui::note(),
267
            ui::dim(format!(
268
                "{} is {} behind {remote}/{}: oxy bookmark switch {} then oxy pull to take {}",
269
                b.name,
270
                plural(n, "change"),
271
                b.name,
272
                b.name,
273
                if n == 1 { "it" } else { "them" }
274
            ))
275
        );
276
    }
277
}
278
c6aa5bd9 Andy McCall ↶ 279
pub fn pull(remote: Option<&str>, as_json: bool) -> Result<i32> {
fce44ae9 Andy McCall ↶ 280
    let repo = open()?;
281
    let remote = remote_name(remote);
135212de Andy McCall ↶ 282
    let sp = ui::spinner(&format!("Pulling from {remote}…"));
fce44ae9 Andy McCall ↶ 283
    let r = repo.pull(&remote)?;
c6aa5bd9 Andy McCall ↶ 284
    if as_json {
285
        sp.clear();
cc7a0056 Andy McCall ↶ 286
        let conflicted = repo
287
            .snapshot_status("oxy pull")?
288
            .base_snapshot
289
            .is_conflicted();
75167672 Andy McCall 290
        println!(
291
            "{}",
292
            json::obj([
293
                ("remote", json::s(&remote)),
7d9dab13 Andy McCall ↶ 294
                ("bookmark", json::s(&repo.config.bookmark)),
75167672 Andy McCall 295
                ("heads", json::hashes(&r.heads)),
296
                ("changed", json::b(r.changed)),
297
                ("checked_out", json::b(r.checked_out)),
14edbaaf Andy McCall ↶ 298
                ("conflicted", json::b(conflicted)),
299
                ("others", json::arr(r.others.iter().map(|o| json::s(o))))
75167672 Andy McCall 300
            ])
301
        );
c6aa5bd9 Andy McCall ↶ 302
        return Ok(0);
303
    }
fce44ae9 Andy McCall ↶ 304
    if !r.changed {
75167672 Andy McCall 305
        sp.done(&format!(
306
            "{} {}",
307
            ui::ok(ui::glyph("✔", "*")),
7d9dab13 Andy McCall ↶ 308
            ui::dim(format!(
309
                "{} is up to date with {remote}.",
310
                repo.config.bookmark
311
            ))
75167672 Andy McCall 312
        ));
14edbaaf Andy McCall ↶ 313
        others_pulled(&r.others);
7d9dab13 Andy McCall ↶ 314
        others_behind(&repo, &remote);
fce44ae9 Andy McCall ↶ 315
        return Ok(0);
316
    }
135212de Andy McCall ↶ 317
    sp.done(&format!(
318
        "{} {} now has {}{}",
94084087 Andy McCall ↶ 319
        ui::done("Pulled:"),
320
        ui::bookmark(&repo.config.bookmark),
fce44ae9 Andy McCall ↶ 321
        plural(r.heads.len(), "head"),
75167672 Andy McCall 322
        ui::dim(if r.checked_out {
323
            "; working copy updated"
324
        } else {
325
            "; run oxy checkout to update the working copy"
326
        })
135212de Andy McCall ↶ 327
    ));
fce44ae9 Andy McCall ↶ 328
    let snap = repo.snapshot("oxy pull")?;
329
    if snap.base_snapshot.is_conflicted() {
75167672 Andy McCall 330
        println!(
331
            "  {} {}",
332
            ui::caution(),
333
            ui::warn("the result has conflicts; see oxy status")
334
        );
fce44ae9 Andy McCall ↶ 335
    }
14edbaaf Andy McCall ↶ 336
    others_pulled(&r.others);
7d9dab13 Andy McCall ↶ 337
    others_behind(&repo, &remote);
14edbaaf Andy McCall ↶ 338
    subrepos_behind(&repo);
98a1620a Andy McCall ↶ 339
    super::workspace::where_now(&repo);
fce44ae9 Andy McCall ↶ 340
    Ok(0)
341
}
342
14edbaaf Andy McCall ↶ 343
/// The other bookmarks a pull moved.
344
fn others_pulled(others: &[String]) {
345
    if others.is_empty() {
346
        return;
347
    }
348
    let shown: Vec<String> = others.iter().take(8).map(ui::bookmark).collect();
349
    let more = others.len().saturating_sub(8);
350
    println!(
351
        "  {} {}{}",
352
        ui::done("Also updated"),
353
        shown.join(", "),
354
        if more > 0 {
355
            ui::dim(format!(" and {more} more"))
356
        } else {
357
            String::new()
358
        }
359
    );
360
}
361
f8c69576 Andy McCall ↶ 362
/// `oxy push [REMOTE] --delete NAME`.
363
pub fn push_delete(remote: Option<&str>, name: &str, as_json: bool) -> Result<i32> {
364
    let repo = open()?;
365
    let remote = remote_name(remote);
366
    let sp = ui::spinner(&format!("Deleting {remote}/{name}…"));
367
    let results = repo.push_delete(&remote, name)?;
368
    sp.clear();
369
    if as_json {
370
        println!(
371
            "{}",
372
            json::arr(results.iter().map(|r| {
373
                json::obj([
374
                    ("name", json::s(&r.name)),
375
                    ("ok", json::b(r.ok)),
376
                    ("reason", json::s(&r.reason)),
377
                    ("state", json::s("")),
378
                ])
379
            }))
380
        );
381
        return Ok(if results.iter().all(|r| r.ok) { 0 } else { 1 });
382
    }
383
    let mut code = 0;
384
    for r in results {
385
        if r.ok {
386
            println!(
387
                "{} {}",
388
                ui::done("Deleted"),
389
                ui::id(format!("{remote}/{}", r.name))
390
            );
391
        } else {
392
            println!(
393
                "{} {} {}{}{}",
394
                ui::err(ui::glyph("✖", "x")),
395
                ui::err("Rejected"),
396
                ui::id(format!("{remote}/{}", r.name)),
397
                ui::sep(),
398
                r.reason
399
            );
400
            code = 1;
401
        }
402
    }
403
    Ok(code)
404
}
405
75167672 Andy McCall 406
pub fn push(
407
    remote: Option<&str>,
408
    force: bool,
409
    bookmark: Option<&str>,
410
    as_json: bool,
411
) -> Result<i32> {
fce44ae9 Andy McCall ↶ 412
    let repo = open()?;
413
    let remote = remote_name(remote);
135212de Andy McCall ↶ 414
    let sp = ui::spinner(&format!("Pushing to {remote}…"));
7b9809e6 Andy McCall ↶ 415
    let results = match bookmark {
416
        Some(b) => repo.push_bookmark(&remote, b, force)?,
417
        None => repo.push(&remote, force)?,
418
    };
135212de Andy McCall ↶ 419
    sp.clear();
c6aa5bd9 Andy McCall ↶ 420
    if as_json {
60a1223c Andy McCall ↶ 421
        // The state each accepted bookmark now holds on the remote (its local
422
        // state object), so a caller can look up what the push started, such
423
        // as the checks a forge runs on it.
424
        let view = repo.view()?;
75167672 Andy McCall 425
        println!(
426
            "{}",
60a1223c Andy McCall ↶ 427
            json::arr(results.iter().map(|r| {
428
                let state = view
429
                    .bookmarks
430
                    .get(&r.name)
431
                    .filter(|_| r.ok)
432
                    .map(|st| st.to_hex())
433
                    .unwrap_or_default();
434
                json::obj([
435
                    ("name", json::s(&r.name)),
436
                    ("ok", json::b(r.ok)),
437
                    ("reason", json::s(&r.reason)),
438
                    ("state", json::s(&state)),
439
                ])
440
            }))
75167672 Andy McCall 441
        );
c6aa5bd9 Andy McCall ↶ 442
        return Ok(if results.iter().all(|r| r.ok) { 0 } else { 1 });
443
    }
fce44ae9 Andy McCall ↶ 444
    let mut code = 0;
135212de Andy McCall ↶ 445
    for r in results {
fce44ae9 Andy McCall ↶ 446
        if r.ok {
75167672 Andy McCall 447
            println!(
448
                "{} {}{}{}",
449
                ui::done("Pushed"),
450
                ui::id(format!("{remote}/{}", r.name)),
451
                ui::sep(),
452
                ui::dim(if r.reason.is_empty() {
453
                    "updated"
454
                } else {
455
                    r.reason.as_str()
456
                })
457
            );
fce44ae9 Andy McCall ↶ 458
        } else {
75167672 Andy McCall 459
            println!(
460
                "{} {} {}{}{}",
461
                ui::err(ui::glyph("✖", "x")),
462
                ui::err("Rejected"),
463
                ui::id(format!("{remote}/{}", r.name)),
464
                ui::sep(),
465
                r.reason
466
            );
fce44ae9 Andy McCall ↶ 467
            code = 1;
468
        }
469
    }
470
    Ok(code)
471
}
472
75167672 Andy McCall 473
pub fn serve(
474
    root: PathBuf,
475
    bind: &str,
476
    tokens: Vec<String>,
477
    tls: Option<(PathBuf, PathBuf)>,
478
) -> Result<i32> {
fce44ae9 Andy McCall ↶ 479
    let listener = std::net::TcpListener::bind(bind).with_context(|| format!("binding {bind}"))?;
480
    let root = root.canonicalize()?;
2b5ee371 Andy McCall ↶ 481
    let tls = tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key });
fce44ae9 Andy McCall ↶ 482
    println!(
2b5ee371 Andy McCall ↶ 483
        "Serving repositories under {} on {}://{bind}/<repo>/oxy/v1/ ({})",
fce44ae9 Andy McCall ↶ 484
        root.display(),
2b5ee371 Andy McCall ↶ 485
        if tls.is_some() { "https" } else { "http" },
75167672 Andy McCall 486
        if tokens.is_empty() {
487
            "open: no push tokens configured"
488
        } else {
489
            "push requires a token"
490
        }
fce44ae9 Andy McCall ↶ 491
    );
75167672 Andy McCall 492
    if tls.is_none()
493
        && !bind.starts_with("127.")
494
        && !bind.starts_with("localhost")
495
        && !bind.starts_with("[::1]")
496
    {
2b5ee371 Andy McCall ↶ 497
        eprintln!("warning: serving plain HTTP on a non-loopback address; put a TLS terminator in front or pass --tls-cert/--tls-key");
498
    }
499
    oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens, tls })?;
fce44ae9 Andy McCall ↶ 500
    Ok(0)
501
}
79ab8528 Andy McCall ↶ 502
30ea9c1e Andy McCall ↶ 503
pub fn git_export(git_dir: &std::path::Path, branch: Option<&str>) -> Result<i32> {
79ab8528 Andy McCall ↶ 504
    let repo = open()?;
135212de Andy McCall ↶ 505
    let sp = ui::spinner("Exporting to Git…");
30ea9c1e Andy McCall ↶ 506
    let r = match branch {
507
        Some(b) => repo.git_export_as(git_dir, b)?,
508
        None => repo.git_export(git_dir)?,
509
    };
135212de Andy McCall ↶ 510
    sp.clear();
79ab8528 Andy McCall ↶ 511
    println!(
94084087 Andy McCall ↶ 512
        "{} {} to {} as branch {} {}",
513
        ui::done("Exported"),
e8b4e156 Andy McCall ↶ 514
        plural(r.exported, "new state"),
94084087 Andy McCall ↶ 515
        ui::path(git_dir.display()),
516
        ui::bookmark(&r.branch),
75167672 Andy McCall 517
        ui::dim(format!(
518
            "(currently {}; {} already exported)",
519
            plural(r.heads, "head"),
520
            r.skipped
521
        ))
79ab8528 Andy McCall ↶ 522
    );
523
    Ok(0)
524
}
c3069537 Andy McCall ↶ 525
30ea9c1e Andy McCall ↶ 526
pub fn git_fetch(url: &str, branch: &str) -> Result<i32> {
527
    let repo = open()?;
528
    let sp = ui::spinner(&format!("Fetching {url} {branch}…"));
529
    let r = repo.git_fetch(url, branch)?;
530
    sp.clear();
531
    if r.commits == 0 {
75167672 Andy McCall 532
        println!(
533
            "{} {}",
534
            ui::done("Fetched"),
535
            ui::dim(format!("nothing new from {url} {branch}"))
536
        );
30ea9c1e Andy McCall ↶ 537
    } else {
538
        println!(
539
            "{} {} ({} merges) from {} {}{}{} now has {}",
540
            ui::done("Fetched"),
541
            plural(r.commits, "new commit"),
542
            r.merges,
543
            ui::path(url),
544
            ui::bookmark(branch),
545
            ui::sep(),
546
            ui::bookmark(&repo.config.bookmark),
547
            plural(r.heads, "head")
548
        );
549
        if !repo.snapshot("oxy git fetch")?.changed {
550
            println!("  {} {}", ui::note(), ui::dim("working copy updated"));
551
        } else {
75167672 Andy McCall 552
            println!(
553
                "  {} {}",
554
                ui::note(),
555
                ui::dim("run oxy checkout to update the working copy")
556
            );
30ea9c1e Andy McCall ↶ 557
        }
558
    }
4dee277c Andy McCall ↶ 559
    if r.submodules > 0 {
75167672 Andy McCall 560
        println!(
561
            "  {} {}",
562
            ui::note(),
563
            ui::dim(format!(
564
                "{} submodule {} imported as subrepos with Git pins",
565
                r.submodules,
566
                if r.submodules == 1 {
567
                    "entry"
568
                } else {
569
                    "entries"
570
                }
571
            ))
572
        );
30ea9c1e Andy McCall ↶ 573
    }
574
    Ok(0)
575
}
576
577
pub fn git_push(url: &str, branch: &str) -> Result<i32> {
578
    let repo = open()?;
579
    let sp = ui::spinner(&format!("Pushing to {url} {branch}…"));
580
    let r = repo.git_push(url, branch)?;
581
    sp.clear();
582
    println!(
583
        "{} {} to {} {} {}",
584
        ui::done("Pushed"),
585
        plural(r.exported, "new commit"),
586
        ui::path(url),
587
        ui::bookmark(&r.branch),
588
        ui::dim(format!("({} already there)", r.skipped))
589
    );
590
    Ok(0)
591
}
592
62794ecc Andy McCall ↶ 593
/// `oxy git verify`: exit 1 if any commit's tree is not reproduced.
7eac74ab Andy McCall ↶ 594
pub fn git_verify(git_dir: &std::path::Path, branch: &str, last: Option<usize>) -> Result<i32> {
62794ecc Andy McCall ↶ 595
    let repo = open()?;
596
    let started = std::time::Instant::now();
597
    let sp = ui::spinner(&format!(
598
        "Rebuilding every commit of {} {branch}…",
599
        git_dir.display()
600
    ));
7eac74ab Andy McCall ↶ 601
    let r = repo.verify_git_import_last(git_dir, branch, last)?;
62794ecc Andy McCall ↶ 602
    sp.clear();
603
    for (_, what) in &r.mismatches {
604
        println!("{} {what}", ui::err(ui::glyph("✖", "x")));
605
    }
606
    if r.mismatches.is_empty() {
607
        println!(
608
            "{}",
7eac74ab Andy McCall ↶ 609
            ui::done(if r.skipped > 0 {
610
                format!(
611
                    "The last {} commits reproduced ({} followed), {} files compared, in {:.1}s",
612
                    r.commits - r.skipped,
613
                    r.skipped,
614
                    r.files_compared,
615
                    started.elapsed().as_secs_f64()
616
                )
617
            } else {
618
                format!(
619
                    "Every commit reproduced: {} commits, {} files compared, in {:.1}s",
620
                    r.commits,
621
                    r.files_compared,
622
                    started.elapsed().as_secs_f64()
623
                )
624
            })
62794ecc Andy McCall ↶ 625
        );
626
        Ok(0)
627
    } else {
628
        println!(
629
            "{} differences in {} commits checked",
630
            r.mismatches.len(),
631
            r.commits
632
        );
633
        Ok(1)
634
    }
635
}
636
c3069537 Andy McCall ↶ 637
pub fn git_import(git_dir: &std::path::Path, dest: &std::path::Path, branch: &str) -> Result<i32> {
638
    let repo = Repo::init(dest)?;
639
    let started = std::time::Instant::now();
135212de Andy McCall ↶ 640
    let sp = ui::spinner(&format!("Importing {} {branch}…", git_dir.display()));
c3069537 Andy McCall ↶ 641
    let r = repo.import_git(git_dir, branch)?;
135212de Andy McCall ↶ 642
    sp.clear();
c3069537 Andy McCall ↶ 643
    println!(
94084087 Andy McCall ↶ 644
        "{} {} ({} merges) from {} {} into {} {}",
645
        ui::done("Imported"),
c3069537 Andy McCall ↶ 646
        plural(r.commits, "commit"),
647
        r.merges,
94084087 Andy McCall ↶ 648
        ui::path(git_dir.display()),
649
        ui::bookmark(branch),
650
        ui::path(repo.root().display()),
75167672 Andy McCall 651
        ui::dim(format!(
652
            "in {:.1?}; {} now has {}",
653
            started.elapsed(),
654
            repo.config.bookmark,
655
            plural(r.heads, "head")
656
        ))
c3069537 Andy McCall ↶ 657
    );
4dee277c Andy McCall ↶ 658
    if r.submodules > 0 {
75167672 Andy McCall 659
        println!(
660
            "  {} {}",
661
            ui::note(),
662
            ui::dim(format!(
663
                "{} submodule {} imported as subrepos with Git pins",
664
                r.submodules,
665
                if r.submodules == 1 {
666
                    "entry"
667
                } else {
668
                    "entries"
669
                }
670
            ))
671
        );
4dee277c Andy McCall ↶ 672
    }
673
    Ok(0)
674
}
675
676
pub fn submodule_add(url: &str, path: &str, bookmark: Option<&str>) -> Result<i32> {
677
    let repo = open()?;
678
    let sp = ui::spinner(&format!("Cloning {url} into {path}…"));
679
    let pin = repo.submodule_add(url, path, bookmark)?;
75167672 Andy McCall 680
    sp.done(&format!(
681
        "{} subrepo {} {} {}",
682
        ui::done("Added"),
683
        ui::path(path),
684
        ui::dim(ui::glyph("→", "->")),
685
        ui::dim(pin.summary())
686
    ));
687
    println!(
688
        "  {} {}",
689
        ui::note(),
690
        ui::dim("recorded in .oxymodules; commit to pin it")
691
    );
4dee277c Andy McCall ↶ 692
    Ok(0)
693
}
694
695
pub fn submodule_update(init: bool, paths: &[String]) -> Result<i32> {
696
    let repo = open()?;
697
    let sp = ui::spinner("Updating subrepos…");
698
    let results = repo.submodule_update(init, paths)?;
699
    sp.clear();
14edbaaf Andy McCall ↶ 700
    Ok(print_subrepo_results(results))
701
}
702
703
fn print_subrepo_results(results: Vec<(String, oxy_core::repo::SubrepoSync)>) -> i32 {
4dee277c Andy McCall ↶ 704
    if results.is_empty() {
705
        println!("{}", ui::dim("No subrepos."));
706
    }
707
    let mut code = 0;
708
    for (path, r) in results {
709
        use oxy_core::repo::SubrepoSync::*;
710
        let line = match r {
711
            Updated => ui::done("Updated"),
712
            UpToDate => ui::ok(format!("{} up to date", ui::glyph("✔", "*"))),
713
            Missing => {
714
                code = 1;
75167672 Andy McCall 715
                ui::warn(format!(
716
                    "{} missing (pass --init to clone)",
717
                    ui::glyph("⚠", "!")
718
                ))
4dee277c Andy McCall ↶ 719
            }
720
            Dirty => {
721
                code = 1;
75167672 Andy McCall 722
                ui::warn(format!(
723
                    "{} has uncommitted changes; left as is",
724
                    ui::glyph("⚠", "!")
725
                ))
4dee277c Andy McCall ↶ 726
            }
1a3b6203 Andy McCall ↶ 727
            KeptBookmark { bookmark, newer } => ui::warn(format!(
728
                "{} checked out at the pin; its bookmark {bookmark} keeps {} the pin lacks",
729
                ui::glyph("⚠", "!"),
730
                plural(newer, "newer change")
731
            )),
4dee277c Andy McCall ↶ 732
            Failed(e) => {
733
                code = 1;
734
                ui::err(format!("{} {e}", ui::glyph("✖", "x")))
735
            }
736
        };
737
        println!("{line} {}", ui::path(path));
738
    }
14edbaaf Andy McCall ↶ 739
    code
4dee277c Andy McCall ↶ 740
}
741
c6aa5bd9 Andy McCall ↶ 742
fn pin_json(p: &oxy_core::subrepo::Pin) -> String {
743
    match p {
75167672 Andy McCall 744
        oxy_core::subrepo::Pin::Oxy {
745
            repo,
746
            heads,
747
            bookmark,
748
        } => json::obj([
749
            ("kind", json::s("oxy")),
750
            (
751
                "repo",
752
                json::s(&repo.iter().map(|b| format!("{b:02x}")).collect::<String>()),
753
            ),
754
            ("heads", json::hashes(heads)),
755
            ("bookmark", json::s(bookmark)),
756
        ]),
757
        oxy_core::subrepo::Pin::Git { sha, branch } => json::obj([
758
            ("kind", json::s("git")),
759
            ("sha", json::s(sha)),
760
            ("branch", json::s(branch)),
761
        ]),
c6aa5bd9 Andy McCall ↶ 762
    }
763
}
764
765
pub fn submodule_status(as_json: bool) -> Result<i32> {
4dee277c Andy McCall ↶ 766
    let repo = open()?;
767
    let subs = repo.subrepos()?;
c6aa5bd9 Andy McCall ↶ 768
    if as_json {
769
        println!(
770
            "{}",
771
            json::arr(subs.iter().map(|s| {
772
                json::obj([
773
                    ("path", json::s(&s.path)),
774
                    ("pin", pin_json(&s.pin)),
775
                    ("actual", json::opt(s.actual.as_ref(), pin_json)),
776
                    ("at_pin", json::b(s.actual.as_ref() == Some(&s.pin))),
777
                    ("dirty", json::b(s.dirty)),
e7ac6c2e Andy McCall ↶ 778
                    ("committed", json::b(s.committed)),
c6aa5bd9 Andy McCall ↶ 779
                    ("url", json::opt(s.url.as_deref(), json::s)),
780
                ])
781
            }))
782
        );
783
        return Ok(0);
784
    }
4dee277c Andy McCall ↶ 785
    if subs.is_empty() {
786
        println!("{}", ui::dim("No subrepos."));
94084087 Andy McCall ↶ 787
    }
4dee277c Andy McCall ↶ 788
    for s in subs {
789
        let state = match (&s.actual, s.dirty) {
e7ac6c2e Andy McCall ↶ 790
            _ if !s.committed => {
791
                ui::warn(format!("{} added, not committed yet", ui::glyph("+", "+")))
792
            }
4dee277c Andy McCall ↶ 793
            (None, _) => ui::warn(format!("{} missing", ui::glyph("⚠", "!"))),
794
            (Some(a), dirty) if *a == s.pin => {
75167672 Andy McCall 795
                if dirty {
796
                    ui::warn(format!(
797
                        "{} at pin, uncommitted changes",
798
                        ui::glyph("✎", "~")
799
                    ))
800
                } else {
801
                    ui::ok(format!("{} at pin", ui::glyph("✔", "*")))
802
                }
4dee277c Andy McCall ↶ 803
            }
804
            (Some(a), _) => ui::warn(format!("{} at {}", ui::glyph("↕", "<>"), a.summary())),
805
        };
75167672 Andy McCall 806
        println!(
807
            "{} {}{}{}{}{state}",
808
            ui::ok(ui::glyph("⧉", "#")),
809
            ui::path(&s.path),
810
            ui::sep(),
811
            ui::dim(s.pin.summary()),
812
            ui::sep()
813
        );
4dee277c Andy McCall ↶ 814
        if let Some(u) = s.url {
815
            println!("    {} {}", ui::dim(ui::glyph("↳", "->")), ui::dim(u));
816
        }
817
    }
818
    Ok(0)
819
}
820
821
pub fn submodule_remove(path: &str) -> Result<i32> {
822
    let repo = open()?;
823
    repo.submodule_remove(path)?;
75167672 Andy McCall 824
    println!(
825
        "{} subrepo {} {}",
826
        ui::done("Removed"),
827
        ui::path(path),
828
        ui::dim("(directory left in place; commit to record the removal)")
829
    );
c3069537 Andy McCall ↶ 830
    Ok(0)
831
}
2863456a Andy McCall ↶ 832
833
pub fn deepen(remote: &str, depth: u32) -> Result<i32> {
834
    let repo = open()?;
835
    if !repo.is_shallow() {
75167672 Andy McCall 836
        println!(
837
            "{}",
838
            ui::dim("This repository is not shallow; nothing to deepen.")
839
        );
2863456a Andy McCall ↶ 840
        return Ok(0);
841
    }
842
    let sp = ui::spinner(&format!("Fetching {depth} more levels from {remote}…"));
843
    let n = repo.deepen(remote, depth)?;
75167672 Andy McCall 844
    sp.done(&format!(
845
        "{} {}; {}",
846
        ui::ok("Deepened by"),
847
        plural(n, "change"),
848
        if repo.is_shallow() {
849
            "still shallow".to_string()
850
        } else {
851
            ui::ok("history is now complete")
852
        }
853
    ));
2863456a Andy McCall ↶ 854
    Ok(0)
855
}