upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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
-rw-r--r--tests/git_remote_nostr/fetch.rs4
-rw-r--r--tests/git_remote_nostr/list.rs8
5 files changed, 207 insertions, 96 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)?);
diff --git a/tests/git_remote_nostr/fetch.rs b/tests/git_remote_nostr/fetch.rs
index 26d0b5b..17fd6d4 100644
--- a/tests/git_remote_nostr/fetch.rs
+++ b/tests/git_remote_nostr/fetch.rs
@@ -140,8 +140,7 @@ mod when_first_git_server_fails_ {
140#[tokio::test] 140#[tokio::test]
141#[serial] 141#[serial]
142async fn creates_commits_from_open_proposal_with_no_warnings_printed() -> Result<()> { 142async fn creates_commits_from_open_proposal_with_no_warnings_printed() -> Result<()> {
143 let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?; 143 let (events, _) = prep_source_repo_and_events_including_proposals().await?;
144 let source_path = source_git_repo.dir.to_str().unwrap().to_string();
145 144
146 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = ( 145 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
147 Relay::new(8051, None, None), 146 Relay::new(8051, None, None),
@@ -166,7 +165,6 @@ async fn creates_commits_from_open_proposal_with_no_warnings_printed() -> Result
166 let mut p = cli_tester_after_fetch(&git_repo)?; 165 let mut p = cli_tester_after_fetch(&git_repo)?;
167 p.send_line(format!("fetch {proposal_tip} refs/heads/{branch_name}").as_str())?; 166 p.send_line(format!("fetch {proposal_tip} refs/heads/{branch_name}").as_str())?;
168 p.send_line("")?; 167 p.send_line("")?;
169 p.expect(format!("fetching {source_path} over filesystem...\r\n").as_str())?;
170 // expect no errors 168 // expect no errors
171 p.expect_after_whitespace("\r\n")?; 169 p.expect_after_whitespace("\r\n")?;
172 p.exit()?; 170 p.exit()?;
diff --git a/tests/git_remote_nostr/list.rs b/tests/git_remote_nostr/list.rs
index d173cc7..37cff30 100644
--- a/tests/git_remote_nostr/list.rs
+++ b/tests/git_remote_nostr/list.rs
@@ -52,6 +52,10 @@ mod without_state_announcement {
52 assert_eq!( 52 assert_eq!(
53 res.split("\r\n") 53 res.split("\r\n")
54 .map(|e| e.to_string()) 54 .map(|e| e.to_string())
55 .filter(|s| !s.contains("remote: ")
56 && !s.contains("Receiving objects")
57 && !s.contains("Resolving deltas")
58 && !s.contains("fetching /"))
55 .collect::<HashSet<String>>(), 59 .collect::<HashSet<String>>(),
56 HashSet::from([ 60 HashSet::from([
57 "@refs/heads/main HEAD".to_string(), 61 "@refs/heads/main HEAD".to_string(),
@@ -220,6 +224,10 @@ mod with_state_announcement {
220 assert_eq!( 224 assert_eq!(
221 res.split("\r\n") 225 res.split("\r\n")
222 .map(|e| e.to_string()) 226 .map(|e| e.to_string())
227 .filter(|s| !s.contains("remote: ")
228 && !s.contains("Receiving objects")
229 && !s.contains("Resolving deltas")
230 && !s.contains("fetching /"))
223 .collect::<HashSet<String>>(), 231 .collect::<HashSet<String>>(),
224 HashSet::from([ 232 HashSet::from([
225 "@refs/heads/main HEAD".to_string(), 233 "@refs/heads/main HEAD".to_string(),