Chapters ▾ 2nd Edition

7.13 ابزارهای گیت (Git Tools) - جایگزینی (Replace)

جایگزینی (Replace)

دستور replace به شما اجازه می‌دهد یک شیء در گیت مشخص کنید و بگویید «هر بار که به این شیء ارجاع داده می‌شود، تظاهر کن که این شیء، شیء دیگری است». این بیشتر برای جایگزینی یک کامیت در تاریخچه شما با کامیت دیگری مفید است بدون اینکه مجبور باشید کل تاریخچه را مثلاً با git filter-branch بازسازی کنید.

برای مثال، فرض کنید تاریخچه کد بسیار بزرگی دارید و می‌خواهید مخزن خود را به دو بخش تقسیم کنید: یک تاریخچه کوتاه برای توسعه‌دهندگان جدید و یک تاریخچه بسیار طولانی و بزرگ‌تر برای کسانی که به داده‌کاوی علاقه‌مندند. می‌توانید یک تاریخچه را روی دیگری پیوند بزنید، به این صورت که اولین کامیت در خط جدید را با آخرین کامیت در خط قدیمی «جایگزین» کنید. این کار خوب است چون به این معنی است که در واقع نیازی نیست هر کامیت در تاریخچه جدید را بازنویسی کنید، همان‌طور که معمولاً برای پیوستن آن‌ها به هم باید انجام می‌دادید (زیرا والد بودن روی SHA-1 ها تأثیر می‌گذارد).

بیایید این را امتحان کنیم. یک مخزن موجود را می‌گیریم، آن را به دو مخزن تقسیم می‌کنیم، یکی جدید و یکی تاریخی، و سپس می‌بینیم چگونه می‌توانیم آن‌ها را بدون تغییر مقادیر SHA-1 مخزن جدید از طریق replace دوباره ترکیب کنیم.

ما از یک مخزن ساده با پنج کامیت ساده استفاده خواهیم کرد:

$ git log --oneline
ef989d8 Fifth commit
c6e1e95 Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

ما می‌خواهیم این را به دو شاخه تاریخچه تقسیم کنیم. یک شاخه از کامیت اول تا کامیت چهارم است — که تاریخچه تاریخی خواهد بود. شاخه دوم فقط کامیت‌های چهارم و پنجم خواهد بود — که تاریخچه جدید است.

Example Git history
نمودار 163. Example Git history

خب، ساختن تاریخچه تاریخی آسان است، می‌توانیم یک شاخه در تاریخچه ایجاد کنیم و سپس آن شاخه را به شاخه master یک مخزن راه دور جدید push کنیم.

$ git branch history c6e1e95
$ git log --oneline --decorate
ef989d8 (HEAD, master) Fifth commit
c6e1e95 (history) Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit
Creating a new `history` branch
نمودار 164. Creating a new history branch

حالا می‌توانیم شاخه جدید history را به شاخه master مخزن جدید خود push کنیم:

$ git remote add project-history https://github.com/schacon/project-history
$ git push project-history history:master
Counting objects: 12, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 907 bytes, done.
Total 12 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (12/12), done.
To git@github.com:schacon/project-history.git
 * [new branch]      history -> master

خوب، پس تاریخچه ما منتشر شده است. حال بخش سخت‌تر، کوتاه کردن تاریخچه جدیدمان است تا کوچک‌تر شود. ما نیاز به هم‌پوشانی داریم تا بتوانیم یک کامیت را در یکی با کامیت معادل در دیگری جایگزین کنیم، بنابراین این تاریخچه را فقط به کامیت‌های چهارم و پنجم محدود می‌کنیم (پس کامیت چهارم هم‌پوشانی دارد).

$ git log --oneline --decorate
ef989d8 (HEAD, master) Fifth commit
c6e1e95 (history) Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

در این حالت مفید است که یک کامیت پایه ایجاد کنیم که دستورالعمل‌هایی درباره چگونگی گسترش تاریخچه دارد، تا توسعه‌دهندگان دیگر بدانند اگر به اولین کامیت در تاریخچه کوتاه شده برخورد کردند و به تاریخچه بیشتری نیاز داشتند، چه کار کنند. پس کاری که می‌کنیم این است که یک شیء کامیت اولیه به عنوان نقطه پایه با دستورالعمل‌ها ایجاد کنیم، سپس کامیت‌های باقی‌مانده (چهارم و پنجم) را روی آن ری‌بیس کنیم.

برای این کار، باید نقطه‌ای برای تقسیم انتخاب کنیم، که برای ما کامیت سوم است، که در زبان SHA برابر با 9c68fdc است. پس کامیت پایه ما بر اساس آن درخت خواهد بود. می‌توانیم کامیت پایه خود را با استفاده از دستور commit-tree ایجاد کنیم، که فقط یک درخت را می‌گیرد و یک شیء کامیت جدید بدون والد با SHA-1 جدید به ما می‌دهد.

$ echo 'Get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf
یادداشت

