Прим: Дополняет мою статью "Скрипт полной миграции из GitLab на свой сервер".

1. После успешной миграции

А что делать со старым репозиторием?
Было бы логично использовать GitLab и другие хостинги в качестве вторичных (запасных).

Но как это делать? И еще не вручную?
Для синхрон��зации с remotes одновременно на свой Gogs и на все GitLab, GitHub, Bitbucket, ... remotes, об этом и других "хаках" ниже:

TL;DR Отредактировать свой конфиг:

  • Добавить соответствующие [remote ] секции, в каждой прописать url, fetch и несколько pushurl.

  • Опционально - прописать скрипт и использовать флаг для пуша.

  • Итого: набор команд для Использования

Начну из далека, но цель близка.

1.1 Нужен clone (если еще нет локального репо, иначе к сл. пункту)

Клонируем local repo (репозиторий) с remote (удаленный).
В качестве remote выбираем свой https://gogs:3000 сервер с репозиторием test.git

m:\Projects>git clone https://gogs:3000/User0/test.git

1.2 Переходим в папку локального репозитория

m:\Projects>cd test

Прим: Все команды запускаются в папке проекта m:\Projects\test. Поэтому m:\Projects> не указываю.

1.3 Определимся с тремя понятиями:

Remote (удаленный): сервер.
Remote-tracking branch (удаленно-отслеживаемая ветка): это локальная служебная копия («кэш») состояния удаленного сервера (например, origin/main). Это не рабочая ветка для кода, это указатель на последнее известное состояние сервера. И команды гита fetch или pull его обновляют ("кэшируют").
Local branch (локальная ветка): Моя рабочая ветка (просто main, а не origin/main), тут я пишу код.

2. Подготовка и настройка Remotes

2.1 Файл .git\config

В папке проекта есть файл m:\Projects\test\.git\config
и он может выглядеть примерно так:

[remote "origin"]
	url = https://gogs:3000/User1/test.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
	remote = origin
	merge = refs/heads/main
[branch "brA"]
	remote = origin
	merge = refs/heads/brA
[branch "brB"]
	remote = origin
	merge = refs/heads/brB

В секции [remote "origin"] виден удаленный адрес для origin(первоисточник).

Прим: При клонировании автоматически создается стандартный алиаз origin. В примере указывает на https://gogs:3000. origin используется в командах git, например git push origin main или git pull origin
В секциях [branch "xxxxxx"] видны настройки существующих бранчей (branch или ветка)

  • remote: указывает ветке в какой репозиторий remote «смотреть» (например, Git0 или origin).

  • merge : указывает ветке какая именно ветка на удаленном сервере является «паро��» для локальной. Под термином upstream понимают совокупность, например для локальной ветки [branch "brA"]:

  • Куда идти? remote = origin

  • Какую ветку там брать? merge = refs/heads/brA. (Не обязательно локальная [branch "brA"] имеет такое же имя brA в merge = refs/heads/brA). Говорят "У ветки brA установлен upstream origin/brA".

Прим: Можно редактировать .git/config, и не использовать команды в CLI.git перечитывает файлы конфигурации (системный, глобальный и локальный) каждый раз при запуске любой команды. Если внесена ошибка в конфиг, то с помощью команды git config можно проверить синтаксис.
Мне проще редактировать конфиг, чем использовать команды.

2.2 Remote origin, давай досвидания.

Давайте посмотрим куда указывает origin:

>git remote -v
origin  https://gogs:3000/User0/test.git (fetch)
origin  https://gogs:3000/User0/test.git (push)

Переименовываю origin в Git0 для наглядности и абстракции. Т.к. у нас будет несколько remote, любой из них может выступать в роли origin.

>git remote rename origin Git0

>git remote -v
Git0    https://gogs:3000/User0/test.git (fetch)
Git0    https://gogs:3000/User0/test.git (push)

Соответсвенно в файл test/.git/config добавятся:

[remote "Git0"]
	url = https://gogs:3000/User1/test.git
	fetch = +refs/heads/*:refs/remotes/Git0/*
[branch "main"]
	remote = Git0
	merge = refs/heads/main
[branch "brA"]
	remote = Git0
	merge = refs/heads/brA
[branch "brB"]
	remote = Git0
	merge = refs/heads/brB

2.3 Алиазы вместо длинных URL

Сразу добавляю URL алиазы для дальнейшего удобства использования.

>git config --local url."https://Gogs:3000/User0". insteadOf "url0"
>git config --local url."https://Gitlab.com/User1".insteadOf "url1"
>git config --local url."https://Github.com/User2".insteadOf "url2"
>git config --local url."https://xxxxxx.com/User3".insteadOf "url3"

файл test/.git/config:

[url "https://Gogs:3000/User0"]
	insteadOf = url0
[url "https://Gitlab.com/User1"]
	insteadOf = url1
[url "https://Github.com/User2"]
	insteadOf = url2
[url "https://xxxxxx.com/User3"]
	insteadOf = url3

2.4 Секция [remote "origin"]

Забегая вперед, важное замечание. Обычно конфиг содержит одну [remote "origin"] секцию. Выглядит примерно так:

[remote "origin"]
	url = https://hosting.com/user1/test1.git
	fetch = +refs/heads/*:refs/remotes/origin/*
	pushurl = https://hosting.com/user1/test1.git 
  • url = может быть только один (используется для fetch и для push, если не задан pushurl).

  • fetch = может быть несколько (они определяют правила сопоставления веток, git позволяет скачивать данные из разных групп веток одного репозитория).

Прим: в теории, если нет строки fetch =, то команда git fetch имя (без указания веток) не будет знать, какие ветки скачивать и куда их сохранять локально. Она просто ничего не обновит в ваших refs/remotes/. Каждый раз придется указывать git fetch Git0 main:refs/remotes/Git0/main

  • pushurl = опционально и обычно отсутствует, т.к. совпадает с url. Прим: без pushurl, git использует основной url как для скачивания (fetch), так и для отправки (push). Может быть несколько строк pushurl (в таком случае, git отправит данные во все указанные адреса одновременно при выполнении git push).

Прим: git пушит в указанном порядке и если возникнет любая ошибка (например сервер лежит), то git "не парится" по поводу оставшихся в списке

2.5 Команды remote set-url ***

Для обновления конфига нужны команды:

: обновить fetch-URL. Прим: Если нет отдельной строки pushurl, эта команда обновит адрес и для fetch, и для push одновременно.
>git remote set-url                 Git0 url0/test.git
: обновить push-URL.
>git remote set-url          --push Git0 url0/test.git
: добавить еще (+1) push-URL.
>git remote set-url --add    --push Git0 url0/test.git
: удалить push-URL
>git remote set-url --delete --push Git0 url0/test.git

3. Обновление конфига для зеркалирования ВСЕХ репозиториев

3.1 Для синхронного git fetch и git push для всех репозиториев, я сделал такой конфиг:

[remote "Git0"]
    url = url0/test.git
    fetch = +refs/heads/*:refs/remotes/Git0/*
    # fetch = +refs/heads/*:refs/remotes/DONT_SET_ANY_OTHERS/*
    pushurl = url0/test.git # ONLY HERE THE ORIGIN OUR Git0
    pushurl = url1/test.git
    pushurl = url2/test.git
    pushurl = url3/test.git
[remote "Git1"]
    url = url1/test.git
    fetch = +refs/heads/*:refs/remotes/Git1/*
    # fetch = +refs/heads/*:refs/remotes/DONT_SET_ANY_OTHERS/*
    pushurl = url1/test.git
    pushurl = url2/test.git
    pushurl = url3/test.git
[remote "Git2"]
    url = url2/test.git
    fetch = +refs/heads/*:refs/remotes/Git2/*
    # fetch = +refs/heads/*:refs/remotes/DONT_SET_ANY_OTHERS/*
    pushurl = url2/test.git
    pushurl = url1/test.git
    pushurl = url3/test.git
[remote "Git3"]
    url = url3/test.git
    fetch = +refs/heads/*:refs/remotes/Git3/*
    # fetch = +refs/heads/*:refs/remotes/DONT_SET_ANY_OTHERS/*
    pushurl = url3/test.git
    pushurl = url1/test.git
    pushurl = url2/test.git

Нужно оставить remote (удаленные) хостинги только реально используемые,
т.к. несуществующие будут мешать остальным, такие строки нужно или удалить или закомментирвоать.
Тут и далее для примера выбрал кол-во 4 (а можно еще Bitbucket, Codeberg, GitFlic, ...).

3.2 Основной remote

Обращаю внимание, основным remote м.б. выбран любой remote. Я выбрал Git0 (бывший origin). Поэтому именно для него (в секции [remote "Git0"] ) и только для него есть pushurlurl0)

[remote "Git0"]
    pushurl = url0/test.git # only here the origin our Git0

4. Теория и тестирование.

4.1 Флаг --all в fetch, pull, push только пишется одинаково

Важно различать, что именно обозначает флаг --all в разных командах:
1)git fetch --all - репозитории,
2)git pull --all - репозитории, но только для текущей ветки, есть нюансы! (см. ниже)
3)git push --all - ветки

Прим: pull и push по умолчанию работают с одним origin.

1)git fetch --all:

  • Обновляет информацию (коллеги в команде что-то уже запушили) обо ВСЕХ ветках (remote-tracking branches) из ВСЕХ удаленных репозиториев (Git0, Git1, Git2, Git3), позволяя заренее видеть чужие коммиты. «Облегчает жизнь новостями» о конфликтах в коде перед будущим мержом.

  • Локальные ветки НЕ меняются.

2)git pull --all: - это сокращение для git fetch --all && git merge, как я понимаю. Могу ошибаться, я не пользуюсь pull --all.

  • Обновляет информацию (как fetch --all),

  • ДРУГИЕ локальные ветки НЕ меняются.

  • В ТЕКУЩУЮ ветку пытается выполнить merge по очереди из репозиториев, где есть ветка с таким же именем. pull вместе с --all редко используется, так как обычно ветка связана только с одним репозиторием).
    git pull --all - коварная команда. Игнорирует тот факт, что у ветки обычно только один «хозяин» (upstream).

    Пройдет по всем репозиториям и, если найдет там ветку с таким же названием (пусть даже с разным содержимым), попытается влить её в текущую.
    Для синхронизации зеркал эта команда вредная. Именно поэтому безопаснее связка fetch --all + merge из основного источника.

  • Вопрос: Пусть текущая ветка brA. Идет ли речь про мерж в brA, читая секцию [branch "brA"] из remote = Git0, из Git0/brA ?

    Ответ: Нет. В этом и заключается «коварство». Если используется флаг --all, Git игнорирует строку remote = Git0 в секции [branch "brA"] (игнорирует настройки конкретного upstream для этой ветки). Просто берется имя текущей ветки brA и ищется во всех объявленных [remote "..."]

  • Вопрос: "...из репозиториев, где есть ветка с таким же именем." Т.е. читается конфиг, ищутся все [remote ..] секции, и на этих серверах ищется ветка из merge = refs/heads/brA ?

    Ответ: Если кратко - да, идет цикл по этим серверам: Если там есть ветка brA, то сделать merge сервер/brA в локальную.

  • Но гугль пишет, в стандартном поведении Git чаще всего выполнит мерж из того удаленного репозитория, который первым вернул результат или является дефолтным.

3)git push --all:

  • Отправляет ВСЕ ветки (которые существуют локально) в один(!) удаленный репозиторий (по умолчанию origin). Т.е. это по сути git push Git0 --all (где all - ветки)

4.2 Проверка: fetch и push с опцией --all

>git fetch --all & echo ======= & git push --all
Fetching Git0
Fetching Git1
Fetching Git3
Fetching Git4
=======
Everything up-to-date
Everything up-to-date
Everything up-to-date
Everything up-to-date

4.3 Тестирование (1): коммит и пуш для текущей ветки

Для теста сделал коммит и проверяю, как push проявится во всех репозиториях для текущей ветки:

>git fetch & echo ======= & git push
............
=======
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 292 bytes | 292.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://Gogs:3000/AndrBell/test.git
   aac4183..6a982fb  brB -> brB
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 292 bytes | 292.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote:
remote: To create a merge request for brB, visit:
remote:   https://gitlab.com/AndrBell/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=comm
remote:
To https://Gitlab.com/AndrBell/test.git
   aac4183..6a982fb  brB -> brB
............

Прим: Выше использовал команды без --all. Это означает, что изменения запушатся только с текущей ветки.

4.4 Тестирование (2): коммит и пуш для всех веток

Для теста сделал коммиты в разные ветки и проверяю, как push проявится во всех репозиториях для всех веток:

>git fetch --all & echo ======= & git push --all
............
To https://Gogs:3000/AndrBell/test.git
   2372260..0410db0  brA -> brA
   a086469..e7bfa98  brB -> brB
............
To https://Gitlab.com/AndrBell/test.git
   2372260..0410db0  brA -> brA
   a086469..e7bfa98  brB -> brB
............

Прим: git pull --all. Команда pull объединяет fetch и merge. Git не умеет делать merge (слияние) сразу изо всех репозиториев во все локальные ветки. git pull --all обновит только текущую ветку.
Решение: Вместо git pull --all лучше использовать git fetch --all и затем делать git merge из основного источника (Git0).
Для push лучше всего будет, когда Git0 инициирует отправку на все свои pushurl

>git fetch --all & echo ======= & git push Git0 --all

Пример: отправить изменения в ТОЛЬКО текущей ветке во все pushurl, перечисленные для Git0

>git push Git0

Прим: Внимание!, если один хостинг из списка будет недоступен, то git запушив изменения на предыдущие, не продолжит со следующего. А когда станет доступен, git зальет изменения на оставшиеся

4.5 Итого: набор команд для использования

: Стандартно сфетчить все серверы (обновить локальные Remote-tracking branchs) перед пушем
> git fetch --all
: Пуш ТЕКУЩЕЙ ветки на все серверы (через прописанный в Git0 список pushurl)
> git push Git0
: Пуш ВСЕХ веток ...--"--. Думаю, что такой вариант полной синхронизации (всех веток) нужен в редких случаях.
> git push Git0 --all

5. Размышления о хорошем

5.1 Что такое хорошо и что такое плохо. (с) Маяковский

А чем хорошо: git push Git0 --all
и плохо просто: git push --all

  • git push Git0 --all принудительно отправляет все локальные ветки из [remote "Git0"] по очереди на url0, url1, url2 и url3. Т.е. обновит все зеркала из [remote "Git0"].

  • git push --all ведет себя по-разному, т.к. не указано имя (Git0). git смотрит на текущую ветку.

    В зависимости от настроек Git:

    • Если она отслеживает (upstream) репозиторий именно Git0, то сработает так же. Ок.

    • Но если текущая ветка настроена на отслеживание например Git1, то git push --all отправит все ветки только в Git1 (и соотвественно по его списку pushurl).

    • А вот если ветка ничего не отслеживает, git может выдать ошибку или использовать репозиторий по умолчанию (origin), если он настроен. В моей конфигурации origin не указан, поэтому git, скорее всего, попросит уточнить. Не пробовал.

5.2 Еще лучше! Level Up

Добавил в конфиг .git/config вот такой скрипт:

[alias]
    push-all-gits = "!f() { for r in $(git remote | grep '^Git'); do echo \"Pushing to $r...\"; git push $r --all --quiet && echo \"[OK] $r\" || echo \"[FAIL] $r\"; done; }; f"

Или тоже самое добавить через консоль:

>git config --local alias.push-all-gits "!f() { for r in $(git remote | grep '^Git'); do echo \"Pushing to $r...\"; git push $r --all --quiet && echo \"[OK] $r\" || echo \"[FAIL] $r\"; done; }; f"

Скрипт выполняет шаги:

  • git remote составляет список всех удаленных репозиториев.

  • grep '^Git' фильтрует их, оставляя только те, названия которых начинаются на "Git". См. ниже примечание.

  • for r in ... проходит циклом по каждому найденному репозиторию.

  • git push $r --all отправляет все ваши ветки в этот репозиторий.

  • echo "[OK]" или "[FAIL]" выводит результат операции для каждого сервера.

Напомню: Git останавливается при ошибке одного из pushurl. Скрипт push-all-gits решает проблему, обходя репозитории по отдельности. И тогда пуш пройдет на все живые сервера.

5.3 Стресс-тест скрипта (на время теста я отключил Git0):

>git push-all-gits
fatal: unable to access 'https://Gogs:3000/AndrBell/test.git/': Failed to connect to Gogs port 3000 after 2082 ms: Could not connect to server
[ERROR] Git0
Everything up-to-date
[SUCCESS] Git1
Everything up-to-date
[SUCCESS] Git2
Everything up-to-date
[SUCCESS] Git3

Прим: несколько слов о grep '^Git'
Это защита от случайного пуша, например: в upstream (чужой репозиторий, откуда только берется код) или в origin (если хочется обновлять только зеркала).
Если убрать | grep '^Git', будет цикл по всем доступным именам из git remote. Когда использовать без фильтрации? Если удаленные репозитории называются стандартно (origin, backup, mirror), т.е просто, одной командой обновить абсолютно все.

Прим: Для гиков: --local флаг указывает Git записать настройки только для текущего репозитория (в файл .git/config). Но можно же чтобы команда git push-all-gits работала везде на компьютере, тогда нужно использовать флаг --global. Как вариант с фильтром или без grep '^Git', создать два алиаза push-all-gitsиpush-all`.

5.4 Итого: команда для использования с push-all-gits

Прим: В конфиге добавлен скрипт push-all-gits

: Одной "кнопкой"
> git fetch --all && git push-all-gits

Прим: это от��азоустойчивый вариант: Если один сервер недоступен, остальные всё равно обновятся.

5.5 А если все плохо! Troubleshooting

Если в процессе экспериментов сломался .git/config, то где именно проверить можно:

>git config --list --local

5.6 Бонус, к теме не относится, но не сделайте плохо всем! (git push: спам тегами tags)

git push не отправляет теги (tags). Для этого есть флаг --tags.

git push <remote> --tags
git push --tags 

И все теги из локального репо (даже те, которые на других ветках) мгновенно окажутся в общем репозитории.
Правильно:

  • Либо использовать git push origin v1.x.x - улетит только один конкретный тег. Это самый безопасный способ.

  • Либо настроить git config --global push.followTags true (начиная с версии 2.4). Теперь при обычном git push Git запушит новый тег, теги с других веток при этом не улетят.


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