upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/sub_commands/prs/create.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2023-10-01 00:00:00 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2023-10-01 00:00:00 +0100
commit6e9245542f070c39a1975f0d53d88913c4ac667d (patch)
tree835c1d3db05f76f437c5d8ebc5591f1796cdab60 /src/sub_commands/prs/create.rs
parentaa48a626c08cec353d5563a8831239d2e69c9f3d (diff)
feat(prs-create) find commits and create events
- identify commits - create pull request event - create patch events
Diffstat (limited to 'src/sub_commands/prs/create.rs')
-rw-r--r--src/sub_commands/prs/create.rs371
1 files changed, 371 insertions, 0 deletions
diff --git a/src/sub_commands/prs/create.rs b/src/sub_commands/prs/create.rs
new file mode 100644
index 0000000..dd32c65
--- /dev/null
+++ b/src/sub_commands/prs/create.rs
@@ -0,0 +1,371 @@
1use anyhow::{bail, Context, Result};
2use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind};
3
4use crate::{
5 cli_interactor::{Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms},
6 git::{Repo, RepoActions},
7 login, Cli,
8};
9
10#[derive(Debug, clap::Args)]
11pub struct SubCommandArgs {
12 #[clap(short, long)]
13 /// title of pull request (defaults to first line of first commit)
14 title: Option<String>,
15 #[clap(short, long)]
16 /// optional description
17 description: Option<String>,
18 #[clap(long)]
19 /// branch to get changes from (defaults to head)
20 from_branch: Option<String>,
21 #[clap(long)]
22 /// destination branch (defaults to main or master)
23 to_branch: Option<String>,
24}
25
26pub fn launch(
27 cli_args: &Cli,
28 _pr_args: &super::SubCommandArgs,
29 args: &SubCommandArgs,
30) -> Result<()> {
31 let git_repo = Repo::discover().context("cannot find a git repository")?;
32
33 let (from_branch, to_branch, ahead, behind) =
34 identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?;
35
36 if ahead.is_empty() {
37 bail!(format!(
38 "'{from_branch}' is 0 commits ahead of '{to_branch}' so no patches were created"
39 ));
40 }
41
42 if behind.is_empty() {
43 println!(
44 "creating patch for {} commits from '{from_branch}' that can be merged into '{to_branch}'",
45 ahead.len(),
46 );
47 } else {
48 if !Interactor::default().confirm(
49 PromptConfirmParms::default()
50 .with_prompt(
51 format!(
52 "'{from_branch}' is {} commits behind '{to_branch}' and {} ahead. Consider rebasing before sending patches. Proceed anyway?",
53 behind.len(),
54 ahead.len(),
55 )
56 )
57 .with_default(false)
58 ).context("failed to get confirmation response from interactor confirm")? {
59 bail!("aborting so branch can be rebased");
60 }
61 println!(
62 "creating patch for {} commit{} from '{from_branch}' that {} {} behind '{to_branch}'",
63 ahead.len(),
64 if ahead.len() > 1 { "s" } else { "" },
65 if ahead.len() > 1 { "are" } else { "is" },
66 behind.len(),
67 );
68 }
69
70 let title = match &args.title {
71 Some(t) => t.clone(),
72 None => Interactor::default().input(PromptInputParms::default().with_prompt("title"))?,
73 };
74
75 let description = match &args.description {
76 Some(t) => t.clone(),
77 None => Interactor::default()
78 .input(PromptInputParms::default().with_prompt("description (Optional)"))?,
79 };
80
81 let root_commit = git_repo
82 .get_root_commit(to_branch.as_str())
83 .context("failed to get root commit of the repository")?;
84 // create PR event
85
86 let keys = login::launch(&cli_args.nsec, &cli_args.password)?;
87
88 let pr_event = EventBuilder::new(
89 nostr::event::Kind::Custom(318),
90 format!("{title}\r\n\r\n{description}"),
91 &[Tag::Hashtag(format!("r-{root_commit}"))],
92 // TODO: suggested branch name
93 // Tag::Generic(
94 // TagKind::Custom("suggested-branch-name".to_string()),
95 // vec![],
96 // ),
97 // TODO: add Repo event as root
98 // TODO: people tag maintainers
99 // TODO: add relay tags
100 )
101 .to_event(&keys)
102 .context("failed to create pr event")?;
103
104 let mut patch_events = vec![];
105 for commit in &ahead {
106 let commit_parent = git_repo
107 .get_commit_parent(commit)
108 .context("failed to create patch event")?;
109 patch_events.push(
110 EventBuilder::new(
111 nostr::event::Kind::Custom(317),
112 git_repo
113 .make_patch_from_commit(commit)
114 .context(format!("cannot make patch for commit {commit}"))?,
115 &[
116 Tag::Hashtag(format!("r-{root_commit}")),
117 Tag::Hashtag(commit.to_string()),
118 Tag::Hashtag(commit_parent.to_string()),
119 Tag::Event(
120 pr_event.id,
121 None, // TODO: add relay
122 Some(Marker::Root),
123 ),
124 Tag::Generic(
125 TagKind::Custom("commit".to_string()),
126 vec![commit.to_string()],
127 ),
128 Tag::Generic(
129 TagKind::Custom("parent-commit".to_string()),
130 vec![commit_parent.to_string()],
131 ),
132 // TODO: add Repo event tags
133 // TODO: people tag maintainers
134 // TODO: add relay tags
135 ],
136 )
137 .to_event(&keys),
138 );
139 }
140
141 // TODO check if there is already a similarly named PR
142 // TODO connect to relays and post
143
144 Ok(())
145}
146
147// TODO
148// - find profile
149// - file relays
150// - find repo events
151// -
152
153/**
154 * returns `(from_branch,to_branch,ahead,behind)`
155 */
156fn identify_ahead_behind(
157 git_repo: &Repo,
158 from_branch: &Option<String>,
159 to_branch: &Option<String>,
160) -> Result<(String, String, Vec<Sha1Hash>, Vec<Sha1Hash>)> {
161 let (from_branch, from_tip) = match from_branch {
162 Some(name) => (
163 name.to_string(),
164 git_repo
165 .get_tip_of_local_branch(name)
166 .context(format!("cannot find from_branch '{name}'"))?,
167 ),
168 None => (
169 "head".to_string(),
170 git_repo
171 .get_head_commit()
172 .context("failed to get head commit")
173 .context(
174 "checkout a commit or specify a from_branch. head does not reveal a commit",
175 )?,
176 ),
177 };
178
179 let (to_branch, to_tip) = match to_branch {
180 Some(name) => (
181 name.to_string(),
182 git_repo
183 .get_tip_of_local_branch(name)
184 .context(format!("cannot find to_branch '{name}'"))?,
185 ),
186 None => {
187 let (name, commit) = git_repo
188 .get_main_or_master_branch()
189 .context("a destination branch (to_branch) is not specified and the defaults (main or master) do not exist")?;
190 (name.to_string(), commit)
191 }
192 };
193
194 match git_repo.get_commits_ahead_behind(&to_tip, &from_tip) {
195 Err(e) => {
196 if e.to_string().contains("is not an ancestor of") {
197 return Err(e).context(format!(
198 "'{from_branch}' is not branched from '{to_branch}'"
199 ));
200 }
201 Err(e).context(format!(
202 "failed to get commits ahead and behind from '{from_branch}' to '{to_branch}'"
203 ))
204 }
205 Ok((ahead, behind)) => Ok((from_branch, to_branch, ahead, behind)),
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use test_utils::git::GitTestRepo;
212
213 use super::*;
214 mod identify_ahead_behind {
215
216 use super::*;
217 use crate::git::oid_to_sha1;
218
219 #[test]
220 fn when_from_branch_doesnt_exist_return_error() -> Result<()> {
221 let test_repo = GitTestRepo::default();
222 let git_repo = Repo::from_path(&test_repo.dir)?;
223
224 test_repo.populate()?;
225 let branch_name = "doesnt_exist";
226 assert_eq!(
227 identify_ahead_behind(&git_repo, &Some(branch_name.to_string()), &None)
228 .unwrap_err()
229 .to_string(),
230 format!("cannot find from_branch '{}'", &branch_name),
231 );
232 Ok(())
233 }
234
235 #[test]
236 fn when_to_branch_doesnt_exist_return_error() -> Result<()> {
237 let test_repo = GitTestRepo::default();
238 let git_repo = Repo::from_path(&test_repo.dir)?;
239
240 test_repo.populate()?;
241 let branch_name = "doesnt_exist";
242 assert_eq!(
243 identify_ahead_behind(&git_repo, &None, &Some(branch_name.to_string()))
244 .unwrap_err()
245 .to_string(),
246 format!("cannot find to_branch '{}'", &branch_name),
247 );
248 Ok(())
249 }
250
251 #[test]
252 fn when_to_branch_is_none_and_no_main_or_master_branch_return_error() -> Result<()> {
253 let test_repo = GitTestRepo::new("notmain")?;
254 let git_repo = Repo::from_path(&test_repo.dir)?;
255
256 test_repo.populate()?;
257
258 assert_eq!(
259 identify_ahead_behind(&git_repo, &None, &None)
260 .unwrap_err()
261 .to_string(),
262 "a destination branch (to_branch) is not specified and the defaults (main or master) do not exist",
263 );
264 Ok(())
265 }
266
267 #[test]
268 fn when_from_branch_is_none_return_as_head() -> Result<()> {
269 let test_repo = GitTestRepo::default();
270 let git_repo = Repo::from_path(&test_repo.dir)?;
271
272 test_repo.populate()?;
273 // create feature branch with 1 commit ahead
274 test_repo.create_branch("feature")?;
275 test_repo.checkout("feature")?;
276 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
277 let head_oid = test_repo.stage_and_commit("add t3.md")?;
278
279 // make feature branch 1 commit behind
280 test_repo.checkout("main")?;
281 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
282 let main_oid = test_repo.stage_and_commit("add t4.md")?;
283 // checkout feature
284 test_repo.checkout("feature")?;
285
286 let (from_branch, to_branch, ahead, behind) =
287 identify_ahead_behind(&git_repo, &None, &None)?;
288
289 assert_eq!(from_branch, "head");
290 assert_eq!(ahead, vec![oid_to_sha1(&head_oid)]);
291 assert_eq!(to_branch, "main");
292 assert_eq!(behind, vec![oid_to_sha1(&main_oid)]);
293 Ok(())
294 }
295
296 #[test]
297 fn when_from_branch_is_not_head_return_as_from_branch() -> Result<()> {
298 let test_repo = GitTestRepo::default();
299 let git_repo = Repo::from_path(&test_repo.dir)?;
300
301 test_repo.populate()?;
302 // create feature branch with 1 commit ahead
303 test_repo.create_branch("feature")?;
304 test_repo.checkout("feature")?;
305 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
306 let head_oid = test_repo.stage_and_commit("add t3.md")?;
307
308 // make feature branch 1 commit behind
309 test_repo.checkout("main")?;
310 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
311 let main_oid = test_repo.stage_and_commit("add t4.md")?;
312
313 let (from_branch, to_branch, ahead, behind) =
314 identify_ahead_behind(&git_repo, &Some("feature".to_string()), &None)?;
315
316 assert_eq!(from_branch, "feature");
317 assert_eq!(ahead, vec![oid_to_sha1(&head_oid)]);
318 assert_eq!(to_branch, "main");
319 assert_eq!(behind, vec![oid_to_sha1(&main_oid)]);
320 Ok(())
321 }
322
323 #[test]
324 fn when_to_branch_is_not_main_return_as_to_branch() -> Result<()> {
325 let test_repo = GitTestRepo::default();
326 let git_repo = Repo::from_path(&test_repo.dir)?;
327
328 test_repo.populate()?;
329 // create dev branch with 1 commit ahead
330 test_repo.create_branch("dev")?;
331 test_repo.checkout("dev")?;
332 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
333 let dev_oid_first = test_repo.stage_and_commit("add t3.md")?;
334
335 // create feature branch with 1 commit ahead of dev
336 test_repo.create_branch("feature")?;
337 test_repo.checkout("feature")?;
338 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
339 let feature_oid = test_repo.stage_and_commit("add t4.md")?;
340
341 // make feature branch 1 behind
342 test_repo.checkout("dev")?;
343 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
344 let dev_oid = test_repo.stage_and_commit("add t3.md")?;
345
346 let (from_branch, to_branch, ahead, behind) = identify_ahead_behind(
347 &git_repo,
348 &Some("feature".to_string()),
349 &Some("dev".to_string()),
350 )?;
351
352 assert_eq!(from_branch, "feature");
353 assert_eq!(ahead, vec![oid_to_sha1(&feature_oid)]);
354 assert_eq!(to_branch, "dev");
355 assert_eq!(behind, vec![oid_to_sha1(&dev_oid)]);
356
357 let (from_branch, to_branch, ahead, behind) =
358 identify_ahead_behind(&git_repo, &Some("feature".to_string()), &None)?;
359
360 assert_eq!(from_branch, "feature");
361 assert_eq!(
362 ahead,
363 vec![oid_to_sha1(&feature_oid), oid_to_sha1(&dev_oid_first)]
364 );
365 assert_eq!(to_branch, "main");
366 assert_eq!(behind, vec![]);
367
368 Ok(())
369 }
370 }
371}