Когда-то пару лет назад на работе ко мне обратился коллега (привет!), который знает мою любовь к автоматизации с довольно нетривиальной просьбой. Нужно было почистить старые репозитории в корпоративной орге Github, но не совсем удалить, а сохранить на "всякий случай". Да и не просто сохранить, а сохранить с git историей. Мы с ним довольно быстро набросали скрипт на баше, который принимал аргументом orgName/projectName. После окончания работы скрипта он пушил код в отдельную ветку, и потом можно было это померджить в основную "ветку-хранилище". Скрипт был написан быстро, задачу решал, но все равно оставалось пара действий, которые надо было сделать "руками". Но в том случае это было нормально, так как требовалось подтверждение на архивирование старого проекта. Еще тогда у меня появилась идея сделать Github Actions workflow, с которым я только познакомился. Но свободного времени на это не было. И вот спустя год-полтора я наконец-то сделал задуманное - так появился на свет Bygone project, который доступен на github.
В 2017-2018 я учился в филиале французской школы 42 и мне приходилось много писать на Си. Было много учебных проектов, начиная от реверс-инжениринга стандартной библиотеки Си, printf, sha/md5/des алгоритмов, заканчивая своими собственными проектами, которые не имели отношения к Школе 42. Был простенький DNS proxy, сниффер сетевых пакетов, реверс-инжениринг игры Сапер. Сейчас я понимаю насколько ужасен тот код, но хочется его сохранить как память о бессонных ночах над сегфолтами, бас ероррами, потерянными указателями, утечках памяти. Два десятка репозиториев воспоминаний.
Мой github давно хотел чистки, и я все откладывал идею сделать полностью автоматический сборщик до более свободных дней. И вот у меня четвертый день кашель и второй день температура 37,5 - спать не получается - настало время доделать "хотелку". Репозиторий для проекта я создал несколько месяцев назад, но руки так и не доходили сделать.
Сбор требований
Скрипт должен складывать каждую ветку проекта в отдельную папку
Скрипт должен сохранять историю коммитов
Скрипт должен быть устойчив к мердж конфликтам типа (rename/rename)
Скрипт должен работать через Gihub Actions и запускать вручную
Скрипт должен принимать переменными окружения репозиторий архива, архивируемый репозиторий, основную ветку архива в которую сливать все, а так же токен и имя пользователя github, чтобы запушить новое
Скрипт должен работать без вмешательства через консоль (например с браузера телефона можно добавить в архив новый проект)
Скрипт должен быть максимально простым, чтобы побудить людей поучаствовать в его доработке (<3 open source)
Реализация
Согласно списку требований я решил написать сам скрипт на баше, хотя я в нем и не силен. Логика довольно простая: получаем список веток, копируем содержимое проекта и переносим все в подпапку, коммитимся, повторяем для всех веток, потом добавляем новый удаленный репозиторий, и мерджим все ветки в архив. Но есть один нюанс, если мы переносим все в подпапку, а потом тоже самое делаем с соседней веткой - мы можем получить мердж конфликт типа (rename/rename). Поэтому пришлось добавить коммит в одном месте, который не сработает, если нет мердж конфликта (гит проигнорирует команду, так как нечего коммитить), но если был мердж конфликт (rename/rename) - проблему решит просто git add . && git commit.
Код конфига для Gihub Actions:
name: archivist
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Archived Repository
uses: actions/checkout@v2
with:
repository: ${{ secrets.ARCHIVED_REPOSITORY }}
token: ${{ secrets.WRITE_GITHUB_TOKEN }}
path: archived
fetch-depth: 0
- run: |
cd ./archived
git config user.name github-actions
git config user.email github-actions@github.com
existBranches=($(git branch -a | grep remotes | awk -F '/' '{ print $3 }'));
for i in "${existBranches[@]}"; do
git checkout -- .
git checkout -b ${i}-move origin/${i} || true
git reset --hard HEAD
mkdir -p ../tmp/${{ secrets.ARCHIVED_REPOSITORY }}
mv ./.git ../tmp/.git
cd ..
mv ./archived/ tmp/${{ secrets.ARCHIVED_REPOSITORY }}/${i}/
cd ./tmp/
git add .
git commit -m "Preparing ${{ secrets.ARCHIVED_REPOSITORY }}/${i} for move"
cd ../
mv ./tmp/ ./archived/
cd ./archived
done;
git config -l | grep 'http\..*\.extraheader' | cut -d= -f1 | xargs -L1 git config --unset-all
git remote add archive "https://${{ secrets.GIT_USERNAME }}:${{ secrets.WRITE_GITHUB_TOKEN }}@github.com/${{ secrets.ARCHIVE_REPOSITORY }}"
git fetch archive
git checkout -b tmp_${{ secrets.ARCHIVE_REPOSITORY_DEFAULT_BRANCH }} archive/${{ secrets.ARCHIVE_REPOSITORY_DEFAULT_BRANCH }} || true
for i in "${existBranches[@]}"; do
git merge ${i}-move --allow-unrelated-histories --no-edit --no-ff > /dev/null 2>&1 || true
git add .
git commit -m "resolve conflicts for ${{ secrets.ARCHIVE_REPOSITORY }}/$i" || true
git push --force --quiet "https://${{ secrets.GIT_USERNAME }}:${{ secrets.WRITE_GITHUB_TOKEN }}@github.com/${{ secrets.ARCHIVE_REPOSITORY }}" tmp_${{ secrets.ARCHIVE_REPOSITORY_DEFAULT_BRANCH }}:${{ secrets.ARCHIVE_REPOSITORY_DEFAULT_BRANCH }}
done;
>&2 echo "All done" ;
Итоги
Скрипт я написал за пару часов, и потом еще минут 40 потратил чтобы слить туда более 20 репозиториев (и наконец-то их удалить). Конечно хотелось бы сразу добавить список, но мне подошло и текущее решение. В любой момент можно добавить новый проект в архив.
gorilych
а где история коммитов оригинальных реп?
setnemo Автор
в истории архивного репозитория. Вот, например https://github.com/setnemo/archive/commits/main?after=91a451bd54493ea723ef55d995516f910030a758+104&branch=main
gorilych
всё равно нифига не понятно. Как в интерфейсе гитхаба перейти к истории коммитов конкретной репы? Или почему в этом коммите список файлов показывает конкретную репу? https://github.com/setnemo/archive/tree/ba91697ea0daafe3b2ae01ffa5576c99287d7343
У гитхаба есть опция заархивировать репу, btw
setnemo Автор
цель была почистить от репозиториев с сохранением в один репозиторий с историей.
Интерфейс гитхаба не самый удобный для git blame