Last active
June 8, 2026 19:34
-
-
Save cesasol/616da45a5751bb765dff941df9d167e1 to your computer and use it in GitHub Desktop.
Script to grab all your repos from github, gitlab and codeberg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| # Dependencies: | |
| # gh - GitHub CLI, authenticated with: gh auth login | |
| # glab - GitLab CLI, authenticated with: glab auth login | |
| # tea - Gitea/Forgejo CLI, configured for Codeberg | |
| # jq | |
| # fzf - only required for interactive selection | |
| # git-grab - provides: git grab | |
| # | |
| # Example Codeberg login: | |
| # tea login add --name codeberg --url https://codeberg.org | |
| # | |
| # Usage: | |
| # ./git supergrab # Select repositories interactively | |
| # ./git supergrab --list # List accessible repositories | |
| # ./git supergrab --all # Clone every listed repository | |
| # | |
| # Optional: | |
| # GRAB_HOME="$HOME/src" ./grab-repos.sh | |
| die() { | |
| printf 'error: %s\n' "$*" >&2 | |
| exit 1 | |
| } | |
| require() { | |
| command -v "$1" >/dev/null 2>&1 || | |
| die "required command not found: $1" | |
| } | |
| github_repos() { | |
| # Includes repositories owned by the user, collaborations, and | |
| # repositories available through organization membership. | |
| gh api --paginate \ | |
| -H 'Accept: application/vnd.github+json' \ | |
| '/user/repos?per_page=100&affiliation=owner,collaborator,organization_member' \ | |
| --jq '.[] | ["github", .full_name, .ssh_url] | @tsv' | |
| } | |
| gitlab_repos() { | |
| # membership=true returns projects where the authenticated user | |
| # is an explicit member. | |
| glab api --paginate \ | |
| 'projects?membership=true&simple=true&per_page=100&order_by=path&sort=asc' | | |
| jq -r '.[] | ["gitlab", .path_with_namespace, .ssh_url_to_repo] | @tsv' | |
| } | |
| codeberg_repos() { | |
| # tea's JSON field names have varied between releases, so this accepts | |
| # the common snake_case and camelCase variants. | |
| tea repos list \ | |
| --login codeberg.org \ | |
| --limit 1000 \ | |
| --output json | | |
| jq -r ' | |
| def owner_login: | |
| (.owner | if type == "object" then (.login // .username) else . end); | |
| .[] | | |
| [ | |
| "codeberg", | |
| ( | |
| .full_name // | |
| .fullName // | |
| ((owner_login) + "/" + .name) | |
| ), | |
| ( | |
| .ssh // | |
| .ssh_url // | |
| .sshUrl // | |
| .ssh_clone_url // | |
| .sshCloneUrl | |
| ) | |
| ] | | |
| select(.[1] != null and .[2] != null) | | |
| @tsv | |
| ' | |
| } | |
| list_repos() { | |
| { | |
| github_repos || printf 'warning: failed to query GitHub\n' >&2 | |
| gitlab_repos || printf 'warning: failed to query GitLab\n' >&2 | |
| codeberg_repos || printf 'warning: failed to query Codeberg\n' >&2 | |
| } | awk -F '\t' '!seen[$3]++' | sort -t $'\t' -k1,1 -k2,2 | |
| } | |
| clone_repos() { | |
| local row provider name ssh_url | |
| while IFS=$'\t' read -r provider name ssh_url; do | |
| [[ -n "${ssh_url:-}" ]] || continue | |
| printf '\n[%s] %s\n' "$provider" "$name" | |
| if ! git grab "$ssh_url"; then | |
| printf 'warning: failed to grab %s (%s)\n' "$name" "$ssh_url" >&2 | |
| fi | |
| done | |
| } | |
| main() { | |
| require jq | |
| require git | |
| require git-grab | |
| local mode="${1:-interactive}" | |
| local repos | |
| case "$mode" in | |
| --list) | |
| require gh | |
| require glab | |
| require tea | |
| list_repos | | |
| awk -F '\t' 'BEGIN { | |
| printf "%-10s %-55s %s\n", "PROVIDER", "REPOSITORY", "SSH URL" | |
| } { | |
| printf "%-10s %-55s %s\n", $1, $2, $3 | |
| }' | |
| ;; | |
| --all) | |
| require gh | |
| require glab | |
| require tea | |
| list_repos | clone_repos | |
| ;; | |
| interactive) | |
| require gh | |
| require glab | |
| require tea | |
| require fzf | |
| repos="$(list_repos)" | |
| [[ -n "$repos" ]] || die "no repositories were returned" | |
| printf '%s\n' "$repos" | | |
| fzf \ | |
| --multi \ | |
| --delimiter=$'\t' \ | |
| --with-nth=1,2 \ | |
| --bind='space:toggle,ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all' \ | |
| --header='TAB/SPACE: toggle C-a: all C-d: none ENTER: clone selected ESC: cancel' \ | |
| --prompt='Repository> ' | | |
| clone_repos | |
| ;; | |
| *) | |
| die "usage: $0 [--list|--all]" | |
| ;; | |
| esac | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment