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

Distributed version-control system
Distributed version-control system

در این مطلب قصد دارم نحوه کار با گیت را به شما توضیح بدهم. پیش نیاز این مبحث مطلب زیر ‌می‌باشد:

https://vrgl.ir/gExZt

مفاهیم

همانطور که در پست قبلی اشاره کردم:

"گیت یک سیستم کنترل نسخه برای نظارت بر روی تغییرات اعمال شده در طول توسعه نرم افزار می‌باشد."

ابتدا با مفهوم کنترل نسخه (Version Control) آشنا خواهیم شد. وظیفه کنترل نسخه ثبت تغییرات و ارائه تاریخچه ای از آن ها است. به شرح تصویر برای جلوگیری از چنین کاری:

کنترل نسخه دستی =))
کنترل نسخه دستی =))

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

  • سریع و کم حجم
  • شاخه بندی
  • تاریخچه عملیات
  • بازگردانی تغییرات
  • مقایسه تغییرات

?مباحث این مطلب در محیط خط فرمان Git Bash توضیح داده شده‌اند اما در نرم افزار های دیگر هم قابل استفاده هستند.

git-bash.exe
git-bash.exe

تنظیمات

سیستم گیت مانند هر نرم افزار دیگری دارای تنظیمات مختص به خود می‌باشد. تنظیمات آن به سه بخش تقسیم می‌شود:

  • تنظیم مجزا برای هر پروژه (Local)
  • تنظیم کلی برای تمام پروژه های کاربر فعلی (Global)
  • تنظیم کلی برای تمام پروژه های کاربران سیستم عامل (System)

یک بار تنظیم Global اکثر نیاز های شما را پاسخ می‌دهد. این تنظیمات در یک فایل کانفیگ با نام .gitconfig در دایرکتوری یوزر شما (C:\Users\<Username>\.gitconfig) ذخیره می‌شوند. این فایل از ترمینال Git Bash با این دستور قابل دسترس است:

$ git config --global --edit

اگر در صورت نوشتن این دستور با محیط ترمینال وحشتناکی مواجه شدید حتما دارید از Vim استفاده می‌کنید:

؟?؟
؟?؟

نگران نباشید برای خروج از Vim متن زیر را تایپ و کلید اینتر را بزنید:

:qa!

توصیه می‌شود در صورت نصب بودن از Visual Studio Code استفاده کنید:

$ code .gitconfig

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

$ git config --global user.name 'foo bar'

و برای تعریف ایمیل:

$ git config --global user.email 'foo@bar.baz'
کانفیگ گلوبال در git bash
کانفیگ گلوبال در git bash

حال اگر دوباره به gitconfig نگاه کنید شاهد ذخیره شدن این تغییرات خواهید بود

code .gitconfig
code .gitconfig

برای مشاهده تمام تنظیمات اعمال شده:

$ git config --list

تنظیم Local هم شبیه به Global است با این تفاوت که gitconfig فقط config نامیده شده و داخل پوشه مخفی git در هر پروژه قرار دارد. نوشتن دستور git config بدون --global بصورت local اعمال می‌شود.

ایجاد

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

$ clear

بصورت پیش فرض Git Bash در دایرکتوری کاربر یعنی C:\Users اجرا می‌شود که در یونیکس معادل ~ است. برای رفتن به دسکتاپ می‌توان از دستور Change Directory استفاده کرد:

$ cd ~/Desktop

می‌خواهیم با دستور Make Directory پوشه ای بر روی صفحه دسکتاپ ایجاد کرده و به درون آن برویم:

$ mkdir myGit

و برای رفتن به داخل آن:

$ cd myGit

در صورت نیاز برای خارج شدن از یک فولدر کافی است روبروی دستور cd از دو کاراکتر نقطه استفاده کنید. همچنین می‌توان از دستور Print Working Directory از دایکرتوری فعلی اطمینان حاصل کرد:

$ pwd
ایجاد فولدر بر روی دسکتاپ و باز کردن آن
ایجاد فولدر بر روی دسکتاپ و باز کردن آن

به پروژه هایی که توسط گیت مدیریت می‌شوند اصطلاحا ریپو (ابتدای کلمه Repository) گفته می‌شود. برای ایجاد یک repo خالی کافی است از دستور زیر استفاده کنیم:

$ git init

این دستور یک پوشه مخفی git درون دایکرتوری فعلی ایجاد می‌کند. با دستور List Structure دیده می‌شود:

$ ls -a
.git
.git

تا اینجای کار یک ریپو ایجاد کردیم. ریپوی گیت برای ارائه کنترل نسخه نیاز به محتوا برای کار کردن دارد پس:

$ touch note.txt

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

$ git status
ایجاد فایل و بررسی وضعیت آن
ایجاد فایل و بررسی وضعیت آن

به متن قرمز رنگ دقت کنید، اسامی فایل های Untracked در اینجا نمایان می‌شوند. قبل از ثبت شدن فایل ها به تاریخچه گیت، فایل ها باید آماده ثبت شوند که به آن Stage کردن می‌گویند:

$ git add note.txt
آوردن فایل به Stage
آوردن فایل به Stage

وضعیت فایل از Untracked تغییر کرده و به رنگ سبز در آمده است. حال فایل Stage شده آماده ثبت در تاریخچه گیت می‌باشد که به آن Commit گفته می‌شود. عمل Commit نیازمند ارائه توضیحاتی در مورد تغییرات رخ داده در ریپو است که بطور متداول ابتدای آن با فعل امری از عمل رخ داده نوشته می‌شود:

$ git commit -m &quotCreate note.txt&quot

اولین کامیت به گیت ارسال شده است و در آینده می‌توان وضعیت فایل را به وضعیت کامیت های ثبت شده بازگرداند. این نکته را هم به یاد داشته باشید که مرسوم است متن اولین کامیت "Initial commit" ثبت شود. برای مشاهده کامیت های ثبت شده در ریپو از دستور زیر استفاده کنید:

$ git log

دستور log اطلاعاتی همچون مشخصه کامیت به همراه شاخه متعلق به آن، نام و آدرس ایمیل فرد ثبت کننده، تاریخ دقیق و جزئیات کامیت را چاپ می‌کند. مشخصه های کامیت با الگوریتم رمزنگاری SHA-1 تولید و Hash می‌شوند. مشاهده log مرتب و خطی نیز بدین صورت است:

$ git log --oneline
دو نوع نمایش git log
دو نوع نمایش git log

تصویر زیر نحوه ثبت شدن فایل در ریپو را شرح می‌دهد، آن را به خاطر بسپارید:

How git works
How git works

در ادامه برای مرور می‌خواهیم چندین فایل جدید به ریپو اضافه کنیم:

$ touch file1 file2 file3

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

ایجاد چندین فایل
ایجاد چندین فایل

فایل های جدید در حالت Untracked قرار گرفتند. می‌توان تمام تغییرات را همزمان به Staging اضافه کرد:

$ git add .

در صورت منصرف شدن از Stage کردن:

$ git restore --staged .

و مانند قبل برای کامیت فایل های Stage شده:

$ git commit -m &quotAdd multiple files&quot
کامیت چندین فایل
کامیت چندین فایل

کامیت ها دنباله ای از لیست های پیوندی هستند که علاوه بر تغییرات فعلی، کامیت های قبل خود را نیز شامل می‌شوند. کامیت ها فقط می‌توانند یک والد (Parent) داشته باشند مگر اینکه از دو شاخه (Branch) ادغام (Merge) شده باشند.

لیست پیوندی
لیست پیوندی


حذف

حذف یک یا چند فایل از ریپو نیز با کامیت یک مجزا امکان پذیر است. در حالاتی که می‌خواهید تمام فایل ها را با git add به stage برده و آن ها را کامیت کنید، این دستور جایگزین سریعتری است:

$ git commit -am &quotRemove file&quot

اما استفاده از این نوع کامیت دقت کار را کاهش داده و از همین رو استفاده از آن پیشنهاد نمی‌شود.

(git add + git commit)
(git add + git commit)

کنترل نسخه گیت فقط به حذف و اضافه پرونده ها محدود نمی‌شود بلکه محتویات درون آن ها را هم نظارت می‌کند که در ادامه خواهیم دید.


تغییر

می‌خواهیم تغییری در یکی از فایل ها ایجاد کرده و آن را به تاریخچه گیت اضافه کنیم. یکی از خصوصیات برنامه Visual Studio Code تشخیص خودکار ریپو و باز کردن آن بصورت Workspace می‌باشد. کافی است داخل دایرکتوری ریپو دستور زیر را بنویسیم:

$ code .

یکی از فایل های Workspace باز شده را از پنل Explorer سمت چپ باز کرده و متنی داخل آن می‌نویسیم. فایل را با کلید میانبر Ctrl + S ذخیره می‌کنیم.

نوشتن متن در note.txt و ذخیره آن
نوشتن متن در note.txt و ذخیره آن

سپس دوباره به ترمینال Git Bash رفته و status می‌گیریم:

بررسی تغییرات
بررسی تغییرات

