О разделе

Раздел закрывает базу по трём CLI-инструментам. Акцент на том, что команда делает под капотом — потому что команды, выученные наизусть без понимания, перестают помогать на первой нестандартной ситуации.

ИнструментЧто здесьЧто в других разделах
BashНавигация, поиск, процессы, права, пайпыsystemd, SSH, cron — в Nginx & Docker
Gitinit → push, три состояния, отмена, ветки, remoterebase vs merge, cherry-pick, hooks — в Testing & DevOps
ComposerЗависимости, lock, версии, автозагрузкаPSR-4 — в PHP Core
Bash: навигация и файлы
Где я и что вокруг
pwd                 # print working directory — где я сейчас
ls                  # список файлов
ls -la              # -l подробно, -a включая скрытые (начинаются с точки)
ls -lh              # -h размеры по-человечески: 4.0K вместо 4096
ls -lt              # -t сортировка по времени, свежие сверху

cd /var/www         # абсолютный путь — от корня
cd ../logs          # относительный: .. это на уровень вверх
cd ~                # домашняя папка
cd -                # вернуться туда, где был до этого
СимволЗначение
.Текущая папка
..Родительская папка
~Домашняя папка пользователя
/Корень файловой системы
- (в cd)Предыдущая папка
Создание, копирование, удаление
mkdir logs              # создать папку
mkdir -p a/b/c          # -p создать всю цепочку, не ругаться если есть
touch file.txt          # создать пустой файл / обновить дату

cp file.txt backup.txt  # копировать файл
cp -r src/ dist/        # -r рекурсивно, для папок обязателен
cp -a src/ dist/        # -a сохранить права, владельца, даты

mv old.txt new.txt      # переименовать
mv file.txt /tmp/       # переместить — та же команда

rm file.txt             # удалить файл
rm -r folder/           # удалить папку рекурсивно
rm -f file.txt          # -f не спрашивать и не ругаться на отсутствие
rm не спрашивает и не кладёт в корзину. Удалённое не восстановить. Особенно опасна связка rm -rf с переменной: если переменная пустая, rm -rf $DIR/ превращается в rm -rf /. Привычка, которая спасает: сначала ls на том же пути, и только потом rm.
mv — это и «переименовать», и «переместить». Разницы нет: файл просто получает новый путь.
Чтение файлов
cat file.txt            # вывести целиком
less file.txt           # постранично: q выход, / поиск, G в конец
head -20 file.txt       # первые 20 строк
tail -20 file.txt       # последние 20 строк
tail -f storage/logs/laravel.log   # следить за новыми строками в реальном времени
wc -l file.txt          # сколько строк
tail -f на логе — самая частая команда при отладке на сервере. Выход — Ctrl+C.
Bash: поиск и обработка текста
grep — поиск по содержимому
grep "error" app.log            # строки со словом error
grep -i "error" app.log         # -i без учёта регистра
grep -r "TODO" app/             # -r рекурсивно по папке
grep -n "error" app.log         # -n показать номера строк
grep -c "error" app.log         # -c только посчитать совпадения
grep -v "debug" app.log         # -v инверсия: всё КРОМЕ совпавшего
grep -A3 -B3 "error" app.log    # 3 строки после (After) и до (Before)
grep -E "error|warning" app.log # -E регулярка, | это ИЛИ
grep на macOS: почему примеры из интернета не работают

В macOS предустановлен BSD grep, а подавляющее большинство примеров в сети написано под GNU grep из Linux. Команды выглядят одинаково, но часть ключей у BSD отсутствует — и строка, которая у автора статьи работала, на Mac падает с ошибкой.

$ grep --version
grep (BSD grep, GNU compatible) 2.6.0-FreeBSD   # macOS
grep (GNU grep) 3.11                            # Linux

Чаще всего спотыкаются об один ключ — -P, то есть регулярные выражения в стиле Perl. Именно он стоит почти во всех однострочниках с \d, \w и \s:

$ grep -oP '\d+' app.log
grep: invalid option -- P
usage: grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz] ...
КлючНа macOSЧто делать
-P (Perl-регулярки)Нет, ошибка invalid optionПереписать на -E или поставить GNU-версию
-E (расширенные)ЕстьОсновной рабочий вариант
-i -r -n -c -v -w -oЕстьРаботают одинаково
-A -B -C (контекст)ЕстьРаботают
--include, --excludeЕстьРаботают

