Chapters ▾ 2nd Edition

7.8 ابزارهای گیت (Git Tools) - ادغام پیشرفته (Advanced Merging)

ادغام پیشرفته (Advanced Merging)

ادغام در گیت معمولاً نسبتاً آسان است. از آنجا که گیت ادغام شاخه‌ای دیگر را چندین بار آسان می‌کند، این بدان معناست که می‌توانید یک شاخه بسیار طولانی‌مدت داشته باشید و در عین حال آن را به‌روز نگه دارید، به‌گونه‌ای که اغلب تعارضات کوچک را حل کنید، به جای اینکه در پایان با یک تعارض عظیم و ناگهانی مواجه شوید.

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

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

تعارض‌های ادغام (Merge Conflicts)

در حالی که برخی اصول اولیه حل تعارض‌های ادغام را در مرج کانفیلیکت پایه (Basic Merge Conflicts) پوشش دادیم، برای تعارض‌های پیچیده‌تر، گیت چند ابزار فراهم می‌کند تا به شما کمک کند بفهمید چه اتفاقی می‌افتد و چگونه بهتر با تعارض برخورد کنید.

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

بیایید با یک مثال بسیار ساده پیش برویم. یک فایل روبی بسیار ساده داریم که hello world را چاپ می‌کند.

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

در مخزن خود، یک شاخه جدید به نام whitespace ایجاد می‌کنیم و تمام انتهای خطوط یونیکس را به انتهای خطوط DOS تغییر می‌دهیم، به عبارتی هر خط فایل را فقط از نظر فضای سفید تغییر می‌دهیم. سپس خط "hello world" را به "hello mundo" تغییر می‌دهیم.

$ git checkout -b whitespace
Switched to a new branch 'whitespace'

$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'Convert hello.rb to DOS'
[whitespace 3270f76] Convert hello.rb to DOS
 1 file changed, 7 insertions(+), 7 deletions(-)

$ vim hello.rb
$ git diff -b
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-  puts 'hello world'
+  puts 'hello mundo'^M
 end

 hello()

$ git commit -am 'Use Spanish instead of English'
[whitespace 6d338d2] Use Spanish instead of English
 1 file changed, 1 insertion(+), 1 deletion(-)

حالا برمی‌گردیم به شاخه master و مقداری مستندات برای تابع اضافه می‌کنیم.

$ git checkout master
Switched to branch 'master'

$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello world'
 end

$ git commit -am 'Add comment documenting the function'
[master bec6336] Add comment documenting the function
 1 file changed, 1 insertion(+)

اکنون سعی می‌کنیم شاخه whitespace را ادغام کنیم و به دلیل تغییرات فضای سفید با تعارض مواجه می‌شویم.

$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

لغو ادغام (Aborting a Merge)

حالا چند گزینه داریم. اول، بیایید بررسی کنیم چگونه از این وضعیت خارج شویم. اگر انتظار تعارض نداشتید و نمی‌خواهید فعلاً با آن درگیر شوید، می‌توانید به سادگی با دستور git merge --abort از ادغام خارج شوید.

$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master

گزینه git merge --abort تلاش می‌کند تا به حالتی که قبل از اجرای عملیات ادغام (merge) داشتید برگردد. تنها زمانی ممکن است این کار را به‌طور کامل نتواند انجام دهد که هنگام اجرای ادغام، تغییراتی بدون ذخیره‌سازی (unstashed) یا بدون کامیت در دایرکتوری کاری خود داشته باشید؛ در غیر این صورت، این گزینه به‌خوبی کار خواهد کرد.

اگر به هر دلیلی بخواهید از اول شروع کنید، می‌توانید دستور git reset --hard HEAD را اجرا کنید تا مخزن شما به آخرین حالت کامیت شده بازگردد. توجه داشته باشید که هر تغییر ذخیره‌نشده‌ای از دست خواهد رفت؛ پس مطمئن شوید که هیچ کدام از تغییرات خود را نمی‌خواهید حفظ کنید.

نادیده گرفتن فاصله‌ها (Ignoring Whitespace)