همانطور که مشاهده می‌کنید دیگر متن Untracked نمایان نمی‌شود زیرا گیت در حال Track کردن تغییرات آن است و متن not staged در اینجا دیده می‌شود. این بدان معناست که گیت بر روی این فایل نظارت دارد و تغییرات آن را با متن قرمز رنگ modified اعلام می‌کند. مانند قبل برای ثبت تغییرات:

$ git add note.txt

سعی کنید برای کامیت پیامی بنویسید که حداقل برای خودتان و در پروژه های تیمی برای همه واضح باشد:

$ git commit -m &quotUpdate note.txt with greetings&quot


?دستورات گیت در ترمینال Microsoft Visual Studio Code نیز قابل اجرا هستند:

 Ctrl + Shift + `
Ctrl + Shift + `

می‌خواهیم با یکی دیگر از دستورات مفید گیت آشنا شویم. تغییرات بیشتری داخل note.txt ایجاد می‌کنیم:

چند خط به note.txt اضافه کردیم
چند خط به note.txt اضافه کردیم

برای اینکه از جرئیات تغییرات اعمال شده مطلع شویم کافی است دستور زیر را بنویسیم:

$ git diff
تغییرات فایل note.txt
تغییرات فایل note.txt

تغییرات قبل و بعد نمایش داده می‌شود. دقت کنید تغییرات note.txt در Stage قرار نگرفته‌اند. پس از اضافه کردن فایل به Stage اگر از دستور git diff استفاده کنیم با همچین رخدادی روبرو خواهیم شد:

تغییر بی تغییر؟
تغییر بی تغییر؟

باید برای مشاهده جزئیات تغییرات فایل های Stage شده از دستور زیر استفاده کنیم:

$ git diff --staged

پس از مطمئن شدن از تغییرات آن ها را کامیت می‌کنیم:

$ git commit -m &quotAdd new lines to note.txt&quot

از دیگر مشکلات این چنین که ممکن است با آن روبرو شویم هنگام تغییر نام فایل است. از دستور یونیکس Move برای انتقال فایل به فایل دیگری و از این رو تغییر نام آن استفاده می‌کنیم:

(چند بار جمله قبل رو توی ذهنتون تکرار کنید ? )

$ mv file2 file_new

پارامتر اول نام فایل قدیمی و پارامتر دوم نام فایل جدید است یعنی file2 به file_new تغییر نام داده است:

rename with mv
rename with mv

اما از دید گیت اتفاق دیگری رخ داده به این صورت که file2 حذف و file_new ایجاد شده است.

file2 =/= file_new
file2 =/= file_new

شیوه صحیح تعریف کردن این عمل تغییر نام به گیت بدین صورت است که ابتدا فایل جدید را به Staging می‌بریم:

$ git add file_new

و سپس فایل قبلی را پاک می‌کنیم:

$ git rm file2
file2 == file_new
file2 == file_new

توانستیم با موفقیت گیت را از این تغییر با خبر کنیم و حالا نوبت ثبت این تغییر است:

$ git commit -m &quotRename file2 to file_new&quot

به یاد داشته باشید می‌توانید تعداد نتایج git log خود را با نوشتن یک عدد مقابل آن محدود کنید:

$ git log -3 --oneline
فقط نمایش سه کامیت آخر
فقط نمایش سه کامیت آخر


بازگردانی

همانطور که در ابتدای مطلب اشاره شد، گیت یک سیستم کنترل نسخه است به این معنی که تاریخچه ای از تمام تغییرات نگهداری می‌کند. این تغییرات قابل بازیابی هستند و گیت می‌تواند حالت فعلی ریپو را به یک نسخه‌ی قدیمی‌تر بازگرداند. برای این کار باید از دستور checkout به همراه کل یا بخشی از مشخصه SHA-1 کامیت (مانند مشخصه oneline) استفاده کنیم.

$ git checkout <SHA>
سفر در زمان
سفر در زمان

می‌خواهیم به قبل اضافه شدن خطوط جدید به فایل note.txt بازگردیم. مشخصه آن برای شما متفاوت است.

جدا شدن HEAD
جدا شدن HEAD

تا به الان با کلمه HEAD زیاد مواجه شده‌ایم. در واقع HEAD یک اشاره گر (pointer) به شاخه فعلی است. متن آبی رنگ در تصویر را با دقت نگاه کنید، این اشاره گر با دستور Concatenate هم قابل فراخوانی است:

$ cat .git/HEAD
اشاره HEAD به کامیت در تاریخچه
اشاره HEAD به کامیت در تاریخچه

در حالت عادی HEAD به refs/heads/master اشاره می‌کند. فایل note.txt را باز می‌کنیم.

