From 06be0bc44011411b78217459f505ed12281b32c4 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 1 Dec 2023 00:00:00 +0000 Subject: feat(prs-list) list and pull selected as branch - fetch prs and present as a selectable list - create and / or checkout branch for selected pr - apply latest patches as commits --- src/sub_commands/prs/create.rs | 90 ++++++++++------- src/sub_commands/prs/list.rs | 225 +++++++++++++++++++++++++++++++++++++++++ src/sub_commands/prs/mod.rs | 3 + 3 files changed, 284 insertions(+), 34 deletions(-) create mode 100644 src/sub_commands/prs/list.rs (limited to 'src/sub_commands') diff --git a/src/sub_commands/prs/create.rs b/src/sub_commands/prs/create.rs index ce30c12..5c4e578 100644 --- a/src/sub_commands/prs/create.rs +++ b/src/sub_commands/prs/create.rs @@ -301,9 +301,9 @@ mod tests_unique_and_duplicate { pub static PR_KIND: u64 = 318; pub static PATCH_KIND: u64 = 317; -fn generate_pr_and_patch_events( - title: &String, - description: &String, +pub fn generate_pr_and_patch_events( + title: &str, + description: &str, to_branch: &str, git_repo: &Repo, commits: &Vec, @@ -314,6 +314,7 @@ fn generate_pr_and_patch_events( .context("failed to get root commit of the repository")?; let mut pr_tags = vec![ + Tag::Identifier(root_commit.to_string()), Tag::Reference(format!("r-{root_commit}")), Tag::Name(title.to_string()), Tag::Description(description.to_string()), @@ -341,42 +342,63 @@ fn generate_pr_and_patch_events( let mut events = vec![pr_event]; for commit in commits { - let commit_parent = git_repo - .get_commit_parent(commit) - .context("failed to create patch event")?; events.push( - EventBuilder::new( - nostr::event::Kind::Custom(PATCH_KIND), - git_repo - .make_patch_from_commit(commit) - .context(format!("cannot make patch for commit {commit}"))?, - &[ - Tag::Reference(format!("r-{root_commit}")), - Tag::Reference(commit.to_string()), - Tag::Reference(commit_parent.to_string()), - Tag::Event( - pr_event_id, - None, // TODO: add relay - Some(Marker::Root), - ), - Tag::Generic( - TagKind::Custom("commit".to_string()), - vec![commit.to_string()], - ), - Tag::Generic( - TagKind::Custom("parent-commit".to_string()), - vec![commit_parent.to_string()], - ), - // TODO: add Repo event tags - // TODO: people tag maintainers - // TODO: add relay tags - ], - ) - .to_event(keys)?, + generate_patch_event(git_repo, &root_commit, commit, pr_event_id, keys) + .context("failed to generate patch event")?, ); } Ok(events) } + +pub fn generate_patch_event( + git_repo: &Repo, + root_commit: &Sha1Hash, + commit: &Sha1Hash, + pr_event_id: nostr::EventId, + keys: &nostr::Keys, +) -> Result { + let commit_parent = git_repo + .get_commit_parent(commit) + .context("failed to get parent commit")?; + EventBuilder::new( + nostr::event::Kind::Custom(PATCH_KIND), + git_repo + .make_patch_from_commit(commit) + .context(format!("cannot make patch for commit {commit}"))?, + &[ + Tag::Reference(format!("r-{root_commit}")), + Tag::Reference(commit.to_string()), + Tag::Reference(commit_parent.to_string()), + Tag::Event( + pr_event_id, + None, // TODO: add relay + Some(Marker::Root), + ), + Tag::Generic( + TagKind::Custom("commit".to_string()), + vec![commit.to_string()], + ), + Tag::Generic( + TagKind::Custom("parent-commit".to_string()), + vec![commit_parent.to_string()], + ), + Tag::Description(git_repo.get_commit_message(commit)?.to_string()), + Tag::Generic( + TagKind::Custom("author".to_string()), + git_repo.get_commit_author(commit)?, + ), + Tag::Generic( + TagKind::Custom("committer".to_string()), + git_repo.get_commit_comitter(commit)?, + ), + // TODO: add Repo event tags + // TODO: people tag maintainers + // TODO: add relay tags + ], + ) + .to_event(keys) + .context("failed to sign event") +} // TODO // - find profile // - file relays diff --git a/src/sub_commands/prs/list.rs b/src/sub_commands/prs/list.rs new file mode 100644 index 0000000..13f29fd --- /dev/null +++ b/src/sub_commands/prs/list.rs @@ -0,0 +1,225 @@ +use anyhow::{Context, Result}; + +#[cfg(not(test))] +use crate::client::Client; +#[cfg(test)] +use crate::client::MockConnect; +use crate::{ + cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms}, + client::Connect, + git::{Repo, RepoActions}, + repo_ref, + sub_commands::prs::create::{PATCH_KIND, PR_KIND}, + Cli, +}; + +#[derive(Debug, clap::Args)] +pub struct SubCommandArgs { + /// TODO ignore merged, and closed + #[arg(long, action)] + open_only: bool, +} + +pub async fn launch( + _cli_args: &Cli, + _pr_args: &super::SubCommandArgs, + _args: &SubCommandArgs, +) -> Result<()> { + let git_repo = Repo::discover().context("cannot find a git repository")?; + + let (main_or_master_branch_name, _) = git_repo + .get_main_or_master_branch() + .context("no main or master branch")?; + + let root_commit = git_repo + .get_root_commit(main_or_master_branch_name) + .context("failed to get root commit of the repository")?; + + // TODO: check for empty repo + // TODO: check for existing maintaiers file + // TODO: check for other claims + + #[cfg(not(test))] + let client = Client::default(); + #[cfg(test)] + let client = ::default(); + + let repo_ref = repo_ref::fetch( + root_commit.to_string(), + &client, + client.get_more_fallback_relays().clone(), + ) + .await?; + + println!("finding PRs..."); + + let pr_events: Vec = client + .get_events( + repo_ref.relays.clone(), + vec![ + nostr::Filter::default() + .kind(nostr::Kind::Custom(PR_KIND)) + .reference(format!("r-{root_commit}")), + ], + ) + .await? + .iter() + .filter(|e| { + e.kind.as_u64() == PR_KIND + && e.tags + .iter() + .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("r-{root_commit}"))) + }) + .map(std::borrow::ToOwned::to_owned) + .collect(); + + // let pr_branch_names: Vec = pr_events + // .iter() + // .map(|e| { + // format!( + // "{}-{}", + // &e.id.to_string()[..5], + // if let Some(t) = e.tags.iter().find(|t| t.as_vec()[0] == + // "branch-name") { t.as_vec()[1].to_string() + // } else { + // "".to_string() + // } // git_repo.get_checked_out_branch_name(), + // ) + // }) + // .collect(); + + let selected_index = Interactor::default().choice( + PromptChoiceParms::default() + .with_prompt("All PRs") + .with_choices( + pr_events + .iter() + .map(|e| { + if let Ok(name) = tag_value(e, "name") { + name + } else { + e.id.to_string() + } + }) + .collect(), + ), + )?; + // println!("prs:{:?}", &pr_events); + + println!("finding commits..."); + + let commits_events: Vec = client + .get_events( + repo_ref.relays.clone(), + vec![ + nostr::Filter::default() + .kind(nostr::Kind::Custom(PATCH_KIND)) + .event(pr_events[selected_index].id) + .reference(format!("r-{root_commit}")), + ], + ) + .await? + .iter() + .filter(|e| { + e.kind.as_u64() == PATCH_KIND + && e.tags.iter().any(|t| { + t.as_vec().len() > 2 + && t.as_vec()[1].eq(&pr_events[selected_index].id.to_string()) + }) + && e.tags + .iter() + .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("r-{root_commit}"))) + }) + .map(std::borrow::ToOwned::to_owned) + .collect(); + + // TODO: are there outstanding changes to prevent checking out a new branch? + + let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events) + .context("cannot get most recent patch for PR")?; + + let branch_name = tag_value(&pr_events[selected_index], "branch-name")?; + + let applied = git_repo + .apply_patch_chain(&branch_name, most_recent_pr_patch_chain) + .context("cannot apply patch chain")?; + + if applied.is_empty() { + println!("checked out PR branch. no new commits to pull"); + } else { + println!( + "checked out PR branch. pulled {} new commits", + applied.len(), + ); + } + + // // TODO: look for mapping of existing branch + + // // if latest_commit_id exists locally + // if local_branch_base == latest_commit_id { + // // TODO: check if its in the main / master branch (already merged) + // // TODO: check if it has any decendants and warn. maybe the user has + // // been working on a updates to be pushed? Suggest checking + // // out that branch. + // // we could search nostr for decendants of the commit as well? + // // perhaps this is overkill + // // TODO: check out the branch which it is the tip of. if the name of the + // // branch is different then ask the user if they would like to + // // use the existing branch or create one with the name of the PR. + // // TODO: if there are no decendants and its not the tip then + // // its an ophan commit so just make a branch from this commit. + // } + // // if commits ahead exist in a branch other than main or master + // // TODO: Identify probable existing branches - check remote tracker? + // // TODO: beind head + // else { + // // TODO: look for existing branch with same name + // // TODO: create remote tracker + // git_repo.create_branch_at_commit(&branch_name, &local_branch_base); + // git_repo.checkout(&branch_name)?; + // ahead.reverse(); + // for event in ahead { + // git_repo.apply_patch(event, branch_name)?; + // } + // println!("applied!") + // } + // // TODO: check if commits in pr exist, if so look for branches with they are + // in // could we suggest pulling updates into that branch? + // // + + // TODO: checkout PR branch + Ok(()) +} + +pub fn tag_value(event: &nostr::Event, tag_name: &str) -> Result { + Ok(event + .tags + .iter() + .find(|t| t.as_vec()[0].eq(tag_name)) + .context(format!("tag '{tag_name}'not present"))? + .as_vec()[1] + .clone()) +} + +pub fn get_most_recent_patch_with_ancestors( + mut patches: Vec, +) -> Result> { + patches.sort_by_key(|e| e.created_at); + + let mut res = vec![]; + + let latest_commit_id = tag_value(patches.first().context("no patches found")?, "commit")?; + + let mut commit_id_to_search = latest_commit_id; + + while let Some(event) = patches.iter().find(|e| { + tag_value(e, "commit") + .context("patch event doesnt contain commit tag") + .unwrap() + .eq(&commit_id_to_search) + }) { + res.push(event.clone()); + commit_id_to_search = tag_value(event, "parent-commit")?; + } + Ok(res) +} diff --git a/src/sub_commands/prs/mod.rs b/src/sub_commands/prs/mod.rs index 982e866..a41c495 100644 --- a/src/sub_commands/prs/mod.rs +++ b/src/sub_commands/prs/mod.rs @@ -3,6 +3,7 @@ use clap::Subcommand; use crate::Cli; pub mod create; +pub mod list; #[derive(clap::Parser)] pub struct SubCommandArgs { @@ -13,10 +14,12 @@ pub struct SubCommandArgs { #[derive(Debug, Subcommand)] pub enum Commands { Create(create::SubCommandArgs), + List(list::SubCommandArgs), } pub async fn launch(cli_args: &Cli, pr_args: &SubCommandArgs) -> Result<()> { match &pr_args.prs_command { Commands::Create(args) => create::launch(cli_args, pr_args, args).await, + Commands::List(args) => list::launch(cli_args, pr_args, args).await, } } -- cgit v1.2.3