Skip to main content

rns_git/
clone_cli.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use crate::client::{decode_status, SyncClient};
5use crate::config::ClientConfig;
6use crate::logging;
7use crate::protocol;
8use crate::util::{
9    default_reticulum_dir, default_rngit_dir, parse_rns_url_with_aliases, resolve_rns_url_aliases,
10};
11use crate::{Error, Result};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CloneCommand {
15    Fork,
16    Mirror,
17}
18
19impl CloneCommand {
20    fn path(self) -> &'static str {
21        match self {
22            CloneCommand::Fork => protocol::PATH_FORK,
23            CloneCommand::Mirror => protocol::PATH_MIRROR,
24        }
25    }
26
27    fn name(self) -> &'static str {
28        match self {
29            CloneCommand::Fork => "fork",
30            CloneCommand::Mirror => "mirror",
31        }
32    }
33}
34
35pub fn main<I>(command: CloneCommand, args: I) -> Result<()>
36where
37    I: IntoIterator<Item = String>,
38{
39    let options = CloneOptions::parse(args)?;
40    let rngit_dir = options.config_dir.unwrap_or_else(default_rngit_dir);
41    let rns_dir = options.rns_config_dir.or_else(default_reticulum_dir);
42    let mut config = ClientConfig::load_or_create_for_run(rngit_dir, rns_dir)?;
43    logging::init_file_logger(&config.dir.join("client_log"), config.log_level)?;
44    let (dest_hash, repository) =
45        parse_rns_url_with_aliases(&options.target, &config.destination_aliases)?;
46    if let Some(identity_path) = options.identity_path {
47        config.identity_path = identity_path;
48    }
49
50    let source = resolve_rns_url_aliases(&options.source, &config.destination_aliases)?;
51    let client = SyncClient::connect(config, dest_hash)?;
52    let response = client.request_with_timeout(
53        command.path(),
54        protocol::remote_clone_request(&repository, &source),
55        Duration::from_secs(7200),
56    )?;
57    let bytes = protocol::response_bin(&response.data)?;
58    decode_status(bytes)?;
59    println!("Repository {}ed to {repository}", command.name());
60    Ok(())
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64struct CloneOptions {
65    config_dir: Option<PathBuf>,
66    rns_config_dir: Option<PathBuf>,
67    identity_path: Option<PathBuf>,
68    source: String,
69    target: String,
70}
71
72impl CloneOptions {
73    fn parse<I>(args: I) -> Result<Self>
74    where
75        I: IntoIterator<Item = String>,
76    {
77        let mut config_dir = None;
78        let mut rns_config_dir = None;
79        let mut identity_path = None;
80        let mut positional = Vec::new();
81        let mut iter = args.into_iter();
82        while let Some(arg) = iter.next() {
83            match arg.as_str() {
84                "--config" => config_dir = Some(next_path(&mut iter, "--config")?),
85                "--rnsconfig" => rns_config_dir = Some(next_path(&mut iter, "--rnsconfig")?),
86                "-i" | "--identity" => identity_path = Some(next_path(&mut iter, "--identity")?),
87                "-h" | "--help" => return Err(Error::msg(usage())),
88                other if other.starts_with('-') => {
89                    return Err(Error::msg(format!("unknown clone option {other}")));
90                }
91                _ => positional.push(arg),
92            }
93        }
94        if positional.len() != 2 {
95            return Err(Error::msg(usage()));
96        }
97        Ok(Self {
98            config_dir,
99            rns_config_dir,
100            identity_path,
101            source: positional.remove(0),
102            target: positional.remove(0),
103        })
104    }
105}
106
107fn next_path(iter: &mut impl Iterator<Item = String>, flag: &str) -> Result<PathBuf> {
108    iter.next()
109        .map(PathBuf::from)
110        .ok_or_else(|| Error::msg(format!("{flag} requires a value")))
111}
112
113fn usage() -> &'static str {
114    "usage: rngit <fork|mirror> [--config DIR] [--rnsconfig DIR] [-i|--identity PATH] <source-url> <rns://destination/group/repo>"
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn parses_clone_options() {
123        let opts = CloneOptions::parse([
124            "--config".into(),
125            "cfg".into(),
126            "--rnsconfig".into(),
127            "rns".into(),
128            "-i".into(),
129            "id".into(),
130            "https://example.invalid/repo.git".into(),
131            "rns://00112233445566778899aabbccddeeff/group/repo".into(),
132        ])
133        .unwrap();
134        assert_eq!(opts.config_dir, Some(PathBuf::from("cfg")));
135        assert_eq!(opts.rns_config_dir, Some(PathBuf::from("rns")));
136        assert_eq!(opts.identity_path, Some(PathBuf::from("id")));
137        assert_eq!(opts.source, "https://example.invalid/repo.git");
138        assert_eq!(
139            opts.target,
140            "rns://00112233445566778899aabbccddeeff/group/repo"
141        );
142    }
143
144    #[test]
145    fn rejects_wrong_clone_positional_count() {
146        assert!(CloneOptions::parse(["source".into()]).is_err());
147    }
148}