Show enclosing symbol context in code diffs #3090
Service Desk opened 8 hours ago

Originated from and supersedes issue #3089 after the desired UX was clarified with @zaakiy, @servicedesk, and @robin.

Motivation

A diff currently shows changed and surrounding lines, but it does not identify the function, method, or class containing those lines. In a large file, readers must expand or scroll through unchanged code to discover where a change belongs. This increases review time and makes similar-looking changes harder to distinguish.

For example, this pseudocode contains two nearly identical changes:

class AccountService {
    function validateNewAccount(account) {
        check(account.email)
    }

    function validatePasswordReset(account) {
        check(account.email)
    }
}

Without enclosing-symbol context, a diff fragment such as check(account.email) does not tell the reviewer which function changed. A context label makes that distinction immediately visible.

Current UX

┌──────────────────────────────────────────────────────┐
│ @@ -42,3 +42,3 @@                                    │
│ - check(account.email)                               │
│ + check(account.primaryEmail)                        │
│                                                      │
│ No function or class context is shown.               │
└──────────────────────────────────────────────────────┘

Proposed UX

Provide both per-hunk context and sticky context. Old and new revisions must have independent context because a changed line can move between different symbols.

Split view:

┌───────────────────────────┬───────────────────────────┐
│ OLD — sticky              │ NEW — sticky              │
│ AccountService >          │ AccountService >          │
│ validateNewAccount        │ validateImportedAccount   │
├───────────────────────────┼───────────────────────────┤
│ @@ old context:           │ @@ new context:           │
│ validateNewAccount        │ validateImportedAccount   │
│ - check(account.email)    │ + check(account.email)    │
└───────────────────────────┴───────────────────────────┘

Unified view:

┌──────────────────────────────────────────────────────┐
│ OLD — AccountService > validateNewAccount            │
│ NEW — AccountService > validateImportedAccount       │
├──────────────────────────────────────────────────────┤
│ @@ old: validateNewAccount                           │
│ @@ new: validateImportedAccount                      │
│ - check(account.email)                               │
│ + check(account.email)                               │
└──────────────────────────────────────────────────────┘

Expected behavior

  • Show the nearest enclosing function, method, class, or equivalent symbol in every diff hunk header.
  • When old and new contexts differ, identify both contexts separately.
  • In split view, keep independent sticky context elements above the old and new columns; each updates from its own revision while scrolling.
  • In unified view, show two compact, clearly labeled sticky elements for old and new contexts; each updates independently.
  • Keep the labels useful when only one side has an applicable visible line or symbol.
  • Enable this behavior by default; there is currently no setting providing it.

This issue captures the finalized product direction. Language-specific symbol extraction and edge-case fallback behavior can be decided during implementation.

  • Service Desk referenced from other issue 8 hours ago
  • Service Desk commented 8 hours ago

    @zaakiy, this issue is the canonical follow-up to #3089 and contains the finalized design, motivation, ASCII UX diagrams, and pseudocode example. It is assigned to @robin as requested.

  • Service Desk referenced from other issue 8 hours ago
  • Zak Siddiqui commented 8 hours ago

    @servicedesk it would be worth providing additional guidance with respect to whatever underlying Git engine/pager you are using and its capability to display this context per hunk.

  • Service Desk commented 8 hours ago

    @zaakiy, the relevant implementation detail is that OneDev code-review diffs do not go through a Git pager or consume Git-produced patch hunks. JGit 6.10 is used to identify changed files, but the web diff then loads the old and new blobs, computes line blocks with OneDev DiffUtils/diff-match-patch, and renders them in BlobTextDiffPanel.

    Native Git can append one TEXT value to a hunk header via .gitattributes and diff.<driver>.xfuncname, and --function-context uses the same mechanism. That is useful for command-line patches, but it is not sufficient for this design: a patch hunk has only one context field, while this issue requires independent old/new contexts, nested symbol names, and sticky updates. JGit DiffFormatter.writeHunkHeader likewise writes ranges only and is not the source of the review UI hunks.

    The best fit is therefore to implement this above the diff engine using OneDev existing commons-jsymbol infrastructure. CodeSearchService.getSymbols(...) can retrieve symbols separately for the old blob ID/path and new blob ID/path; each symbol exposes getScope(), getParent(), and getFQN(). For each rendered hunk, OneDev can resolve the narrowest scope containing that side hunk anchor, walk its parent chain for the label, and attach the old/new labels to the rendered hunk metadata. The browser can then update the two sticky labels from that metadata while scrolling. Symbol lookup should be cached per blob during rendering, and a side should remain unlabeled when its language has no extractor or no containing scope.

    References: Git custom hunk headers and JGit DiffFormatter API. This captures the engine constraints and the recommended integration point for implementation.

  • Zak Siddiqui commented 8 hours ago

    I'm wondering how much extra processing needs to happen on the OneDev server. Since OneDev is iterating through the symbols, it's almost as if it might be quite computationally intensive. What are your thoughts on how computationally intensive this would be?

    The goal here is to keep OneDev highly performant. But also give the user the context they need. What would this mean for a file with a very large number of changes, in terms of server performance impact? One might surmise that the intention of the way that OneDev uses Git is to keep everything lightweight, and what we are doing here seems to negate that design intention.

    At the same time, I really want this feature!!!

  • Service Desk commented 8 hours ago

    @zaakiy, that performance concern is valid. I checked the current symbol-loading and diff-display paths, and the important distinction is that CodeSearchService.getSymbols(...) reads a pre-extracted symbol list from the Lucene code index, keyed by blob hash and path. It does not need to parse the source again while rendering the diff. The incremental server work is therefore an index lookup, deserialization, and mapping hunk anchors to symbol ranges.

    I would tighten the implementation around these guardrails:

    • Fetch symbols only for textual diffs that are actually displayed and have a registered extractor.
    • Fetch at most once per unique old/new blob in the request. Do not call getSymbols per hunk. If profiling shows cluster round trips matter, batch the blob-side lookups.
    • Build a sorted interval/index structure once per blob, then resolve each hunk by binary search or a linear sweep. That makes mapping roughly O(S log S + H log S) (or O(S + H) with a sweep), not O(S * H), where S is symbols and H is hunks.
    • Use indexed symbols only for this feature; do not synchronously fall back to extracting symbols during a diff request. A missing/stale index, excessive symbol count, or exceeded time budget should simply mean no context label for that side.
    • Add timing/serialized-size metrics and benchmark cold and warm requests against synthetic symbol-heavy files before enabling it by default.

    The current diff UI already bounds the workload: at most 200 changed files are considered, a single displayed diff is capped at 2,000 changed lines, total displayed changed lines at 5,000, and textual diffs above 65,535 combined old/new lines are rejected. Those limits prevent change volume from growing without bound. They do not directly cap symbol count, so generated or highly fragmented files remain the case to benchmark and protect with a symbol/time budget.

    With those constraints, I would expect the symbol-context cost to be modest compared with blob loading, line diffing, and rendering. The expensive version would be reparsing each revision on demand or scanning every symbol for every hunk; neither is necessary for this design.

1/1
Type
New Feature
Priority
Normal
Assignee
Labels
No labels
Issue Votes (0)
Watchers (3)
Reference
OD-3090
Please wait...
Connection lost or session expired, reload to recover
Page is in error, reload to recover