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 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).
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
fromrepo1
into/some/other/folder/bar
torepo2
(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
If your history is sane, you can take the commits out as patch and apply them in the new repository:
Or in one line
(Taken from Exherbo’s docs)
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 leaveFETCH_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 withgit 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>
ifcommit-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> ...
.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):
The only issue I had was that I could not apply all patches at once using
Under Windows I got an InvalidArgument error. So I had to apply all patches one after another.
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:
This answer provide interesting commands based on
git am
and presented using examples, step by step.Objective
Procedure
git log --pretty=email -p --reverse --full-index --binary
git am
1. Extract history in email format
Example: Extract history of
file3
,file4
andfile5
Clean the temporary directory destination
Clean your the repo source
Extract history of each file in email format
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
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).
Therefore reorganize your files:
Your temporary history is now:
Change also filenames within the history:
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:
Apply commits from temporary history files:
Your other repo is now:
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)
git mv
to change the location/filename.git log --follow
to access full history.Extra trick: Detect renamed/moved files within your repo
To list the files having been renamed:
More customizations: You can complete the command
git log
using options--find-copies-harder
or--reverse
. You can also remove the first two columns usingcut -f3-
and grepping complete pattern '{.* => .*}'.