upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-05-23 21:53:35 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2025-05-23 21:53:35 +0100
commita75a1441b7c1cec93ebc0cb796c21360abbc5573 (patch)
tree79242e269749fdca294ace312e259c86595ba072
parentde095c6993ed6e99dd22972519e1cdcf4014f7d0 (diff)
feat(push): avoid out of sync issues for ngit relay
we need to be careful with git servers with their own permissions so a ngit user doesn't inadvertantly push changes on top of a another user who pushed directly to the git server without using the force flag. We dont have this problem with ngit-relay so we can always force push, even if the user didnt as nostr is the authority of state.
-rw-r--r--src/bin/git_remote_nostr/list.rs30
-rw-r--r--src/bin/git_remote_nostr/push.rs36
2 files changed, 39 insertions, 27 deletions
diff --git a/src/bin/git_remote_nostr/list.rs b/src/bin/git_remote_nostr/list.rs
index bf16f89..ea29531 100644
--- a/src/bin/git_remote_nostr/list.rs
+++ b/src/bin/git_remote_nostr/list.rs
@@ -31,7 +31,7 @@ pub async fn run_list(
31 git_repo: &Repo, 31 git_repo: &Repo,
32 repo_ref: &RepoRef, 32 repo_ref: &RepoRef,
33 for_push: bool, 33 for_push: bool,
34) -> Result<HashMap<String, HashMap<String, String>>> { 34) -> Result<HashMap<String, (HashMap<String, String>, bool)>> {
35 let nostr_state = (get_state_from_cache(Some(git_repo.get_path()?), repo_ref).await).ok(); 35 let nostr_state = (get_state_from_cache(Some(git_repo.get_path()?), repo_ref).await).ok();
36 36
37 let term = console::Term::stderr(); 37 let term = console::Term::stderr();
@@ -46,7 +46,7 @@ pub async fn run_list(
46 46
47 let mut state = if let Some(nostr_state) = nostr_state { 47 let mut state = if let Some(nostr_state) = nostr_state {
48 for (name, value) in &nostr_state.state { 48 for (name, value) in &nostr_state.state {
49 for (url, remote_state) in &remote_states { 49 for (url, (remote_state, _is_ngit_relay)) in &remote_states {
50 let remote_name = get_short_git_server_name(git_repo, url); 50 let remote_name = get_short_git_server_name(git_repo, url);
51 if let Some(remote_value) = remote_state.get(name) { 51 if let Some(remote_value) = remote_state.get(name) {
52 if value.ne(remote_value) { 52 if value.ne(remote_value) {
@@ -74,15 +74,16 @@ pub async fn run_list(
74 } 74 }
75 nostr_state.state 75 nostr_state.state
76 } else { 76 } else {
77 repo_ref 77 let (state, _is_ngit_relay) = repo_ref
78 .git_server 78 .git_server
79 .iter() 79 .iter()
80 .filter_map(|server| remote_states.get(server)) 80 .filter_map(|server| remote_states.get(server))
81 .cloned() 81 .cloned()
82 .collect::<Vec<HashMap<String, String>>>() 82 .collect::<Vec<(HashMap<String, String>, bool)>>()
83 .first() 83 .first()
84 .context("failed to get refs from git server")? 84 .context("failed to get refs from git server")?
85 .clone() 85 .clone();
86 state
86 }; 87 };
87 88
88 state.retain(|k, _| !k.starts_with("refs/heads/pr/")); 89 state.retain(|k, _| !k.starts_with("refs/heads/pr/"));
@@ -112,7 +113,7 @@ async fn get_open_and_draft_proposals_state(
112 term: &console::Term, 113 term: &console::Term,
113 git_repo: &Repo, 114 git_repo: &Repo,
114 repo_ref: &RepoRef, 115 repo_ref: &RepoRef,
115 remote_states: &HashMap<String, HashMap<String, String>>, 116 remote_states: &HashMap<String, (HashMap<String, String>, bool)>,
116) -> Result<HashMap<String, String>> { 117) -> Result<HashMap<String, String>> {
117 // we cannot use commit_id in the latest patch in a proposal because: 118 // we cannot use commit_id in the latest patch in a proposal because:
118 // 1) the `commit` tag is optional 119 // 1) the `commit` tag is optional
@@ -121,7 +122,7 @@ async fn get_open_and_draft_proposals_state(
121 122
122 // without trusting commit_id we must apply each patch which requires the oid of 123 // without trusting commit_id we must apply each patch which requires the oid of
123 // the parent so we much do a fetch 124 // the parent so we much do a fetch
124 for (git_server_url, oids_from_git_servers) in remote_states { 125 for (git_server_url, (oids_from_git_servers, is_ngit_relay)) in remote_states {
125 if fetch_from_git_server( 126 if fetch_from_git_server(
126 git_repo, 127 git_repo,
127 &oids_from_git_servers 128 &oids_from_git_servers
@@ -132,7 +133,7 @@ async fn get_open_and_draft_proposals_state(
132 git_server_url, 133 git_server_url,
133 &repo_ref.to_nostr_git_url(&None), 134 &repo_ref.to_nostr_git_url(&None),
134 term, 135 term,
135 is_ngit_relay(git_server_url, &repo_ref.ngit_relays()), 136 *is_ngit_relay,
136 ) 137 )
137 .is_ok() 138 .is_ok()
138 { 139 {
@@ -178,22 +179,17 @@ pub fn list_from_remotes(
178 git_servers: &Vec<String>, 179 git_servers: &Vec<String>,
179 decoded_nostr_url: &NostrUrlDecoded, 180 decoded_nostr_url: &NostrUrlDecoded,
180 ngit_relays: &[String], 181 ngit_relays: &[String],
181) -> HashMap<String, HashMap<String, String>> { 182) -> HashMap<String, (HashMap<String, String>, bool)> {
182 let mut remote_states = HashMap::new(); 183 let mut remote_states = HashMap::new();
183 let mut errors = HashMap::new(); 184 let mut errors = HashMap::new();
184 for url in git_servers { 185 for url in git_servers {
185 match list_from_remote( 186 let is_ngit_relay = is_ngit_relay(url, ngit_relays);
186 term, 187 match list_from_remote(term, git_repo, url, decoded_nostr_url, is_ngit_relay) {
187 git_repo,
188 url,
189 decoded_nostr_url,
190 is_ngit_relay(url, ngit_relays),
191 ) {
192 Err(error) => { 188 Err(error) => {
193 errors.insert(url, error); 189 errors.insert(url, error);
194 } 190 }
195 Ok(state) => { 191 Ok(state) => {
196 remote_states.insert(url.to_string(), state); 192 remote_states.insert(url.to_string(), (state, is_ngit_relay));
197 } 193 }
198 } 194 }
199 } 195 }
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs
index a34d729..c4953f8 100644
--- a/src/bin/git_remote_nostr/push.rs
+++ b/src/bin/git_remote_nostr/push.rs
@@ -50,13 +50,14 @@ use crate::{
50}; 50};
51 51
52#[allow(clippy::too_many_lines)] 52#[allow(clippy::too_many_lines)]
53#[allow(clippy::type_complexity)]
53pub async fn run_push( 54pub async fn run_push(
54 git_repo: &Repo, 55 git_repo: &Repo,
55 repo_ref: &RepoRef, 56 repo_ref: &RepoRef,
56 stdin: &Stdin, 57 stdin: &Stdin,
57 initial_refspec: &str, 58 initial_refspec: &str,
58 client: &Client, 59 client: &Client,
59 list_outputs: Option<HashMap<String, HashMap<String, String>>>, 60 list_outputs: Option<HashMap<String, (HashMap<String, String>, bool)>>,
60) -> Result<()> { 61) -> Result<()> {
61 let refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?; 62 let refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?;
62 63
@@ -93,7 +94,8 @@ pub async fn run_push(
93 .iter() 94 .iter()
94 .find(|&url| list_outputs.contains_key(url)) 95 .find(|&url| list_outputs.contains_key(url))
95 { 96 {
96 list_outputs.get(url).unwrap().to_owned() 97 let (state, _is_ngit_relay) = list_outputs.get(url).unwrap().to_owned();
98 state
97 } else { 99 } else {
98 bail!( 100 bail!(
99 "failed to connect to git servers: {}", 101 "failed to connect to git servers: {}",
@@ -706,13 +708,14 @@ fn create_rejected_refspecs_and_remotes_refspecs(
706 git_repo: &Repo, 708 git_repo: &Repo,
707 refspecs: &Vec<String>, 709 refspecs: &Vec<String>,
708 nostr_state: &HashMap<String, String>, 710 nostr_state: &HashMap<String, String>,
709 list_outputs: &HashMap<String, HashMap<String, String>>, 711 list_outputs: &HashMap<String, (HashMap<String, String>, bool)>,
710) -> Result<(HashMapUrlRefspecs, HashMapUrlRefspecs)> { 712) -> Result<(HashMapUrlRefspecs, HashMapUrlRefspecs)> {
711 let mut refspecs_for_remotes = HashMap::new(); 713 let mut refspecs_for_remotes = HashMap::new();
712 714
713 let mut rejected_refspecs: HashMapUrlRefspecs = HashMap::new(); 715 let mut rejected_refspecs: HashMapUrlRefspecs = HashMap::new();
714 716
715 for (url, remote_state) in list_outputs { 717 for (url, (remote_state, is_ngit_relay)) in list_outputs {
718 let is_ngit_relay = is_ngit_relay.to_owned();
716 let short_name = get_short_git_server_name(git_repo, url); 719 let short_name = get_short_git_server_name(git_repo, url);
717 let mut refspecs_for_remote = vec![]; 720 let mut refspecs_for_remote = vec![];
718 for refspec in refspecs { 721 for refspec in refspecs {
@@ -747,12 +750,7 @@ fn create_rejected_refspecs_and_remotes_refspecs(
747 if is_remote_tip_ancestor_of_commit { 750 if is_remote_tip_ancestor_of_commit {
748 refspecs_for_remote.push(refspec.clone()); 751 refspecs_for_remote.push(refspec.clone());
749 } else { 752 } else {
750 // this is a force push so we need to force push to git server too 753 refspecs_for_remote.push(ensure_force_push_refspec(refspec));
751 if refspec.starts_with('+') {
752 refspecs_for_remote.push(refspec.clone());
753 } else {
754 refspecs_for_remote.push(format!("+{refspec}"));
755 }
756 } 754 }
757 } else if let Ok(remote_value_tip) = 755 } else if let Ok(remote_value_tip) =
758 git_repo.get_commit_or_tip_of_reference(remote_value) 756 git_repo.get_commit_or_tip_of_reference(remote_value)
@@ -778,6 +776,9 @@ fn create_rejected_refspecs_and_remotes_refspecs(
778 if ahead_of_nostr.is_empty() { 776 if ahead_of_nostr.is_empty() {
779 // ancestor of nostr and we are force pushing anyway... 777 // ancestor of nostr and we are force pushing anyway...
780 refspecs_for_remote.push(refspec.clone()); 778 refspecs_for_remote.push(refspec.clone());
779 } else if is_ngit_relay {
780 // an ngit-relay can only be pushed to via nostr so can force push
781 refspecs_for_remote.push(ensure_force_push_refspec(refspec));
781 } else { 782 } else {
782 rejected_refspecs 783 rejected_refspecs
783 .entry(refspec.to_string()) 784 .entry(refspec.to_string())
@@ -794,6 +795,8 @@ fn create_rejected_refspecs_and_remotes_refspecs(
794 )?; 795 )?;
795 } 796 }
796 } 797 }
798 } else if is_ngit_relay {
799 refspecs_for_remote.push(ensure_force_push_refspec(refspec));
797 } else { 800 } else {
798 // remote_value oid is not present locally 801 // remote_value oid is not present locally
799 // TODO can we download the remote reference? 802 // TODO can we download the remote reference?
@@ -827,6 +830,8 @@ fn create_rejected_refspecs_and_remotes_refspecs(
827 if ahead.is_empty() { 830 if ahead.is_empty() {
828 // can soft push 831 // can soft push
829 refspecs_for_remote.push(refspec.clone()); 832 refspecs_for_remote.push(refspec.clone());
833 } else if is_ngit_relay {
834 refspecs_for_remote.push(ensure_force_push_refspec(refspec));
830 } else { 835 } else {
831 // cant soft push 836 // cant soft push
832 rejected_refspecs 837 rejected_refspecs
@@ -841,6 +846,8 @@ fn create_rejected_refspecs_and_remotes_refspecs(
841 ).as_str(), 846 ).as_str(),
842 )?; 847 )?;
843 } 848 }
849 } else if is_ngit_relay {
850 refspecs_for_remote.push(ensure_force_push_refspec(refspec));
844 } else { 851 } else {
845 // havn't fetched oid from remote 852 // havn't fetched oid from remote
846 // TODO fetch oid from remote 853 // TODO fetch oid from remote
@@ -878,6 +885,15 @@ fn create_rejected_refspecs_and_remotes_refspecs(
878 Ok((rejected_refspecs, remotes_refspecs_without_rejected)) 885 Ok((rejected_refspecs, remotes_refspecs_without_rejected))
879} 886}
880 887
888fn ensure_force_push_refspec(refspec: &str) -> String {
889 // Check if the refspec starts with '+' or ':'
890 if refspec.starts_with('+') || refspec.starts_with(':') {
891 refspec.to_string() // Return as is
892 } else {
893 format!("+{refspec}") // Add '+' prefix
894 }
895}
896
881fn generate_updated_state( 897fn generate_updated_state(
882 git_repo: &Repo, 898 git_repo: &Repo,
883 existing_state: &HashMap<String, String>, 899 existing_state: &HashMap<String, String>,