合并保留 B 与 U,并创建以两者为父级的 M。变基在 U 后重建 B,得到身份不同的 B′。

merge 与 rebase 画出的历史线不同

merge 与 rebase 都能整合工作,却对提交身份和祖先关系产生不同影响。合并通常保留既有提交并连接历史,变基则把选定变化移到另一基线。选择不只是图形看起来是否整齐,还影响评审引用、冲突处理、签名,以及已经依赖旧提交的协作者。应依据仓库流程与真实分支关系决定,而不是把“线性一定更好”或“有合并提交就一定安全”当成通用判断。 图示已分叉历史上的普通变基;快进与保留合并结构的变基有所不同。箭头表达父级关系,不代表墙上时钟顺序。 合并记录什么 两个末端已经分叉时,普通双分支合并创建以这两个末端为父级的新提交,并记录结果目录树;既有提交保持原身份。图保留的是祖先关系,不是所有活动的完整时间排序。提交日期与界面展示顺序也不能替代父级关系,否则可能把并行工作误读成严格前后发生的一串事件,进而错误判断某项修改究竟基于什么版本。 如果当前末端是传入末端的祖先,合并可以直接快进分支,不创建额外合并提交。要求始终创建合并提交,是通过相应选项表达的另一项流程规则。反过来,即便结果保留两个父级,也不能证明双方预期行为都正确保留。仍需审阅内容并运行集成测试,不能用提交拓扑替应用接口、数据规则或发布依赖完成验收。 变基改变什么 普通变基识别选定提交,再把它们的变化重新应用到新基线。重新创建的提交只要父级不同,对象标识就不同,即便补丁看起来相同。原作者信息通常保留,但新对象仍需自己的验证,以及流程要求的签名。指向旧对象的评审链接,也不会自动变成指向新对象的链接;后续说明需要让读者能够找到实际接受审阅的结果。 并不是每次变基都会重写所有提交:分支已经更新时可能无需重放;上游等价变化或应用后的空结果,也会影响哪些提交留下。普通变基从重放序列中省略合并提交,让选定工作趋于线性;明确启用重建合并的模式,则会尝试恢复分支结构。因此,“变基永远得到直线”同样过度概括,具体结果取决于参数和输入历史。 合并协调的是分支末端,变基通常逐个处理提交,所以冲突出现的位置与次数可能不同,同一种取舍还可能需要多次执行。完成后应比较最终目录树及重放变化,并重新运行相关测试。补丁相似,不证明新基线上行为完全相同;依赖、接口和上下文已经改变时,文本能够落地只是审查的开始。 坏掉的样子 重写其他工作已经依赖的提交,会产生协调成本。私人且未发布的分支,通常比共享集成分支更适合变基。明确协商过的共享历史重写并非不可能,但必须识别受影响引用及消费者。偏好线性历史,不等于获得替换别人已发布工作的权限;是否应该执行,需要独立于工具是否支持来判断。 变基本身并不必然要求强制推送。新的远端分支可以普通创建;远端末端仍是本地结果祖先时,也可以正常快进。需要替换已被重写的远端提交,才属于非快进情况。即便使用租约,它也只是根据预期远端值作并发检查,不代表已经批准替换,也不能证明所有协作者都已被考虑。发布前应先确定真实引用关系,而不是机械套用强推命令。 变基冲突中,合并后端所称的本方,是目前已构建的变基历史,包含上游基线;对方则是正在重放的提交。这可能与开发者按原功能分支理解的“我的版本”相反。应检查实际文件版本和目标契约,再决定如何解决;暂存具体文件后继续。跳过会丢弃当前补丁,并不是自动修好这次冲突,不能为了让过程继续就忽略它的业务含义。 决定放弃操作时,中止变基会恢复起初的分支状态。--quit 则只停止管理变基,保留当前 HEAD、索引与工作文件,并不是等价的撤销命令。需要恢复起点时,不能因为两者都会结束流程就随意替换。从干净工作区开始,有助于明确恢复结果。不要把中止当成在过程中保存任意无关编辑的通用备份,也不要用强制清理作为常规冲突处理方式。应先理解当前序列、索引与文件中有哪些有效成果,再选择与操作意图匹配的恢复路径。 快速验证 在 POSIX 系统上使用 Python 3 与 Git 2.28 或更新版本运行下面实验。它在临时仓库中使用示例身份,关闭钩子与签名,没有远端,并排除继承的 Git 路由及个人配置。两个分支从同一个主题提交起步,以不同方式整合一项独立上游变化;第三个分支有意制造新增同名文件的冲突,检查中止恢复,而不碰当前项目的实际历史。 ...