در این مورد خاص، تداخل‌ها به دلیل فاصله‌ها هستند. این موضوع را می‌دانیم چون مسئله ساده است، اما در موارد واقعی هم وقتی به تداخل نگاه می‌کنید، به‌راحتی قابل تشخیص است چون هر خط در یک طرف حذف شده و دوباره در طرف دیگر اضافه شده است. به‌طور پیش‌فرض، گیت تمام این خطوط را به‌عنوان تغییر یافته می‌بیند و نمی‌تواند فایل‌ها را ادغام کند.

با این حال، استراتژی پیش‌فرض ادغام می‌تواند آرگومان‌هایی دریافت کند که برخی از آنها مربوط به نادیده گرفتن صحیح فاصله‌ها هستند. اگر در یک ادغام با مشکلات زیادی درباره فاصله مواجه شدید، می‌توانید به‌سادگی ادغام را لغو کنید و دوباره اجرا کنید، این‌بار با گزینه‌های -Xignore-all-space یا -Xignore-space-change. گزینه اول هنگام مقایسه خطوط، فاصله‌ها را کاملاً نادیده می‌گیرد و گزینه دوم دنباله‌ای از یک یا چند کاراکتر فاصله را معادل در نظر می‌گیرد.

$ git merge -Xignore-space-change whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

از آنجا که در این مورد تغییرات واقعی فایل‌ها با هم تداخل نداشتند، وقتی فاصله‌ها را نادیده می‌گیریم، همه چیز به‌خوبی ادغام می‌شود.

این قابلیت بسیار مفید است اگر در تیم شما کسی باشد که گاهی همه چیز را از فضاها به تب‌ها یا برعکس تغییر فرمت می‌دهد.

ادغام مجدد دستی فایل‌ها (Manual File Re-merging)

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

کاری که باید انجام دهیم این است که فایل مورد نظر برای ادغام را قبل از تلاش برای ادغام واقعی، با برنامه‌ای مانند dos2unix پردازش کنیم. چگونه این کار را انجام دهیم؟

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

تهیه سه نسخه فایل کار چندان دشواری نیست. گیت همه این نسخه‌ها را در ایندکس تحت عنوان "مرحله‌ها" (stages) نگه می‌دارد که هر کدام شماره‌ای دارند. مرحله ۱ نسخه اجدادی مشترک است، مرحله ۲ نسخه شما و مرحله ۳ نسخه‌ای است که از MERGE_HEAD می‌آید، یعنی نسخه‌ای که می‌خواهید ادغام کنید ("طرف مقابل").

می‌توانید هر یک از این نسخه‌های فایل درگیر در تداخل را با دستور git show و یک نحو خاص استخراج کنید.

$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb

اگر بخواهید کمی حرفه‌ای‌تر عمل کنید، می‌توانید با دستور کمکی ls-files -u شناسه SHA-1 بلاک‌های گیت برای هر یک از این فایل‌ها را به دست آورید.

$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1	hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2	hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3	hello.rb

:1:hello.rb فقط یک کوتاه‌نویسی برای جستجوی آن شناسه SHA-1 بلاک است.

حالا که محتوای هر سه مرحله را در دایرکتوری کاری خود داریم، می‌توانیم نسخه طرف مقابل را به‌صورت دستی برای رفع مشکل فاصله اصلاح کنیم و سپس با دستور کمتر شناخته شده git merge-file فایل را مجدداً ادغام کنیم.

$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p \
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# prints out a greeting
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()

در این مرحله فایل به‌خوبی ادغام شده است. در واقع، این روش بهتر از گزینه ignore-space-change عمل می‌کند چون ابتدا مشکل فاصله‌ها را رفع می‌کند و بعد ادغام می‌کند، نه اینکه فقط آنها را نادیده بگیرد. در ادغام با گزینه ignore-space-change، در واقع چند خط دارای انتهای خط DOS باقی می‌ماند که باعث مخلوط شدن فرمت‌ها می‌شود.

اگر بخواهید قبل از نهایی کردن این کامیت، ایده‌ای از تغییرات واقعی بین یک طرف و طرف دیگر داشته باشید، می‌توانید از git diff استفاده کنید تا تغییرات موجود در دایرکتوری کاری که می‌خواهید به‌عنوان نتیجه ادغام کامیت کنید را با هر یک از این مرحله‌ها مقایسه کنید. بیایید همه آنها را مرور کنیم.

