Теги, релизы, archive и bundle

Рейтинг: 70.1% · 9 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

Теги, релизы, archive и bundle

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle (вы здесь)
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Зачем вообще нужен git tag, если есть коммиты

Представь: вышла версия 1.4.0, прод горит, тебе пишут "откатись на тот релиз". А у тебя в истории три тысячи коммитов с хешами вида a3f9c21. Какой из них был релизом? Без меток ты будешь искать по дате сборки, по чату, по памяти - и ошибёшься. Тег решает ровно эту боль: ты прибиваешь к нужному коммиту читаемое имя v1.4.0, и теперь "откатись на релиз" - это git checkout v1.4.0, а не археология по reflog.

Ветка тоже именованная метка, но ветка движется: сделал коммит - и main уехала вперёд. Тег - метка неподвижная. Поставил v1.4.0 на коммит - он там и останется навсегда, что бы ты потом ни коммитил. Это и есть смысл версионирования git: зафиксировать точку в истории так, чтобы через год её можно было найти, собрать, развернуть - байт в байт ту же самую.

В этом уроке разберём, как создать тег git правильно, чем annotated отличается от lightweight, как git describe вытаскивает имя версии из любого коммита, и как двумя командами - git archive и git bundle - превратить тег в готовый артефакт релиза без всего служебного мусора .git.

Изображение

Два вида тегов: git tag annotated против lightweight

Внутри Git теги бывают двух сортов, и разница не косметическая - это разные сущности в объектной модели.

Lightweight тег - это просто файл в .git/refs/tags/ с хешем коммита внутри. Ничего больше. Ни кто поставил, ни когда, ни зачем. Создаётся так:

Код: Выделить всё

git tag v0.1-draft
Заглянем под капот:

Код: Выделить всё

$ cat .git/refs/tags/v0.1-draft
7c3a1f9e0b2d4a6c8e0f1a2b3c4d5e6f7a8b9c0d
$ git cat-file -t v0.1-draft
commit
Видишь: имя тега указывает прямо на коммит. Это удобно для временной локальной закладки ("вот сюда я хочу вернуться вечером"), но для релиза не годится - в нём нет ни автора, ни даты, ни сообщения.

Annotated тег (git tag annotated) - это полноценный объект в базе, как коммит или дерево. У него есть собственный SHA, автор, дата, сообщение, и опционально - подпись. Создаётся флагом -a:

Код: Выделить всё

git tag -a v1.4.0 -m "Release 1.4.0: новый биллинг, фикс гонки в кеше"
Теперь refs/tags/v1.4.0 указывает не на коммит, а на тег-объект, а уже тот указывает на коммит:

Код: Выделить всё

$ git cat-file -t v1.4.0
tag
$ git cat-file -p v1.4.0
object 7c3a1f9e0b2d4a6c8e0f1a2b3c4d5e6f7a8b9c0d
type commit
tag v1.4.0
tagger Anton K <anton@example.com> 1781712000 +0300

Release 1.4.0: новый биллинг, фикс гонки в кеше
Вот эта дополнительная прослойка - tag-объект - и хранит метаданные. Поле object внутри ведёт к настоящему коммиту. Правило простое: всё, что ты публикуешь и от чего зависят люди - annotated. Lightweight - только для своих сиюминутных пометок. На собеседованиях это любят спрашивать, а в реальной жизни lightweight-тег в роли релиза однажды укусит тебя через git describe, о чём ниже.

Подписанный тег - тот же annotated, но с криптоподписью, чтобы любой мог проверить "это собрал именно мейнтейнер, а не подменили". В 2026 рекомендуемый простой путь - SSH-подпись:

Код: Выделить всё

git config gpg.format ssh
git config user.signingkey ~/.ssh/id_ed25519.pub
git tag -s v1.4.0 -m "Release 1.4.0"
git tag -v v1.4.0
Флаг -s ставит подпись, -v её проверяет (нужен настроенный allowedSignersFile, чтобы Git знал, чьи ключи доверенные). GPG тоже работает, но это легаси-путь для корпораций с готовой PKI; для нового проекта SSH-подпись проще и без головной боли с keyserver.

