rtk-ai/rtk · error
templated hg log must parse
Error message
templated hg log must parse
What it means
`.expect("templated hg log must parse")` panic: `condense_unified_diff_strict()` returned None on an `hg log -p --template '{node|short} {desc}\n'` fixture. The stream has a prose prologue with no `changeset:` line, followed by hg's echo of the template, then patches; the condenser must treat that prose as noise (not facts) and still condense the patches. None means the prose prologue forced the whole-stream raw fallback.
Solutions
- Open a message/prose region on recognized producer headers even without a `changeset:` line (as done for mbox `From`), so templated prologues are skipped rather than fatal.
- Only force raw fallback when unread column-0 noise appears after a diff context starts, not before the first header.
- Regenerate the fixture from the exact hg version/template if the recorded echo format drifted.
- Add a fixture-less unit test: prose + patches must condense.
Example fix
// before
if !saw_changeset_header { dropped_prologue = true; } // later => raw fallback
// after
if !saw_changeset_header { open_message_region(); } // prose skipped, patches still condensed Defensive patterns
Strategy: validation
Validate before calling
assert!(condense_unified_diff_strict(&hg_template_fixture)
.map_or(false, |o| o.contains("[file] f.txt (+1 -1)")), "templated prologue must not force raw"); Type guard
fn strictly_parses(diff: &str) -> bool { crate::cmds::git::diff_cmd::condense_unified_diff_strict(diff).is_some() } Try / catch
let out = condense_unified_diff_strict(hg_output).unwrap_or_else(|| hg_output.to_string()); // production callers never panic: condense_stdin already does this (diff_cmd.rs:498)
Prevention
- Treat any column-0 prose before the first header as a skippable message region, not a fatal condition.
- Only force raw fallback when noise appears after diff context has started.
- Add fixtures for custom hg templates if users are known to use them with rtk.
- Keep marker-localization (LC_ALL) cases in the same test module as prose cases.
When it happens
Trigger: Calling `condense_unified_diff_strict()` on tests/fixtures/diff/hg_log_p_template_raw.txt returns None because column-0 prose before the first recognized header (no `changeset:` line, custom template output) latches the dropped-prologue/raw-fallback path. Expected output still contains `[file] bin.dat (binary)` and `[file] f.txt (+1 -1)`.
Common situations: Users with custom hg templates or aliases feeding rtk; Mercurial log output with `--template` used together with `-p`; locale-localized diff markers (same test also covers a French `diff -u` `No newline` marker).
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
AI-assisted analysis of rtk-ai/rtk@0924356b4c (2026-09-19).
Data as JSON: /api/errors/89941d829a6dff36.
Report an issue: GitHub.
Appendix: source
Thrown at src/cmds/git/diff_cmd.rs:6617
"got:\n{out}"
);
// `--submodule=diff`: nested column-0 sections under the range.
let sd = include_str!("../../../tests/fixtures/diff/git_diff_submodule_diff_raw.txt");
assert_eq!(
condense_unified_diff_strict(sd).as_deref(),
Some(
"[file] o.txt (+1 -1)\n-k\n+k2\n[file] sub (submodule, 0641fef..19e2e29)\n[file] sub/s.txt (+1 -1)\n-s\n+s2\n[file] sub/sb.bin (binary)"
)
);
// `git log --numstat -p`: `-<TAB>-<TAB>b.bin` before the diff.
let ns = include_str!("../../../tests/fixtures/diff/git_log_numstat_p_raw.txt");
let out = condense_unified_diff_strict(ns).expect("--numstat -p must parse");
assert!(out.contains("[file] b.bin (binary)"), "got:\n{out}");
assert!(out.contains("[file] d/e/f (+1 -0)"), "got:\n{out}");
// `hg log -p --template '{node|short} {desc}\n'`: a prose prologue
// with no `changeset:` line, then hg's echo — prose is not a fact.
let tmpl = include_str!("../../../tests/fixtures/diff/hg_log_p_template_raw.txt");
let out = condense_unified_diff_strict(tmpl).expect("templated hg log must parse");
assert!(out.contains("[file] bin.dat (binary)"), "got:\n{out}");
assert!(out.contains("[file] f.txt (+1 -1)"), "got:\n{out}");
// `LC_ALL=fr_FR.UTF-8 diff -u`: the marker is localized, position
// decides — kept after `-x` and after `+y`.
let fr = include_str!("../../../tests/fixtures/diff/diff_u_fr_no_newline_raw.txt");
assert_eq!(
condense_unified_diff_strict(fr).as_deref(),
Some(
"[file] n2 (+1 -1)\n-x\n\\ Pas de fin de ligne à la fin du fichier\n+y\n\\ Pas de fin de ligne à la fin du fichier"
)
);
// A git binary section followed by another producer's header pair
// (two streams concatenated): the pair opens its own section
// rather than renaming the binary one and stealing its note.
let concat = "diff --git a/x.bin b/x.bin\nindex 1111111..2222222 100644\nBinary files a/x.bin and b/x.bin differ\n--- g1/m.txt\t2026-01-01 00:00:00.000000000 +0000\n+++ g2/m.txt\t2026-01-01 00:00:00.000000000 +0000\n@@ -1 +1 @@\n-x\n+y\n";
assert_eq!(
condense_unified_diff_strict(concat).as_deref(),
Some("[file] x.bin (binary)\n[file] g2/m.txt (+1 -1)\n-x\n+y")View on GitHub (pinned to 0924356b4c)