diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-25 16:46:02 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-26 15:26:14 +0000 |
| commit | 5c305e922e19e4ac65c6a1473be67145a1c73f2b (patch) | |
| tree | fb03feb94b324297df4b3560af379c6c89b1ed6e /src/bin | |
| parent | 3017faf6d346fa9328c5979c6e9c6bc471bd3942 (diff) | |
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.
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/git_remote_nostr/main.rs | 14 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/push.rs | 16 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/send.rs | 38 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/sync.rs | 1 |
4 files changed, 52 insertions, 17 deletions
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}; | |||
| 28 | struct PushOptions { | 28 | struct PushOptions { |
| 29 | title: Option<String>, | 29 | title: Option<String>, |
| 30 | description: Option<String>, | 30 | description: Option<String>, |
| 31 | git_server_extras: Vec<String>, | ||
| 31 | } | 32 | } |
| 32 | 33 | ||
| 33 | /// Strip git's c-style quoting from a push-option value. | 34 | /// Strip git's c-style quoting from a push-option value. |
| @@ -118,6 +119,7 @@ mod list; | |||
| 118 | mod push; | 119 | mod push; |
| 119 | 120 | ||
| 120 | #[tokio::main] | 121 | #[tokio::main] |
| 122 | #[allow(clippy::too_many_lines)] | ||
| 121 | async fn main() -> Result<()> { | 123 | async fn main() -> Result<()> { |
| 122 | if std::env::var("NGITTEST").is_ok() { | 124 | if std::env::var("NGITTEST").is_ok() { |
| 123 | std::env::set_var("NGIT_VERBOSE", "1"); | 125 | std::env::set_var("NGIT_VERBOSE", "1"); |
| @@ -177,16 +179,23 @@ async fn main() -> Result<()> { | |||
| 177 | } | 179 | } |
| 178 | ["option", "push-option", rest @ ..] => { | 180 | ["option", "push-option", rest @ ..] => { |
| 179 | let option = strip_git_quoting(&rest.join(" ")); | 181 | let option = strip_git_quoting(&rest.join(" ")); |
| 180 | if let Some((key, value)) = option.split_once('=') { | 182 | let handled_by_ngit = if let Some((key, value)) = option.split_once('=') { |
| 181 | match key { | 183 | match key { |
| 182 | "title" => { | 184 | "title" => { |
| 183 | push_options.title = Some(decode_push_option_escapes(value)); | 185 | push_options.title = Some(decode_push_option_escapes(value)); |
| 186 | true | ||
| 184 | } | 187 | } |
| 185 | "description" => { | 188 | "description" => { |
| 186 | push_options.description = Some(decode_push_option_escapes(value)); | 189 | push_options.description = Some(decode_push_option_escapes(value)); |
| 190 | true | ||
| 187 | } | 191 | } |
| 188 | _ => {} | 192 | _ => false, |
| 189 | } | 193 | } |
| 194 | } else { | ||
| 195 | false | ||
| 196 | }; | ||
| 197 | if !handled_by_ngit { | ||
| 198 | push_options.git_server_extras.push(option); | ||
| 190 | } | 199 | } |
| 191 | println!("ok"); | 200 | println!("ok"); |
| 192 | } | 201 | } |
| @@ -206,6 +215,7 @@ async fn main() -> Result<()> { | |||
| 206 | &mut client, | 215 | &mut client, |
| 207 | list_outputs.clone(), | 216 | list_outputs.clone(), |
| 208 | title_description, | 217 | title_description, |
| 218 | push_options.git_server_extras.clone(), | ||
| 209 | ) | 219 | ) |
| 210 | .await?; | 220 | .await?; |
| 211 | push_options = PushOptions::default(); | 221 | 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; | |||
| 45 | use crate::{client::Client, git::Repo}; | 45 | use crate::{client::Client, git::Repo}; |
| 46 | 46 | ||
| 47 | #[allow(clippy::too_many_lines)] | 47 | #[allow(clippy::too_many_lines)] |
| 48 | #[allow(clippy::too_many_arguments)] | ||
| 48 | #[allow(clippy::type_complexity)] | 49 | #[allow(clippy::type_complexity)] |
| 49 | pub async fn run_push( | 50 | pub async fn run_push( |
| 50 | git_repo: &Repo, | 51 | git_repo: &Repo, |
| @@ -54,6 +55,7 @@ pub async fn run_push( | |||
| 54 | client: &mut Client, | 55 | client: &mut Client, |
| 55 | list_outputs: Option<HashMap<String, (HashMap<String, String>, bool)>>, | 56 | list_outputs: Option<HashMap<String, (HashMap<String, String>, bool)>>, |
| 56 | title_description: Option<(String, String)>, | 57 | title_description: Option<(String, String)>, |
| 58 | git_server_push_options: Vec<String>, | ||
| 57 | ) -> Result<()> { | 59 | ) -> Result<()> { |
| 58 | let refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?; | 60 | let refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?; |
| 59 | 61 | ||
| @@ -132,6 +134,7 @@ pub async fn run_push( | |||
| 132 | existing_state, | 134 | existing_state, |
| 133 | &term, | 135 | &term, |
| 134 | title_description.as_ref(), | 136 | title_description.as_ref(), |
| 137 | &git_server_push_options, | ||
| 135 | ) | 138 | ) |
| 136 | .await?; | 139 | .await?; |
| 137 | 140 | ||
| @@ -159,6 +162,8 @@ pub async fn run_push( | |||
| 159 | .cloned() | 162 | .cloned() |
| 160 | .collect::<Vec<String>>(); | 163 | .collect::<Vec<String>>(); |
| 161 | if !refspecs.is_empty() { | 164 | if !refspecs.is_empty() { |
| 165 | let push_options_refs: Vec<&str> = | ||
| 166 | git_server_push_options.iter().map(String::as_str).collect(); | ||
| 162 | let _ = push_to_remote( | 167 | let _ = push_to_remote( |
| 163 | git_repo, | 168 | git_repo, |
| 164 | &git_server_url, | 169 | &git_server_url, |
| @@ -166,6 +171,7 @@ pub async fn run_push( | |||
| 166 | &remote_refspecs, | 171 | &remote_refspecs, |
| 167 | &term, | 172 | &term, |
| 168 | is_grasp_server_clone_url(&git_server_url), | 173 | is_grasp_server_clone_url(&git_server_url), |
| 174 | &push_options_refs, | ||
| 169 | ); | 175 | ); |
| 170 | } | 176 | } |
| 171 | } | 177 | } |
| @@ -187,6 +193,7 @@ async fn create_and_publish_events_and_proposals( | |||
| 187 | existing_state: HashMap<String, String>, | 193 | existing_state: HashMap<String, String>, |
| 188 | term: &Term, | 194 | term: &Term, |
| 189 | title_description: Option<&(String, String)>, | 195 | title_description: Option<&(String, String)>, |
| 196 | git_server_push_options: &[String], | ||
| 190 | ) -> Result<(Vec<String>, bool)> { | 197 | ) -> Result<(Vec<String>, bool)> { |
| 191 | let (signer, mut user_ref, _) = load_existing_login( | 198 | let (signer, mut user_ref, _) = load_existing_login( |
| 192 | &Some(git_repo), | 199 | &Some(git_repo), |
| @@ -281,6 +288,7 @@ async fn create_and_publish_events_and_proposals( | |||
| 281 | &signer, | 288 | &signer, |
| 282 | term, | 289 | term, |
| 283 | title_description, | 290 | title_description, |
| 291 | git_server_push_options, | ||
| 284 | ) | 292 | ) |
| 285 | .await?; | 293 | .await?; |
| 286 | for e in proposal_events { | 294 | for e in proposal_events { |
| @@ -315,6 +323,7 @@ async fn process_proposal_refspecs( | |||
| 315 | signer: &Arc<dyn NostrSigner>, | 323 | signer: &Arc<dyn NostrSigner>, |
| 316 | term: &Term, | 324 | term: &Term, |
| 317 | title_description: Option<&(String, String)>, | 325 | title_description: Option<&(String, String)>, |
| 326 | git_server_push_options: &[String], | ||
| 318 | ) -> Result<(Vec<Event>, Vec<String>)> { | 327 | ) -> Result<(Vec<Event>, Vec<String>)> { |
| 319 | let mut events = vec![]; | 328 | let mut events = vec![]; |
| 320 | let mut rejected_proposal_refspecs = vec![]; | 329 | let mut rejected_proposal_refspecs = vec![]; |
| @@ -357,6 +366,7 @@ async fn process_proposal_refspecs( | |||
| 357 | signer, | 366 | signer, |
| 358 | term, | 367 | term, |
| 359 | title_description, | 368 | title_description, |
| 369 | git_server_push_options, | ||
| 360 | ) | 370 | ) |
| 361 | .await? | 371 | .await? |
| 362 | { | 372 | { |
| @@ -398,6 +408,7 @@ async fn process_proposal_refspecs( | |||
| 398 | signer, | 408 | signer, |
| 399 | term, | 409 | term, |
| 400 | title_description, | 410 | title_description, |
| 411 | git_server_push_options, | ||
| 401 | ) | 412 | ) |
| 402 | .await? | 413 | .await? |
| 403 | { | 414 | { |
| @@ -469,6 +480,7 @@ async fn process_proposal_refspecs( | |||
| 469 | signer, | 480 | signer, |
| 470 | term, | 481 | term, |
| 471 | title_description, | 482 | title_description, |
| 483 | git_server_push_options, | ||
| 472 | ) | 484 | ) |
| 473 | .await? | 485 | .await? |
| 474 | { | 486 | { |
| @@ -492,6 +504,7 @@ async fn generate_patches_or_pr_event_or_pr_updates( | |||
| 492 | signer: &Arc<dyn NostrSigner>, | 504 | signer: &Arc<dyn NostrSigner>, |
| 493 | term: &Term, | 505 | term: &Term, |
| 494 | title_description: Option<&(String, String)>, | 506 | title_description: Option<&(String, String)>, |
| 507 | git_server_push_options: &[String], | ||
| 495 | ) -> Result<Vec<Event>> { | 508 | ) -> Result<Vec<Event>> { |
| 496 | let parent_is_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST)); | 509 | let parent_is_pr = root_proposal.is_some_and(|proposal| proposal.kind.eq(&KIND_PULL_REQUEST)); |
| 497 | let use_pr = parent_is_pr || git_repo.are_commits_too_big_for_patches(ahead); | 510 | 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( | |||
| 499 | if use_pr { | 512 | if use_pr { |
| 500 | let tip = ahead.first().context("no commits")?; // ahead is youngest first | 513 | let tip = ahead.first().context("no commits")?; // ahead is youngest first |
| 501 | let first_commit = ahead.last().context("no commits")?; | 514 | let first_commit = ahead.last().context("no commits")?; |
| 515 | let push_options_refs: Vec<&str> = | ||
| 516 | git_server_push_options.iter().map(String::as_str).collect(); | ||
| 502 | select_servers_push_refs_and_generate_pr_or_pr_update_event( | 517 | select_servers_push_refs_and_generate_pr_or_pr_update_event( |
| 503 | client, | 518 | client, |
| 504 | git_repo, | 519 | git_repo, |
| @@ -512,6 +527,7 @@ async fn generate_patches_or_pr_event_or_pr_updates( | |||
| 512 | signer, | 527 | signer, |
| 513 | false, | 528 | false, |
| 514 | term, | 529 | term, |
| 530 | &push_options_refs, | ||
| 515 | ) | 531 | ) |
| 516 | .await | 532 | .await |
| 517 | .context(format!( | 533 | .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 { | |||
| 50 | /// publish as Patches even if they may be > 60kb | 50 | /// publish as Patches even if they may be > 60kb |
| 51 | #[arg(long, action)] | 51 | #[arg(long, action)] |
| 52 | pub(crate) force_patch: bool, | 52 | pub(crate) force_patch: bool, |
| 53 | #[clap(long = "push-option", short = 'o', value_parser, num_args = 0..)] | ||
| 54 | /// git push options to pass to the git server (eg. -o secret-scanning.skip) | ||
| 55 | pub(crate) push_options: Vec<String>, | ||
| 53 | } | 56 | } |
| 54 | 57 | ||
| 55 | /// Validates send command arguments for non-interactive mode. | 58 | /// 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 | |||
| 351 | let events = if as_pr { | 354 | let events = if as_pr { |
| 352 | let tip = commits.last().context("no commits")?; // commits has been reversed to oldest first | 355 | let tip = commits.last().context("no commits")?; // commits has been reversed to oldest first |
| 353 | let first_commit = commits.first().context("no commits")?; | 356 | let first_commit = commits.first().context("no commits")?; |
| 354 | select_servers_push_refs_and_generate_pr_or_pr_update_event( | 357 | { |
| 355 | &client, | 358 | let push_options_refs: Vec<&str> = |
| 356 | &git_repo, | 359 | args.push_options.iter().map(String::as_str).collect(); |
| 357 | &repo_ref, | 360 | select_servers_push_refs_and_generate_pr_or_pr_update_event( |
| 358 | tip, | 361 | &client, |
| 359 | first_commit, | 362 | &git_repo, |
| 360 | git_repo.get_commit_parent(first_commit).ok().as_ref(), | 363 | &repo_ref, |
| 361 | &mut user_ref, | 364 | tip, |
| 362 | root_proposal.as_ref(), | 365 | first_commit, |
| 363 | &cover_letter_title_description, | 366 | git_repo.get_commit_parent(first_commit).ok().as_ref(), |
| 364 | &signer, | 367 | &mut user_ref, |
| 365 | true, | 368 | root_proposal.as_ref(), |
| 366 | &console::Term::stdout(), | 369 | &cover_letter_title_description, |
| 367 | ) | 370 | &signer, |
| 368 | .await? | 371 | true, |
| 372 | &console::Term::stdout(), | ||
| 373 | &push_options_refs, | ||
| 374 | ) | ||
| 375 | .await? | ||
| 376 | } | ||
| 369 | } else { | 377 | } else { |
| 370 | let events = generate_cover_letter_and_patch_events( | 378 | let events = generate_cover_letter_and_patch_events( |
| 371 | cover_letter_title_description.clone(), | 379 | 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<()> { | |||
| 187 | &refspecs, | 187 | &refspecs, |
| 188 | &term, | 188 | &term, |
| 189 | *is_grasp_server || is_grasp_server_clone_url(url), | 189 | *is_grasp_server || is_grasp_server_clone_url(url), |
| 190 | &[], | ||
| 190 | ) { | 191 | ) { |
| 191 | Err(error) => { | 192 | Err(error) => { |
| 192 | term.write_line(&format!( | 193 | term.write_line(&format!( |