آموزش مبتدی Git: بخش دوم

Tracking changes in Git
Tracking changes in Git

این پست ادامه مطلب دیگری است که مطالعه آن به دانش بخش قبلی نیاز دارد:

https://vrgl.ir/TatUg

مقدمه

در سیستم کنترل گیت بازگردانی محتویات فایل در چند مقطع امکان پذیر است:

  • هنگام انجام تغییرات (Working)
  • هنگام ردیابی کردن تغییرات (Staging)
  • هنگام ثبت تغییرات در تاریخچه (Committing)

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

در مسیر دلخواه یک دایرکتوری ایجاد کرده و به داخل آن می‌رویم. این بار ابتدا چندین فایل آماده را به پروژه اضافه کرده و سپس گیت را فراخوانی می‌کنیم:

$ touch file.txt readme.md

مرسوم است فایل readme با پسوند md در ریشه دایرکتوری گیت ایجاد شود تا توضیحات در آن ارائه گردد.

گیت را فراخوانی کرده و فایل های موجود را در آن ثبت می‌کنیم. کامیت Initial commit اولین نقطه قابل بازگشت پروژه ما است که هیچ محتوایی درون file.txt و readme.md قرار ندارد. می‌خواهیم مقاطع مختلف بازرگردانی را بررسی کنیم.


بازگردانی

مقطع Working directory

این مقطع زمانی است که تغییراتی در فایل ها ایجاد و آن ها را ذخیره می‌کنیم اما در گیت اضافه نمی‌کنیم. می‌توان با دستور echo بدون باز کردن فایل، به آن متنی اضافه کرد:

$ echo 'Hello, World!' >> file.txt

متن جدید به فایل اضافه شده‌ است اما برفرض این کار به اشتباه انجام شده و می‌خواهیم آن را لغو کنیم. دقت داشته باشید این تغییر حتی به Stage هم اضافه نشده است. دستور زیر را استفاده می‌کنیم:

$ git checkout -- file.txt

تا به الان از دستور checkout برای ایجاد و تغییر شاخه ها استفاده می‌کردیم اما اینبار با نوشتن دو خط تیره و سپس نام فایل می‌توانیم تغییرات Stage نشده را لغو کنیم.

تغییرات اعمال شده بر روی file.txt لغو شده و در نتیجه به حالت کامیت Initial بازگشتیم.


مقطع Staging area

متن جدیدی همزمان به جفت فایل پروژه با دستور زیر اضافه می‌کنیم:

$ echo 'this file is not empty' | tee file.txt readme.txt

تغییرات را به مرحله Stage می‌بریم. در این مرحله برحسب فرض از آماده کردن فایل ها برای کامیت پشیمان شدیم. با دستور reset می‌توانیم فایل ها را از مقطع Stage به Working directory بازگردانیم:

$ git reset HEAD

توجه داشته باشید اگر فقط یک فایل را می‌خواهید بازگردانید کافی است نام آن را روبروی HEAD بنویسید.

حال می‌توان مانند مقطع قبلی تغییرات Stage نشده را این بار برای هر دو فایل لغو کرد:

$ git checkout -- .

با موفقیت به مقطع Initial commit بازگشتیم.


مقطع Repository

این نوع بازگردانی را در بخش اول با دستور checkout انجام داده‌ایم. این بار روش دیگری را بررسی می‌کنیم.

$ echo 'Adding more new stuff' | tee file.txt readme.md > /dev/null

دستور بالا مانند قبل متن جدیدی را به چند فایل اضافه می‌کند با این تفاوت که آن را دوباره چاپ نمی‌کند.

تغییرات را Stage و سپس Commit می‌کنیم. برحسب فرض این تغییرات به اشتباه صورت گرفته است. این دستور تغییرات کامیت با مشخصه SHA-1 وارد شده را لغو کرده و محتوای فایل را بازمی‌گرداند:

$ git revert <SHA>

