feat(mr): support Forgejo/Codeberg pull request URLs (#1267)

* feat(mr): support Forgejo/Codeberg pull request URLs

Codeberg and other Forgejo instances expose pull requests under
refs/pull/<id>/head instead of GitLab's refs/merge-requests/<id>/head,
so passing a Codeberg pull request URL to git mr fetched the wrong
ref. Recognize the /pulls/<id> URL shape and switch to the matching
ref for that case, leaving the GitLab URL and bare numeric id
behavior unchanged.

Closes #1213

* test(git-mr): cover Forgejo/Codeberg URLs, regenerate man docs

Add a bats test that exercises the pulls URL branch via a local
insteadOf remote rewrite (no real HTTP endpoint needed), and bring
git-mr.1/git-mr.html in line with the git-mr.md changes.

---------

Co-authored-by: vjymisal0 <vijay.looprai@gmail.com>
This commit is contained in:
Vijay Misal 2026-08-27 11:29:00 +05:30 committed by GitHub
parent dbf68bac59
commit 4c38988ba9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 126 additions and 6 deletions

View file

@ -1428,8 +1428,8 @@ $ git reset-file .htaccess dc82b19
## git mr ## git mr
Checks out a merge request from GitLab. Usage: `git mr <ID|URL> [REMOTE]`. Checks out a merge request from GitLab, or a pull request from Forgejo/Codeberg.
Default remote is `origin`. Usage: `git mr <ID|URL> [REMOTE]`. Default remote is `origin`.
``` bash ``` bash
$ git mr 51 $ git mr 51
@ -1447,6 +1447,15 @@ From gitlab.com:owner/repository
Switched to branch 'mr/51' Switched to branch 'mr/51'
``` ```
A Forgejo/Codeberg pull request URL is also supported:
``` bash
$ git mr https://codeberg.org/owner/repository/pulls/51
From codeberg.org:owner/repository
* [new ref] refs/pull/51/head -> mr/51
Switched to branch 'mr/51'
```
Just like [git pr](#git-pr), `git mr` accepts a `clean` argument to trash all Just like [git pr](#git-pr), `git mr` accepts a `clean` argument to trash all
`mr/` branches. Ensure current branch is not one. `mr/` branches. Ensure current branch is not one.

View file

@ -7,6 +7,8 @@ if [ -z "${1-}" ] ; then
exit 1 exit 1
fi fi
remote_ref_kind=merge-requests
if test "$1" = "clean"; then if test "$1" = "clean"; then
git for-each-ref refs/heads/mr/* --format='%(refname)' | while read -r ref; do git for-each-ref refs/heads/mr/* --format='%(refname)' | while read -r ref; do
git branch -D "${ref#refs/heads/}" git branch -D "${ref#refs/heads/}"
@ -15,13 +17,19 @@ if test "$1" = "clean"; then
elif [[ $1 =~ ^(https?://[^/]+/(.+))/merge_requests/([0-9]+).*$ ]]; then elif [[ $1 =~ ^(https?://[^/]+/(.+))/merge_requests/([0-9]+).*$ ]]; then
remote=${BASH_REMATCH[1]}.git remote=${BASH_REMATCH[1]}.git
id=${BASH_REMATCH[3]} id=${BASH_REMATCH[3]}
elif [[ $1 =~ ^(https?://[^/]+/(.+))/pulls/([0-9]+).*$ ]]; then
# Forgejo/Codeberg pull request URL, e.g.
# https://codeberg.org/owner/repository/pulls/453
remote=${BASH_REMATCH[1]}.git
id=${BASH_REMATCH[3]}
remote_ref_kind=pull
else else
id=$1 id=$1
remote=${2:-origin} remote=${2:-origin}
fi fi
branch=mr/$id branch=mr/$id
remote_ref=refs/merge-requests/$id/head remote_ref=refs/$remote_ref_kind/$id/head
git fetch -fu "$remote" "$remote_ref:$branch" git fetch -fu "$remote" "$remote_ref:$branch"
git checkout "$branch" git checkout "$branch"
git config --local --replace "branch.$branch.merge" "$remote_ref" git config --local --replace "branch.$branch.merge" "$remote_ref"

View file

@ -28,7 +28,7 @@ The name of the remote to fetch from\. Defaults to \fBorigin\fR\.
<url> <url>
. .
.P .P
GitLab merge request URL in the format \fBhttps://gitlab\.tld/owner/repository/merge_requests/453\fR\. GitLab merge request URL in the format \fBhttps://gitlab\.tld/owner/repository/merge_requests/453\fR, or a Forgejo/Codeberg pull request URL in the format \fBhttps://codeberg\.tld/owner/repository/pulls/453\fR\.
. .
.SH "EXAMPLES" .SH "EXAMPLES"
This checks out merge request \fB!51\fR from remote \fBorigin\fR to branch \fBmr/51\fR\. This checks out merge request \fB!51\fR from remote \fBorigin\fR to branch \fBmr/51\fR\.
@ -46,6 +46,22 @@ Switched to branch \'mr/51\'
. .
.IP "" 0 .IP "" 0
. .
.P
This checks out pull request \fB#51\fR from a Forgejo/Codeberg URL to branch \fBmr/51\fR\.
.
.IP "" 4
.
.nf
$ git mr https://codeberg\.org/owner/repository/pulls/51
From codeberg\.org:owner/repository
* [new ref] refs/pull/51/head \-> mr/51
Switched to branch \'mr/51\'
.
.fi
.
.IP "" 0
.
.SH "AUTHOR" .SH "AUTHOR"
Written by Étienne BERSAC \fIbersace03@gmail\.com\fR from git\-pr(1)\. Written by Étienne BERSAC \fIbersace03@gmail\.com\fR from git\-pr(1)\.
. .

View file

@ -94,7 +94,9 @@
<p> &lt;url&gt;</p> <p> &lt;url&gt;</p>
<p> GitLab merge request URL in the format <p> GitLab merge request URL in the format
<code>https://gitlab.tld/owner/repository/merge_requests/453</code>.</p> <code>https://gitlab.tld/owner/repository/merge_requests/453</code>, or a
Forgejo/Codeberg pull request URL in the format
<code>https://codeberg.tld/owner/repository/pulls/453</code>.</p>
<h2 id="EXAMPLES">EXAMPLES</h2> <h2 id="EXAMPLES">EXAMPLES</h2>
@ -106,6 +108,14 @@ From gitlab.com:owner/repository
Switched to branch 'mr/51' Switched to branch 'mr/51'
</code></pre> </code></pre>
<p>This checks out pull request <code>#51</code> from a Forgejo/Codeberg URL to branch <code>mr/51</code>.</p>
<pre><code>$ git mr https://codeberg.org/owner/repository/pulls/51
From codeberg.org:owner/repository
* [new ref] refs/pull/51/head -&gt; mr/51
Switched to branch 'mr/51'
</code></pre>
<h2 id="AUTHOR">AUTHOR</h2> <h2 id="AUTHOR">AUTHOR</h2>
<p>Written by Étienne BERSAC <a href="&#109;&#x61;&#x69;&#x6c;&#116;&#x6f;&#58;&#x62;&#101;&#x72;&#115;&#97;&#x63;&#101;&#48;&#x33;&#64;&#103;&#x6d;&#x61;&#x69;&#108;&#x2e;&#99;&#x6f;&#109;" data-bare-link="true">&#x62;&#101;&#114;&#115;&#x61;&#x63;&#101;&#48;&#51;&#64;&#x67;&#109;&#97;&#x69;&#x6c;&#46;&#x63;&#111;&#109;</a> from <a class="man-ref" href="git-pr.html">git-pr<span class="s">(1)</span></a>.</p> <p>Written by Étienne BERSAC <a href="&#109;&#x61;&#x69;&#x6c;&#116;&#x6f;&#58;&#x62;&#101;&#x72;&#115;&#97;&#x63;&#101;&#48;&#x33;&#64;&#103;&#x6d;&#x61;&#x69;&#108;&#x2e;&#99;&#x6f;&#109;" data-bare-link="true">&#x62;&#101;&#114;&#115;&#x61;&#x63;&#101;&#48;&#51;&#64;&#x67;&#109;&#97;&#x69;&#x6c;&#46;&#x63;&#111;&#109;</a> from <a class="man-ref" href="git-pr.html">git-pr<span class="s">(1)</span></a>.</p>

View file

@ -21,7 +21,9 @@ git-mr(1) -- Checks out a merge request locally
&lt;url&gt; &lt;url&gt;
GitLab merge request URL in the format GitLab merge request URL in the format
`https://gitlab.tld/owner/repository/merge_requests/453`. `https://gitlab.tld/owner/repository/merge_requests/453`, or a
Forgejo/Codeberg pull request URL in the format
`https://codeberg.tld/owner/repository/pulls/453`.
## EXAMPLES ## EXAMPLES
@ -33,6 +35,13 @@ This checks out merge request `!51` from remote `origin` to branch `mr/51`.
* [new ref] refs/merge-requests/51/head -> mr/51 * [new ref] refs/merge-requests/51/head -> mr/51
Switched to branch 'mr/51' Switched to branch 'mr/51'
This checks out pull request `#51` from a Forgejo/Codeberg URL to branch `mr/51`.
$ git mr https://codeberg.org/owner/repository/pulls/51
From codeberg.org:owner/repository
* [new ref] refs/pull/51/head -> mr/51
Switched to branch 'mr/51'
## AUTHOR ## AUTHOR
Written by Étienne BERSAC <bersace03@gmail.com> from git-pr(1). Written by Étienne BERSAC <bersace03@gmail.com> from git-pr(1).

68
tests/git-mr.bats Normal file
View file

@ -0,0 +1,68 @@
# shellcheck shell=bash
source "$BATS_TEST_DIRNAME/test_util.sh"
setup_file() {
test_util.setup_file
PATH="$BATS_TEST_DIRNAME/bin:$PATH"
}
setup() {
test_util.cd_test
test_util.git_init
touch ./tracked
git add ./tracked
git commit -m 'Initial commit'
# Local bare "remote" standing in for a real GitLab/Forgejo instance,
# with a merge-requests ref and a pull ref pointing at the same commit.
remote_dir="$BATS_TEST_TMPDIR/remote.git"
git init --bare --initial-branch main "$remote_dir"
git push "$remote_dir" HEAD:refs/merge-requests/51/head
git push "$remote_dir" HEAD:refs/pull/51/head
git remote add origin "$remote_dir"
}
@test "checks out a merge request by numeric id" {
run git mr 51
assert_success
assert_output --partial "refs/merge-requests/51/head"
run git rev-parse --abbrev-ref HEAD
assert_output 'mr/51'
run git config --get branch.mr/51.merge
assert_output 'refs/merge-requests/51/head'
}
@test "checks out a merge request by numeric id and explicit remote" {
run git mr 51 "$remote_dir"
assert_success
assert_output --partial "refs/merge-requests/51/head"
run git rev-parse --abbrev-ref HEAD
assert_output 'mr/51'
}
@test "checks out a Forgejo/Codeberg pull request by URL" {
# git-mr only recognizes http(s) URLs, so rewrite the fake Codeberg URL
# to our local bare "remote" via insteadOf rather than needing a real
# HTTP(S) endpoint.
git config --local url."$remote_dir".insteadOf "https://codeberg.org/owner/repository.git"
run git mr "https://codeberg.org/owner/repository/pulls/51"
assert_success
assert_output --partial "refs/pull/51/head"
run git rev-parse --abbrev-ref HEAD
assert_output 'mr/51'
run git config --get branch.mr/51.merge
assert_output 'refs/pull/51/head'
run git config --get branch.mr/51.remote
assert_output 'https://codeberg.org/owner/repository.git'
}