mirror of
https://github.com/tailscale/tailscale.git
synced 2026-08-02 10:48:26 -04:00
Add a large blob check to the pre-push hook, using the same git tree diff logic as corp's check-file-size CI workflow (the check-git-accidental-large-file GitHub Action): diff the pushed tree against the remote's old tree (or the merge base with the remote's default branch for new refs) and reject any new or changed blob over 1.5 MB. Unlike the CI check, which only guards PRs into main, the hook runs before pushing to any branch, catching mistakes before they permanently bloat the remote repo. Set TS_SKIP_LARGE_FILE_CHECK=1 to push a large file intentionally, mirroring the skip-large-file-check commit message tag honored by CI. This folds the go.mod replace check and the new check into a single CheckPrePush entry point so both share one read of the hook's stdin; corp's git-hook.go needs the matching call site update when it next bumps its tailscale.com dependency. Updates tailscale/corp#9863 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I1c8cf2a277ce854d45c0ea809bed7c06b3295374
117 lines
3.0 KiB
Go
117 lines
3.0 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package githook
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"slices"
|
|
"strings"
|
|
|
|
"golang.org/x/mod/modfile"
|
|
)
|
|
|
|
// PrePushConfig configures CheckPrePush.
|
|
type PrePushConfig struct {
|
|
// WatchedRemotes are the remote URLs whose pushes are subject to
|
|
// the go.mod replace check.
|
|
WatchedRemotes []string
|
|
|
|
// AllowedReplaceDirs are the directory-path go.mod replace targets
|
|
// that are permitted in pushed commits.
|
|
AllowedReplaceDirs []string
|
|
|
|
// MaxBlobSize, if positive, is the largest new or changed blob in
|
|
// bytes allowed in a push to any remote. Pushes adding larger
|
|
// blobs are rejected unless the TS_SKIP_LARGE_FILE_CHECK
|
|
// environment variable is set to a non-empty value.
|
|
MaxBlobSize int64
|
|
}
|
|
|
|
// CheckPrePush reads pushes from stdin and validates them per cfg.
|
|
// args is the pre-push hook's argv (remoteName, remoteLoc).
|
|
//
|
|
// Intended as a pre-push hook.
|
|
// https://git-scm.com/docs/githooks#_pre_push
|
|
func CheckPrePush(args []string, cfg PrePushConfig) error {
|
|
if len(args) < 2 {
|
|
return fmt.Errorf("pre-push: expected 2 args, got %d", len(args))
|
|
}
|
|
remoteName, remoteLoc := args[0], args[1]
|
|
|
|
pushes, err := readPushes()
|
|
if err != nil {
|
|
return fmt.Errorf("reading pushes: %w", err)
|
|
}
|
|
watched := slices.Contains(cfg.WatchedRemotes, remoteLoc)
|
|
for _, p := range pushes {
|
|
if watched && !p.isDoNotMergeRef() {
|
|
if err := checkCommit(p.localSHA, cfg.AllowedReplaceDirs); err != nil {
|
|
return fmt.Errorf("not allowing push of %v to %v: %v", p.localSHA, p.remoteRef, err)
|
|
}
|
|
}
|
|
if cfg.MaxBlobSize > 0 {
|
|
if err := checkLargeBlobs(remoteName, p, cfg.MaxBlobSize); err != nil {
|
|
return fmt.Errorf("not allowing push of %v to %v: %v", p.localSHA, p.remoteRef, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkCommit(sha string, allowedReplaceDirs []string) error {
|
|
if sha == zeroRef {
|
|
// Allow ref deletions.
|
|
return nil
|
|
}
|
|
goMod, err := exec.Command("git", "show", sha+":go.mod").Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mf, err := modfile.Parse("go.mod", goMod, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse its go.mod: %v", err)
|
|
}
|
|
for _, r := range mf.Replace {
|
|
if !modfile.IsDirectoryPath(r.New.Path) {
|
|
continue
|
|
}
|
|
allowed := slices.Contains(allowedReplaceDirs, r.New.Path)
|
|
if !allowed {
|
|
return fmt.Errorf("go.mod contains replace from %v => %v", r.Old.Path, r.New.Path)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const zeroRef = "0000000000000000000000000000000000000000"
|
|
|
|
type push struct {
|
|
localRef string
|
|
localSHA string
|
|
remoteRef string
|
|
remoteSHA string
|
|
}
|
|
|
|
func (p *push) isDoNotMergeRef() bool {
|
|
return strings.HasSuffix(p.remoteRef, "/DO-NOT-MERGE")
|
|
}
|
|
|
|
func readPushes() (pushes []push, err error) {
|
|
bs := bufio.NewScanner(os.Stdin)
|
|
for bs.Scan() {
|
|
f := strings.Fields(bs.Text())
|
|
if len(f) != 4 {
|
|
return nil, fmt.Errorf("unexpected push line %q", bs.Text())
|
|
}
|
|
pushes = append(pushes, push{f[0], f[1], f[2], f[3]})
|
|
}
|
|
if err := bs.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return pushes, nil
|
|
}
|