Struktur proyek hampir tidak pernah diam: berkas berganti nama, modul dipindah ke direktori baru, folder dirombak saat proyek membesar. Dengan mv biasa, git status membelah satu kejadian menjadi dua — path lama tercatat deleted, path baru tampil untracked — dan keduanya harus kita satukan sendiri sebelum commit. Perintah git mv mengerjakan dua lapisan itu sekaligus: memindahkan berkas di disk sekaligus memperbarui index. Kejutan justru tersimpan di balik layar: Git tidak pernah mencatat kata “rename” di objeknya — status renamed adalah kesimpulan dari kemiripan isi yang dihitung ulang setiap kali ditampilkan. Chapter ini membedah perintahnya, membongkarnya menjadi mv + git rm --cached + git add, mengatur deteksinya lewat -M, lalu menelusuri riwayat lintas rename dengan git log --follow.

git mv: Satu Perintah untuk Dua Lapisan #

Repository latihan menyiapkan tiga berkas dalam tiga commit; beranda.txt sengaja punya dua versi supaya riwayatnya kelihatan panjang:

bash
mkdir latihan-mv && cd latihan-mv
git init -b main
git config user.name "Budi Santoso"
git config user.email "[email protected]"
printf 'beranda toko v1\nsalam pembeli\n' > beranda.txt
printf 'galeri baris 1\nbaris 2\nbaris 3\nbaris 4\nbaris 5\nbaris 6\nbaris 7\n' > galeri.txt
git add beranda.txt galeri.txt
git commit -m "Buat beranda dan galeri toko"
printf 'beranda toko v2\nsalam pembeli dan penawaran\n' > beranda.txt
git add beranda.txt
git commit -m "Perbarui beranda"
printf 'kontak toko\n' > kontak.txt
git add kontak.txt
git commit -m "Tambah kontak"

Kita ganti nama beranda.txt menjadi utama.txt:

bash
git mv beranda.txt utama.txt
git status

Output yang Dihasilkan:

text
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	renamed:    beranda.txt -> utama.txt

Perpindahan langsung duduk di bagian Changes to be committed sebagai renamed: beranda.txt -> utama.txt — siap di-commit tanpa satu langkah pun tambahan. Perintah git mv sendiri tidak mencetak apa pun ketika berhasil; konfirmasinya kita baca dari status. Format ringkas memperlihatkan kodenya:

bash
git status --short

Output yang Dihasilkan:

text
R  beranda.txt -> utama.txt

Kode R menempati kolom status staged — bukan D plus ?? yang akan kita lihat sebentar lagi. Dua hal dikerjakan git mv secara diam-diam: sistem berkas mengganti nama berkasnya, dan index membuang entri beranda.txt lalu menambahkan entri utama.txt yang menunjuk blob yang sama.

Kontras: mv Biasa Membelah Status #

mv biasa hanya menyentuh disk. Git membandingkan index dengan disk dan melihat dua kejadian terpisah:

bash
mv kontak.txt telepon.txt
git status

Output yang Dihasilkan:

text
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	renamed:    beranda.txt -> utama.txt

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	deleted:    kontak.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	telepon.txt

Status kini memuat tiga kelompok: renamed: beranda.txt -> utama.txt dari langkah sebelumnya, deleted: kontak.txt di bagian Changes not staged, dan telepon.txt sebagai untracked. Perpindahan yang sama, tapi setengah pekerjaan: penghapusan kontak.txt dari index belum dicatat dan telepon.txt belum dikenal Git.

Membongkar git mv: mv + git rm –cached + git add #

git mv adalah kepraktian, bukan mekanisme penyimpanan baru. Perintah itu ekuivalen dengan tiga langkah yang bisa kita kerjakan tangan sendiri: mv untuk disk, git rm --cached untuk membuang entri lama dari index (pola dari chapter git rm ), dan git add untuk mendaftarkan path baru:

bash
git rm --cached kontak.txt
git add telepon.txt
git status

Output yang Dihasilkan:

text
rm 'kontak.txt'

Output yang Dihasilkan:

text
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	renamed:    kontak.txt -> telepon.txt
	renamed:    beranda.txt -> utama.txt

Status staged kini menampilkan renamed: kontak.txt -> telepon.txt — identik dengan hasil git mv pada beranda.txt, padahal dikerjakan tanpa git mv sama sekali. Kedua jalur berakhir di commit yang sama:

bash
git commit -m "Pindahkan beranda dan kontak"

Output yang Dihasilkan:

text
[main b5180c9] Pindahkan beranda dan kontak
 2 files changed, 0 insertions(+), 0 deletions(-)
 rename kontak.txt => telepon.txt (100%)
 rename beranda.txt => utama.txt (100%)

Ringkasan commit menampilkan dua baris rename ... (100%) — termasuk pasangan kontak.txt => telepon.txt yang kita rakit manual. Inilah bukti bahwa label rename bukan data yang disimpan: commit hanya merekam “path kontak.txt hilang, path telepon.txt muncul dengan blob identik”, lalu Git menyimpulkan rename karena kemiripan isinya 100%. Hash b5180c9 milik sandbox — nilai di mesin pembaca menyesuaikan.

Deteksi Rename di diff: -M dan similarity index #

Kasus yang lebih realistis: berkas di-rename sekaligus disunting. Kita ubah satu baris galeri.txt, lalu pindahkan namanya sebelum commit:

bash
printf 'galeri baris 1\nbaris 2 UBAH\nbaris 3\nbaris 4\nbaris 5\nbaris 6\nbaris 7\n' > galeri.txt
git add galeri.txt
git mv galeri.txt arsip-galeri.txt
git add arsip-galeri.txt
git status

