Когда-то пару лет назад на работе ко мне обратился коллега (привет!), который знает мою любовь к автоматизации с довольно нетривиальной просьбой. Нужно было почистить старые репозитории в корпоративной орге Github, но не совсем удалить, а сохранить на "всякий случай". Да и не просто сохранить, а сохранить с git историей. Мы с ним довольно быстро набросали скрипт на баше, который принимал аргументом orgName/projectName. После окончания работы скрипта он пушил код в отдельную ветку, и потом можно было это померджить в основную "ветку-хранилище". Скрипт был написан быстро, задачу решал, но все равно оставалось пара действий, которые надо было сделать "руками". Но в том случае это было нормально, так как требовалось подтверждение на архивирование старого проекта. Еще тогда у меня появилась идея сделать Github Actions workflow, с которым я только познакомился. Но свободного времени на это не было. И вот спустя год-полтора я наконец-то сделал задуманное - так появился на свет Bygone project, который доступен на github.


В 2017-2018 я учился в филиале французской школы 42 и мне приходилось много писать на Си. Было много учебных проектов, начиная от реверс-инжениринга стандартной библиотеки Си, printf, sha/md5/des алгоритмов, заканчивая своими собственными проектами, которые не имели отношения к Школе 42. Был простенький DNS proxy, сниффер сетевых пакетов, реверс-инжениринг игры Сапер. Сейчас я понимаю насколько ужасен тот код, но хочется его сохранить как память о бессонных ночах над сегфолтами, бас ероррами, потерянными указателями, утечках памяти. Два десятка репозиториев воспоминаний.

Мой github давно хотел чистки, и я все откладывал идею сделать полностью автоматический сборщик до более свободных дней. И вот у меня четвертый день кашель и второй день температура 37,5 - спать не получается - настало время доделать "хотелку". Репозиторий для проекта я создал несколько месяцев назад, но руки так и не доходили сделать.

Сбор требований

Скриншот моего архива в Github
Скриншот моего архива в Github
  1. Скрипт должен складывать каждую ветку проекта в отдельную папку

  2. Скрипт должен сохранять историю коммитов

  3. Скрипт должен быть устойчив к мердж конфликтам типа (rename/rename)

  4. Скрипт должен работать через Gihub Actions и запускать вручную

  5. Скрипт должен принимать переменными окружения репозиторий архива, архивируемый репозиторий, основную ветку архива в которую сливать все, а так же токен и имя пользователя github, чтобы запушить новое

  6. Скрипт должен работать без вмешательства через консоль (например с браузера телефона можно добавить в архив новый проект)

  7. Скрипт должен быть максимально простым, чтобы побудить людей поучаствовать в его доработке (<3 open source)

Реализация

Согласно списку требований я решил написать сам скрипт на баше, хотя я в нем и не силен. Логика довольно простая: получаем список веток, копируем содержимое проекта и переносим все в подпапку, коммитимся, повторяем для всех веток, потом добавляем новый удаленный репозиторий, и мерджим все ветки в архив. Но есть один нюанс, если мы переносим все в подпапку, а потом тоже самое делаем с соседней веткой - мы можем получить мердж конфликт типа (rename/rename). Поэтому пришлось добавить коммит в одном месте, который не сработает, если нет мердж конфликта (гит проигнорирует команду, так как нечего коммитить), но если был мердж конфликт (rename/rename) - проблему решит просто git add . && git commit.

По комментарию коммитов видно, что ветка master слилась без проблем, а на ветке fps был конфликт (rename/rename), который разрешился обычным  git add . && git commit.
По комментарию коммитов видно, что ветка master слилась без проблем, а на ветке fps был конфликт (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 репозиториев (и наконец-то их удалить). Конечно хотелось бы сразу добавить список, но мне подошло и текущее решение. В любой момент можно добавить новый проект в архив.

Ссылка на проект

Ссылка на архив

Комментарии (7)


  1. gorilych
    30.07.2021 09:36

    а где история коммитов оригинальных реп?


    1. setnemo Автор
      30.07.2021 11:21

      в истории архивного репозитория. Вот, например https://github.com/setnemo/archive/commits/main?after=91a451bd54493ea723ef55d995516f910030a758+104&branch=main


      1. gorilych
        30.07.2021 11:31

        всё равно нифига не понятно. Как в интерфейсе гитхаба перейти к истории коммитов конкретной репы? Или почему в этом коммите список файлов показывает конкретную репу? https://github.com/setnemo/archive/tree/ba91697ea0daafe3b2ae01ffa5576c99287d7343

        У гитхаба есть опция заархивировать репу, btw


        1. setnemo Автор
          30.07.2021 11:48

          цель была почистить от репозиториев с сохранением в один репозиторий с историей.

          Интерфейс гитхаба не самый удобный для git blame


  1. nin-jin
    30.07.2021 10:06

    Странно, что вы не использовали git-subtree.


  1. Harrix
    06.08.2021 18:46
    +1

    Огромное спасибо! Всё работает как надо. Теперь можно прибраться в своих репозиториях.


    1. setnemo Автор
      06.08.2021 18:48
      +1

      рад что поделка принесет пользу не только мне)