برای مقایسه نتیجه خود با آنچه قبل از ادغام در شاخه‌تان داشتید، یعنی دیدن تغییراتی که ادغام وارد کرده است، می‌توانید دستور git diff --ours را اجرا کنید:

$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@

 # prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

اینجا به‌راحتی می‌بینیم که در شاخه خودمان چه اتفاقی افتاده، یعنی دقیقاً چه چیزی را با این ادغام به فایل اضافه می‌کنیم، که تغییر یک خط است.

اگر بخواهید ببینید نتیجه ادغام چقدر با نسخه طرف مقابل تفاوت دارد، می‌توانید دستور git diff --theirs را اجرا کنید. در این مثال و مثال بعدی، باید گزینه -b را اضافه کنید تا فاصله‌ها حذف شوند، چون مقایسه را با آنچه در گیت است انجام می‌دهیم، نه فایل پاک‌سازی‌شده hello.theirs.rb خودمان.

$ git diff --theirs -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello mundo'
 end

در نهایت، می‌توانید ببینید که فایل از هر دو طرف چگونه تغییر کرده است با دستور git diff --base.

$ git diff --base -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

در این مرحله می‌توانیم از دستور git clean استفاده کنیم تا فایل‌های اضافی‌ای که برای انجام ادغام دستی ایجاد کرده‌ایم ولی دیگر نیازی به آن‌ها نداریم را پاک کنیم.

$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb

Checking Out Conflicts (بررسی تعارض‌ها)

شاید به دلیلی از نتیجه حل تعارض راضی نیستیم، یا شاید ویرایش دستی یک یا هر دو طرف هنوز خوب جواب نداده و به زمینه‌ی بیشتری نیاز داریم.

بیایید مثال را کمی تغییر دهیم. در این مثال، دو شاخه بلندمدت داریم که هر کدام چند کامیت در خود دارند ولی هنگام ادغام، یک تعارض محتوایی واقعی ایجاد می‌کنند.

$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) Update README
* 9af9d3b Create README
* 694971d Update phrase to 'hola world'
| * e3eb223 (mundo) Add more tests
| * 7cff591 Create initial testing script
| * c3ffff1 Change text to 'hello mundo'
|/
* b7dcc89 Initial hello world code

اکنون سه کامیت منحصر به فرد داریم که فقط روی شاخه master هستند و سه کامیت دیگر که روی شاخه mundo قرار دارند. اگر بخواهیم شاخه mundo را ادغام کنیم، با تعارض مواجه می‌شویم.

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

می‌خواهیم ببینیم تعارض ادغام چیست. اگر فایل را باز کنیم، چیزی شبیه این می‌بینیم:

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

هر دو طرف ادغام محتوایی به این فایل اضافه کرده‌اند، اما برخی از کامیت‌ها فایل را در یک نقطه مشابه تغییر داده‌اند که باعث این تعارض شده است.

بیایید چند ابزار که اکنون در اختیار دارید را بررسی کنیم تا بفهمیم این تعارض چگونه به وجود آمده است. شاید واضح نباشد که دقیقاً چگونه باید این تعارض را رفع کنید. به زمینه‌ی بیشتری نیاز دارید.

یکی از ابزارهای مفید، دستور git checkout با گزینه --conflict است. این دستور فایل را دوباره چک‌اوت می‌کند و نشانگرهای تعارض ادغام را جایگزین می‌کند. این می‌تواند زمانی مفید باشد که بخواهید نشانگرها را ریست کرده و دوباره سعی کنید آن‌ها را حل کنید.

می‌توانید به --conflict مقدار diff3 یا merge (که پیش‌فرض است) بدهید. اگر diff3 را بدهید، گیت از نسخه‌ای کمی متفاوت از نشانگرهای تعارض استفاده می‌کند که نه تنها نسخه‌های “ours” و “theirs” بلکه نسخه “base” را هم به صورت درون‌خطی نشان می‌دهد تا زمینه بیشتری به شما بدهد.

$ git checkout --conflict=diff3 hello.rb

