DimaSOS

Git

Повседневные команды и то, что нужно, когда что-то уже сломалось.

#Повседневное

git status -sb
git diff                     # незастейдженное
git diff --staged            # то, что уйдёт в коммит
git diff main...HEAD         # что моя ветка добавила относительно main

Три точки в main...HEAD — сравнение с точкой ветвления, а не с текущим состоянием main. Именно это показывает PR.

git add -p                   # по кускам, интерактивно
git add -u                   # только отслеживаемые
git restore file.kt          # откатить изменения в файле
git restore --staged file.kt # убрать из индекса, изменения оставить

git add -p — способ разложить кашу изменений на осмысленные коммиты. restore заменил невнятный checkout --.

git commit -m "fix: не падать при пустом ответе"
git commit --amend                     # поправить последний
git commit --amend --no-edit           # добавить забытое, сообщение не менять

--amend переписывает коммит: после push потребуется --force-with-lease.

git log --oneline -20
git log --oneline --graph --all -20
git log -p file.kt
git log -S "functionName"              # когда появилась/исчезла строка
git log --author="Дмитрий" --since="2 weeks ago"

-S (pickaxe) ищет коммиты, изменившие число вхождений строки — лучший способ найти, когда что-то было добавлено.

git switch feature/login          # переключиться
git switch -c feature/new         # создать и переключиться
git switch -                      # на предыдущую ветку
git branch -vv                    # с указанием upstream

switch для ветвления, restore для файлов — вместо перегруженного checkout.

git stash push -m "недоделанный фильтр"
git stash list
git stash pop                     # применить и удалить
git stash apply stash@{1}         # применить, оставить в списке
git stash push -- file.kt         # только один файл

pop удаляет запись, apply оставляет. При конфликте pop запись не удалит — не потеряется.

#Rebase и история

git fetch origin
git rebase origin/main
git rebase --continue
git rebase --abort

--abort возвращает всё как было — запомнить в первую очередь.

git rebase -i HEAD~5

# в редакторе:
#   pick    — оставить
#   reword  — изменить сообщение
#   squash  — слить с предыдущим, объединив сообщения
#   fixup   — слить с предыдущим, сообщение выбросить
#   drop    — удалить коммит
#   edit    — остановиться и дать поправить

Интерактивный rebase. Порядок строк = порядок коммитов, строки можно менять местами.

git commit --fixup=abc1234
git rebase -i --autosquash HEAD~10

Автоматический squash: коммит помечается как правка конкретного, а --autosquash сам расставит fixup в нужные места. Удобнее ручной перестановки строк.

git pull --rebase
git config --global pull.rebase true

Без этого git pull плодит merge-коммиты «Merge branch main into main».

git push --force-with-lease

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

Правило: --force не использовать никогда, --force-with-lease — только в своей ветке.

git cherry-pick abc1234
git cherry-pick abc1234..def5678
git cherry-pick -x abc1234        # добавить ссылку на источник

Перенос отдельных коммитов, например хотфикса в релизную ветку.

git revert abc1234
git revert -m 1 abc1234           # для merge-коммита

revert создаёт обратный коммит и не переписывает историю — единственный безопасный способ откатить то, что уже в общей ветке.

#Конфликты

git status                        # список конфликтных файлов
git diff --name-only --diff-filter=U

U = unmerged. Точный список того, что нужно разрулить.

<<<<<<< HEAD
твоя версия (текущая ветка)
=======
их версия (то, что вливается)
>>>>>>> origin/main

Что означают маркеры. При rebase «твоё» и «их» меняются местами относительно интуиции: HEAD — это то, на что перебазируешься.

Отсюда правило: при конфликте в rebase не полагаться на память, а смотреть git log --oneline HEAD -1.

git checkout --ours  file.kt      # оставить свою версию
git checkout --theirs file.kt      # оставить их версию
git add file.kt                    # пометить решённым

Для файлов, где мержить нечего: сгенерированные, lock-файлы.

git config --global merge.conflictstyle zdiff3

Показывает ещё и общего предка — становится видно, что именно каждая сторона изменила. Резко упрощает разбор нетривиальных конфликтов.

git mergetool
git merge --abort
git rebase --abort

Если запутался — откатиться и начать заново дешевле, чем героически доразруливать.

git config --global rerere.enabled true

Git запомнит, как ты решил конфликт, и применит то же решение при повторе. Спасает при многократном rebase долгой ветки.

#Когда всё сломалось

