@@ -18,6 +18,7 @@ import (
1818 "context"
1919 "fmt"
2020 "io"
21+ "regexp"
2122 "strings"
2223
2324 "gg-scm.io/pkg/git"
@@ -42,8 +43,11 @@ func pull(ctx context.Context, cc *cmdContext, args []string) error {
4243 If no revisions are specified, then all the remote's branches and tags
4344 will be fetched. If the source is a named remote, then its remote
4445 tracking branches will be pruned.` )
45- remoteRefArgs := f .MultiString ("r" , "`ref`s to pull" )
46- forceTags := f .Bool ("force-tags" , false , "update any tags pulled" )
46+ var input pullInput
47+ f .MultiStringVar (& input .remoteRefArgs , "r" , "`ref`s to pull" )
48+ f .RegexpVar (& input .remoteRefPattern , "p" , "`regexp` of branch or tag names to pull (can be specified multiple times)" )
49+ f .Alias ("p" , "pattern" )
50+ f .BoolVar (& input .forceTags , "force-tags" , false , "update any tags pulled" )
4751 update := f .Bool ("u" , false , "update to new head if new descendants were pulled" )
4852 if err := f .Parse (args ); flag .IsHelp (err ) {
4953 f .Help (cc .stdout )
@@ -58,35 +62,35 @@ func pull(ctx context.Context, cc *cmdContext, args []string) error {
5862 if err != nil {
5963 return err
6064 }
61- remotes : = cfg .ListRemotes ()
65+ input . remotes = cfg .ListRemotes ()
6266 headBranch := currentBranch (ctx , cc )
63- repo : = f .Arg (0 )
64- if repo == "" {
65- repo = "origin"
66- if _ , ok := remotes [repo ]; ! ok {
67- return fmt .Errorf ("no source given and no remote named %q found" , repo )
67+ input . repo = f .Arg (0 )
68+ if input . repo == "" {
69+ input . repo = "origin"
70+ if _ , ok := input . remotes [input . repo ]; ! ok {
71+ return fmt .Errorf ("no source given and no remote named %q found" , input . repo )
6872 }
6973 }
70- allLocalRefs , err : = cc .git .ListRefsVerbatim (ctx )
74+ input . localRefs , err = cc .git .ListRefsVerbatim (ctx )
7175 if err != nil {
7276 return err
7377 }
74- allRemoteRefs , err : = cc .git .ListRemoteRefs (ctx , repo )
78+ input . remoteRefs , err = cc .git .ListRemoteRefs (ctx , input . repo )
7579 if err != nil {
7680 return err
7781 }
7882
79- remote := remotes [repo ]
80- gitArgs , ops , err := buildFetchArgs (repo , remotes , allLocalRefs , allRemoteRefs , * remoteRefArgs , * forceTags )
83+ gitArgs , ops , err := input .buildFetchArgs ()
8184 if err != nil {
8285 return err
8386 }
87+ remote := input .remotes [input .repo ]
8488 if remote == nil {
8589 // Delete anything under refs/ggpull/...
8690 // (Need to do this before fetching, but after validating that this
8791 // invocation will be reasonable.)
88- localMuts := make (map [git.Ref ]git.RefMutation , len (allLocalRefs ))
89- for ref := range allLocalRefs {
92+ localMuts := make (map [git.Ref ]git.RefMutation , len (input . localRefs ))
93+ for ref := range input . localRefs {
9094 if strings .HasPrefix (ref .String (), "refs/ggpull/" ) {
9195 localMuts [ref ] = git .DeleteRef ()
9296 }
@@ -109,7 +113,7 @@ func pull(ctx context.Context, cc *cmdContext, args []string) error {
109113 var target git.Ref
110114 if remote != nil {
111115 headRef := git .BranchRef (headBranch )
112- for _ , spec := range remotes [ repo ] .Fetch {
116+ for _ , spec := range remote .Fetch {
113117 target = spec .Map (headRef )
114118 if target != "" {
115119 break
@@ -125,6 +129,21 @@ func pull(ctx context.Context, cc *cmdContext, args []string) error {
125129 return nil
126130}
127131
132+ type pullInput struct {
133+ remoteRefArgs []string
134+ remoteRefPattern * regexp.Regexp
135+ forceTags bool
136+
137+ // repo is the name or URL of the remote to fetch from.
138+ repo string
139+ // remotes is the configured set of remotes.
140+ remotes map [string ]* git.Remote
141+ // localRefs is the set of refs in the local repository.
142+ localRefs map [git.Ref ]git.Hash
143+ // remoteRefs is the set of refs in the repository being fetched.
144+ remoteRefs map [git.Ref ]git.Hash
145+ }
146+
128147type deferredFetchOps struct {
129148 remote * git.Remote
130149 localRefs map [git.Ref ]git.Hash
@@ -136,91 +155,116 @@ type deferredFetchOps struct {
136155// buildFetchArgs computes the set of branches and tags to fetch.
137156// If buildFetchArgs returns an empty list of arguments,
138157// then fetch should not be run.
139- func buildFetchArgs ( repo string , remotes map [ string ] * git. Remote , localRefs , remoteRefs map [git. Ref ]git. Hash , remoteRefArgs [] string , forceTags bool ) (gitArgs []string , ops * deferredFetchOps , _ error ) {
158+ func ( input * pullInput ) buildFetchArgs ( ) (gitArgs []string , ops * deferredFetchOps , _ error ) {
140159 ops = & deferredFetchOps {
141- remote : remotes [repo ],
142- localRefs : localRefs ,
143- remoteRefs : remoteRefs ,
160+ remote : input . remotes [input . repo ],
161+ localRefs : input . localRefs ,
162+ remoteRefs : input . remoteRefs ,
144163 deletedRefs : make (map [git.Ref ]git.Hash ),
145164 }
146165 gitArgs = []string {"fetch" }
147166 var prevRemoteRefs map [git.Ref ]git.Hash
148167 if ops .remote != nil {
149- prevRemoteRefs = reverseFetchMap (ops .remote .Fetch , localRefs )
168+ prevRemoteRefs = reverseFetchMap (ops .remote .Fetch , input . localRefs )
150169 } else {
151170 gitArgs = append (gitArgs , "--refmap=+refs/heads/*:refs/ggpull/*" )
152171 }
153172
154173 // Convert remoteRefArgs into a set of refs.
155174 var resolvedRefs []git.Ref
156- if len (remoteRefArgs ) == 0 {
175+ if len (input . remoteRefArgs ) == 0 && input . remoteRefPattern == nil {
157176 // Empty means everything.
158- for ref := range remoteRefs {
177+ for ref := range input . remoteRefs {
159178 if ref .IsBranch () || ref .IsTag () {
160179 resolvedRefs = append (resolvedRefs , ref )
161180 }
162181 }
163182 for ref := range prevRemoteRefs {
164- if _ , exists := remoteRefs [ref ]; ref .IsBranch () && ! exists {
183+ if _ , exists := input . remoteRefs [ref ]; ref .IsBranch () && ! exists {
165184 resolvedRefs = append (resolvedRefs , ref )
166185 }
167186 }
168187 } else {
169188 // Add refs/heads/ or refs/tags/ as appropriate.
170- for _ , arg := range remoteRefArgs {
189+ for _ , arg := range input . remoteRefArgs {
171190 if ref := git .Ref (arg ); ref .IsBranch () || ref .IsTag () {
172191 resolvedRefs = append (resolvedRefs , ref )
173192 continue
174193 }
175194 branchRef := git .BranchRef (arg )
176- _ , hasBranch := remoteRefs [branchRef ]
195+ _ , hasBranch := input . remoteRefs [branchRef ]
177196 _ , hasPrevBranch := prevRemoteRefs [branchRef ]
178197 if hasBranch || hasPrevBranch {
179198 resolvedRefs = append (resolvedRefs , branchRef )
180199 continue
181200 }
182201 tagRef := git .TagRef (arg )
183- if _ , hasTag := remoteRefs [tagRef ]; hasTag {
202+ if _ , hasTag := input . remoteRefs [tagRef ]; hasTag {
184203 resolvedRefs = append (resolvedRefs , tagRef )
185204 continue
186205 }
187- return nil , nil , fmt .Errorf ("can't find ref %q on remote %q" , arg , repo )
206+ return nil , nil , fmt .Errorf ("can't find ref %q on remote %q" , arg , input .repo )
207+ }
208+
209+ if input .remoteRefPattern != nil {
210+ for ref := range input .remoteRefs {
211+ s := ref .Branch ()
212+ if s == "" {
213+ s = ref .Tag ()
214+ if s == "" {
215+ continue
216+ }
217+ }
218+ if input .remoteRefPattern .MatchString (s ) {
219+ resolvedRefs = append (resolvedRefs , ref )
220+ }
221+ }
222+ for ref := range prevRemoteRefs {
223+ branch := ref .Branch ()
224+ if branch == "" {
225+ continue
226+ }
227+ _ , exists := input .remoteRefs [ref ]
228+ if ! exists && input .remoteRefPattern .MatchString (ref .String ()) {
229+ resolvedRefs = append (resolvedRefs , ref )
230+ }
231+ }
188232 }
189233 }
190234
191235 // Build fetch arguments and validate ref selection.
192- gitArgs = append (gitArgs , "--" , repo )
236+ gitArgs = append (gitArgs , "--" , input . repo )
193237 zeroFetchArgsLen := len (gitArgs )
194238 for _ , ref := range resolvedRefs {
195239 switch {
196240 case ref .IsBranch ():
197- _ , hasBranch := remoteRefs [ref ]
241+ _ , hasBranch := input . remoteRefs [ref ]
198242 _ , hasPrevBranch := prevRemoteRefs [ref ]
199243 switch {
200244 case hasBranch :
201245 gitArgs = append (gitArgs , ref .String ()+ ":" )
202246 ops .branches = append (ops .branches , ref )
203247 case hasPrevBranch :
204248 trackingRef := ops .remote .MapFetch (ref )
205- ops .deletedRefs [trackingRef ] = localRefs [trackingRef ]
206- if isRefOrphaned (remotes , localRefs , repo , ref ) {
207- ops .deletedRefs [ref ] = localRefs [ref ]
249+ ops .deletedRefs [trackingRef ] = input . localRefs [trackingRef ]
250+ if isRefOrphaned (input . remotes , input . localRefs , input . repo , ref ) {
251+ ops .deletedRefs [ref ] = input . localRefs [ref ]
208252 }
209253 default :
210- return nil , nil , fmt .Errorf ("can't find ref %q on remote %q" , ref , repo )
254+ return nil , nil , fmt .Errorf ("can't find ref %q on remote %q" , ref , input . repo )
211255 }
212256 case ref .IsTag ():
213- localHash , hasLocalTag := localRefs [ref ]
214- remoteHash , hasRemoteTag := remoteRefs [ref ]
257+ localHash , hasLocalTag := input . localRefs [ref ]
258+ remoteHash , hasRemoteTag := input . remoteRefs [ref ]
215259 refspec := ref .String () + ":" + ref .String ()
216260 switch {
217261 case ! hasRemoteTag :
218262 return nil , nil , fmt .Errorf ("tag %q does not exist on remote" , ref .Tag ())
219263 case ! hasLocalTag :
220264 gitArgs = append (gitArgs , refspec )
221- case localHash != remoteHash && forceTags :
265+ case localHash != remoteHash && input . forceTags :
222266 gitArgs = append (gitArgs , "+" + refspec )
223- case localHash != remoteHash && ! forceTags :
267+ case localHash != remoteHash && ! input . forceTags :
224268 return nil , nil , fmt .Errorf ("tag %q is %v on remote, does not match %v locally" , ref .Tag (), remoteHash , localHash )
225269 }
226270 default :
0 commit comments