Конвертирование репозитория Git из табуляций в пробелы

от автора

imageЭта статья о том, что сказано в заголовке.

Я имел обыкновение работать на Yelp. По историческим причинам — на самом деле «это предпочли первоначальные разработчики» — их кодовая база, по большей части на Python, всегда содержала отступы с табуляциями. Это совершенно контрастирует с большей частью огромной экосистемы Python, которая, в основном, использует рекомендацию гайда по стилю стандартной библиотеки о четырех пробелах. Присутствие табуляций периодически вызывало небольшую головную боль и ворчание среди Python-разработчиков, которых сейчас бесчисленное множество, и которые привыкли к пробелам.

В конце 2013 я пожаловал в Yelp с рождественским подарком: я конвертировал табуляции в четыре пробела во всей их первичной кодовой базе. Вряд ли кто-либо еще захочет повторить то же самое, поэтому вот как я это сделал. Вообще-то. Это было два с половиной года назад, но я вовремя записал большую часть этого опыта, так что все должно быть в порядке.

Пожалуйста, заметьте: мне плевать, что вы думаете о табуляциях против пробелов. Это для другой статьи! Я больше не работаю на Yelp, в любом случае — каковы бы ни были ваши аргументы, я больше не могу отменить то, что я сделал.


Необходимые условия

Во-первых, будьте абсолютно уверены, что вы никогда не передумаете. Если вы, как организация, не уверены насчет необходимости пробелов или у вас есть влиятельные сотрудники, которые используют эту статью во зло, чтобы вернуть все назад к табуляциям как только вы уйдете, возможно, вам стоит призадуматься.

Исправление смешанных отступов

Если вы используете пробельно-чувствительный язык, вы должны исправить любые смешанные отступы. (Вы могли бы захотеть это в любом случае, либо ваш код будет выглядеть нелепо.) Под «смешанные» я имею в виду любой код, который изменит относительные уровни отступов, если ширина табуляций изменится. Представьте:

....if foo: ------->print("true") 

Если табуляция предполагается шириной в восемь клеток, как решило доброе Божество, тогда все в порядке. Но если вы собираетесь заменить все ваши табуляции четырьмя пробелами, дополнительный уровень отступа исчезает и, к сожалению, это проблема для Python.

Вы не поверите как много подобных случаев я нашел. Я наиболее нежно запомнил файл, который, по каким-то причинам, где-то был выравнен n табуляциями плюс одиночный пробел, а где-то нет. Не представляю как это случилось. (Между прочим, необходимость микроменеджмента невидимых символов переменной ширины — это одна из причин, по которой я хотел избавиться от табуляций.) (Пожалуйста, не оставляйте комментариев по этому поводу.) (Также рассмотрите set shiftround, если вы используете vim, который довольно хорошо устраняет эту проблему, но трагически недоиспользуется.)

С другой стороны, если вы какой-то монстр и хотите заменить каждую табуляцию восемью пробелами, тогда вам не нужно беспокоиться об этом.

Вы могли бы просто просканировать вашу кодовую базу на наличие ведущих пробелов, но если у вас смесь из табулированных файлов и файлов с пробелами, вы получите тонну ложных срабатываний и это будет огромной болью в заднице. Несколько более надежный подход конкретно для Python — это:

python -tt -m compileall . | grep Sorry 

-tt говорит интерпертатору рассматривать несовместимые отступы как SyntaxError. Модуль compileall рекурсивно ищет файлы .py и производит байткод .pyc, который требует разбор каждого файла, что вызовет SyntaxError. И любые ошибки, полученные во время компиляции модулей, произведут строку, начинающуюся с Sorry, продолжающуюся именем файла, номером строки и номером столбца.

Теперь вы можете провести вторую половину дня исправляя все это руками и пытаясь понять, почему этот файл был написан с отступом в 3 клетки.

Распространите определение фильтра Git

Фактический процесс использует фильтр Git, чтобы исправить любые табуляции в ветках на лету и убедиться, что ни одна новая табуляция не найдет свой путь в репозиторий. Конфигурация того, для каких файлов какие фильтры запускать хранится как часть репозитория, но, к сожалению, конфигурация того, что делает каждый фильтр — нет.

Так или иначе, вы должны добавить этот блок в конфигурацию Git ваших разработчиков — любой, занимающийся постоянной разработкой, кто не имеет определение фильтра, будет совершенно запутан. Это, вероятно, самая сложная часть процесса. К счастью, Yelp в основном проводит работу на мускулистых машинах коллективной разработки, поэтому мне нужно было только уговорить ответственного за операции прикрепить это заклинание в /etc/gitconfig и дождаться Puppet. У вас может быть другой подход.