Если ключ -P нужен ради \d и подобных сокращений, их почти всегда можно заменить на переносимые классы POSIX — они работают и на Mac, и на Linux:

# было (GNU, на macOS упадёт)
grep -oP '\d+' app.log
grep -oP '\w+@\w+' users.txt

# стало (работает везде)
grep -oE '[[:digit:]]+' app.log
grep -oE '[[:alnum:]_]+@[[:alnum:]_]+' users.txt
Сокращение PerlКласс POSIXЧто означает
\d[[:digit:]]Цифра
\w[[:alnum:]_]Буква, цифра или подчёркивание
\s[[:space:]]Пробел, табуляция, перенос строки
\b-w у самого grepГраница слова

Второй путь — поставить настоящий GNU grep через Homebrew. Он встанет рядом, под именем ggrep, чтобы не ломать системные скрипты:

brew install grep

ggrep -oP '\d+' app.log      # GNU-версия, -P работает
grep  -oP '\d+' app.log      # системный BSD, по-прежнему ошибка

Можно сделать GNU-версию основной, добавив её каталог в начало PATH — тогда grep будет означать GNU grep:

# в ~/.zshrc
export PATH="/opt/homebrew/opt/grep/libexec/gnubin:$PATH"
Подменять системный grep стоит осознанно. Часть скриптов в системе рассчитывает на поведение BSD-версии. Безопаснее оставить ggrep отдельной командой, а в своих скриптах писать переносимо — на -E и классах POSIX. Тогда они одинаково пойдут и на Mac, и на сервере с Linux.
Та же история с другими утилитами. На macOS BSD-версии у sed, awk, date, xargs. Самое заметное: sed -i на Mac требует аргумент — sed -i '' 's/a/b/' file, иначе получите «invalid command code». GNU-версии ставятся тем же способом: brew install gnu-sed даёт gsed.
find — поиск файлов
find . -name "*.php"                  # по имени, с текущей папки
find . -iname "*.PHP"                 # -iname без учёта регистра
find . -type f -size +10M             # файлы больше 10 МБ
find . -type d -name node_modules     # -type d только папки
find . -mtime -7                      # изменённые за последние 7 дней
find . -name "*.log" -delete          # найти и удалить
find . -name "*.php" -exec grep -l "TODO" {} \;   # выполнить команду над каждым
find против grep. find ищет файлы — по имени, размеру, дате, типу. grep ищет внутри файлов — по содержимому. Их часто комбинируют: find отбирает файлы, grep смотрит, что внутри.
Обработка потока
sort file.txt           # отсортировать строки
sort -u file.txt        # -u убрать дубликаты
sort -rn file.txt       # -r обратный порядок, -n числовая сортировка
uniq -c file.txt        # -c посчитать повторы (нужен отсортированный вход)
cut -d: -f1 /etc/passwd # -d разделитель, -f номер поля
sed 's/old/new/g' f.txt # замена: s/что/на что/g — g значит все вхождения
awk '{print $1, $3}' f  # вывести 1-ю и 3-ю колонки
Bash: процессы, диск, сеть
Процессы
ps aux                  # все процессы системы
ps aux | grep php       # отфильтровать по имени
top                     # интерактивно, обновляется (q выход)
htop                    # то же, удобнее — ставится отдельно

kill 1234               # послать SIGTERM — «завершись корректно»
kill -9 1234            # SIGKILL — убить немедленно, без шансов
pkill -f "artisan queue" # убить по совпадению в командной строке
kill -9 — последнее средство. Процесс не успевает закрыть файлы, сбросить буферы и снять блокировки. Для воркера очереди это означает джоб, брошенный на середине. Сначала обычный kill, и только если не умер — -9.
Диск
df -h                   # сколько места на разделах
du -sh /var/www         # -s итог по папке, -h по-человечески
du -sh * | sort -rh     # что занимает больше всего в текущей папке
du -sh * | sort -rh | head -10   # топ-10 тяжёлых
df — сколько свободно на диске. du — сколько занимает конкретная папка. Диск забился — начинай с df -h, потом du по подозрительным путям.
Сеть
ping example.com                      # доступен ли хост
lsof -i :8000                         # кто занял порт 8000
netstat -tulpn | grep 80              # что слушает 80 порт (Linux)
lsof -i :порт — первое, что запускают при «Address already in use».
curl: флаги, которые реально используют