ویرایشگر تعریف شده در گیت (پیش فرض Vim) باز شده و به ترتیب:

  • نام کامیت جدید برای ثبت
  • مشخصه کامیت برگشت داده شده
  • راهنمای ثبت کامیت
  • شاخه در حال تغییر
  • لیست تغییرات

را ارائه می‌دهد. آن را ذخیره کرده و پس از خروج از ویرایشگر تغییرات به عنوان کامیت ثبت خواهد شد. برای ذخیره و خروج از ویرایشگر پیش فرض Vim ابتدا کلید ESC و سپس دستور زیر را بنویسید:

:x

تغییرات کامیت شده با موفقیت لغو شدند.


ویرایش

از دیگر قابلیت های گیت می‌توان به ویرایش کامیت ها اشاره کرد. فایل جدیدی ایجاد و آن را کامیت می‌کنیم.

بطور مثال پرونده جدیدی با پسوند rtf ایجاد کردیم اما به اشتباه آن را با پسوند docx کامیت کردیم. دستور:

$ git commit --amend

آخرین کامیت را برای تغییر در ویرایشگر پیش فرض باز می‌کند.

تغییرات لازم را ایجاد و از ویرایشگر خارج می‌شویم. در ویرایشگر vim کلید i به حالت تایپ رفته و پس از پایان کلید ESC و سپس با کاراکتر : و به دنبال آن حرف x از ویرایشگر با ذخیره تغییرات خارج می‌شویم.

نام و مشخصه کامیت با موفقیت تغییر می‌کند.


حالات دیگر

بازگردانی تغییرات با دستور reset فقط به Stage محدود نمی‌شود و کاربرد های دیگری هم دارد:

$ git reset --<MODE> <SHA>

سه حالت برای سویچ reset ممکن است:

  • نرم یا soft
  • سخت یا hard
  • ترکیبی یا mixed (پیش فرض)

در حالت نرم اشاره گر HEAD به مشخصه کامیت وارد شده تغییر و فهرست Stage تغییرات انجام شده پس از ریسِت کامیت را شامل می‌شود. با ثبت دستور git commit دوباره به کامیتی که از آن بازگشتیم، باز خواهیم گشت. در حالت ترکیبی اشاره گر HEAD به کامیت مشخص شده حرکت می‌کند و تنها تغییرات آن کامیت را در فهرست Stage شامل می‌شود و تمام تغییرات پس از آن در Working directory بصورت Untracked هستند. در حالت سخت اشاره گر به کامیت مشخص شده حرکت کرده اما تغییراتی که ثبت نشده‌اند و تغییرات کامیت های پس از کامیتی که اشاره گر به آن اشاره می‌کند از بین خواهند رفت. برای مثال:

$ git reset --hard <SHA>

پروژه ما شامل لیستی از تغییرات و کامیت ها می‌باشد. پس از ریست سخت به Initial commit تمام فایل ها و تغییرات و حتی تاریخچه کامیت ها از بین خواهند رفت.

حالت soft بیشترین کاربرد و حالت hard بیشترین خطر از دست رفتن تاریخچه را دارد.


چشم پوشی

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

چندین فایل جدید برای آزمایش تولید می‌کنیم:

$ touch 1.tmp 2.tmp 3.tmp password.txt crashlog.mdmp need.ext

از بین تمام این فایل ها برفرض فقط به need.ext در گیت نیاز خواهیم داشت. یک پوشه هم ایجاد می‌کنیم:

$ mkdir folder && touch folder/secret.file

برای چشم پوشی کردن در گیت باید یک فایل تحت نام gitignore ایجاد کنیم:

$ touch .gitignore

آن را با یک ویرایشگر متن باز می‌کنیم:

$ code .gitignore

برای چشم پوشی کردن از یک دایرکتوری و تمام محتویات داخل آن کافی است نام آن را بنویسیم:

اگر یک status دیگر بگیریم متوجه تغییر خواهیم شد.

گیت به folder دیگر اهمیتی نمی‌دهد. فرآیند افزودن فایل هم مانند دایرکتوری است.

