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:
Diffstat (limited to 'src')
-rw-r--r--src/git.rs33
-rw-r--r--src/repo_ref.rs184
-rw-r--r--src/sub_commands/claim.rs52
-rw-r--r--src/sub_commands/prs/create.rs2
-rw-r--r--src/sub_commands/prs/list.rs1
-rw-r--r--src/sub_commands/pull.rs1
-rw-r--r--src/sub_commands/push.rs1
7 files changed, 247 insertions, 27 deletions
diff --git a/src/git.rs b/src/git.rs
index e2d8196..29670eb 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -1,6 +1,6 @@
1use std::env::current_dir;
2#[cfg(test)] 1#[cfg(test)]
3use std::path::PathBuf; 2use std::path::PathBuf;
3use std::{env::current_dir, path::Path};
4 4
5use anyhow::{bail, Context, Result}; 5use anyhow::{bail, Context, Result};
6use git2::{Oid, Revwalk}; 6use git2::{Oid, Revwalk};
@@ -30,6 +30,8 @@ impl Repo {
30// pub type Sha1 = [u8; 20]; 30// pub type Sha1 = [u8; 20];
31 31
32pub trait RepoActions { 32pub trait RepoActions {
33 fn get_path(&self) -> Result<&Path>;
34 fn get_origin_url(&self) -> Result<String>;
33 fn get_local_branch_names(&self) -> Result<Vec<String>>; 35 fn get_local_branch_names(&self) -> Result<Vec<String>>;
34 fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>; 36 fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>;
35 fn get_checked_out_branch_name(&self) -> Result<String>; 37 fn get_checked_out_branch_name(&self) -> Result<String>;
@@ -61,6 +63,23 @@ pub trait RepoActions {
61} 63}
62 64
63impl RepoActions for Repo { 65impl RepoActions for Repo {
66 fn get_path(&self) -> Result<&Path> {
67 self.git_repo
68 .path()
69 .parent()
70 .context("cannot find repositiory path as .git has no parent")
71 }
72
73 fn get_origin_url(&self) -> Result<String> {
74 Ok(self
75 .git_repo
76 .find_remote("origin")
77 .context("cannot find origin")?
78 .url()
79 .context("cannot find origin url")?
80 .to_string())
81 }
82
64 fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)> { 83 fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)> {
65 let main_branch_name = { 84 let main_branch_name = {
66 let local_branches = self 85 let local_branches = self
@@ -860,6 +879,18 @@ mod tests {
860 } 879 }
861 } 880 }
862 881
882 mod get_origin_url {
883 use super::*;
884
885 #[test]
886 fn returns_origin_url() -> Result<()> {
887 let test_repo = GitTestRepo::default();
888 test_repo.add_remote("origin", "https://localhost:1000")?;
889 let git_repo = Repo::from_path(&test_repo.dir)?;
890 assert_eq!(git_repo.get_origin_url()?, "https://localhost:1000");
891 Ok(())
892 }
893 }
863 mod get_checked_out_branch_name { 894 mod get_checked_out_branch_name {
864 use super::*; 895 use super::*;
865 896
diff --git a/src/repo_ref.rs b/src/repo_ref.rs
index a92b5b3..a5b35c9 100644
--- a/src/repo_ref.rs
+++ b/src/repo_ref.rs
@@ -1,20 +1,26 @@
1use std::{fs::File, io::BufReader, str::FromStr};
2
1use anyhow::{bail, Context, Result}; 3use anyhow::{bail, Context, Result};
2use nostr::Tag; 4use nostr::{secp256k1::XOnlyPublicKey, FromBech32, Tag, ToBech32};
5use serde::{Deserialize, Serialize};
3 6
4#[cfg(not(test))] 7#[cfg(not(test))]
5use crate::client::Client; 8use crate::client::Client;
6use crate::client::Connect;
7#[cfg(test)] 9#[cfg(test)]
8use crate::client::MockConnect; 10use crate::client::MockConnect;
11use crate::{
12 client::Connect,
13 git::{Repo, RepoActions},
14};
9 15
10#[derive(Default)] 16#[derive(Default)]
11pub struct RepoRef { 17pub struct RepoRef {
12 pub name: String, 18 pub name: String,
13 pub description: String, 19 pub description: String,
14 pub root_commit: String, 20 pub root_commit: String,
21 pub git_server: String,
15 pub relays: Vec<String>, 22 pub relays: Vec<String>,
16 // git_server: String, 23 pub maintainers: Vec<XOnlyPublicKey>,
17 // other maintainers
18 // code languages and hashtags 24 // code languages and hashtags
19} 25}
20 26
@@ -35,6 +41,10 @@ impl TryFrom<nostr::Event> for RepoRef {
35 r.description = t.as_vec()[1].clone(); 41 r.description = t.as_vec()[1].clone();
36 } 42 }
37 43
44 if let Some(t) = event.tags.iter().find(|t| t.as_vec()[0].eq("git-server")) {
45 r.git_server = t.as_vec()[1].clone();
46 }
47
38 if let Some(t) = event.tags.iter().find(|t| t.as_vec()[0].eq("d")) { 48 if let Some(t) = event.tags.iter().find(|t| t.as_vec()[0].eq("d")) {
39 r.root_commit = t.as_vec()[1].clone(); 49 r.root_commit = t.as_vec()[1].clone();
40 } 50 }
@@ -46,6 +56,15 @@ impl TryFrom<nostr::Event> for RepoRef {
46 .map(|t| t.as_vec()[1].clone()) 56 .map(|t| t.as_vec()[1].clone())
47 .collect(); 57 .collect();
48 58
59 for tag in event.tags.iter().filter(|t| t.as_vec()[0].eq("p")) {
60 let pk = tag.as_vec()[1].clone();
61 r.maintainers.push(
62 nostr_sdk::prelude::XOnlyPublicKey::from_str(&pk)
63 .context(format!("cannot convert {pk} into a valid nostr public key"))
64 .context("invalid repository event")?,
65 );
66 }
67
49 Ok(r) 68 Ok(r)
50 } 69 }
51} 70}
@@ -62,10 +81,17 @@ impl RepoRef {
62 Tag::Reference(format!("r-{}", self.root_commit)), 81 Tag::Reference(format!("r-{}", self.root_commit)),
63 Tag::Name(self.name.clone()), 82 Tag::Name(self.name.clone()),
64 Tag::Description(self.description.clone()), 83 Tag::Description(self.description.clone()),
84 Tag::Generic(
85 nostr::TagKind::Custom("git-server".to_string()),
86 vec![self.git_server.clone()],
87 ),
88 Tag::Reference(self.git_server.clone()),
65 ], 89 ],
66 self.relays.iter().map(|r| Tag::Relay(r.into())).collect(), 90 self.relays.iter().map(|r| Tag::Relay(r.into())).collect(),
67 // git_servers 91 self.maintainers
68 // other maintainers 92 .iter()
93 .map(|pk| Tag::PubKey(*pk, None))
94 .collect(),
69 // code languages and hashtags 95 // code languages and hashtags
70 ] 96 ]
71 .concat(), 97 .concat(),
@@ -76,24 +102,30 @@ impl RepoRef {
76} 102}
77 103
78pub async fn fetch( 104pub async fn fetch(
105 git_repo: &Repo,
79 root_commit: String, 106 root_commit: String,
80 #[cfg(test)] client: &MockConnect, 107 #[cfg(test)] client: &MockConnect,
81 #[cfg(not(test))] client: &Client, 108 #[cfg(not(test))] client: &Client,
82 // TODO: more rubust way of finding repo events 109 // TODO: more rubust way of finding repo events
83 relays: Vec<String>, 110 fallback_relays: Vec<String>,
84) -> Result<RepoRef> { 111) -> Result<RepoRef> {
85 // TODO: fetch relay information from file 112 let repo_config = get_repo_config_from_yaml(git_repo);
86 113
87 let events: Vec<nostr::Event> = client 114 // TODO: check events only from maintainers. get relay list of maintainters.
88 .get_events( 115 // check those relays.
89 relays, 116
90 vec![ 117 let mut repo_event_filter = nostr::Filter::default()
91 nostr::Filter::default() 118 .kind(nostr::Kind::Custom(REPO_REF_KIND))
92 .kind(nostr::Kind::Custom(REPO_REF_KIND)) 119 .identifier(root_commit);
93 .identifier(root_commit), 120
94 ], 121 let mut relays = fallback_relays;
95 ) 122 if let Ok(repo_config) = repo_config {
96 .await?; 123 repo_event_filter =
124 repo_event_filter.pubkeys(extract_pks(repo_config.maintainers.clone())?);
125 relays = repo_config.relays.clone();
126 }
127
128 let events: Vec<nostr::Event> = client.get_events(relays, vec![repo_event_filter]).await?;
97 129
98 RepoRef::try_from( 130 RepoRef::try_from(
99 events 131 events
@@ -105,6 +137,68 @@ pub async fn fetch(
105 ) 137 )
106} 138}
107 139
140#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
141pub struct RepoConfigYaml {
142 pub maintainers: Vec<String>,
143 pub relays: Vec<String>,
144}
145
146pub fn get_repo_config_from_yaml(git_repo: &Repo) -> Result<RepoConfigYaml> {
147 let path = git_repo.get_path()?.join("maintainers.yaml");
148 let file = File::open(path)
149 .context("should open maintainers.yaml if it exists")
150 .context("maintainers.yaml doesnt exist")?;
151 let reader = BufReader::new(file);
152 let repo_config_yaml: RepoConfigYaml = serde_yaml::from_reader(reader)
153 .context("should read maintainers.yaml with serde_yaml")
154 .context("maintainers.yaml incorrectly formatted")?;
155 Ok(repo_config_yaml)
156}
157
158pub fn extract_pks(pk_strings: Vec<String>) -> Result<Vec<XOnlyPublicKey>> {
159 let mut pks: Vec<XOnlyPublicKey> = vec![];
160 for s in pk_strings {
161 pks.push(
162 nostr_sdk::prelude::XOnlyPublicKey::from_bech32(s.clone())
163 .context(format!("cannot convert {s} into a valid nostr public key"))?,
164 );
165 }
166 Ok(pks)
167}
168
169pub fn save_repo_config_to_yaml(
170 git_repo: &Repo,
171 maintainers: Vec<XOnlyPublicKey>,
172 relays: Vec<String>,
173) -> Result<()> {
174 let path = git_repo.get_path()?.join("maintainers.yaml");
175 let file = if path.exists() {
176 std::fs::OpenOptions::new()
177 .create(true)
178 .write(true)
179 .truncate(true)
180 .open(path)
181 .context("cannot open maintainers.yaml file with write and truncate options")?
182 } else {
183 std::fs::File::create(path).context("cannot create maintainers.yaml file")?
184 };
185 let mut maintainers_npubs = vec![];
186 for m in maintainers {
187 maintainers_npubs.push(
188 m.to_bech32()
189 .context("cannot convert public key into npub")?,
190 );
191 }
192 serde_yaml::to_writer(
193 file,
194 &RepoConfigYaml {
195 maintainers: maintainers_npubs,
196 relays,
197 },
198 )
199 .context("cannot write maintainers to maintainers.yaml file serde_yaml")
200}
201
108#[cfg(test)] 202#[cfg(test)]
109mod tests { 203mod tests {
110 use test_utils::*; 204 use test_utils::*;
@@ -116,7 +210,9 @@ mod tests {
116 name: "test name".to_string(), 210 name: "test name".to_string(),
117 description: "test description".to_string(), 211 description: "test description".to_string(),
118 root_commit: "23471389461".to_string(), 212 root_commit: "23471389461".to_string(),
213 git_server: "https://localhost:1000".to_string(),
119 relays: vec!["ws://relay1.io".to_string(), "ws://relay2.io".to_string()], 214 relays: vec!["ws://relay1.io".to_string(), "ws://relay2.io".to_string()],
215 maintainers: vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()],
120 } 216 }
121 .to_event(&TEST_KEY_1_KEYS) 217 .to_event(&TEST_KEY_1_KEYS)
122 .unwrap() 218 .unwrap()
@@ -146,12 +242,28 @@ mod tests {
146 } 242 }
147 243
148 #[test] 244 #[test]
245 fn git_server() {
246 assert_eq!(
247 RepoRef::try_from(create()).unwrap().git_server,
248 "https://localhost:1000",
249 )
250 }
251
252 #[test]
149 fn relays() { 253 fn relays() {
150 assert_eq!( 254 assert_eq!(
151 RepoRef::try_from(create()).unwrap().relays, 255 RepoRef::try_from(create()).unwrap().relays,
152 vec!["ws://relay1.io".to_string(), "ws://relay2.io".to_string()], 256 vec!["ws://relay1.io".to_string(), "ws://relay2.io".to_string()],
153 ) 257 )
154 } 258 }
259
260 #[test]
261 fn maintainers() {
262 assert_eq!(
263 RepoRef::try_from(create()).unwrap().maintainers,
264 vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()],
265 )
266 }
155 } 267 }
156 268
157 mod to_event { 269 mod to_event {
@@ -186,6 +298,21 @@ mod tests {
186 } 298 }
187 299
188 #[test] 300 #[test]
301 fn git_server() {
302 assert!(create().tags.iter().any(|t| t.as_vec()[0].eq("git-server")
303 && t.as_vec()[1].eq("https://localhost:1000")))
304 }
305
306 #[test]
307 fn git_server_as_reference() {
308 assert!(
309 create().tags.iter().any(
310 |t| t.as_vec()[0].eq("r") && t.as_vec()[1].eq("https://localhost:1000")
311 )
312 )
313 }
314
315 #[test]
189 fn root_commit_as_reference() { 316 fn root_commit_as_reference() {
190 assert!( 317 assert!(
191 create() 318 create()
@@ -209,8 +336,27 @@ mod tests {
209 } 336 }
210 337
211 #[test] 338 #[test]
339 fn maintainers() {
340 let event = create();
341 let p_tags = event
342 .tags
343 .iter()
344 .filter(|t| t.as_vec()[0].eq("p"))
345 .collect::<Vec<&nostr::Tag>>();
346 assert_eq!(p_tags[0].as_vec().len(), 2);
347 assert_eq!(
348 p_tags[0].as_vec()[1],
349 TEST_KEY_1_KEYS.public_key().to_string()
350 );
351 assert_eq!(
352 p_tags[1].as_vec()[1],
353 TEST_KEY_2_KEYS.public_key().to_string()
354 );
355 }
356
357 #[test]
212 fn no_other_tags() { 358 fn no_other_tags() {
213 assert_eq!(create().tags.len(), 6) 359 assert_eq!(create().tags.len(), 10)
214 } 360 }
215 } 361 }
216 } 362 }
diff --git a/src/sub_commands/claim.rs b/src/sub_commands/claim.rs
index c0d26dd..2573267 100644
--- a/src/sub_commands/claim.rs
+++ b/src/sub_commands/claim.rs
@@ -10,7 +10,7 @@ use crate::{
10 client::Connect, 10 client::Connect,
11 git::{Repo, RepoActions}, 11 git::{Repo, RepoActions},
12 login, 12 login,
13 repo_ref::RepoRef, 13 repo_ref::{extract_pks, get_repo_config_from_yaml, save_repo_config_to_yaml, RepoRef},
14 Cli, 14 Cli,
15}; 15};
16 16
@@ -22,6 +22,9 @@ pub struct SubCommandArgs {
22 #[clap(short, long)] 22 #[clap(short, long)]
23 /// optional description 23 /// optional description
24 description: Option<String>, 24 description: Option<String>,
25 #[clap(short, long, value_parser, num_args = 1..)]
26 /// relays contributors push patches and comments to
27 relays: Vec<String>,
25} 28}
26 29
27pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { 30pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
@@ -37,6 +40,8 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
37 40
38 // TODO: check for empty repo 41 // TODO: check for empty repo
39 // TODO: check for existing maintaiers file 42 // TODO: check for existing maintaiers file
43
44 let repo_config_result = get_repo_config_from_yaml(&git_repo);
40 // TODO: check for other claims 45 // TODO: check for other claims
41 46
42 let name = match &args.title { 47 let name = match &args.title {
@@ -50,6 +55,8 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
50 .input(PromptInputParms::default().with_prompt("description (Optional)"))?, 55 .input(PromptInputParms::default().with_prompt("description (Optional)"))?,
51 }; 56 };
52 57
58 let git_server = git_repo.get_origin_url()?;
59
53 #[cfg(not(test))] 60 #[cfg(not(test))]
54 let mut client = Client::default(); 61 let mut client = Client::default();
55 #[cfg(test)] 62 #[cfg(test)]
@@ -59,11 +66,41 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
59 66
60 client.set_keys(&keys).await; 67 client.set_keys(&keys).await;
61 68
62 // TODO: choice input defaulting to user relay list filtered by non paid relays 69 let mut maintainers = vec![keys.public_key()];
63 let repo_relays: Vec<String> = vec![ 70
64 "ws://localhost:8055".to_string(), 71 let repo_relays: Vec<String> = if !args.relays.is_empty() {
65 "ws://localhost:8056".to_string(), 72 args.relays.clone()
66 ]; 73 } else if let Ok(config) = &repo_config_result {
74 config.relays.clone()
75 } else {
76 // TODO: choice input defaulting to user relay list filtered by non paid relays
77 // TODO: allow manual input for more relays
78 // TODO: reccommend some free relays
79 user_ref.relays.write()
80 };
81
82 if let Ok(config) = &repo_config_result {
83 maintainers = extract_pks(config.maintainers.clone())?;
84 }
85
86 // if yaml file doesnt exist or needs updating
87 if match &repo_config_result {
88 Ok(config) => {
89 !(extract_pks(config.maintainers.clone())?.eq(&maintainers)
90 && config.relays.eq(&repo_relays))
91 }
92 Err(_) => true,
93 } {
94 save_repo_config_to_yaml(&git_repo, maintainers.clone(), repo_relays.clone())?;
95 println!(
96 "maintainers.yaml {}. commit and push.",
97 if repo_config_result.is_err() {
98 "created"
99 } else {
100 "updated"
101 }
102 );
103 }
67 104
68 println!("publishing repostory reference..."); 105 println!("publishing repostory reference...");
69 106
@@ -71,10 +108,13 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
71 name, 108 name,
72 description, 109 description,
73 root_commit: root_commit.to_string(), 110 root_commit: root_commit.to_string(),
111 git_server,
74 relays: repo_relays.clone(), 112 relays: repo_relays.clone(),
113 maintainers,
75 } 114 }
76 .to_event(&keys)?; 115 .to_event(&keys)?;
77 116
117 // TODO: send repo event to blaster
78 send_events( 118 send_events(
79 &client, 119 &client,
80 vec![repo_event], 120 vec![repo_event],
diff --git a/src/sub_commands/prs/create.rs b/src/sub_commands/prs/create.rs
index 5c4e578..5e41a56 100644
--- a/src/sub_commands/prs/create.rs
+++ b/src/sub_commands/prs/create.rs
@@ -100,12 +100,12 @@ pub async fn launch(
100 generate_pr_and_patch_events(&title, &description, &to_branch, &git_repo, &ahead, &keys)?; 100 generate_pr_and_patch_events(&title, &description, &to_branch, &git_repo, &ahead, &keys)?;
101 101
102 let repo_ref = repo_ref::fetch( 102 let repo_ref = repo_ref::fetch(
103 &git_repo,
103 git_repo 104 git_repo
104 .get_root_commit(&to_branch) 105 .get_root_commit(&to_branch)
105 .context("failed to get root commit of the repository")? 106 .context("failed to get root commit of the repository")?
106 .to_string(), 107 .to_string(),
107 &client, 108 &client,
108 // TODO: get relay list from local yaml file
109 user_ref.relays.write(), 109 user_ref.relays.write(),
110 ) 110 )
111 .await?; 111 .await?;
diff --git a/src/sub_commands/prs/list.rs b/src/sub_commands/prs/list.rs
index 13f29fd..a6aa8b7 100644
--- a/src/sub_commands/prs/list.rs
+++ b/src/sub_commands/prs/list.rs
@@ -45,6 +45,7 @@ pub async fn launch(
45 let client = <MockConnect as std::default::Default>::default(); 45 let client = <MockConnect as std::default::Default>::default();
46 46
47 let repo_ref = repo_ref::fetch( 47 let repo_ref = repo_ref::fetch(
48 &git_repo,
48 root_commit.to_string(), 49 root_commit.to_string(),
49 &client, 50 &client,
50 client.get_more_fallback_relays().clone(), 51 client.get_more_fallback_relays().clone(),
diff --git a/src/sub_commands/pull.rs b/src/sub_commands/pull.rs
index a6513e8..a656e09 100644
--- a/src/sub_commands/pull.rs
+++ b/src/sub_commands/pull.rs
@@ -38,6 +38,7 @@ pub async fn launch() -> Result<()> {
38 let client = <MockConnect as std::default::Default>::default(); 38 let client = <MockConnect as std::default::Default>::default();
39 39
40 let repo_ref = repo_ref::fetch( 40 let repo_ref = repo_ref::fetch(
41 &git_repo,
41 root_commit.to_string(), 42 root_commit.to_string(),
42 &client, 43 &client,
43 client.get_more_fallback_relays().clone(), 44 client.get_more_fallback_relays().clone(),
diff --git a/src/sub_commands/push.rs b/src/sub_commands/push.rs
index 968aa0a..7c64bb2 100644
--- a/src/sub_commands/push.rs
+++ b/src/sub_commands/push.rs
@@ -41,6 +41,7 @@ pub async fn launch(cli_args: &Cli) -> Result<()> {
41 let mut client = <MockConnect as std::default::Default>::default(); 41 let mut client = <MockConnect as std::default::Default>::default();
42 42
43 let repo_ref = repo_ref::fetch( 43 let repo_ref = repo_ref::fetch(
44 &git_repo,
44 root_commit.to_string(), 45 root_commit.to_string(),
45 &client, 46 &client,
46 client.get_more_fallback_relays().clone(), 47 client.get_more_fallback_relays().clone(),