Перейти к основному содержимому
08ФУНДАМЕНТ · ГЛАВА 08Git: push, PR и командная работа

Git: push, PR и командная работа

Прочитано 0%Квизы 0/8Тренажёры 0/5Уровень 1 · Новичок

Зачем это нужно

Всё, что ты делал в прошлых двух главах — git init, git add, git commit, ветки, слияния, — происходило только на твоём диске. Это надёжная история изменений, но она пока живёт в одном месте. У этого две проблемы. Первая — сохранность: сгорел блок питания, слетела система, украли ноутбук — и весь репозиторий, вся история коммитов исчезает безвозвратно вместе с ним, потому что копии не было нигде больше. Вторая — одиночество: наставник не может посмотреть твой код, пока тот не покинул твой компьютер, и напарник по команде не может ни увидеть твои изменения, ни добавить свои. Обе проблемы решает удалённый репозиторий (remote) — точная копия репозитория на сервере в интернете, с которой локальный репозиторий умеет обмениваться коммитами в обе стороны.

Для тебя это не абстрактная тема «на будущее». На чемпионате «Профессионалы» и в практикумах платформы сдача работы буквально и есть отправка коммитов на сервер: пока код лежит только у тебя на диске, для проверяющего задания не существует, сколько бы гениального кода там ни было. А в командных модулях через общий удалённый репозиторий идёт вся совместная работа — и именно здесь на сцену выходит самая опасная команда во всём Git, --force, которая умеет тихо и безвозвратно стереть чужую работу, если пользоваться ею не глядя. В этой главе, как и в двух предыдущих, каждая команда и весь вывод терминала — не пересказ по памяти, а дословный результат настоящего Git, полученный специально для этой главы: два «компьютера» двух участников команды и «сервер» между ними были собраны прямо на этом компьютере, чтобы увидеть по шагам, что происходит на самом деле, когда два человека пушат в один репозиторий.

origin: сервер под привычным именем

У локального репозитория может быть несколько удалённых — remote — но чаще всего он один, и по многолетней традиции Git-сообщества его называют origin («источник», «откуда пришло»). Это просто имя-ярлык, а не зарезервированное слово: под ним скрывается настоящий адрес сервера, который можно посмотреть в любой момент.

$ git pushтвой компьютер · localсервер · origin (GitHub)git pushgit pull / fetch
Local и origin: git push отправляет твои коммиты на сервер, git pull / fetch забирает оттуда чужие.
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).

Отвечено вопросов: 0 из 4
Что означают две строки в выводе git remote -v — одна с (fetch), другая с (push)?
Чем bare-репозиторий отличается от обычного?
git clone server-project.git ivan вывел warning: You appear to have cloned an empty repository. Это ошибка?
Что Git делает автоматически во время git clone, помимо копирования истории?
Отвечено верно: 0 из 4

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 — потренироваться дополнительно, помимо практикума.

GIT CLONE ПРОТИВ FORKgit cloneчужой репозиторийтвой дискpush только если есть право записиFork → git cloneчужой репозиторийForkтвой форк на GitHubpush всегда — это твой репозиторий
git clone копирует чужой репозиторий сразу на диск; Fork сначала создаёт твою копию на GitHub, и уже её клонируешь — зато push работает без вопросов.

А сами практикумы платформы выдаются проще, и форк для них не нужен: наставник заранее создаёт тебе лично приватный репозиторий вида 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 — нет. Разберём эту ситуацию подробно в разделе «Типичные ошибки» чуть ниже — там же и её исправление.

Отвечено вопросов: 0 из 4
Чем git clone отличается от Fork на GitHub?
Что именно запоминает флаг -u в git push -u origin main?
После git clone ветка main уже отслеживает origin/main, а свежесозданная git switch -c feature-login — нет. Почему?
Что предлагает сделать Git в сообщении fatal: ...has no upstream branch?
Отвечено верно: 0 из 4

pull = fetch + merge: что происходит на самом деле

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

GIT PULL = FETCH + MERGEorigin (сервер)1 · git fetchorigin/main обновиласьрабочие файлыпока не тронуты2 · git mergemain и рабочие файлы обновились
git fetch только обновляет служебную origin/main, рабочие файлы не трогает; переносит их в текущую ветку уже следующий шаг, git merge.

Пока Анна ничего не делала, Иван успел закоммитить и отправить ещё одну правку:

# у Ивана:
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 для этого слишком тороплив: он вливает всё сразу.

А теперь то же самое — не текстом, а картинкой: прокликай оба сценария по шагам и посмотри, какой указатель двигает каждая команда.

Локальный репозиторий
ABmainorigin/main
origin (сервер)
ABCmain
исходное состояние
origin уехал вперёд: коллега запушил коммит C. Локально о нём никто не знает — и main, и origin/main всё ещё стоят на B.
Шаг 1 из 3
Отвечено вопросов: 0 из 4
После git fetch содержимое файла report.md на диске у Анны изменилось?
git status после fetch написал: Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded. Что значит вторая часть фразы?
Вывод git pull состоит из блока From ... -> origin/main и блока Updating ... Fast-forward .... Откуда взялись эти два блока?
В каком случае имеет смысл делать fetch и merge раздельно, а не просто git pull?
Отвечено верно: 0 из 4
Квест «Нажми «Коммит коллеги», получи rejected при git push и почини рассинхрон через git pull»: слей ветку командой git merge
Слева — твой локальный репозиторий, справа — origin. Доступны git push и git pull. Кнопка «Коммит коллеги» двигает origin вперёд — так возникает рассинхрон. Список команд — help.
project (main) $
Команды: git config · init · status · add · commit -m · diff · log · branch · switch · merge · restore · push · pull · edit <файл> <текст> — изменить файл · help · clear
Файлы проекта
README.mdзакоммичен
Проект PGK
local
первый коммитb41c6e2HEAD → main
origin
первый коммитb41c6e2HEAD → main
Как это устроено под капотом
Внутри — не настоящий git, а его модель в памяти: коммит — объект с хешем, сообщением, списком родителей и полным снимком файлов, а ветка — просто указатель на хеш коммита. Команды меняют этот граф объектов, при этом формулировки вывода сняты с настоящего git 2.x, чтобы глаз привыкал к реальному терминалу. Граф справа рисуется по тем же объектам: SVG-кружки — коммиты, линии — связи «родитель — потомок», а merge с расхождением создаёт коммит с двумя родителями, ровно как в настоящем git. Push и pull в режиме с origin — упрощённая пересылка недостающих коммитов между двумя такими репозиториями.

Pull Request: код проверяют до слияния

В командах не принято отправлять коммиты прямо в main — один неудачный коммит сломал бы проект сразу всем. Вместо этого работу ведут в отдельной ветке, отправляют её на сервер и открывают pull request (PR) — предложение «влейте мою ветку в main». Это не команда Git, а функция самого GitHub, целиком живущая в браузере.

Руки над клавиатурой ноутбука, на экране — код
Командная работа — это когда твой код читают другие: PR и ревью до слияния.Фото: Pexels, pexels.com/photo/574071 — свободная лицензия Pexels, атрибуция не требуется

Продолжим пример: Анна завела ветку 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

  1. Жёлтая плашка «Compare & pull request». Как только GitHub видит свежую ветку без открытого PR, на главной странице репозитория (и на странице самой ветки) сам предлагает эту кнопку — искать в меню ничего не нужно.
  2. Экран создания PR. Две выпадающие вкладки сверху — base (куда вливаем, обычно main) и compare (что вливаем, здесь anna-conclusions). Ниже — заголовок и текст описания: сюда пишут, что сделано и зачем, иногда со ссылкой на задачу. Есть отдельная кнопка Create pull request и рядом — вариант оформить его как черновик (draft pull request): такой PR нельзя слить, пока его не переключат в статус «Готов к проверке» — удобно, чтобы показать работу в процессе, не запрашивая ревью раньше времени.
  3. Открытый 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 спрятаны три проблемы — найди их кликом по строкам.

Найдено: 0 из 3 · Промахи: 0
Кликай по строкам, в которых видишь проблему.

Слияние: три способа нажать одну кнопку

Когда замечаний больше нет, в самом низу PR появляется кнопка Merge pull request — а рядом с ней стрелка с выбором одного из трёх способов:

  • Create a merge commit — обычное слияние, ровно то, что git merge делал в прошлой главе: создаётся merge-коммит с двумя родителями, вся история ветки видна целиком.
  • Squash and merge — сжимает все коммиты ветки в один-единственный, с заголовком PR как сообщением; удобно, когда в ветке было много промежуточных «wip»-коммитов, а в общей истории main хочется видеть только итог.
  • Rebase and merge — переносит коммиты ветки на верхушку main по одному, без merge-коммита вовсе, будто их изначально писали прямо в main подряд.

Выбор обычно задаёт правило команды, а не каждый автор PR по отдельности — единый стиль истории читается проще, чем смесь всех трёх.

Отвечено вопросов: 0 из 4
Pull request — это команда Git или функция GitHub?
Чем draft pull request отличается от обычного?
На вкладке Files changed ревьюер выбрал Request changes. Что это значит для дальнейшей судьбы PR?
Автору PR попросили поправить одну строку кода. Нужно ли открывать новый pull request после исправления?
Отвечено верно: 0 из 4

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 совершенно безопасен.

Отвечено вопросов: 0 из 4
После git rebase origin/main хеш коммита Анны изменился с 2cb7024 на 62b2dd1. Значит ли это, что содержимое коммита испортилось?
Почему нельзя просто взять и rebase-нуть ветку, которую уже отправили на сервер, а кто-то другой успел её к себе забрать?
В выводе git rebase появилось Successfully rebased and updated refs/heads/anna-conclusions. Что здесь обновилось?
Когда rebase считается безопасным по общему правилу этого раздела?
Отвечено верно: 0 из 4

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. При этом Анна остаётся честно указана автором строки, которую придумала она, — только сам коммит теперь существует в двух местах истории сразу, с разными хешами, но одинаковым содержимым и одинаковым автором.

