Git Stash: Yarım Kalan İşi Kenara Koymak
git stash ile yarım işi saklamak: untracked dosyalar için -u, isimli kayıtlar, list ve show, apply ile pop farkı, --staged ve --index, stash.index ayarı, pop sırasında conflict, stash branch ve Git 2.51'in stash export/import komutları.
Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir; --staged Git 2.35'ten, stash export/import Git 2.51'den, stash.index Git 2.52'den beri var.
Önce Tahmin Et
Ada indirim özelliğinin ortasında. src/cart.js değişmiş, kupon kodları için yeni bir src/coupons.js dosyası açılmış ama henüz hiçbiri commit'lenmemiş. Tam o sırada Alan yazıyor: Emma'nın fiyatı canlıda yanlış, hemen düzeltilmesi lazım. Ada yarım işi kenara koymak için git stash çalıştırıyor:
$ git status -s
M src/cart.js
?? src/coupons.js
$ git stash
Saved working directory and index state WIP on main: 7f7eeec Add cart total
Şimdi git status -s ne gösterir? Working tree temiz mi?
$ git status -s
?? src/coupons.js
Temiz değil. git stash varsayılan olarak yalnızca Git'in takip ettiği dosyalardaki değişiklikleri saklar; src/coupons.js untracked olduğu için yerinde kaldı. Ada bu hâliyle başka branch'e geçse dosya peşinden gelir, yanlışlıkla düzeltme commit'ine de girebilir. Stash, yarım işi bir rafa kaldırmak gibidir; ama raf yalnızca kendisine verdiğin şeyi alır.
-u ve -m: Her Şeyi, Bir İsimle Saklamak
Ada stash'i geri alıp bu kez doğru seçeneklerle yeniden saklıyor:
$ git stash pop
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: src/cart.js
Untracked files:
(use "git add <file>..." to include in what will be committed)
src/coupons.js
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (a0de88f6a4b8823c8467d9d9447ade09958eb939)
$ git stash push -u -m "discount work in progress"
Saved working directory and index state On main: discount work in progress
$ git status -s
pop değişiklikleri geri getirdi ve stash kaydını sildi; son satırdaki Dropped bunu söylüyor. Sonra git stash push ile iki seçenek verdi. -u (--include-untracked) untracked dosyaları da stash'e alır. -m kayda bir isim verir; isimsiz stash'ler WIP on main diye listelenir ve üç tane biriktiğinde hangisinin ne olduğunu kimse hatırlamaz. Düz git stash, aslında seçeneksiz bir git stash push'tur. Eski yazılarda göreceğin git stash save "mesaj" biçimi push lehine deprecated durumda.
Working tree temiz, Ada düzeltmeyi yapıp commit'liyor:
$ git commit -am "Fix Emma price"
[main eb8683a] Fix Emma price
1 file changed, 1 insertion(+), 1 deletion(-)
Stash'in İçine Bakmak
Raf boşalmadan önce üzerinde ne olduğuna bakalım:
$ git stash list
stash@{0}: On main: discount work in progress
$ git stash show
src/cart.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
$ git stash show -p --include-untracked
diff --git a/src/cart.js b/src/cart.js
index 4876df7..3f2d943 100644
--- a/src/cart.js
+++ b/src/cart.js
@@ -1,5 +1,6 @@
-function total(items) {
- return items.reduce((sum, item) => sum + item.price, 0);
+function total(items, discount = 0) {
+ const sum = items.reduce((acc, item) => acc + item.price, 0);
+ return sum - discount;
}
module.exports = { total };
diff --git a/src/coupons.js b/src/coupons.js
new file mode 100644
index 0000000..037f9a7
--- /dev/null
+++ b/src/coupons.js
@@ -0,0 +1,3 @@
+const coupons = { WELCOME10: 10 };
+
+module.exports = { coupons };
stash@{0} en son saklanan kayıt; yeni bir stash eklendiğinde eskisi stash@{1} olur. Numaralar kayar, isimler kaymaz; -m'nin değeri burada ortaya çıkıyor. git stash show varsayılan olarak yalnızca istatistik verir ve untracked dosyaları göstermez. -p diff'in tamamını, --include-untracked da stash'e alınan untracked dosyaları ekler.
Perde arkasında bir stash kaydı sıradan commit'lerden oluşur: biri working tree'nin, biri index'in, -u verdiysen biri de untracked dosyaların hâlini tutar. En son kayıt refs/stash ref'inde durur, öncekiler o ref'in reflog'unda. Bu yüzden stash yereldir; push ettiğinde gitmez.
apply mı, pop mı?
İkisi de stash'teki değişiklikleri working tree'ye uygular. Fark sonrasında: pop başarılı olursa kaydı siler, apply kaydı yerinde bırakır:
$ git stash apply -q
$ git stash list
stash@{0}: On main: discount work in progress
$ git stash drop
Dropped refs/stash@{0} (10e2323fc1fba3609ea631c1bc15b94b7f75457b)
$ git status -s
M src/cart.js
?? src/coupons.js
apply sonrasında kayıt listede durmaya devam etti; işim bittiğinde drop ile kendim sildim. Aynı değişikliği birden fazla branch'e uygulamak istediğinde ya da uygulamanın sorunsuz geçeceğinden emin olmadığında apply daha güvenli. Değişikliğin yerine oturduğunu gördükten sonra silersin. Gündelik kullanımda pop yeterli, çünkü conflict çıkarsa kaydı zaten silmiyor; aşağıda göreceğiz.
--staged: Yalnızca Stage'i Saklamak
Bazen yarım işin bir kısmını saklamak istersin. Git 2.35'ten beri --staged, yalnızca index'teki değişiklikleri stash'e alır ve geri kalanını working tree'de bırakır. Ada indirimi stage'liyor, README'deki notu stage'lemiyor:
$ git add src/cart.js
$ git status -s
M README.md
M src/cart.js
?? src/coupons.js
$ git stash push --staged -m "discount only"
Saved working directory and index state On main: discount only
$ git status -s
M README.md
?? src/coupons.js
$ git stash pop -q
$ git status -s
M README.md
M src/cart.js
?? src/coupons.js
Stage'lenmiş indirim gitti, README ve untracked dosya yerinde kaldı. Ama dikkat: pop değişikliği geri getirdi, stage'lenmiş olarak değil. Varsayılan davranış, stash'teki index hâlini geri yüklemek değil, bütün değişiklikleri working tree'ye uygulamaktır. Index'i de geri istiyorsan --index verirsin:
$ git add src/cart.js
$ git stash push --staged -m "discount only"
Saved working directory and index state On main: discount only
$ git stash pop -q --index
$ git status -s
M README.md
M src/cart.js
?? src/coupons.js
Bu kez M ile, yani stage'lenmiş olarak döndü. Bunu her seferinde yazmak istemiyorsan Git 2.52'den beri stash.index ayarı var; açıkken apply ve pop, --index verilmiş gibi davranır:
$ git stash push --staged -m "discount only"
Saved working directory and index state On main: discount only
$ git config set stash.index true
$ git stash pop -q
$ git status -s
M README.md
M src/cart.js
?? src/coupons.js
$ git config unset stash.index
Ayar, git pull ya da git rebase gibi komutların --autostash ile yaptığı otomatik stash'leri de etkiler.
Pop Sırasında Conflict
Ada README'yi commit'liyor ve indirim işini yeniden stash'liyor. Bu arada main'e, sepet toplamının aynı satırını değiştiren bir commit giriyor:
$ git commit -q -m "Document how to run the cart" -- README.md
$ git status -s
M src/cart.js
?? src/coupons.js
==== conflict
$ git stash push -u -m "discount work in progress"
Saved working directory and index state On main: discount work in progress
$ git commit -am "Multiply price by quantity"
[main 03ce348] Multiply price by quantity
1 file changed, 1 insertion(+), 1 deletion(-)
$ git stash pop
Auto-merging src/cart.js
CONFLICT (content): Merge conflict in src/cart.js
On branch main
Your branch is ahead of 'origin/main' by 3 commits.
(use "git push" to publish your local commits)
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: src/cart.js
Untracked files:
(use "git add <file>..." to include in what will be committed)
src/coupons.js
no changes added to commit (use "git add" and/or "git commit -a")
The stash entry is kept in case you need it again.
$ git status -s
UU src/cart.js
?? src/coupons.js
$ git stash list
stash@{0}: On main: discount work in progress
İki önemli satır var. UU, dosyanın iki taraftan da değiştiğini ve conflict içinde olduğunu söylüyor. Son satır ise pop'un kaydı silmediğini: conflict çıktığı için stash listede duruyor, yani hiçbir şey kaybolmadı. Dosyanın içinde onuncu yazıdan tanıdığın marker'lar var, sadece etiketleri farklı:
$ cat src/cart.js
<<<<<<< Updated upstream
function total(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
=======
function total(items, discount = 0) {
const sum = items.reduce((acc, item) => acc + item.price, 0);
return sum - discount;
>>>>>>> Stashed changes
}
module.exports = { total };
Updated upstream, şu anki commit'teki hâl; Stashed changes, stash'teki hâl. İkisini birleştirip conflict'i çözüyor, sonra kaydı elle siliyorsun:
$ git add src/cart.js
$ git stash drop
Dropped refs/stash@{0} (c1113e6f760849d644f0c1555fb1e3e082b5884b)
$ git status -s
M src/cart.js
?? src/coupons.js
Burada git add yalnızca conflict'in çözüldüğünü işaretler; değişiklik commit'e hazır hâlde stage'de bekler. drop'u unutursan kayıt listede kalır ve ileride yanlışlıkla ikinci kez uygulanabilir.
git stash branch
Conflict'ten kaçınmanın bir yolu daha var. Stash'i, saklandığı commit'ten açılan yeni bir branch'e uygulamak. O commit'te değişiklik zaten temiz oturur:
$ git stash push -m "spring coupon"
Saved working directory and index state On main: spring coupon
$ git commit -q -am "Raise welcome coupon"
$ git stash branch feature/spring-coupon
Switched to a new branch 'feature/spring-coupon'
On branch feature/spring-coupon
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: src/coupons.js
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (bc59c1b149f72f756a039591a90189bc0434029a)
$ git branch -v
* feature/spring-coupon a1b82e5 Add welcome coupon
main 49a67f2 [ahead 6] Raise welcome coupon
git stash branch, stash'in alındığı commit'ten (a1b82e5) yeni bir branch açtı, değişikliği oraya uyguladı ve başarılı olduğu için kaydı sildi. main bu sırada 49a67f2'ye ilerlemişti ve aynı satırı değiştirmişti; ama branch eski noktadan açıldığı için conflict çıkmadı. Uzun süre rafta kalmış bir stash'i geri almak için en sakin yol bu. Sonra o branch'i normal bir iş gibi commit'leyip merge edersin; conflict çıkacaksa merge sırasında, alışık olduğun araçlarla çıkar.
Stash'i Başka Makineye Taşımak
Stash yerel olduğu için başka bir makineye taşımanın resmi yolu uzun süre yoktu. Git 2.51 git stash export ve git stash import ile bir taşıma biçimi getirdi: stash kayıtları sıradan bir ref'e yazılır, o ref push ve fetch edilir, karşı tarafta yeniden stash listesine alınır:
$ git stash push -m "spring coupon"
Saved working directory and index state On feature/spring-coupon: spring coupon
$ git stash list
stash@{0}: On feature/spring-coupon: spring coupon
$ git stash export --to-ref refs/stashes/ada
$ git push origin refs/stashes/ada
To ../bookstore.git
* [new reference] refs/stashes/ada -> refs/stashes/ada
Ada'nın dizüstündeki klonda:
$ git fetch origin refs/stashes/ada:refs/stashes/ada
From ../bookstore
* [new ref] refs/stashes/ada -> refs/stashes/ada
$ git stash import refs/stashes/ada
$ git stash list
stash@{0}: On feature/spring-coupon: spring coupon
--to-ref yerine --print verirsen export bir ref yazmaz, oluşturduğu commit'in hash'ini basar. Ref adı serbest; refs/stashes/ altına koymak, branch ve tag'lerle karışmasını önler. Yine de çoğu durumda yarım işi bir branch'e commit'leyip push etmek daha basit; export/import, gerçekten stash olarak taşımak istediğin durumlar için.
Sık Düşülen Tuzaklar
İlk tuzak stash'i uzun süreli depo gibi kullanmak. Bir hafta sonra stash@{3}'ün ne olduğunu hatırlamazsın, saklandığı commit'ten de uzaklaşmış olursun. Birkaç saatten uzun yaşayacak iş için bir branch aç ve commit'le.
İkincisi untracked dosyaları unutmak. Önce Tahmin Et'te gördüğün gibi düz git stash yeni dosyaları almaz. Yarım işte yeni dosya varsa -u kullan. .gitignore'daki dosyaları da almak için -a var, ama genellikle istemezsin.
Üçüncüsü git stash clear ve yanlış drop. İkisi de kaydı onay sormadan siler. Silinen stash commit'leri bir süre object veritabanında kalır ve git fsck --unreachable ile bulunabilir, ama bu bir kurtarma operasyonudur; reflog yazısında bu tür kurtarmalara bakacağız.
Dördüncüsü conflict'ten sonra stash'in silindiğini sanmak. Silinmez; çözdükten sonra git stash drop senin işin.
Küçük Bir Deneme
★☆☆ Bir dosyayı değiştir, yeni bir dosya ekle. Önce düz git stash, sonra git stash push -u -m "..." dene ve her birinden sonra git status -s ile git stash list çıktısına bak.
★★☆ Bir değişikliği stage'le, başka bir değişikliği stage'leme ve git stash push --staged çalıştır. Kaydı önce --index olmadan, sonra --index ile apply et; git status -s'in iki sütunu nasıl değişiyor?
★★★ Bir stash al, sonra aynı satırı değiştiren bir commit at. git stash pop ile conflict'i yaşa ve kaydın git stash list'te kaldığını doğrula. Conflict'i çözmeden working tree'yi temizle, sonra aynı kaydı git stash branch ile conflict'siz uygula.
Bu yazıyla geri alma ve kenara koyma araçlarını tamamladık. Bir sonraki yazıda tek bir commit'i bir branch'ten diğerine taşımayı göreceğiz: git cherry-pick ile tek commit ve aralık seçmek, -x ile kaynağını kayda geçirmek, conflict'te akış ve bir hotfix'i eski sürüm branch'ine taşımak.