Если вы работаете в стиле «одна задача — свой 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 × N плюс git status в каждой папке. С двумя каталогами терпимо, с пятью — уже ритуал. git worktree list тут помогает лишь частично: он работает только внутри одного репозитория и не показывает ни dirty‑состояние, ни ahead/behind.
Лёгкой утилиты, которая отвечала бы на всё это одной командой по всей папке проектов, мне не попалось — так и появился lsw.
Что показывает lsw
Запускаю lsw в папке с проектами:

Логика простая: внутри репозитория lsw показывает его worktree, внутри обычной папки — сканирует репозитории на уровень вглубь (-2 и -3 — глубже). Для каждого worktree — ветка, грязность и дистанция до upstream.
Как читать вывод:
Токен |
Значение |
|---|---|
|
заголовок группы: путь и origin в компактном виде (если origin есть) |
|
каталог linked worktree — так вложенный чекаут отличается в своей группе |
|
worktree, в котором вы сейчас находитесь |
|
незакоммиченные изменения (жёлтым) |
|
изменений нет (зелёным) |
|
ветка впереди upstream на N коммитов (циан) |
|
позади upstream на N (красный) |
|
у ветки нет remote tracking |
|
remote‑ветка удалена |
|
bare‑репозиторий |
Флаг -b переключает на карточки: ветка, статус и remote tracking branch в каждом блоке:

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

Есть в worktree‑хозяйстве и тихая беда: симлинки на внешнее. Внешняя библиотека, примонтированные данные, локальные секреты — всё это живёт линками, ведущими за пределы репозитория, и когда цель исчезает (mount не поднят, путь переехал), git status молчит: для него symlink — просто маленький файл. С v0.1.5 флаг -s проверяет верхний уровень корня каждого worktree и красит битые красным токеном N broken links:

А -bps показывает, куда именно каждый битый линк указывает — и сразу видно, что чинить:

Чего в lsw сознательно нет
Четыре принципа, вокруг которых всё построено:
Read‑only. Никакого состояния, конфигов и демона.
lswникогда не пишет в ваши репозитории — только читает.Ноль зависимостей. Rust std и тот самый
git, который у вас уже стоит. Ни libgit2, ни OpenSSL — нечего аудитить. Один статический бинарник ~1 МБ.Быстро. Весь ввод‑вывод — один
git worktree listна репозиторий и одинgit statusна worktree. Дальше только разбор текста.Одна функция.
lswтолько показывает. Создание, удаление, prune — за бортом осознанно:git worktreeсам отлично умеет управлять 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 разворачивает полный список файлов. Проверка симлинков (-s) — это чистый std::fs: один проход read_dir по верхнему уровню корня, symlink_metadata плюс контрольный metadata для самих линков, ни одного лишнего вызова git — дефолтный запрос остаётся таким же быстрым. 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‑пакет уже готов — ждал открытия регистрации AUR (её закрывали на наплыв ботов) и уйдёт в репозиторий, как только откроют.
dsignmark
у нас worktree по задаче на каждого исполнителя, их бывает штук по двадцать на один репо, так что боль знакома) от себя бы добавил одну вещь которой очень нехватало
node_modules мы не ставим в каждое дерево, а симлинкаем из одного, иначе диск кончается за неделю. и вот однажды снесли "старое чистое" дерево, а на него смотрели симлинки еще из трех. в тех трех vitest потом честно отрапортовал зелёным, потому что не нашел ни одного теста. заметили не сразу
так что для удаления мне бы в lsw хотелось кроме dirty и ahead/behind видеть еще кто ссылается на это дерево снаружи. ну и размер, чтобы понимать что сносить первым
а ветки которые уже влиты в main он как то помечает? у нас основной мусор как раз такие
aboyarinov Автор
благодарю за отзыв. напишу в ЛС по подробностям
aboyarinov Автор
добавил функционал, интересный кейс. благодарю за идею. буду рад отзыву по его работе