وقتی این دستور را اجرا کنیم، فایل به این شکل خواهد بود:

#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()

اگر این فرمت را دوست دارید، می‌توانید آن را به عنوان پیش‌فرض برای تعارض‌های ادغام آینده با تنظیم گزینه merge.conflictstyle روی diff3 قرار دهید.

$ git config --global merge.conflictstyle diff3

دستور git checkout همچنین گزینه‌های --ours و --theirs را می‌پذیرد که راه بسیار سریعی برای انتخاب فقط یکی از دو طرف بدون انجام ادغام کامل است.

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

گزارش ادغام (Merge Log)

ابزار مفید دیگری که هنگام حل تعارض‌های ادغام کاربرد دارد، git log است. این می‌تواند به شما کمک کند تا بفهمید چه چیزی ممکن است به ایجاد تعارض‌ها کمک کرده باشد. مرور کمی از تاریخچه برای به یاد آوردن اینکه چرا دو خط توسعه به یک بخش مشابه از کد دست زده‌اند، گاهی بسیار مفید است.

برای دریافت فهرست کامل تمام کامیت‌های منحصر به فردی که در هر یک از شاخه‌های دخیل در این ادغام وجود دارند، می‌توانیم از نحو «سه نقطه» که در سه نقطه (Triple Dot) یاد گرفته‌ایم استفاده کنیم.

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 Update README
< 9af9d3b Create README
< 694971d Update phrase to 'hola world'
> e3eb223 Add more tests
> 7cff591 Create initial testing script
> c3ffff1 Change text to 'hello mundo'

این فهرست خوبی از شش کامیت کلی درگیر است، همچنین مشخص می‌کند هر کامیت مربوط به کدام شاخه توسعه بوده است.

اما ما می‌توانیم این را ساده‌تر کنیم تا زمینه بسیار مشخص‌تری به دست بیاوریم. اگر گزینه --merge را به git log اضافه کنیم، فقط کامیت‌هایی را نشان می‌دهد که در هر دو طرف عملیات ادغام ویرایشی روی فایلی داشته‌اند که در حال حاضر دچار تعارض است.

$ git log --oneline --left-right --merge
< 694971d Update phrase to 'hola world'
> c3ffff1 Change text to 'hello mundo'

اگر به جای آن، این فرمان را با گزینه -p اجرا کنید، فقط تفاوت‌های مربوط به فایلی که در نهایت دچار تعارض شده را خواهید دید. این می‌تواند بسیار مفید باشد تا سریعاً زمینه‌ای که به شما کمک می‌کند بفهمید چرا تعارض رخ داده و چگونه می‌توان آن را با هوشمندی بیشتری حل کرد را به دست آورید.

قالب تفاوت ترکیبی (Combined Diff Format)

از آنجا که گیت هر نتیجه ادغامی که موفق باشد را مرحله‌بندی می‌کند، وقتی در حالت ادغام دچار تعارض هستید و git diff را اجرا می‌کنید، فقط مواردی را می‌بینید که هنوز دچار تعارض هستند. این می‌تواند به شما کمک کند ببینید چه چیزهایی هنوز باید حل و فصل شود.

وقتی که بلافاصله پس از یک تعارض ادغام git diff را اجرا می‌کنید، اطلاعاتی در قالب خروجی تفاوتی نسبتاً منحصر به فرد دریافت خواهید کرد.

$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
  #! /usr/bin/env ruby

  def hello
++<<<<<<< HEAD
 +  puts 'hola world'
++=======
+   puts 'hello mundo'
++>>>>>>> mundo
  end

  hello()

این قالب به نام "تفاوت ترکیبی" شناخته می‌شود و دو ستون داده کنار هر خط به شما نشان می‌دهد. ستون اول نشان می‌دهد که آیا آن خط بین شاخه “ours” و فایل در دایرکتوری کاری شما متفاوت است (حذف یا اضافه شده) و ستون دوم همین را بین شاخه “theirs” و کپی دایرکتوری کاری شما نشان می‌دهد.

مثلاً در این مثال می‌بینید خطوط <<<<<<< و >>>>>>> در کپی کاری وجود دارند اما در هیچ‌کدام از دو طرف ادغام نیستند. این منطقی است چون ابزار ادغام آن‌ها را برای زمینه ما قرار داده، اما انتظار می‌رود که آن‌ها را حذف کنیم.