一月 18, 2028 · 2 分钟 · XB Field Notes
合并 M 的第一父级是原当前末端 C,第二父级是传入末端 I。文本合并干净不证明行为正确。

git merge 用一个新的提交把分叉的两条线接起来

合并连接两段历史,并记录项目的结果快照。普通的双分支分叉合并,会创建同时把两个原末端作为父级的新提交。这些父级关系让更早的提交仍然可达,却不能证明最终文件保留了每一项预期行为。合并既是图上的连接,也是内容上的决定;只检查其中一面,会漏掉另一面真实存在的错误,不能把“两个父提交都还在”当作功能没有丢失的证据。 记录合并前先审阅行为 父级保留历史,不把它改写成单线 如果当前末端是传入末端的祖先,快进可以直接移动当前分支引用,不需要额外合并提交。当两侧各自拥有新提交后,普通合并才产生双父级结果:第一个父级是合并前的当前末端,第二个是传入末端。图中依然包含两条历史线,并没有把它们重新写成一串线性提交。阅读历史时,应区分分支名字的变化与提交图本身的形状。 Git 通常借助合并基点协调两侧末端;更复杂的历史可能涉及多个基点。相对共同祖先比较,可以区分某一侧确实修改的内容和保持不变的上下文。但文本结果干净,只表示选定合并机制找到了一种组合文件的方法,不代表 Git 理解了应用接口、业务规则或发布依赖。这些含义仍需通过源码关系、约定和测试来判断。 记录连接之前先审阅结果树 开始前应明确并保留当前工作状态。未提交修改可能阻止合并,也可能让失败后重建原状态变得复杂。不能假定中止操作能够恢复任意在合并前或合并期间发生的编辑。还应记录真正被连接的分支末端对象,让审阅和后续检查针对稳定对象,而不是继续变化的名称;否则两次观察可能根本不是同一组输入。 如果流程要求显式审阅合并提交,可以让 Git 在记录提交之前暂停。但不提交选项无法阻止快进,所以即使线性情况也要求生成合并提交时,需要同时关闭快进。暂停后检查暂存变化,把候选结果与两个父级分别比较,并运行任意一侧所影响的测试。下面的实验正是在这个暂停点发现行为不兼容,先修复再记录合并,而不是把有问题的组合直接当成成功。 压缩合并是另一种选择:--squash 准备组合内容,但不记录 MERGE_HEAD,之后普通提交也不会把传入末端记作父级。因此,即便最终文件看起来相同,祖先关系以及未来合并的判断基础也可能不同。需要保留双父级证据时,不能把压缩提交误认为普通合并的等价记录。 坏掉的样子 文本冲突不限于两边改动同一行。删除、重命名、二进制变化及其他不兼容操作,都可能需要人工决定。应使用工作区状态及索引中的未合并条目识别未解决路径。普通文本冲突中的标记方便查看双方内容,但没有发现这些字符串,不能证明所有冲突已经解决,更不能代替对二进制文件或路径结构的检查。 解决应遵循组合后程序的真实契约,再暂存具体路径并继续或提交。删除标记、执行暂存,只是在告诉 Git 已经提供了解法,不是在证明解法正确。生成文件应先协调权威输入,再通过生成器重建;不能把两个生成结果拼成看似合理的输出,否则下次构建还会覆盖人工拼接,甚至与输入表达的规则不一致。 更隐蔽的是文本干净却行为损坏。一条分支可能收紧函数参数形式,另一条同时增加仍按旧形式调用的代码。它们修改不同文件,各自测试都可以成功,组合后却失败。因此,集成测试必须覆盖这次组合新形成的调用关系,不能只是把两份分支测试摘要并排放在一起,就认为结果也已经通过同样验证。 策略选择还可能主动丢弃内容。偏向传入方的冲突选项,会优先采用传入方的冲突片段,同时保留其他非冲突变化;它不是自动保留双方意图的通用规则。另一种名为 ours 的策略则保留当前目录树,却仍把另一侧历史记录为已合并。因此,双父级图不能证明传入实现真的被采用。类似选择必须有明确依据并审查内容,不能只为让冲突提示消失而使用。 快速验证 在 POSIX 系统上使用 Python 3 和 Git 2.28 或更新版本运行下面实验。全部工作都在临时仓库,使用示例身份,没有远端,关闭钩子与签名,并排除继承的 Git 路由和个人配置。一条分支要求 Python 接口使用关键字参数,另一条分支新增位置参数调用。关闭字节码写入,是为了避免测试产生的缓存混入工作树状态,便于准确检查最终仓库是否干净。 ...

