// Copyright 2023 Harness, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package hook import ( "context" "errors" "os/signal" "syscall" "gopkg.in/alecthomas/kingpin.v2" ) const ( // ParamPreReceive is the parameter under which the pre-receive operation is registered. ParamPreReceive = "pre-receive" // ParamUpdate is the parameter under which the update operation is registered. ParamUpdate = "update" // ParamPostReceive is the parameter under which the post-receive operation is registered. ParamPostReceive = "post-receive" // CommandNamePreReceive is the command used by git for the pre-receive hook // (os.args[0] == "hooks/pre-receive"). CommandNamePreReceive = "hooks/pre-receive" // CommandNameUpdate is the command used by git for the update hook // (os.args[0] == "hooks/update"). CommandNameUpdate = "hooks/update" // CommandNamePostReceive is the command used by git for the post-receive hook // (os.args[0] == "hooks/post-receive"). CommandNamePostReceive = "hooks/post-receive" ) // SanitizeArgsForGit sanitizes the command line arguments (os.Args) if the command indicates they are coming from git. // Returns the santized args and true if the call comes from git, otherwise the original args are returned with false. func SanitizeArgsForGit(command string, args []string) ([]string, bool) { switch command { case CommandNamePreReceive: return append([]string{ParamPreReceive}, args...), true case CommandNameUpdate: return append([]string{ParamUpdate}, args...), true case CommandNamePostReceive: return append([]string{ParamPostReceive}, args...), true default: return args, false } } // KingpinRegister is an abstraction of an entity that allows to register commands. // This is required to allow registering hook commands both on application and sub command level. type KingpinRegister interface { Command(name, help string) *kingpin.CmdClause } var ( // ErrDisabled can be returned by the loading function to indicate the githook has been disabled. // Returning the error will cause the githook execution to be skipped (githook is noop and returns success). ErrDisabled = errors.New("githook disabled") ) // LoadCLICoreFunc is a function that creates a new CLI core that's used for githook cli execution. // This allows users to initialize their own CLI core with custom Client and configuration. type LoadCLICoreFunc func() (*CLICore, error) // RegisterAll registers all githook commands. func RegisterAll(cmd KingpinRegister, loadCoreFn LoadCLICoreFunc) { RegisterPreReceive(cmd, loadCoreFn) RegisterUpdate(cmd, loadCoreFn) RegisterPostReceive(cmd, loadCoreFn) } // RegisterPreReceive registers the pre-receive githook command. func RegisterPreReceive(cmd KingpinRegister, loadCoreFn LoadCLICoreFunc) { c := &preReceiveCommand{ loadCoreFn: loadCoreFn, } cmd.Command(ParamPreReceive, "hook that is executed before any reference of the push is updated"). Action(c.run) } // RegisterUpdate registers the update githook command. func RegisterUpdate(cmd KingpinRegister, loadCoreFn LoadCLICoreFunc) { c := &updateCommand{ loadCoreFn: loadCoreFn, } subCmd := cmd.Command(ParamUpdate, "hook that is executed before the specific reference gets updated"). Action(c.run) subCmd.Arg("ref", "reference for which the hook is executed"). Required(). StringVar(&c.ref) subCmd.Arg("old", "old commit sha"). Required(). StringVar(&c.oldSHA) subCmd.Arg("new", "new commit sha"). Required(). StringVar(&c.newSHA) } // RegisterPostReceive registers the post-receive githook command. func RegisterPostReceive(cmd KingpinRegister, loadCoreFn LoadCLICoreFunc) { c := &postReceiveCommand{ loadCoreFn: loadCoreFn, } cmd.Command(ParamPostReceive, "hook that is executed after all references of the push got updated"). Action(c.run) } type preReceiveCommand struct { loadCoreFn LoadCLICoreFunc } func (c *preReceiveCommand) run(*kingpin.ParseContext) error { return run(c.loadCoreFn, func(ctx context.Context, core *CLICore) error { return core.PreReceive(ctx) }) } type updateCommand struct { loadCoreFn LoadCLICoreFunc ref string oldSHA string newSHA string } func (c *updateCommand) run(*kingpin.ParseContext) error { return run(c.loadCoreFn, func(ctx context.Context, core *CLICore) error { return core.Update(ctx, c.ref, c.oldSHA, c.newSHA) }) } type postReceiveCommand struct { loadCoreFn LoadCLICoreFunc } func (c *postReceiveCommand) run(*kingpin.ParseContext) error { return run(c.loadCoreFn, func(ctx context.Context, core *CLICore) error { return core.PostReceive(ctx) }) } func run(loadCoreFn LoadCLICoreFunc, fn func(ctx context.Context, core *CLICore) error) error { core, err := loadCoreFn() if errors.Is(err, ErrDisabled) { // complete operation successfully without making any calls to the server. return nil } if err != nil { return err } // Create context that listens for the interrupt signal from the OS and has a timeout. ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() ctx, cancel := context.WithTimeout(ctx, core.executionTimeout) defer cancel() return fn(ctx, core) }