From 4c38988ba9c9b174477ef34affa1538cbc4b619a Mon Sep 17 00:00:00 2001
From: Vijay Misal
Date: Thu, 27 Aug 2026 11:29:00 +0530
Subject: [PATCH] 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//head instead of GitLab's refs/merge-requests//head,
so passing a Codeberg pull request URL to git mr fetched the wrong
ref. Recognize the /pulls/ 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
---
Commands.md | 13 +++++++--
bin/git-mr | 10 ++++++-
man/git-mr.1 | 18 ++++++++++++-
man/git-mr.html | 12 ++++++++-
man/git-mr.md | 11 +++++++-
tests/git-mr.bats | 68 +++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 126 insertions(+), 6 deletions(-)
create mode 100644 tests/git-mr.bats
diff --git a/Commands.md b/Commands.md
index 0c6ddbf..4cc2eee 100644
--- a/Commands.md
+++ b/Commands.md
@@ -1428,8 +1428,8 @@ $ git reset-file .htaccess dc82b19
## git mr
-Checks out a merge request from GitLab. Usage: `git mr [REMOTE]`.
-Default remote is `origin`.
+Checks out a merge request from GitLab, or a pull request from Forgejo/Codeberg.
+Usage: `git mr [REMOTE]`. Default remote is `origin`.
``` bash
$ git mr 51
@@ -1447,6 +1447,15 @@ From gitlab.com:owner/repository
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
`mr/` branches. Ensure current branch is not one.
diff --git a/bin/git-mr b/bin/git-mr
index 9a359d3..5cac8df 100755
--- a/bin/git-mr
+++ b/bin/git-mr
@@ -7,6 +7,8 @@ if [ -z "${1-}" ] ; then
exit 1
fi
+remote_ref_kind=merge-requests
+
if test "$1" = "clean"; then
git for-each-ref refs/heads/mr/* --format='%(refname)' | while read -r ref; do
git branch -D "${ref#refs/heads/}"
@@ -15,13 +17,19 @@ if test "$1" = "clean"; then
elif [[ $1 =~ ^(https?://[^/]+/(.+))/merge_requests/([0-9]+).*$ ]]; then
remote=${BASH_REMATCH[1]}.git
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
id=$1
remote=${2:-origin}
fi
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 checkout "$branch"
git config --local --replace "branch.$branch.merge" "$remote_ref"
diff --git a/man/git-mr.1 b/man/git-mr.1
index d5483b4..65af4b8 100644
--- a/man/git-mr.1
+++ b/man/git-mr.1
@@ -28,7 +28,7 @@ The name of the remote to fetch from\. Defaults to \fBorigin\fR\.
.
.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"
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
.
+.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"
Written by Étienne BERSAC \fIbersace03@gmail\.com\fR from git\-pr(1)\.
.
diff --git a/man/git-mr.html b/man/git-mr.html
index cb90daf..87bc1c8 100644
--- a/man/git-mr.html
+++ b/man/git-mr.html
@@ -94,7 +94,9 @@
<url>
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
@@ -106,6 +108,14 @@ From gitlab.com:owner/repository
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
Written by Étienne BERSAC bersace03@gmail.com from git-pr(1).
diff --git a/man/git-mr.md b/man/git-mr.md
index 03a62fd..4f9b2b8 100644
--- a/man/git-mr.md
+++ b/man/git-mr.md
@@ -21,7 +21,9 @@ git-mr(1) -- Checks out a merge request locally
<url>
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
@@ -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
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
Written by Étienne BERSAC from git-pr(1).
diff --git a/tests/git-mr.bats b/tests/git-mr.bats
new file mode 100644
index 0000000..651c0a6
--- /dev/null
+++ b/tests/git-mr.bats
@@ -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'
+}