@@ -9,6 +9,7 @@ use crate::{
99 api_service:: state:: ProtocolApiState ,
1010 protocol:: {
1111 SmartSession ,
12+ import_refs:: Refs ,
1213 smart:: { self , PktLine , add_pkt_line_string, try_read_pkt_line} ,
1314 } ,
1415 } ,
@@ -32,6 +33,54 @@ pub fn build_v2_capability_advertisement(hash_kind: HashKind) -> BytesMut {
3233 buf
3334}
3435
36+ fn encode_ls_refs_response (
37+ head_hash : & str ,
38+ git_refs : & [ Refs ] ,
39+ ref_prefixes : & [ String ] ,
40+ symrefs : bool ,
41+ peel : bool ,
42+ ) -> BytesMut {
43+ let mut buf = BytesMut :: new ( ) ;
44+
45+ if !is_protocol_zero_id ( head_hash) {
46+ if symrefs {
47+ add_pkt_line_string (
48+ & mut buf,
49+ format ! ( "{head_hash} HEAD symref-target:refs/heads/main\n " ) ,
50+ ) ;
51+ } else {
52+ add_pkt_line_string ( & mut buf, format ! ( "{head_hash} HEAD\n " ) ) ;
53+ }
54+ }
55+
56+ for git_ref in git_refs {
57+ if !ref_prefixes. is_empty ( )
58+ && !ref_prefixes
59+ . iter ( )
60+ . any ( |prefix| git_ref. ref_name . starts_with ( prefix) )
61+ {
62+ continue ;
63+ }
64+ if peel {
65+ add_pkt_line_string (
66+ & mut buf,
67+ format ! (
68+ "{} {} peeled:{}\n " ,
69+ git_ref. ref_hash, git_ref. ref_name, git_ref. ref_hash
70+ ) ,
71+ ) ;
72+ } else {
73+ add_pkt_line_string (
74+ & mut buf,
75+ format ! ( "{} {}\n " , git_ref. ref_hash, git_ref. ref_name) ,
76+ ) ;
77+ }
78+ }
79+
80+ buf. put ( Bytes :: from_static ( smart:: PKT_LINE_END_MARKER ) ) ;
81+ buf
82+ }
83+
3584pub async fn handle_v2_ls_refs (
3685 session : & SmartSession ,
3786 state : & ProtocolApiState ,
@@ -67,53 +116,13 @@ pub async fn handle_v2_ls_refs(
67116 let ( head_hash, git_refs) = repo_handler. refs_with_head_hash ( ) . await ?;
68117 session. ensure_advertised_object_ids ( & head_hash, & git_refs) ?;
69118
70- let mut buf = BytesMut :: new ( ) ;
71-
72- if symrefs {
73- let head_ref = if is_protocol_zero_id ( & head_hash) {
74- "capabilities^{}"
75- } else {
76- "HEAD"
77- } ;
78- add_pkt_line_string (
79- & mut buf,
80- format ! ( "{head_hash} {head_ref} symref-target:refs/heads/main\n " ) ,
81- ) ;
82- } else {
83- let head_ref = if is_protocol_zero_id ( & head_hash) {
84- "capabilities^{}"
85- } else {
86- "HEAD"
87- } ;
88- add_pkt_line_string ( & mut buf, format ! ( "{head_hash} {head_ref}\n " ) ) ;
89- }
90-
91- for git_ref in & git_refs {
92- if !ref_prefixes. is_empty ( )
93- && !ref_prefixes
94- . iter ( )
95- . any ( |prefix| git_ref. ref_name . starts_with ( prefix) )
96- {
97- continue ;
98- }
99- if peel {
100- add_pkt_line_string (
101- & mut buf,
102- format ! (
103- "{} {} peeled:{}\n " ,
104- git_ref. ref_hash, git_ref. ref_name, git_ref. ref_hash
105- ) ,
106- ) ;
107- } else {
108- add_pkt_line_string (
109- & mut buf,
110- format ! ( "{} {}\n " , git_ref. ref_hash, git_ref. ref_name) ,
111- ) ;
112- }
113- }
114-
115- buf. put ( Bytes :: from_static ( smart:: PKT_LINE_END_MARKER ) ) ;
116- Ok ( buf)
119+ Ok ( encode_ls_refs_response (
120+ & head_hash,
121+ & git_refs,
122+ & ref_prefixes,
123+ symrefs,
124+ peel,
125+ ) )
117126}
118127
119128/// Response of a protocol v2 `fetch` command.
@@ -398,7 +407,37 @@ pub fn is_v2_upload_pack_request(body: &mut Bytes) -> bool {
398407
399408#[ cfg( test) ]
400409mod tests {
410+ use std:: path:: PathBuf ;
411+
412+ use tempfile:: TempDir ;
413+
401414 use super :: * ;
415+ use crate :: {
416+ ceres:: {
417+ api_service:: state:: ProtocolApiState ,
418+ protocol:: { ServiceType , TransportProtocol } ,
419+ } ,
420+ common:: utils:: ZERO_ID ,
421+ jupiter:: storage:: git_db_storage:: fu18_support:: { test_cache, wired_storage} ,
422+ } ;
423+
424+ fn git_ref ( name : & str , hash : & str , default_branch : bool ) -> Refs {
425+ Refs {
426+ id : 0 ,
427+ ref_name : name. to_owned ( ) ,
428+ ref_hash : hash. to_owned ( ) ,
429+ default_branch,
430+ }
431+ }
432+
433+ fn expected_pkt_lines ( lines : & [ String ] ) -> BytesMut {
434+ let mut expected = BytesMut :: new ( ) ;
435+ for line in lines {
436+ add_pkt_line_string ( & mut expected, line. clone ( ) ) ;
437+ }
438+ expected. extend_from_slice ( smart:: PKT_LINE_END_MARKER ) ;
439+ expected
440+ }
402441
403442 #[ test]
404443 fn v2_capability_advertisement_includes_expected_capabilities ( ) {
@@ -450,6 +489,95 @@ mod tests {
450489 assert ! ( err. to_string( ) . contains( "missing command" ) ) ;
451490 }
452491
492+ #[ test]
493+ fn ls_refs_zero_head_flush_only ( ) {
494+ let prefix = [ "refs/heads/" . to_owned ( ) ] ;
495+ for ( ref_prefixes, symrefs, peel) in [
496+ ( & [ ] [ ..] , false , false ) ,
497+ ( & [ ] [ ..] , true , false ) ,
498+ ( & [ ] [ ..] , true , true ) ,
499+ ( & prefix[ ..] , false , false ) ,
500+ ] {
501+ let response = encode_ls_refs_response ( ZERO_ID , & [ ] , ref_prefixes, symrefs, peel) ;
502+ assert_eq ! ( & response[ ..] , smart:: PKT_LINE_END_MARKER ) ;
503+ }
504+ }
505+
506+ #[ test]
507+ fn ls_refs_zero_head_keeps_other_refs ( ) {
508+ let feature = "1111111111111111111111111111111111111111" ;
509+ let tag = "2222222222222222222222222222222222222222" ;
510+ let refs = vec ! [
511+ git_ref( "refs/heads/feature" , feature, false ) ,
512+ git_ref( "refs/tags/v1" , tag, false ) ,
513+ ] ;
514+
515+ let response = encode_ls_refs_response ( ZERO_ID , & refs, & [ ] , false , false ) ;
516+ let expected = expected_pkt_lines ( & [
517+ format ! ( "{feature} refs/heads/feature\n " ) ,
518+ format ! ( "{tag} refs/tags/v1\n " ) ,
519+ ] ) ;
520+ assert_eq ! ( response, expected) ;
521+
522+ let prefixes = vec ! [ "refs/heads/" . to_owned( ) ] ;
523+ let response = encode_ls_refs_response ( ZERO_ID , & refs, & prefixes, true , true ) ;
524+ let expected =
525+ expected_pkt_lines ( & [ format ! ( "{feature} refs/heads/feature peeled:{feature}\n " ) ] ) ;
526+ assert_eq ! ( response, expected) ;
527+ }
528+
529+ #[ test]
530+ fn ls_refs_nonzero_head_unchanged ( ) {
531+ let head = "3333333333333333333333333333333333333333" ;
532+ let feature = "4444444444444444444444444444444444444444" ;
533+ let refs = vec ! [
534+ git_ref( "refs/heads/main" , head, true ) ,
535+ git_ref( "refs/heads/feature" , feature, false ) ,
536+ ] ;
537+
538+ let with_symrefs = encode_ls_refs_response ( head, & refs, & [ ] , true , true ) ;
539+ let expected_with_symrefs = expected_pkt_lines ( & [
540+ format ! ( "{head} HEAD symref-target:refs/heads/main\n " ) ,
541+ format ! ( "{head} refs/heads/main peeled:{head}\n " ) ,
542+ format ! ( "{feature} refs/heads/feature peeled:{feature}\n " ) ,
543+ ] ) ;
544+ assert_eq ! ( with_symrefs, expected_with_symrefs) ;
545+
546+ let without_symrefs = encode_ls_refs_response ( head, & refs, & [ ] , false , false ) ;
547+ let expected_without_symrefs = expected_pkt_lines ( & [
548+ format ! ( "{head} HEAD\n " ) ,
549+ format ! ( "{head} refs/heads/main\n " ) ,
550+ format ! ( "{feature} refs/heads/feature\n " ) ,
551+ ] ) ;
552+ assert_eq ! ( without_symrefs, expected_without_symrefs) ;
553+ }
554+
555+ #[ tokio:: test]
556+ async fn ls_refs_missing_monorepo_path_flush_only ( ) {
557+ let temp = TempDir :: new ( ) . unwrap ( ) ;
558+ let storage = wired_storage ( temp. path ( ) ) . await ;
559+ let state = ProtocolApiState {
560+ storage : storage. clone ( ) ,
561+ git_object_cache : test_cache ( ) . await ,
562+ entity_store : storage. entity_store ( ) ,
563+ } ;
564+ let session = SmartSession :: new (
565+ PathBuf :: from ( "/project/hp16-missing" ) ,
566+ ServiceType :: UploadPack ,
567+ TransportProtocol :: Http ,
568+ ) ;
569+
570+ for mut request in [
571+ Bytes :: from_static ( smart:: PKT_LINE_END_MARKER ) ,
572+ Bytes :: from_static ( b"000csymrefs\n 0000" ) ,
573+ ] {
574+ let response = handle_v2_ls_refs ( & session, & state, & mut request)
575+ . await
576+ . unwrap ( ) ;
577+ assert_eq ! ( & response[ ..] , smart:: PKT_LINE_END_MARKER ) ;
578+ }
579+ }
580+
453581 #[ test]
454582 fn add_packfile_section_header_writes_pkt_line_header ( ) {
455583 let mut buf = BytesMut :: new ( ) ;
0 commit comments