重新定位Git子模块和父级仓库

3

我的情况:您有一个基于主分支的特性分支,然后有人向主分支提交了代码。现在您的历史记录如下:

A - B - C - D (master)
         \
           E - F - G  (feature)

你想将功能分支基于主分支进行变基,以便获得一个干净的历史记录,这是常见的做法。但请考虑以下情况:该仓库是另一个仓库的子模块,并且父级仓库引用了子模块的提交,类似于:

A - B - C - D (submodule:master)
         \
           E - F - G  (submodule:feature)
           *    *
           *     *
           X - Y - Z             (parent:feature)
              (asterisks represent references to submodule)

如果我粗略地rebase子模块,父模块对子模块提交的引用将会失效。假设一些feature分支中的提交足够有意义需要单独处理,所以将它们压缩成一个提交是不合适的。

有什么方法可以实现并保持这些引用?(两个'feature'分支都可以自由重写)。

3个回答

3

父模块对子模块提交的引用将失效。

只有在重新定义时它们才会失效。

你需要做的就是在重新定义 feature 之前添加一个名为 old_feature 的分支。
不要忘记 推送 old_feature 分支。

然后,一旦 feature 被重新定义并推送,你需要进入父存储库,确保其子模块遵循 feature 分支, 并进行 远程更新

git submodule update --remote

提交记录 XZ 可以保留其对 old_feature 分支的旧引用,而新的提交记录将保留对子模块被变基后的 feature 分支的引用。

jthill评论中添加时,reflog仍然在本地保留90天,如果您已经重新基于feature的旧状态而没有引用它:

您有一个月的时间来修复该存储库中的错误,git branch old-feature feature@{last.monday}

然后推送old-feature分支,以确保EF在远程存储库中被引用,该存储库的提交由XZ引用。


不必着急,你有一个月的时间来修复那个仓库中的错误,可以使用 git branch old-feature feature@{last.monday} 或其他方法。 - jthill
@jthill 同意。我更喜欢按顺序明确地完成它,而不是事后记得并完成它。 - VonC
是的,没错,我只是在回应“之前”的强调。如果你要提高“可怕”的分子,那我就要提高“宽容”的分母 :-) - jthill
@jthill 指出的很好。我已经将您的评论包含在答案中,以增加其可见性。 - VonC
个人而言,我更喜欢标记而不是分支。 - Nicolas Voron

1
A - B - C - D (submodule:master)
         \
           E - F - G  (submodule:feature)
           *    *
           *     *
           X - Y - Z             (parent:feature)
              (asterisks represent references to submodule)
重新定位子模块(例如将 feature 定位到 master),并更新父级的 git 链接:
  1. 在子模块的变基尖端(如'feature'的'E')创建另一个分支(如'old_feature')。

  2. 在子模块中进行变基。

  3. 请注意,您可以确定旧子模块提交(E->G)与新提交(E'->G')及其ID之间的映射关系。稍后会用到这些。

  4. 您还应该知道要在父存储库中更改的提交范围(X->Z),以及需要更新git链接的特定提交。

  5. 在这些提交上对父存储库进行交互式变基。每个需要更改的提交后插入“break”命令。

  6. 因为每次Git都会转到shell:

    1. 对于当前父提交的旧git链接,在子模块中检出相应的新提交。

    2. 在父存储库中暂存子模块和git commit --amend。这将更新git链接。

    3. 继续变基。如果有冲突(应该有很多),则优先选择具有子模块旧提交的git链接的冲突,因为这些冲突发生在我们更新它们之前。(当我这样做时,这是git mergetool中本地与远程选项中的“使用远程更改”选项)。

  7. 完成


这相当复杂(如果你有很多的话可能会很慢),这取决于你是否认为它值得。

更简单的方法,参见@NicolasVoron

  1. 经典、干净的方法:执行合并而不是变基

执行合并。E、F、G将链接到合并提交。以这种方式, X、Y、Z将永远保持不变,即使分支被删除(在这种情况下只有分支的引用)。但代价是主分支上出现了一个非线性的历史记录。当你推送父分支时,X、Y、Z也将被推送。


