Skip to content

feat(util): add a comment-preserving YAML merge helper - #14010

Merged
jdx merged 2 commits into
mainfrom
dotfiles-yaml-merge
Oct 6, 2026
Merged

jdx merged 2 commits into
mainfrom
dotfiles-yaml-merge

Conversation

@jdx

@jdx jdx commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

https://entire.io/gh/jdx/mise/trails/459

Adds an internal helper that sets keys on a YAML document by editing its syntax tree in place, so comments, key order, quoting, and flow/block style on everything else survive. Nothing calls it yet; it is the foundation for the merge dotfile entries stacked on top (#13986).

merge_yaml("# note\ntheme: dark  # ui\nkeep: 1\n", "theme: light\n")
// => "# note\ntheme: light  # ui\nkeep: 1\n"
  • Mappings that exist on both sides merge recursively; any other value, including a sequence, is replaced by the source's. A value that already equals the source's is left untouched, formatting included.
  • An empty or comment-only target is an empty mapping. A non-mapping root or a multi-document file is an error.

It is built on yaml-edit (pure Rust on rowan, MSRV 1.70). aube's comment-preserving path uses yamlpatch/yamlpath, which are pinned below their latest releases for MSRV and pull in tree-sitter; this avoids both. yaml-edit adds rowan 0.16 and nothing else new to Cargo.lock.

Verified with unit tests, cargo clippy --workspace --all-features --all-targets -- -D warnings, and cargo +1.95 check.

AI-assisted — Tool: Claude Code; model: anthropic/claude-sonnet-5-5; version: 2.1.288.

🤖 Generated with Claude Code


Note

Low Risk
New internal utility with no production call sites yet; dependency adds yaml-edit/rowan but behavior is covered by unit tests.

Overview
Adds a merge_yaml helper in mise-util that merges a source YAML mapping into an existing target by editing the text in place via the new yaml-edit dependency, so comments, key order, quoting, and flow/block style on untouched keys stay intact.

Merge rules: nested mappings recurse; sequences and other values are replaced from the source; equal values are left unchanged. Empty or comment-only targets become empty mappings; non-mapping roots and multi-document files error. A post-merge verify step re-parses with serde_yaml to ensure every source key is present, and replace_value works around a yaml-edit block-scalar line-break bug. The module is exported but not wired to any callers yet (intended for upcoming dotfile merge support).

Reviewed by Cursor Bugbot for commit 688eb51. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added YAML configuration merging that combines nested mappings, adds missing keys, and replaces values where needed.
    • Existing formatting and untouched content are preserved where possible, and unchanged values are left as-is.
    • Empty or comment-only targets can be used as the starting point for a merge.
  • Bug Fixes

    • Invalid YAML, multiple-document input, and non-mapping root values now return errors instead of being merged.

Add `mise_util::yaml_merge::merge_yaml`, which sets the keys of a source
mapping on a target YAML document by editing the target's syntax tree in
place. Comments, key order and formatting on keys the source does not
mention survive, and a value that already equals the source's is left
untouched. Built on `yaml-edit` (pure Rust, MSRV 1.70), so it avoids the
tree-sitter based `yamlpatch`/`yamlpath` pair that aube has to pin below
their latest releases for MSRV.

Nothing calls it yet; the dotfiles `merge` entries stacked on top use it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M48GV40G87HKFRTDCF7A64V8
@jdx
jdx added this pull request to stack #14013 October 6, 2026 12:11
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9b3dda4f-d5f8-4f62-8faa-cae73fc588ec
📥 Commits

Reviewing files that changed from the base of the PR and between 83f3e90 and 688eb51.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • crates/mise-util/Cargo.toml
  • crates/mise-util/src/lib.rs
  • crates/mise-util/src/yaml_merge.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

Adds and exports merge_yaml, which merges source YAML mappings into a target while preserving untouched text and formatting. The function checks document roots, verifies the merged values, and returns errors for malformed YAML, multiple documents, and invalid roots.

Changes

YAML merge utility

Layer / File(s) Summary
Public entry point and YAML input handling
crates/mise-util/Cargo.toml, crates/mise-util/src/lib.rs, crates/mise-util/src/yaml_merge.rs
Adds the yaml-edit dependency and exports yaml_merge. Parses YAML input and checks document roots.
Merge, preserve formatting, and verify output
crates/mise-util/src/yaml_merge.rs
Recursively merges mappings, replaces differing values, and handles block-scalar replacement. Tests cover formatting preservation, merge behavior, idempotence, and errors.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Merge Risk: ⚪ Minimal · up to 688eb

No established issue currently blocks merging this helper.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a comment-preserving YAML merge helper.
Docstring Coverage ✅ Passed Docstring coverage is 94.12% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 2 files. (1 skipped: 1 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​yaml-edit@​0.3.299100100100100

View full report

@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Adds a YAML merge utility with a new dependency.

The PR appears safe to merge; no new actionable issue was found.

Summary

Adds merge_yaml to merge YAML mappings while keeping untouched text.

  • Nested mappings merge recursively; other values are replaced.
  • The latest changes replace block scalars without joining the following key onto their line.
  • The helper now checks that the result parses and contains the source values.
  • Added tests address the previous formatting-test suggestion with flow mappings, block scalars, and nested indentation.

Reviews (2) · Last reviewed commit: "fix(util): replace YAML block scalars cl..."

Comment thread crates/mise-util/src/yaml_merge.rs Resolved
Replacing a block scalar (`|` or `>`) in place made `yaml-edit` drop the line
break after it, gluing the next key onto the same line (`text: newk: 1`).
Such an entry is now removed and re-inserted at the same position, and the
merged text is parsed again to confirm it holds every source key before it
is returned.

Add tests for flow mappings, block scalars, and inserts into a nested mapping
with a different indentation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M48VKFPSRAZB7Q9FYRHN7TQ5
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

Nothing was compared, and so nothing was gated. No series appears on both sides: either the base has no measurements recorded, or the two were measured on different runner classes, which are deliberately not comparable — counts shift between machine types by more than a real regression does.

New, nothing to compare against: env on jdx-perf-v1-ubuntu24.04-x64-mise-rust1.97.1-img5263c143, hook-env on jdx-perf-v1-ubuntu24.04-x64-mise-rust1.97.1-img5263c143, ls on jdx-perf-v1-ubuntu24.04-x64-mise-rust1.97.1-img5263c143, registry on jdx-perf-v1-ubuntu24.04-x64-mise-rust1.97.1-img5263c143, startup on jdx-perf-v1-ubuntu24.04-x64-mise-rust1.97.1-img5263c143

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

688eb512357b vs 2f0975c0e883 · measured on the runner, not pushed to the history.

@jdx
jdx merged commit b6beeb8 into main Oct 6, 2026
50 of 53 checks passed
@jdx
jdx deleted the dotfiles-yaml-merge branch October 6, 2026 18:48
jdx added a commit that referenced this pull request Oct 6, 2026
…4011)

<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/mise/trails/460
<!-- entire-trail-link-end -->

Adds `mise_util::structured_merge`, which sets some keys of a JSON,
TOML, or YAML file and leaves the rest alone, plus `contains`, which
reports whether a file already holds those keys. Nothing calls it yet;
the `merge` dotfile entries stacked on top use it (#13986).

- **TOML** edits in place through `toml_edit`: comments, key order, and
the formatting of untouched keys survive.
- **YAML** goes through the `yaml_merge` helper from the PR below.
- **JSON** keeps key order and the file's own indent, and keeps a
trailing newline if there was one.
- Tables merge recursively; scalars and arrays are replaced; nothing is
ever removed. An empty target is an empty table.
- `contains` compares parsed values and looks only at the source's keys,
so a target that already satisfies the source is returned byte for byte
and keys the application added are not drift.

`serde_json`'s `preserve_order` feature was already enabled
workspace-wide through feature unification; `mise-util` now asks for it
explicitly. `Cargo.lock` does not change.

Verified with unit tests (per-format merges, idempotence, unparseable
and non-table input), `cargo clippy --workspace --all-features
--all-targets -- -D warnings`, and `cargo +1.95 check`.

Stacked on #14010.

*AI-assisted — Tool: Claude Code; model: anthropic/claude-sonnet-5-5;
version: 2.1.288.*

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> New, unused library code with tests only—no behavior change to
existing mise flows until wired in.
> 
> **Overview**
> Adds **`mise_util::structured_merge`** so callers can patch only the
keys they own in shared config files (JSON, TOML, YAML) without
rewriting the rest. **`merge`** applies source keys with shared
semantics (nested objects merge recursively; scalars and arrays are
replaced; extra target keys are never removed), and returns the target
**byte-for-byte** when it already matches. **`contains`** checks drift
using only the source’s keys.
> 
> Format-specific writers aim to preserve hand-edited formatting: JSON
uses **`serde_json` `raw_value`** to keep untouched value text, key
order, and indent; TOML merges via **`toml_edit`** to keep comments and
decorations; YAML delegates to the existing **`yaml_merge`** helper.
**`serde_json`** in `mise-util` now explicitly enables
**`preserve_order`** and **`raw_value`**. The module is exported from
**`lib.rs`** and covered by unit tests; nothing in the repo calls it
yet.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
44ec1f6. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added structured merging for JSON, TOML, and YAML configuration files.
Merges retain keys that exist only in the target, and files that already
contain all source values are left unchanged.
* Improved preservation of existing formatting during merges, including
JSON indentation and newline style and TOML value decorations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
jdx pushed a commit that referenced this pull request Oct 7, 2026
<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/mise/trails/430
<!-- entire-trail-link-end -->

### 🚀 Features

- **(bootstrap)** add a tool-update service that keeps auto_update tools
current by @jdx in [#14040](#14040)
- **(config)** add global section write targets by @jdx in
[#14018](#14018)
- **(config)** move mise's auto-update settings under self_update.* by
@jdx in [#14038](#14038)
- **(dotfiles)** report notification availability and add notify command
by @jdx in [#14009](#14009)
- **(dotfiles)** add merge entries that own only some keys of a config
file by @jdx in [#14012](#14012)
- **(http)** add headers option for bearer and API-key auth by @jdx in
[#14022](#14022)
- **(install)** add an opt-in identity-based install layout by @jdx in
[#13951](#13951)
- **(install)** keep identity-layout installations in a shorter
directory on Windows by @jdx in
[#13952](#13952)
- **(installs)** list installations and choose the one requests without
a lockfile use by @jdx in
[#13953](#13953)
- **(installs)** move legacy installations into the identity layout by
@jdx in [#13955](#13955)
- **(lock)** preserve shared lockfile entries on request by @jdx in
[#13980](#13980)
- **(npm)** install packages from git repositories by @jdx in
[#14044](#14044)
- **(prune)** add prune.exclude to keep chosen tools from being pruned
by @jdx in [#14030](#14030)
- **(secrets)** add [secrets.fnox] project config and mise secrets ls by
@jdx in [#13967](#13967)
- **(secrets)** add --secrets and --secrets-all to mise run and mise x
by @jdx in [#13975](#13975)
- **(task)** give tasks only the fnox secrets they list by @jdx in
[#13974](#13974)
- **(task)** allow {{ secrets.X }} in task env values by @jdx in
[#13978](#13978)
- **(upgrade)** update global tools before they run with auto_update by
@jdx in [#14026](#14026)
- **(util)** add a comment-preserving YAML merge helper by @jdx in
[#14010](#14010)
- **(util)** add format-aware merge for JSON, TOML, and YAML files by
@jdx in [#14011](#14011)

### 🐛 Bug Fixes

- **(backend)** read a repository's packslip release list once per
command by @jdx in [#13991](#13991)
- **(backend)** only warn about a backend switch when the backend kind
changes by @jdx in [#14032](#14032)
- **(bootstrap)** don't create history state or fail on its lock unless
writing files by @jdx in
[#14029](#14029)
- **(cargo)** serialize rustup state across cargo install fallbacks by
@jdx in [#14042](#14042)
- **(config)** keep global config when a project ignore resolves onto it
via symlink by @jdx in [#14006](#14006)
- **(doctor)** only warn about backend mismatches the registry serves by
@jdx in [#14005](#14005)
- **(dotenv)** leave a value ending in escaped whitespace unquoted by
@jdx in [#13956](#13956)
- **(dotfiles)** explain missing sources in conflicting declaration
errors by @himkt in [#13973](#13973)
- **(dotfiles)** skip legacy link discovery for group entries by @jdx in
[#14023](#14023)
- **(dotnet)** disable lockfile url enforcement for dotnet tool backend
by @james-newell-forge in
[#13971](#13971)
- **(env)** fully redact secrets when redaction values overlap by @jdx
in [#13962](#13962)
- **(github)** log partial attestation verification failures by @jdx in
[#14035](#14035)
- **(github)** warn when attestations are skipped because the Sigstore
trust root is unreachable by @jdx in
[#14036](#14036)
- **(http)** fetch a cached GitHub release once when callers race by
@jdx in [#13990](#13990)
- **(http)** don't retry a 429 whose Retry-After outlasts the backoff by
@jdx in [#14027](#14027)
- **(http)** retry downloads when an HTTP/2 stream reset fails the body
read by @jdx in [#14049](#14049)
- **(install)** switch backends into a new installation, and resolve
CLI-named versions without guessing by @jdx in
[#13957](#13957)
- **(install)** keep a version incomplete until its postinstall hook
succeeds by @jdx in [#14037](#14037)
- **(install)** keep incomplete-install markers out of the cache by @jdx
in [#14051](#14051)
- **(mcp)** reject run_task task names that start with a dash by @jdx in
[#13961](#13961)
- **(oci)** stream large blob uploads in one PATCH by @jdx in
[#14017](#14017)
- **(outdated)** don't bump vendor-only requests like java@temurin by
@jdx in [#14031](#14031)
- **(prune)** keep versions that a running process was started from by
@jdx in [#14020](#14020)
- **(prune)** snapshot what templated tool versions render to in the
identity layout by @jdx in
[#14025](#14025)
- **(pypi)** build uv tool venvs on mise's python by @jdx in
[#14024](#14024)
- **(pypi)** lock from <root>/simple/ for JSON URLs by @deiga in
[#14007](#14007)
- **(python)** create the auto uv venv with mise's python during install
by @halms in [#13981](#13981)
- **(release)** skip the vfox-tiny test fixture when vendoring embedded
plugins by @jdx in [#14047](#14047)
- **(ruby)** keep Gemfile reload watches out of executable hooks by
@DahanItamar in [#13985](#13985)
- **(task)** list monorepo subproject tasks in the mise://tasks MCP
resource by @jdx in [#14053](#14053)
- **(uninstall)** report each version once in a dry run by @jdx in
[#13992](#13992)

### 📚 Documentation

- **(http)** show a package proxy recipe for url_replacements by @jdx in
[#14050](#14050)
- **(readme)** add Star History badges by @jdx in
[#13968](#13968)
- keep code blocks and alert markers out of llms.txt summaries by
@DahanItamar in [#14034](#14034)

### ⚡ Performance

- **(secrets)** read cached fnox secrets from the fnox daemon by @jdx in
[#13979](#13979)

### 🧪 Testing

- **(e2e)** stabilize task timing assertions by @jdx in
[#13977](#13977)
- **(go)** disable minimum_release_age in go install e2e by @jdx in
[#13989](#13989)

### 📦️ Dependency Updates

- lock file maintenance by @renovate[bot] in
[#13963](#13963)
- update github actions by @renovate[bot] in
[#13994](#13994)
- update rust crates by @renovate[bot] in
[#13996](#13996)
- update rust crate nodejs-semver to v7 by @renovate[bot] in
[#13999](#13999)
- update jdx/mise-action action to v5 by @renovate[bot] in
[#13997](#13997)
- update jdx/renovate-config action to v1.0.2 by @renovate[bot] in
[#14003](#14003)
- update dependency usage to latest by @renovate[bot] in
[#14004](#14004)
- update jdx crates to v6.12.1 by @renovate[bot] in
[#14002](#14002)
- update rust crate sigstore-verify to 0.14 by @jdx in
[#14019](#14019)
- update rattler by @renovate[bot] in
[#13998](#13998)
- update rust crate clx to v3.0.4 by @renovate[bot] in
[#14028](#14028)

### 📦 Registry

- update moved plugin references by @jdx in
[#13972](#13972)

### Chore

- **(ci)** update pr-closer to v1.3.0 by @jdx in
[83f3e90](83f3e90)

### Ci

- update pr closer action by @jdx in
[#13970](#13970)
- add manual M4 sandbox pilot by @jdx in
[09e8f8f](09e8f8f)

### Security

- **(env)** keep secrets inherited from a parent mise out of templates,
caches and other tasks by @jdx in
[#13966](#13966)

### New Contributors

- @DahanItamar made their first contribution in
[#14034](#14034)
- @deiga made their first contribution in
[#14007](#14007)
- @james-newell-forge made their first contribution in
[#13971](#13971)

## 📦 Aqua Registry Updates

### New Packages (3)

-
[`editorconfig/editorconfig-core-go`](https://github.com/editorconfig/editorconfig-core-go)
- [`jdx/jactionlint`](https://github.com/jdx/jactionlint)
- [`tontinton/maki`](https://github.com/tontinton/maki)

### Updated Packages (3)

- [`anchore/syft`](https://github.com/anchore/syft)
- [`eksctl-io/eksctl`](https://github.com/eksctl-io/eksctl)
-
[`regclient/regclient/regsync`](https://github.com/regclient/regclient)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant