upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/bin/ngit/cli.rs5
-rw-r--r--src/bin/ngit/main.rs1
-rw-r--r--src/bin/ngit/sub_commands/checkout.rs269
-rw-r--r--src/bin/ngit/sub_commands/mod.rs1
4 files changed, 276 insertions, 0 deletions
diff --git a/src/bin/ngit/cli.rs b/src/bin/ngit/cli.rs
index 47f4b27..5c1a097 100644
--- a/src/bin/ngit/cli.rs
+++ b/src/bin/ngit/cli.rs
@@ -106,6 +106,11 @@ pub enum Commands {
106 Send(sub_commands::send::SubCommandArgs), 106 Send(sub_commands::send::SubCommandArgs),
107 /// list PRs; checkout, apply or download selected 107 /// list PRs; checkout, apply or download selected
108 List, 108 List,
109 /// checkout a proposal branch by event-id or nevent
110 Checkout {
111 /// Proposal event-id (hex) or nevent (bech32)
112 id: String,
113 },
109 /// update repo git servers to reflect nostr state (add, update or delete 114 /// update repo git servers to reflect nostr state (add, update or delete
110 /// remote refs) 115 /// remote refs)
111 Sync(sub_commands::sync::SubCommandArgs), 116 Sync(sub_commands::sync::SubCommandArgs),
diff --git a/src/bin/ngit/main.rs b/src/bin/ngit/main.rs
index b6b51d0..2c9e10f 100644
--- a/src/bin/ngit/main.rs
+++ b/src/bin/ngit/main.rs
@@ -53,6 +53,7 @@ async fn main() {
53 } 53 }
54 Commands::Send(args) => sub_commands::send::launch(&cli, args, false).await, 54 Commands::Send(args) => sub_commands::send::launch(&cli, args, false).await,
55 Commands::Sync(args) => sub_commands::sync::launch(args).await, 55 Commands::Sync(args) => sub_commands::sync::launch(args).await,
56 Commands::Checkout { id } => sub_commands::checkout::launch(id).await,
56 } 57 }
57 } else { 58 } else {
58 // Handle the case where no command is provided 59 // Handle the case where no command is provided
diff --git a/src/bin/ngit/sub_commands/checkout.rs b/src/bin/ngit/sub_commands/checkout.rs
new file mode 100644
index 0000000..0df1134
--- /dev/null
+++ b/src/bin/ngit/sub_commands/checkout.rs
@@ -0,0 +1,269 @@
1use std::collections::HashSet;
2
3use anyhow::{Context, Result, bail};
4use ngit::{
5 client::{
6 Params, get_all_proposal_patch_pr_pr_update_events_from_cache,
7 get_proposals_and_revisions_from_cache,
8 },
9 fetch::fetch_from_git_server,
10 git_events::{
11 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch,
12 get_pr_tip_event_or_most_recent_patch_with_ancestors, tag_value,
13 },
14 repo_ref::{RepoRef, is_grasp_server_in_list},
15};
16use nostr::nips::nip19::Nip19;
17use nostr_sdk::{EventId, FromBech32};
18
19use crate::{
20 client::{Client, Connect, fetching_with_report, get_repo_ref_from_cache},
21 git::{Repo, RepoActions, str_to_sha1},
22 git_events::{event_to_cover_letter, patch_supports_commit_ids},
23 repo_ref::get_repo_coordinates_when_remote_unknown,
24};
25
26pub async fn launch(id: &str) -> Result<()> {
27 let event_id = parse_event_id(id)?;
28
29 let git_repo = Repo::discover().context("failed to find a git repository")?;
30 let git_repo_path = git_repo.get_path()?;
31
32 let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));
33
34 let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?;
35
36 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
37
38 let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
39
40 let proposals_and_revisions: Vec<nostr::Event> =
41 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()).await?;
42
43 let proposal = proposals_and_revisions
44 .iter()
45 .find(|e| e.id == event_id)
46 .context(format!("proposal with id {} not found in cache", event_id.to_hex()))?;
47
48 let cover_letter = event_to_cover_letter(proposal)
49 .context("failed to extract proposal details from proposal root event")?;
50
51 let commits_events: Vec<nostr::Event> = get_all_proposal_patch_pr_pr_update_events_from_cache(
52 git_repo_path,
53 &repo_ref,
54 &proposal.id,
55 )
56 .await?;
57
58 let most_recent_proposal_patch_chain_or_pr_or_pr_update =
59 get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone())
60 .context("failed to find any PR or patch events on this proposal")?;
61
62 if most_recent_proposal_patch_chain_or_pr_or_pr_update
63 .iter()
64 .any(|e| [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&e.kind))
65 {
66 checkout_pr(
67 &git_repo,
68 &repo_ref,
69 &cover_letter,
70 &most_recent_proposal_patch_chain_or_pr_or_pr_update,
71 )
72 } else {
73 checkout_patch(
74 &git_repo,
75 &cover_letter,
76 &most_recent_proposal_patch_chain_or_pr_or_pr_update,
77 )
78 }
79}
80
81fn parse_event_id(id: &str) -> Result<EventId> {
82 if let Ok(nip19) = Nip19::from_bech32(id) {
83 match nip19 {
84 Nip19::Event(e) => return Ok(e.event_id),
85 Nip19::EventId(event_id) => return Ok(event_id),
86 _ => {}
87 }
88 }
89 if let Ok(event_id) = EventId::from_hex(id) {
90 return Ok(event_id);
91 }
92 bail!("invalid event-id or nevent: {id}")
93}
94
95fn checkout_pr(
96 git_repo: &Repo,
97 repo_ref: &RepoRef,
98 cover_letter: &crate::git_events::CoverLetter,
99 most_recent_proposal_patch_chain_or_pr_or_pr_update: &[nostr::Event],
100) -> Result<()> {
101 let branch_name = cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?;
102 let local_branch_tip = git_repo.get_tip_of_branch(&branch_name).ok();
103 let proposal_tip_event = most_recent_proposal_patch_chain_or_pr_or_pr_update
104 .first()
105 .context("most_recent_proposal_patch_chain_or_pr_or_pr_update will always contain an event with c tag")?;
106 let proposal_tip = tag_value(proposal_tip_event, "c")?;
107
108 if let Some(local_branch_tip) = local_branch_tip {
109 git_repo
110 .checkout(&branch_name)
111 .context("cannot checkout existing proposal branch")?;
112 if local_branch_tip.to_string() == proposal_tip {
113 println!("checked out up-to-date proposal branch '{branch_name}'");
114 return Ok(());
115 }
116 if git_repo.does_commit_exist(&proposal_tip)? {
117 git_repo.create_branch_at_commit(&branch_name, &proposal_tip)?;
118 git_repo.checkout(&branch_name)?;
119 println!("checked out proposal branch and updated tip '{branch_name}'");
120 return Ok(());
121 }
122 }
123
124 fetch_oid_for_from_servers_for_pr(
125 &proposal_tip,
126 git_repo,
127 repo_ref,
128 proposal_tip_event,
129 )?;
130 git_repo.create_branch_at_commit(&branch_name, &proposal_tip)?;
131 git_repo.checkout(&branch_name)?;
132 if local_branch_tip.is_some() {
133 println!("checked out proposal branch and pulled updates '{branch_name}'");
134 } else {
135 println!("created and checked out proposal branch '{branch_name}'");
136 }
137 Ok(())
138}
139
140fn checkout_patch(
141 git_repo: &Repo,
142 cover_letter: &crate::git_events::CoverLetter,
143 most_recent_proposal_patch_chain_or_pr_or_pr_update: &[nostr::Event],
144) -> Result<()> {
145 let no_support_for_patches_as_branch = most_recent_proposal_patch_chain_or_pr_or_pr_update
146 .iter()
147 .any(|event| !patch_supports_commit_ids(event));
148
149 if no_support_for_patches_as_branch {
150 bail!(
151 "this proposal cannot be checked out as a branch because some patches do not have a parent commit.\n\
152 Try `ngit apply --stdout` to apply patches to the current branch, or use `ngit list` for interactive options."
153 );
154 }
155
156 let proposal_base_commit = str_to_sha1(&tag_value(
157 most_recent_proposal_patch_chain_or_pr_or_pr_update
158 .last()
159 .context("there should be at least one patch")?,
160 "parent-commit",
161 )?)
162 .context("failed to get valid parent commit id from patch")?;
163
164 let (main_branch_name, _master_tip) = git_repo.get_main_or_master_branch()?;
165
166 if !git_repo.does_commit_exist(&proposal_base_commit.to_string())? {
167 bail!(
168 "the proposal parent commit doesn't exist in your local repository.\n\
169 Try running `git pull` on '{main_branch_name}' first, or use `ngit apply --stdout` to apply patches to the current branch."
170 );
171 }
172
173 if git_repo.has_outstanding_changes()? {
174 bail!(
175 "working directory is not clean. Discard or stash (un)staged changes and try again."
176 );
177 }
178
179 let branch_name = cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?;
180 let branch_exists = git_repo
181 .get_local_branch_names()
182 .context("failed to get local branch names")?
183 .iter()
184 .any(|n| n.eq(&branch_name));
185
186 if !branch_exists {
187 let _ = git_repo
188 .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain_or_pr_or_pr_update.to_vec())
189 .context("failed to apply patch chain")?;
190 println!("checked out proposal as '{branch_name}' branch");
191 return Ok(());
192 }
193
194 let local_branch_tip = git_repo.get_tip_of_branch(&branch_name)?;
195
196 let proposal_tip = str_to_sha1(
197 &get_commit_id_from_patch(
198 most_recent_proposal_patch_chain_or_pr_or_pr_update
199 .first()
200 .context("there should be at least one patch")?,
201 )
202 .context("failed to get valid commit_id from patch")?,
203 )
204 .context("failed to get valid commit_id from patch")?;
205
206 if proposal_tip.eq(&local_branch_tip) {
207 git_repo.checkout(&branch_name)?;
208 println!("branch '{branch_name}' checked out and up-to-date");
209 return Ok(());
210 }
211
212 git_repo.create_branch_at_commit(&branch_name, &proposal_base_commit.to_string())?;
213 git_repo.checkout(&branch_name)?;
214 let _ = git_repo
215 .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain_or_pr_or_pr_update.to_vec())
216 .context("failed to apply patch chain")?;
217 println!("checked out updated proposal as '{branch_name}' branch");
218 Ok(())
219}
220
221fn fetch_oid_for_from_servers_for_pr(
222 oid: &str,
223 git_repo: &Repo,
224 repo_ref: &RepoRef,
225 pr_or_pr_update_event: &nostr::Event,
226) -> Result<()> {
227 let git_servers = {
228 let mut seen: HashSet<String> = HashSet::new();
229 let mut out: Vec<String> = vec![];
230 for tag in pr_or_pr_update_event.tags.as_slice() {
231 if tag.kind().eq(&nostr::event::TagKind::Clone) {
232 for clone_url in tag.as_slice().iter().skip(1) {
233 seen.insert(clone_url.clone());
234 }
235 }
236 }
237 for server in &repo_ref.git_server {
238 if seen.insert(server.clone()) {
239 out.push(server.clone());
240 }
241 }
242 out
243 };
244
245 let mut errors = vec![];
246 let term = console::Term::stderr();
247
248 for git_server_url in &git_servers {
249 if let Err(error) = fetch_from_git_server(
250 git_repo,
251 &[oid.to_string()],
252 git_server_url,
253 &repo_ref.to_nostr_git_url(&None),
254 &term,
255 is_grasp_server_in_list(git_server_url, &repo_ref.grasp_servers()),
256 ) {
257 errors.push(error);
258 } else {
259 println!("fetched proposal git data from {git_server_url}");
260 break;
261 }
262 }
263 if !git_repo.does_commit_exist(oid)? {
264 bail!(
265 "cannot find proposal git data from proposal git server hint or repository git servers"
266 )
267 }
268 Ok(())
269}
diff --git a/src/bin/ngit/sub_commands/mod.rs b/src/bin/ngit/sub_commands/mod.rs
index 9c84ef2..e9f91db 100644
--- a/src/bin/ngit/sub_commands/mod.rs
+++ b/src/bin/ngit/sub_commands/mod.rs
@@ -1,3 +1,4 @@
1pub mod checkout;
1pub mod create; 2pub mod create;
2pub mod export_keys; 3pub mod export_keys;
3pub mod init; 4pub mod init;