Skip to content

Instantly share code, notes, and snippets.

@cesasol
Last active June 8, 2026 19:34
Show Gist options
  • Select an option

  • Save cesasol/616da45a5751bb765dff941df9d167e1 to your computer and use it in GitHub Desktop.

Select an option

Save cesasol/616da45a5751bb765dff941df9d167e1 to your computer and use it in GitHub Desktop.
Script to grab all your repos from github, gitlab and codeberg
#!/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