Cut and paste Git history safely using bundles and git-filter-repo, with strong dry-run previews
Project description
git-clipboard
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.
Minimal clipboard flow
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-paste
Example 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.
Install
- 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 PATH
git-cut
Create 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]
Key options
- -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
Outputs
- 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.
git-paste
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]
Default behavior
- 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)
Ref selection
- 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.
List refs in a 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"
# }
Clipboard default and obvious mode
- 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
Trailers (provenance in commit messages)
- 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: git@github.com: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...
Notes for dry-run
- 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.
Dry-run JSON fields
-
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
Notes and assumptions
- 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.
Examples
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"
Try it
Quick smoke test and demo:
# Run the end-to-end test; it prints JSON previews and ends with "E2E OK"
bash ./e2e.sh
Tests
Run the end-to-end test script (creates temporary repos, cuts, dry-runs paste, then imports and merges):
bash ./e2e.sh
Troubleshooting
- 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.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file git_clipboard-0.2.3.tar.gz.
File metadata
- Download URL: git_clipboard-0.2.3.tar.gz
- Upload date:
- Size: 15.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
953899b5351b95365f6a7d635d07fc9e78883cbe4fdeb649032634158c43dbc4
|
|
| MD5 |
859fe65b9baed5b7a865fcc608e26114
|
|
| BLAKE2b-256 |
73a86474ec70dd1870a4fe23ac34f37fd5b6c7b20760218c01e4a55fe08857a3
|
File details
Details for the file git_clipboard-0.2.3-py3-none-any.whl.
File metadata
- Download URL: git_clipboard-0.2.3-py3-none-any.whl
- Upload date:
- Size: 16.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48cbfb20c24469d16be43ad67638d392630ee2b0aed5f97ec2441670d6f94587
|
|
| MD5 |
3c0fa2e7948c913531fb31fb06be1cd8
|
|
| BLAKE2b-256 |
427bf05daf12ea9ed2c090c0d99a5956b1bddf167dca5274b733027bfa0c1559
|