refscope: add ExcludeRefs for exact ref-name exclusion · Entire
refscope: add ExcludeRefs for exact ref-name exclusion
9cae29e→main·
nodo·2w ago·8 files·+81 added/-7 removed
RefScope.ExcludeRefs excludes exact ref names (whole-name match) from auto-discovery, alongside the existing prefix-based ExcludeRefPrefixes. Unlike a prefix, an exact entry does not match children — so a caller can reserve a directory-anchor ref like refs/heads/entire while still mirroring refs/heads/entire/foo (a prefix would also catch refs/heads/entirely). Threaded through RefScope -> internalbridge -> syncer.Config -> planner.PlanConfig; IsRefExcluded now takes both a prefix and an exact list.
Needed by entiredb's unmirrored-branches feature so the mirror worker can skip exactly the reserved anchor refs without dropping their mirrored children.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01H8ar8ZFaruGMsovF1cJzkx
Changes
8
MCHANGELOG.md+4
Mclient.go+1
internal
internalbridge
Mconfig.go+2
planner
Aexclude_test.go+47
Mplanner.go+8/-3
Mtypes.go+10/-2
syncer
Msyncer.go+4/-2
Mtypes.go+5
6 unmodified lines
7
8
9
10
11
12
13
14
15
16
6 unmodified lines
## [Unreleased]
### Added
- `RefScope.ExcludeRefs` — exact ref-name exclusion, alongside the existing prefix-based `ExcludeRefPrefixes`. An excluded exact name is not pulled, pushed, or pruned, but — unlike a prefix — its children are unaffected, so a caller can reserve a directory-anchor ref like `refs/heads/entire` while still mirroring `refs/heads/entire/foo`. Threaded through `RefScope` → planner `PlanConfig`; `IsRefExcluded` now takes both prefix and exact lists.
### Removed
- The built-in Entire DB credential store integration (`hosts.json` active-user lookup, the file/keyring token store, and OAuth refresh-token handling). `auth.Resolve` now resolves only explicit token/bearer credentials; everything else defers to the git credential helper on a 401, exactly as for any other remote. The Entire mirroring pipeline and the `git-remote-entire` helper already supply credentials directly (installation / repo-scoped tokens at the transport layer), so nothing produced the `hosts.json`/token-store layout this code read. This drops the `github.com/zalando/go-keyring` dependency and, with the file token store gone, the package now compiles on Windows without a `flock` shim.
MCHANGELOG.md+4
216 unmodified lines
217
218
219
220
221
222
223
216 unmodified lines
Mappings: mappings,
AllRefs: scope.AllRefs,
ExcludeRefPrefixes: append([]string(nil), scope.ExcludeRefPrefixes...),
ExcludeRefs: append([]string(nil), scope.ExcludeRefs...),
}
}
Mclient.go+1
43 unmodified lines
44
45
46
47
48
49
50
31 unmodified lines
82
83
84
85
86
87
88
43 unmodified lines
Mappings []RefMapping
AllRefs bool
ExcludeRefPrefixes []string
ExcludeRefs []string
}
type SyncPolicy struct {
31 unmodified lines
Mappings: ToValidationMappings(scope.Mappings),
AllRefs: scope.AllRefs,
ExcludeRefPrefixes: append([]string(nil), scope.ExcludeRefPrefixes...),
ExcludeRefs: append([]string(nil), scope.ExcludeRefs...),
IncludeTags: policy.IncludeTags,
DryRun: dryRun,
ShowStats: collectStats,
}
Minternal/internalbridge/config.go+2
package planner
import ( "testing"
"github.com/go-git/go-git/v6/plumbing" )
func TestIsRefExcluded(t *testing.T) { t.Parallel() cases := []struct { name string ref string prefixes []string exact []string want bool }{ {"no filters", "refs/heads/main", nil, nil, false},
// Prefix matching (unchanged behavior). {"prefix under", "refs/pull/1/head", []string{"refs/pull/"}, nil, true}, {"prefix boundary sibling", "refs/pullx", []string{"refs/pull/"}, nil, false}, {"prefix blank entry skipped", "refs/heads/main", []string{" "}, nil, false},
// Exact matching: matches the whole name only, never children — so a // caller can reserve refs/heads/entire while still mirroring // refs/heads/entire/foo. {"exact hit", "refs/heads/entire", nil, []string{"refs/heads/entire"}, true}, {"exact does not match child", "refs/heads/entire/foo", nil, []string{"refs/heads/entire"}, false}, {"exact does not match sibling", "refs/heads/entirely", nil, []string{"refs/heads/entire"}, false}, {"exact blank entry skipped", "refs/heads/main", nil, []string{" "}, false},
// Combined: prefix and exact together. {"prefix wins", "refs/heads/entire/unmirrored/x", []string{"refs/heads/entire/unmirrored/"}, []string{"refs/heads/entire"}, true}, {"exact wins", "refs/heads/entire", []string{"refs/heads/entire/unmirrored/"}, []string{"refs/heads/entire"}, true}, {"neither", "refs/heads/entire/foo", []string{"refs/heads/entire/unmirrored/"}, []string{"refs/heads/entire"}, false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { t.Parallel() got := IsRefExcluded(plumbing.ReferenceName(c.ref), c.prefixes, c.exact) if got != c.want { t.Errorf("IsRefExcluded(%q, %v, %v) = %v, want %v", c.ref, c.prefixes, c.exact, got, c.want) } }) } }
Ainternal/planner/exclude_test.go+47
27 unmodified lines
28 29 30 31 32 33 34 35 36 37 38 44 unmodified lines
83 84 85 81 86 87 88 89 15 unmodified lines
105 106 107 103 108 109 110 111 145 unmodified lines
257 258 259 255 260 261 262 263
27 unmodified lines
// whose name starts with any of these prefixes is not pulled, pushed, // or pruned. Explicit Mappings are not subject to this filter. ExcludeRefPrefixes []string // ExcludeRefs subtracts exact ref names (whole-name match) from // auto-discovery, same effect as ExcludeRefPrefixes but without matching // children — so a caller can reserve refs/heads/entire while still // mirroring refs/heads/entire/foo. Explicit Mappings are not subject to it. ExcludeRefs []string }
// BuildDesiredRefs constructs the set of desired refs and managed targets from
selected := SelectBranches(branches, cfg.Branches) for branch, hash := range selected { refName := plumbing.NewBranchReferenceName(branch) if IsRefExcluded(refName, cfg.ExcludeRefPrefixes) { if IsRefExcluded(refName, cfg.ExcludeRefPrefixes, cfg.ExcludeRefs) { continue } if err := addManaged(refName, refName, RefKindBranch, hash); err != nil { 15 unmodified lines
default: continue } if IsRefExcluded(refName, cfg.ExcludeRefPrefixes) { if IsRefExcluded(refName, cfg.ExcludeRefPrefixes, cfg.ExcludeRefs) { continue } if _, ok := desired[refName]; ok { continue } } }
Minternal/planner/planner.go+8/-3
104 unmodified lines
105 106 107 108 109 108 109 110 111 112 6 unmodified lines
119 120 121 122 123 124 125 126 127 128 129 130 131 132
104 unmodified lines
// IsRefExcluded reports whether name matches any of the exclude prefixes. // Empty prefixes are ignored. Used to subtract specific namespaces from // auto-discovery (e.g. refs/pull/* under --all-refs against GitHub). func IsRefExcluded(name plumbing.ReferenceName, excludePrefixes []string) bool { if len(excludePrefixes) == 0 { func IsRefExcluded(name plumbing.ReferenceName, excludePrefixes, excludeExact []string) bool { if len(excludePrefixes) == 0 && len(excludeExact) == 0 { return false } s := name.String() 6 unmodified lines
return true } }
// Exact names match the whole ref, so a caller can reserve // refs/heads/entire without also excluding refs/heads/entire/foo — which // a prefix cannot express (it would also catch refs/heads/entirely). for _, e := range excludeExact { if e = strings.TrimSpace(e); e != "" && s == e { return true } } return false }
Minternal/planner/types.go+10/-2
71 unmodified lines
72 73 74 75 76 77 78 440 unmodified lines
519 520 521 522 523 524 525 520 unmodified lines
1046 1047 1048 1047 1049 1050 1051 1052 215 unmodified lines
1268 1269 1270 1269 1271 1272 1273 1274
71 unmodified lines
Mappings []RefMapping AllRefs bool ExcludeRefPrefixes []string ExcludeRefs []string IncludeTags bool DryRun bool Verbose bool 440 unmodified lines
IncludeTags: cfg.IncludeTags, AllRefs: cfg.AllRefs, ExcludeRefPrefixes: cfg.ExcludeRefPrefixes, ExcludeRefs: cfg.ExcludeRefs, Force: cfg.ForceAny(), Prune: cfg.Prune, } 520 unmodified lines
if _, ok := desiredRefs[targetRef]; ok { continue } if planner.IsRefExcluded(targetRef, s.cfg.ExcludeRefPrefixes) { if planner.IsRefExcluded(targetRef, s.cfg.ExcludeRefPrefixes, s.cfg.ExcludeRefs) { continue } // AllRefs overrides per-namespace allowlists: under "all refs" a 215 unmodified lines
func (s *syncSession) newProbeResult() ProbeResult { refInfos := make([]RefInfo, 0, len(s.sourceRefMap)) for name, hash := range s.sourceRefMap { if planner.IsRefExcluded(name, s.cfg.ExcludeRefPrefixes) { if planner.IsRefExcluded(name, s.cfg.ExcludeRefPrefixes, s.cfg.ExcludeRefs) { continue } refInfos = append(refInfos, RefInfo{Name: name.String(), Hash: hash}) } }
Minternal/syncer/syncer.go+4/-2
90 unmodified lines
91 92 93 94 95 96 97 98 99 100 101
90 unmodified lines
Mappings []RefMapping json:"mappings"
AllRefs bool json:"allRefs,omitempty"
ExcludeRefPrefixes []string json:"excludeRefPrefixes,omitempty"
// ExcludeRefs subtracts exact ref names from auto-discovery: matched
// whole (not by prefix), so a caller can reserve a directory-anchor name
// like refs/heads/entire without also excluding its children
// (refs/heads/entire/foo). Not applied to explicit Mappings.
ExcludeRefs []string json:"excludeRefs,omitempty"
}
// SyncPolicy controls high-level sync behavior. BestEffort downgrades per-ref