@@ -134,11 +134,15 @@ fn gix_rev_distance(
134134 Some ( count)
135135}
136136
137- /// Auto-detects the default branch (main or master) .
137+ /// Auto-detects the repository's default branch.
138138///
139139/// Strategy:
140140/// 1. Try `git symbolic-ref refs/remotes/origin/HEAD`
141141/// 2. Fall back to checking if `main` or `master` exists locally
142+ /// 3. Fall back to the currently checked-out local branch
143+ ///
144+ /// The final fallback deliberately returns `None` for detached HEAD rather
145+ /// than inventing a default branch.
142146pub fn detect_default_branch ( project_root : & Path ) -> Option < String > {
143147 let repo = gix:: open ( project_root) . ok ( ) ?;
144148
@@ -164,7 +168,72 @@ pub fn detect_default_branch(project_root: &Path) -> Option<String> {
164168 }
165169 }
166170
167- None
171+ current_branch ( project_root)
172+ }
173+
174+ #[ cfg( test) ]
175+ mod default_branch_tests {
176+ use super :: * ;
177+
178+ fn run_git ( project_root : & Path , args : & [ & str ] ) {
179+ let output = std:: process:: Command :: new ( crate :: git:: git_program ( ) )
180+ . args ( args)
181+ . current_dir ( project_root)
182+ . output ( )
183+ . unwrap ( ) ;
184+ assert ! (
185+ output. status. success( ) ,
186+ "git {args:?} failed: {}" ,
187+ String :: from_utf8_lossy( & output. stderr)
188+ ) ;
189+ }
190+
191+ fn custom_default_repo ( ) -> ( tempfile:: TempDir , PathBuf ) {
192+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
193+ let project_root = temp. path ( ) . to_path_buf ( ) ;
194+ run_git ( & project_root, & [ "init" , "-b" , "trunk" ] ) ;
195+ run_git ( & project_root, & [ "config" , "user.email" , "test@example.com" ] ) ;
196+ run_git ( & project_root, & [ "config" , "user.name" , "TraceDecay Test" ] ) ;
197+ std:: fs:: write ( project_root. join ( "fixture" ) , b"fixture" ) . unwrap ( ) ;
198+ run_git ( & project_root, & [ "add" , "fixture" ] ) ;
199+ run_git ( & project_root, & [ "commit" , "-m" , "fixture" ] ) ;
200+ ( temp, project_root)
201+ }
202+
203+ #[ test]
204+ fn detects_checked_out_custom_default_without_origin_head ( ) {
205+ let ( _temp, project_root) = custom_default_repo ( ) ;
206+
207+ assert_eq ! (
208+ detect_default_branch( & project_root) . as_deref( ) ,
209+ Some ( "trunk" )
210+ ) ;
211+ }
212+
213+ #[ test]
214+ fn detached_custom_default_does_not_guess ( ) {
215+ let ( _temp, project_root) = custom_default_repo ( ) ;
216+ run_git ( & project_root, & [ "checkout" , "--detach" , "HEAD" ] ) ;
217+
218+ assert_eq ! ( detect_default_branch( & project_root) , None ) ;
219+ }
220+
221+ #[ tokio:: test]
222+ async fn detached_legacy_store_refuses_to_invent_default_metadata ( ) {
223+ let ( temp, project_root) = custom_default_repo ( ) ;
224+ run_git ( & project_root, & [ "checkout" , "--detach" , "HEAD" ] ) ;
225+ let data_dir = temp. path ( ) . join ( "profile-shard" ) ;
226+ std:: fs:: create_dir ( & data_dir) . unwrap ( ) ;
227+ std:: fs:: write ( data_dir. join ( crate :: config:: DB_FILENAME ) , b"graph" ) . unwrap ( ) ;
228+
229+ let Err ( error) = prepare_branch_tracking_in_layout ( & project_root, "trunk" , & data_dir) . await
230+ else {
231+ panic ! ( "detached legacy store must not invent a default branch" )
232+ } ;
233+
234+ assert ! ( error. to_string( ) . contains( "default branch is unknown" ) ) ;
235+ assert ! ( !data_dir. join( crate :: storage:: BRANCH_META_FILENAME ) . exists( ) ) ;
236+ }
168237}
169238
170239/// Sanitizes a branch name for use as a filename.
@@ -355,7 +424,8 @@ pub fn find_nearest_tracked_ancestor(
355424pub enum BranchAddOutcome {
356425 /// The project has no `.tracedecay/` index; nothing was done.
357426 NotIndexed ,
358- /// The branch was already tracked; no copy/sync was performed.
427+ /// The branch was already tracked; no copy/sync was performed. Legacy
428+ /// single-DB metadata may have been persisted for the default branch.
359429 AlreadyTracked ,
360430 /// A new branch DB was created from the nearest ancestor and synced.
361431 Added ,
@@ -408,8 +478,8 @@ pub async fn prepare_branch_tracking_in_layout(
408478 } ;
409479
410480 let meta_path = tracedecay_dir. join ( "branch-meta.json" ) ;
411- let mut meta = match branch_meta:: load_branch_meta ( tracedecay_dir) {
412- Some ( meta) => meta,
481+ let ( mut meta, metadata_was_missing ) = match branch_meta:: load_branch_meta ( tracedecay_dir) {
482+ Some ( meta) => ( meta, false ) ,
413483 None if meta_path. exists ( ) => {
414484 return Err ( crate :: errors:: TraceDecayError :: Config {
415485 message : format ! (
@@ -419,13 +489,26 @@ pub async fn prepare_branch_tracking_in_layout(
419489 } ) ;
420490 }
421491 None => {
422- let default = detect_default_branch ( project_root) . unwrap_or_else ( || "main" . to_string ( ) ) ;
423- branch_meta:: BranchMeta :: new_for_dir ( tracedecay_dir, & default)
492+ let default = detect_default_branch ( project_root) . ok_or_else ( || {
493+ crate :: errors:: TraceDecayError :: Config {
494+ message : format ! (
495+ "cannot initialize missing branch metadata at '{}': repository default branch is unknown (detached HEAD or no default ref)" ,
496+ meta_path. display( )
497+ ) ,
498+ }
499+ } ) ?;
500+ (
501+ branch_meta:: BranchMeta :: for_legacy_single_db ( tracedecay_dir, & default) ,
502+ true ,
503+ )
424504 }
425505 } ;
426506 prune_missing_branch_dbs ( tracedecay_dir, & mut meta) ;
427507
428508 if meta. is_tracked ( branch_name) {
509+ if metadata_was_missing {
510+ branch_meta:: save_branch_meta ( tracedecay_dir, & meta) ?;
511+ }
429512 return Ok ( BranchTrackingPreparation :: AlreadyTracked ) ;
430513 }
431514
@@ -485,6 +568,60 @@ pub async fn prepare_branch_tracking_in_layout(
485568 } ) )
486569}
487570
571+ #[ cfg( test) ]
572+ #[ tokio:: test]
573+ async fn default_branch_bootstrap_persists_canonical_metadata ( ) {
574+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
575+ let project_root = temp. path ( ) . join ( "repo" ) ;
576+ std:: fs:: create_dir_all ( & project_root) . unwrap ( ) ;
577+ let run_git = |args : & [ & str ] | {
578+ let output = std:: process:: Command :: new ( crate :: git:: git_program ( ) )
579+ . args ( args)
580+ . current_dir ( & project_root)
581+ . output ( )
582+ . unwrap ( ) ;
583+ assert ! (
584+ output. status. success( ) ,
585+ "git {args:?} failed: {}" ,
586+ String :: from_utf8_lossy( & output. stderr)
587+ ) ;
588+ } ;
589+ run_git ( & [ "init" , "-b" , "main" ] ) ;
590+ std:: fs:: write ( project_root. join ( "fixture" ) , b"fixture" ) . unwrap ( ) ;
591+ run_git ( & [ "add" , "fixture" ] ) ;
592+ run_git ( & [
593+ "-c" ,
594+ "user.email=test@example.com" ,
595+ "-c" ,
596+ "user.name=TraceDecay Test" ,
597+ "commit" ,
598+ "-m" ,
599+ "fixture" ,
600+ ] ) ;
601+
602+ let data_dir = temp. path ( ) . join ( "profile-shard" ) ;
603+ std:: fs:: create_dir_all ( & data_dir) . unwrap ( ) ;
604+ std:: fs:: write ( data_dir. join ( crate :: config:: DB_FILENAME ) , b"graph" ) . unwrap ( ) ;
605+ let meta_path = data_dir. join ( crate :: storage:: BRANCH_META_FILENAME ) ;
606+ assert ! ( !meta_path. exists( ) ) ;
607+
608+ let outcome = prepare_branch_tracking_in_layout ( & project_root, "main" , & data_dir)
609+ . await
610+ . unwrap ( ) ;
611+
612+ assert ! ( matches!( outcome, BranchTrackingPreparation :: AlreadyTracked ) ) ;
613+ let meta = crate :: branch_meta:: load_branch_meta ( & data_dir) . unwrap ( ) ;
614+ assert_eq ! ( meta. default_branch, "main" ) ;
615+ assert_eq ! ( meta. branches. len( ) , 1 ) ;
616+ let default = meta. branches . get ( "main" ) . unwrap ( ) ;
617+ assert_eq ! ( default . db_file, crate :: config:: db_filename( & data_dir) ) ;
618+ assert ! ( default . parent. is_none( ) ) ;
619+ assert_eq ! ( default . created_at, "0" ) ;
620+ assert_eq ! ( default . last_synced_at, "0" ) ;
621+ assert ! ( !meta_path. with_extension( "json.tmp" ) . exists( ) ) ;
622+ assert ! ( !data_dir. join( "branches" ) . exists( ) ) ;
623+ }
624+
488625pub fn finalize_prepared_branch_tracking ( tracedecay_dir : & Path , prepared : & PreparedBranchTracking ) {
489626 if let Some ( mut meta) = crate :: branch_meta:: load_branch_meta ( tracedecay_dir) {
490627 meta. touch_synced ( & prepared. branch_name ) ;
0 commit comments