Detach (move) subdirectory into separate Git repos

2018-12-31 00:14发布

I have a Git repository which contains a number of subdirectories. Now I have found that one of the subdirectories is unrelated to the other and should be detached to a separate repository.

How can I do this while keeping the history of the files within the subdirectory?

I guess I could make a clone and remove the unwanted parts of each clone, but I suppose this would give me the complete tree when checking out an older revision etc. This might be acceptable, but I would prefer to be able to pretend that the two repositories doesn't have a shared history.

Just to make it clear, I have the following structure:

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

But I would like this instead:

XYZ/
    .git/
    XY1/
    XY2/
ABC/
    .git/
    ABC/

22条回答
千与千寻千般痛.
2楼-- · 2018-12-31 00:36

Here is a small modification to CoolAJ86's "The Easy Way™" answer in order to split multiple sub folders (let's say sub1and sub2) into a new git repository.

The Easy Way™ (multiple sub folders)

  1. Prepare the old repo

    pushd <big-repo>
    git filter-branch --tree-filter "mkdir <name-of-folder>; mv <sub1> <sub2> <name-of-folder>/" HEAD
    git subtree split -P <name-of-folder> -b <name-of-new-branch>
    popd
    

    Note: <name-of-folder> must NOT contain leading or trailing characters. For instance, the folder named subproject MUST be passed as subproject, NOT ./subproject/

    Note for windows users: when your folder depth is > 1, <name-of-folder> must have *nix style folder separator (/). For instance, the folder named path1\path2\subproject MUST be passed as path1/path2/subproject. Moreover don't use mvcommand but move.

    Final note: the unique and big difference with the base answer is the second line of the script "git filter-branch..."

  2. Create the new repo

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Link the new repo to Github or wherever

    git remote add origin <git@github.com:my-user/new-repo.git>
    git push origin -u master
    
  4. Cleanup, if desired

    popd # get out of <new-repo>
    pushd <big-repo>
    
    git rm -rf <name-of-folder>
    

    Note: This leaves all the historical references in the repository.See the Appendix in the original answer if you're actually concerned about having committed a password or you need to decreasing the file size of your .git folder.

查看更多
初与友歌
3楼-- · 2018-12-31 00:37

The original question wants XYZ/ABC/(*files) to become ABC/ABC/(*files). After implementing the accepted answer for my own code, I noticed that it actually changes XYZ/ABC/(*files) into ABC/(*files). The filter-branch man page even says,

The result will contain that directory (and only that) as its project root."

In other words, it promotes the top-level folder "up" one level. That's an important distinction because, for example, in my history I had renamed a top-level folder. By promoting folders "up" one level, git loses continuity at the commit where I did the rename.

I lost contiuity after filter-branch

My answer to the question then is to make 2 copies of the repository and manually delete the folder(s) you want to keep in each. The man page backs me up with this:

[...] avoid using [this command] if a simple single commit would suffice to fix your problem

查看更多
骚的不知所云
4楼-- · 2018-12-31 00:38

Edit: Bash script added.

The answers given here worked just partially for me; Lots of big files remained in the cache. What finally worked (after hours in #git on freenode):

git clone --no-hardlinks file:///SOURCE /tmp/blubb
cd blubb
git filter-branch --subdirectory-filter ./PATH_TO_EXTRACT  --prune-empty --tag-name-filter cat -- --all
git clone file:///tmp/blubb/ /tmp/blooh
cd /tmp/blooh
git reflog expire --expire=now --all
git repack -ad
git gc --prune=now

With the previous solutions, the repository size was around 100 MB. This one brought it down to 1.7 MB. Maybe it helps somebody :)


The following bash script automates the task:

!/bin/bash

