1. Что сделал?

  • Полная миграция данных. Переехал с GitLab на свой сервер с идеей перенести сразу все (или выбранные) репозитории, при этом не потерять настройки, описания, картинки и мердж/пулл-реквесты. Решений по полной миграции я не нашел. Спойлер: закодил свой Python скрипт.

  • Синхронизация с remotes. Настроил простой git fetch/push одновременно на свой и на все GitLab, GitHub, ... remotes. Спойлер: решение оформляю в другую статью.

2. Зачем?

  • Локальный (Self-hosted) сервис независим от РКН, КВН, "чебурнета" и гео-блокировки аккаунта.

  • Приватные репозитории на облачных хостингах могут утечь в интернет по независящим от вас причинам.

  • Локальные файлы хостинга можно бэкапить со всеми мердж-реквестами, настройками, описаниями и картинками.

3. Выбор локального хостинга

И тут конечно же не грех спросить ИИ. После удаления лишней воды от ИИ, получил сухой остаток в виде следующей таблицы.

Система

Ресурсы (min)

CI/CD

Лицензия

Краткий вывод

GitHub Server

Платная коммерческая

Для корпоратов

GitLab (CE)

4+ vCPU, 8-16 ГБ RAM

Встроенный мощный CI/CD

Open Core (MIT+)

Для корпоратов с мощным железом

Gitea

1 vCPU, 512 МБ RAM

Gitea Actions (как GitHub)

MIT (Commercial)

Золотая середина. Очень похож на GitHub, идеален для команд.

Forgejo

1 vCPU, 512 МБ RAM

Forgejo Actions

GPL-3.0 (Open Source)

Полная независимость от корпораций и свобода.

Gogs

1 vCPU, 256 МБ RAM

Отсутствует (внешние)

MIT

Супер легкий. Старые компы/Raspberry.

Я выбрал Gogs как самый легковесный сервис, т.к. как мне не нужны сторонние CI/CD.

4. Настройка Gogs

Скачать с https://gogs.io и установить (кроссплатформенно, можно в докер). Настройка не сложная, но есть нюансы (например по умолчанию протокол http вместо https). Расписывать в статье не стал.
А вы не забыли настроить свой хостинг перед миграцией?

5. Как?

У меня несколько десятков pet-проектов на Gitlab. Я пробовал варианты миграции через:

  • WEB интерфейс Казалось бы просто, все можно сделать через веб-интерфейс ? Но если побороть "лень" и с каждым репозиторием откликать мышкой в web интерфейсе. Недостатки встроенных средств миграции через web

    • Каждый проект - только индивидуально (а хочется все сразу).

    • Gitlab ограничен только экспортом в другой Gitlab проект (т.е. экспорта по сути нет).

    • Прим: если выбрать миграцию через web интерфейс в Gogs "Тип миграции(Migration Type)" с установленной галкой "Зеркало(Mirror)", то Gogs будет периодически подключаться и подтягивать новые изменения с GitLab. Но это read only, т.е. нельзя git push в Gogs.

      А при обычной миграции (галка снята), после копирования связь с GitLab разрывается.

    • После миграции через веб-интерфейс сразу бросилось в глаза, что логотип(аватар) не мигрировал. Данные, относящиеся к git, перенеслись. Похоже веб-миграция ограничена под капотом простыми запусками команд git.

    • Вариант миграции из Gitlab групп (не пользователя) скорее всего пройдет. А вот с миграцией в Gogs организацию (не пользователя) возможно будут проблемы с правами, проверять не стал.

  • CLI bash на Linux

    • Если просто git clone/push --mirror в bash, то результат все равно будет не удовлетворительным. Команды git переносят только историю коммитов, ветки и теги.

    • Поэтому в bash нужно добавлять обращение к API платформы. Но это путь копипасты длиннющих заклинаний curl, jq, grep, cut, xargs, git по несколько раз и с многократным исправлением ошибок.

    • А на Windows с bat будет еще сложнее.

Гуглил похожие решения, подходящих не нашел. Но есть примеры (со своими особенностями):
Опыт миграции из Gitea в GitLab. Сложно, но успешно
Быстрый бэкап всех ваших репозиториев Github


И что делать? Написал скрипт на питоне. Это проще, универсальнее и более функционально.