Смотрим, отправляем и удаляем теги

Список всех тегов - git tag без аргументов. С фильтром по маске:

Код: Выделить всё

$ git tag -l "v1.4.*"
v1.4.0
v1.4.1
v1.4.2
Подробности по конкретному тегу - git show. Для annotated он покажет и метаданные тега, и сам коммит с диффом:

Код: Выделить всё

$ git show v1.4.0
tag v1.4.0
Tagger: Anton K <anton@example.com>
Date:   Tue Jun 17 12:00:00 2026 +0300

Release 1.4.0: новый биллинг, фикс гонки в кеше

commit 7c3a1f9e0b2d4a6c8e0f1a2b3c4d5e6f7a8b9c0d
Author: ...
Теперь главная грабля, на которой спотыкаются почти все. git push НЕ отправляет теги по умолчанию. Ты сделал git tag -a v1.4.0, потом git push - и тег остался лежать только у тебя на машине. На сервере его нет, коллеги его не видят, CI по тегу не собрался. Запомни: push шлёт коммиты веток, но не теги.

Отправить конкретный тег:

Код: Выделить всё

git push origin v1.4.0
Отправить разом все локальные теги (осторожно - вытолкнет и старый мусор, и черновики):

Код: Выделить всё

git push origin --tags
А вот рекомендуемый в реальной работе вариант - git push tags умно, флагом --follow-tags:

Код: Выделить всё

git push --follow-tags
--follow-tags отправляет вместе с коммитами только annotated-теги, которые указывают на отправляемые коммиты. Lightweight-черновики он игнорирует - и это ровно то поведение, которое нужно: релизные annotated-теги уезжают автоматически, а твои локальные закладки остаются дома. Можно прописать в конфиг push.followTags=true, и забыть про --tags навсегда.

Удаление. Локально:

Код: Выделить всё

git tag -d v1.4.0-rc1
На сервере (синтаксис "пуш пустоты в ссылку"):

Код: Выделить всё

git push origin --delete v1.4.0-rc1
git describe: имя версии для любого коммита

Ты собираешь бинарь из произвольного коммита - не обязательно с тега. Что писать в --version у программы? Тут выходит git describe. Он берёт коммит, идёт по истории назад до ближайшего тега и строит человекочитаемое имя:

Код: Выделить всё

$ git describe
v1.4.0-14-g7c3a1f9
Разбираем строку по частям:
  • v1.4.0 - ближайший annotated-тег позади тебя;
  • 14 - сколько коммитов ты ушёл вперёд от этого тега;
  • g7c3a1f9 - буква g (git) плюс короткий хеш текущего коммита.
То есть "это четырнадцатый коммит после релиза 1.4.0". Если ты стоишь ровно на теге, describe выдаст просто v1.4.0 без суффикса. Эту строку зашивают в сборку - и по логу прода всегда видно, из какого именно состояния собран бинарь. Связь тегов с версионированием git тут прямая: одна команда превращает граф коммитов в осмысленный номер версии.

Важная тонкость про describe и тему annotated vs lightweight. По умолчанию git describe видит только annotated-теги. Если все твои релизы - lightweight, чистый git describe скажет "fatal: No annotated tags can describe". Тогда нужен флаг --tags:

Код: Выделить всё

$ git describe --tags
v1.3.0-99-g7c3a1f9
--tags заставляет учитывать и lightweight-теги тоже. Это ещё одна причина делать релизы annotated: тогда describe работает из коробки и не цепляет случайные черновики. Полезен и --dirty - он добавит суффикс -dirty, если в рабочем дереве есть незакоммиченные правки:

Код: Выделить всё

$ git describe --dirty
v1.4.0-14-g7c3a1f9-dirty
В CI это золото: сразу видно, что артефакт собран из грязного дерева, а не из чистого коммита.

git archive: снимок релиза без .git

Тег у тебя есть. Теперь нужно отдать код наружу - на сервер, в Docker-образ, в архив для заказчика. Делать git clone и тащить всю историю не надо: команда git archive экспортирует снимок дерева по тегу в tar или zip, без папки .git, без истории, только файлы как они есть на этом коммите.

