upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/git_remote_nostr
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/bin/git_remote_nostr
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/bin/git_remote_nostr')
-rw-r--r--src/bin/git_remote_nostr/fetch.rs117
-rw-r--r--src/bin/git_remote_nostr/list.rs92
2 files changed, 143 insertions, 66 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(