diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 51e175fba8..3e0b5496d0 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -293,6 +293,7 @@ const (
 	OneDevService                          // 6 onedev service
 	GitBucketService                       // 7 gitbucket service
 	CodebaseService                        // 8 codebase service
+	ForgejoService                         // 9 forgejo service
 )
 
 // Name represents the service type's name
@@ -318,6 +319,8 @@ func (gt GitServiceType) Title() string {
 		return "GitBucket"
 	case CodebaseService:
 		return "Codebase"
+	case ForgejoService:
+		return "Forgejo"
 	case PlainGitService:
 		return "Git"
 	}
@@ -359,7 +362,7 @@ type MigrateRepoOptions struct {
 // TokenAuth represents whether a service type supports token-based auth
 func (gt GitServiceType) TokenAuth() bool {
 	switch gt {
-	case GithubService, GiteaService, GitlabService:
+	case GithubService, GiteaService, GitlabService, ForgejoService:
 		return true
 	}
 	return false
@@ -370,6 +373,7 @@ func (gt GitServiceType) TokenAuth() bool {
 var SupportedFullGitService = []GitServiceType{
 	GithubService,
 	GitlabService,
+	ForgejoService,
 	GiteaService,
 	GogsService,
 	OneDevService,
diff --git a/public/assets/img/svg/gitea-forgejo.svg b/public/assets/img/svg/gitea-forgejo.svg
new file mode 100644
index 0000000000..ef617c00f3
--- /dev/null
+++ b/public/assets/img/svg/gitea-forgejo.svg
@@ -0,0 +1,9 @@
+<svg width="32" height="32" viewBox="-15 0 256 256" xmlns="http://www.w3.org/2000/svg">
+  <g transform="translate(28,28)">
+    <path d="M58 168 v-98 a50 50 0 0 1 50-50 h20" fill="none" stroke="#ff6600" stroke-width="25" />
+    <path d="M58 168 v-30 a50 50 0 0 1 50-50 h20" fill="none" stroke="#d40000" stroke-width="25" />
+    <circle cx="142" cy="20" r="18" fill="none" stroke="#ff6600" stroke-width="15" />
+    <circle cx="142" cy="88" r="18" fill="none" stroke="#d40000" stroke-width="15" />
+    <circle cx="58" cy="180" r="18" fill="none" stroke="#d40000" stroke-width="15" />
+  </g>
+</svg>
diff --git a/services/convert/utils.go b/services/convert/utils.go
index cdce60831c..fe35fd2dac 100644
--- a/services/convert/utils.go
+++ b/services/convert/utils.go
@@ -36,6 +36,8 @@ func ToGitServiceType(value string) structs.GitServiceType {
 		return structs.OneDevService
 	case "gitbucket":
 		return structs.GitBucketService
+	case "forgejo":
+		return structs.ForgejoService
 	default:
 		return structs.PlainGitService
 	}
diff --git a/services/convert/utils_test.go b/services/convert/utils_test.go
index 1ac03a3097..b464d8bb68 100644
--- a/services/convert/utils_test.go
+++ b/services/convert/utils_test.go
@@ -28,6 +28,8 @@ func TestToGitServiceType(t *testing.T) {
 		typ: "gitlab", enum: 4,
 	}, {
 		typ: "gogs", enum: 5,
+	}, {
+		typ: "forgejo", enum: 9,
 	}, {
 		typ: "trash", enum: 1,
 	}}
diff --git a/services/migrations/forgejo_downloader.go b/services/migrations/forgejo_downloader.go
new file mode 100644
index 0000000000..25dbb6ec51
--- /dev/null
+++ b/services/migrations/forgejo_downloader.go
@@ -0,0 +1,20 @@
+// Copyright 2023 The Forgejo Authors
+// SPDX-License-Identifier: MIT
+
+package migrations
+
+import (
+	"code.gitea.io/gitea/modules/structs"
+)
+
+func init() {
+	RegisterDownloaderFactory(&ForgejoDownloaderFactory{})
+}
+
+type ForgejoDownloaderFactory struct {
+	GiteaDownloaderFactory
+}
+
+func (f *ForgejoDownloaderFactory) GitServiceType() structs.GitServiceType {
+	return structs.ForgejoService
+}
diff --git a/services/migrations/forgejo_downloader_test.go b/services/migrations/forgejo_downloader_test.go
new file mode 100644
index 0000000000..5bd37551cc
--- /dev/null
+++ b/services/migrations/forgejo_downloader_test.go
@@ -0,0 +1,16 @@
+// Copyright 2023 The Forgejo Authors
+// SPDX-License-Identifier: MIT
+
+package migrations
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/modules/structs"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestForgejoDownload(t *testing.T) {
+	require.NotNil(t, getFactoryFromServiceType(structs.ForgejoService))
+}
diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go
index 0b83f3b4a3..ae164a7add 100644
--- a/services/migrations/migrate.go
+++ b/services/migrations/migrate.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	base "code.gitea.io/gitea/modules/migration"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 )
 
@@ -139,19 +140,25 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str
 	return uploader.repo, nil
 }
 