Код: Выделить всё

git archive --format=tar.gz --prefix=myapp-1.4.0/ v1.4.0 -o myapp-1.4.0.tar.gz
Разбор флагов: --format задаёт формат (можно tar, tar.gz, zip), --prefix добавляет папку-обёртку внутри архива (чтобы при распаковке не насыпать файлы в текущую директорию), v1.4.0 - что именно архивируем (тег, ветка или хеш), -o - выходной файл. Проверим, что внутри нет ничего служебного:

Код: Выделить всё

$ tar tzf myapp-1.4.0.tar.gz | head
myapp-1.4.0/
myapp-1.4.0/README.md
myapp-1.4.0/src/
myapp-1.4.0/src/main.go
Никакого .git - чистый слепок версии. Именно так форджи (GitHub, GitLab) под капотом генерируют те самые "Source code (tar.gz)" на странице релиза: это git archive по тегу.

Часто в архив не должно попадать что-то, что нужно только в репозитории - CI-конфиги, внутренние заметки, тесты для заказчика. Для этого есть атрибут export-ignore. Создаёшь файл .gitattributes в корне:

Код: Выделить всё

.github/    export-ignore
tests/      export-ignore
RELEASE_NOTES_INTERNAL.md export-ignore
Теперь перечисленные пути git archive в архив не положит, хотя в самом репозитории они останутся. Удобно: один код, но наружу уходит причёсанная версия. Есть и парный атрибут export-subst - он подставляет в файлы значения вроде $Format:%H$ (хеш коммита) при экспорте, чтобы зашить версию прямо в исходник.

git bundle: весь репозиторий одним файлом

archive отдаёт снимок без истории. Но иногда историю надо сохранить, а сети нет - например, передать репозиторий на флешке в изолированный контур, или отдать релиз туда, где нет доступа к твоему серверу. Тут выходит git bundle: он упаковывает коммиты, ветки и теги в один файл, который сам по себе является валидным remote - из него можно клонировать и в него можно тянуть.

Упаковать всё:

Код: Выделить всё

git bundle create myrepo.bundle --all
Получатель клонирует из файла, как из обычного удалённого репозитория:

Код: Выделить всё

$ git clone myrepo.bundle myrepo
Cloning into 'myrepo'...
Receiving objects: 100% (1284/1284), done.
$ cd myrepo && git log --oneline -1
7c3a1f9 (HEAD -> main, tag: v1.4.0) Release 1.4.0
Вся история и теги на месте - в отличие от archive. Можно собрать и инкрементальный bundle - только то, что появилось после прошлой передачи:

Код: Выделить всё

git bundle create update.bundle v1.3.0..v1.4.0
Перед отправкой полезно проверить, что bundle самодостаточен и применим:

Код: Выделить всё

$ git bundle verify myrepo.bundle
The bundle contains these 2 refs:
7c3a1f9... refs/heads/main
7c3a1f9... refs/tags/v1.4.0
The bundle records a complete history.
myrepo.bundle is okay
Итого: archive - снимок без .git для деплоя, bundle - вся история одним файлом для офлайн-раздачи. Разные инструменты под разные задачи, но оба отлично дружат с тегами как точкой релиза.

SemVer и почему опубликованный тег нельзя двигать

Имена релизов берут с потолка редко - есть стандарт SemVer (Semantic Versioning): MAJOR.MINOR.PATCH, например 2.7.3.
  • MAJOR (2) - ломающие обратную совместимость изменения;
  • MINOR (7) - новая функциональность без поломки старого;
  • PATCH (3) - только багфиксы.
Глядя на 1.4.0 -> 1.4.1, человек понимает: можно обновляться спокойно, это патч. А 1.4.0 -> 2.0.0 кричит "читай changelog, что-то сломали". В 2026 поверх SemVer обычно крутят Conventional Commits плюс release-please или semantic-release: робот читает префиксы коммитов (feat, fix, BREAKING CHANGE), сам считает следующую версию и ставит тег. Меньше ручного труда, меньше человеческих ошибок в нумерации.

