Skip to content

Instantly share code, notes, and snippets.

@Potherca
Last active September 25, 2025 10:19
Show Gist options
  • Save Potherca/9dd4306eb27cad26d27874acd4b31376 to your computer and use it in GitHub Desktop.
Save Potherca/9dd4306eb27cad26d27874acd4b31376 to your computer and use it in GitHub Desktop.
BASH script to clone all git repository in a Group on GitLab, or Organization on GitHub.

Introduction

Starting at a new employer always mean checking out various git repositories.

As the amount of repositories a company has grows, the time needed to clone all of those repositories also grows.

This script automates this task.

In order for this script to work, a personal access token is needed.

To avoid unexpected behaviour, use Group's ID, rather than its key/name. Informations about Groups can be found in the API at https://gitlab.example.com/api/v4/groups.

Usage

Call bash gitlab-clone-projects.sh <gitlab-domain> <group-name> <gitlab-token>

For full usage details gitlab-clone-projects --help is available:

Usage: gitlab-clone-projects.sh [-dh] <gitlab-domain> <group-id> <gitlab-token>

Where:
      - <gitlab-domain> is the domain where gitlab lives (for instance: 'gitlab.com')
      - <group-id> is the ID of the group who's repos should be cloned
      - <gitlab-token> is the API access token to make REST API calls with

Options:
  -d|--dry-run   Only list the repositories, without actually cloning them
  -h|--help      Print this help dialogue and exit
  -u|--user      The given ID is a user, not a group

The repositories will be cloned into a sub-directory under the path from Where
this script has been called. The repository will be cloned into ./${group-id}/${repo-name}

The git executable can be overridden by setting the GIT environmental variable
before calling this script:

       GIT=/usr/local/git-plus gitlab-clone-projects.sh <gitlab-domain> <group-id> <gitlab-token>
