@@ -2,17 +2,76 @@ package git
22
33import (
44 "fmt"
5+ "os"
56 "os/exec"
67 "strconv"
78 "strings"
89 "time"
910)
1011
12+ // mainlineBranchNames are exact branch names considered mainline.
13+ var mainlineBranchNames = map [string ]bool {
14+ "main" : true ,
15+ "master" : true ,
16+ "trunk" : true ,
17+ "develop" : true ,
18+ "dev" : true ,
19+ }
20+
21+ // mainlineGitflowPrefixes are branch name prefixes considered mainline.
22+ var mainlineGitflowPrefixes = []string {
23+ "release/" ,
24+ "hotfix/" ,
25+ "support/" ,
26+ }
27+
28+ // matchesPatterns reports whether name matches any user-defined pattern.
29+ // Each pattern is tested as both an exact match and a prefix, so "JIRA-"
30+ // matches "JIRA-123" and "feat/" matches "feat/new-thing". Patterns ending
31+ // in "/" are prefix-only (never an exact branch name).
32+ func matchesPatterns (name string , patterns []string ) bool {
33+ for _ , p := range patterns {
34+ if p == "" {
35+ continue
36+ }
37+ if name == p || strings .HasPrefix (name , p ) {
38+ return true
39+ }
40+ }
41+ return false
42+ }
43+
44+ // isMainlineBranch reports whether a branch name is a mainline or gitflow branch.
45+ func isMainlineBranch (name string ) bool {
46+ if mainlineBranchNames [name ] {
47+ return true
48+ }
49+ for _ , prefix := range mainlineGitflowPrefixes {
50+ if strings .HasPrefix (name , prefix ) {
51+ return true
52+ }
53+ }
54+ return false
55+ }
56+
1157// RainOptions controls how rain/hydrate updates local branches from remotes.
1258type RainOptions struct {
1359 // RiskyMode allows destructive realignment of local-only commits after
1460 // creating a backup branch.
1561 RiskyMode bool
62+
63+ // BranchMode controls which branches are eligible for syncing.
64+ // Defaults to BranchSyncMainline when zero.
65+ BranchMode BranchSyncMode
66+
67+ // SyncTags fetches all tags from the remote in addition to branch refs.
68+ // Off by default because tag fetches can pull large or unwanted history.
69+ SyncTags bool
70+
71+ // MainlinePatterns extends the built-in mainline branch list with user-defined
72+ // patterns. Each entry is either an exact branch name or a prefix ending in "/"
73+ // (e.g. "feat/", "JIRA-"). Only used when BranchMode == BranchSyncMainline.
74+ MainlinePatterns []string
1675}
1776
1877const (
@@ -77,7 +136,11 @@ func RainRepository(repoPath string, opts RainOptions) (RainResult, error) {
77136 return result , err
78137 }
79138 if hasRemote {
80- cmd := exec .Command ("git" , "fetch" , "--all" , "--prune" )
139+ fetchArgs := []string {"fetch" , "--all" , "--prune" }
140+ if opts .SyncTags {
141+ fetchArgs = append (fetchArgs , "--tags" )
142+ }
143+ cmd := exec .Command ("git" , fetchArgs ... )
81144 cmd .Dir = repoPath
82145 if output , fetchErr := cmd .CombinedOutput (); fetchErr != nil {
83146 return result , commandError ("git fetch --all --prune" , fetchErr , output )
@@ -89,6 +152,40 @@ func RainRepository(repoPath string, opts RainOptions) (RainResult, error) {
89152 return result , err
90153 }
91154
155+ // Resolve effective branch sync mode.
156+ mode := opts .BranchMode
157+ if mode == "" {
158+ mode = BranchSyncMainline
159+ }
160+
161+ // all-branches: create local tracking refs for remote branches not yet local.
162+ if mode == BranchSyncAllBranches && hasRemote {
163+ newBranches , trackErr := createLocalTrackingFromRemotes (repoPath , branches )
164+ if trackErr != nil {
165+ fmt .Fprintf (os .Stderr , "note: could not enumerate remote branches: %v\n " , trackErr )
166+ } else {
167+ branches = append (branches , newBranches ... )
168+ }
169+ }
170+
171+ // Filter branches to those the selected mode covers.
172+ filtered := branches [:0 ]
173+ for _ , b := range branches {
174+ switch mode {
175+ case BranchSyncMainline :
176+ if isMainlineBranch (b .Branch ) || matchesPatterns (b .Branch , opts .MainlinePatterns ) {
177+ filtered = append (filtered , b )
178+ }
179+ case BranchSyncCheckedOut :
180+ if checkedOut [b .Branch ] {
181+ filtered = append (filtered , b )
182+ }
183+ default : // BranchSyncAllLocal, BranchSyncAllBranches
184+ filtered = append (filtered , b )
185+ }
186+ }
187+ branches = filtered
188+
92189 hasStaged , err := HasStagedChanges (repoPath )
93190 if err != nil {
94191 return result , fmt .Errorf ("check staged changes: %w" , err )
@@ -362,6 +459,66 @@ func updateBranchRef(repoPath, branch, oldSHA, newSHA string) error {
362459 return nil
363460}
364461
462+ // createLocalTrackingFromRemotes enumerates remote-tracking refs and creates
463+ // local tracking branches for any remote branch that does not yet exist locally.
464+ // Returns the newly-created localBranchTracking entries.
465+ func createLocalTrackingFromRemotes (repoPath string , existing []localBranchTracking ) ([]localBranchTracking , error ) {
466+ localSet := make (map [string ]bool , len (existing ))
467+ for _ , b := range existing {
468+ localSet [b .Branch ] = true
469+ }
470+
471+ remotes , err := listRepoRemotes (repoPath )
472+ if err != nil {
473+ return nil , err
474+ }
475+
476+ cmd := exec .Command ("git" , "for-each-ref" , "refs/remotes" , "--format=%(refname:short)" )
477+ cmd .Dir = repoPath
478+ out , err := cmd .CombinedOutput ()
479+ if err != nil {
480+ return nil , commandError ("git for-each-ref refs/remotes" , err , out )
481+ }
482+
483+ var created []localBranchTracking
484+ for _ , line := range strings .Split (strings .TrimSpace (string (out )), "\n " ) {
485+ ref := strings .TrimSpace (line )
486+ if ref == "" {
487+ continue
488+ }
489+ var branchName , upstream string
490+ for _ , remote := range remotes {
491+ prefix := remote + "/"
492+ if strings .HasPrefix (ref , prefix ) {
493+ name := strings .TrimPrefix (ref , prefix )
494+ if name == "HEAD" {
495+ break
496+ }
497+ branchName = name
498+ upstream = ref
499+ break
500+ }
501+ }
502+ if branchName == "" || localSet [branchName ] {
503+ continue
504+ }
505+ trackCmd := exec .Command ("git" , "branch" , "--track" , branchName , upstream )
506+ trackCmd .Dir = repoPath
507+ if trackOut , trackErr := trackCmd .CombinedOutput (); trackErr != nil {
508+ fmt .Fprintf (os .Stderr , "note: could not create local tracking branch %s: %v (%s)\n " ,
509+ branchName , trackErr , strings .TrimSpace (string (trackOut )))
510+ continue
511+ }
512+ localSet [branchName ] = true
513+ created = append (created , localBranchTracking {
514+ Branch : branchName ,
515+ Upstream : upstream ,
516+ Current : false ,
517+ })
518+ }
519+ return created , nil
520+ }
521+
365522func createRainBackupBranch (repoPath , branch , sha string ) (string , error ) {
366523 timestamp := time .Now ().Format ("20060102-150405" )
367524 shortSHA := sha
0 commit comments