Skip to content

Commit 10729ed

Browse files
Handle + force-push prefix in git push branch completion (#232)
Co-authored-by: Oz <oz-agent@warp.dev> Co-authored-by: lucieleblanc <14223323+lucieleblanc@users.noreply.github.com>
1 parent 31c96f8 commit 10729ed

2 files changed

Lines changed: 263 additions & 23 deletions

File tree

command-signatures/json/git.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3991,11 +3991,12 @@
39913991
"generatorName": "remotes"
39923992
},
39933993
{
3994-
"name": "branch",
3994+
"name": "refspec",
3995+
"description": "Refspec to push (supports +<branch> for force-push)",
39953996
"isOptional": true,
39963997
"generatorName": [
3997-
"local_branches",
3998-
"tags"
3998+
"push_refspec_branches",
3999+
"push_refspec_tags"
39994000
]
40004001
}
40014002
]

command-signatures/src/generators/git.rs

Lines changed: 259 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -529,15 +529,81 @@ pub fn commits_generator() -> Generator {
529529
)
530530
}
531531

532-
pub fn local_branches_generator() -> Generator {
533-
Generator::script(
534-
CommandBuilder::single_command(
535-
"git --no-optional-locks branch --no-color --sort=-committerdate",
536-
),
537-
post_process_branches,
532+
fn local_branches_command() -> CommandBuilder {
533+
CommandBuilder::single_command(
534+
"git --no-optional-locks branch --no-color --sort=-committerdate",
538535
)
539536
}
540537

538+
fn tags_command() -> CommandBuilder {
539+
CommandBuilder::single_command("git --no-optional-locks tag --list --sort=-creatordate")
540+
}
541+
542+
pub fn local_branches_generator() -> Generator {
543+
Generator::script(local_branches_command(), post_process_branches)
544+
}
545+
546+
fn post_process_tags(output: &str) -> GeneratorResults {
547+
output
548+
.lines()
549+
.filter(|line| !line.is_empty())
550+
.map(|line| Suggestion::with_description(line, "tag"))
551+
.collect_ordered_results()
552+
}
553+
554+
const FORCE_PREFIX_MARKER: &str = "__FORCE_PREFIX__";
555+
const FORCE_PREFIX_MARKER_LINE: &str = concat!("__FORCE_PREFIX__", "\n");
556+
557+
/// Wraps a command to prepend a force-prefix marker if the last token starts with `+`.
558+
/// This handles the `git push origin +<branch>` force-push refspec syntax where
559+
/// the `+` prefix would otherwise prevent branch name matching.
560+
fn with_force_prefix_detection(
561+
tokens: &[&str],
562+
has_trailing_whitespace: bool,
563+
cmd: CommandBuilder,
564+
) -> CommandBuilder {
565+
if !has_trailing_whitespace && tokens.last().is_some_and(|t| t.starts_with('+')) {
566+
CommandBuilder::and(
567+
CommandBuilder::single_command(format!("printf '{FORCE_PREFIX_MARKER}\\n'")),
568+
cmd,
569+
)
570+
} else {
571+
cmd
572+
}
573+
}
574+
575+
/// Strips the force-prefix marker from generator output, returning the prefix
576+
/// to prepend to suggestions and the remaining output.
577+
fn strip_force_prefix(out: &str) -> (&str, &str) {
578+
match out.strip_prefix(FORCE_PREFIX_MARKER_LINE) {
579+
Some(rest) => ("+", rest),
580+
None => ("", out),
581+
}
582+
}
583+
584+
/// Prepends a string to the `exact_string` of every suggestion in the results.
585+
fn prepend_to_suggestions(prefix: &str, results: &mut GeneratorResults) {
586+
if !prefix.is_empty() {
587+
for suggestion in &mut results.suggestions {
588+
suggestion.exact_string = format!("{prefix}{}", suggestion.exact_string);
589+
}
590+
}
591+
}
592+
593+
fn post_process_push_refspec_branches(out: &str) -> GeneratorResults {
594+
let (prefix, branch_output) = strip_force_prefix(out);
595+
let mut results = post_process_branches(branch_output);
596+
prepend_to_suggestions(prefix, &mut results);
597+
results
598+
}
599+
600+
fn post_process_push_refspec_tags(out: &str) -> GeneratorResults {
601+
let (prefix, tag_output) = strip_force_prefix(out);
602+
let mut results = post_process_tags(tag_output);
603+
prepend_to_suggestions(prefix, &mut results);
604+
results
605+
}
606+
541607
pub fn generator() -> CommandSignatureGenerators {
542608
CommandSignatureGenerators::new("git")
543609
.add_generator("commits", commits_generator())
@@ -664,18 +730,7 @@ pub fn generator() -> CommandSignatureGenerators {
664730
.collect_unordered_results()
665731
}),
666732
)
667-
.add_generator(
668-
"tags",
669-
Generator::script(
670-
CommandBuilder::single_command("git --no-optional-locks tag --list --sort=-committerdate"),
671-
|output| {
672-
output
673-
.lines()
674-
.filter(|&line| !line.is_empty()).map(|line| Suggestion::with_description(line, "tag"))
675-
.collect_ordered_results()
676-
},
677-
),
678-
)
733+
.add_generator("tags", Generator::script(tags_command(), post_process_tags))
679734
.add_generator(
680735
"files_for_staging",
681736
Generator::script(
@@ -717,12 +772,41 @@ pub fn generator() -> CommandSignatureGenerators {
717772
if tokens.contains(&"-r") || tokens.contains(&"--remotes") {
718773
CommandBuilder::single_command("git --no-optional-locks branch -r --no-color --sort=-committerdate")
719774
} else {
720-
CommandBuilder::single_command("git --no-optional-locks branch --no-color --sort=-committerdate")
775+
local_branches_command()
721776
}
722777
},
723778
post_process_branches,
724779
),
725780
)
781+
// Generators for `git push` refspec arguments. These handle the `+` force-push prefix
782+
// (e.g. `git push origin +branch`) by detecting it in the current token and prepending
783+
// it to branch/tag suggestions so the completer's prefix matcher can match correctly.
784+
.add_generator(
785+
"push_refspec_branches",
786+
Generator::command_from_tokens(
787+
|tokens, has_trailing_whitespace, _| {
788+
with_force_prefix_detection(
789+
tokens,
790+
has_trailing_whitespace,
791+
local_branches_command(),
792+
)
793+
},
794+
post_process_push_refspec_branches,
795+
),
796+
)
797+
.add_generator(
798+
"push_refspec_tags",
799+
Generator::command_from_tokens(
800+
|tokens, has_trailing_whitespace, _| {
801+
with_force_prefix_detection(
802+
tokens,
803+
has_trailing_whitespace,
804+
tags_command(),
805+
)
806+
},
807+
post_process_push_refspec_tags,
808+
),
809+
)
726810
.add_alias(
727811
"alias",
728812
Alias::new(
@@ -754,7 +838,10 @@ pub fn generator() -> CommandSignatureGenerators {
754838

755839
#[cfg(test)]
756840
mod tests {
757-
use crate::generators::git::{post_process_branches, post_process_tracked_files};
841+
use crate::generators::git::{
842+
post_process_branches, post_process_push_refspec_branches, post_process_push_refspec_tags,
843+
post_process_tags, post_process_tracked_files,
844+
};
758845
use warp_completion_metadata::{
759846
GeneratorResults, IconType, Importance, Order, Priority, Suggestion,
760847
};
@@ -857,4 +944,156 @@ mod tests {
857944
}
858945
);
859946
}
947+
948+
#[test]
949+
fn test_post_process_tags() {
950+
let command_output = "v1.0.0\nv2.0.0\nv0.1.0";
951+
assert_eq!(
952+
post_process_tags(command_output),
953+
GeneratorResults {
954+
suggestions: vec![
955+
Suggestion {
956+
exact_string: "v1.0.0".to_owned(),
957+
display_name: None,
958+
description: Some("tag".to_owned()),
959+
priority: Priority::Default,
960+
icon: None,
961+
is_hidden: false,
962+
},
963+
Suggestion {
964+
exact_string: "v2.0.0".to_owned(),
965+
display_name: None,
966+
description: Some("tag".to_owned()),
967+
priority: Priority::Default,
968+
icon: None,
969+
is_hidden: false,
970+
},
971+
Suggestion {
972+
exact_string: "v0.1.0".to_owned(),
973+
display_name: None,
974+
description: Some("tag".to_owned()),
975+
priority: Priority::Default,
976+
icon: None,
977+
is_hidden: false,
978+
},
979+
],
980+
is_ordered: true,
981+
}
982+
);
983+
}
984+
985+
#[test]
986+
fn test_post_process_tags_filters_empty_lines() {
987+
let command_output = "v1.0.0\n\nv2.0.0\n";
988+
assert_eq!(post_process_tags(command_output).suggestions.len(), 2);
989+
}
990+
991+
#[test]
992+
fn test_push_refspec_branches_without_force_prefix() {
993+
// Without the __FORCE_PREFIX__ marker, results should match normal branch processing.
994+
let command_output = "* main\n feature/foo\n develop";
995+
assert_eq!(
996+
post_process_push_refspec_branches(command_output),
997+
post_process_branches(command_output),
998+
);
999+
}
1000+
1001+
#[test]
1002+
fn test_push_refspec_branches_with_force_prefix() {
1003+
// With the __FORCE_PREFIX__ marker, all branch exact_strings should be prefixed with "+".
1004+
let command_output = "__FORCE_PREFIX__\n* main\n feature/foo\n develop";
1005+
let results = post_process_push_refspec_branches(command_output);
1006+
assert_eq!(
1007+
results,
1008+
GeneratorResults {
1009+
suggestions: vec![
1010+
Suggestion {
1011+
exact_string: "+main".to_owned(),
1012+
display_name: None,
1013+
description: Some("Current branch".to_owned()),
1014+
priority: Priority::most_important(),
1015+
icon: Some(IconType::GitBranch),
1016+
is_hidden: false,
1017+
},
1018+
Suggestion {
1019+
exact_string: "+feature/foo".to_owned(),
1020+
display_name: None,
1021+
description: Some("Branch".to_owned()),
1022+
priority: Priority::Default,
1023+
icon: Some(IconType::GitBranch),
1024+
is_hidden: false,
1025+
},
1026+
Suggestion {
1027+
exact_string: "+develop".to_owned(),
1028+
display_name: None,
1029+
description: Some("Branch".to_owned()),
1030+
priority: Priority::Default,
1031+
icon: Some(IconType::GitBranch),
1032+
is_hidden: false,
1033+
},
1034+
],
1035+
is_ordered: true,
1036+
}
1037+
);
1038+
}
1039+
1040+
#[test]
1041+
fn test_push_refspec_tags_without_force_prefix() {
1042+
let command_output = "v1.0.0\nv2.0.0";
1043+
let results = post_process_push_refspec_tags(command_output);
1044+
assert_eq!(
1045+
results,
1046+
GeneratorResults {
1047+
suggestions: vec![
1048+
Suggestion {
1049+
exact_string: "v1.0.0".to_owned(),
1050+
display_name: None,
1051+
description: Some("tag".to_owned()),
1052+
priority: Priority::Default,
1053+
icon: None,
1054+
is_hidden: false,
1055+
},
1056+
Suggestion {
1057+
exact_string: "v2.0.0".to_owned(),
1058+
display_name: None,
1059+
description: Some("tag".to_owned()),
1060+
priority: Priority::Default,
1061+
icon: None,
1062+
is_hidden: false,
1063+
},
1064+
],
1065+
is_ordered: true,
1066+
}
1067+
);
1068+
}
1069+
1070+
#[test]
1071+
fn test_push_refspec_tags_with_force_prefix() {
1072+
let command_output = "__FORCE_PREFIX__\nv1.0.0\nv2.0.0";
1073+
let results = post_process_push_refspec_tags(command_output);
1074+
assert_eq!(
1075+
results,
1076+
GeneratorResults {
1077+
suggestions: vec![
1078+
Suggestion {
1079+
exact_string: "+v1.0.0".to_owned(),
1080+
display_name: None,
1081+
description: Some("tag".to_owned()),
1082+
priority: Priority::Default,
1083+
icon: None,
1084+
is_hidden: false,
1085+
},
1086+
Suggestion {
1087+
exact_string: "+v2.0.0".to_owned(),
1088+
display_name: None,
1089+
description: Some("tag".to_owned()),
1090+
priority: Priority::Default,
1091+
icon: None,
1092+
is_hidden: false,
1093+
},
1094+
],
1095+
is_ordered: true,
1096+
}
1097+
);
1098+
}
8601099
}

0 commit comments

Comments
 (0)