#!/usr/bin/env bash
# ==============================================================================
# Copyright (C) 2021-2023 Potherca
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# ==============================================================================
# There are a few standards this code tries to adhere to, these are listed below.
#
# - Code follows the BASH style-guide described at:
# http://guides.dealerdirect.io/code-styling/bash/
#
# - Variables are named using an adaption of Systems Hungarian explained at:
# http://blog.pother.ca/VariableNamingConvention
#
# ==============================================================================
set -o errexit # Exit script when a command exits with non-zero status.
set -o errtrace # Exit on error inside any functions or sub-shells.
set -o nounset # Exit script on use of an undefined variable.
set -o pipefail # Return exit status of the last command in the pipe that exited with a non-zero exit code
# ==============================================================================
## Git Clone All Projects in GitHub Organisation
# ------------------------------------------------------------------------------
## Usage: $0 [-dhu] <domain> <organisation-id> <api-token>
##
## Where:
## - <domain> is the domain where GitHub lives (for instance: 'github.com')
## - <organization-id> is the ID of the organization whose repos should be cloned
## - <api-token> is the API access token to make REST API calls with
##
## Options:
## -d|--dry-run Only list the repositories, without actually cloning them
## -h|--help Print this help dialogue and exit
## -u|--user The given ID is a user, not an organization
##
## The repositories will be cloned into a sub-directory under the path from Where
## this script has been called. The repository will be cloned into ./${organisation-id}/${repo-name}
##
## The git and cUrl executable can be overridden by setting their respective environmental variable
## before calling this script:
##
## CURL=/usr/local/curl GIT=/usr/local/git-plus $0 <domain> <organisation-id> <api-token>
# ==============================================================================
: readonly "${CURL:=curl}"
: readonly "${GIT:=git}"
usage() {
local sScript sUsage
sScript="$(basename "$0")"
readonly sScript
sUsage="$(grep '^##' < "$0" | cut -c4-)"
readonly sUsage
echo -e "${sUsage//\$0/${sScript}}"
}
github-clone-projects() {
call-url() {
local sToken sHeaderResult sPaginationUrl sPrevious sResult sUrl
readonly sToken="${1?Two parameters required: <api-token> <url> [previous-content]}"
readonly sUrl="${2?Two parameters required: <api-token> <url> [previous-content]}"
readonly sPrevious="${3:-''}"
sHeaderResult=$("${CURL}" \
--head \
--header "Accept: application/vnd.github.v3+json" \
--header "Authorization: token ${sToken}" \
--silent \
"${sUrl}")
sResult=$("${CURL}" \
--header "Accept: application/vnd.github.v3+json" \
--header "Authorization: token ${sToken}" \
--silent \
"${sUrl}")
sPaginationUrl=$(
echo "${sHeaderResult}" \
| grep -oE '<[^>]+>; rel="next"' \
| grep -oE 'https?://[^>]+'
) || true
if [[ "${sPaginationUrl}" == '' ]]; then
printf '%s\n%s' "${sPrevious}" "${sResult}"
else
call-url "${sToken}" "${sPaginationUrl}" "${sResult}"
fi
}
call-api() {
local sToken sUrl
readonly sToken="${1?Two parameters required: <api-token> <url>}"
readonly sUrl="${2?Two parameters required: <api-token> <url>}"
call-url "${sToken}" "${sUrl}"
}
fetch-projects() {
local iId sDomain sToken sSubject
readonly sToken="${1?Four parameters required: <api-token> <domain> <subject> <id>}"
readonly sDomain="${2?Four parameters required: <api-token> <domain> <subject> <id>}"
readonly sSubject="${3?Four parameters required: <api-token> <domain> <subject> <id>}"
readonly iId="${4?Four parameters required: <api-token> <domain> <subject> <id>}"
call-api "${sToken}" "https://api.${sDomain}/${sSubject}/${iId}/repos?per_page=100" \
| grep -E -o '"ssh_url"\s*:\s*"[^"]+"' \
| cut -d '"' -f4
}
fetch-organisation-projects() {
local iId sDomain sToken
readonly sToken="${1?Three parameters required: <api-token> <domain> <id>}"
readonly sDomain="${2?Three parameters required: <api-token> <domain> <id>}"
readonly iId="${3?Three parameters required: <api-token> <domain> <id>}"
fetch-projects "${sToken}" "${sDomain}" 'orgs' "${iId}"
}
fetch-user-projects() {
local iId sDomain sToken
readonly sToken="${1?Three parameters required: <api-token> <domain> <id>}"
readonly sDomain="${2?Three parameters required: <api-token> <domain> <id>}"
readonly iId="${3?Three parameters required: <api-token> <domain> <id>}"
fetch-projects "${sToken}" "${sDomain}" 'users' "${iId}"
}
local -a aParameters=() aRepos=()
local bDryRun=false bIsUser=false
local sDirectory sDomain sToken sId sRepo sRepos
for sArg in "$@"; do
case "${sArg}" in
-h | --help)
usage
exit
;;
-d | --dry-run)
readonly bDryRun=true
shift
;;
-u | --user)
# @TODO: Is there a way we can detect if this is a user or organisation?
readonly bIsUser=true
shift
;;
*)
aParameters+=("$1")
shift
;;
esac
done
readonly aParameters
readonly sDomain="${aParameters[0]?Three parameters required: <domain> <organisation-id> <api-token>}"
readonly sId="${aParameters[1]?Three parameters required: <domain> <organisation-id> <api-token>}"
readonly sToken="${aParameters[2]?Three parameters required: <domain> <organisation-id> <api-token>}"
if [[ ${bIsUser} == 'true' ]]; then
sRepos="$(fetch-user-projects "${sToken}" "${sDomain}" "${sId}")"
else
sRepos="$(fetch-organisation-projects "${sToken}" "${sDomain}" "${sId}")"
fi
readonly sRepos
while read -r sRepo; do
aRepos+=("${sRepo}")
done <<< "${sRepos}"
readonly aRepos
echo ' =====> Found ' ${#aRepos[@]} ' repositories'
for sRepo in "${aRepos[@]}"; do
# Grab repo name
sDirectory="$(echo "${sRepo}" | grep -o -E ':(.*)\.')"
# Lowercase the name
sDirectory="$(echo "${sDirectory}" | tr '[:upper:]' '[:lower:]')"
# Append the current location
sDirectory="$(realpath --canonicalize-missing --relative-to=./ "${sDirectory:1:-1}")"
if [[ -d ${sDirectory} ]]; then
echo " -----> Skipping '${sRepo}', directory '${sDirectory}' already exists"
else
echo " -----> Cloning '${sRepo}' into directory '${sDirectory}'"
if [[ ${bDryRun} != 'true' ]]; then
mkdir -p "${sDirectory}"
"${GIT}" clone --recursive "${sRepo}" "${sDirectory}" \
|| {
rm -rf "${sDirectory}"
echo -e "\n ! ERROR !\n Could not clone ${sRepo}"
}
echo ""
fi
fi
done
}
if [[ ${BASH_SOURCE[0]} != "$0" ]]; then
export -f github-clone-projects
else
github-clone-projects "${@}"
exit $?
fi
#EOF
#!/usr/bin/env bash
# ==============================================================================
# Copyright (C) 2018, 2020-2023 Potherca
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# ==============================================================================
# There are a few standards this code tries to adhere to, these are listed below.
#
# - Code follows the BASH style-guide described at:
# http://guides.dealerdirect.io/code-styling/bash/
#
# - Variables are named using an adaption of Systems Hungarian explained at:
# http://blog.pother.ca/VariableNamingConvention
#
# ==============================================================================
set -o errexit # Exit script when a command exits with non-zero status.
set -o errtrace # Exit on error inside any functions or sub-shells.
set -o nounset # Exit script on use of an undefined variable.
set -o pipefail # Return exit status of the last command in the pipe that exited with a non-zero exit code
# ==============================================================================
## Git Clone All Projects in Gitlab Group
# ------------------------------------------------------------------------------
## Usage: $0 [-dhu] <domain> <group-id> <api-token>
##
## Where:
## - <domain> is the domain where gitlab lives (for instance: 'gitlab.com')
## - <group-id> is the ID of the group whose repos should be cloned
## - <api-token> is the API access token to make REST API calls with
##
## Options:
## -d|--dry-run Only list the repositories, without actually cloning them
## -h|--help Print this help dialogue and exit
## -u|--user The given ID is a user, not a group
##
## The repositories will be cloned into a sub-directory under the path from Where
## this script has been called. The repository will be cloned into ./${group-id}/${repo-name}
##
## The git and cUrl executable can be overridden by setting their respective environmental variable
## before calling this script:
##
## CURL=/usr/local/curl GIT=/usr/local/git-plus $0 <domain> <group-id> <api-token>
# ==============================================================================
: readonly "${CURL:=curl}"
: readonly "${GIT:=git}"
usage() {
local sScript sUsage
sScript="$(basename "$0")"
readonly sScript
sUsage="$(grep '^##' < "$0" | cut -c4-)"
readonly sUsage
echo -e "${sUsage//\$0/${sScript}}"
}
gitlab-clone-projects() {
call-url() {
local sToken sHeaderResult sPaginationUrl sPrevious sResult sUrl
readonly sToken="${1?Two parameters required: <api-token> <url> [previous-content]}"
readonly sUrl="${2?Two parameters required: <api-token> <url> [previous-content]}"
readonly sPrevious="${3:-''}"
sHeaderResult=$("${CURL}" \
--head \
--header "PRIVATE-TOKEN: ${sToken}" \
--silent \
"${sUrl}")
sResult=$("${CURL}" \
--header "PRIVATE-TOKEN: ${sToken}" \
--silent \
"${sUrl}")
sPaginationUrl=$(
echo "${sHeaderResult}" \
| grep -oE '<[^>]+>; rel="next"' \
| grep -oE 'https?://[^>]+'
) || true
if [[ "${sPaginationUrl}" == '' ]]; then
printf '%s\n%s' "${sPrevious}" "${sResult}"
else
call-url "${sToken}" "${sPaginationUrl}" "${sResult}"
fi
}
call-api() {
local sToken sUrl
readonly sToken="${1?Two parameters required: <api-token> <url>}"
readonly sUrl="${2?Two parameters required: <api-token> <url>}"
call-url "${sToken}" "${sUrl}"
}
fetch-projects() {
local iId sDomain sToken sSubject
readonly sToken="${1?Four parameters required: <api-token> <domain> <subject> <id>}"
readonly sDomain="${2?Four parameters required: <api-token> <domain> <subject> <id>}"
readonly sSubject="${3?Four parameters required: <api-token> <domain> <subject> <id>}"
readonly iId="${4?Four parameters required: <api-token> <domain> <subject> <id>}"
call-api "${sToken}" "https://${sDomain}/api/v4/${sSubject}/${iId}/projects?per_page=100" \
| grep -E -o '"ssh_url_to_repo"\s*:\s*"[^"]+"' \
| cut -d '"' -f4
}
fetch-group-projects() {
local iId sDomain sToken
readonly sToken="${1?Three parameters required: <api-token> <domain> <id>}"
readonly sDomain="${2?Three parameters required: <api-token> <domain> <id>}"
readonly iId="${3?Three parameters required: <api-token> <domain> <id>}"
fetch-projects "${sToken}" "${sDomain}" 'groups' "${iId}"
}
fetch-user-projects() {
local iId sDomain sToken
readonly sToken="${1?Three parameters required: <api-token> <domain> <id>}"
readonly sDomain="${2?Three parameters required: <api-token> <domain> <id>}"
readonly iId="${3?Three parameters required: <api-token> <domain> <id>}"
fetch-projects "${sToken}" "${sDomain}" 'users' "${iId}"
}
local -a aParameters=() aRepos=()
local bDryRun=false bIsUser=false
local sDirectory sDomain sToken sId sRepo sRepos
for sArg in "$@"; do
case "${sArg}" in
-h | --help)
usage
exit
;;
-d | --dry-run)
readonly bDryRun=true
shift
;;
-u | --user)
# @TODO: Is there a way we can detect if this is a user or organisation?
readonly bIsUser=true
shift
;;
*)
aParameters+=("$1")
shift
;;
esac
done
readonly aParameters
readonly sDomain="${aParameters[0]?Three parameters required: <domain> <group-id> <api-token>}"
readonly sId="${aParameters[1]?Three parameters required: <domain> <group-id> <api-token>}"
readonly sToken="${aParameters[2]?Three parameters required: <domain> <group-id> <api-token>}"
if [[ ${bIsUser} == 'true' ]]; then
sRepos="$(fetch-user-projects "${sToken}" "${sDomain}" "${sId}")"
else
sRepos="$(fetch-group-projects "${sToken}" "${sDomain}" "${sId}")"
fi
readonly sRepos
while read -r sRepo; do
aRepos+=("${sRepo}")
done <<< "${sRepos}"
readonly aRepos
echo ' =====> Found ' ${#aRepos[@]} ' repositories'
for sRepo in "${aRepos[@]}"; do
# Grab repo name
sDirectory="$(echo "${sRepo}" | grep -o -E ':(.*)\.')"
# Lowercase the name
sDirectory="$(echo "${sDirectory}" | tr '[:upper:]' '[:lower:]')"
# Append the current location
sDirectory="$(realpath --canonicalize-missing --relative-to=./ "${sDirectory:1:-1}")"
if [[ -d ${sDirectory} ]]; then
echo " -----> Skipping '${sRepo}', directory '${sDirectory}' already exists"
else
echo " -----> Cloning '${sRepo}' into directory '${sDirectory}'"
if [[ ${bDryRun} != 'true' ]]; then
mkdir -p "${sDirectory}"
"${GIT}" clone --recursive "${sRepo}" "${sDirectory}" \
|| {
rm -rf "${sDirectory}"
echo -e "\n ! ERROR !\n Could not clone ${sRepo}"
}
echo ""
fi
fi
done
}
if [[ ${BASH_SOURCE[0]} != "$0" ]]; then
export -f gitlab-clone-projects
else
gitlab-clone-projects "${@}"
exit $?
fi
#EOF
@Potherca
Copy link
Author

Note to self: Use parallel to speed things up (also available on alpine).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment