Git 如何自动删除取消自己的提交?

Git 如何自动删除取消自己的提交?,git,rebase,simplify,Git,Rebase,Simplify,我有一个开发分支,有很多承诺。这些提交包括“真实”更改(例如添加功能)以及临时更改(例如,在一次提交中添加测试代码,然后在以后的提交中删除) 这一分支的真正变化正在逐渐添加到主文档中。每次添加之后,我都会在新母版上重新设置开发分支的基础。随着每个周期的进行,开发分支和主分支之间的差异越来越小。但是,重定基础的开发分支包含所有原始(现在已重定基础)提交。其中一些提交现在没有任何净效果 简单的例子: DEV_BRANCH Commit ... Commit D: Remove all

我有一个开发分支,有很多承诺。这些提交包括“真实”更改(例如添加功能)以及临时更改(例如,在一次提交中添加测试代码,然后在以后的提交中删除)

这一分支的真正变化正在逐渐添加到主文档中。每次添加之后,我都会在新母版上重新设置开发分支的基础。随着每个周期的进行,开发分支和主分支之间的差异越来越小。但是,重定基础的开发分支包含所有原始(现在已重定基础)提交。其中一些提交现在没有任何净效果

简单的例子:

DEV_BRANCH
    Commit ...
    Commit D: Remove all test code from f.cpp
    Commit ...
    Commit C: Add new feature to f.cpp
    Commit ...
    Commit B: Add more test code to f.cpp
    Commit A: Add some test code to f.cpp
    Commit ...
MASTER
    Commit X: Add new feature to f.cpp
    Commit ...
此时,从commit
C
到f.cpp的更改已作为commit
X
在主控中存在,并且组合后的commit
A
+
B
+
D
对f.cpp没有更改。换句话说,分支和主分支之间的差异对于文件f.cpp不显示任何内容

(实际上,提交A、B、C和D也可能包括对其他文件的更改。)

是否有任何方法可以在重新基址期间或以其他方式自动简化开发分支中的提交?

在上面的简单示例中,是否可以删除提交
C
(更改已合并为master,因此现在为“空”),并自动提交
A
B
D
(组合后无更改)

在一个更复杂的场景中,当提交到f.cpp也修改其他文件时,是否可以从开发分支中的提交中自动删除对文件f.cpp的更改(以简化这些更改),但如果提交包含对其他文件的更改,则保留提交


换言之,如果将开发分支中的所有提交中的所有更改应用于一个文件会导致一个与主文件中的文件相同的文件,那么是否可以从开发分支提交中删除对此文件的更改?(我确实意识到,如果对其他文件的更改需要与对没有净影响的文件的更改同步进行,这可能会导致副作用。但是,在我的场景中,这不是问题,因为我不需要从开发分支中挑选任何中间状态。)

我看到人们在评论中,解决了复杂性问题

一般来说,这是不可能的,因为这个问题太难了。如果您将问题定义为“empty”(与前面的内容没有任何更改)提交,那么这很容易,而且事实上已经在Git中了:rebase已经做到了,除非您要求
--keep empty

(就此而言,
git filter branch
,有点像类固醇上的rebase,也可以这样做,尽管它的默认值是相反的:它保持这样的提交,除非给定
——prune empty
。如果您使用的是提交过滤器,您可以使用
git\u commit\u non\u empty\u tree
,它只是将当前树与以前的树进行比较在实际调用
git commit tree
之前执行。在这种情况下,
git filter branch
可能比您想要的更重。)

你在评论中写道:

我考虑从主分支创建一个新分支,并从我的原始开发分支复制所有更改的文件。但是,这会创建一个新的一次性提交,并降低所有粒度(这在我完成挑选所有真正更改之前非常有用)

Git就是这样设计的:只需“detach”HEAD(
Git checkout--detach
或按原始SHA-1 ID签出)并进行任何您喜欢的临时提交。一旦切换回命名分支,临时提交将仅在受
HEAD
reflog保护的情况下保持(对于无法从tip提交访问,默认为30天)。(好吧,如果它们是松散对象,它们也将获得14天的宽限期,但14小于30。)

您甚至不必进行提交:只需
git diff
(或
git diff tree
)master顶端的树与
dev_branch
顶端的树。换句话说,完整的源代码树的两种形式都已经在git中,作为两个单独的提交。它们有(其他提交的)任何历史记录在它们之间:

          o-------o    <-- master
         /
