在补丁添加后从意外的git commit -a中恢复(git add -p)

时间:2017-05-21 03:15:20

标签: git git-commit

今天早些时候,我正在将一个大的git提交分成几个较小的提交,所以我像往常一样创建了一个新的分支,做了一个git reset HEAD^,并继续git add -p以交互式选择件这个大型提交分成了第一个较小的提交。我花了20-30分钟选择了我想要的线,然后决定提交第一个补丁。出于习惯,我反复输入git commit -a而不仅仅是git commit并点击进入,当然我丢失了所有的补丁选项,我最终得到了我之前所拥有的:一个大的提交

有没有办法恢复到以前版本的分阶段更改?我想要我想要的是恢复到我的.git / index文件的前一版本,但是从我可以告诉我的阶段更改没有版本化。所以我觉得我搞砸了,我必须重新开始。

我每天都在使用Git几年,过去也发生过几次这种情况。每当我意识到自己所做的一切,我都会面对并重新开始。我怀疑这可以通过预提交钩子来解决,该钩子制作.git / index(也许是.git / index.bak)的副本以获得一级手动“撤销”,但我不确定是否会足够,因为我对Git内部没有非常深刻的理解。我发现内部信息的最佳位置是Git书的Chapter 10,尽管我已经考虑过使用Git源代码来更好地理解暂存区域的工作原理。

提前感谢任何想法。

2 个答案:

答案 0 :(得分:3)

TL; DR:您可以使用git fsck --lost-found

但它会很痛苦。

修改:如果你的目标是阻止自己将来这样做,那有点棘手。你可以在某处保存原始(预提交)索引文件;在--all覆盖它们之前,此索引文件中包含保存精心分段文件的blob对象的哈希ID。但是,找到这个索引文件的路径是很棘手的:你的预提交钩子被调用,GIT_INDEX_FILE被设置为(现在被锁定)"真实"的路径。包含更新的哈希值的索引,而不是保留的回滚索引。你可以使用它可能只是.git/index的可怕黑客,但是在添加工作树的情况下这是错误的(参见git-worktree)。另请参阅builtin/commit.c near line 400

更安全的技巧可能是write your own git front end script or alias(参见,例如this answer)。如果您运行的命令为git commit且其中一个参数为-a,请检查索引是否与HEAD匹配(使用git diff-index --cached;请参阅require_clean_work_tree } git-sh-setup.sh。如果没有,你可能想要某种"我的意思是它的选择。

详细说明

不幸的是,当使用带有pathspecs的--all--include时,Git只是用附加或所有文件覆盖现有的索引内容。 (好吧,如果提交失败,Git将恢复旧索引,但可能提交没有失败。)当使用带有pathspecs的--only时,情况会更复杂,但是&# 39;不是这里的情况。

这意味着文件的交互式修补版本已丢失。

好的一面,丢失这意味着丢失,而不是被破坏:他们仍然在你的存储库中,他们只是没有简单的方法要找到。对象通常在第一次写入后至少保留在存储库中14天,以便让Git的各个部分有足够的时间将名称附加到它们上面,从而可以找到它们。 (如果没有此宽限期,任何触发自动git gc的随机后台操作都可能会破坏某些其他 Git进程仍在处理的对象。)

这个意味着您可以运行:

git fsck --lost-found

并且Git将慢慢地,痛苦地遍历存储库中的每个对象,而不仅仅是那些容易找到的对象,以确定是否可以找到对象。如果对象有正常的方法来查找它 - 例如,在提交中 - 则没有什么特别的事情发生。如果对象有找到它的方法,Git称之为悬空对象(更准确地说,是一个悬空blob 悬挂提交 -there'技术性的东西在这里是特殊的提交,不适用于blob)。

blob 这个词本质上意味着文件。使用--lost-found,这些"悬挂的blob"被复制到其扩展的原始文本文件表示中,并填入.git/lost-found/other/。这里的主要问题是文件的名称是真正销毁的(它只在索引中),所以这些文件现在都是用它们的哈希ID命名的。

这里的每个文件可能会有很多版本。你必须仔细研究它们,并发现9ab30ad...稍微错误而不是57e4eea...,但是哎呀,看看这个,e83c1d7稍微好于其中任何一个,并且。 .. 反正你懂这个意思。 : - )

如果某个机会,修补的文件foo的版本碰巧与任何提交中任何文件的任何版本完全匹配 - 例如,x.doc的正确版本可能是,你还有另一个空文件已提交 - 然后就不会成为该文件的悬空blob,因为只有一个特定版本数据的副本。 (例如,空文件的哈希标识为e69de29bb2d1d6434b8b29ae775ad8c2e48c5391每个空文件都有相同的哈希!文件hello\n有哈希ce013625030ba8dba906f756967f9e9ca394464a。要看到这个,运行echo hello | git hash-object -t blob --stdin。)

答案 1 :(得分:0)

我写了一个git commit钩子(~/.git/hooks/pre-commit),如果有阶段性更改(即您之前使用过git commit -a),它将中止git add -p。它不是100%防弹的,但对我来说效果很好:

#!/bin/bash

# `git commit -a` changes GIT_INDEX_FILE to .git/index.lock:
if GIT_INDEX_FILE=.git/index git diff --cached --diff-filter=ad --quiet ; then
    exit 0  # no modifications staged, no problem
fi

if git diff --diff-filter=ad --quiet ; then
    exit 0  # no modifications left over, no problem
fi

check_args () {
    while [ $# -gt 0 -a "$1" != commit ] ; do shift ; done ; shift
    while [ $# -gt 0 ] ; do
        case $1 in
            -a|--all)
                echo >&2 "Attention, staged changes and $1, aborting!"
                return 1
                ;;
            -m|--message|--author|--date|--cleanup|--pathspec-from-file|\
            -t|--template|-F|--file|--fixup|--squash|-c|--reedit-message|\
            -C|--reuse-message)
                # options with argument
                shift ; shift
                ;;
            --)
                shift ; break
                ;;
            -*)
                shift
                ;;
            *)
                break
                ;;
        esac
    done
    if [ $# -gt 0 ] ; then
        echo >&2 "Attention, staged changes and pathspec given, aborting!"
        return 1
    fi
}

pstree=$(pstree -spl $$) || exit 1
gitpid=$(echo "$pstree" | sed -n 's/^.*---git(\([0-9]\+\))---.*$/\1/p')
if [ -z "$gitpid" ] ; then
    echo "Could not find PID of git process, aborting!"
    echo "pstree: $pstree"
    exit 1
fi

mapfile -d '' cmdline < "/proc/$gitpid/cmdline"
check_args "${cmdline[@]}"
exit $?