20

这是我当前的 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)

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

首先,git branch如前所述,当当前分支存在时(有一个*前置),解析输出并非易事。但这不是一个障碍,在我的情况下,我可以很容易地手动提供名称featurenew或者other确保当前分支是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

4 回答 4

5

在这些情况下,一个不错的技巧是将所有要移动的分支合并(连接)到最终提交节点中。之后,使用 rebase 和 --preserve-merges移动生成的封闭子树(分支集)的选项。

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

封闭子树的末端是一个人工节点,在移动树后可能会被删除,以及可能为合并其他分支而创建的其他节点。

让我们看看下面的案例。

开发人员想要将一个新的提交(master)插入其他 3 个开发分支(b11b2b3)。其中之一 ( b11 ) 是特征分支b12的合并,两者均基于b1。其他 2 个分支(b2b3)发散。

当然,开发人员可以在每个分支中挑选新的提交,但开发人员可能不希望在 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的临时分支 。

合并到中可能会产生冲突,但这并不重要,因为这些合并稍后将被丢弃。只需指示 git 自动解决它们,添加 options -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的父级),在这种情况下为 9b4a890 。这个提交不是 rebase,它是 3 个 rebase 分支的起源。当然,pack 是整个子树的最终引用。

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

在 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 个分支共享。

于 2017-08-23T16:20:08.413 回答
2

啊,我没有发现您的命令是基于对您的第一个链接问题的公认答案。在任何情况下,您都非常明确地要求您得到什么:E、F 和 G 中的每一个都重新基于 master。

我想你想要的是:

git rebase ... --onto master A feature

从改变

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

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

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

接着:

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

将 new 和 other 从 D 中分离出来,并将它们移植到 D' 上。请注意,在您重新设置功能基础后,feature^意味着D'而不是D.


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

移植树.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假设所有变基都成功,供您使用,应该可以工作。它看起来像:

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

如果其中一个变基失败,是否应该让您手动修复它并以某种方式恢复?它应该能够回滚整个事情吗?

于 2013-06-26T11:50:24.600 回答
1

This is my first attempt at using "plumbing" commands, so feel free to suggest improvements.

The two powerful commands you can use here, to figure out how to move this tree, are git for-each-ref and git rev-list:

  • git for-each-ref refs/heads --contains B gives you all (local) refs you'd want to move, that is feature, new, other
  • git rev-list ^B^ feature new other gives you all commits you want to move.

Now thanks to rebase, we don't really need to move every commit, only the nodes in your tree that are leaves or forks. Thus those that have zero or 2 (or more) child commits.

Let us assume, for the sake of consistency with rebase that you give arguments as:
git transplant-onto H A <list of branches>, then you could use the following (and put it under git-transplant-onto in your path):

#!/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

Another way is to get all individual commits with their parent in an order that you may transpose (say git rev-list --topo-order --reverse --parents) and then use git am on each individual commit.

于 2016-07-13T17:52:54.177 回答
0

我认为您不应该真正想要案件的自动化。即使是简单的分支的变基通常也需要引起足够的重视。如果在老化的分支和/或多个提交上完成,那么成功的机会非常小。你似乎在那里有一片完整的森林。

虽然如果情况不是真的那么模糊并且 rebase 主要工作,只需发出交互式 rebase 命令并手动切割 todo 只需几秒钟。

同样从本质上看,我没有看到一个全自动的解决方案。

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

  • 首先创建一个可以分析源历史并给我树分支的程序。对于上面的示例,它将在 A 上给出 B..D;E..E 在 D 上;F..G 上 D。
  • 将 A' 标记为 H(来自外部输入)
  • 应用基于 A 的最低块:cherry-pick (or rebase to) B..D to A';
  • 将新顶部标记为 D'
  • 应用已完成映射的剩余块
于 2013-06-26T09:22:34.357 回答