我有以下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
但有没有办法自动重新分支包括其所有子/后代的分支?
答案 0 :(得分:38)
git branch --contains C | \
xargs -n 1 \
git rebase --committer-date-is-author-date --preserve-merges --onto B C^
答案 1 :(得分:11)
几年前,我写了一些东西来处理这类事情。 (改进的评论当然是受欢迎的,但不要判断太多 - 这是很久以前了!我甚至都不知道Perl!)
这适用于更多静态情况 - 您可以通过设置branch.<branch>.autorebaseparent
形式的配置参数来配置它。它不会触及任何没有配置参数集的分支。如果这不是你想要的,你可能会把它砍到你想要的地方而不会有太多麻烦。在过去的一两年里,我并没有真正使用它,但是当我使用它时,它总是看起来非常安全和稳定,只要这可以通过大规模自动化变基。
所以在这里。通过将其保存到git-auto-rebase
中名为PATH
的文件中来使用它。在您真正尝试之前使用干运行(-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
我发现的一件事,因为我最初解决这个问题,有时答案是你实际上根本不想改变!首先要在正确的共同祖先处开始主题分支,而不是在此之后尝试将它们向前推进,这是可以说的。但这是你和你的工作流程之间的。
答案 2 :(得分: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
答案 3 :(得分:0)
如果需要更新提交者日期,可以使用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-date
或GIT_COMMITTER_DATE
以保证C'
,Ca'
和Cb'
提交的相同校验和(在重新定位时< em> feature , feature-a 和 feature-b 相应的。)