Git Tag ve Release: Sürümleri İşaretlemek, git describe ve GitHub Releases
Sürümleri git tag ile işaretliyoruz: lightweight ve annotated tag farkı, tag object'i, semver, push'un neden tag göndermediği, --tags ile --follow-tags, git describe çıktısını okumak, GitHub Releases, tag silmek ve yayınlanmış bir tag'i taşımanın neden tehlikeli olduğu.
Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir.
Önce Tahmin Et
bookstore ilk sürümüne hazır. Ada son commit'i işaretleyip push'luyor. Remote olarak yine yerel bir bare repo (../bookstore.git) kullanıyoruz; GitHub'daki repo'nun yerine geçiyor.
$ git tag v1.0.0
$ git push
Everything up-to-date
Everything up-to-date ne demek? Tag şu an GitHub'da görünüyor mu? Cevabı görmek için remote'taki tag'leri listeleyelim:
$ git ls-remote --tags origin
$ git tag -d v1.0.0
Deleted tag 'v1.0.0' (was 2262247)
Liste boş. git push branch'leri gönderir, tag'leri göndermez. Ada tag'i sildi, çünkü bu yazıda onu doğru şekilde yeniden oluşturacağız.
Tag Nedir: Hareket Etmeyen Bir İsim
Branch yazısında branch'in bir commit'i gösteren hareketli bir pointer olduğunu gördük; her commit'te ileri kayar. Tag de bir commit'i gösteren bir isimdir, ama yerinden oynamaz. v1.0.0 bugün hangi commit'i gösteriyorsa beş yıl sonra da onu gösterir.
Kitap analojisi: branch kitabın arasında duran ayraçtır, okudukça ilerler. Tag ise kitabın künyesindeki baskı bilgisidir; "ikinci baskı" ifadesi hangi metni kastediyorsa hep onu kasteder.
Bu yüzden tag'ler sürümler için kullanılır. Müşteri "1.0.0'da bir hata var" dediğinde, tam olarak hangi kodun yayınlandığını bilmen gerekir. CI/CD tarafında da tag çoğu zaman bir release pipeline'ını tetikleyen olaydır; bunu GitHub Actions serisinde kuracağız.
Lightweight ve Annotated Tag
Git'te iki tür tag var:
$ git tag v1.0.0-rc.1
$ git tag -a v1.0.0 -m "Release 1.0.0"
$ git tag
v1.0.0
v1.0.0-rc.1
$ git cat-file -t v1.0.0-rc.1
commit
$ git cat-file -t v1.0.0
tag
İlki lightweight tag: yalnızca bir commit'i gösteren bir isim, branch'in hareket etmeyen hâli. İkincisi annotated tag: -a ile oluşturulur ve Git'in object veritabanında kendi object'ini yaratır. Commit yazısında gördüğümüz blob, tree ve commit'in yanında dördüncü object tipi budur. İçine bakalım:
$ git cat-file -p v1.0.0
object 226224745a84b8abf90db3158c728bb837e3734a
type commit
tag v1.0.0
tagger Ada Lovelace <ada@example.com> 1794380880 +0300
Release 1.0.0
$ git show v1.0.0 --stat
tag v1.0.0
Tagger: Ada Lovelace <ada@example.com>
Date: Wed Nov 11 10:08:00 2026 +0300
Release 1.0.0
commit 226224745a84b8abf90db3158c728bb837e3734a (HEAD -> main, tag: v1.0.0-rc.1, tag: v1.0.0, origin/main, origin/HEAD)
Author: Ada Lovelace <ada@example.com>
Date: Wed Nov 11 10:05:00 2026 +0300
...
Annotated tag kimin, ne zaman, hangi mesajla işaretlediğini taşır, ayrıca imzalanabilir. Kural basit: yayınlayacağın sürümler için her zaman annotated tag kullan. Lightweight tag'i kendine aldığın geçici notlar için sakla, "şu commit'e sonra bakacağım" gibi. Birazdan göreceğin git describe ve --follow-tags da bu ayrıma göre davranır.
Tag'i geçmişteki bir commit'e de koyabilirsin; commit'i komutun sonuna eklemen yeterli:
$ git tag -a v0.9.0 HEAD~1 -m "Beta release"
$ git tag -l "v1.*"
v1.0.0
v1.0.0-rc.1
$ git log --oneline -3
2262247 (HEAD -> main, tag: v1.0.0-rc.1, tag: v1.0.0, origin/main, origin/HEAD) Add search by title
2112fd8 (tag: v0.9.0) Add .gitignore
558fced Add cart total
-l bir pattern ile filtreler. Annotated tag'de -m vermezsen Git mesaj için editör açar.
Semver ile Sürüm Numarası
Tag adı serbesttir, ama ekiplerin çoğu Semantic Versioning (semver) kullanır: MAJOR.MINOR.PATCH. Geriye dönük uyumsuz bir değişiklik MAJOR'ı artırır, uyumlu yeni bir özellik MINOR'ı, yalnızca hata düzeltmesi PATCH'i. 1.4.2'den sonra bir bug fix gelirse 1.4.3, yeni bir özellik gelirse 1.5.0, API kırılırsa 2.0.0 olur. Yayın öncesi sürümler tire ile eklenir: 1.0.0-rc.1, 1.0.0-beta.2.
Başa konan v semver'in parçası değildir, ama Git dünyasında yerleşik bir alışkanlıktır: v1.0.0. Hangisini seçersen seç, repo içinde tutarlı ol. Bir uyarı: git tag listeyi alfabetik sıralar, bu yüzden v1.10.0 v1.9.0'dan önce gelir. Sürüm sırasına göre görmek için git tag --sort=-v:refname kullan.
Tag'leri Push'lamak: --tags ve --follow-tags
Tek bir tag'i adıyla push'larsın:
$ git push origin v1.0.0
To ../bookstore.git
* [new tag] v1.0.0 -> v1.0.0
$ git ls-remote --tags origin
89d6b8689dce60f31d5d941cca5425730752be5e refs/tags/v1.0.0
226224745a84b8abf90db3158c728bb837e3734a refs/tags/v1.0.0^{}
İki satırın sebebi annotated tag: ilki tag object'inin kendisi, ^{} ile biten ikincisi gösterdiği commit.
Hepsini birden göndermek için iki seçenek var. git push --tags yereldeki bütün tag'leri gönderir: lightweight'ler, kendine aldığın notlar, yarım kalmış denemeler dahil. --follow-tags ise daha seçicidir: yalnızca push'ladığın commit'lerden ulaşılabilen annotated tag'leri gönderir. Ada bir commit daha attı, kendine bir lightweight not ve yeni sürüm için annotated bir tag ekledi:
$ git tag try-perf
$ git tag -a v1.1.0 -m "Release 1.1.0"
$ git push --follow-tags
To ../bookstore.git
2262247..9d469ef main -> main
* [new tag] v0.9.0 -> v0.9.0
* [new tag] v1.1.0 -> v1.1.0
main ile birlikte iki annotated tag gitti: yeni v1.1.0 ve daha önce eski bir commit'e koyduğumuz v0.9.0, çünkü o da push'lanan geçmişin içinde. try-perf ve v1.0.0-rc.1 lightweight oldukları için yerelde kaldı. Bunu varsayılan yapmak istersen git config set push.followTags true.
git describe: Bu Build Hangi Sürümden?
Tag'lerin en kullanışlı yan ürünü git describe. Son sürümden sonra iki commit daha atıldı:
$ git describe
v1.1.0-2-g1879802
$ git describe --abbrev=0
v1.1.0
$ git describe v1.1.0
v1.1.0
Çıktıyı parçalara ayır: en yakın annotated tag v1.1.0, ondan sonra 2 commit, ve g ön ekiyle o anki commit'in kısa hash'i 1879802. Yani "1.1.0'dan iki commit sonra, şu commit". --abbrev=0 yalnızca tag'i verir; tam tag'in üzerindeysen çıktı sadece tag adıdır.
Varsayılan hâliyle describe lightweight tag'lere bakmaz:
$ git tag snapshot
$ git describe
v1.1.0-2-g1879802
$ git describe --tags
snapshot
$ git describe --dirty
v1.1.0-2-g1879802-dirty
--tags lightweight'leri de hesaba katar. --dirty ise working tree'de commit'lenmemiş değişiklik varsa sona -dirty ekler. Build script'lerinde bu çıktıyı uygulamanın sürüm bilgisine gömmek yaygın bir yöntemdir: bir hata raporundaki v1.1.0-2-g1879802-dirty sana hem sürümü, hem commit'i, hem de build'in temiz olmadığını söyler.
GitHub Releases
GitHub'da bir release, bir tag'in üzerine kurulmuş bir sayfadır. Repo sayfasında Releases bölümüne girip "Draft a new release" dersin. Var olan bir tag'i seçersin ya da yeni bir tag adı yazarsın; GitHub onu seçtiğin branch'in ucunda oluşturur. Sonra başlık ve açıklama yazarsın. "Generate release notes" düğmesi, iki tag arasında merge edilen PR'lardan bir liste üretir. Derlenmiş dosyaları ek olarak yükleyebilir, sürümü "pre-release" olarak işaretleyebilir ve yayınlarsın.
Aynı işi komut satırından gh release create v1.1.0 --generate-notes ile de yapabilirsin. Önemli olan şu: release bir GitHub kavramıdır, tag ise bir Git kavramı. Tag repo'yla birlikte her clone'a gider, release sayfası GitHub'da kalır. Release'i silmek tag'i silmez; tag'i ayrıca silmen gerekir.
Tag'i Silmek ve Taşımak
Yerel tag'leri silmek kolay, ve yalnızca senin makineni etkiler:
$ git tag -d v1.0.0-rc.1 try-perf snapshot
Deleted tag 'v1.0.0-rc.1' (was 2262247)
Deleted tag 'try-perf' (was 9d469ef)
Deleted tag 'snapshot' (was 1879802)
$ git push origin --delete v0.9.0
To ../bookstore.git
- [deleted] v0.9.0
Remote'tan silmek de tek komut, ama tag'i daha önce fetch'lemiş herkesin makinesinde durmaya devam eder.
Asıl tehlikeli olan, yayınlanmış bir tag'i başka bir commit'e taşımak. Diyelim ki v1.1.0'ı yayınladıktan sonra bir hata buldun, düzelttin ve tag'i yeni commit'e almak istiyorsun:
$ git tag -a -f v1.1.0 -m "Release 1.1.0"
Updated tag 'v1.1.0' (was aad46fe)
$ git push origin v1.1.0
To ../bookstore.git
! [rejected] v1.1.0 -> v1.1.0 (already exists)
error: failed to push some refs to '../bookstore.git'
hint: Updates were rejected because the tag already exists in the remote.
$ git push --force origin v1.1.0
To ../bookstore.git
+ aad46fe...6234b58 v1.1.0 -> v1.1.0 (forced update)
Git önce durdurdu, --force ile geçtik. Şimdi tag'i önceden fetch'lemiş Alan'ın makinesine bakalım:
$ git fetch
$ git rev-parse --short v1.1.0^{commit}
9d469ef
$ git fetch --tags
From ../bookstore
! [rejected] v1.1.0 -> v1.1.0 (would clobber existing tag)
$ git fetch --tags --force
From ../bookstore
t [tag update] v1.1.0 -> v1.1.0
$ git rev-parse --short v1.1.0^{commit}
1879802
Düz git fetch hiçbir şey söylemedi ve Alan'ın v1.1.0'ı eski commit'te kaldı. --tags ile açıkça istediğinde Git var olan tag'i ezmeyi reddetti. Ancak --force ile güncellendi. Yani tag'i taşıdığında ekipte, CI'da ve onu çekmiş her makinede "1.1.0" iki farklı kodu ifade edebilir ve kimse fark etmeyebilir. Doğru yol neredeyse her zaman yeni bir sürüm çıkarmaktır: v1.1.1. Tag taşımayı ancak kimsenin henüz fetch'lemediğinden emin olduğunda düşün.
Sık Düşülen Tuzaklar
Tag'in push'landığını sanmak. git push tag göndermez. Ya tag'i adıyla push'la ya da --follow-tags kullan.
--tags ile çöp göndermek. Kendine aldığın lightweight not'lar da gider ve remote'tan temizlemek, herkesin makinesinden temizlemek demek değildir.
Sürümler için lightweight tag kullanmak. Kimin ne zaman işaretlediği kaybolur, git describe onu görmez, --follow-tags göndermez.
Yayınlanmış tag'i taşımak. Yukarıda gördüğün gibi, sessizce iki farklı "aynı sürüm" yaratırsın. Yeni bir PATCH sürümü çıkar.
Detached HEAD'e şaşırmak. git switch --detach v1.0.0 ile bir sürümü inceleyebilirsin; tag bir branch olmadığı için orada attığın commit'ler hiçbir branch'e ait olmaz. Düzeltme yapacaksan önce git switch -c hotfix/1.0.1 v1.0.0 ile bir branch aç.
Küçük Bir Deneme
★☆☆ bookstore repo'nda aynı commit'e bir lightweight ve bir annotated tag koy. git cat-file -t ve git show çıktılarını karşılaştır, farkı tek cümleyle yaz.
★★☆ Bir bare repo ile bir lightweight ve iki annotated tag oluştur; annotated'lerden birini push'lamayacağın bir branch'teki commit'e koy. git push --follow-tags sonrasında git ls-remote --tags origin çıktısında hangilerinin olacağını önce tahmin et, sonra kontrol et.
★★★ İki klonla bir tag'i taşı ve diğer klonda git fetch, git fetch --tags ve git fetch --tags --force davranışını gözle. Ardından git describe çıktısını kendi build script'inde bir sürüm dosyasına yazan küçük bir komut hazırla.
Tag ile hangi commit'in yayınlandığını artık biliyorsun. Peki iki sürüm arasında bir hata girdiyse, yüzlerce commit'in içinden hangisinin bozduğunu nasıl bulursun? Bir sonraki yazıda git bisect ile bunu ikili aramayla birkaç adımda yapacak, sonra git worktree ile aynı repo'da iki branch'te aynı anda çalışmayı göreceğiz.