upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/apply.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/ngit/sub_commands/apply.rs')
-rw-r--r--src/bin/ngit/sub_commands/apply.rs169
1 files changed, 146 insertions, 23 deletions
diff --git a/src/bin/ngit/sub_commands/apply.rs b/src/bin/ngit/sub_commands/apply.rs
index 4b13975..3700c37 100644
--- a/src/bin/ngit/sub_commands/apply.rs
+++ b/src/bin/ngit/sub_commands/apply.rs
@@ -1,4 +1,5 @@
1use std::{ 1use std::{
2 collections::HashSet,
2 io::Write, 3 io::Write,
3 process::{Command, Stdio}, 4 process::{Command, Stdio},
4 time::Duration, 5 time::Duration,
@@ -8,7 +9,13 @@ use anyhow::{Context, Result, bail};
8use indicatif::{ProgressBar, ProgressStyle}; 9use indicatif::{ProgressBar, ProgressStyle};
9use ngit::{ 10use ngit::{
10 client::get_all_proposal_patch_pr_pr_update_events_from_cache, 11 client::get_all_proposal_patch_pr_pr_update_events_from_cache,
11 git_events::get_pr_tip_event_or_most_recent_patch_with_ancestors, 12 fetch::fetch_from_git_server,
13 git::str_to_sha1,
14 git_events::{
15 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE,
16 get_pr_tip_event_or_most_recent_patch_with_ancestors, tag_value,
17 },
18 repo_ref::{RepoRef, is_grasp_server_in_list},
12}; 19};
13use nostr::nips::nip19::Nip19; 20use nostr::nips::nip19::Nip19;
14use nostr_sdk::{EventId, FromBech32}; 21use nostr_sdk::{EventId, FromBech32};
@@ -123,17 +130,15 @@ pub async fn launch(id: &str, stdout: bool, offline: bool) -> Result<()> {
123 let patches = get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone()) 130 let patches = get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone())
124 .context("failed to find any PR or patch events on this proposal")?; 131 .context("failed to find any PR or patch events on this proposal")?;
125 132
126 if patches.iter().any(|e| { 133 if patches
127 [ 134 .iter()
128 ngit::git_events::KIND_PULL_REQUEST, 135 .any(|e| [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&e.kind))
129 ngit::git_events::KIND_PULL_REQUEST_UPDATE, 136 {
130 ] 137 let pr_event = patches
131 .contains(&e.kind) 138 .first()
132 }) { 139 .context("patch chain should contain at least one event")?;
133 bail!( 140 apply_pr(&git_repo, &repo_ref, pr_event, stdout)?;
134 "this proposal uses PR format (not patches). Use `ngit checkout {}` instead.", 141 return Ok(());
135 event_id.to_hex()
136 );
137 } 142 }
138 143
139 if stdout { 144 if stdout {
@@ -159,16 +164,124 @@ fn parse_event_id(id: &str) -> Result<EventId> {
159 bail!("invalid event-id or nevent: {id}") 164 bail!("invalid event-id or nevent: {id}")
160} 165}
161 166
162fn output_patches_to_stdout(mut patches: Vec<nostr::Event>) { 167fn fetch_oid_for_pr(
163 patches.reverse(); 168 oid: &str,
164 for patch in patches { 169 git_repo: &Repo,
165 print!("{}\n\n", patch.content); 170 repo_ref: &RepoRef,
171 pr_event: &nostr::Event,
172) -> Result<()> {
173 let git_servers = {
174 let mut seen: HashSet<String> = HashSet::new();
175 let mut out: Vec<String> = vec![];
176 for tag in pr_event.tags.as_slice() {
177 if tag.kind().eq(&nostr::event::TagKind::Clone) {
178 for clone_url in tag.as_slice().iter().skip(1) {
179 seen.insert(clone_url.clone());
180 out.push(clone_url.clone());
181 }
182 }
183 }
184 for server in &repo_ref.git_server {
185 if seen.insert(server.clone()) {
186 out.push(server.clone());
187 }
188 }
189 out
190 };
191
192 let term = console::Term::stderr();
193 for git_server_url in &git_servers {
194 if fetch_from_git_server(
195 git_repo,
196 &[oid.to_string()],
197 git_server_url,
198 &repo_ref.to_nostr_git_url(&None),
199 &term,
200 is_grasp_server_in_list(git_server_url, &repo_ref.grasp_servers()),
201 )
202 .is_ok()
203 {
204 return Ok(());
205 }
206 }
207 if !git_repo.does_commit_exist(oid)? {
208 bail!(
209 "cannot find proposal git data from proposal git server hint or repository git servers"
210 );
166 } 211 }
212 Ok(())
167} 213}
168 214
169fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> { 215fn apply_pr(
216 git_repo: &Repo,
217 repo_ref: &RepoRef,
218 pr_event: &nostr::Event,
219 stdout: bool,
220) -> Result<()> {
221 let tip_oid = tag_value(pr_event, "c").context("PR event is missing 'c' (tip commit) tag")?;
222
223 // Ensure the tip commit is available locally
224 if !git_repo.does_commit_exist(&tip_oid)? {
225 fetch_oid_for_pr(&tip_oid, git_repo, repo_ref, pr_event)?;
226 }
227
228 let tip = str_to_sha1(&tip_oid).context("invalid tip commit OID in PR event")?;
229
230 // Determine the base commit: prefer the merge-base tag, fall back to
231 // computing the divergence point from main/master.
232 let base = if let Ok(merge_base_oid) = tag_value(pr_event, "merge-base") {
233 str_to_sha1(&merge_base_oid).context("invalid merge-base OID in PR event")?
234 } else {
235 let (_, main_tip) = git_repo
236 .get_main_or_master_branch()
237 .context("could not determine main branch to compute PR base commit")?;
238 let (ahead, _behind) = git_repo
239 .get_commits_ahead_behind(&main_tip, &tip)
240 .context("failed to compute commits between main and PR tip")?;
241 // ahead is youngest-first; the last element is the oldest PR commit,
242 // whose parent is the effective base.
243 let oldest_pr_commit = ahead
244 .last()
245 .context("no commits found between main and PR tip")?;
246 git_repo
247 .get_commit_parent(oldest_pr_commit)
248 .context("failed to get parent of the oldest PR commit")?
249 };
250
251 // Collect commits from base..tip (youngest-first from get_commits_ahead_behind)
252 let (commits_youngest_first, _) = git_repo
253 .get_commits_ahead_behind(&base, &tip)
254 .context("failed to enumerate commits in PR")?;
255
256 if commits_youngest_first.is_empty() {
257 bail!("no commits found between base and PR tip");
258 }
259
260 let total = commits_youngest_first.len() as u64;
261
262 // Generate patches oldest-first
263 let mut patch_texts: Vec<String> = Vec::with_capacity(commits_youngest_first.len());
264 for (i, commit) in commits_youngest_first.iter().rev().enumerate() {
265 let series_count = Some((i as u64 + 1, total));
266 let patch = git_repo
267 .make_patch_from_commit(commit, &series_count)
268 .with_context(|| format!("failed to generate patch for commit {commit}"))?;
269 patch_texts.push(patch);
270 }
271
272 if stdout {
273 for patch in &patch_texts {
274 print!("{patch}\n\n");
275 }
276 } else {
277 apply_patch_texts(patch_texts)?;
278 }
279
280 Ok(())
281}
282
283fn apply_patch_texts(patch_texts: Vec<String>) -> Result<()> {
170 println!("applying to current branch with `git am`"); 284 println!("applying to current branch with `git am`");
171 patches.reverse();
172 285
173 let mut am = std::process::Command::new("git") 286 let mut am = std::process::Command::new("git")
174 .arg("am") 287 .arg("am")
@@ -183,15 +296,25 @@ fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> {
183 .as_mut() 296 .as_mut()
184 .context("git am process failed to take stdin")?; 297 .context("git am process failed to take stdin")?;
185 298
186 for patch in patches { 299 for patch in patch_texts {
187 stdin 300 stdin
188 .write(format!("{}\n\n", patch.content).as_bytes()) 301 .write(format!("{patch}\n\n").as_bytes())
189 .context("failed to write patch content into git am stdin buffer")?; 302 .context("failed to write patch content into git am stdin buffer")?;
190 } 303 }
191 stdin.flush()?; 304 stdin.flush()?;
192 let output = am 305 am.wait_with_output()
193 .wait_with_output()
194 .context("failed to read git am stdout")?; 306 .context("failed to read git am stdout")?;
195 print!("{:?}", output.stdout);
196 Ok(()) 307 Ok(())
197} 308}
309
310fn output_patches_to_stdout(mut patches: Vec<nostr::Event>) {
311 patches.reverse();
312 for patch in patches {
313 print!("{}\n\n", patch.content);
314 }
315}
316
317fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> {
318 patches.reverse();
319 apply_patch_texts(patches.into_iter().map(|p| p.content).collect())
320}