از کاراکتر # برای افزودن کامنت یا توضیحات استفاده می‌شود و تاثیری در نحوه عملکرد گیت ندارد.

کاراکتر * به معنای همه می‌باشد. در اینجا تمام فایل ها با پسوند tmp و تمام پسوند ها با نام crashlog از دید گیت چشم پوشی و به ریپو اضافه نمی‌شوند. مرسوم است فایل gitignore جداگانه کامیت شود.

و در آخر بر حسب فرض فایل need.ext که تنها فایل مورد نیاز ما بود را کامیت خواهیم کرد. دقت داشته باشید gitignore تغییری در فایل ایجاد نمی‌کند بلکه تنها آن را در ساختار ریپو نادیده می‌گیرد.

کاراکتر ! برای مشخص کردن استثنا به کار می‌رود. در اینجا یعنی همه پسوند های tmp به غیر از عنوان 2.

قالب های زیادی برای gitignore وجود دارد و بر اساس کاربرد خود مانند زبان برنامه نویسی یا محیط توسعه یکپارچه (مانند Visual Studio) می‌توانید قالبی برای پروژه خود در این ریپو پیدا کنید:

https://github.com/github/gitignore


تاریخچه

با دستور log در بخش اول آشنا شدیم و گفتیم که اگر روبروی آن عددی بنویسیم به تعداد آن عدد تاریخچه ما محدود می‌شود. در این بخش چند سویچ دیگر را مشاهده خواهیم کرد. سویچ since برای نمایش تاریخچه از تاریخ مشخصی مانند سه روز پیش یا دیروز یا یک ساعت پیش و این چنین به کار می‌رود:

$ git log --since='yesterday'

یا

$ git log --since='1 hour ago'

سویچ دیگر until است که همراه since کاربرد دارد. مثلا از ساعت 12 تا ساعت مشخصی:

$ git log --since='12:00' --until='16:45:30'

سویچ پر کاربرد دیگری مشخصات فرد ثبت کننده کامیت است:

$ git log --author='foo'

یا

$ git log --author='foo@bar.baz'

همچنین راهی برای فیلتر کردن نام کامیت ها با استفاده از سویچ grep وجود دارد:

$ git log --grep='Create'

دستور دیگری برای مشاهده تغییرات اعمال شده در کامیت مشخص وجود دارد:

$ git log -p <HASH>

اگر تغییرات بین چندین فایل است می‌توانیم نام فایل مشخصی را به آن ارائه دهیم، برای مثال:

$ git log -p 861894c file.txt

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

$ git log --stat

و

$ git log --summary

که در صورت ادغام این دو سویچ می‌توان بیشترین جزئیات را برای هر کامیت در تاریخچه مشاهده کرد. حتی می‌توان تمام سویچ های گفته شده را در کنار هم به کار برد:

$ git log --since='3 days ago' --until='yesterday' --author='foobar' --grep='file.txt' --oneline -5

فهرست 5 کامیت آخری که از 3 روز پیش تا دیروز توسط foobar شامل file.txt ثبت شده را خطی نمایش بده!

برای مشاهده تمام قابلیت های git log به مستندات رسمی گیت مراجعه کنید:

https://git-scm.com/docs/git-log


شاخه بندی

گاهی لازم است که تغییرات جدیدی به پروژه خود اضافه کنیم بدون آنکه اشتباهی در اسناد مهم خود مرتکب شویم. گاهی اوقات هم در یک تیم بر روی بخش های مختلف یک فایل کار می‌کنیم و تغییرات هر شخص ممکن است باعث از بین رفتن یا عملکرد نادرست تغییرات افراد دیگری شود. برای رفع این مشکل و اضافه کردن مجموعه تغییرات جدید، شاخه های جدیدی در کنار شاخه master ایجاد کرده و در پایان ادغام می‌کنیم. در بخش اول با دستور branch آشنا شدیم که فهرستی از شاخه های موجود ارائه می‌کند. کافی است روبروی دستور branch از یک نام دلخواه استفاده کنیم تا آن شاخه ایجاد شود:

$ git branch <NAME>

در صورت نیاز برای پاک کردن شاخه:

$ git branch -d <NAME>

و پاک کردن شاخه ای که شامل تغییرات ادغام نشده است:

$ git branch -D <NAME>

و برای رفتن به شاخه جدید ایجاد شده:

$ git checkout <NAME>

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

$ git checkout -b <NAME>

شاخه ای جدید ایجاد و به دلخواه تغییراتی در آن اعمال و ثبت می‌کنیم. به یاد داشته باشید می‌توانید نام شاخه فعلی را تغییر دهید:

$ git branch -m <NAME>

این تغییر در تاریخچه به همراه شاخه ای که به آن تعلق دارد ثبت می‌گردد. اما این تاریخچه اختصاصی است!


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

$ git diff master..<BRANCH>

نوبت ادغام رسیده است. در بخش اول عمل merge را اینگونه انجام داده‌ایم:

$ git merge <BRANCH>

به این حالت fast-forward گفته می‌شود که تمام کامیت های شاخه مورد نظر را ادغام می‌کند. حالت دیگر:

$ git merge <BRANCH> --no-ff

حالت عادی merge برای زمانی استفاده می‌شود که محتوای گیت تغییر کرده و آن تغییرات از master جلوتر است اما در حالت بدون fast-forward زمانی کاربرد دارد که ساختار گیت در دو شاخه متفاوت باشد. این دستور مانند یک کامیت ثبت شده و نیاز به توضیحات دارد.


حالت سریعتر برای نوشتن پیام کامیت fast-forward این گونه است:

$ git merge <BRANCH> --no-ff --message='<DESC>' 

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

$ git log --oneline --graph

احتمال دارد هنگام merge کردن با مشکل تعارض (conflict) برخورد کنید. در این صورت مانند بخش قبل با ویرایشگر متن فایل های مورد تعارض را تغییر داده و آن را دوباره Stage و کامیت کنید.


انباشتن

هرزگاهی نیاز است هنگام اعمال تغییرات به ریپوی گیت، شاخه جدیدی ایجاد کرد. در صورت داشتن تغییرات کامیت نشده در Staging area موقع ایجاد شاخه جدید، فهرست تغییرات Stage به شاخه ساخته شده انتقال پیدا می‌کند که می‌تواند اثر مخربی داشته باشد. راهکار گیت برای تغییراتی که آماده کامیت نیستند اما باید کنار گذاشته شوند دستور stash است که تغییرات را انبار می‌کند.

تغییراتی اعمال می‌کنیم. برای انبار کردن آن از دستور زیر استفاده می‌کنیم:

$ git stash save '<DESC>'

برای مشاهده فهرست ذخایر انبار از این دستور استفاده می‌کنیم:

$ git stash list

و برای مشاهده تغییرات انباشته شده هر مشخصه در فهرست:

$ git stash show <STASH_ID>

برای فراخوانی آخرین تغییرات انباشته شده را با دستور زیر برمی‌گردانیم:

$ git stash pop

یا با استفاده از مشخصه آن، دستور زیر را به کار می‌بریم:

$ git stash pop <STASH_ID>

دستور pop تغییر انبار شده را فراخوانی و آن را از انبار پاک می‌کند. شاید بخواهیم چندین بار از تغییر انبار شده در آینده استفاده کنیم بدون آنکه از انبار پاک شود، در چنین شرایطی به جای pop از apply استفاده می‌کنیم:

$ git stash apply <STASH_ID>

در صورت نیاز برای لغو تغییرات انبار شده از این دستور استفاده می‌کنیم:

$ git stash drop <STASH_ID>

اگر انبار تغییراتی زیادی را ذخیره کرده باشد اما به هیچکدام از آن ها نیاز نداشته باشیم این دستور جایز است:

$ git stash clear


در بخش بعدی کار با گیت بصورت remote و مطالب مربوط به همکاری تیمی گفته خواهد شد:

https://vrgl.ir/WnfSe

موفق باشید ?