[filter "spabs"]     clean = expand --initial -t 4     smudge = expand --initial -t 4     required [merge]     renormalize = true 

Я объясню что это все делает позже. О, и может помочь наличие установленного expand. Большинство Unix-подобных ОС уже должны его содержать. Если у вас есть разработчики, использующие Windows, expand — это одна из утилит проекта unixutils. Хоть у BSD (т.е. OS X) expand нет аргумента --initial, но, пока у вас не вошло в привычку разбрасывать символы табуляции внутри строковых литералов, вы можете благополучно обойтись без него.


Делаем это

Здесь хорошая часть.

Если это вообще возможно, заставьте всех сотрудников остановить работу на день, пока вы все не организуете. Рождество работает довольно хорошо! Я сделал это 26 декабря, когда мы были так неукомплектованы, что даже никаких развертываний не было запланировано.

Вызов ядреной опции Git

Для начала, создайте или измените .gitattributes в корне своего репозитория со следующим:

*.py    filter=spabs 

Вы можете добавить столько исходникоподобных типов файлов, сколько хотите, добавляя больше строк с различными расширениями. Я охватил все, что я мог найти из того, что мы использовали в любом репозитории, включая, но неограничиваясь: .css, .scss, .js, .html, .xml, .xsl, .txt, .md, .sh и т.д. (Я оставил .c и .h в покое. Казалось как-то кощунственно изменять табулированный C код.)

Вот короткое разъяснение. .gitattributes — это магический файл, который говорит Git как обрабатывать содержимое файлов. Наиболее распространенное использование — это, вероятно, преобразование концов строк для проектов, редактируемых и на Windows, и на Unix; я также видел использование его для описания различий на понятном языке для некоторых файлов (т.е. для каждого куска определяется какая функция в нем содержится и название функции помещается в строку заголовка куска).

Что я здесь сделал — так это добавил собственный фильтр, т.е. запуск программы на загрузку и выгрузку. Необходимая программа, expand, была указана в конфигурации Git, которую вы (надеюсь) предоставили каждому. Когда Git прикрепляет файл к репозиторию (посредством add, commit, чего угодно), он запускает фильтр clean; когда он обновляет файл на диске, основанный на репозитории, он запускает фильтр smudge. В этом случае я хочу быть предельно уверенным, что там нигде нет никаких табуляций, поэтому я заставил оба фильтра делать одно и то же: конвертировать все ведущие табуляции в четыре пробела. (строка required из конфига вынудит Git пожаловаться, если expand не завершается с кодом 0 — это означает, что что-то действительно идет не так.)

Это не идеальное решение, как мы увидим позже, но это хоть какая-то защита против проскальзывания табуляций в вашу кодовую базу. Я надеюсь, мы все можем согласиться, что смешивание табулированных строк и строк с пробелами — гораздо хуже, чем использование только табуляций или только пробелов.

Если хотите, можете отдельно закоммитить .gitattributes. Если вы это сделаете, ПОКА НЕ ОТПРАВЛЯЙТЕ ИЗМЕНЕНИЯ.

Проводим преобразование

Я параноик, а кодовая база Yelp была коллосальной, поэтому я написал целый скрипт, который проверял каждый отдельный текстоподобный файл в кодовой базе и вручную запускал на нем expand, и очень внимательно и последовательно подгонял .gitattributes. Приятной вещью в этом было то, что любой мог затем запустить этот скрипт на одном из бесчисленных более мелких репозиториев Yelp, без какого-либо понимания этого Git-колдовства. (Гитовства?) К сожалению, я ушел и у меня его больше нет.

Более быстрый путь это сделать:

git checkout HEAD -- "$(git rev-parse --show-toplevel)" 

Эта команда просит git checkout перевытащить каждый отдельный файл во всем вашем репозитории. Как побочный эффект, будет запущена команда smudge, конвертирующая все ваши табуляции в пробелы. В конечном итоге вы получите огромное количество пробельных изменений.

Вероятно, вы захотите прогнать набор своих тестов примерно прямо сейчас.

Затем, зафиксируйте изменения! По традиции Yelp, когда переписывается каждый отдельный файл во всей кодовой базе, я приписал коммит любимому талисману Yelp — Дарвину. Это лучше выделяется в git blame и сохранило чрезвычайно критичную целостность моей статистики коммитов.

Отправляйте в master и вы закончили. Более или менее.


Влияние на Git

