# add memory limiter

`488a575`·

Soph·3mo ago·4 files·+188 added/-8 removed

## Sessions

16bfc6ff6845View transcript

[?\
this should work right, or did the syntax change:Codex·GPT-5.4·1 step](/content/gh/entireio/git-sync/session/019d85eb-8e2e-7e83-8d90-640dce44d5f9#timeline-16bfc6ff6845/index.html)

## Changes

4

- internal/syncer

- Mintegration_test.go+1/-1

- Alimited_storer.go+131

- Alimited_storer_test.go+48

- Msyncer.go+8/-7

```
158 unmodified lines

159
160
161
162
162
163
164
165

158 unmodified lines

if err == nil {
		t.Fatal("expected materialized limit failure")
	}
	if !strings.Contains(err.Error(), "materialized push requires") {
	if !strings.Contains(err.Error(), "materialized") || !strings.Contains(err.Error(), "limit") {
		t.Fatalf("expected materialized limit error, got %v", err)
	}
}
```

Minternal/syncer/integration_test.go+1/-1

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

package syncer

import (
	"fmt"
	"io"

"github.com/go-git/go-git/v6/plumbing"
	"github.com/go-git/go-git/v6/plumbing/storer"
)

type materializedFetchLimitError struct {
	count int
	limit int
}

func (e materializedFetchLimitError) Error() string {
	return fmt.Sprintf(
		"materialized fetch exceeded object limit after %d objects (limit %d); narrow refs, use an empty target/bootstrap, or raise --materialized-max-objects",
		e.count,
		e.limit,
	)
}

type limitedStorer struct {
	base  storer.Storer
	limit int
	count int
}

func newLimitedStorer(base storer.Storer, limit int) *limitedStorer {
	return &limitedStorer{base: base, limit: limit}
}

func effectiveMaterializedMaxObjects(limit int) int {
	if limit > 0 {
		return limit
	}
	return DefaultMaterializedMaxObjects
}

func (s *limitedStorer) RawObjectWriter(typ plumbing.ObjectType, sz int64) (io.WriteCloser, error) {
	w, err := s.base.RawObjectWriter(typ, sz)
	if err != nil {
		return nil, err
	}
	return &countingWriteCloser{parent: s, inner: w}, nil
}

func (s *limitedStorer) NewEncodedObject() plumbing.EncodedObject {
	return s.base.NewEncodedObject()
}

func (s *limitedStorer) SetEncodedObject(obj plumbing.EncodedObject) (plumbing.Hash, error) {
	if err := s.bump(); err != nil {
		return plumbing.ZeroHash, err
	}
	return s.base.SetEncodedObject(obj)
}

func (s *limitedStorer) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) {
	return s.base.EncodedObject(t, h)
}

func (s *limitedStorer) IterEncodedObjects(t plumbing.ObjectType) (storer.EncodedObjectIter, error) {
	return s.base.IterEncodedObjects(t)
}

func (s *limitedStorer) HasEncodedObject(h plumbing.Hash) error {
	return s.base.HasEncodedObject(h)
}

func (s *limitedStorer) EncodedObjectSize(h plumbing.Hash) (int64, error) {
	return s.base.EncodedObjectSize(h)
}

func (s *limitedStorer) AddAlternate(remote string) error {
	return s.base.AddAlternate(remote)
}

func (s *limitedStorer) SetReference(ref *plumbing.Reference) error {
	return s.base.SetReference(ref)
}

func (s *limitedStorer) CheckAndSetReference(newRef, old *plumbing.Reference) error {
	return s.base.CheckAndSetReference(newRef, old)
}

func (s *limitedStorer) Reference(name plumbing.ReferenceName) (*plumbing.Reference, error) {
	return s.base.Reference(name)
}

func (s *limitedStorer) IterReferences() (storer.ReferenceIter, error) {
	return s.base.IterReferences()
}

func (s *limitedStorer) RemoveReference(name plumbing.ReferenceName) error {
	return s.base.RemoveReference(name)
}

func (s *limitedStorer) CountLooseRefs() (int, error) {
	return s.base.CountLooseRefs()
}

