重新绑定树(提交/分支及其所有子项)

时间:2013-06-26 08:37:12

标签: git

这是我目前的git树:

A - H (master)
|
\- B - C - D - E (feature)
           |
           \- F (new)
           |
           \- G (other)

而且我想修改侧支,以便它取决于H而不是A

A - H (master)
    |
    \- B'- C'- D'- E'(feature)
               |
               \- F'(new)
               |
               \- G'(other)

简单的概念,似乎很难自动完成。这已被问及herehere,但建议的解决方案对我不起作用。

首先,正如前面所指出的那样,git branch输出在当前分支存在时(前面有*)解析并不容易。但这不是一个限制因素,在我的情况下,我可以轻松地手动提供名称featurenewother,或确保当前分支为master

然后我尝试了这些命令:

git rebase --committer-date-is-author-date --preserve-merges --onto master feature^ feature
git rebase --committer-date-is-author-date --preserve-merges --onto master feature^ new
git rebase --committer-date-is-author-date --preserve-merges --onto master feature^ other

我最终得到了:

A - H (master)
    |
    \- E'(feature)
    |
    \- B' - C' - D' - F'(new)
    |
    \- B" - C" - D" - G'(other)

绝对不是我想要的!或者,如果我使用B^代替feature^,那么我还会在B - C - D分支中获得feature历史记录。

那么,关于如何自动完成这项工作的任何进一步建议?

编辑:它可以解决这个问题:

git checkout feature
git merge other
git merge new
git rebase -p master feature

现在至少树看起来是正确的,我只需要在合并之前将分支头移动到它们的右侧提交...这可以通过以下方式完成:

git checkout master
git branch -f new feature^2
git branch -f feature feature^1
git branch -f other feature^2
git branch -f feature feature^1

4 个答案:

答案 0 :(得分:3)

在这些情况下,一个很好的技巧是合并(加入)所有分支 移动到最终提交节点。之后,使用rebase与 用于移动生成的封闭子树的--preserve-merges选项 (一组分支)。

创建包含所有分支,公开的封闭子树 2个节点(开始和结束),用作rebase的输入参数 命令。

封闭子树的结尾是可能的人工节点 移动树后删除,以及其他节点 可能是为了合并其他分支而创建的。

让我们看看以下案例。

开发人员想要插入新的提交( master ) 其他3个开发分支( b11 b2 b3 )。其中一个( b11 )是 合并功能分支 b12 ,两者均基于 b1 。另外2 分支( b2 b3 )分歧。

当然,开发人员可以选择新的提交 这些分支中的每一个,但开发者可能不愿意 在3个不同的分支中有相同的提交,但只有1个提交 在分支分歧之前。

* baa687d (HEAD -> master) new common commit
| * b507c23 (b11) b11
| *   41849d9 Merge branch 'b12' into b11
| |\
| | * 20459a3 (b12) b12
| * | 1f74dd9 b11
| * | 554afac b11
| * | 67d80ab b11
| |/
| * b1cbb4e b11
| * 18c8802 (b1) b1
|/
| * 7b4e404 (b2) b2
| | * 6ec272b (b3) b3
| | * c363c43 b2 h
| |/
| * eabe01f header
|/
* 9b4a890 (mirror/master) initial
* 887d11b init

为此,第一步是创建一个公共合并提交 包括3个分支。为此,一个名为 pack 的临时分支 使用。

合并到 pack 可能会产生冲突,但这并不重要 因为这些合并将在以后被丢弃。只是指示git 自动解决它们,添加选项-s recursive -Xours

$ git checkout -b pack b11 # create new branch at 'b11' to avoid losing original refs
$ git merge -s recursive -Xours b2 # merges b2 into pack
$ git merge -s recursive -Xours b3 # merges b3 into pack

将所有内容合并到 pack 分支后,这是整个树:

*   b35a4a7 (HEAD -> pack) Merge branch 'b3' into pack
|\
| * 6ec272b (b3) b3
| * c363c43 b2 h
* |   60c9b7c Merge branch 'b2' into pack
|\ \
| * | 7b4e404 (b2) b2
| |/
| * eabe01f header
* | b507c23 (b11) b11
* |   41849d9 Merge branch 'b12' into b11
|\ \
| * | 20459a3 (b12) b12
* | | 1f74dd9 b11
* | | 554afac b11
* | | 67d80ab b11
|/ /
* | b1cbb4e b11
* | 18c8802 (b1) b1
|/
| * baa687d (master) new common commit
|/
* 9b4a890 initial
* 887d11b init

现在是时候移动已创建的子树了。为了那个原因 使用以下命令:

$ git rebase --preserve-merges --onto master master^ pack

引用 master ^ 表示 master 之前的提交( master ' s 父母),在这种情况下9b4a890。这个提交没有重新定位,确实如此 3个重新分支的起源。当然,pack是最终的参考 整个子树。

在rebase期间可能存在一些合并冲突。万一有 在合并之前已经存在冲突,这些将再次出现。是 一定要以同样的方式解决它们。对于创建的人工提交 要合并到临时节点 pack ,请不要打扰 自动解决它们。

在rebase之后,那将是结果树:

*   95c8d3d (HEAD -> pack) Merge branch 'b3' into pack
|\
| * d304281 b3
| * ed66668 b2 h
* |   b8756ee Merge branch 'b2' into pack
|\ \
| * | 8d82257 b2
| |/
| * e133de9 header
* | f2176e2 b11
* |   321356e Merge branch 'b12' into b11
|\ \
| * | c919951 b12
* | | 8b3055f b11
* | | 743fac2 b11
* | | a14be49 b11
|/ /
* | 3fad600 b11
* | c7d72d6 b1
|/
* baa687d (master) new common commit
|
* 9b4a890 initial
* 887d11b init

有时旧的分支引用可能不会被重新定位(即使 树没有它们重新定位)。在那种情况下,你可以恢复 或者手动更改一些参考文献。

现在也是时候撤销可能的预先变更合并了 重新整理整棵树。删除一些,重置/结帐后,这个 是树:

* f2176e2 (HEAD -> b11) b11
*   321356e Merge branch 'b12' into b11
|\
| * c919951 (b12) b12
* | 8b3055f b11
* | 743fac2 b11
* | a14be49 b11
|/
* 3fad600 b11
* c7d72d6 (b1) b1
| * d304281 (b3) b3
| * ed66668 b2 h
| | * 8d82257 (b2) b2
| |/
| * e133de9 header
|/
* baa687d (mirror/master, mirror/HEAD, master) new common commit
* 9b4a890 initial
* 887d11b init

这正是开发人员想要实现的目标:提交 由3个分支共享。

答案 1 :(得分:2)

啊,我没有发现您的命令是基于您对第一个链接问题的接受答案。在任何情况下,你都明确地要求你得到了什么:E,F和G中的每一个都重新掌握在主人身上。

我认为你想要的是:

git rebase ... --onto master A feature

改变
A - H (master)
|
\- B - C - D - E (feature)

A - H (master)
    |
    \- B'- C'- D'- E'(feature)

(master是新的root,A是旧的root。使用feature ^作为旧的root意味着你只移植了最后一次提交的功能,如你所见)

然后:

git rebase ... --onto D' D new
git rebase ... --onto D' D other

从D中分离新的和其他的,并将它们移植到D'上。请注意,在您重新定义功能后,feature^表示D'而不是D


至于自动化过程,我可以向您展示一些有用的东西,但棘手的部分是错误处理和恢复。

transplant_tree.sh

#!/bin/bash
trap "rm -f /tmp/$$.*" EXIT

function transplant() # <from> <to> <branch>
{
    OLD_TRUNK=$1
    NEW_TRUNK=$2
    BRANCH=$3

    # 1. get branch revisions
    REV_FILE="/tmp/$$.rev-list.$BRANCH"
    git rev-list $BRANCH ^$OLD_TRUNK > "$REV_FILE" || exit $?
    OLD_BRANCH_FORK=$(tail -1 "$REV_FILE")
    OLD_BRANCH_HEAD=$(head -1 "$REV_FILE")
    COMMON_ANCESTOR="${OLD_BRANCH_FORK}^"

    # 2. transplant this branch
    git rebase --onto $NEW_TRUNK $COMMON_ANCESTOR $BRANCH

    # 3. find other sub-branches:
    git branch --contains $OLD_BRANCH_FORK | while read sub;
    do
        # 4. figure out where the sub-branch diverges,
        # relative to the (old) branch head
        DISTANCE=$(git rev-list $OLD_BRANCH_HEAD ^$sub | wc -l)

        # 5. transplant sub-branch from old branch to new branch, attaching at
        # same number of commits before new HEAD
        transplant $OLD_BRANCH_HEAD ${BRANCH}~$DISTANCE  $sub
    done
}

transplant $1 $2 $3

供您使用,transplant_tree.sh master master feature应该有效,假设所有的rebase都成功了。它看起来像是:

  • OLD_TRUNK = NEW_TRUNK = master,BRANCH = feature
    1. 获得分支修订
      • OLD_BRANCH_FORK = B
      • OLD_BRANCH_HEAD = E
      • COMMON_ANCESTOR = B ^ == A
    2. 移植这个分支
      • git rebase --onto master B^ feature
    3. 找到其他支行
      • 子=新
        • DISTANCE = $(git rev-list E ^new | wc -l) == 1
        • 使用OLD_TRUNK = E递归,NEW_TRUNK =功能~1,BRANCH =新
      • 子=其它

如果其中一个rebase失败,是否应该让你手动修复它并以某种方式恢复?它应该能够将整个东西卷回来吗?

答案 2 :(得分:1)

这是我第一次尝试使用"plumbing" commands,所以请随时提出改进建议。

您可以在此处使用的两个功能强大的命令,git for-each-refgit rev-list

来确定如何移动此树。
  • git for-each-ref refs/heads --contains B为您提供您想要移动的所有(本地)引用,即featurenewother
  • git rev-list ^B^ feature new other为您提供了所有想要移动的提交。

现在感谢rebase,我们并不需要移动每个提交,只需要移动树中的叶子或叉子的节点。因此那些有零或2(或更多)子提交的人。

让我们假设,为了与rebase保持一致,您将参数设为:git transplant-onto H A <list of branches>,然后您可以使用以下内容(并将其放在路径中的git-transplant-onto下) :

#!/usr/bin/env bash

function usage() {
    echo
    echo "Usage: $0 [-n] [-v] <onto> <from> <ref-list>"
    echo "    Transplants the tree from <from> to <onto>"
    echo "    i.e. performs git rebase --onto <onto> <from> <ref>, for each <ref> in <ref-list>"
    echo "    while maintaining the tree structure inside <ref-list>"
    exit $1
}

dry_run=0
verbose=0
while [[ "${1::1}" == "-" ]]; do
    case $1 in
        -n) dry_run=1 ;;
        -v) verbose=1 ;;
        *) echo Unrecognized option $1; usage -1 ;;
    esac
    shift
done

# verifications
if (( $# < 2 )) || ! onto=$(git rev-parse --verify $1) || ! from=$(git rev-parse --verify $2); then usage $#; fi
git diff-index --quiet HEAD || {echo "Please stash changes before transplanting"; exit 3}

# get refs to move
shift 2
if (( $# == 0 )); then
    refs=$(git for-each-ref --format "%(refname)" refs/heads --contains $from)
else
    refs=$(git show-ref --heads --tags $@ | cut -d' ' -f2)
    if (( $# != $(echo "$refs" | wc -l) )); then echo Some refs passed as arguments were wrong; exit 4; fi
fi

# confirm
echo "Following branches will be moved: "$refs
REPLY=invalid
while [[ ! $REPLY =~ ^[nNyY]?$ ]]; do read -p "OK? [Y/n]"; done
if [[ $REPLY =~ [nN] ]]; then exit 2; fi

# only work with non-redundant refs
independent=$(git merge-base --independent $refs)

if (( verbose )); then
    echo INFO:
    echo independent refs:;git --no-pager show -s --oneline --decorate $independent
    echo redundant refs:;git --no-pager show -s --oneline --decorate $(git show-ref $refs | grep -Fwvf <(echo "$independent") )
fi

# list all commits, keep those that have 0 or 2+ children
# so we rebase only forks or leaves in our tree
tree_nodes=$(git rev-list --topo-order --children ^$from $independent | sed -rn 's/^([0-9a-f]{40})(( [0-9a-f]{40}){2,})?$/\1/p')

# find out ancestry in this node list (taking advantage of topo-order)
declare -A parents
for f in $tree_nodes; do
    for p in ${tree_nodes#*$h} $from; do
        if git merge-base --is-ancestor $p $h ; then
            parents[$h]=$p
            break
        fi
    done
    if [[ ${parents[$h]:-unset} = unset ]]; then echo Failed at finding an ancestor for the following commit; git --no-pager show -s --oneline --decorate $h; exit 2; fi
done

# prepare to rebase, remember mappings
declare -A map
map[$from]=$onto

# IMPORTANT! this time go over in chronological order, so when rebasing a node its ancestor will be already moved
while read h; do
    old_base=${parents[$h]}
    new_base=${map[$old_base]}
    git rebase --preserve-merges --onto $new_base $old_base $h || {
        git rebase --abort
        git for-each-ref --format "%(refname:strip=2)" --contains $old_base refs/heads/ | \
            xargs echo ERROR: Failed a rebase in $old_base..$h, depending branches are:
        exit 1
    }
    map[$h]=$(git rev-parse HEAD)
done < <(echo "$tree_nodes" | tac)

# from here on, all went well, all branches were rebased.
# update refs if no dry-run, otherwise show how

ref_dests=
for ref in $refs; do
    # find current and future hash for each ref we wanted to move
    # all independent tags are in map, maybe by chance some redundant ones as well
    orig=$(git show-ref --heads --tags -s $ref)
    dest=${map[$orig]:-unset}

    # otherwise look for a child in the independents, use map[child]~distance as target
    if [[ $dest = unset ]]; then
        for child in $independent; do
            if git merge-base --is-ancestor $ref $child ; then
                dest=$(git rev-parse ${map[$child]}~$(git rev-list $ref..$child | wc -l) )
                break
            fi
        done
    fi

    # finally update ref
    ref_dests+=" $dest"
    if (( dry_run )); then
        echo git update-ref $ref $dest
    else
        git update-ref $ref $dest
    fi
done

if (( dry_run )); then
    echo
    echo If you apply the update-refs listed above, the tree will be:
    git show-branch $onto $ref_dests
else
    echo The tree now is:
    git show-branch $onto $refs
fi

另一种方法是按照您可以转置的顺序(例如git rev-list --topo-order --reverse --parents),然后在每次提交时使用git am来获取与父级的所有单独提交。

答案 3 :(得分:0)

我认为你不应该真正想要自动化。重新划分甚至简单的分支通常需要公平的关注。如果在老化的分支和/或少数几个提交上完成,那么成功的可能性非常小。你似乎在那里有一个完整的森林。

如果情况不是那么模糊,并且rebase大部分都有效,那么只需发出一个交互式rebase命令并手动切割todo只需几秒钟。

同样从大自然中我看不到完整的自动解决方案。

但要建设性,这就是我尝试解决方案的方法:

  • 首先创建一个程序,可以分析源历史并给我树枝。对于上面的例子,它会在A上给出B..D; E; E在D; F..G on D.
  • 将A'标记为H(来自肾上腺输入)
  • 将基于A:cherry-pick(或rebase)的最低块应用于B..D到A';
  • 将新顶部标记为D'
  • 应用已完成映射的剩余块