Без флагов curl URL делает обычный GET и вываливает тело в терминал. Всё остальное поведение задаётся ключами, и почти всегда их комбинируют: типичная строка из скрипта выглядит как curl -sS -f -L URL. Разберём по группам.

ФлагЧто делаетКогда нужен
Запрос
-X МЕТОДЗадать метод: POST, PUT, DELETE, PATCHКогда нужен не GET. С -d метод POST подставляется сам
-d 'данные'Тело запроса. Автоматически включает POSTОтправка JSON или формы
-d @file.jsonТело из файлаБольшой JSON не влезает в строку
-H 'Заголовок: значение'Добавить заголовок. Можно повторятьContent-Type, Authorization, X-API-Key
-GПереносит -d в строку запросаGET с параметрами, которые нужно экранировать
-u user:passБазовая авторизацияПростая защита, FTP
-T файлЗагрузить файл на сервер (PUT или FTP)Выкладка файла без формы
Вывод
-o файлСохранить тело в указанный файлСкачивание
-OСохранить под именем из URLБыстро скачать архив
-sТихо: без индикатора прогрессаСкрипты, конвейеры. Иначе прогресс попадёт в вывод
-SПоказывать ошибки даже в тихом режимеВсегда в паре: -sS
-iПоказать заголовки ответа вместе с теломПосмотреть, что вернул сервер
-IТолько заголовки, запрос методом HEADПроверить код ответа и редирект, не качая тело
-vПодробный лог: запрос, ответ, рукопожатие TLSРазбор «почему не работает»
-w 'формат'Напечатать служебные значения после ответаКод ответа, время, размер — для проверок
Поведение
-LИдти по редиректамБез него на 301 получите пустое тело
-fВернуть ненулевой код выхода при ошибке HTTPВ скриптах: иначе 500 считается успехом
-kНе проверять TLS-сертификатСамоподписанный сертификат на тестовом стенде
--connect-timeout NСколько ждать установки соединенияЧтобы скрипт не висел на мёртвом хосте
--max-time NОбщий предел на всю операциюСтраховка в конвейерах и cron
--retry NПовторить при сетевой ошибкеНенадёжный канал
--compressedПопросить сжатый ответ и распаковатьБольшие JSON-ответы
-A 'строка'Свой User-AgentСервер отвечает по-разному разным клиентам
-c / -b файлСохранить и отправить cookiesСценарий с логином
Откуда берутся «склеенные» флаги. Односимвольные ключи можно писать слитно: -sk это то же самое, что -s -k — тихий режим плюс отключённая проверка сертификата. Так же читаются -sS, -sSL, -fsSL. Последнее встречается в инструкциях по установке: тихо, падать при ошибке, показывать её, идти по редиректам.
-k отключает защиту, а не чинит сертификат. Он говорит curl: «не проверяй, кому принадлежит сертификат» — то есть снимает защиту от подмены. На тестовом стенде это нормально, в проде и в скриптах, которые ходят в чужие API, — нет. Правильное решение: подложить корневой сертификат через --cacert или починить цепочку на сервере.

Типовые вызовы, которые стоит помнить наизусть:

# Обычный GET
curl https://api.example.com/users

# Только код ответа — удобно в проверках
curl -s -o /dev/null -w '%{http_code}\n' https://example.com

# POST с JSON
curl -X POST https://api.example.com/orders \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer TOKEN' \
  -d '{"total":"100.00"}'

# Тело из файла — когда JSON большой
curl -X POST https://api.example.com/orders \
  -H 'Content-Type: application/json' \
  -d @order.json

# Форма, как её шлёт браузер
curl -X POST https://example.com/login \
  -d 'email=a@b.com' -d 'password=secret'

# Загрузка файла формой
curl -F 'file=@photo.jpg' -F 'title=Фото' https://example.com/upload

# Скачать с продолжением и редиректами
curl -L -C - -O https://example.com/big.zip

# Диагностика: весь обмен целиком
curl -v https://example.com 2>&1 | head -40

# Замер времени ответа
curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} total=%{time_total}\n' https://example.com
jq: читаемый JSON вместо простыни

