Skip to content

Make init/install safe by default - #23

Merged
errfld merged 10 commits into
mainfrom
git-smee-5q6/safe-init-install
Feb 4, 2026
Merged

Make init/install safe by default#23
errfld merged 10 commits into
mainfrom
git-smee-5q6/safe-init-install

Conversation

@errfld

@errfld errfld commented Feb 4, 2026

Copy link
Copy Markdown
Owner

What changed

  • added managed-file safety checks to installer writes
  • git smee install now only overwrites managed hook files by default
  • added --force to git smee install and git smee init for explicit overwrite behavior
  • git smee init now writes a managed header marker into generated config files
  • updated README command docs and safety behavior notes

Tests

  • expanded CLI integration tests for force and non-force init/install paths
  • expanded installer integration tests for managed/unmanaged and force behavior
  • cargo test --workspace

Summary by CodeRabbit

  • New Features

    • Added a --force flag to init and install; install now prints confirmation when hooks are installed.
  • Behavior

    • init/install refuse to overwrite unmanaged config or hook files unless --force is used.
    • install targets Git's effective hooks directory and prefixes managed files with a visible managed header.
  • Validation

    • Configs are validated on load; empty commands and unknown fields are rejected with clear errors.
  • Tests

    • Expanded integration and unit tests covering force, overwrite, managed markers, and validation.

@coderabbitai

coderabbitai Bot commented Feb 4, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Threads a new --force flag through the CLI into a force-aware FileSystemHookInstaller, adds a managed-file marker and header utilities, enforces config validation on load, performs pre-write checks that refuse overwriting unmanaged files, and expands integration tests for these behaviors.

Changes

Cohort / File(s) Summary
Docs
README.md
Document optional --force for git smee init and git smee install, clarify install overwrite semantics and target hooks directory.
CLI
crates/git-smee-cli/src/main.rs
Command::Install and Command::Initialize become struct variants with force: bool; CLI passes force to from_default_with_force; writes managed header on init and prints install confirmation.
CLI tests
crates/git-smee-cli/tests/cli_integration.rs
Add integration tests covering install/init flows with and without --force, validating overwrite refusal/success, managed-marker presence, and hook preservation/overwrite semantics.
Core installer
crates/git-smee-core/src/installer.rs
Add MANAGED_FILE_MARKER and header helpers (with_managed_header*), new error variants for read/write/refuse cases, force-aware constructors (from_*_with_force), force_overwrite field, is_managed_file and ensure_can_write_* pre-checks; install/config writes consult marker+force.
Core tests & exports
crates/git-smee-core/tests/installer_integration.rs, crates/git-smee-core/src/lib.rs
Expand installer integration tests for managed/unmanaged hooks, shebang/marker edge cases; re-export HookInstaller and MANAGED_FILE_MARKER; expose force-aware constructor path.
Config validation
crates/git-smee-core/src/config.rs, crates/git-smee-core/tests/config_integration.rs
Add SmeeConfig::validate and ValidationError enum; apply #[serde(deny_unknown_fields)] to HookDefinition; from_toml validates immediately; add tests for empty commands and unknown fields.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as "CLI Handler"
    participant Installer as "FileSystemHookInstaller"
    participant FS as "File System"

    User->>CLI: git smee install [--force]
    CLI->>Installer: from_default_with_force(force)
    CLI->>Installer: install_hook("pre-commit")
    Installer->>FS: stat/read hook file
    alt file exists
        FS-->>Installer: file contents
        Installer->>Installer: is_managed_file(path) -> bool
        alt is_managed OR force == true
            Installer->>FS: write managed header + new content (overwrite)
            FS-->>Installer: write OK
            Installer-->>CLI: success
        else unmanaged AND force == false
            Installer-->>CLI: Err(RefusingToOverwriteUnmanagedHookFile)
        end
    else not exists
        Installer->>FS: write managed header + content (create)
        FS-->>Installer: write OK
        Installer-->>CLI: success
    end
    CLI-->>User: Hooks installed or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I nibble headers, neat and spry,
"THIS FILE IS MANAGED" — clear and high.
I hop with caution, or shove with force,
guarding hooks along their course.
Tidy files, no chaos, of course.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Make init/install safe by default' accurately summarizes the main objective of the PR, which adds safety checks to prevent unintended overwrites of unmanaged files in the init and install commands by default.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch git-smee-5q6/safe-init-install

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@crates/git-smee-core/src/installer.rs`:
- Around line 200-238: The current is_managed_file scans the whole file for
MANAGED_FILE_MARKER which can false-positive on unrelated occurrences; change
detection to only inspect the file header: read only the start of the file (or
the first two lines), handle an optional shebang (#!) on line 1 and then check
that the next line equals or starts with MANAGED_FILE_MARKER (use
MANAGED_FILE_MARKER.as_str()), and update ensure_can_write_hook and
ensure_can_write_config behavior by relying on this new header-only
is_managed_file implementation so unmanaged files with the marker elsewhere are
not treated as managed.
🧹 Nitpick comments (1)
crates/git-smee-core/src/installer.rs (1)

8-14: Consolidate with_managed_header to avoid duplicate helpers.

The provided context shows a with_managed_header already in crates/git-smee-core/src/config.rs (lines 11-13). Consider re-exporting a single helper to prevent drift.

Comment thread crates/git-smee-core/src/installer.rs
## What changed
- added a dedicated `SmeeConfig::validate()` pass and invoke it during
config load
- reject empty hook lists and empty/whitespace commands with explicit
`ValidationError` variants that include hook name and entry index
- reject unknown fields in `HookDefinition` via
`#[serde(deny_unknown_fields)]`
- expanded config integration coverage for whitespace commands and
unknown hook entry fields
- added a CLI integration test verifying `install` reports config
validation failures
- removed a stray assertion from `installer_integration.rs` that
prevented workspace tests from compiling