+func getFactoryFromServiceType(serviceType structs.GitServiceType) base.DownloaderFactory {
+	for _, factory := range factories {
+		if factory.GitServiceType() == serviceType {
+			return factory
+		}
+	}
+	return nil
+}
+
 func newDownloader(ctx context.Context, ownerName string, opts base.MigrateOptions) (base.Downloader, error) {
 	var (
 		downloader base.Downloader
 		err        error
 	)
 
-	for _, factory := range factories {
-		if factory.GitServiceType() == opts.GitServiceType {
-			downloader, err = factory.New(ctx, opts)
-			if err != nil {
-				return nil, err
-			}
-			break
+	if factory := getFactoryFromServiceType(opts.GitServiceType); factory != nil {
+		downloader, err = factory.New(ctx, opts)
+		if err != nil {
+			return nil, err
 		}
 	}
 
diff --git a/templates/repo/migrate/forgejo.tmpl b/templates/repo/migrate/forgejo.tmpl
new file mode 100644
index 0000000000..3caadbee15
--- /dev/null
+++ b/templates/repo/migrate/forgejo.tmpl
@@ -0,0 +1 @@
+{{template "repo/migrate/gitea" .}}
diff --git a/tests/integration/migrate_test.go b/tests/integration/migrate_test.go
index 2f44de8a4f..44fc890abf 100644
--- a/tests/integration/migrate_test.go
+++ b/tests/integration/migrate_test.go
@@ -4,6 +4,7 @@
 package integration
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -20,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/services/migrations"
+	"code.gitea.io/gitea/services/repository"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -51,7 +53,7 @@ func TestMigrateLocalPath(t *testing.T) {
 	setting.ImportLocalPaths = old
 }
 
-func TestMigrateGiteaForm(t *testing.T) {
+func TestMigrate(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		AllowLocalNetworks := setting.Migrations.AllowLocalNetworks
 		setting.Migrations.AllowLocalNetworks = true
@@ -71,34 +73,45 @@ func TestMigrateGiteaForm(t *testing.T) {
 		session := loginUser(t, ownerName)
 		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadMisc)
 
-		// Step 0: verify the repo is available
-		req := NewRequestf(t, "GET", fmt.Sprintf("/%s/%s", ownerName, repoName))
-		_ = session.MakeRequest(t, req, http.StatusOK)
-		// Step 1: get the Gitea migration form
-		req = NewRequestf(t, "GET", "/repo/migrate/?service_type=%d", structs.GiteaService)
-		resp := session.MakeRequest(t, req, http.StatusOK)
-		// Step 2: load the form
-		htmlDoc := NewHTMLParser(t, resp.Body)
-		link, exists := htmlDoc.doc.Find(`form.ui.form[action^="/repo/migrate"]`).Attr("action")
-		assert.True(t, exists, "The template has changed")
-		// Step 4: submit the migration to only migrate issues
-		migratedRepoName := "otherrepo"
-		req = NewRequestWithValues(t, "POST", link, map[string]string{
-			"_csrf":       htmlDoc.GetCSRF(),
-			"service":     fmt.Sprintf("%d", structs.GiteaService),
-			"clone_addr":  fmt.Sprintf("%s%s/%s", u, ownerName, repoName),
-			"auth_token":  token,
-			"issues":      "on",
-			"repo_name":   migratedRepoName,
-			"description": "",
-			"uid":         fmt.Sprintf("%d", repoOwner.ID),
-		})
-		resp = session.MakeRequest(t, req, http.StatusSeeOther)
-		// Step 5: a redirection displays the migrated repository
-		loc := resp.Header().Get("Location")
-		assert.EqualValues(t, fmt.Sprintf("/%s/%s", ownerName, migratedRepoName), loc)
-		// Step 6: check the repo was created
-		unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: migratedRepoName})
+		for _, s := range []struct {
+			svc structs.GitServiceType
+		}{
+			{svc: structs.GiteaService},
+			{svc: structs.ForgejoService},
+		} {
+			// Step 0: verify the repo is available
+			req := NewRequestf(t, "GET", fmt.Sprintf("/%s/%s", ownerName, repoName))
+			_ = session.MakeRequest(t, req, http.StatusOK)
+			// Step 1: get the Gitea migration form
+			req = NewRequestf(t, "GET", "/repo/migrate/?service_type=%d", s.svc)
+			resp := session.MakeRequest(t, req, http.StatusOK)
+			// Step 2: load the form
+			htmlDoc := NewHTMLParser(t, resp.Body)
+			link, exists := htmlDoc.doc.Find(`form.ui.form[action^="/repo/migrate"]`).Attr("action")
+			assert.True(t, exists, "The template has changed")
+			// Step 4: submit the migration to only migrate issues
+			migratedRepoName := "otherrepo"
+			req = NewRequestWithValues(t, "POST", link, map[string]string{
+				"_csrf":       htmlDoc.GetCSRF(),
+				"service":     fmt.Sprintf("%d", s.svc),
+				"clone_addr":  fmt.Sprintf("%s%s/%s", u, ownerName, repoName),
+				"auth_token":  token,
+				"issues":      "on",
+				"repo_name":   migratedRepoName,
+				"description": "",
+				"uid":         fmt.Sprintf("%d", repoOwner.ID),
+			})
+			resp = session.MakeRequest(t, req, http.StatusSeeOther)
+			// Step 5: a redirection displays the migrated repository
+			loc := resp.Header().Get("Location")
+			assert.EqualValues(t, fmt.Sprintf("/%s/%s", ownerName, migratedRepoName), loc)
+			// Step 6: check the repo was created
+			repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: migratedRepoName})
+
+			// Step 7: delete the repository, so we can test with other services
+			err := repository.DeleteRepository(context.Background(), repoOwner, repo, false)
+			assert.NoError(t, err)
+		}
 	})
 }
 
diff --git a/web_src/svg/gitea-forgejo.svg b/web_src/svg/gitea-forgejo.svg
new file mode 100644
index 0000000000..e00e5963cf
--- /dev/null
+++ b/web_src/svg/gitea-forgejo.svg
@@ -0,0 +1,9 @@
+<svg width="64" height="64" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="forgejo-logo" aria-hidden="true">
+  <g transform="translate(28,28)">
+    <path d="M58 168 v-98 a50 50 0 0 1 50-50 h20" fill="none" stroke="#ff6600" stroke-width="25" />
+    <path d="M58 168 v-30 a50 50 0 0 1 50-50 h20" fill="none" stroke="#d40000" stroke-width="25" />
+    <circle cx="142" cy="20" r="18" fill="none" stroke="#ff6600" stroke-width="15" />
+    <circle cx="142" cy="88" r="18" fill="none" stroke="#d40000" stroke-width="15" />
+    <circle cx="58" cy="180" r="18" fill="none" stroke="#d40000" stroke-width="15" />
+  </g>
+</svg>