API отвечает одной длинной строкой без переносов — читать её в терминале невозможно. jq форматирует JSON и умеет доставать из него нужное. Ставится одной командой: brew install jq на macOS, apt install jq на Debian и Ubuntu.

# Просто отформатировать — точка означает «весь документ»
curl -s https://api.example.com/users | jq

# Достать одно поле
curl -s https://api.example.com/user/1 | jq '.email'
# "ivan@example.com"  — в кавычках, это JSON-строка

# -r убирает кавычки: нужно, когда значение идёт в переменную
TOKEN=$(curl -s -X POST https://api.example.com/login \
  -d '{"email":"a@b.com","password":"secret"}' \
  -H 'Content-Type: application/json' | jq -r '.token')

# Перебрать массив и оставить только нужные поля
curl -s https://api.example.com/users | jq '.[] | {id, email}'

# Сколько элементов пришло
curl -s https://api.example.com/users | jq 'length'

# Фильтр по условию
curl -s https://api.example.com/orders | jq '.[] | select(.total > 1000)'

# Вложенное поле и значения по умолчанию, если его нет
curl -s https://api.example.com/user/1 | jq '.profile.city // "не указан"'

# Собрать плоскую строку для отчёта
curl -s https://api.example.com/users | jq -r '.[] | "\(.id)\t\(.email)"'
Связка, которую пишут чаще всего: curl -sS URL | jq. Тихий режим убирает индикатор прогресса, который иначе попал бы в конвейер и сломал разбор, -S оставляет сообщения об ошибках, а jq приводит ответ в читаемый вид.
Если jq ругается на разбор — почти всегда сервер вернул не JSON. Самое частое: HTML страницы ошибки или редирект, который вы не прошли. Посмотрите, что пришло на самом деле: curl -i URL | head -20. Второй по частоте случай — забытый -s, и в конвейер попал индикатор прогресса.

Если ставить jq некуда, минимальное форматирование есть в самом Python:

curl -s https://api.example.com/users | python3 -m json.tool
Bash: права доступа
Как читать rwx
$ ls -l
-rw-r--r--  1 observer  staff   1024 Oct 16 12:00 file.txt
drwxr-xr-x  5 observer  staff    160 Oct 16 12:00 folder/
│└┬┘└┬┘└┬┘
│ │  │  └── остальные (others)
│ │  └───── группа (group)
│ └──────── владелец (user)
└────────── тип: - файл, d папка, l симлинк
БукваЧислоДля файлаДля папки
r read4Читать содержимоеСмотреть список файлов
w write2ИзменятьСоздавать и удалять файлы внутри
x execute1ЗапускатьВходить внутрь (cd)
Откуда берутся числа вроде 755. Права складываются: rwx = 4+2+1 = 7, r-x = 4+1 = 5, rw- = 4+2 = 6. Три цифры — это владелец, группа, остальные. 755 = владельцу всё, остальным читать и заходить. 644 = владельцу читать-писать, остальным только читать.
chmod и chown
chmod 755 script.sh     # числом
chmod +x script.sh      # добавить право запуска всем
chmod -R 775 storage/   # -R рекурсивно

chown www-data file     # сменить владельца
chown -R www-data:www-data /var/www/app   # владелец:группа
ПраваДля чего
644Обычные файлы: конфиги, исходники
755Папки и исполняемые скрипты
775storage/ и bootstrap/cache/ в Laravel — пишет и владелец, и группа веб-сервера
600Секреты: .env, приватные ключи
chmod -R 777 — не решение, а дыра. Это «всем всё можно», включая запись чужими процессами. Проблема почти всегда в владельце, а не в правах: нужен chown -R www-data:www-data, а дальше хватит 775.
Bash: пайпы и перенаправление
Пайп — выход одной команды на вход другой
cat app.log | grep error | wc -l       # сколько строк с ошибками
ps aux | grep php | grep -v grep       # процессы PHP без самого grep
du -sh * | sort -rh | head -5          # топ-5 тяжёлых папок
git log --oneline | head -20           # последние 20 коммитов
Символ | соединяет команды в конвейер: то, что первая напечатала, вторая получает на вход. Так из простых команд собираются сложные запросы.
Перенаправление в файл
command > file.txt      # записать, ПЕРЕЗАПИСАВ файл
command >> file.txt     # дописать в конец
command 2> errors.txt   # только ошибки (поток 2)
command > out.txt 2>&1  # и вывод, и ошибки в один файл
command > /dev/null 2>&1 # выбросить всё
ПотокНомерЧто это
stdin0Вход
stdout1Обычный вывод
stderr2Ошибки — отдельный поток
Ошибки не идут в пайп. command | grep x фильтрует только stdout. Сообщение об ошибке пролетит мимо фильтра прямо в терминал. Чтобы поймать и его — command 2>&1 | grep x.
> затирает файл целиком, >> дописывает. Перепутать — потерять лог.
Цепочки команд
cmd1 && cmd2            # cmd2 только если cmd1 успешна (код 0)
cmd1 || cmd2            # cmd2 только если cmd1 упала
cmd1 ; cmd2             # подряд, независимо от результата
cmd &                   # запустить в фоне

