把工作区改动保存起来、稍后可以重新应用的栈。

git stash 把改动暂存到别处

Git stash 记录选定的未提交工作,让检出可以暂时回到较早的已提交状态。它适合短暂中断,但保存和恢复都不是对目录中所有文件的无条件承诺。真正需要确认的是包含了什么、工作原来基于哪个版本、目标现在处于什么状态,以及保存条目何时才能移除。只看到命令报告成功,还不足以证明所有想保留的数据都已经纳入快照。 明确快照包含的范围 普通保存操作记录受跟踪文件的工作树变化与索引,再把这些修改退回到 HEAD 对应状态。未跟踪文件默认留下,只有加入相应选项才会一起保存;被忽略文件在包含未跟踪文件时仍然留下,需要更大的包含范围才会纳入。选择之前先检查,因为忽略路径里可能有凭据、大型构建输出或不适合进入 Git 对象的本地数据,不能为了追求“全部收起来”就不看内容。 选定路径、只处理已暂存内容、保留索引等选项,会产生不同结果。不要习惯性组合参数,再假定它们代表同一份快照。应先检查状态与预期差异,为条目写明含义,并查看实际保存的内容,再把原编辑视为已经能够恢复。尤其要区分磁盘文本和索引文本,它们可能在同一路径上分别代表两阶段尚未完成的工作。 stash 使用 Git 提交对象表示,不是对象库之外的特殊文件。最新条目由专用引用指向,更旧的条目通过该引用的日志保留,所以普通分支历史不一定显示它们。忘记使用并不自动等于丢失,但删除保留引用之后,对象最终可能无法恢复。本地分支或 stash 也不会仅凭存在,就提供一份独立的异地备份;保存形式与备份位置是不同问题。 应用与弹出不同 应用命令尝试恢复,同时保留保存条目,适合先审阅恢复结果,再决定是否删除副本。默认不能期待原来的已暂存与未暂存区别自动重建;索引选项会额外尝试恢复这一状态,但如果冲突或目标变化妨碍恢复,仍可能失败。应检查索引和磁盘各自的结果,而不只看最终文件里是否出现了某段文字。 弹出操作只在应用成功后才从列表移除条目。发生冲突时,保存项保留,但能够干净应用的路径可能已经改变。因此,非零返回不代表整个工作树完全没动。再次操作前应查看索引和文件,不能在部分恢复的目录上反复弹出同一条目,指望重复执行自动修复第一次冲突。 条目不属于某个分支或关联工作树私有。描述中出现的原分支名称是背景,不是强制只能在那里恢复的规则。随着新增和删除,数字选择器还会变化,所以敏感操作前要检查并记录准确对象。在 Shell 中传入带花括号的选择器时应引用,避免外层语法干扰。需要删除的究竟是哪份内容,应由对象证据确认,而不是仅凭它此刻排在第一位。 坏掉的样子 在变化后的基线上恢复可能冲突;即便文本没有冲突,也可能需要业务适配。应依据最终代码意图解决,暂存具体路径并验证结果,不能反复向半恢复状态应用同一份变化。stash 应用没有一个通用中止命令,能够自动撤销任何已经产生的部分效果,因此“先试试,不行就一键回去”不是可靠的恢复设计。 如果适合回到原始基线继续,可以使用建立分支的恢复命令:它从记录的起点创建分支,再应用保存工作。通过 stash 日志指定条目且应用成功时,条目也会被移除。这是与记录基线有关的恢复流程,不要求必须有两份 stash,也不存在超过某个时间就必须采用的固定期限。应按当前基线与恢复意图判断,而不是按存放时长机械选择。 移除应发生在验证之后,不能代替验证。清空整个列表可能丢弃无关工作,已经删除的对象以后还可能被回收。需要持续评审的工作,应在适合的分支形成有意提交,并遵守项目的备份与共享规则。stash 也不承诺捕获编辑器未保存缓冲区、任意文件系统元数据或构建使用的全部外部资源,不能把它当作整套开发环境的恢复镜像。 快速验证 使用 Python 3 和 Git 2.28 或更新版本运行下面实验。全部修改与移除都发生在可丢弃仓库,使用示例身份,关闭钩子及签名,排除个人 Git 配置和路由变量,没有远端。样例故意让索引与工作文件包含不同值,再检查保存范围、索引恢复、冲突后的条目保留,以及验证后才删除的顺序。 import os import subprocess import tempfile from pathlib import Path env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")} env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull) with tempfile.TemporaryDirectory(prefix="git-stash-demo-") as directory: root = Path(directory) def git(*args, check=True): return subprocess.run( ["git", "-c", "user.name=Stash Example", "-c", "user.email=stash@example.invalid", "-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", *args], cwd=root, env=env, text=True, capture_output=True, check=check, ) git("init", "-q", "-b", "main") (root / ".gitignore").write_text("ignored.txt\n") (root / "note.txt").write_text("base\n") git("add", ".gitignore", "note.txt") git("commit", "-qm", "base") (root / "note.txt").write_text("staged\n") git("add", "note.txt") (root / "note.txt").write_text("working\n") (root / "new.txt").write_text("untracked\n") (root / "ignored.txt").write_text("ignored\n") git("stash", "push", "-qm", "tracked only") saved = git("rev-parse", "refs/stash").stdout.strip() print("default-leaves-untracked:", (root / "new.txt").exists() and (root / "ignored.txt").exists() and (root / "note.txt").read_text() == "base\n") git("stash", "apply", "--index", saved) restored = (git("show", ":note.txt").stdout == "staged\n" and (root / "note.txt").read_text() == "working\n") assert restored print("index-and-work-restored:", restored) print("apply-keeps-entry:", git("rev-parse", "stash@{0}").stdout.strip() == saved) assert git("rev-parse", "stash@{0}").stdout.strip() == saved git("stash", "drop", "-q", "stash@{0}") git("stash", "push", "-qu", "-m", "tracked and untracked") saved = git("rev-parse", "stash@{0}").stdout.strip() print("include-untracked-not-ignored:", not (root / "new.txt").exists() and (root / "ignored.txt").exists()) (root / "note.txt").write_text("upstream\n") git("add", "note.txt") git("commit", "-qm", "new base") conflict = git("stash", "pop", "stash@{0}", check=False) print("conflicting-pop-keeps-entry:", conflict.returncode != 0 and bool(git("ls-files", "-u").stdout) and git("rev-parse", "stash@{0}").stdout.strip() == saved) (root / "note.txt").write_text("upstream\nworking\n") git("add", "note.txt", "new.txt") assert not git("ls-files", "-u").stdout assert git("show", ":note.txt").stdout == "upstream\nworking\n" assert (root / "new.txt").read_text() == "untracked\n" git("commit", "-qm", "retain both fixture values") assert git("rev-parse", "stash@{0}").stdout.strip() == saved git("stash", "drop", "-q", "stash@{0}") print("resolved-and-verified:", not git("status", "--porcelain").stdout and not git("stash", "list").stdout) 六个判断都应为真。默认保存留下未跟踪和忽略文件;带索引应用恢复两个受跟踪状态,同时保留条目。包含未跟踪文件仍不会包含忽略文件。后续冲突弹出保留对应条目;样例明确决定保留两项预期值,提交并验证结果之后,才删除已经核对的那个保存项。最后由 Python 清理包括忽略文件在内的全部演示数据,不操作真实项目的保存列表。 ...

十二月 11, 2027 · 2 分钟 · XB Field Notes