diff --git a/antora.yml b/antora.yml index ede6c87..c741c17 100644 --- a/antora.yml +++ b/antora.yml @@ -1,5 +1,5 @@ name: book -version: latest +version: git-dev title: DevTools Book nav: - modules/ROOT/nav.adoc diff --git a/modules/ROOT/images/crya.png b/modules/ROOT/images/crya.png index 0c0087c..ca3ab39 100644 Binary files a/modules/ROOT/images/crya.png and b/modules/ROOT/images/crya.png differ diff --git a/modules/ROOT/nav.adoc b/modules/ROOT/nav.adoc index 1dbc797..94f69da 100644 --- a/modules/ROOT/nav.adoc +++ b/modules/ROOT/nav.adoc @@ -3,6 +3,9 @@ * xref:code-of-conduct.adoc[Code of conduct] * xref:dev-guides.adoc[Гайды по разработке] +.Git +* xref:git.adoc[] + .Linux FS * xref:linux/file-types.adoc[Типы файлов] * xref:linux/fhs.adoc[FHS] diff --git a/modules/ROOT/pages/authors.adoc b/modules/ROOT/pages/authors.adoc index 353905e..ee6bd9f 100644 --- a/modules/ROOT/pages/authors.adoc +++ b/modules/ROOT/pages/authors.adoc @@ -8,4 +8,10 @@ GitHub: https://github.com/1BAH[1BAH] + GitLab: https://gitlab.com/lBAH[lBAH] +== Виктория Попова +Почта: v.popova@phystech.edu + +GitHub: https://github.com/cerisesav[cerisesav] + +GitLab: https://gitlab.com/cerisesav[karmaxxkoma] + +Telegram: https://t.me/karmaxxkoma + _Тут типо инфа.._ diff --git a/modules/ROOT/pages/git.adoc b/modules/ROOT/pages/git.adoc new file mode 100644 index 0000000..db8521f --- /dev/null +++ b/modules/ROOT/pages/git.adoc @@ -0,0 +1,1427 @@ += Git +:page-authors: Виктория Попова +:description: Работа с git + +Что вообще такое git? Если верить поисковику: + +[quote] +Git - это бесплатная распределённая система контроля версий, которая позволяет отслеживать изменения в файлах проекта, сохранять разные версии кода и удобно работать в команде. Программу создал Линус Торвальдс в 2005 году. + +Мальчик Ваня решил поспорить: «А зачем мне git? Я могу на компьютере хранить свои файлики и делать откат изменений через Ctrl+Z (Cmd + Z). Мне не нужен git!». А потом у него сгорел компьютер вместе с его проектом :) + +Git активно используется в самых разных компаниях, так как важно поддерживать структуру проекта и работать вместе, не мешая друг другу при этом. + +Прежде чем говорить о командах git, нужно познакомиться с базовыми терминами и определениями. + +== Основные объекты + +Репозиторий (repository, repo):: +Хранилище проекта: сам код плюс вся история его изменений. Бывает локальным (у вас на компьютере) и удалённым (например, на GitHub). Создать локальный репозиторий в текущей папке: ++ +[,console] +---- +$ git init +Initialized empty Git repository in /home/user/project/.git/ +---- + +Рабочая директория (working directory):: +Папка с файлами проекта, с которыми вы прямо сейчас работаете. Всё, что лежит рядом с папкой `.git`, - это ваша рабочая директория. + +Индекс / область подготовки (staging area, index):: +«Промежуточный склад», куда попадают изменения, которые вы собираетесь зафиксировать в следующем коммите. Добавить файл в индекс: ++ +[,console] +---- +$ git add main.py +$ git status +On branch main +Changes to be committed: + (use "git restore --staged ..." to unstage) + new file: main.py +---- + +Коммит (commit):: +Зафиксированный снимок состояния проекта с уникальным идентификатором (хэшем), сообщением и указанием автора. Основная единица истории в Git. ++ +[,console] +---- +$ git commit -m "Add hello world" +[main (root-commit) 3f2a1b0] Add hello world + 1 file changed, 1 insertion(+) + create mode 100644 main.py +---- + +Хэш коммита (SHA):: +40-значный идентификатор коммита; обычно достаточно первых 7 символов. ++ +[,console] +---- +$ git log --oneline +3f2a1b0 (HEAD -> main) Add hello world +---- + +HEAD:: +Указатель на текущий коммит (обычно на последний коммит в вашей активной ветке). ++ +[,console] +---- +$ cat .git/HEAD +ref: refs/heads/main +---- + +Ветка (branch):: +Независимая линия разработки. По сути - подвижный указатель на коммит. Главная ветка обычно называется `main` (раньше - `master`, можете почитать почему убрали такое интересное название). ++ +[,console] +---- +$ git branch feature-login <1> +$ git branch <2> + feature-login +* main +---- +<1> Создать новую ветку. +<2> Показать список веток; звёздочкой отмечена текущая. + +Тег (tag):: +Именованная метка на конкретном коммите, чаще всего для версий (`v1.0.0`). ++ +[,console] +---- +$ git tag v1.0.0 +$ git tag +v1.0.0 +---- + +== Работа с удалённым репозиторием + +Remote:: +Ссылка на удалённый репозиторий. Стандартное имя - `origin`. ++ +[,console] +---- +$ git remote -v +origin git@github.com:username/project.git (fetch) +origin git@github.com:username/project.git (push) +---- + +Clone:: +Скачать удалённый репозиторий к себе на компьютер вместе со всей историей. ++ +[,console] +---- +$ git clone git@github.com:username/project.git +Cloning into 'project'... +---- + +Fork:: +Своя копия чужого репозитория на сервере (термин GitHub/GitLab, не самого Git). Делается кнопкой *Fork* на странице репозитория - у команды `git` аналога нет. + +Push:: +Отправить свои локальные коммиты в удалённый репозиторий. ++ +[,console] +---- +$ git push origin main +---- + +Pull:: +Забрать изменения с удалённого репозитория и сразу применить их к своей ветке (по сути `fetch` + `merge`). ++ +[,console] +---- +$ git pull origin main +---- + +Fetch:: +Забрать изменения с удалённого репозитория, но не применять их к рабочей ветке. ++ +[,console] +---- +$ git fetch origin +---- + +Pull request (PR) / Merge request (MR):: +Запрос на слияние ваших изменений в чужую или основную ветку. Мы так, например, ревьювим кода на GitHub/GitLab. На работе тоже будете создавать реквесты. Создаётся через веб-интерфейс сервиса. + +== Изменения и история + +Merge (слияние):: +Объединение двух веток в одну. ++ +[,console] +---- +$ git checkout main +$ git merge feature-login +---- + +Merge conflict (конфликт слияния):: +Ситуация, когда Git не может автоматически объединить изменения (как пример, правили одну и ту же строку в разных ветках) и просит человека выбрать нужный вариант. Конфликтные места в файлах помечаются так: ++ +[,console] +---- +<<<<<<< HEAD +print("Hello, world!") +======= +print("Привет, мир!") +>>>>>>> feature-login +---- +TODO: добавить все виды конфликтов + +Rebase:: +«Перенос» коммитов одной ветки поверх другой; переписывает историю, делая её линейной. + +// TODO: + +[,console] +---- +$ git checkout feature-login +$ git rebase main +---- + +// TODO: `switch` и `restore`, `checkout` - объяснить шо не надо лепить + +[,console] +---- +$ git checkout main +$ git switch feature-login +---- + +Reset:: +Откат ветки к другому коммиту (может удалить последующие коммиты из истории). ++ +[,console] +---- +$ git reset --hard HEAD~1 +---- ++ +WARNING: `--hard` удаляет и коммит, и все связанные с ним изменения в файлах. + +Подробнее о флагах и видах можно будет почитать дальше. +// TODO: как добавить блять клик на раздел +Revert:: +Создание нового коммита, отменяющего изменения указанного (безопасный откат без переписывания истории). ++ +[,console] +---- +$ git revert HEAD +---- + +Log:: +Просмотр истории коммитов. ++ +[,console] +---- +$ git log --oneline --graph +* 3f2a1b0 (HEAD -> main) Add hello world +---- + +Diff:: +Просмотр различий между версиями файлов. ++ +[,console] +---- +$ git diff <1> +$ git diff --staged <2> +$ git diff main..feature-login <3> +---- +<1> Изменения в рабочей директории, ещё не добавленные в индекс. +<2> Изменения, уже добавленные в индекс. +<3> Разница между двумя ветками. + +== Сервисные понятия + +`.gitignore`:: +Файл со списком путей, которые Git должен игнорировать (например, `node_modules/`, бинарники, локальные конфиги). ++ +.Пример `.gitignore` +[,bash] +---- +# зависимости +node_modules/ +venv/ + +# сборки и бинарники +*.o +*.exe +build/ + +# среды разработки +.idea/ +.vscode/ + +# системные файлы +.DS_Store +---- + +CAUTION: git и GitHub - разные понятия! Git - это система контроля версий, а GitHub/GitLab/Bitbucket - веб-сервисы для хостинга Git-репозиториев с дополнительными возможностями. + +Теперь стоит поговорить про каждое в отдельности, дабы понять как этим пользоваться и какие есть примеры. + +== Первичная настройка Git + +Сразу после установки Git нужно один раз представиться - эти данные будут вписываться в каждый ваш коммит: + +[,console] +---- +$ git config --global user.name "Ivan Ivanov" +$ git config --global user.email "ivan@example.com" +---- + +Флаг `--global` означает «для всех репозиториев этого пользователя». Если для какого-то конкретного проекта нужно другое имя или почта (например, рабочая), запустите те же команды без `--global` внутри папки проекта - тогда настройки сохранятся только для него. + +Полезно также задать имя ветки по умолчанию и включить цветной вывод: + +[,console] +---- +$ git config --global init.defaultBranch main +$ git config --global color.ui auto +---- + +Проверить все текущие настройки: + +[,console] +---- +$ git config --list +---- + +== SSH: что это и зачем оно нужно + +*SSH (Secure Shell)* - это протокол для безопасного соединения с удалённой машиной. GitHub и GitLab используют его, чтобы убедиться: тот, кто пытается сделать `push`, действительно имеет на это право. + +Аутентификация в SSH основана на *паре ключей*: у вас есть *приватный* ключ (лежит только у вас на компьютере, никому не показываем, иначе мальчик Ваня зайдет и сольет все в даркнет) и *публичный* ключ (его мы привязываем и показываем). Всё, что зашифровано публичным ключом, может расшифровать только владелец приватного. GitHub хранит ваш публичный ключ, а при соединении проверяет, что вы владеете соответствующим приватным. + +Альтернатива SSH - HTTPS с логином и токеном, но SSH удобнее: настроил один раз и забыл, никаких паролей на каждый `push`. + +Подробнее об SSH вы узнаете чуть-чуть позже. + +=== Проверка существующих ключей + +[,console] +---- +$ ls -la ~/.ssh +---- + +Если видите файлы вида `id_ed25519` и `id_ed25519.pub` (или `id_rsa`/`id_rsa.pub`), ключ уже есть - переходите к добавлению на GitHub. + +=== Генерация нового ключа + +[,console] +---- +$ ssh-keygen -t ed25519 -C "ivan@example.com" +---- + +* `-t ed25519` - тип ключа (сейчас самый популярный; можно использовать `-t rsa -b 4096`). +* `-C` - комментарий, обычно почта; по нему потом легко узнать ключ на GitHub, если их несколько. + +`ssh-keygen` спросит две вещи: + +* *путь для сохранения* - нажмите Enter, чтобы принять значение по умолчанию (`~/.ssh/id_ed25519`); +* *passphrase* - пароль на сам ключ. Можно оставить пустым (Enter), тогда ключ будет использоваться без пароля. Если задали - придётся вводить passphrase при каждом использовании (или запомнить его в `ssh-agent`). + +=== Добавление ключа в ssh-agent + +`ssh-agent` - это фоновый процесс, который держит расшифрованный ключ в памяти, чтобы не вводить passphrase каждый раз: + +[,console] +---- +$ eval "$(ssh-agent -s)" +Agent pid 12345 +$ ssh-add ~/.ssh/id_ed25519 +---- + +=== Публикация ключа на GitHub + +Скопируйте содержимое *публичного* ключа - это файл с расширением `.pub`. Никогда не публикуйте файл без `.pub` - это приватный ключ: + +[,console] +---- +$ cat ~/.ssh/id_ed25519.pub +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... ivan@example.com +---- + +Затем на GitHub: *Settings → SSH and GPG keys → New SSH key*, вставьте содержимое в поле *Key*, придумайте название (обычно имя компьютера) и сохраните. + +=== Проверка соединения + +[,console] +---- +$ ssh -T git@github.com +Hi username! You've successfully authenticated, but GitHub does not provide shell access. +---- + +Если увидели такое сообщение - всё работает. При первом соединении Git спросит, доверяете ли вы отпечатку сервера - отвечайте `yes`. + +== Создание нового репозитория с нуля + +Правильный порядок действий, если вы начинаете новый проект. + +=== Шаг 1. Создать папку и инициализировать репозиторий + +[,console] +---- +$ mkdir my-project +$ cd my-project +$ git init +Initialized empty Git repository in /home/user/my-project/.git/ +---- + +=== Шаг 2. Сразу добавить `.gitignore` + +Это важно сделать *до первого коммита*, иначе рискуете залить в историю всякий мусор: собранные бинарники, кэши IDE, локальные конфиги с паролями. Готовые шаблоны на все языки лежат здесь: https://github.com/github/gitignore. + +=== Шаг 3. Добавить README.md + +Я думаю, вы уже поняли, что это, когда делали первую таску, но если забыли: +Короткое описание проекта - что это и как запустить. Первое, что видит любой, кто открывает ваш репозиторий. + +=== Шаг 4. Первый коммит + +[,console] +---- +$ git add . +$ git status <1> +$ git commit -m "Initial commit" +---- + +=== Шаг 5. Создать пустой репозиторий на GitHub + +На сайте: *New repository*, задайте имя, *не* ставьте галочки «Add README», «Add .gitignore», «Add license». Иначе на сервере создадутся коммиты, которых нет у вас локально, и первый `push` не пройдёт без дополнительных манипуляций. + +=== Шаг 6. Привязать remote и запушить + +GitHub на странице пустого репозитория покажет готовые команды, они выглядят так: + +[,console] +---- +$ git remote add origin git@github.com:username/my-project.git +$ git branch -M main <1> +$ git push -u origin main <2> +---- +<1> Переименовать текущую ветку в `main` (на случай, если она называется `master`). +<2> Флаг `-u` (`--set-upstream`) запоминает связь `main → origin/main`, чтобы дальше можно было писать просто `git push` и `git pull` без указания ветки. + +== Как залить существующий локальный проект на GitHub + +Если у вас уже есть папка с кодом, но без Git: + +[,console] +---- +$ cd существующий-проект +$ git init +$ echo "node_modules/" > .gitignore <1> +$ git add . +$ git commit -m "Initial commit" +$ git branch -M main +$ git remote add origin git@github.com:username/project.git +$ git push -u origin main +---- +<1> Обязательно перед `git add .` - иначе всё лишнее попадёт в первый коммит, и вычистить его оттуда потом будет непросто. + +Если репозиторий уже был инициализирован раньше и нужно только привязать его к remote: + +[,console] +---- +$ git remote add origin git@github.com:username/project.git +$ git push -u origin main +---- + +== Как склонировать чужой репозиторий и работать в нём + +[,console] +---- +$ git clone git@github.com:someuser/cool-project.git +$ cd cool-project +---- + +Дальше есть два сценария в зависимости от того, есть ли у вас права на push. + +=== Сценарий 1. У вас есть права (свой проект, командная разработка) + +Не работайте прямо в `main` - заведите отдельную ветку под каждую задачу, в проде не принято все пушить в 'main': + +[,console] +---- +$ git checkout -b feature/login <1> +# ...пишете код, коммитите... +$ git push -u origin feature/login <2> +---- +<1> Создать ветку и сразу переключиться на неё. Эквивалент: `git switch -c feature/login`. +<2> Первый `push` новой ветки - с `-u`, чтобы связать её с одноимённой веткой на сервере. + +После этого на GitHub заходите в репозиторий и создаёте *Pull Request* из вашей ветки в `main`. + +=== Сценарий 2. У вас нет прав (open source, чужой проект) + +Здесь используется механизм *fork*: + +. На GitHub нажимаете *Fork* - появляется ваша копия репозитория. +. Клонируете *свой fork*, а не оригинал: ++ +[,console] +---- +$ git clone git@github.com:ВАШ_username/cool-project.git +---- +. Часто добавляют второй remote с именем `upstream`, указывающий на оригинал - чтобы забирать оттуда свежие изменения: ++ +[,console] +---- +$ git remote add upstream git@github.com:someuser/cool-project.git +$ git fetch upstream +$ git merge upstream/main +---- +. Работу ведёте в отдельной ветке, пушите в свой fork, оттуда создаёте Pull Request в оригинальный репозиторий. + +== Коммиты: практика + +=== Как сделать коммит + +Полный цикл выглядит так: + +[,console] +---- +$ git status <1> +$ git add file1.py file2.py <2> +$ git status <3> +$ git commit -m "Fix off-by-one error in pagination" <4> +---- +<1> Посмотреть, что вообще изменилось. +<2> Добавить нужные файлы в индекс. Можно `git add .` - добавить всё, но лучше выбирать осознанно. +<3> Убедиться, что в индексе именно то, что нужно. +<4> Зафиксировать снимок с сообщением. + +Полезные варианты `git add`: + +[,console] +---- +$ git add . <1> +$ git add -u <2> +$ git add -p <3> +---- +<1> Все изменения в текущей папке и ниже. +<2> Только уже отслеживаемые файлы (новые файлы не добавит). +<3> Интерактивно: Git показывает изменения по кускам (hunks) и спрашивает про каждый, добавлять ли. Отличный способ разбить кашу изменений на аккуратные отдельные коммиты. + +=== Удаление и переименование файлов + +Если удалить файл через `rm` или в проводнике, Git заметит это как обычное изменение, и вам потом придётся `git add`-ать само удаление. Проще делать всё одной командой: + +[,console] +---- +$ git rm secrets.txt <1> +$ git rm --cached secrets.txt <2> +$ git mv old.py new.py <3> +---- +<1> Удалить и с диска, и из Git. +<2> Оставить файл на диске, но перестать за ним следить (полезно, когда случайно закоммитили то, что должно было попасть в `.gitignore`). +<3> Переименовать. Аналогично `mv old.py new.py && git add old.py new.py`, но короче. + +=== Как писать хорошие сообщения коммитов + +Как минимум, лучше писать на английском языке. + +Устоявшаяся структура сообщения: + +[,text] +---- +Коротко суть (до ~50 символов, повелительное наклонение) + +Развёрнутое пояснение при необходимости: что именно изменено, +почему это нужно было сделать и что важно знать читателю коммита. +Строки обычно переносят по ~72 символа. + +Писать в коммите 'aboba' не стоит... +---- + +Чтобы удобно писать сообщение с телом, запускайте `git commit` *без* `-m` - откроется редактор (по умолчанию `vim` (как закрыть `vim` сами нагуглите, это первая трудность начинающего программиста); поменять можно так): + +[,console] +---- +$ git config --global core.editor "nano" +---- + +=== Как изменить сообщение последнего коммита + +[,console] +---- +$ git commit --amend -m "Новое сообщение" +---- + +Или без `-m`, чтобы отредактировать в редакторе: + +[,console] +---- +$ git commit --amend +---- + +`--amend` можно использовать и чтобы *дописать* забытые изменения в последний коммит: + +[,console] +---- +$ git add забытый_файл.py +$ git commit --amend --no-edit <1> +---- +<1> `--no-edit` - оставить старое сообщение как есть. + +WARNING: `--amend` создаёт *новый* коммит вместо старого (со своим хэшем). Если старый коммит уже был запушен, после `amend` придётся делать `git push --force-with-lease`, а это ломает историю для тех, кто уже успел скачать старый вариант. Правило простое: `amend` - только для коммитов, которых ещё нет на сервере. + +=== Как изменить сообщение старого коммита + +Например, чтобы изменить сообщение коммита три шага назад: + +[,console] +---- +$ git rebase -i HEAD~3 +---- + +Откроется редактор со списком коммитов. Замените `pick` на `reword` напротив нужного коммита, сохраните файл - Git откроет ещё один редактор для нового сообщения. + +CAUTION: Та же оговорка, что с `--amend`: `rebase` переписывает историю. Не делайте этого с коммитами, которые уже видели другие люди. + +== Мониторинг: смотрим, что происходит + +Пока писали код, стало ничего не понятно: где мы, что поменяли, кто это вообще написал. Для всего этого у Git есть куча команд. +Или пришел ваш коллега и накоммитил кучу файлов, вообще запутались. + +=== git status + +Первая команда, которую вы будете вводить сотни раз в день. Показывает: + +* на какой вы ветке; +* какие файлы изменены, но не добавлены в индекс; +* какие файлы уже в индексе, ждут коммита; +* какие файлы Git вообще не знает. + +Правило: перед каждым `git add` и перед каждым `git commit` - `git status`. Ваня забил на это правило и запушил в main файл с логинами тестовой БД. Не будьте как Ваня. + +=== git log + +История коммитов. Флагов у него куча, самое полезное: + +[,console] +---- +$ git log --oneline <1> +$ git log --graph --all <2> +$ git log --stat <3> +$ git log -p <4> +$ git log --author="Ivan" <5> +$ git log --grep="fix" <6> +$ git log main..feature <7> +---- +<1> Компактно, по коммиту в строку. +<2> ASCII-график веток. +<3> Плюс список изменённых файлов в каждом коммите. +<4> Плюс сам diff каждого коммита. +<5> Только коммиты определённого автора. +<6> Только коммиты, у которых в сообщении встречается слово. +<7> Что есть в feature, но нет в main. + +Красивый набор для ежедневного использования: + +[,console] +---- +$ git log --oneline --graph --decorate --all +---- + +Такое длинно писать каждый раз лень, поэтому обычно ставят алиас: + +[,console] +---- +$ git config --global alias.lg "log --oneline --graph --decorate --all" +$ git lg +---- + +=== git show + +Подробности одного коммита: автор, дата, сообщение, полный diff: + +[,console] +---- +$ git show HEAD +$ git show 3f2a1b0 +$ git show HEAD:path/to/file.py <1> +---- +<1> Версия файла в этом коммите (без diff-а, просто содержимое). + +=== git blame + +Показывает, кто и в каком коммите написал каждую строку файла. Незаменимая штука, когда смотришь на непонятный кусок кода и хочешь понять, «кто это придумал»: + +[,console] +---- +$ git blame main.py +3f2a1b0 (Ivan 2026-06-15) def calculate_total(items): +c4d5e6f (Anna 2026-06-18) return sum(item.price for item in items) +---- + +Ваня использует `blame`, чтобы понять, кому отправлять баг-репорт. + +=== git bisect + +Двоичный поиск сломавшего коммита. Незаменим в ситуации «неделю назад работало, сегодня нет, а коммитов между ними полсотни»: + +[,console] +---- +$ git bisect start +$ git bisect bad <1> +$ git bisect good 3f2a1b0 <2> +# git переключается на коммит посередине, +# вы проверяете, работает или нет: +$ git bisect good <3> +# git продолжает делить пополам, пока не найдёт первый плохой +$ git bisect reset <4> +---- +<1> Текущий коммит сломан. +<2> А вот этот работал. +<3> Или `git bisect bad`, в зависимости от результата проверки. +<4> Выйти из режима bisect и вернуться на свою ветку. + +Логарифм вместо линейного перебора, очень удобно. (Надеюсь, вы знаете в чем разница...) + +== Ветки: подробнее + +Ветка в Git - это просто подвижный указатель на коммит. Никакого волшебства: файлик в `.git/refs/heads/` с хэшем внутри. Создавать их дёшево, поэтому не стесняйтесь. + +=== Создание, переключение, удаление + +[,console] +---- +$ git branch feature-x <1> +$ git switch -c feature-x <2> +$ git checkout -b feature-x <3> +---- +<1> Создать, но не переключаться. +<2> Создать и сразу переключиться (современный способ). +<3> То же самое, старый способ. `checkout` умеет много всего, поэтому в новых версиях его разделили на `switch` (переключение веток) и `restore` (восстановление файлов). + +[,console] +---- +$ git branch <1> +$ git branch -r <2> +$ git branch -a <3> +$ git branch -v <4> +---- +<1> Локальные ветки. +<2> Удалённые. +<3> Все. +<4> Плюс последний коммит в каждой. + +[,console] +---- +$ git branch -d feature-x <1> +$ git branch -D feature-x <2> +$ git branch -M new-name <3> +---- +<1> Удалить ветку, если она уже смержена. +<2> Удалить принудительно (даже если есть незамерженные коммиты). +<3> Переименовать текущую ветку. + +=== HEAD, ~ и ^ + +`HEAD` - указатель на текущий коммит. Обычно он указывает на ветку, а ветка - на коммит. + +От любого коммита можно ходить назад через `~` и `^`: + +* `HEAD~1` - на один коммит назад от HEAD (родитель). +* `HEAD~2` - на два коммита назад. +* `HEAD^` - то же, что `HEAD~1`. Но если у коммита несколько родителей (например, merge-коммит), `HEAD^1` это первый родитель, `HEAD^2` - второй. +* `HEAD~2^2` - можно комбинировать. + +Правило-подсказка: `~` идёт вглубь по истории, `^` выбирает родителя, если их несколько. + +Работает не только с HEAD, а с любым коммитом или веткой: `main~3`, `feature-x^`. + +=== Detached HEAD + +Обычно HEAD указывает на ветку. Но если сделать `git checkout` на конкретный коммит (по хэшу или тегу), HEAD будет указывать прямо на него, минуя ветку. Это состояние называется *detached HEAD*, «оторванная голова»: + +[,console] +---- +$ git checkout 3f2a1b0 +Note: switching to '3f2a1b0'. +You are in 'detached HEAD' state... +---- + +В этом состоянии можно смотреть код, экспериментировать, даже коммитить. Но если переключиться на другую ветку, эти коммиты потеряются - на них не будет никакого указателя, и через некоторое время `git gc` их удалит. Если что-то нужно сохранить, создайте из этого места ветку: + +[,console] +---- +$ git switch -c experiment +---- + +Ваня как-то полдня фиксил баг в detached HEAD, потом переключился на main «проверить кое-что» и всё потерял. Не будьте как Ваня. Впрочем, если очень надо - см. раздел про `reflog` ниже, обычно данные ещё можно вытащить. + +=== Теги + +Тег - именованный указатель на коммит, но, в отличие от ветки, он не двигается. + +[,console] +---- +$ git tag v1.0.0 <1> +$ git tag -a v1.0.0 -m "Release" <2> +$ git tag <3> +$ git show v1.0.0 <4> +$ git push origin v1.0.0 <5> +$ git push origin --tags <6> +---- +<1> Лёгкий тег - просто ссылка на коммит. +<2> Аннотированный тег - отдельный объект с автором, датой и сообщением. Для релизов используют именно такие. +<3> Список всех тегов. +<4> Показать, что за коммит помечен. +<5> Запушить конкретный тег. Обычный `git push` теги не отправляет! +<6> Запушить все теги. + +== Работа с удалёнными репозиториями: подробнее + +Про `git push`, `git pull` и `git fetch` вы уже видели. Теперь несколько деталей, о которых часто забывают. + +=== Что делает push + +Обычно достаточно просто `git push`, если ветка уже связана с удалённой (через `-u` при первом пуше): + +[,console] +---- +$ git push <1> +$ git push origin feature-x <2> +$ git push -u origin feature-x <3> +---- +<1> Текущую ветку в её upstream. +<2> Явно указать remote и ветку. +<3> То же самое, плюс запомнить связь `feature-x → origin/feature-x`. + +=== fetch vs pull + +`fetch` скачивает данные с сервера, но ничего не трогает в ваших ветках. После fetch у вас обновляются указатели `origin/main`, `origin/feature-x`, и можно посмотреть, что там прилетело, прежде чем сливать к себе: + +[,console] +---- +$ git fetch origin +$ git log HEAD..origin/main <1> +---- +<1> Показать, что нового в origin/main по сравнению с текущей веткой. + +`pull` = `fetch` + автоматическое применение изменений. По умолчанию через merge, но можно и через rebase: + +[,console] +---- +$ git pull <1> +$ git pull --rebase <2> +$ git config --global pull.rebase true <3> +---- +<1> `fetch` + `merge`. +<2> `fetch` + `rebase`. +<3> Чтобы rebase был по умолчанию всегда. + +Разница на пальцах: + +* `pull` (через merge) - проще, но каждый раз создаёт merge-коммиты вида «Merge branch 'main' of ...». История становится ветвистой и грязной. +* `pull --rebase` - переносит ваши локальные коммиты поверх свежих чужих. История линейная, но ваши коммиты меняют хэши. Не критично, пока эти коммиты не запушены. + +В большинстве команд негласно принят `pull --rebase`, потому его обычно включают глобально. + +=== Upstream и трекинг веток + +Каждая локальная ветка может быть *связана* с удалённой - это называется *upstream*. Связь настраивается флагом `-u` (он же `--set-upstream`) при первом push: + +[,console] +---- +$ git push -u origin feature-x +---- + +После этого: + +* `git status` показывает `ahead 2, behind 1` - на сколько коммитов вы обогнали удалённую и сколько отстали. +* `git pull` и `git push` работают без аргументов. +* Можно писать `@\{upstream}` (или короче `@\{u}`) вместо `origin/feature-x`. + +Посмотреть связи: + +[,console] +---- +$ git branch -vv +* feature-x c4d5e6f [origin/feature-x: ahead 2] Add login + main 3f2a1b0 [origin/main] Initial commit +---- + +Управлять связью: + +[,console] +---- +$ git branch --set-upstream-to=origin/other-branch <1> +$ git branch --unset-upstream <2> +---- +<1> Изменить upstream уже существующей ветки. +<2> Отвязать. + +=== Удаление удалённой ветки + +[,console] +---- +$ git push origin --delete feature-x +---- + +Или короче, но менее очевидно: + +[,console] +---- +$ git push origin :feature-x +---- + +Пустое имя перед двоеточием означает «запушить пустоту в feature-x», то есть удалить её. + +=== Синхронизация с несколькими remote (fork + upstream) + +Классическая схема для open-source: у вас есть свой fork (`origin`) и оригинал (`upstream`): + +[,console] +---- +$ git remote add upstream git@github.com:owner/project.git +$ git fetch upstream <1> +$ git switch main +$ git rebase upstream/main <2> +$ git push <3> +---- +<1> Забрать свежие изменения из оригинала. +<2> Обновить свою main поверх оригинала. +<3> Запушить обновлённую main в свой fork. + +== Merge, rebase, cherry-pick + +Три способа перенести изменения из одной ветки в другую. Разница между ними это одна из самых частых тем на собеседованиях, поэтому разберём подробно. + +=== Merge + +Объединяет две ветки, создавая новый *merge-коммит* с двумя родителями: + +[,console] +---- +$ git switch main +$ git merge feature-x +---- + +Если `main` не двигалась с момента ответвления feature-x, merge будет *fast-forward*: main просто «догонит» feature-x, никакого нового коммита не создастся. Если main тоже уходила вперёд - создастся merge-коммит. + +Плюс merge: сохраняет полную историю «как было на самом деле». Минус: история становится ветвистой, и в `git log` без графа выглядит запутанно. + +=== Rebase + +«Отрывает» ваши коммиты и переносит их поверх другой ветки: + +[,console] +---- +$ git switch feature-x +$ git rebase main +---- + +Было: + +[,text] +---- + A---B---C feature-x + / +D---E---F main +---- + +Стало: + +[,text] +---- + A'--B'--C' feature-x + / +D---E---F main +---- + +Коммиты `A'`, `B'`, `C'` - новые, со своими хэшами (даже если содержимое такое же). История линейная и красивая, но переписанная. + +=== Interactive rebase - швейцарский нож + +Самая мощная штука, которая есть в Git. Позволяет редактировать, объединять, переставлять, удалять коммиты: + +[,console] +---- +$ git rebase -i HEAD~5 +---- + +Откроется редактор со списком последних 5 коммитов и командами: + +* `pick` - оставить как есть; +* `reword` - изменить сообщение; +* `edit` - остановиться на этом коммите, чтобы что-то поменять в файлах; +* `squash` - слить с предыдущим (сообщения объединятся); +* `fixup` - как `squash`, но сообщение выбросить (часто именно то, что нужно); +* `drop` - удалить коммит; +* поменять местами строки - поменяются местами и коммиты. + +Полезно перед созданием Pull Request: подчистить историю, слить мелкие «fix» коммиты, переформулировать сообщения. + +=== Cherry-pick + +Взять один конкретный коммит и применить его к текущей ветке: + +[,console] +---- +$ git cherry-pick 3f2a1b0 +$ git cherry-pick 3f2a1b0..c4d5e6f <1> +---- +<1> Диапазон коммитов. + +Полезно, когда, например, нужно перенести один хотфикс из main в старую релизную ветку, но остальные изменения из main трогать нельзя. + +=== Что когда использовать (кратко) + +* Ваша ветка локальная или живёт только в вашем fork - смело `rebase`. +* Ветка общая, на неё смотрит кто-то ещё - только `merge`. Иначе сломаете коллегам историю. +* Нужен один конкретный коммит из другой ветки - `cherry-pick`. + +CAUTION: `rebase` и `cherry-pick` создают *новые* коммиты с новыми хэшами. Если делать это с уже запушенными коммитами, потом придётся `push --force`, а это плохо (см. дальше). + +== Три дерева Git, reset и revert + +Чтобы понять, что делают `reset` и `revert`, надо разобраться с *тремя деревьями* Git. + +=== Три дерева + +*Три дерева* - это три состояния файлов, которые Git одновременно держит в голове: + +* *HEAD* - что записано в последнем коммите текущей ветки. +* *Index* (staging area) - что готовится к следующему коммиту. +* *Working directory* - что реально лежит в файлах на диске. + +Обычный цикл работы двигает файлы по этим уровням слева направо: + +[,text] +---- +working directory --git add--> index --git commit--> HEAD +---- + +* `git add` берёт изменения из working directory и кладёт в index. +* `git commit` берёт то, что в index, и записывает в новый коммит, на который начинает указывать HEAD. + +`git reset` двигает всё это обратно. + +=== Три режима reset + +У `reset` три режима, которые отличаются тем, что именно он трогает: + +* `--soft` - двигает только указатель ветки (и HEAD) на нужный коммит. Index и working directory не трогает. Все ваши изменения остаются в индексе, как будто вы их только что `git add`-нули. +* `--mixed` (по умолчанию) - двигает HEAD и сбрасывает индекс, но working directory не трогает. Изменения остаются в файлах, но теперь их надо снова добавлять командой `git add`. +* `--hard` - двигает всё три уровня. Все изменения после этого коммита теряются. Совсем. Даже из файлов на диске. + +Пример: у вас три коммита A - B - C, вы стоите на C. + +[,console] +---- +$ git reset --soft HEAD~1 <1> +$ git reset HEAD~1 <2> +$ git reset --hard HEAD~1 <3> +---- +<1> HEAD на B, но изменения B→C лежат в индексе, готовы к новому коммиту. +<2> HEAD на B, изменения B→C в рабочей директории, но не в индексе. +<3> HEAD на B, всё удалено. Осторожно! + +Ещё один трюк: + +[,console] +---- +$ git reset HEAD file.py <1> +$ git restore --staged file.py <2> +---- +<1> Убрать файл из индекса, не трогая содержимое. Полезно, когда `git add` захватил лишнее. +<2> То же самое современным способом. + +=== Reset vs revert + +Кратко: + +* `reset` двигает указатель ветки назад, как будто коммитов не было. Историю *переписывает*. +* `revert` не трогает историю. Он делает *новый* коммит, который применяет обратные изменения к указанному: + +[,console] +---- +$ git revert HEAD <1> +$ git revert 3f2a1b0 <2> +---- +<1> Отменить последний коммит новым коммитом. +<2> Отменить произвольный коммит из истории. + +Правило: если коммит уже на сервере и его видели другие, только `revert`. `reset` в этом случае сломает историю всем, кто уже успел склонировать. + +== Мерж-конфликты: как это работает изнутри + +При merge Git ищет *общего предка* двух веток (base) и смотрит, как изменился файл в каждой ветке относительно этого base. + +Три случая: + +. В одной ветке файл изменился, в другой нет - берётся изменённая версия. Никакого конфликта. +. В обеих ветках файл изменился, но в *разных местах* - Git применяет оба набора изменений. Тоже без конфликта, это называется *auto-merge*. +. В обеих ветках изменена *одна и та же строка* (или соседние) - Git не знает, какой вариант правильный, и объявляет конфликт. + +Конфликтные места помечаются в файле: + +[,text] +---- +<<<<<<< HEAD +print("Hello, world!") +======= +print("Привет, мир!") +>>>>>>> feature-x +---- + +* Между `<<<<<<< HEAD` и `=======` - ваш вариант. +* Между `=======` и `>>>>>>> feature-x` - вариант из ветки feature-x. + +Как разрешить: + +. Открываете файл, руками выбираете нужный вариант (или комбинируете), удаляете все три маркера. +. `git add FILE` - сообщить Git, что конфликт разрешён. +. `git commit` - завершить merge. + +Если запутались и хотите откатить всё: + +[,console] +---- +$ git merge --abort <1> +$ git rebase --abort <2> +---- +<1> Отменить незавершённый merge. +<2> То же для незавершённого rebase. + +TIP: Очень полезная опция - включить трёхсторонний вид конфликтов: ++ +[,console] +---- +$ git config --global merge.conflictstyle diff3 +---- ++ +После этого маркеры будут выглядеть так: ++ +[,text] +---- +<<<<<<< HEAD +print("Hello, world!") +||||||| merged common ancestors +print("hello") +======= +print("Привет, мир!") +>>>>>>> feature-x +---- ++ +Средний блок - оригинальная строка до расхождения. Гораздо проще понять, что каждая ветка меняла. + +== Stash: временно спрятать изменения + +Ситуация: вы работаете над задачей, у вас куча несохранённых изменений, и тут срочно нужно переключиться на другую ветку и что-то починить. Коммитить пока рано (код не готов), а изменения потерять нельзя. + +Тут спасает `stash`: + +[,console] +---- +$ git stash <1> +$ git switch main <2> +# ... делаем срочные дела, коммитим ... +$ git switch feature-x <3> +$ git stash pop <4> +---- +<1> Спрятать все изменения (рабочая директория чистая). +<2> Переключиться на другую ветку и работать там. +<3> Вернуться на свою задачу. +<4> Вернуть спрятанные изменения обратно. + +Другие полезные команды: + +[,console] +---- +$ git stash list <1> +$ git stash show <2> +$ git stash apply <3> +$ git stash drop <4> +$ git stash push -m "wip: login" <5> +$ git stash push path/to/file.py <6> +---- +<1> Что у нас в загашнике. +<2> Какие файлы изменены в последнем stash. +<3> Применить, но не удалять из stash. +<4> Удалить последний stash. +<5> Спрятать с сообщением - потом легче найти. +<6> Спрятать только один файл. + +Stash - штука отдельная, не привязанная к ветке. Можно спрятать в одной, применить в другой. + +== Игнорирование файлов: .gitignore и .gitkeep + +`.gitignore` мы уже видели. Ещё немного полезного синтаксиса: + +[,bash] +---- +build/ # всё в этой папке +*.log # конкретное расширение +!important.log # но не этот файл (отрицание) +/config.yaml # только в корне репозитория +**/tmp/ # в любой папке +---- + +Порядок правил важен: последнее правило, подходящее под путь, побеждает. + +*Важный подвох*: `.gitignore` работает только для *ещё не отслеживаемых* файлов. Если вы уже закоммитили `secrets.txt`, добавление его в `.gitignore` ничего не изменит - Git продолжит за ним следить. Чтобы перестать отслеживать, но оставить на диске: + +[,console] +---- +$ git rm --cached secrets.txt +$ git commit -m "Stop tracking secrets" +---- + +=== Хитрость с .gitkeep + +Git не умеет отслеживать пустые папки, он вообще работает только с файлами. А иногда пустая папка нужна (например, `logs/`, куда программа будет писать при запуске). Общепринятый трюк: положить внутрь пустой файл `.gitkeep`. + +Никакой магии тут нет: `.gitkeep` это не команда Git и не что-то встроенное. Это просто конвенция, договорённость называть такие файлы именно так. Можно назвать хоть `.please_keep_me` - работать будет одинаково. + +== Хуки (git hooks) + +Хук - это скрипт, который Git запускает автоматически в определённый момент: перед коммитом, после мержа, перед пушем. Живут в `.git/hooks/`. + +Примеры полезных хуков: + +* `pre-commit` - запустить линтер или быстрые тесты перед коммитом, отменить коммит если что-то сломано. +* `commit-msg` - проверить формат сообщения (например, чтобы был указан номер задачи). +* `pre-push` - запустить полные тесты перед push. + +По умолчанию в `.git/hooks/` лежат примеры с расширением `.sample`. Чтобы включить хук, нужно переименовать (убрать `.sample`) и сделать исполняемым: + +[,console] +---- +$ mv .git/hooks/pre-commit.sample .git/hooks/pre-commit +$ chmod +x .git/hooks/pre-commit +---- + +Простейший `pre-commit`, который запрещает коммитить в `main`: + +[,bash] +---- +#!/bin/bash +branch=$(git symbolic-ref HEAD | sed 's|refs/heads/||') +if [ "$branch" = "main" ]; then + echo "DO NOT COMMIT TO MAIN SOS ALARM WAIT BRO" + exit 1 +fi +---- + +WARNING: Папка `.git/hooks/` не версионируется - она лежит внутри `.git`, а он в репозиторий не входит. Каждый разработчик ставит хуки себе сам. Чтобы синхронизировать команду, используют инструменты вроде *pre-commit*: конфиг лежит в самом репо, хуки ставятся автоматически при `install`. + +== Сабмодули (submodules) + +Иногда нужно включить один Git-репозиторий в состав другого - например, общую библиотеку, которая живёт отдельно. Для этого есть *сабмодули*: + +[,console] +---- +$ git submodule add git@github.com:user/library.git libs/library +---- + +Внутри вашего репозитория появится папка `libs/library` с содержимым другого репозитория, а сам факт «здесь сабмодуль на такой-то коммит такого-то репозитория» запишется в файл `.gitmodules` (его надо закоммитить). + +Клонирование репозитория с сабмодулями: + +[,console] +---- +$ git clone --recurse-submodules git@github.com:user/project.git +# или, если забыли: +$ git submodule update --init --recursive +---- + +Обновить сабмодули до свежих версий из их удалённых репозиториев: + +[,console] +---- +$ git submodule update --remote +---- + +== Git LFS (Large File Storage) + +Git плохо работает с большими бинарниками. Если закоммитить файл на 100 МБ, каждый клон репозитория будет тянуть эти 100 МБ, даже если файл сто раз с тех пор поменяли - Git хранит все версии. +Заливать бинари на git - принцип плохого тона. +Git LFS решает это: вместо самого файла Git хранит маленькую метку-указатель, а сам файл лежит на отдельном сервере и скачивается по запросу. + +Установка (один раз на систему): + +[,console] +---- +$ git lfs install +---- + +Настройка в конкретном репозитории: + +[,console] +---- +$ git lfs track "*.psd" <1> +$ git lfs track "*.mp4" +$ git add .gitattributes <2> +$ git add file.psd +$ git commit -m "Add design file" +---- +<1> Все `.psd` теперь идут через LFS. +<2> LFS запишет свои настройки сюда, надо закоммитить. + +Что уже под LFS: + +[,console] +---- +$ git lfs ls-files +---- + +== Bundles + +Bundle - это архив с частью репозитория (или всем целиком), который можно передать без сетевого соединения. Полезно, когда надо скинуть репу по флешке коллеге без интернета. + +[,console] +---- +$ git bundle create repo.bundle --all <1> +$ git bundle create fix.bundle main..fix-x <2> +---- +<1> Весь репозиторий в один файл. +<2> Только коммиты, которые есть в fix-x, но нет в main. + +Применение: + +[,console] +---- +$ git clone repo.bundle new-project <1> +$ git bundle verify fix.bundle <2> +$ git fetch fix.bundle fix-x:fix-x <3> +---- +<1> Склонировать из bundle, как из обычного remote. +<2> Проверить, что bundle не побит. +<3> Забрать из bundle ветку fix-x в свою локальную fix-x. + +Штука редкая, но иногда очень выручает - особенно в местах с плохим интернетом. + +== Как устроен .git изнутри + +Мы люди уважающие себя, умеем уже пользоваться терминалом (пожалуйста, не коммитьте через веб-интерфейс), давайте заглянем в `.git/`. Понимание внутренностей сильно упрощает жизнь: становится ясно, почему `checkout` быстрый, что такое хэш и как reflog спасает после `reset --hard`. + +=== .git/objects/ + +Здесь лежат *все* данные репозитория: коммиты, деревья, содержимое файлов. Каждый объект это gzip-сжатый файл, названный своим SHA-1 хэшем. Первые 2 символа хэша - имя папки, остальные - имя файла: + +[,console] +---- +$ ls .git/objects +3f/ c4/ d5/ ... +$ ls .git/objects/3f +2a1b0e5d8f4a9c... +---- + +Посмотреть содержимое объекта: + +[,console] +---- +$ git cat-file -t 3f2a1b0 <1> +$ git cat-file -p 3f2a1b0 <2> +---- +<1> Тип объекта: `blob`, `tree`, `commit`, `tag`. +<2> Содержимое (уже разжатое). + +Всего 4 типа объектов: + +* *blob* - содержимое файла (без имени файла - имя лежит в дереве); +* *tree* - список файлов и подпапок (имена + хэши blob'ов и поддеревьев); +* *commit* - указатель на tree корня + автор + сообщение + хэши родителей; +* *tag* - аннотированный тег. + +Обычная ветка `main` при этом это просто файл `.git/refs/heads/main`, содержащий хэш коммита. Всё, никакой магии. + +=== Pack-файлы и git gc + +Если хранить по отдельному файлу на каждый объект, репозиторий разрастётся до миллионов файлов. Git время от времени пакует объекты в компактные *pack-файлы* (`.git/objects/pack/`) и удаляет свободные копии. + +Вручную запустить упаковку и уборку мусора: + +[,console] +---- +$ git gc +---- + +`gc` заодно удаляет *недостижимые* объекты - те, до которых нельзя добраться ни от одной ветки, тега или reflog. Например, коммиты, которые вы сбросили через `git reset --hard`, ещё какое-то время лежат в `.git/objects` (и их можно восстановить), но потом `gc` их окончательно удалит. + +=== .git/refs/ + +Все ссылки: ветки, теги, remote-ветки. + +[,console] +---- +$ ls .git/refs +heads/ remotes/ tags/ +$ cat .git/refs/heads/main +3f2a1b0e5d8f4a9c... +---- + +Файл `.git/HEAD` содержит либо `ref: refs/heads/branch-name` (когда HEAD указывает на ветку), либо сразу хэш коммита (detached HEAD). + +=== .git/logs/ и reflog + +Здесь лежит *reflog* - лог всех движений HEAD и веток. Каждый раз, когда вы делаете `commit`, `checkout`, `reset`, `merge`, `rebase`, в reflog записывается строчка. + +[,console] +---- +$ git reflog +c4d5e6f HEAD@{0}: commit: Add login +3f2a1b0 HEAD@{1}: reset: moving to HEAD~1 +5a6b7c8 HEAD@{2}: commit: Broken commit +---- + +Reflog *локальный*, живёт около 90 дней и это ваша страховка от катастроф. Даже если вы сделали `git reset --hard` и «потеряли» коммиты, они ещё в reflog - можно восстановить: + +[,console] +---- +$ git reset --hard HEAD@{2} +---- + +Ваня, потерявший свой detached HEAD, скорее всего мог всё вернуть через reflog. Знал бы - не потерял бы полдня. + +=== Внимание: git push -f не гарантирует удаление данных на сервере + +Есть распространённое заблуждение: «я запушил секретный токен в репозиторий, сейчас сделаю `git reset` + `git push --force`, и всё, следов не останется». Это не так, и на этом периодически горят даже опытные ребята, их потом сливают в даркнет. + +Что происходит на самом деле: + +. Вы сбрасываете локальную ветку и делаете force-push. Да, ветка на сервере теперь указывает на другой коммит, без ваших секретов. +. Но старые коммиты никуда не деваются: они остаются в `.git/objects` на сервере, потому что на них могут ещё указывать pull request'ы, кэши CI, форки, чей-то reflog. +. GitHub и GitLab держат «висячие» коммиты доступными по прямому URL с хэшем ещё долгое время. Условный `github.com/user/repo/commit/3f2a1b0` продолжит открываться. +. Если кто-то успел склонировать репозиторий или сделать fork - у него все ваши секреты и в объектах, и в reflog. И он о них скорее всего даже не знает. +. Боты и сервисы вроде GitHub secret scanning мониторят публичные репозитории в реальном времени и могли скачать секрет за те несколько минут, что он висел в открытом доступе. + +Вывод: если утёк ключ, токен или пароль - *немедленно отзывайте / меняйте сам ключ*, а не пытайтесь стереть его из истории. Все считаем данные утёкшими, ротируем, идём дальше. + +Отдельно почистить историю можно инструментами вроде `git filter-repo` или `BFG Repo-Cleaner` и попросить поддержку GitHub удалить кэши - но это уже post-mortem, а не защита. Так что тип всё, данные утекли. + +== gh и glab: работа с GitHub/GitLab из терминала + +Стандартный `git` знает только про репозитории и коммиты. Всё, что вокруг них - Pull Request, issues, actions, releases, - это надстройки конкретных сервисов, и до недавнего времени с этим приходилось лезть в браузер. + +Официальные CLI-утилиты закрывают эту дыру: + +* `gh` для GitHub - https://cli.github.com; +* `glab` для GitLab - https://gitlab.com/gitlab-org/cli. + +Что можно делать из терминала: + +[,console] +---- +$ gh auth login <1> +$ gh repo clone user/project <2> +$ gh repo create my-project --public <3> +$ gh pr create --fill <4> +$ gh pr list <5> +$ gh pr checkout 42 <6> +$ gh pr review --approve <7> +$ gh issue list +$ gh run watch <8> +---- +<1> Один раз залогиниться в GitHub. +<2> То же, что `git clone`, но короче. +<3> Создать репу и сразу привязать remote. +<4> Создать PR из текущей ветки, заголовок и описание взять из последнего коммита. +<5> Список открытых PR. +<6> Переключиться на ветку PR #42, даже если она в чужом fork. +<7> Заапрувить PR прямо из терминала. +<8> Смотреть, как крутится CI. + +`glab` устроен в целом точно так же, только для GitLab. \ No newline at end of file