OXYGENHUB
# d8c086ca3eb2 oxygen / src / cli / sync.rs
12.3 KiB · Rust · last changed Subrepositories: subrepo node kind with Oxygen and Git pins, .oxymodules, nested repositories skipped by the walker and moved by checkout, oxy submodule add|update|status|remove, Git gitlink import and export (spec 0.8 section 7.8) 1 month ago source blame history raw
fce44ae9 Andy McCall ↶ 1
//! Commands that talk to remotes, and the reference server.
2
3
use super::format::plural;
4
use super::open;
135212de Andy McCall ↶ 5
use super::ui;
fce44ae9 Andy McCall ↶ 6
use anyhow::{Context, Result};
7
use oxy_core::repo::Repo;
8
use std::path::PathBuf;
9
2863456a Andy McCall ↶ 10
pub fn clone(url: &str, dest: Option<PathBuf>, max_blob: Option<&str>, sparse: &[String], depth: Option<u32>) -> Result<i32> {
fce44ae9 Andy McCall ↶ 11
    let dest = dest.unwrap_or_else(|| {
12
        let base = url.trim_end_matches('/').rsplit('/').next().unwrap_or("repo");
13
        PathBuf::from(base)
14
    });
b3c5a59a Andy McCall ↶ 15
    let max_blob = max_blob.map(oxy_core::repo::parse_size).transpose()?;
135212de Andy McCall ↶ 16
    let sp = ui::spinner(&format!("Cloning {url}…"));
2863456a Andy McCall ↶ 17
    let repo = match depth {
18
        Some(d) => Repo::clone_shallow(url, &dest, d, max_blob, sparse)?,
19
        None => Repo::clone_with(url, &dest, max_blob, sparse)?,
20
    };
fce44ae9 Andy McCall ↶ 21
    let heads = repo.bookmark_heads()?;
94084087 Andy McCall ↶ 22
    sp.done(&format!("{} into {} {}", ui::done("Cloned"), ui::path(repo.root().display()), ui::dim(format!("({url}, {} with {})", ui::bookmark(&repo.config.bookmark), plural(heads.len(), "head")))));
fce44ae9 Andy McCall ↶ 23
    Ok(0)
24
}
25
26
pub fn remote_add(name: &str, url: &str) -> Result<i32> {
27
    let mut repo = open()?;
28
    repo.config.set(&format!("remote.{name}.url"), url)?;
29
    repo.save_config()?;
94084087 Andy McCall ↶ 30
    println!("{} remote {} {} {url}", ui::done("Added"), ui::bookmark(name), ui::dim(ui::glyph("→", "->")));
fce44ae9 Andy McCall ↶ 31
    Ok(0)
32
}
33
34
pub fn remote_remove(name: &str) -> Result<i32> {
35
    let mut repo = open()?;
36
    repo.config.set(&format!("remote.{name}.url"), "")?;
37
    repo.save_config()?;
94084087 Andy McCall ↶ 38
    println!("{} remote {}", ui::done("Removed"), ui::bookmark(name));
fce44ae9 Andy McCall ↶ 39
    Ok(0)
40
}
41
42
pub fn remote_list() -> Result<i32> {
43
    let repo = open()?;
44
    let view = repo.view()?;
94084087 Andy McCall ↶ 45
    if repo.config.remotes.is_empty() {
46
        println!("{}", ui::dim("No remotes; add one with oxy remote add <name> <url>."));
47
    }
fce44ae9 Andy McCall ↶ 48
    for (name, url) in &repo.config.remotes {
94084087 Andy McCall ↶ 49
        println!("{} {}{}{}", ui::ok(ui::glyph("⇄", "<>")), ui::bookmark(name), ui::sep(), ui::dim(url));
fce44ae9 Andy McCall ↶ 50
        if let Some(bms) = view.remotes.get(name) {
51
            for (bm, st) in bms {
52
                let heads = repo.store().get_state(st).map(|s| s.heads.len()).unwrap_or(0);
94084087 Andy McCall ↶ 53
                println!("    {} {}{}{}", ui::dim(ui::glyph("↳", "->")), ui::id(format!("{name}/{bm}")), ui::sep(), ui::dim(plural(heads, "head")));
fce44ae9 Andy McCall ↶ 54
            }
55
        }
56
    }
57
    Ok(0)
58
}
59
60
fn remote_name(remote: Option<&str>) -> String {
61
    remote.unwrap_or("origin").to_string()
62
}
63
64
pub fn fetch(remote: Option<&str>) -> Result<i32> {
65
    let repo = open()?;
66
    let remote = remote_name(remote);
135212de Andy McCall ↶ 67
    let sp = ui::spinner(&format!("Fetching from {remote}…"));
83e0677b Andy McCall ↶ 68
    let report = repo.fetch(&remote)?;
135212de Andy McCall ↶ 69
    sp.clear();
94084087 Andy McCall ↶ 70
    if report.refs.is_empty() {
71
        println!("{} {}", ui::done("Fetched"), ui::dim(format!("nothing new from {remote}")));
72
    }
83e0677b Andy McCall ↶ 73
    for (name, heads, n) in &report.refs {
94084087 Andy McCall ↶ 74
        let what = if *n == 0 { ui::dim("up to date") } else { ui::ok(plural(*n, "new change")) };
75
        println!("{} {}{}{}{}{what}", ui::done("Fetched"), ui::id(format!("{remote}/{name}")), ui::sep(), ui::dim(plural(heads.len(), "head")), ui::sep());
83e0677b Andy McCall ↶ 76
    }
132b884b Andy McCall ↶ 77
    if report.new_tags > 0 {
94084087 Andy McCall ↶ 78
        println!("  {} {}", ui::note(), ui::dim(format!("{} adopted", plural(report.new_tags, "tag"))));
132b884b Andy McCall ↶ 79
    }
83e0677b Andy McCall ↶ 80
    if report.unverified {
94084087 Andy McCall ↶ 81
        eprintln!("{} {remote} has no trust policy; nothing fetched from it was verified", ui::caution());
fce44ae9 Andy McCall ↶ 82
    }
83
    Ok(0)
84
}
85
86
pub fn pull(remote: Option<&str>) -> Result<i32> {
87
    let repo = open()?;
88
    let remote = remote_name(remote);
135212de Andy McCall ↶ 89
    let sp = ui::spinner(&format!("Pulling from {remote}…"));
fce44ae9 Andy McCall ↶ 90
    let r = repo.pull(&remote)?;
91
    if !r.changed {
94084087 Andy McCall ↶ 92
        sp.done(&format!("{} {}", ui::ok(ui::glyph("✔", "*")), ui::dim("Already up to date.")));
fce44ae9 Andy McCall ↶ 93
        return Ok(0);
94
    }
135212de Andy McCall ↶ 95
    sp.done(&format!(
96
        "{} {} now has {}{}",
94084087 Andy McCall ↶ 97
        ui::done("Pulled:"),
98
        ui::bookmark(&repo.config.bookmark),
fce44ae9 Andy McCall ↶ 99
        plural(r.heads.len(), "head"),
94084087 Andy McCall ↶ 100
        ui::dim(if r.checked_out { "; working copy updated" } else { "; run oxy checkout to update the working copy" })
135212de Andy McCall ↶ 101
    ));
fce44ae9 Andy McCall ↶ 102
    let snap = repo.snapshot("oxy pull")?;
103
    if snap.base_snapshot.is_conflicted() {
94084087 Andy McCall ↶ 104
        println!("  {} {}", ui::caution(), ui::warn("the result has conflicts; see oxy status"));
fce44ae9 Andy McCall ↶ 105
    }
106
    Ok(0)
107
}
108
7b9809e6 Andy McCall ↶ 109
pub fn push(remote: Option<&str>, force: bool, bookmark: Option<&str>) -> Result<i32> {
fce44ae9 Andy McCall ↶ 110
    let repo = open()?;
111
    let remote = remote_name(remote);
135212de Andy McCall ↶ 112
    let sp = ui::spinner(&format!("Pushing to {remote}…"));
7b9809e6 Andy McCall ↶ 113
    let results = match bookmark {
114
        Some(b) => repo.push_bookmark(&remote, b, force)?,
115
        None => repo.push(&remote, force)?,
116
    };
135212de Andy McCall ↶ 117
    sp.clear();
fce44ae9 Andy McCall ↶ 118
    let mut code = 0;
135212de Andy McCall ↶ 119
    for r in results {
fce44ae9 Andy McCall ↶ 120
        if r.ok {
94084087 Andy McCall ↶ 121
            println!("{} {}{}{}", ui::done("Pushed"), ui::id(format!("{remote}/{}", r.name)), ui::sep(), ui::dim(if r.reason.is_empty() { "updated" } else { r.reason.as_str() }));
fce44ae9 Andy McCall ↶ 122
        } else {
94084087 Andy McCall ↶ 123
            println!("{} {} {}{}{}", ui::err(ui::glyph("✖", "x")), ui::err("Rejected"), ui::id(format!("{remote}/{}", r.name)), ui::sep(), r.reason);
fce44ae9 Andy McCall ↶ 124
            code = 1;
125
        }
126
    }
127
    Ok(code)
128
}
129
2b5ee371 Andy McCall ↶ 130
pub fn serve(root: PathBuf, bind: &str, tokens: Vec<String>, tls: Option<(PathBuf, PathBuf)>) -> Result<i32> {
fce44ae9 Andy McCall ↶ 131
    let listener = std::net::TcpListener::bind(bind).with_context(|| format!("binding {bind}"))?;
132
    let root = root.canonicalize()?;
2b5ee371 Andy McCall ↶ 133
    let tls = tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key });
fce44ae9 Andy McCall ↶ 134
    println!(
2b5ee371 Andy McCall ↶ 135
        "Serving repositories under {} on {}://{bind}/<repo>/oxy/v1/ ({})",
fce44ae9 Andy McCall ↶ 136
        root.display(),
2b5ee371 Andy McCall ↶ 137
        if tls.is_some() { "https" } else { "http" },
fce44ae9 Andy McCall ↶ 138
        if tokens.is_empty() { "open: no push tokens configured" } else { "push requires a token" }
139
    );
2b5ee371 Andy McCall ↶ 140
    if tls.is_none() && !bind.starts_with("127.") && !bind.starts_with("localhost") && !bind.starts_with("[::1]") {
141
        eprintln!("warning: serving plain HTTP on a non-loopback address; put a TLS terminator in front or pass --tls-cert/--tls-key");
142
    }
143
    oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens, tls })?;
fce44ae9 Andy McCall ↶ 144
    Ok(0)
145
}
79ab8528 Andy McCall ↶ 146
30ea9c1e Andy McCall ↶ 147
pub fn git_export(git_dir: &std::path::Path, branch: Option<&str>) -> Result<i32> {
79ab8528 Andy McCall ↶ 148
    let repo = open()?;
135212de Andy McCall ↶ 149
    let sp = ui::spinner("Exporting to Git…");
30ea9c1e Andy McCall ↶ 150
    let r = match branch {
151
        Some(b) => repo.git_export_as(git_dir, b)?,
152
        None => repo.git_export(git_dir)?,
153
    };
135212de Andy McCall ↶ 154
    sp.clear();
79ab8528 Andy McCall ↶ 155
    println!(
94084087 Andy McCall ↶ 156
        "{} {} to {} as branch {} {}",
157
        ui::done("Exported"),
e8b4e156 Andy McCall ↶ 158
        plural(r.exported, "new state"),
94084087 Andy McCall ↶ 159
        ui::path(git_dir.display()),
160
        ui::bookmark(&r.branch),
161
        ui::dim(format!("(currently {}; {} already exported)", plural(r.heads, "head"), r.skipped))
79ab8528 Andy McCall ↶ 162
    );
163
    Ok(0)
164
}
c3069537 Andy McCall ↶ 165
30ea9c1e Andy McCall ↶ 166
pub fn git_fetch(url: &str, branch: &str) -> Result<i32> {
167
    let repo = open()?;
168
    let sp = ui::spinner(&format!("Fetching {url} {branch}…"));
169
    let r = repo.git_fetch(url, branch)?;
170
    sp.clear();
171
    if r.commits == 0 {
172
        println!("{} {}", ui::done("Fetched"), ui::dim(format!("nothing new from {url} {branch}")));
173
    } else {
174
        println!(
175
            "{} {} ({} merges) from {} {}{}{} now has {}",
176
            ui::done("Fetched"),
177
            plural(r.commits, "new commit"),
178
            r.merges,
179
            ui::path(url),
180
            ui::bookmark(branch),
181
            ui::sep(),
182
            ui::bookmark(&repo.config.bookmark),
183
            plural(r.heads, "head")
184
        );
185
        if !repo.snapshot("oxy git fetch")?.changed {
186
            println!("  {} {}", ui::note(), ui::dim("working copy updated"));
187
        } else {
188
            println!("  {} {}", ui::note(), ui::dim("run oxy checkout to update the working copy"));
189
        }
190
    }
4dee277c Andy McCall ↶ 191
    if r.submodules > 0 {
192
        println!("  {} {}", ui::note(), ui::dim(format!("{} submodule {} imported as subrepos with Git pins", r.submodules, if r.submodules == 1 { "entry" } else { "entries" })));
30ea9c1e Andy McCall ↶ 193
    }
194
    Ok(0)
195
}
196
197
pub fn git_push(url: &str, branch: &str) -> Result<i32> {
198
    let repo = open()?;
199
    let sp = ui::spinner(&format!("Pushing to {url} {branch}…"));
200
    let r = repo.git_push(url, branch)?;
201
    sp.clear();
202
    println!(
203
        "{} {} to {} {} {}",
204
        ui::done("Pushed"),
205
        plural(r.exported, "new commit"),
206
        ui::path(url),
207
        ui::bookmark(&r.branch),
208
        ui::dim(format!("({} already there)", r.skipped))
209
    );
210
    Ok(0)
211
}
212
c3069537 Andy McCall ↶ 213
pub fn git_import(git_dir: &std::path::Path, dest: &std::path::Path, branch: &str) -> Result<i32> {
214
    let repo = Repo::init(dest)?;
215
    let started = std::time::Instant::now();
135212de Andy McCall ↶ 216
    let sp = ui::spinner(&format!("Importing {} {branch}…", git_dir.display()));
c3069537 Andy McCall ↶ 217
    let r = repo.import_git(git_dir, branch)?;
135212de Andy McCall ↶ 218
    sp.clear();
c3069537 Andy McCall ↶ 219
    println!(
94084087 Andy McCall ↶ 220
        "{} {} ({} merges) from {} {} into {} {}",
221
        ui::done("Imported"),
c3069537 Andy McCall ↶ 222
        plural(r.commits, "commit"),
223
        r.merges,
94084087 Andy McCall ↶ 224
        ui::path(git_dir.display()),
225
        ui::bookmark(branch),
226
        ui::path(repo.root().display()),
227
        ui::dim(format!("in {:.1?}; {} now has {}", started.elapsed(), repo.config.bookmark, plural(r.heads, "head")))
c3069537 Andy McCall ↶ 228
    );
4dee277c Andy McCall ↶ 229
    if r.submodules > 0 {
230
        println!("  {} {}", ui::note(), ui::dim(format!("{} submodule {} imported as subrepos with Git pins", r.submodules, if r.submodules == 1 { "entry" } else { "entries" })));
231
    }
232
    Ok(0)
233
}
234
235
pub fn submodule_add(url: &str, path: &str, bookmark: Option<&str>) -> Result<i32> {
236
    let repo = open()?;
237
    let sp = ui::spinner(&format!("Cloning {url} into {path}…"));
238
    let pin = repo.submodule_add(url, path, bookmark)?;
239
    sp.done(&format!("{} subrepo {} {} {}", ui::done("Added"), ui::path(path), ui::dim(ui::glyph("→", "->")), ui::dim(pin.summary())));
240
    println!("  {} {}", ui::note(), ui::dim("recorded in .oxymodules; commit to pin it"));
241
    Ok(0)
242
}
243
244
pub fn submodule_update(init: bool, paths: &[String]) -> Result<i32> {
245
    let repo = open()?;
246
    let sp = ui::spinner("Updating subrepos…");
247
    let results = repo.submodule_update(init, paths)?;
248
    sp.clear();
249
    if results.is_empty() {
250
        println!("{}", ui::dim("No subrepos."));
251
    }
252
    let mut code = 0;
253
    for (path, r) in results {
254
        use oxy_core::repo::SubrepoSync::*;
255
        let line = match r {
256
            Updated => ui::done("Updated"),
257
            UpToDate => ui::ok(format!("{} up to date", ui::glyph("✔", "*"))),
258
            Missing => {
259
                code = 1;
260
                ui::warn(format!("{} missing (pass --init to clone)", ui::glyph("⚠", "!")))
261
            }
262
            Dirty => {
263
                code = 1;
264
                ui::warn(format!("{} has uncommitted changes; left as is", ui::glyph("⚠", "!")))
265
            }
266
            Failed(e) => {
267
                code = 1;
268
                ui::err(format!("{} {e}", ui::glyph("✖", "x")))
269
            }
270
        };
271
        println!("{line} {}", ui::path(path));
272
    }
273
    Ok(code)
274
}
275
276
pub fn submodule_status() -> Result<i32> {
277
    let repo = open()?;
278
    let subs = repo.subrepos()?;
279
    if subs.is_empty() {
280
        println!("{}", ui::dim("No subrepos."));
94084087 Andy McCall ↶ 281
    }
4dee277c Andy McCall ↶ 282
    for s in subs {
283
        let state = match (&s.actual, s.dirty) {
284
            (None, _) => ui::warn(format!("{} missing", ui::glyph("⚠", "!"))),
285
            (Some(a), dirty) if *a == s.pin => {
286
                if dirty { ui::warn(format!("{} at pin, uncommitted changes", ui::glyph("✎", "~"))) } else { ui::ok(format!("{} at pin", ui::glyph("✔", "*"))) }
287
            }
288
            (Some(a), _) => ui::warn(format!("{} at {}", ui::glyph("↕", "<>"), a.summary())),
289
        };
290
        println!("{} {}{}{}{}{state}", ui::ok(ui::glyph("⧉", "#")), ui::path(&s.path), ui::sep(), ui::dim(s.pin.summary()), ui::sep());
291
        if let Some(u) = s.url {
292
            println!("    {} {}", ui::dim(ui::glyph("↳", "->")), ui::dim(u));
293
        }
294
    }
295
    Ok(0)
296
}
297
298
pub fn submodule_remove(path: &str) -> Result<i32> {
299
    let repo = open()?;
300
    repo.submodule_remove(path)?;
301
    println!("{} subrepo {} {}", ui::done("Removed"), ui::path(path), ui::dim("(directory left in place; commit to record the removal)"));
c3069537 Andy McCall ↶ 302
    Ok(0)
303
}
2863456a Andy McCall ↶ 304
305
pub fn deepen(remote: &str, depth: u32) -> Result<i32> {
306
    let repo = open()?;
307
    if !repo.is_shallow() {
308
        println!("{}", ui::dim("This repository is not shallow; nothing to deepen."));
309
        return Ok(0);
310
    }
311
    let sp = ui::spinner(&format!("Fetching {depth} more levels from {remote}…"));
312
    let n = repo.deepen(remote, depth)?;
313
    sp.done(&format!("{} {}; {}", ui::ok("Deepened by"), plural(n, "change"), if repo.is_shallow() { "still shallow".to_string() } else { ui::ok("history is now complete") }));
314
    Ok(0)
315
}