...--o--o
         \
          o--o--o--o   <-- dev_branch

o--------o我认为对于您的特定用例,有两个相当简单的解决方案,首先考虑了复制是如何生成的

解决方案1:对于发散分支上的少量提交

假设您在
dev
中有以下一组提交,如
git-rebase--interactive
模式所示:

pick bf45b13 Add feature X
pick b1f790f Cleanup feature X
pick 40b299a Add feature Y
现在您决定将
40b299a
挑选到
master

问题是,当您重新设置
dev
的基础时,新的提交将是空的。我建议您在以后的基础上执行
git rebase-I master
。然后,您可以删除从所选内容中选择的行,只留下您想要的提交

由于您希望“自动”完成此操作,因此您可以创建自己的git脚本来同时执行cherry pick和rebase,从而在将其添加到master时消除
dev
中的正确提交

如果您调用以下脚本,例如
git transfer
,并将其添加到您的
路径中的某个地方,那么您可以将其作为
git transfer dev master commit[…]来调用:

#!/bin/bash

usage() {
    echo "Usage: git transfer from-branch to-branch commits [...]"
    if [ -n "${1}" ]
    then
        echo
        for line; do echo "${line}"; done
    fi
    exit 1
}

FROM="$(git rev-parse --abbrev-ref "${1}")"
[ -z "${FROM}" ] && usage 'from-branch must be a valid branch name' || shift
TO="$(git rev-parse --abbrev-ref "${1}")"
[ -z "${TO}" ] && usage 'to-branch must be a valid branch name' || shift

ORIGINAL="$(git rev-parse --abbrev-ref HEAD)"

if [ "${ORIGINAL}" == "${TO}" ]
then
    echo "Already on branch ${TO}"
else
    echo "Switching from ${ORIGINAL} to ${TO}"
    git checkout "${TO}" || exit
fi

while [ $# -gt 0 ]
do
    for COMMIT in "$(git rev-parse "${1}" | grep '^[^^]')"
    do
        echo "Moving ${COMMIT} to ${TO}"
        git cherry-pick "${COMMIT}"
        echo "Removing ${COMMIT} from ${FROM}"
        EDITOR="sed -i '/^pick $(git rev-parse --short ${COMMIT})/ d'" git rebase -i "${TO}" "${FROM}"
    done
    shift
done

if [ "${ORIGINAL}" != "${TO}" ]
then
    echo "Switching back to ${ORIGINAL} from ${TO}"
    git checkout "${ORIGINAL}"
fi
该脚本接受将被重定基础的“from”分支的名称、将被选中的“to”分支的名称以及提交列表、提交范围等。它在很大程度上基于本文和(也是针对循环的)中提出的思想

此解决方案适用于少量提交(最有可能一次提交一个),也适用于已发生显著分歧的分支

解决方案2:针对单个时间线上的大量提交

#!/bin/bash

usage() {
    echo "Usage: git transfer from-branch to-branch commits [...]"
    if [ -n "${1}" ]
    then
        echo
        for line; do echo "${line}"; done
    fi
    exit 1
}

FROM="$(git rev-parse --abbrev-ref "${1}")"
[ -z "${FROM}" ] && usage 'from-branch must be a valid branch name' || shift
TO="$(git rev-parse --abbrev-ref "${1}")"
[ -z "${TO}" ] && usage 'to-branch must be a valid branch name' || shift

ORIGINAL="$(git rev-parse --abbrev-ref HEAD)"

if [ "${ORIGINAL}" == "${TO}" ]
then
    echo "Already on branch ${TO}"
else
    echo "Switching from ${ORIGINAL} to ${TO}"
    git checkout "${TO}" || exit
fi

while [ $# -gt 0 ]
do
    for COMMIT in "$(git rev-parse "${1}" | grep '^[^^]')"
    do
        echo "Moving ${COMMIT} to ${TO}"
        git cherry-pick "${COMMIT}"
        echo "Removing ${COMMIT} from ${FROM}"
        EDITOR="sed -i '/^pick $(git rev-parse --short ${COMMIT})/ d'" git rebase -i "${TO}" "${FROM}"
    done
    shift
done

if [ "${ORIGINAL}" != "${TO}" ]
then
    echo "Switching back to ${ORIGINAL} from ${TO}"
    git checkout "${ORIGINAL}"
fi