1
Fork 0
mirror of https://github.com/RGBCube/github2forgejo synced 2026-01-13 00:31:02 +00:00
github2forgejo/github2forgejo
2025-10-24 19:56:20 +03:00

160 lines
6.3 KiB
Text
Executable file

#!/usr/bin/env nu
def or-default [default: closure] {
if ($in == null) {
do $default
} else {
$in
}
}
# Migrates a GitHub users or organizations repositories to a Forgejo instance.
#
# Accepted environment variables:
#
# DRY:
# Only print the actions that will be taken, and don't execute.
# Defaults to false, and a lack of the variable will be interpreted as false.
#
# GITHUB_USER:
# The user or organization to fetch the repositories from.
# Case sensitive.
# GITHUB_TOKEN:
# An access token for fetching private repositories.
# Optional. Set to empty string to not ask interactively.
#
# FORGEJO_URL:
# The URL to the Forgejo instance.
# Must include the protocol (http(s)://) as it is not just a domain.
# FORGEJO_USER:
# The user or organization to migrate the repositories to.
# FORGEJO_TOKEN:
# An access token for the specified user.
#
# STRATEGY:
# The strategy. Valid options are "mirrored" or "cloned" (case insensitive).
# "mirrored" will mirror the repository and tell the Forgejo instance to
# periodically update it, "cloned" will only clone once. "cloned" is
# useful if you are never going to use GitHub again.
#
# FORCE_SYNC:
# Whether to delete a mirrored repo from the Forgejo instance if the
# source on GitHub doesn't exist anymore. Must be either "true" or "false".
#
# To leave an environment variable unspecified, set it to an empty string.
def main [] {
let dry = $env | get --optional DRY | into bool --relaxed
let github_user = $env | get --optional GITHUB_USER | or-default { input $"(ansi red)GitHub username: (ansi reset)" }
let github_token = $env | get --optional GITHUB_TOKEN | or-default { input $"(ansi red)GitHub access token (ansi yellow)\((ansi blue)optional, only used for private repositories(ansi yellow))(ansi red): (ansi reset)" }
let forgejo_url = $env | get --optional FORGEJO_URL | or-default { input $"(ansi green)Forgejo instance URL \(with https://): (ansi reset)" } | str trim --right --char "/"
let forgejo_user = $env | get --optional FORGEJO_USER | or-default { input $"(ansi green)Forgejo username or organization to migrate to: (ansi reset)" }
let forgejo_token = $env | get --optional FORGEJO_TOKEN | or-default { input $"(ansi green)Forgejo access token: (ansi reset)" }
let strategy = $env | get --optional STRATEGY | or-default { [ Mirrored Cloned ] | input list $"(ansi cyan)Should the repos be mirrored, or just cloned once?(ansi reset)" } | str downcase
let force_sync = $env | get --optional FORCE_SYNC | try { into bool } | or-default { [ "Yup, delete them" Nope ] | input list $"(ansi yellow)Should mirrored repos that don't have a GitHub source anymore be deleted?(ansi reset)" | $in != "Nope" }
if $strategy not-in [ "mirrored", "cloned" ] {
print --stderr $"(ansi red)Invalid strategy! Must be either: (ansi green)mirrored(ansi red) or (ansi cyan)cloned(ansi red).(ansi reset)"
}
let github_repos = do {
def get-repos-at [page_nr: number] {
if $github_token == "" {
http get $"https://api.github.com/users/($github_user)/repos?per_page=100?page=($page_nr)"
} else {
# This fetchs all repos you have access to and have interacted with. Not just your own.
# We filter that later.
(http get $"https://api.github.com/user/repos?per_page=100&page=($page_nr)"
-H [ Authorization $"token ($github_token)" ])
}
}
mut repos = []
mut page_nr = 1
loop {
let page = get-repos-at $page_nr
$repos ++= $page
if ($page | length) < 100 {
# Less than the per_page means it's the end.
break;
}
$page_nr += 1
}
$repos | where { get owner.login | $in == $github_user }
}
# Delete mirrored repos that do not exist on GitHub.
if $force_sync {
let github_repo_names = ($github_repos | get name)
let forgejo_mirrored_repos = (
http get $"($forgejo_url)/api/v1/user/repos"
-H [ Authorization $"token ($forgejo_token)" ]
| where { get mirror }
| where { if $github_token == "" { not $in.private } else { true } }
)
let forgejo_not_on_github = ($forgejo_mirrored_repos | where { $in.name not-in $github_repo_names })
$forgejo_not_on_github | each {|forgejo_repo|
print --no-newline $"(ansi red)Deleting (ansi yellow)($forgejo_url)/($forgejo_repo.full_name)(ansi red) because the mirror source doesn't exist on GitHub anymore...(ansi reset)"
if $dry {
print $" (ansi yellow)Dry run, so can't know result as I'm not actually doing it.(ansi reset)"
return
}
(http delete $"($forgejo_url)/api/v1/repos/($forgejo_repo.full_name)"
-H [ Authorization $"token ($forgejo_token)" ])
print $" (ansi green_bold)Success!(ansi reset)"
}
}
# Mirror repos that do exist on GitHub to Forgejo.
$github_repos | each {|github_repo|
print --no-newline $"(ansi blue)(match $strategy {
"mirrored" => "Mirroring"
"cloned" => "Cloning"
}) (if $github_repo.private {
$"(ansi red)private(ansi blue)"
} else {
$"(ansi green)public(ansi blue)(char space)"
}) repository (ansi purple)($github_repo.html_url)(ansi blue) to (ansi white_bold)($forgejo_url)/($forgejo_user)/($github_repo.name)(ansi blue)...(ansi reset)"
if $dry {
print $" (ansi yellow)Dry run, so can't know result as I'm not actually doing it.(ansi reset)"
return
}
let response = (
http post $"($forgejo_url)/api/v1/repos/migrate"
--allow-errors
-t application/json
-H [ Authorization $"token ($forgejo_token)" ]
({
clone_addr: $github_repo.html_url
mirror: ($strategy == "mirrored")
private: $github_repo.private
repo_owner: $forgejo_user
repo_name: $github_repo.name
} | merge (if $github_token != "" { { auth_token: $github_token } } else { {} }))
)
let error_message = ($response | get --optional message)
if ($error_message != null and $error_message =~ "already exists") {
print $" (ansi yellow)Already mirrored!(ansi reset)"
} else if ($error_message != null) {
print $" (ansi red)Unknown error: ($error_message)(ansi reset)"
} else {
print $" (ansi green_bold)Success!(ansi reset)"
}
}
null
}