From 5c305e922e19e4ac65c6a1473be67145a1c73f2b Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 25 Feb 2026 16:46:02 +0000 Subject: feat: forward unrecognised push options to git servers Any -o option passed to `git push` that is not handled by ngit (title, description) is forwarded verbatim to the git server via git2::PushOptions::remote_push_options. This allows options such as `-o secret-scanning.skip` to pass through transparently. `ngit send` gains a matching -o / --push-option flag for the same purpose. --- CHANGELOG.md | 4 ++++ src/bin/git_remote_nostr/main.rs | 14 ++++++++++++-- src/bin/git_remote_nostr/push.rs | 16 ++++++++++++++++ src/bin/ngit/sub_commands/send.rs | 38 +++++++++++++++++++++++--------------- src/bin/ngit/sub_commands/sync.rs | 1 + src/lib/push.rs | 19 ++++++++++++++++++- 6 files changed, 74 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 070a414..3d58e1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- git server push option passthrough, enabling `-o secret-scanning.skip` for grasp servers + ## [2.2.1] - 2026-02-25 ### Fixed diff --git a/src/bin/git_remote_nostr/main.rs b/src/bin/git_remote_nostr/main.rs index e0821e9..6186ed3 100644 --- a/src/bin/git_remote_nostr/main.rs +++ b/src/bin/git_remote_nostr/main.rs @@ -28,6 +28,7 @@ use crate::{client::Client, git::Repo}; struct PushOptions { title: Option, description: Option, + git_server_extras: Vec, } /// Strip git's c-style quoting from a push-option value. @@ -118,6 +119,7 @@ mod list; mod push; #[tokio::main] +#[allow(clippy::too_many_lines)] async fn main() -> Result<()> { if std::env::var("NGITTEST").is_ok() { std::env::set_var("NGIT_VERBOSE", "1"); @@ -177,16 +179,23 @@ async fn main() -> Result<()> { } ["option", "push-option", rest @ ..] => { let option = strip_git_quoting(&rest.join(" ")); - if let Some((key, value)) = option.split_once('=') { + let handled_by_ngit = if let Some((key, value)) = option.split_once('=') { match key { "title" => { push_options.title = Some(decode_push_option_escapes(value)); + true } "description" => { push_options.description = Some(decode_push_option_escapes(value)); + true } - _ => {} + _ => false, } + } else { + false + }; + if !handled_by_ngit { + push_options.git_server_extras.push(option); } println!("ok"); } @@ -206,6 +215,7 @@ async fn main() -> Result<()> { &mut client, list_outputs.clone(), title_description, + push_options.git_server_extras.clone(), ) .await?; push_options = PushOptions::default(); diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index b64cdd9..06624f4 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -45,6 +45,7 @@ use repo_state::RepoState; use crate::{client::Client, git::Repo}; #[allow(clippy::too_many_lines)] +#[allow(clippy::too_many_arguments)] #[allow(clippy::type_complexity)] pub async fn run_push( git_repo: &Repo, @@ -54,6 +55,7 @@ pub async fn run_push( client: &mut Client, list_outputs: Option, bool)>>, title_description: Option<(String, String)>, + git_server_push_options: Vec, ) -> Result<()> { let refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?; @@ -132,6 +134,7 @@ pub async fn run_push( existing_state, &term, title_description.as_ref(), + &git_server_push_options, ) .await?; @@ -159,6 +162,8 @@ pub async fn run_push( .cloned() .collect::>(); if !refspecs.is_empty() { + let push_options_refs: Vec<&str> = + git_server_push_options.iter().map(String::as_str).collect(); let _ = push_to_remote( git_repo, &git_server_url, @@ -166,6 +171,7 @@ pub async fn run_push( &remote_refspecs, &term, is_grasp_server_clone_url(&git_server_url), + &push_options_refs, ); } } @@ -187,6 +193,7 @@ async fn create_and_publish_events_and_proposals( existing_state: HashMap, term: &Term, title_description: Option<&(String, String)>, + git_server_push_options: &[String], ) -> Result<(Vec, bool)> { let (signer, mut user_ref, _) = load_existing_login( &Some(git_repo), @@ -281,6 +288,7 @@ async fn create_and_publish_events_and_proposals( &signer, term, title_description, + git_server_push_options, ) .await?; for e in proposal_events { @@ -315,6 +323,7 @@ async fn process_proposal_refspecs( signer: &Arc, term: &Term, title_description: Option<&(String, String)>, + git_server_push_options: &[String], ) -> Result<(Vec, Vec)> { let mut events = vec![]; let mut rejected_proposal_refspecs = vec![]; @@ -357,6 +366,7 @@ async fn process_proposal_refspecs( signer, term, title_description, + git_server_push_options, ) .await? { @@ -398,6 +408,7 @@ async fn process_proposal_refspecs( signer, term, title_description, + git_server_push_options, ) .await? { @@ -469,6 +480,7 @@ async fn process_proposal_refspecs( signer, term, title_description, + git_server_push_options, ) .await? { @@ -492,6 +504,7 @@ async fn generate_patches_or_pr_event_or_pr_updates( signer: &Arc, term: &Term, title_description: Option<&(String, String)>, + git_server_push_options: &[String], ) -> Result> { let parent_is_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST)); let use_pr = parent_is_pr || git_repo.are_commits_too_big_for_patches(ahead); @@ -499,6 +512,8 @@ async fn generate_patches_or_pr_event_or_pr_updates( if use_pr { let tip = ahead.first().context("no commits")?; // ahead is youngest first let first_commit = ahead.last().context("no commits")?; + let push_options_refs: Vec<&str> = + git_server_push_options.iter().map(String::as_str).collect(); select_servers_push_refs_and_generate_pr_or_pr_update_event( client, git_repo, @@ -512,6 +527,7 @@ async fn generate_patches_or_pr_event_or_pr_updates( signer, false, term, + &push_options_refs, ) .await .context(format!( diff --git a/src/bin/ngit/sub_commands/send.rs b/src/bin/ngit/sub_commands/send.rs index 325ad89..6b18e84 100644 --- a/src/bin/ngit/sub_commands/send.rs +++ b/src/bin/ngit/sub_commands/send.rs @@ -50,6 +50,9 @@ pub struct SubCommandArgs { /// publish as Patches even if they may be > 60kb #[arg(long, action)] pub(crate) force_patch: bool, + #[clap(long = "push-option", short = 'o', value_parser, num_args = 0..)] + /// git push options to pass to the git server (eg. -o secret-scanning.skip) + pub(crate) push_options: Vec, } /// Validates send command arguments for non-interactive mode. @@ -351,21 +354,26 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re let events = if as_pr { let tip = commits.last().context("no commits")?; // commits has been reversed to oldest first let first_commit = commits.first().context("no commits")?; - select_servers_push_refs_and_generate_pr_or_pr_update_event( - &client, - &git_repo, - &repo_ref, - tip, - first_commit, - git_repo.get_commit_parent(first_commit).ok().as_ref(), - &mut user_ref, - root_proposal.as_ref(), - &cover_letter_title_description, - &signer, - true, - &console::Term::stdout(), - ) - .await? + { + let push_options_refs: Vec<&str> = + args.push_options.iter().map(String::as_str).collect(); + select_servers_push_refs_and_generate_pr_or_pr_update_event( + &client, + &git_repo, + &repo_ref, + tip, + first_commit, + git_repo.get_commit_parent(first_commit).ok().as_ref(), + &mut user_ref, + root_proposal.as_ref(), + &cover_letter_title_description, + &signer, + true, + &console::Term::stdout(), + &push_options_refs, + ) + .await? + } } else { let events = generate_cover_letter_and_patch_events( cover_letter_title_description.clone(), diff --git a/src/bin/ngit/sub_commands/sync.rs b/src/bin/ngit/sub_commands/sync.rs index daebb1b..b377ab4 100644 --- a/src/bin/ngit/sub_commands/sync.rs +++ b/src/bin/ngit/sub_commands/sync.rs @@ -187,6 +187,7 @@ pub async fn launch(args: &SubCommandArgs) -> Result<()> { &refspecs, &term, *is_grasp_server || is_grasp_server_clone_url(url), + &[], ) { Err(error) => { term.write_line(&format!( diff --git a/src/lib/push.rs b/src/lib/push.rs index 274a16a..5544066 100644 --- a/src/lib/push.rs +++ b/src/lib/push.rs @@ -49,6 +49,7 @@ pub fn push_to_remote( remote_refspecs: &[String], term: &Term, is_grasp_server: bool, + git_server_push_options: &[&str], ) -> Result>> { let server_url = git_server_url.parse::()?; let protocols_to_attempt = @@ -69,6 +70,7 @@ pub fn push_to_remote( decoded_nostr_url.ssh_key_file_path().as_ref(), remote_refspecs, term, + git_server_push_options, ) { Err(error) => { term.write_line( @@ -149,6 +151,7 @@ pub fn push_to_remote_url( ssh_key_file: Option<&String>, remote_refspecs: &[String], term: &Term, + git_server_push_options: &[&str], ) -> Result>> { let git_config = git_repo.git_repo.config()?; let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?; @@ -262,6 +265,9 @@ pub fn push_to_remote_url( } }); push_options.remote_callbacks(remote_callbacks); + if !git_server_push_options.is_empty() { + push_options.remote_push_options(git_server_push_options); + } git_server_remote.push(remote_refspecs, Some(&mut push_options))?; let _ = git_server_remote.disconnect(); let reporter = push_reporter.lock().unwrap(); @@ -417,6 +423,7 @@ pub async fn select_servers_push_refs_and_generate_pr_or_pr_update_event( signer: &Arc, interactive: bool, term: &Term, + git_server_push_options: &[&str], ) -> Result> { let git_repo_path = git_repo.get_path()?; let mut to_try = vec![]; @@ -483,6 +490,7 @@ pub async fn select_servers_push_refs_and_generate_pr_or_pr_update_event( git_ref.clone(), signer, term, + git_server_push_options, ) .await?; for url in to_try { @@ -717,6 +725,7 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( git_ref: Option, signer: &Arc, term: &Term, + git_server_push_options: &[&str], ) -> Result<(Option>, Vec<(String, Result<()>)>)> { let mut responses: Vec<(String, Result<()>)> = vec![]; @@ -747,7 +756,14 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( let refspec = format!("{tip}:{git_ref_used}"); let res = if is_grasp_server_clone_url(clone_url) { - push_to_remote_url(git_repo, clone_url, None, &[refspec], term) + push_to_remote_url( + git_repo, + clone_url, + None, + &[refspec], + term, + git_server_push_options, + ) } else { // anticipated only when pushing to user's own repo or a personal-fork with // non-grasp git servers. this is used to extract prefered protocols / ssh @@ -769,6 +785,7 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event( &[refspec], term, false, + git_server_push_options, ) }; -- cgit v1.2.3