Skip to main content

git-retell

PyPI Changelog Tests License

Synthetic Git histories for code review

Explain a real code transformation B → H with a synthetic Git history B → S1 → S2 → … → H. Each commit is a slide; its message explains its diff. An agent or human authors the steps. This CLI supplies the authoring worktree, validation, metrics, optional per-step tests, and terminal and browser slideshows. No LLM API required.

The endpoints must be exact. Intermediate implementations can be temporary, change the same lines repeatedly, or fail tests. They are explanations, not a claim about how the code was originally developed.

Installation

Install git-retell with uv:

uv tool install git-retell

You can also install it with pipx or pip:

pipx install git-retell
pip install git-retell

Requires Git.

Usage

For help, run:

git-retell --help

Example workflow

After installation, run git-retell in the repository you want to explain:

git-retell --help

# B and H are commits; see below for uncommitted work.
git-retell start demo --from HEAD~1 --to HEAD \
  --worktree /tmp/retell-demo --budget 40

# In /tmp/retell-demo, use ordinary Git to author the explanation:
# edit files, git add ..., git commit -m 'Explain why this step exists'
# Repeat, amend, or rebase as needed. Finish at exactly H's tree.

# Review the retelling using the installed CLI:
git-retell validate demo --json
git-retell finish demo   # validate, then remove /tmp/retell-demo
git-retell show demo --step 1
git-retell view demo
git-retell web demo
git-retell test demo --timeout 60 -- pytest
git-retell resume demo --worktree /tmp/retell-demo   # revise it later
git-retell list

Installing the CLI makes git-retell available while working in any repository. All commands explain their options with --help.

Partial retellings and building from scratch

