From bc6be27ae6d1d2befe0027c19aba3f6cd2d22535 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 12 Feb 2026 09:58:19 +0000 Subject: feat: add ngit apply command for non-interactive patch application Adds a new 'ngit apply ' command that applies proposal patches to the current branch. Supports --stdout flag to output patches for piping to git am. Phase 2 of non-interactive ngit list implementation. --- src/bin/ngit/cli.rs | 8 +++ src/bin/ngit/main.rs | 1 + src/bin/ngit/sub_commands/apply.rs | 122 +++++++++++++++++++++++++++++++++++++ src/bin/ngit/sub_commands/mod.rs | 1 + 4 files changed, 132 insertions(+) create mode 100644 src/bin/ngit/sub_commands/apply.rs diff --git a/src/bin/ngit/cli.rs b/src/bin/ngit/cli.rs index 5c1a097..8f55274 100644 --- a/src/bin/ngit/cli.rs +++ b/src/bin/ngit/cli.rs @@ -111,6 +111,14 @@ pub enum Commands { /// Proposal event-id (hex) or nevent (bech32) id: String, }, + /// apply proposal patches to current branch + Apply { + /// Proposal event-id or nevent + id: String, + /// Output patches to stdout instead of applying + #[arg(long)] + stdout: bool, + }, /// update repo git servers to reflect nostr state (add, update or delete /// remote refs) Sync(sub_commands::sync::SubCommandArgs), diff --git a/src/bin/ngit/main.rs b/src/bin/ngit/main.rs index 2c9e10f..ab57f89 100644 --- a/src/bin/ngit/main.rs +++ b/src/bin/ngit/main.rs @@ -54,6 +54,7 @@ async fn main() { Commands::Send(args) => sub_commands::send::launch(&cli, args, false).await, Commands::Sync(args) => sub_commands::sync::launch(args).await, Commands::Checkout { id } => sub_commands::checkout::launch(id).await, + Commands::Apply { id, stdout } => sub_commands::apply::launch(id, *stdout).await, } } else { // Handle the case where no command is provided diff --git a/src/bin/ngit/sub_commands/apply.rs b/src/bin/ngit/sub_commands/apply.rs new file mode 100644 index 0000000..d32cd4f --- /dev/null +++ b/src/bin/ngit/sub_commands/apply.rs @@ -0,0 +1,122 @@ +use std::io::Write; + +use anyhow::{Context, Result, bail}; +use ngit::client::get_all_proposal_patch_pr_pr_update_events_from_cache; +use ngit::git_events::get_pr_tip_event_or_most_recent_patch_with_ancestors; +use nostr::nips::nip19::Nip19; +use nostr_sdk::{EventId, FromBech32}; + +use crate::client::{Client, Connect, get_repo_ref_from_cache}; +use crate::git::{Repo, RepoActions}; +use crate::repo_ref::get_repo_coordinates_when_remote_unknown; + +pub async fn launch(id: &str, stdout: bool) -> Result<()> { + let event_id = parse_event_id(id)?; + + let git_repo = Repo::discover().context("failed to find a git repository")?; + let git_repo_path = git_repo.get_path()?; + + let client = Client::new(ngit::client::Params::with_git_config_relay_defaults(&Some( + &git_repo, + ))); + + let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?; + + crate::client::fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; + + let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?; + + let proposals_and_revisions: Vec = + ngit::client::get_proposals_and_revisions_from_cache( + git_repo_path, + repo_ref.coordinates(), + ) + .await?; + + let proposal = proposals_and_revisions + .iter() + .find(|e| e.id == event_id) + .context(format!( + "proposal with id {} not found in cache", + event_id.to_hex() + ))?; + + let commits_events: Vec = get_all_proposal_patch_pr_pr_update_events_from_cache( + git_repo_path, + &repo_ref, + &proposal.id, + ) + .await?; + + let patches = get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone()) + .context("failed to find any PR or patch events on this proposal")?; + + if patches + .iter() + .any(|e| [ngit::git_events::KIND_PULL_REQUEST, ngit::git_events::KIND_PULL_REQUEST_UPDATE].contains(&e.kind)) + { + bail!( + "this proposal uses PR format (not patches). Use `ngit checkout {}` instead.", + event_id.to_hex() + ); + } + + if stdout { + output_patches_to_stdout(patches); + } else { + launch_git_am_with_patches(patches)?; + } + + Ok(()) +} + +fn parse_event_id(id: &str) -> Result { + if let Ok(nip19) = Nip19::from_bech32(id) { + match nip19 { + Nip19::Event(e) => return Ok(e.event_id), + Nip19::EventId(event_id) => return Ok(event_id), + _ => {} + } + } + if let Ok(event_id) = EventId::from_hex(id) { + return Ok(event_id); + } + bail!("invalid event-id or nevent: {id}") +} + +fn output_patches_to_stdout(mut patches: Vec) { + patches.reverse(); + for patch in patches { + print!("{}\n\n", patch.content); + } +} + +fn launch_git_am_with_patches(mut patches: Vec) -> Result<()> { + println!("applying to current branch with `git am`"); + patches.reverse(); + + let mut am = std::process::Command::new("git") + .arg("am") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .spawn() + .context("failed to spawn git am")?; + + let stdin = am + .stdin + .as_mut() + .context("git am process failed to take stdin")?; + + for patch in patches { + stdin + .write(format!("{}\n\n", patch.content).as_bytes()) + .context("failed to write patch content into git am stdin buffer")?; + } + stdin.flush()?; + let output = am + .wait_with_output() + .context("failed to read git am stdout")?; + print!("{:?}", output.stdout); + Ok(()) +} diff --git a/src/bin/ngit/sub_commands/mod.rs b/src/bin/ngit/sub_commands/mod.rs index e9f91db..6b94248 100644 --- a/src/bin/ngit/sub_commands/mod.rs +++ b/src/bin/ngit/sub_commands/mod.rs @@ -1,3 +1,4 @@ +pub mod apply; pub mod checkout; pub mod create; pub mod export_keys; -- cgit v1.2.3