diff options
Diffstat (limited to 'src/bin/ngit/sub_commands/sync.rs')
| -rw-r--r-- | src/bin/ngit/sub_commands/sync.rs | 72 |
1 files changed, 70 insertions, 2 deletions
diff --git a/src/bin/ngit/sub_commands/sync.rs b/src/bin/ngit/sub_commands/sync.rs index 146bcbc..c326817 100644 --- a/src/bin/ngit/sub_commands/sync.rs +++ b/src/bin/ngit/sub_commands/sync.rs | |||
| @@ -228,6 +228,10 @@ pub async fn launch(args: &SubCommandArgs) -> Result<()> { | |||
| 228 | // delete ref from remote | 228 | // delete ref from remote |
| 229 | let mut not_deleted = vec![]; | 229 | let mut not_deleted = vec![]; |
| 230 | for remote_ref_name in remote_state.keys() { | 230 | for remote_ref_name in remote_state.keys() { |
| 231 | // skip peeled-tag dereference markers — not real refs | ||
| 232 | if remote_ref_name.ends_with("^{}") { | ||
| 233 | continue; | ||
| 234 | } | ||
| 231 | // skip unspecified refs | 235 | // skip unspecified refs |
| 232 | if let Some(full_ref_name) = &full_ref_name { | 236 | if let Some(full_ref_name) = &full_ref_name { |
| 233 | if remote_ref_name != full_ref_name { | 237 | if remote_ref_name != full_ref_name { |
| @@ -253,6 +257,11 @@ pub async fn launch(args: &SubCommandArgs) -> Result<()> { | |||
| 253 | // add or update ref on remote | 257 | // add or update ref on remote |
| 254 | let mut not_updated = vec![]; | 258 | let mut not_updated = vec![]; |
| 255 | for nostr_ref_name in nostr_state.state.keys() { | 259 | for nostr_ref_name in nostr_state.state.keys() { |
| 260 | // skip peeled-tag dereference markers (e.g. refs/tags/v1.0.0^{}) | ||
| 261 | // — these are not real git refs and cannot appear in refspecs | ||
| 262 | if nostr_ref_name.ends_with("^{}") { | ||
| 263 | continue; | ||
| 264 | } | ||
| 256 | // skip unspecified refs | 265 | // skip unspecified refs |
| 257 | if let Some(full_ref_name) = &full_ref_name { | 266 | if let Some(full_ref_name) = &full_ref_name { |
| 258 | if nostr_ref_name != full_ref_name { | 267 | if nostr_ref_name != full_ref_name { |
| @@ -391,8 +400,9 @@ pub async fn launch(args: &SubCommandArgs) -> Result<()> { | |||
| 391 | } | 400 | } |
| 392 | 401 | ||
| 393 | fn invalid_nostr_state_ref(ref_name: &str) -> bool { | 402 | fn invalid_nostr_state_ref(ref_name: &str) -> bool { |
| 394 | ref_name.starts_with("refs/heads/pr/") | 403 | ref_name.ends_with("^{}") |
| 395 | && !(ref_name.starts_with("refs/heads/") || ref_name.starts_with("refs/tags/")) | 404 | || ref_name.starts_with("refs/heads/pr/") |
| 405 | || (!ref_name.starts_with("refs/heads/") && !ref_name.starts_with("refs/tags/")) | ||
| 396 | } | 406 | } |
| 397 | 407 | ||
| 398 | fn identify_missing_refs(git_repo: &Repo, state: &HashMap<String, String>) -> Vec<String> { | 408 | fn identify_missing_refs(git_repo: &Repo, state: &HashMap<String, String>) -> Vec<String> { |
| @@ -490,3 +500,61 @@ fn fetch_missing_refs( | |||
| 490 | missing_refs | 500 | missing_refs |
| 491 | } | 501 | } |
| 492 | } | 502 | } |
| 503 | |||
| 504 | #[cfg(test)] | ||
| 505 | mod tests { | ||
| 506 | use super::invalid_nostr_state_ref; | ||
| 507 | |||
| 508 | // Regression test: annotated-tag peeled refs (ending with ^{}) must be | ||
| 509 | // treated as invalid nostr state refs so they are never used to build | ||
| 510 | // git refspecs. Before the fix, these passed through and caused git2 to | ||
| 511 | // reject the push with "invalid refspec refs/remotes/origin/v1.4.4^{}:…". | ||
| 512 | #[test] | ||
| 513 | fn annotated_tag_peeled_ref_is_invalid() { | ||
| 514 | assert!( | ||
| 515 | invalid_nostr_state_ref("refs/tags/v1.4.4^{}"), | ||
| 516 | "peeled annotated-tag ref must be invalid" | ||
| 517 | ); | ||
| 518 | assert!( | ||
| 519 | invalid_nostr_state_ref("refs/tags/v1.0.0^{}"), | ||
| 520 | "peeled annotated-tag ref must be invalid" | ||
| 521 | ); | ||
| 522 | } | ||
| 523 | |||
| 524 | #[test] | ||
| 525 | fn pr_ref_is_invalid() { | ||
| 526 | assert!( | ||
| 527 | invalid_nostr_state_ref("refs/heads/pr/42"), | ||
| 528 | "PR branch refs must be invalid" | ||
| 529 | ); | ||
| 530 | } | ||
| 531 | |||
| 532 | #[test] | ||
| 533 | fn arbitrary_non_heads_non_tags_ref_is_invalid() { | ||
| 534 | assert!( | ||
| 535 | invalid_nostr_state_ref("refs/notes/commits"), | ||
| 536 | "refs outside heads/tags must be invalid" | ||
| 537 | ); | ||
| 538 | assert!(invalid_nostr_state_ref("HEAD"), "HEAD must be invalid"); | ||
| 539 | } | ||
| 540 | |||
| 541 | #[test] | ||
| 542 | fn normal_branch_and_tag_refs_are_valid() { | ||
| 543 | assert!( | ||
| 544 | !invalid_nostr_state_ref("refs/heads/main"), | ||
| 545 | "normal branch must be valid" | ||
| 546 | ); | ||
| 547 | assert!( | ||
| 548 | !invalid_nostr_state_ref("refs/heads/feature/foo"), | ||
| 549 | "feature branch must be valid" | ||
| 550 | ); | ||
| 551 | assert!( | ||
| 552 | !invalid_nostr_state_ref("refs/tags/v1.4.4"), | ||
| 553 | "lightweight tag must be valid" | ||
| 554 | ); | ||
| 555 | assert!( | ||
| 556 | !invalid_nostr_state_ref("refs/tags/v2.0.0-rc1"), | ||
| 557 | "release-candidate tag must be valid" | ||
| 558 | ); | ||
| 559 | } | ||
| 560 | } | ||