Rebase git 子模块和父仓库

Rebase git submodule and parent repo

我的情况:你有一个基于 master 的功能分支,有人提交给 master。现在你的历史是这样的:

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

因此,您希望将功能变基到 master 上以获得干净的历史记录,正如您所做的那样。但是考虑一下:那个 repo 是另一个的子模块,父 repo 像这样引用子模块的提交:

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

如果我天真地变基子模块,父模块对子模块提交的引用将无效。让我们假设功能分支中的一些提交足够有意义,可以分开,所以将它们压缩成一个提交是不可能的。

有什么方法可以做到这一点并维护这些引用? (两个 'feature' 分支都可以自由重写)。

the parent's references to submodule's commits will be invalid.

那些引用只有在它们被 rebase 丢失时才会变得无效。

您需要做的就是添加一个 old_feature 分支,其中 feature 变基功能之前。
不要忘记 push old_feature 分支。

然后,一旦 feature 变基并被推送,你就转到父仓库,make sure its submodule follows the feature branch, and do an update remote:

git submodule update --remote

提交 XZ 可以保留对 old_feature 分支的旧引用,而新提交将保留对 rebased[=49] 的引用=] feature 子模块的分支。


由于 jthill adds , reflog 仍然存在于本地,持续 90 天,如果您 已经 重新设置基准而不首先引用 feature 的旧状态:

you've got a month to fix the oops in that repo, git branch old-feature feature@{last.monday}

然后推送 old-feature 分支,以确保 EF 在远程存储库中继续被引用,其提交被 XZ.

我在公司经常遇到这种情况,我们找到了解决这个问题的两个主要方法。他们都使用 git 的能力在某些情况下将提交保留在历史记录中。顺便说一句,好的做法是永远不要推送指向可删除分支(即功能分支)的子模块引用。

我假设你和我一样,不想推送 feature_branch 以免污染你的远程存储库,并且对清理所有旧推送功能的调查员模式工作伙伴保持健壮分支机构

问题是,当您进行变基时,提交会在 master 的历史记录之上重新播放。它们是新的提交(假设 X'、Y' 和 Z'),具有新的哈希值,并且分支的头部移动到 Z' 的顶部。因此 X、Y、Z 将保留在死分支上,并会随着时间的推移被删除(默认设置为 90 天)。除此之外,当你推送你的 rebased 分支时,它们不会被默认推送。

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

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

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

将 X、Y、Z 提交保留在历史记录中的一个解决方案是标记 Z(在变基之前(标记将保留给它),或之后(但您必须使用 reflog 在历史记录中重新找到它), 没关系)。如果此标签随后被推送,则不会清除 X、Y、Z。这很脏并且有副作用,但可以完成工作。当然,您必须按下该标签! (老实说,这相当于推送一个分支。区别在于你可以具体命名它,例如 DONT_DELETE_THIS_FOOL ;))

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

#!/bin/bash

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

    refmajor=`echo `
    refminor=`echo `
    refrevision=`echo `

    echo "Check for anterior tags in ..."
    for tag in $(git tag -l -n "*.*.*" | 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 : $tagMajor.$tagMinor.$tagRevision (want to apply $refmajor.$refminor.$refrevision)"

    if [[ tagMajor -gt refmajor ]]; then
        echo "Cannot tag with $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 $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 $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 $refmajor.$refminor.$refrevision : anterior tag with equal major, minor, and revision number (found $tagMajor.$tagMinor.$tagRevision, wanted $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  }'))

    currentDirectory=$(pwd)

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

        check_tags     $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 || :'
A - B - C - D (submodule:master)
         \
           E - F - G  (submodule:feature)
           *    *
           *     *
           X - Y - Z             (parent:feature)
              (asterisks represent references to submodule)

变基一个子模块(例如,将功能放到主模块上)并更新父模块的 gitlinks:

  1. 在子模块的 rebase 尖端创建另一个分支(如 'old_feature' 在 'feature')。

  2. 在子模块中做变基。

  3. 请注意,您可以计算出旧子模块提交 (E -> G) 和新子模块提交 (E' -> G') 及其 ID 之间的映射。稍后您将需要这些。

  4. 您还应该知道父仓库中要更改的提交范围(X -> Z),以及哪些特定提交有 gitlinks 需要待更新。

  5. 在父项中对所述提交进行交互式变基。在每个需要修改的后面插入一个'break'命令

  6. 随着 git 每次下降到 shell:

    1. 对于当前父提交的旧 gitlink,在子模块中检出适当的新提交。

    2. 在父级中,暂存子模块和git commit --amend。这会更新 gitlink.

    3. 继续变基。如果有冲突(应该很多),优先选择带有 gitlinks 的那些而不是子模块的 old 提交,因为这些冲突发生在我们更新它们之前。 (当我这样做时,这是本地选项 "use the remote changes" 与 git mergetool 中的远程选项)。

  7. 完成


它相当复杂(如果你有很多,可能会很慢),这是一个你是否认为值得的问题。

更简单的方法,根据

  1. Classic, clean way : do a merge instead of a rebase

Do a merge. E,F,G will be linked to the merge commit. In that way, X,Y,Z will remain for ever and unchanged, even if the branch is deleted (only the ref of the branch would be in that case. But at the expense of a un-linear history on master). When you will push parent branch, X,Y,Z will be pushed too.