NoMethodError: undefined method `lines' for nil in LinesUnfolder causes 500 on discussions.json

Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.

Summary

When loading the discussions for a merge request, a 500 Internal Server Error is returned if any DiffNote on the MR references a file path that no longer resolves to a valid blob at the recorded base SHA. The crash is a code-path failure, not an infrastructure problem — the request itself is fast and Gitaly/DB/Redis are all healthy.

Steps to reproduce

  1. Create a merge request with diff notes on a file
  2. The file is renamed or moved such that the old_path stored in the note's position no longer exists at the recorded base_sha
  3. Load GET /:namespace/:project/-/merge_requests/:iid/discussions.json
  4. Observe 500 error

Expected behavior

The discussions endpoint should handle stale/unreachable diff note positions gracefully. Rather than crashing or silently dropping context, notes whose blob is no longer reachable should be marked as outdated and the UI should surface a message such as "This comment was left on a file that no longer exists at this path" — consistent with how GitLab already handles outdated diff notes on force-pushed branches.

Actual behavior

A NoMethodError is raised and the entire discussions endpoint returns 500, making the MR page unable to load any discussions — not just the stale one.

NoMethodError: undefined method `lines' for nil
  lib/gitlab/diff/lines_unfolder.rb:62:in `block in unfold_required?'
  lib/gitlab/diff/lines_unfolder.rb:55:in `unfold_required?'
  lib/gitlab/diff/lines_unfolder.rb:158:in `calculate_from_blob_line!'
  lib/gitlab/diff/lines_unfolder.rb:21:in `initialize'
  lib/gitlab/diff/file.rb:195:in `unfold_diff_lines'
  lib/gitlab/diff/position.rb:134:in `diff_file'
  app/models/diff_note.rb:189:in `fetch_diff_file'
  app/models/diff_note.rb:100:in `diff_file'
  app/models/diff_note.rb:110:in `diff_line'
  app/models/concerns/discussion_on_diff.rb:12:in `diff_line'
  app/models/concerns/discussion_on_diff.rb:43:in `truncated_diff_lines'
  app/serializers/discussion_serializer.rb:7:in `represent'
  app/controllers/concerns/issuable_actions.rb:163:in `discussions'

Root cause

LinesUnfolder#initialize sets @blob from diff_file.old_blob. When the blob no longer exists at the recorded base_sha + old_path — most likely because the file was renamed or moved after the note was created — old_blob resolves to nil via the lazy loader.

The commit itself remains reachable, which rules out force-push or GC as the cause. The file simply no longer exists at the path recorded in the note's position.

unfold_required? then calls @blob.lines at line 62 without first guarding against a nil blob, raising NoMethodError. Because this happens during serialization of a single note, the entire discussions.json response fails — all discussions on the MR become inaccessible.

Proposed fix

This warrants a two-layer fix:

Layer 1 — stop the crash (lib/gitlab/diff/lines_unfolder.rb)

Add a nil guard in unfold_required? before the .lines call:

next false if @blob.nil?  # guard against stale/unreachable blob
next false if @position.old_line > @blob.lines.size

Layer 2 — surface the staleness gracefully (app/models/diff_note.rb or app/models/concerns/discussion_on_diff.rb)

Silently skipping the unfold is not sufficient on its own. When diff_file or truncated_diff_lines detects an unreachable blob, the note's position should be treated as outdated. GitLab already has the infrastructure for this (DiffNote#active?, position vs original_position, and existing UI for outdated diff notes on force-pushed branches). The UI should show something like "This comment was left on a file that no longer exists at this path" rather than silently omitting the diff context.

Silently ignoring the error risks confusing reviewers who may not realise their diff context is missing.

Environment

  • GitLab 19.0 (self-managed)
  • Reproducible when a MR has diff notes on a file that has since been renamed or moved
Edited by 🤖 GitLab Bot 🤖