繁体   English   中英

GIT 拆分存储库目录保留*移动/重命名*历史

[英]GIT Split Repository directory preserving *move / renames* history

假设您有存储库:

myCode/megaProject/moduleA
myCode/megaProject/moduleB

随着时间的推移(几个月),您重新组织项目。 重构代码使模块独立。 megaProject 目录中的文件被移动到它们自己的目录中。 强调移动- 这些文件的历史被保留。

myCode/megaProject
myCode/moduleA
myCode/moduleB

现在您希望将这些模块移动到它们自己的 GIT 存储库中。 只留下 megaProject 的原件。

myCode/megaProject
newRepoA/moduleA
newRepoB/moduleB

filter-branch命令被记录为执行此操作,但当文件移出目标目录时它不会遵循历史记录。 所以当文件被移动到他们的新目录时,历史就开始了,而不是文件在旧的 megaProject 目录中的历史。

如何根据目标目录拆分 GIT 历史记录,并遵循此路径之外的历史记录 - 仅留下与这些文件相关的提交历史记录而不留下其他任何内容?

关于 SO 的许多其他答案都集中在通常将回购分开 - 但没有提到分开并遵循移动历史。

这是一个基于@rksawyer 脚本的版本,但它使用git-filter-repo代替。 我发现它比 git-filter-branch(现在被 git 推荐作为替代品)更容易使用并且更快。

# This script should run in the same folder as the project folder is.
# This script uses git-filter-repo (https://github.com/newren/git-filter-repo).
# The list of files and folders that you want to keep should be named <your_repo_folder_name>_KEEP.txt. I should contain a line end in the last line, otherwise the last file/folder will be skipped.
# The result will be the folder called <your_repo_folder_name>_REWRITE_CLONE. Your original repo won't be changed.
# Tags are not preserved, see line below to preserve tags.
# Running subsequent times will backup the last run in <your_repo_folder_name>_REWRITE_CLONE_BKP.

# Define here the name of the folder containing the repo: 
GIT_REPO="git-test-orig"

clone="$GIT_REPO"_REWRITE_CLONE
temp=/tmp/git_rewrite_temp
rm -Rf "$clone"_BKP
mv "$clone" "$clone"_BKP
rm -Rf "$temp"
mkdir "$temp"
git clone "$GIT_REPO" "$clone"
cd "$clone"
git remote remove origin
open .
open "$temp"

# Comment line below to preserve tags
git tag | xargs git tag -d

echo 'Start logging file history...'
echo "# git log results:\n" > "$temp"/log.txt

while read p
do
    shopt -s dotglob
    find "$p" -type f > "$temp"/temp
    while read f
    do
        echo "## " "$f" >> "$temp"/log.txt
        # print every file and follow to get any previous renames
        # Then remove blank lines.  Then remove every other line to end up with the list of filenames       
        git log --pretty=format:'%H' --name-only --follow -- "$f" | awk 'NF > 0' | awk 'NR%2==0' | tee -a "$temp"/log.txt
        
        echo "\n\n" >> "$temp"/log.txt
    done < "$temp"/temp
done < ../"$GIT_REPO"_KEEP.txt > "$temp"/PRESERVE

mv "$temp"/PRESERVE "$temp"/PRESERVE_full
awk '!a[$0]++' "$temp"/PRESERVE_full > "$temp"/PRESERVE

sort -o "$temp"/PRESERVE "$temp"/PRESERVE

echo 'Starting filter-branch --------------------------'
git filter-repo --paths-from-file "$temp"/PRESERVE --force --replace-refs delete-no-add
echo 'Finished filter-branch --------------------------'

它将git log的结果记录到/tmp/git_rewrite_temp/log.txt中的一个文件中,因此如果您不需要 log.txt 并希望它运行得更快,您可以去掉这些行。

在克隆的存储库中运行git filter-branch --subdirectory-filter将删除所有不影响该子目录中内容的提交,其中包括那些影响文件移动之前的提交。

相反,您需要使用带有脚本的--index-filter标志来删除您不感兴趣的所有文件,并使用--prune-empty标志来忽略任何影响其他内容的提交。