Я думаю, было обработано где-то около двух миллионов строк кода, и Git справился с этим на удивление хорошо. Фиксирование изменений было практически мгновенным, и не было каких-либо заметных проблем с производительностью, за исключением пары шероховатостей, раскрытых далее.

Влияние на рабочий процесс в Git довольно минимальное. Большинство осложнений случится с людьми, которые случайным образом выполняют колдовские действия и не совсем понимают что они делают. Разработчики, которые просто фиксируют изменения и производят слияния не должны ощутить никаких проблем.

  • Свежая выгрузка репозитория (или master, хотя бы) будет содержать пробелы, потому что загруженные файлы содержат пробелы.
  • Любые текущие ответвления будут содержать табуляции, потому что они не видели файл .gitattributes или коммита массового преобразования.
  • Слияние текущей ветки с master (в любом направлении) прозрачно конвертирует все табуляции в ветке в пробелы перед слиянием. Разработчик даже не должен заметить ничего необычного.

    Это волшебная вещь, которую делает настройка merge.renormalize. renormalize — это опция для стратегии слияния по-умолчанию (recursive), которая применяет фильтры перед слиянием; настройка merge.renormalize включает это поведение по-умолчанию для git merge. Т.к. слитый .gitattributes содержит фильтр, он применяется с обеих сторон. Я думаю.

    Замечание: Я не знаю, работает ли renormalize с более экзотическими стратегиями слияния. Я также не знаю, что происходит, если есть конфликты слияния внутри самого .gitattributes.

    Замечание 2: renormalize не применяется к новым файлам, созданным в ветке — они существуют только на одной стороне, поэтому нет необходимости сливать их. См. ниже.

  • Перемещение текущей ветки не сработает, или, точнее, произведет гадзиллион конфликтов слияния. merge.renormalize не применяется к git rebase, а настройки rebase.renormalize нет.

    К счастью, вы можете делать то же самое вручную с -X. git rebase -Xrenormalize origin/master должно нормально работать.

    -X поддерживается всеми командами Git, которые делают что-либо похожее на слияние, то же самое применимо, например, к git cherry-pick или git pull --rebase. Вы можете также использовать эту опцию и с git merge, но настройка делает это необязательным.

  • Старые скрытые коммиты, вероятно, не обработаются начисто, а git stash apply трагически игнорирует -X. Я знаю два обхода:
    1. Конвертировать скрытый коммит в ветку с помощью git stash branch, затем слить или переместить, или что угодно, как показано выше.
    2. Обработать скрытый коммит вручную, например, с помощью git cherry-pick 'stash@{0}' -n -m 1 -Xrenormalize. Вам понадобится -m 1 («использовать diff с родителем 1»), потому что под капотом скрытый коммит — это слияние между несколькими отдельными коммитами, которые содержат разные части скрытого коммита, а cherry-pick необходимо знать с каким родителем использовать diff для создания патча. -n просто предотвращает фиксирование изменений, поэтому описание вашего скрытого коммита «в процессе работы: эта хрень не работает» автоматически не превратится в сообщение коммита.
  • Аннотации, на самом деле, не разрушаются окончательно. git blame -w игнорирует изменения, связанные с пробелами.
  • Полный размер вашего репозитория увеличится, но не так сильно, как можно подумать. Git, в конечном счете, хранит сжатые двоичные патчи, а патч, который, в основном, содержит одни и те же два символа, сжимается очень хорошо. Я хочу сказать, что репозиторий Yelp вырос всего на 1% или около того. (Увеличение может быть больше в краткосрочной перспективе, но git gc, в конечном счете, все это посжимает.)

Возможные проблемы

Относительно незначительные, учитывая величину изменений. Некоторые краткосрочные, некоторые сохраняющиеся в течение всей жизни вашего проекта, извините.

Старые ветки, которые привнесли новые табулированные файлы

Около недели после преобразования, как только разработчики стеклись обратно с каникул, произошел внезапный всплеск путаницы вокруг фантомного файла, показанного в git status. Он был помечен как измененный и ни git checkout, ни git reset не могли заставить его исчезнуть. Каждый, у кого была эта проблема, видел один и тот же файл, помеченный как измененный, но никто к нему не прикасался.

Выяснилось, что у кого-то была текущая ветка со вновь созданным файлом, выравненным с помощью табуляций. Эта ветка была влита в master где-то через неделю после преобразования, а разработчики видели новый файл как якобы измененный, после их последующих взаимодействий с master.