if (( $# < 3 ))
then
    echo "Usage:   $0 </path/to/repo/> <directory/to/extract/> <newName>"
    echo
    echo "Example: $0 /Projects/42.git first/answer/ firstAnswer"
    exit 1
fi


clone=/tmp/${3}Clone
newN=/tmp/${3}

git clone --no-hardlinks file://$1 ${clone}
cd ${clone}

git filter-branch --subdirectory-filter $2  --prune-empty --tag-name-filter cat -- --all

git clone file://${clone} ${newN}
cd ${newN}

git reflog expire --expire=now --all
git repack -ad
git gc --prune=now
查看更多
伤终究还是伤i
5楼-- · 2018-12-31 00:41

I recommend GitHub's guide to splitting subfolders into a new repository. The steps are similar to Paul's answer, but I found their instructions easier to understand.

I have modified the instructions so that they apply for a local repository, rather than one hosted on GitHub.


Splitting a subfolder out into a new repository

  1. Open Git Bash.

  2. Change the current working directory to the location where you want to create your new repository.

  3. Clone the repository that contains the subfolder.

git clone OLD-REPOSITORY-FOLDER NEW-REPOSITORY-FOLDER
  1. Change the current working directory to your cloned repository.

cd REPOSITORY-NAME
  1. To filter out the subfolder from the rest of the files in the repository, run git filter-branch, supplying this information:
    • FOLDER-NAME: The folder within your project that you'd like to create a separate repository from.
      • Tip: Windows users should use / to delimit folders.
    • BRANCH-NAME: The default branch for your current project, for example, master or gh-pages.

git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME  BRANCH-NAME 
# Filter the specified branch in your directory and remove empty commits
Rewrite 48dc599c80e20527ed902928085e7861e6b3cbe6 (89/89)
Ref 'refs/heads/BRANCH-NAME' was rewritten
查看更多
皆成旧梦
6楼-- · 2018-12-31 00:44

It appears that most (all?) of the answers here rely on some form of git filter-branch --subdirectory-filter and its ilk. This may work "most times" however for some cases, for instance the case of when you renamed the folder, ex:

 ABC/
    /move_this_dir # did some work here, then renamed it to

ABC/
    /move_this_dir_renamed

If you do a normal git filter style to extract "move_me_renamed" you will lose file change history that occurred from back when it was initially move_this_dir (ref).

It thus appears that the only way to really keep all change history (if yours is a case like this), is, in essence, to copy the repository (create a new repo, set that to be the origin), then nuke everything else and rename the subdirectory to the parent like this:

  1. Clone the multi-module project locally
  2. Branches - check what's there: git branch -a
  3. Do a checkout to each branch to be included in the split to get a local copy on your workstation: git checkout --track origin/branchABC
  4. Make a copy in a new directory: cp -r oldmultimod simple
  5. Go into the new project copy: cd simple
  6. Get rid of the other modules that aren't needed in this project:
  7. git rm otherModule1 other2 other3
  8. Now only the subdir of the target module remains
  9. Get rid of the module subdir so that the module root becomes the new project root
  10. git mv moduleSubdir1/* .
  11. Delete the relic subdir: rmdir moduleSubdir1
  12. Check changes at any point: git status
  13. Create the new git repo and copy its URL to point this project into it:
  14. git remote set-url origin http://mygithost:8080/git/our-splitted-module-repo
  15. Verify this is good: git remote -v
  16. Push the changes up to the remote repo: git push
  17. Go to the remote repo and check it's all there
  18. Repeat it for any other branch needed: git checkout branch2

This follows the github doc "Splitting a subfolder out into a new repository" steps 6-11 to push the module to a new repo.

This will not save you any space in your .git folder, but it will preserve all your change history for those files even across renames. And this may not be worth it if there isn't "a lot" of history lost, etc. But at least you are guaranteed not to lose older commits!

查看更多
ら面具成の殇う
7楼-- · 2018-12-31 00:44

The Easier Way

  1. install git splits. I created it as a git extension, based on jkeating's solution.
  2. Split the directories into a local branch #change into your repo's directory cd /path/to/repo #checkout the branch git checkout XYZ
    #split multiple directories into new branch XYZ git splits -b XYZ XY1 XY2

  3. Create an empty repo somewhere. We'll assume we've created an empty repo called xyz on GitHub that has path : git@github.com:simpliwp/xyz.git

  4. Push to the new repo. #add a new remote origin for the empty repo so we can push to the empty repo on GitHub git remote add origin_xyz git@github.com:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master

  5. Clone the newly created remote repo into a new local directory
    #change current directory out of the old repo cd /path/to/where/you/want/the/new/local/repo #clone the remote repo you just pushed to git clone git@github.com:simpliwp/xyz.git

查看更多
登录 后发表回答