مشاهده کامیت
مشاهده کامیت

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

کامیت جدید
کامیت جدید

پس از تغییر در زمان گذشته می‌خواهیم به زمان حال برگردیم:

$ git checkout master

اما اتفاقی ناخوشایند در انتظار ما است!

تغییر در گذشته زمان حال را تغییر نمی‌دهد
تغییر در گذشته زمان حال را تغییر نمی‌دهد

تغییراتی که در گذشته اعمال کردیم به زمان حال انتقال پیدا نکرده‌اند. فایل note.txt را باز کنید و ببینید.

$ cat note.txt
محتویات فایل تغییری نکرد
محتویات فایل تغییری نکرد

چه اتفاقی رخ داده است؟

master branch
master branch

تغییرات detached به شاخه master تعلق ندارند و کامیت 1f0517d به هیچ شاخه ای متصل نیست. می‌خواهیم تغییراتی که در گذشته اعمال کردیم جایگزین محتوای فعلی note.txt شوند اما checkout کردن کامیت از طریق SHA غیرمتصل ممکن نیست. از تاریخچه HEAD با دستور reflog می‌توان آن را پیدا کرد:

$ git reflog
بررسی تاریخچه قرارگیری HEAD
بررسی تاریخچه قرارگیری HEAD

کافی است مشخصه اشاره گر را checkout کنیم:

$ git checkout HEAD@{1}

محتوای فایل را در صورت نیاز تغییر داده و پس از ذخیره آن را کامیت می‌کنیم:

تغییر کلی متن و کامیت آن
تغییر کلی متن و کامیت آن

برای اطمینان log را بررسی می‌کنیم:

کامیت های detached
کامیت های detached

نوبت ایجاد یک شاخه جدید است. برای این کار از سویچ b مقابل دستور checkout استفاده می‌کنیم:

$ git checkout -b <NAME>
ایجاد شاخه و ورود به آن
ایجاد شاخه و ورود به آن

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

$ git branch
لیست شاخه ها
لیست شاخه ها

متن سبز رنگ و کاراکتر asterisk کنار آن نشانگر شاخه فعلی است. شاخه مانند دو جهان موازی در کنار هم وجود دارند اما سرانجام باید به شاخه اصلی یعنی master ختم شوند. برای اعمال تغییرات شاخه جدید در master باید عمل ادغام (merge) صورت گیرد. برای ادغام به شاخه ای که قرار است عمل merge بر روی آن انجام شود checkout می‌کنیم. این بار تغییرات اعمال شده بر روی branch قرار دارند و مستقیما قابل دستیابی هستند.

می‌توانید برای متوجه شدن این عمل جفت فایل note.txt در master و شاخه جدید را cat کنید:

$ cat note.txt

تغییر شاخه

$ git checkout master

بررسی مجدد

$ cat note.txt
تفاوت note.txt در دو شاخه موازی
تفاوت note.txt در دو شاخه موازی

تغییرات temp-fix-note را در master ادغام می‌کنیم:

$ git merge <temp-fix-note>

اما با مشکل رایجی تحت نام Conflict برخورد خواهیم کرد.

تعارض در ادغام
تعارض در ادغام

همانطور که مشاهده می‌کنید گیت پیغام وجود conflict در فایل note.txt را گزارش می‌دهد. این اتفاق زمانی رخ می‌دهد که محتوای فایل در شاخه های مختلف احتمال جایگزینی و از بین رفتن محتویات فعلی شوند. اگر از دستور cat دوباره استفاده کنید متوجه خواهید شد که گیت تفاوت این دو فایل را نمایش می‌دهد:

تفاوت در ادغام
تفاوت در ادغام

متن Hello, World در هر دو یکسان است اما از جایی که مشخص گردیده تفاوت دو شاخه دیده می‌شود. برنامه VS Code این تفاوت را بهتر نمایش می‌دهد:

$ code note.txt
افزونه GitLens
افزونه GitLens

رفع conflict به عهده خودتان هست. باید یکی از محتویات یا هر دو را پاک کنید. برای سادگی کار تمام متن بعد از Hello, World را پاک و آن را کامیت می‌کنیم:

$ git commit -a -m &quotResolve merge conflict by deleting both&quot

و در نهایت برای اطمینان عمل ادغام را تکرار می‌کنیم:

$ git merge temp-fix-note
ادغام موفقیت آمیز
ادغام موفقیت آمیز

در بخش بعدی نگاه عمیق تری به شاخه ها و دستوراتی همچون reset و revert خواهیم داشت:

https://vrgl.ir/0JsOI

موفق باشید ?