1
我在公司经常面临这种情况,我们找到了两个主要解决方案来解决这个问题。它们都利用了git的能力,在某些情况下保留提交历史记录。顺便说一句,好的做法是永远不要推送指向可删除分支(即功能分支)的子模块引用。
我假设,像我一样,你不想推送feature_branch,以免污染你的远程仓库,并且对于清除所有旧的已推送的feature_branch的好奇工作伙伴而言,具有鲁棒性。
问题在于当你进行rebase时,提交会被重新应用到master历史的顶部。它们是新的提交(假设为X'、Y'和Z'),具有新的哈希值,并且分支头移动到Z'的顶部。因此,X、Y、Z将停留在一个死分支上,并将随着时间(默认设置为90天)而被删除。除此之外,当你推送你的rebased分支时,默认情况下它们不会被推送。
1. 经典、干净的方法:使用合并而不是rebase

进行合并操作。E、F、G将链接到合并提交。这样,即使分支被删除(在这种情况下,只有分支的引用),X、Y、Z也会永久保留并且不会改变,但代价是主分支上出现了非线性历史记录。当您推送父分支时,X、Y、Z也将被推送。

  1. 使用标签保留历史记录中的提交

保留X、Y、Z提交历史记录的一种解决方案是对Z打标签(在rebase之前(标签将幸存于此),或之后(但您必须在reflog中重新找到它),都可以)。如果随后推送此标签,则X、Y、Z将不会被清除。这相当于一种比较脏的方式,并且有副作用,但是可以完成任务。当然,您必须推送该标签!(说实话,这与推送分支相当等效。唯一的区别是您可以具体命名,例如DONT_DELETE_THIS_FOOL ;))

以下是一个小脚本,您可以运行它以确保不会丢失任何子模块依赖项(通过为每个子模块打标签并推送它们)(使用-h获取帮助):

#!/bin/bash

check_tags () {
    slength=${#1}
    slength=$((slength+1))

    refmajor=`echo $2`
    refminor=`echo $3`
    refrevision=`echo $4`

    echo "Check for anterior tags in $5..."
    for tag in $(git tag -l -n "$1*.*.*" | cut -d" " -f1 | cut -c "$slength"- ); do
        temptagMajor=`echo $tag | cut -d. -f1`
        temptagMinor=`echo $tag | cut -d. -f2`
        temptagRevision=`echo $tag | cut -d. -f3`

        if [[ temptagMajor -gt tagMajor ]] || [[ temptagMajor -eq tagMajor && temptagMinor -gt tagMinor ]] || [[ temptagMajor -eq tagMajor && temptagMinor -eq tagMinor && temptagRevision -gt tagRevision ]]; then
            tagMajor=$temptagMajor
            tagMinor=$temptagMinor
            tagRevision=$temptagRevision
        fi
    done

    echo "Latest versioning tag found : $1$tagMajor.$tagMinor.$tagRevision (want to apply $1$refmajor.$refminor.$refrevision)"

    if [[ tagMajor -gt refmajor ]]; then
        echo "Cannot tag with $1$refmajor.$refminor.$refrevision : anterior tag with greater major revision number (found $tagMajor, wanted $refmajor). Use -f to ignore."
        return 1
    elif [[ tagMajor -eq refmajor ]] && [[ tagMinor -gt refminor ]]; then
        echo "Cannot tag with $1$refmajor.$refminor.$refrevision : anterior tag with equal major revision number but greater minor revision number (found $tagMinor, wanted $refminor). Use -f to ignore."
        return 1
    elif [[ tagMajor -eq refmajor ]] && [[ tagMinor -eq refminor ]] && [[ tagRevision -gt refrevision ]]; then
        echo "Cannot tag with $1$refmajor.$refminor.$refrevision : anterior tag with equal major and minor revision number, but greater revision number (found $tagRevision, wanted $refrevision). Use -f to ignore."
        return 1
    elif [[ tagMajor -eq refmajor ]] && [[ tagMinor -eq refminor ]] && [[ tagRevision -eq refrevision ]]; then
        echo "Cannot tag with $1$refmajor.$refminor.$refrevision : anterior tag with equal major, minor, and revision number (found $1$tagMajor.$tagMinor.$tagRevision, wanted $1$refmajor.$refminor.$refrevision). Use -f to ignore."      
        return 1
    else
        return 0
    fi
}

function subcheck() {  

  if [[ ! -d .git ]]; then
        echo "not a git repo"
        return 1
    fi;

    if ! [[ `git submodule status` ]]; then
        echo 'no submodule'
        return 1
    fi

    submodules=($(git config --file .gitmodules --get-regexp path | awk '{ print $2 }'))

    currentDirectory=$(pwd)

    for submodule in "${submodules[@]}"
    do
        printf "\n\nEntering '$submodule'\n"
        cd "$currentDirectory/$submodule"

        check_tags $1 $2 $3 $4 $submodule

        if [[ $? -eq 1 ]]; then
            cd "$currentDirectory"
            return 1
        fi

    done

  cd "$currentDirectory"
}

#export -f check_tags

# Check arguments
while getopts v:hfti: option
do
case "${option}"
in
v) VERSION=${OPTARG};;
h) HELP='help';;
f) FORCE='force';;
t) TEST='test';;
i) INDEX=${OPTARG};;
esac
done

