如何使用Git命令在不修改工作树的情况下保存一个暂存区?

75

我一直想使用一个可以在不修改我的工作树的情况下保存stash(轻量级备份),以免受git重置或其他可能会破坏索引的操作的影响。基本上相当于“git stash save && git stash apply”的功能等效,但是工作副本从未被触及,因为这可能会使某些文本编辑器/IDE出现问题。

类似这样的命令已经接近了我需要的东西,但还不完全符合:

git update-ref refs/stash `git stash create "Stash message"`

这个功能是可以正常使用的,但我遇到的问题是尽管实际的stash提交中有我的消息,但在"git stash list"中没有显示任何stash消息。考虑到stash可能会变得非常大,stash消息非常重要。

5个回答

47

git stash store "$(git stash create)"

这将创建类似于使用git stash命令创建的stash条目,但不会实际触及或清除您的工作目录和索引。

如果您检查stash列表或查看所有提交图形(包括stash),您将看到与正常调用git stash相似的结果。只是stash列表中的消息不同(通常是类似于"stash@{0}: WIP on master: 14e009e init commit",这里我们将得到"stash@{0}: Created via "git stash store""

$ git status --short
M file.txt
A  file2.txt

$ git stash list

$ git stash store "$(git stash create)"

$ git stash list
stash@{0}: Created via "git stash store".

$ git stash show 'stash@{0}'
 file.txt  | 2 +-
 file2.txt | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

$ git log --oneline --graph --all
*   85f937b (refs/stash) WIP on master: 14e009e init commit
|\
| * 26295a3 index on master: 14e009e init commit
|/
* 14e009e (HEAD -> master) init commit

$ git status
M file.txt
A  file2.txt

更详细的说明:

一个 git stash 条目由正常提交所表示,具有一些定义的结构。基本上它是一个普通的提交对象,有两个父对象(如果您使用 --include-untracked 选项,则有三个)(更多信息请参见 1,2)。

git stash create 创建这些代表 stash 条目的提交对象,并返回提交对象(具有 2 或 3 个父对象)的 对象名称(SHA-1)。这是一个悬空的提交(您可以在 git stash create 之后调用 git fsck 进行验证)。您需要使 refs/stash 指向此悬空提交,方法是通过 git stash store(或通过其他答案中所述的 git update-ref 进行操作,因为 git stash store 使用 git update-ref 完成其工作)。

查看 git stash push 的实际源代码并了解它基本上是调用 git stash creategit stash store,然后执行 一些逻辑 来清理文件(具体取决于您在 git stash push 中使用了哪些选项)。


1
非常好的答案和解释。绝对是我工作流程中的新部分。当我想要执行 sed -i 's/oldname/newname/' 时,它非常有效。无需担心备份后缀或任何其他问题。 - Nathan Chappell
哦,太棒了!正是我所需要的。我稍微调整了一下,加了一个注释,即 git stash store "$(git stash create 'comment')" - Matthew MacFarland
通过 "git stash create "message"提供的消息将出现在存储提交中。使用git stash store -m "message" 提供的消息将显示在存储 reflog 中。如果您想要两者,可以使用以下命令提供消息:git stash store $(git stash create "message") -m "message",否则在 git stash store之后,存储 reflog 中的消息将变为Created via "git stash store".`。 - webninja
1
无法将 --include-untracked 传递给 git stash create,因此可以通过将新创建的存储应用回工作树来包含未跟踪的文件(虽然更具侵入性)进行存储(使用 --include-untracked--keep-index)。这里是我用于执行任何一种类型的存储而不破坏当前工作树的完整函数的 shell 代码。 - webninja
我将这个添加到了我的 .bash_aliases 列表中。最后一个选项让我传递任何创建参数,包括注释。请注意,那些是反引号,围绕着 git stash create。别名 gs='git stash store `git stash create $@`' - bfuzze
如果您在运行git stash store "$(git stash create)"后继续处理工作,但意识到出现问题并想要返回到存储的WIP,请问该怎么做?我尝试了git stash pop,但是出现了error: Your local changes to the following files would be overwritten by merge: Please commit your changes or stash them before you merge.错误提示。 - LLaP

35

感谢 Charles 的提示,我编写了一份 Bash 脚本,以完全符合我的要求(我在将此实现为别名时遇到了问题)。它像 git stash save 一样接受一个可选的存储信息。如果没有提供,则使用由 git stash 生成的默认消息。

#!/bin/sh
#
# git-stash-snap
# Save snapshot of working tree into the stash without modifying working tree.
# First argument (optional) is the stash message.
if [ -n "$1" ]; then
        git update-ref -m "$1" refs/stash "$(git stash create \"$1\")"
else
        HASH=`git stash create`
        MESSAGE=`git log --no-walk --pretty="tformat:%-s" "$HASH"`
        git update-ref -m "$MESSAGE" refs/stash "$HASH"
fi

编辑:正如下面的评论所指出的那样,将此脚本保存为路径中的git-stash-snap就足以通过键入git stash-snap来调用它。

好处在于,即使您放弃使用此方法创建的某个stash,您仍然可以使用悬空提交的git log [commit-hash]查看stash消息!

编辑:自git 2.6.0版本以来,您可以向update-ref添加--create-reflog,然后即使之前没有使用git stashgit stash list也会显示此内容。

编辑:Git引入了一个名为stash push的新stash子命令,因此我已经将我的建议从git-stash-push更改为git-stash-snap


5
如果你将文件命名为git-stash-push并将其放在PATH中的某个位置,就不需要为它创建别名。git stash-push会找到(并调用)git-stash-push - Stefan Näwe
真的吗?有趣,我会试试看。 - Eliot
@Eliot,这是惊人的东西,感谢您的整理! - Dan Rosenstark
6
根据这个答案,我已经将以下内容添加到我的~/.gitconfig文件的[alias]部分,这应该提供相同的行为:stash-push = "!f() { if (( $# > 0)); then branch=$(git branch | sed -n 's/^\\* //p'); git update-ref -m \"On ${branch}: $*\" refs/stash $(git stash create \"On ${branch}: $*\"); else hash=$(git stash create); msg=$(git log --no-walk --pretty=\"tformat:%-s\" $hash); git update-ref -m \"$msg\" refs/stash $hash; fi;}; f" - me_and
1
@Eliot,我认为如果git使用push匹配pop,那么stash-save本来应该是一个很好的名称,但他们已经使用了它。 ;) 也许可以用stash-snapshot或者简称stash-snap - cp.engr
显示剩余3条评论

