Render correspoding hunk on inline credit hover

finchie
Dec 10, 2025, 7:04 AM
DUZBRB3U7ERDXRM6A7NFI4IGUB6IR4V2L7JJSLCQBSJYJDUZQR6AC

Dependencies

  • [2] WFWTKCJN Create initial Visual Studio Code extension
  • [3] 72K45XKD Refactor inline credit to improve hover messages
  • [4] M2NAH3DC Store modified time of `FileContents` as `jiff::Timestamp`
  • [5] TIPGJCUW Consistently use `EditorWorkingCopy` in `pijul-extension`

Change contents

  • replacement in pijul-extension/src/lib.rs at line 17
    [2.10892][2.10892:10928]()
    use libpijul::change::ChangeHeader;
    [2.10892]
    [2.10955]
    use libpijul::change::{Atom, BaseHunk, Change, ChangeHeader, TextSerError};
  • replacement in pijul-extension/src/lib.rs at line 23
    [2.11192][2.11192:11244]()
    use libpijul::{ArcTxn, ChangeId, ChannelRef, TxnT};
    [2.11192]
    [5.0]
    use libpijul::{ArcTxn, ChangeId, ChannelRef, TxnT, Vertex};
  • replacement in pijul-extension/src/lib.rs at line 28
    [2.11350][5.39:105]()
    use crate::file_system::changes::{FileCredits, FileCreditsError};
    [2.11350]
    [5.105]
    use crate::file_system::changes::{
    ActiveHunk, CreditSource, FileCredits, FileCreditsError, HunkDiff,
    };
  • edit in pijul-extension/src/lib.rs at line 75
    [2.12850]
    [2.12850]
    }
    #[derive(Debug, thiserror::Error)]
    pub enum ActiveHunkError<C: std::error::Error + 'static> {
    #[error("File is not tracked by Pijul")]
    Untracked,
    #[error("Missing file contents for hunk: {hunk:#?}")]
    MissingFileContents {
    hunk: Box<BaseHunk<Atom<Option<libpijul::Hash>>, libpijul::change::Local>>,
    },
    #[error("Change not found: {change_id:?}")]
    ChangeNotFound { change_id: ChangeId },
    #[error("No hunk found for vertex: {vertex:?}")]
    NoHunkForVertex { vertex: Vertex<ChangeId> },
    #[error("Failed to get change contents: {0}")]
    ChangeContents(#[from] TextSerError<C>),
    #[error("Hunk not yet implemented: {hunk:#?}")]
    UnimplementedHunk {
    hunk: Box<BaseHunk<Atom<Option<libpijul::Hash>>, libpijul::change::Local>>,
    },
  • edit in pijul-extension/src/lib.rs at line 148
    [2.14246]
    [2.14246]
    // TODO: LRU cache for recent changes
  • replacement in pijul-extension/src/lib.rs at line 197
    [2.15761][2.15761:15902]()
    pub fn get_change_header(&self, change_id: &ChangeId) -> Option<&ChangeHeader> {
    self.channel_state.get_change_header(change_id)
    [2.15761]
    [2.15902]
    pub fn get_change(&self, change_id: &ChangeId) -> Option<&Change> {
    self.channel_state.get_change(change_id)
    }
    pub fn get_active_hunk(
    &self,
    open_file: &OpenFile,
    credit_source: CreditSource,
    ) -> Result<ActiveHunk, ActiveHunkError<C::Error>> {
    let Some(credits) = open_file.credits.as_ref() else {
    return Err(ActiveHunkError::Untracked);
    };
    let change_contents = match credit_source {
    CreditSource::Tracked { vertex } => {
    let Some(change) = self.channel_state.get_change(&vertex.change) else {
    return Err(ActiveHunkError::ChangeNotFound {
    change_id: vertex.change,
    });
    };
    &change.contents
    }
    CreditSource::Untracked { hunk_index: _ } => &credits.unrecorded_state.change_contents,
    };
    let (hunk_index, hunk) = match credit_source {
    CreditSource::Tracked { vertex } => self
    .channel_state
    .find_vertex_hunk(vertex)
    .ok_or_else(|| ActiveHunkError::NoHunkForVertex { vertex })?,
    CreditSource::Untracked { hunk_index } => {
    // Return 1-indexed hunks
    (hunk_index + 1, &credits.unrecorded_state.hunks[hunk_index])
    }
    };
    // TODO: properly handle encoding
    let hunk_diff = match hunk {
    BaseHunk::FileAdd {
    contents,
    add_name: _,
    add_inode: _,
    path: _,
    encoding,
    } => {
    let Some(contents) = contents else {
    return Err(ActiveHunkError::MissingFileContents {
    hunk: Box::new(hunk.clone()),
    });
    };
    let file_contents = libpijul::change::get_change_contents(
    &self.change_store,
    contents,
    change_contents,
    )?;
    let file_text = String::from_utf8(file_contents).unwrap();
    HunkDiff::TextChange {
    lines_added: file_text.lines().map(str::to_string).collect(),
    lines_removed: Vec::new(),
    }
    }
    BaseHunk::Replacement {
    change,
    replacement,
    local: _,
    encoding,
    } => {
    let old_contents = libpijul::change::get_change_contents(
    &self.change_store,
    change,
    change_contents,
    )?;
    let new_contents = libpijul::change::get_change_contents(
    &self.change_store,
    replacement,
    change_contents,
    )?;
    let old_text = String::from_utf8(old_contents).unwrap();
    let new_text = String::from_utf8(new_contents).unwrap();
    HunkDiff::TextChange {
    lines_added: new_text.lines().map(str::to_string).collect(),
    lines_removed: old_text.lines().map(str::to_string).collect(),
    }
    }
    BaseHunk::Edit {
    change,
    local: _,
    encoding,
    } => {
    let new_contents = libpijul::change::get_change_contents(
    &self.change_store,
    change,
    change_contents,
    )?;
    let text = String::from_utf8(new_contents).unwrap();
    if let Atom::EdgeMap(edge) = change
    && (edge.edges.is_empty() || edge.edges[0].flag.is_deleted())
    {
    HunkDiff::TextChange {
    lines_added: Vec::new(),
    lines_removed: text.lines().map(str::to_string).collect(),
    }
    } else {
    HunkDiff::TextChange {
    lines_added: text.lines().map(str::to_string).collect(),
    lines_removed: Vec::new(),
    }
    }
    }
    _ => {
    return Err(ActiveHunkError::UnimplementedHunk {
    hunk: Box::new(hunk.clone()),
    });
    }
    };
    Ok(ActiveHunk {
    index: hunk_index,
    diff: hunk_diff,
    })
  • replacement in pijul-extension/src/file_system/open_file/mod.rs at line 36
    [2.23725][2.23725:23851]()
    None => Vec::from([Span {
    value: CreditSource::Untracked,
    lines,
    }]),
    [2.23725]
    [2.23851]
    None => Vec::new(),
  • replacement in pijul-extension/src/file_system/changes/unrecorded.rs at line 8
    [2.26665][2.26665:26704]()
    use crate::file_system::changes::Span;
    [2.26665]
    [2.26784]
    use crate::file_system::changes::{HunkOffset, Span};
  • edit in pijul-extension/src/file_system/changes/unrecorded.rs at line 18
    [2.27080][2.27080:27322]()
    #[derive(Clone, Copy, Debug)]
    pub enum SpanKind {
    Replacement {
    old_line_count: usize,
    new_line_count: usize,
    },
    Addition {
    lines_added: usize,
    },
    Deletion {
    lines_removed: usize,
    },
    }
  • replacement in pijul-extension/src/file_system/changes/unrecorded.rs at line 20
    [5.5667][2.27377:27413](),[2.27377][2.27377:27413]()
    pub spans: Vec<Span<SpanKind>>,
    [5.5667]
    [2.27413]
    pub change_contents: Vec<u8>,
    pub hunks: Vec<BaseHunk<Atom<Option<libpijul::Hash>>, libpijul::change::Local>>,
    pub spans: Vec<Span<HunkOffset>>,
  • edit in pijul-extension/src/file_system/changes/unrecorded.rs at line 57
    [2.28276]
    [2.28276]
  • edit in pijul-extension/src/file_system/changes/unrecorded.rs at line 59
    [2.28323]
    [2.28323]
    let mut hunks = Vec::with_capacity(unrecorded_state.actions.len());
  • replacement in pijul-extension/src/file_system/changes/unrecorded.rs at line 65
    [2.28478][2.28478:28514]()
    match globalized_hunk {
    [2.28478]
    [2.28514]
    match &globalized_hunk {
  • replacement in pijul-extension/src/file_system/changes/unrecorded.rs at line 74
    [2.28811][2.28811:28844]()
    &change,
    [2.28811]
    [2.28844]
    change,
  • replacement in pijul-extension/src/file_system/changes/unrecorded.rs at line 79
    [2.29026][2.29026:29064]()
    &replacement,
    [2.29026]
    [2.29064]
    replacement,
  • replacement in pijul-extension/src/file_system/changes/unrecorded.rs at line 92
    [2.29552][2.29552:29607]()
    value: SpanKind::Replacement {
    [2.29552]
    [2.29607]
    value: HunkOffset::Replacement {
  • replacement in pijul-extension/src/file_system/changes/unrecorded.rs at line 106
    [2.30104][2.30104:30137]()
    &change,
    [2.30104]
    [2.30137]
    change,
  • edit in pijul-extension/src/file_system/changes/unrecorded.rs at line 115
    [2.30391][2.30391:30464]()
    // Skip deletions as they don't contribute any spans
  • replacement in pijul-extension/src/file_system/changes/unrecorded.rs at line 119
    [2.30670][2.30670:30726]()
    value: SpanKind::Deletion {
    [2.30670]
    [2.30726]
    value: HunkOffset::Deletion {
  • replacement in pijul-extension/src/file_system/changes/unrecorded.rs at line 126
    [2.30962][2.30962:31018]()
    value: SpanKind::Addition {
    [2.30962]
    [2.31018]
    value: HunkOffset::Addition {
  • edit in pijul-extension/src/file_system/changes/unrecorded.rs at line 141
    [2.31539]
    [2.31539]
    hunks.push(globalized_hunk);
  • edit in pijul-extension/src/file_system/changes/unrecorded.rs at line 147
    [2.31634]
    [2.31634]
    change_contents: change_contents.clone(),
    hunks,
  • replacement in pijul-extension/src/file_system/changes/recorded.rs at line 33
    [2.33034][2.33034:33066]()
    spans: Vec<Span<ChangeId>>,
    [2.33034]
    [2.33066]
    spans: Vec<Span<Vertex<ChangeId>>>,
  • replacement in pijul-extension/src/file_system/changes/recorded.rs at line 41
    [2.33219][2.33219:33255]()
    pub spans: Vec<Span<ChangeId>>,
    [2.33219]
    [2.33255]
    pub spans: Vec<Span<Vertex<ChangeId>>>,
  • replacement in pijul-extension/src/file_system/changes/recorded.rs at line 89
    [2.34769][2.34769:34823]()
    value: CreditSource::Tracked(span.value),
    [2.34769]
    [2.34823]
    value: CreditSource::Tracked { vertex: span.value },
  • replacement in pijul-extension/src/file_system/changes/recorded.rs at line 134
    [2.36296][2.36296:36330]()
    value: vertex.change,
    [2.36296]
    [2.36330]
    value: vertex,
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 7
    [5.6372][2.36884:36930](),[2.36884][2.36884:36930]()
    use libpijul::{ArcTxn, ChangeId, ChannelRef};
    [5.6372]
    [2.36930]
    use libpijul::{ArcTxn, ChangeId, ChannelRef, Vertex};
  • edit in pijul-extension/src/file_system/changes/mod.rs at line 15
    [2.37161]
    [2.37161]
    // TODO: conflicts etc
    #[derive(Clone, Debug)]
    pub enum HunkDiff {
    TextChange {
    lines_added: Vec<String>,
    lines_removed: Vec<String>,
    },
    }
  • edit in pijul-extension/src/file_system/changes/mod.rs at line 26
    [2.37186]
    [2.37186]
    pub struct ActiveHunk {
    pub index: usize,
    pub diff: HunkDiff,
    }
    #[derive(Clone, Copy, Debug)]
    pub enum HunkOffset {
    Replacement {
    old_line_count: usize,
    new_line_count: usize,
    },
    Addition {
    lines_added: usize,
    },
    Deletion {
    lines_removed: usize,
    },
    }
    #[derive(Clone, Debug)]
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 89
    [2.38486][2.38486:38525]()
    #[derive(Clone, Debug, PartialEq, Eq)]
    [2.38486]
    [2.38525]
    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 91
    [2.38549][2.38549:38587]()
    Tracked(ChangeId),
    Untracked,
    [2.38549]
    [2.38587]
    Tracked { vertex: Vertex<ChangeId> },
    Untracked { hunk_index: usize },
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 145
    [2.40142][5.7344:7425]()
    let mut untracked_spans = self.unrecorded_state.spans.iter().peekable();
    [2.40142]
    [2.40219]
    let mut untracked_spans = self.unrecorded_state.spans.iter().enumerate().peekable();
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 149
    [2.40327][2.40327:40393]()
    while let Some(untracked_span) = untracked_spans.next() {
    [2.40327]
    [2.40393]
    while let Some((hunk_index, untracked_span)) = untracked_spans.next() {
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 160
    [2.40836][2.40836:40888]()
    unrecorded::SpanKind::Replacement {
    [2.40836]
    [2.40888]
    HunkOffset::Replacement {
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 165
    [2.41028][2.41028:41084]()
    value: CreditSource::Untracked,
    [2.41028]
    [2.41084]
    value: CreditSource::Untracked { hunk_index },
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 173
    [2.41414][2.41414:41482]()
    unrecorded::SpanKind::Addition { lines_added } => {
    [2.41414]
    [2.41482]
    HunkOffset::Addition { lines_added } => {
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 175
    [2.41527][2.41527:41583]()
    value: CreditSource::Untracked,
    [2.41527]
    [2.41583]
    value: CreditSource::Untracked { hunk_index },
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 181
    [2.41773][2.41773:41843]()
    unrecorded::SpanKind::Deletion { lines_removed } => {
    [2.41773]
    [2.41843]
    HunkOffset::Deletion { lines_removed } => {
  • replacement in pijul-extension/src/file_system/changes/mod.rs at line 190
    [2.42180][2.42180:42250]()
    if let Some(next_untracked_span) = untracked_spans.peek()
    [2.42180]
    [2.42250]
    if let Some((_next_hunk_index, next_untracked_span)) = untracked_spans.peek()
  • replacement in pijul-extension/src/channel_state.rs at line 4
    [2.45982][2.45982:46018]()
    use libpijul::change::ChangeHeader;
    [2.45982]
    [2.46018]
    use libpijul::change::{Atom, BaseHunk, Change, ChangeHeader, Local};
  • replacement in pijul-extension/src/channel_state.rs at line 8
    [2.46151][2.46151:46217]()
    use libpijul::{ArcTxn, ChangeId, ChannelRef, GraphTxnT, TxnTExt};
    [2.46151]
    [2.46217]
    use libpijul::{ArcTxn, ChangeId, ChannelRef, GraphTxnT, Hash, TxnTExt, Vertex};
  • replacement in pijul-extension/src/channel_state.rs at line 24
    [2.46701][2.46701:46747]()
    changes: HashMap<ChangeId, ChangeHeader>,
    [2.46701]
    [2.46747]
    // TODO: lazy access from ChangeStore?
    changes: HashMap<ChangeId, Change>,
  • replacement in pijul-extension/src/channel_state.rs at line 62
    [2.47964][2.47964:48010]()
    .get_header(&hash.into())
    [2.47964]
    [2.48010]
    .get_change(&hash.into())
  • edit in pijul-extension/src/channel_state.rs at line 65
    [2.48074][2.48074:48121]()
    dbg!(&change_header.authors);
  • replacement in pijul-extension/src/channel_state.rs at line 75
    [2.48336][2.48336:48421]()
    pub fn get_change_header(&self, change_id: &ChangeId) -> Option<&ChangeHeader> {
    [2.48336]
    [2.48421]
    pub fn find_vertex_hunk(
    &self,
    vertex: Vertex<ChangeId>,
    ) -> Option<(usize, &BaseHunk<Atom<Option<Hash>>, Local>)> {
    let change = self.changes.get(&vertex.change)?;
    change.changes.iter().enumerate().find_map(|(index, hunk)| {
    if hunk.iter().any(|atom| match atom {
    Atom::NewVertex(new_vertex) => {
    // Changes that depend on this one may later fragment the original contents,
    // so the current vertex may be a subset of the original contents.
    new_vertex.start <= vertex.start && new_vertex.end >= vertex.end
    }
    Atom::EdgeMap(_edge_map) => false,
    }) {
    // Hunks are 1-indexed
    Some((index + 1, hunk))
    } else {
    None
    }
    })
    }
    pub fn get_change(&self, change_id: &ChangeId) -> Option<&Change> {
  • replacement in pijul-extension/src/channel_state.rs at line 103
    [2.48543][2.48543:48573]()
    self.changes.values()
    [2.48543]
    [2.48573]
    self.changes.values().map(|change| &change.header)
  • edit in extensions/vscode/templates/hover/untracked.html at line 1
    [3.81]
    {{ hunk_template }}
  • replacement in extensions/vscode/templates/hover/tracked.html at line 20
    [3.493][3.493:538]()
    {% if let Some(description) = description %}
    [3.493]
    [3.538]
    <hr />
    {%- if let Some(description) = description %}
  • replacement in extensions/vscode/templates/hover/tracked.html at line 24
    [3.560][3.560:571]()
    {% endif %}
    [3.560]
    <hr />
    {% endif %}
    {{ hunk_template }}
  • file addition: diff.md (----------)
    [3.40]
    **{{ heading }}**
    ```diff
    {%- for removed_line in lines_removed %}
    - {{removed_line}}
    {%- endfor %}
    {%- for added_line in lines_added %}
    + {{added_line}}
    {%- endfor %}
    ```
  • replacement in extensions/vscode/src/inline_credit/mod.rs at line 91
    [3.3552][3.3552:3592]()
    &credit_span.value,
    [3.3552]
    [3.3592]
    open_file,
    credit_span.value,
  • replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 61
    [3.5336][3.5336:5559]()
    CreditSource::Tracked(change_id) => {
    let change_header = repository.get_change_header(change_id).ok_or_else(|| {
    napi::Error::from_reason(format!("unable to get change {change_id:#?}"))
    [3.5336]
    [3.5559]
    CreditSource::Tracked { vertex } => {
    let change = repository.get_change(&vertex.change).ok_or_else(|| {
    napi::Error::from_reason(format!("Unable to get change for verte {vertex:#?}"))
  • edit in extensions/vscode/src/inline_credit/line_annotation.rs at line 65
    [3.5576]
    [3.5576]
  • replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 67
    [3.5613][3.5613:5664]()
    .authors_for_change(change_header)
    [3.5613]
    [3.5664]
    .authors_for_change(&change.header)
  • replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 84
    [3.6406][3.6406:6507]()
    timestamp: change_header.timestamp,
    message: &change_header.message,
    [3.6406]
    [2.126772]
    timestamp: change.header.timestamp,
    message: &change.header.message,
  • replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 88
    [4.1178][4.1178:1215]()
    CreditSource::Untracked => {
    [4.1178]
    [4.1215]
    CreditSource::Untracked { .. } => {
  • replacement in extensions/vscode/src/inline_credit/hover.rs at line 12
    [3.7421][3.7421:7478]()
    use pijul_extension::file_system::changes::CreditSource;
    [3.7421]
    [3.7478]
    use pijul_extension::file_system::changes::{CreditSource, HunkDiff};
    use pijul_extension::file_system::open_file::OpenFile;
  • edit in extensions/vscode/src/inline_credit/hover.rs at line 21
    [3.7607]
    [3.7607]
    }
    #[localize("l10n/**/inline_credit/hover.ftl")]
    struct HunkHeading {
    index: usize,
    }
    #[derive(askama::Template)]
    #[template(path = "hover/diff.md", escape = "none")]
    struct HunkHoverTemplate {
    heading: String,
    lines_added: Vec<String>,
    lines_removed: Vec<String>,
  • edit in extensions/vscode/src/inline_credit/hover.rs at line 48
    [3.8052]
    [3.8052]
    hunk_template: HunkHoverTemplate,
  • replacement in extensions/vscode/src/inline_credit/hover.rs at line 53
    [3.8126][3.8126:8157]()
    struct UntrackedHoverTemplate;
    [3.8126]
    [3.8157]
    struct UntrackedHoverTemplate {
    hunk_template: HunkHoverTemplate,
    }
  • replacement in extensions/vscode/src/inline_credit/hover.rs at line 61
    [3.8292][3.8292:8326]()
    credit_source: &CreditSource,
    [3.8292]
    [3.8326]
    open_file: &OpenFile,
    credit_source: CreditSource,
  • edit in extensions/vscode/src/inline_credit/hover.rs at line 64
    [3.8387]
    [3.8387]
    let active_hunk = repository
    .get_active_hunk(open_file, credit_source)
    .map_err(|error| napi::Error::from_reason(format!("Unable to get active hunk: {error}")))?;
    let mut hunk_heading_buffer = String::new();
    let hunk_heading = HunkHeading {
    index: active_hunk.index,
    };
    hunk_heading.localize(localization_context, &mut hunk_heading_buffer);
    let hunk_template = match active_hunk.diff {
    HunkDiff::TextChange {
    lines_added,
    lines_removed,
    } => HunkHoverTemplate {
    heading: hunk_heading_buffer,
    lines_added,
    lines_removed,
    },
    };
  • replacement in extensions/vscode/src/inline_credit/hover.rs at line 86
    [3.8440][3.8440:8663]()
    CreditSource::Tracked(change_id) => {
    let change_header = repository.get_change_header(change_id).ok_or_else(|| {
    napi::Error::from_reason(format!("unable to get change {change_id:#?}"))
    [3.8440]
    [3.8663]
    CreditSource::Tracked { vertex } => {
    let change = repository.get_change(&vertex.change).ok_or_else(|| {
    napi::Error::from_reason(format!("Unable to get change for vertex: {vertex:#?}"))
  • replacement in extensions/vscode/src/inline_credit/hover.rs at line 94
    [3.8816][3.8816:8871]()
    .authors_for_change(change_header)
    [3.8816]
    [3.8871]
    .authors_for_change(&change.header)
  • replacement in extensions/vscode/src/inline_credit/hover.rs at line 111
    [3.9631][3.9631:9657]()
    change_header
    [3.9631]
    [3.9657]
    change
    .header
  • replacement in extensions/vscode/src/inline_credit/hover.rs at line 123
    [3.10148][3.10148:10236]()
    let zoned_timestamp = change_header.timestamp.to_zoned(TimeZone::system());
    [3.10148]
    [3.10236]
    let zoned_timestamp = change.header.timestamp.to_zoned(TimeZone::system());
  • replacement in extensions/vscode/src/inline_credit/hover.rs at line 141
    [3.11091][3.11091:11138]()
    title: &change_header.message,
    [3.11091]
    [3.11138]
    title: &change.header.message,
  • replacement in extensions/vscode/src/inline_credit/hover.rs at line 147
    [3.11312][3.11312:11429]()
    change_id: change_id.to_base32(),
    description: change_header.description.as_deref(),
    [3.11312]
    [3.11429]
    change_id: vertex.change.to_base32(),
    description: change.header.description.as_deref(),
    hunk_template,
  • replacement in extensions/vscode/src/inline_credit/hover.rs at line 154
    [3.11491][3.11491:11585]()
    CreditSource::Untracked => {
    let hover_template = UntrackedHoverTemplate;
    [3.11491]
    [3.11585]
    CreditSource::Untracked { .. } => {
    let hover_template = UntrackedHoverTemplate { hunk_template };
  • replacement in extensions/vscode/l10n/en-US/inline_credit/hover.ftl at line 2
    [3.12081][3.12081:12102]()
    change-id = Change ID
    [3.12081]
    change-id = Change ID
    hunk-heading = Hunk { $index }