| // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: | |
| //go:build go1.26 | |
| package hooks | |
| import ( | |
| "bytes" | |
| "errors" | |
| "fmt" | |
| "slices" | |
| "strings" | |
| "text/template" | |
| "github.com/spf13/cobra" | |
| ) | |
| const maxMessages = 10 | |
| func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) { | |
| out := hookTemplate | |
| if strings.Contains(hookTemplate, "{{") { | |
| // Message may be a template. | |
| msgContext := commandInfo{cmd: cmd} | |
| tmpl, err := template.New("").Funcs(template.FuncMap{ | |
| "command": msgContext.command, | |
| "flagValue": msgContext.flagValue, | |
| "argValue": msgContext.argValue, | |
| // kept for backward-compatibility with old templates. | |
| "flag": func(_ any, flagName string) (string, error) { return msgContext.flagValue(flagName) }, | |
| "arg": func(_ any, i int) (string, error) { return msgContext.argValue(i) }, | |
| }).Parse(hookTemplate) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var b bytes.Buffer | |
| err = tmpl.Execute(&b, msgContext) | |
| if err != nil { | |
| return nil, err | |
| } | |
| out = b.String() | |
| } | |
| if n := strings.Count(out, "\n"); n > maxMessages { | |
| return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages) | |
| } | |
| lines := strings.SplitN(out, "\n", maxMessages) | |
| // Skip messages containing only whitespace, but preserve empty lines | |
| // in messages that have content (e.g., for paragraph spacing). | |
| if !slices.ContainsFunc(lines, func(line string) bool { | |
| return strings.TrimSpace(line) != "" | |
| }) { | |
| return nil, nil | |
| } | |
| return lines, nil | |
| } | |
| var ErrHookTemplateParse = errors.New("failed to parse hook template") | |
| // commandInfo provides info about the command for which the hook was invoked. | |
| // It is used for templated hook-messages. | |
| type commandInfo struct { | |
| cmd *cobra.Command | |
| } | |
| // Name returns the name of the (sub)command for which the hook was invoked. | |
| // | |
| // It's used for backward-compatibility with old templates. | |
| func (c commandInfo) Name() string { | |
| return c.command() | |
| } | |
| // command returns the name of the (sub)command for which the hook was invoked. | |
| func (c commandInfo) command() string { | |
| if c.cmd == nil { | |
| return "" | |
| } | |
| return c.cmd.Name() | |
| } | |
| // flagValue returns the value that was set for the given flag when the hook was invoked. | |
| func (c commandInfo) flagValue(flagName string) (string, error) { | |
| if c.cmd == nil { | |
| return "", fmt.Errorf("%w: flagValue: cmd is nil", ErrHookTemplateParse) | |
| } | |
| f := c.cmd.Flag(flagName) | |
| if f == nil { | |
| return "", fmt.Errorf("%w: flagValue: no flags found", ErrHookTemplateParse) | |
| } | |
| return f.Value.String(), nil | |
| } | |
| // argValue returns the value of the nth argument. | |
| func (c commandInfo) argValue(n int) (string, error) { | |
| if c.cmd == nil { | |
| return "", fmt.Errorf("%w: arg: cmd is nil", ErrHookTemplateParse) | |
| } | |
| flags := c.cmd.Flags() | |
| v := flags.Arg(n) | |
| if v == "" && n >= flags.NArg() { | |
| return "", fmt.Errorf("%w: arg: %dth argument not set", ErrHookTemplateParse, n) | |
| } | |
| return v, nil | |
| } |