diff options
Diffstat (limited to 'src/bin/ngit/sub_commands/apply.rs')
| -rw-r--r-- | src/bin/ngit/sub_commands/apply.rs | 169 |
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 @@ | |||
| 1 | use std::{ | 1 | use 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}; | |||
| 8 | use indicatif::{ProgressBar, ProgressStyle}; | 9 | use indicatif::{ProgressBar, ProgressStyle}; |
| 9 | use ngit::{ | 10 | use 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 | }; |
| 13 | use nostr::nips::nip19::Nip19; | 20 | use nostr::nips::nip19::Nip19; |
| 14 | use nostr_sdk::{EventId, FromBech32}; | 21 | use 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 | ||
| 162 | fn output_patches_to_stdout(mut patches: Vec<nostr::Event>) { | 167 | fn 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 | ||
| 169 | fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> { | 215 | fn 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 | |||
| 283 | fn 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 | |||
| 310 | fn 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 | |||
| 317 | fn 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 | } | ||