Initial commit

This commit is contained in:
Jeena 2025-08-19 10:13:22 +09:00
commit 3a0d6186c9
6 changed files with 358 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

13
Pipfile Normal file
View file

@ -0,0 +1,13 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
requests = "*"
python-dotenv = "*"
[requires]
python_version = "3.11"

148
Pipfile.lock generated Normal file
View file

@ -0,0 +1,148 @@
{
"_meta": {
"hash": {
"sha256": "67ce3909f0bc2928002eee2dc1c7699080fb382e69d870314013aadc72d077c2"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.11"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"certifi": {
"hashes": [
"sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407",
"sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"
],
"markers": "python_version >= '3.7'",
"version": "==2025.8.3"
},
"charset-normalizer": {
"hashes": [
"sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91",
"sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0",
"sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154",
"sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601",
"sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884",
"sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07",
"sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c",
"sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64",
"sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe",
"sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f",
"sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432",
"sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc",
"sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa",
"sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9",
"sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae",
"sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19",
"sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d",
"sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e",
"sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4",
"sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7",
"sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312",
"sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92",
"sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31",
"sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c",
"sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f",
"sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99",
"sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b",
"sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15",
"sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392",
"sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f",
"sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8",
"sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491",
"sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0",
"sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc",
"sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0",
"sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f",
"sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a",
"sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40",
"sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927",
"sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849",
"sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce",
"sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14",
"sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05",
"sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c",
"sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c",
"sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a",
"sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc",
"sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34",
"sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9",
"sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096",
"sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14",
"sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30",
"sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b",
"sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b",
"sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942",
"sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db",
"sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5",
"sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b",
"sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce",
"sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669",
"sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0",
"sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018",
"sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93",
"sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe",
"sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049",
"sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a",
"sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef",
"sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2",
"sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca",
"sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16",
"sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f",
"sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb",
"sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1",
"sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557",
"sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37",
"sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7",
"sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72",
"sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c",
"sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"
],
"markers": "python_version >= '3.7'",
"version": "==3.4.3"
},
"idna": {
"hashes": [
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
],
"markers": "python_version >= '3.6'",
"version": "==3.10"
},
"python-dotenv": {
"hashes": [
"sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc",
"sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==1.1.1"
},
"requests": {
"hashes": [
"sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c",
"sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.32.4"
},
"urllib3": {
"hashes": [
"sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760",
"sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"
],
"markers": "python_version >= '3.9'",
"version": "==2.5.0"
}
},
"develop": {}
}

59
README.md Normal file
View file

@ -0,0 +1,59 @@
# GitHub to Forgejo Migration Script
This repository contains a Python script to migrate repositories from **GitHub** to **Forgejo**.
It supports private and public repositories owned by the authenticated GitHub user.
## Setup
1. Clone this repository:
```bash
git clone git@your-forgejo-instance:username/github-to-forgejo.git
cd github-to-forgejo
```
2. Install dependencies with Pipenv:
```bash
pipenv install
```
## Authentication
You need to create **personal access tokens** for both GitHub and Forgejo.
### GitHub Token
1. Go to [GitHub Personal Access Tokens](https://github.com/settings/tokens).
2. Create a new **classic** token with the following scopes:
- `repo` (required for private repos)
- `read:project` (if you want project boards)
- `read:org` (optional, if repos are in orgs)
- `read:discussion` (optional, usually not needed)
3. Copy the generated token.
### Forgejo Token
1. Log into your Forgejo instance.
2. Go to **Settings → Applications → Manage Access Tokens**.
3. Create a token with these scopes:
- `repository (read/write)`
- `issue (read/write)`
- `misc (read/write)`
- `user (read/write)`
4. Copy the generated token.
### .env file
The settings are stored in the .env file. Copy the env-example file and call
it .env
Put in both tokens and fill in the rest accordingly like usernames and URLs.
## Usage
Run the script to migrate repositories:
```bash
pipenv run python migrate_github_to_forgejo.py
```

15
env-example Normal file
View file

@ -0,0 +1,15 @@
# .env
GITHUB_TOKEN=
GITHUB_USERNAME=
FORGEJO_TOKEN=
FORGEJO_USERNAME=
FORGEJO_URL=
MIRROR=False
PRIVATE=True
IMPORT_ISSUES=True
IMPORT_LABELS=True
IMPORT_MILESTONES=True
IMPORT_PR=True
IMPORT_WIKI=True

122
migrate_github_to_forgejo.py Executable file
View file

@ -0,0 +1,122 @@
#!/usr/bin/env python3
import requests
import json
from dotenv import load_dotenv
import os
# ------------------ CONFIG ------------------
load_dotenv()
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
GITHUB_USERNAME = os.getenv("GITHUB_USERNAME")
FORGEJO_TOKEN = os.getenv("FORGEJO_TOKEN")
FORGEJO_USERNAME = os.getenv("FORGEJO_USERNAME") # only for repo path
FORGEJO_URL = os.getenv("FORGEJO_URL")
# Migration options
def str2bool(v):
return str(v).lower() in ("yes", "true", "1")
MIRROR = str2bool(os.getenv("MIRROR", "False"))
IMPORT_ISSUES = str2bool(os.getenv("IMPORT_ISSUES", "True"))
IMPORT_LABELS = str2bool(os.getenv("IMPORT_LABELS", "True"))
IMPORT_MILESTONES = str2bool(os.getenv("IMPORT_MILESTONES", "True"))
IMPORT_PR = str2bool(os.getenv("IMPORT_PR", "True"))
IMPORT_WIKI = str2bool(os.getenv("IMPORT_WIKI", "True"))
# --------------------------------------------
# Get Forgejo user ID using personal token
def get_forgejo_uid():
headers = {"Authorization": f"token {FORGEJO_TOKEN}"}
resp = requests.get(f"{FORGEJO_URL}/api/v1/user", headers=headers)
resp.raise_for_status()
return resp.json()["id"]
# Get GitHub repos including archived status and privacy
def get_github_repos():
headers = {"Authorization": f"token {GITHUB_TOKEN}"}
repos = []
page = 1
while True:
# Use /user/repos for authenticated user and visibility=all
url = f"https://api.github.com/user/repos?per_page=100&page={page}&visibility=all&affiliation=owner"
resp = requests.get(url, headers=headers)
resp.raise_for_status()
data = resp.json()
if not data:
break
for r in data:
repos.append({
"name": r["name"],
"archived": r["archived"],
"private": r["private"]
})
page += 1
return repos
# Check if repo exists in Forgejo
def get_forgejo_repo(repo_name):
headers = {"Authorization": f"token {FORGEJO_TOKEN}"}
resp = requests.get(f"{FORGEJO_URL}/api/v1/repos/{FORGEJO_USERNAME}/{repo_name}", headers=headers)
if resp.status_code == 200:
return resp.json()
return None
# Migrate a repo to Forgejo using authenticated clone if private
def migrate_repo(repo_name, uid, private):
# Use HTTPS clone URL for GitHub
clone_url = f"https://{GITHUB_USERNAME}:{GITHUB_TOKEN}@github.com/{GITHUB_USERNAME}/{repo_name}.git" if private else f"https://github.com/{GITHUB_USERNAME}/{repo_name}.git"
headers = {"Authorization": f"token {FORGEJO_TOKEN}", "Content-Type": "application/json"}
payload = {
"clone_addr": clone_url,
"uid": uid,
"repo_name": repo_name,
"mirror": MIRROR,
"private": private,
"auth_username": GITHUB_USERNAME if private else "",
"auth_password": GITHUB_TOKEN if private else "",
"issues": IMPORT_ISSUES,
"labels": IMPORT_LABELS,
"milestones": IMPORT_MILESTONES,
"pull_requests": IMPORT_PR,
"wiki": IMPORT_WIKI
}
resp = requests.post(f"{FORGEJO_URL}/api/v1/repos/migrate", headers=headers, data=json.dumps(payload))
if resp.status_code == 201:
print(f"[SUCCESS] Migrated {repo_name}")
else:
print(f"[ERROR] Failed to migrate {repo_name}: {resp.status_code} {resp.text}")
# Update existing repo visibility or archive
def update_repo(repo_name, private, archived):
headers = {"Authorization": f"token {FORGEJO_TOKEN}", "Content-Type": "application/json"}
payload = {"private": private, "archived": archived}
resp = requests.patch(f"{FORGEJO_URL}/api/v1/repos/{FORGEJO_USERNAME}/{repo_name}", headers=headers, data=json.dumps(payload))
if resp.status_code == 200:
print(f"[UPDATED] {repo_name}: private={private}, archived={archived}")
else:
print(f"[ERROR] Failed to update {repo_name}: {resp.status_code} {resp.text}")
def main():
uid = get_forgejo_uid()
repos = get_github_repos()
print(f"Found {len(repos)} repos on GitHub.")
for repo in repos:
name = repo["name"]
private = repo["private"]
archived = repo["archived"]
existing = get_forgejo_repo(name)
if existing:
# Update visibility and archive status if needed
update_repo(name, private, archived)
continue
migrate_repo(name, uid, private)
if archived:
update_repo(name, private, archived) # archive after migration
if __name__ == "__main__":
main()