十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Dagger TypeScript SDK Changeset 类完全指南:目录差异、Git 补丁与合并

Dagger TypeScript SDK Changeset 类完全指南:目录差异、Git 补丁与合并 Dagger TypeScript SDK Changeset 类完全指南目录差异、Git 补丁与合并【免费下载链接】daggerAutomation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud项目地址: https://gitcode.com/GitHub_Trending/da/dagger导读Changeset是 Dagger 引擎中用于表示两个目录快照之间差异的核心抽象它把旧目录 → 新目录的变化新增、修改、删除、重命名编码成一个可查询、可导出、可合并的对象并且可以直接生成 Git 兼容的 patch 文件。本文以 TypeScript SDK 生成的Changeset类 API 文档 为主体结合仓库核心实现core/changeset.go、core/schema/directory.go与集成测试core/integration/changeset_test.go完整讲解每个方法的行为、参数、返回值与底层实现原理。读完本文你将掌握在 Dagger 中比较目录、提取变更路径、导出差异、生成补丁以及多变更集合并冲突处理的完整实战方案。Changeset 是什么目录差异的可操作化表示核心概念before 与 after 两个快照Changeset的官方定义是 A comparison between two directories representing changes that can be applied两个目录之间的比较表示可以被应用的变化。在底层它本质上是两个Directory快照的配对before被比较的旧下层快照即 before() 方法返回的Directory文档描述为 The older/lower snapshot to compare againstafter新的上层快照即 after() 方法返回的Directory描述为 The newer/upper snapshot。对应的 Go 核心实现位于 core/changeset.go#L348-L359type Changeset struct { Before dagql.ObjectResult[*Directory] field:true doc:The older/lower snapshot to compare against. After dagql.ObjectResult[*Directory] field:true doc:The newer/upper snapshot. // ...缓存与序列化辅助字段 }NewChangeset工厂函数core/changeset.go#L36-L42把 before/after 两个目录对象封装成ChangesetNewEmptyChangesetcore/changeset.go#L44-L59则创建一个 before 与 after 相同的空变更集。在 GraphQL 层面的挂载点Changeset不是凭空产生的它由Directory.changes()字段生成。在 core/schema/directory.go#L217-L224 中定义Return the difference between this directory and another directory, typically an older snapshot. The difference is encoded as a changeset, which also tracks removed files, and can be applied to other directories.其参数为fromThe base directory snapshot to compare against即要对比的基础目录快照。对应实现 core/schema/directory.go#L1286-L1299func (s *directorySchema) changes(ctx context.Context, parent dagql.ObjectResult[*core.Directory], args struct { From core.DirectoryID }) (res *core.Changeset, _ error) { // ... dir, err : args.From.Load(ctx, srv) // ... return core.NewChangeset(ctx, dir, parent) }注意这里的语义调用newDir.changes(from: oldDir)时parentnewDir作为afterfrom参数oldDir作为before。集成测试 core/integration/changeset_test.go#L33-L51 正是这样构造变更集的oldDir : c.Directory(). WithNewFile(file1.txt, content1). WithNewFile(dir/file2.txt, content2). WithNewFile(removed.txt, to be removed) newDir : c.Directory(). WithNewFile(file1.txt, content1). WithNewFile(dir/file2.txt, content2) changes : newDir.Changes(oldDir) removedPaths, err : changes.RemovedPaths(ctx) require.Contains(t, removedPaths, removed.txt)TypeScript 侧对应为directory.changes(from)后得到一个Changeset实例相关调用链可参考 Directory 类文档。一个关键设计点Changeset 能追踪被删除的文件相比Directory.diff()core/schema/directory.go#L205-L210将差异编码为一个目录changes()生成的变化集能追踪被移除的文件并且可以应用apply到其他目录上——这正是withChangescore/schema/directory.go#L225-L230与export()等能力的基础。选择哪种 API取决于你是否需要保留删除信息。类构造与继承关系Changeset继承自BaseClient构造器签名如下仅内部使用客户端代码不要直接newnew Changeset( ctx?: Context, _id?: ChangesetID, _export?: string, _isEmpty?: boolean, _sync?: ChangesetID ): Changeset参数依次为Context、ChangesetIDan identifier for an object of type Changeset、导出的路径字符串、是否为空标志以及同步 ID。构造函数注释明确说明Constructor is used for internal usage only, do not create object from it——真实场景中你永远通过directory.changes()或后续的withChangeset()/withChangesets()链式调用来获得 Changeset 对象。ChangesetID 类型ChangesetID定义为string object即带 branded 类型的字符串用于在客户端与引擎之间引用 Changeset 对象。这与 Go 侧的 JSON 持久化机制对应Changeset实现了自定义MarshalJSON/UnmarshalJSON序列化时只保存 before/after 两个目录的 IDcore/changeset.go#L361-L396并通过ResolveRefs反序列化后重新加载core/changeset.go#L398-L414。因此一个 Changeset 的身份完全由beforeIdafterId决定。方法全景快照访问器before() 与 after()方法签名说明before()(): Directory返回被比较的旧/下层快照after()(): Directory返回新/上层快照注意这两个方法是同步返回Directory对象非 Promise因为它们只是对已引用目录的懒加载引用。after 快照就是调用changes(from)时那个parent目录before 则是from参数。路径查询addedPaths() / modifiedPaths() / removedPaths()这三个方法都返回Promisestring[]用于按类别列出发生变化的路径addedPaths()Files and directories that were added in the newer directory——在较新目录中被新增的文件和目录modifiedPaths()Files and directories that existed before and were updated in the newer directory——新旧两个目录中都存在、但在新目录中被更新的文件和目录removedPaths()Files and directories that were removed. Directories are indicated by a trailing slash, and their child paths are not included——被删除的文件和目录。目录以尾部斜杠标识如remove-dir/并且不包含其子路径。removedPaths 的目录折叠语义这是最容易踩坑的语义点集成测试 core/integration/changeset_test.go#L53-L83 用专门的用例验证// 删除整个 remove-dir/ 后 require.Contains(t, removedPaths, remove-dir/) require.Contains(t, removedPaths, empty-dir/) require.NotContains(t, removedPaths, remove-dir/file.txt) require.NotContains(t, removedPaths, remove-dir/subdir/) require.NotContains(t, removedPaths, remove-dir/subdir/nested.txt)即目录整体删除时结果只报告remove-dir/这一条带尾斜杠其下所有子文件和子目录路径都被折叠掉。该行为来自 core/changeset.go 中computeChangesetPaths的collapseChildPaths(allRemoved)处理同时内部保留未折叠的AllRemovedcore/changeset.go#L61-L67 的ChangesetPaths结构体供 diff 统计使用。目录用尾斜杠区分的约定来自listSubdirectories见冲突检测注释 core/changeset.go#L1040-L1047。路径计算与重命名展开路径计算的入口是ComputePathscore/changeset.go#L160-L178它使用sync.Once保证只计算一次并把结果缓存。内部流程core/changeset.go#L180-L206先比较 before/after 的 content-preferred digest若相同则直接返回空结果快速路径IsEmpty也采用同样的快速判断见 core/changeset.go#L564-L575否则把两个目录挂载到临时路径withMountedDirscore/changeset.go#L303-L346优先用基于元数据的 delta diff 计算computeChangesetPathsDelta若 delta diff 失败回退到全量内容 diffcomputeChangesetPaths并打印警告日志 changeset delta diff failed; falling back to full content diff。值得一提的是重命名会被展开进 added/removed 两个列表中ChangesetPaths结构体里专门有Renamed map[string]string // newPath → oldPathcomputeChangesetPaths中Expand renames into Added/Removed so addedPaths/removedPaths stay completecore/changeset.go#L276-L282保证调用方看到的 added/removed 路径是完整的。差异导出export()export(path):Promisestring—— Applies the diff represented by this changeset to a path on the host.参数path是宿主机上的目标路径文档示例为logs/。这个方法把变更集所代表的差异应用到宿主机路径新增与修改的文件会被写入被删除的文件会从宿主机上移除。Go 侧实现Exportcore/changeset.go#L924-L985的流程值得注意ComputePaths算出所有变更路径通过srv.Select(ch.Before, dir, {Field: diff, Args: {other: afterID}})得到 before→after 的 diff 目录复用Directory.diff能力求值后调用bk.LocalDirExport(ctx, root, destPath, true, paths.Removed)其中第 4 个参数正是被移除的路径列表用于在宿主机上同步删除——这就是应用 diff的关键。schema 层changesetExportcore/schema/directory.go#L1370-L1401还做了一个贴心处理若导出的变更集触及 workspace 配置changesetTouchesWorkspaceConfig如dagger setup的产物会尝试使相关 workspace 缓存失效。Git 兼容补丁asPatch()asPatch():File—— Return a Git-compatible patch of the changes返回一个代表统一 diff 补丁的File对象文件名为diff.patch常量定义于 core/changeset.go#L562可以直接导出、或作为输入传给其他 API例如用git apply应用到仓库。底层实现AsPatchcore/changeset.go#L667-L781做了大量细节工作先把 before/after 目录以 bind mount 方式挂到临时目录的a/、b/下用ComputePaths预先算出变更路径只对实际变化的部分做 diffgitDiffPathSpecs见 core/changeset.go#L208-L258而不是对整个目录树做 diff若没有变化no-op Changeset直接返回空补丁避免昂贵的空 diffcore/changeset.go#L751-L755执行git diff --binary --no-prefix --no-renames --no-index a bcore/changeset.go#L789-L832。为什么用 --no-renames代码注释core/changeset.go#L790-L794解释得很清楚配合--no-prefix时git 会从---/行去掉a/、b/挂载目录前缀但不会从 rename from/to 行去掉导致重命名条目生成无法应用的补丁inconsistent old filename。把重命名拆成 deleteadd配合--binary结果完全一致且可应用。diff --git 头重写由于两个目录被命名为a/、b/git 生成的diff --git头在不同变更类型下前缀不一致新增文件是diff --git b/f b/f删除是diff --git a/f a/f而git apply期望恒为diff --git a/path b/path。diffGitHeaderRewriter与fixDiffGitHeadercore/changeset.go#L834-L922专门把头部统一重写为a/与b/前缀且能正确处理含空格、含引号git C 风格转义的路径。最后git diff的退出码 1表示两个树有差异被特意当作成功处理core/changeset.go#L818-L830。变更集快照layer()layer():Directory—— Return a snapshot containing only the created and modified files返回一个只包含被创建和修改文件的Directory快照不含删除与未变化文件。在 Go 侧对应 schema 的changesetLayercore/schema/directory.go#L1331是生成器generator等场景中只看产出物、忽略删除的便捷视图。状态判断isEmpty() 与 id()isEmpty():Promiseboolean—— Returns true if the changeset is empty (i.e. there are no changes)。底层实现IsEmptycore/changeset.go#L564-L596先比较 digest相同直接返回 true否则走 delta 检查失败则回退到directoriesAreIdentical全量比较。注意 core/changeset.go#L1243-L1249 的注释WithChangesets过滤空变更集时用的是ComputePaths而非IsEmpty因为前者会统计纯目录变化而后者像git diff --quiet一样忽略纯目录变化。id():PromiseChangesetID—— 返回该 Changeset 的唯一标识本质是 before/after 目录 ID 的序列化组合见上文序列化机制。强制求值sync()sync():PromiseChangeset—— Force evaluation in the engine.Dagger 采用懒求值模型sync()强制引擎物化materialize该变更集的 before/after 目录返回求值后的Changeset本身。Go 侧Sync/Evaluatecore/changeset.go#L505-L520中有一处非常值得玩味的注释在 generator 场景下sync 会真正触发生成器的 exec如 SDK codegen因此在生成器 span 内同步变更集可以让失败归属于对应的生成器 span前端即可渲染出红色生成器及其执行日志而不是把失败推迟到合并阶段才暴露。链式工具with()with(arg):Changeset—— Call the provided function with current Changeset. This is useful for reusability and readability by not breaking the calling chain.参数arg是(param) Changeset形式的回调函数把当前 Changeset 传给回调并返回其结果。这是 SDK 在各类型上的通用工具方法用于把一段复用逻辑内联进调用链避免破坏链式结构例如const result await client.directory() .changes(fromDir) .with(cs applySomePolicy(cs)) .sync();合并withChangeset() 与 withChangesets()withChangeset()二元三方合并withChangeset(changes,opts?):Changeset—— Add changes to an existing changeset. By default the operation will fail in case of conflicts, for instance a file modified in both changesets. The behavior can be adjusted using onConflict argumentchanges:Changeset要合并进当前变更集的另一个变更集opts?:ChangesetWithChangesetOpts其中唯一可选属性onConflict?: ChangesetMergeConflict即冲突处理策略。ChangesetWithChangesetOpts的完整定义type ChangesetWithChangesetOpts { /** What to do on a merge conflict */ onConflict?: ChangesetMergeConflict; }底层WithChangesetcore/changeset.go#L1172-L1217的算法是 git 风格三方合并分别计算双方ch与other的路径集合CheckConflictscore/changeset.go#L1031-L1087用 O(1) 的 path set 做文件级冲突检测三类冲突对应三个错误ErrAddedTwice两方都新增同一路径、ErrModifiedTwice两方都修改同一路径、ErrModifiedRemoved一方修改另一方删除若冲突非空且策略为FailEarlyOnConflict直接报错返回合并双方的 before 目录mergeBeforeDirectories得到合并基物化双方的变更内容后执行gitMergeChangesets按策略决定结果。ChangesetMergeConflict 枚举对应ChangesetMergeConflict定义于 Go 侧 core/changeset.go#L1120-L1144TypeScript 成员值行为FailEarlyFAIL_EARLY合并前若检测到文件级冲突直接失败engine ≥ v0.15.0 才暴露FailFAIL尝试合并git merge 因冲突失败时报错engine ≥ v0.15.0 才暴露LeaveConflictMarkersLEAVE_CONFLICT_MARKERS让 git 在文件中留下冲突标记modify/delete 冲突保留修改版本二进制冲突失败PreferOursPREFER_OURS用-X ours策略冲突时采用调用方变更集的版本PreferTheirsPREFER_THEIRS用-X theirs策略冲突时采用另一方变更集的版本FailEarly/Fail两个成员仅在引擎 v0.15.0 之后通过AfterVersion(v0.15.0)暴露core/changeset.go#L1125-L1137原因是避免与多变更集枚举的同名值冲突。withChangesets()Octopus 多路合并withChangesets(changes,opts?):Changeset—— Add changes from multiple changesets using git octopus merge strategy. This is more efficient than chaining multiple withChangeset calls when merging many changesets. Only FAIL and FAIL_EARLY conflict strategies are supported (octopus merge cannot use -X ours/theirs).changes:Changeset[]要合并的变更集列表opts?:ChangesetWithChangesetsOptsonConflict?: ChangesetsMergeConflict。对应ChangesetsMergeConflictGo 侧 core/schema/directory.go#L1498-L1522TypeScript 成员值行为FailEarlyFAIL_EARLY若任意两个变更集之间存在文件级冲突合并前直接失败FailFAIL尝试 octopus 合并git merge 失败时报错注意与withChangeset不同octopus 策略不支持-X ours/theirs因此只提供两种策略。底层WithChangesetscore/changeset.go#L1238-L1335的实现值得展开先剔除空变更集并行maxParallelChangesets 8见 core/changeset.go#L1219-L1232计算每个变更集的路径过滤掉无变化的避免无谓的挂载与遍历单元素短路只剩一个时降级为更高效的WithChangeset两路合并FailEarlyOnConflicts映射为FailEarlyOnConflict其余映射为FailOnConflict见 core/changeset.go#L1273-L1283两两冲突预检checkAllPairwiseConflicts在正式合并前检查所有变更集两两之间的冲突若策略为FailEarlyOnConflicts则提前失败合并 before 目录mergeBeforeDirectoriescore/changeset.go#L1337-L1400把各方 before 目录叠加合并连续重复的目录会被折叠去重因为同一个基线重复 N 次会白做 N 次全树拷贝并剔除.git目录合并过程会创建自己的临时.git并行物化内容每个变更集的差异物化content在 8 并发限制下并行执行执行 octopus 合并gitOctopusMergeChangesets一次完成多路合并。代码注释特别强调连续合并 N 个 against 同一基线的变更集时withChangesets比逐个withChangeset链式调用更高效——这也正是 API 文档所说 more efficient than chaining multiple withChangeset calls 的底层原因。完整实战示例下面给出一个把上述 API 串起来的 TypeScript 示例演示生成变更 → 查看路径 → 导出 → 生成补丁 → 合并 → 冲突处理的完整链路import { client, Directory, Changeset } from dagger.io/dagger; // 1. 构造新旧两个目录快照 const base client.directory() .withNewFile(README.md, # v1) .withNewFile(src/main.ts, console.log(1)) .withNewFile(legacy.ts, old code); const updated base .withNewFile(README.md, # v2) // 修改 .withNewFile(src/extra.ts, new file) // 新增 .withoutFile(legacy.ts); // 删除 // 2. 生成变更集updated 为 afterbase 为 before const changes: Changeset updated.changes(base); // 3. 查询各类变更路径 const added await changes.addedPaths(); // [src/extra.ts] const modified await changes.modifiedPaths(); // [README.md] const removed await changes.removedPaths(); // [legacy.ts] // 4. 判断是否为空 / 强制求值 if (await changes.isEmpty()) { /* 无变化 */ } await changes.sync(); // 强制引擎求值 // 5. 生成 Git 兼容补丁并导出 const patch changes.asPatch(); // File名为 diff.patch await patch.export(./patches/changes.patch); // 6. 把差异应用到宿主机目录含删除同步 await changes.export(./workspace); // 返回导出的路径字符串 // 7. 合并另一个变更集指定冲突策略 const other anotherDir.changes(base); const merged changes.withChangeset(other, { onConflict: ChangesetMergeConflict.PreferOurs, // 冲突时采用调用方版本 }); // 8. 一次合并多个变更集octopus const mergedAll changes.withChangesets( [cs1, cs2, cs3], { onConflict: ChangesetsMergeConflict.FailEarly }, // 有冲突直接失败 );小结Changeset 的核心能力地图能力方法返回底层实现参考快照访问before()/after()Directorycore/changeset.go#L348-L359变更路径addedPaths()/modifiedPaths()/removedPaths()string[]ComputePathscore/changeset.go#L160-L206差异导出export(path)stringcore/changeset.go#L924-L985Git 补丁asPatch()Filecore/changeset.go#L667-L781产出快照layer()Directorycore/schema/directory.go#L1331状态判断isEmpty()/id()boolean/ChangesetIDcore/changeset.go#L564-L596强制求值sync()Changesetcore/changeset.go#L505-L520链式工具with(fn)Changeset—二路合并withChangeset(changes, opts?)Changesetcore/changeset.go#L1172-L1217多路合并withChangesets(changes, opts?)Changesetcore/changeset.go#L1238-L1335当你在 Dagger 中需要比较两个目录快照、提取差异、生成可应用的补丁、或把多个来源的修改合并到一起时Changeset就是那个把 diff 变成一等公民对象的类型它由Directory.changes(from)产生、以 before/after 双快照为内核、以 git 为合并与补丁引擎并且完整保留删除语义——这使它成为代码生成器、模块迁移、工作区变更管理等场景下不可替代的基础设施。更深入的机制delta diff 优化、冲突检测、octopus 合并细节可以继续阅读 core/changeset.go 与其集成测试 core/integration/changeset_test.go。【免费下载链接】daggerAutomation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud项目地址: https://gitcode.com/GitHub_Trending/da/dagger创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表