upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/push.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/ngit/sub_commands/push.rs')
-rw-r--r--src/bin/ngit/sub_commands/push.rs219
1 files changed, 219 insertions, 0 deletions
diff --git a/src/bin/ngit/sub_commands/push.rs b/src/bin/ngit/sub_commands/push.rs
new file mode 100644
index 0000000..a77f356
--- /dev/null
+++ b/src/bin/ngit/sub_commands/push.rs
@@ -0,0 +1,219 @@
1use anyhow::{bail, Context, Result};
2use ngit::{
3 client::send_events,
4 git_events::{is_event_proposal_root_for_branch, tag_value},
5};
6use nostr_sdk::PublicKey;
7
8use crate::{
9 cli::Cli,
10 client::{
11 fetching_with_report, get_all_proposal_patch_events_from_cache,
12 get_proposals_and_revisions_from_cache, get_repo_ref_from_cache, Client, Connect,
13 },
14 git::{identify_ahead_behind, str_to_sha1, Repo, RepoActions},
15 git_events::{
16 generate_patch_event, get_commit_id_from_patch, get_most_recent_patch_with_ancestors,
17 },
18 login,
19 repo_ref::get_repo_coordinates,
20 sub_commands,
21};
22
23#[derive(Debug, clap::Args)]
24pub struct SubCommandArgs {
25 #[arg(long, action)]
26 /// send proposal revision from checked out proposal branch
27 force: bool,
28}
29
30#[allow(clippy::too_many_lines)]
31pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
32 let git_repo = Repo::discover().context("cannot find a git repository")?;
33 let git_repo_path = git_repo.get_path()?;
34
35 let (main_or_master_branch_name, _) = git_repo
36 .get_main_or_master_branch()
37 .context("no main or master branch")?;
38
39 let root_commit = git_repo
40 .get_root_commit()
41 .context("failed to get root commit of the repository")?;
42
43 let branch_name = git_repo
44 .get_checked_out_branch_name()
45 .context("cannot get checked out branch name")?;
46
47 if branch_name == main_or_master_branch_name {
48 bail!("checkout a branch associated with a proposal first")
49 }
50 let mut client = Client::default();
51
52 let repo_coordinates = get_repo_coordinates(&git_repo, &client).await?;
53
54 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
55
56 let repo_ref = get_repo_ref_from_cache(git_repo_path, &repo_coordinates).await?;
57
58 let logged_in_public_key =
59 if let Ok(Some(npub)) = git_repo.get_git_config_item("nostr.npub", None) {
60 PublicKey::parse(npub).ok()
61 } else {
62 None
63 };
64
65 let proposal_root_event =
66 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates())
67 .await?
68 .iter()
69 .find(|e| {
70 is_event_proposal_root_for_branch(e, &branch_name, &logged_in_public_key)
71 .unwrap_or(false)
72 })
73 .context("cannot find proposal that matches the current branch name")?
74 .clone();
75
76 let commit_events =
77 get_all_proposal_patch_events_from_cache(git_repo_path, &repo_ref, &proposal_root_event.id)
78 .await?;
79
80 let most_recent_proposal_patch_chain = get_most_recent_patch_with_ancestors(commit_events)
81 .context("cannot get most recent patch for proposal")?;
82
83 let branch_tip = git_repo.get_tip_of_branch(&branch_name)?;
84
85 let most_recent_patch_commit_id = str_to_sha1(
86 &get_commit_id_from_patch(
87 most_recent_proposal_patch_chain
88 .first()
89 .context("no patches found")?,
90 )
91 .context("latest patch event doesnt have a commit tag")?,
92 )
93 .context("latest patch event commit tag isn't a valid SHA1 hash")?;
94
95 let proposal_base_commit_id = str_to_sha1(
96 &tag_value(
97 most_recent_proposal_patch_chain
98 .last()
99 .context("no patches found")?,
100 "parent-commit",
101 )
102 .context("patch is incorrectly formatted")?,
103 )
104 .context("latest patch event parent-commit tag isn't a valid SHA1 hash")?;
105
106 if most_recent_patch_commit_id.eq(&branch_tip) {
107 bail!("proposal already up-to-date with local branch");
108 }
109
110 if args.force {
111 println!("preparing to force push proposal revision...");
112 sub_commands::send::launch(
113 cli_args,
114 &sub_commands::send::SubCommandArgs {
115 // if not ahead of master prompt, otherwise assume proposal revision is all commits
116 // ahead
117 since_or_range: if let Ok((_, _, ahead, _)) =
118 identify_ahead_behind(&git_repo, &None, &None)
119 {
120 if ahead.is_empty() {
121 String::new()
122 } else {
123 format!("HEAD~{}", ahead.len())
124 }
125 } else {
126 String::new()
127 },
128 in_reply_to: vec![proposal_root_event.id.to_string()],
129 title: None,
130 description: None,
131 no_cover_letter: true,
132 },
133 true,
134 )
135 .await?;
136 println!("force pushed proposal revision");
137 return Ok(());
138 }
139
140 if most_recent_proposal_patch_chain.iter().any(|e| {
141 let c = tag_value(e, "parent-commit").unwrap_or_default();
142 c.eq(&branch_tip.to_string())
143 }) {
144 bail!("proposal is ahead of local branch");
145 }
146
147 let Ok((ahead, behind)) = git_repo
148 .get_commits_ahead_behind(&most_recent_patch_commit_id, &branch_tip)
149 .context("the latest patch in proposal doesnt share an ancestor with your branch.")
150 else {
151 if git_repo.ancestor_of(&proposal_base_commit_id, &branch_tip)? {
152 bail!("local unpublished proposal ammendments. consider force pushing.");
153 }
154 bail!("local unpublished proposal has been rebased. consider force pushing");
155 };
156
157 if !behind.is_empty() {
158 bail!(
159 "your local proposal branch is {} behind patches on nostr. consider rebasing or force pushing",
160 behind.len()
161 )
162 }
163
164 println!(
165 "{} commits ahead. preparing to create creating patch events.",
166 ahead.len()
167 );
168
169 let (signer, user_ref) = login::launch(
170 &git_repo,
171 &cli_args.bunker_uri,
172 &cli_args.bunker_app_key,
173 &cli_args.nsec,
174 &cli_args.password,
175 Some(&client),
176 false,
177 false,
178 )
179 .await?;
180
181 let mut patch_events: Vec<nostr::Event> = vec![];
182 for commit in &ahead {
183 patch_events.push(
184 generate_patch_event(
185 &git_repo,
186 &root_commit,
187 commit,
188 Some(proposal_root_event.id),
189 &signer,
190 &repo_ref,
191 patch_events.last().map(|e| e.id),
192 None,
193 None,
194 &None,
195 &[],
196 )
197 .await
198 .context("cannot make patch event from commit")?,
199 );
200 }
201 println!("pushing {} commits", ahead.len());
202
203 client.set_signer(signer).await;
204
205 send_events(
206 &client,
207 git_repo_path,
208 patch_events,
209 user_ref.relays.write(),
210 repo_ref.relays.clone(),
211 !cli_args.disable_cli_spinners,
212 false,
213 )
214 .await?;
215
216 println!("pushed {} commits", ahead.len());
217
218 Ok(())
219}