Mark as ready removes only one Draft: prefix when the title has several

Summary

When a merge request title contains the draft prefix more than once (for example Draft: Draft: Fix a thing), the Mark as ready action removes only the first prefix. The MR stays in draft state, and the user has to click Mark as ready again for each extra prefix.

Steps to reproduce

  1. Create a merge request with the title Draft: Draft: Fix a thing.
  2. Select Mark as ready.
  3. The title becomes Draft: Fix a thing and the MR is still a draft.

A doubled prefix is easy to create by accident: glab mr create --draft --title "Draft: My title" sends Draft: My title while also setting the draft flag, and the result is stored as Draft: Draft: My title. The API accepts a doubled prefix directly too.

Actual vs expected

Actual one prefix is stripped per action; the MR remains a draft
Expected Mark as ready clears the draft state in one action, whatever the prefix count

Root cause

MergeRequest::DRAFT_REGEX (app/models/merge_request.rb:935) looks like it already handles repeats, because of the +:

DRAFT_REGEX = /\A*#{Gitlab::Regex.merge_request_draft}+\s*/i

But Gitlab::Regex.merge_request_draft carries its own \A anchor, so the compiled pattern is:

/\A*(?-mix:\A(?i)(\[draft\]|\(draft\)|draft:))+\s*/i

The + applies to a group that starts with \A. A second repetition would have to match at the start of the string as well, which is impossible — so the + can never match more than once. MergeRequest.draftless_title is a single String#sub, so exactly one prefix is removed.

Mark as ready goes through app/services/merge_requests/base_service.rb:181:

when 'ready' then MergeRequest.draftless_title(title)

The resulting title still matches DRAFT_REGEX, so MergeRequest#draft? stays true.

The outer \A* is also degenerate — a quantified anchor, which is almost certainly not what was intended.

Verified behaviour

Run against master (bundle exec rails runner):

title after draftless_title still a draft?
Draft: Fix a thing Fix a thing no
Draft: Draft: Fix a thing Draft: Fix a thing yes
Draft:Draft: Fix a thing Draft: Fix a thing yes
Draft: Draft: Draft: Fix a thing Draft: Draft: Fix a thing yes
[Draft] Draft: Fix a thing Draft: Fix a thing yes

The no-space case (Draft:Draft:) is the interesting one: it shows the blocker is the \A inside the interpolated pattern, not the whitespace between prefixes.

Possible fix

Strip repeated prefixes rather than one. Either loop sub until the title no longer matches, or build the regex so the anchor sits outside the repeated group, roughly:

DRAFT_REGEX = /\A(?:#{Gitlab::Regex.merge_request_draft_without_anchor}\s*)+/i

Both draft? and draftless_title should then agree for any prefix count. Worth adding specs for the doubled and mixed-delimiter cases in the table above.

Low priority — cosmetic annoyance rather than data loss — but it is confusing, because the button appears not to work.

Edited by 🤖 GitLab Bot 🤖