feature: add Split-ByPath — range-aware path extraction into a separate PR - #23
Open
dkattan wants to merge 1 commit into
Open
feature: add Split-ByPath — range-aware path extraction into a separate PR#23dkattan wants to merge 1 commit into
dkattan wants to merge 1 commit into
Conversation
…te PR Split-ByPath extracts the net change to a set of paths across a BaseRef..HEAD range into a new destination branch, and (destructive by default, mirroring `mv`) rewrites the current branch to drop those paths' changes. It is the range-aware counterpart to Split-Commit / Move-Commit, which both operate on a single commit. Two modes: - -Squash (default): pure git commit-from-index. `git reset --soft BaseRef` stages the whole range; commit the remainder, then stage + commit the extract paths. Handles adds, modifies, and deletions uniformly (no `git rm` special case). Source collapses to one squashed commit. - -Squash:$false (preserve): git-filter-repo --invert-paths in a temp clone, fetched back. Keeps the source's commit structure with the paths excised. Destination defaults to stacked on the rewritten source tip (PR diff shows only the extracted paths); -DestinationBase or copy mode gives a flat sibling. The builder (New-SplitByPathPlan) emits a reviewable pure-git plan via the existing plan/execute model, so -OutputScriptPath yields an auditable script and the destructive-by-default behavior is documented inline for review bots. Design: docs/design-split-bypath.md. Tests: 15 new Pester cases (adds, modifies, deletions, multi-path, empty-source, stacked/flat, explicit destination base, script output, AutoStash, preserve destructive/copy) — full suite green (258). Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
There was a problem hiding this comment.
Pull request overview
Adds range-aware path extraction to GitSplit, creating a destination branch while optionally rewriting the source branch.
Changes:
- Adds squash and history-preserving extraction modes.
- Exports the new
Split-ByPathcmdlet. - Adds Pester coverage and design documentation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
GitSplit.psm1 |
Implements planning and execution. |
GitSplit.psd1 |
Exports the cmdlet. |
GitSplit.Tests.ps1 |
Adds behavioral tests. |
docs/design-split-bypath.md |
Documents the design and semantics. |
Suppressed comments (2)
GitSplit.psm1:4979
- Preserve mode is not range-aware here: without a
--refs/range restriction,filter-reporemoves these paths from every ancestor, includingBaseRef. A file that existed at the base therefore disappears from the rewritten source instead of reverting to its base version; for a deletion atHEAD, the subsequent stacked destination commit has no path change at all and fails as empty. Restrict the rewrite toBaseRef..$expectedBranch(or replay only that range) while retaining the original base as the parent.
' $filterArgs = @("--force", "--invert-paths", "--prune-empty=$pruneArg")'
' foreach ($p in $paths) { $filterArgs += @("--path", $p) }'
' & git -C $clonePath filter-repo @filterArgs 2>&1 | ForEach-Object { $_ | Out-String | Write-Host }'
GitSplit.psm1:5004
- The preserve-mode destination repeats the explicit-base bug from squash mode: resetting to an ancestor and then staging the working tree records the full
DestinationBase..HEADpath state, rather than only the requestedBaseRef..HEADnet change. Apply the frozen range delta to$destBaseinstead of copying theHEADsnapshot.
' & git @longPathGitArgs -C $worktreePath -c "core.hooksPath=$disabledHooksPath" reset --soft $destBase 2>&1 | ForEach-Object { $_ | Out-String | Write-Host }'
' if ($LASTEXITCODE -ne 0) { throw "git reset --soft (dest) failed" }'
' & git @longPathGitArgs -C $worktreePath add -A -- @paths 2>&1 | ForEach-Object { $_ | Out-String | Write-Host }'
' if ($LASTEXITCODE -ne 0) { throw "git add -A -- <paths> (dest) failed" }'
' & git @longPathGitArgs -C $worktreePath -c "core.hooksPath=$disabledHooksPath" commit -m $destinationMessage --quiet -- @paths 2>&1 | ForEach-Object { $_ | Out-String | Write-Host }'
Comment on lines
+5057
to
+5060
| ' Write-Host "Split-ByPath complete."' | ||
| ' Write-Host " source: $expectedBranch -> $(if ($removeFromSource) { $sourceTip } else { $expectedHead + '' (unchanged)'' })"' | ||
| ' Write-Host " destination: $destinationBranch -> $destSha"' | ||
| ' Write-Host " (History rewrite changes commit SHAs; old SHAs cited in review threads may become dangling on GitHub after force-push.)"' |
Comment on lines
+4952
to
+4956
| ' & git @longPathGitArgs -C $worktreePath -c "core.hooksPath=$disabledHooksPath" reset --soft $destBase 2>&1 | ForEach-Object { $_ | Out-String | Write-Host }' | ||
| ' if ($LASTEXITCODE -ne 0) { throw "git reset --soft (flat) failed" }' | ||
| ' & git @longPathGitArgs -C $worktreePath add -A -- @paths 2>&1 | ForEach-Object { $_ | Out-String | Write-Host }' | ||
| ' if ($LASTEXITCODE -ne 0) { throw "git add -A -- <paths> (flat) failed" }' | ||
| ' & git @longPathGitArgs -C $worktreePath -c "core.hooksPath=$disabledHooksPath" commit -m $destinationMessage --quiet -- @paths 2>&1 | ForEach-Object { $_ | Out-String | Write-Host }' |
| -Path <string[]> # paths to extract (repo-relative; matched at current HEAD names) | ||
| -DestinationBranch <string> # mandatory | ||
| [-BaseRef <string>] # default: merge-base(HEAD, origin/HEAD) | ||
| [-CreateDestinationBranch] # create dest; requires DestinationBase (or default) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Split-ByPathis the range-aware counterpart toSplit-Commit/Move-Commit. Every existing GitSplit verb operates on a single commit; none can express "one file's changes are interleaved across many commits in a branch, and I want them as a single separate PR."Split-ByPathdoes exactly that: it extracts the net change to a set of paths acrossBaseRef..HEADinto a new destination branch, and (destructive by default, mirroringmv) rewrites the current branch to drop those paths' changes.This came up concretely when splitting a workflow file out of a 21-commit driver branch — the resolution was done by hand with
git reset --soft <base>+git restore --staged <paths>. This PR captures that as a first-class verb.📋 Full design & rationale:
docs/design-split-bypath.mdModes
-Squash(default) — pure git, commit-from-index.git reset --soft BaseRefstages the entire range; commit the remainder, then stage + commit the extract paths. Handles adds, modifies, and deletions uniformly — nogit rmspecial-case. The source collapses to one squashed commit.-Squash:$false(preserve) —git filter-repo --invert-pathsin a temp--local --no-hardlinksclone, fetched back. Keeps the source's commit structure with the paths excised from each commit. Requiresgit-filter-repo.Destination placement
-DestinationBase <ref>, or copy mode): destination parented on-DestinationBase(orBaseRef) as an independent sibling.Design decisions (per maintainer)
-TipRef— the tip is always the current branch HEAD. A free tip would make the destructive default ambiguous about which branch it destroys.-RemoveFromSourcedefaults$true), likemv; opt out with-RemoveFromSource:$false. Documented inline for review bots.-Squashdefaults$true(commit-from-index pure git); preserve mode is the opt-in.New-SplitByPathPlan) emits a reviewable pure-git plan via the existing plan/execute model —-OutputScriptPathyields an auditable script, so the destructive behavior is reviewable before execution.Examples
Tests
15 new Pester cases covering: detached-HEAD throw, BaseRef-not-ancestor, destination-exists, no-path-changes, BaseRef==HEAD, stacked destructive squash, flat copy squash, explicit destination base, deletion extraction, multi-path (empty-source collapse), reviewable script output, AutoStash, dirty-tree refusal, and preserve mode (destructive + copy). Full suite: 258 passing, PSScriptAnalyzer clean against the two CI rules.
Generated with Claude Code
via Happy
Co-Authored-By: Claude noreply@anthropic.com
Co-Authored-By: Happy yesreply@happy.engineering