# Типичный деплой — каждый шаг только при успехе предыдущего
git pull && composer install --no-dev && php artisan migrate --force
Почему &&, а не ;. С точкой запятой команды выполнятся все подряд, даже если git pull упал с конфликтом. С && цепочка остановится на первой ошибке — и ты не накатишь миграции на неполный код.
Git: от init до push
Начало работы
git init                        # создать репозиторий в текущей папке
git clone URL                   # скачать существующий
git clone URL my-folder         # в папку с другим именем
git clone --depth 1 URL         # только последний коммит, без истории

# Кто автор коммитов — без этого Git откажется коммитить
git config --global user.name "Имя"
git config --global user.email "mail@example.com"
git config --list               # посмотреть все настройки
Базовый цикл
git status                  # что изменилось — главная команда
git add file.txt            # добавить файл в индекс
git add .                   # всё из текущей папки и ниже
git add -p                  # по кускам: спросит про каждый фрагмент

git commit -m "Описание"    # зафиксировать то, что в индексе
git commit -am "Описание"   # -a добавить все ОТСЛЕЖИВАЕМЫЕ и закоммитить

git push                    # отправить на сервер
git pull                    # забрать и влить чужие изменения
git commit -am не видит новые файлы. Флаг -a берёт только то, что Git уже отслеживает. Только что созданный файл не попадёт в коммит, и это легко не заметить. Для новых файлов нужен явный git add.
История и просмотр
git log                     # полная история
git log --oneline           # по строке на коммит
git log --oneline --graph --all   # с деревом веток
git log -5                  # последние 5
git log -p file.txt         # история конкретного файла с диффами
git log --author="Имя"      # коммиты одного автора

git diff                    # что изменено, но НЕ в индексе
git diff --staged           # что в индексе и уйдёт в коммит
git diff main..feature      # разница между ветками

