1use std::{fmt::Display, fs::create_dir_all, path::PathBuf, str::FromStr};
2
3mod auth;
4mod cli;
5mod client;
6mod keys;
7mod server;
8
9use anyhow::Context;
10
11pub use cli::{Args, ClientArgs};
12pub use client::Client;
13pub use server::Server;
14use tokio::io::{AsyncWrite, AsyncWriteExt};
15
16pub const GIT_IROH_ALPN: &[u8] = b"git/iroh/0";
18
19fn config_dir() -> Result<PathBuf, anyhow::Error> {
20 let config_dir = dirs::config_local_dir()
21 .unwrap_or(PathBuf::from(
22 std::env::var("HOME").context("HOME not set")?,
23 ))
24 .join("git-remote-iroh");
25 create_dir_all(&config_dir)?;
26 Ok(config_dir)
27}
28
29fn keys_dir(config_dir: &PathBuf) -> PathBuf {
30 config_dir.join("keys")
31}
32
33#[derive(Debug, Clone, Copy, PartialEq)]
35pub enum GitOp {
36 ClientFetchServerPush,
38 ClientPushServerFetch,
40}
41
42impl GitOp {
43 pub fn resource_id<S: Display>(self, resource: S) -> Vec<u8> {
48 format!(
49 "{}:{}",
50 match self {
51 GitOp::ClientFetchServerPush => "fetch",
52 GitOp::ClientPushServerFetch => "push",
53 },
54 resource
55 )
56 .as_bytes()
57 .to_vec()
58 }
59
60 pub async fn write_request<W: AsyncWrite + Unpin, S: Display>(
61 self,
62 w: &mut W,
63 resource: S,
64 ) -> Result<(), anyhow::Error> {
65 let resource_id = self.resource_id(resource);
66 w.write_u16_le(resource_id.len().try_into()?).await?;
67 w.write(resource_id.as_slice()).await?;
68 w.write_u8(match self {
69 GitOp::ClientFetchServerPush => 1u8,
71 GitOp::ClientPushServerFetch => 2u8,
73 })
74 .await?;
75 Ok(())
76 }
77
78 pub fn from_resource_id(resource_id: &[u8]) -> Result<GitOp, anyhow::Error> {
79 Ok(match resource_id.split(|c| *c == b':').next() {
80 Some(b"fetch") => GitOp::ClientFetchServerPush,
81 Some(b"push") => GitOp::ClientPushServerFetch,
82 _ => anyhow::bail!("invalid resource"),
83 })
84 }
85
86 pub fn to_byte(self) -> u8 {
87 match self {
88 GitOp::ClientFetchServerPush => 1,
89 GitOp::ClientPushServerFetch => 2,
90 }
91 }
92
93 pub fn from_byte(b: u8) -> Option<Self> {
94 match b {
95 1 => Some(GitOp::ClientFetchServerPush),
96 2 => Some(GitOp::ClientPushServerFetch),
97 _ => None,
98 }
99 }
100}
101
102impl FromStr for GitOp {
103 type Err = anyhow::Error;
104
105 fn from_str(s: &str) -> Result<Self, Self::Err> {
106 Ok(match s {
107 "git-upload-pack" => GitOp::ClientFetchServerPush,
108 "git-receive-pack" => GitOp::ClientPushServerFetch,
109 _ => anyhow::bail!("unsupported command {s}"),
110 })
111 }
112}
113
114impl Display for GitOp {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 GitOp::ClientPushServerFetch => f.write_str("git-receive-pack"),
118 GitOp::ClientFetchServerPush => f.write_str("git-upload-pack"),
119 }
120 }
121}
122
123fn parse_iroh_url(url: &str) -> Result<iroh::PublicKey, anyhow::Error> {
125 let node_id_str = url.strip_prefix("iroh://").ok_or(anyhow::anyhow!(
126 "invalid iroh URL (expected iroh://): {}",
127 url
128 ))?;
129
130 Ok(node_id_str.parse::<iroh::PublicKey>()?)
131}