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

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