5656 const repositoryData = repos.map((repo) => ({
5757 cloneUrl: repo.clone_url,
5858 name: repo.name,
59+ sizeKiB: repo.size,
5960 targetVisibility: repo.visibility === 'public' ? 'public' : 'private',
6061 ...gitlabTarget(repo),
6162 }));
@@ -149,6 +150,57 @@ jobs:
149150 curl "${curl_args[@]}" "${url}"
150151 }
151152
153+ wait_for_gitlab_import() {
154+ local project_id="$1"
155+ local source_name="$2"
156+ local deadline="$((SECONDS + 10800))"
157+ local import_error
158+ local import_status
159+ local status
160+
161+ while ((SECONDS < deadline)); do
162+ if ! status="$(gitlab_request GET "${GITLAB_API_URL}/projects/${project_id}/import")"; then
163+ echo "::error title=Mirror failed: ${source_name}::Unable to query the GitLab import."
164+ return 1
165+ fi
166+ if [[ "${status}" != "200" ]]; then
167+ if [[ "${status}" == "403" ]]; then
168+ echo "::error title=Mirror failed: ${source_name}::GITLAB_TOKEN needs Import API Read access."
169+ return 1
170+ fi
171+ echo "::error title=Mirror failed: ${source_name}::Unable to query the GitLab import (HTTP ${status})."
172+ return 1
173+ fi
174+
175+ import_status="$(jq -er '.import_status' "${response_file}")"
176+ case "${import_status}" in
177+ finished)
178+ echo "GitLab server-side import finished."
179+ return 0
180+ ;;
181+ scheduled | started)
182+ echo "GitLab server-side import status: ${import_status}."
183+ sleep 30
184+ ;;
185+ failed)
186+ import_error="$(jq -r '.import_error // "No import error detail was provided."' "${response_file}")"
187+ import_error="${import_error//$'\r'/ }"
188+ import_error="${import_error//$'\n'/ }"
189+ echo "GitLab import failed: ${import_error}"
190+ echo "::error title=Mirror failed: ${source_name}::GitLab server-side import failed."
191+ return 1
192+ ;;
193+ *)
194+ echo "::error title=Mirror failed: ${source_name}::Unexpected GitLab import status: ${import_status}."
195+ return 1
196+ ;;
197+ esac
198+ done
199+
200+ echo "::error title=Mirror failed: ${source_name}::GitLab server-side import timed out after three hours."
201+ return 1
202+ }
203+
152204 encoded_group="$(jq -rn --arg value "${GITLAB_GROUP}" '$value | @uri')"
153205 if ! status="$(gitlab_request GET "${GITLAB_API_URL}/groups/${encoded_group}")"; then
154206 echo "::error::Unable to query the GitLab group."
@@ -163,12 +215,17 @@ jobs:
163215 github_auth="$(printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 --wrap=0)"
164216 echo "::add-mask::${github_auth}"
165217
218+ # GitLab.com limits each Git push to 5 GiB. Seed large empty projects through GitLab's import API,
219+ # using a lower threshold because GitHub's reported repository size is not the resulting pack size.
220+ gitlab_import_threshold_kib="$((4 * 1024 * 1024))"
221+
166222 repository_count="$(jq -er 'length' "${repository_file}")"
167223 for ((index = 0; index < repository_count; index++)); do
168224 repository="$(jq -ec ".[${index}]" "${repository_file}")"
169225 repository_number="$((index + 1))"
170226 source_name="$(jq -er '.name' <<< "${repository}")"
171227 source_clone_url="$(jq -er '.cloneUrl' <<< "${repository}")"
228+ source_size_kib="$(jq -er '.sizeKiB' <<< "${repository}")"
172229 target_name="$(jq -er '.targetName' <<< "${repository}")"
173230 target_path="$(jq -er '.targetPath' <<< "${repository}")"
174231 target_visibility="$(jq -er '.targetVisibility' <<< "${repository}")"
@@ -180,6 +237,8 @@ jobs:
180237 echo "Visibility: ${target_visibility}"
181238 echo "Checking for the GitLab project..."
182239
240+ project_created=false
241+
183242 if ! status="$(gitlab_request GET "${GITLAB_API_URL}/projects/${encoded_project_path}")"; then
184243 echo "::error title=Mirror failed: ${source_name}::Unable to query the GitLab project."
185244 exit 1
@@ -210,6 +269,7 @@ jobs:
210269 echo "::error title=Mirror failed: ${source_name}::${error_message}"
211270 exit 1
212271 fi
272+ project_created=true
213273 elif [[ "${status}" != "200" ]]; then
214274 echo "::error title=Mirror failed: ${source_name}::Unable to query the GitLab project (HTTP ${status})."
215275 exit 1
@@ -218,6 +278,13 @@ jobs:
218278 fi
219279
220280 project_id="$(jq -er '.id' "${response_file}")"
281+ if [[ "${project_created}" == "true" ]]; then
282+ project_empty=true
283+ else
284+ project_empty="$(
285+ jq -r '(.empty_repo == true) or (.default_branch == null)' "${response_file}"
286+ )"
287+ fi
221288
222289 # Make a private source private before changing metadata or pushing any Git data.
223290 if [[ "${target_visibility}" == "private" ]]; then
@@ -267,6 +334,75 @@ jobs:
267334
268335 target_clone_url="$(jq -er '.ssh_url_to_repo' "${response_file}")"
269336
337+ gitlab_import_started=false
338+ gitlab_incremental_seed=false
339+ if [[ "${project_empty}" == "true" ]] && \
340+ ((source_size_kib >= gitlab_import_threshold_kib)); then
341+ echo "Empty large project detected (${source_size_kib} KiB reported by GitHub)."
342+ echo "Scheduling a GitLab server-side Git import to avoid the 5 GiB push limit..."
343+ if [[ "${target_visibility}" == "private" ]]; then
344+ import_payload="$(
345+ jq -nc \
346+ --arg import_url "${source_clone_url}" \
347+ --arg import_url_user "x-access-token" \
348+ --arg import_url_password "${GITHUB_TOKEN}" \
349+ '{
350+ import_url: $import_url,
351+ import_url_user: $import_url_user,
352+ import_url_password: $import_url_password
353+ }'
354+ )"
355+ else
356+ import_payload="$(
357+ jq -nc \
358+ --arg import_url "${source_clone_url}" \
359+ '{import_url: $import_url}'
360+ )"
361+ fi
362+
363+ if ! status="$(
364+ gitlab_request POST "${GITLAB_API_URL}/projects/${project_id}/import/git" "${import_payload}"
365+ )"; then
366+ echo "::error title=Mirror failed: ${source_name}::Unable to schedule the GitLab import."
367+ exit 1
368+ fi
369+ if [[ "${status}" == "403" ]]; then
370+ echo "::error title=Mirror failed: ${source_name}::GITLAB_TOKEN needs Import API Create access."
371+ exit 1
372+ fi
373+
374+ import_response_message="$(jq -r '.message // "No response message was provided."' "${response_file}")"
375+ import_response_message="${import_response_message//$'\r'/ }"
376+ import_response_message="${import_response_message//$'\n'/ }"
377+ if [[ "${status}" == "409" ]]; then
378+ case "${import_response_message}" in
379+ "Import already in progress")
380+ echo "A GitLab import is already in progress; continuing to monitor it."
381+ gitlab_import_started=true
382+ ;;
383+ "Project already has a repository")
384+ echo "GitLab has repository storage from an earlier push; using incremental seeding."
385+ gitlab_incremental_seed=true
386+ ;;
387+ *)
388+ echo "GitLab import conflict: ${import_response_message}"
389+ echo "::error title=Mirror failed: ${source_name}::Unable to schedule the GitLab import."
390+ exit 1
391+ ;;
392+ esac
393+ elif [[ "${status}" != "201" ]] && [[ "${status}" != "202" ]]; then
394+ echo "GitLab import response: ${import_response_message}"
395+ echo "::error title=Mirror failed: ${source_name}::Unable to schedule the GitLab import" \
396+ "(HTTP ${status})."
397+ exit 1
398+ else
399+ gitlab_import_started=true
400+ fi
401+ elif [[ "${project_empty}" != "true" ]] && \
402+ ((source_size_kib >= gitlab_import_threshold_kib)); then
403+ echo "Large project is already populated; using an incremental Git push."
404+ fi
405+
270406 mirror_dir="${temp_root}/repository.git"
271407 echo "Cloning ${source_clone_url} as a mirror..."
272408 if ! git \
@@ -275,6 +411,39 @@ jobs:
275411 echo "::error title=Mirror failed: ${source_name}::Unable to clone the GitHub repository."
276412 exit 1
277413 fi
414+
415+ mapfile -t pull_refs < <(
416+ git -C "${mirror_dir}" for-each-ref --format='%(refname)' refs/pull/
417+ )
418+ if ((${#pull_refs[@]} > 0)); then
419+ echo "Removing ${#pull_refs[@]} GitHub-only pull request refs from the mirror..."
420+ printf 'delete %s\n' "${pull_refs[@]}" | git -C "${mirror_dir}" update-ref --stdin
421+ fi
422+
423+ if [[ "${gitlab_import_started}" == "true" ]]; then
424+ echo "Waiting for the GitLab server-side import to finish..."
425+ if ! wait_for_gitlab_import "${project_id}" "${source_name}"; then
426+ exit 1
427+ fi
428+ fi
429+
430+ if [[ "${gitlab_incremental_seed}" == "true" ]]; then
431+ mapfile -t seed_refs < <(
432+ git -C "${mirror_dir}" for-each-ref --format='%(refname)' refs/heads/ refs/tags/
433+ )
434+ echo "Seeding ${#seed_refs[@]} branches and tags in separate pushes..."
435+ for seed_ref in "${seed_refs[@]}"; do
436+ echo "Seeding ${seed_ref}..."
437+ if ! git \
438+ -C "${mirror_dir}" \
439+ -c core.sshCommand="${gitlab_ssh_command}" \
440+ push --progress "${target_clone_url}" "+${seed_ref}:${seed_ref}"; then
441+ echo "::error title=Mirror failed: ${source_name}::Unable to seed ${seed_ref} on GitLab."
442+ exit 1
443+ fi
444+ done
445+ fi
446+
278447 echo "Pushing the mirror to ${target_clone_url}..."
279448 if ! git \
280449 -C "${mirror_dir}" \
0 commit comments