14

你需要将消息传递给update-ref而不是stash create,因为stash create不接受消息(它不会更新任何参照,因此没有reflog条目可填充)。

git update-ref -m "Stash message" refs/stash "$(git stash create)"

谢谢,这就是我要找的!但是你关于 git stash create 不接受消息是错误的。然而,提供给它的消息将成为提交消息。因此,为了完美地模拟由 git stash save 创建的存储提交,我认为以下命令可以完成任务:git update-ref -m "Stash message" refs/stash "$(git stash create 'Stash message')"。你能想到一种聪明的方法直接将其添加为 git 别名吗?如果不行,我可以将其制作成一个简单的 bash 脚本。 - Eliot
@Eliot:在 git stash list 中显示的消息来自于 reflog 而不是提交消息。这就是我所指的。你正确地指出了 create 确实需要一个消息,但这并不是一个记录在文档中的功能,因此我不知道这是否是故意设计成这样工作的,还是只是实现的副产品。 - CB Bailey
是的,说得好。我是偶然发现的。我在git维基上找到了一些关于在别名中使用参数的文档,所以我可以开始了! - Eliot
谢谢@Charles Bailey和+1,你帮我解决了又一个git问题。我稍微修改了一下这个链接的内容https://dev59.com/tWw15IYBdhLWcg3weLoF#6589093。 - Dan Rosenstark

11
以下结合了 @Mariusz Pawelski的答案一篇类似的答案,让您可以舒适地使用带消息的 git stash store
  1. 使用 git stash create 创建一个存储提交,然后使用 git stash store 将其保存到存储区。这不会更改工作目录中的任何文件。您可以添加有用的消息以便稍后找到正确的版本。总之:

    git stash store -m "saving intermediate results: my note 1" $(git stash create)
    
  2. 当您决定放弃当前工作并恢复到先前存储的状态时,请首先执行另一个 git stash。这将“丢弃”任何未提交的更改,将它们隐藏在存储区中,以便在下一步应用另一个存储状态时不会出现合并冲突。

    git stash
    
  3. 现在查看您在存储区引用中保存的内容:

    $ git stash list
    
    stash@{0}: saving intermediate results: my note 1
    stash@{1}: saving intermediate results: my note 2
    
  4. 最后,要恢复到存储的先前工作状态,丢弃当前的工作目录状态,请执行以下操作:

    git stash apply 1
    

    选择要恢复的存储提交的索引号,如上一步列表中的 stash@{...} 中所示。

  5. 如果您发现需要找回已丢弃的工作:它也保存在存储区中,并且可以使用与上述相同的技术进行恢复。

将包装作为Git别名

在上面的第1步中使用的命令可以与Git别名一起使用,使其更加方便。要创建别名(请注意最终#技术):

git config --global alias.stash-copy \
  '!git stash store -m "saving intermediate results: $1" $(git stash create) #'

从现在开始,您可以像这样使用Git别名:

git stash-copy "my note"

6
受Eliot方法的启发,我稍微扩展了他的脚本:
#!/bin/sh
#
# git-stash-push
# Push working tree onto the stash without modifying working tree.
# First argument (optional) is the stash message.
#
# If the working dir is clean, no stash will be generated/saved.
#
# Options:
#   -c "changes" mode, do not stash if there are no changes since the
#      last stash.
if [ "$1" == "-c" ]; then
        CHECK_CHANGES=1
        shift
fi


if [ -n "$1" ]; then
        MESSAGE=$1
        HASH=$( git stash create "$MESSAGE" )
else
        MESSAGE=`git log --no-walk --pretty="tformat:%-s" "HEAD"`
        MESSAGE="Based on: $MESSAGE"
        HASH=$( git stash create )
fi

if [ "$CHECK_CHANGES" ]; then
        # "check for changes" mode: only stash if there are changes
        # since the last stash

        # check if nothing has changed since last stash
        CHANGES=$( git diff stash@{0} )
        if [ -z "$CHANGES" ] ; then
                echo "Nothing changed since last stash."
                exit 0
        fi
fi

if [ -n "$HASH" ]; then
        git update-ref -m "$MESSAGE" refs/stash "$HASH"
        echo "Working directory stashed."
else
        echo "Working tree clean, nothing to do."
fi

我对Eliot的脚本做了以下更改:
  1. 当工作目录干净时,脚本会优雅地退出
  2. 当使用开关-c时,如果与上一个存储的差异没有变化,脚本将退出。如果您将此脚本用作“时间机器”,每10分钟自动进行一次存储,则这很有用。如果没有任何更改,将不会创建新的存储。如果没有此开关,则可能会得到n个连续的相同存储。

请注意,为了使开关-c正常工作,必须至少存在一个存储,否则脚本在git diff stash@{0}上抛出错误并且不会执行任何操作。

我使用以下bash循环将此脚本作为“时间机器”每10分钟拍摄一次快照:

 while true ; do date ; git stash-push ; sleep 600 ; done

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接