CODE VERIFIED

Gitea 28.2.0 Stops Manual-Merge Detection from Closing Empty Pull Requests

Gitea 28.2.0 prevents manual-merge autodetection from closing empty pull requests. See the equality guard, integration coverage, and remaining trade-off.

Oct 28, 2026, 5:10 pm PDT · 6 min read
Git history diagram with the PR head and merge base pointing to the same commit while an empty PR remains open.

Quick Answer

Gitea 28.2.0, released on October 18, 2026, fixes a case in which manual-merge autodetection could mark an empty pull request as merged and close it. The behavior depended on the repository having manual-merge autodetection enabled. The release notes list the fix through upstream PR #39807 and its release-branch backport, #39812.

The code change adds a specific boundary: when the pull request’s head commit equals its merge base, the detection routine returns a not-found result. It no longer continues down a path that could use that head commit as evidence of a fast-forward merge. This is a fix to how Gitea interprets repository history when updating PR status, rather than a new merge operation.

In Plain English

A pull request can exist even when its branch has no commits of its own to contribute. The reported example was an empty PR opened for discussion. Its head commit can already belong to the target branch’s history without anyone having merged that PR. The upstream PR describes how autodetection could nevertheless treat this situation as a completed manual merge.

The distinction matters because a branch relationship and a PR workflow event answer different questions. In this detection path, the relevant check asks whether the PR head is an ancestor of the base branch. Git documents that relationship in git-merge-base. The editorial interpretation here is that reachability alone cannot establish why the relationship exists, or whether someone intended to finish a particular PR.

For a branch that really was merged manually, Gitea needs to recognize work done outside its normal merge interface. For an empty discussion PR, the same broad history relationship can be present before there is any work to merge. The fix gives the latter case an explicit negative result at the point where the routine would otherwise look for a merge commit.

Code Evidence

Where the earlier fallback became ambiguous

The release-branch detection code first gates this work on the repository’s manual-merge autodetection setting. When that setting is disabled, the caller returns without attempting detection. Within detection, the code checks whether the PR head is an ancestor of the base branch.

If the head is an ancestor, the routine searches the base branch’s ancestry path for merge commits. When it finds no merge commit, the existing fast-forward fallback uses the PR head itself as the merge commit. That fallback explains the false positive: an empty PR could supply a head already present in the base history, while offering no distinct merge event for the routine to find.

The added equality guard

The following is the exact added guard from the release-branch patch. It runs after the PR head commit ID has been resolved and before the subsequent repository-opening step:

if prHeadCommitID == pr.MergeBase {
	return nil, util.ErrNotExist // empty PR, no merge commit is found
}

The observed code fact is narrow: equality between the head commit ID and the stored merge base causes an immediate return of the not-found sentinel. The routine therefore does not proceed to identify a merge commit for that case. The editorial implication is that an already-shared starting point should not become positive evidence that the PR was completed.

Code-flow comparison showing an equal PR head and merge base reaching the old fast-forward fallback, then exiting with a not-found result after the guard is added.
The added equality check stops this case before merge-commit detection can use the PR head as its fast-forward fallback.

A negative result has an explicit meaning

The patch also changes the not-an-ancestor result. That path previously returned a nil commit and a nil error; it now returns the same not-found sentinel. As the caller shows, the sentinel means a normal negative detection result. Other errors are logged and also produce a false result.

This separates an expected absence of merge evidence from an operational failure while keeping both from being reported as successful detection. That is an interpretation of the control flow, not a claim that the patch changes every error-handling path in Gitea. The reviewed change concerns this manual-merge detection service and its integration coverage.

Why It Matters

For repositories that enable the option, a false positive changes the meaning of an open conversation: Gitea can mark a PR merged even though there was no contribution to merge. The original issue was filed on August 30, 2024, against Gitea 1.22.1 and was closed by the later fix. That report establishes the reported failure; it does not establish which other versions or installations experienced it.

The engineering lesson is about the strength of evidence used to change application state. Commit ancestry is useful evidence for locating work in a repository, but an application may attach a stronger meaning to the result: this specific PR was merged. In this case, the extra equality check prevents one ambiguous graph relationship from making that stronger claim.

The integration coverage also matters because simply declining every detection would avoid this false closure while losing useful behavior. The test scenario keeps both expectations in view: changed PRs should still be detected as manually merged, and the empty PR should remain unmerged. The supported conclusion is limited to that tested scenario, rather than a guarantee covering every possible history.

An Open Question

Should Gitea persist the relevant pre-merge state when a PR is created so it can distinguish an empty discussion PR from a fast-forward manual merge after the merge base has been refreshed?

The backport discussion documents a remaining false-negative case. If a real fast-forward manual merge is rechecked after its merge base has already been refreshed to the PR head, autodetection may miss it. The equality that protects an empty PR can then also describe a PR whose work really was merged.

The maintainers explain that distinguishing these ambiguous cases would require storing a value at PR creation. The current trade-off favors avoiding a wrong closure. Resolving the open question would require a decision about preserving that earlier state and evidence that detection can then distinguish both cases. This patch does not add the database field or new user-facing setting that such a design might involve.

Scope and Limitations

The integration test enables manual-merge autodetection, creates two PRs with changes and one empty PR, merges the changed branches locally, and pushes them together. It checks that the changed PRs are marked manually merged with their expected merge commits. It also checks that the empty PR remains unmerged with ancestor status. These are assertions visible in the linked upstream test, not results from an independent execution for this article.

The main PR merged on October 16, 2026, and reports 24 checks passed. The release/v28 backport merged that day and reports 31 checks passed. Those counts are GitHub-reported PR checks. No tests or runtime checks were independently performed for this article.

Release availability is supported by the official v28.2.0 release page dated October 18, 2026. It does not establish that any particular running server has installed the release. The fix also should not be read as eliminating the documented refreshed-merge-base false negative.

The verified release-branch comparison changes two files: the pull-request detection service and its integration test. Its exact base is c4e9d96162dca70ef4517249d7d5a20b09f506ac, and its target is 95d6b572fe87193a47f019371b370c1894ee8fc5, as recorded by the official commit verification. These references identify the reviewed comparison; this article does not claim an independent repository checkout or runtime test.

Sources