اگر تعارض را حل کنیم و دوباره git diff را اجرا کنیم، همان خروجی را می‌بینیم ولی کمی مفیدتر است.

$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

این به ما نشان می‌دهد که “hola world” در سمت ما بوده ولی در کپی کاری نیست، “hello mundo” در سمت آن‌ها بوده ولی در کپی کاری نیست و در نهایت “hola mundo” در هیچ‌کدام از دو طرف نبوده ولی اکنون در کپی کاری وجود دارد. این برای بازبینی قبل از ثبت نهایی حل تعارض مفید است.

شما همچنین می‌توانید این خروجی را از git log هر ادغام ببینید تا بفهمید بعد از ادغام چگونه مسئله حل شده است. گیت این قالب را وقتی git show را روی یک کامیت ادغام اجرا کنید یا اگر گزینه --cc را به git log -p اضافه کنید (که به طور پیش‌فرض فقط پچ‌های کامیت‌های غیرادغام را نشان می‌دهد) نمایش می‌دهد.

$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Sep 19 18:14:49 2014 +0200

    Merge branch 'mundo'

    Conflicts:
        hello.rb

diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

بازگرداندن ادغام‌ها (Undoing Merges)

حالا که می‌دانید چگونه یک کامیت ادغام ایجاد کنید، احتمالاً بعضی را اشتباهی خواهید ساخت. یکی از چیزهای عالی در کار با گیت این است که اشتباه کردن اشکالی ندارد، چون امکان اصلاح آن وجود دارد و در بسیاری موارد آسان است.

کامیت‌های ادغام هم همین‌طور هستند. فرض کنید روی یک شاخه موضوعی کار می‌کردید، اشتباهی آن را به master ادغام کردید و حالا تاریخچه کامیت شما این‌گونه شده است:

Accidental merge commit
نمودار 155. Accidental merge commit

دو راه برای رفع این مشکل وجود دارد، بسته به اینکه نتیجه دلخواه شما چیست.

اصلاح اشاره‌گرها (Fix the references)

اگر کامیت ادغام ناخواسته فقط روی مخزن محلی شما وجود دارد، ساده‌ترین و بهترین راه این است که شاخه‌ها را جابجا کنید تا به جایگاه دلخواه برسند. در بیشتر موارد، اگر بعد از ادغام اشتباهی git merge، دستور git reset --hard HEAD~ را اجرا کنید، اشاره‌گر شاخه‌ها را به این شکل تنظیم می‌کند:

History after `git reset --hard HEAD~`
نمودار 156. History after git reset --hard HEAD~

ما پیش‌تر reset را در بازنشانی به زبان ساده (Reset Demystified) پوشش داده‌ایم، بنابراین نباید دشوار باشد که بفهمید اینجا چه اتفاقی می‌افتد. یک یادآوری سریع: reset --hard معمولاً سه مرحله دارد:

  1. حرکت دادن شاخه‌ای که HEAD به آن اشاره می‌کند. در این مورد، می‌خواهیم master را به جایی که پیش از کامیت ادغام بود (C6) برگردانیم.

  2. همسان‌سازی شاخص (index) با HEAD.

  3. همسان‌سازی دایرکتوری کاری با شاخص.

نقطه ضعف این روش این است که تاریخچه را بازنویسی می‌کند، که در مخزن مشترک می‌تواند مشکل‌ساز باشد. برای اطلاعات بیشتر در این زمینه به خطرات بازپایه‌گذاری (The Perils of Rebasing) مراجعه کنید؛ خلاصه این است که اگر دیگران کامیت‌هایی که شما بازنویسی می‌کنید را دارند، بهتر است از reset استفاده نکنید. همچنین این روش زمانی کار نمی‌کند که کامیت‌های دیگری بعد از ادغام ساخته شده باشند؛ جابجایی اشاره‌گرها باعث از دست رفتن آن تغییرات خواهد شد.

معکوس کردن کامیت (Reverse the commit)