دستور commit-tree یکی از مجموعه دستورات است که معمولاً به آن‌ها دستورات «لوله‌کشی» (plumbing) می‌گویند. این‌ها دستورات هستند که معمولاً برای استفاده مستقیم نیستند، بلکه توسط دستورات دیگر گیت برای انجام کارهای کوچک‌تر استفاده می‌شوند. در مواقعی که کارهای عجیب‌تری مثل این انجام می‌دهیم، این دستورات اجازه می‌دهند کارهای بسیار سطح پایین انجام دهیم ولی برای استفاده روزمره نیستند. می‌توانید درباره دستورات لوله‌کشی بیشتر در ابزارها و دستورات سطح پایین (Plumbing and Porcelain) بخوانید.

Creating a base commit using `commit-tree`
نمودار 165. Creating a base commit using commit-tree

خوب، حالا که یک کامیت پایه داریم، می‌توانیم بقیه تاریخچه خود را با git rebase --onto روی آن قرار دهیم. آرگومان --onto همان SHA-1 است که از commit-tree گرفتیم و نقطه ری‌بیس همان کامیت سوم (والد اولین کامیتی که می‌خواهیم نگه داریم، یعنی 9c68fdc) خواهد بود:

$ git rebase --onto 622e88 9c68fdc
First, rewinding head to replay your work on top of it...
Applying: fourth commit
Applying: fifth commit
Rebasing the history on top of the base commit
نمودار 166. Rebasing the history on top of the base commit

خوب، حالا تاریخچه جدیدمان را روی یک کامیت پایه قابل دور ریختن که حالا دستورالعمل‌هایی درباره چگونگی بازسازی کل تاریخچه در خود دارد، بازنویسی کرده‌ایم. می‌توانیم آن تاریخچه جدید را به یک پروژه جدید push کنیم و حالا وقتی دیگران آن مخزن را کلون کنند، فقط دو کامیت اخیر و یک کامیت پایه با دستورالعمل‌ها را خواهند دید.

حالا فرض کنیم نقش کسی را داشته باشیم که برای اولین بار پروژه را کلون می‌کند و می‌خواهد کل تاریخچه را داشته باشد. برای دریافت داده تاریخچه پس از کلون کردن این مخزن کوتاه شده، باید یک ریموت دوم برای مخزن تاریخی اضافه کرد و fetch انجام داد:

$ git clone https://github.com/schacon/project
$ cd project

$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
622e88e Get history from blah blah blah

$ git remote add project-history https://github.com/schacon/project-history
$ git fetch project-history
From https://github.com/schacon/project-history
 * [new branch]      master     -> project-history/master

حالا همکار شما کامیت‌های اخیرش را در شاخه‌ی master دارد و کامیت‌های تاریخی را در شاخه‌ی project-history/master.

$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
622e88e Get history from blah blah blah

$ git log --oneline project-history/master
c6e1e95 Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

برای ترکیب آن‌ها، کافی است دستور git replace را با کامیتی که می‌خواهید جایگزین شود و سپس کامیتی که می‌خواهید جایگزینش کنید، اجرا کنید. پس ما می‌خواهیم کامیت «چهارم» در شاخه‌ی master را با کامیت «چهارم» در شاخه‌ی project-history/master جایگزین کنیم:

$ git replace 81a708d c6e1e95

حالا اگر تاریخچه‌ی شاخه‌ی master را نگاه کنید، به این شکل به نظر می‌رسد:

$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

جالب نیست؟ بدون اینکه لازم باشد همه‌ی SHA-1های بالادستی را تغییر دهیم، توانستیم یک کامیت در تاریخچه‌مان را با کامیتی کاملاً متفاوت جایگزین کنیم و همه‌ی ابزارهای معمول (bisect، blame و غیره) دقیقاً طبق انتظار ما کار می‌کنند.

Combining the commits with `git replace`
نمودار 167. Combining the commits with git replace

نکته‌ی جالب این است که هنوز 81a708d را به عنوان SHA-1 نشان می‌دهد، حتی اگر در واقع داده‌های کامیت c6e1e95 که جایگزینش کردیم استفاده شود. حتی اگر دستوری مثل cat-file اجرا کنید، داده‌های جایگزین شده را به شما نشان می‌دهد:

$ git cat-file -p 81a708d
tree 7bc544cf438903b65ca9104a1e30345eee6c083d
parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252
author Scott Chacon <schacon@gmail.com> 1268712581 -0700
committer Scott Chacon <schacon@gmail.com> 1268712581 -0700

fourth commit

به خاطر داشته باشید که والد واقعی 81a708d کامیت جایگزین‌کننده‌ی ما (622e88e) بود، نه 9c68fdce که اینجا نوشته شده است.

نکته‌ی جالب دیگر این است که این داده‌ها در مراجع ما نگهداری می‌شوند:

$ git for-each-ref
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/heads/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/remotes/history/master
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/HEAD
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/replace/81a708dd0e167a3f691541c7a6463343bc457040

این یعنی به راحتی می‌توانیم جایگزینی خود را با دیگران به اشتراک بگذاریم، چون می‌توانیم این را روی سرور خودمان پوش کنیم و دیگران به آسانی می‌توانند آن را دانلود کنند. این موضوع در سناریوی الحاق تاریخچه‌ای که اینجا بررسی کردیم چندان مفید نیست (چون همه به هر صورت هر دو تاریخچه را دانلود می‌کنند، پس چرا آن‌ها را جدا کنیم؟) ولی در شرایط دیگر می‌تواند کاربردی باشد.

scroll-to-top