Git: push, PR и командная работа
Зачем это нужно
Всё, что ты делал в прошлых двух главах — git init, git add, git commit, ветки, слияния, — происходило только на твоём диске. Это надёжная история изменений, но она пока живёт в одном месте. У этого две проблемы. Первая — сохранность: сгорел блок питания, слетела система, украли ноутбук — и весь репозиторий, вся история коммитов исчезает безвозвратно вместе с ним, потому что копии не было нигде больше. Вторая — одиночество: наставник не может посмотреть твой код, пока тот не покинул твой компьютер, и напарник по команде не может ни увидеть твои изменения, ни добавить свои. Обе проблемы решает удалённый репозиторий (remote) — точная копия репозитория на сервере в интернете, с которой локальный репозиторий умеет обмениваться коммитами в обе стороны.
Для тебя это не абстрактная тема «на будущее». На чемпионате «Профессионалы» и в практикумах платформы сдача работы буквально и есть отправка коммитов на сервер: пока код лежит только у тебя на диске, для проверяющего задания не существует, сколько бы гениального кода там ни было. А в командных модулях через общий удалённый репозиторий идёт вся совместная работа — и именно здесь на сцену выходит самая опасная команда во всём Git, --force, которая умеет тихо и безвозвратно стереть чужую работу, если пользоваться ею не глядя. В этой главе, как и в двух предыдущих, каждая команда и весь вывод терминала — не пересказ по памяти, а дословный результат настоящего Git, полученный специально для этой главы: два «компьютера» двух участников команды и «сервер» между ними были собраны прямо на этом компьютере, чтобы увидеть по шагам, что происходит на самом деле, когда два человека пушат в один репозиторий.
- Зачем это нужно
- origin: сервер под привычным именем
- clone vs fork: два разных способа получить чужой код
- push -u: что делает флаг -u
- pull = fetch + merge: что происходит на самом деле
- Pull Request: код проверяют до слияния
- rebase: переписывание истории простыми словами
- cherry-pick: перенести один конкретный коммит
- Типичные ошибки
- Символы и команды главы
- Слова главы
- Потренируйся печатать
- Куда дальше: проверенные ресурсы
- Челлендж ⭐
- Что должен уметь
origin: сервер под привычным именем
У локального репозитория может быть несколько удалённых — remote — но чаще всего он один, и по многолетней традиции Git-сообщества его называют origin («источник», «откуда пришло»). Это просто имя-ярлык, а не зарезервированное слово: под ним скрывается настоящий адрес сервера, который можно посмотреть в любой момент.
git remote -v
origin /home/ivan/server-project.git (fetch)
origin /home/ivan/server-project.git (push)
Две строки — потому что у remote отдельно хранится адрес для скачивания (fetch) и отдельно для отправки (push); почти всегда это один и тот же адрес, но в редких случаях их можно развести. -v здесь значит verbose — «подробно», без флага команда вывела бы только имя origin без адреса.
Что физически лежит на сервере. На настоящем GitHub, как и в примерах этой главы, на «той стороне» находится не обычный репозиторий с рабочими файлами, а bare-репозиторий («голый») — только содержимое папки .git: объекты коммитов, ветки, теги, без единого файла проекта на виду. Смысл в том, что у сервера просто нет причин держать рабочую копию — с ней никто не сидит и не редактирует файлы прямо там, серверу нужна лишь база данных истории, которой обмениваются клиенты. Собственно, каждый твой обычный (не bare) репозиторий — это ровно та же база данных плюс рабочая копия файлов поверх неё; убери рабочую копию — и получится в точности то, что хранит GitHub.
Дальше в этой главе роль GitHub целиком играет ровно такой bare-репозиторий на этом же компьютере — server-project.git — а роль двух участников команды играют два обычных клона рядом с ним, ivan/ и anna/. Все команды и весь вывод ниже настоящие: единственное, что заменено ради читаемости, — путь к серверу показан как ~/server-project.git вместо длинного пути во временной папке; на боевом GitHub на этом месте был бы адрес вида git@github.com:pgk-champs/team-report.git.
git clone: скачать репозиторий целиком
git clone server-project.git ivan
Cloning into 'ivan'...
warning: You appear to have cloned an empty repository.
done.
«Клонирую в ivan»… «похоже, ты склонировал пустой репозиторий»… «готово». Это не ошибка — предупреждение: в server-project.git пока действительно нет ни одного коммита, мы создали его этой же минутой. clone скачал репозиторий целиком: код, все ветки, всю историю коммитов — просто скачивать пока было нечего. Заодно, за кулисами, Git сам прописал адрес сервера как origin — это видно как раз в выводе git remote -v чуть выше.
Второй аргумент, ivan, — необязательный: имя папки, куда положить результат. Не укажешь — Git сам возьмёт имя репозитория из адреса (для server-project.git это была бы папка server-project).
clone vs fork: два разных способа получить чужой код
git clone скачивает репозиторий, но не даёт прав его менять на сервере — это два разных вопроса. Если ты состоишь в команде и у тебя уже есть право на запись (write access) в pgk-champs/team-report, обычный git clone — это всё, что нужно: клонировал, закоммитил, git push отправит изменения прямо туда.
Но что делать, если репозиторий чужой — например, открытый учебный шаблон чемпионата, — и прав на запись у тебя нет и не будет? Прямой git push в такой репозиторий Git-сервер просто отклонит: «доступ запрещён». Здесь и нужен форк (fork) — не команда Git, а функция самого GitHub. Кнопка Fork в правом верхнем углу страницы репозитория создаёт твою личную копию этого репозитория прямо на GitHub, под твоим аккаунтом — github.com/<твой-логин>/team-report — и вот в неё у тебя уже есть полное право писать, потому что это теперь твой собственный репозиторий. Дальше как обычно: клонируешь свой форк (не оригинал!) и работаешь в нём.
git clone чужого репозитория | Fork → git clone своего форка | |
|---|---|---|
| Что происходит | Код скачивается к тебе на диск | На GitHub появляется отдельная копия репозитория под твоим аккаунтом, и уже её ты клонируешь |
| Где хранится | Только локально, у тебя на диске | И у тебя на диске, и на GitHub — как отдельный, твой репозиторий |
Можно ли git push | Только если у тебя есть право записи в оригинал | Всегда — в свой форк, это твой собственный репозиторий |
| Как вернуть изменения в оригинал | Не нужно (ты и так пишешь прямо туда) | Через pull request — предложение «влейте мои изменения», разберём дальше в этой главе |
Форк — это про любой чужой открытый репозиторий: увидел интересный проект, нажал Fork, получил свою копию с правом записи и экспериментируешь сколько угодно. Именно так стоит брать себе и учебные шаблоны заданий из организации pgk-champs — потренироваться дополнительно, помимо практикума.
А сами практикумы платформы выдаются проще, и форк для них не нужен: наставник заранее создаёт тебе лично приватный репозиторий вида pgk-champs/<спринт>-<логин> (например, pgk-champs/sprint1-ivanov) из шаблона задания и сразу выдаёт тебе право записи в него. На почту приходит приглашение стать соавтором (collaborator) — принимаешь его, клонируешь свой репозиторий и коммитишь прямо в него: право на git push у тебя уже есть с первой минуты. Если такого репозитория у тебя ещё нет — не форкай шаблон «на всякий случай», а напиши наставнику: репозиторий выдаёт он.
push -u: что делает флаг -u
Возвращаемся к паре ivan/server-project.git. У Ивана есть первый коммит, пора отправить его на сервер.
git push -u origin main
To ~/server-project.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
Первая строка после To — куда отправляли. * [new branch] main -> main — на сервере создалась новая ветка main, до этого её там не было вовсе. Третья строка — ровно то, что делает флаг -u: «ветка main настроена отслеживать origin/main». С этого момента Git знает, куда отправлять и откуда забирать по умолчанию, и вторая и все следующие команды в этой ветке можно писать просто как git push и git pull, без явного origin main каждый раз.
Важная тонкость: не каждая ветка нуждается в -u. Ветку, которую ты получил вместе с git clone (обычно main), Git сам связывает с origin в момент клонирования — это видно ещё до первого коммита, если заглянуть в конфиг:
git config --list | grep branch
branch.main.remote=origin
branch.main.merge=refs/heads/main
Связка уже здесь, хотя мы ещё не сделали ни одного push. А вот ветку, которую ты сам завёл командой git switch -c, никто ни с чем не связывал — Git понятия не имеет, где ей место на сервере, пока ты не скажешь явно. Проверим на практике: заведём новую ветку и отправим её без -u.
git switch -c feature-login
echo "экран входа: черновик" > login.txt
git add login.txt
git commit -m "черновик экрана входа"
git push
fatal: The current branch feature-login has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin feature-login
To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.
Ровно то расхождение, о котором предупреждали: у main upstream был готов заранее, у feature-login — нет. Разберём эту ситуацию подробно в разделе «Типичные ошибки» чуть ниже — там же и её исправление.
pull = fetch + merge: что происходит на самом деле
git pull выглядит как одна команда, но внутри — ровно две последовательные операции, каждую из которых можно выполнить и по отдельности. Разберём их порознь, на примере Анны — второй участницы команды, которая клонировала тот же server-project.git уже после первого коммита Ивана.
Пока Анна ничего не делала, Иван успел закоммитить и отправить ещё одну правку:
# у Ивана:
git commit -am "Добавил раздел Итоги недели"
git push
To ~/server-project.git
53e837a..7bb01e1 main -> main
Шаг 1 — git fetch: скачать, но не трогать рабочую копию
# у Анны:
git fetch
From ~/server-project
53e837a..7bb01e1 main -> origin/main
Git скачал новый коммит — но давай проверим, что реально изменилось у Анны на диске:
git log --oneline
cat report.md
53e837a Начальный отчёт
# Отчёт команды
Статус: черновик
Ничего. Ни локальная ветка main, ни файл report.md не сдвинулись ни на байт. fetch обновил только служебную «фотографию» состояния сервера — ветку origin/main, — но твоей собственной ветки main и рабочих файлов не коснулся вовсе. Убедиться в этом можно ещё и через git status:
git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
Шаг 2 — git merge: перенести скачанное в свою ветку
git merge origin/main
Updating 53e837a..7bb01e1
Fast-forward
report.md | 1 +
1 file changed, 1 insertion(+)
Вот теперь файл на диске у Анны действительно обновился. merge слил origin/main — ту самую фотографию, что подтянул fetch, — в текущую ветку. Раз локальная ветка не успела уйти в сторону собственными коммитами, слияние прошло перемоткой (fast-forward) — той же самой операцией, что уже разбирали в прошлой главе про ветки, только на этот раз сливали не локальную ветку с локальной, а origin/main с main.
git pull — то же самое одной командой
Иван снова закоммитил и отправил третью правку. Теперь Анна делает то же самое, что и в двух шагах выше, но одной командой:
git pull
From ~/server-project
7bb01e1..ef6db91 main -> origin/main
Updating 7bb01e1..ef6db91
Fast-forward
report.md | 1 +
1 file changed, 1 insertion(+)
Присмотрись к структуре вывода — она в точности склеена из двух кусков, что мы только что видели порознь: сверху блок From ... -> origin/main, слово в слово повторяющий вывод git fetch, снизу блок Updating ... Fast-forward ..., слово в слово повторяющий вывод git merge. git pull в буквальном смысле запускает fetch, а следом — merge с тем, что скачал. Это не метафора и не упрощение для новичков — это ровно так и реализовано внутри Git.
Зачем вообще делать их порознь, если есть pull одной командой? Иногда полезно посмотреть, что именно изменилось на сервере, прежде чем впускать это в свою рабочую ветку — например, командой git log main..origin/main сразу после fetch, до merge. pull для этого слишком тороплив: он вливает всё сразу.
А теперь то же самое — не текстом, а картинкой: прокликай оба сценария по шагам и посмотри, какой указатель двигает каждая команда.
Проект PGK
Как это устроено под капотом
Pull Request: код проверяют до слияния
В командах не принято отправлять коммиты прямо в main — один неудачный коммит сломал бы проект сразу всем. Вместо этого работу ведут в отдельной ветке, отправляют её на сервер и открывают pull request (PR) — предложение «влейте мою ветку в main». Это не команда Git, а функция самого GitHub, целиком живущая в браузере.

Продолжим пример: Анна завела ветку anna-conclusions, закоммитила в неё и опубликовала на сервере.
git switch -c anna-conclusions
echo "Раздел «Выводы»: добавлен" >> report.md
git commit -am "Добавил раздел Выводы"
git push -u origin anna-conclusions
To ~/server-project.git
* [new branch] anna-conclusions -> anna-conclusions
branch 'anna-conclusions' set up to track 'origin/anna-conclusions'.
Шаг за шагом: что видно на экране GitHub
- Жёлтая плашка «Compare & pull request». Как только GitHub видит свежую ветку без открытого PR, на главной странице репозитория (и на странице самой ветки) сам предлагает эту кнопку — искать в меню ничего не нужно.
- Экран создания PR. Две выпадающие вкладки сверху —
base(куда вливаем, обычноmain) иcompare(что вливаем, здесьanna-conclusions). Ниже — заголовок и текст описания: сюда пишут, что сделано и зачем, иногда со ссылкой на задачу. Есть отдельная кнопка Create pull request и рядом — вариант оформить его как черновик (draft pull request): такой PR нельзя слить, пока его не переключат в статус «Готов к проверке» — удобно, чтобы показать работу в процессе, не запрашивая ревью раньше времени. - Открытый PR — пять вкладок.
Conversation— общее обсуждение: описание, лента комментариев, статус проверок.Commits— список всех коммитов ветки по отдельности.Checks— результаты автоматических проверок (например, сборки и тестов из CI), если они настроены.Files changed— тот самый diff: построчно, что добавлено и что удалено, — здесь и происходит основное ревью. Пятая,Results, показывает предупреждения автоматического анализа кода, если он подключён.
Ревью: approve, request changes, comments
На вкладке Files changed наставник или напарник видит код построчно и может оставить комментарий прямо под конкретной строкой — не «в общем письме», а адресно, к месту. Собрав замечания, ревьюер завершает разбор одним из трёх вариантов:
- Comment — просто оставить комментарии без вердикта: «посмотрел, есть пара вопросов», не блокирует слияние.
- Approve — «одобряю»: явный зелёный свет на слияние.
- Request changes — «нужны правки»: PR помечается как требующий доработки, пока автор не внесёт изменения и ревьюер не снимет этот статус.
Автор PR правит код обычными коммитами в той же ветке и пушит их той же командой git push, которой отправлял ветку впервые — GitHub сам подтягивает новые коммиты в уже открытый PR, отдельно ничего пересоздавать не нужно.
Побудь теперь сам ревьюером: в этом фрагменте с вкладки Files changed спрятаны три проблемы — найди их кликом по строкам.
Слияние: три способа нажать одну кнопку
Когда замечаний больше нет, в самом низу PR появляется кнопка Merge pull request — а рядом с ней стрелка с выбором одного из трёх способов:
- Create a merge commit — обычное слияние, ровно то, что
git mergeделал в прошлой главе: создаётся merge-коммит с двумя родителями, вся история ветки видна целиком. - Squash and merge — сжимает все коммиты ветки в один-единственный, с заголовком PR как сообщением; удобно, когда в ветке было много промежуточных «wip»-коммитов, а в общей истории
mainхочется видеть только итог. - Rebase and merge — переносит коммиты ветки на верхушку
mainпо одному, без merge-коммита вовсе, будто их изначально писали прямо вmainподряд.
Выбор обычно задаёт правило команды, а не каждый автор PR по отдельности — единый стиль истории читается проще, чем смесь всех трёх.
rebase: переписывание истории простыми словами
Перебазирование (rebase) решает ту же задачу, что и merge, — объединить работу двух разошедшихся веток, — но действует иначе. Пока Анна работала над anna-conclusions, Иван добавил в main ещё один коммит:
# у Ивана:
echo "report.md" > .gitignore
git add .gitignore
git commit -m "Добавил gitignore"
git push
История Анны к этому моменту разошлась с main в двух направлениях:
# у Анны, после git fetch:
git log --oneline --graph --all
* 2cb7024 Добавил раздел Выводы
| * a52f194 Добавил gitignore
|/
* 9ab71e4 Добавил раздел Планы
* 373c9a5 Добавил раздел Итоги недели
* 34ad4bc Начальный отчёт
Обычный git merge origin/main здесь создал бы merge-коммит с двумя родителями — ровно как в прошлой главе. rebase поступает иначе: он перекладывает коммит Анны так, будто она начала писать его не от старой точки, а уже после свежего коммита Ивана.
git rebase origin/main
Rebasing (1/1)Successfully rebased and updated refs/heads/anna-conclusions.
git log --oneline --graph --all
* 62b2dd1 Добавил раздел Выводы
* a52f194 Добавил gitignore
* 9ab71e4 Добавил раздел Планы
* 373c9a5 Добавил раздел Итоги недели
* 34ad4bc Начальный отчёт
Линия снова прямая, никакой развилки — коммит Анны переехал на самый верх, поверх коммита Ивана про .gitignore. Но присмотрись к хешу: было 2cb7024, стало 62b2dd1. Это не тот же коммит, передвинутый по истории, — это новый коммит с тем же содержимым и сообщением, но с другим родителем, а значит, и с другим hash: rebase не двигает старые коммиты, а создаёт их копии в новом месте, а старые становятся ничьими и позже уберутся сборщиком мусора Git.
Отсюда главное правило rebase: никогда не перебазируй то, что уже отправлено на сервер и есть у других людей. Раз у переставленных коммитов новые хеши, для Git это буквально другие коммиты — если Анна уже отправила 2cb7024 на сервер и кто-то третий успел его к себе забрать, после её rebase и повторного push у них с этим человеком начнётся разная, несовместимая история одних и тех же по смыслу изменений. Официальная документация Git отдельно предупреждает об этом в разделе «Опасности перемещения»: не перемещай коммиты, уже отправленные в публичный репозиторий. Пока коммит существует только локально, как в примере выше, — rebase совершенно безопасен.
cherry-pick: перенести один конкретный коммит
Ветка целиком ещё не готова к слиянию, но иногда нужен только один коммит из неё — например, срочная правка, случайно оказавшаяся в середине черновой ветки. Именно для этого существует cherry-pick — «сорвать одну вишню», а не всю гроздь.
Ивану нужен только коммит Анны про раздел «Выводы» из её ветки anna-conclusions, прямо в main, не дожидаясь, пока весь PR пройдёт ревью:
git log --oneline origin/anna-conclusions
62b2dd1 Добавил раздел Выводы
a52f194 Добавил gitignore
9ab71e4 Добавил раздел Планы
373c9a5 Добавил раздел Итоги недели
34ad4bc Начальный отчёт
git cherry-pick 62b2dd1
[main a67902c] Добавил раздел Выводы
Author: Anna Sokolova <anna@example.com>
Date: Tue Sep 1 07:12:13 2026 +0400
1 file changed, 1 insertion(+)
Как и rebase, cherry-pick создал новый коммит: a67902c, а не 62b2dd1. Тот же самый принцип: у копии другой родитель (в main, а не в anna-conclusions), значит, другой hash. При этом Анна остаётся честно указана автором строки, которую придумала она, — только сам коммит теперь существует в двух местах истории сразу, с разными хешами, но одинаковым содержимым и одинаковым автором.
Типичные ошибки
Пять ситуаций, с которыми рано или поздно столкнётся каждый, кто работает с удалённым репозиторием. Тексты — снова дословный, живьём проверенный вывод настоящего Git, а не пересказ по памяти.
1. Забыл -u для новой ветки
git switch -c feature-login
git commit -am "черновик экрана входа"
git push
fatal: The current branch feature-login has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin feature-login
To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.
Перевод: «фатальная ошибка: у текущей ветки feature-login нет вышестоящей ветки (upstream). Чтобы отправить текущую ветку и установить remote как upstream, используй: git push --set-upstream origin feature-login. Чтобы это происходило автоматически для веток без отслеживаемого upstream, смотри push.autoSetupRemote в git help config». Причина: ветку, склонированную вместе с репозиторием, Git сам связывает с origin ещё при клонировании — а вот новую ветку, заведённую локально через switch -c, никто ни с чем не связывал. Как починить: сделать ровно то, что предлагает сообщение — git push -u origin feature-login (--set-upstream и -u — длинная и короткая форма одного и того же флага).
2. push отклонён: rejected non-fast-forward
Двое участников начали от одного и того же коммита. Первый успел отправить свою правку раньше:
# у Ивана — уже отправлено
Второй, не забрав чужие изменения, пробует отправить свою:
# у Анны, без предварительного pull:
git push
To ~/server-project.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to '~/server-project.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Перевод: «! [отклонено] main -> main (сначала fetch)». «Ошибка: не удалось отправить некоторые ссылки». «Подсказка: обновления отклонены, потому что на сервере есть работа, которой нет у тебя локально. Обычно это вызвано тем, что в ту же ссылку запушил кто-то другой. Если хочешь включить удалённые изменения — сделай git pull перед повторной отправкой». Причина: push в норме умеет только продолжать историю сервера вперёд (fast-forward) — а раз на сервере уже есть коммит, которого нет у Анны локально, продолжить нечем: истории разошлись. Как починить: сделать именно то, что советует подсказка, — git pull, и уже после этого повторить git push.
Почему нельзя просто дописать --force
Соблазн очевиден: git push --force заставит сервер принять версию Анны, что бы там ни было. И это сработает — вопрос в цене. Проверим на отдельном примере, что именно произойдёт: у Ивана в файле есть важный абзац, уже отправленный на сервер; Анна, не забрав его, пишет свою правку в ту же строку и форсит:
git push --force
To ~/team-report.git
+ 5da948c...62177c6 main -> main (forced update)
«Принудительное обновление» — сервер молча заменил свою историю версией Анны. Проверим, что осталось на сервере:
git show origin/main:report.txt
report v1
Anna's draft note
Абзаца Ивана больше нет — ни в файле, ни в истории коммитов сервера: коммит с его правкой не удалён откуда-то специально, он просто больше ни на одну ветку не указывает и для всех практических целей потерян. Никакого предупреждения при этом Git не покажет — --force буквально означает «мне не важно, что там на сервере, перезапиши как я скажу».
Более безопасная альтернатива — --force-with-lease. Она форсит только в том случае, если с момента твоего последнего fetch на сервере ничего не изменилось; если кто-то успел запушить в промежутке — откажется, как обычный push.
Проверим на практике:
git push --force-with-lease
To ~/team-report.git
! [rejected] main -> main (stale info)
«(stale info)» — «устаревшие сведения»: Git заметил, что то, что ты считаешь текущим состоянием сервера, уже не актуально, и отказался затирать чужую работу вслепую. Это не делает --force-with-lease полностью безопасным — если успеть сделать свежий fetch прямо перед этим, проверка пройдёт, — но она защищает именно от самого частого сценария: забыл, что кто-то запушил, форснул не глядя.
3. pull отказывается решать сам
Анна поступает правильно и делает git pull вместо --force — но получает ещё одно сообщение:
git pull
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.
Перевод: «у тебя разошедшиеся ветки, и нужно указать, как их согласовать... фатальная ошибка: нужно указать способ согласования разошедшихся веток». Причина: современный Git начиная с версии 2.x сознательно отказывается молча выбирать между merge и rebase, когда у локальной и удалённой веток разная история, — раньше он тихо делал merge по умолчанию, что новичков иногда удивляло задним числом. Как починить: указать способ явно прямо в команде — для повседневной работы обычно подходит слитие с созданием merge-коммита:
git pull --no-rebase
4. Конфликт при pull
После --no-rebase из предыдущего пункта Git пробует слить сам — но обе стороны успели поменять одну и ту же строку по-разному:
Auto-merging report.md
CONFLICT (content): Merge conflict in report.md
Automatic merge failed; fix conflicts and then commit the result.
Перевод: «автоматическое слияние main.py... КОНФЛИКТ (по содержимому): конфликт слияния в report.md. Автоматическое слияние не удалось; исправь конфликты и закоммить результат». Причина: та же самая, что уже разбирали в главе про ветки, — просто теперь конфликтующие версии пришли не из двух локальных веток, а по одной из них через сеть. Как починить: ровно тот же алгоритм — открыть файл, найти маркеры <<<<<<</=======/>>>>>>>, выбрать или объединить нужный текст, убрать маркеры целиком, затем git add и git commit (без -m — Git уже подготовил сообщение слияния) и, наконец, git push, чтобы отправить решённый конфликт обратно на сервер.
5. detached HEAD: потеря коммитов без ветки
git checkout 7bb01e1
Note: switching to '7bb01e1'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 7bb01e1 Добавил раздел Итоги недели
Перевод: «переключаюсь на 7bb01e1»... «ты в состоянии «отсоединённый HEAD». Можешь осматриваться, делать экспериментальные изменения и коммитить их, и можешь отбросить любые сделанные в этом состоянии коммиты, просто вернувшись на ветку, без последствий для веток». Причина: обычно HEAD (указатель «где я сейчас нахожусь») смотрит на ветку, а ветка — на коммит; здесь же HEAD указывает прямо на коммит 7bb01e1 в обход какой-либо ветки — Git специально предупреждает об этом заранее, до того как что-то случится. git status в этом состоянии тоже честно об этом сообщает:
git status
HEAD detached at 7bb01e1
nothing to commit, working tree clean
Само по себе detached HEAD не опасно — опасно то, что случится, если в этом состоянии закоммитить и потом просто уйти на ветку, забыв про предупреждение:
echo "experimental idea" > scratch.txt
git add scratch.txt
git commit -m "experimental idea, not on any branch"
git switch main
Warning: you are leaving 1 commit behind, not connected to
any of your branches:
9e32f88 experimental idea, not on any branch
If you want to keep it by creating a new branch, this may be a good time
to do so with:
git branch <new-branch-name> 9e32f88
Switched to branch 'main'
Перевод: «предупреждение: ты оставляешь позади 1 коммит, не связанный ни с одной из твоих веток... если хочешь сохранить его, создав новую ветку, сейчас — хороший момент сделать это командой git branch...». Причина: коммит 9e32f88 существует в базе .git, но ни одна ветка на него больше не указывает — обычным git log или git checkout main его уже не найти, хотя физически объект ещё какое-то время лежит на диске. Как починить: либо сделать это заранее — создать ветку прямо в момент коммита (git switch -c <имя> вместо голого checkout на hash), либо, если предупреждение уже проехало, — то самое git branch <имя> 9e32f88 из подсказки, пока Git не почистил объект окончательно.
Символы и команды главы
| Команда / символ | Что делает | Пример |
|---|---|---|
git remote -v | показывает адреса удалённого репозитория (fetch и push) | git remote -v |
git clone <адрес> | скачивает репозиторий целиком: код, ветки, историю; настраивает origin | git clone server-project.git |
| Fork (GitHub) | создаёт твою личную копию чужого репозитория на сервере — не команда Git | кнопка Fork в интерфейсе GitHub |
git push -u origin <ветка> | отправляет ветку и настраивает отслеживание origin ↔ ветка | git push -u origin main |
git fetch | скачивает новые коммиты в origin/<ветка>, не трогая рабочую копию | git fetch |
git merge origin/<ветка> | вливает скачанное fetch в текущую локальную ветку | git merge origin/main |
git pull | fetch и merge одной командой | git pull |
git pull --no-rebase | явно указать способ согласования — слиянием | git pull --no-rebase |
| Pull request (PR) | предложение влить ветку в main через ревью на GitHub | кнопка Create pull request |
| Approve / Request changes / Comment | три вердикта ревью на вкладке Files changed | результат ревью PR |
git rebase <база> | переставляет коммиты текущей ветки на новую базу, меняя их хеши | git rebase origin/main |
git cherry-pick <hash> | переносит один конкретный коммит в текущую ветку, с новым хешем | git cherry-pick 62b2dd1 |
! [rejected] ... (fetch first) | push отклонён — на сервере есть коммиты, которых нет локально | вывод git push |
--force | перезаписывает историю сервера без проверок — может стереть чужую работу | git push --force |
--force-with-lease | форсит, только если сервер не менялся с последнего fetch | git push --force-with-lease |
| detached HEAD | HEAD указывает прямо на коммит, в обход ветки | git checkout <hash> |
Слова главы
Потренируйся печатать
Цель: скорость ≥ 140 зн/мин, точность ≥ 90%
git push -u origin main
Более длинная связка — rebase и сразу push:
Цель: скорость ≥ 150 зн/мин, точность ≥ 90%
git rebase origin/main && git push
Углубиться
push.autoSetupRemote. Сообщение об отсутствующем upstream само подсказывало этот параметр конфига. Если включить его (git config --global push.autoSetupRemote true), первый git push для любой новой ветки сам создаст соответствующую ветку на сервере и настроит отслеживание — без явного -u. Многие опытные разработчики держат его выключенным намеренно: явный -u в первый раз — это маленькая защита от случайной отправки веток, которые вообще не должны были покидать твой диск.
Что именно проверяет --force-with-lease. Он сравнивает не содержимое файлов, а сохранённое у тебя локально представление о том, где сейчас находится origin/main (значение из последнего fetch) с тем, где origin/main находится на сервере на самом деле в момент push. Если кто-то успел подвинуть ветку в промежутке — Git видит расхождение и отказывается, как в примере с «(stale info)» выше. Отсюда практический вывод: --force-with-lease без свежего fetch перед ним — по-прежнему риск, просто меньший, чем голый --force.
git reflog — сеть на самый крайний случай. Даже коммит, ставший «сиротой» после detached HEAD (или случайно потерянный после rebase), какое-то время физически остаётся в .git. git reflog показывает журнал всех перемещений HEAD за последние недели, включая хеши, которые уже ни одна ветка не показывает напрямую, — по нему почти всегда можно найти и вернуть то, что казалось потерянным. Тема отдельного, более глубокого разбора, но знать, что эта страховка вообще существует, стоит уже сейчас.
Дальше по теме:
- git-push: раздел про --force-with-lease — официальное описание всех вариантов принудительной отправки
- git-rebase: официальная документация — полный список флагов, включая интерактивный режим
-i, не затронутый в этой главе
→ Отдельная тема: Rebase мастерски — золотое правило переписывания истории, интерактивный режим и страховка через reflog.
Куда дальше: проверенные ресурсы
- Pro Git: Основы — Работа с удалёнными репозиториями — remote, fetch, push и pull в официальной книге Git на русском.
- Pro Git: Перебазирование — rebase подробно, включая раздел «Опасности перемещения», процитированный в этой главе.
- GitHub Docs: О запросах на вытягивание — как устроен pull request с точки зрения GitHub, включая draft PR.
- GitHub Docs: Об обзорах пул-реквестов — Comment, Approve, Request changes подробно.
- GitHub Docs: Слияние pull request — три способа слияния (merge commit, squash, rebase) с точки зрения кнопки Merge.
- Skillfactory: Pull Request в GitHub — дружелюбный разбор PR для тех, кто впервые открывает этот экран.
-
git-push — man-страница отправки, включая все варианты force.
-
git-cherry-pick — man-страница переноса отдельных коммитов.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Челлендж ⭐
Ступень 1. Пройди полный путь «получил чужой проект — отправил свою работу» на учебном шаблоне платформы. Это тренировка связки fork → clone → branch → push на безопасной копии, а не сдача практикума: настоящий практикум наставник выдаёт личным репозиторием pgk-champs/<спринт>-<логин>, и форк там не нужен.
- Открой на GitHub репозиторий
track-mobile-01-kotlin-basicsв организацииpgk-champsи нажми Fork — своя личная копия появится под твоим аккаунтом. - Клонируй свой форк (не оригинал!) на компьютер.
- Создай ветку
feature/<твоя-фамилия>латиницей. - Добавь в неё коммит — например, файл
about.txtс твоим именем и группой. - Отправь ветку командой
git push -u origin feature/<твоя-фамилия>и убедись в браузере, что она появилась в твоём форке, а сверху предлагается Compare & pull request.
Ступень 2 ⭐. Стань на минуту собственной командой из двух человек — и на практике проверь, чем git pull лучше слепого --force, без единого сетевого запроса, только на своём диске.
- Заведи bare-репозиторий
team-drill.gitи склонируй его дважды — в папкиmeиpartner(это твои «два компьютера»). - Из
me: сделай коммит вnotes.txtиgit push -u origin main. - Из
partner: забери ветку с сервера (git fetch, затемgit switch main— клон делался, когдаmainна сервере ещё не было, поэтому ветку нужно взять явно), затем сделай свой коммит в ту же строкуnotes.txt— но не отправляй его. - Вернись в
me: сделай ещё один коммит в ту же строку и отправь (git push). - Вернись в
partner: попробуйgit push— должен появиться! [rejected] ... (fetch first). Не форси. Сделайgit pull, реши конфликт (если он возникнет), заверши слияние иgit pushснова.
Проверка: git log --oneline --graph в обеих папках после всего показывает оба коммита — и из me, и из partner, — ни один не потерян.
Решение (сначала попробуй сам)
Ступень 1 — после Fork на GitHub, в терминале (замени <логин> и фамилию на свои):
git clone https://github.com/<логин>/track-mobile-01-kotlin-basics.git
cd track-mobile-01-kotlin-basics
git switch -c feature/ivanov
echo "Иванов Иван, группа ИС-11" > about.txt
git add about.txt
git commit -m "Добавил файл о себе"
git push -u origin feature/ivanov
Ступень 2 ⭐:
mkdir team-drill.git && (cd team-drill.git && git init --bare)
git clone team-drill.git me
git clone team-drill.git partner
cd me
git config user.name "Я"; git config user.email "me@example.com"
git symbolic-ref HEAD refs/heads/main
echo "строка версии 1" > notes.txt
git add notes.txt
git commit -m "начальная версия"
git push -u origin main
cd ../partner
git config user.name "Напарник"; git config user.email "partner@example.com"
# клон делался из ещё пустого team-drill.git, поэтому ветки main тут пока нет:
git fetch origin # * [new branch] main -> origin/main
git switch main # Switched to a new branch 'main' + связка с origin/main
echo "правка напарника" > notes.txt
git commit -am "правка напарника"
# пока НЕ пушим
cd ../me
echo "моя правка" > notes.txt
git commit -am "моя правка"
git push
cd ../partner
git push
# ! [rejected] main -> main (fetch first) — именно этого мы и ждали
git pull --no-rebase
# скорее всего CONFLICT — открой notes.txt, оставь одну версию строки, убери маркеры
git add notes.txt
git commit -m "слил обе правки"
git push
cd ../me
git pull # Fast-forward: забираем слияние напарника к себе
git log --oneline --graph в любой из папок покажет оба коммита-правки в общей истории — потерь нет, хотя без --force пушить «через голову» друг друга и не получилось бы с первой попытки.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Что должен уметь
- Объяснять, что такое remote и почему его обычно зовут
origin, и читать выводgit remote -v. - Объяснять разницу между
git cloneчужого репозитория и Fork на GitHub — и когда нужен именно форк. - Формулировать, что делает флаг
-uуgit push, и почему ветке, полученной черезgit clone, он для первого пуша не нужен, а свежесозданной локальной ветке — нужен. - Раскладывать
git pullнаgit fetch+git mergeи объяснять, что меняется на диске после каждой из двух половин по отдельности. - Проходить создание pull request на GitHub по шагам: base/compare, черновик, пять вкладок открытого PR, три вердикта ревью, три способа слияния.
- Объяснять простыми словами, что делает
rebase: почему у переставленных коммитов новый hash и почему нельзя перебазировать уже отправленную и скачанную другими историю. - Использовать
cherry-pickдля переноса одного конкретного коммита и объяснять, что при этом происходит с автором и с hash. - Узнавать по точному тексту пять типичных ситуаций — отсутствующий upstream,
rejected (fetch first), разошедшиеся ветки при pull, конфликт при pull, detached HEAD — и знать, что делать в каждой, включая то, почему--force— не решение по умолчанию и чем--force-with-leaseбезопаснее голого--force.
Комментарии
Комментарии появятся после настройки. Нужен аккаунт GitHub — вход прямо в виджете выше.