И последнее, самое важное правило. Опубликованный тег двигать нельзя. Технически можно: git tag -f v1.4.0 другой-коммит перебьёт метку. Но если ты уже сделал push, у коллег и в CI v1.4.0 указывает на старый коммит, а у тебя теперь на новый. Git при fetch по умолчанию не обновит уже существующий тег - он не двигается за тобой, как ветка. В итоге у разных людей "версия 1.4.0" - это физически разные коммиты. Сборки расходятся, баг невоспроизводим, доверие к номерам версий рушится. Тег - это обещание "вот эта точка, навсегда". Ошибся в релизе - не двигай старый, выпусти новый: v1.4.1. Так работает весь мир, и так не приходится по ночам выяснять, чей именно 1.4.0 был на проде.

Мини-лаба: от тега до артефакта релиза

Повтори руками по шагам - так уляжется надёжнее любого конспекта.

Код: Выделить всё

mkdir tag-lab && cd tag-lab
git init -b main
echo "v1" > app.txt
git add app.txt && git commit -m "first feature"

git tag draft-fast
git tag -a v1.0.0 -m "Release 1.0.0"
git cat-file -t draft-fast
git cat-file -t v1.0.0

echo "v2" >> app.txt
git commit -am "more work"
git describe --tags

printf "secret.txt export-ignore\n" > .gitattributes
echo "internal" > secret.txt
git add . && git commit -m "add ignore + secret"
git archive --format=tar -o rel.tar v1.0.0
tar tf rel.tar

git bundle create lab.bundle --all
git bundle verify lab.bundle
Что увидеть и осознать:
  • git cat-file -t покажет commit для draft-fast и tag для v1.0.0 - вот она разница lightweight против annotated;
  • git describe --tags выдаст что-то вроде v1.0.0-1-gХХХХ - один коммит после релиза;
  • в rel.tar по версии v1.0.0 не будет ни secret.txt (export-ignore), ни .git;
  • verify подтвердит, что bundle - самодостаточный репозиторий.
Контрольные вопросы
  • Ты сделал git tag -a v2.0.0 и git push. Коллега тег не видит. Почему и как починить одной командой?
  • Чем git archive по тегу отличается от git bundle? В каком случае что брать?
  • git describe выдаёт "No annotated tags can describe". Что это значит и какой флаг чаще всего спасает?
  • Почему нельзя передвинуть уже опубликованный тег v1.4.0 на новый коммит, даже если очень хочется?
Итог

Теги - это неподвижные метки релизов: для всего публичного делай annotated (а лучше подписанный), lightweight оставь для личных закладок. Помни, что push не шлёт теги сам - включи push.followTags. git describe превращает любой коммит в человеческий номер версии. git archive отдаёт чистый снимок по тегу для деплоя, git bundle - всю историю одним файлом для офлайна. И железное правило версионирования git: опубликованный тег - обещание навсегда, ошибся - выпускай следующий, а не переписывай прошлый.
👍 ❤️4 🔥1 😄 🤔
Аватара пользователя
geek_vlad
Сообщения: 1
Зарегистрирован: 05 июн 2026, 18:52

Re: Теги, релизы, archive и bundle

Сообщение geek_vlad »

Поймал ровно эту боль: тегнул релиз, запушил, а на сервере тега нет. Оказалось push без --follow-tags его не шлёт. Спасибо, теперь в конфиг прописал push.followTags=true и забыл
👍1 ❤️1 🔥 😄 🤔2
Аватара пользователя
fpga_pilot
Сообщения: 1
Зарегистрирован: 21 май 2026, 18:52

Re: Теги, релизы, archive и bundle

Сообщение fpga_pilot »

А git archive можно прямо по ветке делать или только по тегу? И export-ignore работает, если .gitattributes лежит не в корне, а в подпапке?
👍2 ❤️ 🔥 😄 🤔2
Ответить
← Предыдущая глава
git blame, mailmap и археология кода
Следующая глава →
Подмодули: внешние репозитории внутри проекта

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: git tag: теги и релизы

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость