tailscale/misc/git_hook/githook/commit-msg.go
Fernando Serboncini 514d7d28e7
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>
2026-04-17 16:24:39 -04:00

65 lines
1.6 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package githook
import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"io"
"os"
"os/exec"
)
// AddChangeID strips comments from the commit message at args[0] and
// prepends a random Change-Id trailer.
//
// Intended as a commit-msg hook.
// https://git-scm.com/docs/githooks#_commit_msg
func AddChangeID(args []string) error {
if len(args) != 1 {
return errors.New("usage: commit-msg message.txt")
}
file := args[0]
msg, err := os.ReadFile(file)
if err != nil {
return err
}
msg = filterCutLine(msg)
var id [20]byte
if _, err := io.ReadFull(rand.Reader, id[:]); err != nil {
return fmt.Errorf("could not generate Change-Id: %v", err)
}
cmdLines := [][]string{
{"git", "stripspace", "--strip-comments"},
{"git", "interpret-trailers", "--no-divider", "--where=start", "--if-exists", "doNothing", "--trailer", fmt.Sprintf("Change-Id: I%x", id)},
}
for _, cmdLine := range cmdLines {
if len(msg) == 0 {
// Don't let commands turn an empty message into a non-empty one (issue 2205).
break
}
cmd := exec.Command(cmdLine[0], cmdLine[1:]...)
cmd.Stdin = bytes.NewReader(msg)
msg, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to run %v: %w\n%s", cmd, err, msg)
}
}
return os.WriteFile(file, msg, 0666)
}
var gitCutLine = []byte("# ------------------------ >8 ------------------------")
// filterCutLine strips a `git commit -v`-style cutline and everything
// after it from msg.
func filterCutLine(msg []byte) []byte {
if before, _, ok := bytes.Cut(msg, gitCutLine); ok {
return before
}
return msg
}