十二月 29, 2027 · 2 分钟 · XB Field Notes
默认保存受跟踪变化和索引;-u 加入未跟踪文件;-a 再加入忽略文件。实心标记表示纳入保存。

git stash 把改动暂存到别处

Git stash 记录选定的未提交工作,让检出可以暂时回到较早的已提交状态。它适合短暂中断,但保存和恢复都不是对目录中所有文件的无条件承诺。真正需要确认的是包含了什么、工作原来基于哪个版本、目标现在处于什么状态,以及保存条目何时才能移除。只看到命令报告成功,还不足以证明所有想保留的数据都已经纳入快照。 矩阵列出常规保存范围:默认包含受跟踪变化与索引,-u 加入未跟踪文件,-a 再加入忽略文件。apply 保留条目;pop 只在应用成功后移除。冲突弹出可能已经部分修改文件。删除前先检查、验证并核对准确条目。 明确快照包含的范围 普通保存操作记录受跟踪文件的工作树变化与索引,再把这些修改退回到 HEAD 对应状态。未跟踪文件默认留下,只有加入相应选项才会一起保存;被忽略文件在包含未跟踪文件时仍然留下,需要更大的包含范围才会纳入。选择之前先检查,因为忽略路径里可能有凭据、大型构建输出或不适合进入 Git 对象的本地数据,不能为了追求“全部收起来”就不看内容。 选定路径、只处理已暂存内容、保留索引等选项,会产生不同结果。不要习惯性组合参数,再假定它们代表同一份快照。应先检查状态与预期差异,为条目写明含义,并查看实际保存的内容,再把原编辑视为已经能够恢复。尤其要区分磁盘文本和索引文本,它们可能在同一路径上分别代表两阶段尚未完成的工作。 stash 使用 Git 提交对象表示,不是对象库之外的特殊文件。最新条目由专用引用指向,更旧的条目通过该引用的日志保留,所以普通分支历史不一定显示它们。忘记使用并不自动等于丢失,但删除保留引用之后,对象最终可能无法恢复。本地分支或 stash 也不会仅凭存在,就提供一份独立的异地备份;保存形式与备份位置是不同问题。 应用与弹出不同 应用命令尝试恢复,同时保留保存条目,适合先审阅恢复结果,再决定是否删除副本。默认不能期待原来的已暂存与未暂存区别自动重建;索引选项会额外尝试恢复这一状态,但如果冲突或目标变化妨碍恢复,仍可能失败。应检查索引和磁盘各自的结果,而不只看最终文件里是否出现了某段文字。 弹出操作只在应用成功后才从列表移除条目。发生冲突时,保存项保留,但能够干净应用的路径可能已经改变。因此,非零返回不代表整个工作树完全没动。再次操作前应查看索引和文件,不能在部分恢复的目录上反复弹出同一条目,指望重复执行自动修复第一次冲突。 条目不属于某个分支或关联工作树私有。描述中出现的原分支名称是背景,不是强制只能在那里恢复的规则。随着新增和删除,数字选择器还会变化,所以敏感操作前要检查并记录准确对象。在 Shell 中传入带花括号的选择器时应引用,避免外层语法干扰。需要删除的究竟是哪份内容,应由对象证据确认,而不是仅凭它此刻排在第一位。 坏掉的样子 在变化后的基线上恢复可能冲突;即便文本没有冲突,也可能需要业务适配。应依据最终代码意图解决,暂存具体路径并验证结果,不能反复向半恢复状态应用同一份变化。stash 应用没有一个通用中止命令,能够自动撤销任何已经产生的部分效果,因此“先试试,不行就一键回去”不是可靠的恢复设计。 如果适合回到原始基线继续,可以使用建立分支的恢复命令:它从记录的起点创建分支,再应用保存工作。通过 stash 日志指定条目且应用成功时,条目也会被移除。这是与记录基线有关的恢复流程,不要求必须有两份 stash,也不存在超过某个时间就必须采用的固定期限。应按当前基线与恢复意图判断,而不是按存放时长机械选择。 ...

