Skip to content

About

A git plugin (`git splice`) that slices your messy working-tree diff into a stack of minimal, independently-applicable commits using hunk-adjacency and shared-identifier analysis, then shows the…

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

git-splice

A git plugin that slices your messy working-tree diff into a stack of minimal, independently-applicable commits. It looks at hunk adjacency and shared identifiers across your changes to figure out which edits actually belong together, then (in later milestones) shows the proposed stack as an interactive ASCII dependency graph before writing anything.

Install

Requires Python 3.9+. No third-party dependencies.

pip install -e .

This installs the git-splice command (invokable as git splice once it is on your PATH, since git dispatches git <subcommand> to any git-<subcommand> executable it finds there).

Usage

Run it inside a git repository with a working-tree diff:

git-splice

This runs git diff, parses it into hunks, builds a graph linking hunks that are either adjacent in the same file or share a changed identifier, then greedily clusters connected hunks into independent patch sets and prints the resulting stack:

2 file(s), 3 hunk(s), 2 patch set(s)
patch set 1:
  hunk#0 foo.py @@ -1,4 +1,5 @@
  hunk#1 foo.py @@ -10,3 +11,3 @@
patch set 2:
  hunk#2 bar.py @@ -1,1 +1,2 @@

The clustering is deterministic: hunks are visited in a canonical order (file path, then position in the file, then hunk id) so the same diff always produces the same patch sets, in the same order, regardless of dict/set iteration order. See git_splice/clustering.py and tests/test_clustering.py for the golden fixtures that pin this down.

Pass --cached to inspect the staged diff instead, or one or more paths to limit the diff (both are forwarded to git diff):

git-splice --cached src/

Each patch set is a group of hunks that the shared-identifier/adjacency analysis believes must travel together. Pass --graph to see the same patch sets rendered as an ASCII boxes-and-arrows dependency graph instead of the flat hunk listing:

git-splice --graph
+-------------------+
| patch set 1       |
| files: a.py, b.py |
| hunks: 3          |
+-------------------+
          |
          v
+-------------------+
| patch set 2       |
| files: c.py       |
| hunks: 1          |
+-------------------+

Patch sets are otherwise independent, but two of them can still touch the same file at different positions; applying them out of order would shift the context each hunk expects. The graph captures that as an ordering dependency: an arrow points from the patch set that touches a shared file earlier to the one that touches it later, labeled with the file that creates the dependency. If two patch sets disagree about ordering across different shared files (a cycle), the graph still renders in a deterministic index-based order and prints a note listing the conflict instead of failing. See git_splice/stack_graph.py and git_splice/render.py for the implementation, and tests/test_stack_graph.py / tests/test_render.py for the fixtures that pin the behavior down.

Before any commit gets written, you can review and rearrange the stack by hand. Pass --interactive (or -i) to open an arrow-key terminal UI over the same patch sets:

git-splice --interactive
[up/down: move, m: merge with next, enter/q: confirm]

> patch set 1 (2 hunk(s)) -- a.py, b.py
  patch set 2 (1 hunk(s)) -- c.py

Up/down moves the selected patch set, swapping it with its neighbor; m folds the patch set below the selection into the selected one (hunks from both are re-sorted into canonical file/position order so the merged result is independent of which side they came from); Enter or q confirms the stack as currently arranged and prints it. Nothing is written to the repository by this command -- it only decides the order and grouping that a later milestone will use when creating commits. The editing rules themselves (git_splice/interactive.py's ReorderState and handle_key) are plain, terminal-free Python so they are covered by tests/test_interactive.py without needing a real TTY; only the thin curses loop that reads actual keypresses requires one, so --interactive needs to run attached to a real terminal.

Once you're happy with the stack, pass --apply to write it as a real chain of commits on top of HEAD, one commit per patch set, in the same dependency order the graph displays:

git-splice --apply
Wrote 2 commit(s):
  538b13f18a splice: patch set 1 (f1.py)
  8512c30608 splice: patch set 2 (f2.py)

--apply never edits your working-tree files directly. Each commit's tree is built with plain git plumbing (hash-object, update-index, write-tree, commit-tree) against a throwaway index file, starting from HEAD's content and layering on just the hunks committed so far for each touched file -- the same reconstruction either produces a file's fully-diffed content (once every one of its hunks has landed) or an intermediate state (while only some have). Only after every commit in the chain exists does it move the branch (or bare HEAD, if detached) forward and refresh the real index to match, via git reset --mixed, which also never touches working-tree files -- so the files on disk end up exactly where they already were, just now described by real history instead of one uncommitted diff. Combine with --interactive to pick the final order and groupings before they are committed:

git-splice --interactive --apply

As a safety check, plain --apply (without --cached) refuses to run if the index already has staged changes beyond HEAD, since those would otherwise be silently folded into the first commit; commit or unstage them first. See git_splice/apply.py for the reconstruction logic (unit-tested without a real repository in tests/test_apply.py) and the git-plumbing driver (exercised against a real throwaway repository, with exact commit-count/message/content assertions, in tests/test_apply_integration.py).

Status

This project is built autonomously, milestone by milestone, and every change is gated on its test suite passing before it is kept. Run the tests with:

python -m unittest discover -s tests

About

A git plugin (`git splice`) that slices your messy working-tree diff into a stack of minimal, independently-applicable commits using hunk-adjacency and shared-identifier analysis, then shows the…

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages