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