get_all_src.sh
· 7.5 KiB · Bash
原始文件
#!/bin/bash
set -euo pipefail
# ── Colors ───────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ── Logging ──────────────────────────────────────────────────────────
log_info() { printf "${CYAN}[INFO]${NC} %s\n" "$*"; }
log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; }
log_ok() { printf "${GREEN}[ OK ]${NC} %s\n" "$*"; }
log_error() { printf "${RED}[ERR]${NC} %s\n" "$*" >&2; }
# ── Usage ────────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Clone every project from every group (and subgroups, recursively) on a
GitLab instance, preserving the path_with_namespace directory structure.
Options:
-h, --help Show this help
-t, --token-file=FILE File containing the GitLab personal access token
-g, --gitlab-url=URL GitLab instance URL (default: https://git.mws-team.ru)
Examples:
$(basename "$0") -t ~/.gitlab_token
$(basename "$0") --token-file=token.txt --gitlab-url=https://gitlab.com
EOF
exit 1
}
# ── Dependency check ─────────────────────────────────────────────────
for cmd in curl jq git; do
if ! command -v "$cmd" &>/dev/null; then
log_error "Required command not found: ${cmd}"
exit 1
fi
done
# ── Parse arguments ──────────────────────────────────────────────────
GITLAB_URL="https://git.mws-team.ru"
TOKEN_FILE=""
translated=()
while (( $# )); do
case "$1" in
--help) translated+=("-h"); shift ;;
--token-file=*) translated+=("-t" "${1#*=}"); shift ;;
--gitlab-url=*) translated+=("-g" "${1#*=}"); shift ;;
--) translated+=("--"); shift; translated+=("$@"); break ;;
*) translated+=("$1"); shift ;;
esac
done
set -- "${translated[@]}"
while getopts ":ht:g:" opt; do
case "$opt" in
h) usage ;;
t) TOKEN_FILE="$OPTARG" ;;
g) GITLAB_URL="$OPTARG" ;;
:) log_error "Missing argument for -${OPTARG}"; usage ;;
\?) log_error "Invalid option: -${OPTARG}"; usage ;;
esac
done
shift $((OPTIND - 1))
# ── Validate inputs ─────────────────────────────────────────────────
[[ -z "$TOKEN_FILE" ]] && { log_error "Missing --token-file"; usage; }
[[ -z "$GITLAB_URL" ]] && { log_error "Missing --gitlab-url"; usage; }
[[ ! -f "$TOKEN_FILE" ]] && { log_error "Token file not found: ${TOKEN_FILE}"; exit 1; }
GITLAB_URL="${GITLAB_URL%/}"
TOKEN="$(<"$TOKEN_FILE")"
readonly TOKEN GITLAB_URL
# ── GitLab API helpers ───────────────────────────────────────────────
# Authenticated GET request; returns JSON body.
gitlab_get() {
curl --silent --fail --header "Authorization: Bearer ${TOKEN}" "$1"
}
# Return total page count for a paginated endpoint.
gitlab_total_pages() {
local headers
headers="$(curl -X HEAD --silent -w '%{header_json}' \
--header "Authorization: Bearer ${TOKEN}" \
-o /dev/null "$1" 2>/dev/null)" || true
local pages
pages="$(echo "$headers" | jq -r '."x-total-pages"[0] // "1"')"
[[ -z "$pages" || "$pages" == "null" ]] && pages=1
echo "$pages"
}
# Iterate all pages of an endpoint, call a callback with each extracted value.
# Usage: gitlab_paginate "URL?per_page=100" '.[].id' callback_fn
gitlab_paginate() {
local base_url="$1"
local jq_filter="$2"
local callback="$3"
local total_pages
total_pages="$(gitlab_total_pages "$base_url")"
local page
for (( page = 1; page <= total_pages; page++ )); do
local response
response="$(gitlab_get "${base_url}&page=${page}")" || {
log_warn "API request failed: ${base_url}&page=${page}"
continue
}
local item
while IFS= read -r item; do
[[ -z "$item" ]] && continue
"$callback" "$item"
done < <(echo "$response" | jq -r "$jq_filter")
done
}
# ── Clone a single project ──────────────────────────────────────────
CLONE_COUNT=0
SKIP_COUNT=0
clone_project() {
local project_id="$1"
local project_json
project_json="$(gitlab_get "${GITLAB_URL}/api/v4/projects/${project_id}")" || {
log_warn "Failed to fetch project ${project_id}"
return
}
local path_ns ssh_url
path_ns="$(echo "$project_json" | jq -r '.path_with_namespace')"
ssh_url="$(echo "$project_json" | jq -r '.ssh_url_to_repo')"
if [[ -z "$path_ns" || "$path_ns" == "null" ]]; then
log_warn "Could not determine path for project ${project_id} — skipping"
return
fi
if [[ -d "$path_ns" ]]; then
log_info "Already cloned: ${path_ns}"
(( SKIP_COUNT++ )) || true
return
fi
log_info "Cloning: ${path_ns}"
mkdir -p "$(dirname "$path_ns")"
if git clone --quiet "$ssh_url" "$path_ns"; then
log_ok "Cloned: ${path_ns}"
(( CLONE_COUNT++ )) || true
else
log_warn "git clone failed for ${path_ns}"
fi
}
# ── Clone all projects in a group ────────────────────────────────────
clone_group_projects() {
local group_id="$1"
gitlab_paginate \
"${GITLAB_URL}/api/v4/groups/${group_id}/projects?per_page=100&archived=no" \
'.[].id' clone_project
}
# ── Process a group and all its subgroups recursively ────────────────
process_group() {
local group_id="$1"
clone_group_projects "$group_id"
# Collect subgroup IDs, then recurse
local subgroup_ids=()
while IFS= read -r sid; do
[[ -z "$sid" ]] && continue
subgroup_ids+=("$sid")
done < <(
local total
total="$(gitlab_total_pages "${GITLAB_URL}/api/v4/groups/${group_id}/subgroups?per_page=100")"
for (( p = 1; p <= total; p++ )); do
gitlab_get "${GITLAB_URL}/api/v4/groups/${group_id}/subgroups?per_page=100&page=${p}" \
| jq -r '.[].id'
done
)
local sid
for sid in "${subgroup_ids[@]}"; do
log_info "Entering subgroup ${sid}"
process_group "$sid"
done
}
# ── Main ─────────────────────────────────────────────────────────────
main() {
log_info "GitLab URL: ${GITLAB_URL}"
log_info "Token file: ${TOKEN_FILE}"
echo
# Iterate over every top-level group visible to the token
gitlab_paginate \
"${GITLAB_URL}/api/v4/groups?per_page=100&top_level_only=true" \
'.[].id' process_group
echo
log_ok "Done — cloned ${CLONE_COUNT} repo(s), skipped ${SKIP_COUNT} already present"
}
main
| 1 | #!/bin/bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | # ── Colors ─────────────────────────────────────────────────────────── |
| 5 | RED='\033[0;31m' |
| 6 | GREEN='\033[0;32m' |
| 7 | YELLOW='\033[1;33m' |
| 8 | CYAN='\033[0;36m' |
| 9 | NC='\033[0m' |
| 10 | |
| 11 | # ── Logging ────────────────────────────────────────────────────────── |
| 12 | log_info() { printf "${CYAN}[INFO]${NC} %s\n" "$*"; } |
| 13 | log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; } |
| 14 | log_ok() { printf "${GREEN}[ OK ]${NC} %s\n" "$*"; } |
| 15 | log_error() { printf "${RED}[ERR]${NC} %s\n" "$*" >&2; } |
| 16 | |
| 17 | # ── Usage ──────────────────────────────────────────────────────────── |
| 18 | usage() { |
| 19 | cat <<EOF |
| 20 | Usage: $(basename "$0") [OPTIONS] |
| 21 | |
| 22 | Clone every project from every group (and subgroups, recursively) on a |
| 23 | GitLab instance, preserving the path_with_namespace directory structure. |
| 24 | |
| 25 | Options: |
| 26 | -h, --help Show this help |
| 27 | -t, --token-file=FILE File containing the GitLab personal access token |
| 28 | -g, --gitlab-url=URL GitLab instance URL (default: https://git.mws-team.ru) |
| 29 | |
| 30 | Examples: |
| 31 | $(basename "$0") -t ~/.gitlab_token |
| 32 | $(basename "$0") --token-file=token.txt --gitlab-url=https://gitlab.com |
| 33 | EOF |
| 34 | exit 1 |
| 35 | } |
| 36 | |
| 37 | # ── Dependency check ───────────────────────────────────────────────── |
| 38 | for cmd in curl jq git; do |
| 39 | if ! command -v "$cmd" &>/dev/null; then |
| 40 | log_error "Required command not found: ${cmd}" |
| 41 | exit 1 |
| 42 | fi |
| 43 | done |
| 44 | |
| 45 | # ── Parse arguments ────────────────────────────────────────────────── |
| 46 | GITLAB_URL="https://git.mws-team.ru" |
| 47 | TOKEN_FILE="" |
| 48 | |
| 49 | translated=() |
| 50 | while (( $# )); do |
| 51 | case "$1" in |
| 52 | --help) translated+=("-h"); shift ;; |
| 53 | --token-file=*) translated+=("-t" "${1#*=}"); shift ;; |
| 54 | --gitlab-url=*) translated+=("-g" "${1#*=}"); shift ;; |
| 55 | --) translated+=("--"); shift; translated+=("$@"); break ;; |
| 56 | *) translated+=("$1"); shift ;; |
| 57 | esac |
| 58 | done |
| 59 | set -- "${translated[@]}" |
| 60 | |
| 61 | while getopts ":ht:g:" opt; do |
| 62 | case "$opt" in |
| 63 | h) usage ;; |
| 64 | t) TOKEN_FILE="$OPTARG" ;; |
| 65 | g) GITLAB_URL="$OPTARG" ;; |
| 66 | :) log_error "Missing argument for -${OPTARG}"; usage ;; |
| 67 | \?) log_error "Invalid option: -${OPTARG}"; usage ;; |
| 68 | esac |
| 69 | done |
| 70 | shift $((OPTIND - 1)) |
| 71 | |
| 72 | # ── Validate inputs ───────────────────────────────────────────────── |
| 73 | [[ -z "$TOKEN_FILE" ]] && { log_error "Missing --token-file"; usage; } |
| 74 | [[ -z "$GITLAB_URL" ]] && { log_error "Missing --gitlab-url"; usage; } |
| 75 | [[ ! -f "$TOKEN_FILE" ]] && { log_error "Token file not found: ${TOKEN_FILE}"; exit 1; } |
| 76 | |
| 77 | GITLAB_URL="${GITLAB_URL%/}" |
| 78 | |
| 79 | TOKEN="$(<"$TOKEN_FILE")" |
| 80 | readonly TOKEN GITLAB_URL |
| 81 | |
| 82 | # ── GitLab API helpers ─────────────────────────────────────────────── |
| 83 | |
| 84 | # Authenticated GET request; returns JSON body. |
| 85 | gitlab_get() { |
| 86 | curl --silent --fail --header "Authorization: Bearer ${TOKEN}" "$1" |
| 87 | } |
| 88 | |
| 89 | # Return total page count for a paginated endpoint. |
| 90 | gitlab_total_pages() { |
| 91 | local headers |
| 92 | headers="$(curl -X HEAD --silent -w '%{header_json}' \ |
| 93 | --header "Authorization: Bearer ${TOKEN}" \ |
| 94 | -o /dev/null "$1" 2>/dev/null)" || true |
| 95 | |
| 96 | local pages |
| 97 | pages="$(echo "$headers" | jq -r '."x-total-pages"[0] // "1"')" |
| 98 | [[ -z "$pages" || "$pages" == "null" ]] && pages=1 |
| 99 | echo "$pages" |
| 100 | } |
| 101 | |
| 102 | # Iterate all pages of an endpoint, call a callback with each extracted value. |
| 103 | # Usage: gitlab_paginate "URL?per_page=100" '.[].id' callback_fn |
| 104 | gitlab_paginate() { |
| 105 | local base_url="$1" |
| 106 | local jq_filter="$2" |
| 107 | local callback="$3" |
| 108 | |
| 109 | local total_pages |
| 110 | total_pages="$(gitlab_total_pages "$base_url")" |
| 111 | |
| 112 | local page |
| 113 | for (( page = 1; page <= total_pages; page++ )); do |
| 114 | local response |
| 115 | response="$(gitlab_get "${base_url}&page=${page}")" || { |
| 116 | log_warn "API request failed: ${base_url}&page=${page}" |
| 117 | continue |
| 118 | } |
| 119 | |
| 120 | local item |
| 121 | while IFS= read -r item; do |
| 122 | [[ -z "$item" ]] && continue |
| 123 | "$callback" "$item" |
| 124 | done < <(echo "$response" | jq -r "$jq_filter") |
| 125 | done |
| 126 | } |
| 127 | |
| 128 | # ── Clone a single project ────────────────────────────────────────── |
| 129 | CLONE_COUNT=0 |
| 130 | SKIP_COUNT=0 |
| 131 | |
| 132 | clone_project() { |
| 133 | local project_id="$1" |
| 134 | |
| 135 | local project_json |
| 136 | project_json="$(gitlab_get "${GITLAB_URL}/api/v4/projects/${project_id}")" || { |
| 137 | log_warn "Failed to fetch project ${project_id}" |
| 138 | return |
| 139 | } |
| 140 | |
| 141 | local path_ns ssh_url |
| 142 | path_ns="$(echo "$project_json" | jq -r '.path_with_namespace')" |
| 143 | ssh_url="$(echo "$project_json" | jq -r '.ssh_url_to_repo')" |
| 144 | |
| 145 | if [[ -z "$path_ns" || "$path_ns" == "null" ]]; then |
| 146 | log_warn "Could not determine path for project ${project_id} — skipping" |
| 147 | return |
| 148 | fi |
| 149 | |
| 150 | if [[ -d "$path_ns" ]]; then |
| 151 | log_info "Already cloned: ${path_ns}" |
| 152 | (( SKIP_COUNT++ )) || true |
| 153 | return |
| 154 | fi |
| 155 | |
| 156 | log_info "Cloning: ${path_ns}" |
| 157 | mkdir -p "$(dirname "$path_ns")" |
| 158 | if git clone --quiet "$ssh_url" "$path_ns"; then |
| 159 | log_ok "Cloned: ${path_ns}" |
| 160 | (( CLONE_COUNT++ )) || true |
| 161 | else |
| 162 | log_warn "git clone failed for ${path_ns}" |
| 163 | fi |
| 164 | } |
| 165 | |
| 166 | # ── Clone all projects in a group ──────────────────────────────────── |
| 167 | clone_group_projects() { |
| 168 | local group_id="$1" |
| 169 | gitlab_paginate \ |
| 170 | "${GITLAB_URL}/api/v4/groups/${group_id}/projects?per_page=100&archived=no" \ |
| 171 | '.[].id' clone_project |
| 172 | } |
| 173 | |
| 174 | # ── Process a group and all its subgroups recursively ──────────────── |
| 175 | process_group() { |
| 176 | local group_id="$1" |
| 177 | |
| 178 | clone_group_projects "$group_id" |
| 179 | |
| 180 | # Collect subgroup IDs, then recurse |
| 181 | local subgroup_ids=() |
| 182 | while IFS= read -r sid; do |
| 183 | [[ -z "$sid" ]] && continue |
| 184 | subgroup_ids+=("$sid") |
| 185 | done < <( |
| 186 | local total |
| 187 | total="$(gitlab_total_pages "${GITLAB_URL}/api/v4/groups/${group_id}/subgroups?per_page=100")" |
| 188 | for (( p = 1; p <= total; p++ )); do |
| 189 | gitlab_get "${GITLAB_URL}/api/v4/groups/${group_id}/subgroups?per_page=100&page=${p}" \ |
| 190 | | jq -r '.[].id' |
| 191 | done |
| 192 | ) |
| 193 | |
| 194 | local sid |
| 195 | for sid in "${subgroup_ids[@]}"; do |
| 196 | log_info "Entering subgroup ${sid}" |
| 197 | process_group "$sid" |
| 198 | done |
| 199 | } |
| 200 | |
| 201 | # ── Main ───────────────────────────────────────────────────────────── |
| 202 | main() { |
| 203 | log_info "GitLab URL: ${GITLAB_URL}" |
| 204 | log_info "Token file: ${TOKEN_FILE}" |
| 205 | echo |
| 206 | |
| 207 | # Iterate over every top-level group visible to the token |
| 208 | gitlab_paginate \ |
| 209 | "${GITLAB_URL}/api/v4/groups?per_page=100&top_level_only=true" \ |
| 210 | '.[].id' process_group |
| 211 | |
| 212 | echo |
| 213 | log_ok "Done — cloned ${CLONE_COUNT} repo(s), skipped ${SKIP_COUNT} already present" |
| 214 | } |
| 215 | |
| 216 | main |