اگر جابجا کردن اشاره‌گر شاخه‌ها برای شما مناسب نیست، گیت این امکان را می‌دهد که کامیت جدیدی بسازید که تمام تغییرات یک کامیت موجود را برگرداند. گیت این عملیات را “revert” می‌نامد و در این موقعیت خاص، دستور به این شکل است:

$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"

پرچم -m 1 مشخص می‌کند کدام والد “mainline” است و باید نگه داشته شود. وقتی یک ادغام را روی HEAD اجرا می‌کنید (git merge topic)، کامیت جدید دو والد دارد: اولی HEAD (C6) است و دومی سر شاخه‌ای که ادغام شده (C4). در اینجا می‌خواهیم همه تغییراتی که از ادغام والد شماره ۲ (C4) آمده را برگردانیم و همه محتویات والد شماره ۱ (C6) را نگه داریم.

تاریخچه با کامیت ریورت شده این‌گونه خواهد بود:

History after `git revert -m 1`
نمودار 157. History after git revert -m 1

کامیت جدید ^M دقیقاً همان محتوای C6 را دارد، بنابراین از اینجا به بعد انگار ادغام هرگز انجام نشده، البته کامیت‌های ادغام نشده هنوز در تاریخچه HEAD باقی هستند. اگر بخواهید دوباره شاخه topic را به master ادغام کنید، گیت گیج خواهد شد:

$ git merge topic
Already up-to-date.

هیچ چیزی در شاخه‌ی topic وجود ندارد که قبلاً از شاخه‌ی master قابل دسترسی نباشد. بدتر از آن، اگر روی topic کار اضافه کنید و دوباره ادغام (merge) کنید، گیت فقط تغییراتی را که بعد از ادغام معکوس شده ایجاد شده‌اند وارد می‌کند:

History with a bad merge
نمودار 158. History with a bad merge

بهترین راه برای حل این مشکل این است که ادغام اصلی را لغو معکوس نکنید، چون حالا می‌خواهید تغییراتی که قبلاً معکوس شده‌اند را وارد کنید، سپس یک کامیت ادغام جدید ایجاد کنید:

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
History after re-merging a reverted merge
نمودار 159. History after re-merging a reverted merge

در این مثال، M و ^M همدیگر را خنثی می‌کنند. ^^M عملاً تغییرات از C3 و C4 را ادغام می‌کند و C8 تغییرات از C7 را وارد می‌کند، بنابراین اکنون شاخه‌ی topic کاملاً ادغام شده است.

انواع دیگر ادغام‌ها (Other Types of Merges)

تا اینجا ما ادغام معمول دو شاخه را پوشش دادیم که معمولاً با استراتژی ادغام به نام “recursive” انجام می‌شود. اما روش‌های دیگری هم برای ادغام شاخه‌ها وجود دارد. بیایید چند مورد از آن‌ها را سریع بررسی کنیم.

اولویت ما یا آن‌ها (Our or Theirs Preference)

اول از همه، یک قابلیت مفید دیگر در حالت معمول ادغام “recursive” وجود دارد. ما قبلاً گزینه‌های ignore-all-space و ignore-space-change را که با -X منتقل می‌شوند دیدیم، اما می‌توانیم به گیت بگوییم که در صورت بروز تعارض، طرف یکی از شاخه‌ها را ترجیح دهد.

به طور پیش‌فرض، وقتی گیت تعارضی بین دو شاخه در حال ادغام می‌بیند، نشانگرهای تعارض را در کد اضافه می‌کند و فایل را به حالت تعارض‌دار درمی‌آورد تا شما آن را حل کنید. اگر ترجیح می‌دهید گیت به‌جای اینکه شما دستی تعارض را حل کنید، یکی از طرفین را به طور کامل انتخاب کند و طرف دیگر را نادیده بگیرد، می‌توانید به دستور merge گزینه‌های -Xours یا -Xtheirs را بدهید.

اگر گیت این گزینه‌ها را ببیند، نشانگر تعارض نمی‌گذارد. هر تفاوتی که قابل ادغام باشد را ادغام می‌کند. هر تفاوتی که تعارض داشته باشد، به سادگی طرف مشخص شده توسط شما را به طور کامل انتخاب می‌کند، حتی فایل‌های باینری را.

