Skip to content

fix: maxGroups cap violation in groupFilesHeuristic + surface real errors in validateBranchName - #218

Open
bh462007 wants to merge 1 commit into
gunjanghate:mainfrom
bh462007:fix/validate-branch-name-error-handling
Open

fix: maxGroups cap violation in groupFilesHeuristic + surface real errors in validateBranchName#218
bh462007 wants to merge 1 commit into
gunjanghate:mainfrom
bh462007:fix/validate-branch-name-error-handling

Conversation

@bh462007

@bh462007 bh462007 commented Jul 5, 2026

Copy link
Copy Markdown

Fixes #217

Summary

This PR addresses the two issues discussed in #217.

1. groupFilesHeuristic() (cli/helpers/splitLogic.js)

Previously, groupFilesHeuristic() could exceed the requested maxGroups limit when non-source categories (docs/tests/config/styles) had already consumed the available group budget.

The root cause was the interaction between remainingSlots reaching 0 and negative array slicing (slice(-1) / slice(0, -1)), which caused the function to create new source groups even though no slots remained. As a result, requesting maxGroups: 3 could still produce 6 groups.

Fix

  • When no group budget remains, any leftover source files are merged into the last existing group instead of creating new ones.
  • This guarantees that groups.length never exceeds maxGroups.
  • Ensures no files are dropped during grouping.

2. validateBranchName() (cli/helpers/safeBranchOps.js)

Previously, validateBranchName() caught every error thrown by git show-ref and treated it as "branch name is available."

While this worked for the expected "reference not found" case, it also masked genuine failures such as:

  • Running outside a Git repository
  • Git not being installed or not being available in PATH
  • Permission or other Git execution errors

This could cause the function to incorrectly return true and continue execution despite an actual failure.

Fix

  • Distinguish Git's expected "reference not found" case from real execution failures.
  • Return true only when the branch genuinely does not exist.
  • Re-throw all other errors with a descriptive message so callers can handle them appropriately.

Testing

Added tests

cli/tests/splitLogic.test.js

  • Verifies groups.length <= maxGroups even when the group budget is exhausted before source files are processed.
  • Confirms that no files are dropped while merging leftover source files.

cli/tests/safeBranchOps.test.js

  • Covers invalid input.
  • Covers invalid branch names.
  • Verifies an available branch name in a real Git repository.
  • Verifies an existing branch name.

Verification

Summary by CodeRabbit

  • Bug Fixes

    • Improved branch-name validation so unexpected Git errors are surfaced instead of being treated as a valid branch state.
    • Refined file-grouping behavior to stay within the configured group limit, even when source-file slots run out.
  • Tests

    • Added coverage for branch validation edge cases and branch existence checks.
    • Added coverage for grouping logic to verify all files are included and group limits are respected.

Copilot AI review requested due to automatic review settings July 5, 2026 05:01
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

@bh462007 is attempting to deploy a commit to the Gunjan Ghate's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR modifies error handling in validateBranchName to distinguish "not a valid ref" errors from other git failures, adjusts group slot allocation logic in groupFilesHeuristic to prevent exceeding maxGroups, and adds standalone test scripts for both functions.

Changes

Branch Name Validation Fix

Layer / File(s) Summary
validateBranchName error handling
cli/helpers/safeBranchOps.js
Adds JSDoc and updates catch logic to check error.stderr for "not a valid ref" before returning true; throws for other errors instead of always assuming the branch is available.
Branch validation tests
cli/tests/safeBranchOps.test.js
New test script with assert/assertThrows helpers validating invalid inputs, invalid characters, unique branch availability, and existing main branch detection.

Estimated code review effort: 2 (Simple) | ~15 minutes

Split Grouping Heuristic Fix

Layer / File(s) Summary
groupFilesHeuristic slot allocation
cli/helpers/splitLogic.js
Folds source files into the last group when remainingSlots <= 0 and broadens the single-group condition to remainingSlots <= 1 to avoid exceeding maxGroups.
Grouping heuristic tests
cli/tests/splitLogic.test.js
New test script asserting group count stays within maxGroups and all input files are represented in the output.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

  • gunjanghate/GitGenie#29: Introduced the safeBranchOps module and validateBranchName function that this PR modifies.
  • gunjanghate/GitGenie#99: Refactored the split command into cli/commands/split.js, which relies on the groupFilesHeuristic logic changed here.

Suggested labels: ECWoC26, Medium

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately captures both main code changes in the pull request.
Description check ✅ Passed The description covers the issue link, summary, and testing, though several template sections are left unfilled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

cli/helpers/safeBranchOps.js

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

cli/helpers/splitLogic.js

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

cli/tests/safeBranchOps.test.js

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 1 others

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.

Warning

⚠️ This pull request shows signs of AI-generated slop (redundant_comments). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
cli/helpers/splitLogic.js (1)

176-268: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

maxGroups can still be exceeded by non-source categories