十二月 11, 2027 · 2 分钟 · XB Field Notes
普通创建提交的 cherry-pick:源 S 指向父级 P,结果 R 指向目标 T。虚线表示把 P 到 S 的差异应用为 R,不是祖先关系;S 仍保留在源历史中。

git cherry-pick 重新应用一次提交

cherry-pick 把选定提交引入的改动应用到当前开发线,适合旧发布分支只需要一个修复、不希望同时接收整条开发分支的场景。它不会搬走或删除源提交,也不会为目标建立与源分支的合并关系。移植后的结果必须在目标环境中重新审阅;源分支上已经通过的测试,只能说明原来的组合,不能直接证明换了依赖与历史之后仍然正确。 cherry-pick 移植相对父级的变化 选择的是相对父提交的变化 对于普通的单父提交,相关变化是它相对父级的差异。Git 利用可用历史和合并机制,把这份变化应用到目标,而不是简单地用源提交的完整文件快照覆盖目标。目标原本已有的其他修改应继续保留,除非选定变化或人工解决明确要求改变它们。审阅时既要看搬来了什么,也要确认没有误把目标自己的工作一起替换掉。 通常,cherry-pick 会在目标末端后面记录新提交。父级不同,结果目录树也可能不同,因此对象身份会变化。原作者身份和作者日期通常保留,而提交者表示实际记录这次移植的人;消息不一定变化。不能把这项操作解释成必然换作者、必然改写消息,更不能根据对象标识不同就断言原作者署名已经丢失。 默认行为会创建提交,但不提交选项会把变化留在索引与工作树中,供审阅或组合;使用允许快进的选项时,满足条件的操作还可以直接前进到已有源提交。这些明确模式说明,“所有 cherry-pick 都一定生成新对象”也不是普遍成立的规则。解释一次具体结果时,应把实际参数与所处历史一起纳入判断。 移植之前检查依赖 修复可能依赖目标分支尚不存在的接口、数据结构、配置或前置重构。补丁在文本上干净落地,并不能证明行为正确。应阅读源变化及其前提,在真正的维护分支运行受影响行为测试,并只引入该修复实际必需的依赖。如果需要适配,说明为何不能原样采用,以及新分支上的实现如何满足同一修复目标。 在共享分支之间干净应用时,-x 会把源提交标识写进新消息,方便追踪,但它不是等价性或批准状态的密码学证明。手动解决冲突后也要检查最终消息,确认来源关系没有丢失。原提交的签名不会自动成为新对象的有效签名;目标要求签名时,应遵循目标发布流程,而不是把保留作者信息等同于继承了所有验证证据。 坏掉的样子 在默认创建提交的模式中,发生冲突时,HEAD 停在最后一个已成功创建的提交,Git 用 CHERRY_PICK_HEAD 记录当前难以应用的源提交。使用 --no-commit 时不会设置该引用,因此必须检查状态和未合并条目,不能仅凭这个标记是否存在来判断冲突。能够干净应用的路径可能已经更新,冲突路径则在索引中保留未合并条目。文本冲突标记便于查看,但二进制冲突及其他类型不能只靠搜索标记字符串识别,还应检查工作区状态和索引,确认当前到底有哪些未完成事项。 解决时应确定目标需要的行为,暂存具体已解决路径,运行相关测试,再继续序列。文件已经暂存只说明冲突被标记为解决,不证明代码逻辑正确。中止会返回序列开始之前的状态;退出序列管理则只是遗忘操作状态,并不执行相同的回退。开始前应保证工作树干净且状态明确,不能把中止功能当作保存无关未提交工作的替代品。 目标已经包含某项变化时,应用结果可能为空。跳过之前,要确认目标确实具有预期行为,而不是只看到类似代码就作判断。选取合并提交还会多一个语义选择:主线参数指定以编号中的哪个父级作为差异基准。这不是通用冲突绕过开关,必须依据想移植的实际变化选择,而且结果不会重现原合并提交的多父级拓扑。 快速验证 在 POSIX 系统上使用 Python 3 与 Git 2.28 或更新版本运行下面实验。它建立私有临时仓库,排除个人 Git 配置和继承的 Git 路由变量,使用示例身份,关闭钩子与签名,没有远端。一条目标线能够接收修复;另一条故意把同一行改成不同内容,用来观察冲突停止及中止恢复。全部修改都限于这份临时数据,不触碰当前项目的提交或文件。 ...

