upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-12 09:58:19 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-12 09:58:19 +0000
commitbc6be27ae6d1d2befe0027c19aba3f6cd2d22535 (patch)
treeff77150dff308f48b680787c57482634a1c3561d
parent8293af72744bc89beac57a194602938536b429db (diff)
feat: add ngit apply command for non-interactive patch application
Adds a new 'ngit apply <event-id>' 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.
-rw-r--r--src/bin/ngit/cli.rs8
-rw-r--r--src/bin/ngit/main.rs1
-rw-r--r--src/bin/ngit/sub_commands/apply.rs122
-rw-r--r--src/bin/ngit/sub_commands/mod.rs1
4 files changed, 132 insertions, 0 deletions
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 {
111 /// Proposal event-id (hex) or nevent (bech32) 111 /// Proposal event-id (hex) or nevent (bech32)
112 id: String, 112 id: String,
113 }, 113 },
114 /// apply proposal patches to current branch
115 Apply {
116 /// Proposal event-id or nevent
117 id: String,
118 /// Output patches to stdout instead of applying
119 #[arg(long)]
120 stdout: bool,
121 },
114 /// update repo git servers to reflect nostr state (add, update or delete 122 /// update repo git servers to reflect nostr state (add, update or delete
115 /// remote refs) 123 /// remote refs)
116 Sync(sub_commands::sync::SubCommandArgs), 124 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() {
54 Commands::Send(args) => sub_commands::send::launch(&cli, args, false).await, 54 Commands::Send(args) => sub_commands::send::launch(&cli, args, false).await,
55 Commands::Sync(args) => sub_commands::sync::launch(args).await, 55 Commands::Sync(args) => sub_commands::sync::launch(args).await,
56 Commands::Checkout { id } => sub_commands::checkout::launch(id).await, 56 Commands::Checkout { id } => sub_commands::checkout::launch(id).await,
57 Commands::Apply { id, stdout } => sub_commands::apply::launch(id, *stdout).await,
57 } 58 }
58 } else { 59 } else {
59 // Handle the case where no command is provided 60 // 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 @@
1use std::io::Write;
2
3use anyhow::{Context, Result, bail};
4use ngit::client::get_all_proposal_patch_pr_pr_update_events_from_cache;
5use ngit::git_events::get_pr_tip_event_or_most_recent_patch_with_ancestors;
6use nostr::nips::nip19::Nip19;
7use nostr_sdk::{EventId, FromBech32};
8
9use crate::client::{Client, Connect, get_repo_ref_from_cache};
10use crate::git::{Repo, RepoActions};
11use crate::repo_ref::get_repo_coordinates_when_remote_unknown;
12
13pub async fn launch(id: &str, stdout: bool) -> Result<()> {
14 let event_id = parse_event_id(id)?;
15
16 let git_repo = Repo::discover().context("failed to find a git repository")?;
17 let git_repo_path = git_repo.get_path()?;
18
19 let client = Client::new(ngit::client::Params::with_git_config_relay_defaults(&Some(
20 &git_repo,
21 )));
22
23 let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?;
24
25 crate::client::fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
26
27 let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
28
29 let proposals_and_revisions: Vec<nostr::Event> =
30 ngit::client::get_proposals_and_revisions_from_cache(
31 git_repo_path,
32 repo_ref.coordinates(),
33 )
34 .await?;
35
36 let proposal = proposals_and_revisions
37 .iter()
38 .find(|e| e.id == event_id)
39 .context(format!(
40 "proposal with id {} not found in cache",
41 event_id.to_hex()
42 ))?;
43
44 let commits_events: Vec<nostr::Event> = get_all_proposal_patch_pr_pr_update_events_from_cache(
45 git_repo_path,
46 &repo_ref,
47 &proposal.id,
48 )
49 .await?;
50
51 let patches = get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone())
52 .context("failed to find any PR or patch events on this proposal")?;
53
54 if patches
55 .iter()
56 .any(|e| [ngit::git_events::KIND_PULL_REQUEST, ngit::git_events::KIND_PULL_REQUEST_UPDATE].contains(&e.kind))
57 {
58 bail!(
59 "this proposal uses PR format (not patches). Use `ngit checkout {}` instead.",
60 event_id.to_hex()
61 );
62 }
63
64 if stdout {
65 output_patches_to_stdout(patches);
66 } else {
67 launch_git_am_with_patches(patches)?;
68 }
69
70 Ok(())
71}
72
73fn parse_event_id(id: &str) -> Result<EventId> {
74 if let Ok(nip19) = Nip19::from_bech32(id) {
75 match nip19 {
76 Nip19::Event(e) => return Ok(e.event_id),
77 Nip19::EventId(event_id) => return Ok(event_id),
78 _ => {}
79 }
80 }
81 if let Ok(event_id) = EventId::from_hex(id) {
82 return Ok(event_id);
83 }
84 bail!("invalid event-id or nevent: {id}")
85}
86
87fn output_patches_to_stdout(mut patches: Vec<nostr::Event>) {
88 patches.reverse();
89 for patch in patches {
90 print!("{}\n\n", patch.content);
91 }
92}
93
94fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> {
95 println!("applying to current branch with `git am`");
96 patches.reverse();
97
98 let mut am = std::process::Command::new("git")
99 .arg("am")
100 .stdin(std::process::Stdio::piped())
101 .stdout(std::process::Stdio::inherit())
102 .stderr(std::process::Stdio::inherit())
103 .spawn()
104 .context("failed to spawn git am")?;
105
106 let stdin = am
107 .stdin
108 .as_mut()
109 .context("git am process failed to take stdin")?;
110
111 for patch in patches {
112 stdin
113 .write(format!("{}\n\n", patch.content).as_bytes())
114 .context("failed to write patch content into git am stdin buffer")?;
115 }
116 stdin.flush()?;
117 let output = am
118 .wait_with_output()
119 .context("failed to read git am stdout")?;
120 print!("{:?}", output.stdout);
121 Ok(())
122}
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 @@
1pub mod apply;
1pub mod checkout; 2pub mod checkout;
2pub mod create; 3pub mod create;
3pub mod export_keys; 4pub mod export_keys;