docs/tests/config/styles are pushed unconditionally before the source-file budget check runs, so groups.length can already exceed maxGroups when those categories alone are present. If the contract is “never exceed maxGroups,” this needs the same cap/merge logic outside the source block too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/helpers/splitLogic.js` around lines 176 - 268, The grouping logic in
splitLogic.js can still exceed maxGroups because the docs/tests/config/styles
branches push groups before the source budget check runs. Update the
group-building flow in the same function that assembles groups so all
categories, not just source files, are capped or merged against maxGroups. Reuse
the existing grouping behavior around filesByCategory and groups to either stop
adding new groups when the limit is reached or fold extra non-source files into
an existing group.
🧹 Nitpick comments (2)
cli/tests/splitLogic.test.js (1)

16-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a case for remainingSlots === 1.

Current test only exercises the remainingSlots <= 0 fold branch (line 229 in splitLogic.js). The broadened remainingSlots <= 1 condition (previously likely === 1) isn't directly exercised by a scenario where remainingSlots equals exactly 1 with multiple source directories.

Example additional case
// Test: remainingSlots === 1 with multiple source dirs still merges into one group
const filesData2 = {
  files: [
    { path: 'README.md' },
    { path: 'test/foo.test.js' },
    { path: 'package.json' },
    { path: 'a/one.js' },
    { path: 'b/two.js' },
  ]
};
const groups2 = groupFilesHeuristic(filesData2, 4);
assert(groups2.length <= 4, `groups2.length (${groups2.length}) should be <= maxGroups (4)`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/tests/splitLogic.test.js` around lines 16 - 32, Add a test in
splitLogic.test.js that specifically exercises the `remainingSlots === 1` path
in `groupFilesHeuristic`, since the current case only covers the `remainingSlots
<= 0` fold behavior. Create a second scenario with multiple source directories
and a `maxGroups` value that leaves exactly one slot after non-source categories
are accounted for, then assert the grouping still stays within the limit and the
files remain present. Use the existing `groupFilesHeuristic` helper and the
current test style so the new case clearly targets the `remainingSlots` branch
in splitLogic.js.
cli/tests/safeBranchOps.test.js (1)

39-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded 'main' assumes repo state.

This test assumes a local main branch always exists. If the checkout's default/current branch differs (e.g., master), this test fails for reasons unrelated to the code under test.