Kevin Deldycke 的博客文章中有一个很好的例子:

git filter-branch --prune-empty --tree-filter 'find ./ -maxdepth 1 -not -path "./e107*" -and -not -path "./wordpress-e107*" -and -not -path "./.git" -and -not -path "./" -print -exec rm -rf "{}" \;' -- --all

此命令有效地依次检查每个提交,从工作目录中删除所有无用的文件,如果自上次提交以来有任何更改,则将其签入(重写历史记录)。 您需要调整该命令以删除所有文件,例如/moduleA/megaProject/moduleA中的文件以及您希望从/megaProject中保留的特定文件。

我知道没有简单的方法可以做到这一点,但是可以做到。

filter-branch的问题在于它的工作原理

在每个修订版上应用自定义过滤器

如果您可以创建一个不会删除您的文件的过滤器,它们将在目录之间进行跟踪。 当然,对于任何不平凡的存储库来说,这可能都是不平凡的。

首先:让我们假设它是一个普通的存储库。 您从未重命名过文件,也从未在两个模块中拥有同名文件。 您需要做的就是获取模块中的文件列表find megaProject/moduleA -type f -printf "%f\n" > preserve然后使用这些文件名和您的目录运行过滤器:

保存.sh

cmd="find . -type f ! -name d1"
while read f; do
  cmd="$cmd ! -name $f"
done < /path/to/myCode/preserve
for i in $($cmd)
do
  rm $i
done

git filter-branch --prune-empty --tree-filter '/path/to/myCode/preserve.sh' HEAD

当然,重命名使这变得困难。 git filter-branch做的一件好事是给你$GIT_COMMIT环境变量。 然后你可以花哨并使用类似的东西:

for f in megaProject/moduleA
do
 git log --pretty=format:'%H' --name-only --follow -- $f |  awk '{ if($0 != ""){ printf $0 ":"; next; } print; }'
done > preserve

通过提交构建一个文件名历史记录,可以用来代替简单示例中的简单preserve文件,但是您有责任跟踪每次提交时应该出现哪些文件。 这实际上不应该太难编码,但我还没有看到有人这样做过。

继续上面的答案。 首先遍历使用 git log --follow 保留的目录中的所有文件,以 git 之前移动/重命名的旧路径/名称。 然后使用 filter-branch 遍历每个修订,删除不在步骤 1 中创建的列表中的任何文件。

#!/bin/bash
DIRNAME=dirD