git show HEAD               # последний коммит целиком
git blame file.txt          # кто и когда менял каждую строку
.gitignore
# Типичный для Laravel
/vendor
/node_modules
/public/storage
/storage/*.key
.env
.env.backup
.phpunit.result.cache
.DS_Store
.gitignore не действует на уже отслеживаемые файлы. Закоммитил .env, потом добавил в ignore — он продолжит отслеживаться. Нужно убрать из индекса, оставив на диске:

git rm --cached .env

И помнить: из истории он при этом никуда не денется — для этого нужна чистка истории.
Git: три состояния файла
Рабочая папка → индекс → репозиторий

Это ключевая модель Git. Без неё команды отмены выглядят набором случайных заклинаний.

┌──────────────┐ git add ┌──────────────┐ git commit ┌──────────────┐ │ Working │ ───────────→ │ Staging │ ───────────→ │ Repository │ │ directory │ │ (index) │ │ (.git) │ │ │ ←─────────── │ │ ←─────────── │ │ └──────────────┘ git restore └──────────────┘ git reset └──────────────┘ файлы на диске «что войдёт в зафиксированная как ты их видишь следующий коммит» история git checkout / git restore --source ←──────────────────────────────────────────────────────────
СостояниеГде лежитЧто значит
ModifiedРабочая папкаИзменён, но не помечен к коммиту
StagedИндексПомечен — войдёт в следующий коммит
CommittedРепозиторийСохранён в истории
UntrackedРабочая папкаGit о нём вообще не знает
Зачем вообще индекс. Он позволяет собрать коммит выборочно: наменял в пяти файлах, но в коммит кладёшь только два относящихся к одной задаче. Отсюда же git add -p — добавить не файл целиком, а отдельные куски.
Git: отмена изменений
Что отменять и чем
ЗадачаКоманда
Откатить правки в файлеgit restore file.txt
Убрать файл из индекса, правки оставитьgit restore --staged file.txt
Поправить последний коммитgit commit --amend
Откатить коммит, сохранив изменения в индексеgit reset --soft HEAD~1
Откатить коммит, изменения в рабочей папкеgit reset HEAD~1
Откатить коммит и выбросить измененияgit reset --hard HEAD~1
Отменить коммит новым коммитомgit revert HASH
Удалить неотслеживаемые файлыgit clean -fd
reset против revert. reset переписывает историю — коммита больше нет. revert создаёт новый коммит, который отменяет старый, — история сохраняется. Правило: в своей локальной ветке можно reset; в том, что уже запушено и видно другим, — только revert.
git reset --hard удаляет изменения безвозвратно. Незакоммиченная работа пропадёт без возможности вернуть — она нигде не сохранена. Если не уверен, сначала git stash: он спрячет изменения, и их можно будет достать обратно.
Спасательный круг — git reflog. Он помнит, где HEAD был раньше, даже после reset --hard по коммитам. Нашёл нужный хеш — git reset --hard HASH вернёт состояние. Работает только для закоммиченного.
Git: ветки и слияние
Работа с ветками
git branch                  # список локальных веток
git branch -a               # включая удалённые
git branch feature-x        # создать, но не переключаться

git switch feature-x        # переключиться (современный способ)
git switch -c feature-x     # создать и сразу перейти
git checkout -b feature-x   # то же самое, старый синтаксис

git branch -d feature-x     # удалить влитую ветку
git branch -D feature-x     # удалить принудительно, даже не влитую
switch и restore появились, чтобы разделить обязанности перегруженного checkout: он умел и переключать ветки, и восстанавливать файлы. Старый синтаксис работает, но новый понятнее.
Слияние и конфликты
git switch main
git merge feature-x         # влить feature-x в текущую ветку

# При конфликте Git остановится и разметит файл:
# <<<<<<< HEAD        — что в твоей ветке
# =======              — граница
# >>>>>>> feature-x   — что во вливаемой

git status                  # покажет конфликтные файлы
# правишь руками, убираешь маркеры, затем:
git add file.txt
git commit                  # завершить слияние

git merge --abort           # передумал — откатить слияние целиком
Забытые маркеры конфликта. Если <<<<<<< или >>>>>>> останутся в коде, он закоммитится и сломается при запуске. Перед коммитом слияния: grep -rn "<<<<<<<" .
Rebase, squash и cherry-pick разобраны в Testing & DevOps → Git Advanced.
Git: remote и теги
Удалённые репозитории
git remote -v                       # какие remote настроены
git remote add origin URL           # добавить
git remote set-url origin NEW_URL   # сменить адрес

git fetch                   # забрать изменения, НЕ применяя
git pull                    # fetch + merge одной командой
git pull --rebase           # fetch + rebase — история линейнее

git push                            # отправить текущую ветку
git push -u origin feature-x        # -u связать с удалённой при первом пуше
git push --force-with-lease         # безопасный force
fetch против pull. fetch только скачивает изменения — ты можешь посмотреть их через git log origin/main и решить, что делать. pull сразу вливает в текущую ветку. Когда не уверен в состоянии ветки, безопаснее сначала fetch.
--force против --force-with-lease. Обычный --force затирает удалённую ветку безусловно — вместе с коммитами коллеги, которые тот запушил минуту назад. --force-with-lease сначала проверяет, что на сервере то, что ты видел последним, и откажется, если кто-то успел запушить. Привычка должна быть на вторую форму.
Теги
git tag                         # список тегов
git tag v1.0.0                  # лёгкий тег на текущий коммит
git tag -a v1.0.0 -m "Релиз"    # аннотированный: с автором и датой
git tag -a v1.0.0 HASH          # тег на конкретный коммит

git push origin v1.0.0          # теги НЕ уходят с обычным push
git push --tags                 # отправить все теги
git tag -d v1.0.0               # удалить локально
Теги не отправляются при обычном git push — их нужно пушить явно. Для релизов используют аннотированные (-a): в них есть автор, дата и сообщение.
Composer: база и зависимости
Что такое Composer

Менеджер зависимостей для PHP. Решает две задачи: ставит библиотеки нужных версий со всеми их зависимостями и настраивает автозагрузку, чтобы классы подключались без ручных require.

ФайлЧто этоВ Git?
composer.jsonЧто тебе нужно — список зависимостей с допустимыми версиямиДа
composer.lockЧто реально установлено — точные версии до коммитаДа
vendor/Сами библиотекиНет
Основные команды
composer init                       # создать composer.json интерактивно
composer require guzzlehttp/guzzle  # поставить пакет и записать в json
composer require --dev phpunit/phpunit   # только для разработки
composer require guzzlehttp/guzzle:^7.5  # конкретное ограничение версии

composer remove guzzlehttp/guzzle   # удалить пакет
composer show                       # что установлено
composer show guzzlehttp/guzzle     # подробности о пакете
composer outdated                   # у чего есть обновления
composer why vendor/package         # КТО тянет этот пакет
composer validate                   # проверить composer.json
composer audit                      # известные уязвимости в зависимостях
composer why отвечает на вопрос «откуда в проекте взялся этот пакет, я его не ставил» — покажет цепочку зависимостей, которая его притянула.
Composer: install против update
Главное различие
composer installcomposer update
Читаетcomposer.lockcomposer.json
СтавитТочно те версии, что в lockСамые свежие в рамках ограничений
Меняет lockНетДа, перезаписывает
Где запускатьНа проде, в CI, у новых разработчиковЛокально, осознанно
ВоспроизводимостьПолная — у всех одинаковоЗависит от момента запуска
Зачем нужен composer.lock. В composer.json написано "^7.5" — это «любая 7.x от 7.5». Поставишь сегодня — приедет 7.8, через месяц — 7.9 с поправленным поведением. Lock фиксирует точный коммит каждой библиотеки, и install воспроизводит ровно его. Поэтому lock-файл обязательно в Git: именно он гарантирует, что на проде и у коллеги соберётся то же самое.
composer update на проде — частая и дорогая ошибка. Она подтянет свежие версии, которых никто не тестировал, и перепишет lock прямо на сервере. На проде только:

composer install --no-dev --optimize-autoloader
Флаги для прода
composer install --no-dev --optimize-autoloader --no-interaction

# --no-dev               не ставить require-dev (phpunit, faker, debugbar)
# --optimize-autoloader  собрать статическую карту классов вместо поиска по файлам
# --no-interaction       ничего не спрашивать — обязательно для CI
# --prefer-dist          ставить архивы вместо клонирования репозиториев

composer update vendor/package      # обновить ОДИН пакет, не всё подряд
composer update --lock              # пересобрать lock без переустановки
--optimize-autoloader на проде даёт заметный прирост: вместо поиска файла по PSR-4 при каждом обращении к классу Composer собирает готовую карту «класс → путь».
Composer: версии и ограничения
Semver и операторы

Версия пишется как MAJOR.MINOR.PATCH, например 7.5.2. Правило семантического версионирования: MAJOR меняется при ломающих изменениях, MINOR — при новых совместимых возможностях, PATCH — при исправлениях.

ЗаписьЧто разрешаетДиапазон
^7.5Не менять MAJOR — самый частый вариант>= 7.5.0, < 8.0.0
~7.5Не менять MINOR>= 7.5.0, < 8.0.0
~7.5.2Только PATCH>= 7.5.2, < 7.6.0
7.5.*Любой PATCH внутри 7.5>= 7.5.0, < 7.6.0
7.5.2Ровно эта версия= 7.5.2
>=7.5 <8.0Явный диапазонкак написано
*Любая — так не делаютвсё подряд
^ против ~ — в чём подвох. Для записи из двух чисел они совпадают: ^7.5 и ~7.5 оба означают «до 8.0». Разница появляется с тремя: ^7.5.2 разрешит 7.9.0, а ~7.5.2 остановится на 7.5.x. Проще держать в голове так: ^ гибче, ~ консервативнее.
Фиксация точной версии мешает безопасности. "guzzlehttp/guzzle": "7.5.2" означает, что патчи с исправлением уязвимостей не приедут никогда. Для фиксации есть composer.lock — в composer.json держат диапазон, а воспроизводимость обеспечивает lock.
Composer: автозагрузка и скрипты
Секция autoload
{
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/"
        },
        "files": ["app/helpers.php"],
        "classmap": ["database/seeders"]
    },
    "autoload-dev": {
        "psr-4": { "Tests\\": "tests/" }
    }
}
СпособКак работает
psr-4Namespace ↔ папка. App\Services\Foo → app/Services/Foo.php. Основной способ
filesПодключается всегда, при каждом запросе. Для функций-хелперов, у которых нет класса
classmapСканирует папку и строит карту классов. Для кода без namespace
composer dump-autoload              # пересобрать карту автозагрузки
composer dump-autoload -o           # -o оптимизированная (= --optimize)
Добавил files или новый namespace — нужен dump-autoload. Правка composer.json сама по себе ничего не меняет: карта автозагрузки лежит в vendor/composer/ и пересобирается только этой командой. Симптом — «Class not found» на файле, который очевидно существует.
Скрипты
{
    "scripts": {
        "test": "phpunit",
        "lint": "pint --test",
        "post-autoload-dump": [
            "@php artisan package:discover --ansi"
        ],
        "post-install-cmd": ["@php artisan optimize"]
    }
}
composer test               # запустить свой скрипт
composer run-script test    # то же явно
composer list               # все доступные команды
ХукКогда срабатывает
post-install-cmdПосле composer install
post-update-cmdПосле composer update
post-autoload-dumpПосле пересборки автозагрузки — здесь Laravel ищет пакеты
Скрипты удобны как единая точка входа: composer test работает одинаково на любой машине, независимо от того, где лежит бинарник phpunit.
Шпаргалка
Bash — что нужно каждый день
ЗадачаКоманда
Следить за логомtail -f storage/logs/laravel.log
Найти по содержимомуgrep -rn "текст" app/
Найти файлfind . -name "*.php"
Что занимает местоdu -sh * | sort -rh | head
Кто занял портlsof -i :8000
Убить процессpkill -f "artisan queue"
Права для Laravelchown -R www-data:www-data storage bootstrap/cache
Git — что нужно каждый день
ЗадачаКоманда
Что изменилосьgit status
История короткоgit log --oneline --graph
Новая веткаgit switch -c feature-x
Откатить правки в файлеgit restore file.php
Убрать из индексаgit restore --staged file.php
Поправить последний коммитgit commit --amend
Отменить запушенный коммитgit revert HASH
Спасти после reset --hardgit reflog
Composer — что нужно каждый день
ЗадачаКоманда
Поставить пакетcomposer require vendor/package
Поставить по lockcomposer install
Деплой на продcomposer install --no-dev --optimize-autoloader
Обновить один пакетcomposer update vendor/package
Откуда взялся пакетcomposer why vendor/package
Что устарелоcomposer outdated
Уязвимостиcomposer audit
Class not foundcomposer dump-autoload
Подводные камни
Bash
rm -rf с пустой переменной. rm -rf $DIR/ при незаданной $DIR превращается в rm -rf /. Всегда кавычки и проверка: rm -rf "${DIR:?переменная не задана}".
> вместо >>. Одна лишняя стрелка — и лог перезаписан вместо дописывания.
Ошибки не попадают в пайп. cmd | grep x фильтрует только stdout. Нужен cmd 2>&1 | grep x.
chmod 777 как решение любой проблемы с правами. Почти всегда настоящая причина — неверный владелец. Сначала chown, потом 775.
Git
git commit -am не берёт новые файлы. Только отслеживаемые. Новый файл тихо не попадёт в коммит.
.gitignore не влияет на уже отслеживаемое. Нужен git rm --cached. А из истории файл при этом не исчезнет.
push --force затирает чужую работу. Всегда --force-with-lease.
reset --hard съедает незакоммиченное без следа. reflog спасёт только то, что было в коммитах.
Маркеры конфликта в коммите. Перед завершением слияния: grep -rn "<<<<<<<" .
Composer
composer update на проде. Подтянет непротестированные версии и перепишет lock на сервере. Только install.
composer.lock не в Git. Тогда у всех разные версии, и «у меня работает» становится нормой.
vendor/ в Git. Десятки тысяч чужих файлов в истории, конфликты при каждом merge. Место vendor/ — в .gitignore.
Забытый dump-autoload. Добавил namespace или хелпер — получи «Class not found» на существующем файле.
--no-dev на машине разработчика. Снесёт phpunit и остальные dev-зависимости. Этот флаг только для прода и CI.