package hook import ( "context" "fmt" "sort" "strconv" "strings" "github.com/barerepo/server/internal/gitx" ) // blob is one object a push adds, with the path it takes in the tree. type blob struct { Path string Size int64 } // pushed is what one update adds, which both limits in chapter 20.2 are measured against. type pushed struct { Over []blob Bytes int64 } // 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. func inspect(ctx context.Context, dir string, u Update, blobLimitMB int, counted map[string]bool) pushed { if u.Deleting() || !gitx.ValidRev(u.New) { return pushed{} } limit := int64(blobLimitMB) << 20 // The pushed range, not the whole repository: --all still holds the old tips inside a hook. listed, err := gitx.Run(ctx, dir, "rev-list", "--objects", u.New, "--not", "--all") if err != nil { return pushed{} } path := map[string]string{} ids := make([]string, 0, 64) for _, line := range strings.Split(strings.TrimRight(listed, "\n"), "\n") { id, name, ok := strings.Cut(line, " ") if !ok || len(id) < 40 { continue } if counted[id] { continue } counted[id] = true path[id] = name ids = append(ids, id) } if len(ids) == 0 { return pushed{} } checked, err := gitx.RunStdin(ctx, dir, strings.Join(ids, "\n")+"\n", "cat-file", "--batch-check") if err != nil { return pushed{} } var out pushed for _, line := range strings.Split(strings.TrimRight(checked, "\n"), "\n") { f := strings.Fields(line) if len(f) != 3 { continue } n, err := strconv.ParseInt(f[2], 10, 64) if err != nil { continue } out.Bytes += n if f[1] == "blob" && blobLimitMB > 0 && n > limit { out.Over = append(out.Over, blob{Path: path[f[0]], Size: n}) } } // 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. sort.Slice(out.Over, func(i, j int) bool { if out.Over[i].Size == out.Over[j].Size { return out.Over[i].Path < out.Over[j].Path } return out.Over[i].Size > out.Over[j].Size }) return out } // tooMuch is the whole push, which config calls loose because a first import is what meets it. func tooMuch(bytes int64, limitMB int) string { return fmt.Sprintf("this push adds %s of objects. the limit is %dmb.\n"+ "push it in parts, or ask whoever runs this barerepo to raise [limits] max_push_mb.", blobSize(bytes), limitMB) } // tooBig is chapter 20.2's message, which names the file, because "push too large" sends a user hunting. func tooBig(over []blob, limitMB int) string { var b strings.Builder fmt.Fprintf(&b, "%s is %s. the limit is %dmb.\n", over[0].Path, blobSize(over[0].Size), limitMB) if len(over) > 1 { fmt.Fprintf(&b, "%d more files in this push are over it too.\n", len(over)-1) } b.WriteString("\nlarge files belong in object storage, with a url or a checksum in the\n") b.WriteString("repository. the build fetches them.") return b.String() } func blobSize(n int64) string { switch { case n < 1024: return fmt.Sprintf("%db", n) case n < 1024*1024: return blobUnit(float64(n)/1024, "kb") default: return blobUnit(float64(n)/(1024*1024), "mb") } } // 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. func blobUnit(v float64, suffix string) string { if v < 10 { return fmt.Sprintf("%.1f%s", v, suffix) } return fmt.Sprintf("%.0f%s", v, suffix) }