Investigate proper handling of nil positions in UpdateDiffPositionService
<!--IssueSummary start--> <details> <summary> Everyone can contribute. [Help move this issue forward](https://handbook.gitlab.com/handbook/marketing/developer-relations/contributor-success/community-contributors-workflows/#contributor-links) while earning points, leveling up and collecting rewards. </summary> - [Close this issue](https://contributors.gitlab.com/manage-issue?action=close&projectId=278964&issueIid=560134) </details> <!--IssueSummary end--> ## Problem The `UpdateDiffPositionService` currently crashes when the position tracer returns a nil position for outdated diff discussions. While MR !200242 fixes the immediate crash with a defensive nil check, this reveals a deeper architectural issue that needs investigation. ## Current Behavior When `Gitlab::Diff::PositionTracer#trace` returns `{ position: nil, outdated: true }`, the service attempts to: 1. Update discussion notes with `note.change_position = position` (nil) 2. Call `position.added?` on the nil position, causing a crash 3. Create system notes for outdated discussions ## Root Cause Analysis The issue stems from conflicting expectations: 1. **Position Tracer** legitimately returns nil when lines are no longer in the MR: ```ruby # If the line is no longer in the MR, we unfortunately cannot show # the current state on the CD diff or any change on the BD diff, # so we treat it as outdated. { position: nil, outdated: true } ``` 2. **DiffNote Validation** requires complete positions when original_position is nil: ```ruby def positions_complete return if self.original_position.complete? && self.position.complete? errors.add(:position, "is incomplete") end ``` 3. **Position Processing** assumes position is never nil: ```ruby if position.added? trace_added_line(position) elsif position.removed? trace_removed_line(position) else # unchanged trace_unchanged_line(position) end ``` ## Investigation Areas 1. **Position Tracer Contract**: Should the tracer return nil positions, or should it return a special "untraceable" position object? 2. **DiffNote Validation**: Should validation be updated to handle nil change_position when the discussion is outdated? 3. **System Notes**: Should `SystemNotes::MergeRequestService#diff_discussion_outdated` be updated to handle nil positions gracefully? 4. **Service Architecture**: Should the UpdateDiffPositionService have different code paths for traceable vs untraceable positions? ## Proposed Solutions to Evaluate 1. **Update Position Tracer**: Return a special position object instead of nil 2. **Update DiffNote Model**: Allow nil change_position for outdated discussions 3. **Update System Notes**: Handle nil positions in outdated discussion notes 4. **Service Refactoring**: Separate handling of traceable vs untraceable positions ## Acceptance Criteria - [ ] Determine the correct architectural approach for handling untraceable positions - [ ] Implement a solution that doesn't require defensive nil checks - [ ] Ensure consistent behavior across all position-related services - [ ] Add comprehensive tests for edge cases - [ ] Document the expected behavior for future developers ## Related - MR !200242 - Temporary fix for the crash - Error logs: https://log.gprd.gitlab.net/app/r/s/bLbsG ## Labels ~"backend" ~"devops::create" ~"group::code review" ~"type::maintenance" ~"maintenance::usability"
issue