Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 1h ago 4b8c9e1d
history blame raw 5.7 KiB
1package hook
2
3// Quotas. A repository is a place several people write to, and every one of them
4// can choose how much: a contributor can add branches in their own namespace, any
5// writer can add issues and comments, and a maintainer can add tags. Without a
6// bound, one account can make every anonymous page read slowly or fill the disk,
7// and the instance's only answer is an admin deleting things by hand.
8//
9// The checks run in pre-receive, where a refusal moves no refs. They are counts at
10// the *new* tip, so they describe the repository the push would leave behind, not
11// the one it found. The one thing that cannot be seen here is bytes still in the
12// quarantine; that is why a push is also bounded by receive_max_bytes, and why the
13// size check refuses a repository that is already over its cap: growth is bounded
14// by the cap plus one permitted push.
15
16import (
17 "fmt"
18 "strings"
19
20 "rodent.software/vole/core/git"
21 "rodent.software/vole/internal/format"
22 "rodent.software/vole/metadata"
23)
24
25// quotaProblems reports every limit this push would break. An empty result means
26// the push is inside every quota.
27func quotaProblems(cfg Config, g *git.Repo, changes []change) []string {
28 var problems []string
29
30 if cfg.MaxRefs > 0 {
31 if n, err := refsAfter(g, changes); err != nil {
32 problems = append(problems, fmt.Sprintf("cannot count refs to check the repository quota: %v", err))
33 } else if n > cfg.MaxRefs {
34 problems = append(problems, fmt.Sprintf(
35 "this push would leave %d branches and tags, over the limit of %d; delete some or raise repo_max_refs",
36 n, cfg.MaxRefs))
37 }
38 }
39
40 if cfg.MaxMetadataEntries > 0 || cfg.MaxMetadataBytes > 0 {
41 if tip := metadataAfter(changes); tip != "" {
42 entries, bytes, err := metadataSize(g, tip)
43 if err != nil {
44 problems = append(problems, fmt.Sprintf("cannot measure the metadata branch to check its quota: %v", err))
45 } else {
46 if cfg.MaxMetadataEntries > 0 && entries > cfg.MaxMetadataEntries {
47 problems = append(problems, fmt.Sprintf(
48 "%s would hold %d issues, requests, runs and releases, over the limit of %d; raise repo_max_metadata_entries or remove some",
49 metadata.Ref, entries, cfg.MaxMetadataEntries))
50 }
51 if cfg.MaxMetadataBytes > 0 && bytes > cfg.MaxMetadataBytes {
52 problems = append(problems, fmt.Sprintf(
53 "%s would hold %s, over the limit of %s; raise repo_max_metadata_bytes or remove some",
54 metadata.Ref, format.Bytes(bytes), format.Bytes(cfg.MaxMetadataBytes)))
55 }
56 }
57 }
58 }
59
60 if cfg.MaxRepoBytes > 0 {
61 if used, err := g.DiskUsage(); err != nil {
62 problems = append(problems, fmt.Sprintf("cannot measure the repository to check its quota: %v", err))
63 } else if used > cfg.MaxRepoBytes {
64 problems = append(problems, fmt.Sprintf(
65 "this repository holds %s, over the limit of %s; an admin can run `vole admin gc` or raise repo_max_bytes",
66 format.Bytes(used), format.Bytes(cfg.MaxRepoBytes)))
67 }
68 }
69
70 if cfg.MaxLFSBytes > 0 && cfg.LFS != nil && cfg.RepoID != "" {
71 if used, err := lfsUsage(cfg.LFS, cfg.RepoID); err != nil {
72 problems = append(problems, fmt.Sprintf("cannot measure the object store to check its quota: %v", err))
73 } else if used > cfg.MaxLFSBytes {
74 problems = append(problems, fmt.Sprintf(
75 "this repository's LFS objects, logs and artifacts take %s, over the limit of %s; raise repo_max_lfs_bytes or remove some",
76 format.Bytes(used), format.Bytes(cfg.MaxLFSBytes)))
77 }
78 }
79
80 return problems
81}
82
83// refsAfter is the number of branches and tags the repository would hold if this
84// push landed: what is there now, plus the refs the push creates, minus the ones it
85// deletes. Pre-receive sees the refs as they were, so the change list is what makes
86// the number describe the outcome.
87func refsAfter(g *git.Repo, changes []change) (int, error) {
88 now, err := g.RefCount()
89 if err != nil {
90 return 0, err
91 }
92 for _, c := range changes {
93 if !strings.HasPrefix(c.ref, "refs/heads/") && !strings.HasPrefix(c.ref, "refs/tags/") {
94 continue
95 }
96 switch {
97 case c.old == "" || c.old == git.ZeroSHA:
98 if c.new != "" && c.new != git.ZeroSHA {
99 now++
100 }
101 case c.new == "" || c.new == git.ZeroSHA:
102 if now > 0 {
103 now--
104 }
105 }
106 }
107 return now, nil
108}
109
110// metadataAfter is the metadata tip the push would leave, or "" when it does not
111// touch that branch (or deletes it).
112func metadataAfter(changes []change) string {
113 for _, c := range changes {
114 if c.ref == metadata.Ref && c.new != "" && c.new != git.ZeroSHA {
115 return c.new
116 }
117 }
118 return ""
119}
120
121// metadataSize counts what a page load on a repository has to parse: every issue,
122// request, comment and approval file, and their bytes.
123func metadataSize(g *git.Repo, tip string) (entries int, bytes int64, err error) {
124 es, err := g.LsTreeSizes(tip)
125 if err != nil {
126 return 0, 0, err
127 }
128 for _, e := range es {
129 if e.Type != "blob" {
130 continue
131 }
132 // Everything the metadata branch carries as a document: issues and their
133 // comments, requests with approvals and comments, and the run and release
134 // records a runner or a maintainer adds. These are what a page load parses.
135 for _, dir := range []string{
136 metadata.IssuesDir, metadata.ChangesDir, metadata.ReleasesDir,
137 } {
138 if strings.HasPrefix(e.Name, dir+"/") {
139 entries++
140 bytes += e.Size
141 break
142 }
143 }
144 }
145 return entries, bytes, nil
146}
147
148func lfsUsage(store lfsStore, repoID string) (int64, error) {
149 var total int64
150 err := store.RepoWalk(repoID, func(_ string, size int64) error {
151 total += size
152 return nil
153 })
154 return total, err
155}
156
157// lfsStore is the part of the object store the quota check needs. It is an
158// interface so a test can answer without a directory.
159type lfsStore interface {
160 RepoWalk(repo string, fn func(oid string, size int64) error) error
161}
162