Главное, что стоит знать: почти всё восстанавливается. Git не удаляет коммиты сразу, а reflog помнит, где HEAD был раньше.

git reflog | head -20
git reset --hard HEAD@{3}          # вернуться к состоянию 3 шага назад

Первое действие после «я потерял коммиты». reflog хранит записи по умолчанию 90 дней.

git reset --soft HEAD~1     # коммит отменить, изменения в индексе
git reset --mixed HEAD~1    # коммит отменить, изменения в рабочей копии
git reset --hard HEAD~1     # отменить всё, изменения ПОТЕРЯНЫ

Три режима по возрастанию разрушительности. --soft — если просто рано закоммитил.

git restore --source=HEAD~3 -- file.kt
git show abc1234:path/to/file.kt > file.kt

Достать конкретный файл из прошлого, не трогая остальное.

git fsck --lost-found
git show <hash>

Поиск «висячих» коммитов, на которые не осталось ссылок. Последняя надежда, если reflog не помог.

git checkout -b rescue abc1234

Нашёл нужный коммит в reflog — сразу сделать на нём ветку, чтобы больше не терять.

# случайно закоммитил секрет — недостаточно удалить файл следующим коммитом
git log --all --oneline -S "AKIA" | head
# история переписывается: git filter-repo (или BFG),
# затем ротация самого секрета — он уже утёк

Секрет в истории остаётся доступен по хешу коммита. Переписывание истории обязательно, но первично — отозвать и перевыпустить сам ключ.

#Разбор и поиск

git blame file.kt
git blame -L 40,60 file.kt
git blame -w -C file.kt           # игнорировать пробелы и перемещения

-w -C убирает шум от форматирования и переносов кода между файлами.

git bisect start
git bisect bad                    # текущий сломан
git bisect good v1.4.0            # здесь работало
# git проверяет середину, ты говоришь good/bad
git bisect reset

Двоичный поиск коммита, который сломал поведение. На истории из 1000 коммитов это 10 проверок.

git bisect start HEAD v1.4.0
git bisect run ./gradlew :app:testDebugUnitTest --tests '*LoginTest*'

Автоматический bisect: скрипт с кодом 0 = good, иначе bad. Полностью автоматизированный поиск регрессии.

git show abc1234
git show abc1234 --stat
git show abc1234:file.kt

Коммит целиком, только список файлов, или файл на момент коммита.

git shortlog -sn --since="3 months ago"
git log --diff-filter=D --name-only | head -20     # удалённые файлы
git log --follow file.kt                            # с учётом переименований

--follow нужен, чтобы история не обрывалась на переименовании.

#Настройки, которые стоит поставить сразу

git config --global user.name "Имя Фамилия"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global core.editor "nvim"

База.

git config --global pull.rebase true              # без merge-мусора
git config --global rebase.autosquash true        # fixup сам встанет
git config --global rerere.enabled true           # помнить решения конфликтов
git config --global merge.conflictstyle zdiff3    # видеть общего предка
git config --global diff.algorithm histogram      # осмысленнее diff
git config --global fetch.prune true              # чистить удалённые ветки

Набор, который заметно снижает бытовую боль. fetch.prune убирает мёртвые origin/*, которые копятся месяцами.

git config --global alias.st "status -sb"
git config --global alias.lg "log --oneline --graph --decorate -20"
git config --global alias.last "log -1 HEAD --stat"
git config --global alias.unstage "restore --staged"

Алиасы.

# .gitignore для Android
*.iml
.gradle/
local.properties
.idea/
build/
captures/
.externalNativeBuild/
.cxx/
*.apk
*.aab
*.keystore
*.jks

local.properties содержит путь к SDK и у каждого свой — коммитить нельзя. Keystore в репозитории — прямая утечка подписи.

git rm --cached local.properties
echo "local.properties" >> .gitignore
git commit -m "chore: убрать local.properties из индекса"

Если файл уже закоммичен, добавление в .gitignore ничего не изменит — нужен rm --cached.

ЗадачаКоманда
Отменить последний коммит, изменения оставитьgit reset --soft HEAD~1
Поправить сообщение последнего коммитаgit commit --amend
Откатить коммит, который уже в maingit revert
Найти потерянные коммитыgit reflog
Найти, какой коммит сломал тестgit bisect run
Найти, когда появилась строкаgit log -S "строка"
Перенести коммит в другую веткуgit cherry-pick
Запушить переписанную историюgit push --force-with-lease
Выкинуть файл из индекса, оставив на дискеgit rm --cached