#![cfg_attr(not(test), warn(clippy::pedantic))] #![allow(clippy::large_futures, clippy::module_name_repetitions)] // 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, path::{Path, PathBuf}, }; use anyhow::{Context, Result, bail}; use client::{Connect, consolidate_fetch_reports, get_repo_ref_from_cache, is_verbose}; use git::{RepoActions, nostr_url::NostrUrlDecoded}; use ngit::{ client::{self, Params}, git::{self, utils::set_git_timeout}, login::existing::load_existing_login, utils::read_line, }; use nostr::nips::nip19::Nip19Coordinate; use crate::{client::Client, git::Repo}; #[derive(Default, Clone)] struct PushOptions { title: Option, description: Option, } mod fetch; mod list; mod push; #[tokio::main] async fn main() -> Result<()> { if std::env::var("NGITTEST").is_ok() { std::env::set_var("NGIT_VERBOSE", "1"); } let Some((decoded_nostr_url, git_repo)) = process_args().await? else { return Ok(()); }; let git_repo_path = git_repo.get_path()?; let mut client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo))); if let Ok((signer, _, _)) = load_existing_login( &Some(&git_repo), &None, &None, &None, None, true, false, false, ) .await { // signer for to respond to relay auth request client.set_signer(signer).await; } fetching_with_report_for_helper(git_repo_path, &client, &decoded_nostr_url.coordinate).await?; let mut repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &decoded_nostr_url.coordinate).await?; repo_ref.set_nostr_git_url(decoded_nostr_url.clone()); let _ = set_git_timeout(); let stdin = io::stdin(); let mut line = String::new(); let mut list_outputs = None; let mut push_options: PushOptions = PushOptions::default(); loop { let tokens = read_line(&stdin, &mut line)?; match tokens.as_slice() { ["capabilities"] => { println!("option"); println!("push"); println!("fetch"); println!("push-options"); println!(); } ["option", "verbosity"] => { println!("ok"); } ["option", "push-option", rest @ ..] => { let option = rest.join(" "); if let Some((key, value)) = option.split_once('=') { match key { "title" => push_options.title = Some(value.to_string()), "description" => push_options.description = Some(value.to_string()), _ => {} } } println!("ok"); } ["option", ..] => { println!("unsupported"); } ["fetch", oid, refstr] => { fetch::run_fetch(&git_repo, &repo_ref, &stdin, oid, refstr).await?; } ["push", refspec] => { push::run_push( &git_repo, &repo_ref, &stdin, refspec, &client, list_outputs.clone(), push_options.title.as_ref().zip(push_options.description.as_ref()).map(|(t, d)| (t.clone(), d.clone())), ) .await?; push_options = PushOptions::default(); } ["list"] => { list_outputs = Some(list::run_list(&git_repo, &repo_ref, false).await?); } ["list", "for-push"] => { list_outputs = Some(list::run_list(&git_repo, &repo_ref, true).await?); } [] => { return Ok(()); } _ => { bail!(format!("unknown command: {}", line.trim().to_owned())); } } } } async fn process_args() -> Result> { let args = env::args(); let args = args.skip(1).take(2).collect::>(); if env::args().nth(1).as_deref() == Some("--version") { const VERSION: &str = env!("CARGO_PKG_VERSION"); println!("v{VERSION}"); return Ok(None); } let ([_, nostr_remote_url] | [nostr_remote_url]) = args.as_slice() else { println!("nostr plugin for git"); println!("Usage:"); println!( " - clone a nostr repository, or add as a remote, by using the url format nostr://pub123/identifier" ); println!( " - remote branches beginning with `pr/` are open PRs from contributors; `ngit list` can be used to view all PRs" ); println!( " - to open a PR, push a branch with the prefix `pr/` or use `ngit send` for advanced options" ); println!("- publish a repository to nostr with `ngit init`"); return Ok(None); }; 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 decoded_nostr_url = NostrUrlDecoded::parse_and_resolve(nostr_remote_url, &Some(&git_repo)) .await .context("invalid nostr url")?; Ok(Some((decoded_nostr_url, git_repo))) } async fn fetching_with_report_for_helper( git_repo_path: &Path, client: &Client, trusted_maintainer_coordinate: &Nip19Coordinate, ) -> Result<()> { let term = console::Term::stderr(); let verbose = is_verbose(); if verbose { term.write_line("nostr: fetching...")?; } let (relay_reports, progress_reporter) = client .fetch_all( Some(git_repo_path), Some(trusted_maintainer_coordinate), &HashSet::new(), ) .await?; let has_errors = relay_reports.iter().any(std::result::Result::is_err); if !has_errors || !verbose { let _ = progress_reporter.clear(); } let report = consolidate_fetch_reports(relay_reports); if report.to_string().is_empty() { if verbose { term.write_line("nostr: no updates")?; } } else { term.write_line(&format!("nostr updates: {report}"))?; } Ok(()) }