# Catch all files including hidden files
shopt -s dotglob
for f in $DIRNAME/*
do
# print every file and follow to get any previous renames
# Then remove blank lines.  Then remove every other line to end up with the list of filenames
 git log --pretty=format:'%H' --name-only --follow -- $f | awk 'NF > 0' | awk 'NR%2==0'
done > /tmp/PRESERVE

sort -o /tmp/PRESERVE /tmp/PRESERVE
cat /tmp/PRESERVE

然后创建一个脚本 (preserve.sh),filter-branch 将调用每个修订版。

#!/bin/bash
DIRNAME=dirD

# Delete everything that's not in the PRESERVE list
echo 'delete this files:'
cmd=`find . -type f -not -path './.git/*' -not -path './$DIRNAME/*'`
echo $cmd > /tmp/ALL


# Convert to one filename per line and remove the lead ./
cat /tmp/ALL | awk '{NF++;while(NF-->1)print $NF}' | cut -c3- > /tmp/ALL2
sort -o /tmp/ALL2 /tmp/ALL2

#echo 'before:'
#cat /tmp/ALL2

comm -23 /tmp/ALL2 /tmp/PRESERVE > /tmp/DELETE_THESE
echo 'delete these:'
cat /tmp/DELETE_THESE
#exit 0

while read f; do
  rm $f
done < /tmp/DELETE_THESE

现在使用 filter-branch,如果在修订版中删除了所有文件,则修剪该提交及其消息。

 git filter-branch --prune-empty --tree-filter '/FULL_PATH/preserve.sh' master

这是我为 linux/wsl 编写的@Roberto 发布的脚本版本。 如果您不指定“myrepo_KEEP.txt”,它将根据当前文件结构创建一个。 传递 repo 以处理:

prune.sh MyRepo

# This script should run one level up from the git repo folder (i.e. the  containing folder)
# This script uses git-filter-repo (github.com/newren/git-filter-repo).
# The result will be the folder called <your_repo_folder_name>_REWRITE_CLONE. Your original repo won't be changed.
# Tags are not preserved, see line below to preserve tags.
# Running subsequent times will backup the last run in <your_repo_folder_name>_REWRITE_CLONE_BKP.
# Optionally, list the files and folders that you want to keep the KEEP_FILE (<your_repo_folder_name>_KEEP.txt) 
## It should contain a line end in the last line, otherwise the last file/folder will be skipped.
## If this file is missing it will be created by this script with all current folders listed. 

echo "Prune git repo"

# User needs to pass in the repo name
GIT_REPO=$1

if [ -z $GIT_REPO ]; then
    echo "Pass in the directory to prune"
else
    KEEP_FILE="${GIT_REPO}"_KEEP.txt

    # Build up a list of current directories in the repo, if one hasn't been supplied
    if [ ! -f "${KEEP_FILE}" ]; then
        echo "Keeping all current files in repo (generating keep file)"
        cd $GIT_REPO
        find . -type d -not -path '*/\.*' > "../${KEEP_FILE}"
        cd ..
    fi

    echo "Pruning $GIT_REPO"

    clone="${GIT_REPO}_REWRITE_CLONE"
    
    # Shift backup
    bkp="${clone}_BKP"
    temp=/tmp/git_rewrite_temp
    echo $clone
    rm -Rf "$bkp"
    mv "$clone" "$bkp"
    
    # Setup temp
    rm -Rf "$temp"
    mkdir "$temp"   
    
    # Clone
    echo "Cloning repo...from $GIT_REPO to $clone"
    if git clone "$GIT_REPO" "$clone"; then
        cd "$clone"
        git remote remove origin

        # Comment line below to preserve tags
        git tag | xargs git tag -d

        echo 'Start logging file history...'
        echo "# git log results:\n" > "$temp"/log.txt

        # Follow the renames
        while read p
        do
            shopt -s dotglob
            find "$p" -type f > "$temp"/temp
            while read f
            do
                echo "## " "$f" >> "$temp"/log.txt
                # print every file and follow to get any previous renames
                # Then remove blank lines.  Then remove every other line to end up with the list of filenames       
                git log --pretty=format:'%H' --name-only --follow -- "$f" | awk 'NF > 0' | awk 'NR%2==0' | tee -a "$temp"/log.txt

                echo "\n\n" >> "$temp"/log.txt
            done < "$temp"/temp
        done < ../"${KEEP_FILE}" > "$temp"/PRESERVE

        mv "$temp"/PRESERVE "$temp"/PRESERVE_full
        awk '!a[$0]++' "$temp"/PRESERVE_full > "$temp"/PRESERVE

        sort -o "$temp"/PRESERVE "$temp"/PRESERVE

        echo 'Starting filter-branch --------------------------'
        git filter-repo --paths-from-file "$temp"/PRESERVE --force --replace-refs delete-no-add
        echo 'Finished filter-branch --------------------------'
        cd ..
    fi
fi

感谢@rksawyer 和@Roberto。

我们将自己描绘成一个更糟糕的角落,数十个分支中有数十个项目,每个项目都依赖 1-4 个其他项目,总共有 56k 次提交。 filter-branch 最多需要 24 小时才能拆分单个目录。

我最终使用 libgit2sharp 和原始文件系统访问在 .NET 中编写了一个工具,以便为每个项目拆分任意数量的目录,并且只保留新存储库中每个项目的相关提交/分支/标签。 它没有修改源回购,而是写出 N 个其他回购,仅包含配置的路径/参考。

欢迎您查看这是否适合您的需求,修改它等。 https://github.com/CurseStaff/GitSplit

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM