Skip to content

feat: block git checkout/switch with --force and --discard-changes flags#36

Merged
kenryu42 merged 5 commits intomainfrom
feat/add-new-git-rules
Mar 21, 2026
Merged

feat: block git checkout/switch with --force and --discard-changes flags#36
kenryu42 merged 5 commits intomainfrom
feat/add-new-git-rules

Conversation

@kenryu42
Copy link
Owner

@kenryu42 kenryu42 commented Mar 20, 2026

Summary

  • Block git checkout --force / -f to prevent discarding uncommitted changes
  • Block git switch --discard-changes and git switch --force / -f
  • Fix short-option parsing in extractShortOpts to stop at options that consume a value (e.g., -cfeature no longer mis-identifies trailing chars as flags)

Changes

  • src/core/rules-git.ts — Added analyzeGitCheckout force-flag detection (--force, -f, combined -qf) with a dedicated hasCheckoutForceFlag() helper that correctly handles short-opt clusters. Added analyzeGitSwitch() to detect --discard-changes, --force, and -f. Added new REASON_* constants and CHECKOUT_SHORT_OPTS_WITH_VALUE / SWITCH_SHORT_OPTS_WITH_VALUE sets for accurate option parsing.
  • src/core/shell.ts — Extended extractShortOpts() to accept an optional { shortOptsWithValue } parameter; when a short option that consumes a value is encountered, parsing stops so the remainder is not misinterpreted as flags.
  • tests/core/rules-git.test.ts — Added 66 lines of new tests covering git checkout --force/-f/-qf, git switch --discard-changes/--force/-f/-qf/-cfeature, and edge cases (branch creation with force, --detach, double-dash, -C repo prefix).
  • tests/core/analyze/parsing-helpers.test.ts — Added tests for the new shortOptsWithValue behavior in extractShortOpts.
  • tests/bin/explain/command.test.ts — Added explain-command tests for git switch --discard-changes and git switch -f.
  • dist/ — Rebuilt dist bundles reflecting the above changes.

Testing

bun run check

Related Issue

None

PR Checklist

  • I have read the CONTRIBUTING.md
  • Code follows project conventions (type hints, naming, etc.)
  • bun run check passes (lint, types, dead code, rules, tests)
  • Tests added for new rules (minimum 90% coverage required)
  • Tested locally with Claude Code, OpenCode, Gemini CLI or GitHub Copilot CLI
  • Updated documentation if needed (README, AGENTS.md)
  • No version changes in package.json

Summary by CodeRabbit

  • New Features

    • Blocks additional destructive git invocations: git switch with --discard-changes, --force or -f; expanded git checkout force detection.
    • Improved parsing of combined short options so flags that take values are handled correctly.
  • Tests

    • Added unit and integration tests covering new git switch and short-option parsing behaviors.

Extend extractShortOpts with a shortOptsWithValue option so that
bundled chars after a value-consuming flag are treated as the flag's
argument rather than additional flags. Apply this to git switch -c/-C
so that `git switch -cfeature` is correctly allowed instead of being
misread as containing -f.
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a254d9cd-2ca2-4b5b-9647-7a5deb4378ad

📥 Commits

Reviewing files that changed from the base of the PR and between 7dcdf2b and 012132b.

⛔ Files ignored due to path filters (3)
  • dist/bin/cc-safety-net.js is excluded by !**/dist/**
  • dist/core/shell.d.ts is excluded by !**/dist/**
  • dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (4)
  • src/core/rules-git.ts
  • src/core/shell.ts
  • tests/core/analyze/parsing-helpers.test.ts
  • tests/core/rules-git.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/core/shell.ts
  • tests/core/analyze/parsing-helpers.test.ts
  • tests/core/rules-git.test.ts

📝 Walkthrough

Walkthrough

analyzeGit() now recognizes git switch and delegates to analyzeGitSwitch(). git checkout now detects --force/-f earlier. extractShortOpts() gained an options param to stop scanning when short options that take values are encountered.

Changes

Cohort / File(s) Summary
Core git command analysis
src/core/rules-git.ts
Added analyzeGitSwitch() and routing for the git switch subcommand. git checkout analysis now classifies --force/-f as destructive (REASON_CHECKOUT_FORCE). New reason codes: REASON_SWITCH_DISCARD_CHANGES, REASON_SWITCH_FORCE. Introduced CHECKOUT_SHORT_OPTS_WITH_VALUE and SWITCH_SHORT_OPTS_WITH_VALUE.
Shell utilities
src/core/shell.ts
Updated extractShortOpts() signature to accept readonly string[] and an optional options object with shortOptsWithValue (a ReadonlySet). Parsing of combined short options now stops when encountering a short option that takes a value.
Parsing utility tests
tests/core/analyze/parsing-helpers.test.ts
Added tests verifying extractShortOpts() respects shortOptsWithValue (terminates scanning after value-taking option) and accepts readonly string[] inputs.
Explain/integration tests
tests/bin/explain/command.test.ts
Added integration tests asserting explainCommand blocks git switch --discard-changes main and git switch -f main, with reasons referencing the corresponding switch semantics.
Git command tests
tests/core/rules-git.test.ts
Extended git checkout tests to cover --force/-f variants and delimiter cases; added a new git switch test suite covering blocked (--discard-changes, --force/-f) and allowed switch forms.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through tokens, short and long,
Found switches where the flags belong.
A force detected, a discard too,
I nibbled bugs till safety grew.
Hooray — no stray branch slips through! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main changes: blocking destructive git flags (--force/-f for checkout and --discard-changes/--force for switch).
Description check ✅ Passed The description includes a comprehensive summary, detailed changes across all modified files, testing instructions, and a complete checklist with all items marked as done.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-new-git-rules

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov
Copy link

codecov bot commented Mar 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.53%. Comparing base (7cf01eb) to head (012132b).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #36   +/-   ##
=======================================
  Coverage   99.53%   99.53%           
=======================================
  Files          50       50           
  Lines        5330     5365   +35     
=======================================
+ Hits         5305     5340   +35     
  Misses         25       25           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps
Copy link

greptile-apps bot commented Mar 20, 2026

Greptile Summary

This PR extends the safety net with two new protections — blocking git checkout --force/-f and all git switch --discard-changes/--force/-f variants — and fixes a pre-existing short-option parsing bug where characters trailing a value-consuming option (e.g. -cfeature) were incorrectly identified as additional flags.

Key changes:

  • extractShortOpts() in shell.ts gains an optional shortOptsWithValue parameter; when a value-consuming short option is encountered inside a cluster, parsing stops so the remaining characters are not mis-identified as flags
  • analyzeGitCheckout() now calls a new hasCheckoutForceFlag() helper (applied only to tokens before --) before any other checks, so force wins over branch-creation flags like -b/-B
  • New analyzeGitSwitch() dispatched from analyzeGit(); covers --discard-changes, --force, and -f short-opt clusters, correctly ignoring tokens after --
  • Three new REASON_* constants and two new short-option sets (CHECKOUT_SHORT_OPTS_WITH_VALUE, SWITCH_SHORT_OPTS_WITH_VALUE) added
  • 66 new test lines covering all new code paths plus edge cases (double-dash passthrough, combined clusters, git -C repo switch -f, --detach allowlist)

Minor concerns found:

  • CHECKOUT_SHORT_OPTS_WITH_VALUE / CHECKOUT_SHORT_OPTS_WITH_OPTIONAL_VALUE store bare single characters ('b', 't'), while SWITCH_SHORT_OPTS_WITH_VALUE stores dash-prefixed strings ('-c', '-C'). Both are functionally correct in their respective call-sites, but the format mismatch is a silent trap for future maintainers adding new options.
  • The optional-value guard in hasCheckoutForceFlag() that breaks on -t when not last in a cluster relies on POSIX optional-argument semantics; a brief explanatory comment would make the intent clear to future readers.

Confidence Score: 4/5

  • Safe to merge — all new detection logic is correct and well-tested; the two style observations are non-blocking.
  • The core logic is sound: force-flag detection is correctly scoped to pre--- tokens, the shortOptsWithValue stop-on-value fix prevents false positives, and the test suite covers the important edge cases (combined clusters, double-dash passthrough, git -C prefix). The only concerns are a naming-convention inconsistency between the two short-option sets and an undocumented reliance on POSIX optional-argument semantics — neither causes a runtime bug today.
  • src/core/rules-git.ts lines 57-59 (short-option set format inconsistency) and lines 244-247 (optional-value guard reasoning).

Important Files Changed

Filename Overview
src/core/rules-git.ts Adds force-flag detection for git checkout and a new analyzeGitSwitch() for git switch. Logic is sound; minor style inconsistency in how short-option sets are formatted (bare chars vs. dash-prefixed) could confuse future maintainers.
src/core/shell.ts Backward-compatible extension of extractShortOpts() with an optional shortOptsWithValue parameter that stops cluster parsing when a value-consuming option is encountered. Implementation is clean and correct.
tests/core/rules-git.test.ts Good coverage of the new rules: force-flag variants for git checkout (--force, -f, -qf, combined with -b), all git switch flags (--discard-changes, --force, -f, -qf), double-dash edge cases, and allowlisted paths (-cfeature, -Cfixup, --detach, -- -f).
tests/core/analyze/parsing-helpers.test.ts New tests cleanly verify the shortOptsWithValue stop-on-value behavior in extractShortOpts(), covering single leading opt, multiple leading opts, and capital-letter variant.
tests/bin/explain/command.test.ts Two new integration-level tests for git switch --discard-changes and git switch -f through the full explain-command path. Both are straightforward and correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["analyzeGit(tokens)"] --> B{subcommand?}
    B -->|checkout| C["analyzeGitCheckout(rest)"]
    B -->|switch| D["analyzeGitSwitch(rest)"]
    B -->|other| E["existing handlers …"]

    C --> C1["splitAtDoubleDash → before / after"]
    C1 --> C2{"hasCheckoutForceFlag(before)?"}
    C2 -->|yes| C3["🚫 REASON_CHECKOUT_FORCE"]
    C2 -->|no| C4{"-b / -B / --orphan in tokens?"}
    C4 -->|yes| C5["✅ null (allowed)"]
    C4 -->|no| C6["existing double-dash / ambiguous checks …"]

    D --> D1["splitAtDoubleDash → before"]
    D1 --> D2{"before includes\n'--discard-changes'?"}
    D2 -->|yes| D3["🚫 REASON_SWITCH_DISCARD_CHANGES"]
    D2 -->|no| D4["extractShortOpts(before,\n{shortOptsWithValue: SWITCH_SHORT_OPTS})"]
    D4 --> D5{"before includes '--force'\nor shortOpts has '-f'?"}
    D5 -->|yes| D6["🚫 REASON_SWITCH_FORCE"]
    D5 -->|no| D7["✅ null (allowed)"]

    subgraph extractShortOpts ["extractShortOpts (updated)"]
        S1["for each token"] --> S2{"starts with '-'\nnot '--' ?"}
        S2 -->|no| S1
        S2 -->|yes| S3["loop over chars"]
        S3 --> S4{"char in\nshortOptsWithValue?"}
        S4 -->|yes| S5["add opt → break cluster"]
        S4 -->|no| S6["add opt → continue"]
        S6 --> S3
    end
Loading

Last reviewed commit: "fix(git): stop short..."

Comment on lines +57 to +59
const CHECKOUT_SHORT_OPTS_WITH_VALUE = new Set(['b', 'B', 'U']);
const CHECKOUT_SHORT_OPTS_WITH_OPTIONAL_VALUE = new Set(['t']);
const SWITCH_SHORT_OPTS_WITH_VALUE = new Set(['-c', '-C']);
Copy link

Choose a reason for hiding this comment

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

P2 Inconsistent format between short-option sets

CHECKOUT_SHORT_OPTS_WITH_VALUE and CHECKOUT_SHORT_OPTS_WITH_OPTIONAL_VALUE store bare single characters (e.g. 'b', 't'), while SWITCH_SHORT_OPTS_WITH_VALUE stores dash-prefixed strings (e.g. '-c', '-C'). Both happen to be correct because they are consumed by different APIs:

  • The bare-char sets are compared against char (a single letter) inside hasCheckoutForceFlag()'s inner loop.
  • The dash-prefixed set is compared against the shortOpt string (-${char}) produced inside extractShortOpts().

A future maintainer adding a new option to either set could easily choose the wrong format and introduce a silent bug where the lookup never matches. Consider aligning the naming or adding an inline comment explaining the format difference, e.g.:

// Chars WITHOUT the leading '-': compared against individual chars inside hasCheckoutForceFlag()
const CHECKOUT_SHORT_OPTS_WITH_VALUE = new Set(['b', 'B', 'U']);
const CHECKOUT_SHORT_OPTS_WITH_OPTIONAL_VALUE = new Set(['t']);

// Strings WITH the leading '-': passed to extractShortOpts() which builds '-<char>' before lookup
const SWITCH_SHORT_OPTS_WITH_VALUE = new Set(['-c', '-C']);

Comment on lines +244 to +247
}
if (CHECKOUT_SHORT_OPTS_WITH_OPTIONAL_VALUE.has(char) && i < chars.length - 1) {
break;
}
Copy link

Choose a reason for hiding this comment

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

P2 Potential false-negative for git checkout -tf <branch>

The optional-value guard breaks out of the inner character loop whenever -t is not the last character in a cluster:

if (CHECKOUT_SHORT_OPTS_WITH_OPTIONAL_VALUE.has(char) && i < chars.length - 1) {
  break;
}

For the token -tf, t is at index 0 and chars.length - 1 is 1, so the condition is true and the loop breaks — f is never inspected.

However, in POSIX semantics, an optional argument must be attached directly to the option character in the same cluster. This means -t f (space-separated) gives -t with no optional value and then a positional f, whereas -tf would indeed consume f as -t's optional value. So git would not see a force flag in -tf, and the break is correct.

If git's internal parser ever diverges from strict POSIX for this option (it has done so for others), the break would silently miss git checkout -tf <branch> as a force invocation. Adding a comment here explaining the POSIX rationale would help future readers:

// -t takes an *optional* value; in POSIX, optional args must be attached
// in the same cluster, so chars that follow '-t' within the token are its
// value, not separate flags.
if (CHECKOUT_SHORT_OPTS_WITH_OPTIONAL_VALUE.has(char) && i < chars.length - 1) {
  break;
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/core/shell.ts (1)

793-796: Accept a readonly token array here.

extractShortOpts() never mutates tokens, but the mutable signature forces callers like Line 264 in src/core/rules-git.ts to allocate [...before] just to satisfy typing. readonly string[] removes that copy and better matches the rest of the parser APIs.

As per coding guidelines, "Use readonly arrays where mutation isn't needed in TypeScript."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/shell.ts` around lines 793 - 796, Change the extractShortOpts
signature to accept a readonly token array (e.g., tokens: readonly string[] or
ReadonlyArray<string>) since the function does not mutate tokens; update the
function declaration export function extractShortOpts(tokens: readonly string[],
options?: { readonly shortOptsWithValue?: ReadonlySet<string> }): Set<string>
and ensure any callers that currently do a defensive copy (e.g., using
[...before]) can pass their readonly arrays directly; verify there are no
internal mutations of tokens inside extractShortOpts and adjust any related type
annotations to use readonly arrays for consistency with other parser APIs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/rules-git.ts`:
- Line 58: Remove the 't' entry from the CHECKOUT_SHORT_OPTS_WITH_OPTIONAL_VALUE
Set so short-option clusters like "-tf" are parsed to reach the 'f' force flag;
then run/update the hasCheckoutForceFlag logic to ensure it now detects combined
force flags in clusters, and add a regression test in the rules-git test suite
that asserts detection for a command like "git checkout -tf main".

---

Nitpick comments:
In `@src/core/shell.ts`:
- Around line 793-796: Change the extractShortOpts signature to accept a
readonly token array (e.g., tokens: readonly string[] or ReadonlyArray<string>)
since the function does not mutate tokens; update the function declaration
export function extractShortOpts(tokens: readonly string[], options?: { readonly
shortOptsWithValue?: ReadonlySet<string> }): Set<string> and ensure any callers
that currently do a defensive copy (e.g., using [...before]) can pass their
readonly arrays directly; verify there are no internal mutations of tokens
inside extractShortOpts and adjust any related type annotations to use readonly
arrays for consistency with other parser APIs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c733981e-db83-4159-ac22-e13865266677

📥 Commits

Reviewing files that changed from the base of the PR and between 7cf01eb and 7dcdf2b.

⛔ Files ignored due to path filters (3)
  • dist/bin/cc-safety-net.js is excluded by !**/dist/**
  • dist/core/shell.d.ts is excluded by !**/dist/**
  • dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (5)
  • src/core/rules-git.ts
  • src/core/shell.ts
  • tests/bin/explain/command.test.ts
  • tests/core/analyze/parsing-helpers.test.ts
  • tests/core/rules-git.test.ts

@kenryu42 kenryu42 changed the title feat: add new git rules feat: block git checkout/switch with --force and --discard-changes flags Mar 20, 2026
…as force

CHECKOUT_SHORT_OPTS_WITH_OPTIONAL_VALUE treated 't' as consuming the next
character in combined short opts, causing -tf to not be detected as --force.
Also accept readonly arrays in extractShortOpts to avoid unnecessary spreads.
…LUE and replace hasCheckoutForceFlag with extractShortOpts
@kenryu42 kenryu42 merged commit 10a79b3 into main Mar 21, 2026
8 checks passed
@kenryu42 kenryu42 deleted the feat/add-new-git-rules branch March 21, 2026 04:39
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