## Validation
- `cargo test --workspace`

Closes git-smee-4q5.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Configuration validation now enforces non-empty hook entries and
commands, reporting detailed errors that identify the specific hook and
entry with issues.
  * Unknown configuration fields are now rejected during parsing.

* **Tests**
* Added integration tests covering validation error reporting for
invalid configurations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@errfld

errfld commented Feb 4, 2026

Copy link
Copy Markdown
Owner Author

Follow-up on the nitpick about duplicate with_managed_header: I checked the current tree and there is no with_managed_header in crates/git-smee-core/src/config.rs (only in installer.rs). I kept it installer-scoped because marker semantics are installer behavior, so there isn't an actual duplication to consolidate right now.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/git-smee-core/src/installer.rs (1)

270-278: ⚠️ Potential issue | 🟡 Minor

Wrong error variant used for config file write failure.

install_config_file maps write errors to Error::FailedToWriteHook, which produces a misleading error message referencing "hook" for a config file failure.

🐛 Proposed fix: add a dedicated error variant or rename appropriately

Option 1: Add a new error variant:

#[error("Failed to write config file '{path}': {source}")]
FailedToWriteConfigFile {
    path: String,
    #[source]
    source: std::io::Error,
},

Then update install_config_file:

-        fs::write(&config_path, config_content).map_err(|source| Error::FailedToWriteHook {
+        fs::write(&config_path, config_content).map_err(|source| Error::FailedToWriteConfigFile {
             path: config_path.to_string_lossy().to_string(),
             source,
         })?;

Option 2: Rename the existing variant to something generic like FailedToWriteFile.

🤖 Fix all issues with AI agents
In `@crates/git-smee-core/src/installer.rs`:
- Around line 243-247: The loop that scans the header bytes (the for loop
iterating over header.split(|byte| *byte == b'\n').take(8)) incorrectly uses
break when encountering an empty normalized_line, which can miss markers after a
shebang plus blank line; update the loop in installer.rs (the block handling
header/normalized_line) to skip empty lines instead of breaking (replace the
break with continue) or remove the empty-line check entirely so the subsequent
lines (like "# THIS FILE IS MANAGED BY git-smee") are still examined within the
take(8) window.
- Around line 229-256: Move the per-iteration format allocations out of
is_managed_file: compute the two marker byte sequences once before the loop
(e.g., create marker_hash = format!("# {MANAGED_FILE_MARKER}").into_bytes() and
marker_rem = format!("REM {MANAGED_FILE_MARKER}").into_bytes() or equivalent)
and then compare normalized_line to marker_hash.as_slice() and
marker_rem.as_slice() inside the loop; this keeps the detection logic the same
but avoids reallocating the format strings on every iteration.

Comment thread crates/git-smee-core/src/installer.rs
Comment thread crates/git-smee-core/src/installer.rs
@errfld

errfld commented Feb 4, 2026

Copy link
Copy Markdown
Owner Author

Addressed the outside-diff note in commit ceebac8 as well: install_config_file now maps write failures to a dedicated FailedToWriteConfigFile variant (instead of FailedToWriteHook), so config write errors are reported with accurate context.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@crates/git-smee-core/src/installer.rs`:
- Around line 2-15: The helper with_managed_header currently always prepends the
marker with a '#' which can end up above a script shebang; change it to accept a
comment prefix parameter (e.g., fn with_managed_header(content: &str,
comment_prefix: &str) -> String) so callers can choose the correct prefix for
their target file (or add an overload/variant for script-safe behavior), and
update all call sites to pass the appropriate prefix; also update the doc
comment on with_managed_header to state its intended use and mention that
callers should use a script-safe prefix (or not use the helper) for hook
scripts.

Comment thread crates/git-smee-core/src/installer.rs
@errfld

errfld commented Feb 4, 2026

Copy link
Copy Markdown
Owner Author

Addressed the remaining review request in commit 73fff27:

  • with_managed_header is now shebang-safe (marker inserted after a leading shebang)
  • added with_managed_header_with_prefix(content, comment_prefix) for explicit prefix selection
  • updated docs to clarify shebang-preserving behavior
  • added unit tests for shebang placement and custom prefix behavior

I also replied on the inline thread with details.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@crates/git-smee-core/src/installer.rs`:
- Around line 2-31: The current with_managed_header_with_prefix inserts the
marker before the shebang when content starts with "#!" but contains no newline;
update the logic in with_managed_header_with_prefix to detect starts_with("#!")
separately: if content starts_with("#!") then try to find the first '\n' and, if
found, split at shebang_end as now; otherwise (no newline) treat the entire
content as the shebang line and return a string composed of the shebang
(content), then a newline, then the marker_line, then two newlines (so the
shebang remains first and executable). Keep all other behavior unchanged for
non-shebang input and reference the function name
with_managed_header_with_prefix and the constant MANAGED_FILE_MARKER.

Comment thread crates/git-smee-core/src/installer.rs
@errfld

errfld commented Feb 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 4, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Comment thread crates/git-smee-core/src/installer.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant