OXYGENHUB
# 6d3eefbce2f0 oxygen / src / cli / sync.rs
6.7 KiB · Rust · last changed Lazy blobs: clone --max-blob / filter.max-blob, absent-blob registry fetched on first read, objects endpoint (spec 0.6e) 1 month ago source blame history raw
1
//! Commands that talk to remotes, and the reference server.
2
3
use super::format::plural;
4
use super::open;
5
use super::ui;
6
use anyhow::{Context, Result};
7
use oxy_core::repo::Repo;
8
use std::path::PathBuf;
9
10
pub fn clone(url: &str, dest: Option<PathBuf>, max_blob: Option<&str>, sparse: &[String], depth: Option<u32>) -> Result<i32> {
11
    let dest = dest.unwrap_or_else(|| {
12
        let base = url.trim_end_matches('/').rsplit('/').next().unwrap_or("repo");
13
        PathBuf::from(base)
14
    });
15
    let max_blob = max_blob.map(oxy_core::repo::parse_size).transpose()?;
16
    let sp = ui::spinner(&format!("Cloning {url}…"));
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
    };
21
    let heads = repo.bookmark_heads()?;
22
    sp.done(&format!("{} into {} ({} on {}: {})", ui::ok("Cloned"), ui::path(repo.root().display()), url, ui::id(&repo.config.bookmark), plural(heads.len(), "head")));
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()?;
30
    println!("Added remote {name} = {url}");
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()?;
38
    println!("Removed remote {name}");
39
    Ok(0)
40
}
41
42
pub fn remote_list() -> Result<i32> {
43
    let repo = open()?;
44
    let view = repo.view()?;
45
    for (name, url) in &repo.config.remotes {
46
        println!("{name}\t{url}");
47
        if let Some(bms) = view.remotes.get(name) {
48
            for (bm, st) in bms {
49
                let heads = repo.store().get_state(st).map(|s| s.heads.len()).unwrap_or(0);
50
                println!("    {bm}: {}", plural(heads, "head"));
51
            }
52
        }
53
    }
54
    Ok(0)
55
}
56
57
fn remote_name(remote: Option<&str>) -> String {
58
    remote.unwrap_or("origin").to_string()
59
}
60
61
pub fn fetch(remote: Option<&str>) -> Result<i32> {
62
    let repo = open()?;
63
    let remote = remote_name(remote);
64
    let sp = ui::spinner(&format!("Fetching from {remote}…"));
65
    let report = repo.fetch(&remote)?;
66
    sp.clear();
67
    for (name, heads, n) in &report.refs {
68
        println!("{} {}, {}", ui::id(format!("{remote}/{name}")), plural(heads.len(), "head"), plural(*n, "new change"));
69
    }
70
    if report.new_tags > 0 {
71
        println!("{}", ui::dim(format!("{} adopted", plural(report.new_tags, "tag"))));
72
    }
73
    if report.unverified {
74
        eprintln!("{} {remote} has no trust policy; nothing fetched from it was verified", ui::warn("warning:"));
75
    }
76
    Ok(0)
77
}
78
79
pub fn pull(remote: Option<&str>) -> Result<i32> {
80
    let repo = open()?;
81
    let remote = remote_name(remote);
82
    let sp = ui::spinner(&format!("Pulling from {remote}…"));
83
    let r = repo.pull(&remote)?;
84
    if !r.changed {
85
        sp.done(&ui::dim("Already up to date."));
86
        return Ok(0);
87
    }
88
    sp.done(&format!(
89
        "{} {} now has {}{}",
90
        ui::ok("Pulled:"),
91
        ui::id(&repo.config.bookmark),
92
        plural(r.heads.len(), "head"),
93
        if r.checked_out { "; working copy updated" } else { "; run `oxy checkout` to update the working copy" }
94
    ));
95
    let snap = repo.snapshot("oxy pull")?;
96
    if snap.base_snapshot.is_conflicted() {
97
        println!("  {}", ui::warn("the result has conflicts; see oxy status"));
98
    }
99
    Ok(0)
100
}
101
102
pub fn push(remote: Option<&str>, force: bool) -> Result<i32> {
103
    let repo = open()?;
104
    let remote = remote_name(remote);
105
    let sp = ui::spinner(&format!("Pushing to {remote}…"));
106
    let results = repo.push(&remote, force)?;
107
    sp.clear();
108
    let mut code = 0;
109
    for r in results {
110
        if r.ok {
111
            println!("{} {}", ui::id(format!("{remote}/{}", r.name)), ui::ok(if r.reason.is_empty() { "updated" } else { r.reason.as_str() }));
112
        } else {
113
            println!("{} {} {}", ui::id(format!("{remote}/{}", r.name)), ui::err("rejected:"), r.reason);
114
            code = 1;
115
        }
116
    }
117
    Ok(code)
118
}
119
120
pub fn serve(root: PathBuf, bind: &str, tokens: Vec<String>, tls: Option<(PathBuf, PathBuf)>) -> Result<i32> {
121
    let listener = std::net::TcpListener::bind(bind).with_context(|| format!("binding {bind}"))?;
122
    let root = root.canonicalize()?;
123
    let tls = tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key });
