Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/conductor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func main() {
//• Applypatch Hooks Order: applypatch-msg → pre-applypatch → post-applypatch
}

// run executes a sequence of hook steps with the provided hook data.
func run(data []config.HookStep, hookData string) {
for _, v := range data {
pterm.Info.Println(fmt.Sprintf("Name: %s", v.Name))
Expand Down Expand Up @@ -111,10 +112,12 @@ func run(data []config.HookStep, hookData string) {
}
}

// printCurrentHook prints the name of the Git hook currently being executed.
func printCurrentHook(hook string) {
pterm.DefaultSection.Println("Running hook:", hook)
}

// ptermWriter is an io.Writer that routes its output through pterm.
type ptermWriter struct {
printFunc func(...any)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/blueprint/blueprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type BluePrint struct {
Data string
}

// NewBluePrint generates a new blueprint to be used for write to the filesystem
// NewBluePrint generates a new blueprint to be used for writing to the filesystem.
func NewBluePrint(name, writePath, data string, values any) *BluePrint {
return &BluePrint{
Name: name,
Expand All @@ -27,12 +27,12 @@ func NewBluePrint(name, writePath, data string, values any) *BluePrint {
}
}

// Exists allows you to check if the file within the blueprint exists
// Exists checks if the file specified in the blueprint already exists on the filesystem.
func (b *BluePrint) Exists() (os.FileInfo, error) {
return os.Stat(b.WritePath)
}

// Write takes the BluePrint data then templates it out to the filesystem
// Write renders the BluePrint's template data with its values and writes the result to the filesystem at WritePath.
func (b *BluePrint) Write() error {
tmpl := template.Must(template.New(b.Name).Parse(b.Data))
file, err := os.OpenFile(b.WritePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
Expand Down
1 change: 1 addition & 0 deletions internal/blueprint/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/devbytes-cloud/freight/internal/validate"
)

// NewGitHook creates a new BluePrint for a specific Git hook.
func NewGitHook(gh *githooks.GitHook) (*BluePrint, error) {
dir, err := validate.CurrentWD()
if err != nil {
Expand Down
31 changes: 31 additions & 0 deletions internal/commands/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package commands

import (
"os"
"testing"

"github.com/stretchr/testify/require"
)

func withTempGitDir(t *testing.T) (string, func()) {
t.Helper()

tmpDir, err := os.MkdirTemp("", "freight-test-*")
require.NoError(t, err)

origWd, err := os.Getwd()
require.NoError(t, err)

err = os.Chdir(tmpDir)
require.NoError(t, err)

err = os.MkdirAll(".git/hooks", 0o755)
require.NoError(t, err)

cleanup := func() {
_ = os.Chdir(origWd)
_ = os.RemoveAll(tmpDir)
}

return tmpDir, cleanup
}
114 changes: 114 additions & 0 deletions internal/commands/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package commands

import (
"os"
"sort"
"testing"

"github.com/devbytes-cloud/freight/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestInitMergeAllow(t *testing.T) {
_, cleanup := withTempGitDir(t)
defer cleanup()

fingerprintPath := ".git/hooks/.fingerprint.yaml"

t.Run("Initial init with specific allow", func(t *testing.T) {
cmd := NewRootCmd()
cmd.SetArgs([]string{"init", "--allow", "pre-commit,commit-msg"})
err := cmd.Execute()
require.NoError(t, err)

// Check fingerprint
data, err := os.ReadFile(fingerprintPath)
require.NoError(t, err)

var cfg config.FreightConfig
err = yaml.Unmarshal(data, &cfg)
require.NoError(t, err)

sort.Strings(cfg.Allow)
assert.Equal(t, []string{"commit-msg", "pre-commit"}, cfg.Allow)
})

t.Run("Merge new allow with existing", func(t *testing.T) {
cmd := NewRootCmd()
cmd.SetArgs([]string{"init", "--allow", "post-commit"})
err := cmd.Execute()
require.NoError(t, err)

// Check fingerprint
data, err := os.ReadFile(fingerprintPath)
require.NoError(t, err)

var cfg config.FreightConfig
err = yaml.Unmarshal(data, &cfg)
require.NoError(t, err)

sort.Strings(cfg.Allow)
assert.Equal(t, []string{"commit-msg", "post-commit", "pre-commit"}, cfg.Allow)
})

t.Run("No duplicates when merging", func(t *testing.T) {
cmd := NewRootCmd()
cmd.SetArgs([]string{"init", "--allow", "pre-commit,post-checkout"})
err := cmd.Execute()
require.NoError(t, err)

// Check fingerprint
data, err := os.ReadFile(fingerprintPath)
require.NoError(t, err)

var cfg config.FreightConfig
err = yaml.Unmarshal(data, &cfg)
require.NoError(t, err)

sort.Strings(cfg.Allow)
assert.Equal(t, []string{"commit-msg", "post-checkout", "post-commit", "pre-commit"}, cfg.Allow)
})

t.Run("Only specified hooks are initialized with --allow", func(t *testing.T) {
// Clean up and start fresh
err := os.RemoveAll(".git/hooks")
require.NoError(t, err)
err = os.MkdirAll(".git/hooks", 0o755)
require.NoError(t, err)

// 1. Init with pre-commit
cmd := NewRootCmd()
cmd.SetArgs([]string{"init", "--allow", "pre-commit"})
err = cmd.Execute()
require.NoError(t, err)

require.FileExists(t, ".git/hooks/pre-commit")
_, err = os.Stat(".git/hooks/post-commit")
assert.True(t, os.IsNotExist(err))

// 2. Init with post-commit. It should NOT re-initialize pre-commit (though pre-commit should still exist if it was there)
// To truly test it ONLY runs post-commit, we can delete pre-commit and see if it comes back.
err = os.Remove(".git/hooks/pre-commit")
require.NoError(t, err)

cmd = NewRootCmd()
cmd.SetArgs([]string{"init", "--allow", "post-commit"})
err = cmd.Execute()
require.NoError(t, err)

require.FileExists(t, ".git/hooks/post-commit")
_, err = os.Stat(".git/hooks/pre-commit")
assert.True(t, os.IsNotExist(err), "pre-commit should not have been re-initialized")

// 3. Check fingerprint has BOTH
data, err := os.ReadFile(fingerprintPath)
require.NoError(t, err)
var cfg config.FreightConfig
err = yaml.Unmarshal(data, &cfg)
require.NoError(t, err)
sort.Strings(cfg.Allow)
assert.Equal(t, []string{"post-commit", "pre-commit"}, cfg.Allow)
})
}
70 changes: 66 additions & 4 deletions internal/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"sort"
"strings"

"github.com/devbytes-cloud/freight/internal/blueprint"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/devbytes-cloud/freight/internal/validate"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

var allowHooks = map[string]struct{}{
Expand Down Expand Up @@ -55,7 +57,48 @@ func NewRootCmd() *cobra.Command {
os.Exit(1)
}

validatedAllow, err := validateAllowHooks(userAllow)
fingerprintPath := ".git/hooks/.fingerprint.yaml"

var freightConfig config.FreightConfig
if data, err := os.ReadFile(fingerprintPath); err == nil {
_ = yaml.Unmarshal(data, &freightConfig)
}

var hooksToSetup []string

if len(userAllow) > 0 {
hooksToSetup = userAllow
existingAllow := make(map[string]struct{})
for _, a := range freightConfig.Allow {
existingAllow[a] = struct{}{}
}
for _, a := range userAllow {
existingAllow[a] = struct{}{}
}
var mergedAllow []string
for a := range existingAllow {
mergedAllow = append(mergedAllow, a)
}
sort.Strings(mergedAllow)
freightConfig.Allow = mergedAllow
} else if len(freightConfig.Allow) == 0 {
// If no user allow and no existing allow, use default all hooks
var allHooks []string
for h := range allowHooks {
allHooks = append(allHooks, h)
}
sort.Strings(allHooks)
freightConfig.Allow = allHooks
hooksToSetup = freightConfig.Allow
} else {
// If no user allow but existing allow exists, use existing allow
hooksToSetup = freightConfig.Allow
}

pterm.Debug.Printfln("Effective allow: %v", freightConfig.Allow)
pterm.Debug.Printfln("Hooks to setup: %v", hooksToSetup)

validatedAllow, err := validateAllowHooks(hooksToSetup)
if err != nil {
cmd.PrintErrln(err)
os.Exit(1)
Expand All @@ -77,18 +120,36 @@ func NewRootCmd() *cobra.Command {
if err := installBinary(); err != nil {
cmd.PrintErrln(err)
}

// Save fingerprint
freightConfig.Version = Version
data, err := yaml.Marshal(freightConfig)
if err == nil {
pterm.DefaultSection.Println("Writing fingerprint file")
comment := "# This file is managed by Freight. It keeps track of the version and allowed hooks.\n"
finalData := append([]byte(comment), data...)
if err := os.WriteFile(fingerprintPath, finalData, 0o644); err != nil {
pterm.Error.Printfln("✖ Failed to write fingerprint: %v", err)
} else {
pterm.Success.Printfln("✔ Fingerprint .fingerprint.yaml successfully written to .git/hooks")
}
}

pterm.Success.Println("Freight initialized successfully!")
},
}

initCmd.Flags().BoolP("config-force", "c", false, "If you wish to force write the config")
initCmd.Flags().StringSliceP("allow", "a", []string{}, "Specific Git hooks to install (default: all). Valid options: pre-commit, prepare-commit-msg, commit-msg, post-commit, post-checkout")
rootCmd.AddCommand(initCmd)
rootCmd.AddCommand(statusCommand())
rootCmd.AddCommand(versionCommand())

return rootCmd
}

// setupHooks initializes and writes the Git hooks.
// setupHooks initializes and writes the Git hooks to the .git/hooks directory.
// It only writes the hooks that are included in the allowedHooks map.
func setupHooks(allowedHooks map[string]struct{}) error {
pterm.DefaultSection.Println("Generating .git/hooks")
pterm.Debug.Printfln("Allowed hooks: %v", allowedHooks)
Expand Down Expand Up @@ -125,7 +186,8 @@ func writeConfig(v *githooks.GitHook) error {
return nil
}

// setupConfig creates and writes the configuration file.
// setupConfig initializes the Railcar configuration file (railcar.json).
// If forceWrite is true, it overwrites any existing configuration.
func setupConfig(forceWrite bool) error {
pterm.DefaultSection.Println("Writing config file")

Expand All @@ -151,7 +213,7 @@ func setupConfig(forceWrite bool) error {
return nil
}

// installBinary writes the embedded binary to the filesystem.
// installBinary extracts and writes the embedded Conductor binary to the current directory.
func installBinary() error {
pterm.DefaultSection.Println("Installing Conductor binary")
err := embed.WriteBinary()
Expand Down
Loading