diff options
Diffstat (limited to 'src/bin/ngit/sub_commands/list.rs')
| -rw-r--r-- | src/bin/ngit/sub_commands/list.rs | 245 |
1 files changed, 244 insertions, 1 deletions
diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs index 981307d..7873fae 100644 --- a/src/bin/ngit/sub_commands/list.rs +++ b/src/bin/ngit/sub_commands/list.rs | |||
| @@ -14,6 +14,8 @@ use ngit::{ | |||
| 14 | repo_ref::{RepoRef, is_grasp_server_in_list}, | 14 | repo_ref::{RepoRef, is_grasp_server_in_list}, |
| 15 | }; | 15 | }; |
| 16 | use nostr::filter::{Alphabet, SingleLetterTag}; | 16 | use nostr::filter::{Alphabet, SingleLetterTag}; |
| 17 | use nostr::nips::nip19::Nip19; | ||
| 18 | use nostr::{FromBech32, ToBech32}; | ||
| 17 | use nostr_sdk::Kind; | 19 | use nostr_sdk::Kind; |
| 18 | 20 | ||
| 19 | use crate::{ | 21 | use crate::{ |
| @@ -30,7 +32,248 @@ use crate::{ | |||
| 30 | }; | 32 | }; |
| 31 | 33 | ||
| 32 | #[allow(clippy::too_many_lines)] | 34 | #[allow(clippy::too_many_lines)] |
| 33 | pub async fn launch() -> Result<()> { | 35 | pub async fn launch(status: String, json: bool, id: Option<String>) -> Result<()> { |
| 36 | if std::env::var("NGIT_INTERACTIVE_MODE").is_ok() { | ||
| 37 | return launch_interactive().await; | ||
| 38 | } | ||
| 39 | |||
| 40 | let git_repo = Repo::discover().context("failed to find a git repository")?; | ||
| 41 | let git_repo_path = git_repo.get_path()?; | ||
| 42 | |||
| 43 | let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo))); | ||
| 44 | |||
| 45 | let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?; | ||
| 46 | |||
| 47 | fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; | ||
| 48 | |||
| 49 | let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?; | ||
| 50 | |||
| 51 | let proposals_and_revisions: Vec<nostr::Event> = | ||
| 52 | get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()).await?; | ||
| 53 | if proposals_and_revisions.is_empty() { | ||
| 54 | println!("no proposals found... create one? try `ngit send`"); | ||
| 55 | return Ok(()); | ||
| 56 | } | ||
| 57 | |||
| 58 | let statuses: Vec<nostr::Event> = { | ||
| 59 | let mut statuses = get_events_from_local_cache( | ||
| 60 | git_repo_path, | ||
| 61 | vec![ | ||
| 62 | nostr::Filter::default() | ||
| 63 | .kinds(status_kinds().clone()) | ||
| 64 | .events(proposals_and_revisions.iter().map(|e| e.id)), | ||
| 65 | nostr::Filter::default() | ||
| 66 | .custom_tags( | ||
| 67 | SingleLetterTag::uppercase(Alphabet::E), | ||
| 68 | proposals_and_revisions.iter().map(|e| e.id), | ||
| 69 | ) | ||
| 70 | .kinds(status_kinds().clone()), | ||
| 71 | ], | ||
| 72 | ) | ||
| 73 | .await?; | ||
| 74 | statuses.sort_by_key(|e| e.created_at); | ||
| 75 | statuses.reverse(); | ||
| 76 | statuses | ||
| 77 | }; | ||
| 78 | |||
| 79 | let mut open_proposals: Vec<&nostr::Event> = vec![]; | ||
| 80 | let mut draft_proposals: Vec<&nostr::Event> = vec![]; | ||
| 81 | let mut closed_proposals: Vec<&nostr::Event> = vec![]; | ||
| 82 | let mut applied_proposals: Vec<&nostr::Event> = vec![]; | ||
| 83 | |||
| 84 | let proposals: Vec<nostr::Event> = proposals_and_revisions | ||
| 85 | .iter() | ||
| 86 | .filter(|e| !event_is_revision_root(e)) | ||
| 87 | .cloned() | ||
| 88 | .collect(); | ||
| 89 | |||
| 90 | for proposal in &proposals { | ||
| 91 | let status_kind = get_status(proposal, &repo_ref, &statuses, &proposals); | ||
| 92 | if status_kind.eq(&Kind::GitStatusOpen) { | ||
| 93 | open_proposals.push(proposal); | ||
| 94 | } else if status_kind.eq(&Kind::GitStatusClosed) { | ||
| 95 | closed_proposals.push(proposal); | ||
| 96 | } else if status_kind.eq(&Kind::GitStatusDraft) { | ||
| 97 | draft_proposals.push(proposal); | ||
| 98 | } else if status_kind.eq(&Kind::GitStatusApplied) { | ||
| 99 | applied_proposals.push(proposal); | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | let status_filter: HashSet<&str> = status.split(',').map(str::trim).collect(); | ||
| 104 | |||
| 105 | let filtered_proposals: Vec<(&nostr::Event, Kind)> = proposals | ||
| 106 | .iter() | ||
| 107 | .filter_map(|p| { | ||
| 108 | let status_kind = get_status(p, &repo_ref, &statuses, &proposals); | ||
| 109 | let status_str = match status_kind { | ||
| 110 | Kind::GitStatusOpen => "open", | ||
| 111 | Kind::GitStatusDraft => "draft", | ||
| 112 | Kind::GitStatusClosed => "closed", | ||
| 113 | Kind::GitStatusApplied => "applied", | ||
| 114 | _ => "unknown", | ||
| 115 | }; | ||
| 116 | if status_filter.contains(status_str) || status_filter.contains("unknown") { | ||
| 117 | Some((p, status_kind)) | ||
| 118 | } else { | ||
| 119 | None | ||
| 120 | } | ||
| 121 | }) | ||
| 122 | .collect(); | ||
| 123 | |||
| 124 | if let Some(ref event_id_or_nevent) = id { | ||
| 125 | return show_proposal_details(&filtered_proposals, &repo_ref, event_id_or_nevent, json); | ||
| 126 | } | ||
| 127 | |||
| 128 | if json { | ||
| 129 | output_json(&filtered_proposals, &repo_ref)?; | ||
| 130 | } else { | ||
| 131 | output_table(&filtered_proposals, &repo_ref); | ||
| 132 | } | ||
| 133 | |||
| 134 | Ok(()) | ||
| 135 | } | ||
| 136 | |||
| 137 | fn status_kind_to_str(kind: Kind) -> &'static str { | ||
| 138 | match kind { | ||
| 139 | Kind::GitStatusOpen => "open", | ||
| 140 | Kind::GitStatusDraft => "draft", | ||
| 141 | Kind::GitStatusClosed => "closed", | ||
| 142 | Kind::GitStatusApplied => "applied", | ||
| 143 | _ => "unknown", | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | fn output_table(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef) { | ||
| 148 | if proposals.is_empty() { | ||
| 149 | println!("No proposals found matching the filter."); | ||
| 150 | return; | ||
| 151 | } | ||
| 152 | |||
| 153 | println!("{:<8} {:<8} TITLE", "ID", "STATUS"); | ||
| 154 | for (proposal, status_kind) in proposals { | ||
| 155 | let id = &proposal.id.to_string()[..7]; | ||
| 156 | let status = status_kind_to_str(*status_kind); | ||
| 157 | let title = if let Ok(cl) = event_to_cover_letter(proposal) { | ||
| 158 | cl.title | ||
| 159 | } else if let Ok(msg) = tag_value(proposal, "description") { | ||
| 160 | msg.split('\n').collect::<Vec<&str>>()[0].to_string() | ||
| 161 | } else { | ||
| 162 | proposal.id.to_string() | ||
| 163 | }; | ||
| 164 | println!("{id:<8} {status:<8} {title}"); | ||
| 165 | } | ||
| 166 | |||
| 167 | println!(); | ||
| 168 | println!("To checkout: ngit checkout <id>"); | ||
| 169 | println!("To apply: ngit apply <id>"); | ||
| 170 | } | ||
| 171 | |||
| 172 | fn output_json(proposals: &[(&nostr::Event, Kind)], _repo_ref: &RepoRef) -> Result<()> { | ||
| 173 | let json_output: Vec<serde_json::Value> = proposals | ||
| 174 | .iter() | ||
| 175 | .map(|(proposal, status_kind)| { | ||
| 176 | let id = proposal.id.to_string(); | ||
| 177 | let status = status_kind_to_str(*status_kind).to_string(); | ||
| 178 | let (title, author, branch) = if let Ok(cl) = event_to_cover_letter(proposal) { | ||
| 179 | ( | ||
| 180 | cl.title.clone(), | ||
| 181 | proposal.pubkey.to_bech32().unwrap_or_default(), | ||
| 182 | cl.get_branch_name_with_pr_prefix_and_shorthand_id() | ||
| 183 | .unwrap_or_default(), | ||
| 184 | ) | ||
| 185 | } else { | ||
| 186 | let title = tag_value(proposal, "description").map_or_else( | ||
| 187 | |_| proposal.id.to_string(), | ||
| 188 | |d| d.split('\n').collect::<Vec<&str>>()[0].to_string(), | ||
| 189 | ); | ||
| 190 | ( | ||
| 191 | title, | ||
| 192 | proposal.pubkey.to_bech32().unwrap_or_default(), | ||
| 193 | String::new(), | ||
| 194 | ) | ||
| 195 | }; | ||
| 196 | serde_json::json!({ | ||
| 197 | "id": id, | ||
| 198 | "status": status, | ||
| 199 | "title": title, | ||
| 200 | "author": author, | ||
| 201 | "branch": branch | ||
| 202 | }) | ||
| 203 | }) | ||
| 204 | .collect(); | ||
| 205 | |||
| 206 | println!("{}", serde_json::to_string_pretty(&json_output)?); | ||
| 207 | Ok(()) | ||
| 208 | } | ||
| 209 | |||
| 210 | fn show_proposal_details( | ||
| 211 | proposals: &[(&nostr::Event, Kind)], | ||
| 212 | _repo_ref: &RepoRef, | ||
| 213 | event_id_or_nevent: &str, | ||
| 214 | json: bool, | ||
| 215 | ) -> Result<()> { | ||
| 216 | let target_id = if event_id_or_nevent.starts_with("nevent") { | ||
| 217 | let nip19 = Nip19::from_bech32(event_id_or_nevent) | ||
| 218 | .context("failed to parse nevent")?; | ||
| 219 | match nip19 { | ||
| 220 | Nip19::EventId(id) => id, | ||
| 221 | Nip19::Event(event) => event.event_id, | ||
| 222 | _ => bail!("invalid nevent format"), | ||
| 223 | } | ||
| 224 | } else { | ||
| 225 | nostr::EventId::from_hex(event_id_or_nevent).context("failed to parse event id")? | ||
| 226 | }; | ||
| 227 | |||
| 228 | let (proposal, status_kind) = proposals | ||
| 229 | .iter() | ||
| 230 | .find(|(p, _)| p.id == target_id) | ||
| 231 | .context("proposal not found")?; | ||
| 232 | |||
| 233 | let cover_letter = event_to_cover_letter(proposal) | ||
| 234 | .context("failed to extract proposal details from proposal root event")?; | ||
| 235 | |||
| 236 | if json { | ||
| 237 | let json_output = serde_json::json!({ | ||
| 238 | "id": proposal.id.to_string(), | ||
| 239 | "status": status_kind_to_str(*status_kind), | ||
| 240 | "title": cover_letter.title, | ||
| 241 | "author": proposal.pubkey.to_bech32().unwrap_or_default(), | ||
| 242 | "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, | ||
| 243 | "description": cover_letter.description, | ||
| 244 | }); | ||
| 245 | println!("{}", serde_json::to_string_pretty(&json_output)?); | ||
| 246 | return Ok(()); | ||
| 247 | } | ||
| 248 | |||
| 249 | println!("Title: {}", cover_letter.title); | ||
| 250 | println!( | ||
| 251 | "Author: {}", | ||
| 252 | proposal.pubkey.to_bech32().unwrap_or_default() | ||
| 253 | ); | ||
| 254 | println!("Status: {}", status_kind_to_str(*status_kind)); | ||
| 255 | println!( | ||
| 256 | "Branch: {}", | ||
| 257 | cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()? | ||
| 258 | ); | ||
| 259 | |||
| 260 | if !cover_letter.description.is_empty() { | ||
| 261 | println!(); | ||
| 262 | println!("Description:"); | ||
| 263 | for line in cover_letter.description.lines() { | ||
| 264 | println!(" {line}"); | ||
| 265 | } | ||
| 266 | } | ||
| 267 | |||
| 268 | println!(); | ||
| 269 | println!("To checkout: ngit checkout {}", &proposal.id.to_string()[..7]); | ||
| 270 | println!("To apply: ngit apply {}", &proposal.id.to_string()[..7]); | ||
| 271 | |||
| 272 | Ok(()) | ||
| 273 | } | ||
| 274 | |||
| 275 | #[allow(clippy::too_many_lines)] | ||
| 276 | async fn launch_interactive() -> Result<()> { | ||
| 34 | let git_repo = Repo::discover().context("failed to find a git repository")?; | 277 | let git_repo = Repo::discover().context("failed to find a git repository")?; |
| 35 | let git_repo_path = git_repo.get_path()?; | 278 | let git_repo_path = git_repo.get_path()?; |
| 36 | 279 | ||