Prevent reopening merge request when a branch is missing

What does this MR do and why?

Reopening a closed merge request whose source branch was deleted (and whose merge request head ref has since expired) recreated the diff from a nil source branch SHA. That produced an empty diff which replaced the stored one and became the latest version, silently hiding the commits and changes that were still visible while the merge request was closed.

Reopening is now rejected when the source or target branch no longer exists. Branch existence is validated as part of the reopen itself, via a flag-gated model validation (MergeRequest#validate_branch_existence_on_reopen, enabled while MergeRequests::ReopenService sets the reopening flag):

  • The reopen state transition's own save fails, so the merge request stays closed and no empty diff is created.
  • The error is durable — it is re-added on every valid?/save, so it survives the later save in IssuableBaseService#update (a combined reopen + attribute change now fails atomically instead of silently dropping the error and persisting the other change) and the REST API's revalidation.
  • It surfaces everywhere: the REST API returns 422, the GraphQL mergeRequestUpdate mutation returns the error, the web controller renders it, and the MR widget shows it in a toast ("Cannot reopen this merge request because the source or target branch no longer exists.").
  • The EE reopen side effects (approval deletion, policy resync, event publishing) only run when the merge request is actually reopened.
  • A deleted fork (nil source_project) is handled too — it is rejected with the error rather than reopening.

The behaviour is gated behind the default-off prevent_reopen_merge_request_without_branch feature flag, so there is no change until it is enabled.

References

Screenshots or screen recordings

Not applicable — no visual UI changes (the widget reuses its existing failure toast).

How to set up and validate locally

  1. Enable the feature flag in the Rails console:

    Feature.enable(:prevent_reopen_merge_request_without_branch)
  2. Create a merge request with commits, then close it.

  3. Delete the source branch of the closed merge request.

  4. Attempt to reopen it:

    • In the UI, click Reopen in the merge request widget — it stays closed and a toast shows "Cannot reopen this merge request because the source or target branch no longer exists."
    • Via REST (PUT /projects/278964/merge_requests/:iid with state_event=reopen) — returns 422 with that message.
    • Via the GraphQL mergeRequestUpdate mutation with state: OPEN — returns the same error.
  5. Confirm the merge request stays closed and its original diff/commits remain visible (no empty diff created). Disable the flag and confirm reopening behaves as before.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Patrick Bajao

Merge request reports

Loading