misc/git_hook: extract shared githook package; auto-rebuild on version bump (#19440)
Pull the hook logic into a reusable githook library package so tailscale/corp can share it via a thin wrapper main instead of keeping a forked copy in sync. The install flow also changes: a wrapper scripts now build the binary and reinstall the git hooks. Pulling new shared code no longer requires re-running the installer. Updates tailscale/corp#39860 Change-Id: I4d606d11c8c883015c190c54e3387a7f9fe4dd32 Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
This commit is contained in:
committed by
GitHub
parent
1fbb834dc3
commit
514d7d28e7
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package githook
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/modfile"
|
||||
)
|
||||
|
||||
// CheckGoModReplaces reads pushes from stdin and, for pushes to a
|
||||
// remote URL in watchedRemotes, rejects any commit whose go.mod has a
|
||||
// directory-path replace that is not in allowedReplaceDirs. 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 CheckGoModReplaces(args []string, watchedRemotes, allowedReplaceDirs []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("pre-push: expected 2 args, got %d", len(args))
|
||||
}
|
||||
remoteLoc := args[1]
|
||||
|
||||
watched := false
|
||||
for _, r := range watchedRemotes {
|
||||
if r == remoteLoc {
|
||||
watched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !watched {
|
||||
return nil
|
||||
}
|
||||
|
||||
pushes, err := readPushes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading pushes: %w", err)
|
||||
}
|
||||
for _, p := range pushes {
|
||||
if p.isDoNotMergeRef() {
|
||||
continue
|
||||
}
|
||||
if err := checkCommit(p.localSHA, allowedReplaceDirs); 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 := false
|
||||
for _, a := range allowedReplaceDirs {
|
||||
if a == r.New.Path {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user