Pull to refresh

Comments 13

всё это работает на маленьком проекте. На большом и старом все тесты будут долгими, формат вслепую тоже не стоит делать.

"git add ." - это вообще за гранью зла, особенно если есть специально не добавленные в коммит файлы.

Это будет хорошо работать для одного маленького бинариника, но если мы говорим про промышленную разработку где миллион строк кода то при каждом коммите проверять весь проект просто не реально

"Очевидно, что повествование здесь очень жирно намекает на использование..." на самом деле на использование CI в первую очередь.

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

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

... и если есть какие-то проблемы а коде - в твоей личной ветке, созданной для решения конкретной таски, отработает настроенный для проекта линтер, анализатор, юнит-тесты, даже через лысый со сборкой всего бинаря и большим пулом end-to-end тестов и обо всём этом ты узнаешь после завершения CI джобов.

Причём некоторые стейджи (допустим, полную сборку с тяжеловесными тестами) ты сможешь запустить вручную непосредственно перед тем, когда почувствуешь в себе готовность запросить у коллег код-ревью и аппрувы на коммит в мастер.

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

`git add .` сломает первую же попытку использовать `git add -i`.
Запускать тесты в рабочем каталоге -- отвратительная идея. Есть есть какой-то текущий стейт, то будет проверен он, а не содержимое репозитория. Незакоммиченные файлы, временные файлы и т.п.

Наша задача - не делать разработку потной; наша задача - не выкатить баг в продакшн.

Разработка должна быть быстрой и приятной, а все эти чеки пусть за нас делает облако в нашей любимой CI - системе. Пока мы будем на сэкономленное время попивать кофе с ощущением выполненной работы, оно где-то там всё проверит само и без того, чтобы отнимать наши нервы локально.

Если всё будет хорошо, то CI система скажет CD системе, что пора радовать заказчика, и та автоматически отправит готовый продукт без багов куда нужно. Наше распитие кофеин-содержащих продуктов не будет прервано ни на секунду.

В случае ошибки же, CI даст нам по шапке автоматически, но зато без этого неприятного чувства вины перед начальством, коллегами и заказчиком. Мы, спокойно закончив попивать кофе, идём чинить баг.

Все нервы сэкономлены, доза кофеина принята, баг чинится, зарплата капает...

CI работает дольше и дороже, чем локальный тестинг. ИМХО, вначале лучше протестировать локально, чем гонять CIпо 20 раз из-за ошибок линтера

Коллега, вы делаете мне больно своим кодом. Можно же без аллокации вектора 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 })
    }
}

Отличная реализация, спасибо за код! Растовик из меня не самый опытный, поэтому я не особо знал, куда можно деться от chars(), я вроде пытался мыкаться туда-сюда, но безуспешно. Код утащил к себе)

Начинание отличное, но зачем же останавливаться на полпути? ?

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, особенно на больших проектах, можно вообще писать статьи и книги. Просто любая автоматизация проверок будет лучше, чем никакой автоматизации.

Sign up to leave a comment.

Articles