518

Our Git repositories started out as parts of a single monster SVN repository where the individual projects each had their own tree like so:

project1/branches
        /tags
        /trunk
project2/branches
        /tags
        /trunk

Obviously, it was pretty easy to move files from one to another with svn mv. But in Git, each project is in its own repository, and today I was asked to move a subdirectory from project2 to project1. I did something like this:

$ git clone project2 
$ cd project2
$ git filter-branch --subdirectory-filter deeply/buried/java/source/directory/A -- --all
$ git remote rm origin  # so I don't accidentally overwrite the repo ;-)
$ mkdir -p deeply/buried/different/java/source/directory/B
$ for f in *.java; do 
>  git mv $f deeply/buried/different/java/source/directory/B
>  done
$ git commit -m "moved files to new subdirectory"
$ cd ..
$
$ git clone project1
$ cd project1
$ git remote add p2 ../project2
$ git fetch p2
$ git branch p2 remotes/p2/master
$ git merge p2 # --allow-unrelated-histories for git 2.9+
$ git remote rm p2
$ git push

But that seems pretty convoluted. Is there a better way to do this sort of thing in general? Or have I adopted the right approach?

Note that this involves merging the history into an existing repository, rather than simply creating a new standalone repository from part of another one (as in an earlier question).

lbonn
  • 2,299
  • 21
  • 31