func (s *limitedStorer) PackRefs() error {
	return s.base.PackRefs()
}

func (s *limitedStorer) bump() error {
	s.count++
	if s.count > s.limit {
		return materializedFetchLimitError{count: s.count, limit: s.limit}
	}
	return nil
}

type countingWriteCloser struct {
	parent *limitedStorer
	inner  io.WriteCloser
}

func (w *countingWriteCloser) Write(p []byte) (int, error) {
	return w.inner.Write(p)
}

func (w *countingWriteCloser) Close() error {
	if err := w.parent.bump(); err != nil {
		_ = w.inner.Close()
		return err
	}
	return w.inner.Close()
}
```

Ainternal/syncer/limited_storer.go+131

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

package syncer

import (
	"errors"
	"testing"

"github.com/go-git/go-git/v6/plumbing"
	"github.com/go-git/go-git/v6/storage/memory"
)

func TestLimitedStorerSetEncodedObjectStopsAtLimit(t *testing.T) {
	base := memory.NewStorage()
	store := newLimitedStorer(base, 1)

if _, err := store.SetEncodedObject(testBlob("one")); err != nil {
		t.Fatalf("first object should fit within limit: %v", err)
	}
	err := func() error {
		_, err := store.SetEncodedObject(testBlob("two"))
		return err
	}()
	var limitErr materializedFetchLimitError
	if !errors.As(err, &limitErr) {
		t.Fatalf("expected materialized fetch limit error, got %v", err)
	}
	if limitErr.limit != 1 {
		t.Fatalf("unexpected limit error payload: %+v", limitErr)
	}
}

func TestEffectiveMaterializedMaxObjects(t *testing.T) {
	if got := effectiveMaterializedMaxObjects(123); got != 123 {
		t.Fatalf("effectiveMaterializedMaxObjects(123) = %d, want 123", got)
	}
	if got := effectiveMaterializedMaxObjects(0); got != DefaultMaterializedMaxObjects {
		t.Fatalf("effectiveMaterializedMaxObjects(0) = %d, want %d", got, DefaultMaterializedMaxObjects)
	}
}

func testBlob(content string) plumbing.EncodedObject {
	obj := &plumbing.MemoryObject{}
	obj.SetType(plumbing.BlobObject)
	obj.SetSize(int64(len(content)))
	if _, err := obj.Write([]byte(content)); err != nil {
		panic(err)
	}
	return obj
}
}
```

Ainternal/syncer/limited_storer_test.go+48

```
316 unmodified lines

317
318
319
320
321
322
323
324
325
320
321
322
323
324
325
326
327
328
107 unmodified lines

436
437
438
439
439
440
441
442
443

316 unmodified lines

}

type targetSession struct {
	conn      *gitproto.Conn
	adv       *packp.AdvRefs
	refMap    map[plumbing.ReferenceName]plumbing.Hash
	features  gitproto.TargetFeatures
	policy    planner.RelayTargetPolicy
	pusher    gitproto.Pusher
	conn     *gitproto.Conn
	adv      *packp.AdvRefs
	refMap   map[plumbing.ReferenceName]plumbing.Hash
	features gitproto.TargetFeatures
	policy   planner.RelayTargetPolicy
	pusher   gitproto.Pusher
}

// newSession performs the shared setup: protocol validation, mapping validation,
107 unmodified lines

return Result{}, fmt.Errorf("init in-memory repository: %w", err)
	}
	gpDesired := convert.DesiredRefs(desiredRefs)
	if err := sourceService.FetchToStore(ctx, repo.Storer, s.sourceConn, gpDesired, targetRefMap); err != nil {
	fetchStore := storer.Storer(newLimitedStorer(repo.Storer, effectiveMaterializedMaxObjects(cfg.MaterializedMaxObjects)))
	if err := sourceService.FetchToStore(ctx, fetchStore, s.sourceConn, gpDesired, targetRefMap); err != nil {
		if !errors.Is(err, git.NoErrAlreadyUpToDate) {
			return Result{}, err
		}
	}
}
```

Minternal/syncer/syncer.go+8/-7