Отвечено вопросов: 0 из 4
Чем cherry-pick одного коммита отличается от merge всей ветки?
После git cherry-pick 62b2dd1 в выводе значится Author: Anna Sokolova, хотя команду запустил Иван. Почему?
Коммит 62b2dd1 у Анны и коммит a67902c у Ивана после cherry-pick — это один и тот же коммит с двух точек зрения?
В каком сценарии cherry-pick подходит лучше, чем ждать, пока весь pull request будет готов и его смогут слить?
Отвечено верно: 0 из 4

Типичные ошибки

Пять ситуаций, с которыми рано или поздно столкнётся каждый, кто работает с удалённым репозиторием. Тексты — снова дословный, живьём проверенный вывод настоящего 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 не почистил объект окончательно.

Отвечено вопросов: 0 из 4
git push отклонён с текстом ! [rejected] main -> main (fetch first). Что означает часть "(fetch first)"?
Почему git push --force в примере с абзацем Ивана не показал вообще никакого предупреждения о том, что чужой коммит пропадёт?
Чем git push --force-with-lease отличается от голого --force в примере с "(stale info)"?
После detached HEAD и коммита 9e32f88 Git предупредил и ты всё равно переключился на main, не создав ветку. Коммит удалён навсегда?
Отвечено верно: 0 из 4

Символы и команды главы

Команда / символЧто делаетПример
git remote -vпоказывает адреса удалённого репозитория (fetch и push)git remote -v
git clone <адрес>скачивает репозиторий целиком: код, ветки, историю; настраивает origingit 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 pullfetch и 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форсит, только если сервер не менялся с последнего fetchgit push --force-with-lease
detached HEADHEAD указывает прямо на коммит, в обход веткиgit checkout <hash>

Слова главы

remote
1 / 9

Потренируйся печатать

Цель: скорость ≥ 140 зн/мин, точность ≥ 90%

git push -u origin main
клавиша ``клавиша 11клавиша 22клавиша 33клавиша 44клавиша 55клавиша 66клавиша 77клавиша 88клавиша 99клавиша 00клавиша --клавиша ==клавиша ⌫клавиша TabTabклавиша QQклавиша WWклавиша EEклавиша RRклавиша TTклавиша YYклавиша UUклавиша IIклавиша OOклавиша PPклавиша [[клавиша ]]клавиша \\клавиша Caps LockCaps Lockклавиша AAклавиша SSклавиша DDклавиша FFклавиша GGклавиша HHклавиша JJклавиша KKклавиша LLклавиша ;;клавиша ''клавиша EnterEnterклавиша ShiftShiftклавиша ZZклавиша XXклавиша CCклавиша VVклавиша BBклавиша NNклавиша MMклавиша ,,клавиша ..клавиша //клавиша ShiftShiftклавиша CtrlCtrlклавиша WinWinклавиша AltAltклавиша ПробелПробелклавиша AltAltклавиша WinWinклавиша MenuMenuклавиша CtrlCtrl
следующая: G — указательный левой руки

Более длинная связка — 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 за последние недели, включая хеши, которые уже ни одна ветка не показывает напрямую, — по нему почти всегда можно найти и вернуть то, что казалось потерянным. Тема отдельного, более глубокого разбора, но знать, что эта страховка вообще существует, стоит уже сейчас.

Дальше по теме:

→ Отдельная тема: Rebase мастерски — золотое правило переписывания истории, интерактивный режим и страховка через reflog.

Куда дальше: проверенные ресурсы

Челлендж ⭐

Ступень 1. Пройди полный путь «получил чужой проект — отправил свою работу» на учебном шаблоне платформы. Это тренировка связки fork → clone → branch → push на безопасной копии, а не сдача практикума: настоящий практикум наставник выдаёт личным репозиторием pgk-champs/<спринт>-<логин>, и форк там не нужен.

  1. Открой на GitHub репозиторий track-mobile-01-kotlin-basics в организации pgk-champs и нажми Fork — своя личная копия появится под твоим аккаунтом.
  2. Клонируй свой форк (не оригинал!) на компьютер.
  3. Создай ветку feature/<твоя-фамилия> латиницей.
  4. Добавь в неё коммит — например, файл about.txt с твоим именем и группой.
  5. Отправь ветку командой git push -u origin feature/<твоя-фамилия> и убедись в браузере, что она появилась в твоём форке, а сверху предлагается Compare & pull request.

Ступень 2 ⭐. Стань на минуту собственной командой из двух человек — и на практике проверь, чем git pull лучше слепого --force, без единого сетевого запроса, только на своём диске.

  1. Заведи bare-репозиторий team-drill.git и склонируй его дважды — в папки me и partner (это твои «два компьютера»).
  2. Из me: сделай коммит в notes.txt и git push -u origin main.
  3. Из partner: забери ветку с сервера (git fetch, затем git switch main — клон делался, когда main на сервере ещё не было, поэтому ветку нужно взять явно), затем сделай свой коммит в ту же строку notes.txt — но не отправляй его.
  4. Вернись в me: сделай ещё один коммит в ту же строку и отправь (git push).
  5. Вернись в 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 — вход прямо в виджете выше.