Git 3 向合并之前的交互式 Rebase 补丁
Git Interactive Rebase Patch Before a 3-Way Merge
我有以下 Git 历史记录:
我想从提交 1f63
(之前 2 次提交)到 feature/project-setup
处的 HEAD 进行交互变基,如下所示:
git rebase -i HEAD~2
git-rebase-todo
文件包含以下行:
pick ff7abc8 Install initial project site packages
pick 1696181 Add `.bumpversion.cfg`
如果我将第一行更改为 edit
,应用我的更改,然后执行 git commit --amend
和 git rebase --continue
,我的提交历史现在如下所示:
我知道交互式 git rebase 正在挑选两个提交到 rebase 的根目录(在本例中,提交 1f63
)。我的问题是,如何用 e16f
覆盖 1696
并使分支保持不变? (我希望我的最终历史看起来像原来的那样,但是 1696
将被有我更改的新提交 e16f
取代)
我最初的想法是我可能首先需要挑选那些提交,然后删除它们,添加一个中断,签出 feature/project-setup
然后进行快进合并提交。有什么想法吗?
编辑: 如果有更简单的方法不需要重置开发、主控和 0cea8
然后重新合并,请告诉我。
If there's a simpler way to do this that doesn't require resetting develop, master, and 0cea8
and then remerging, please let me know.
没有,但重置 0cea8
的想法也是胡说八道。只能“重置”分支名称(以及 Git 的索引和您的工作树,因为 Git 在 git reset
命令中塞进了太多东西)。请参阅下面关于分支名称的一件大事。
关于提交有几点需要注意:
- 它们完全是只读的。
- 他们的 true 名称是他们的哈希 ID,它是根据提交的内容计算的(包括元数据——从技术上讲,它仅根据元数据计算,因为保存的快照是元数据) .
- 任何一个给定提交的元数据给出了其前身提交(如果是常规单亲提交)或其所有父提交(如果是合并提交)的原始哈希 ID。
这些,放在一起,意味着你总是可以复制一个提交到一个新的和改进的版本,但是这样做之后,你现在必须复制每个“下游”(后续) 犯罪。那是因为,给出类似的东西:
... <-F <-G <-H
其中 H
是序列中的“最后”提交,如果我们将 F
复制到新的和改进的 F'
,我们将需要一个新的和-改进 G
其中改进或至少其中之一是 G'
的父级是 F'
。然后我们还需要将 H
复制到一个新的和改进的 H'
以便它的父级可以是 G'
.
关于 分支名称 ,如 master
和 develop
,真的只有一件大事需要了解——几乎所有其他事情都源于这件大事——而且那就是每个人只持有 one 提交哈希 ID。无论存储在分支名称中的哈希 ID 是什么,该提交都是该分支的 last 提交:
...--F--G--H <-- branch
分支 branch
的 最后 提交是提交 H
。是提交 H
本身导致提交 G
成为分支 branch
的一部分,然后是提交 G
本身导致提交 F
成为分支的一部分分支等等,通过历史倒退。历史 是 提交集:不多也不少。
因此,如果您有一些您不喜欢的历史记录,您可以建立您喜欢的新历史记录(新提交),但即使对某些 过去 提交,新的和改进的提交具有不同的哈希 ID,并且此更改会波及其余提交。
Git 2.18 学到了一个新的 --rebase-merges
选项,它可以让你重建某些历史记录,不仅可以自动复制单个提交,还可以重复合并操作(合并提交不能 copied 因此必须重新执行合并)。这可以帮助您完成大部分工作,但仍然需要手动调整分支名称,以便指向新的和改进的提交。
更新:2021 年 5 月 6 日
这是一个(相对)简单的 bash 脚本,它将更新分支名称和标签。 使用风险自负!将其放入
/usr/bin/ # On Linux
或
C:\Program Files\Git\usr\bin # On Windows
并命名
git-rebase-bti
(没有文件扩展名),并使其可执行
chmod +x path/to/git-rebase-bti
或 Windows
icacls path/to/git-rebase-bti /grant your_usrnm:(rx)
虽然我相信 Windows 默认情况下为文件提供可执行权限? (不要引用我的话)
#! /bin/sh -
#
# Git Rebase Branch Names, Tags, and Forks
#
# File:
# git-rebase-bti
#
# Installation Instructions:
# Place this file in the following folder:
# - Linux: `/usr/bin/`
# - Windows: `C:\Program Files\Git\usr\bin`
#
# Usage:
# git rebase-bti <SHA>
#
# Authors:
# Copyleft 2020 Adam Hendry. All rights reserved.
#
# Original Author:
# Copyleft 2020 Adam Hendry. All rights reserved.
#
# License:
# GNU GPL vers. 2.0
#
# This script is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation.
#
# This script is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this script; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
# Boston, MA 02111-1307 USA
GIT_DIR='.git'
REBASE_DIR="${GIT_DIR}/rebase-merge"
TODO_FILE="${REBASE_DIR}/git-rebase-todo"
TODO_BACKUP="${TODO_FILE}.backup"
HEADS_FOLDER='refs/heads'
TAGS_FOLDER='refs/tags'
REWRITTEN_FOLDER='refs/rewritten'
# Initialize associative array (dictionary) variables
declare -A labels_by_sha # Rebase label names indexed by original SHA
declare -A shas_by_label # Original SHAs indexed by rebase label names
# Get heads (remove '.git/refs/heads' from beginning)
heads=($(find "${GIT_DIR}/${HEADS_FOLDER}" -type f | cut -d '/' -f 4-))
# Get tags (remove '.git/refs/tags' from beginning)
tags=($(find "${GIT_DIR}/${TAGS_FOLDER}" -type f | cut -d '/' -f 4-))
# Start the rebase operation in the background
git rebase -i --rebase-merges &
# Capture the process ID
pid_main=$!
# Wait until the todo file is created
until [ -e "$TODO_FILE" ] && [ -e "$TODO_BACKUP" ]
do
continue
done
# Store rebase message
rebase_message=$(tac $TODO_FILE | sed '/^$/q' | tac)
# Store todo list
rebase_message_length=$(echo "$rebase_message" | wc -l)
todo_list=$(cat $TODO_FILE | head -n -"$rebase_message_length")
# Prompt user
printf "Calculating todo file. Please wait..." > $TODO_FILE
# Get label names
label_names=($(grep -oP '^(l|label) \K[^ ]*$' -- $TODO_BACKUP))
for label_name in "${label_names[@]}"
do
if [ $label_name = 'onto' ]
then
continue
fi
command_line=$(grep -B 1 -P '^(l|label) '"$label_name"'$' $TODO_BACKUP | head -n 1 | sed 's/\n//g')
command_name=$(echo "$command_line" | grep -oP '^(p|pick|m|merge)(?= )')
label_sha=
if [ "$command_name" = 'p' ] || [ "$command_name" = 'pick' ]
then
label_sha=$(echo $command_line | grep -oP '^(p|pick) \K[[:alnum:]]*' | cut -c1-7)
elif [ "$command_name" = 'm' ] || [ "$command_name" = 'merge' ]
then
label_sha=$(echo $command_line | grep -oP '^(m|merge) -[cC] \K[[:alnum:]]*' | cut -c1-7)
fi
shas_by_label["$label_name"]="$label_sha"
labels_by_sha["$label_sha"]="$label_name"
done
# Restore Branch Names
todo_list+="\n\n# Restore Branch Names\n"
for head in "${heads[@]}"
do
sha=$(cat "${GIT_DIR}/${HEADS_FOLDER}/${head}" | cut -c1-7)
if [ -n "${labels_by_sha[$sha]}" ]
then
todo_list+='exec git update-ref '"${HEADS_FOLDER}/${head}"' '"${REWRITTEN_FOLDER}/${labels_by_sha[$sha]}\n"
fi
# elif in `git rev-list`, pick sha and label it, then `git update-ref` here`
done
todo_list+='\n# Restore Tag Names\n'
for tag in "${tags[@]}"
do
sha=$(cat "${GIT_DIR}/${TAGS_FOLDER}/${tag}" | cut -c1-7)
if [ -n "${labels_by_sha[$sha]}" ]
then
todo_list+='exec git update-ref '"${TAGS_FOLDER}/${tag}"' '"${REWRITTEN_FOLDER}/${labels_by_sha[$sha]}\n"
fi
done
todo_list+="$rebase_message"
# Update todo file
printf "$todo_list" > $TODO_FILE
# Wait until the rebase operation is completed
wait $pid_main
# Exit the script
exit 0
答案:
交互式变基可用于影响这些更改,但存在于 HEAD
和变基根之间的分支和标记名称,否则会阻止 Git 的垃圾收集删除这些较旧的提交,必须首先删除,然后在变基后重新应用。不幸的是,为了正常工作,变基必须从你的历史记录开始(即 develop
分支)
从 develop
变基:
git checkout develop
git branch -D feature/project-setup
git branch -D master
git tag -d 0.1.0
git rebase -i --rebase-merges
将 edit
添加到您希望更改的提交中,然后暂存更改 (git add -A
),修改提交 (git commit --amend
),并完成变基 (git rebase --continue
).
之后,将分支和标签名称一一添加回去
git branch master cfa8
git branch feature/project-setup 1696
git checkout master
git tag 0.1.0
虽然 here 开发的 git-rebasetags 脚本是一个好的开始,但它只适用于 Linux 机器,只变基标签而不是分支名称,匹配标签提交消息(不适用于非注释标签),并使用 python 而不是 shell 脚本,后者的可移植性稍差。
或者,rebase-todo
可以更新如下:
label onto
# Branch feature-project-setup
reset onto
pick ff7abc83 Install initial project site packages
pick 1696181f Add `.bumpversion.cfg`
label feature-project-setup
# Branch release-0-1-0
reset 8e2d63e # Initial commit
merge -C c598c3bf feature-project-setup # Merge branch 'feature/project-setup' into develop
label branch-point
pick 0cea85a3 Bump version: 0.0.0 → 0.1.0
label release-0-1-0
# Branch 0-1-0
reset 8e2d63e # Initial commit
merge -C cfa8ed17 release-0-1-0 # Merge branch 'release/0.1.0' into master
label 0-1-0
reset branch-point # Merge branch 'feature/project-setup' into develop
merge -C a22db135 0-1-0 # Merge tag '0.1.0' into develop
label develop
# Reset branch and tag names
reset feature-project-setup
exec git branch -D feature/project-setup
exec git branch feature/project-setup
reset 0-1-0
exec git tag -d 0.1.0
exec git tag 0.1.0
exec git branch -D master
exec git branch master
reset develop
或者,由于 git rebase 将标签写入 refs/rewritten
,这可以使用一些管道命令在更少的行中完成:
exec git update-ref refs/heads/feature/project-setup refs/rewritten/feature-project-setup
exec git update-ref refs/heads/master refs/rewritten/0-1-0
exec git update-ref refs/tags/0.1.0 refs/rewritten/0-1-0
上面的标签 0-1-0
在此特定实例中适用于 master
和标签 0.1.0
。
如果可以将其作为 rebase 的额外选项,如 --rebase-tags
和 rebase-branch-names
,那就太好了。不幸的是,pre-rebase
挂钩将不起作用,因为这发生在 rebase-todo
之前。此外,没有 post-rebase
挂钩。因此,单独的 shell 脚本似乎是明智的。
最后,上面没有变基分叉点,如果变基路径修订列表中有未合并的分叉点,这也是需要的。如果发生这种情况,您可以尝试在待办事项列表的末尾添加以下内容:
reset new_base_branch # Be sure to `label new_base_branch` before here at right spot
exec git branch temp_name # Give new base a temp name
exec git checkout branch_to_rebase
exec git rebase temp_name
exec git branch -D temp_name
这也是一个很好的附加选项(如 --rebase-forks
),但代码还需要检查 branch_to_rebase
实际上没有合并回 onto
在变基路径之外的分支。为了最好的安全,我总是 rebase -i --rebase-merges
从你的存储库的提示提交。
我有以下 Git 历史记录:
我想从提交 1f63
(之前 2 次提交)到 feature/project-setup
处的 HEAD 进行交互变基,如下所示:
git rebase -i HEAD~2
git-rebase-todo
文件包含以下行:
pick ff7abc8 Install initial project site packages
pick 1696181 Add `.bumpversion.cfg`
如果我将第一行更改为 edit
,应用我的更改,然后执行 git commit --amend
和 git rebase --continue
,我的提交历史现在如下所示:
我知道交互式 git rebase 正在挑选两个提交到 rebase 的根目录(在本例中,提交 1f63
)。我的问题是,如何用 e16f
覆盖 1696
并使分支保持不变? (我希望我的最终历史看起来像原来的那样,但是 1696
将被有我更改的新提交 e16f
取代)
我最初的想法是我可能首先需要挑选那些提交,然后删除它们,添加一个中断,签出 feature/project-setup
然后进行快进合并提交。有什么想法吗?
编辑: 如果有更简单的方法不需要重置开发、主控和 0cea8
然后重新合并,请告诉我。
If there's a simpler way to do this that doesn't require resetting develop, master, and
0cea8
and then remerging, please let me know.
没有,但重置 0cea8
的想法也是胡说八道。只能“重置”分支名称(以及 Git 的索引和您的工作树,因为 Git 在 git reset
命令中塞进了太多东西)。请参阅下面关于分支名称的一件大事。
关于提交有几点需要注意:
- 它们完全是只读的。
- 他们的 true 名称是他们的哈希 ID,它是根据提交的内容计算的(包括元数据——从技术上讲,它仅根据元数据计算,因为保存的快照是元数据) .
- 任何一个给定提交的元数据给出了其前身提交(如果是常规单亲提交)或其所有父提交(如果是合并提交)的原始哈希 ID。
这些,放在一起,意味着你总是可以复制一个提交到一个新的和改进的版本,但是这样做之后,你现在必须复制每个“下游”(后续) 犯罪。那是因为,给出类似的东西:
... <-F <-G <-H
其中 H
是序列中的“最后”提交,如果我们将 F
复制到新的和改进的 F'
,我们将需要一个新的和-改进 G
其中改进或至少其中之一是 G'
的父级是 F'
。然后我们还需要将 H
复制到一个新的和改进的 H'
以便它的父级可以是 G'
.
关于 分支名称 ,如 master
和 develop
,真的只有一件大事需要了解——几乎所有其他事情都源于这件大事——而且那就是每个人只持有 one 提交哈希 ID。无论存储在分支名称中的哈希 ID 是什么,该提交都是该分支的 last 提交:
...--F--G--H <-- branch
分支 branch
的 最后 提交是提交 H
。是提交 H
本身导致提交 G
成为分支 branch
的一部分,然后是提交 G
本身导致提交 F
成为分支的一部分分支等等,通过历史倒退。历史 是 提交集:不多也不少。
因此,如果您有一些您不喜欢的历史记录,您可以建立您喜欢的新历史记录(新提交),但即使对某些 过去 提交,新的和改进的提交具有不同的哈希 ID,并且此更改会波及其余提交。
Git 2.18 学到了一个新的 --rebase-merges
选项,它可以让你重建某些历史记录,不仅可以自动复制单个提交,还可以重复合并操作(合并提交不能 copied 因此必须重新执行合并)。这可以帮助您完成大部分工作,但仍然需要手动调整分支名称,以便指向新的和改进的提交。
更新:2021 年 5 月 6 日
这是一个(相对)简单的 bash 脚本,它将更新分支名称和标签。 使用风险自负!将其放入
/usr/bin/ # On Linux
或
C:\Program Files\Git\usr\bin # On Windows
并命名
git-rebase-bti
(没有文件扩展名),并使其可执行
chmod +x path/to/git-rebase-bti
或 Windows
icacls path/to/git-rebase-bti /grant your_usrnm:(rx)
虽然我相信 Windows 默认情况下为文件提供可执行权限? (不要引用我的话)
#! /bin/sh -
#
# Git Rebase Branch Names, Tags, and Forks
#
# File:
# git-rebase-bti
#
# Installation Instructions:
# Place this file in the following folder:
# - Linux: `/usr/bin/`
# - Windows: `C:\Program Files\Git\usr\bin`
#
# Usage:
# git rebase-bti <SHA>
#
# Authors:
# Copyleft 2020 Adam Hendry. All rights reserved.
#
# Original Author:
# Copyleft 2020 Adam Hendry. All rights reserved.
#
# License:
# GNU GPL vers. 2.0
#
# This script is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation.
#
# This script is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this script; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
# Boston, MA 02111-1307 USA
GIT_DIR='.git'
REBASE_DIR="${GIT_DIR}/rebase-merge"
TODO_FILE="${REBASE_DIR}/git-rebase-todo"
TODO_BACKUP="${TODO_FILE}.backup"
HEADS_FOLDER='refs/heads'
TAGS_FOLDER='refs/tags'
REWRITTEN_FOLDER='refs/rewritten'
# Initialize associative array (dictionary) variables
declare -A labels_by_sha # Rebase label names indexed by original SHA
declare -A shas_by_label # Original SHAs indexed by rebase label names
# Get heads (remove '.git/refs/heads' from beginning)
heads=($(find "${GIT_DIR}/${HEADS_FOLDER}" -type f | cut -d '/' -f 4-))
# Get tags (remove '.git/refs/tags' from beginning)
tags=($(find "${GIT_DIR}/${TAGS_FOLDER}" -type f | cut -d '/' -f 4-))
# Start the rebase operation in the background
git rebase -i --rebase-merges &
# Capture the process ID
pid_main=$!
# Wait until the todo file is created
until [ -e "$TODO_FILE" ] && [ -e "$TODO_BACKUP" ]
do
continue
done
# Store rebase message
rebase_message=$(tac $TODO_FILE | sed '/^$/q' | tac)
# Store todo list
rebase_message_length=$(echo "$rebase_message" | wc -l)
todo_list=$(cat $TODO_FILE | head -n -"$rebase_message_length")
# Prompt user
printf "Calculating todo file. Please wait..." > $TODO_FILE
# Get label names
label_names=($(grep -oP '^(l|label) \K[^ ]*$' -- $TODO_BACKUP))
for label_name in "${label_names[@]}"
do
if [ $label_name = 'onto' ]
then
continue
fi
command_line=$(grep -B 1 -P '^(l|label) '"$label_name"'$' $TODO_BACKUP | head -n 1 | sed 's/\n//g')
command_name=$(echo "$command_line" | grep -oP '^(p|pick|m|merge)(?= )')
label_sha=
if [ "$command_name" = 'p' ] || [ "$command_name" = 'pick' ]
then
label_sha=$(echo $command_line | grep -oP '^(p|pick) \K[[:alnum:]]*' | cut -c1-7)
elif [ "$command_name" = 'm' ] || [ "$command_name" = 'merge' ]
then
label_sha=$(echo $command_line | grep -oP '^(m|merge) -[cC] \K[[:alnum:]]*' | cut -c1-7)
fi
shas_by_label["$label_name"]="$label_sha"
labels_by_sha["$label_sha"]="$label_name"
done
# Restore Branch Names
todo_list+="\n\n# Restore Branch Names\n"
for head in "${heads[@]}"
do
sha=$(cat "${GIT_DIR}/${HEADS_FOLDER}/${head}" | cut -c1-7)
if [ -n "${labels_by_sha[$sha]}" ]
then
todo_list+='exec git update-ref '"${HEADS_FOLDER}/${head}"' '"${REWRITTEN_FOLDER}/${labels_by_sha[$sha]}\n"
fi
# elif in `git rev-list`, pick sha and label it, then `git update-ref` here`
done
todo_list+='\n# Restore Tag Names\n'
for tag in "${tags[@]}"
do
sha=$(cat "${GIT_DIR}/${TAGS_FOLDER}/${tag}" | cut -c1-7)
if [ -n "${labels_by_sha[$sha]}" ]
then
todo_list+='exec git update-ref '"${TAGS_FOLDER}/${tag}"' '"${REWRITTEN_FOLDER}/${labels_by_sha[$sha]}\n"
fi
done
todo_list+="$rebase_message"
# Update todo file
printf "$todo_list" > $TODO_FILE
# Wait until the rebase operation is completed
wait $pid_main
# Exit the script
exit 0
答案:
交互式变基可用于影响这些更改,但存在于 HEAD
和变基根之间的分支和标记名称,否则会阻止 Git 的垃圾收集删除这些较旧的提交,必须首先删除,然后在变基后重新应用。不幸的是,为了正常工作,变基必须从你的历史记录开始(即 develop
分支)
从 develop
变基:
git checkout develop
git branch -D feature/project-setup
git branch -D master
git tag -d 0.1.0
git rebase -i --rebase-merges
将 edit
添加到您希望更改的提交中,然后暂存更改 (git add -A
),修改提交 (git commit --amend
),并完成变基 (git rebase --continue
).
之后,将分支和标签名称一一添加回去
git branch master cfa8
git branch feature/project-setup 1696
git checkout master
git tag 0.1.0
虽然 here 开发的 git-rebasetags 脚本是一个好的开始,但它只适用于 Linux 机器,只变基标签而不是分支名称,匹配标签提交消息(不适用于非注释标签),并使用 python 而不是 shell 脚本,后者的可移植性稍差。
或者,rebase-todo
可以更新如下:
label onto
# Branch feature-project-setup
reset onto
pick ff7abc83 Install initial project site packages
pick 1696181f Add `.bumpversion.cfg`
label feature-project-setup
# Branch release-0-1-0
reset 8e2d63e # Initial commit
merge -C c598c3bf feature-project-setup # Merge branch 'feature/project-setup' into develop
label branch-point
pick 0cea85a3 Bump version: 0.0.0 → 0.1.0
label release-0-1-0
# Branch 0-1-0
reset 8e2d63e # Initial commit
merge -C cfa8ed17 release-0-1-0 # Merge branch 'release/0.1.0' into master
label 0-1-0
reset branch-point # Merge branch 'feature/project-setup' into develop
merge -C a22db135 0-1-0 # Merge tag '0.1.0' into develop
label develop
# Reset branch and tag names
reset feature-project-setup
exec git branch -D feature/project-setup
exec git branch feature/project-setup
reset 0-1-0
exec git tag -d 0.1.0
exec git tag 0.1.0
exec git branch -D master
exec git branch master
reset develop
或者,由于 git rebase 将标签写入 refs/rewritten
,这可以使用一些管道命令在更少的行中完成:
exec git update-ref refs/heads/feature/project-setup refs/rewritten/feature-project-setup
exec git update-ref refs/heads/master refs/rewritten/0-1-0
exec git update-ref refs/tags/0.1.0 refs/rewritten/0-1-0
上面的标签 0-1-0
在此特定实例中适用于 master
和标签 0.1.0
。
如果可以将其作为 rebase 的额外选项,如 --rebase-tags
和 rebase-branch-names
,那就太好了。不幸的是,pre-rebase
挂钩将不起作用,因为这发生在 rebase-todo
之前。此外,没有 post-rebase
挂钩。因此,单独的 shell 脚本似乎是明智的。
最后,上面没有变基分叉点,如果变基路径修订列表中有未合并的分叉点,这也是需要的。如果发生这种情况,您可以尝试在待办事项列表的末尾添加以下内容:
reset new_base_branch # Be sure to `label new_base_branch` before here at right spot
exec git branch temp_name # Give new base a temp name
exec git checkout branch_to_rebase
exec git rebase temp_name
exec git branch -D temp_name
这也是一个很好的附加选项(如 --rebase-forks
),但代码还需要检查 branch_to_rebase
实际上没有合并回 onto
在变基路径之外的分支。为了最好的安全,我总是 rebase -i --rebase-merges
从你的存储库的提示提交。