diff --git a/cmd/conductor/main.go b/cmd/conductor/main.go index 9e639f5..6e00c18 100644 --- a/cmd/conductor/main.go +++ b/cmd/conductor/main.go @@ -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)) @@ -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) } diff --git a/internal/blueprint/blueprint.go b/internal/blueprint/blueprint.go index 74761a1..3ea0c8c 100644 --- a/internal/blueprint/blueprint.go +++ b/internal/blueprint/blueprint.go @@ -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, @@ -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) diff --git a/internal/blueprint/hooks.go b/internal/blueprint/hooks.go index 8c82220..5e87595 100644 --- a/internal/blueprint/hooks.go +++ b/internal/blueprint/hooks.go @@ -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 { diff --git a/internal/commands/helpers_test.go b/internal/commands/helpers_test.go new file mode 100644 index 0000000..40f515b --- /dev/null +++ b/internal/commands/helpers_test.go @@ -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 +} diff --git a/internal/commands/init_test.go b/internal/commands/init_test.go new file mode 100644 index 0000000..6d12e6e --- /dev/null +++ b/internal/commands/init_test.go @@ -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) + }) +} diff --git a/internal/commands/root.go b/internal/commands/root.go index 9c330b2..681ddff 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "sort" "strings" "github.com/devbytes-cloud/freight/internal/blueprint" @@ -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{}{ @@ -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) @@ -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) @@ -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") @@ -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() diff --git a/internal/commands/status.go b/internal/commands/status.go new file mode 100644 index 0000000..1ae6457 --- /dev/null +++ b/internal/commands/status.go @@ -0,0 +1,175 @@ +package commands + +import ( + "bytes" + "html/template" + "os" + "path/filepath" + "sort" + + "github.com/devbytes-cloud/freight/internal/config" + "github.com/devbytes-cloud/freight/internal/githooks" + "github.com/devbytes-cloud/freight/internal/validate" + "github.com/pterm/pterm" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +// statusCommand returns a cobra.Command that reports the current state of Freight in the repository. +func statusCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "status", + Short: "Report the current state of Freight in the repository", + Run: func(cmd *cobra.Command, args []string) { + pterm.SetDefaultOutput(cmd.OutOrStdout()) + pterm.DefaultHeader.Println("Freight Status Report") + + // Check Git repository + gitErr := validate.GitDirs() + if gitErr == nil { + pterm.Success.Println("Valid Git repository") + } else { + pterm.Error.Println("Not a valid Git repository") + } + + railcarPath := "railcar.json" + railcarExists := fileExists(railcarPath) + printStatus("Railcar Manifest (railcar.json)", railcarExists) + + fingerprintPath := ".git/hooks/.fingerprint.yaml" + fingerprintExists := fileExists(fingerprintPath) + printStatus("Fingerprint (.git/hooks/.fingerprint.yaml)", fingerprintExists) + + conductorExists := fileExists("conductor") + printStatus("Conductor Binary", conductorExists) + + var freightConfig config.FreightConfig + if fingerprintExists { + data, err := os.ReadFile(fingerprintPath) + if err == nil { + _ = yaml.Unmarshal(data, &freightConfig) + } + } + + if freightConfig.Version != "" { + pterm.Info.Printfln("Last Applied Freight Version: %s", freightConfig.Version) + } else { + pterm.Info.Println("Last Applied Freight Version: Unknown") + } + + pterm.Info.Printfln("Current Freight Version: %s", Version) + + pterm.DefaultSection.Println("Git Hooks Status") + + hooksData := pterm.TableData{ + {"Hook", "Exists", "Managed by Freight", "Drift"}, + } + + managedHooks := make(map[string]bool) + for _, hook := range freightConfig.Allow { + managedHooks[hook] = true + } + + gitHooks := githooks.NewGitHooks() + hookMap := make(map[string]githooks.GitHook) + var allHooks []string + for _, hookGroup := range gitHooks.Hooks { + for _, h := range hookGroup { + hookMap[h.Name] = h + allHooks = append(allHooks, h.Name) + } + } + sort.Strings(allHooks) + + wd, _ := validate.CurrentWD() + + for _, hookName := range allHooks { + hookPath := filepath.Join(".git/hooks", hookName) + exists := fileExists(hookPath) + managed := managedHooks[hookName] + drift := "N/A" + + if managed { + if !exists { + drift = "missing" + } else { + // Drift detection + gh, ok := hookMap[hookName] + if ok { + expected, err := renderTemplate(gh, wd) + if err != nil { + drift = "error" + } else { + actual, err := os.ReadFile(hookPath) + if err != nil { + drift = "error" + } else if bytes.Equal(actual, []byte(expected)) { + drift = "none" + } else { + drift = "drifted" + } + } + } + } + } + + hooksData = append(hooksData, []string{ + hookName, + boolToCheck(exists), + boolToCheck(managed), + drift, + }) + } + + _ = pterm.DefaultTable.WithHasHeader().WithData(hooksData).Render() + }, + } + + return cmd +} + +// fileExists checks if a file exists at the given path. +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// printStatus prints a status message for a given component, indicating whether it was found or is missing. +func printStatus(name string, exists bool) { + if exists { + pterm.Info.Printfln("%s: Found", name) + } else { + pterm.Error.Printfln("%s: Missing", name) + } +} + +// boolToCheck converts a boolean value to a checkmark (✔) or a cross (✘) symbol. +func boolToCheck(b bool) string { + if b { + return "✔" + } + return "✘" +} + +// renderTemplate renders the Git hook template with the provided workspace directory and hook information. +func renderTemplate(gh githooks.GitHook, wd string) (string, error) { + tmpl, err := template.New(gh.Name).Parse(gh.Template) + if err != nil { + return "", err + } + + path := struct { + Path string + Type string + }{ + Path: wd, + Type: gh.Name, + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, path); err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/internal/commands/status_test.go b/internal/commands/status_test.go new file mode 100644 index 0000000..9edef6c --- /dev/null +++ b/internal/commands/status_test.go @@ -0,0 +1,69 @@ +package commands + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStatusCommand(t *testing.T) { + _, cleanup := withTempGitDir(t) + defer cleanup() + + t.Run("Status without Freight initialized", func(t *testing.T) { + var buf bytes.Buffer + cmd := NewRootCmd() + cmd.SetOut(&buf) + cmd.SetArgs([]string{"status"}) + err := cmd.Execute() + require.NoError(t, err) + + output := buf.String() + assert.Contains(t, output, "Freight Status Report") + }) + + t.Run("Status after Freight init", func(t *testing.T) { + // Initialize Freight + initCmd := NewRootCmd() + initCmd.SetArgs([]string{"init", "--allow", "pre-commit"}) + err := initCmd.Execute() + require.NoError(t, err) + + // Run status + var buf bytes.Buffer + statusCmd := NewRootCmd() + statusCmd.SetOut(&buf) + statusCmd.SetArgs([]string{"status"}) + err = statusCmd.Execute() + require.NoError(t, err) + + output := buf.String() + assert.Contains(t, output, "pre-commit") + }) + + t.Run("Status with drift", func(t *testing.T) { + // Initialize Freight + initCmd := NewRootCmd() + initCmd.SetArgs([]string{"init", "--allow", "pre-commit"}) + err := initCmd.Execute() + require.NoError(t, err) + + // Cause drift in pre-commit + err = os.WriteFile(".git/hooks/pre-commit", []byte("#!/bin/bash\necho drifted"), 0o755) + require.NoError(t, err) + + // Run status + var buf bytes.Buffer + statusCmd := NewRootCmd() + statusCmd.SetOut(&buf) + statusCmd.SetArgs([]string{"status"}) + err = statusCmd.Execute() + require.NoError(t, err) + + output := buf.String() + assert.Contains(t, output, "drifted") + }) +} diff --git a/internal/config/config.go b/internal/config/config.go index 363f534..d93cfd8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,12 +25,13 @@ type CommitOperations struct { PrepareCommitMsg []HookStep `json:"prepare-commit-msg,omitempty"` // CommitMsg This hook is called after the user has edited the commit message. It’s used to validate or enforce specific commit message formats. If it exits non-zero, the commit is aborted. CommitMsg []HookStep `json:"commit-msg,omitempty"` - // CommitMsgPass This hook is + // CommitMsgPass This hook is called after a commit message is accepted. CommitMsgPass []HookStep `json:"commit-msg-pass,omitempty"` // PostCommit This hook is invoked after a commit is made. It cannot affect the commit process but can be used for notifications or logging. PostCommit []HookStep `json:"post-commit,omitempty"` } +// CheckoutOperation represents the configuration for checkout-related Git operations. type CheckoutOperation struct { // PostCheckout represents the steps to be executed after a checkout operation is completed. PostCheckout []HookStep `json:"post-checkout"` @@ -53,3 +54,11 @@ type HookStep struct { // of the Git hook process. Command string `json:"command"` } + +// FreightConfig represents the configuration stored in .git/hooks/.fingerprint.yaml. +type FreightConfig struct { + // Version is the version of Freight used to initialize the repo. + Version string `yaml:"version" json:"version"` + // Allow is the list of allowed Git hooks. + Allow []string `yaml:"allow" json:"allow"` +} diff --git a/internal/embed/embeder.go b/internal/embed/embeder.go index 09015b2..3f8924d 100644 --- a/internal/embed/embeder.go +++ b/internal/embed/embeder.go @@ -27,7 +27,8 @@ const ( windowsARM64 string = "windows-arm64" ) -// WriteBinary will install conductor into your working directory +// WriteBinary installs the conductor binary into the current working directory. +// It detects the system's OS and architecture to select the appropriate binary. func WriteBinary() error { systemInfo := fmt.Sprintf("%s-%s", fetchOS(), fetchArch()) binary := fetchBinary(systemInfo) @@ -44,7 +45,7 @@ func WriteBinary() error { return nil } -// fetchBinary will return the proper conductor binary for your system +// fetchBinary returns the appropriate conductor binary for the given system information. func fetchBinary(systemInfo string) []byte { switch systemInfo { case macOSSilicon: @@ -66,12 +67,12 @@ func fetchBinary(systemInfo string) []byte { } } -// fetchOS returns the current os that is running +// fetchOS returns the current operating system (runtime.GOOS). func fetchOS() string { return runtime.GOOS } -// fetchArch returns the current architecture that is running +// fetchArch returns the current architecture (runtime.GOARCH). func fetchArch() string { return runtime.GOARCH } diff --git a/internal/githooks/githooks.go b/internal/githooks/githooks.go index 7e3284d..6a67dbc 100644 --- a/internal/githooks/githooks.go +++ b/internal/githooks/githooks.go @@ -23,7 +23,7 @@ type GitHook struct { Template string } -// NewGitHooks returns a pointer to a GitHooks instance with commit hooks initialized +// NewGitHooks returns a pointer to a GitHooks instance with commit and checkout hooks initialized. func NewGitHooks() *GitHooks { hooks := map[string][]GitHook{ "Commit": generateHooks(getCommitHook()), diff --git a/internal/validate/validate.go b/internal/validate/validate.go index 874ec2e..e3711db 100644 --- a/internal/validate/validate.go +++ b/internal/validate/validate.go @@ -14,7 +14,7 @@ var ( gitHookDir = filepath.Join(gitDir, "hooks") ) -// GitDirs checks to see if there is a valid .git file in the repo +// GitDirs verifies if the current directory contains a valid .git directory and the hooks subdirectory. func GitDirs() error { if _, err := os.Stat(gitDir); err != nil { return fmt.Errorf(".git directory missing: %w", err) @@ -26,7 +26,7 @@ func GitDirs() error { return nil } -// CurrentWD returns the current directory that you are in +// CurrentWD returns the absolute path of the current working directory. func CurrentWD() (string, error) { dir, err := os.Getwd() if err != nil { diff --git a/website/docs/cli/index.md b/website/docs/cli/index.md index 7fb9234..18fe655 100644 --- a/website/docs/cli/index.md +++ b/website/docs/cli/index.md @@ -9,6 +9,7 @@ For installation instructions, see the [Installation Guide](../installation.md). | Command | Description | | :--- | :--- | | [`init`](./init.md) | Bootstrap Freight in the current repository. | +| [`status`](./status.md) | Report the current state of Freight in the repository. | | [`version`](./version.md) | Print version information. | | `help` | Show help for any command. | diff --git a/website/docs/cli/status.md b/website/docs/cli/status.md new file mode 100644 index 0000000..d708cd4 --- /dev/null +++ b/website/docs/cli/status.md @@ -0,0 +1,78 @@ +# freight status + +Report the current state of Freight in the repository. + +## Description + +The `status` command provides a comprehensive overview of the current Freight setup in your repository. It checks for the existence of required files, displays version information, and analyzes the status of Git hooks. + +**What it checks:** +- **Valid Git repository:** Verifies that the current directory is within a Git repository. +- **Railcar Manifest (`railcar.json`):** Checks if the manifest file exists. +- **Fingerprint (`.git/hooks/.fingerprint.yaml`):** Checks if the Freight fingerprint file exists in `.git/hooks/`. +- **Conductor Binary:** Checks if the `conductor` binary is present in the repository root. +- **Version Information:** Displays both the Freight version used during initialization and the current binary version. +- **Git Hooks Status:** Provides a detailed table showing: + - Which hooks are present. + - Which hooks are managed by Freight. + - Detection of "drift" (if the hook content differs from what Freight expects). + +## Status Fields + +### Core Status + +- **Valid Git repository**: Confirms if the current directory is a valid Git repository where Freight can operate. +- **Railcar Manifest (`railcar.json`)**: The configuration file that defines your hook steps (commands to run for each hook). +- **Fingerprint (`.git/hooks/.fingerprint.yaml`)**: A file stored in `.git/hooks/` that tracks the state of hooks managed by Freight and the version used for initialization. +- **Conductor Binary**: The small executable that Freight installs into your repository to orchestrate the execution of hooks defined in `railcar.json`. +- **Freight Version**: The version recorded in the fingerprint when the repository was last initialized or migrated. +- **Current Freight Binary Version**: The version of the `freight` CLI tool you are currently running. + +### Git Hooks Table + +The Git Hooks Status table provides a granular look at each supported hook: + +- **Hook**: The name of the Git hook (e.g., `pre-commit`). +- **Exists**: Indicates whether a file for this hook exists in the `.git/hooks/` directory. +- **Managed by Freight**: Shows if Freight is configured to manage this hook. This is determined by the "allow list" created during `freight init`. If a hook is not managed, Freight will not attempt to rewire it or check for drift. +- **Drift**: Measures whether the current hook file in `.git/hooks/` matches the template Freight expects. + - `none`: The hook is correctly managed and matches the expected state. + - `drifted`: The hook file exists and is managed by Freight, but its content has been modified or overwritten by something else. + - `missing`: The hook is supposed to be managed by Freight, but the file is missing from `.git/hooks/`. + - `error`: An error occurred while trying to read the hook file or calculate the expected state. + - `N/A`: Drift detection is not applicable (usually because the hook is not managed by Freight). + +## Usage + +```bash +freight status +``` + +## Examples + +Check the status of Freight in your repository: +```bash +freight status +``` + +**Output:** +```text + + Freight Status Report + + SUCCESS Valid Git repository + INFO Railcar Manifest (railcar.json): Found + INFO Fingerprint (.git/hooks/.fingerprint.yaml): Found + INFO Conductor Binary: Found + INFO Last Applied Freight Version: dev + INFO Current Freight Version: dev + +# Git Hooks Status + +Hook | Exists | Managed by Freight | Drift +commit-msg | ✔ | ✔ | none +post-checkout | ✔ | ✔ | none +post-commit | ✔ | ✔ | none +pre-commit | ✔ | ✔ | none +prepare-commit-msg | ✔ | ✔ | none +``` diff --git a/website/sidebars.ts b/website/sidebars.ts index 7965cf8..45a042c 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -25,6 +25,7 @@ const sidebars: SidebarsConfig = { }, items: [ 'cli/init', + 'cli/status', 'cli/version', ], },