# refscope: add ExcludeRefs for exact ref-name exclusion

`9cae29e`→[main](/content/gh/entireio/git-sync/commits/main/index.html)·

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
