私たちのGitリポジトリは、個々のプロジェクトがそれぞれ独自のツリーを持つような単一のモンスターSVNリポジトリの一部として始まりました。
project1/branches
/tags
/trunk
project2/branches
/tags
/trunk
明らかに、svn mv
を使ってファイルを移動するのはとても簡単でした。しかしGitでは、各プロジェクトはそれ自身のリポジトリにあり、そして今日私はproject2
からproject1
にサブディレクトリを移動するように頼まれました。私はこのようなことをしました:
$ 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
しかし、それはかなり複雑に思えます。この種のことを一般的に行うためのより良い方法はありますか?それとも、私は正しいアプローチを採用しましたか?
これには、単に別のリポジトリの一部から新しいスタンドアロンリポジトリを作成するのではなく、履歴を既存のリポジトリにマージすることが含まれます( 前の質問のように )。
うん、--subdirectory-filter
のfilter-branch
を打つのがキーでした。あなたがそれを使ったという事実は本質的にもっと簡単な方法がないことを証明します - あなたはファイルの(名前を変えられた)サブセットだけに終わってしまいたかったので歴史を書き直すしかないのです。標準のコマンド(例:pull
)は履歴を書き換えないので、これを実現するためにそれらを使用することはできません。
もちろん、細部を洗練することもできます - クローン作成や分岐のいくつかは厳密には必要ではありませんでした - しかし、全体的なアプローチは良いです!それはそれが複雑であることは残念ですが、もちろん、gitのポイントは歴史を書き換えやすくすることではありません。
あなたの歴史が正しければ、あなたはパッチとしてコミットを取り除き、それらを新しいリポジトリに適用することができます:
cd repository
git log --pretty=email --patch-with-stat --reverse --full-index --binary -- path/to/file_or_folder > patch
cd ../another_repository
git am < ../repository/patch
または一行で
git log --pretty=email --patch-with-stat --reverse -- path/to/file_or_folder | (cd /path/to/new_repository && git am)
( Exherboのドキュメント から取得)
あるGitリポジトリから別のGitリポジトリにファイルやフォルダを移動するためにさまざまな方法を試してみましたが、確実に機能すると思われる唯一の方法を以下に概説します。
ファイルまたはフォルダの移動元のリポジトリのクローン作成、そのファイルまたはフォルダのルートへの移動、Git履歴の書き換え、ターゲットリポジトリのクローン作成、履歴付きのファイルまたはフォルダのこのターゲットリポジトリへの直接コピーを行います。
次の手順でこのコピーに大きな変更を加えたため、リポジトリAのコピーを作成してください。プッシュしてはいけません。
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)
それにcd
cd <git repository A directory>
# eg. cd /c/Working/GIT/myprojects
誤ってリモートから変更を加えないようにするために、元のリポジトリへのリンクを削除してください(例:プッシュ)。
git remote rm Origin
履歴とファイルを調べて、ディレクトリ1にないものをすべて削除します。その結果、ディレクトリ1の内容がリポジトリAのベースに展開されます。
git filter-branch --subdirectory-filter <directory> -- --all
# eg. git filter-branch --subdirectory-filter subfolder1/subfolder2/FOLDER_TO_KEEP -- --all
単一ファイル移動のみの場合:残りの部分を調べて、目的のファイル以外のすべてを削除します。 (同じ名前の不要なファイルを削除してコミットする必要があるかもしれません。)
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
クリーンアップステップ
git reset --hard
クリーンアップステップ
git gc --aggressive
クリーンアップステップ
git Prune
これらのファイルをルートではなくディレクトリ内のリポジトリBにインポートすることができます。
そのディレクトリを作る
mkdir <base directory> eg. mkdir FOLDER_TO_KEEP
ファイルをそのディレクトリに移動します
git mv * <base directory> eg. git mv * FOLDER_TO_KEEP
そのディレクトリにファイルを追加する
git add .
変更を確定すると、これらのファイルを新しいリポジトリにマージする準備が整いました
git commit
まだ持っていない場合は、リポジトリBのコピーを作成してください。
git clone <git repository B url>
# eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git
(FOLDER_TO_KEEPがコピー先の新しいリポジトリの名前であると仮定します)
それにcd
cd <git repository B directory>
# eg. cd /c/Working/GIT/FOLDER_TO_KEEP
リポジトリBへのブランチとしてリポジトリAへのリモート接続を作成します。
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
このブランチ(移動したいディレクトリーのみを含む)からリポジトリーBに引き出します。
git pull repo-A-branch master --allow-unrelated-histories
Pullはファイルと履歴の両方をコピーします。注:プルの代わりにマージを使用できますが、プルの方がうまく機能します。
最後に、リポジトリAへのリモート接続を削除して、少しクリーンアップしたいと思うでしょう。
git remote rm repo-A-branch
プッシュするだけで、すべての設定は完了です。
git Push
this とても便利です。新しいレポジトリに適用されるパッチを作成するのはとても簡単な方法です。詳しくはリンク先のページをご覧ください。
それは3つのステップだけを含みます(ブログからコピーされた):
# 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
私が持っていた唯一の問題は私が一度にすべてのパッチを適用することができなかったということでした
git am --3way <patch-directory>/*.patch
Windowsでは、InvalidArgumentエラーが発生しました。だから私はすべてのパッチを次々に適用しなければなりませんでした。
ディレクトリ名をキープする
Subdirectory-filter(またはより短いコマンドgit subtree)はうまくいきますが、コミット情報からディレクトリ名を削除するので私にはうまくいきませんでした。私のシナリオでは、あるリポジトリの一部を別のリポジトリにマージし、履歴WITHフルパス名を保持したいだけです。
私の解決策は、ツリーフィルタを使用し、ソースリポジトリの一時的なクローンから不要なファイルやディレクトリを削除し、次にそのクローンから5つの簡単な手順でターゲットリポジトリに移動することでした。
# 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
この答えは git am
に基づいて、そして例を使って段階的に提示された興味深いコマンドを提供します。
git log --pretty=email -p --reverse --full-index --binary
git am
を使って新しい履歴を適用する例:file3
、file4
およびfile5
の履歴の抽出
my_repo
├── dirA
│ ├── file1
│ └── file2
├── dirB ^
│ ├── subdir | To be moved
│ │ ├── file3 | with history
│ │ └── file4 |
│ └── file5 v
└── dirC
├── file6
└── file7
一時ディレクトリの宛先を消去します
export historydir=/tmp/mail/dir # Absolute path
rm -rf "$historydir" # Caution when cleaning
リポジトリのソースを削除します
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
各ファイルの履歴を電子メール形式で抽出する
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"' {} ';'
残念ながら、オプション--follow
または--find-copies-harder
を--reverse
と組み合わせることはできません。これが、ファイルの名前が変更されたとき(または親ディレクトリの名前が変更されたとき)に履歴が切り捨てられる理由です。
変更後:電子メール形式の一時的な履歴
/tmp/mail/dir
├── subdir
│ ├── file3
│ └── file4
└── file5
この3つのファイルをこの別のリポジトリに移動するとします(同じリポジトリにすることができます)。
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
したがって、ファイルを再編成してください。
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
あなたの一時的な歴史は今です:
/tmp/mail/dir
└── dirB
├── dirB1
│ ├── file33
│ └── file44
└── dirB2
└── file5
履歴内のファイル名も変更します。
cd "$historydir"
find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i "$0"' {} ';'
注:パスとファイル名の変更を反映するように履歴を書き換えます。
(つまり、新しいリポジトリ内の新しい場所/名前の変更)
あなたの他のレポは:
my_other_repo
├── dirF
│ ├── file55
│ └── file56
└── dirH
└── file77
一時履歴ファイルからコミットを適用します。
cd my_other_repo
find "$historydir" -type f -exec cat {} + | git am
あなたの他のレポは今です:
my_other_repo
├── dirF
│ ├── file55
│ └── file56
├── dirB ^
│ ├── dirB1 | New files
│ │ ├── file33 | with
│ │ └── file44 | history
│ └── dirB2 | kept
│ └── file5 v
└── dirH
└── file77
コミットされたコミットの量を確認するにはgit status
を使用してください:-)
注:履歴はパスとファイル名の変更を反映するように書き直されました。
(すなわち、前のリポジトリ内の場所/名前と比較)
git mv
は必要ありません。git log --follow
を使用する必要はありません。名前が変更されたファイルを一覧表示するには
find -name .git -Prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'
その他のカスタマイズ:オプションgit log
または--find-copies-harder
を使用して、コマンド--reverse
を完成させることができます。 cut -f3-
と完全パターン '{。* =>。*}'を使用して最初の2つの列を削除することもできます。
find -name .git -Prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
私がいつも使用しているものはここにあります http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ 。シンプルで速い.
スタックオーバーフロー標準に準拠するために、次の手順に従います。
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
スクラッチに似たかゆみがあったこと(ある特定のリポジトリのいくつかのファイルについてだけですが)、このスクリプトは本当に役に立ちました: git-import
簡単に言うと、既存のリポジトリから、指定されたファイルまたはディレクトリ($object
)のパッチファイルを作成します。
cd old_repo
git format-patch --thread -o "$temp" --root -- "$object"
これは新しいリポジトリに適用されます。
cd new_repo
git am "$temp"/*.patch
詳細は見てください:
これを試して
cd repo1
これは、言及されたもの以外のすべてのディレクトリを削除し、これらのディレクトリの履歴のみを保存します。
git filter-branch --index-filter 'git rm --ignore-unmatch --cached -qr -- . && git reset -q $GIT_COMMIT -- dir1/ dir2/ dir3/ ' --Prune-empty -- --all
今、あなたはあなたのgitリモコンにあなたの新しいレポを追加し、それにそれをプッシュすることができます
git remote remove Origin <old-repo>
git remote add Origin <new-repo>
git Push Origin <current-branch>
上書きする-f
を追加
堅牢で再利用可能なもの(ワンコマンドアンドゴー+元に戻す機能)が欲しいので、次のbashスクリプトを書きました。何度か仕事をしてくれたので、ここで共有したいと思いました。
任意のフォルダ/path/to/foo
をrepo1
から/some/other/folder/bar
からrepo2
に移動することができます(フォルダパスは同じでも異なっていてもよく、ルートフォルダからの距離は異なっていてもかまいません)。
入力フォルダー内のファイルに触れるコミットのみを対象とするため(ソースリポジトリのすべてのコミットを対象とするわけではありません)、すべてのフォルダーに触れられていない深くネストされたサブフォルダーを抽出すれば、かなり高速になります。コミット。
これは、古いリポジトリの履歴をすべて使って孤立したブランチを作成し、それをHEADにマージすることなので、ファイル名が衝突した場合でも機能します(そして、最後にマージを解決する必要があります)。 。
ファイル名の衝突がない場合は、最後にgit commit
を実行してマージを完了させる必要があります。
欠点は、それに対応するためにGitHubで歓迎されているソースリポジトリ内のファイル名の変更(REWRITE_FROM
フォルダーの外側)には追いつかないだろうということです。
GitHubリンク: git-move-folder-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
http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ からのインスピレーションを使って、私はこのPowershell関数を作成しました同じことをしてくれたことは、これまで私にとって非常にうまくいっています。
# 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 }
}
この例の使用法
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"
これが終わったら、マージする前にmigrate-from-project2
ブランチのファイルを再編成することができます。
問題のファイルのパスが2つのレポジトリで同じで、1つのファイルまたは少数の関連ファイルだけを持ってきたい場合は、git cherry-pick
を使用するのが簡単な方法です。
最初のステップはgit fetch <remote-url>
を使って他のリポジトリからのコミットをあなた自身のローカルリポジトリに持ってくることです。これはFETCH_HEAD
が他のリポジトリからのヘッドコミットを指しているままにします。他のフェッチを行った後でそのコミットへの参照を保持したい場合は、それにgit tag other-head FETCH_HEAD
というタグを付けます。
次に、そのファイルの初期コミット(存在しない場合)を作成するか、ファイルを他のリポジトリからの最初のコミットでパッチできる状態にするためのコミットを作成する必要があります。もしgit cherry-pick <commit-0>
があなたが望むファイルを紹介したのであればcommit-0
でこれを行うことができます、あるいはあなたは '手で'コミットを構築する必要があるかもしれません。初期コミットを変更する必要がある場合、チェリーピックオプションに-n
を追加します。
その後は、必要に応じてgit cherry-pick
を使用して、-n
後続のコミットを続けることができます。最も単純な場合(すべてのコミットがまさにあなたが望むものであり、きれいに適用される)、チェリーピックコマンドラインでコミットの完全なリストを与えることができます:git cherry-pick <commit-1> <commit-2> <commit-3> ...
。
私の場合は、自分が移行していたリポジトリを保存したり、以前の履歴を保存したりする必要はありませんでした。別のリモートから同じブランチのパッチを入手しました
#Source directory
git remote rm Origin
#Target directory
git remote add branch-name-from-old-repo ../source_directory
これら2つのステップで、私は他のリポジトリのブランチを同じリポジトリに表示することができました。
最後に、私は(私が他のリポジトリからインポートした)このブランチをターゲットリポジトリのメインラインに従うように設定しました。
git br --set-upstream-to=Origin/mainline
今はまるでそれが私が同じレポに対して押した別のブランチであるかのように振る舞いました。