upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-09-23 10:46:26 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-09-23 13:36:53 +0100
commit2cdbb8c7ac6c98af6e36c4458c08bef2299794e1 (patch)
treefcf7c56d2f5eda0776643f8a52090965f1de06a8 /src
parent51358320c50afece31fc25945a09e3d7aac8f39c (diff)
fix(remote): add resilience to `prs`
make poorly formatted patches fail silently. we stop trusting that the `commit` tag in the latest patch can be produced by apply the patches. to achieve this we must recreate the commit during the list command, which require fetching the parent oids. support patches without optional `commit` and `parent-commit` tags.
Diffstat (limited to 'src')
-rw-r--r--src/bin/git_remote_nostr/fetch.rs117
-rw-r--r--src/bin/git_remote_nostr/list.rs92
-rw-r--r--src/lib/git/mod.rs82
3 files changed, 198 insertions, 93 deletions
diff --git a/src/bin/git_remote_nostr/fetch.rs b/src/bin/git_remote_nostr/fetch.rs
index 2e16297..46e7ad3 100644
--- a/src/bin/git_remote_nostr/fetch.rs
+++ b/src/bin/git_remote_nostr/fetch.rs
@@ -1,11 +1,12 @@
1use core::str; 1use core::str;
2use std::{ 2use std::{
3 collections::HashMap,
3 io::Stdin, 4 io::Stdin,
4 sync::{Arc, Mutex}, 5 sync::{Arc, Mutex},
5 time::Instant, 6 time::Instant,
6}; 7};
7 8
8use anyhow::{anyhow, bail, Result}; 9use anyhow::{anyhow, bail, Context, Result};
9use auth_git2::GitAuthenticator; 10use auth_git2::GitAuthenticator;
10use git2::{Progress, Repository}; 11use git2::{Progress, Repository};
11use ngit::{ 12use ngit::{
@@ -19,7 +20,7 @@ use ngit::{
19 repo_ref::RepoRef, 20 repo_ref::RepoRef,
20}; 21};
21use nostr::nips::nip19; 22use nostr::nips::nip19;
22use nostr_sdk::ToBech32; 23use nostr_sdk::{Event, ToBech32};
23 24
24use crate::utils::{ 25use crate::utils::{
25 count_lines_per_msg_vec, fetch_or_list_error_is_not_authentication_failure, 26 count_lines_per_msg_vec, fetch_or_list_error_is_not_authentication_failure,
@@ -78,65 +79,97 @@ pub async fn run_fetch(
78 79
79 fetch_batch.retain(|refstr, _| refstr.contains("refs/heads/pr/")); 80 fetch_batch.retain(|refstr, _| refstr.contains("refs/heads/pr/"));
80 81
81 if !fetch_batch.is_empty() { 82 fetch_proposals(git_repo, &term, repo_ref, &fetch_batch).await?;
83 term.flush()?;
84 println!();
85 Ok(())
86}
87
88pub fn make_commits_for_proposal(
89 git_repo: &Repo,
90 repo_ref: &RepoRef,
91 patches_ancestor_last: &[Event],
92) -> Result<String> {
93 let patches_ancestor_first: Vec<&Event> = patches_ancestor_last.iter().rev().collect();
94 let mut tip_commit_id = if let Ok(parent_commit) = tag_value(
95 patches_ancestor_first
96 .first()
97 .context("proposal should have at least one patch")?,
98 "parent-commit",
99 ) {
100 parent_commit
101 } else {
102 // TODO choose most recent commit on master before patch timestamp so it doesnt
103 // constantly get rebased
104 let (_, hash) = git_repo.get_main_or_master_branch()?;
105 hash.to_string()
106 };
107
108 for patch in &patches_ancestor_first {
109 let commit_id = git_repo
110 .create_commit_from_patch(patch, Some(tip_commit_id.clone()))
111 .context(format!(
112 "cannot create commit for patch {}",
113 nip19::Nip19Event {
114 event_id: patch.id(),
115 author: Some(patch.author()),
116 kind: Some(patch.kind()),
117 relays: if let Some(relay) = repo_ref.relays.first() {
118 vec![relay.to_string()]
119 } else {
120 vec![]
121 },
122 }
123 .to_bech32()
124 .unwrap_or_default()
125 ))?;
126 tip_commit_id = commit_id.to_string();
127 }
128 Ok(tip_commit_id)
129}
130
131async fn fetch_proposals(
132 git_repo: &Repo,
133 term: &console::Term,
134 repo_ref: &RepoRef,
135 proposal_refs: &HashMap<String, String>,
136) -> Result<()> {
137 if !proposal_refs.is_empty() {
82 let open_proposals = get_open_proposals(git_repo, repo_ref).await?; 138 let open_proposals = get_open_proposals(git_repo, repo_ref).await?;
83 139
84 let current_user = get_curent_user(git_repo)?; 140 let current_user = get_curent_user(git_repo)?;
85 141
86 for (refstr, oid) in fetch_batch { 142 for refstr in proposal_refs.keys() {
87 if let Some((_, (_, patches))) = 143 if let Some((_, (_, patches))) =
88 find_proposal_and_patches_by_branch_name(&refstr, &open_proposals, &current_user) 144 find_proposal_and_patches_by_branch_name(refstr, &open_proposals, &current_user)
89 { 145 {
90 if !git_repo.does_commit_exist(&oid)? { 146 if let Err(error) = make_commits_for_proposal(git_repo, repo_ref, patches) {
91 let mut patches_ancestor_first = patches.clone(); 147 term.write_line(
92 patches_ancestor_first.reverse(); 148 format!("WARNING: cannot create branch for {refstr}, error: {error}",)
93 if git_repo.does_commit_exist(&tag_value( 149 .as_str(),
94 patches_ancestor_first.first().unwrap(), 150 )?;
95 "parent-commit", 151 break;
96 )?)? {
97 for patch in &patches_ancestor_first {
98 if let Err(error) = git_repo.create_commit_from_patch(patch) {
99 term.write_line(
100 format!(
101 "WARNING: cannot create branch for {refstr}, error: {error} for patch {}",
102 nip19::Nip19Event {
103 event_id: patch.id(),
104 author: Some(patch.author()),
105 kind: Some(patch.kind()),
106 relays: if let Some(relay) = repo_ref.relays.first() {
107 vec![relay.to_string()]
108 } else { vec![]},
109 }.to_bech32().unwrap_or_default()
110 )
111 .as_str(),
112 )?;
113 break;
114 }
115 }
116 } else {
117 term.write_line(
118 format!("WARNING: cannot find parent commit for {refstr}").as_str(),
119 )?;
120 }
121 } 152 }
122 } else {
123 term.write_line(format!("WARNING: cannot find proposal for {refstr}").as_str())?;
124 } 153 }
125 } 154 }
126 } 155 }
127
128 term.flush()?;
129 println!();
130 Ok(()) 156 Ok(())
131} 157}
132 158
133fn fetch_from_git_server( 159pub fn fetch_from_git_server(
134 git_repo: &Repo, 160 git_repo: &Repo,
135 oids: &[String], 161 oids: &[String],
136 git_server_url: &str, 162 git_server_url: &str,
137 decoded_nostr_url: &NostrUrlDecoded, 163 decoded_nostr_url: &NostrUrlDecoded,
138 term: &console::Term, 164 term: &console::Term,
139) -> Result<()> { 165) -> Result<()> {
166 let already_have_oids = oids
167 .iter()
168 .all(|oid| git_repo.does_commit_exist(oid).is_ok_and(|outcome| outcome));
169 if already_have_oids {
170 return Ok(());
171 }
172
140 let server_url = git_server_url.parse::<CloneUrl>()?; 173 let server_url = git_server_url.parse::<CloneUrl>()?;
141 174
142 let protocols_to_attempt = get_read_protocols_to_try(git_repo, &server_url, decoded_nostr_url); 175 let protocols_to_attempt = get_read_protocols_to_try(git_repo, &server_url, decoded_nostr_url);
diff --git a/src/bin/git_remote_nostr/list.rs b/src/bin/git_remote_nostr/list.rs
index 959b8c8..378a124 100644
--- a/src/bin/git_remote_nostr/list.rs
+++ b/src/bin/git_remote_nostr/list.rs
@@ -5,14 +5,13 @@ use anyhow::{anyhow, Context, Result};
5use auth_git2::GitAuthenticator; 5use auth_git2::GitAuthenticator;
6use client::get_state_from_cache; 6use client::get_state_from_cache;
7use git::RepoActions; 7use git::RepoActions;
8use git_events::{event_to_cover_letter, get_commit_id_from_patch};
9use ngit::{ 8use ngit::{
10 client, 9 client,
11 git::{ 10 git::{
12 self, 11 self,
13 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, 12 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol},
14 }, 13 },
15 git_events, 14 git_events::event_to_cover_letter,
16 login::get_curent_user, 15 login::get_curent_user,
17 repo_ref, 16 repo_ref,
18}; 17};
@@ -20,6 +19,7 @@ use nostr_sdk::hashes::sha1::Hash as Sha1Hash;
20use repo_ref::RepoRef; 19use repo_ref::RepoRef;
21 20
22use crate::{ 21use crate::{
22 fetch::{fetch_from_git_server, make_commits_for_proposal},
23 git::Repo, 23 git::Repo,
24 utils::{ 24 utils::{
25 fetch_or_list_error_is_not_authentication_failure, get_open_proposals, 25 fetch_or_list_error_is_not_authentication_failure, get_open_proposals,
@@ -88,6 +88,61 @@ pub async fn run_list(
88 88
89 state.retain(|k, _| !k.starts_with("refs/heads/pr/")); 89 state.retain(|k, _| !k.starts_with("refs/heads/pr/"));
90 90
91 let proposals_state =
92 get_open_proposals_state(&term, git_repo, repo_ref, decoded_nostr_url, &remote_states)
93 .await?;
94
95 state.extend(proposals_state);
96
97 // TODO 'for push' should we check with the git servers to see if any of them
98 // allow push from the user?
99 for (name, value) in state {
100 if value.starts_with("ref: ") {
101 if !for_push {
102 println!("{} {name}", value.replace("ref: ", "@"));
103 }
104 } else {
105 println!("{value} {name}");
106 }
107 }
108
109 println!();
110 Ok(remote_states)
111}
112
113async fn get_open_proposals_state(
114 term: &console::Term,
115 git_repo: &Repo,
116 repo_ref: &RepoRef,
117 decoded_nostr_url: &NostrUrlDecoded,
118 remote_states: &HashMap<String, HashMap<String, String>>,
119) -> Result<HashMap<String, String>> {
120 // we cannot use commit_id in the latest patch in a proposal because:
121 // 1) the `commit` tag is optional
122 // 2) if the commit tag is wrong, it will cause errors which stop clone from
123 // working
124
125 // without trusting commit_id we must apply each patch which requires the oid of
126 // the parent so we much do a fetch
127 for (git_server_url, oids_from_git_servers) in remote_states {
128 if fetch_from_git_server(
129 git_repo,
130 &oids_from_git_servers
131 .values()
132 .filter(|v| !v.starts_with("ref: "))
133 .cloned()
134 .collect::<Vec<String>>(),
135 git_server_url,
136 decoded_nostr_url,
137 term,
138 )
139 .is_ok()
140 {
141 break;
142 }
143 }
144
145 let mut state = HashMap::new();
91 let open_proposals = get_open_proposals(git_repo, repo_ref).await?; 146 let open_proposals = get_open_proposals(git_repo, repo_ref).await?;
92 let current_user = get_curent_user(git_repo)?; 147 let current_user = get_curent_user(git_repo)?;
93 for (_, (proposal, patches)) in open_proposals { 148 for (_, (proposal, patches)) in open_proposals {
@@ -102,32 +157,21 @@ pub async fn run_list(
102 } else { 157 } else {
103 branch_name 158 branch_name
104 }; 159 };
105 if let Some(patch) = patches.first() { 160 match make_commits_for_proposal(git_repo, repo_ref, &patches) {
106 // TODO this isn't resilient because the commit id stated may not be correct 161 Ok(tip) => {
107 // we will need to check whether the commit id exists in the repo or apply the 162 state.insert(format!("refs/heads/{branch_name}"), tip);
108 // proposal and each patch to check
109 if let Ok(commit_id) = get_commit_id_from_patch(patch) {
110 state.insert(format!("refs/heads/{branch_name}"), commit_id);
111 } 163 }
112 } 164 Err(error) => {
113 } 165 let _ = term.write_line(
114 } 166 format!("WARNING: cannot fetch branch {branch_name} error: {error}")
115 } 167 .as_str(),
116 168 );
117 // TODO 'for push' should we check with the git servers to see if any of them 169 }
118 // allow push from the user? 170 };
119 for (name, value) in state {
120 if value.starts_with("ref: ") {
121 if !for_push {
122 println!("{} {name}", value.replace("ref: ", "@"));
123 } 171 }
124 } else {
125 println!("{value} {name}");
126 } 172 }
127 } 173 }
128 174 Ok(state)
129 println!();
130 Ok(remote_states)
131} 175}
132 176
133pub fn list_from_remotes( 177pub fn list_from_remotes(
diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs
index 7ebdefd..05186b0 100644
--- a/src/lib/git/mod.rs
+++ b/src/lib/git/mod.rs
@@ -79,7 +79,11 @@ pub trait RepoActions {
79 branch_name: &str, 79 branch_name: &str,
80 patch_and_ancestors: Vec<nostr::Event>, 80 patch_and_ancestors: Vec<nostr::Event>,
81 ) -> Result<Vec<nostr::Event>>; 81 ) -> Result<Vec<nostr::Event>>;
82 fn create_commit_from_patch(&self, patch: &nostr::Event) -> Result<Oid>; 82 fn create_commit_from_patch(
83 &self,
84 patch: &nostr::Event,
85 parent_commit_id_override: Option<String>,
86 ) -> Result<Oid>;
83 fn parse_starting_commits(&self, starting_commits: &str) -> Result<Vec<Sha1Hash>>; 87 fn parse_starting_commits(&self, starting_commits: &str) -> Result<Vec<Sha1Hash>>;
84 fn ancestor_of(&self, decendant: &Sha1Hash, ancestor: &Sha1Hash) -> Result<bool>; 88 fn ancestor_of(&self, decendant: &Sha1Hash, ancestor: &Sha1Hash) -> Result<bool>;
85 fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>>; 89 fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>>;
@@ -540,19 +544,30 @@ impl RepoActions for Repo {
540 let commit_id = get_commit_id_from_patch(patch)?; 544 let commit_id = get_commit_id_from_patch(patch)?;
541 // only create new commits - otherwise make them the tip 545 // only create new commits - otherwise make them the tip
542 if !self.does_commit_exist(&commit_id)? { 546 if !self.does_commit_exist(&commit_id)? {
543 self.create_commit_from_patch(patch)?; 547 self.create_commit_from_patch(patch, None)?;
544 } 548 }
545 self.create_branch_at_commit(branch_name, &commit_id)?; 549 self.create_branch_at_commit(branch_name, &commit_id)?;
546 self.checkout(branch_name)?; 550 self.checkout(branch_name)?;
547 } 551 }
548 Ok(patches_to_apply) 552 Ok(patches_to_apply)
549 } 553 }
550 fn create_commit_from_patch(&self, patch: &nostr::Event) -> Result<Oid> { 554 fn create_commit_from_patch(
551 let commit_id = get_commit_id_from_patch(patch)?; 555 &self,
552 if self.does_commit_exist(&commit_id)? { 556 patch: &nostr::Event,
553 return Ok(Oid::from_str(&commit_id)?); 557 parent_commit_id_override: Option<String>,
558 ) -> Result<Oid> {
559 let commit_id = get_commit_id_from_patch(patch);
560 if let Ok(commit_id) = &commit_id {
561 if self.does_commit_exist(commit_id).unwrap_or(false) {
562 return Ok(Oid::from_str(commit_id)?);
563 }
554 } 564 }
555 let parent_commit_id = tag_value(patch, "parent-commit")?; 565
566 let parent_commit_id = if let Some(commit_id) = parent_commit_id_override.clone() {
567 commit_id
568 } else {
569 tag_value(patch, "parent-commit")?
570 };
556 571
557 let parent_commit = self 572 let parent_commit = self
558 .git_repo 573 .git_repo
@@ -604,25 +619,38 @@ impl RepoActions for Repo {
604 // were identical when in a scenario when they should be different but I dont 619 // were identical when in a scenario when they should be different but I dont
605 // think we have a test case for it. surely we should be using the 620 // think we have a test case for it. surely we should be using the
606 // extract_sig_from_patch_tags outputs to address this? 621 // extract_sig_from_patch_tags outputs to address this?
607 if !applied_oid.to_string().eq(&commit_id) { 622 let custom_parent = if let Some(ovderride_parent) = parent_commit_id_override {
608 let commit = self.git_repo.find_commit(applied_oid)?; 623 if let Ok(tag_parent) = tag_value(patch, "parent-commit") {
609 applied_oid = commit 624 ovderride_parent != tag_parent
610 .amend( 625 } else {
611 None, 626 true
612 Some(&commit.author()), 627 }
613 Some(&commit.committer()), 628 } else {
614 None, 629 false
615 None, 630 };
616 None, 631 if !custom_parent {
617 ) 632 if let Ok(commit_id) = &commit_id {
618 .context("cannot amend commit to produce new oid")?; 633 if !applied_oid.to_string().eq(commit_id) {
619 } 634 let commit = self.git_repo.find_commit(applied_oid)?;
620 if !applied_oid.to_string().eq(&commit_id) { 635 applied_oid = commit
621 bail!( 636 .amend(
622 "when applied the patch commit id ({}) doesn't match the one specified in the event tag ({})", 637 None,
623 applied_oid.to_string(), 638 Some(&commit.author()),
624 get_commit_id_from_patch(patch)?, 639 Some(&commit.committer()),
625 ); 640 None,
641 None,
642 None,
643 )
644 .context("cannot amend commit to produce new oid")?;
645 }
646 if !applied_oid.to_string().eq(commit_id) {
647 bail!(
648 "when applied the patch commit id ({}) doesn't match the one specified in the event tag ({})",
649 applied_oid.to_string(),
650 get_commit_id_from_patch(patch)?,
651 );
652 }
653 }
626 } 654 }
627 self.git_repo.set_index(&mut existing_index)?; 655 self.git_repo.set_index(&mut existing_index)?;
628 Ok(applied_oid) 656 Ok(applied_oid)
@@ -1651,7 +1679,7 @@ mod tests {
1651 test_repo.populate()?; 1679 test_repo.populate()?;
1652 let git_repo = Repo::from_path(&test_repo.dir)?; 1680 let git_repo = Repo::from_path(&test_repo.dir)?;
1653 println!("{:?}", &patch_event); 1681 println!("{:?}", &patch_event);
1654 git_repo.create_commit_from_patch(&patch_event)?; 1682 git_repo.create_commit_from_patch(&patch_event, None)?;
1655 let commit_id = tag_value(&patch_event, "commit")?; 1683 let commit_id = tag_value(&patch_event, "commit")?;
1656 // does commit with id exist? 1684 // does commit with id exist?
1657 assert!(git_repo.does_commit_exist(&commit_id)?); 1685 assert!(git_repo.does_commit_exist(&commit_id)?);