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>2023-05-21 11:14:47 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2023-05-21 11:14:47 +0000
commit0067804cc00e94ce2b7043e67f9ff50968525479 (patch)
tree2accdc6d4e9b73df4f20499238ec24f24a52a1b8 /src
parent5c5feaa732363e32e2a980a887fa42b4394b1a5e (diff)
v0.0.1-alpha funcs
Diffstat (limited to 'src')
-rw-r--r--src/funcs/apply_patches.rs95
-rw-r--r--src/funcs/checkout_branch.rs47
-rw-r--r--src/funcs/create_branch_and_pr.rs212
-rw-r--r--src/funcs/create_local_branch_from_user_input.rs57
-rw-r--r--src/funcs/create_patches.rs122
-rw-r--r--src/funcs/find_commits_ahead.rs42
-rw-r--r--src/funcs/find_latest_patch.rs137
-rw-r--r--src/funcs/find_select_recent_repos.rs52
-rw-r--r--src/funcs/get_branch_event_from_user_input.rs73
-rw-r--r--src/funcs/get_updates_of_patches.rs263
-rw-r--r--src/funcs/mod.rs10
11 files changed, 1110 insertions, 0 deletions
diff --git a/src/funcs/apply_patches.rs b/src/funcs/apply_patches.rs
new file mode 100644
index 0000000..7d3147e
--- /dev/null
+++ b/src/funcs/apply_patches.rs
@@ -0,0 +1,95 @@
1use std::{path::PathBuf, process::Command, fs::{self, File}, io::Write};
2
3use git2::Repository;
4use nostr::Event;
5
6use crate::{utils::save_event, ngit_tag::{tag_extract_value, tag_is_commit}};
7
8
9pub fn apply_patches(
10 git_repo: &Repository,
11 repo_dir_path: &PathBuf,
12 patches_correctly_ordered:&mut Vec<Event>,
13
14) {
15 // check git is installed
16 match Command::new("git").output() {
17 Ok(_o) => (),
18 Err(_e) => {
19 panic!("git isn't installed :( Install git and then you can use ngit :)");
20 }
21 }
22 let ngit_path = repo_dir_path.join(".ngit");
23
24 println!("{} commits to apply",patches_correctly_ordered.len());
25 fs::create_dir(ngit_path.join("patches/mbox"))
26 .expect("patches/mbox to be created by create_dir");
27 for (i, event) in patches_correctly_ordered.iter().enumerate() {
28 save_event(
29 ngit_path.join(format!(
30 "patches/{}.json",
31 tag_extract_value(
32 &event.tags.iter().find(|t| tag_is_commit(t))
33 .expect("each patch contains commit tag")
34 )
35 )),
36 &event,
37 )
38 .expect("patch to be saved with [commit_id].json using save_event");
39 // extract mbox patch and save to file for 'git am' to recieve
40 let patch_path = format!("patches/mbox/{:0>5}.patch",i);
41 let mut f = File::create(ngit_path.join(&patch_path))
42 .expect("patch file can be created at patch_path location");
43 f.write_all(&event.content.as_bytes())
44 .expect("can use write_all to write event content to patch file");
45 // gitoxide or libgit2 do not support applying patches whilst maintaining the commit ids so we fall back to indirectly using git
46 // it turns out that git am doesnt retain commit ids. for now we will modify the committer author and timestamp to correct the commit id.
47 match Command::new("git")
48 .current_dir(&repo_dir_path)
49 .args([
50 "am",
51 ngit_path.join(&patch_path).to_string_lossy().to_string().as_str(),
52 ])
53 .output() {
54 Ok(_o) => {
55 let mut revwalk = git_repo.revwalk()
56 .expect("revwalk not to error");
57 revwalk.push_head()
58 .expect("revwalk.push_head not to error");
59
60 for (i, oid) in revwalk.enumerate() {
61 if i == 0 {
62 let old_commit = git_repo.find_commit(
63 oid
64 .expect("oid of newly added commit")
65 )
66 .expect("commit from newly added commit oid");
67 // create commit using amend with relects the original commit id (assumes committer should be identical to author
68 // TODO: in git push add a tag if the committer information is different to author. Then here use that info instead.
69 let updated_commit_oid = old_commit.amend(
70 None,
71 None,
72 Some(&old_commit.author()),
73 None,
74 None,
75 None,
76 )
77 .expect("ammend commit to produce new oid");
78 // replace the commit with the wrong oid with the newly created one with the correct oid
79 git_repo.head()
80 .expect("to return head of git_repo")
81 .set_target(
82 updated_commit_oid,
83 "ref commit with fix committer details",
84 )
85 .expect("branch to be updated with fixed commt");
86 }
87 };
88 },
89 Err(_e) => { panic!(":( git error: {:#?}",_e); },
90 }
91 }
92 // clear up by removing mbox directory
93 fs::remove_dir_all(ngit_path.join("patches/mbox"))
94 .expect("patches/mbox to be removed recursively now we are done with it");
95}
diff --git a/src/funcs/checkout_branch.rs b/src/funcs/checkout_branch.rs
new file mode 100644
index 0000000..df08ce8
--- /dev/null
+++ b/src/funcs/checkout_branch.rs
@@ -0,0 +1,47 @@
1use git2::{Branch, Repository};
2
3pub fn checkout_branch(
4 git_repo: &Repository,
5 branch: Branch,
6) {
7 // checkout branch
8 let (object, reference) = git_repo.revparse_ext(
9 branch.name()
10 .expect("valid name not to error")
11 .expect("valid name name to exist")
12 )
13 .expect("object to be located from branch name");
14 match git_repo.checkout_tree(&object, None) {
15 Ok(_) => (),
16 Err(err) => {
17 panic!("You cannot checkout branch because {}", err.message());
18 }
19 }
20 // set head to branch
21 match reference {
22 Some(gref) => git_repo.set_head(gref.name().unwrap()),
23 None => git_repo.set_head_detached(object.id()),
24 }
25 .expect("succesfully set head");
26}
27
28pub fn checkout_branch_from_name(
29 git_repo: &Repository,
30 branch_name: &String,
31) {
32 // checkout branch
33 let (object, reference) = git_repo.revparse_ext(branch_name)
34 .expect("object to be located from branch name");
35 match git_repo.checkout_tree(&object, None) {
36 Ok(_) => (),
37 Err(err) => {
38 panic!("You cannot checkout branch because {}", err.message());
39 }
40 }
41 // set head to branch
42 match reference {
43 Some(gref) => git_repo.set_head(gref.name().unwrap()),
44 None => git_repo.set_head_detached(object.id()),
45 }
46 .expect("succesfully set head");
47} \ No newline at end of file
diff --git a/src/funcs/create_branch_and_pr.rs b/src/funcs/create_branch_and_pr.rs
new file mode 100644
index 0000000..f67dfb3
--- /dev/null
+++ b/src/funcs/create_branch_and_pr.rs
@@ -0,0 +1,212 @@
1use std::{path::PathBuf};
2
3use dialoguer::{Input, theme::ColorfulTheme, Confirm};
4use nostr::{Keys, prelude::{Nip19Event, ToBech32}};
5use nostr_sdk::blocking::Client;
6
7use crate::{repos::{repo::Repo, init::InitializeRepo}, branch_refs::BranchRefs, cli_helpers::multi_select_with_add, groups::{init::{InitializeGroup}, group::{Group}}, config::{MyConfig, save_conifg}, repo_config::RepoConfig, pull_request::initialize_pull_request};
8
9struct PullRequest {
10 pub title: String,
11 pub description: String,
12 pub tags: Vec<String>
13}
14/// returns branch_id
15pub fn create_branch_and_pr(
16 local_branch_name: &String,
17 commits_to_push: usize,
18 repo_dir_path: &PathBuf,
19 repo: &Repo,
20 branch_refs: &mut BranchRefs,
21 keys: &Keys,
22 cfg: &mut MyConfig,
23 client: &Client,
24) -> String {
25 let new_branch_name: String = Input::with_theme(&ColorfulTheme::default())
26 .with_prompt(format!(
27 "push {} commits to a branch named",
28 commits_to_push,
29 ))
30 .with_initial_text(&local_branch_name.to_string())
31 .interact_text()
32 .unwrap();
33 let pr_details = match Confirm::with_theme(&ColorfulTheme::default())
34 .with_prompt("open a pull request?")
35 .default(true)
36 .interact()
37 .unwrap() {
38 false => None,
39 true => {
40 let title = Input::with_theme(&ColorfulTheme::default())
41 .with_prompt("title")
42 .with_initial_text(&new_branch_name)
43 .interact_text()
44 .unwrap();
45 let tags = multi_select_with_add(
46 vec![
47 "bugfix".to_string(),
48 "feature".to_string(),
49 ],
50 vec![
51 false,
52 false,
53 ],
54 "tags",
55 "new tag",
56 );
57 let description = Input::with_theme(&ColorfulTheme::default())
58 .with_prompt("description")
59 .interact_text()
60 .unwrap();
61 // into main / master (lookup (yes/no) - if no select from existing branches with mapping
62 Some(
63 PullRequest {
64 title,
65 tags,
66 description,
67 }
68 )
69 },
70 };
71 // create it now immediately before pushing the patches
72 let mut events_to_broadcast = vec![];
73
74 // create/store admin group
75 let admin_group = match &cfg.default_admin_group_event_serialized {
76 None => {
77 let new_admin_group = Group::new(
78 &InitializeGroup::new()
79 .members(
80 vec![
81 keys.public_key().to_string(),
82 ],
83 vec![],
84 )
85 .relays(&repo.relays),
86 &keys,
87 ).unwrap();
88 cfg.default_admin_group_event_serialized = Some(new_admin_group.events[0].as_json());
89 save_conifg(&cfg);
90 new_admin_group
91 },
92 Some(admin) => Group::new_from_json_event(admin.clone())
93 .expect("admin group event in MyConfig loads into Group"),
94 };
95 events_to_broadcast.push(admin_group.events[0].clone());
96 branch_refs.update(admin_group.events[0].clone());
97
98
99 // create group
100 let branch_group_ref = match branch_refs.is_authorized(
101 None,
102 &keys.public_key(),
103 )
104 .expect("main repo maintainers group is cached in .ngit")
105 {
106 // use repo maintainers group
107 true => branch_refs.maintainers_group(None)
108 .expect("main repo maintainers group is cached in .ngit")
109 .get_ref(),
110 // create branch group
111 false => {
112 let new_group = Group::new(
113 &InitializeGroup::new()
114 .name(
115 format!(
116 "branch-of:{},named:{}",
117 match &repo.name {
118 None => "untitled",
119 Some(s) => s.as_str(),
120 },
121 &new_branch_name,
122 ),
123 )
124 .admin(admin_group.get_ref())
125 .members(
126 vec![keys.public_key().to_string()],
127 vec![
128 branch_refs.maintainers_group(None)
129 .expect("repo maintainers group to exist in .ngit directory")
130 .get_ref()
131 ]
132 )
133 .relays(&repo.relays)
134 ,
135 &keys,
136 )
137 .expect("new branch group to be created");
138
139 events_to_broadcast.push(new_group.events[0].clone());
140 branch_refs.update(new_group.events[0].clone());
141 new_group.get_ref()
142 }
143 };
144
145 // create branch
146 let branch_init = InitializeRepo::new()
147 .name(&new_branch_name)
148 .relays(&repo.relays)
149 .root_repo(repo.id.to_string())
150 .maintainers_group(branch_group_ref)
151 .initialize(&keys);
152 events_to_broadcast.push(branch_init.clone());
153 branch_refs.update(branch_init.clone());
154
155 // TODO: create PR
156 match pr_details {
157 None => (),
158 Some(pr_details) => {
159 let pull_request_init = initialize_pull_request(
160 &keys,
161 &repo.id.to_string(),
162 &repo.id.to_string(),
163 &branch_init.id.to_string(),
164 &pr_details.title,
165 &pr_details.description,
166 pr_details.tags
167 );
168 events_to_broadcast.push(pull_request_init.clone());
169 branch_refs.update(pull_request_init.clone());
170 println!(
171 "pull request '{}' created with id: {}",
172 &pr_details.title,
173 Nip19Event::new(
174 pull_request_init.id.clone(),
175 vec![&repo.relays[0]],
176 )
177 .to_bech32()
178 .expect("Nip19Event to convert to to_bech32")
179 );
180 },
181 }
182
183 // add mapping to conifg.json
184 RepoConfig::open(repo_dir_path).set_mapping(
185 local_branch_name,
186 &branch_init.id.to_string(),
187 );
188
189 println!(
190 "branch '{}' created with id: {}",
191 &new_branch_name,
192 Nip19Event::new(
193 branch_init.id.clone(),
194 vec![&repo.relays[0]],
195 )
196 .to_bech32()
197 .expect("Nip19Event to convert to to_bech32")
198 );
199
200 // broadcast events
201 for e in &events_to_broadcast {
202 match client.send_event(e.clone()) {
203 Ok(_) => (),
204 // TODO: this isn't working - if a relay is specified with a type it will wait 30ish secs and then return successful
205 Err(e) => { println!("error broadcasting event: {}",e); },
206 }
207 // TODO: better error handling here / reporting. potentially warn if taking a while and report on troublesome relays
208 }
209
210 // return branch_id
211 branch_init.id.to_string()
212}
diff --git a/src/funcs/create_local_branch_from_user_input.rs b/src/funcs/create_local_branch_from_user_input.rs
new file mode 100644
index 0000000..94abb62
--- /dev/null
+++ b/src/funcs/create_local_branch_from_user_input.rs
@@ -0,0 +1,57 @@
1use std::path::PathBuf;
2
3use dialoguer::{Input, theme::ColorfulTheme};
4use git2::{Repository, Oid};
5
6use crate::repo_config::RepoConfig;
7
8use super::checkout_branch::checkout_branch;
9
10
11/// prompts user for name of local branch, creates the branch (checking if it is valid, if not looping) and returns it.
12pub fn create_local_branch_from_user_input(
13 repo_dir_path: &PathBuf,
14 git_repo: &Repository,
15 suggestion:&Option<String>,
16 commit_id:&String,
17 branch_id: &String,
18) -> String {
19
20 let target_commit = git_repo.find_commit(
21 Oid::from_str(
22 commit_id
23 )
24 .expect("commit_id supplied to be a valid Oid")
25 )
26 .expect("commit_id supplied exists in git repository");
27
28 loop {
29 let response: String = Input::with_theme(&ColorfulTheme::default())
30 .with_prompt("local branch name")
31 .with_initial_text(match &suggestion {
32 None => "".to_string(),
33 Some(s) => s.to_string(),
34 })
35 .report(true)
36 .interact_text()
37 .unwrap();
38 match git_repo.branch(
39 &response,
40 &target_commit,
41 false,
42 ) {
43 Ok(branch) => {
44 // check out branch
45 checkout_branch(git_repo, branch);
46 // set mapping
47 let mut repo_config = RepoConfig::open(repo_dir_path);
48 repo_config.set_mapping(&response, branch_id);
49 break response;
50 },
51 Err(_) => {
52 println!("not a valid nevent, note or hex string. try again. (or a local branch with that name exists)");
53 continue
54 },
55 }
56 }
57}
diff --git a/src/funcs/create_patches.rs b/src/funcs/create_patches.rs
new file mode 100644
index 0000000..2a877a0
--- /dev/null
+++ b/src/funcs/create_patches.rs
@@ -0,0 +1,122 @@
1use git2::{Email, EmailCreateOptions};
2use indicatif::ProgressBar;
3use nostr::{Keys, Event};
4
5use crate::{repos::repo::Repo, utils::{create_client, load_event, save_event}, ngit_tag::{tag_is_commit, tag_extract_value}, patch::initialize_patch, repo_config::RepoConfig};
6
7pub fn create_and_broadcast_patches_from_oid(
8 oids_ancestors_first: Vec<git2::Oid>,
9 git_repo: &git2::Repository,
10 repo_dir_path: &std::path::PathBuf,
11 repo: &Repo,
12 branch_id: &String,
13 keys: &Keys,
14) {
15 let mut patches: Vec<Event> = vec![];
16 for oid in oids_ancestors_first {
17 patches.push(
18 create_and_save_patch_from_oid(
19 &oid,
20 &patches,
21 &git_repo,
22 &repo_dir_path.join(".ngit"),
23 &repo,
24 &branch_id,
25 &keys,
26 )
27 );
28 }
29
30 // update branch update timestamp
31 match patches.last() {
32 Some(p) => {
33 let mut repo_config = RepoConfig::open(&repo_dir_path);
34 repo_config.set_last_patch_update_time(
35 branch_id.clone(),
36 p.created_at.clone(),
37 );
38 }
39 None => (),
40 };
41
42
43 // broadcast patches
44 let spinner = ProgressBar::new_spinner();
45 spinner.set_message(format!("Broadcasting... if this takes 20s+, there was a problem broadcasting to one or more relays even if it says 'Pushed {} patches!'.",patches.len()));
46
47 let client = create_client(&keys, repo.relays.clone())
48 .expect("create_client to return client for create_and_broadcast_patches");
49 for e in &patches {
50 match client.send_event(e.clone()) {
51 Ok(_) => (),
52 // TODO: this isn't working - if a relay is specified with a type it will wait 30ish secs and then return successful
53 Err(e) => { println!("error broadcasting patch event: {}",e); },
54 }
55 // TODO: better error handling here / reporting. potentially warn if taking a while and report on troublesome relays
56 }
57 spinner.finish_with_message(format!("Pushed {} commits!.",patches.len()));
58}
59
60pub fn create_and_save_patch_from_oid(
61 oid: &git2::Oid,
62 patches: &Vec<Event>,
63 git_repo: &git2::Repository,
64 ngit_path: &std::path::PathBuf,
65 repo: &Repo,
66 branch_id: &String,
67 keys: &Keys,
68) -> Event {
69 let commit_id = format!("{}",oid);
70 let commit = git_repo.find_commit(*oid)
71 .expect("revwalk returns oid that matches a comit in the repository");
72 let message = match commit.message() {
73 None => "",
74 Some(m) => m
75 }.to_string();
76 let email = Email::from_commit(
77 &commit,
78 &mut EmailCreateOptions::default(),
79 ).expect("renders a commit as an email diff");
80 let parent_commit_id: Option<String> = match &commit.parent_id(0) {
81 Ok(parent_oid) => Some(format!("{}",parent_oid)),
82 Err(_) => None,
83 };
84 let parent_patch_id = match &parent_commit_id {
85 None => None,
86 Some(id) => Some({
87 // search for parent in current batch of patches
88 match patches.iter().find(|p|
89 p.tags.iter().find(|t|tag_is_commit(t) && id.clone() == tag_extract_value(&t)).is_some()
90 ) {
91 Some(p) => p.id.to_string(),
92 None => {
93 let parent_patch_path = ngit_path.join(format!("patches/{}.json",id));
94 if parent_patch_path.exists() {
95 load_event(parent_patch_path)
96 .expect("patch in json file that exists produces valid event")
97 .id.to_string()
98 } else {
99 panic!("cannot find parent patch. ngit may have ordered ancestors without patches incorrectly");
100 }
101 },
102 }
103 }),
104 };
105 let event = initialize_patch(
106 &keys,
107 &repo.id.to_string(),
108 &branch_id,
109 &email.as_slice(),
110 &message,
111 &vec![commit_id.to_string()],
112 parent_patch_id,
113 parent_commit_id,
114 );
115 // save patch
116 save_event(ngit_path.join(
117 // TODO: consider what happens if a commit gets published twice, which one would get priority? The one from a maintiner?
118 format!("patches/{}.json",commit_id),
119 ), &event)
120 .expect("save_event to store event in /patches");
121 event
122}
diff --git a/src/funcs/find_commits_ahead.rs b/src/funcs/find_commits_ahead.rs
new file mode 100644
index 0000000..3358b11
--- /dev/null
+++ b/src/funcs/find_commits_ahead.rs
@@ -0,0 +1,42 @@
1use std::path::PathBuf;
2
3use git2::{Repository, Oid};
4
5pub fn find_commits_ahead (
6 git_repo: &Repository,
7 repo_dir_path: &PathBuf,
8 branch_name: &String,
9
10) -> Vec<Oid> {
11 // get revwalk of commits
12 let mut revwalk = git_repo.revwalk()
13 .expect("revwalk not to error");
14 // using the specified branch
15 match revwalk.push_glob(&branch_name) {
16 Ok(_) => (),
17 // errors when there are no commits
18 Err(_) => { return vec![]; }
19 }
20
21 // find commit that need pushing
22 let mut revwalk = git_repo.revwalk()
23 .expect("git_repo.revwalk() to not error");
24 revwalk.push_head()
25 .expect("revwalk.push_head not to error. already checked for some commits. headless?");
26
27 let mut new_commits = vec![];
28
29 for oid in revwalk {
30 // whatever branch we are on, we are only interested in returning how many unpublished commits we are ahead.
31 if repo_dir_path.join(".ngit").join(format!(
32 "patches/{}.json",
33 oid.as_ref().expect("oid to refernce commits").clone(),
34 )).exists() {
35 break;
36 }
37 new_commits.push(oid.expect("oid to refernce commits"));
38 }
39 // most often used ancestor first
40 new_commits.reverse();
41 new_commits
42}
diff --git a/src/funcs/find_latest_patch.rs b/src/funcs/find_latest_patch.rs
new file mode 100644
index 0000000..717d6c1
--- /dev/null
+++ b/src/funcs/find_latest_patch.rs
@@ -0,0 +1,137 @@
1use std::path::PathBuf;
2
3use nostr::{ Event };
4
5use crate::{ngit_tag::{tag_is_branch, tag_extract_value, tag_is_commit}, branch_refs::BranchRefs, utils::load_event, kind::Kind};
6
7/// finds latest patch that needs applying. It might not be the latest created_at date if an earlier patch was 'merged' more recently
8pub fn find_latest_patch(
9 branch_id: &String,
10 patch_events:&Vec<Event>,
11 merges_into_branch: &Vec<Event>,
12 branch_refs:&BranchRefs,
13 repo_dir_path: &PathBuf,
14) -> Option<Event> {
15
16 // ensure only patch events make it into patch_events - we cant rely on relays for this
17 let patch_events:Vec<Event> = patch_events.iter().filter(|p|
18 // kind is patch
19 p.kind == nostr_sdk::Kind::Custom(u64::from(Kind::Patch))
20 ).map(|p|p.clone()).collect();
21
22 let directly_authorised_patches: Vec<Event> = patch_events.iter().filter(|p|
23 // kind is patch
24 p.kind == nostr_sdk::Kind::Custom(u64::from(Kind::Patch))
25 // on branch
26 && p.tags.iter().any(
27 |t|tag_is_branch(t)
28 && tag_extract_value(t) == branch_id.clone()
29 )
30 // authorized
31 && match &branch_refs.is_authorized(Some(&branch_id), &p.pubkey) {
32 None => { false },
33 Some(is_authorized) => { is_authorized.clone() }
34 }
35 ).map(|p|p.clone()).collect();
36
37 let latest_authorized_patch = find_latest_event(&directly_authorised_patches);
38
39 let authorised_merges: Vec<Event> = merges_into_branch.iter().filter(|m|
40 // kind is merge
41 m.kind == nostr_sdk::Kind::Custom(u64::from(Kind::Merge))
42 // into branch
43 && m.tags.iter().any(
44 |t|tag_is_branch(t)
45 && tag_extract_value(t) == branch_id.clone()
46 )
47 // merge authorized
48 && match &branch_refs.is_authorized(Some(&branch_id), &m.pubkey) {
49 None => { false },
50 Some(is_authorized) => { is_authorized.clone() }
51 }
52 ).map(|p|p.clone()).collect();
53
54 let latest_authorised_merge = find_latest_event(&authorised_merges);
55
56 // find latest patch
57
58 match latest_authorised_merge {
59 // no merge - return patch or None
60 None => latest_authorized_patch,
61 Some(m) => {
62 match latest_authorized_patch {
63 // merge but no patch, return the patch related to the merge
64 None => {
65 Some(get_merge_patch(
66 &m,
67 &patch_events,
68 repo_dir_path,
69 ))
70 },
71 // a merge and a patch
72 Some(p) => {
73 // return the patch if it is later than merge
74 if m.created_at < p.created_at {
75 Some(p.clone())
76 }
77 // return the patch related to the merge if the merge is later
78 else {
79 Some(get_merge_patch(
80 &m,
81 &patch_events,
82 repo_dir_path,
83 ))
84 }
85 }
86 }
87 }
88 }
89}
90
91fn find_latest_event(events:&Vec<Event>) -> Option<Event> {
92 let mut latest = match events.get(0) {
93 None => { return None },
94 Some(e) => e.clone(),
95 };
96 for e in events.iter() {
97 if e.created_at > latest.created_at {
98 latest = e.clone();
99 }
100 }
101 Some(latest)
102}
103
104fn get_merge_patch(
105 merge: &Event,
106 patch_events: &Vec<Event>,
107 repo_dir_path: &PathBuf,
108) -> Event{
109 let commit_id = tag_extract_value(
110 merge.tags.iter().find(|tag| tag_is_commit(tag))
111 .expect("merge event will have a commit tag")
112 );
113 // search in patch_events vector
114 match patch_events.iter().find(|p|
115 tag_extract_value(
116 p.tags.iter().find(|tag| tag_is_commit(tag))
117 .expect("patch event will have a commit tag")
118 ) == *commit_id
119 ) {
120 // found merge patch in patch_events
121 Some(patch) => patch.clone(),
122 None => {
123 let patch_path = repo_dir_path.join(format!(
124 ".ngit/patches/{}.json",
125 commit_id
126 ));
127 if patch_path.exists() {
128 // found merge patch in .ngit/patches
129 load_event(patch_path)
130 .expect("patch at path that exists renders as event")
131 }
132 else {
133 panic!("cannot find patch from merge event in event vector or .ngit folder");
134 }
135 },
136 }
137}
diff --git a/src/funcs/find_select_recent_repos.rs b/src/funcs/find_select_recent_repos.rs
new file mode 100644
index 0000000..79054c0
--- /dev/null
+++ b/src/funcs/find_select_recent_repos.rs
@@ -0,0 +1,52 @@
1use dialoguer::Select;
2use nostr::{Event, EventId, Filter};
3use nostr_sdk::blocking::Client;
4
5use crate::{kind::Kind, repos::repo::Repo};
6
7pub fn find_select_recent_repos(
8 client: &Client,
9) -> EventId {
10
11 let mut repo_events: Vec<Event> = client.get_events_of(
12 vec![
13 Filter::new()
14 .hashtag("ngit-format-0.0.1")
15 .kind(
16 Kind::InitializeRepo.into_sdk_custom_kind(),
17 )
18 .limit(10),
19 ],
20 None,
21 )
22 .expect("get_events_of to not return an error");
23
24 repo_events.sort();
25 repo_events.dedup();
26
27 if repo_events.is_empty() {
28 panic!("could not find any repositories. Create one with ngit init?")
29 }
30
31 let repos: Vec<Repo> = repo_events.iter().map(|r|
32 Repo::new_from_event(r.clone())
33 .expect("repo to be well formed event")
34 ).collect();
35 let repo_names: Vec<String> = repos.iter().map(|r|
36 match r.name.clone() {
37 None => "(untitled)".to_string(),
38 Some(name) => name,
39 }
40 ).collect();
41
42 // select pr to review
43 let i = Select::new()
44 .with_prompt("clone for a repository on selected relays")
45 .items(&repo_names)
46 .report(false)
47 .interact()
48 .unwrap();
49 // display nevent
50 println!("selected repo: {} {}",repo_names[i], repos[i].nevent());
51 repos[i].id
52} \ No newline at end of file
diff --git a/src/funcs/get_branch_event_from_user_input.rs b/src/funcs/get_branch_event_from_user_input.rs
new file mode 100644
index 0000000..1b2cd03
--- /dev/null
+++ b/src/funcs/get_branch_event_from_user_input.rs
@@ -0,0 +1,73 @@
1use std::path::PathBuf;
2
3use nostr::{Event};
4
5use crate::{branch_refs::BranchRefs, repo_config::RepoConfig, cli_helpers::valid_event_id_from_input};
6
7pub fn get_branch_event_from_user_input(
8 branch_string_param:&Option<String>,
9 branch_refs: &BranchRefs,
10 repo_dir_path: &PathBuf,
11) -> Event {
12 get_branch_event_with_options(
13 branch_string_param,
14 branch_refs,
15 repo_dir_path,
16 true,
17 )
18}
19
20pub fn get_unmapped_branch_event_from_user_input(
21 branch_string_param:&Option<String>,
22 branch_refs: &BranchRefs,
23 repo_dir_path: &PathBuf,
24) -> Event {
25 get_branch_event_with_options(
26 branch_string_param,
27 branch_refs,
28 repo_dir_path,
29 false,
30 )
31}
32
33fn get_branch_event_with_options(
34 branch_string_param:&Option<String>,
35 branch_refs: &BranchRefs,
36 repo_dir_path: &PathBuf,
37 retrun_unmapped_branches: bool,
38) -> Event {
39
40 let mut string_param = branch_string_param.clone();
41 loop {
42 let valid_id = valid_event_id_from_input(
43 string_param.clone(),
44 &"nevent note or hex of remote branch to pull".to_string(),
45 );
46
47 match branch_refs.branches.iter().find(|g| g.id.eq(&valid_id)) {
48 Some(branch_event) => {
49 let repo_config = RepoConfig::open(repo_dir_path);
50 if !retrun_unmapped_branches {
51 match repo_config.branch_name_from_id(&valid_id.to_string()) {
52 // branch is alreay mapped
53 Some(name) => {
54 println!(
55 "local branch '{}' is already linked to this nostr branch. having multiple local branches linked to a nostr branch isn't supported right now. feel free to create a feature request :)",
56 name,
57 );
58 string_param = None;
59 continue
60 }
61 None => (),
62 }
63 }
64 break branch_event.clone();
65 },
66 None => {
67 println!("valid id but the branch cannot be found in this respository on the specified relays. try again.");
68 string_param = None;
69 continue
70 }
71 }
72 }
73}
diff --git a/src/funcs/get_updates_of_patches.rs b/src/funcs/get_updates_of_patches.rs
new file mode 100644
index 0000000..ad28774
--- /dev/null
+++ b/src/funcs/get_updates_of_patches.rs
@@ -0,0 +1,263 @@
1use std::{path::PathBuf, str::FromStr};
2
3use git2::Repository;
4use nostr::{Event, Filter, EventId};
5use nostr_sdk::blocking::Client;
6
7use crate::{ngit_tag::{tag_is_patch_parent, tag_is_initial_commit, tag_extract_value, tag_is_patch, tag_is_branch, tag_is_commit_parent, tag_is_commit}, utils::{load_event}, funcs::find_latest_patch::find_latest_patch, patch::{patch_commit_id, patch_is_commit}, branch_refs::BranchRefs, repo_config::RepoConfig, kind::Kind};
8
9
10/// ancessor patch events first
11pub fn get_updates_of_patches (
12 client: &Client,
13 branch_refs: &mut BranchRefs,
14 git_repo: &Repository,
15 repo_dir_path: &PathBuf,
16 branch_id: &String,
17 branch_name: &Option<String>,
18 pull_new_branch: bool,
19) -> Vec<Event> {
20
21 let repo_config = RepoConfig::open(repo_dir_path);
22 let last_patch_timestamp = repo_config.last_patch_update_time(branch_id.clone());
23
24 // create direct patches filter
25 let direct_patches_filter = Filter::new()
26 .event(
27 EventId::from_str(branch_id)
28 .expect("branch_id to render as EventId")
29 )
30 .kinds(vec![Kind::Patch.into_sdk_custom_kind()]);
31
32 let mut filters = vec![
33 match &last_patch_timestamp {
34 None => direct_patches_filter,
35 Some(timestamp) => {
36 direct_patches_filter.since(timestamp.clone())
37 }
38 }
39 ];
40
41 // get maintainers group
42 if branch_refs.maintainers_group(Some(&branch_id)).is_none() {
43 // fetch branch mantainers group and check again
44 client.add_relays(
45 branch_refs.branch_as_repo(Some(branch_id))
46 .relays
47 .clone().iter().map(|url| (url, None)).collect()
48 )
49 .expect("branch relays to be added to client");
50 let mut group_events = client.get_events_of(
51 vec![
52 // use the opportunity to get all the remaining groups
53 Filter::new().ids(branch_refs.group_ids_for_branches_without_cached_groups()),
54 ],
55 None,
56 )
57 .expect("get_events_of to not return an error");
58 group_events.sort();
59 group_events.dedup();
60 branch_refs.updates(group_events);
61 }
62
63 // create indirect pacthes filter
64 let merges_into_branch: Vec<Event> = branch_refs.merges.iter().filter(|event|
65 // merged into branch
66 event.tags.iter().any(|t|
67 tag_is_branch(t)
68 && tag_extract_value(t) == branch_id.clone()
69 )
70 // merge timestamp is after last_patch_timestamp - we already have patches before this date
71 && match &last_patch_timestamp {
72 None => true,
73 Some(timestamp) => timestamp < &event.created_at
74 }
75 // author is member of branch maintainers group
76 && branch_refs.is_authorized(Some(branch_id), &event.pubkey)
77 .expect("found group event for branch after checking on speficied relays")
78 ).map(|e|e.clone())
79 .collect();
80
81 if !merges_into_branch.is_empty() {
82 filters.push(
83 // ids for all patches referenced in merges
84 Filter::new()
85 .ids(
86 merges_into_branch.iter().flat_map(|event|
87 event.tags.iter()
88 .filter(|t| tag_is_patch(t))
89 .map(|t| tag_extract_value(t).clone())
90 .collect::<Vec<String>>()
91 )
92 .collect::<Vec<String>>()
93 )
94 // .kinds(vec![Kind::Patch.into_sdk_custom_kind()])
95 )
96 }
97
98 // find patch events
99 let mut patch_events: Vec<Event> = client.get_events_of(
100 filters,
101 None,
102 )
103 .expect("get_events_of to not return an error when looking for patches");
104
105 patch_events.sort();
106 patch_events.dedup();
107
108 // find patch tip on branch
109 let latest_patch_on_branch = match find_latest_patch(
110 &branch_id,
111 &patch_events,
112 &merges_into_branch,
113 &branch_refs,
114 &repo_dir_path,
115 ) {
116 // no patches return empty vector
117 None => { return vec![] }, // for pull_new_branch do we set the branch to the latest commit referneced even if we have it?
118 Some(event) => event,
119 };
120
121 let mut new_patches_on_branch = vec![];
122 // for pull_new_branch - cycle through patch parents until we find any patch that exists in our commit history
123 if pull_new_branch {
124 let mut patch_event_id = latest_patch_on_branch.id.to_string();
125 let mut patch_commit_id = tag_extract_value(
126 latest_patch_on_branch.tags.iter().find(|t|tag_is_commit(t))
127 .expect("all patch events to have a commit tag")
128 );
129
130 loop {
131 let patch = match patch_events.iter().find(|p| p.id.to_string() == patch_event_id.clone()) {
132 // patch event found in patch_events
133 Some(patch) => patch,
134 None => {
135 // loop for parent locally
136 if repo_dir_path.join(format!(
137 ".ngit/patches/{}.json",
138 patch_commit_id,
139 )).exists() {
140 // break out of loop when we identify the commit where the branch begins
141 break
142 }
143 else {
144 panic!("cannot find parent patch locally or in patch_events. This will fail if the branch does not share a commit with main / master")
145 }
146 }
147 };
148 // add patch to list of patches to apply to new branch
149 new_patches_on_branch.push(patch.clone());
150 // prepare loop for next patch - set patch_event_id to current patches parent
151 patch_event_id = tag_extract_value(
152 patch.tags.iter().find(|t|tag_is_patch_parent(t))
153 .expect("patch to always have a patch parent.")
154 );
155 patch_commit_id = tag_extract_value(
156 patch.tags.iter().find(|t|tag_is_commit_parent(t))
157 .expect("patch to always have a commit parent. This will fail if the branch does not share a commit with main / master")
158 );
159 };
160 }
161
162 // cycle through patch parents until we the latest commit in our local branch, or error if detects a rebase (it exists in our branch history)
163 else {
164 // revwalk through branch to identify forced push
165 let mut revwalk = git_repo.revwalk()
166 .expect("revwalk to not error on git_repo");
167 match &branch_name {
168 Some(name) => {
169 revwalk.push(
170 git_repo.find_branch(
171 name.as_str(),
172 git2::BranchType::Local
173 )
174 .expect("branch found from the branch_name")
175 .get()
176 .peel_to_commit()
177 .expect("branch reference to peel back to a commit")
178 .id()
179 )
180 .expect("revwalk push_glob(branch_name) not to error if branch name is not None");
181 }
182 None => (),
183 }
184 let commit_ids_in_branch: Vec<String> = if branch_name.is_none() { vec![] } else {
185 revwalk.map(|oid|
186 oid
187 .expect("revwalk to produce oids without error")
188 .to_string()
189 ).collect()
190 };
191
192 let latest_commit: Option<&String> = match commit_ids_in_branch.get(0) {
193 None => None,
194 Some(latest_commit) => {
195 // return empty if latest patch is in current chain
196 if commit_ids_in_branch.iter().any(|id|
197 patch_commit_id(&latest_patch_on_branch) == id.to_string()
198 ) { return vec![]; }
199 Some(latest_commit)
200 },
201 };
202
203 // work back thorugh commit chain until we reach a commit in our branch history (tip or ealier for rebase)
204 new_patches_on_branch = vec![latest_patch_on_branch.clone()];
205 loop {
206 let next_parent_patch = new_patches_on_branch.last()
207 .expect("chain to contain at least latest_patch_on_main")
208 .clone();
209 match next_parent_patch.tags.iter().find(|t|tag_is_patch_parent(t)) {
210 None => {
211 // found root patch or error
212 next_parent_patch.tags.iter().find(|t|tag_is_initial_commit(t))
213 // tag_is_initial_commit is false when it should be true. is it always false or just the oposite?
214 .expect(
215 &format!(
216 "reach a patch which doesn't contain a either a tag_is_patch_parent or tag_is_initial_commit{:#?}",
217 &next_parent_patch
218 )
219 );
220 break;
221 },
222 Some(t) => {
223 let next_patch = match patch_events.iter().find(|event|event.id.to_string() == tag_extract_value(t)) {
224 None => {
225 let patch_path = repo_dir_path.join(format!(
226 ".ngit/patches/{}.json",
227 tag_extract_value(
228 next_parent_patch.tags.iter().find(|t|tag_is_commit_parent(t))
229 .expect("patch to always have a commit parent if it has a patch parent")
230 ),
231 ));
232 if patch_path.exists() {
233 load_event(patch_path)
234 .expect("patch json at location that exists loads into event")
235 }
236 else {
237 panic!("cannot find parent patch id {} from patch {:#?}",tag_extract_value(t), next_parent_patch);
238 }
239 },
240 Some(event) => event.clone(),
241 };
242 // if reached current tip - break
243 if latest_commit.is_some() && patch_is_commit(
244 &next_patch,
245 latest_commit.unwrap(),
246 ) { break; }
247 // detect rebase
248 if commit_ids_in_branch.iter().any(|id|
249 patch_commit_id(&next_patch) == id.to_string()
250 ) {
251 panic!("force push detected. This branch has been force pushed since you last pulled. ngit doesnt handle this yet");
252 }
253 // new patch
254 new_patches_on_branch.push(next_patch.clone());
255
256 },
257 }
258 }
259 }
260 // oldest first
261 new_patches_on_branch.reverse();
262 new_patches_on_branch
263}
diff --git a/src/funcs/mod.rs b/src/funcs/mod.rs
new file mode 100644
index 0000000..4d93921
--- /dev/null
+++ b/src/funcs/mod.rs
@@ -0,0 +1,10 @@
1pub mod apply_patches;
2pub mod checkout_branch;
3pub mod create_branch_and_pr;
4pub mod create_local_branch_from_user_input;
5pub mod create_patches;
6pub mod find_commits_ahead;
7pub mod find_latest_patch;
8pub mod find_select_recent_repos;
9pub mod get_branch_event_from_user_input;
10pub mod get_updates_of_patches; \ No newline at end of file