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}