ebneter
  • 18,039
  • 9
  • 27
  • 30
  • 1
    I wonder why you don't do `git fetch p2 && git merge p2` instead of `git fetch p2 && git branch .. && git merge p2`? Edit: alright, it looks like you want to get the changes in a new branch named p2, not the current branch. – Lekensteyn Dec 21 '11 at 22:25
  • To only bring over the commits from the other repo which involves the directory you are moving, you should add `--prune-empty` to the `git filter-branch` – Martin Apr 17 '12 at 10:01
  • I also found this thread in case the files you moved out from project2 had a lot of history and you wanted to permanently erase all records of them, since you have the history in a second project: http://dalibornasevic.com/posts/2-permanently-remove-files-and-folders-from-a-git-repository – Andrew Mao Dec 29 '12 at 21:00
  • 1
    That sounds like a reasonable approach to me; I can't think of any obvious way to significantly improve your method. It's nice that Git actually does make this easy (I wouldn't want to try to move a directory of files between *different repositories* in Subversion, for example). – Greg Hewgill Sep 02 '09 at 02:54
  • I also would not want to try moving a directory between two different svn repos! (I'm imagining some nightmare involving svnadmin dump and svn dumpfilter, bleah.) – ebneter Sep 02 '09 at 19:40
  • 1
    @ebneter - I've done this (moved history from one svn repo to another) manually, using shell scripts. Basically I replayed the history (diffs, commit logs messages) from particular files/dirs into a second repository. – Adam Monsen Nov 10 '11 at 14:29
  • 1
    Is there no way to prevent --filter-branch from destroying the directory structure? That "git mv" step results in a massive commit full of file deletions and file creations. – Edward Falk Sep 05 '14 at 02:48
  • 1
    Note that as of git 2.9 merging unrelated histories is disallowed by default. To make it work, add `--allow-unrelated-histories` to the last `git merge` to make it work. – Scott Berrevoets Jun 23 '16 at 20:20
  • Possible duplicate of [Detach (move) subdirectory into separate Git repository](https://stackoverflow.com/questions/359424/detach-move-subdirectory-into-separate-git-repository) – Liam Jul 27 '18 at 09:03
  • @Liam, it's not really a duplicate, as this involves merging things into an existing repo's history. – ebneter Jul 31 '18 at 20:44
  • Hey, that “overwrite” in the comment “so I don’t accidentally the repo” was intentionally omitted! I guess that meme’s been forgotten, though. :-) – ebneter Mar 27 '21 at 05:56

16 Answers16

319

If your history is sane, you can take the commits out as patch and apply them in the new repository:

cd repository
git log --pretty=email --patch-with-stat --reverse --full-index --binary -- path/to/file_or_folder > patch
cd ../another_repository
git am --committer-date-is-author-date < ../repository/patch 

Or in one line

git log --pretty=email --patch-with-stat --reverse -- path/to/file_or_folder | (cd /path/to/new_repository && git am --committer-date-is-author-date)

(Taken from Exherbo’s docs)

weakish
  • 23,766
  • 4
  • 44
  • 54
Smar
  • 6,535
  • 2
  • 31
  • 46
  • 25
    For the three or 4 files I needed to move this was a much more simple solution than the accepted answer. I ended up trimming the paths out in the patch file with find-replace to get it to fit into my new repo's directory structure. – Rian Sanderson Oct 12 '12 at 20:29
  • 9
    I have added options so that binary files (like images) are also properly migrated: `git log --pretty=email --patch-with-stat --full-index --binary --reverse -- client > patch`. Works without problems AFAICT. – Emmanuel Touzery Apr 23 '13 at 09:21
  • 5
    Here is another, similar method that I have been using: http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ – Karol May 29 '13 at 18:51
  • 3
    Just to provide a bit of cost/benefit analysis: I tried this for a collection of 29 files (total 3.5k lines) and application of the patch took about 15 minutes! Probably could have done a dumb copy in a few seconds - The question to ask for actual complex variations of this is "do I *really* need the history?" – robert Apr 01 '14 at 03:47
  • Thanks for cool tip. If your history is NOT sane, try this one: http://stackoverflow.com/a/17373088/893945 – Joon Hong Apr 16 '14 at 07:14
  • 37
    On the apply step I used the `--committer-date-is-author-date` option to preserve the original commit date instead of the date the files were moved. – darrenmc May 29 '14 at 10:25
  • This looks very promising but I could not make it work on a file that have been moved. I wanted to keep the complete history (5 commits) but only the latest one is taken into account. And this latest commit is the one dealing with moving it into another folder. Any idea on how to get around this ? – SeB.Fr Jul 23 '14 at 13:16
  • @SeB.Fr: you need to take rest of commits using the old name of the file. – Smar Jul 24 '14 at 10:54
  • 7
    Doesn't work for files that have been moved/renamed. I assume you need to make individual patches for each of those files and add the `--follow` option to `git log` (which only works with one file at a time). – Daniel Golden Jul 25 '14 at 04:17
  • 1
    It's worth nothing that you need to add `--color=never` to `git log` if you use `--color=always` for whatever reason in your gitconfig. – rr- Jun 09 '15 at 08:49
  • any way of moving the tags as well? – lev Nov 27 '15 at 03:48
  • @lev Well, tags are just pointers to a commit, so I guess you should look at the tags you want to transfer and set them in the new repo by hand. – Smar Nov 27 '15 at 13:35
  • @Smar I'm planning on moving 10's of tags and I hoping to have a simpler solution – lev Nov 28 '15 at 06:43
  • @lev Well, you get the list of the tags, you can convert them to retagging commands with simple `sed` expression. But since there ought to be tags that are not relevant (as you are not moving all necessary commits), I’d find that kind of practice dangerous. – Smar Nov 28 '15 at 09:49
  • 6
    merge commits in the history break the "am" command. You can add "-m --first-parent" to the git log command above, then it worked for me. – Gábor Lipták Dec 02 '15 at 08:26
  • 2
    @Smar I created a patch using above command. I tried to apply the patch using `git am` but I ran into `Patch is empty. Was it split wrong?`. The error wasn't thrown immediately but after applying some changes. – phanin Feb 24 '16 at 16:24
  • @phani: I’ve encountered few situations where something like that happens (most likely due to faulty merges), but git does allow empty commits, so you should check whether the commit in question really contains something (and that it’s not empty in the patch; if not, test merging it by hand at that point, `git apply` or `patch -p1` helps. – Smar Feb 24 '16 at 16:27
  • @Smar, @phani - I've also encountered those 'empty patch' errors and I think it's because of the way git log treats merge commits. It's worth adding `-m --first-parent` to have those diffs in your patch, as Gábor Lipták suggests – tsayen Apr 14 '16 at 11:02
  • 8
    @Daniel Golden I've managed to fix the problem with files that have been moved (which is a consequence of a bug in `git log`, so that it doesn't work with both `--follow` and `--reverse` correctly). I used [this answer](http://stackoverflow.com/a/35380344/2519373), and here is a [complete script that I use now to move files](https://gist.github.com/tsayen/f1c1c4d62d4fda77abf1586bd39f9b74) – tsayen Apr 14 '16 at 11:08
  • 1
    If you want to move multiple files, be sure to combine the patches in ONE patch file. Otherwise the order of commits is not chronologically sorted. And use `--committer-date-is-author-date`, see the very helpful comment of @darrenmc – dr pain Apr 12 '18 at 09:28
  • @tsayen - Great script, which fixes similar one I've already had before. Unfortunately, it doesn't work all the way through. In my case, it stopped right at merge commit which introduced rename, so that's the very first commit. – xZero Mar 05 '19 at 10:14
  • This does not preserve history for files that have been moved or renamed. – jlicht Jan 16 '20 at 12:35
  • Note that also multiple files can be added to the list – Treviño May 11 '21 at 15:56
79

Having tried various approaches to move a file or folder from one Git repository to another, the only one which seems to work reliably is outlined below.

It involves cloning the repository you want to move the file or folder from, moving that file or folder to the root, rewriting Git history, cloning the target repository and pulling the file or folder with history directly into this target repository.

Stage One

  1. Make a copy of repository A as the following steps make major changes to this copy which you should not push!

    git clone --branch <branch> --origin origin --progress \
      -v <git repository A url>
    # eg. git clone --branch master --origin origin --progress \
    #   -v https://username@giturl/scm/projects/myprojects.git
    # (assuming myprojects is the repository you want to copy from)
    
  2. cd into it

    cd <git repository A directory>
    #  eg. cd /c/Working/GIT/myprojects
    
  3. Delete the link to the original repository to avoid accidentally making any remote changes (eg. by pushing)

    git remote rm origin
    
  4. Go through your history and files, removing anything that is not in directory 1. The result is the contents of directory 1 spewed out into to the base of repository A.

    git filter-branch --subdirectory-filter <directory> -- --all
    # eg. git filter-branch --subdirectory-filter subfolder1/subfolder2/FOLDER_TO_KEEP -- --all
    
  5. For single file move only: go through what's left and remove everything except the desired file. (You may need to delete files you don't want with the same name and commit.)

    git filter-branch -f --index-filter \
    'git ls-files -s | grep $'\t'FILE_TO_KEEP$ |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
    git update-index --index-info && \
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE || echo "Nothing to do"' --prune-empty -- --all
    # eg. FILE_TO_KEEP = pom.xml to keep only the pom.xml file from FOLDER_TO_KEEP
    

Stage Two

  1. Cleanup step

    git reset --hard
    
  2. Cleanup step

    git gc --aggressive
    
  3. Cleanup step

    git prune
    

You may want to import these files into repository B within a directory not the root:

  1. Make that directory

    mkdir <base directory>             eg. mkdir FOLDER_TO_KEEP
    
  2. Move files into that directory

    git mv * <base directory>          eg. git mv * FOLDER_TO_KEEP
    
  3. Add files to that directory

    git add .
    
  4. Commit your changes and we’re ready to merge these files into the new repository

    git commit
    

Stage Three

  1. Make a copy of repository B if you don’t have one already

    git clone <git repository B url>
    # eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git
    

    (assuming FOLDER_TO_KEEP is the name of the new repository you are copying to)

  2. cd into it

    cd <git repository B directory>
    #  eg. cd /c/Working/GIT/FOLDER_TO_KEEP
    
  3. Create a remote connection to repository A as a branch in repository B

    git remote add repo-A-branch <git repository A directory>
    # (repo-A-branch can be anything - it's just an arbitrary name)
    
    # eg. git remote add repo-A-branch /c/Working/GIT/myprojects
    
  4. Pull from this branch (containing only the directory you want to move) into repository B.

    git pull repo-A-branch master --allow-unrelated-histories
    

    The pull copies both files and history. Note: You can use a merge instead of a pull, but pull works better.

  5. Finally, you probably want to clean up a bit by removing the remote connection to repository A

    git remote rm repo-A-branch
    
  6. Push and you’re all set.

    git push
    
wass rubleff
  • 107
  • 9
mcarans
  • 1,037
  • 7
  • 10
  • 1
    I have gone through most steps outlined here however it seems to only copy over the commit history of the file or dir from the master (and not from any other branches). Is that right? – Bao-Long Nguyen-Trong Jan 27 '15 at 06:00
  • I think that's right and that you'd have to go through similar steps for any branches from which you want to move files or folders ie. switch to the branch eg. MyBranch in repository A, filter-branch etc. You would then "git pull repo-A-branch MyBranch" in repository B. – mcarans Jan 27 '15 at 08:06
  • Thanks for the reply. Do you know if tags on the branches will be migrated as well? – Bao-Long Nguyen-Trong Feb 02 '15 at 18:26
  • I'm afraid I don't know, but would guess that they would be. – mcarans Feb 03 '15 at 12:52
  • 1
    I ran through these steps (thanks for the attention to detail!), but I noticed in GitHub it doesn't show the history for any file except as the merge commit. However, if I do blame, or gitk, I see the commit history. Any idea why? – Newtang Jun 30 '15 at 20:08
  • I'm sorry. I haven't tried with Github and don't know why it would behave differently to any other repository. – mcarans Jul 02 '15 at 08:36
  • @Bao-LongNguyen-Trong you can push all tags by this cmd "git push origin --tags" – Sumit Nov 09 '16 at 06:02
  • How does it compare to https://stackoverflow.com/questions/28830916/how-to-move-files-from-one-git-repo-to-another-preserving-history-using-git-for – Paweł Prażak Aug 23 '17 at 14:13
  • What do you want to compare? – mcarans Aug 23 '17 at 19:56
  • How would we go about keeping multiple files instead of just one? What if FILES_TO_KEEP = {pom.xml, a.txt}? Can we modify the grep command? – Shail Sep 18 '17 at 15:32
  • @mcarans I have one doubt: In my case I want to copy the directory from repository A (branch: _repo-A-branch_) to repository B (branch: _repo-B-branch_). So is the following changes correct for **"Stage Three"**? : a) For repository B, I change the branch to repo-B-branch b) Then in **step 4**, instead of **master** I mention **repo-B-branch** – Kaushik Acharya May 04 '18 at 09:57
  • @KaushikAcharya Yes I think that would do it. You can try knowing that until you push you can just try again if it doesn't work. – mcarans May 04 '18 at 13:31
  • 1
    @mcarans **1.** I guess your answer is similar to [Greg Bayer's blog](https://gbayer.com/development/moving-files-from-one-git-repository-to-another-preserving-history/) **2.** I didn't run the 1st three commands in **Stage Two** I moved to the steps of moving files to new directory. **Do I need to also move the folder .git into the new directory** **3.** I didn't understood the prune step in **Stage Two** There are other branches present which I don't want to touch. – Kaushik Acharya May 07 '18 at 06:51
  • 1
    @mcarans Getting the following error: **fatal: Couldn't find remote ref repo-B-branch** on applying in step 4 of Stage Three, **git pull repo-A-branch repo-B-branch --allow-unrelated-histories** But **repo-B-branch** is present in _repo B_ – Kaushik Acharya May 07 '18 at 08:58
  • @KaushikAcharya The pruning steps reduce the size of the repo. The step "git remote rm origin" prevents messing up the original remote repo. (The local copy will be messed up though.) You don't need to move the .git folder. I'm not sure exactly what you did, but if you follow all the steps other than changing stage 3, step 4, making sure you create the branch using "git checkout -b repo-B-branch" after stage 3 step 2, does that work? – mcarans May 08 '18 at 08:43
  • Part 5 of Step One doesn't work. It gives a "WARNING: Ref 'refs/heads/master' is unchanged" error and all of the unwanted files are still in the directory and part of the repository. The command is so complicated I can't begin to parse it to figure out where it's going wrong. – TimeHorse Nov 14 '18 at 21:31
  • This worked: git filter-branch -f --tree-filter "find . -not -path './.git' -not -path './.git/*' -not -path './' -delete" --prune-empty – TimeHorse Nov 14 '18 at 22:30
  • 2
    @mcarans Unfortunately, this is NOT reliable way, although it seems to be. It suffers from the same problem as all other solutions - It doesn't retain history past rename. In my case, very first commit is when I renamed the directory/file. Everything beyond that is lost. – xZero Mar 05 '19 at 10:22
  • @xZero Did you manage to find a solution that will retain history past renaming? – mcarans Dec 18 '19 at 14:16
59

Yep, hitting on the --subdirectory-filter of filter-branch was key. The fact that you used it essentially proves there's no easier way - you had no choice but to rewrite history, since you wanted to end up with only a (renamed) subset of the files, and this by definition changes the hashes. Since none of the standard commands (e.g. pull) rewrite history, there's no way you could use them to accomplish this.

You could refine the details, of course - some of your cloning and branching wasn't strictly necessary - but the overall approach is good! It's a shame it's complicated, but of course, the point of git isn't to make it easy to rewrite history.

StackzOfZtuff
  • 1,671
  • 18
  • 19
Cascabel
  • 422,485
  • 65
  • 357
  • 307
  • 2
    what if your file has moved through several directories, and now resides in one--will subdirectory-filter still work? (i.e. I'm assuming that if I just want to move one file, I can move it to its own subdirectory and this will work?) – rogerdpack Apr 03 '12 at 16:17
  • 1
    @rogerdpack: No, this won't follow the file through renames. I believe it will appear to have been created at the point it was moved into the selected subdirectory. If you want to select just one file, have a look at `--index-filter` in the `filter-branch` manpage. – Cascabel Apr 03 '12 at 16:48
  • 11
    Is there any recipe on how I can follow renames? – Night Warrier Apr 01 '15 at 23:46
  • I think maintaining and curating history is one of the main points of git. – artburkart Feb 04 '20 at 10:43
  • 1
    About following renames: https://stackoverflow.com/questions/65220628/git-extract-directory-from-repository-commit-history-prior-to-renaming-is-miss (no answer yet, but hopefully there will be in the future) – Maciej Krawczyk Dec 09 '20 at 16:54
22

I found this very useful. It is a very simple approach where you create patches that are applied to the new repo. See the linked page for more details.

It only contains three steps (copied from the blog):

# Setup a directory to hold the patches
mkdir <patch-directory>

# Create the patches
git format-patch -o <patch-directory> --root /path/to/copy

# Apply the patches in the new repo using a 3 way merge in case of conflicts
# (merges from the other repo are not turned into patches). 
# The 3way can be omitted.
git am --3way <patch-directory>/*.patch

The only issue I had was that I could not apply all patches at once using

git am --3way <patch-directory>/*.patch

Under Windows I got an InvalidArgument error. So I had to apply all patches one after another.

darthbith
  • 13,602
  • 7
  • 55
  • 70
anhoppe
  • 3,441
  • 3
  • 41
  • 53
  • Didn't work for me as at some point sha-hashes were missing. This helped me: http://stackoverflow.com/questions/17371150/moving-git-repository-content-to-another-repository-preserving-history – dr0i Jan 11 '17 at 13:46
  • Unlike the "git log" approach, this option worked perfectly for me! thanks! – AlejandroVD Jan 17 '17 at 14:43
  • 1
    Tried different approaches for moving projects to new repo. This is the only one that worked for me. Can't believe that such a common task must be that complicated. – Chris_D_Turk Dec 17 '17 at 15:29
  • Thanks for sharing [Ross Hendrickson's blog](http://savorywatt.com/2015/01/25/move-files-and-folders-between-git-repos-using-patches/). This approach worked for me. – Kaushik Acharya May 12 '18 at 04:42
  • Wth the file in the root of the project, I found it necessary to use `--src-prefix=./ --dst-prefix=./` to avoid an error when attempting to apply the patches. – Ed Randall Feb 17 '19 at 14:54
  • 1
    This is very elegant solution, however, again, it suffers from the same issue as all other solutions - It will NOT retain history past rename. – xZero Mar 05 '19 at 10:28
9

The one I always use is here http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ . Simple and fast.

For compliance with stackoverflow standards, here is the procedure:

mkdir /tmp/mergepatchs
cd ~/repo/org
export reposrc=myfile.c #or mydir
git format-patch -o /tmp/mergepatchs $(git log $reposrc|grep ^commit|tail -1|awk '{print $2}')^..HEAD $reposrc
cd ~/repo/dest
git am /tmp/mergepatchs/*.patch
Hugh Perkins
  • 6,646
  • 6
  • 50
  • 63
  • if `git log` displays in color for you, the `grep ^commit` might not work. if so, add `--no-color` to that `git log` command. (e.g., `git log --no-color $reposrc`) – Kurt Sep 08 '20 at 17:38
8

KEEPING THE DIRECTORY NAME

The subdirectory-filter (or the shorter command git subtree) works good but did not work for me since they remove the directory name from the commit info. In my scenario I just want to merge parts of one repository into another and retain the history WITH full path name.

My solution was to use the tree-filter and to simply remove the unwanted files and directories from a temporary clone of the source repository, then pull from that clone into my target repository in 5 simple steps.

# 1. clone the source
git clone ssh://<user>@<source-repo url>
cd <source-repo>
# 2. remove the stuff we want to exclude
git filter-branch --tree-filter "rm -rf <files to exclude>" --prune-empty HEAD
# 3. move to target repo and create a merge branch (for safety)
cd <path to target-repo>
git checkout -b <merge branch>
# 4. Add the source-repo as remote 
git remote add source-repo <path to source-repo>
# 5. fetch it
git pull source-repo master
# 6. check that you got it right (better safe than sorry, right?)
gitk
Joachim Nilsson
  • 1,461
  • 12
  • 22
  • This [script](https://github.com/vangorra/git_split) will not make any modifications to your original repo. If the dest repo specified in the map file doesn't exist, then this script will try to create it. – Chetabahana Apr 09 '15 at 15:25
  • 2
    I think also that keeping the directory names intact is tremendously important. Otherwise you will get extra renaming commits to the target repository. – ipuustin Aug 21 '15 at 09:15
5

This answer provide interesting commands based on git am and presented using examples, step by step.

Objective

  • You want to move some or all files from one repository to another.
  • You want to keep their history.
  • But you do not care about keeping tags and branches.
  • You accept limited history for renamed files (and files in renamed directories).

Procedure

  1. Extract history in email format using
    git log --pretty=email -p --reverse --full-index --binary
  2. Reorganize file tree and update filename change in history [optional]
  3. Apply new history using git am

1. Extract history in email format

Example: Extract history of file3, file4 and file5

my_repo
├── dirA
│   ├── file1
│   └── file2
├── dirB            ^
│   ├── subdir      | To be moved
│   │   ├── file3   | with history
│   │   └── file4   | 
│   └── file5       v
└── dirC
    ├── file6
    └── file7

Clean the temporary directory destination

export historydir=/tmp/mail/dir  # Absolute path
rm -rf "$historydir"             # Caution when cleaning

Clean your the repo source

git commit ...           # Commit your working files
rm .gitignore            # Disable gitignore
git clean -n             # Simulate removal
git clean -f             # Remove untracked file
git checkout .gitignore  # Restore gitignore

Extract history of each file in email format

cd my_repo/dirB
find -name .git -prune -o -type d -o -exec bash -c 'mkdir -p "$historydir/${0%/*}" && git log --pretty=email -p --stat --reverse --full-index --binary -- "$0" > "$historydir/$0"' {} ';'

Unfortunately option --follow or --find-copies-harder cannot be combined with --reverse. This is why history is cut when file is renamed (or when a parent directory is renamed).

After: Temporary history in email format

/tmp/mail/dir
    ├── subdir
    │   ├── file3
    │   └── file4
    └── file5

2. Reorganize file tree and update filename change in history [optional]

Suppose you want to move these three files in this other repo (can be the same repo).

my_other_repo
├── dirF
│   ├── file55
│   └── file56
├── dirB              # New tree
│   ├── dirB1         # was subdir
│   │   ├── file33    # was file3
│   │   └── file44    # was file4
│   └── dirB2         # new dir
│        └── file5    # = file5
└── dirH
    └── file77

Therefore reorganize your files:

cd /tmp/mail/dir
mkdir     dirB
mv subdir dirB/dirB1
mv dirB/dirB1/file3 dirB/dirB1/file33
mv dirB/dirB1/file4 dirB/dirB1/file44
mkdir    dirB/dirB2
mv file5 dirB/dirB2

Your temporary history is now:

/tmp/mail/dir
    └── dirB
        ├── dirB1
        │   ├── file33
        │   └── file44
        └── dirB2
             └── file5

Change also filenames within the history:

cd "$historydir"
find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i "$0"' {} ';'

Note: This rewrites the history to reflect the change of path and filename.
      (i.e. the change of the new location/name within the new repo)


3. Apply new history

Your other repo is:

my_other_repo
├── dirF
│   ├── file55
│   └── file56
└── dirH
    └── file77

Apply commits from temporary history files:

cd my_other_repo
find "$historydir" -type f -exec cat {} + | git am 

Your other repo is now:

my_other_repo
├── dirF
│   ├── file55
│   └── file56
├── dirB            ^
│   ├── dirB1       | New files
│   │   ├── file33  | with
│   │   └── file44  | history
│   └── dirB2       | kept
│        └── file5  v
└── dirH
    └── file77

Use git status to see amount of commits ready to be pushed :-)

Note: As the history has been rewritten to reflect the path and filename change:
      (i.e. compared to the location/name within the previous repo)

  • No need to git mv to change the location/filename.
  • No need to git log --follow to access full history.

Extra trick: Detect renamed/moved files within your repo

To list the files having been renamed:

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'

More customizations: You can complete the command git log using options --find-copies-harder or --reverse. You can also remove the first two columns using cut -f3- and grepping complete pattern '{.* => .*}'.

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
oHo
  • 41,098
  • 25
  • 141
  • 183
4

This becomes simpler by using git-filter-repo.

In order to move project2/sub/dir to project1/sub/dir:

# Create a new repo containing only the subdirectory:
git clone project2 project2_subdir
cd project2_subdir
git filter-repo --force --path sub/dir

# Merge the new repo:
cd ../project1
git remote add project2_subdir ../project2_subdir/
git merge remotes/project2_subdir/master --allow-unrelated-histories
git remote remove project2_subdir

To install the tool simply: pip3 install git-filter-repo (more details and options in README)

# Before: (root)
.
|-- project1
|   `-- 3
`-- project2
    |-- 1
    `-- sub
        `-- dir
            `-- 2

# After: (project1)
.
├── 3
└── sub
    └── dir
        └── 2
Tapuzi
  • 131
  • 1
  • 5
  • Between the `git remote add` and the `git merge` you need to run `git fetch` to make the target repository aware of the changes in the source repository. – sanzante May 05 '21 at 14:05
3

Having had a similar itch to scratch (altough only for some files of a given repository) this script proved to be really helpful: git-import

The short version is that it creates patch files of the given file or directory ($object) from the existing repository:

cd old_repo
git format-patch --thread -o "$temp" --root -- "$object"

which then get applied to a new repository:

cd new_repo
git am "$temp"/*.patch 

For details please look up:

jskroch
  • 191
  • 3
  • 8
ViToni
  • 803
  • 8
  • 12
2

Try this

cd repo1

This will remove all the directories except the ones mentioned, preserving history only for these directories

git filter-branch --index-filter 'git rm --ignore-unmatch --cached -qr -- . && git reset -q $GIT_COMMIT -- dir1/ dir2/ dir3/ ' --prune-empty -- --all

Now you can add your new repo in your git remote and push it to that

git remote remove origin <old-repo>
git remote add origin <new-repo>
git push origin <current-branch>

add -f to overwrite

  • 1
    WARNING: git-filter-branch has a glut of gotchas generating mangled history rewrites. Hit Ctrl-C before proceeding to abort, then use an alternative filtering tool such as 'git filter-repo' (https://github.com/newren/git-filter-repo/) instead. See the filter-branch manual page for more details; to squelch this warning, set FILTER_BRANCH_SQUELCH_WARNING=1. – Colin May 19 '20 at 11:30
1

Using inspiration from http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ , I created this Powershell function for doing the same, which has worked great for me so far:

# Migrates the git history of a file or directory from one Git repo to another.
# Start in the root directory of the source repo.
# Also, before running this, I recommended that $destRepoDir be on a new branch that the history will be migrated to.
# Inspired by: http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/
function Migrate-GitHistory
{
    # The file or directory within the current Git repo to migrate.
    param([string] $fileOrDir)
    # Path to the destination repo
    param([string] $destRepoDir)
    # A temp directory to use for storing the patch file (optional)
    param([string] $tempDir = "\temp\migrateGit")

    mkdir $tempDir

    # git log $fileOrDir -- to list commits that will be migrated
    Write-Host "Generating patch files for the history of $fileOrDir ..." -ForegroundColor Cyan
    git format-patch -o $tempDir --root -- $fileOrDir

    cd $destRepoDir
    Write-Host "Applying patch files to restore the history of $fileOrDir ..." -ForegroundColor Cyan
    ls $tempDir -Filter *.patch  `
        | foreach { git am $_.FullName }
}

Usage for this example:

git clone project2
git clone project1
cd project1
# Create a new branch to migrate to
git checkout -b migrate-from-project2
cd ..\project2
Migrate-GitHistory "deeply\buried\java\source\directory\A" "..\project1"

After you've done this, you can re-organize the files on the migrate-from-project2 branch before merging it.

crimbo
  • 8,295
  • 6
  • 42
  • 54
1

I wanted something robust and reusable (one-command-and-go + undo function) so I wrote the following bash script. Worked for me on several occasions, so I thought I'd share it here.

It is able to move an arbitrary folder /path/to/foo from repo1 into /some/other/folder/bar to repo2 (folder paths can be the same or different, distance from root folder may be different).

Since it only goes over the commits that touch the files in input folder (not over all commits of the source repo), it should be quite fast even on big source repos, if you just extract a deeply nested subfolder that was not touched in every commit.

Since what this does is to create an orphaned branch with all the old repo's history and then merge it to the HEAD, it will even work in case of file name clashes (then you'd have to resolve a merge at the end of course).

If there are no file name clashes, you just need to git commit at the end to finalize the merge.

The downside is that it will likely not follow file renames (outside of REWRITE_FROM folder) in the source repo - pull requests welcome on GitHub to accommodate for that.

GitHub link: git-move-folder-between-repos-keep-history

#!/bin/bash

# Copy a folder from one git repo to another git repo,
# preserving full history of the folder.

SRC_GIT_REPO='/d/git-experimental/your-old-webapp'
DST_GIT_REPO='/d/git-experimental/your-new-webapp'
SRC_BRANCH_NAME='master'
DST_BRANCH_NAME='import-stuff-from-old-webapp'
# Most likely you want the REWRITE_FROM and REWRITE_TO to have a trailing slash!
REWRITE_FROM='app/src/main/static/'
REWRITE_TO='app/src/main/static/'

verifyPreconditions() {
    #echo 'Checking if SRC_GIT_REPO is a git repo...' &&
      { test -d "${SRC_GIT_REPO}/.git" || { echo "Fatal: SRC_GIT_REPO is not a git repo"; exit; } } &&
    #echo 'Checking if DST_GIT_REPO is a git repo...' &&
      { test -d "${DST_GIT_REPO}/.git" || { echo "Fatal: DST_GIT_REPO is not a git repo"; exit; } } &&
    #echo 'Checking if REWRITE_FROM is not empty...' &&
      { test -n "${REWRITE_FROM}" || { echo "Fatal: REWRITE_FROM is empty"; exit; } } &&
    #echo 'Checking if REWRITE_TO is not empty...' &&
      { test -n "${REWRITE_TO}" || { echo "Fatal: REWRITE_TO is empty"; exit; } } &&
    #echo 'Checking if REWRITE_FROM folder exists in SRC_GIT_REPO' &&
      { test -d "${SRC_GIT_REPO}/${REWRITE_FROM}" || { echo "Fatal: REWRITE_FROM does not exist inside SRC_GIT_REPO"; exit; } } &&
    #echo 'Checking if SRC_GIT_REPO has a branch SRC_BRANCH_NAME' &&
      { cd "${SRC_GIT_REPO}"; git rev-parse --verify "${SRC_BRANCH_NAME}" || { echo "Fatal: SRC_BRANCH_NAME does not exist inside SRC_GIT_REPO"; exit; } } &&
    #echo 'Checking if DST_GIT_REPO has a branch DST_BRANCH_NAME' &&
      { cd "${DST_GIT_REPO}"; git rev-parse --verify "${DST_BRANCH_NAME}" || { echo "Fatal: DST_BRANCH_NAME does not exist inside DST_GIT_REPO"; exit; } } &&
    echo '[OK] All preconditions met'
}

# Import folder from one git repo to another git repo, including full history.
#
# Internally, it rewrites the history of the src repo (by creating
# a temporary orphaned branch; isolating all the files from REWRITE_FROM path
# to the root of the repo, commit by commit; and rewriting them again
# to the original path).
#
# Then it creates another temporary branch in the dest repo,
# fetches the commits from the rewritten src repo, and does a merge.
#
# Before any work is done, all the preconditions are verified: all folders
# and branches must exist (except REWRITE_TO folder in dest repo, which
# can exist, but does not have to).
#
# The code should work reasonably on repos with reasonable git history.
# I did not test pathological cases, like folder being created, deleted,
# created again etc. but probably it will work fine in that case too.
#
# In case you realize something went wrong, you should be able to reverse
# the changes by calling `undoImportFolderFromAnotherGitRepo` function.
# However, to be safe, please back up your repos just in case, before running
# the script. `git filter-branch` is a powerful but dangerous command.
importFolderFromAnotherGitRepo(){
    SED_COMMAND='s-\t\"*-\t'${REWRITE_TO}'-'

    verifyPreconditions &&
    cd "${SRC_GIT_REPO}" &&
      echo "Current working directory: ${SRC_GIT_REPO}" &&
      git checkout "${SRC_BRANCH_NAME}" &&
      echo 'Backing up current branch as FILTER_BRANCH_BACKUP' &&
      git branch -f FILTER_BRANCH_BACKUP &&
      SRC_BRANCH_NAME_EXPORTED="${SRC_BRANCH_NAME}-exported" &&
      echo "Creating temporary branch '${SRC_BRANCH_NAME_EXPORTED}'..." &&
      git checkout -b "${SRC_BRANCH_NAME_EXPORTED}" &&
      echo 'Rewriting history, step 1/2...' &&
      git filter-branch -f --prune-empty --subdirectory-filter ${REWRITE_FROM} &&
      echo 'Rewriting history, step 2/2...' &&
      git filter-branch -f --index-filter \
       "git ls-files -s | sed \"$SED_COMMAND\" |
        GIT_INDEX_FILE=\$GIT_INDEX_FILE.new git update-index --index-info &&
        mv \$GIT_INDEX_FILE.new \$GIT_INDEX_FILE" HEAD &&
    cd - &&
    cd "${DST_GIT_REPO}" &&
      echo "Current working directory: ${DST_GIT_REPO}" &&
      echo "Adding git remote pointing to SRC_GIT_REPO..." &&
      git remote add old-repo ${SRC_GIT_REPO} &&
      echo "Fetching from SRC_GIT_REPO..." &&
      git fetch old-repo "${SRC_BRANCH_NAME_EXPORTED}" &&
      echo "Checking out DST_BRANCH_NAME..." &&
      git checkout "${DST_BRANCH_NAME}" &&
      echo "Merging SRC_GIT_REPO/" &&
      git merge "old-repo/${SRC_BRANCH_NAME}-exported" --no-commit &&
    cd -
}

# If something didn't work as you'd expect, you can undo, tune the params, and try again
undoImportFolderFromAnotherGitRepo(){
  cd "${SRC_GIT_REPO}" &&
    SRC_BRANCH_NAME_EXPORTED="${SRC_BRANCH_NAME}-exported" &&
    git checkout "${SRC_BRANCH_NAME}" &&
    git branch -D "${SRC_BRANCH_NAME_EXPORTED}" &&
  cd - &&
  cd "${DST_GIT_REPO}" &&
    git remote rm old-repo &&
    git merge --abort
  cd -
}

importFolderFromAnotherGitRepo
#undoImportFolderFromAnotherGitRepo
jakub.g
  • 30,051
  • 7
  • 78
  • 118
  • Thanks for this script, it really helped. Two minor fixes: 1. the sed expression fails in case REWRITE_TO contains a dash sign. For example "my-folder". Therefore, I modified it to use @ as a separator: `SED_COMMAND='s@\t\"*@\t'${REWRITE_TO}'@'` 2. In modern git, you must provide the --allow-unrelated-histories flag to merge: `git merge "old-repo/${SRC_BRANCH_NAME}-exported" --no-commit --allow-unrelated-histories &&` I hope it will help someone, Ori. – orid Nov 15 '20 at 16:51
0

In my case, I didn't need to preserve the repo I was migrating from or preserve any previous history. I had a patch of the same branch, from a different remote

#Source directory
git remote rm origin
#Target directory
git remote add branch-name-from-old-repo ../source_directory

In those two steps, I was able to get the other repo's branch to appear in the same repo.

Finally, I set this branch (that I imported from the other repo) to follow the target repo's mainline (so I could diff them accurately)

git br --set-upstream-to=origin/mainline

Now it behaved as-if it was just another branch I had pushed against that same repo.

Jason D
  • 6,955
  • 7
  • 32
  • 36
0

If the paths for the files in question are the same in the two repos and you're wanting to bring over just one file or a small set of related files, one easy way to do this is to use git cherry-pick.

The first step is to bring the commits from the other repo into your own local repo using git fetch <remote-url>. This will leave FETCH_HEAD pointing to the head commit from the other repo; if you want to preserve a reference to that commit after you've done other fetches you may want to tag it with git tag other-head FETCH_HEAD.

You will then need to create an initial commit for that file (if it doesn't exist) or a commit to bring the file to a state that can be patched with the first commit from the other repo you want to bring in. You may be able to do this with a git cherry-pick <commit-0> if commit-0 introduced the files you want, or you may need to construct the commit 'by hand'. Add -n to the cherry-pick options if you need to modify the initial commit to, e.g., drop files from that commit you don't want to bring in.

After that, you can continue to git cherry-pick subsequent commits, again using -n where necessary. In the simplest case (all commits are exactly what you want and apply cleanly) you can give the full list of commits on the cherry-pick command line: git cherry-pick <commit-1> <commit-2> <commit-3> ....

cjs
  • 21,799
  • 6
  • 79
  • 93
0

What I did:

  1. Clone repository to a folder
  2. cd existing-project
  3. open here a git terminal
  4. git remote set-url origin <NEW_GIT_URL>
  5. git push -u origin --all
  6. git push origin --tags
Tamás Lévai
  • 106
  • 1
  • 5
  • That would copy all files of the source repository to the new repository. Essentially, it creates a copy o f the complete source repository. OP wants only parts of the original repository. – sanzante May 05 '21 at 09:18
-2

The below method to migrate my GIT Stash to GitLab by maintaining all branches and preserving history.

Clone the old repository to local.

git clone --bare <STASH-URL>

Create an empty repository in GitLab.

git push --mirror <GitLab-URL>

The above I performed when we migrated our code from stash to GitLab and it worked very well.