Convenience CLI wrappers to cut and paste Git history using git-filter-repo and bundles.
- git-cut: produce a portable bundle containing only selected paths and their full history.
- git-paste: import that bundle into another repository, preserving history and optionally merging.
The heavy lifting is done by git-filter-repo; these commands make common flows feel like a simple clipboard.
Copy a couple of paths (with full history) from one repo and paste them into another — no extra flags.
# In a source repo
git-cut fileA dirB
# In a target repo
git-pasteExample output (abbreviated):
/abs/path/.git-clipboard/clip-20250101-120000.bundle
/abs/path/.git-clipboard/clip-20250101-120000.json
Imported branch: clip/clip-20250101-120000
{
"action": "merge-preview",
"target": "main",
"source": "clip/clip-20250101-120000",
"no_ff": false,
"squash": false,
"conflicts": false,
"allow_unrelated_histories": false,
"auto_allow_unrelated_histories": false,
"trailers": false,
"note": null
}
Auto-merge now? [y/N]: y
Merged clip/clip-20250101-120000 into main
That’s it: cut some paths, paste them, confirm merge if it’s clean. If a merge might conflict (or histories are unrelated), you’ll see a preview and no auto-merge is attempted.
- Requirements: git, git-filter-repo
- macOS:
brew install git-filter-repo
Python package (pipx) install for convenience:
pipx install .
# Afterwards the commands git-cut, git-paste, git-clipboard are available on your PATHCreate a bundle containing only the specified paths (files/folders) and their history. The source repo is never modified.
Usage
git-cut [PATH ...] [--repo REPO] [--to-subdir DIR] [--out-dir DIR] [--name NAME] [--force]- -r/--repo: path to the source repository (default: current dir)
- -t/--to-subdir: re-root the content into a subdirectory inside the clip
- -o/--out-dir: where to write the .bundle and .json (default: ./.git-clipboard)
- -n/--name: base filename for the outputs (default: clip-YYYYmmdd-HHMMSS)
- -f/--force: overwrite existing outputs
- -d/--dry-run: print a JSON plan without creating output files
- --no-follow-renames: by default git-cut follows file renames and includes historical names so history isn’t lost across moves; use this to disable
- NAME.bundle: a git bundle with all refs from the filtered repo
- NAME.json: metadata capturing paths, subdir, source remotes, and default branch
- The last clip pointer is also written to ~/.git-clipboard/last for easy pasting without specifying a path.
Import a previously created bundle into a target repository. By default it creates a new branch from the bundle and you can choose to merge it.
Usage
git-paste [BUNDLE] [-m META.json] [-r REPO] [-a NAME] [--ref REF] [--list-refs|-L] [-b BRANCH] [--merge|-M|--squash|-s|--rebase|-R] [--no-ff|-F] [--message|-j MSG] [--dry-run|-d] [--allow-unrelated-histories|-U] [--prompt-merge|-p] [--trailers|-T]- Creates a branch
clip/<bundle-base-name>from the bundle's first head - If --merge/--squash/--rebase is given, merges into the current branch (or --branch)
- Cleans up a temporary remote used for fetching from the bundle
- Merges allow unrelated histories by default (you can still dry-run to preview conflicts first)
- Use
--refto pick a specific ref from the bundle (e.g.,--ref mainor--ref refs/heads/main). - If omitted, git-paste tries the metadata
default_branchfrom the clip. If not available, it falls back to the first head in the bundle.
- Use
--list-refs(-L) to print all refs in the bundle as JSON and exit. - If a metadata file is available,
default_refwill be included based on the clip’sdefault_branch.
Example:
git-paste ./clips/clip.bundle --list-refs
# {
# "action": "list-refs",
# "bundle": "/abs/path/clips/clip.bundle",
# "refs": [
# {"sha": "abc123", "ref": "refs/heads/main"}
# ],
# "default_ref": "refs/heads/main"
# }- If BUNDLE is omitted, git-paste looks up the last clip pointer at
~/.git-clipboard/lastwritten by git-cut, and uses that bundle. - If you pass no merge flags, git-paste runs a quick merge preview and, if clean, prompts whether to auto-merge the imported branch into the current (or --branch) target. If conflicts are likely or unknown, it won’t auto-merge.
Example (clipboard + obvious mode):
# In source repo
git-cut path/to/subtree --out-dir ../clips --to-subdir imported
# In target repo, just paste with no args. If clean, confirm to auto-merge.
git-paste- Use
--trailers(-T) to append clip metadata as trailers to merge or squash commit messages. - Trailers include:
Clip-Bundle,Clip-Source(if available),Clip-Paths,Clip-Subdir,Clip-Created-At,Clip-Ref(imported ref), andClip-Head(SHA of imported branch head). - Behavior:
- If you pass
--message, trailers are added as an extra paragraph. - If you don’t pass
--message, for non-squash merges the default merge message is preserved and we amend to append trailers. - For squash merges, trailers are appended to the squash commit message.
- If you pass
Example:
git-paste ../clips/clip.bundle --merge --allow-unrelated-histories --message "Import clip" --trailers
# Commit message ends with:
# Clip-Bundle: clip.bundle
# Clip-Source: [email protected]:me/repo.git
# Clip-Paths: proj/a
# Clip-Subdir: imported
# Clip-Created-At: 2025-01-01T12:00:00Z
# Clip-Ref: refs/heads/main
# Clip-Head: abcdef1234567890...- With
--dry-run, paste clones the target repo into a temporary directory, simulates the import and prints a JSON summary, and previews merge conflicts usinggit merge-treewhen possible. The real repo is not modified. - Real merges allow unrelated histories by default; this is typically what you want for cut/paste between repos.
- Use
--prompt-mergeto preview conflicts and, if clean, interactively confirm an automatic merge.
-
import-branch: action, as_branch, source_ref, remote, head, source_summary
- source_summary now includes: commit_count, top_level_paths (+ totals), file_count, total_size_bytes, largest_files[{path,size}]
-
merge-preview: action, target, source, no_ff, squash, conflicts, allow_unrelated_histories, auto_allow_unrelated_histories, trailers, source_summary, diff_summary, note
- diff_summary: range, files_changed, insertions, deletions, changes_sample (up to 50 items; includes rename tuples)
- head: SHA of the imported branch tip
- source_summary: quick provenance of the imported branch
- commit_count: integer
- top_level_paths: up to 50 entries from the branch root
- top_level_paths_total: total entries
- top_level_paths_truncated: true if truncated
-
merge-preview includes:
- target, source, no_ff, squash, conflicts
- allow_unrelated_histories, auto_allow_unrelated_histories, trailers
- source_summary: same shape as above
- note: reason when merge-base is unknown
- Assumes git-filter-repo is installed and available as either
git filter-repoorgit-filter-repo. git-cutclones your repository into a temp directory, filters there, and never touches your working copy.- We bundle
--allrefs after filtering to maximize portability; paste selects the first head by default. - If you used
--to-subdirwhen cutting, the directory structure is already remapped inside the bundle; paste doesn’t need to move files. - Merge strategies in paste are standard Git merge/rebase flows; resolve conflicts as usual if they arise.
Cut history of two folders and paste into another repo under a new branch, then merge:
# from source repo
git-cut dotfiles/.config nvim/ --to-subdir configs --out-dir ../clips
# in target repo
git-paste ../clips/clip-20250101-120000.bundle --dry-run --merge
# If clean, perform the merge (often with unrelated histories allowed):
git-paste ../clips/clip-20250101-120000.bundle --merge --allow-unrelated-histories --message "Import configs"Quick smoke test and demo:
# Run the end-to-end test; it prints JSON previews and ends with "E2E OK"
bash ./e2e.shRun the end-to-end test script (creates temporary repos, cuts, dry-runs paste, then imports and merges):
bash ./e2e.sh- If you see
git: 'filter-repo' is not a git command, install git-filter-repo. - Bundles list heads:
git bundle list-heads path/to.bundle. - You can delete the generated branch and retry paste safely; the origin repo is never modified by these tools.
MIT