Move Files Between Git Repositories and Preserve History

Deepak Prasad
Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package git 2.52.0
git-filter-repo 2.47.0
Applies to Linux, macOS, and Windows with Git, Python 3, and git-filter-repo; GitHub, GitLab, Bitbucket, or self-hosted remotes
Privilege Read or clone access to the source repository; push access to the target repository; push access to the source only if you remove the moved paths afterward (optional Step 9)
Scope Move selected files or directories from a source repo into an existing target repo while keeping relevant commit history; does not cover whole-repository mirror migration or copying files without history
Related guides Git tutorial
git delete file or directory
Short on time?Beta Get the gist of this 13-minute read in seconds, then ask follow-up questions

If you need to move one or more files or directories from an existing Git repository into another repository without losing their relevant commit history, use git filter-repo to extract that history from a disposable source clone, then merge the filtered repository into the target.

This tutorial covers that case: selected paths into an existing target repository. You need read or clone access to the source and push access to the target. The filter step rewrites only your local migration clone; it does not modify the source remote. Replace SOURCE_REPO, TARGET_REPO, and path/to/directory/ with your own remote URLs and paths. The examples use main; replace it with the branch you want to migrate when your repository uses a different branch.


Quick answer: choose the correct Git migration method

Goal Approach
Selected files or folder into an existing repository git filter-repo + merge
Directory into a new repository git filter-repo + push
Entire repository to a new remote bare clone + mirror push
Files only, history not required copy + commit

The rest of this article walks through the main workflow. If you do not need the original commit history, simply copy the files into the target repository and create a normal commit.


Move files to another Git repository and preserve history

git filter-repo rewrites the repository so the selected paths and the history relevant to them remain. In commits that originally changed both selected and unrelated files, only the selected paths remain in the rewritten commit. Commits that become empty may be removed. Retained commits receive new commit IDs because the history was rewritten. git-filter-repo removes commit and tag signatures during the rewrite because changing a commit or tag changes its object ID, so the original signature would no longer validate.

This workflow requires git-filter-repo. Check first:

bash
git filter-repo --version

If the command is missing, install it. On RHEL and Fedora, package installs use dnf command with sudo:

bash
sudo dnf install git-filter-repo

On Debian and Ubuntu, update indexes and install with apt command:

bash
sudo apt update && sudo apt install git-filter-repo

On macOS with Homebrew:

bash
brew install git-filter-repo

If the packaged version is too old, use pipx install git-filter-repo instead.

NOTE
Do not substitute git filter-branch for this workflow. Git recommends git-filter-repo for new history-rewriting work.

1. Clone the source repository

Clone SOURCE_REPO into a disposable working directory:

bash
git clone SOURCE_REPO source-repo-migration

Change into the clone:

bash
cd source-repo-migration

git filter-repo has a safety check that normally expects you to run destructive filtering on a fresh clone. Use a disposable clone instead of your everyday working copy, and do not pass --force in the normal workflow. For a local filesystem source, clone with --no-local. Do not use a shallow clone (--depth) when you need complete history.

2. Check the history you want to preserve

List the commits that touched the path you plan to move:

bash
git log --oneline -- path/to/directory/

Note one or two commit subjects so you can confirm they remain after filtering.

IMPORTANT
If this path had an older name, do not filter yet. git-filter-repo does not automatically follow renames during path filtering — the same limitation applies when you rename a file or directory inside one repo and later need every old path in the filter. Run git filter-repo --analyze and review .git/filter-repo/analysis/. The reports can help identify paths and renames that existed earlier in the repository's history. Include any relevant historical path names in the filter. See Path renamed in old commits if that applies.

3. Filter the files or directory

Keep a directory and its history at the same path:

bash
git filter-repo --path path/to/directory/

git filter-repo removes the origin remote as a safety measure and rewrites the clone so only the selected paths remain. This is expected. Removing origin helps prevent you from accidentally pushing the rewritten history back to the original repository. The source remote on your hosting site is not changed.

Use the command that matches what you want to move:

Goal Command
Single file git filter-repo --path path/to/file
Multiple paths git filter-repo --path dir1/ --path shared/config.yaml
Directory contents at repository root git filter-repo --subdirectory-filter path/to/directory
Different destination path in target git filter-repo --path dir1/ --path-rename dir1/:components/dir1/

Each --path adds another path to the retained history. Changes to paths you did not select are removed. Commits that become empty as a result may also be removed. --subdirectory-filter lifts the directory contents to the repository root instead of keeping the directory prefix.

Path renamed in old commits

If legacy-app/ became path/to/directory/ in an older commit, include every historical name:

bash
git filter-repo --path legacy-app/ --path path/to/directory/

Adding both paths keeps history from both names. Older commits still refer to the historical path unless you rewrite that path too. Verify with:

bash
git log --oneline --all -- legacy-app/ path/to/directory/

For one file, git log --follow can help follow a detected rename inside the filtered tree.

4. Verify the filtered history

Confirm the filtered clone before you touch the target:

bash
git ls-files

Review the rewritten history:

bash
git log --oneline --all

For a path that was not renamed:

bash
git log --oneline -- path/to/directory/

For a path with historical names:

bash
git log --oneline --all -- legacy-app/ path/to/directory/

You should see only the selected files in git ls-files and new commit IDs because the history was rewritten. If the path was not renamed, the commit subjects you noted in Step 2 should still appear in git log -- path/to/directory/. If the path was renamed in the past, verify all historical path names together. git filter-repo retains the selected old and new paths but does not automatically rename the old path inside historical commits.

5. Import the filtered history into the target

Clone the existing target repository next to the filtered source clone, then create a branch for the migration with git switch:

bash
cd ..
git clone TARGET_REPO target-repo
cd target-repo
git switch -c import-history

Using a separate branch lets you review the migration before it reaches your target branch.

Register the filtered clone, fetch it, and list remote branches. See git remote add, git fetch, and git list branches for command options:

bash
git remote add filtered-source ../source-repo-migration
git fetch filtered-source
git branch -r

Find the filtered source branch you want to import. The examples below use filtered-source/main; replace main with the actual branch name when necessary.

Make sure the target working tree is clean before starting the merge:

bash
git status

These repositories were created independently, so Git sees them as two unrelated histories. A normal merge is rejected because they have no shared ancestor. --allow-unrelated-histories tells Git that joining them is intentional:

bash
git merge --allow-unrelated-histories filtered-source/main -m "Import filtered history from source repository"

6. Resolve conflicts if necessary

If both repositories contain different versions of the same path, Git may stop with a merge conflict. Run:

bash
git status

Edit the conflicted files, choose the content you want to keep, stage each resolved file, and complete the merge:

bash
git add path/to/resolved-file

Repeat git add for each resolved path, then finish the merge:

bash
git commit

For a broader merge conflict walkthrough, see git merge examples.

7. Verify the imported history

Check that the moved paths are tracked:

bash
git ls-files path/to/directory/

Review the combined graph and path history:

bash
git log --oneline --graph --all

For a path that was not renamed:

bash
git log --oneline -- path/to/directory/

For a path with historical names:

bash
git log --oneline --all -- legacy-app/ path/to/directory/

For a single file, follow renames inside the filtered tree:

bash
git log --follow --oneline -- path/to/directory/file

The graph should show that the target history and imported source history both lead into the migration merge commit. If the path was renamed in the past, verify all historical path names together rather than relying on git log -- path/to/directory/ alone.

8. Push the migration

Push the import branch to the remote:

bash
git push -u origin import-history

Open a pull or merge request and use a normal merge. Do not squash the migration branch if you need the imported commits to stay reachable as individual revisions — see git squash commits for when squash is appropriate on ordinary feature work, not history imports.

Remove the temporary remote when you are done; see git remove remote if you need to drop other remotes later:

bash
git remote remove filtered-source

Delete the source-repo-migration directory when you no longer need it.

9. Remove the content from the source repository (optional)

After verifying the target, remove the original paths from a normal checkout of the source repository with git rm. This step needs push access to the source remote:

bash
git rm -r path/to/directory/

Commit the removal:

bash
git commit -m "Move directory to target repository"

Push the removal. Replace main with the source branch you are cleaning up:

bash
git push origin main

Do this only after verifying the migration. Skip it when you intend to copy or split the content instead.


Common migration cases

Move to a new empty repository

When the destination is empty rather than an existing repository, filter the source clone, add a new remote, and push:

bash
git remote add new-origin NEW_REPO_URL

Push the filtered main branch to the empty repository:

bash
git push -u new-origin main

That creates a standalone repository with the filtered history. It does not merge into existing target commits the way Step 5 does.

Move the entire repository instead

When you want to move the entire repository, including its branches and tags, path filtering is unnecessary:

bash
git clone --bare OLD_REPO_URL

Enter the bare repository directory:

bash
cd old-repo.git

Mirror every ref to the new remote:

bash
git push --mirror NEW_REPO_URL

git push --mirror force-updates matching refs on the destination and can delete destination refs that do not exist in the source. Use it when the new remote should replace the old one, especially when the destination starts empty.

What happens to branches and tags?

git filter-repo normally rewrites the branches and tags available in the source clone. This tutorial imports only filtered-source/main into the target. Other filtered branches and tags remain separate refs and are not automatically published on the target by the merge shown here. If you need selected-path history for multiple branches or tags, plan those refs explicitly and check for naming conflicts.

