Skip to content

Instantly share code, notes, and snippets.

@ikouchiha47
Created May 7, 2026 16:59
Show Gist options
  • Select an option

  • Save ikouchiha47/385392b709245c9f3f01bc5ae0be7894 to your computer and use it in GitHub Desktop.

Select an option

Save ikouchiha47/385392b709245c9f3f01bc5ae0be7894 to your computer and use it in GitHub Desktop.
Migrate repos and groups from github to gitlab
#!/usr/bin/env bash
set -uo pipefail
VISIBILITY="${VISIBILITY:-private}"
WORKDIR="$HOME/dev/migrate/_mirror"
LOG="$HOME/dev/migrate/migrate.log"
SSH_HOST="${SSH_HOST_KEY:-gitlab.com}"
DRY_RUN=0
TEST=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--test) TEST=1 ;;
-h|--help)
cat <<EOF
Usage: $0 [--dry-run] [--test]
--dry-run List repos that would migrate, no changes made.
--test Migrate only the single smallest non-fork/non-archived repo
from each source (ikouchiha47 user, go-batteries group).
(no flag) Full migration.
Env:
VISIBILITY=private|public (default: private)
EOF
exit 0 ;;
esac
done
mkdir -p "$WORKDIR"
[ "$DRY_RUN" -eq 1 ] || : > "$LOG"
list_repos() {
local owner="$1"
gh repo list "$owner" --limit 500 --json name,isFork,isArchived,diskUsage \
-q '.[] | select(.isFork|not) | select(.isArchived|not) | select(.diskUsage > 0) | select(.name | startswith(".") | not) | "\(.diskUsage)\t\(.name)"' \
| sort -n
}
migrate_repo() {
local gh_owner="$1" gl_namespace="$2" name="$3"
local gl_path="$gl_namespace/$name"
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY $gh_owner/$name -> $gl_path"
return 0
fi
local enc
enc=$(printf '%s' "$gl_path" | jq -sRr @uri)
if glab api "projects/$enc" >/dev/null 2>&1; then
echo "SKIP exists: $gl_path" | tee -a "$LOG"
return 0
fi
if [ "$gl_namespace" = "ikouchiha47" ]; then
local priv=true
[ "$VISIBILITY" = public ] && priv=false
glab repo create "$name" --private="$priv" >/dev/null 2>>"$LOG" \
|| { echo "FAIL create $gl_path" | tee -a "$LOG"; return 1; }
else
local nsid
nsid=$(glab api "groups/$gl_namespace" | jq .id)
glab api -X POST projects \
-f name="$name" -f path="$name" \
-f namespace_id="$nsid" \
-f visibility="$VISIBILITY" >/dev/null 2>>"$LOG" \
|| { echo "FAIL create $gl_path" | tee -a "$LOG"; return 1; }
fi
cd "$WORKDIR"
rm -rf "$name.git"
git clone --mirror "git@github.com:$gh_owner/$name.git" "$name.git" >>"$LOG" 2>&1 \
|| { echo "FAIL clone $gh_owner/$name" | tee -a "$LOG"; return 1; }
cd "$name.git"
if [ -z "$(git for-each-ref)" ]; then
cd "$WORKDIR"; rm -rf "$name.git"
echo "OK $gh_owner/$name -> $gl_path (empty, project created)" | tee -a "$LOG"
return 0
fi
git remote add gitlab "git@$SSH_HOST:$gl_path.git"
git push --mirror gitlab >>"$LOG" 2>&1 \
|| { echo "FAIL push $gl_path" | tee -a "$LOG"; cd "$WORKDIR"; rm -rf "$name.git"; return 1; }
cd "$WORKDIR"
rm -rf "$name.git"
echo "OK $gh_owner/$name -> $gl_path" | tee -a "$LOG"
}
run_source() {
local gh_owner="$1" gl_namespace="$2"
if [ "$TEST" -eq 1 ]; then
list_repos "$gh_owner" | head -1 | cut -f2 \
| while read -r r; do migrate_repo "$gh_owner" "$gl_namespace" "$r"; done
else
list_repos "$gh_owner" | cut -f2 \
| while read -r r; do migrate_repo "$gh_owner" "$gl_namespace" "$r"; done
fi
}
echo "Done. Log: $LOG"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment