From 0067804cc00e94ce2b7043e67f9ff50968525479 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Sun, 21 May 2023 11:14:47 +0000 Subject: v0.0.1-alpha funcs --- src/funcs/apply_patches.rs | 95 ++++++++ src/funcs/checkout_branch.rs | 47 ++++ src/funcs/create_branch_and_pr.rs | 212 ++++++++++++++++++ src/funcs/create_local_branch_from_user_input.rs | 57 +++++ src/funcs/create_patches.rs | 122 +++++++++++ src/funcs/find_commits_ahead.rs | 42 ++++ src/funcs/find_latest_patch.rs | 137 ++++++++++++ src/funcs/find_select_recent_repos.rs | 52 +++++ src/funcs/get_branch_event_from_user_input.rs | 73 +++++++ src/funcs/get_updates_of_patches.rs | 263 +++++++++++++++++++++++ src/funcs/mod.rs | 10 + 11 files changed, 1110 insertions(+) create mode 100644 src/funcs/apply_patches.rs create mode 100644 src/funcs/checkout_branch.rs create mode 100644 src/funcs/create_branch_and_pr.rs create mode 100644 src/funcs/create_local_branch_from_user_input.rs create mode 100644 src/funcs/create_patches.rs create mode 100644 src/funcs/find_commits_ahead.rs create mode 100644 src/funcs/find_latest_patch.rs create mode 100644 src/funcs/find_select_recent_repos.rs create mode 100644 src/funcs/get_branch_event_from_user_input.rs create mode 100644 src/funcs/get_updates_of_patches.rs create mode 100644 src/funcs/mod.rs (limited to 'src/funcs') 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 @@ +use std::{path::PathBuf, process::Command, fs::{self, File}, io::Write}; + +use git2::Repository; +use nostr::Event; + +use crate::{utils::save_event, ngit_tag::{tag_extract_value, tag_is_commit}}; + + +pub fn apply_patches( + git_repo: &Repository, + repo_dir_path: &PathBuf, + patches_correctly_ordered:&mut Vec, + +) { + // check git is installed + match Command::new("git").output() { + Ok(_o) => (), + Err(_e) => { + panic!("git isn't installed :( Install git and then you can use ngit :)"); + } + } + let ngit_path = repo_dir_path.join(".ngit"); + + println!("{} commits to apply",patches_correctly_ordered.len()); + fs::create_dir(ngit_path.join("patches/mbox")) + .expect("patches/mbox to be created by create_dir"); + for (i, event) in patches_correctly_ordered.iter().enumerate() { + save_event( + ngit_path.join(format!( + "patches/{}.json", + tag_extract_value( + &event.tags.iter().find(|t| tag_is_commit(t)) + .expect("each patch contains commit tag") + ) + )), + &event, + ) + .expect("patch to be saved with [commit_id].json using save_event"); + // extract mbox patch and save to file for 'git am' to recieve + let patch_path = format!("patches/mbox/{:0>5}.patch",i); + let mut f = File::create(ngit_path.join(&patch_path)) + .expect("patch file can be created at patch_path location"); + f.write_all(&event.content.as_bytes()) + .expect("can use write_all to write event content to patch file"); + // gitoxide or libgit2 do not support applying patches whilst maintaining the commit ids so we fall back to indirectly using git + // 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. + match Command::new("git") + .current_dir(&repo_dir_path) + .args([ + "am", + ngit_path.join(&patch_path).to_string_lossy().to_string().as_str(), + ]) + .output() { + Ok(_o) => { + let mut revwalk = git_repo.revwalk() + .expect("revwalk not to error"); + revwalk.push_head() + .expect("revwalk.push_head not to error"); + + for (i, oid) in revwalk.enumerate() { + if i == 0 { + let old_commit = git_repo.find_commit( + oid + .expect("oid of newly added commit") + ) + .expect("commit from newly added commit oid"); + // create commit using amend with relects the original commit id (assumes committer should be identical to author + // TODO: in git push add a tag if the committer information is different to author. Then here use that info instead. + let updated_commit_oid = old_commit.amend( + None, + None, + Some(&old_commit.author()), + None, + None, + None, + ) + .expect("ammend commit to produce new oid"); + // replace the commit with the wrong oid with the newly created one with the correct oid + git_repo.head() + .expect("to return head of git_repo") + .set_target( + updated_commit_oid, + "ref commit with fix committer details", + ) + .expect("branch to be updated with fixed commt"); + } + }; + }, + Err(_e) => { panic!(":( git error: {:#?}",_e); }, + } + } + // clear up by removing mbox directory + fs::remove_dir_all(ngit_path.join("patches/mbox")) + .expect("patches/mbox to be removed recursively now we are done with it"); +} 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 @@ +use git2::{Branch, Repository}; + +pub fn checkout_branch( + git_repo: &Repository, + branch: Branch, +) { + // checkout branch + let (object, reference) = git_repo.revparse_ext( + branch.name() + .expect("valid name not to error") + .expect("valid name name to exist") + ) + .expect("object to be located from branch name"); + match git_repo.checkout_tree(&object, None) { + Ok(_) => (), + Err(err) => { + panic!("You cannot checkout branch because {}", err.message()); + } + } + // set head to branch + match reference { + Some(gref) => git_repo.set_head(gref.name().unwrap()), + None => git_repo.set_head_detached(object.id()), + } + .expect("succesfully set head"); +} + +pub fn checkout_branch_from_name( + git_repo: &Repository, + branch_name: &String, +) { + // checkout branch + let (object, reference) = git_repo.revparse_ext(branch_name) + .expect("object to be located from branch name"); + match git_repo.checkout_tree(&object, None) { + Ok(_) => (), + Err(err) => { + panic!("You cannot checkout branch because {}", err.message()); + } + } + // set head to branch + match reference { + Some(gref) => git_repo.set_head(gref.name().unwrap()), + None => git_repo.set_head_detached(object.id()), + } + .expect("succesfully set head"); +} \ 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 @@ +use std::{path::PathBuf}; + +use dialoguer::{Input, theme::ColorfulTheme, Confirm}; +use nostr::{Keys, prelude::{Nip19Event, ToBech32}}; +use nostr_sdk::blocking::Client; + +use 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}; + +struct PullRequest { + pub title: String, + pub description: String, + pub tags: Vec +} +/// returns branch_id +pub fn create_branch_and_pr( + local_branch_name: &String, + commits_to_push: usize, + repo_dir_path: &PathBuf, + repo: &Repo, + branch_refs: &mut BranchRefs, + keys: &Keys, + cfg: &mut MyConfig, + client: &Client, +) -> String { + let new_branch_name: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "push {} commits to a branch named", + commits_to_push, + )) + .with_initial_text(&local_branch_name.to_string()) + .interact_text() + .unwrap(); + let pr_details = match Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("open a pull request?") + .default(true) + .interact() + .unwrap() { + false => None, + true => { + let title = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("title") + .with_initial_text(&new_branch_name) + .interact_text() + .unwrap(); + let tags = multi_select_with_add( + vec![ + "bugfix".to_string(), + "feature".to_string(), + ], + vec![ + false, + false, + ], + "tags", + "new tag", + ); + let description = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("description") + .interact_text() + .unwrap(); + // into main / master (lookup (yes/no) - if no select from existing branches with mapping + Some( + PullRequest { + title, + tags, + description, + } + ) + }, + }; + // create it now immediately before pushing the patches + let mut events_to_broadcast = vec![]; + + // create/store admin group + let admin_group = match &cfg.default_admin_group_event_serialized { + None => { + let new_admin_group = Group::new( + &InitializeGroup::new() + .members( + vec![ + keys.public_key().to_string(), + ], + vec![], + ) + .relays(&repo.relays), + &keys, + ).unwrap(); + cfg.default_admin_group_event_serialized = Some(new_admin_group.events[0].as_json()); + save_conifg(&cfg); + new_admin_group + }, + Some(admin) => Group::new_from_json_event(admin.clone()) + .expect("admin group event in MyConfig loads into Group"), + }; + events_to_broadcast.push(admin_group.events[0].clone()); + branch_refs.update(admin_group.events[0].clone()); + + + // create group + let branch_group_ref = match branch_refs.is_authorized( + None, + &keys.public_key(), + ) + .expect("main repo maintainers group is cached in .ngit") + { + // use repo maintainers group + true => branch_refs.maintainers_group(None) + .expect("main repo maintainers group is cached in .ngit") + .get_ref(), + // create branch group + false => { + let new_group = Group::new( + &InitializeGroup::new() + .name( + format!( + "branch-of:{},named:{}", + match &repo.name { + None => "untitled", + Some(s) => s.as_str(), + }, + &new_branch_name, + ), + ) + .admin(admin_group.get_ref()) + .members( + vec![keys.public_key().to_string()], + vec![ + branch_refs.maintainers_group(None) + .expect("repo maintainers group to exist in .ngit directory") + .get_ref() + ] + ) + .relays(&repo.relays) + , + &keys, + ) + .expect("new branch group to be created"); + + events_to_broadcast.push(new_group.events[0].clone()); + branch_refs.update(new_group.events[0].clone()); + new_group.get_ref() + } + }; + + // create branch + let branch_init = InitializeRepo::new() + .name(&new_branch_name) + .relays(&repo.relays) + .root_repo(repo.id.to_string()) + .maintainers_group(branch_group_ref) + .initialize(&keys); + events_to_broadcast.push(branch_init.clone()); + branch_refs.update(branch_init.clone()); + + // TODO: create PR + match pr_details { + None => (), + Some(pr_details) => { + let pull_request_init = initialize_pull_request( + &keys, + &repo.id.to_string(), + &repo.id.to_string(), + &branch_init.id.to_string(), + &pr_details.title, + &pr_details.description, + pr_details.tags + ); + events_to_broadcast.push(pull_request_init.clone()); + branch_refs.update(pull_request_init.clone()); + println!( + "pull request '{}' created with id: {}", + &pr_details.title, + Nip19Event::new( + pull_request_init.id.clone(), + vec![&repo.relays[0]], + ) + .to_bech32() + .expect("Nip19Event to convert to to_bech32") + ); + }, + } + + // add mapping to conifg.json + RepoConfig::open(repo_dir_path).set_mapping( + local_branch_name, + &branch_init.id.to_string(), + ); + + println!( + "branch '{}' created with id: {}", + &new_branch_name, + Nip19Event::new( + branch_init.id.clone(), + vec![&repo.relays[0]], + ) + .to_bech32() + .expect("Nip19Event to convert to to_bech32") + ); + + // broadcast events + for e in &events_to_broadcast { + match client.send_event(e.clone()) { + Ok(_) => (), + // TODO: this isn't working - if a relay is specified with a type it will wait 30ish secs and then return successful + Err(e) => { println!("error broadcasting event: {}",e); }, + } + // TODO: better error handling here / reporting. potentially warn if taking a while and report on troublesome relays + } + + // return branch_id + branch_init.id.to_string() +} 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 @@ +use std::path::PathBuf; + +use dialoguer::{Input, theme::ColorfulTheme}; +use git2::{Repository, Oid}; + +use crate::repo_config::RepoConfig; + +use super::checkout_branch::checkout_branch; + + +/// prompts user for name of local branch, creates the branch (checking if it is valid, if not looping) and returns it. +pub fn create_local_branch_from_user_input( + repo_dir_path: &PathBuf, + git_repo: &Repository, + suggestion:&Option, + commit_id:&String, + branch_id: &String, +) -> String { + + let target_commit = git_repo.find_commit( + Oid::from_str( + commit_id + ) + .expect("commit_id supplied to be a valid Oid") + ) + .expect("commit_id supplied exists in git repository"); + + loop { + let response: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("local branch name") + .with_initial_text(match &suggestion { + None => "".to_string(), + Some(s) => s.to_string(), + }) + .report(true) + .interact_text() + .unwrap(); + match git_repo.branch( + &response, + &target_commit, + false, + ) { + Ok(branch) => { + // check out branch + checkout_branch(git_repo, branch); + // set mapping + let mut repo_config = RepoConfig::open(repo_dir_path); + repo_config.set_mapping(&response, branch_id); + break response; + }, + Err(_) => { + println!("not a valid nevent, note or hex string. try again. (or a local branch with that name exists)"); + continue + }, + } + } +} 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 @@ +use git2::{Email, EmailCreateOptions}; +use indicatif::ProgressBar; +use nostr::{Keys, Event}; + +use 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}; + +pub fn create_and_broadcast_patches_from_oid( + oids_ancestors_first: Vec, + git_repo: &git2::Repository, + repo_dir_path: &std::path::PathBuf, + repo: &Repo, + branch_id: &String, + keys: &Keys, +) { + let mut patches: Vec = vec![]; + for oid in oids_ancestors_first { + patches.push( + create_and_save_patch_from_oid( + &oid, + &patches, + &git_repo, + &repo_dir_path.join(".ngit"), + &repo, + &branch_id, + &keys, + ) + ); + } + + // update branch update timestamp + match patches.last() { + Some(p) => { + let mut repo_config = RepoConfig::open(&repo_dir_path); + repo_config.set_last_patch_update_time( + branch_id.clone(), + p.created_at.clone(), + ); + } + None => (), + }; + + + // broadcast patches + let spinner = ProgressBar::new_spinner(); + 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())); + + let client = create_client(&keys, repo.relays.clone()) + .expect("create_client to return client for create_and_broadcast_patches"); + for e in &patches { + match client.send_event(e.clone()) { + Ok(_) => (), + // TODO: this isn't working - if a relay is specified with a type it will wait 30ish secs and then return successful + Err(e) => { println!("error broadcasting patch event: {}",e); }, + } + // TODO: better error handling here / reporting. potentially warn if taking a while and report on troublesome relays + } + spinner.finish_with_message(format!("Pushed {} commits!.",patches.len())); +} + +pub fn create_and_save_patch_from_oid( + oid: &git2::Oid, + patches: &Vec, + git_repo: &git2::Repository, + ngit_path: &std::path::PathBuf, + repo: &Repo, + branch_id: &String, + keys: &Keys, +) -> Event { + let commit_id = format!("{}",oid); + let commit = git_repo.find_commit(*oid) + .expect("revwalk returns oid that matches a comit in the repository"); + let message = match commit.message() { + None => "", + Some(m) => m + }.to_string(); + let email = Email::from_commit( + &commit, + &mut EmailCreateOptions::default(), + ).expect("renders a commit as an email diff"); + let parent_commit_id: Option = match &commit.parent_id(0) { + Ok(parent_oid) => Some(format!("{}",parent_oid)), + Err(_) => None, + }; + let parent_patch_id = match &parent_commit_id { + None => None, + Some(id) => Some({ + // search for parent in current batch of patches + match patches.iter().find(|p| + p.tags.iter().find(|t|tag_is_commit(t) && id.clone() == tag_extract_value(&t)).is_some() + ) { + Some(p) => p.id.to_string(), + None => { + let parent_patch_path = ngit_path.join(format!("patches/{}.json",id)); + if parent_patch_path.exists() { + load_event(parent_patch_path) + .expect("patch in json file that exists produces valid event") + .id.to_string() + } else { + panic!("cannot find parent patch. ngit may have ordered ancestors without patches incorrectly"); + } + }, + } + }), + }; + let event = initialize_patch( + &keys, + &repo.id.to_string(), + &branch_id, + &email.as_slice(), + &message, + &vec![commit_id.to_string()], + parent_patch_id, + parent_commit_id, + ); + // save patch + save_event(ngit_path.join( + // TODO: consider what happens if a commit gets published twice, which one would get priority? The one from a maintiner? + format!("patches/{}.json",commit_id), + ), &event) + .expect("save_event to store event in /patches"); + event +} 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 @@ +use std::path::PathBuf; + +use git2::{Repository, Oid}; + +pub fn find_commits_ahead ( + git_repo: &Repository, + repo_dir_path: &PathBuf, + branch_name: &String, + +) -> Vec { + // get revwalk of commits + let mut revwalk = git_repo.revwalk() + .expect("revwalk not to error"); + // using the specified branch + match revwalk.push_glob(&branch_name) { + Ok(_) => (), + // errors when there are no commits + Err(_) => { return vec![]; } + } + + // find commit that need pushing + let mut revwalk = git_repo.revwalk() + .expect("git_repo.revwalk() to not error"); + revwalk.push_head() + .expect("revwalk.push_head not to error. already checked for some commits. headless?"); + + let mut new_commits = vec![]; + + for oid in revwalk { + // whatever branch we are on, we are only interested in returning how many unpublished commits we are ahead. + if repo_dir_path.join(".ngit").join(format!( + "patches/{}.json", + oid.as_ref().expect("oid to refernce commits").clone(), + )).exists() { + break; + } + new_commits.push(oid.expect("oid to refernce commits")); + } + // most often used ancestor first + new_commits.reverse(); + new_commits +} 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 @@ +use std::path::PathBuf; + +use nostr::{ Event }; + +use crate::{ngit_tag::{tag_is_branch, tag_extract_value, tag_is_commit}, branch_refs::BranchRefs, utils::load_event, kind::Kind}; + +/// finds latest patch that needs applying. It might not be the latest created_at date if an earlier patch was 'merged' more recently +pub fn find_latest_patch( + branch_id: &String, + patch_events:&Vec, + merges_into_branch: &Vec, + branch_refs:&BranchRefs, + repo_dir_path: &PathBuf, +) -> Option { + + // ensure only patch events make it into patch_events - we cant rely on relays for this + let patch_events:Vec = patch_events.iter().filter(|p| + // kind is patch + p.kind == nostr_sdk::Kind::Custom(u64::from(Kind::Patch)) + ).map(|p|p.clone()).collect(); + + let directly_authorised_patches: Vec = patch_events.iter().filter(|p| + // kind is patch + p.kind == nostr_sdk::Kind::Custom(u64::from(Kind::Patch)) + // on branch + && p.tags.iter().any( + |t|tag_is_branch(t) + && tag_extract_value(t) == branch_id.clone() + ) + // authorized + && match &branch_refs.is_authorized(Some(&branch_id), &p.pubkey) { + None => { false }, + Some(is_authorized) => { is_authorized.clone() } + } + ).map(|p|p.clone()).collect(); + + let latest_authorized_patch = find_latest_event(&directly_authorised_patches); + + let authorised_merges: Vec = merges_into_branch.iter().filter(|m| + // kind is merge + m.kind == nostr_sdk::Kind::Custom(u64::from(Kind::Merge)) + // into branch + && m.tags.iter().any( + |t|tag_is_branch(t) + && tag_extract_value(t) == branch_id.clone() + ) + // merge authorized + && match &branch_refs.is_authorized(Some(&branch_id), &m.pubkey) { + None => { false }, + Some(is_authorized) => { is_authorized.clone() } + } + ).map(|p|p.clone()).collect(); + + let latest_authorised_merge = find_latest_event(&authorised_merges); + + // find latest patch + + match latest_authorised_merge { + // no merge - return patch or None + None => latest_authorized_patch, + Some(m) => { + match latest_authorized_patch { + // merge but no patch, return the patch related to the merge + None => { + Some(get_merge_patch( + &m, + &patch_events, + repo_dir_path, + )) + }, + // a merge and a patch + Some(p) => { + // return the patch if it is later than merge + if m.created_at < p.created_at { + Some(p.clone()) + } + // return the patch related to the merge if the merge is later + else { + Some(get_merge_patch( + &m, + &patch_events, + repo_dir_path, + )) + } + } + } + } + } +} + +fn find_latest_event(events:&Vec) -> Option { + let mut latest = match events.get(0) { + None => { return None }, + Some(e) => e.clone(), + }; + for e in events.iter() { + if e.created_at > latest.created_at { + latest = e.clone(); + } + } + Some(latest) +} + +fn get_merge_patch( + merge: &Event, + patch_events: &Vec, + repo_dir_path: &PathBuf, +) -> Event{ + let commit_id = tag_extract_value( + merge.tags.iter().find(|tag| tag_is_commit(tag)) + .expect("merge event will have a commit tag") + ); + // search in patch_events vector + match patch_events.iter().find(|p| + tag_extract_value( + p.tags.iter().find(|tag| tag_is_commit(tag)) + .expect("patch event will have a commit tag") + ) == *commit_id + ) { + // found merge patch in patch_events + Some(patch) => patch.clone(), + None => { + let patch_path = repo_dir_path.join(format!( + ".ngit/patches/{}.json", + commit_id + )); + if patch_path.exists() { + // found merge patch in .ngit/patches + load_event(patch_path) + .expect("patch at path that exists renders as event") + } + else { + panic!("cannot find patch from merge event in event vector or .ngit folder"); + } + }, + } +} 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 @@ +use dialoguer::Select; +use nostr::{Event, EventId, Filter}; +use nostr_sdk::blocking::Client; + +use crate::{kind::Kind, repos::repo::Repo}; + +pub fn find_select_recent_repos( + client: &Client, +) -> EventId { + + let mut repo_events: Vec = client.get_events_of( + vec![ + Filter::new() + .hashtag("ngit-format-0.0.1") + .kind( + Kind::InitializeRepo.into_sdk_custom_kind(), + ) + .limit(10), + ], + None, + ) + .expect("get_events_of to not return an error"); + + repo_events.sort(); + repo_events.dedup(); + + if repo_events.is_empty() { + panic!("could not find any repositories. Create one with ngit init?") + } + + let repos: Vec = repo_events.iter().map(|r| + Repo::new_from_event(r.clone()) + .expect("repo to be well formed event") + ).collect(); + let repo_names: Vec = repos.iter().map(|r| + match r.name.clone() { + None => "(untitled)".to_string(), + Some(name) => name, + } + ).collect(); + + // select pr to review + let i = Select::new() + .with_prompt("clone for a repository on selected relays") + .items(&repo_names) + .report(false) + .interact() + .unwrap(); + // display nevent + println!("selected repo: {} {}",repo_names[i], repos[i].nevent()); + repos[i].id +} \ 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 @@ +use std::path::PathBuf; + +use nostr::{Event}; + +use crate::{branch_refs::BranchRefs, repo_config::RepoConfig, cli_helpers::valid_event_id_from_input}; + +pub fn get_branch_event_from_user_input( + branch_string_param:&Option, + branch_refs: &BranchRefs, + repo_dir_path: &PathBuf, +) -> Event { + get_branch_event_with_options( + branch_string_param, + branch_refs, + repo_dir_path, + true, + ) +} + +pub fn get_unmapped_branch_event_from_user_input( + branch_string_param:&Option, + branch_refs: &BranchRefs, + repo_dir_path: &PathBuf, +) -> Event { + get_branch_event_with_options( + branch_string_param, + branch_refs, + repo_dir_path, + false, + ) +} + +fn get_branch_event_with_options( + branch_string_param:&Option, + branch_refs: &BranchRefs, + repo_dir_path: &PathBuf, + retrun_unmapped_branches: bool, +) -> Event { + + let mut string_param = branch_string_param.clone(); + loop { + let valid_id = valid_event_id_from_input( + string_param.clone(), + &"nevent note or hex of remote branch to pull".to_string(), + ); + + match branch_refs.branches.iter().find(|g| g.id.eq(&valid_id)) { + Some(branch_event) => { + let repo_config = RepoConfig::open(repo_dir_path); + if !retrun_unmapped_branches { + match repo_config.branch_name_from_id(&valid_id.to_string()) { + // branch is alreay mapped + Some(name) => { + println!( + "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 :)", + name, + ); + string_param = None; + continue + } + None => (), + } + } + break branch_event.clone(); + }, + None => { + println!("valid id but the branch cannot be found in this respository on the specified relays. try again."); + string_param = None; + continue + } + } + } +} 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 @@ +use std::{path::PathBuf, str::FromStr}; + +use git2::Repository; +use nostr::{Event, Filter, EventId}; +use nostr_sdk::blocking::Client; + +use 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}; + + +/// ancessor patch events first +pub fn get_updates_of_patches ( + client: &Client, + branch_refs: &mut BranchRefs, + git_repo: &Repository, + repo_dir_path: &PathBuf, + branch_id: &String, + branch_name: &Option, + pull_new_branch: bool, +) -> Vec { + + let repo_config = RepoConfig::open(repo_dir_path); + let last_patch_timestamp = repo_config.last_patch_update_time(branch_id.clone()); + + // create direct patches filter + let direct_patches_filter = Filter::new() + .event( + EventId::from_str(branch_id) + .expect("branch_id to render as EventId") + ) + .kinds(vec![Kind::Patch.into_sdk_custom_kind()]); + + let mut filters = vec![ + match &last_patch_timestamp { + None => direct_patches_filter, + Some(timestamp) => { + direct_patches_filter.since(timestamp.clone()) + } + } + ]; + + // get maintainers group + if branch_refs.maintainers_group(Some(&branch_id)).is_none() { + // fetch branch mantainers group and check again + client.add_relays( + branch_refs.branch_as_repo(Some(branch_id)) + .relays + .clone().iter().map(|url| (url, None)).collect() + ) + .expect("branch relays to be added to client"); + let mut group_events = client.get_events_of( + vec![ + // use the opportunity to get all the remaining groups + Filter::new().ids(branch_refs.group_ids_for_branches_without_cached_groups()), + ], + None, + ) + .expect("get_events_of to not return an error"); + group_events.sort(); + group_events.dedup(); + branch_refs.updates(group_events); + } + + // create indirect pacthes filter + let merges_into_branch: Vec = branch_refs.merges.iter().filter(|event| + // merged into branch + event.tags.iter().any(|t| + tag_is_branch(t) + && tag_extract_value(t) == branch_id.clone() + ) + // merge timestamp is after last_patch_timestamp - we already have patches before this date + && match &last_patch_timestamp { + None => true, + Some(timestamp) => timestamp < &event.created_at + } + // author is member of branch maintainers group + && branch_refs.is_authorized(Some(branch_id), &event.pubkey) + .expect("found group event for branch after checking on speficied relays") + ).map(|e|e.clone()) + .collect(); + + if !merges_into_branch.is_empty() { + filters.push( + // ids for all patches referenced in merges + Filter::new() + .ids( + merges_into_branch.iter().flat_map(|event| + event.tags.iter() + .filter(|t| tag_is_patch(t)) + .map(|t| tag_extract_value(t).clone()) + .collect::>() + ) + .collect::>() + ) + // .kinds(vec![Kind::Patch.into_sdk_custom_kind()]) + ) + } + + // find patch events + let mut patch_events: Vec = client.get_events_of( + filters, + None, + ) + .expect("get_events_of to not return an error when looking for patches"); + + patch_events.sort(); + patch_events.dedup(); + + // find patch tip on branch + let latest_patch_on_branch = match find_latest_patch( + &branch_id, + &patch_events, + &merges_into_branch, + &branch_refs, + &repo_dir_path, + ) { + // no patches return empty vector + None => { return vec![] }, // for pull_new_branch do we set the branch to the latest commit referneced even if we have it? + Some(event) => event, + }; + + let mut new_patches_on_branch = vec![]; + // for pull_new_branch - cycle through patch parents until we find any patch that exists in our commit history + if pull_new_branch { + let mut patch_event_id = latest_patch_on_branch.id.to_string(); + let mut patch_commit_id = tag_extract_value( + latest_patch_on_branch.tags.iter().find(|t|tag_is_commit(t)) + .expect("all patch events to have a commit tag") + ); + + loop { + let patch = match patch_events.iter().find(|p| p.id.to_string() == patch_event_id.clone()) { + // patch event found in patch_events + Some(patch) => patch, + None => { + // loop for parent locally + if repo_dir_path.join(format!( + ".ngit/patches/{}.json", + patch_commit_id, + )).exists() { + // break out of loop when we identify the commit where the branch begins + break + } + else { + panic!("cannot find parent patch locally or in patch_events. This will fail if the branch does not share a commit with main / master") + } + } + }; + // add patch to list of patches to apply to new branch + new_patches_on_branch.push(patch.clone()); + // prepare loop for next patch - set patch_event_id to current patches parent + patch_event_id = tag_extract_value( + patch.tags.iter().find(|t|tag_is_patch_parent(t)) + .expect("patch to always have a patch parent.") + ); + patch_commit_id = tag_extract_value( + patch.tags.iter().find(|t|tag_is_commit_parent(t)) + .expect("patch to always have a commit parent. This will fail if the branch does not share a commit with main / master") + ); + }; + } + + // 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) + else { + // revwalk through branch to identify forced push + let mut revwalk = git_repo.revwalk() + .expect("revwalk to not error on git_repo"); + match &branch_name { + Some(name) => { + revwalk.push( + git_repo.find_branch( + name.as_str(), + git2::BranchType::Local + ) + .expect("branch found from the branch_name") + .get() + .peel_to_commit() + .expect("branch reference to peel back to a commit") + .id() + ) + .expect("revwalk push_glob(branch_name) not to error if branch name is not None"); + } + None => (), + } + let commit_ids_in_branch: Vec = if branch_name.is_none() { vec![] } else { + revwalk.map(|oid| + oid + .expect("revwalk to produce oids without error") + .to_string() + ).collect() + }; + + let latest_commit: Option<&String> = match commit_ids_in_branch.get(0) { + None => None, + Some(latest_commit) => { + // return empty if latest patch is in current chain + if commit_ids_in_branch.iter().any(|id| + patch_commit_id(&latest_patch_on_branch) == id.to_string() + ) { return vec![]; } + Some(latest_commit) + }, + }; + + // work back thorugh commit chain until we reach a commit in our branch history (tip or ealier for rebase) + new_patches_on_branch = vec![latest_patch_on_branch.clone()]; + loop { + let next_parent_patch = new_patches_on_branch.last() + .expect("chain to contain at least latest_patch_on_main") + .clone(); + match next_parent_patch.tags.iter().find(|t|tag_is_patch_parent(t)) { + None => { + // found root patch or error + next_parent_patch.tags.iter().find(|t|tag_is_initial_commit(t)) + // tag_is_initial_commit is false when it should be true. is it always false or just the oposite? + .expect( + &format!( + "reach a patch which doesn't contain a either a tag_is_patch_parent or tag_is_initial_commit{:#?}", + &next_parent_patch + ) + ); + break; + }, + Some(t) => { + let next_patch = match patch_events.iter().find(|event|event.id.to_string() == tag_extract_value(t)) { + None => { + let patch_path = repo_dir_path.join(format!( + ".ngit/patches/{}.json", + tag_extract_value( + next_parent_patch.tags.iter().find(|t|tag_is_commit_parent(t)) + .expect("patch to always have a commit parent if it has a patch parent") + ), + )); + if patch_path.exists() { + load_event(patch_path) + .expect("patch json at location that exists loads into event") + } + else { + panic!("cannot find parent patch id {} from patch {:#?}",tag_extract_value(t), next_parent_patch); + } + }, + Some(event) => event.clone(), + }; + // if reached current tip - break + if latest_commit.is_some() && patch_is_commit( + &next_patch, + latest_commit.unwrap(), + ) { break; } + // detect rebase + if commit_ids_in_branch.iter().any(|id| + patch_commit_id(&next_patch) == id.to_string() + ) { + panic!("force push detected. This branch has been force pushed since you last pulled. ngit doesnt handle this yet"); + } + // new patch + new_patches_on_branch.push(next_patch.clone()); + + }, + } + } + } + // oldest first + new_patches_on_branch.reverse(); + new_patches_on_branch +} 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 @@ +pub mod apply_patches; +pub mod checkout_branch; +pub mod create_branch_and_pr; +pub mod create_local_branch_from_user_input; +pub mod create_patches; +pub mod find_commits_ahead; +pub mod find_latest_patch; +pub mod find_select_recent_repos; +pub mod get_branch_event_from_user_input; +pub mod get_updates_of_patches; \ No newline at end of file -- cgit v1.2.3