Конвертування репозиторію 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.)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.