124
    println!(
125
        "Serving repositories under {} on {}://{bind}/<repo>/oxy/v1/ ({})",
126
        root.display(),
127
        if tls.is_some() { "https" } else { "http" },
128
        if tokens.is_empty() { "open: no push tokens configured" } else { "push requires a token" }
129
    );
130
    if tls.is_none() && !bind.starts_with("127.") && !bind.starts_with("localhost") && !bind.starts_with("[::1]") {
131
        eprintln!("warning: serving plain HTTP on a non-loopback address; put a TLS terminator in front or pass --tls-cert/--tls-key");
132
    }
133
    oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens, tls })?;
134
    Ok(0)
135
}
136
137
pub fn git_export(git_dir: &std::path::Path) -> Result<i32> {
138
    let repo = open()?;
139
    let sp = ui::spinner("Exporting to Git…");
140
    let r = repo.git_export(git_dir)?;
141
    sp.clear();
142
    println!(
143
        "Exported {} to {} (branch {}: {}{}); {} already exported.",
144
        plural(r.exported, "change"),
145
        git_dir.display(),
146
        r.branch,
147
        plural(r.heads, "head"),
148
        if r.heads > 1 { ", joined by a synthetic merge commit" } else { "" },
149
        r.skipped
150
    );
151
    Ok(0)
152
}
153
154
pub fn git_import(git_dir: &std::path::Path, dest: &std::path::Path, branch: &str) -> Result<i32> {
155
    let repo = Repo::init(dest)?;
156
    let started = std::time::Instant::now();
157
    let sp = ui::spinner(&format!("Importing {} {branch}…", git_dir.display()));
158
    let r = repo.import_git(git_dir, branch)?;
159
    sp.clear();
160
    println!(
161
        "Imported {} ({} merges) from {} {branch} into {} in {:.1?}: {} now has {}.{}",
162
        plural(r.commits, "commit"),
163
        r.merges,
164
        git_dir.display(),
165
        repo.root().display(),
166
        started.elapsed(),
167
        repo.config.bookmark,
168
        plural(r.heads, "head"),
169
        if r.skipped_submodules > 0 { format!(" Skipped {} submodule entries.", r.skipped_submodules) } else { String::new() }
170
    );
171
    Ok(0)
172
}
173
174
pub fn deepen(remote: &str, depth: u32) -> Result<i32> {
175
    let repo = open()?;
176
    if !repo.is_shallow() {
177
        println!("{}", ui::dim("This repository is not shallow; nothing to deepen."));
178
        return Ok(0);
179
    }
180
    let sp = ui::spinner(&format!("Fetching {depth} more levels from {remote}…"));
181
    let n = repo.deepen(remote, depth)?;
182
    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") }));
183
    Ok(0)
184
}