Output yang Dihasilkan:

text
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	renamed:    galeri.txt -> arsip-galeri.txt

Status tetap menyebut renamed meski isinya sudah berubah. Rinciannya terbuka di git diff :

bash
git diff --cached

Output yang Dihasilkan:

text
diff --git a/galeri.txt b/arsip-galeri.txt
similarity index 80%
rename from galeri.txt
rename to arsip-galeri.txt
index 2f94ac0..37fc0cc 100644
--- a/galeri.txt
+++ b/arsip-galeri.txt
@@ -1,5 +1,5 @@
 galeri baris 1
-baris 2
+baris 2 UBAH
 baris 3
 baris 4
 baris 5

Mari kita bedah baris demi baris:

  1. similarity index 80% — enam dari tujuh baris identik; Git menilai kemiripan isi 80%, jauh di atas ambang bawaan 50%, sehingga pasangan ini tetap dilaporkan sebagai rename.
  2. rename from galeri.txt / rename to arsip-galeri.txt — pasangan path yang ditalikan deteksi.
  3. @@ -1,5 +1,5 @@ — satu-satunya hunk isi: -baris 2 diganti +baris 2 UBAH.

Deteksi rename ini diaktifkan lewat opsi -M — sinonimnya --find-renames — dan sejak Git 2.9 opsi itu menjadi perilaku bawaan git diff dan git status:

bash
git diff --cached -M
git diff --cached --no-renames

Output yang Dihasilkan:

text
diff --git a/galeri.txt b/arsip-galeri.txt
similarity index 80%
rename from galeri.txt
rename to arsip-galeri.txt
index 2f94ac0..37fc0cc 100644
--- a/galeri.txt
+++ b/arsip-galeri.txt
@@ -1,5 +1,5 @@
 galeri baris 1
-baris 2
+baris 2 UBAH
 baris 3
 baris 4
 baris 5

Output yang Dihasilkan:

text
diff --git a/arsip-galeri.txt b/arsip-galeri.txt
new file mode 100644
index 0000000..37fc0cc
--- /dev/null
+++ b/arsip-galeri.txt
@@ -0,0 +1,7 @@
+galeri baris 1
+baris 2 UBAH
+baris 3
+baris 4
+baris 5
+baris 6
+baris 7
diff --git a/galeri.txt b/galeri.txt
deleted file mode 100644
index 2f94ac0..0000000
--- a/galeri.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-galeri baris 1
-baris 2
-baris 3
-baris 4
-baris 5
-baris 6
-baris 7

Blok pertama identik dengan diff tanpa opsi — -M kini hanya menegaskan yang sudah aktif; bentuk -M90% menaikkan ambang kemiripan menjadi 90% sehingga rename yang “terlalu banyak berubah” tak lagi ditalikan. Blok kedua mematikan deteksi: perpindahan tampil sebagai pasangan new file + deleted file — persis seperti yang tersimpan di snapshot.

Opsi diffPerilaku
tanpa opsideteksi rename aktif — bawaan sejak Git 2.9
-M, --find-renamesmenyalakan deteksi eksplisit; -M90% mengubah ambang kemiripan
--no-renamesmematikan deteksi: rename tampil sebagai add + delete

Rename Lintas Direktori #

Perpindahan ke direktori lain tidak membutuhkan opsi khusus — path barunya saja yang lebih panjang. Direktori tujuan harus sudah ada; git mv tidak membuatnya:

bash
mkdir arsip
git mv arsip-galeri.txt arsip/galeri.txt
git status

Output yang Dihasilkan:

text
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	renamed:    arsip-galeri.txt -> arsip/galeri.txt

Status menampilkan renamed: arsip-galeri.txt -> arsip/galeri.txt — path di dalam arsip/ diperlakukan sama seperti rename sederhana, karena memang hanya string path yang berubah. Kita bekukan dan lanjut ke pertanyaan yang lebih menarik:

bash
git commit -m "Rapikan galeri ke direktori arsip"

Menelusuri Riwayat Lintas Rename: git log –follow #

git log pada sebuah path hanya mencari commit yang menyentuh path persis itu. Setelah tiga kali berpindah, arsip/galeri.txt hanya mengenal satu commit:

bash
git log --oneline arsip/galeri.txt

Output yang Dihasilkan:

text
fd7ff69 Rapikan galeri ke direktori arsip

Opsi --follow menautkan riwayat mundur melewati rename — git log berjalan dari commit paling baru, mendeteksi rename di tiap langkah, lalu melanjutkan pencarian dengan path lama:

bash
git log --oneline --follow arsip/galeri.txt

Output yang Dihasilkan:

text
fd7ff69 Rapikan galeri ke direktori arsip
5e24c6b Pindahkan galeri dengan suntingan
06cb5e9 Buat beranda dan galeri toko

Tiga commit muncul: Rapikan galeri ke direktori arsip, Pindahkan galeri dengan suntingan (saat masih bernama arsip-galeri.txt), sampai Buat beranda dan galeri toko (saat masih galeri.txt). Satu batasan kerap menjegal: --follow hanya menerima tepat satu pathspec:

bash
git log --oneline --follow utama.txt arsip/galeri.txt

Output yang Dihasilkan:

text
fatal: --follow requires exactly one pathspec

Rename adalah kesimpulan dari isi, bukan catatan yang disimpan — dan selama isinya cukup mirip, Git menautkan riwayat itu otomatis, dengan atau tanpa git mv. Metadata berikutnya justru kebalikannya: satu bit permission yang berubah tanpa satu byte isi pun ikut berubah, dan tetap memicu diff. Lanjut ke File Mode dan Permission .