start --partial lets a retelling leave out some of the files that differ between B and H, such as lock files, generated code, or tests. Each file is all or nothing: a file the history changes must end at exactly its target version, and every other file must stay exactly as in B. There is no list of excluded files to maintain; the omitted files are whatever still differs from the target, so which files to leave out is up to the author (or the agent's prompt). configure NAME --partial or --no-partial changes the setting later.

Pass --from-scratch instead of --from to build the files from nothing. start then anchors the retelling at a new parentless commit of the empty tree, stored at the usual base ref. This pairs well with --partial: retell a few core files from scratch and leave the rest of the repository out.

git-retell start core --from main --to feature --worktree /tmp/core --partial
git-retell start fresh --from-scratch --to HEAD --worktree /tmp/fresh --partial

Retelling uncommitted work

Without any --to option, the target is HEAD; if you have uncommitted changes, start notes that they are left out. The viewers and test use the same default and note.

The target does not have to be committed. --to-uncommitted takes everything git status shows: staged, unstaged, and untracked files that are not ignored. --to-staged takes only the index. Either one commits a synthetic snapshot of those files on top of HEAD and pins it as the target; --from then defaults to HEAD. Your checkout, index, and HEAD are left untouched, and start lists any untracked files the snapshot captured so a stray file is easy to spot (or leave out with --partial).

git-retell start wip --to-uncommitted --worktree /tmp/wip
git-retell start wip --to-staged --worktree /tmp/wip

The snapshot is frozen, so you can keep editing while the retelling is written. To pick up later edits, configure NAME --to-uncommitted takes a new snapshot and pins it as the target; --to REV re-pins a commit, for example after rebasing the real change. The history is then validated against the new target, and the base stays where it was.

What is validated?

  • The branch is anchored at the exact pinned base commit, hence its exact tree.
  • Every subsequent commit has exactly one parent: the preceding step.
  • The final tree ID equals the pinned target tree ID. File contents, names, modes, symlinks, and submodule pointers all participate in tree identity. For a partial retelling, no path may differ both between the base and the final tree and between the final tree and the target: every file is either retold exactly or left exactly as in the base.
  • Each complete rendered diff has at most the configured number of lines.

validate exits 0 only when all four hold; it exits 1 for an unfinished or invalid history. No test result or expansion threshold changes that verdict. Only committed states participate. Untracked files and working-tree edits are excluded. An empty history is valid only if the pinned endpoints already have equal trees.

The renderer uses ordinary Git unified diffs with configurable context (three lines by default), headers, containing-function text, and no rename detection. Binary changes use full Git binary patches. External diff tools and text conversions are disabled so no edits are replaced by a summary. The same renderer serves validation and viewing. Diff lines are counted by newline, including headers, context, blank lines, and "no newline" markers. No file, hunk, import, or supporting edit is hidden. Git attributes can still influence hunk headers and text/binary classification.

Metrics and the review frame

Churn means additions + deletions, using Git numstat without rename detection. Expansion is cumulative synthetic churn divided by real B→H churn (for a partial retelling, real churn of the retold files only). It is informational: 2× may be a better explanation than 1×. The ratio is undefined for zero real churn or any binary edits; text churn remains available. Mode-only changes still consume presentation lines even when their line churn is zero. Reports include each step's hash, subject, churn and diff lines, plus totals. A partial retelling's report also lists each omitted file with its real churn.

The budget covers the diff at the selected context setting. Messages, navigation, wrapping, and extra context can require more terminal space. The viewer pages through slides of any size, including on terminals smaller than the authoring budget. It labels invalid histories; paging does not waive budget validation.

Viewer controls:

  • n / right arrow and p / left arrow: next and previous step.
  • j / down arrow and k / up arrow: scroll one line.
  • f / PageDown and b / PageUp: page down and up.
  • Space: page down, then advance to the next step at the bottom.
  • g / G: top / bottom of the current slide.
  • + (or =) / -: add / remove three diff context lines, down to zero.
  • 0: reset to the initial context (the flag or saved setting). Context persists across steps.
  • r: redraw after resizing; q: quit.

Changing context returns to the top of the slide. The footer shows the step, context (U3, U6, etc.), and visible line range. All message and diff lines remain accessible, including wrapped lines. Context is a viewing preference; saved settings and code churn remain unchanged. The VALID/INVALID status is recomputed at the displayed context, so expanding context can exceed the budget. show NAME --all prints every slide for a pager or text export. Terminal controls are visibly escaped and tabs expanded.

Web viewer

git-retell web NAME writes a self-contained HTML slideshow and opens it in your browser. Use -o FILE to choose where it is written and --no-open to skip the browser. The page needs no server or network access, so the file can be shared as is. It is a snapshot: rerun the command after editing the history.

Transitions show how each step connects to its neighbors. Every line keeps one identity for as long as it is unchanged, using Git's own line matching, so the viewer can animate the difference between any two slides:

  • Lines the next slide no longer shows collapse; new lines expand into place.
  • Lines added in one step settle from green to plain context in the next; context lines the next step deletes fade to gray.
  • Moving to another part of the same file collapses the old hunk, glides the view, and expands the new hunk. Each file header has a small ruler showing which part of the file is visible and where it changed: additions are green ticks along the top half, deletions red ticks along the bottom.
  • Files that leave the slide swipe away; files that join it slide in. New files are flagged with a highlighted header.
  • Like Git's hunk-header function context, a hidden stretch above a hunk names its enclosing scopes, such as class History › def load(...), with the header's line number. Scopes come from indentation (definitions and other block openers, not control flow) or from Markdown heading levels, and update as you change context.

Line identities also show which code is temporary. Added lines that a later step removes get hatched line numbers and a label such as "rewritten in step 5" (a hunk replaced them) or "removed in step 5"; deleted lines that an earlier step added are muted and labeled "from step 2", so full-strength red and green mark the real B → H change. Click a line number or label to jump to the step that removes or added the line; for a line that survives, clicking shows it in place in the target with a few lines around it. The sidebar counts each kind of line. Identity follows Git's line matching within one path, so a line moved to another file or edited in place counts as temporary; its tooltip names where identical text appears in the target.

A seek bar charts each step's additions and deletions, with temporary lines drawn lighter and away from the axis. A fixed row marks the step where each file first appears in the retelling (◆), whether new or already in the base; file names label them where space allows. Hover for a step's subject and the files it introduces or deletes, and click or drag to jump.

To follow one file, each file header shows where the step falls among that file's changes, such as ‹ edit 2 of 4 ›; the arrows go to its previous and next change. Its last change reads "final edit" ("only edit" if one step changes it), and the sidebar marks it too. Together with ◆, this lets you read any file's journey from its first appearance to its final version in context, without filtering. Code is syntax highlighted with Pygments.

A partial retelling shows a Partial tag in the header; click it or the sidebar's "not in this retelling" section to list the files left as in the base, with their real line counts. A retelling built from scratch is tagged as well.

Web viewer controls: right/left arrow, n/p, or space: next and previous step. Home/End: first/last step. j/k: scroll. +/-: context, 0: reset, f: whole files. /: choose files. t: theme. ?: help and the line-lifetime legend. Esc: close a peek. The URL fragment remembers the step and file filter. The VALID/INVALID badge reflects the context given on the command line. Like view, live context changes in the page do not revalidate or change settings.

Filtering files while viewing

Viewers can hide files after the fact, for example to skip tests or to follow one file through the history. Filters change only what is shown: validation, budgets, and the VALID/INVALID status always cover every file.

In the web viewer, the ◎ button on a file's header shows only that file (press it again to show all). The Files panel (/) lists every file the history touches, with checkboxes, plus a match box: plain text matches anywhere in a path, * and ? match within a directory, and ** spans directories. Its Show and Hide buttons (or Enter and Shift+Enter) show or hide the matching files; Only shows them and hides everything else. Steps that change none of the shown files fade on the seek bar and are skipped: next/previous pass over them, and clicking one on the seek bar lands on the nearest shown step.

show, view, and web accept --include and --exclude with Git pathspecs, each repeatable. show --all and view skip steps that change none of the selected files; step numbers stay those of the full history. For web, the flags only choose the files shown at first. The page still embeds everything.

git-retell show demo --all --exclude tests/ --exclude '*.lock'
git-retell view demo --include src/parser.py
git-retell web demo --exclude tests/

Viewing real commits

show, view, web, and test also work on real commits, without a retelling. Pass --from REV instead of a name to step through the commits on the current branch since REV, each against the commit before it:

git-retell web --from main
git-retell view --from v1.2 --to v1.3
git-retell show --from main --to-uncommitted --all
git-retell test --from main -- pytest

The commits are the first-parent chain reachable from --to (default HEAD) but not from --from, like git log --first-parent main..HEAD. A branch therefore starts where it forked even after main moves on, and a merge of main into the branch is one slide showing everything the merge brought in. --from-scratch goes back to the first commit. --to-uncommitted or --to-staged adds a final slide with a snapshot of your work in progress.

Real commits have no budget or validation, so the viewers drop the VALID status, and slides show each commit's author and date. Everything else works as for a retelling, including filters, context, and line lifetimes, which here show code that a later commit rewrote or removed. Pages for very long histories can get large, since they embed every version of every file.

Configuration

start --budget N --context N saves defaults for a retelling. The defaults are 60 presentation lines per step and 3 unchanged context lines on each side of a hunk. Zero context is allowed. Context can change hunk grouping and presentation size; it does not change tree identity or code churn.

git-retell start demo --from main --to feature --worktree /tmp/demo --context 6
git-retell validate demo --context 0 --budget 40 --json
git-retell show demo --step 2 --context 12
git-retell view demo --context 12

validate, show, and view use saved context unless you pass --context. Overrides apply only to that invocation. The validation report includes the context used for every presentation-size metric. test runs code and does not need a diff context setting. Retellings without a saved setting use its default. To print or update saved defaults later, use git-retell configure demo or git-retell configure demo --budget 80 --context 6.

Finishing, resuming, listing, and deleting retellings

git-retell finish demo
git-retell resume demo --worktree /tmp/demo
git-retell list
git-retell list --json
git-retell delete demo

The authoring worktree is only needed while writing steps; every other command reads the retelling's refs. finish validates with the saved settings and, only if the retelling is valid, removes its authoring worktree while keeping the synthetic branch. An invalid retelling keeps its worktree and exits 1. resume checks the branch out in a new worktree so you can amend, rebase, or add steps, then finish again.

list shows names, pinned endpoints, tips, step counts, saved settings, and attached worktrees. It includes unfinished and incomplete entries; use validate for the full correctness and budget report.

delete removes the named synthetic branch, endpoint refs, saved settings, and authoring worktree, leaving real development branches intact. finish and delete refuse current or locked worktrees and those with uncommitted changes or untracked files; a refused delete changes nothing. Ignored files, such as build output, are removed with the worktree. If a worktree directory was already deleted (for example by the system's /tmp cleanup), finish, resume, and delete clear only that retelling's stale Git entry; other stale worktrees are left alone. Export a Git bundle first if you want to keep the retelling; deletion is not archival.

Suggested agent prompt

The CLI does not prescribe an explanatory style. Here is an editable starting point that favors progressive refinement; change the style, budget, tests, and scope to suit your review. Replace the angle-bracket placeholders before use.

Use git-retell to explain <BASE> → <TARGET> as <NAME> in <WORKTREE>, with a
<LINES>-line budget and <CONTEXT> context lines. Read git-retell --help first.

Favor progressive refinement: show the end-to-end behavior early, then add
detail. Introduce variables, functions, and classes alongside their first
use, rather than as advance preparation. Use simple implementations or explicit
stubs when needed to fit usage and definition together, then refine them.

Make each step substantial and coherent, with a commit message explaining its
purpose and temporary limitations. Treat these as preferences, not rigid rules.

<Optional, with start --partial: Leave out lock files and other generated
files; retell every other changed file completely.>

Preserve existing retellings. Reach the exact target tree, run git-retell
finish, and report the view command, expansion, and any test results or
limitations.

Testing every step

test NAME -- COMMAND ... runs the exact argument vector, without a shell, on each explanatory commit in a fresh detached worktree. It prints a pass/fail line per step with the output of failed steps, then exits 1 if any step failed. --json instead emits exit codes, stdout, stderr, and timeout results for every step. The base is not a step. Worktrees are removed after success, failure, or timeout; on POSIX the timed-out process group is killed. Commands can install dependencies or create files without modifying the authoring worktree. They execute project code with your permissions; a worktree is not a security sandbox. Submodules and Git LFS content are not automatically fetched. Nothing runs unless requested.

Pure Git storage

For retelling demo, the only stored state is:

  • refs/heads/retell/demo: the ordinary synthetic commit chain.
  • refs/retell/demo/base and refs/retell/demo/target: pinned real commits (or a synthetic empty base commit when building from scratch, and a synthetic snapshot commit when retelling uncommitted work).
  • refs/retell/demo/settings: a JSON blob holding the default line budget, context lines per hunk, and whether the retelling is partial.

Branch movement cannot silently move the pinned endpoints. There is no tutorial format, sidecar file, or Git config entry. Commit messages should identify the synthetic nature when read outside this CLI; every CLI slide explicitly labels it. To share a history, include its endpoints, since the target may not be an ancestor of the synthetic branch. A normal Git bundle works:

git bundle create demo.bundle refs/heads/retell/demo \
  refs/retell/demo/base refs/retell/demo/target refs/retell/demo/settings
# In another repository:
git fetch /path/to/demo.bundle 'refs/heads/retell/demo:refs/heads/retell/demo' \
  'refs/retell/demo/*:refs/retell/demo/*'

start refuses an existing retelling or branch. It never resets the main checkout. To remove just an authoring worktree, use git-retell finish NAME (or git worktree remove PATH for an unfinished retelling); the retelling stays available and resume checks it out again. Use git-retell delete NAME to remove the retelling's refs and settings too. Retellings are not merged back into the real development branch.

Development

To contribute to this tool, use uv. Run uv sync --locked to install the locked dependencies. The following command will establish the virtual environment and run tests:

uv run pytest

To run git-retell locally, use:

uv run git-retell

When running from source during retelling authoring, use the original checkout so the tool remains available while the synthetic implementation is incomplete.

Tests exercise real temporary repositories and worktrees, including exact trees, nonlinear histories, diff budgets, binary patches, unusual paths, test cleanup, and terminal navigation.

Metadata

Release files for git-retell 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for git-retell 0.2.0
File Size Uploaded
git_retell-0.2.0.tar.gz 65.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for git-retell 0.2.0
File Interpreter ABI Platform
git_retell-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 127.8 kB

Release files / git_retell-0.2.0.tar.gz

Download URL git_retell-0.2.0.tar.gz
Size 65.2 kB
Tags Source
SHA-256 checksum
How to use checksums
d70922e2df7de44d2f4e4ccd308011d9ec1d898e3b7dbfdac216b86014f19ad4
BLAKE2b-256 checksum
How to use checksums
811c561f18aced337f9ff2003a10424f4aa24e03d492cafdd57f297564026ae6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / git_retell-0.2.0-py3-none-any.whl

Download URL git_retell-0.2.0-py3-none-any.whl
Size 62.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ebc19b8c86bf9f257eb9a815e22649a0535b6dd4a1670eca6edf930a30b214ac
BLAKE2b-256 checksum
How to use checksums
5077b8b5e61f4d34febc96ef6d75a21be2f9320e9adb5eb4ca8c06b2ea604e72
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page