【问题标题】:Rebasing a branch including all its children重新定位一个分支,包括它的所有子分支
【发布时间】:2011-08-01 19:42:49
【问题描述】:

我有以下 Git 存储库拓扑:

A-B-F (master)
   \   D (feature-a)
    \ /
     C (feature)
      \
       E (feature-b)

通过变基 feature 分支,我希望变基整个子树(包括子分支):

$ git rebase feature master

A-B-F (master)
     \   D (feature-a)
      \ /
       C (feature)
        \
         E (feature-b)

然而,这是实际的结果:

      C' (feature)
     /
A-B-F (master)
   \   D (feature-a)
    \ /
     C
      \
       E (feature-b)

我知道我可以很容易地通过执行手动修复它:

$ git rebase --onto feature C feature-a
$ git rebase --onto feature C feature-b

但是有没有办法自动变基分支,包括它的所有子/后代?

【问题讨论】:

标签: git version-control branch rebase git-rebase


【解决方案1】:

使用git-branchless 工具套件,您可以直接变基子树:

$ git move -b feature -d master

免责声明:我是作者。

【讨论】:

    【解决方案2】:

    Adam's answer 为基础,将任一侧分支上的多个提交处理为:

    A-B-F (master)
       \
        O   D (feature-a)
         \ /
          C (feature)
           \
            T-E (feature-b)
    

    这里有一个更稳定的方法:

    [alias]
        # rebases branch with its sub-branches (one level down)
        # useage: git move <upstream> <branch>
        move = "!mv() { git rebase $1 $2; git branch --format='%(refname:short)' --contains $2@{1} | xargs -n 1 git rebase --onto $2 $2@{1}; }; mv"
    

    这样git move master feature 的结果就是预期的:

    A-B-F (master)
         \
          O`   D` (feature-a)
           \ /
            C` (feature)
             \
              T`-E` (feature-b)
    

    工作原理分解:

    • git rebase $1 $2 结果
    A-B--------------------F (master)
       \                    \
        O   D (feature-a)    O`
         \ /                  \
          C                    C` (feature)
           \
            T-E (feature-b)
    

    注意 feature 现在位于 C` 而不是 C

    • 让我们解压git branch --format='%(refname:short)' --contains $2@{1} 这将返回包含 C 作为 feature 先前位置的分支列表,并将输出格式化为
    feature-a
    feature-b
    

    feature的前一个位置来自reflogs$2@{1},简单来说就是“第二个参数(特征分支)前一个位置”。

    • | xargs -n 1 git rebase --onto $2 $2@{1}这个位管道上面提到的分支列表到每个单独的rebase命令,并真正转换为git rebase --onto feature C feature-a; git rebase --onto feature C feature-b

    【讨论】:

    • 非常有趣的方法!你能解释一下它是如何工作的吗?
    • 您在回答中包含了很多知识:git 别名,具有多个命令的别名,使用 ! 在别名中定义 shell 命令,使用 git 别名中的 shell 函数来正确处理位置参数,访问git reflog 通过@{n} 表示法,...我学到了很多东西。谢谢你,塔拉斯!
    【解决方案3】:
    git branch --format='%(refname:short)' --contains C | \
    xargs -n 1 \
    git rebase --committer-date-is-author-date --onto F C^
    

    【讨论】:

    • 重新定位到需要最旧提交的父级来分隔开始 - 因此 C^
    • “git branch”命令是否在当前分支之前输出一个星号,如果当前已签出要变基的分支之一,是否会搞砸这个脚本?
    • git branch 不是瓷器命令吗?有没有办法做到这一点,更有前途?
    • Adam:不确定这是要走的路,您确实希望与 * 有线条,您只是不想要 * 本身。有点像 | tr -d * 会更适合。我的问题是:为什么要使用--onto B?我认为它应该重新建立在大师之上。 C^ 不是和 B 不一样吗?所以我们从 B(不包括?)变基到每个包含 C 的分支... B。结果会不会和以前完全一样?
    • 不应该是--onto F 而不是--onto B,因为所有这些提交都在 B 上,我们将它们移到 F 上?
    【解决方案4】:

    如果需要更新提交者日期,可以使用GIT_COMMITTER_DATE 环境变量(manual)。也可以使用--format 选项来获取分支名称,而无需额外的格式。

    export GIT_COMMITTER_DATE=$( date -Iseconds )
    git branch --format='%(refname)' --contains C | xargs -n 1 | git rebase -p --onto master C^
    unset GIT_COMMITTER_DATE
    # don't forget to unset this variable to avoid effect for the further work
    

    注意:需要设置--committer-date-is-author-dateGIT_COMMITTER_DATE 以保证C'Ca'Cb' 提交的校验和相同(基于变基功能feature-afeature-b 对应)。

    【讨论】:

      【解决方案5】:

      几年前,我写了一些东西来处理这种事情。 (当然欢迎提出改进意见,但不要过多评判——那是很久以前的事了!我什至还不知道 Perl!)

      它适用于更多静态情况 - 您可以通过设置 branch.&lt;branch&gt;.autorebaseparent 形式的配置参数来配置它。它不会触及任何没有设置该配置参数的分支。如果那不是您想要的,您可以轻松地将其破解到您想要的位置。在过去的一两年里我并没有真正使用它,但是当我使用它时,它似乎总是非常安全和稳定,只要大规模自动化变基是可能的。

      原来如此。通过将其保存到您的PATH 中名为git-auto-rebase 的文件中来使用它。在您真正尝试之前使用试运行 (-n) 选项可能也是一个好主意。它可能比您真正想要的要详细一些,但它会向您展示它将尝试变基的内容以及内容。可能会为你省点心。

      #!/bin/bash
      
      CACHE_DIR=.git/auto-rebase
      TODO=$CACHE_DIR/todo
      TODO_BACKUP=$CACHE_DIR/todo.backup
      COMPLETED=$CACHE_DIR/completed
      ORIGINAL_BRANCH=$CACHE_DIR/original_branch
      REF_NAMESPACE=refs/pre-auto-rebase
      
      print_help() {
          echo "Usage:  git auto-rebase [opts]"
          echo "Options:"
          echo "    -n   dry run"
          echo "    -c   continue previous auto-rebase"
          echo "    -a   abort previous auto-rebase"
          echo "         (leaves completed rebases intact)"
      }
      
      cleanup_autorebase() {
          rm -rf $CACHE_DIR
          if [ -n "$dry_run" ]; then
              # The dry run should do nothing here. It doesn't create refs, and won't
              # run unless auto-rebase is empty. Leave this here to catch programming
              # errors, and for possible future -f option.
              git for-each-ref --format="%(refname)" $REF_NAMESPACE |
              while read ref; do
                  echo git update-ref -d $ref
              done
          else
              git for-each-ref --format="%(refname)" $REF_NAMESPACE |
              while read ref; do
                  git update-ref -d $ref
              done
          fi
      }
      
      # Get the rebase relationships from branch.*.autorebaseparent
      get_config_relationships() {
          mkdir -p .git/auto-rebase
          # We cannot simply read the indicated parents and blindly follow their
          # instructions; they must form a directed acyclic graph (like git!) which
          # furthermore has no sources with two sinks (i.e. a branch may not be
          # rebased onto two others).
          # 
          # The awk script checks for cycles and double-parents, then sorts first by
          # depth of hierarchy (how many parents it takes to get to a top-level
          # parent), then by parent name. This means that all rebasing onto a given
          # parent happens in a row - convenient for removal of cached refs.
          IFS=$'\n'
          git config --get-regexp 'branch\..+\.autorebaseparent' | \
          awk '{
              child=$1
              sub("^branch[.]","",child)
              sub("[.]autorebaseparent$","",child)
              if (parent[child] != 0) {
                  print "Error: branch "child" has more than one parent specified."
                  error=1
                  exit 1
              }
              parent[child]=$2
          }
          END {
              if ( error != 0 )
                  exit error
              # check for cycles
              for (child in parent) {
                  delete cache
                  depth=0
                  cache[child]=1
                  cur=child
                  while ( parent[cur] != 0 ) {
                      depth++
                      cur=parent[cur]
                      if ( cache[cur] != 0 ) {
                          print "Error: cycle in branch."child".autorebaseparent hierarchy detected"
                          exit 1
                      } else {
                          cache[cur]=1
                      }
                  }
                  depths[child]=depth" "parent[child]" "child
              }
              n=asort(depths, children)
              for (i=1; i<=n; i++) {
                  sub(".* ","",children[i])
              }
              for (i=1; i<=n; i++) {
                  if (parent[children[i]] != 0)
                      print parent[children[i]],children[i]
              }
          }' > $TODO
      
          # Check for any errors. If the awk script's good, this should really check
          # exit codes.
          if grep -q '^Error:' $TODO; then
              cat $TODO
              rm -rf $CACHE_DIR
              exit 1
          fi
      
          cp $TODO $TODO_BACKUP
      }
      
      # Get relationships from config, or if continuing, verify validity of cache
      get_relationships() {
          if [ -n "$continue" ]; then
              if [ ! -d $CACHE_DIR ]; then
                  echo "Error: You requested to continue a previous auto-rebase, but"
                  echo "$CACHE_DIR does not exist."
                  exit 1
              fi
              if [ -f $TODO -a -f $TODO_BACKUP -a -f $ORIGINAL_BRANCH ]; then
                  if ! cat $COMPLETED $TODO | diff - $TODO_BACKUP; then
                      echo "Error: You requested to continue a previous auto-rebase, but the cache appears"
                      echo "to be invalid (completed rebases + todo rebases != planned rebases)."
                      echo "You may attempt to manually continue from what is stored in $CACHE_DIR"
                      echo "or remove it with \"git auto-rebase -a\""
                      exit 1
                  fi
              else
                  echo "Error: You requested to continue a previous auto-rebase, but some cached files"
                  echo "are missing."
                  echo "You may attempt to manually continue from what is stored in $CACHE_DIR"
                  echo "or remove it with \"git auto-rebase -a\""
                  exit 1
              fi
          elif [ -d $CACHE_DIR ]; then
              echo "A previous auto-rebase appears to have been left unfinished."
              echo "Either continue it with \"git auto-rebase -c\" or remove the cache with"
              echo "\"git auto-rebase -a\""
              exit 1
          else
              get_config_relationships
          fi
      }
      
      # Verify that desired branches exist, and pre-refs do not.
      check_ref_existence() {
          local parent child
          for pair in "${pairs[@]}"; do
              parent="${pair% *}"
              if ! git show-ref -q --verify "refs/heads/$parent" > /dev/null ; then
                  if ! git show-ref -q --verify "refs/remotes/$parent" > /dev/null; then
                      child="${pair#* }"
                      echo "Error: specified parent branch $parent of branch $child does not exist"
                      exit 1
                  fi
              fi
              if [ -z "$continue" ]; then
                  if git show-ref -q --verify "$REF_NAMESPACE/$parent" > /dev/null; then
                      echo "Error: ref $REF_NAMESPACE/$parent already exists"
                      echo "Most likely a previous git-auto-rebase did not complete; if you have fixed all"
                      echo "necessary rebases, you may try again after removing it with:"
                      echo
                      echo "git update-ref -d $REF_NAMESPACE/$parent"
                      echo
                      exit 1
                  fi
              else
                  if ! git show-ref -q --verify "$REF_NAMESPACE/$parent" > /dev/null; then
                      echo "Error: You requested to continue a previous auto-rebase, but the required"
                      echo "cached ref $REF_NAMESPACE/$parent is missing."
                      echo "You may attempt to manually continue from the contents of $CACHE_DIR"
                      echo "and whatever refs in refs/$REF_NAMESPACE still exist, or abort the previous"
                      echo "auto-rebase with \"git auto-rebase -a\""
                      exit 1
                  fi
              fi
          done
      }
      
      # Create the pre-refs, storing original position of rebased parents
      create_pre_refs() {
          local parent prev_parent
          for pair in "${pairs[@]}"; do
              parent="${pair% *}"
              if [ "$prev_parent" != "$parent" ]; then
                  if [ -n "$dry_run" ]; then
                      echo git update-ref "$REF_NAMESPACE/$parent" "$parent" \"\"
                  else
                      if ! git update-ref "$REF_NAMESPACE/$parent" "$parent" ""; then
                          echo "Error: cannot create ref $REF_NAMESPACE/$parent"
                          exit 1
                      fi
                  fi
              fi
      
              prev_parent="$parent"
          done
      }
      
      # Perform the rebases, updating todo/completed as we go
      perform_rebases() {
          local prev_parent parent child
          for pair in "${pairs[@]}"; do
              parent="${pair% *}"
              child="${pair#* }"
      
              # We do this *before* rebasing, assuming most likely any failures will be
              # fixed with rebase --continue, and therefore should not be attempted again
              head -n 1 $TODO >> $COMPLETED
              sed -i '1d' $TODO
      
              if [ -n "$dry_run" ]; then
                  echo git rebase --onto "$parent" "$REF_NAMESPACE/$parent" "$child"
                  echo "Successfully rebased $child onto $parent"
              else
                  echo git rebase --onto "$parent" "$REF_NAMESPACE/$parent" "$child"
                  if ( git merge-ff -q "$child" "$parent" 2> /dev/null && echo "Fast-forwarded $child to $parent." ) || \
                      git rebase --onto "$parent" "$REF_NAMESPACE/$parent" "$child"; then
                      echo "Successfully rebased $child onto $parent"
                  else
                      echo "Error rebasing $child onto $parent."
                      echo 'You should either fix it (end with git rebase --continue) or abort it, then use'
                      echo '"git auto-rebase -c" to continue. You may also use "git auto-rebase -a" to'
                      echo 'abort the auto-rebase. Note that this will not undo already-completed rebases.'
                      exit 1
                  fi
              fi
      
              prev_parent="$parent"
          done
      }
      
      rebase_all_intelligent() {
          if ! git rev-parse --show-git-dir &> /dev/null; then
              echo "Error: git-auto-rebase must be run from inside a git repository"
              exit 1
          fi
      
          SUBDIRECTORY_OK=1
          . "$(git --exec-path | sed 's/:/\n/' | grep -m 1 git-core)"/git-sh-setup
          cd_to_toplevel
      
      
          # Figure out what we need to do (continue, or read from config)
          get_relationships
      
          # Read the resulting todo list
          OLDIFS="$IFS"
          IFS=$'\n'
          pairs=($(cat $TODO))
          IFS="$OLDIFS"
      
          # Store the original branch
          if [ -z "$continue" ]; then
              git symbolic-ref HEAD | sed 's@refs/heads/@@' > $ORIGINAL_BRANCH
          fi
      
          check_ref_existence
          # These three depend on the pairs array
          if [ -z "$continue" ]; then
              create_pre_refs
          fi
          perform_rebases
      
          echo "Returning to original branch"
          if [ -n "$dry_run" ]; then
              echo git checkout $(cat $ORIGINAL_BRANCH)
          else
              git checkout $(cat $ORIGINAL_BRANCH) > /dev/null
          fi
      
          if diff -q $COMPLETED $TODO_BACKUP ; then
              if [ "$(wc -l $TODO | cut -d" " -f1)" -eq 0 ]; then
                  cleanup_autorebase
                  echo "Auto-rebase complete"
              else
                  echo "Error: todo-rebases not empty, but completed and planned rebases match."
                  echo "This should not be possible, unless you hand-edited a cached file."
                  echo "Examine $TODO, $TODO_BACKUP, and $COMPLETED to determine what went wrong."
                  exit 1
              fi
          else
              echo "Error: completed rebases don't match planned rebases."
              echo "Examine $TODO_BACKUP and $COMPLETED to determine what went wrong."
              exit 1
          fi
      }
      
      
      while getopts "nca" opt; do
          case $opt in
              n ) dry_run=1;;
              c ) continue=1;;
              a ) abort=1;;
              * )
                  echo "git-auto-rebase is too dangerous to run with invalid options; exiting"
                  print_help
                  exit 1
          esac
      done
      shift $((OPTIND-1))
      
      
      case $# in
          0 )
              if [ -n "$abort" ]; then
                  cleanup_autorebase
              else
                  rebase_all_intelligent
              fi
              ;;
      
          * )
              print_help
              exit 1
              ;;
      esac
      

      自从我最初解决这个问题以来,我发现的一件事是,有时答案是您实际上根本不想变基!首先要在正确的共同祖先处启动主题分支,然后再尝试将它们向前移动,这是有话要说的。但这在您和您的工作流程之间。

      【讨论】:

      • 赞成“改用合并”。在尝试合并选项之前,我花了几个小时尝试重新设置许多主题和子主题分支,并且合并确实更容易执行,即使新的 master 与原来的 master 有很大的不同。
      • 答案包含:“我什至还不知道 Perl”让我有点害怕 - 特别是因为答案不是用 Perl 编写的...... :-)
      • @PeterV.Mørch,什么意思?
      • 至少我读到好像这个答案的作者知道他需要为此编写一个脚本并决定它应该用 Perl 编写。然后他尝试编写一些 Perl,但意外最终得到了一个可以用 bash(+ 一些嵌入的 awk)执行的脚本,但他仍然认为他已经用 Perl 编写了一些代码。
      猜你喜欢
      • 1970-01-01
      • 2012-08-23
      • 2014-05-11
      • 1970-01-01
      • 2013-01-31
      • 2015-09-29
      • 2015-04-10
      • 1970-01-01
      • 2018-07-16
      相关资源
      最近更新 更多