11// Copyright (c) .NET Foundation and Contributors. All rights reserved.
22// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33
4+ #nullable enable
5+
46using System ;
57using System . Collections . Generic ;
68using System . CommandLine ;
@@ -415,6 +417,121 @@ private static int MainInner(string[] args)
415417 return ( int ) exitCode ;
416418 }
417419
420+ /// <summary>
421+ /// Detects the default branch name for the repository following the algorithm:
422+ /// 1. If the upstream remote exists, use its HEAD reference
423+ /// 2. If the origin remote exists, use its HEAD reference
424+ /// 3. If any remote exists, pick one arbitrarily and use its HEAD reference
425+ /// 4. If only one local branch exists, use that one
426+ /// 5. Use git config init.defaultBranch if the named branch exists locally
427+ /// 6. Use the first local branch that exists from: master, main, develop.
428+ /// </summary>
429+ /// <param name="context">The git context to query.</param>
430+ /// <returns>The detected default branch name, defaulting to "master" if none can be determined.</returns>
431+ private static string DetectDefaultBranch ( GitContext context )
432+ {
433+ if ( context is LibGit2 . LibGit2Context libgit2Context )
434+ {
435+ LibGit2Sharp . Repository repository = libgit2Context . Repository ;
436+
437+ // Step 1-3: Check remotes for HEAD reference
438+ string [ ] remotePreferenceOrder = { "upstream" , "origin" } ;
439+
440+ foreach ( string remoteName in remotePreferenceOrder )
441+ {
442+ LibGit2Sharp . Remote ? remote = repository . Network . Remotes [ remoteName ] ;
443+ if ( remote is object )
444+ {
445+ string ? defaultBranch = GetDefaultBranchFromRemote ( repository , remoteName ) ;
446+ if ( ! string . IsNullOrEmpty ( defaultBranch ) )
447+ {
448+ return defaultBranch ;
449+ }
450+ }
451+ }
452+
453+ // Check any other remotes if upstream/origin didn't work
454+ foreach ( LibGit2Sharp . Remote remote in repository . Network . Remotes )
455+ {
456+ if ( remote . Name != "upstream" && remote . Name != "origin" )
457+ {
458+ string ? defaultBranch = GetDefaultBranchFromRemote ( repository , remote . Name ) ;
459+ if ( ! string . IsNullOrEmpty ( defaultBranch ) )
460+ {
461+ return defaultBranch ;
462+ }
463+ }
464+ }
465+
466+ // Step 4: If only one local branch exists, use that one
467+ LibGit2Sharp . Branch [ ] localBranches = repository . Branches . Where ( b => ! b . IsRemote ) . ToArray ( ) ;
468+ if ( localBranches . Length == 1 )
469+ {
470+ return localBranches [ 0 ] . FriendlyName ;
471+ }
472+
473+ // Step 5: Use git config init.defaultBranch if the named branch exists locally
474+ try
475+ {
476+ string ? configDefaultBranch = repository . Config . Get < string > ( "init.defaultBranch" ) ? . Value ;
477+ if ( ! string . IsNullOrEmpty ( configDefaultBranch ) &&
478+ localBranches . Any ( b => b . FriendlyName == configDefaultBranch ) )
479+ {
480+ return configDefaultBranch ;
481+ }
482+ }
483+ catch
484+ {
485+ // Ignore config read errors
486+ }
487+
488+ // Step 6: Use the first local branch that exists from: master, main, develop
489+ string [ ] commonBranchNames = { "master" , "main" , "develop" } ;
490+ foreach ( string branchName in commonBranchNames )
491+ {
492+ if ( localBranches . Any ( b => b . FriendlyName == branchName ) )
493+ {
494+ return branchName ;
495+ }
496+ }
497+ }
498+
499+ // Fallback to "master" if nothing else works
500+ return "master" ;
501+ }
502+
503+ /// <summary>
504+ /// Gets the default branch name from a remote's HEAD reference.
505+ /// </summary>
506+ /// <param name="repository">The repository to query.</param>
507+ /// <param name="remoteName">The name of the remote.</param>
508+ /// <returns>The default branch name, or null if it cannot be determined.</returns>
509+ private static string ? GetDefaultBranchFromRemote ( LibGit2Sharp . Repository repository , string remoteName )
510+ {
511+ try
512+ {
513+ // Try to get the symbolic reference for the remote HEAD
514+ string remoteHeadRef = $ "refs/remotes/{ remoteName } /HEAD";
515+ LibGit2Sharp . Reference ? remoteHead = repository . Refs [ remoteHeadRef ] ;
516+
517+ if ( remoteHead ? . TargetIdentifier is object )
518+ {
519+ // Extract branch name from refs/remotes/{remote}/{branch}
520+ string targetRef = remoteHead . TargetIdentifier ;
521+ if ( targetRef . StartsWith ( $ "refs/remotes/{ remoteName } /") )
522+ {
523+ return targetRef . Substring ( $ "refs/remotes/{ remoteName } /". Length ) ;
524+ }
525+ }
526+ }
527+ catch
528+ {
529+ // Ignore errors when trying to read remote HEAD
530+ }
531+
532+ return null ;
533+ }
534+
418535 private static async Task < int > OnInstallCommand ( string path , string version , string [ ] source )
419536 {
420537 if ( ! SemanticVersion . TryParse ( string . IsNullOrEmpty ( version ) ? DefaultVersionSpec : version , out SemanticVersion semver ) )
@@ -423,12 +540,28 @@ private static async Task<int> OnInstallCommand(string path, string version, str
423540 return ( int ) ExitCodes . InvalidVersionSpec ;
424541 }
425542
543+ string searchPath = GetSpecifiedOrCurrentDirectoryPath ( path ) ;
544+ if ( ! Directory . Exists ( searchPath ) )
545+ {
546+ Console . Error . WriteLine ( "\" {0}\" is not an existing directory." , searchPath ) ;
547+ return ( int ) ExitCodes . NoGitRepo ;
548+ }
549+
550+ using var context = GitContext . Create ( searchPath , engine : GitContext . Engine . ReadWrite ) ;
551+ if ( ! context . IsRepository )
552+ {
553+ Console . Error . WriteLine ( "No git repo found at or above: \" {0}\" " , searchPath ) ;
554+ return ( int ) ExitCodes . NoGitRepo ;
555+ }
556+
557+ string defaultBranch = DetectDefaultBranch ( context ) ;
558+
426559 var options = new VersionOptions
427560 {
428561 Version = semver ,
429562 PublicReleaseRefSpec = new string [ ]
430563 {
431- @"^refs/heads/master $" ,
564+ $ @ "^refs/heads/{ defaultBranch } $",
432565 @"^refs/heads/v\d+(?:\.\d+)?$" ,
433566 } ,
434567 CloudBuild = new VersionOptions . CloudBuildOptions
@@ -439,26 +572,13 @@ private static async Task<int> OnInstallCommand(string path, string version, str
439572 } ,
440573 } ,
441574 } ;
442- string searchPath = GetSpecifiedOrCurrentDirectoryPath ( path ) ;
443- if ( ! Directory . Exists ( searchPath ) )
444- {
445- Console . Error . WriteLine ( "\" {0}\" is not an existing directory." , searchPath ) ;
446- return ( int ) ExitCodes . NoGitRepo ;
447- }
448-
449- using var context = GitContext . Create ( searchPath , engine : GitContext . Engine . ReadWrite ) ;
450- if ( ! context . IsRepository )
451- {
452- Console . Error . WriteLine ( "No git repo found at or above: \" {0}\" " , searchPath ) ;
453- return ( int ) ExitCodes . NoGitRepo ;
454- }
455575
456576 if ( string . IsNullOrEmpty ( path ) )
457577 {
458578 path = context . WorkingTreePath ;
459579 }
460580
461- VersionOptions existingOptions = context . VersionFile . GetVersion ( ) ;
581+ VersionOptions ? existingOptions = context . VersionFile . GetVersion ( ) ;
462582 if ( existingOptions is not null )
463583 {
464584 if ( ! string . IsNullOrEmpty ( version ) && version != DefaultVersionSpec )
0 commit comments