Initial commit
This commit is contained in:
commit
3a0d6186c9
6 changed files with 358 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.env
|
13
Pipfile
Normal file
13
Pipfile
Normal 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
148
Pipfile.lock
generated
Normal 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
59
README.md
Normal 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
15
env-example
Normal 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
122
migrate_github_to_forgejo.py
Executable 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()
|
Loading…
Add table
Add a link
Reference in a new issue