printf "Tag release script v0.1\n"

# Help
if [ "$HELP" != "" ]; then 
  echo 'GIT Release Script'
  echo "Options :"
  echo 'Use -v to specify version (mandatory). Ex : "-v 1.0.2"' 
  echo 'Use -t to run unit test of -v inputs' 
  echo 'Use -f to force tagging / skip anterior tag versions check' 
  echo 'Use -i to specify index (optional). Ex : "-v 1.0.2 -i A" for a indA + v1.0.2 double tag.'
  exit 1 
fi

# Tests for bad inputs
if [ "$HELP" != "" ]; then 
    array=( ".2.3" "1..3" "1.2." "A.2.3" "1.A.3" "1.2.A" "1A3.123.123" "123.1D3.123" "123.123.1A3" "nougatine" "1.3" )

    arr=(${array[*]})
    echo "Tested valudes : ${#arr[*]}"
    for ix in ${!arr[*]}
    do
        printf "   %s\n" "${arr[$ix]}"
        . release_script.sh -v ${arr[$ix]}
    done
fi

# Version
if [ "$VERSION" == "" ]; then 
  echo "Argument missing"
  echo "Run -h for help"
  exit 1 
fi

major=`echo $VERSION | cut -d. -f1`
minor=`echo $VERSION | cut -d. -f2`
revision=`echo $VERSION | cut -d. -f3`

if [ -n "$(printf '%s\n' "$major" | sed 's/[0-9]//g')" ] || [ "$major" == "" ]; then 
  echo "Invalid major version argument (was \"$major\")"
  echo "Run -h for help"
  exit 1 
fi

if [ -n "$(printf '%s\n' "$minor" | sed 's/[0-9]//g')" ] || [ "$minor" == "" ] ; then 
  echo "Invalid minor version argument (was \"$minor\")"
  echo "Run -h for help"
  exit 1 
fi

if [ -n "$(printf '%s\n' "$revision" | sed 's/[0-9]//g')" ] || [ "$revision" == "" ]; then 
  echo "Invalid revision version argument (was \"$revision\")"
  echo "Run -h for help"
  exit 1 
fi

# Fetching
git fetch --all
echo "Fetching tags"
git fetch --tag

tagMajor=0
tagMinor=0
tagRevision=0

versionLabel=v$VERSION
url=$(git config --get remote.origin.url)
basename=$(basename "$url" .git)

# Check previous available versions if no -f specified
if [ "$FORCE" == "" ]; then 

    if [ "$INDEX" != "" ]; then
        echo "Check for anterior index tag in $basename..."

        for tag in $(git tag -l -n "ind$INDEX" ); do

            if [[ "ind$INDEX" == $tag ]] ; then
                echo "\"ind$INDEX\" tag already exists in $basename. Use -f to ignore."
                exit 1
            fi
        done

        echo "No conflict found for "ind$INDEX" index tag"
    fi

    check_tags v "$major" "$minor" "$revision" "$basename"

    if [[ $? -eq 1 ]]; then
        #echo "Error found, won't tag"
        exit 1
    fi  

    subcheck ${basename}_v $major $minor $revision

    if [[ $? -eq 1 ]]; then
        exit 1
    fi
fi

# Release tag script
versionLabel=v$VERSION
url=$(git config --get remote.origin.url)
basename=$(basename "$url" .git)
echo "Tagging project $basename (\"$versionLabel\")"
git tag $versionLabel

#TODO : check index
if [ "$INDEX" != "" ]; then
    echo "Tagging index $INDEX"
    git tag "ind$INDEX"
fi
echo "Tagging submodules (\"${basename}_$versionLabel\")"
git submodule foreach "git tag ${basename}_$versionLabel || :"
echo "Pushing project tag"
git push --tags
echo "Pushing submodules tags"
git submodule foreach 'git push --tags || :'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接