Комментарии 13
потная разработка ..... это как?
всё это работает на маленьком проекте. На большом и старом все тесты будут долгими, формат вслепую тоже не стоит делать.
Это будет хорошо работать для одного маленького бинариника, но если мы говорим про промышленную разработку где миллион строк кода то при каждом коммите проверять весь проект просто не реально
x && \
y && \
ИМХО проще писать для /bin/bash и сделать set -e
"Очевидно, что повествование здесь очень жирно намекает на использование..." на самом деле на использование CI в первую очередь.
Во-вторую очередь – на ограничения прямого пуша в мастер без предварительного PR в важных участках проекта, где дают по шапке.
А коммиты с поломанным кодом иногда хочется делать, когда время от времени нужно сохранить работу в середине процесса.
... и если есть какие-то проблемы а коде - в твоей личной ветке, созданной для решения конкретной таски, отработает настроенный для проекта линтер, анализатор, юнит-тесты, даже через лысый со сборкой всего бинаря и большим пулом end-to-end тестов и обо всём этом ты узнаешь после завершения CI джобов.
Причём некоторые стейджи (допустим, полную сборку с тяжеловесными тестами) ты сможешь запустить вручную непосредственно перед тем, когда почувствуешь в себе готовность запросить у коллег код-ревью и аппрувы на коммит в мастер.
Но и пре-хуки имеют право на жизнь, как первая линия обороны, всё-таки CI всегда чуть более тормозной, чем локальная работа.
`git add .` сломает первую же попытку использовать `git add -i`.
Запускать тесты в рабочем каталоге -- отвратительная идея. Есть есть какой-то текущий стейт, то будет проверен он, а не содержимое репозитория. Незакоммиченные файлы, временные файлы и т.п.
Наша задача - не делать разработку потной; наша задача - не выкатить баг в продакшн.
Разработка должна быть быстрой и приятной, а все эти чеки пусть за нас делает облако в нашей любимой CI - системе. Пока мы будем на сэкономленное время попивать кофе с ощущением выполненной работы, оно где-то там всё проверит само и без того, чтобы отнимать наши нервы локально.
Если всё будет хорошо, то CI система скажет CD системе, что пора радовать заказчика, и та автоматически отправит готовый продукт без багов куда нужно. Наше распитие кофеин-содержащих продуктов не будет прервано ни на секунду.
В случае ошибки же, CI даст нам по шапке автоматически, но зато без этого неприятного чувства вины перед начальством, коллегами и заказчиком. Мы, спокойно закончив попивать кофе, идём чинить баг.
Все нервы сэкономлены, доза кофеина принята, баг чинится, зарплата капает...
Коллега, вы делаете мне больно своим кодом. Можно же без аллокации вектора char
-ов и без лишней мутабельности:
impl FromStr for Address {
type Err = ParseAddressError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let &[col, row] = s.as_bytes() else {
return Err(ParseAddressError);
};
let col = match col {
b'a'..=b'h' => col - b'a',
b'A'..=b'H' => col - b'A',
_ => return Err(ParseAddressError),
};
let row = match row {
b'1'..=b'8' => row - b'1',
_ => return Err(ParseAddressError),
};
Ok(Self { col, row })
}
}
Начинание отличное, но зачем же останавливаться на полпути? ?
1. Берём pre-commit.
Да, он написан на питоне, а не на расте, но на что только не пойдёшь ради чистого и собирающегося кода, можно даже переквалифицироваться из питон-разработчка в раст-разработчика.
Делаем pre-commit install
- это создаёт хук .git/hooks/pre-commit
, который будет впоследствии автоматически запускать pre-commit run
на изменённые файлы.
Делаем pre-commit sample-config > .pre-commit-config.yaml
- это создаёт конфиг с набором дефолтных хуков, которыми pre-commit
"из коробки" готов проверять изменённые файлы:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
trailing-whitespace
будет ругаться на строчки, оканчивающиеся пробелами, и эти пробелы стирать. Полезно, потому что не во всех редакторах это включено по-умолчанию, или вообще присутствует как класс.end-of-line-fixer
будет ругаться, если в каком-то файле забыли добавить\n
в конец (привет винда!), или наоборот насовали слишком много\n\n\n\n
- и тоже добавлять-стирать нужное.check-yaml
будет проверять изменённые в коммите YAML-файлы на синтаксическую корректность.check-added-large-files
будет давать по шапке за попытку закоммитить в репозиторий бинарный суп на 10+ мегабайт - для такого боги даровали нам Git-LFS, Artifactory, Nexus или простихоспаде файлопомойки вроде SFTP/NFS/SMB.
Если хочется ещё - можно докинуть в список, например, check-merge-conflict
- оно не даст случайно добавить в коммит "сломаный" после неудачного мержа файл с кучей >>> === <<<
.
Если что-то кажется лишним - можно убрать.
2. Подключаем "плагин" для проверки проектов на Rust.
На изкоробочном функционале далеко не уедешь - да это и не требуется, поскольку pre-commit рассчитан на активное переиспользование чужого кода с гитхаба:
- repo: https://github.com/doublify/pre-commit-rust
rev: v1.0
hooks:
- id: cargo-check
- id: clippy
- id: fmt
(содержимое репозитория pre-commit-rust
настолько тривиально, что его можно было бы переписать с нуля - но пока более интересна сама возможность с лёгкостью притащить чужое)
Чем это отличается от описанного в статье (помимо отсутствия cargo test
)? Тремя важными нюансами:
pre-commit работает только с изменениями, претендующими на коммит. Если часть изменений в
main.rs
добавлена в staged, а часть нет - из-за интерактивногоgit add
, или вы просто дописали в файл что-то новенькое после стейджа - pre-commit автоматически спрячет "лишнее" черезgit stash
. Это позволяет избежать дурацких ситуаций, когда текущий файл сам по себе норм - но в стейдж например забыли добавить блок с новой функцией.Автор в хуке делает леденящее
cargo fmt && git add .
, которое не только похерит возможность использованияgit add --interactive
(как уже писали выше), но и будет добавлять в коммит остальные файлы из рабочей директории, даже если вы их не помечали черезgit add
. В свою очередь, pre-commit послеgit stash
считает хук провалившимся как если он вернул ненулевой код, так и если после работы хука в репозитории появились not staged файлы - это именно то, что мы имеем сcargo fmt
.Большая часть хуков в pre-commit срабатывают только на изменённые файлы определённого типа. Так, хук
fmt
дёргаетcargo fmt -- <changed rust files>
: это означает, что во-первых, если форматирование поехало где-то в другой части проекта, ложно-положительных ошибок мы не получим, а во-вторых, если в коммите не трогается код на расте (а например редактируется сугубоREADME.md
- то и растовские хуки срабатывать не будут. Справедливости ради,cargo check
иcargo clippy
не имеют режима анализа только одного файла - так что с ними проверка будет "полная".
3. Добавляем в pre-commit собственные хуки.
Если нам позарез хочется перед каждым коммитом запускать тесты, выполнять полноценную сборку, вызывать сотону или делать другие нестандартные вещи - pre-commit вполне поддерживает написание собственных хуков:
- repo: local
hooks:
- id: cargo-test
name: run Rust tests
description: Why not?
entry: cargo test
language: system
types: [rust]
pass_filenames: false
Хоп, и теперь он будет вызывать содержимое entry
как обычный процесс (благодаря language=system) всякий раз, когда в потенциальном коммите изменяются rust-файлы. Вместо types: [rust]
можно также указать files: <regex>
.
По умолчанию pre-commmit передаёт в хук пути до изменённых файлов - но в нашем случае cargo test some/changed/file.rs
просто скажет, что running 0 tests, test result: ok. 0 passed; 0 failed; 3 filtered out
; поэтому мы велим вызывать команду как она есть, не скармливая в неё файлы.
Аналогично можно будет запускать какой-нибудь самописный tools/linter.sh
, который припасён у вас в репозитории.
Большинство плагинов для pre-commit пишутся из расчёта на то, чтобы вызываться pre, собственно, commit - но сама тулза поддерживает ещё и pre-rebase, pre-push и так далее. Если у вас слишком много тестов, их можно вызывать только перед пушем - для этого в описание хука надо добавить stages: [push]
(работает и для изкоробочных, и для сторонних, и для локальных хуков), а в самое начало конфига рядом с repos:
- добавить default_install_hook_types: [pre-commit, pre-push]
и вызвать pre-commit install
ещё разок.
4. Добавляем pre-commit в CI.
Хуки в гите работают на доверии и добровольческой основе. Любой зашедший с улицы Васян после git clone
получит чистую папку .git/hooks/
и сможет радостно коммитить несобирающийся код с лишними пробелами и переносами строк. И даже если пан Васян соизволят вызвать у себя pre-commit install
для инициализации .git/hooks/pre-commit
, это не помешает им позже сказать "у меня лапки!" и сделать git commit --no-verify
, послав далеко и надолго простыню непонятных ошибок. Едиинственное, что остановит Васяна - разработка через пулл-реквесты и правильно настроенная система Continuous Integration.
Самый простой вариант - иметь один и тот же набор проверок и "на клиенте" и "на сервере". Благо pre-commit может запускаться не только на списке файлов из staged, но и на всех файлах целиком pre-commit run --all-files
), и даже на изменённых файлах из указанного промежутка между коммитами (pre-commit run --from-ref=... --to-ref=...
.
На условном Github Actions это можно сделать так:
name: pre-commit
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: pre-commit linters
run: |
set -ex
pip install pre-commit
# github does not have branch names, but BASE_REF is a name, not a commit SHA
git fetch
if [ -n "$GITHUB_BASE_REF" ]; then
# this is a PR - check only changed files
pre-commit run \
--verbose \
--show-diff-on-failure \
--from-ref=${{ github.event.pull_request.base.sha }} \
--to-ref=${{ github.event.pull_request.head.sha }}
else
# this is a push to main - check everything
pre-commit run \
--verbose \
--show-diff-on-failure \
--all-files
fi
Это не значит, что ваш CI всегда должен плясать от pre-commit; о "правильной настройке" CI, особенно на больших проектах, можно вообще писать статьи и книги. Просто любая автоматизация проверок будет лучше, чем никакой автоматизации.
Делаем разработку на Rust еще более потной с помощью git