Export Git History Safely with git fast-export
You will produce a Git fast-import stream containing a chosen repository history, inspect it, and optionally load it into a separate repository. This is useful for a portable, human-readable history dump or for a controlled history rewrite. It is not a drop-in replacement for every kind of backup: the stream can contain private commits, and some options deliberately require objects that are already present at the destination.
The route
Jump straight to the step you need, or tick off Done means at the end.
These examples match the installed Git 2.43.0 and its git-fast-export(1) manual page. Allow about 10 minutes for a small repository. You need a working Git checkout and enough free space for the stream and any temporary import. No command below needs elevated privileges. Use a shell account that can read the repository; do not run the export as root merely to avoid a permissions problem.
1. Check the repository and choose a destination
Start in the repository you intend to export. Confirm the Git version, the current location and the references that are available. The revision arguments are passed to the same revision machinery used by git rev-parse and git rev-list, so a typo can change the set of objects or make the command fail.
$ git --version
git version 2.43.0
$ git rev-parse --show-toplevel
/path/to/project
$ git show-ref --head
Pick an output path outside the repository if the dump is large. The following placeholder is intentionally explicit. Replace it with a path you control.
$ EXPORT_FILE=/tmp/project-fast-export.dat
$ test ! -e "$EXPORT_FILE" || { echo "Refusing to overwrite $EXPORT_FILE"; exit 1; }
Checkpoint: you know which repository and refs will be exported, and the chosen output path is not an existing file.
2. Export the whole repository
For a complete ordinary export, use --all and redirect standard output to the stream. The command writes the fast-import protocol, not a Git pack file. Keep the original repository unchanged while this runs.
$ git fast-export --all > "$EXPORT_FILE"
$ test -s "$EXPORT_FILE" && sed -n '1,12p' "$EXPORT_FILE"
blob
mark :1
data 42
The exact first lines depend on the repository, so the small sample above is illustrative rather than a required transcript. A non-empty file and a successful exit are the useful checks. Treat this file as sensitive: normal exports preserve names, email addresses, paths, commit messages and file contents.
Safety boundary
Creating the stream does not make an independent encrypted backup. It is plain text, can be edited, and may be much larger than expected. Protect it with normal filesystem permissions and remove it when it is no longer needed.
3. Export a revision range or selected paths
Export only the history you need by supplying revision arguments. For example, this exports commits reachable from main but not from its tenth ancestor, assuming that ref exists.
$ git fast-export 'main~10..main' > /tmp/project-last-ten.dat
$ test -s /tmp/project-last-ten.dat && wc -c /tmp/project-last-ten.dat
A range excludes the older commits by default. The exported commits therefore do not retain those excluded commits as ordinary parents. If the destination already contains the excluded parent commits and you need the stream to refer to them by object ID, add --reference-excluded-parents. The resulting stream is not self-contained and cannot be imported into an unrelated empty repository.
You can also put a pathspec after the revisions to limit the files included in the export.
$ git fast-export main -- src/README.md > /tmp/project-readme-history.dat
Path filtering can leave tagged objects outside the exported set. The default --tag-of-filtered-object=abort stops when a tag points at an object that was filtered out. That is a useful refusal, not a damaged partial backup. Decide deliberately whether such tags should be dropped or rewritten before using --tag-of-filtered-object=drop or --tag-of-filtered-object=rewrite.
Checkpoint: inspect the revision range and pathspec before trusting the resulting stream. If the command failed about a tag, do not hide the error until you have decided what losing or changing that tag means.
4. Verify by importing into a disposable repository
The practical verification is an import into a new, empty repository. This changes only the disposable directory. Do not point the import at a working repository containing valuable refs.
$ IMPORT_DIR=/tmp/project-fast-import-test
$ test ! -e "$IMPORT_DIR" || { echo "Refusing to reuse $IMPORT_DIR"; exit 1; }
$ mkdir "$IMPORT_DIR"
$ git -C "$IMPORT_DIR" init
$ git -C "$IMPORT_DIR" fast-import < "$EXPORT_FILE"
$ git -C "$IMPORT_DIR" fsck --full
$ git -C "$IMPORT_DIR" show-ref
A successful import normally ends without an error, and git fsck --full should report no missing objects. The refs shown depend on the source. Compare the source and destination object counts when you exported the whole repository.
$ git count-objects -v
$ git -C "$IMPORT_DIR" count-objects -v
If the test is no longer needed, remove only the explicitly named disposable directory after checking its contents. This is the one destructive command in the guide; it cannot be undone through Git.
$ find "$IMPORT_DIR" -maxdepth 2 -type f -print
$ rm -rf -- "$IMPORT_DIR"
5. Handle marks for incremental exports
Marks give objects temporary numeric names in the stream. --export-marks writes the commit marks at completion, while --import-marks loads a file before processing the next export. Reusing the same marks file can avoid exporting objects already known to the receiving side.
$ MARKS=/tmp/project.marks
$ git fast-export --all --export-marks="$MARKS" > /tmp/project-full.dat
$ test -s "$MARKS" && sed -n '1,5p' "$MARKS"
:1 0123456789abcdef0123456789abcdef01234567
The displayed object ID is repository-specific. Marks are written only when new marked objects were exported, and blob marks are ignored in the exported marks file. Use --mark-tags when tags need mark identifiers, especially for nested tags, but check that the importer accepts tag marks before relying on them.
Do not guess about incremental state. The marks file must use the format produced by this command and must be readable before the export starts. Keep it with the destination's corresponding import state. Mixing marks from unrelated repositories can make an incremental workflow misleading even when the command itself succeeds.
6. Avoid the two common data traps
Signed tags are not automatically safe after an export or rewrite. Changes to tag names, excluded revisions or rewritten history invalidate their signatures. The default --signed-tags=abort stops at a signed tag. Choose verbatim only when preserving the bytes is more useful than preserving valid signatures, or choose warn-strip or strip when the destination must receive unsigned tags and that loss is understood.
--no-data omits blob contents and refers to their original SHA-1 values. It is suitable for a destination that already has every required object, such as a controlled rewrite inside a repository with the source data. It is not suitable for a standalone archive. Similarly, --reference-excluded-parents assumes the destination already has the referenced parent commits.
For a bug report, --anonymize replaces refnames, paths, contents, messages, names and email addresses while retaining useful history and tree shape. It still preserves commit timestamps, and anonymisation is not a guarantee that every sensitive fact has disappeared. Inspect the output before sharing it. Use --anonymize-map=FROM:TO for a token that must remain recognisable in the reproduction.
Done means
- The revision arguments and paths were checked before export.
- The stream was written to a new, protected file and treated as sensitive.
- A disposable repository imported the stream successfully and passed
git fsck --full. - You did not use
--no-dataor--reference-excluded-parentsunless the destination already contains the required objects. - Signed tags, filtered tags, marks and anonymisation were chosen deliberately for this export.