Last active 1 month ago

Получить исходники из конкретной группы

botanikanet revised this gist 1 month ago. Go to revision

1 file changed, 189 insertions, 81 deletions

get_group_src.sh

@@ -1,59 +1,53 @@
1 1 #!/bin/bash
2 + set -euo pipefail
2 3
3 - function usage {
4 - echo "basename ${0} Get src of group in Gitlab"
5 - echo "--help | -h Print this help"
6 - echo "--token-file= | -t <file> Set file with Gitlab token"
7 - echo "--gitlab-url=* | -g <url> Set Gtilab URL"
8 - echo "--group-id=* | -i <id> Set group id"
9 - exit 1
10 - }
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'
11 10
12 - function checks {
13 - if [ -z "$TOKEN_FILE" ]
14 - then
15 - echo "Missing token file"
16 - usage
17 - fi
18 -
19 - if [ -z "$GITLAB_URL" ]
20 - then
21 - echo "Missing gitlab url"
22 - usage
23 - fi
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; }
24 16
25 - if [ -z "$GROUP" ]
26 - then
27 - echo "Missing group id"
28 - usage
29 - fi
30 -
31 - if [ ! -f "$TOKEN_FILE" ]
32 - then
33 - echo "Token file not found"
34 - usage
35 - fi
36 - }
17 + # ── Usage ────────────────────────────────────────────────────────────
18 + usage() {
19 + cat <<EOF
20 + Usage: $(basename "$0") [OPTIONS]
37 21
38 - function get_subgroups {
39 - for PAGE in $(seq 1 $(curl -X HEAD -w '%{header_json}' --header "Authorization: Bearer ${TOKEN}" ${GITLAB_URL}/api/v4/groups/${1}/subgroups?per_page=100 2>/dev/null | jq -r '."x-total-pages"[0]'))
40 - do
41 - curl --silent --header "Authorization: Bearer ${TOKEN}" "${GITLAB_URL}/api/v4/groups/${1}/subgroups?per_page=100&page=${PAGE:1}" | jq -r '.[].id'
42 - done
22 + Clone all projects from a GitLab group (including subgroups) preserving
23 + 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 + -i, --group-id=ID GitLab group ID to clone
30 +
31 + Examples:
32 + $(basename "$0") -t ~/.gitlab_token -i 42
33 + $(basename "$0") --token-file=token.txt --gitlab-url=https://gitlab.com --group-id=123
34 + EOF
35 + exit 1
43 36 }
44 37
45 - function clone_project {
46 - PROJECT_INFO=$(curl --silent --header "Authorization: Bearer ${TOKEN}" ${GITLAB_URL}/api/v4/projects/${1} | jq .)
47 - echo "INFO: Check project $(echo ${PROJECT_INFO} | jq -r '.path_with_namespace')"
48 - if [ ! -d "$(echo ${PROJECT_INFO} | jq -r '.path_with_namespace')" ]
49 - then
50 - echo "INFO: Clone project $(echo ${PROJECT_INFO} | jq -r '.path_with_namespace')"
51 - mkdir -p $(echo ${PROJECT_INFO} | jq -r '.path_with_namespace')
52 - git clone $(echo ${PROJECT_INFO} | jq -r '. | "\(.ssh_url_to_repo) \(.path_with_namespace)"')
38 + # ── Dependency check ─────────────────────────────────────────────────
39 + for cmd in curl jq git; do
40 + if ! command -v "$cmd" &>/dev/null; then
41 + log_error "Required command not found: ${cmd}"
42 + exit 1
53 43 fi
54 - }
44 + done
45 +
46 + # ── Parse arguments ──────────────────────────────────────────────────
47 + GITLAB_URL="https://git.mws-team.ru"
48 + TOKEN_FILE=""
49 + GROUP=""
55 50
56 - # Map long options to short ones and rebuild argv
57 51 translated=()
58 52 while (( $# )); do
59 53 case "$1" in
@@ -61,54 +55,168 @@ while (( $# )); do
61 55 --token-file=*) translated+=("-t" "${1#*=}"); shift ;;
62 56 --gitlab-url=*) translated+=("-g" "${1#*=}"); shift ;;
63 57 --group-id=*) translated+=("-i" "${1#*=}"); shift ;;
64 - --) translated+=("--"); shift; translated+=("$@"); set -- ;;
58 + --) translated+=("--"); shift; translated+=("$@"); break ;;
65 59 *) translated+=("$1"); shift ;;
66 - esac
60 + esac
67 61 done
68 62 set -- "${translated[@]}"
69 63
70 - declare GITLAB_URL="https://git.mws-team.ru"
71 - declare TOKEN_FILE=""
72 - declare GROUP=""
73 -
74 64 while getopts ":ht:g:i:" opt; do
75 65 case "$opt" in
76 - h) usage ;;
77 - t) TOKEN_FILE=$OPTARG ;;
78 - g) GITLAB_URL=$OPTARG ;;
79 - i) GROUP=$OPTARG ;;
80 - :) echo "Missing arg for -$OPTARG"; usage ;;
81 - \?) echo "Invalid option: -$OPTARG"; usage ;;
66 + h) usage ;;
67 + t) TOKEN_FILE="$OPTARG" ;;
68 + g) GITLAB_URL="$OPTARG" ;;
69 + i) GROUP="$OPTARG" ;;
70 + :) log_error "Missing argument for -${OPTARG}"; usage ;;
71 + \?) log_error "Invalid option: -${OPTARG}"; usage ;;
82 72 esac
83 73 done
74 + shift $((OPTIND - 1))
75 +
76 + # ── Validate inputs ─────────────────────────────────────────────────
77 + [[ -z "$TOKEN_FILE" ]] && { log_error "Missing --token-file"; usage; }
78 + [[ -z "$GITLAB_URL" ]] && { log_error "Missing --gitlab-url"; usage; }
79 + [[ -z "$GROUP" ]] && { log_error "Missing --group-id"; usage; }
80 + [[ ! -f "$TOKEN_FILE" ]] && { log_error "Token file not found: ${TOKEN_FILE}"; exit 1; }
81 +
82 + # Strip trailing slash from URL
83 + GITLAB_URL="${GITLAB_URL%/}"
84 +
85 + TOKEN="$(<"$TOKEN_FILE")"
86 + readonly TOKEN GITLAB_URL GROUP
87 +
88 + # ── GitLab API helpers ───────────────────────────────────────────────
89 +
90 + # Perform an authenticated GET request and return the JSON body.
91 + gitlab_get() {
92 + local url="$1"
93 + curl --silent --fail --header "Authorization: Bearer ${TOKEN}" "$url"
94 + }
95 +
96 + # Return total number of pages for a paginated endpoint.
97 + # Usage: gitlab_total_pages "https://gitlab.com/api/v4/..."
98 + gitlab_total_pages() {
99 + local url="$1"
100 + local headers
101 + headers="$(curl -X HEAD --silent -w '%{header_json}' \
102 + --header "Authorization: Bearer ${TOKEN}" \
103 + -o /dev/null "$url" 2>/dev/null)" || true
104 +
105 + local pages
106 + pages="$(echo "$headers" | jq -r '."x-total-pages"[0] // "1"')"
107 + # Fallback to 1 if empty or null
108 + [[ -z "$pages" || "$pages" == "null" ]] && pages=1
109 + echo "$pages"
110 + }
111 +
112 + # Iterate over all pages of a paginated endpoint, streaming JSON arrays.
113 + # Calls a callback function with each item's specified jq field.
114 + # Usage: gitlab_paginate "/api/v4/groups/42/projects?per_page=100" ".[].id" callback_fn
115 + gitlab_paginate() {
116 + local endpoint="$1"
117 + local jq_filter="$2"
118 + local callback="$3"
119 + local extra_params="${4:-}"
120 +
121 + local base_url="${GITLAB_URL}${endpoint}"
122 + [[ -n "$extra_params" ]] && base_url="${base_url}&${extra_params}"
84 123
85 - checks
124 + local total_pages
125 + total_pages="$(gitlab_total_pages "$base_url")"
86 126
87 - declare -r TOKEN=$(cat ${TOKEN_FILE})
127 + local page
128 + for (( page = 1; page <= total_pages; page++ )); do
129 + local response
130 + response="$(gitlab_get "${base_url}&page=${page}")" || {
131 + log_warn "API request failed: ${base_url}&page=${page}"
132 + continue
133 + }
88 134
89 - for P_PAGE in $(seq 1 $(curl -X HEAD -w '%{header_json}' --header "Authorization: Bearer ${TOKEN}" ${GITLAB_URL}/api/v4/groups/${GROUP}/projects?per_page=100 2>/dev/null| jq -r '."x-total-pages"[0]'))
90 - do
91 - echo "INFO: Check Projects"
92 - for PROJECT in $(curl --silent --header "Authorization: Bearer ${TOKEN}" "${GITLAB_URL}/api/v4/groups/${GROUP}/projects?per_page=100&page=${P_PAGE}" | jq -r '.[].id')
93 - do
94 - clone_project ${PROJECT}
135 + local item
136 + while IFS= read -r item; do
137 + [[ -z "$item" ]] && continue
138 + "$callback" "$item"
139 + done < <(echo "$response" | jq -r "$jq_filter")
95 140 done
141 + }
96 142
97 - done
98 - for SG_PAGE in $(seq 1 $(curl -X HEAD -w '%{header_json}' --header "Authorization: Bearer ${TOKEN}" ${GITLAB_URL}/api/v4/groups/${GROUP}/subgroups?per_page=100 2>/dev/null | jq -r '."x-total-pages"[0]'))
99 - do
100 - for SUBGROUP in $(curl --silent --header "Authorization: Bearer ${TOKEN}" "${GITLAB_URL}/api/v4/groups/${GROUP}/subgroups?per_page=100&page=${SG_PAGE}" | jq -r '.[].id')
101 - do
102 - echo "INFO: Check Subgroups"
103 - for P_SG_PAGE in $(seq 1 $(curl -X HEAD -w '%{header_json}' --header "Authorization: Bearer ${TOKEN}" ${GITLAB_URL}/api/v4/groups/${SUBGROUP}/projects?per_page=100 2>/dev/null| jq -r '."x-total-pages"[0]'))
104 - do
105 - for PROJECT in $(curl --silent --header "Authorization: Bearer ${TOKEN}" "${GITLAB_URL}/api/v4/groups/${SUBGROUP}/projects?per_page=100&page=${P_SG_PAGE}" | jq -r '.[].id')
106 - do
107 - clone_project ${PROJECT}
108 - done
143 + # ── Clone a single project ──────────────────────────────────────────
144 + clone_project() {
145 + local project_id="$1"
146 +
147 + local project_json
148 + project_json="$(gitlab_get "${GITLAB_URL}/api/v4/projects/${project_id}")" || {
149 + log_warn "Failed to fetch project ${project_id}"
150 + return
151 + }
152 +
153 + local path_ns ssh_url
154 + path_ns="$(echo "$project_json" | jq -r '.path_with_namespace')"
155 + ssh_url="$(echo "$project_json" | jq -r '.ssh_url_to_repo')"
156 +
157 + if [[ -z "$path_ns" || "$path_ns" == "null" ]]; then
158 + log_warn "Could not determine path for project ${project_id} — skipping"
159 + return
160 + fi
161 +
162 + if [[ -d "$path_ns" ]]; then
163 + log_info "Already cloned: ${path_ns}"
164 + return
165 + fi
166 +
167 + log_info "Cloning: ${path_ns}"
168 + local parent_dir
169 + parent_dir="$(dirname "$path_ns")"
170 + mkdir -p "$parent_dir"
171 + git clone "$ssh_url" "$path_ns" --quiet || log_warn "git clone failed for ${path_ns}"
172 + }
173 +
174 + # ── Clone all projects in a group ────────────────────────────────────
175 + clone_group_projects() {
176 + local group_id="$1"
177 + log_info "Cloning projects in group ${group_id}"
178 + gitlab_paginate "/api/v4/groups/${group_id}/projects?per_page=100" \
179 + '.[].id' clone_project "archived=no"
180 + }
109 181
182 + # ── Process a group and all its subgroups recursively ────────────────
183 + process_group() {
184 + local group_id="$1"
185 +
186 + # Clone direct projects
187 + clone_group_projects "$group_id"
188 +
189 + # Recurse into subgroups
190 + local subgroup_ids=()
191 + while IFS= read -r sid; do
192 + [[ -z "$sid" ]] && continue
193 + subgroup_ids+=("$sid")
194 + done < <(
195 + local total
196 + total="$(gitlab_total_pages "${GITLAB_URL}/api/v4/groups/${group_id}/subgroups?per_page=100")"
197 + for (( p = 1; p <= total; p++ )); do
198 + gitlab_get "${GITLAB_URL}/api/v4/groups/${group_id}/subgroups?per_page=100&page=${p}" \
199 + | jq -r '.[].id'
110 200 done
201 + )
111 202
203 + for sid in "${subgroup_ids[@]}"; do
204 + log_info "Entering subgroup ${sid}"
205 + process_group "$sid"
112 206 done
113 - done
114 - echo "OK"
207 + }
208 +
209 + # ── Main ─────────────────────────────────────────────────────────────
210 + main() {
211 + log_info "GitLab URL: ${GITLAB_URL}"
212 + log_info "Group ID: ${GROUP}"
213 + log_info "Token file: ${TOKEN_FILE}"
214 + echo
215 +
216 + process_group "$GROUP"
217 +
218 + echo
219 + log_ok "Done"
220 + }
221 +
222 + main

botanikanet revised this gist 8 months ago. Go to revision

No changes

botanikanet revised this gist 8 months ago. Go to revision

1 file changed, 114 insertions

get_group_src.sh(file created)

@@ -0,0 +1,114 @@
1 + #!/bin/bash
2 +
3 + function usage {
4 + echo "basename ${0} Get src of group in Gitlab"
5 + echo "--help | -h Print this help"
6 + echo "--token-file= | -t <file> Set file with Gitlab token"
7 + echo "--gitlab-url=* | -g <url> Set Gtilab URL"
8 + echo "--group-id=* | -i <id> Set group id"
9 + exit 1
10 + }
11 +
12 + function checks {
13 + if [ -z "$TOKEN_FILE" ]
14 + then
15 + echo "Missing token file"
16 + usage
17 + fi
18 +
19 + if [ -z "$GITLAB_URL" ]
20 + then
21 + echo "Missing gitlab url"
22 + usage
23 + fi
24 +
25 + if [ -z "$GROUP" ]
26 + then
27 + echo "Missing group id"
28 + usage
29 + fi
30 +
31 + if [ ! -f "$TOKEN_FILE" ]
32 + then
33 + echo "Token file not found"
34 + usage
35 + fi
36 + }
37 +
38 + function get_subgroups {
39 + for PAGE in $(seq 1 $(curl -X HEAD -w '%{header_json}' --header "Authorization: Bearer ${TOKEN}" ${GITLAB_URL}/api/v4/groups/${1}/subgroups?per_page=100 2>/dev/null | jq -r '."x-total-pages"[0]'))
40 + do
41 + curl --silent --header "Authorization: Bearer ${TOKEN}" "${GITLAB_URL}/api/v4/groups/${1}/subgroups?per_page=100&page=${PAGE:1}" | jq -r '.[].id'
42 + done
43 + }
44 +
45 + function clone_project {
46 + PROJECT_INFO=$(curl --silent --header "Authorization: Bearer ${TOKEN}" ${GITLAB_URL}/api/v4/projects/${1} | jq .)
47 + echo "INFO: Check project $(echo ${PROJECT_INFO} | jq -r '.path_with_namespace')"
48 + if [ ! -d "$(echo ${PROJECT_INFO} | jq -r '.path_with_namespace')" ]
49 + then
50 + echo "INFO: Clone project $(echo ${PROJECT_INFO} | jq -r '.path_with_namespace')"
51 + mkdir -p $(echo ${PROJECT_INFO} | jq -r '.path_with_namespace')
52 + git clone $(echo ${PROJECT_INFO} | jq -r '. | "\(.ssh_url_to_repo) \(.path_with_namespace)"')
53 + fi
54 + }
55 +
56 + # Map long options to short ones and rebuild argv
57 + translated=()
58 + while (( $# )); do
59 + case "$1" in
60 + --help) translated+=("-h"); shift ;;
61 + --token-file=*) translated+=("-t" "${1#*=}"); shift ;;
62 + --gitlab-url=*) translated+=("-g" "${1#*=}"); shift ;;
63 + --group-id=*) translated+=("-i" "${1#*=}"); shift ;;
64 + --) translated+=("--"); shift; translated+=("$@"); set -- ;;
65 + *) translated+=("$1"); shift ;;
66 + esac
67 + done
68 + set -- "${translated[@]}"
69 +
70 + declare GITLAB_URL="https://git.mws-team.ru"
71 + declare TOKEN_FILE=""
72 + declare GROUP=""
73 +
74 + while getopts ":ht:g:i:" opt; do
75 + case "$opt" in
76 + h) usage ;;
77 + t) TOKEN_FILE=$OPTARG ;;
78 + g) GITLAB_URL=$OPTARG ;;
79 + i) GROUP=$OPTARG ;;
80 + :) echo "Missing arg for -$OPTARG"; usage ;;
81 + \?) echo "Invalid option: -$OPTARG"; usage ;;
82 + esac
83 + done
84 +
85 + checks
86 +
87 + declare -r TOKEN=$(cat ${TOKEN_FILE})
88 +
89 + for P_PAGE in $(seq 1 $(curl -X HEAD -w '%{header_json}' --header "Authorization: Bearer ${TOKEN}" ${GITLAB_URL}/api/v4/groups/${GROUP}/projects?per_page=100 2>/dev/null| jq -r '."x-total-pages"[0]'))
90 + do
91 + echo "INFO: Check Projects"
92 + for PROJECT in $(curl --silent --header "Authorization: Bearer ${TOKEN}" "${GITLAB_URL}/api/v4/groups/${GROUP}/projects?per_page=100&page=${P_PAGE}" | jq -r '.[].id')
93 + do
94 + clone_project ${PROJECT}
95 + done
96 +
97 + done
98 + for SG_PAGE in $(seq 1 $(curl -X HEAD -w '%{header_json}' --header "Authorization: Bearer ${TOKEN}" ${GITLAB_URL}/api/v4/groups/${GROUP}/subgroups?per_page=100 2>/dev/null | jq -r '."x-total-pages"[0]'))
99 + do
100 + for SUBGROUP in $(curl --silent --header "Authorization: Bearer ${TOKEN}" "${GITLAB_URL}/api/v4/groups/${GROUP}/subgroups?per_page=100&page=${SG_PAGE}" | jq -r '.[].id')
101 + do
102 + echo "INFO: Check Subgroups"
103 + for P_SG_PAGE in $(seq 1 $(curl -X HEAD -w '%{header_json}' --header "Authorization: Bearer ${TOKEN}" ${GITLAB_URL}/api/v4/groups/${SUBGROUP}/projects?per_page=100 2>/dev/null| jq -r '."x-total-pages"[0]'))
104 + do
105 + for PROJECT in $(curl --silent --header "Authorization: Bearer ${TOKEN}" "${GITLAB_URL}/api/v4/groups/${SUBGROUP}/projects?per_page=100&page=${P_SG_PAGE}" | jq -r '.[].id')
106 + do
107 + clone_project ${PROJECT}
108 + done
109 +
110 + done
111 +
112 + done
113 + done
114 + echo "OK"
Newer Older