diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2023-05-21 11:14:47 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2023-05-21 11:14:47 +0000 |
| commit | 0067804cc00e94ce2b7043e67f9ff50968525479 (patch) | |
| tree | 2accdc6d4e9b73df4f20499238ec24f24a52a1b8 | |
| parent | 5c5feaa732363e32e2a980a887fa42b4394b1a5e (diff) | |
v0.0.1-alpha funcs
| -rw-r--r-- | src/funcs/apply_patches.rs | 95 | ||||
| -rw-r--r-- | src/funcs/checkout_branch.rs | 47 | ||||
| -rw-r--r-- | src/funcs/create_branch_and_pr.rs | 212 | ||||
| -rw-r--r-- | src/funcs/create_local_branch_from_user_input.rs | 57 | ||||
| -rw-r--r-- | src/funcs/create_patches.rs | 122 | ||||
| -rw-r--r-- | src/funcs/find_commits_ahead.rs | 42 | ||||
| -rw-r--r-- | src/funcs/find_latest_patch.rs | 137 | ||||
| -rw-r--r-- | src/funcs/find_select_recent_repos.rs | 52 | ||||
| -rw-r--r-- | src/funcs/get_branch_event_from_user_input.rs | 73 | ||||
| -rw-r--r-- | src/funcs/get_updates_of_patches.rs | 263 | ||||
| -rw-r--r-- | src/funcs/mod.rs | 10 |
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 @@ | |||
| 1 | use std::{path::PathBuf, process::Command, fs::{self, File}, io::Write}; | ||
| 2 | |||
| 3 | use git2::Repository; | ||
| 4 | use nostr::Event; | ||
| 5 | |||
| 6 | use crate::{utils::save_event, ngit_tag::{tag_extract_value, tag_is_commit}}; | ||
| 7 | |||
| 8 | |||
| 9 | pub 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 @@ | |||
| 1 | use git2::{Branch, Repository}; | ||
| 2 | |||
| 3 | pub 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 | |||
| 28 | pub 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 @@ | |||
| 1 | use std::{path::PathBuf}; | ||
| 2 | |||
| 3 | use dialoguer::{Input, theme::ColorfulTheme, Confirm}; | ||
| 4 | use nostr::{Keys, prelude::{Nip19Event, ToBech32}}; | ||
| 5 | use nostr_sdk::blocking::Client; | ||
| 6 | |||
| 7 | 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}; | ||
| 8 | |||
| 9 | struct PullRequest { | ||
| 10 | pub title: String, | ||
| 11 | pub description: String, | ||
| 12 | pub tags: Vec<String> | ||
| 13 | } | ||
| 14 | /// returns branch_id | ||
| 15 | pub 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 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use dialoguer::{Input, theme::ColorfulTheme}; | ||
| 4 | use git2::{Repository, Oid}; | ||
| 5 | |||
| 6 | use crate::repo_config::RepoConfig; | ||
| 7 | |||
| 8 | use 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. | ||
| 12 | pub 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 @@ | |||
| 1 | use git2::{Email, EmailCreateOptions}; | ||
| 2 | use indicatif::ProgressBar; | ||
| 3 | use nostr::{Keys, Event}; | ||
| 4 | |||
| 5 | 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}; | ||
| 6 | |||
| 7 | pub 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 | |||
| 60 | pub 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 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use git2::{Repository, Oid}; | ||
| 4 | |||
| 5 | pub 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 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use nostr::{ Event }; | ||
| 4 | |||
| 5 | use 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 | ||
| 8 | pub 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 | |||
| 91 | fn 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 | |||
| 104 | fn 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 @@ | |||
| 1 | use dialoguer::Select; | ||
| 2 | use nostr::{Event, EventId, Filter}; | ||
| 3 | use nostr_sdk::blocking::Client; | ||
| 4 | |||
| 5 | use crate::{kind::Kind, repos::repo::Repo}; | ||
| 6 | |||
| 7 | pub 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 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use nostr::{Event}; | ||
| 4 | |||
| 5 | use crate::{branch_refs::BranchRefs, repo_config::RepoConfig, cli_helpers::valid_event_id_from_input}; | ||
| 6 | |||
| 7 | pub 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 | |||
| 20 | pub 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 | |||
| 33 | fn 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 @@ | |||
| 1 | use std::{path::PathBuf, str::FromStr}; | ||
| 2 | |||
| 3 | use git2::Repository; | ||
| 4 | use nostr::{Event, Filter, EventId}; | ||
| 5 | use nostr_sdk::blocking::Client; | ||
| 6 | |||
| 7 | 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}; | ||
| 8 | |||
| 9 | |||
| 10 | /// ancessor patch events first | ||
| 11 | pub 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 @@ | |||
| 1 | pub mod apply_patches; | ||
| 2 | pub mod checkout_branch; | ||
| 3 | pub mod create_branch_and_pr; | ||
| 4 | pub mod create_local_branch_from_user_input; | ||
| 5 | pub mod create_patches; | ||
| 6 | pub mod find_commits_ahead; | ||
| 7 | pub mod find_latest_patch; | ||
| 8 | pub mod find_select_recent_repos; | ||
| 9 | pub mod get_branch_event_from_user_input; | ||
| 10 | pub mod get_updates_of_patches; \ No newline at end of file | ||