Skip to main content

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 --ref to pick a specific ref from the bundle (e.g., --ref main or --ref refs/heads/main).
  • If omitted, git-paste tries the metadata default_branch from 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_ref will be included based on the clip’s default_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/last written 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), and Clip-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.

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 using git merge-tree when 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-merge to 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-repo or git-filter-repo.
  • git-cut clones your repository into a temp directory, filters there, and never touches your working copy.
  • We bundle --all refs after filtering to maximize portability; paste selects the first head by default.
  • If you used --to-subdir when 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

git_clipboard-0.2.3.tar.gz (15.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

git_clipboard-0.2.3-py3-none-any.whl (16.5 kB view details)

Uploaded Python 3

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

Hashes for git_clipboard-0.2.3.tar.gz
Algorithm Hash digest
SHA256 953899b5351b95365f6a7d635d07fc9e78883cbe4fdeb649032634158c43dbc4
MD5 859fe65b9baed5b7a865fcc608e26114
BLAKE2b-256 73a86474ec70dd1870a4fe23ac34f37fd5b6c7b20760218c01e4a55fe08857a3

See more details on using hashes here.

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

Hashes for git_clipboard-0.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 48cbfb20c24469d16be43ad67638d392630ee2b0aed5f97ec2441670d6f94587
MD5 3c0fa2e7948c913531fb31fb06be1cd8
BLAKE2b-256 427bf05daf12ea9ed2c090c0d99a5956b1bddf167dca5274b733027bfa0c1559

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page