This repository has been archived on 2025-04-03. You can view files and clone it, but cannot push or open issues or pull requests.
linux-bash-scripts/gitlab2gitea/gitlab2gitea.py

1470 lines
44 KiB
Python

# Description: Script to migrate repositories from GitLab to Gitea
# Author: Enrico Ludwig <enrico.ludwig@zion-networks.de>
# Version: 1.0
# Date: 2024-07-18
# License: MIT (see https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE)
#
# Precedence of settings: arguments > .env file > environment variables > default values
#
# Usage:
# python3 gitlab2gitea.py [options]
#
# --help Show this help message and exit
#
# --gitlab-token <GITLAB_TOKEN> GitLab access token
# --gitlab-url <GITLAB_URL> GitLab URL
# --gitlab-api-version <GITLAB_API_VERSION> GitLab API version (default: v4)
# --gitea-token <GITEA_TOKEN> Gitea access token
# --gitea-url <GITEA_URL> Gitea URL
# --gitea-api-version <GITEA_API_VERSION> Gitea API version (default: v1)
#
# --no-create-missing-groups Do not create missing groups on Gitea (default: False)
# --no-create-missing-users Do not create missing users on Gitea (default: False)
# --no-create-missing-projects Do not create missing projects on Gitea (default: False)
# --no-update-existing-groups Do not update existing groups on Gitea (default: False)
# --no-update-existing-users Do not update existing users on Gitea (default: False)
# --no-update-existing-projects Do not update existing projects on Gitea (default: False)
#
# --override-groups Override existing groups on Gitea (default: False) - not implemented yet
# --override-users Override existing users on Gitea (default: False) - not implemented yet
# --override-projects Override existing projects on Gitea (default: False) - not implemented yet
#
# --skip-empty-groups Skip empty groups (default: False) - not implemented yet
# --skip-empty-projects Skip empty projects (default: False) - not implemented yet
#
# --only-groups Migrate only groups (default: False)
# --only-users Migrate only users (default: False)
# --only-projects Migrate only projects (default: False)
#
# --quiet Enable quiet mode (default: False)
# --debug Enable debug mode (default: False)
# --trace Enable trace mode (default: False)
# --dry-run Enable dry-run mode (default: False)
# --log-file <LOG_FILE> Log file (default: gitlab2gitea.log)
# If not set, logs will be only printed to stdout
# --append-log Append log file (default: False)
#
# Example:
# python3 gitlab2gitea.py \
# --gitlab-url <GITLAB_URL> \
# --gitlab-token <GITLAB_TOKEN> \
# --gitea-url <GITEA_URL> \
# --gitea-token <GITEA_TOKEN>
# Settings - GitLab
GITLAB_URL = ""
GITLAB_TOKEN = ""
GITLAB_API_VERSION = "v4"
# Settings - Gitea
GITEA_URL = ""
GITEA_TOKEN = ""
GITEA_API_VERSION = "v1"
# Settings - General Repository
NO_CREATE_MISSING_GROUPS = False
NO_CREATE_MISSING_USERS = False
NO_CREATE_MISSING_PROJECTS = False
NO_UPDATE_EXISTING_GROUPS = False
NO_UPDATE_EXISTING_USERS = False
NO_UPDATE_EXISTING_PROJECTS = False
OVERWRITE_EXISTING_GROUPS = False
OVERWRITE_EXISTING_USERS = False
OVERWRITE_EXISTING_PROJECTS = False
ONLY_GROUPS = False
ONLY_USERS = False
ONLY_PROJECTS = False
# Settings - General
DEBUG = False
TRACE = False
DRY_RUN = False
LOG_FILE = "gitlab2gitea.log"
APPEND_LOG = False
QUIET = False
# Internal variables - Do not change
GITEA_RESERVED_USERNAMES = ["ghost", "notifications"]
# Imports
import os
import sys
import argparse
import requests
import traceback
# Set cwd to script directory
os.chdir(os.path.dirname(os.path.realpath(__file__)))
# Read environment variables
if "GITLAB_URL" in os.environ:
GITLAB_URL = os.environ["GITLAB_URL"]
if "GITLAB_TOKEN" in os.environ:
GITLAB_TOKEN = os.environ["GITLAB_TOKEN"]
if "GITLAB_API_VERSION" in os.environ:
GITLAB_API_VERSION = os.environ["GITLAB_API_VERSION"]
if "GITEA_URL" in os.environ:
GITEA_URL = os.environ["GITEA_URL"]
if "GITEA_TOKEN" in os.environ:
GITEA_TOKEN = os.environ["GITEA_TOKEN"]
if "GITEA_API_VERSION" in os.environ:
GITEA_API_VERSION = os.environ["GITEA_API_VERSION"]
if "NO_CREATE_MISSING_GROUPS" in os.environ:
NO_CREATE_MISSING_GROUPS = bool(os.environ["NO_CREATE_MISSING_GROUPS"])
if "NO_CREATE_MISSING_USERS" in os.environ:
NO_CREATE_MISSING_USERS = bool(os.environ["NO_CREATE_MISSING_USERS"])
if "NO_CREATE_MISSING_PROJECTS" in os.environ:
NO_CREATE_MISSING_PROJECTS = bool(os.environ["NO_CREATE_MISSING_PROJECTS"])
if "NO_UPDATE_EXISTING_GROUPS" in os.environ:
NO_UPDATE_EXISTING_GROUPS = bool(os.environ["NO_UPDATE_EXISTING_GROUPS"])
if "NO_UPDATE_EXISTING_USERS" in os.environ:
NO_UPDATE_EXISTING_USERS = bool(os.environ["NO_UPDATE_EXISTING_USERS"])
if "NO_UPDATE_EXISTING_PROJECTS" in os.environ:
NO_UPDATE_EXISTING_PROJECTS = bool(os.environ["NO_UPDATE_EXISTING_PROJECTS"])
if "OVERWRITE_EXISTING_GROUPS" in os.environ:
OVERWRITE_EXISTING_GROUPS = bool(os.environ["OVERWRITE_EXISTING_GROUPS"])
if "OVERWRITE_EXISTING_USERS" in os.environ:
OVERWRITE_EXISTING_USERS = bool(os.environ["OVERWRITE_EXISTING_USERS"])
if "OVERWRITE_EXISTING_PROJECTS" in os.environ:
OVERWRITE_EXISTING_PROJECTS = bool(os.environ["OVERWRITE_EXISTING_PROJECTS"])
if "ONLY_GROUPS" in os.environ:
ONLY_GROUPS = bool(os.environ["ONLY_GROUPS"])
if "ONLY_USERS" in os.environ:
ONLY_USERS = bool(os.environ["ONLY_USERS"])
if "ONLY_PROJECTS" in os.environ:
ONLY_PROJECTS = bool(os.environ["ONLY_PROJECTS"])
if "QUIET" in os.environ:
QUIET = bool(os.environ["QUIET"])
if "DEBUG" in os.environ:
DEBUG = bool(os.environ["DEBUG"])
if "TRACE" in os.environ:
TRACE = bool(os.environ["TRACE"])
if "DRY_RUN" in os.environ:
DRY_RUN = bool(os.environ["DRY_RUN"])
if "LOG_FILE" in os.environ:
LOG_FILE = os.environ["LOG_FILE"]
if "APPEND_LOG" in os.environ:
APPEND_LOG = bool(os.environ["APPEND_LOG"])
# Read .env file if exists and override environment variables
if os.path.exists(".env"):
with open(".env", "r") as env_file:
for line in env_file:
# skip comments, empty lines and lines without '='
if line.startswith("#") or not "=" in line:
continue
key, value = line.strip().split("=")
if key == "GITLAB_API_VERSION":
GITLAB_API_VERSION = value
if key == "GITLAB_URL":
GITLAB_URL = value
if key == "GITLAB_TOKEN":
GITLAB_TOKEN = value
if key == "GITEA_URL":
GITEA_URL = value
if key == "GITEA_TOKEN":
GITEA_TOKEN = value
if key == "GITEA_API_VERSION":
GITEA_API_VERSION = value
if key == "NO_CREATE_MISSING_GROUPS":
if value.lower() == "true" or value == "1":
NO_CREATE_MISSING_GROUPS = True
if key == "NO_CREATE_MISSING_USERS":
if value.lower() == "true" or value == "1":
NO_CREATE_MISSING_USERS = True
if key == "NO_CREATE_MISSING_PROJECTS":
if value.lower() == "true" or value == "1":
NO_CREATE_MISSING_PROJECTS = True
if key == "NO_UPDATE_EXISTING_GROUPS":
if value.lower() == "true" or value == "1":
NO_UPDATE_EXISTING_GROUPS = True
if key == "NO_UPDATE_EXISTING_USERS":
if value.lower() == "true" or value == "1":
NO_UPDATE_EXISTING_USERS = True
if key == "NO_UPDATE_EXISTING_PROJECTS":
if value.lower() == "true" or value == "1":
NO_UPDATE_EXISTING_PROJECTS = True
if key == "OVERWRITE_EXISTING_GROUPS":
if value.lower() == "true" or value == "1":
OVERWRITE_EXISTING_GROUPS = True
if key == "OVERWRITE_EXISTING_USERS":
if value.lower() == "true" or value == "1":
OVERWRITE_EXISTING_USERS = True
if key == "OVERWRITE_EXISTING_PROJECTS":
if value.lower() == "true" or value == "1":
OVERWRITE_EXISTING_PROJECTS = True
if key == "ONLY_GROUPS":
if value.lower() == "true" or value == "1":
ONLY_GROUPS = True
if key == "ONLY_USERS":
if value.lower() == "true" or value == "1":
ONLY_USERS = True
if key == "ONLY_PROJECTS":
if value.lower() == "true" or value == "1":
ONLY_PROJECTS = True
if key == "QUIET":
QUIET = bool(value)
if key == "DEBUG":
if value.lower() == "true" or value == "1":
DEBUG = True
if key == "TRACE":
if value.lower() == "true" or value == "1":
TRACE = True
if key == "DRY_RUN":
if value.lower() == "true" or value == "1":
DRY_RUN = True
if key == "LOG_FILE":
LOG_FILE = value
if key == "APPEND_LOG":
APPEND_LOG = bool(value)
# Read arguments and override environment variables
parser = argparse.ArgumentParser(
description="Script to migrate repositories from GitLab to Gitea"
)
# fmt: off
parser.add_argument("--gitlab-api-version", help="GitLab API version", default="v4")
parser.add_argument("--gitlab-token", help="GitLab access token")
parser.add_argument("--gitea-token", help="Gitea access token")
parser.add_argument("--gitlab-url", help="GitLab URL")
parser.add_argument("--gitea-url", help="Gitea URL")
parser.add_argument("--gitea-api-version", help="Gitea API version", default="v1")
parser.add_argument("--no-create-missing-groups", help="Do not create missing groups on Gitea", action="store_true")
parser.add_argument("--no-create-missing-users", help="Do not create missing users on Gitea", action="store_true")
parser.add_argument("--no-create-missing-projects", help="Do not create missing projects on Gitea", action="store_true")
parser.add_argument("--no-update-existing-groups", help="Do not update existing groups on Gitea", action="store_true")
parser.add_argument("--no-update-existing-users", help="Do not update existing users on Gitea", action="store_true")
parser.add_argument("--no-update-existing-projects", help="Do not update existing projects on Gitea", action="store_true")
parser.add_argument("--override-groups", help="Override existing groups on Gitea", action="store_true")
parser.add_argument("--override-users", help="Override existing users on Gitea", action="store_true")
parser.add_argument("--override-projects", help="Override existing projects on Gitea", action="store_true")
parser.add_argument("--only-groups", help="Migrate only groups", action="store_true")
parser.add_argument("--only-users", help="Migrate only users", action="store_true")
parser.add_argument("--only-projects", help="Migrate only projects", action="store_true")
parser.add_argument("--quiet", help="Enable quiet mode", action="store_true")
parser.add_argument("--debug", help="Enable debug mode", action="store_true")
parser.add_argument("--trace", help="Enable trace mode", action="store_true")
parser.add_argument("--dry-run", help="Enable dry-run mode", action="store_true")
parser.add_argument("--log-file", help="Log file", default="gitlab2gitea.log")
parser.add_argument("--append-log", help="Append log file", action="store_true")
# fmt: on
args = parser.parse_args()
if args.gitlab_api_version:
GITLAB_API_VERSION = args.gitlab_api_version
if args.gitlab_token:
GITLAB_TOKEN = args.gitlab_token
if args.gitlab_url:
GITLAB_URL = args.gitlab_url
if args.gitea_token:
GITEA_TOKEN = args.gitea_token
if args.gitea_url:
GITEA_URL = args.gitea_url
if args.gitea_api_version:
GITEA_API_VERSION = args.gitea_api_version
if args.no_create_missing_groups:
NO_CREATE_MISSING_GROUPS = True
if args.no_create_missing_users:
NO_CREATE_MISSING_USERS = True
if args.no_create_missing_projects:
NO_CREATE_MISSING_PROJECTS = True
if args.no_update_existing_groups:
NO_UPDATE_EXISTING_GROUPS = True
if args.no_update_existing_users:
NO_UPDATE_EXISTING_USERS = True
if args.no_update_existing_projects:
NO_UPDATE_EXISTING_PROJECTS = True
if args.override_groups:
OVERWRITE_EXISTING_GROUPS = True
if args.override_users:
OVERWRITE_EXISTING_USERS = True
if args.override_projects:
OVERWRITE_EXISTING_PROJECTS = True
if args.only_groups:
ONLY_GROUPS = True
if args.only_users:
ONLY_USERS = True
if args.only_projects:
ONLY_PROJECTS = True
if args.quiet:
QUIET = True
if args.debug:
DEBUG = True
if args.trace:
TRACE = True
if args.dry_run:
DRY_RUN = True
if args.log_file:
LOG_FILE = args.log_file
if args.append_log:
APPEND_LOG = True
# check for mutually exclusive options
if ONLY_GROUPS and ONLY_USERS:
_error("Options --only-groups and --only-users are mutually exclusive!")
sys.exit(1)
if ONLY_GROUPS and ONLY_PROJECTS:
_error("Options --only-groups and --only-projects are mutually exclusive!")
sys.exit(1)
if ONLY_USERS and ONLY_PROJECTS:
_error("Options --only-users and --only-projects are mutually exclusive!")
sys.exit(1)
if NO_CREATE_MISSING_GROUPS and OVERWRITE_EXISTING_GROUPS:
_error(
"Options --no-create-missing-groups and --override-groups are mutually exclusive!"
)
sys.exit(1)
if NO_CREATE_MISSING_USERS and OVERWRITE_EXISTING_USERS:
_error(
"Options --no-create-missing-users and --override-users are mutually exclusive!"
)
sys.exit(1)
if NO_CREATE_MISSING_PROJECTS and OVERWRITE_EXISTING_PROJECTS:
_error(
"Options --no-create-missing-projects and --override-projects are mutually exclusive!"
)
sys.exit(1)
if NO_UPDATE_EXISTING_GROUPS and OVERWRITE_EXISTING_GROUPS:
_error(
"Options --no-update-existing-groups and --override-groups are mutually exclusive!"
)
sys.exit(1)
if NO_UPDATE_EXISTING_USERS and OVERWRITE_EXISTING_USERS:
_error(
"Options --no-update-existing-users and --override-users are mutually exclusive!"
)
sys.exit(1)
if NO_UPDATE_EXISTING_PROJECTS and OVERWRITE_EXISTING_PROJECTS:
_error(
"Options --no-update-existing-projects and --override-projects are mutually exclusive!"
)
sys.exit(1)
# Remove trailing slashes from URLs
GITLAB_URL = GITLAB_URL.rstrip("/")
GITEA_URL = GITEA_URL.rstrip("/")
# Internal functions
def _trace(message):
if TRACE:
print("\033[1m\033[36m[TRC]\033[0m", message)
if LOG_FILE:
with open(LOG_FILE, "a") as log_file:
log_file.write(f"[TRC] {message}\n")
def _debug(message):
if TRACE or DEBUG:
print("\033[1m\033[34m[DBG]\033[0m", message)
if LOG_FILE:
with open(LOG_FILE, "a") as log_file:
log_file.write(f"[DBG] {message}\n")
def _info(message):
print("\033[1m\033[32m[INF]\033[0m", message)
if LOG_FILE:
with open(LOG_FILE, "a") as log_file:
log_file.write(f"[INF] {message}\n")
def _warn(message):
print("\033[1m\033[33m[WRN]\033[0m", message)
if LOG_FILE:
with open(LOG_FILE, "a") as log_file:
log_file.write(f"[WRN] {message}\n")
def _error(message):
print("\033[1m\033[31m[ERR]\033[0m", message)
if LOG_FILE:
with open(LOG_FILE, "a") as log_file:
log_file.write(f"[ERR] {message}\n")
def _exception(exception, custom_message=None):
exc_type, exc_obj, exc_tb = sys.exc_info()
filename = exc_tb.tb_frame.f_code.co_filename
lineno = exc_tb.tb_lineno
formatted_traceback = traceback.format_exc()
# Prepare the exception message
exception_message = (f"{custom_message}\n" if custom_message else "") + (
f"\033[1m\033[31m[EXC]\033[0m {exception} "
f"(file: {filename}, line: {lineno})\n"
f"{formatted_traceback}\n"
)
# Print the exception message to the console
print(exception_message)
# Write the exception message to the log file if defined
if LOG_FILE:
with open(LOG_FILE, "a") as log_file:
log_file.write(f"[EXC] {exception_message}\n")
# PROGRAM
def is_gitea_reserved_username(username: str) -> bool:
return username in GITEA_RESERVED_USERNAMES
def gitlab2gitea_visibility(visibility: str) -> str:
if visibility == "private":
return "private"
elif visibility == "internal":
return "limited"
elif visibility == "public":
return "public"
else:
return "private"
# Endpoint: GET /api/{GITLAB_API_VERSION}/version
def check_gitlab():
_debug(f"REQUEST: GET {GITLAB_URL}/api/{GITLAB_API_VERSION}/version")
response = requests.get(
f"{GITLAB_URL}/api/{GITLAB_API_VERSION}/version",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Bearer {GITLAB_TOKEN}",
},
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"GitLab endpoint test failed: {response_message}")
else:
_info(f'GitLab endpoint version: {response.json()["version"]}')
# Endpoint: GET /api/{GITEA_API_VERSION}/version
def check_gitea():
_debug(f"REQUEST: GET {GITEA_URL}/api/{GITEA_API_VERSION}/version")
response = requests.get(
f"{GITEA_URL}/api/{GITEA_API_VERSION}/version",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"token {GITEA_TOKEN}",
},
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Gitea endpoint test failed: {response_message}")
else:
_info(f'Gitea endpoint version: {response.json()["version"]}')
# Endpoint: GET /api/{GITLAB_API_VERSION}/groups
def get_gitlab_groups() -> list:
groups = []
_debug(f"REQUEST: GET {GITLAB_URL}/api/{GITLAB_API_VERSION}/groups")
response = requests.get(
f"{GITLAB_URL}/api/{GITLAB_API_VERSION}/groups",
params={"all_available": 1, "per_page": 100, "page": 1, "top_level_only": 1},
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Bearer {GITLAB_TOKEN}",
},
)
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to get GitLab groups: {response_message}")
else:
groups = response.json()
return groups
# Endpoint: GET /api/{GITLAB_API_VERSION}/groups/{group_id}
def get_gitlab_group(group_id: int) -> dict:
_debug(f"REQUEST: GET {GITLAB_URL}/api/{GITLAB_API_VERSION}/groups/{group_id}")
response = requests.get(
f"{GITLAB_URL}/api/{GITLAB_API_VERSION}/groups/{group_id}",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Bearer {GITLAB_TOKEN}",
},
)
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to get GitLab group: {response_message}")
else:
group = response.json()
return group
# Endpoint: GET /api/{GITLAB_API_VERSION}/users
def get_gitlab_users() -> list:
next_page_link = f"{GITLAB_URL}/api/{GITLAB_API_VERSION}/users"
users = []
while next_page_link is not None:
_debug(f'REQUEST: GET {next_page_link.split("?")[0]}')
response = requests.get(
next_page_link,
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Bearer {GITLAB_TOKEN}",
},
)
next_page_link = (
response.links["next"]["url"] if "next" in response.links else None
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to get GitLab users: {response_message}")
else:
users += response.json()
return users
# Endpoint: GET /api/{GITLAB_API_VERSION}/projects
def get_gitlab_projects() -> list:
next_page_link = f"{GITLAB_URL}/api/{GITLAB_API_VERSION}/projects"
projects = []
while next_page_link is not None:
_debug(f'REQUEST: GET {next_page_link.split("?")[0]}')
response = requests.get(
next_page_link,
params={"all_available": 1, "per_page": 100},
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Bearer {GITLAB_TOKEN}",
},
)
next_page_link = (
response.links["next"]["url"] if "next" in response.links else None
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to get GitLab projects: {response_message}")
else:
projects += response.json()
return projects
# Endpoint: POST /api/{GITEA_API_VERSION}/admin/users
def migrate_gitlab_user_to_gitea(user: dict):
if not user:
raise Exception("User is missing!")
# Create Gitea user
_debug(f"REQUEST: POST {GITEA_URL}/api/{GITEA_API_VERSION}/admin/users")
response = requests.post(
f"{GITEA_URL}/api/{GITEA_API_VERSION}/admin/users",
json={
"login_name": user["username"],
"username": user["username"],
"email": user["email"],
"full_name": user["name"],
"password": "12345678", # TODO: Change to random password which will be sent to the user
"send_notify": False, # TODO: Change to True as soon as the password is sent to the user
"must_change_password": True,
"admin": user["is_admin"],
},
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"token {GITEA_TOKEN}",
},
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 201:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to create Gitea user: {response_message}")
else:
user = response.json()
if user["is_admin"]:
_info(f'Admin user "{user["username"]}" created on Gitea')
else:
_info(f'User "{user["username"]}" created on Gitea')
return user
# Endpoint: GET /api/{GITEA_API_VERSION}/orgs
def get_gitea_groups() -> list:
groups = []
_debug(
f"REQUEST: GET {GITEA_URL}/api/{GITEA_API_VERSION}/orgs?all_available=1&per_page=100"
)
response = requests.get(
f"{GITEA_URL}/api/{GITEA_API_VERSION}/orgs",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"token {GITEA_TOKEN}",
},
)
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to get Gitea groups: {response_message}")
else:
groups = response.json()
return groups
# Endpoint: GET /api/{GITEA_API_VERSION}/users
def get_gitea_users() -> list:
users = []
_debug(f"REQUEST: GET {GITEA_URL}/api/{GITEA_API_VERSION}/users/search")
response = requests.get(
f"{GITEA_URL}/api/{GITEA_API_VERSION}/users/search",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"token {GITEA_TOKEN}",
},
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to get Gitea users: {response_message}")
else:
users = response.json()
return users
# Endpoint: GET /api/{GITEA_API_VERSION}/repos
def get_gitea_projects() -> list:
projects = []
_debug(f"REQUEST: GET {GITEA_URL}/api/{GITEA_API_VERSION}/repos/search")
response = requests.get(
f"{GITEA_URL}/api/{GITEA_API_VERSION}/repos/search",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"token {GITEA_TOKEN}",
},
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to get Gitea projects: {response_message}")
else:
projects = response.json()
return projects
# Endpoint: POST /api/{GITEA_API_VERSION}/orgs
def migrate_gitlab_group_to_gitea(gitlab_group: dict):
if not gitlab_group:
raise Exception("GitLab group is missing!")
# Create Gitea group
_debug(f"REQUEST: POST {GITEA_URL}/api/{GITEA_API_VERSION}/orgs")
response = requests.post(
f"{GITEA_URL}/api/{GITEA_API_VERSION}/orgs",
json={
"username": gitlab_group["path"],
"full_name": gitlab_group["full_name"],
"description": gitlab_group["description"],
"website": gitlab_group["web_url"],
"visibility": gitlab2gitea_visibility(gitlab_group["visibility"]),
},
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"token {GITEA_TOKEN}",
},
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 201:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to create Gitea group: {response_message}")
else:
group = response.json()
return group
# Endpoint: PATCH /api/{GITEA_API_VERSION}/orgs/{org}
def update_gitea_org(data: dict) -> dict:
if not data:
raise Exception("Data is missing!")
name = data["path"]
_debug(f"REQUEST: PATCH {GITEA_URL}/api/{GITEA_API_VERSION}/orgs/{name}")
response = requests.patch(
f"{GITEA_URL}/api/{GITEA_API_VERSION}/orgs/{name}",
json=data,
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"token {GITEA_TOKEN}",
},
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to update Gitea group: {response_message}")
else:
group = response.json()
return group
# Endpoint: GET /api/{GITEA_API_VERSION}/users/{username}
def get_gitea_user(username: str) -> dict:
_debug(f"REQUEST: GET {GITEA_URL}/api/{GITEA_API_VERSION}/users/{username}")
response = requests.get(
f"{GITEA_URL}/api/{GITEA_API_VERSION}/users/{username}",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"token {GITEA_TOKEN}",
},
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to get Gitea user: {response_message}")
else:
user = response.json()
return user
# Endpoint: PATCH /api/{GITEA_API_VERSION}/admin/users/{username}
def update_gitea_user(data: dict) -> dict:
if not data:
raise Exception("Data is missing!")
username = data["username"]
current_user = get_gitea_user(username)
updated_user = convert_gitlab_user_to_gitea(data, current_user)
if is_gitea_reserved_username(username):
_warn(f'User "{username}" is a reserved username on Gitea!')
return None
if not current_user:
raise Exception(f'User "{username}" not found on Gitea!')
_debug(f"REQUEST: PATCH {GITEA_URL}/api/{GITEA_API_VERSION}/admin/users/{username}")
response = requests.patch(
f"{GITEA_URL}/api/{GITEA_API_VERSION}/admin/users/{username}",
json=updated_user,
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"token {GITEA_TOKEN}",
},
)
_trace(f"RESPONSE: {response.json()}")
if response.status_code != 200:
response_message = (
response.json()["message"]
if "message" in response.json()
else "Unknown error"
)
raise Exception(f"Failed to update Gitea user: {response_message}")
else:
user = response.json()
return user
def cmp_gitea_userdata(userdata_a: dict, userdata_b: dict) -> bool:
result = {}
for key in userdata_a:
if key in userdata_b:
if userdata_a[key] != userdata_b[key]:
result[key] = True
else:
result[key] = False
else:
result[key] = True
has_changes = False
for key in result:
if result[key]:
has_changes = True
break
return has_changes, result
def convert_gitlab_user_to_gitea(user: dict, extra_data: dict = None) -> dict:
gitea_user = {
"active": user["state"] == "active",
"login_name": user["username"],
"avatar_url": user["avatar_url"],
"created": user["created_at"],
"description": user["bio"],
"email": user["email"],
"full_name": user["name"],
"is_admin": user["is_admin"],
"prohibit_login": user["state"] == "blocked" or user["locked"],
"website": user["website_url"],
}
if extra_data:
gitea_user.update(extra_data)
return gitea_user
def cmp_gitlab_gitea_groups(gitlab_groups: list, gitea_groups: list) -> dict:
compare_result = {}
missing_matches = 0
for gitlab_group in gitlab_groups:
name = gitlab_group["path"]
exists = False
for gitea_group in gitea_groups:
if name == gitea_group["name"]:
exists = True
break
if exists:
_info(f'GITLAB: Group "{name}" exists on both GitLab and Gitea')
else:
_warn(f'GITLAB: Group "{name}" exists on GitLab only')
missing_matches += 1
compare_result[name] = 0 if exists else 1
for gitea_group in gitea_groups:
name = gitea_group["name"]
exists = False
for gitlab_group in gitlab_groups:
if name == gitlab_group["path"]:
exists = True
break
if not exists:
_warn(f'GITEA: Group "{name}" exists on Gitea only')
compare_result[name] = 2
return compare_result, missing_matches
def cmp_gitlab_gitea_users(gitlab_users: list, gitea_users: list) -> dict:
compare_result = {}
missing_matches = 0
for gitlab_user in gitlab_users:
name = gitlab_user["username"]
exists = False
for gitea_user in gitea_users:
if name == gitea_user["login"]:
exists = True
break
if exists:
_info(f'GITLAB: User "{name}" exists on both GitLab and Gitea')
else:
_warn(f'GITLAB: User "{name}" exists on GitLab only')
missing_matches += 1
compare_result[name] = 0 if exists else 1
for gitea_user in gitea_users:
name = gitea_user["login"]
exists = False
for gitlab_user in gitlab_users:
if name == gitlab_user["username"]:
exists = True
break
if not exists:
_warn(f'GITEA: User "{name}" exists on Gitea only')
compare_result[name] = 2
return compare_result, missing_matches
def cmp_gitlab_gitea_projects(gitlab_projects: list, gitea_projects: list) -> dict:
return {}, 0
def create_missing_groups(gitlab_groups: list, gitea_groups: list):
for gitlab_group in gitlab_groups:
name = gitlab_group["path"]
exists = False
for gitea_group in gitea_groups:
if name == gitea_group["name"]:
exists = True
break
if not exists:
_info(f'Creating missing group "{name}" on Gitea...')
try:
migrate_gitlab_group_to_gitea(gitlab_group)
except Exception as e:
_exception(f'Failed to create Gitea group "{name}": {e}', e)
def create_missing_users(gitlab_users: list, gitea_users: list):
for gitlab_user in gitlab_users:
name = gitlab_user["username"]
exists = False
for gitea_user in gitea_users["data"]:
if name == gitea_user["login"]:
exists = True
break
if gitea_user["email"] == None or gitea_user["email"] == "":
_warn(
f'User "{name}" does not have an email address and will not be created!'
)
continue
if gitea_user["username"] == None or gitea_user["username"] == "":
_warn(f'User "{name}" does not have a username and will not be created!')
continue
if is_gitea_reserved_username(name):
_warn(f'User "{name}" is a reserved username on Gitea!')
continue
if not exists:
_info(f'Creating missing user "{name}" on Gitea...')
try:
migrate_gitlab_user_to_gitea(gitlab_user)
except Exception as e:
_exception(f'Failed to create Gitea user "{name}": {e}', e)
def create_missing_projects(gitlab_projects: list, gitea_projects: list):
pass
def update_existing_groups(gitlab_groups: list, gitea_groups: list):
for gitlab_group in gitlab_groups:
name = gitlab_group["path"]
exists = False
for gitea_group in gitea_groups:
if name == gitea_group["name"]:
exists = True
break
if exists:
_info(f'Updating existing group "{name}" on Gitea...')
try:
update_gitea_org(
{
"path": gitlab_group["path"],
"description": gitlab_group["description"],
"website": gitlab_group["web_url"],
"visibility": gitlab2gitea_visibility(
gitlab_group["visibility"]
),
"full_name": gitlab_group["full_name"],
},
)
except Exception as e:
_exception(f'Failed to update Gitea group "{name}": {e}', e)
def update_existing_users(gitlab_users: list, gitea_users: list):
for gitlab_user in gitlab_users:
name = gitlab_user["username"]
exists = False
for gitea_user in gitea_users["data"]:
if name == gitea_user["login"]:
exists = True
break
if gitea_user["email"] == None or gitea_user["email"] == "":
_warn(
f'User "{name}" does not have an email address and will not be updated!'
)
continue
if gitea_user["username"] == None or gitea_user["username"] == "":
_warn(f'User "{name}" does not have a username and will not be updated!')
continue
if exists:
_info(f'Updating existing user "{name}" on Gitea...')
try:
update_gitea_user(gitlab_user)
except Exception as e:
_exception(f'Failed to update Gitea user "{name}": {e}', e)
else:
_warn(f'User "{name}" does not exist on Gitea!')
def update_existing_projects(gitlab_projects: list, gitea_projects: list):
pass
def migrate_groups():
gitlab_groups = get_gitlab_groups()
gitea_groups = get_gitea_groups()
_info(f"Groups on GitLab: {len(gitlab_groups)}")
_trace(f"Groups on GitLab: {gitlab_groups}")
_info(f"Groups on Gitea: {len(gitea_groups)}")
_trace(f"Groups on Gitea: {gitea_groups}")
try:
group_result, missing_matches = cmp_gitlab_gitea_groups(
gitlab_groups, gitea_groups
)
except Exception as e:
_exception(f"Failed to compare GitLab and Gitea groups: {e}", e)
return
if missing_matches > 0:
_warn(f"{missing_matches} groups are missing on Gitea!")
if missing_matches > 0 and not NO_CREATE_MISSING_GROUPS:
_info("Creating missing groups on Gitea...")
if not DRY_RUN:
try:
create_missing_groups(gitlab_groups, gitea_groups)
except Exception as e:
_exception(f"Failed to create missing groups: {e}", e)
else:
_warn(
"Dry-run mode enabled, skipping creation of missing groups on Gitea..."
)
if not NO_UPDATE_EXISTING_GROUPS:
_info("Updating existing groups on Gitea...")
try:
if not DRY_RUN:
update_existing_groups(gitlab_groups, gitea_groups)
else:
_warn(
"Dry-run mode enabled, skipping update of existing groups on Gitea..."
)
except Exception as e:
_exception(f"Failed to update existing groups: {e}", e)
def migrate_users():
gitlab_users = get_gitlab_users()
gitea_users = get_gitea_users()
_info(f"Users on GitLab: {len(gitlab_users)}")
_trace(f"Users on GitLab: {gitlab_users}")
_info(f"Users on Gitea: {len(gitea_users)}")
_trace(f"Users on Gitea: {gitea_users}")
try:
user_result, missing_matches = cmp_gitlab_gitea_users(
gitlab_users, gitea_users["data"]
)
except Exception as e:
_exception(f"Failed to compare GitLab and Gitea users: {e}", e)
return
if missing_matches > 0:
_warn(f"{missing_matches} users are missing on Gitea!")
if missing_matches > 0 and not NO_CREATE_MISSING_USERS:
_info("Creating missing users on Gitea...")
if not DRY_RUN:
create_missing_users(gitlab_users, gitea_users)
else:
_warn(
"Dry-run mode enabled, skipping creation of missing users on Gitea..."
)
if not NO_UPDATE_EXISTING_USERS:
_info("Updating existing users on Gitea...")
try:
if not DRY_RUN:
update_existing_users(gitlab_users, gitea_users)
else:
_warn(
"Dry-run mode enabled, skipping update of existing users on Gitea..."
)
except Exception as e:
_exception(f"Failed to update existing users: {e}", e)
def migrate_projects():
gitlab_projects = get_gitlab_projects()
gitea_projects = get_gitea_projects()
# dump the projects to json files
import json
with open("gitlab_projects.json", "w") as f:
json.dump(gitlab_projects, f, indent=4)
with open("gitea_projects.json", "w") as f:
json.dump(gitea_projects, f, indent=4)
_info(f"Projects on GitLab: {len(gitlab_projects)}")
_trace(f"Projects on GitLab: {gitlab_projects}")
_info(f"Projects on Gitea: {len(gitea_projects)}")
_trace(f"Projects on Gitea: {gitea_projects}")
try:
project_result, missing_matches = cmp_gitlab_gitea_projects(
gitlab_projects, gitea_projects
)
except Exception as e:
_exception(f"Failed to compare GitLab and Gitea projects: {e}", e)
return
if missing_matches > 0:
_warn(f"{missing_matches} projects are missing on Gitea!")
if missing_matches > 0 and not NO_CREATE_MISSING_PROJECTS:
_info("Creating missing projects on Gitea...")
if not DRY_RUN:
create_missing_projects(gitlab_projects, gitea_projects)
else:
_warn(
"Dry-run mode enabled, skipping creation of missing projects on Gitea..."
)
if not NO_UPDATE_EXISTING_PROJECTS:
_info("Updating existing projects on Gitea...")
try:
if not DRY_RUN:
update_existing_projects(gitlab_projects, gitea_projects)
else:
_warn(
"Dry-run mode enabled, skipping update of existing projects on Gitea..."
)
except Exception as e:
_exception(f"Failed to update existing projects: {e}", e)
def run_migration():
if ONLY_GROUPS:
_info("Migrating GitLab groups...")
migrate_groups()
_info("Group migration completed!")
return
elif ONLY_USERS:
_info("Migrating GitLab users...")
migrate_users()
_info("User migration completed!")
return
elif ONLY_PROJECTS:
_info("Migrating GitLab projects...")
migrate_projects()
_info("Project migration completed!")
return
else:
_info("Migrating GitLab groups...")
migrate_groups()
_info("Group migration completed!")
_info("Migrating GitLab users...")
migrate_users()
_info("User migration completed!")
_info("Migrating GitLab projects...")
migrate_projects()
_info("Project migration completed!")
def main():
_info("Gitlab2Gitea v1.0 - by Zion Networks")
_info("------------------------------------")
_info("")
if sys.version_info < (3, 6):
_error("Python 3.6 or higher is required!")
return
else:
_debug(f"Python version: {sys.version_info.major}.{sys.version_info.minor}")
if not GITLAB_TOKEN:
_error("GitLab access token is missing!")
return
if not GITEA_TOKEN:
_error("Gitea access token is missing!")
return
if LOG_FILE and not APPEND_LOG:
with open(LOG_FILE, "w") as log_file:
log_file.write("")
_debug(f"GitLab URL: {GITLAB_URL}")
_debug(f"Gitea URL: {GITEA_URL}")
_debug(f"Logging to: {LOG_FILE}")
if DRY_RUN:
_warn("Running in dry-run mode!")
_info("Testing endpoints...")
try:
check_gitlab()
check_gitea()
except Exception as e:
_exception(f"An error occurred: {e}", e)
return
_info("Starting migration...")
try:
run_migration()
except Exception as e:
_exception(f"An error occurred: {e}", e)
if __name__ == "__main__":
main()