十一月 25, 2027 · 2 分钟 · XB Field Notes
排除 /build/ 时即使否定规则也无法恢复 keep.txt;排除 /build/* 让父目录保持可遍历,子文件例外才有效。已跟踪文件仍被跟踪。

.gitignore 控制未跟踪路径的处理

Git 忽略规则影响普通路径发现和添加操作如何处理尚未跟踪的文件,适合管理生成产物、编辑器状态和本地配置。但它不会形成访问控制边界,不会把已进入索引的文件移出去,也不会抹掉更早的提交。因此,应该把忽略文件看成具有可验证匹配规则的项目约定,不能把它当作敏感文件绝对进不了历史的保证。 忽略规则筛选路径发现,不清除历史 坏掉的样子 某个文件被跟踪之后,再为它添加忽略规则,并不会停止跟踪。继续修改它并执行普通添加操作,新内容依然可能进入暂存区。这就是为什么泄漏配置后才把文件名补进 .gitignore,不能完成修复。索引里有没有这个路径、工作区文件现在是什么内容、历史提交保留了哪些对象,是三个不同状态,需要分别观察,而不是只看文件是否还显示在默认状态列表中。 否定规则也容易令人误判。排除整个目录,会阻止 Git 继续遍历该目录,后面针对某个子文件的例外便无法挽救它。可以只忽略目录里的内容,让父目录仍能被访问;也可以先逐层恢复被排除的父目录,再恢复后代。若 Git 根本不会进入某个目录,藏在里面的嵌套忽略文件也无从发挥作用,规则写得再靠近文件也没有帮助。 优先级并不是“看上去更精确的模式一定获胜”。Git 对命令行模式、目录忽略文件、仓库本地排除项和全局排除项定义了顺序;同一优先级内部,以最后匹配的模式为准。更深目录的忽略文件,可以针对它所管理的路径覆盖上层规则。因此,模式的写法与来源位置都重要,不能仅比较哪个字符串更长,就推导最终结果。 快速验证 在安装了 Git 的环境中运行下列 Python 脚本。它创建临时仓库,执行后自动删除;对子进程清除继承的 Git 路径与配置环境变量,关闭系统、全局配置及钩子,并使用空模板和虚构样本文件。原记录的版本为 Git 2.50.1;修订后的隔离检查也已在 Git 2.52.0、Python 3.12.14 下通过。即使从已有项目目录启动,实际 Git 操作也都以临时目录为工作目录,不会修改启动位置的仓库: from pathlib import Path from tempfile import TemporaryDirectory import os,subprocess 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 TemporaryDirectory(prefix='git-ignore-lab-') as directory: root=Path(directory) hooks=root/'.no-hooks';hooks.mkdir() template=root/'.no-template';template.mkdir() def git(*args, expected=0): result=subprocess.run(['git','-c','core.hooksPath='+str(hooks),'-c','core.excludesFile='+os.devnull,*args],cwd=root,env=env,text=True,capture_output=True) assert result.returncode==expected,(args,result.returncode,result.stderr) return result.stdout def write(path,content='fixture\n'): target=root/path;target.parent.mkdir(parents=True,exist_ok=True);target.write_text(content) git('init','-q','--template='+str(template)) git('config','user.name','Ignore Lab') git('config','user.email','ignore@example.invalid') git('config','commit.gpgsign','false') write('tracked.txt','first version\n') git('add','--','tracked.txt');git('commit','-qm','Track fixture') write('build/keep.txt');write('build/output.bin') write('.gitignore','/build/\n!/build/keep.txt\n') git('check-ignore','-q','build/keep.txt') print('excluded parent prevents child exception') write('.gitignore','/build/*\n!/build/keep.txt\n*.log\n/root-only.txt\ntracked.txt\n') git('check-ignore','-q','build/keep.txt',expected=1) rule=git('check-ignore','-v','build/keep.txt') assert ':2:!/build/keep.txt\t' in rule git('check-ignore','-q','build/output.bin') print('ignoring contents permits keep.txt; verbose output includes its negation') write('debug.log');write('nested/debug.log');write('nested/.gitignore','!debug.log\n') write('root-only.txt');write('nested/root-only.txt') git('check-ignore','-q','debug.log') git('check-ignore','-q','nested/debug.log',expected=1) git('check-ignore','-q','root-only.txt') git('check-ignore','-q','nested/root-only.txt',expected=1) print('nested exception and root anchor checked') write('tracked.txt','second version\n') git('add','-A') assert 'tracked.txt' in git('diff','--cached','--name-only').splitlines() git('check-ignore','-q','tracked.txt',expected=1) assert 'tracked.txt' in git('check-ignore','--no-index','-v','tracked.txt') git('rm','--cached','--','tracked.txt') assert (root/'tracked.txt').is_file() assert 'tracked.txt' not in git('ls-files').splitlines() git('cat-file','-e','HEAD:tracked.txt') print('tracked file still staged; cached removal keeps disk copy and old commit') git('add','-f','--','build/output.bin') assert 'build/output.bin' in git('ls-files').splitlines() print('force-add bypasses ignore rules') 第一项确认,排除 build/ 会使子文件例外失效。修改后的规则只排除目录里面的条目,因此允许 keep.txt,同时继续排除 output.bin。安静模式检查的是实际忽略结果,退出码一表示被检查的路径没有被忽略。这里不是根据文件名或规则长相猜测,而是把预期写成了真实 Git 命令的返回值断言。 ...

十一月 15, 2027 · 1 分钟 · XB Field Notes
功能开发带保留已暂存、未暂存与未跟踪工作;独立热修复从发布提交 B 开始,先复现失败,再修复通过。

用 git worktree 做热修复,不打断功能开发

加急修复不必把尚未完成的功能开发从当前目录赶走。关联工作树可以从选定的发布提交开始,而原目录保留已暂存修改、未暂存编辑和未跟踪文件。这个流程真正成功,需要同时证明修复在正确基线上通过验证,以及原工作完整保留。仅仅多创建一个目录,既不能证明基线正确,也不能证明所有未完成状态没有受到影响。 固定发布提交,在热修复目录复现并修复,再核对功能开发的 HEAD、已暂存和未暂存文本及未跟踪笔记。普通移除前,把评审笔记与忽略日志移到检出之外;热修复分支仍保留。 先选基线,再开另一个目录 检查当前工作树,并确定需要修复的准确发布版本。一个方便使用的分支名称,不一定就是线上部署的提交。如果必须刷新远端跟踪引用,应获取指定远端之后,再记录实际解析到的对象。获取更新的是公共仓库引用,并不是只影响输入命令的那个目录;多个工作树之间仍然共享这部分状态。 创建时明确指定目录、新分支名称和起点。新的热修复分支可以避免与其他目录已检出的分支竞争。如果只是检查、不准备提交,分离工作树也可能足够。不要因为计划使用的名称已经存在,就直接采用重置分支或强制选项;先检查已有工作树和引用,弄清它们是否保存了其他工作,不能让方便命名变成覆盖状态的理由。 新工作树检出的是所选起点中的已提交内容,不会复制原目录的功能改动或未跟踪笔记。这有利于在没有功能开发偶然修复或新增依赖的情况下复现发布问题,但也意味着编辑器缓冲区、本地配置文件等不会自动出现在新目录。需要哪些文件,应依据真正的测试环境准备,而不能假定整个开发上下文被一起复制过去。 在真实热修复目录复现与验证 先确认新 HEAD,再运行针对性复现,随后修改代码,并在同一热修复目录运行相同检查和相关的更广范围测试。在原功能目录跑出的成功,只能证明另一份检出的情况。测试输出应关联准确的提交、目录和配置,让后来审阅的人能够知道观察结果对应哪一套输入,而不是只看到一个没有上下文的成功提示。 放在各目录内部的构建产物是不同文件,但外部状态仍可能竞争。两个服务可能绑定相同端口,两套测试可能写同一个数据库,共享缓存也需要正确的区分键。工作树不是容器、虚拟机或安全边界;运行不熟悉分支的构建命令前,应检查它会执行什么以及访问哪些资源,不能把目录不同解释成任何副作用都彼此隔离。 只暂存预期修复,并在提交前检查差异。新提交通过公共对象库对其他目录可见,但原功能分支和索引不需要因此移动。保留原状态是要求时,应明确复核这些事实,而不是靠“工作树本来就独立”推断没有问题。提交在本地可见,也不意味着分支已经分发或修复已经部署;发布仍然是单独受授权约束的步骤。 坏掉的样子 目录名称可能像某个发布版本,实际 HEAD 却指向另一个位置。应核对解析后的版本,而不是信任文件夹标签。另一个常见错误,是把公共仓库配置当成当前工作树私有配置:为热修复调整的设置,可能影响其他工作树,即便那些目录里的文件完全没有变化。遇到差异时,应同时检查文件和共享配置,而不只比较源码。 结束前检查关联工作树的状态、未跟踪文件以及有价值的忽略产物。普通状态不会显示被忽略的文件,受跟踪差异为空,也不证明目录里没有值得保存的数据。需要保留的日志或制品,应在移除之前移到目录之外。还要停止继续使用该目录的进程,因为 Git 状态干净并不说明服务已经退出,也不说明没有文件仍然被打开。 对确认过的路径使用普通移除命令。遇到未提交或普通未跟踪工作时,它会拒绝,而不会默认这些内容可丢弃;应调查拒绝原因,不要习惯性补上强制参数。移除删除的是关联检出和注册信息,不是分支本身。清理失效管理信息则针对目录已经消失的记录,不是删除活动工作区的替代命令,两种操作不能混用。 快速验证 在 POSIX 系统上使用 Python 3 与 Git 2.28 或更新版本运行下面实验。临时仓库排除个人 Git 配置和继承的 Git 路由,关闭钩子及签名,没有远端。功能目录故意同时保留三种未完成工作;热修复测试先失败,再经过一行修复后成功。全部内容都是合成测试数据,不会改变当前项目的编辑、分支或发布状态。 ...

十一月 6, 2027 · 2 分钟 · XB Field Notes
正常版本 0 与异常版本 6 约束未知候选;箭头由子提交指向父级。需复测候选与父级,跳过可能留下候选集合。

git bisect 收敛第一个坏提交

二分排查回答一个明确的历史问题:在选定历史中,某个可复现观察从哪里开始由正常变成异常?它不会仅凭给出一个提交,就自动确定根因。搜索依赖正确的端点标签和一致的中间判断。应把报告出的边界当作需要检查的证据,再确认边界两侧行为,并解释哪项变化造成了结果差异,不能把一行定位输出当作完整故障分析。 稳定的 accepts(10) 样例将祖先边界收敛至版本 3:父级接受十,版本 3 拒绝十。跳过转换点可能留下 {3, 4}。重放恢复判断记录,不会重新测试;需复测边界并检查补丁,再解释因果。 依据观察选择端点,而不是日期 至少一个已知正常提交和一个已知异常提交可以建立搜索;更多正常版本还可以进一步约束提交图。最近经过验证的正常版本,通常比不必要地选择很古老的基线更能缩小范围。日期不是搜索顺序:Git 使用祖先关系,作者时间戳不一定反映提交在图上的位置。因此,“最早”应放在这次历史边界内解释,而不是直接按墙上时间排序。 赋予标签之前,应在两个端点运行同一份复现。明确什么算目标症状,包括边界输入与相关配置。只检查任意命令有没有失败,可能把产品回归与解释器缺失、服务不可用或构建环境不兼容混在一起。一个无法测试的版本,其结论是未知,而不是自动判坏;如果输入条件没有保持一致,后续再精细的搜索也无法补回缺失的可比性。 使用干净、可丢弃的检出,或专门的关联工作树。二分通常会把 HEAD 切到选定版本并更新文件,因此应提前保存未完成工作,不能认为搜索会自动创建备份。探针和必需证据也应放在不会随历史检出而消失或改变的地方,让测试程序自身的变化与被测源码版本分开,避免把旧脚本缺失当成源码故障。 每次都判断同一个性质 观察当前版本之后,按复现结果标记正常或异常。某个版本由于与目标症状无关的原因无法分类时,可以跳过。靠近转换点的跳过可能使答案不唯一,此时应报告剩余候选集合,而不是把最先显示的对象包装成已经确定的责任提交。未知本身也是有效调查结果,不能为了快速收束而强行填上好坏标签。 简单二分直觉要求所选历史中的转换足够稳定。问题先引入、后修复、再重新引入时,对这个判断条件来说已经不是单一的单调时间线;随机失败的测试也会制造类似的误导。观察与假设矛盾时,应细化问题或区间,而不是把不一致结果塞进方便解释的故事。搜索算法不会替你决定这些不一致到底意味着什么。 合并较多时,搜索全部祖先还是只沿第一父级集成线,会改变问题本身。第一父级搜索可能找到集成分支因某次合并而出现故障的点,而不是主题分支最初引入代码的提交。同样,限制文件路径可能排除真正相关的配置或调用方变化。只有这些限制符合调查目标时才应用,不能只为了让候选数量看起来更少而缩小问题。 自动运行 git bisect run 时,退出码零表示正常,125 表示跳过,1 至 127 中其余值表示异常,其他状态会中止搜索。命令不存在可能返回 127,因此包装脚本必须区分工具不可用与目标回归,不能把每个失败都直接转交为坏版本。手动探针中的异常同样应先调查,而不是自动贴标签。 ...

十月 24, 2027 · 2 分钟 · XB Field Notes
一个公共仓库连接 main 与 review;两者各自保留 HEAD、索引和工作文件。

Git 工作树共享仓库,各自保留检出状态

关联工作树让同一个 Git 仓库拥有另一个工作目录,并保存各自的检出状态。功能开发、发布维护和评审可以并排展开,不必反复替换同一目录中的文件。真正有用的边界是工作文件与暂存状态分离,而不是完全隔离。对象、许多引用以及普通仓库配置仍然共享,所以操作时既要考虑当前目录,也要考虑它背后的公共仓库,不能把关联工作树等同于互不影响的独立克隆。 关联工作树共享对象、分支与标签引用、默认仓库配置和 stash。每份检出保留自己的 HEAD、索引、文件以及 refs/bisect 等操作状态。review 的提交可从 main 读取,但 main 的暂存文本不变。 区分每份检出自己的状态 每个工作树都有自己的 HEAD 和索引。在一个目录暂存修改,不会让另一目录的索引出现同样变化;编辑一个受跟踪文件,也不会同步编辑旁边那份文件。关联工作树中的普通提交推动它当前选中的分支。新对象和共享分支引用则立即能被其他工作树里的 Git 命令看到,不需要在这些目录之间再执行获取操作。 关联目录中的 Git 入口通常是一个文件,指向它的管理目录。真正检出的源文件住在创建工作树时指定的路径,而不是内部的工作树管理条目里。假定 Git 入口必然是目录的工具,可能因此失效。需要定位公共目录、当前管理目录或索引时,应向 Git 查询对应路径,不能自行拼接内部文件名,把偶然的存储结构当作可靠接口。 ...

七月 26, 2027 · 2 分钟 · XB Field Notes