В первой части конспекта я описал основные команды в гит: инициализация, коммиты, отмена коммитов или даже «затирание» истории. В этой части пора посмотреть, как можно параллельно работать над разными фичами в проекте: речь пойдём о ветках и stash.
Ветки (branch). Создание, удаление, переключение между ветками.
Ветка – отдельная линия разработки, где можно что угодно менять, пока основная ветка (main) остаётся стабильной. Это нужно, чтобы не ломать основной проект, пока в разработке какие-то новые функции. Таким образом можно или самостоятельно работать над несколькими функциями, а потом безопасно «вливать» этот код в основную ветку (merge), или работать целой командой, где каждый человек независимо работает в своей «песочнице».
git branch – позволяет посмотреть текущие ветки в проекте.
git branch -a – показывает локальные и удалённые ветки.
Я в своём проекте для практики гита (там только несколько текстовых файлов) получаю такой вывод (текущая ветка обозначена звёздочкой):
C:\Users\ezimo\Desktop\git practice>git branch* main
Чтобы создать новую ветку, нужно выполнить одну из двух команд:
git switch -c new-branchgit checkout -b new-branch2
switch -c (create) создаст ветку с заданным именем, как и checkout -b (branch). После создания автоматически переходим на эту ветку. Чтобы переключиться на другую ветку (в примере это главная ветка) используем одну из команд:
git switch maingit checkout main
Выше я создал две ветки, но мне пока хватит и одной, поэтому удалю new-branch2:
git branch -d new-branch2 – удаляет ветку с заданным именем
Если она не полностью влита, то получим сообщение об ошибке, чтобы не потерять работу:
error: The branch 'new-branch2' is not fully merged.If you are sure you want to delete it, run 'git branch -D new-bran2'.
Если всё же хотим её удалить, то пишем -D заглавной:
git branch -D new-branch2 – принудительное удаление ветки
Объединение веток (merge)
Чтобы поработать с объединением веток, я оказываюсь в ветке new-branch, и в одном из файлов в своём проекте проведу изменения: введу Hello from new-branch.
commit e62e307dfd91dd3c4b229a7 (HEAD -> new-branch)Author: egor-Date: Thu Dec 11 21:52:05 2025 +0500Hello from branch
В логах после коммита вижу, где я нахожусь. Теперь можно слить две ветки. Для этого нужно вернуться в ту ветку, куда мы хотим влить изменения (для нас это main) и использовать команду merge:
git switch maingit merge new-branch
Тогда в логах вижу, что случился Fast-forward, потому что нет никакого конфликта: мы ничего не делали в main ветке, то есть граф у нас такой:
main -- C1 -- C2 \ C3 (new-branch)
В логах увидим такие сообщения:
C:\Users\ezimo\Desktop\git practice>git merge new-branchUpdating 5a2c7e4..e62e337Fast-forwardnewfile.txt | 1 +1 file changed, 1 insertion(+)C:\Users\ezimo\Desktop\git practice>git branch* mainnew-branchC:\Users\ezimo\Desktop\git practice>git logcommit e623407d6d971dd3c456 (HEAD -> main, new-branchAuthor: egor-Date: Thu Dec 11 21:52:05 2025 +0500Hello from branch
Теперь попробуем поменять что-то в каждой ветке, и потом их слить. Если конфликта нет, то получаю ещё такое сообщение о рекурсивной стратегии:
C:\Users\ezimo\Desktop\git practice>git merge new-branchMerge made by the 'recursive' strategy.newfile.txt | 2 +-1 file changed, 1 insertion(+), 1 deletion(-)
Но теперь, ради эксперимента, создам конфликт.
Так как я работаю с одним каталогом на диске, то придётся немного похитрить. В ветку main закоммичу изменённый файл initial file.txt. Перейду в ветку new-branch, сотру изменения из initial file.txt, но изменю файл new file.txt. Таким образом, я имитирую работу двух людей над двумя разными файлами в двух разных ветках. В жизни, конечно, это происходит само.
Конфликт произошёл, потому что файл initial file.txt в двух этих ветках имеет разное состояние. В одном из них у меня дописана строчка, а в другом этой строчки нет. Гит не знает, какой вариант выбрать, поэтому конфликт нужно разрешить самостоятельно. В файле увидим содержимое обеих веток:
<<<<<<< HEADHello from main branchhey=======Hello from main branch>>>>>>> new-branch
Чтобы разрешить конфликт, нужно выбрать подходящую версию и стереть все маркеры гита. После этого делаем git add initial file.txt, git commit (откроется окно текстового редактора для ввода сообщения, но гит уже сам напишет туда о merge, поэтому я просто оставляю, как есть).
Merge свершился!
commit 476878c5a04fb481b (HEAD -> main)Merge: 27da530edAuthor: egor-Date: Thu Dec 11 22:20:32 2025 +0500Merge branch 'new-branch'
Если мы получили конфликт при слиянии, но передумали его делать, то можем написать:
git merge —abort – отменяет слияние и возвращает файлам на диске то состояние,, в котором они «живут» в последнем коммите в HEAD.
Работа с удалёнными ветками
Теперь посмотрим, как работать с ветками в github:
git push -u origin new-branch – заливает новую ветку. Связывает локальную ветку new-branch с этой новой веткой в гитхабе.
git fetch —all – получает все удалённые ветки
git pull origin main – обновляет локальную ветку main данными с GitHub
Pull важная команда, потому что если ветка на гитхабе опережает нашу на несколько коммитов, то нам не позволят запушить в неё новые изменения.
Я имитировал это, создав две идентичных копии локального репозитория. Из одной копии я запушил изменения в initial file.txt (строчка Hello). Во второй папке я добавил в тот же файл Goodbye, и попытался запушить, и получил ошибку, потому что origin теперь опережает состояние в этой локальной папке:
! [rejected] main -> main (fetch first)error: failed to push some refs to 'https://github.com/egor-no/git-practice.git'hint: Updates were rejected because the remote contains work that you dohint: not have locally. This is usually caused by another repository pushinghint: to the same ref. You may want to first integrate the remote changeshint: (e.g., 'git pull ...') before pushing again.hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Для начала нужно сделать pull. Pull это совмещённый fetch и merge (по умолчанию так, хотя можем заменить на rebase, но об этом ниже). Так что при pull у нас тоже будет конфликт: в удалённом репозитории изменён как раз тот файл, над которым мы работаем. Гит опять не знает, что же надо оставить: Hello из origin, или Goodbye в локалке?
CONFLICT (content): Merge conflict in initial file.txtAutomatic merge failed; fix conflicts and then commit the result.
Нужно точно так же открыть файл и вручную удалить маркеры гита и разрешить конфликт, после чего можно уже запушить данные. Тогда и в этом локальном репозитории и в удалённом появятся данные обо всех изменениях: и о Hello, и o Goodbye, и о мерже.
Теперь мы побаловались и можно удалить лишнюю ветку:
git push origin —delete new-branch – удаляет удалённую ветку (каламбур какой-то получился)
Rebase и переписывание истории
Rebase читерская команда, которая позволяет нам все коммиты «перенести» в другую ветку после её последнего коммита, то есть она меняет историю, в отличие от merge, делая её линейной.
Например, после merge наша история весьма ветвистая (где M – merge):
A --- B --- C ------ M \ / D -- E
А при rebase мы получаем:
A --- B --- C --- D' --- E'
Rebase, чтобы не сломать историю в удалённом репозитории для других людей, можно использовать локально. Например, у нас локально пара веток, но мы хотим не загружать их обе в удалённую ветку на github, и сначала сделаем rebase, чтобы история была линейной и понятной.
git rebase new-branch-rebase – переносит коммиты текущей ветки в конец указанной new-branch-rebase. Важно помнить, что аргумент здесь не источник, а ветка назначения (в отличие от merge).
По умолчанию pull – это fetch + merge, то есть он сохраняет ветвления в истории. Но мы можем применить rebase, чтобы сделать историю аккуратнее:
git pull —rebase
Можем даже настроить сам гит так, чтобы это было стандартным поведением:
git config pull.rebase truegit pull
При rebase тоже может быть конфликт (если опять же изменён один и тот же файл), и его так же нужно решить в самом файле, а потом сделать —continue:
git add "newfile.txt"git rebase –continue
git rebase —abort – отменяет процесс.
Теперь мне интересно сломать историю и посмотреть, как её потом чинить.
У меня две идентичные папки. В первой я делаю два коммита и пушу их в удалёнку. Подтягиваю эти изменения во вторую папку с pull.
C:\Users\ezimo\Desktop\git practice>git add .C:\Users\ezimo\Desktop\git practice>git commit -m "Delete second line"C:\Users\ezimo\Desktop\git practice>git add .C:\Users\ezimo\Desktop\git practice>git commit -m "Delete first line"C:\Users\ezimo\Desktop\git practice>git pushC:\Users\ezimo\Desktop\git practice>cd C:\Users\ezimo\Desktop\git practice2C:\Users\ezimo\Desktop\git practice2>git pullC:\Users\ezimo\Desktop\git practice2>git log --oneline --max-count=3bec291d (HEAD -> main, origin/main) Delete first line0568dae Delete second line9912e7c second commit
Но теперь я внезапно решаю переписать историю в первой папке и сделать rebase. Из трёх последних коммитов решаю стереть парочку, разрешить конфликт и запушить эту новую изменённую историю форсированно (с —force-with-lease).
C:\Users\ezimo\Desktop\git practice>git rebase -i HEAD~3Auto-merging newfile.txtCONFLICT (content): Merge conflict in newfile.txtC:\Users\ezimo\Desktop\git practice>git add newfile.txtC:\Users\ezimo\Desktop\git practice>git rebase --continue[detached HEAD bf15fce] Delete first line1 file changed, 2 insertions(+), 2 deletions(-)Successfully rebased and updated refs/heads/mainC:\Users\ezimo\Desktop\git practice>git log --oneline --max-count=5bf15fce (HEAD -> main) Delete first line9912e7c second commit1d6dda3 first commita228a2c Delete line fron new fileb37b668 (new-branch-rebase) Adding line to new fileC:\Users\ezimo\Desktop\git practice>git push --force-with-lease origin main
Обратите внимание, как поменялась история. Пропало любое упоминание Delete second line! Но во второй папке оно всё есть в памяти. Поэтому если там сделать pull, то получим конфликт, который нужно будет разрешать. Push тоже бы не вышел. Именно поэтому rebase нужно делать только в личном проекте, где вы уверены в себе, или только на локальных репозиториях, если работаете с кем-то над проектом.
К тому же, после push из второй папки все удалённые нами коммиты вернулись в историю. Никого не удалось обмануть! Так что оно того, наверно, и не стоит. Всё равно люди в команде сразу поймут, кого винить в их проблемах с неожиданными мёржами.
Берём один коммит из другой ветки с Cherry-pick
git cherry-pick – берёт конкретный коммит (или несколько) по хэшу и повторяет его изменения поверх текущей ветки. Например:
git cherry-pick a1b2c3d
Git создаст новый коммит с новым хэшем, но с теми же изменениями.
Если нужно перенести несколько коммитов для диапазона с двумя точками или с пробелами для списка несвязанных хэшей:
git cherry-pick a1b2c3d..f6e7d8cgit cherry-pick a1b2c3d 5f6e7d 9abc123
Если возник конфликт, то получаем об этом сообщение
CONFLICT (content): Merge conflict in <file>
Тогда опять исправляем конфликт вручную в файле и отмечаешь файл как решённый:
git add <file>git cherry-pick --continue
Эта функция нужна, когда работа ведётся в другой ветке, но например мы хотим добавить в ветку main фикс какого-то важного бага, чтобы не ждать всего слияния. После слияния оба коммита (и наш «вишнёвый» и оригинальный появятся в истории, но что поделать? – граф истории коммитов не всегда красивый).
Временное хранилище изменений в Stash
Иногда нужно срочно переключиться на другую ветку, но гит не даст этого сделать, если в текущей ещё есть незаконченная работа и она конфликтует с целевой веткой. Он попросит сделать коммит, но можно пока его отложить и использовать команду stash. Команда позволяет временно сохранить изменения и убрать их из рабочей директории.
git stash также используется, когда нужно сделать git pull и не потерять текущие изменения, или применить cherry-pick. Если нужно задать имя для сохранения, то пишем команду:
git stash push -m "message"
git stash pop – достаёт и удаляет последнее сохранение из списка
git stash list – показывает список сохранений
git stash apply – применяет последнее сохранение без удаления из списка
git stash apply stash@{1} – применяет конкретные изменения
git stash drop – удаляет последнее сохранение из истории
git stash clear – очищает весь список
git stash branch my-temp-branch – создаёт новую ветку из stash и применяет изменения туда. Удобно, если stash оказался большим.
Я попробовал создать ситуацию, где stash бы пригодился: сделал изменения в файле в одной ветке и закоммиттил, а потом перешёл в другую ветку, сделал изменения в том же файле, но не стал пока добавлять их в staging зону. И после этого меня уже не пустили обратно, поэтому я сделал stash.
error: Your local changes to the following files would be overwritten by checkout:newfile.txtPlease commit your changes or stash them before you switch branches.AbortingC:\Users\ezimo\Desktop\git practice>git stashSaved working directory and index state WIP on stash-test-branch: 89309 Initial file changeC:\Users\ezimo\Desktop\git practice>git switch mainSwitched to branch 'main'
Важно понимать, что stash не привязан к конкретной ветке. Его можно достать в любом месте, можно доставать без удаления даже несколько раз. Тут я попытался это сделать и получил конфликт. Решается он ручным редактированием файла и пометкой конфликта решённым с помощью git add. Никакого –continue здесь нет, как было у rebase или cherry-pick. Можно сразу коммиттить или продолжать работу.
C:\Users\ezimo\Desktop\git practice>git stash popAuto-merging newfile.txtCONFLICT (content): Merge conflict in newfile.txtThe stash entry is kept in case you need it again.C:\Users\ezimo\Desktop\git practice>git add "newfile.txt"
В этой части я посмотрел на основные сценарии работы с ветками: их слияние, переписывание истории, разрешение конфликтов. Если доберусь до следующей части, там будут более продвинутые концепции работы с деревом коммитов.