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--CHANGELOG.md1
-rw-r--r--src/bin/git_remote_nostr/main.rs21
-rw-r--r--src/bin/git_remote_nostr/push.rs22
-rw-r--r--tests/git_remote_nostr/push.rs121
4 files changed, 163 insertions, 2 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cbfddd..2c2b07c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
25- `ngit list --json` - output proposals as JSON 25- `ngit list --json` - output proposals as JSON
26- `ngit list --status` - filter by status (open,draft,closed,applied) 26- `ngit list --status` - filter by status (open,draft,closed,applied)
27- `ngit init --hashtag` - specify repository hashtag 27- `ngit init --hashtag` - specify repository hashtag
28- Push options for PR title/description: `git push --push-option=title="..." --push-option=description="..."`
28 29
29### Removed 30### Removed
30 31
diff --git a/src/bin/git_remote_nostr/main.rs b/src/bin/git_remote_nostr/main.rs
index d7151d8..e57dff7 100644
--- a/src/bin/git_remote_nostr/main.rs
+++ b/src/bin/git_remote_nostr/main.rs
@@ -24,6 +24,12 @@ use nostr::nips::nip19::Nip19Coordinate;
24 24
25use crate::{client::Client, git::Repo}; 25use crate::{client::Client, git::Repo};
26 26
27#[derive(Default, Clone)]
28struct PushOptions {
29 title: Option<String>,
30 description: Option<String>,
31}
32
27mod fetch; 33mod fetch;
28mod list; 34mod list;
29mod push; 35mod push;
@@ -71,6 +77,7 @@ async fn main() -> Result<()> {
71 let mut line = String::new(); 77 let mut line = String::new();
72 78
73 let mut list_outputs = None; 79 let mut list_outputs = None;
80 let mut push_options: PushOptions = PushOptions::default();
74 loop { 81 loop {
75 let tokens = read_line(&stdin, &mut line)?; 82 let tokens = read_line(&stdin, &mut line)?;
76 83
@@ -79,11 +86,23 @@ async fn main() -> Result<()> {
79 println!("option"); 86 println!("option");
80 println!("push"); 87 println!("push");
81 println!("fetch"); 88 println!("fetch");
89 println!("push-options");
82 println!(); 90 println!();
83 } 91 }
84 ["option", "verbosity"] => { 92 ["option", "verbosity"] => {
85 println!("ok"); 93 println!("ok");
86 } 94 }
95 ["option", "push-option", rest @ ..] => {
96 let option = rest.join(" ");
97 if let Some((key, value)) = option.split_once('=') {
98 match key {
99 "title" => push_options.title = Some(value.to_string()),
100 "description" => push_options.description = Some(value.to_string()),
101 _ => {}
102 }
103 }
104 println!("ok");
105 }
87 ["option", ..] => { 106 ["option", ..] => {
88 println!("unsupported"); 107 println!("unsupported");
89 } 108 }
@@ -98,8 +117,10 @@ async fn main() -> Result<()> {
98 refspec, 117 refspec,
99 &client, 118 &client,
100 list_outputs.clone(), 119 list_outputs.clone(),
120 push_options.title.as_ref().zip(push_options.description.as_ref()).map(|(t, d)| (t.clone(), d.clone())),
101 ) 121 )
102 .await?; 122 .await?;
123 push_options = PushOptions::default();
103 } 124 }
104 ["list"] => { 125 ["list"] => {
105 list_outputs = Some(list::run_list(&git_repo, &repo_ref, false).await?); 126 list_outputs = Some(list::run_list(&git_repo, &repo_ref, false).await?);
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs
index c2f4ddd..f20c264 100644
--- a/src/bin/git_remote_nostr/push.rs
+++ b/src/bin/git_remote_nostr/push.rs
@@ -52,6 +52,7 @@ pub async fn run_push(
52 initial_refspec: &str, 52 initial_refspec: &str,
53 client: &Client, 53 client: &Client,
54 list_outputs: Option<HashMap<String, (HashMap<String, String>, bool)>>, 54 list_outputs: Option<HashMap<String, (HashMap<String, String>, bool)>>,
55 title_description: Option<(String, String)>,
55) -> Result<()> { 56) -> Result<()> {
56 let refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?; 57 let refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?;
57 58
@@ -129,6 +130,7 @@ pub async fn run_push(
129 client, 130 client,
130 existing_state, 131 existing_state,
131 &term, 132 &term,
133 title_description.as_ref(),
132 ) 134 )
133 .await?; 135 .await?;
134 136
@@ -174,6 +176,7 @@ pub async fn run_push(
174} 176}
175 177
176#[allow(clippy::too_many_lines)] 178#[allow(clippy::too_many_lines)]
179#[allow(clippy::too_many_arguments)]
177async fn create_and_publish_events_and_proposals( 180async fn create_and_publish_events_and_proposals(
178 git_repo: &Repo, 181 git_repo: &Repo,
179 repo_ref: &RepoRef, 182 repo_ref: &RepoRef,
@@ -182,6 +185,7 @@ async fn create_and_publish_events_and_proposals(
182 client: &Client, 185 client: &Client,
183 existing_state: HashMap<String, String>, 186 existing_state: HashMap<String, String>,
184 term: &Term, 187 term: &Term,
188 title_description: Option<&(String, String)>,
185) -> Result<(Vec<String>, bool)> { 189) -> Result<(Vec<String>, bool)> {
186 let (signer, mut user_ref, _) = load_existing_login( 190 let (signer, mut user_ref, _) = load_existing_login(
187 &Some(git_repo), 191 &Some(git_repo),
@@ -276,6 +280,7 @@ async fn create_and_publish_events_and_proposals(
276 &mut user_ref, 280 &mut user_ref,
277 &signer, 281 &signer,
278 term, 282 term,
283 title_description,
279 ) 284 )
280 .await?; 285 .await?;
281 for e in proposal_events { 286 for e in proposal_events {
@@ -300,6 +305,7 @@ async fn create_and_publish_events_and_proposals(
300} 305}
301 306
302#[allow(clippy::too_many_lines)] 307#[allow(clippy::too_many_lines)]
308#[allow(clippy::too_many_arguments)]
303async fn process_proposal_refspecs( 309async fn process_proposal_refspecs(
304 client: &Client, 310 client: &Client,
305 git_repo: &Repo, 311 git_repo: &Repo,
@@ -308,6 +314,7 @@ async fn process_proposal_refspecs(
308 user_ref: &mut UserRef, 314 user_ref: &mut UserRef,
309 signer: &Arc<dyn NostrSigner>, 315 signer: &Arc<dyn NostrSigner>,
310 term: &Term, 316 term: &Term,
317 title_description: Option<&(String, String)>,
311) -> Result<(Vec<Event>, Vec<String>)> { 318) -> Result<(Vec<Event>, Vec<String>)> {
312 let mut events = vec![]; 319 let mut events = vec![];
313 let mut rejected_proposal_refspecs = vec![]; 320 let mut rejected_proposal_refspecs = vec![];
@@ -349,6 +356,7 @@ async fn process_proposal_refspecs(
349 Some(proposal), 356 Some(proposal),
350 signer, 357 signer,
351 term, 358 term,
359 title_description,
352 ) 360 )
353 .await? 361 .await?
354 { 362 {
@@ -389,6 +397,7 @@ async fn process_proposal_refspecs(
389 Some(proposal), 397 Some(proposal),
390 signer, 398 signer,
391 term, 399 term,
400 title_description,
392 ) 401 )
393 .await? 402 .await?
394 { 403 {
@@ -451,7 +460,15 @@ async fn process_proposal_refspecs(
451 ); 460 );
452 } 461 }
453 for event in generate_patches_or_pr_event_or_pr_updates( 462 for event in generate_patches_or_pr_event_or_pr_updates(
454 client, git_repo, repo_ref, &ahead, user_ref, None, signer, term, 463 client,
464 git_repo,
465 repo_ref,
466 &ahead,
467 user_ref,
468 None,
469 signer,
470 term,
471 title_description,
455 ) 472 )
456 .await? 473 .await?
457 { 474 {
@@ -474,6 +491,7 @@ async fn generate_patches_or_pr_event_or_pr_updates(
474 root_proposal: Option<&Event>, 491 root_proposal: Option<&Event>,
475 signer: &Arc<dyn NostrSigner>, 492 signer: &Arc<dyn NostrSigner>,
476 term: &Term, 493 term: &Term,
494 title_description: Option<&(String, String)>,
477) -> Result<Vec<Event>> { 495) -> Result<Vec<Event>> {
478 let parent_is_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST)); 496 let parent_is_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST));
479 let use_pr = parent_is_pr || git_repo.are_commits_too_big_for_patches(ahead); 497 let use_pr = parent_is_pr || git_repo.are_commits_too_big_for_patches(ahead);
@@ -490,7 +508,7 @@ async fn generate_patches_or_pr_event_or_pr_updates(
490 git_repo.get_commit_parent(first_commit).ok().as_ref(), 508 git_repo.get_commit_parent(first_commit).ok().as_ref(),
491 user_ref, 509 user_ref,
492 root_proposal, 510 root_proposal,
493 &None, 511 &title_description.map(|(t, d)| (t.clone(), d.clone())),
494 signer, 512 signer,
495 false, 513 false,
496 term, 514 term,
diff --git a/tests/git_remote_nostr/push.rs b/tests/git_remote_nostr/push.rs
index 404d07f..38b1f8c 100644
--- a/tests/git_remote_nostr/push.rs
+++ b/tests/git_remote_nostr/push.rs
@@ -1,4 +1,5 @@
1use git2::Signature; 1use git2::Signature;
2use ngit::git_events::KIND_PULL_REQUEST;
2use rstest::*; 3use rstest::*;
3 4
4use super::*; 5use super::*;
@@ -1774,6 +1775,126 @@ async fn push_new_pr_branch_creates_proposal() -> Result<()> {
1774 Ok(()) 1775 Ok(())
1775} 1776}
1776 1777
1778#[tokio::test]
1779#[serial]
1780async fn push_new_pr_branch_with_title_description_options_creates_pr_with_custom_title_description()
1781-> Result<()> {
1782 let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
1783 let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
1784
1785 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
1786 Relay::new(8051, None, None),
1787 Relay::new(8052, None, None),
1788 Relay::new(8053, None, None),
1789 Relay::new(8055, None, None),
1790 Relay::new(8056, None, None),
1791 Relay::new(8057, None, None),
1792 );
1793 r51.events = events.clone();
1794 r55.events = events.clone();
1795
1796 #[allow(clippy::mutable_key_type)]
1797 let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
1798 let branch_name = "pr/my-pr-with-title";
1799
1800 let cli_tester_handle = std::thread::spawn(move || -> Result<String> {
1801 let mut git_repo = clone_git_repo_with_nostr_url()?;
1802 git_repo.delete_dir_on_drop = false;
1803 git_repo.create_branch(branch_name)?;
1804 git_repo.checkout(branch_name)?;
1805
1806 let large_content = "x".repeat(70 * 1024);
1807 std::fs::write(git_repo.dir.join("large_file.txt"), large_content)?;
1808 git_repo.stage_and_commit("large_file.txt")?;
1809
1810 let mut p = CliTester::new_git_with_remote_helper_from_dir(
1811 &git_repo.dir,
1812 [
1813 "push",
1814 "--push-option=title=Custom PR Title",
1815 "--push-option=description=Custom PR description here",
1816 "-u",
1817 "origin",
1818 branch_name,
1819 ],
1820 );
1821 cli_expect_nostr_fetch(&mut p)?;
1822 p.expect("git servers: listing refs...\r\n")?;
1823 p.expect_eventually_and_print(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
1824 let output = p.expect_end_eventually()?;
1825
1826 for p in [51, 52, 53, 55, 56, 57] {
1827 relay::shutdown_relay(8000 + p)?;
1828 }
1829
1830 Ok(output)
1831 });
1832 let _ = join!(
1833 r51.listen_until_close(),
1834 r52.listen_until_close(),
1835 r53.listen_until_close(),
1836 r55.listen_until_close(),
1837 r56.listen_until_close(),
1838 r57.listen_until_close(),
1839 );
1840
1841 let output = cli_tester_handle.join().unwrap()?;
1842
1843 assert_eq!(
1844 output,
1845 format!(" * [new branch] {branch_name} -> {branch_name}\r\nbranch '{branch_name}' set up to track 'origin/{branch_name}'.\r\n").as_str(),
1846 );
1847
1848 let new_events = r55
1849 .events
1850 .iter()
1851 .cloned()
1852 .collect::<HashSet<Event>>()
1853 .difference(&before)
1854 .cloned()
1855 .collect::<Vec<Event>>();
1856 assert_eq!(new_events.len(), 1, "should create exactly 1 PR event");
1857
1858 let pr_event = new_events.first().unwrap();
1859
1860 assert!(
1861 pr_event.kind.eq(&KIND_PULL_REQUEST),
1862 "event should be a PR event"
1863 );
1864
1865 let title_tag = pr_event.tags.iter().find(|t| t.as_slice()[0].eq("subject"));
1866 assert!(
1867 title_tag.is_some(),
1868 "PR event should have a subject tag for title"
1869 );
1870 assert_eq!(
1871 title_tag.unwrap().as_slice()[1],
1872 "Custom PR Title",
1873 "title should match push-option"
1874 );
1875
1876 assert_eq!(
1877 pr_event.content, "Custom PR description here",
1878 "description should match push-option"
1879 );
1880
1881 let branch_name_tag = pr_event
1882 .tags
1883 .iter()
1884 .find(|t| t.as_slice()[0].eq("branch-name"));
1885 assert!(
1886 branch_name_tag.is_some(),
1887 "PR event should have a branch-name tag"
1888 );
1889 assert_eq!(
1890 branch_name_tag.unwrap().as_slice()[1],
1891 branch_name.replace("pr/", ""),
1892 "branch-name tag should match"
1893 );
1894
1895 Ok(())
1896}
1897
1777mod push_from_another_maintainer { 1898mod push_from_another_maintainer {
1778 1899
1779 // TODO that has issued announcement 1900 // TODO that has issued announcement