Git LFS files

Git history stores Git LFS pointer files, not the large objects themselves. When moved paths use LFS, install Git LFS on the push host, confirm the source still has the objects you need, make sure the required LFS objects are transferred to the target remote as part of the migration, and verify the result with a fresh clone. See the Git LFS documentation and GitHub: duplicating a repository.


Common problems

Symptom Likely cause Fix
git filter-repo says the repository is not a fresh clone Safety check failed on a reused working copy Make a new clone; avoid --force unless you understand the risk
Older commits disappeared after filtering Historical path rename not included Add older path names with extra --path values; run --analyze first
fatal: refusing to merge unrelated histories Missing flag on unrelated roots Run git merge --allow-unrelated-histories filtered-source/main
Target already has the same path Content overlap Resolve conflicts with git status, then git add and git commit
Commit IDs changed Expected after git filter-repo Compare subjects and authors, not old object IDs
git log path/ misses old commits Path rename or wrong path filter Use git log --all -- legacy-app/ path/to/directory/ and include legacy paths when filtering
Push rejected by branch protection Direct push to protected branch Push an import branch and merge through the normal review flow without squash

git-filter-repo vs subtree vs format-patch

Method Best fit
git filter-repo Extract selected history before migration
git subtree split Subtree-oriented workflows and splitting
git format-patch + git am Replay selected patches or commits
git clone --bare + push --mirror Entire repository migration

This article uses git filter-repo for selected-path migration. To drop large blobs from one repository instead of moving paths elsewhere, see reduce Git repository size with git-filter-repo.


Final verification checklist

Before you delete data from the source repository, confirm:

  • selected files exist in the target tree
  • unrelated source files were not imported
  • expected commit authors, messages, and dates are visible on the moved paths
  • git log -- path/ shows the history you expect for an unchanged path, or git log --all -- old-path/ new-path/ when the path was renamed
  • git log --follow -- path/file shows the file history you expect
  • the target project still builds or tests cleanly
  • Git LFS objects are available on the target when applicable
  • the source cleanup happens only after target verification
  • the migration pull request was not squash-merged when you need per-commit ancestry

Summary

Moving selected paths between repositories starts with a fresh source clone and git filter-repo --path to keep only the history you need. That rewrite creates new commit IDs, so verify the filtered history before importing it into the target.

The target side fetches the filtered clone as a temporary remote, merges with --allow-unrelated-histories, and pushes a review branch such as import-history. Merge without squash when you need the imported commits to remain individually reachable. Remove the paths from the source only after the target is verified.


References


Frequently Asked Questions

How do I move files from one Git repository to another and keep history?

Clone the source repository into a disposable directory, run git filter-repo with --path for the files or directories you need, clone the target repository, add the filtered clone as a remote, and merge with --allow-unrelated-histories. Push the result on a review branch and merge without squash if you need the imported commits to stay separate.

How do I move one directory to another Git repository?

Use git filter-repo --path path/to/directory/ on a fresh source clone, then merge filtered-source/main into the target branch. Use --subdirectory-filter instead when the directory contents should become the repository root in the filtered clone.

Does git-filter-repo preserve commit history?

It rewrites the repository so the selected paths and their relevant history remain, including authors, dates, and commit messages. Commit IDs change, and commits that contain no retained changes may disappear.

Why did older history disappear after git filter-repo?

The path probably changed in an earlier commit. Include every historical directory or file name with additional --path arguments, then filter again from a fresh clone. Run git filter-repo --analyze first on large repositories.

Should I use git filter-branch or git-filter-repo?

Use git-filter-repo for new work. Git's own documentation warns that filter-branch has safety and performance problems.

What is the difference between --path and --subdirectory-filter?

--path dir1/ keeps the dir1/ prefix in the filtered repository. --subdirectory-filter dir1 moves the contents of dir1/ to the repository root.

Can I move only one file and preserve its history?

Yes. Pass the file path to --path, for example git filter-repo --path src/config.yaml.

How do I move the entire Git repository with all branches and tags?

Use a bare clone and git push --mirror to the new remote when the destination should mirror every ref. That is a whole-repository migration, not the selected-path workflow in this article.

Can I move content into a repository that already has commits?

Yes. That is the primary scenario here. The target keeps its existing history, and the merge adds the filtered source history as a second root line.

Should I squash the migration pull request?

No, not if you need the imported commits to remain individually reachable. Squash merging collapses them into one commit on the target branch.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Steve Alila

Specializes in web design, WordPress development, and data analysis, with proficiency in Python, JavaScript, and data extraction tools. Additionally, he excels in web API development, AI integration, and data presentation using Matplotlib and Plotly.

  • Git
  • JavaScript
  • Laravel
  • Python (programming language)
  • Machine Learning