Статья рассказывает об одном из способов форматирования исходного кода средствами clang-format, git и sh.
Согласитесь, приятно и полезно, когда в проекте исходный код выглядит красиво и единообразно. Это облегчает его понимание и поддержку. В большинстве проектов, которые я встречал, существуют определенные правила оформления кода. Добиться выполнения этих правил всеми участниками проекта иногда нелегко. Тогда на помощь приходят специальные программы, такие как clang-format, astyle, uncrustify. Но форматирование кода этими программами может вызывать некоторые проблемы. Самая главная в том, что форматеры меняют файлы целиком, а не только изменённые строки. Эту проблему мы решили на одном из проектов. Для форматирования кода использовали программу clang-format-diff-6.0. Поначалу, это делалось командой git diff -U0 --no-color | clang-format-diff-6.0 -i -p1. Но с ней возникали проблемы:
- Программа определяла типы файлов только по расширению. Например, файлы с расширением ts, которые у нас имели формат xml, воспринимала как JavaScript и валилась при форматировании. Потом, она зачем то пыталась поправить pro-файлы проектов Qt, наверное, как Protobuf
- Надо было запускать вручную, перед добавлением в индекс git. Можно легко забыть это сделать
В результате получился следующий sh-скрипт запускаемый как pre-commit хук для git:
#!/bin/sh
CLANG_FORMAT="clang-format-diff-6.0 -p1 -v -sort-includes -style=Chromium -iregex '.*\.(cxx|cpp|hpp|h)/pre> "
GIT_DIFF="git diff -U0 --no-color "
GIT_APPLY="git apply -v -p0 - "
FORMATTER_DIFF=$(eval ${GIT_DIFF} --staged | eval ${CLANG_FORMAT})
echo "\n------Format code hook has called-------"
if [ -z "${FORMATTER_DIFF}" ]; then
echo "Nothing to be formatted"
else
echo "${FORMATTER_DIFF}"
echo "${FORMATTER_DIFF}" | eval ${GIT_APPLY} --cached
echo " ---Format of staged area completed. Begin format unstaged files---"
eval ${GIT_DIFF} | eval ${CLANG_FORMAT} | eval ${GIT_APPLY}
fi
echo "------Format code hook has completed----\n"
exit 0
Что делает скрипт:
GIT_DIFF="git diff -U0 --no-color " изменения в коде которые подадут на вход clang-format-diff-6.0
- -U0 обычно git diff выводит т.н. "контекст" несколько неизменёных строк кода вокруг тех что были изменены. Но clang-format-diff-6.0 форматирует их тоже! Поэтому, контекст в данном случае не нужен
CLANG_FORMAT="clang-format-diff-6.0 -p1 -v -sort-includes -style=Chromium -iregex '.*.(cxx|cpp|hpp|h)$' " команда для форматирования diff полученного через стандартный ввод
- clang-format-diff-6.0 скрипт из пакета clang-format-6.0 . Есть другие версии, но все тесты были только на этой
- -p1 взято из примеров в документации. Так понимаю это обеспечивает совместимость с выводом git diff
- -style=Chromium готовый пресет стиля форматирования кода. Возможные значения так же: LLVM, Google, Mozilla, WebKit.
- -sort-includes опция сортировки по алфавиту директив #include . Не обязательна
- -iregex '.*.(cxx|cpp|hpp|h)$' регулярное выражение фильтрующее имена файлов по расширениям. Тут перечислены только те расширения которые надо форматировать. Это убережёт программу от падения и неожиданных глюков. Скорее всего для вас, список надо дополнить. Кроме С++ можно форматировать C/Objective-C/JavaScript/Java/Protobuf файлы. Хотя эти остальные типы мы не тестировали
GIT_APPLY="git apply -v -p0 - " применение патча выданного предыдущей командой к коду
- -p0 по умолчанию git apply пропускает первый компонент в пути к файлу, это несовместимо с форматом который выдаёт clang-format-diff-6.0 . Здесь отключено такое пропускание
FORMATTER_DIFF=$(eval ${GIT_DIFF} --staged | eval ${CLANG_FORMAT}) изменения форматера для индекса
echo "${FORMATTER_DIFF}" | eval ${GIT_APPLY} --cached форматирует исходный код в индексе(после git add). К сожалению, нет такого хука который срабатывал бы перед добавлением файлов в индекс. Поэтому форматирование разделено на две части, того что в индексе и того что не добавлено в индекс.
eval ${GIT_DIFF} | eval ${CLANG_FORMAT} | eval ${GIT_APPLY} форматирование кода не в индексе. Запускается только когда что-то было отформатировано в индексе. Форматирует вообще все текущие изменения в проекте(под контролем версий), а не только из предыдущего шага. Это спорное, на первый взгляд решение. Но оно оказалось удобным т.к. рано или поздно все изменения надо форматировать тоже. Можно заменить "| eval ${GIT_APPLY}" опцией -i которая заставит ${CLANG_FORMAT} менять файлы самостоятельно
- Установить clang-format-6.0
- cd /tmp && mkdir temp_project && cd temp_project
- git init
- Добавить под контроль версий и закомитить любой C++ файл под именем wrong.cpp . Желательно >50 строк неформатированого кода
- Сделать скрипт .git/hooks/pre-commit с содержимым выше
chmod +x .git/hooks/pre-commit
- Запустить скрипт вручную .git/hooks/pre-commit Он должен запускаться с сообщением "Nothing to be formatted", без ошибок интерпретатора
- Создать file.cpp с содержимым int main() { for (int i = 0; i < 100; ++i) { std::cout << "First case" << std::endl; std::cout << "Second case" << std::endl; std::cout << "Third case" << std::endl; } } одной строкой, либо с другим плохим форматированием. В конце перевод строки!
- git add file.cpp && git commit -m "file.cpp" должны быть сообщения от скрипта типа "Патч file.cpp применен без ошибок"
- git log -p -1 должен показать добавление форматированного файла
- Если file.cpp попал в коммит действительно форматированным, значит можно тестировать форматирование только в diff. Измените пару строк wrong.cpp, так что-бы форматер на них среагировал. Например, добавить неадекватные отступы в коде, вместе с другими изменениями. git commit -a -m "Format only diff" должен залить форматированные изменения, но не тронуть другие части файла
git diff --staged ( который здесь ${GIT_DIFF} --staged) выдаёт diff только файлов добавленных в индекс. А clang-format-diff-6.0 обращается к полным версиям файлов за пределами него. Поэтому, если изменить какой то файл сделать git add, потом изменить тот же файл. clang-format-diff-6.0 будет генерировать патч для форматирования кода (в индексе) на основе отличающегося файла. Таким образом файл после git add и до коммита, лучше не редактировать. Ниже пример такой ошибки.
- Добавить в file.cpp , "Second case" лишний std::endl. (std::cout << "Second case" << std::endl << std::endl;) и добавляете несколько табов лишнего отступа перед строкой.
- git add file.cpp
- Очистить строку(в этом же файле) с "First case" так что бы на её месте остался(!) только перенос строки
- git commit -m "Formatter error on commit"
Скрипт должен сообщить "error: при поиске:" т.е. git apply не нашёл контекст патча, выданного clang-format-diff-6.0. Если вы не поняли в чём тут проблема, просто не меняйте файлы после git add их и до git commit. Если надо поменять, можете сделать коммит(без push) и потом git commit --amend с новыми изменениями.
Самое серьёзное ограничение - необходимость иметь в конце каждого файла перевод строки. Это старая особенность git, поэтому большинство редакторов кода, поддерживают автоматическую вставку такого перевода в конец файла. Без этого скрипт будет падать при коммите нового файла, но это не принесет никакого вреда
Очень редко, clang-format-diff-6.0 форматирует код неадекватно. В этом случае, можно добавить какие-нибудь бесполезные элементы в код, типа точки с запятой. Либо, окружить проблемный код комментариями, /* clang-format off */ и /* clang-format on */.
Если хотите посмотреть, какие изменения сделает скрипт на ваших текущих правках(не в индексе), используйте git diff -U0 --no-color | clang-format-diff-6.0 -p1 -v -sort-includes -style=Chromium -iregex '.*.(cxx|cpp|hpp|h)$' Так же можно проверить, как будет работать скрипт на последних(30-и) коммитах git filter-branch -f --tree-filter "${PWD}/.git/hooks/pre-commit " --prune-empty HEAD~30..HEAD . Данная команда должна была форматировать предыдущие коммиты, но по факту меняет только их id. Поэтому, такие эксперименты в отдельной копии проекта! После она станет непригодной для работы.
Субъективно, от такого решения гораздо больше пользы чем вреда. Но надо тестировать поведение clang-format-diff разных версий, на коде вашего проекта, с конфигом подходящим под ваш стиль кода.
К сожалению, такой же git-hook для Windows сделан не был. Если хотите статью для быстрого старта с clang-format, загляните сюда.