6. Перед миграцией

  • Создать PAT токен для Gitlab и для Gogs. С какого-то времени для авторизации вместо пароля используется PAT (Personal Access Token, тот самый "пароль", который для авторизации git по протоколу https).
    Прим: не Project Access Token (в настройках проекта), а именно Personal Access Token (в настройках пользователя)
    Использование PAT необходимо, так как скрипт взаимодействует с API и выполняет клонирование по HTTPS.
    💡 Совет: Сохраните токены в надежном месте, так как GitLab и Gogs показывают их только один раз при создании.

    API_TOKEN
    • Gitlab: (API_TOKEN) Нужно создать PAT, можно по ссылке создать PAT

    • Gogs: (USER_API_TOKEN) Нужно создать Manage Personal Access / Create New Token, можно по ссылке создать PAT.

  • Заменить значения в settings.ini (UTF8) для GitLab и Gogs на свои

    `GitLab` и `Gogs` in settings.ini
    • [SRC_GITLAB_COM] секция. Заменить на свои значения:

      # --- Настройки import from (GitLab) src ---
      [SRC_GITLAB_COM]
      URL = https://gitlab.com
      API_TOKEN = gitlab_api_my_PAT_token
      USER_ID = gitlab_my_id
      
    • [DST_GOGS_LOCAL] секция. Заменить на свои значен��я:

      # --- Настройки export to (Gogs) dst ---
      [DST_GOGS_LOCAL]
      URL = https://gogs:3000
      USER_NAME = gogs_my_name
      USER_PWD = gogs_my_pwd
      USER_API_TOKEN = gogs_api_my_PAT_token
      BASE_PATH = c:\Programs\Gogs
      DESCR_MAX_LEN = 256
      
      • USER_NAME = Gogs_ваш_user_name_login

      • USER_PWD = Gogs_ваш_user_password

      • USER_API_TOKEN = Gogs_PAT

      • BASE_PATH = куда/проинсталлирован/gogs

  • Вы в Gogs как USER_NAME должны обладать админ-правами. Я не стал усложнять скрипт через создание репозиториев пользователя (не админа) с передачей прав от админа, а просто пользуюсь админскими правами пользователя.

  • У вас должен быть установлен относительно свежий python v3.

  • Если при запуске скрипта будет ругаться на отсутствие библиотек, установите.

7. Миграция

  • CLI-Интерфейс (запуск и использование).

    • Проверка GitLab доступа, с выводом списка всех репозиториев:m:\>python migrate_repos.py --settings=path/to/my_settings.ini

    • Запуск миграции (Если все ок):m:\>python migrate_repos.py --settings=path/to/my_settings.ini --run

    • По умолчанию ищется settings.ini в той же папке, где и скрипт:m:\>python migrate_repos.py --run

    • После выполнения скрипта выводится статус-репорт по всем репозиториям.

  • Доп. параметры в settings.ini.

    • В следующий раз, если что то поменялось в Gitlab, можно форсировать обновление Gogs (иначе скрипт игнорирует уже существующее в Gogs репо):

      FORCE_UPDATE_EXISTING_REPO in settings.ini
      [COMMON]
      FORCE_UPDATE_EXISTING_REPO = False
      # False: Скрипт не изменит репо в DST, если был найден до создания (безопасный режим).
      # True: Скрипт будет обновлять (пушить) в DST-репозиторий, даже если он уже существует.
      
    • Надеюсь описания (description) репозиториев в 'Gitlab' у вас небольшие (до 256 символов). Иначе либо обрежет, либо миграция остановится:

      STOP_ON_REPO_DESCRIPTION_LIMITS in settings.ini
      [COMMON]
      STOP_ON_REPO_DESCRIPTION_LIMITS = False
      # Gogs, Gitea has limitations for the description migration:
      # from web: Описание репозитория. Максимальная длина 512 символов
      # Но при попытке залить больше 256 выдает ошибку
      # Якобы нельзя переносы строк (no '\r' '\n' '\t'), но у меня получилось
      
  • Доп. параметры в коде скрипта.

    • Локальные временные репозитории создаются в папке TMP_DIR (создается папка "TmpLocalMigrateRepos" в пользовательской "TEMP" папке)

    • Добавил в код режим пропуска репозиториев и/или путей в URL.

      SKIP_*
      # SKIP (пропустить, если есть в списках)
      SKIP_REPOS  = ["SkipRepo1", "SkipRepo2", "test", "qQq"]
      SKIP_PATHS  = ["Group1/Subgroup1", "Another2/path2/to2"]
      
    • Еще режим для дебага только перечисленных репо или путей:

      DEBUG_*
      # DEBUG (если список не пуст, пропускать всё, что В НЕГО НЕ ВХОДИТ)
      DEBUG_REPOS = []  # Если не пусто, будут обрабатываться ТОЛЬКО эти репозитории
      DEBUG_PATHS = []  # Если не пусто, будут обрабатываться ТОЛЬКО эти пути
      
    • Токены в логе обфусцируются в obfuscate_url()

    • Скрипт честно ищет gogs custom/conf/api.ini конфиг и параметры из него, иначе пробует дефолтные.

    • При выполнении команды push, 'gc' на больших репозиториях, git сначала делает вид, что завис, а потом в лог вываливает кучу строк. Я не победил (в ко��е несколько вариантов run_command). Возможно особенность выполнения гита в питоне под Windows.

    • Форкнул https://gitlab.com/gitlab-org/gitlab-foss (похоже один из самых больших репо: 1.7 Gbytes и куча файлов, 116,627 commits и 4,138 branches). Запустил скрипт. Через несколько часов все появилось в gogs. В коде есть проверка репо на монструозность. И в случае чего, запускается git gc --auto (Возможно не нужно, но сделал) перед push.

