Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

204 lines
6.6 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/zzet/gortex/internal/githooks"
)
var (
githookRegenMermaid bool
githookRegenWiki bool
githookRegenDocs bool
githookRegenChurn bool
githookRegenReleases bool
githookMermaidOutDir string
githookWikiOutDir string
githookDocsOutPath string
githookChurnBranch string
githookReleasesBranch string
githookBinary string
)
var githookCmd = &cobra.Command{
Use: "githook",
Short: "Manage local git hooks that regenerate gortex artefacts",
Long: `Install, uninstall, and inspect git hooks that re-run gortex
commands. Supported hooks: post-commit, post-merge.
The hook is idempotent: re-running install replaces only the gortex
block, leaving any other hook content intact. Uninstall removes the
block and deletes the hook file when it contains nothing else.`,
}
var githookInstallCmd = &cobra.Command{
Use: "install <hook>",
Short: "Install a git hook (post-commit or post-merge)",
Args: cobra.ExactArgs(1),
RunE: runGithookInstall,
}
var githookUninstallCmd = &cobra.Command{
Use: "uninstall <hook>",
Short: "Remove the gortex-managed block from a git hook",
Args: cobra.ExactArgs(1),
RunE: runGithookUninstall,
}
var githookStatusCmd = &cobra.Command{
Use: "status [hook]",
Short: "Report whether the named hook is gortex-managed (default: post-commit)",
Args: cobra.MaximumNArgs(1),
RunE: runGithookStatus,
}
func init() {
githookInstallCmd.Flags().BoolVar(&githookRegenMermaid, "regen-mermaid", false,
"include `gortex export --format mermaid --scope all` in the hook")
githookInstallCmd.Flags().BoolVar(&githookRegenWiki, "regen-wiki", false,
"include `gortex wiki .` in the hook")
githookInstallCmd.Flags().BoolVar(&githookRegenDocs, "regen-docs", false,
"include `gortex docs . --out CHANGELOG_AUTO.md` in the hook")
githookInstallCmd.Flags().BoolVar(&githookRegenChurn, "regen-churn", false,
"include `gortex enrich churn` so get_churn_rate stays fresh without an at-read-time git subprocess")
githookInstallCmd.Flags().StringVar(&githookChurnBranch, "churn-branch", "",
"branch / tag / SHA the churn enricher pins to (default: resolve at hook run-time)")
githookInstallCmd.Flags().BoolVar(&githookRegenReleases, "regen-releases", false,
"include `gortex enrich releases` so analyze kind=releases reads pre-computed Meta")
githookInstallCmd.Flags().StringVar(&githookReleasesBranch, "releases-branch", "",
"branch / tag / SHA the releases enricher restricts to (default: resolve at hook run-time)")
githookInstallCmd.Flags().StringVar(&githookMermaidOutDir, "mermaid-out-dir", "docs/architecture/",
"output directory for mermaid diagrams")
githookInstallCmd.Flags().StringVar(&githookWikiOutDir, "wiki-out-dir", "wiki",
"output directory for the wiki")
githookInstallCmd.Flags().StringVar(&githookDocsOutPath, "docs-out-path", "CHANGELOG_AUTO.md",
"output path for the docs bundle")
githookInstallCmd.Flags().StringVar(&githookBinary, "binary", "gortex",
"gortex binary name (resolved from $PATH at runtime)")
githookCmd.AddCommand(githookInstallCmd)
githookCmd.AddCommand(githookUninstallCmd)
githookCmd.AddCommand(githookStatusCmd)
rootCmd.AddCommand(githookCmd)
}
// supportedHook validates the hook arg. We mirror the package-level
// SupportedHooks list rather than importing it so the CLI surface
// stays decoupled from the install package's internals.
func supportedHook(name string) error {
if name == "post-commit" || name == "post-merge" || name == "post-checkout" {
return nil
}
return fmt.Errorf("unsupported hook %q (supported: post-commit, post-merge, post-checkout)", name)
}
func runGithookInstall(cmd *cobra.Command, args []string) error {
hook := args[0]
if err := supportedHook(hook); err != nil {
return err
}
repoRoot, err := resolveGithookRepoRoot()
if err != nil {
return err
}
if !githookRegenMermaid && !githookRegenWiki && !githookRegenDocs && !githookRegenChurn && !githookRegenReleases {
// Default to mermaid when nothing was chosen — minimum
// useful behaviour.
githookRegenMermaid = true
}
path, err := githooks.InstallHook(repoRoot, hook, githooks.InstallOpts{
Binary: githookBinary,
RegenMermaid: githookRegenMermaid,
RegenWiki: githookRegenWiki,
RegenDocs: githookRegenDocs,
RegenChurn: githookRegenChurn,
ChurnBranch: githookChurnBranch,
RegenReleases: githookRegenReleases,
ReleasesBranch: githookReleasesBranch,
MermaidOutDir: githookMermaidOutDir,
WikiOutDir: githookWikiOutDir,
DocsOutPath: githookDocsOutPath,
})
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(),
"installed %s hook at %s\nactions: mermaid=%t wiki=%t docs=%t churn=%t releases=%t\n",
hook, path, githookRegenMermaid, githookRegenWiki, githookRegenDocs, githookRegenChurn, githookRegenReleases)
return nil
}
func runGithookUninstall(cmd *cobra.Command, args []string) error {
hook := args[0]
if err := supportedHook(hook); err != nil {
return err
}
repoRoot, err := resolveGithookRepoRoot()
if err != nil {
return err
}
path, removed, err := githooks.UninstallHook(repoRoot, hook)
if err != nil {
return err
}
if removed {
fmt.Fprintf(cmd.OutOrStdout(), "removed gortex block from %s\n", path)
} else {
fmt.Fprintf(cmd.OutOrStdout(), "no gortex block found at %s\n", path)
}
return nil
}
func runGithookStatus(cmd *cobra.Command, args []string) error {
hook := "post-commit"
if len(args) > 0 {
if err := supportedHook(args[0]); err != nil {
return err
}
hook = args[0]
}
repoRoot, err := resolveGithookRepoRoot()
if err != nil {
return err
}
hookPath, err := githooks.HookPathFor(repoRoot, hook)
if err != nil {
return err
}
// Read directly; Status() is post-commit-locked and we want per-hook
// detail. Mirrors Status() but parameterised on hook.
body, ferr := os.ReadFile(hookPath)
exists := ferr == nil
managed := false
if exists {
bs := string(body)
begin := "# gortex-managed:" + hook + ":begin"
end := "# gortex-managed:" + hook + ":end"
if strings.Contains(bs, begin) && strings.Contains(bs, end) {
managed = true
}
}
out := cmd.OutOrStdout()
fmt.Fprintf(out, "hook: %s\n", hook)
fmt.Fprintf(out, "hook_path: %s\n", hookPath)
fmt.Fprintf(out, "exists: %t\n", exists)
fmt.Fprintf(out, "managed: %t\n", managed)
return nil
}
// resolveGithookRepoRoot picks the repo root for the active command.
// Today we always use the working directory; the install runs git
// itself to resolve git-dir and core.hooksPath.
func resolveGithookRepoRoot() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Clean(cwd), nil
}