Draft notes API: updating a draft note without position wipes its diff position

🤖 AI-generated

Summary

If you call Update a draft note (PUT /projects/:id/merge_requests/:merge_request_iid/draft_notes/:draft_note_id) with only note, it deletes the diff position of a diff draft note. The draft becomes a general comment, so when the review is published it's posted as a general MR comment instead of on the diff line.

The docs list position as optional ("Position when creating a diff note"), so a caller that only wants to change the text expects the position to stay as it is.

Steps to reproduce

  1. On an MR with a diff, create a diff draft note:
    glab mr note create <iid> --draft --file src/extension.ts --line 9 -m "original"
    GET .../draft_notes/:id returns:
    "position": {
      "base_sha": "830e3185…", "start_sha": "a377257a…", "head_sha": "a758c5c8…",
      "position_type": "text", "old_path": "src/extension.ts", "new_path": "src/extension.ts",
      "new_line": 9, "line_range": null
    }
  2. Update only the body:
    glab api -X PUT projects/<project>/merge_requests/<iid>/draft_notes/<id> -f note="updated"
  3. The response (and every later GET) now has:
    "position": {
      "base_sha": null, "start_sha": null, "head_sha": null,
      "old_path": null, "new_path": null, "position_type": "text",
      "old_line": null, "new_line": null, "line_range": null
    }

I reproduced this on gitlab.com (2026-09-30) with a single-line draft and a line_range draft. It also hit real reviews: 4 diff drafts on gitlab-org/editor-extensions/gitlab-lsp!4253 (merged) lost their positions after their bodies were edited from a CLI.

Expected: leaving out position keeps the existing position. Only a position in the request should change it.

Root cause

lib/api/draft_notes.rb:226, added in f17012fd (2023-07):

draft_note.update!(note: params[:note], position: params[:position])

position is an optional param. When it's left out, params[:position] is nil, and that overwrites the stored position.

Proposed fix

Only update position when the request includes it:

attrs = { note: params[:note] }
attrs[:position] = params[:position] if params.key?(:position)
draft_note.update!(attrs)

Also add a request spec in spec/requests/api/draft_notes_spec.rb: create a diff draft, PUT with only note, and assert the position is unchanged.

Workaround for API clients

Re-send the existing position with the new body. GET the draft, then PUT with note plus its position (without null fields; line_range.start/end as {line_code, type}). I checked that this keeps both single-line and line_range positions.

Edited by 🤖 GitLab Bot 🤖