اگر به مثال “hello world” که قبلاً داشتیم برگردیم، می‌بینیم ادغام شاخه‌ی ما باعث تعارض می‌شود.

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

اما اگر با -Xours یا -Xtheirs اجرا شود، تعارضی ایجاد نمی‌شود.

$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh

در این حالت، به جای اینکه نشانگرهای تعارض در فایل با “hello mundo” در یک طرف و “hola world” در طرف دیگر ظاهر شود، به سادگی “hola world” را انتخاب می‌کند. با این حال، تمام تغییرات غیر متعارض دیگر در آن شاخه به درستی ادغام می‌شوند.

این گزینه همچنین می‌تواند به دستور git merge-file که قبلاً دیدیم داده شود، مثلاً با اجرای git merge-file --ours برای ادغام فایل‌های جداگانه.

اگر بخواهید کاری مشابه انجام دهید اما نخواهید گیت حتی تلاش کند تغییرات طرف دیگر را ادغام کند، گزینه‌ی سختگیرانه‌تری به نام استراتژی ادغام “ours” وجود دارد. این متفاوت است از گزینه‌ی “ours” در ادغام بازگشتی.

این عملاً یک ادغام جعلی انجام می‌دهد. یک کامیت ادغام جدید با هر دو شاخه به عنوان والدین ثبت می‌کند، اما حتی به شاخه‌ای که می‌خواهید ادغام کنید نگاه نمی‌کند. در نتیجه ادغام، دقیقاً کد شاخه‌ی فعلی شما ثبت می‌شود.

$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$

می‌بینید که هیچ تفاوتی بین شاخه‌ای که روی آن بودیم و نتیجه ادغام وجود ندارد.

این اغلب مفید است تا گیت را فریب دهید که شاخه‌ای قبلاً ادغام شده است وقتی بعداً می‌خواهید ادغام کنید. برای مثال، فرض کنید از شاخه‌ی release یک شاخه گرفته‌اید و روی آن کاری انجام داده‌اید که می‌خواهید بعداً به شاخه‌ی master بازگردانید. در همین حین، یک رفع اشکال روی master نیاز به انتقال به شاخه‌ی release دارد. می‌توانید شاخه‌ی رفع اشکال را به release ادغام کنید و همچنین همان شاخه را با گزینه‌ی merge -s ours به master ادغام کنید (حتی اگر رفع اشکال قبلاً در آن باشد) تا وقتی بعداً شاخه‌ی release را دوباره ادغام می‌کنید، تعارضی از رفع اشکال پیش نیاید.

ادغام Subtree (Subtree Merging)

ایده‌ی ادغام subtree این است که شما دو پروژه دارید، و یکی از این پروژه‌ها به یک زیرشاخه (subdirectory) از پروژه‌ی دیگر نگاشت می‌شود. وقتی شما یک ادغام subtree مشخص می‌کنید، Git اغلب به اندازه کافی هوشمند است که تشخیص دهد یکی زیرمجموعه‌ی دیگری است و آن‌ها را به درستی ادغام کند.

ما از طریق یک مثال جلو می‌رویم: اضافه کردن یک پروژه‌ی جداگانه به یک پروژه‌ی موجود و سپس ادغام کدهای پروژه‌ی دوم در یک زیرشاخه از پروژه‌ی اول.

ابتدا، ما برنامه‌ی Rack را به پروژه‌مان اضافه می‌کنیم. پروژه‌ی Rack را به عنوان یک remote reference به پروژه‌ی خودمان اضافه می‌کنیم و سپس آن را در یک شاخه‌ی مجزا checkout می‌کنیم:

$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote --no-tags
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

حالا ما ریشه‌ی پروژه‌ی Rack را در شاخه‌ی rack_branch داریم و پروژه‌ی خودمان را در شاخه‌ی master. اگر یکی را checkout کنید و سپس دیگری را، می‌توانید ببینید که ریشه‌های متفاوتی دارند:

$ ls
AUTHORS         KNOWN-ISSUES   Rakefile      contrib         lib
COPYING         README         bin           example         test
$ git checkout master
Switched to branch "master"
$ ls
README

این کمی مفهوم عجیبی است. همه‌ی شاخه‌های موجود در مخزن شما لزوماً شاخه‌های یک پروژه نیستند. این کار رایج نیست، چون به ندرت کاربردی دارد، اما کاملاً ساده است که شاخه‌هایی با تاریخچه‌های کاملاً متفاوت داشته باشید.

در این حالت، ما می‌خواهیم پروژه‌ی Rack را به عنوان یک زیرشاخه به پروژه‌ی master بیاوریم. ما می‌توانیم این کار را در Git با دستور git read-tree انجام دهیم. شما در بخش (Git Internals) بیشتر درباره‌ی read-tree و ابزارهای مرتبطش یاد خواهید گرفت، اما فعلاً بدانید که این دستور درخت ریشه‌ی یک شاخه را به staging area و working directory فعلی شما می‌خواند. ما به شاخه‌ی master برگشتیم، و شاخه‌ی rack_branch را به زیرشاخه‌ی rack در شاخه‌ی master پروژه‌ی اصلی خودمان کشیدیم:

$ git read-tree --prefix=rack/ -u rack_branch

وقتی commit می‌کنیم، به نظر می‌رسد همه‌ی فایل‌های Rack زیر آن زیرشاخه قرار دارند — انگار آن‌ها را از یک tarball کپی کرده‌ایم. نکته‌ی جالب این است که ما می‌توانیم تغییرات را نسبتاً راحت بین شاخه‌ها ادغام کنیم. پس اگر پروژه‌ی Rack به‌روزرسانی شد، می‌توانیم تغییرات upstream را با سوییچ کردن به آن شاخه و pull گرفتن دریافت کنیم:

$ git checkout rack_branch
$ git pull

سپس می‌توانیم آن تغییرات را دوباره به شاخه‌ی master ادغام کنیم. برای گرفتن تغییرات و آماده کردن پیام commit به‌طور خودکار، از گزینه‌ی --squash همراه با استراتژی ادغام recursive و گزینه‌ی -Xsubtree استفاده کنید. استراتژی recursive اینجا پیش‌فرض است، اما برای وضوح آن را ذکر می‌کنیم.

$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

همه‌ی تغییرات پروژه‌ی Rack ادغام شده‌اند و آماده‌اند که به صورت محلی commit شوند. شما همچنین می‌توانید برعکس این کار را انجام دهید — تغییراتی را در زیرشاخه‌ی rack از شاخه‌ی master انجام دهید و سپس آن‌ها را به شاخه‌ی rack_branch ادغام کنید تا برای نگه‌دارندگان ارسال یا به upstream push شوند.

این روش به ما یک workflow مشابه workflow زیرماژول‌ها (submodules) می‌دهد، بدون اینکه از submodules استفاده کنیم (که در بخش سابماژول ها (Submodules) آن را پوشش خواهیم داد). ما می‌توانیم شاخه‌هایی با پروژه‌های مرتبط دیگر را در مخزن خود نگه داریم و هر از گاهی آن‌ها را به پروژه‌ی اصلی ادغام کنیم. این روش در بعضی جنبه‌ها خوب است، مثلاً همه‌ی کدها در یکجا commit می‌شوند. اما معایبی هم دارد، مثلاً کمی پیچیده‌تر است و احتمال خطا در یکپارچه‌سازی مجدد تغییرات یا push تصادفی یک شاخه به یک مخزن غیرمرتبط بیشتر می‌شود.

یک نکته‌ی عجیب دیگر این است که برای گرفتن diff بین آنچه در زیرشاخه‌ی rack دارید و کدی که در شاخه‌ی rack_branch وجود دارد — برای اینکه ببینید آیا نیاز به ادغام دارند یا نه — نمی‌توانید از دستور معمول diff استفاده کنید. در عوض باید git diff-tree را با شاخه‌ای که می‌خواهید با آن مقایسه کنید اجرا کنید:

$ git diff-tree -p rack_branch

یا، برای مقایسه‌ی آنچه در زیرشاخه‌ی rack دارید با آنچه شاخه‌ی master روی سرور آخرین بار که fetch کرده‌اید بوده است، می‌توانید اجرا کنید:

$ git diff-tree -p rack_remote/master
scroll-to-top