8. О скрипте

  • Скрипт переносит репозитории с использованием git с параметрами, полученными из запросов через Gitlab API и Gogs API * Все текущие репозитории одного пользователя. Скрипт в настоящее время не поддерживает работу с несколькими пользователями одновременно.

    • branches, tags.

    • Описание (с ограничением gogs в 256 символов)

    • Лого-картинка (не сразу научился брать с GitLab). Прим: через Gogs API так и не получилось, поэтому сделал через запись в локальную БД (SQLite). Если у вас другой тип БД или вы скрипт запускаете не на хостинговой машине - нужно беспощадно вырезать соответствующий код в скрипте. Обновил скрипт: если скрипт запущен на той же машине, что и Gogs, он обновит аватары напрямую в SQLite, в противном случае обращение к БД (перенос аватаров)будет безопасно пропускаться.

  • Скрипт не переносит функциональность Merge/Pull Requests из Gitlab в Gogs (сложно, можно сделать, но руки не дошли).

  • Repo path:

    • В gitlab путь к репозиторию может быть с указанием в пути либо имени пользователя: https://gitlab.com/User1/Repo1.git

    • То же самое и в gogs: https://gogs:3000/User1/Repo1.git

    • Либо путь репо (В gitlab) может быть в группах (вместо пользователя): https://gitlab.com/Group1/Subgroup2/.../Repo2.git

    • В gogs аналогией пути с группами выступает путь к организации: https://gogs:3000/Org1/Repo1.git Поэтому в скрипте меняю путь Group1/Subgroup2/.../ (до имени репозитория) на имя организации Org1/. Если она не существует, попутно создаю.

9. Code

Ссылка на код в Gitlab

  • Классы ServiceManager и Src, Dst представляют основной функционал

  • ServiceManager управляет взаимодействиями для миграции репо (из Src в Dst)

  • ServiceManager.init() читает конфигурацию settings.ini сохраняя в Src, Dst

  • ServiceManager.processing()

    • скачивает json из Gitlab c информацией о репозиториях Src.repos_json()

    • В цикле запускает обработку каждого репозитория:

      • сохраняет из json инфу о текущем репозитории в Src.init() и Dst.init()

      • ServiceManager.mirror_repo() миграция 1 репы

      • ServiceManager.status отчет о всех репах после миграции как результат обработки каждого, например:

        пример вывода статусов (заменил часть имен на пробелы чтобы не догадались ))
        local  |org    |org ava|rep upd|rep ava|path/name
        fetched|user's |user's |forced |absent |AndrB   /JapanCrossword
        xxxxxxx|xxxxxxx|xxxxxxx|xxxxxxx|xxxxxxx|[SKIPPED]AndrB   /JapanCrossword2
        fetched|user's |user's |forced |updated|AndrB   /MigrateRepos
        fetched|user's |user's |forced |updated|AndrB   /Prograstegy
        fetched|user's |user's |forced |updated|AndrB   /RamCarDriver
        fetched|user's |user's |forced |updated|AndrB   /RussianAICup2020CodeCraft
        fetched|user's |user's |forced |updated|AndrB   /habr.com
        fetched|user's |user's |forced |updated|AndrB   /japancrossword11plus
        fetched|user's |user's |forced |updated|AndrB   /test
        fetched|user's |user's |forced |updated|AndrB   /test2
        fetched|user's |user's |forced |absent |AndrB   /tgbot
        ...
        xxxxxxx|xxxxxxx|xxxxxxx|xxxxxxx|xxxxxxx|[SKIPPED]Andrey   ovWork/Thinkcell.com_test
        ....
        fetched|alr xst|updated|forced |absent |Panaso   FactorySupport/Orange_Box
        fetched|alr xst|updated|forced |absent |Panaso   FactorySupport/Orange_Web
        fetched|alr xst|updated|forced |absent |Panaso   FactorySupport/Orange_Web_GUI
        fetched|alr xst|updated|forced |absent |Panaso   FactorySupport/Orange_Web_ova
        fetched|alr xst|updated|forced |absent |Panaso   FactorySupport/Pasa_BPM_API
        fetched|alr xst|updated|forced |updated|Panaso   FactorySupport/Pasa_eMMC_EmptyAreasSplitter
        fetched|alr xst|updated|forced |absent |Panaso   FactorySupport/CAN_Sock_Raw_API
        ...
        fetched|alr xst|updated|forced |updated|QQQ77AaaBbbCcc/qqQ
        ...
        fetched|alr xst|updated|forced |updated|ses38   ocal/AdminSharedFoldersGroupsUsersRights
        fetched|alr xst|updated|forced |updated|ses38   ocal/InsiderEngine
        fetched|alr xst|updated|forced |updated|ses380Local/InsiderInfo
        fetched|alr xst|updated|forced |updated|ses380Public/MailSpamAssassin
        fetched|alr xst|updated|forced |updated|ses380Public/WwwEngine
        fetched|alr xst|updated|forced |updated|ses380Public/WwwInfo
        
  • В описании ниже есть зависимости действий от флага FORCE_UPDATE_EXISTING_REPO, но пропущено для упрощения (в коде есть)

  • ServiceManager.mirror_repo() магия из 3 составляющих:

    print(f"▄▄▄▄▄▄▄ §1. Клонирование (зеркальное)/Синхронизация из Src в локальное")
    prepare_local_mirror(src_authenticated_url, local_repo_path)
    print(f"▄▄▄▄▄▄▄ §2. Проверка Dst репо {repo_name} и Инициализация только если не существует")
    is_dst_repo_just_inited_as_new = Dst.init_repo()
    print(f"▄▄▄▄▄▄▄ §3. Пуш (зеркальный) локального в Dst")
    if is_dst_repo_just_inited_as_new : Dst.push(local_repo_path)
    

10. Комментарии на некоторые части кода

  • api_*url() набор служебных функций, возвращающих URL для request к API для разных случаев. Небольшое отличие для Gitlab и для Gogsменя немного удивили. Поэтому сделал универсальный подход:

    def req(method, url, **kwargs): # Unified requester. Usage:
    		# req("GET", url)
    		# req("POST", url, json=data)
    		# req("POST", url, files=files_dict)
    		# req("PATCH", url, json=data)
    	return execute_request(method, url, Dst.api_headers(), **kwargs)
    
  • init_repo()

    • Если is_org и при проверке status_code == 404, то создаю организацию с именем групп из Gitlab репо (см. выше почему)

    • Проверка репо на status_code == 200. Если не существует, создаю

  • wait_for_ready() - ждет статуса после выполнения request. Понадобилась после ошибок при быстром следующем обращении к API- push() c вызовом git push --mirror --force , при этом дополнительно --follow-tags для tags не нужен.

  • Картинки репозиториев и групп , называются avatars. Через Gogs API заливать картинки не получилось, пришлось костылить напрямую с запросом в Gogs БД и с сохранением соответствующих репозиториям и организациям имен файлов и папок в Gogs. Комментарии в коде.

  • Gitlab API по взятию картинок обещал путь из первоначального json, но по факту сработало только через обработку request

  • log() нужен только для дебага. В процессе работы скрипта ожидаемые логические ошибки отлавливаются и такой print не включает log()

    log()
    print(f"{log()}Bla bla {counter} bla bla") # line 649 in func mirror_repo()
    

    Пример вывода вызванных функций и вывод сообщения из этой строки во время дебага

    migrate_repos.py <module>    :696
    migrate_repos.py processing  :628
    migrate_repos.py mirror_repo :649
    Bla bla 42 bla bla
    

√ 🚀 Миграция выполнена.
Ниже то, что не сделано

A. Поддержка Merge Request при миграции.

Команды git (такие как git clone --mirror или git push) переносят только историю коммитов, ветки и теги.
Merge Request это функциональность хостинга (метаданные самой платформы), и не являются частью функциональности git (точно так же как описание и картинки на хостинге репозитория). Поэтому командами git они не переносятся. Мои проекты приватные, поэтому я это не реализовал.

B. API для различных типов хостингов.

Хотя API хостингов в чем-то похожи, но понятно, что они разные, поэтому в статье реализация только Gitlab -> Gogs.

  • Github

  • Bitbucket

  • Codeberg

  • GitFlic Буду думать...


√ 🔄 Вторая часть идеи.
Удобно делать простой git fetch/push сразу во все репозитории, когда один будет ведущим, остальные зеркалами

Настройка Git для одновременного push/fetch в несколько remotes.

Одной командой со всеми Gitlab, Github, ... и Gogs хостингами при выполнении git fetch/push.
Для конфигурирования нужно выполнить команды git в CLI, находясь в папке с клонированным репозиторием.
Либо (альтернативно) отредактировать .git/config, что быстрее, но легко допустить ошибки при редактировании.

В итоге получилось много команд и примечаний, конфигураций и примеров.

Решение оформляю в отдельную статью howto.


Прим: Это pet проект, c идеей попробовать в Python и получить just a Proof-Of-Concept, с целью обмануть свою лень и выполнить миграцию.

PS: Никаких рекламных ссылок на мой Телеграм-канал, тем более что его нет.