@@ -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+
541607pub 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) ]
756840mod 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\n v2.0.0\n v0.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 \n v2.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\n v2.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__\n v1.0.0\n v2.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