133728e barerepo 1mo
1
package hook
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"fmt"
133728e barerepo 1mo
6
"sort"
133728e barerepo 1mo
7
"strconv"
133728e barerepo 1mo
8
"strings"
133728e barerepo 1mo
10
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
13
// blob is one object a push adds, with the path it takes in the tree.
133728e barerepo 1mo
14
type blob struct {
133728e barerepo 1mo
15
Path string
133728e barerepo 1mo
16
Size int64
133728e barerepo 1mo
19
// pushed is what one update adds, which both limits in chapter 20.2 are measured against.
133728e barerepo 1mo
20
type pushed struct {
133728e barerepo 1mo
21
Over []blob
133728e barerepo 1mo
22
Bytes int64
133728e barerepo 1mo
25
// inspect reads the pushed range once since both limits ask the same objects, and counted holds what earlier refs added, because 20.2's push limit is one number for the whole push.
133728e barerepo 1mo
26
func inspect(ctx context.Context, dir string, u Update, blobLimitMB int, counted map[string]bool) pushed {
133728e barerepo 1mo
27
if u.Deleting() || !gitx.ValidRev(u.New) {
133728e barerepo 1mo
28
return pushed{}
133728e barerepo 1mo
30
limit := int64(blobLimitMB) << 20
133728e barerepo 1mo
32
// The pushed range, not the whole repository: --all still holds the old tips inside a hook.
133728e barerepo 1mo
33
listed, err := gitx.Run(ctx, dir, "rev-list", "--objects", u.New, "--not", "--all")
133728e barerepo 1mo
34
if err != nil {
133728e barerepo 1mo
35
return pushed{}
133728e barerepo 1mo
37
path := map[string]string{}
133728e barerepo 1mo
38
ids := make([]string, 0, 64)
133728e barerepo 1mo
39
for _, line := range strings.Split(strings.TrimRight(listed, "\n"), "\n") {
133728e barerepo 1mo
40
id, name, ok := strings.Cut(line, " ")
133728e barerepo 1mo
41
if !ok || len(id) < 40 {
133728e barerepo 1mo
42
continue
133728e barerepo 1mo
44
if counted[id] {
133728e barerepo 1mo
45
continue
133728e barerepo 1mo
47
counted[id] = true
133728e barerepo 1mo
48
path[id] = name
133728e barerepo 1mo
49
ids = append(ids, id)
133728e barerepo 1mo
51
if len(ids) == 0 {
133728e barerepo 1mo
52
return pushed{}
133728e barerepo 1mo
55
checked, err := gitx.RunStdin(ctx, dir, strings.Join(ids, "\n")+"\n", "cat-file", "--batch-check")
133728e barerepo 1mo
56
if err != nil {
133728e barerepo 1mo
57
return pushed{}
133728e barerepo 1mo
59
var out pushed
133728e barerepo 1mo
60
for _, line := range strings.Split(strings.TrimRight(checked, "\n"), "\n") {
133728e barerepo 1mo
61
f := strings.Fields(line)
133728e barerepo 1mo
62
if len(f) != 3 {
133728e barerepo 1mo
63
continue
133728e barerepo 1mo
65
n, err := strconv.ParseInt(f[2], 10, 64)
133728e barerepo 1mo
66
if err != nil {
133728e barerepo 1mo
67
continue
133728e barerepo 1mo
69
out.Bytes += n
133728e barerepo 1mo
70
if f[1] == "blob" && blobLimitMB > 0 && n > limit {
133728e barerepo 1mo
71
out.Over = append(out.Over, blob{Path: path[f[0]], Size: n})
133728e barerepo 1mo
74
// The path settles a tie, or two files of one size take turns being named, and chapter 20.2 wants the rejection to name the file.
133728e barerepo 1mo
75
sort.Slice(out.Over, func(i, j int) bool {
133728e barerepo 1mo
76
if out.Over[i].Size == out.Over[j].Size {
133728e barerepo 1mo
77
return out.Over[i].Path < out.Over[j].Path
133728e barerepo 1mo
79
return out.Over[i].Size > out.Over[j].Size
133728e barerepo 1mo
80
})
133728e barerepo 1mo
81
return out
133728e barerepo 1mo
84
// tooMuch is the whole push, which config calls loose because a first import is what meets it.
133728e barerepo 1mo
85
func tooMuch(bytes int64, limitMB int) string {
133728e barerepo 1mo
86
return fmt.Sprintf("this push adds %s of objects. the limit is %dmb.\n"+
133728e barerepo 1mo
87
"push it in parts, or ask whoever runs this barerepo to raise [limits] max_push_mb.",
133728e barerepo 1mo
88
blobSize(bytes), limitMB)
133728e barerepo 1mo
91
// tooBig is chapter 20.2's message, which names the file, because "push too large" sends a user hunting.
133728e barerepo 1mo
92
func tooBig(over []blob, limitMB int) string {
133728e barerepo 1mo
93
var b strings.Builder
133728e barerepo 1mo
94
fmt.Fprintf(&b, "%s is %s. the limit is %dmb.\n", over[0].Path, blobSize(over[0].Size), limitMB)
133728e barerepo 1mo
95
if len(over) > 1 {
133728e barerepo 1mo
96
fmt.Fprintf(&b, "%d more files in this push are over it too.\n", len(over)-1)
133728e barerepo 1mo
98
b.WriteString("\nlarge files belong in object storage, with a url or a checksum in the\n")
133728e barerepo 1mo
99
b.WriteString("repository. the build fetches them.")
133728e barerepo 1mo
100
return b.String()
133728e barerepo 1mo
103
func blobSize(n int64) string {
133728e barerepo 1mo
104
switch {
133728e barerepo 1mo
105
case n < 1024:
133728e barerepo 1mo
106
return fmt.Sprintf("%db", n)
133728e barerepo 1mo
107
case n < 1024*1024:
133728e barerepo 1mo
108
return blobUnit(float64(n)/1024, "kb")
133728e barerepo 1mo
109
default:
133728e barerepo 1mo
110
return blobUnit(float64(n)/(1024*1024), "mb")
133728e barerepo 1mo
111
}
133728e barerepo 1mo
114
// blobUnit writes a size the way the pages do, so 88kb is not 88.0kb here, and it is written twice because the hook binary cannot reach httpd.
133728e barerepo 1mo
115
func blobUnit(v float64, suffix string) string {
133728e barerepo 1mo
116
if v < 10 {
133728e barerepo 1mo
117
return fmt.Sprintf("%.1f%s", v, suffix)
133728e barerepo 1mo
118
}
133728e barerepo 1mo
119
return fmt.Sprintf("%.0f%s", v, suffix)