lsw: одна команда, чтобы увидеть все свои git worktree

Если вы работаете в стиле «одна задача — свой worktree», у вас наверняка скопилась папка, где рядом лежат полдюжины чекаутов одного проекта. Я в какой-то момент устал бегать по ним с git status и написал lsw — это «ls» для worktree. Одна команда показывает все репозитории и worktree вокруг: ветку каждого, есть ли незакоммиченные правки, насколько ветка ушла от origin. Read-only, без зависимостей, один бинарник.
Откуда это взялось
Напомню на всякий случай, для тех кто не пользуется: git worktree add ../webapp-1042 feature/TASK-1042 создаёт второй рабочий каталог того же репозитория. Две ветки чекнуты одновременно, никаких стэшей и git checkout-каруселей: в одной папке чините прод, в другой спокойно пишете фичу. Штука недооценённая, я почти всю разработку так и веду — каждая задача в своём каталоге. Через месяц рядом с проектом скапливается примерно такое:
webapp/ ← main, главный клон
webapp-1042/ ← фича TASK-1042, в ней сейчас работаю
webapp-hotfix/ ← а тут висят незакоммиченные правки
webapp-review/ ← ревью чужого PR
docs/
И каждый раз, возвращаясь к проекту, я прохожу один и тот же квест: в какой папке я вчера остановился, где висят незакоммиченные правки, какая ветка уехала вперёд от origin, а какая отстала, и вообще жива ли ещё та remote-ветка. Руками это cd по всем каталогам плюс git status в каждом. С двумя терпимо, с пятью — начинает бесить. git worktree list помогает частично: он работает только внутри одного репозитория и ничего не знает ни про dirty-состояние, ни про ahead/behind.
Готовой лёгкой утилиты, которая отвечала бы на всё это одной командой по всей папке проектов, мне не попалось. Так и появился lsw.
Что показывает
Запускаю в папке с проектами:
Внутри репозитория lsw показывает его worktree, внутри обычной папки — сканирует репозитории на уровень вглубь (-2 и -3 — глубже). Для каждого worktree — ветка, грязность и дистанция до upstream.
Токен | Значение |
|---|---|
| заголовок группы: путь и origin в компактном виде |
| worktree, в котором вы сейчас находитесь |
| незакоммиченные изменения (жёлтым) |
| ветка впереди upstream на N коммитов (циан) |
| позади upstream на N (красный) |
| у ветки нет remote tracking |
| remote-ветка удалена |
| bare-репозиторий |
Флаг -b переключает на карточки, там добавляются remote tracking branch и последний коммит:

А -bp расписывает грязное состояние пофайлово, с +добавлено/−удалено:

Чего в lsw намеренно нет
В нём нет нескольких вещей. Состояния, конфигов, демона — lsw только читает, в ваши репозитории не пишет никогда. Сторонних крейтов — только Rust std и тот самый git, который у вас уже стоит; ни libgit2, ни OpenSSL, аудитить нечего, один статический бинарник на ~1 МБ. И управления worktree — создание, удаление, prune я оставил git worktree, он с этим и так справляется, дублировать смысла нет.
Весь ввод-вывод сводится к одному git worktree list на репозиторий и одному git status на worktree, дальше только разбор текста. Я писал это под omarchy и старался держать в духе: маленькие острые тулзы, которые стыкуются друг с другом. Цветной вывод включается только в TTY, в пайпе текст чистый — так что lsw | grep dirty и lsw | fzf работают как надо. NO_COLOR и NO_ICONS уважаются, а без Nerd Font иконки деградируют в ASCII (или можно --no-icons принудительно).
Под капотом
Чистый Rust: около двух тысяч строк вместе с тестами, ноль сторонних крейтов, 53 теста. Собирается на Rust 1.70+ плюс системный git.
Скан каталогов
Обход идёт по уровням (по умолчанию один, -2/-3 — глубже); скрытые каталоги, node_modules и target не трогаем. Узлы различаются по .git: каталог с .git/ внутри — репозиторий, .git-файл — linked worktree (именно так git оформляет связанные чекауты: файл с gitdir:-ссылкой). На каждый найденный репозиторий — один git worktree list, который сразу отдаёт все его worktree, включая bare.
Дальше начинается интересное. Worktree, вложенные внутрь каталога репозитория (классическая раскладка repo/{main,wt1,wt2}), попадают под его заголовок и не дублируются. Worktree-сиблинги, лежащие рядом с репозиторием, получают собственные группы.
Как я поймал баг
Разделить эти два случая с первого раза не получилось. До v0.1.3 дедуп в скане выкидывал worktree, чей «настоящий» корень уже встречался в списке. Для вложенных worktree логика «уже показан этим репозиторием» работала, но сиблинг, встретившийся после своего репозитория по алфавиту, тоже выкидывался — а рендерил его только репозиторий, в чьё поддерево сиблинг не попадал. В итоге две папки просто исчезали из выдачи. Причём результат зависел от порядка обхода: another_tasks (a < c) выживал, а supports и support_tasks (s > c) — нет.
Починилось одной строкой условия: дедуп применяем только к worktree, вложенным внутрь каталога уже показанного репозитория. Писал по TDD — сначала два падающих теста (репро реальной раскладки и проверка независимости от сортировки), потом фикс, потом все 53 зелёные. Докстринги врут даже в коде на несколько сотен строк, я это ещё раз проверил.
Состояние и деградации
Dirty-счётчик и ↑/↓ берутся из одного git status на worktree; -bp разворачивает полный список файлов. Stale-worktree, у которых .git указывает на исчезнувший репозиторий, пропускаются молча, а --debug покажет на stderr, что и почему пропущено. Когда ничего не найдено, lsw не молчит, а печатает короткую заметку: где искал, на какой глубине, сколько каталогов было нечитаемо — и выходит с кодом 0.
Установка
# инсталлятор: prebuilt-бинарник последнего релиза, x86_64-linux
curl -fsSL https://raw.githubusercontent.com/apilot/lsw/main/install.sh | sh
# или сборка из исходников (любая платформа с Rust и git)
cargo install --git https://github.com/apilot/lsw
Скрипт проверяет sha256 скачанного бинарника и кладёт его в ~/.local/bin (перезадаётся через LSW_PREFIX). Работает на любой ОС, где в PATH есть git.
Вместо заключения
Утилита маленькая и специально ограниченная: она не управляет worktree, а просто делает видимым то, что git уже знает. Мне она заменила утренний обход папок одной командой — может, и вам пригодится.
GitHub: apilot/lsw — issue и PR жду, особенно раскладки worktree-хозяйства, которые скан обрабатывает неправильно: таких layout’ов столько, сколько у людей привычек. AUR-пакет уже готов, ждал открытия регистрации (её прикрывали из-за наплыва ботов), уйдёт в репозиторий как только откроют.
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.