Проблема была в том, что git прилежно применил фильтр smudge, когда вытаскивал этот файл наружу на диск, конвертируя его табуляции в пробелы… но у копии в репозитории все еще были табуляции, заставляющие файл выглядеть измененным. git checkout этого не исправила, потому что именно она, в первую очередь, и вызвала проблему: выгрузка бы снова вызвала фильтр и произвела измененный файл. (Я подозреваю, что этого бы не случилось, если бы наши clean и smudge на самом деле были бы противоположны по действию и в репозиторий возвращались бы табулированные файлы, но мы этого определенно не хотели.)

Исправление этого было достаточно простым: я попросил каждого просто зафиксировать псевдоизменения в отдельный коммит, всякий раз, когда бы это ни произошло. (Если файл к тому же был изменен, git diff -w покажет «чистый» diff.) Пробельное изменение произойдет во множестве коммитов, но все они сольются чисто, как только попадут в master, т.к. они все содержат одно и то же изменение. Однажды загруженная копия файла, содеражащая пробелы, решает проблему.

Я видел несколько подобных проявлений в течение нескольких первых недель, но все они разрешились сами собой, пока разработчики фиксировали пробельное изменение, производимое Git-ом. Я думаю это можно было бы предотвратить более разумным перехватчиком git, который применяет фильтры к новым файлам в ходе слияния, но это было бы значительно сложнее.

Прерывисто медленный git status

Один или два разработчика видели git status безбожно медленным, скорее занимающим минуту или более, чем менее полусекунды.

Немного strace показало, что expand запускалась десятки тысяч раз. Упс!

Разработчики, сталкивающиеся с этим, заканчивали созданием свежего клона, который, чудесным образом, решал проблему. Мое лучшее предположение состоит в том, что мы случайно попадали в медленную логику поведения в git status — решение проблемы «пикантный Git». Я должен был здесь сделать некоторые предположения, потому что последствия в той документации описываются не полностью, и, похоже, очень немного людей когда-либо сталкивались с этим.

По сути, Git немного мухлюет, чтобы быстро узнавать «изменен ли этот файл?»: он сравнивает только файловую статистику, вроде размера и времени последнего изменения. У Git-а есть файл, называемый «index», который содержит, ну, в общем, индекс: это описание того, как будет выглядеть следующий коммит, если вы запустите простую команду git commit. Индекс также помнит, какие файлы на диске были изменены и когда в последний раз они записывались. Поэтому, если время последнего имзенения файла раньше, чем время последнего изменения индекса, можно с уверенностью предположить, что индекс все еще корректен. Но также возможно, что файл был изменен, сохраняя свой размер, сразу после того, как индекс был записан — так быстро, что время последнего изменения у них совпадает.

Чтобы это исправить, если Git видит файл, который он считает неизмененным, а время последнего изменения полностью совпадает с индексным (или новее, конечно), Git сравнит полное содержимое файла с тем, что находится в индексе. Естественно, это занимает гораздо больше времени, чем просто сравнение статистики файлов.

Теперь представьте кого-нибудь, кто переключается с очень старой ветки на master. В процессе обновится огромное количество файлов, но машина достаточно быстрая, чтобы все обновленные файлы и индекс Git-а получили одинаковое время последнего изменения.

Я пришел к такому объяснению только после того, как все затронутые разработчики сдались и пересоздали клонов, поэтому я никогда не был уверен, был ли это тот самый случай, и я больше никогда такого не видел. Но это выглядит правдоподобно.

Если вы обнаружите у себя индекс с медленным кэшем, вам просто надо сделать что-нибудь, что обновит индекс. Команды только для чтения, вроде git status или git diff, этого не сделают, но git add сделает. Если у вас пока еще нечего добавлять, вы можете принудительно выполнить обновление вручную:

git update-index somefile 

somefile может быть произвольным файлом в репозитории. Эта команда заставляет Git проверить его и записать признак его измененности в индекс — в качестве побочного эффекта, теперь индекс будет обновлен.


Заключительная уборка

Как только все будет улажено, вы можете захотеть удалить весь хлам, связанный с фильтрацией, и просто добавить перехватчик перед фиксацией изменений, который сразу отклоняет табуляции.

Вы также можете сказать своим разработчикам, что они наконец могут удалить все свои хаки в .vimrc для переключения на табуляции конкретно в вашей кодовой базе. (Может быть сказать им, что следовало бы использовать vim-sleuth.)


Понравился перевод? Поддержите автора donut-ом донатом. Спасибо! 🙂
ссылка на оригинал статьи https://habrahabr.ru/post/316240/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *