From 0134ab8eb413b8b81ec8e179897ddb8ea63e134e Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 26 Jul 2024 10:29:33 +0100 Subject: feat(remote): add nostr git remote helper as a simple proxy to the first git server listed in announcement parse clone url as `nostr://naddr123...` --- src/cli.rs | 44 +++++++++++++ src/git.rs | 2 +- src/git_remote_helper.rs | 156 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 45 +------------ src/sub_commands/fetch.rs | 2 +- src/sub_commands/init.rs | 2 +- src/sub_commands/login.rs | 2 +- src/sub_commands/push.rs | 2 +- src/sub_commands/send.rs | 2 +- 9 files changed, 209 insertions(+), 48 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/git_remote_helper.rs (limited to 'src') diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..d0f934e --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,44 @@ +use clap::{Parser, Subcommand}; + +use crate::sub_commands; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, + /// remote signer address + #[arg(long, global = true)] + pub bunker_uri: Option, + /// remote signer app secret key + #[arg(long, global = true)] + pub bunker_app_key: Option, + /// nsec or hex private key + #[arg(short, long, global = true)] + pub nsec: Option, + /// password to decrypt nsec + #[arg(short, long, global = true)] + pub password: Option, + /// disable spinner animations + #[arg(long, action)] + pub disable_cli_spinners: bool, +} + +#[derive(Subcommand)] +pub enum Commands { + /// update cache with latest updates from nostr + Fetch(sub_commands::fetch::SubCommandArgs), + /// signal you are this repo's maintainer accepting proposals via nostr + Init(sub_commands::init::SubCommandArgs), + /// issue commits as a proposal + Send(sub_commands::send::SubCommandArgs), + /// list proposals; checkout, apply or download selected + List, + /// send proposal revision + Push(sub_commands::push::SubCommandArgs), + /// fetch and apply new proposal commits / revisions linked to branch + Pull, + /// run with --nsec flag to change npub + Login(sub_commands::login::SubCommandArgs), +} diff --git a/src/git.rs b/src/git.rs index c13b46d..eaea512 100644 --- a/src/git.rs +++ b/src/git.rs @@ -10,7 +10,7 @@ use nostr_sdk::hashes::{sha1::Hash as Sha1Hash, Hash}; use crate::sub_commands::list::{get_commit_id_from_patch, tag_value}; pub struct Repo { - git_repo: git2::Repository, + pub git_repo: git2::Repository, } impl Repo { diff --git a/src/git_remote_helper.rs b/src/git_remote_helper.rs new file mode 100644 index 0000000..6050d1a --- /dev/null +++ b/src/git_remote_helper.rs @@ -0,0 +1,156 @@ +#![cfg_attr(not(test), warn(clippy::pedantic))] +#![allow(clippy::large_futures)] +// better solution to dead_code error on multiple binaries than https://stackoverflow.com/a/66196291 +#![allow(dead_code)] +#![cfg_attr(not(test), warn(clippy::expect_used))] + +use core::str; +use std::{ + collections::HashSet, + env, + io::{self}, + path::PathBuf, +}; + +use anyhow::{bail, Context, Result}; +#[cfg(not(test))] +use client::Connect; +use client::{fetching_with_report, get_repo_ref_from_cache}; +use git::RepoActions; +use nostr::nips::nip01::Coordinate; +use nostr_sdk::Url; + +#[cfg(not(test))] +use crate::client::Client; +#[cfg(test)] +use crate::client::MockConnect; +use crate::git::Repo; + +mod cli; +mod cli_interactor; +mod client; +mod config; +mod git; +mod key_handling; +mod login; +mod repo_ref; +mod sub_commands; + +#[tokio::main] +async fn main() -> Result<()> { + let args = env::args(); + let args = args.skip(1).take(2).collect::>(); + + let ([_, url] | [url]) = args.as_slice() else { + bail!("invalid arguments - no url"); + }; + if env::args().nth(1).as_deref() == Some("--version") { + println!("v0.0.1"); + } + + let git_repo = Repo::from_path(&PathBuf::from( + std::env::var("GIT_DIR").context("git should set GIT_DIR when remote helper is called")?, + ))?; + let git_repo_path = git_repo.get_path()?; + + #[cfg(not(test))] + let client = Client::default(); + #[cfg(test)] + let client = ::default(); + + let repo_coordinates = nostr_git_url_to_repo_coordinates(url).context("invalid nostr url")?; + + fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; + + let repo_ref = get_repo_ref_from_cache(git_repo_path, &repo_coordinates).await?; + + let stdin = io::stdin(); + let mut line = String::new(); + + let temp_remote_url = repo_ref + .git_server + .first() + .context("no git server listed in nostr repository announcement")?; + + let mut temp_remote = git_repo.git_repo.remote_anonymous(temp_remote_url)?; + + loop { + let tokens = read_line(&stdin, &mut line)?; + + match tokens.as_slice() { + ["capabilities"] => { + println!("option"); + println!("push"); + println!("fetch"); + println!(); + } + ["option", "verbosity"] => { + println!("ok"); + } + ["option", ..] => { + println!("unsupported"); + } + ["fetch", _oid, refstr] => { + temp_remote.connect(git2::Direction::Fetch)?; + temp_remote.download(&[refstr], None)?; + temp_remote.disconnect()?; + println!(); + } + ["push", refspec] => { + temp_remote.connect(git2::Direction::Push)?; + temp_remote.push(&[refspec], None)?; + temp_remote.disconnect()?; + println!(); + } + ["list"] => { + temp_remote.connect(git2::Direction::Fetch)?; + for head in temp_remote.list()? { + println!("{} {}", head.oid(), head.name()); + } + temp_remote.disconnect()?; + println!(); + } + ["list", "for-push"] => { + temp_remote.connect(git2::Direction::Fetch)?; + for head in temp_remote.list()? { + if head.name() != "HEAD" { + println!("{} {}", head.oid(), head.name()); + } + } + temp_remote.disconnect()?; + println!(); + } + [] => { + return Ok(()); + } + _ => { + bail!(format!("unknown command: {}", line.trim().to_owned())); + } + } + } +} + +/// Read one line from stdin, and split it into tokens. +pub(crate) fn read_line<'a>(stdin: &io::Stdin, line: &'a mut String) -> io::Result> { + line.clear(); + + let read = stdin.read_line(line)?; + if read == 0 { + return Ok(vec![]); + } + let line = line.trim(); + let tokens = line.split(' ').filter(|t| !t.is_empty()).collect(); + + Ok(tokens) +} + +fn nostr_git_url_to_repo_coordinates(url: &str) -> Result> { + let mut repo_coordinattes = HashSet::new(); + let coordinate = Coordinate::parse(Url::parse(url)?.domain().context("no naddr")?)?; + if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) { + repo_coordinattes.insert(coordinate); + } else { + bail!("naddr doesnt point to a git repository announcement"); + } + Ok(repo_coordinattes) +} diff --git a/src/main.rs b/src/main.rs index 81eaf2f..add26f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,10 @@ #![cfg_attr(not(test), warn(clippy::expect_used))] use anyhow::Result; -use clap::{Parser, Subcommand}; +use clap::Parser; +use cli::{Cli, Commands}; +mod cli; mod cli_interactor; mod client; mod config; @@ -14,47 +16,6 @@ mod login; mod repo_ref; mod sub_commands; -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -#[command(propagate_version = true)] -pub struct Cli { - #[command(subcommand)] - command: Commands, - /// remote signer address - #[arg(long, global = true)] - bunker_uri: Option, - /// remote signer app secret key - #[arg(long, global = true)] - bunker_app_key: Option, - /// nsec or hex private key - #[arg(short, long, global = true)] - nsec: Option, - /// password to decrypt nsec - #[arg(short, long, global = true)] - password: Option, - /// disable spinner animations - #[arg(long, action)] - disable_cli_spinners: bool, -} - -#[derive(Subcommand)] -enum Commands { - /// update cache with latest updates from nostr - Fetch(sub_commands::fetch::SubCommandArgs), - /// signal you are this repo's maintainer accepting proposals via nostr - Init(sub_commands::init::SubCommandArgs), - /// issue commits as a proposal - Send(sub_commands::send::SubCommandArgs), - /// list proposals; checkout, apply or download selected - List, - /// send proposal revision - Push(sub_commands::push::SubCommandArgs), - /// fetch and apply new proposal commits / revisions linked to branch - Pull, - /// run with --nsec flag to change npub - Login(sub_commands::login::SubCommandArgs), -} - #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); diff --git a/src/sub_commands/fetch.rs b/src/sub_commands/fetch.rs index ab6e0fc..b1e83c5 100644 --- a/src/sub_commands/fetch.rs +++ b/src/sub_commands/fetch.rs @@ -9,10 +9,10 @@ use crate::client::Client; #[cfg(test)] use crate::client::MockConnect; use crate::{ + cli::Cli, client::{fetching_with_report, Connect}, git::{Repo, RepoActions}, repo_ref::get_repo_coordinates, - Cli, }; #[derive(clap::Args)] diff --git a/src/sub_commands/init.rs b/src/sub_commands/init.rs index 28ba21b..ba188c9 100644 --- a/src/sub_commands/init.rs +++ b/src/sub_commands/init.rs @@ -10,6 +10,7 @@ use crate::client::Client; #[cfg(test)] use crate::client::MockConnect; use crate::{ + cli::Cli, cli_interactor::{Interactor, InteractorPrompt, PromptInputParms}, client::{fetching_with_report, get_repo_ref_from_cache, Connect}, git::{Repo, RepoActions}, @@ -18,7 +19,6 @@ use crate::{ extract_pks, get_repo_config_from_yaml, save_repo_config_to_yaml, try_and_get_repo_coordinates, RepoRef, }, - Cli, }; #[derive(Debug, clap::Args)] diff --git a/src/sub_commands/login.rs b/src/sub_commands/login.rs index 6f49ba8..77fecdd 100644 --- a/src/sub_commands/login.rs +++ b/src/sub_commands/login.rs @@ -5,7 +5,7 @@ use clap; use crate::client::Client; #[cfg(test)] use crate::client::MockConnect; -use crate::{client::Connect, git::Repo, login, Cli}; +use crate::{cli::Cli, client::Connect, git::Repo, login}; #[derive(clap::Args)] pub struct SubCommandArgs { diff --git a/src/sub_commands/push.rs b/src/sub_commands/push.rs index acd91f0..56927fe 100644 --- a/src/sub_commands/push.rs +++ b/src/sub_commands/push.rs @@ -5,6 +5,7 @@ use crate::client::Client; #[cfg(test)] use crate::client::MockConnect; use crate::{ + cli::Cli, client::{fetching_with_report, get_repo_ref_from_cache, Connect}, git::{str_to_sha1, Repo, RepoActions}, login, @@ -21,7 +22,6 @@ use crate::{ identify_ahead_behind, send_events, }, }, - Cli, }; #[derive(Debug, clap::Args)] diff --git a/src/sub_commands/send.rs b/src/sub_commands/send.rs index 73c980b..07eb343 100644 --- a/src/sub_commands/send.rs +++ b/src/sub_commands/send.rs @@ -20,6 +20,7 @@ use crate::client::Client; #[cfg(test)] use crate::client::MockConnect; use crate::{ + cli::Cli, cli_interactor::{ Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptMultiChoiceParms, }, @@ -29,7 +30,6 @@ use crate::{ git::{Repo, RepoActions}, login, repo_ref::{get_repo_coordinates, RepoRef}, - Cli, }; #[derive(Debug, clap::Args)] -- cgit v1.2.3