🔧 Use the current branch instead of a hardcoded name
-// Test 4: an existing branch (main) returns false
-const existsResult = await validateBranchName('main');
-assert(existsResult === false, 'existing branch "main" returns false');
+// Test 4: an existing branch (the current branch) returns false
+const { execa } = await import('execa');
+const { stdout: currentBranch } = await execa('git', ['symbolic-ref', '--short', 'HEAD']);
+const existsResult = await validateBranchName(currentBranch);
+assert(existsResult === false, `existing branch "${currentBranch}" returns false`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/tests/safeBranchOps.test.js` around lines 39 - 41, The safe branch
validation test is hardcoding the branch name and assuming `main` exists in
every checkout. Update the `validateBranchName` test case in
`safeBranchOps.test.js` to use the current repository branch (or another branch
discovered at runtime) instead of the fixed `'main'`, so the assertion remains
valid regardless of whether the default branch is `main` or `master`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/tests/safeBranchOps.test.js`:
- Around line 16-25: `assertThrows` in `safeBranchOps.test.js` is unused, so the
new `validateBranchName` throw path has no coverage. Add a test that actually
calls `assertThrows` against `validateBranchName` from
`cli/helpers/safeBranchOps.js`, ideally by switching to a temporary non-git
directory (or another real git failure case) and restoring `originalCwd`
afterward. Make sure the new test verifies that unexpected git failures are
surfaced as throws rather than swallowed.

---

Outside diff comments:
In `@cli/helpers/splitLogic.js`:
- Around line 176-268: The grouping logic in splitLogic.js can still exceed
maxGroups because the docs/tests/config/styles branches push groups before the
source budget check runs. Update the group-building flow in the same function
that assembles groups so all categories, not just source files, are capped or
merged against maxGroups. Reuse the existing grouping behavior around
filesByCategory and groups to either stop adding new groups when the limit is
reached or fold extra non-source files into an existing group.

---

Nitpick comments:
In `@cli/tests/safeBranchOps.test.js`:
- Around line 39-41: The safe branch validation test is hardcoding the branch
name and assuming `main` exists in every checkout. Update the
`validateBranchName` test case in `safeBranchOps.test.js` to use the current
repository branch (or another branch discovered at runtime) instead of the fixed
`'main'`, so the assertion remains valid regardless of whether the default
branch is `main` or `master`.

In `@cli/tests/splitLogic.test.js`:
- Around line 16-32: Add a test in splitLogic.test.js that specifically
exercises the `remainingSlots === 1` path in `groupFilesHeuristic`, since the
current case only covers the `remainingSlots <= 0` fold behavior. Create a
second scenario with multiple source directories and a `maxGroups` value that
leaves exactly one slot after non-source categories are accounted for, then
assert the grouping still stays within the limit and the files remain present.
Use the existing `groupFilesHeuristic` helper and the current test style so the
new case clearly targets the `remainingSlots` branch in splitLogic.js.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a86e1ff7-be77-4f1b-9efc-e4b19fc2be13

📥 Commits

Reviewing files that changed from the base of the PR and between 6442e3b and 6515899.

📒 Files selected for processing (4)
  • cli/helpers/safeBranchOps.js
  • cli/helpers/splitLogic.js
  • cli/tests/safeBranchOps.test.js
  • cli/tests/splitLogic.test.js

Comment on lines +16 to +25
async function assertThrows(fn, message) {
try {
await fn();
console.log(`❌ ${message} (expected throw, but none occurred)`);
failed++;
} catch (err) {
console.log(`✅ ${message}`);
passed++;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

New throw path is untested; assertThrows is dead code.

The PR's core change is that validateBranchName now throws on real git failures (not-a-repo, missing git, permissions), but assertThrows is defined and never invoked anywhere in this file. The new error-throwing behavior added in cli/helpers/safeBranchOps.js ships without coverage.

Consider exercising it, e.g., by running from a temp non-git directory:

// Test 5: unexpected git failures are surfaced, not swallowed
await assertThrows(async () => {
  const { execa } = await import('execa');
  const tmpDir = await import('node:fs/promises').then(fs => fs.mkdtemp('/tmp/not-a-repo-'));
  process.chdir(tmpDir);
  try {
    await validateBranchName('some-branch');
  } finally {
    process.chdir(originalCwd);
  }
}, 'non-repo directory causes validateBranchName to throw');

Want me to draft this test and open a follow-up?

Also applies to: 35-41

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/tests/safeBranchOps.test.js` around lines 16 - 25, `assertThrows` in
`safeBranchOps.test.js` is unused, so the new `validateBranchName` throw path
has no coverage. Add a test that actually calls `assertThrows` against
`validateBranchName` from `cli/helpers/safeBranchOps.js`, ideally by switching
to a temporary non-git directory (or another real git failure case) and
restoring `originalCwd` afterward. Make sure the new test verifies that
unexpected git failures are surfaced as throws rather than swallowed.

Copilot AI 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.

Pull request overview

This PR targets two CLI correctness gaps: enforcing the maxGroups cap in heuristic file grouping (used by gg split fallback behavior) and making validateBranchName() surface real Git execution failures instead of silently treating all errors as “branch available”.

Changes:

  • Updates groupFilesHeuristic() to avoid exceeding maxGroups when non-source categories already consume the group budget.
  • Updates validateBranchName() to distinguish “ref not found” from real Git failures and rethrow unexpected errors.
  • Adds new unit-test scripts for both behaviors.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
cli/helpers/splitLogic.js Adjusts source grouping logic when the group budget is exhausted.
cli/helpers/safeBranchOps.js Changes error-handling behavior in validateBranchName() and adds docs around it.
cli/tests/splitLogic.test.js Adds a test for maxGroups cap + “no files dropped” behavior.
cli/tests/safeBranchOps.test.js Adds tests for invalid input/characters and “available vs existing” behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cli/helpers/splitLogic.js
const allDirs = Object.keys(sourceByDir);

if (allDirs.length === 1 || remainingSlots === 1) {
if (remainingSlots <= 0 && groups.length > 0) {
}
}

/**
Comment on lines +139 to +144
// git's normal "ref not found" message contains this specific phrase
if (stderr.includes('not a valid ref')) {
return true; // genuinely available — ref simply doesn't exist
}
// anything else (not a repo, git missing, permissions, etc.) is a real failure
throw new Error(`Could not validate branch name: ${stderr || error.message}`);
Comment on lines +35 to +41
// Test 3: a genuinely available name inside this real repo returns true
const available = await validateBranchName('totally-new-unique-branch-name-xyz-123');
assert(available === true, 'available branch name in a real git repo returns true');

// Test 4: an existing branch (main) returns false
const existsResult = await validateBranchName('main');
assert(existsResult === false, 'existing branch "main" returns false');
@@ -0,0 +1,38 @@
import { groupFilesHeuristic } from '../helpers/splitLogic.js';
@@ -0,0 +1,47 @@
import { validateBranchName } from '../helpers/safeBranchOps.js';
@gunjanghate

gunjanghate commented Jul 5, 2026

Copy link
Copy Markdown
Owner

@bh462007 please add demo or screenshot of working or fixed issue

@bh462007

bh462007 commented Jul 6, 2026

Copy link
Copy Markdown
Author

@gunjanghate Sure! Here's a quick demo showing both fixes working with the exact reproduction steps mentioned in the issues.

validateBranchName fix:
Running the "outside a Git repository" scenario now correctly throws an error instead of silently returning true:

Error: Could not validate branch name: fatal: not a git repository (or any of the parent directories): .git

groupFilesHeuristic fix:
Using the same reproduction case from the issue (maxGroups: 3 with docs/tests/config already consuming the available slots) now correctly returns 3 groups instead of 6.

I also added dedicated test suites for both fixes (safeBranchOps.test.js and splitLogic.test.js). All 8 new tests pass, and the existing 39 tests continue to pass as well, so there are no regressions.
Screenshot 2026-07-06 224132
Screenshot 2026-07-06 224124
Screenshot 2026-07-06 224111

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.

[bug]: groupFilesHeuristic() ignores maxGroups when category groups already consume all available slots

3 participants