این پست با هدف یادآوری نکات مهم گیت (Git) نوشته شده. اگر به دنبال یافتن سریع کامندهای موردنظرتون هستید، Git Explorer رو ببینید. از طرفی، همهی IDEهای معروف مثل Visual Studio و محصولات Jetbrains مثل PyCharm، امکانات git رو به شکل گرافیکی و بدون نیاز به نوشتن دستورات، در اختیارتون قرار میدن. بنابراین ممکنه نیازی به نوشتن دستورات نداشته باشید، اما همچنان دانستن مفهوم هر دستور، لازمه. فرض بر اینه که اهمیتِ استفاده از git رو میدونیم، میریم سراغ استفادهاش. اطلاعات کامل، دقیق و بهروز رو میتونید در مستندات git یا کتاب git که نسخه فارسی هم داره ببینید.
اگر پیشنهادی برای اصلاح پست دارید، میتونید از طریق دکمهی "پیشنهاد اصلاح متن" وارد ریپازیتوری وبلاگ در github بشید و تغییرات مورد نظرتون رو پیشنهاد بدید.
نصب
git رو میتونید از طریق git-scm بنا به سیستم عاملی که دارید دانلود و نصب کنید. در زمان نصب هم اگر نیاز خاص و متفاوتی ندارید، همهی گزینهها رو در حالت پیشفرض بذارید و Next بزنید. بعد از نصب، در قسمت جستجوی سیستم عامل عبارت cmd رو جستجو کنید و عبارت زیر رو وارد کنید. اگر همه چیز خوب پیش رفته باشه، نسخهی git نصب شده رو نمایش خواهد داد، مثلا “git version 2.41.0.windows.2”.
|
|
لیستی از دستوراتِ در دسترس git رو میتونید با دستور زیر ببینید.
|
|
تنظیمات
به کمک دستورهای زیر میتونیم نام کاربری و ایمیلمون رو به تنظیمات git اضافه کنیم تا از این طریق شناخته بشیم. با فلگ local میتونیم تنظیمات پروژهای که روش کار میکنیم رو تعیین کنیم. با فلگ global تنظیمات رو برای همهی پروژههای user فعلی اعمال میکنیم. با فلگ system هم تنظیمات برای همهی پروژههای همهی userها اعمال میشه.
|
|
ساختار دستورات
هر دستور git، سه بخش داره. بخش [COMMAND] دستوری است که به git داده میشود. git در هر مرحله تنها یک دستور را اجرا میکند. در بخش [FLAGS] صفر یا تعدادی پرچم (flag) گذاشته میشود. پرچمها گاهی با یک خط تیره و گاهی با دو خط تیره نمایش داده میشوند. پرچمها دستور کلی را تغییر نمیدهند و به کمک آنها از حالات مختلف یک دستور میتوانیم بهره ببریم. در بخش [ARGUMENTS] صفر یا تعدادی آرگومان برای command نوشته میشود. این آرگومانها ورودیهای مورد نیاز برای command هستند.
|
|
ریپازیتوری
برای کار با git در ابتدا باید یک ریپازیتوری بسازیم. زمانی که ما یک پروژه در سیستم لوکال خود داریم، این پروژه فقط شامل فولدرها و فایلهای مربوط به آن است و تاریخچه و امکاناتی که git به ما میدهد را ندارد. برای اینکه در یک پروژه از قابلیتهای git استفاده کنیم باید آن را تبدیل به یک ریپازیتوری کنیم تا هم بتوانیم تاریخچه تغییرات را بررسی کنیم، هم به git نشان دهیم که پروژه شامل چه فایلها و چه فولدرهایی است و چه تغییراتی را باید نگهداری کند.
برای تبدیل یک دایرکتوری به یک ریپازیتوری باید از دستور git init استفاده کنیم. با این دستور میتوان یک ریپازیتوری اولیه و خالی که شامل فایلهای مورد نیاز برای git و فایلهای تمپلیت اولیه است را ساخت، یا یک پروژه موجود را به یک ریپازیتوری تبدیل کرد و از قابلیتهای git در آن استفاده کرد. با این روش میتوانیم یک ریپازیتوری لوکال در سیستم خود بسازیم.
|
|
ریپازیتوریِ راه دور
حال میخواهیم با چگونگی انتقال این پروژه به روی سرور آشنا شویم تا از این پس به صورت از راه دور (remote) پروژه خود را مدیریت کنیم. مزیت این کار آن است که در صورت تیمیبودن پروژه، افراد دیگر میتوانند به راحتی پروژه را از روی سرور دریافت کرده و با آن کار کنند. همچنین در صورت بروز هرگونه مشکلی در سیستم شما (مثلا پاک شدن حافظه)، پروژهتان بر روی سرور محفوظ خواهد بود. برای انتقال یک پروژهی محلی به یک سرور از دستور زیر استفاده میکنیم. در این دستور شما میتوانید یک نام برای ریپازیتوری remote خود انتخاب کنید. ما نام origin را انتخاب کردیم. شما با کمک این دستور میتوانید پروژه لوکال خود را با سرورهای مختلفی همگام کنید و برای اتصال با هر سرور یک نام انتخاب کنید. دقت کنید که این عمل فایلهای ما را روی ریپازیتوری remote نمیبرد و فقط ریپازیتوری remoteی همگام با ریپازیتوری محلی ما میسازد.
|
|
فرستادن ریپازیتوری به گیتهاب
فرض کنید نام کاربری اکانت گیتهاب من ali باشه و در اون، یک ریپازیتوری خالی به نام repo ساخته باشم. حالا قصد دارم پروژهای که در لپتاپ خودم دارم رو روی گیتهاب قرار بدم. اگر قبلا اون پروژه رو به ریپازیتوری تبدیل نکرده باشم (git init نکرده باشم)، از طریق دستورات زیر، در محلِ پروژه، میتونم اون رو به گیتهاب بفرستم.
|
|
اگر قبلا اون رو به ریپازیتوری تبدیل کرده باشم (git init کرده باشم)، از طریق دستورات زیر، در محلِ پروژه، میتونم اون رو به گیتهاب بفرستم.
|
|
کلون ریپازیتوری ریموت
از این دستور در مواردی استفاده میشود که پروژه بر روی سیستم شما نیست و میخواهید بر روی پروژهای که روی سرور است کار کنید. در این حالت باید با دستور git clone یک کپی محلی از این پروژه را بر روی سیستم خودتان بیاورید. دستور clone به صورت کلی به شکل زیر است. در این دستور اگر به [LOCAL_PROJECT_NAME] مقدار دهیم، این مقدار به عنوان اسم پروژه در سیستم شما گذاشته میشود. در غیر این صورت نام پیشفرض پروژه بر روی سیستم ما قرار داده خواهد شد.
|
|
افزودن فایلها به گیت
اولین کاری که پس از ساخت ریپازیتوری باید بکنیم اضافه کردن فایلهای مورد نظرمان به آن است. در ابتدا همه فایلها از نظر git جزو Untracked files (فایلهایی که git تغییرات آنها را دنبال نمیکند) هستند و باید آنها را به ریپازیتوری اضافه کرد. برای اینکار از دستور git add استفاده میکنیم. به طور کلی تغییراتی که بر روی فایلهای پروژه خود انجام میدهیم در ریپازیتوری به طور خودکار ذخیره نمیشوند و باید این تغییرات را با یک commit ثبت کنیم.
تاریخچه فایلها در git از تعدادی commit تشکیل شده که هر کدام مانند یک کپی از همه فایلها در یک لحظه زمانی به همراه یک پیام هستند و نشان میدهند که در یک زمان خاص هر فایل (فایل track شده توسط git) در چه وضعیتی قرار داشته است. هر تغییری که ما بر روی پروژه خود انجام میدهیم برای شناسایی توسط git باید به صورت commit در بیاید. به طور کلی تغییرات در ریپازیتوری ما سه مرحله دارند:
- ابتدا این تغییرات در دایرکتوری ما روی فایلها اعمال میشوند.
- سپس باید این تغییرات را stage کنیم که مشخص میکنیم که از بین تغییرات مختلف کدام تغییرات را میخواهیم در commit بعدی لحاظ کنیم.
- در نهایت با دستور commit یک commit انجام میشود و تغییرات در git مشخص میشوند.
دستور اول، همهی فایلهای دایرکتوری فعلی را اضافه میکند.
دستور دوم، همهی فایلها به جز اونهایی که با . شروع میشوند را اضافه میکند.
دستور سوم، یک فایل خاص را اضافه میکند.
دستور چهارم فایلهای با یک پسوند خاص را اضافه میکند.
|
|
gitignore
گاهی فایلها و فولدرهایی هستند که به هر دلیل نمیخواهیم توسط گیت track بشن. با ساخت فایلی به نام .gitignore و معرفی این فایلها و فولدرها به اون، میشه این کار رو انجام داد. معمولا هر زبان برنامهنویسی و هر محیط توسعه (IDE) تعداد فایل اضافی تولید میکنه که باید ignore بشن. برای ساخت فایل gitignore، میشه از سایت gitignore.io استفاده کرد.
وضعیت فایلها
در هر لحظه از کار میتوان با استفاده از دستور git status وضعیت فایلها را مشاهده کرد.
|
|
وضعیتهای ممکن برای یک فایل به این صورت است:
- فایل توسط git تِرَک نمیشود (Untracked files).
- فایل شناسایی شده و تغییری در آن ایجاد نشده.
- فایل دارای تغییراتی است که وارد Staging Area نشده (Changes not staged for commit).
- که وارد Staging Area شده و میتوان آنها را commit کرد (Changes to be committed).
وقتی یک فایل جدید مثلا به نام file.txt میسازیم، اگر دستور git status رو بزنیم، وضعیت فایل untracked است، یعنی git اون رو پیگیری نمیکنه. اگر بخواهیم git این فایل رو پیگیری کنه و به حالت stage ببره، باید از دستور زیر استفاده کنیم.
|
|
حالا اگر مجددا دستور git status رو بزنیم، این فایل در دستهی Changes to be committedها قرار گرفته است.
commit
commit در واقع یک checkpoint در فرایند توسعه پروژه است. یعنی یک تسک کوچکِ معنادار رو تموم کردم!
وقتی فایلی یا فایلهایی track میشوند در واقع git تغییرات آنها رو پیگیری میکند و وقتی فایلی که track میشود، دچار تغییری میشود، git آن تغییرات را میفهمد ولی تا زمانی که به محیط stage اضافه نشود تغییراتش در commit اعمال نمیشود. در اصل با اولین اجرای دستور git add روی یک فایلی، آن فایل را track میکنیم و با هر بار تغییری که در فایل ایجاد میکنیم نیاز داریم آن فایل را به محیط stage اضافه کنیم تا تغییراتش در commitمان اعمال شود، پس دوباره دستور git add را برای فایل یا فایلهایی که میخواهیم در commitمان باشند اجرا میکنیم.
stage یک مرحله برزخی هست بین ثبت دائمی یک تغییر در تاریخچه ریپازیتوری (به اصطلاح commit کردن آن) و یا undo کردن و حذف آن تغییر (به اصطلاح reset کردن آن). با هر تغییر جدیدی که طی پروژه، در فایل file.txt ایجاد کنید لازمه که این مراحل یعنی add (وارد کردن فایل با تغییرات جدید به stage) و بعد هم commit یا reset مجدد انجام بشه.
هر commit، یک شناسهی یکتا، یک پیام و یک والد (که یک commit دیگر است) داره. git برخلاف ورژن کنترلرهای دیگه تغییرات رو ذخیره نمیکنه بلکه در هر commit، یک اسنپشات از کل فایلها رو نگه میداره. commit یک موجود immutable است، یعنی بعد از ایجاد، تغییرپذیر نیست. بعد از بردن فایلها به مرحلهی stage، با دستور زیر میشه یک commit ثبت کرد.
|
|
به جای message، پیامِ commit رو مینویسیم که مهمه، چون همتیمیها از روی اون متوجه تغییراتی که طی این commit انجام شده میشن و هم ممکنه در آینده لازم بشه به اون برگردیم. برای نوشتنش best practiceهایی وجود داره، به طور خلاصه:
- در متن پیام به جای توصیف چگونگی انجام تغییرات، دلیل commit را توضیح دهید
- حرف اول عنوان بزرگ نوشته شود
- طول عنوان بیشتر از 50 حرف نباشد
- عنوان به صورت امری نوشته شود، مثلا Add به جای Added یا Adding
- در انتهای عنوان نیازی به نشانهگذاری نیست
commitها رو atomic کنید، یعنی هر commit فقط یک کارِ کوچکِ معنادار انجام بده. این کارِ کوچک میتونه در چند فایل اثر بذاره، اما تا جای ممکن کارها رو به بخشهای کوچکتر تقسیم کنید تا هم کار برای کسانی که کد رو review میکنن سادهتر بشه، هم امکان بازگشت یا دریافت commit، بدون تاثیرات ناخواسته فراهم بشه.
commit –amend
با این دستور میتوان تغییرات موجود در بخش staging را با commit آخری که انجام شده ترکیب کرد. در واقع commit آخر حذف میشود و یک commit جدید شامل تغییرات آن commit به علاوه تغییرات جدید ایجاد میشود (commitها تغییرپذیر نیستند). همچنین میتوان فقط برای اصلاح پیام commit آخر نیز از این دستور استفاده کرد. فرض کنید آخرین commit به صورت زیر بوده است.
|
|
ولی file1 را به اشتباه add نکرده بودیم. حالا میتوان دو کار کرد.
- فایل file1 را add کرده و از دستور زیر استفاده کرد تا file1 هم به commit قبل اضافه شود.
|
|
- بدون add کردن file1 از دستور زیر صرفا برای تغییر پیام commit قبل استفاده کرد.
|
|
tag
با استفاده از دستور git tag میتوان در برخی نقاط بخصوص و دارای اهمیت commitی را علامتگذاری یا همان tag کرد. معمولا برای commitهایی که نشاندهنده نسخههای منتشرشده هستند از تگ استفاده میشود (مانند تگهای v1.0 یا v2.1 که در آنها v حرف اول version یا نسخه است). tagها یکتا هستند و tag تکراری نداریم. در git ما دو نوع تگ میتوانیم بسازیم. تگهای lightweight و annotated. تگهای lightweight در واقع یک پوینتر به یک commit هستند و اطلاعات دیگری را ذخیره نمیکنند. از طرف دیگر تگهای annotated دارای اطلاعات مختلفی مانند تاریخ، checksum، یک پیام برای تگ و … هستند.
دستور اول، به آخرین commit، تگ TAG_NAME از نوع lightweight میزند.
دستور دوم، به آخرین commit، تگ TAG_NAME از نوع annotated میزند.
دستور سوم، به commitِ با شناسهی a9c30aad7f تگ TAG_NAME از نوع annotated میزند.
دستور چهارم، لیست تگها را نشان میدهد.
دستور پنجم، در لیست تگها، جستجوی regexی انجام میدهد.
|
|
در حالت عادی، وقتی پروژه را push میکنیم، tagها push نمیشوند.
دستور اول، تگ v4.8.2 را به ریپازیتوری ریموت origin میفرستد.
دستور دوم، همه tagها را به ریپازیتوری ریموت origin میفرستد.
|
|
نسخهبندی معنایی
معمولا بر اساس میزان پیشرفت پروژه، به commitی که تغییرات کامل و معناداری داشته، tagی میزنیم که اصطلاحا به اون versioning (ورژن زدن) میگن. یکی از استانداردهای رایج برای بیان ورژنها، semantic versioning است که به صورت “MAJOR.MINOR.PATCH” بیان میشه، مثلا “4.8.2”.
شمارهی MAJOR زمانی تغییر میکنه که تغییرات API ناسازگار اعمال کرده باشیم، یعنی مثلا APIهای نسخهی جدید، با APIهای نسخهی قبلی که شمارهی MAJOR اون 3 بود ناسازگاری داره، بنابراین شمارهی MAJOR رو به 4 افزایش میدیم.
شمارهی MINOR زمانی تغییر میکنه که یک یا چند قابلیت جدید اضافه بشه، به شرطی که همچنان با APIهای نسخهی قبلی سازگار باشه. مثلا نسخهی “3.5.0” یک یا چند قابلیت جدید نسبت به نسخهی “3.4.0” داره، اما هر دو از APIهای یکسان استفاده میکنن.
شمارهی PATCH زمانی تغییر میکنه که تغییر کوچکی انجام داده باشیم، مثلا قابلیتی بسیار کوچک به برنامه اضافه بشه، bugی رفع بشه، test caseی اضافه شده باشه و…
rm
از دستور git rm برای حذف فایلها از ریپازیتوری استفاده میشود. در طول استفاده از دستور git rm، گیت چک میکند که فایلی که قرار است حذف شود، تغییر commitنشدهای نداشته باشد. اگر تغییر commitنشدهای وجود داشت؛ هشداری میدهد و حذف انجام نمیشود.
دستور اول، فایل file1 را هم از ریپازیتوری و هم از دایرکتوری لوکال حذف میکند.
دستور دوم، فایل file1 از ریپازیتوری حذف میکند ولی به صورت لوکال باقی میماند.
دستور سوم، دایرکتوری dir1 و تمام فایلها و زیردایرکتوریهای داخل آن را حذف میکند.
|
|
clean
گاهی بعد از اعمال تغییراتی که در پروژه انجام میدهیم یک سری فایلهای دنبال نشده به وجود میآیند که میخواهیم آنها را حذف کنیم در این شرایط از دستور git clean استفاده میکنیم. به صورت خلاصه شده کاربرد این دستور در پاک کردن فایلهایی است که در حالت untracked میباشد.
دستور اول، فایلهای untracked را حذف میکند.
دستور دوم، فایلها و فولدرهایی را که شامل هیچ فایل دنبالشدهای نباشند حذف میکند.
دستور سوم، به ما میگوید که اگر واقعا بخواهیم git clean -f را اجرا کنیم چه فایلهایی حذف میشوند، بدون این که واقعا این فایلها را حذف کند.
|
|
alias
در git، بعضی از دستورات، دستورات طولانیای هستند. مثلا، دستور زیر را در نظر بگیرید که فایل file.txt را از بخش staging خارج میکند.
|
|
میتوانیم برای عبارت بالا یک اسم مستعار یا alias تعریف کنیم و هر بار به جای نوشتن کل عبارت، تنها اسم مستعار آن را بنویسیم. مثلا دستور زیر برای این عبارت اسم مستعار unstage را قرار میدهد.
|
|
از این پس برای خارج کردن فایل file.txt از بخش staging کافی است دستور زیر را اجرا کنیم.
|
|
در GitAlias میتونید همهی configها و aliasها رو ببینید.
fetch
این دستور تنها محتوای پروژهی روی سرور را دریافت میکند. در دستور fetch، آخرین وضعیت پروژه از remote دریافت میشود. اما به صورت ایزوله نگهداری شده و با ریپازیتوری محلی merge نمیشود.
|
|
pull
این دستور برای دریافت آخرین تغییرات اعمال شده روی پروژه بر روی سرور است. در این حالت، آخرین وضعیت پروژه از remote دریافت میشود و با برنچ محلیای که الان در آن هستیم، merge میشود.
|
|
اگر هر دو دستور push و pull رو با هم انجام بدیم، پروژهی لوکال (درون سیستم) و پروژهی روی سرور، دقیقا یکی میشوند.
تفاوت fetch و pull
به بیان ساده، git fetch آخرین وضعیت یک پروژه از روی ریپازیتوری ریموت (remote repository) دریافت میکنه اما این تغییرات را روی ریپازیتوری محلی (local repository) اعمال نمیکنه، تا زمانی که ما با یک command دیگه به git دستور بدیم که این تغییرات اعمال بشه (به اصطلاح merge بشن). اما git pull یک دستور کلی تره، به محض اینکه آخرین وضعیت پروژه رو از ریپازیتوری ریموت (remote repository) دریافت میکنه، این تغییرات را روی محیط کاری شخصی ما (working directory) اعمال میکنه. در واقع دستور git pull مجموع دو دستور git fetch و merge به طور هم زمانه.
push
این دستور برای فرستادن تغییرات اخیر در پروژهی روی سیستم شما به سرور استفاده میشود.
|
|
branch
در توسعهی نرمافزار خیلی وقتها پیش میاد که لازم باشه تغییرات گستردهای در کد ایجاد بشه، فیچری اضافه بشه، کدی به صورت تست به برنامه اضافه بشه و… که تا مدتی (مثل چند روز یا چند ماه) ناپایداره یا مشخص نیست که قرار هست به کد اصلی اضافه بشه یا نه. برای همین نیاز داریم که یه تعداد commit انجام بدیم، بدون اینکه به کد پایدار برنامه خللی وارد شه و توی کار بقیه دخالتی انجام شه.
git راه حل سادهای در اختیارمون گذاشته به اسم branch (شاخه). در این شرایط بجای کپی گرفتن از مخزن کافیه که یک شاخهی جدید درست کنیم و روی اون شاخه کار کنیم. commitهایی که روی شاخهی جداگانه انجام میشن ایزوله هستند و تا زمانی که نخوایم با کد اصلی ترکیب نمیشن. هر مخزن gitای که ایجاد میشه به صورت پیشفرض شاخهای به اسم main داره و commitهامون روی main به صورت خطی، جلو میرن. اگر تاریخچهی git رو مثل یک درخت در نظر بگیریم، شاخهی main مثل تنهی درخت میمونه.
Head یک ارجاع به کامیت جاری (آخرین کامیت) روی شاخه کنونی محسوب میشود. به طور کلی Head در گیت میتواند به یک شاخه یا کامیت اشاره کند. زمانی که Head به یک شاخه اشاره بکند، گیت مشکلی نخواهد داشت. اما زمانی که Head به یک کامیت اشاره بکند؛ اما به شاخه آن اشاره نکند، گیت به حالت detached head میرود.
دستور اول، یک branch جدید با نام feature میسازد. این branch روی HEAD از branch فعلی جدا میشود.
دستور دوم و سوم، برای رفتن به برنچ feature استفاده میشود و HEAD را به آن منتقل میکند. (از دستور checkout میتوان برای رفتن به یک commit خاص هم استفاده کرد)
دستور چهارم، همزمان یک branch جدید با نام develop میسازد و آن را checkout میکند.
دستور پنجم، لیست branchها را نشان میدهد.
دستور ششم، برنچ develop را حذف میکند.
دستور هفتم، برای فرستادن یک branch با نام به سرور remote استفاده میشود.
اگر اولین بار برای پوش کردن branch به جای از دستور نهم استفاده کنیم، دفعات بعد میتوانیم صرفا git push را برای پوش کردن commitها اجرا کنیم و نیازی به آوردنِ دوبارهیِ نام branch نیست.
|
|
merge
زمانی که در یک پروژه از branchهای مختلف استفاده میکنیم و در هر branch تغییرات مختلفی اعمال میکنیم، در نهایت نیاز داریم که این تغییرات رو کنار هم داشته باشیم، در غیر این صورت این دو branch در واقعیت دو پروژه متفاوت هستند. برای این اعمال تغییرات در branchهای مختلف از ادغام (merge) استفاده میکنیم. مرج در git به معنی این است که تغییرات اعمال شده در یک branch مبدا را وارد branch مقصد کنیم (طیِ مرج، branch مبدا عوض نمیشه و فقط branch مقصد عوض میشه).
فرض کنید در حال توسعهی یک برنامه هستیم و برنچ main رو داریم. حالا تصمیم بر این شد که ویژگی جدیدی به برنامه اضافه بشه، بنابراین برنچ feature رو ساختیم. حالا در این برنچ دو commit انجام دادهایم و همتیمیها، برنچ main رو با commitهایشان جلو بُردهاند. حالا برنچ main commitهایی دارد که برنچ feature ندارد و برنچ feature کامیتهایی دارد که برنچ main ندارد.
اگر دو برنچ با نامهای main و feature داشته باشیم و بخواهیم تغییرات feature را در main اعمال کنیم باید به برنچ main رفته و از دستور زیر استفاده کنیم. در این شرایط یک commit جدید ساخته میشه که بهش merge commit میگن.
|
|
میدونیم که هر commit، یک والد (parent) داره. اما اینجا جاییه که این commit جدید، دو والد (parent) داره. در نتیجه، commit جدید، هم تاریخچهی commitهای برنچ main و هم تاریخچهی commitهای برنچ feature رو داره.
روشهای merge
در git، دو روش اصلی برای merge کردن داریم، Fast-forward و Three way. قدم اول توی هر دو روش اینه که دستور git merge دنبال یک گره/جد مشترک (common ancestor) بین این دو branch میگرده. این جد مشترک هست که مشخص میکنه که آیا merge ما از نوع Fast-forward قابل انجام است یا خیر.
روش Fast-forward فقط در حالتی توسط git قابل اجراست که ما یک مسیر خطی از نقطه مشترک رفته باشیم، یعنی مثل پایین درحالی که برنچِ Some Feature از main گرفته شده و تغییراتی هم به اون اضافه شده، گره مشترک (برنچ main) نسبت به زمانی که ما ازش منشعب شدیم تغییری نکرده (commitی بهش اضافه نشده). در این روش git اشارهگر برنچ main رو به نوک برنچ Some feature میبره، به طوری که انگار همهی این تغییرات واقعاً توی همین برنچ ما صورت گرفته.
در شکل پایین، برنچِ Some Feature از main گرفته شده و دو تا commit هم داشته، در حالی که خود main هم نسبت به زمانی که ما ازش منشعب شدیم تغییر کرده. توی این حالت دیگه Fast-forward قابل اجرا نیست. git در اینجور مواقع از روش three-way استفاده میکنه. اسمش از اینجا میاد که گیت باید سه تا اشارهگر رو بروزرسانی کنه.
conflict
اگر تغییراتی متناقض در برنچهای مختلف ایجاد کنیم چه اتفاقی میافتد؟ git در merge کردن به طور خودکار خیلی خوب عمل میکند. حتی اگر تغییرات مختلف روی یک فایل اعمال شده باشند، تا وقتی این تغییرات در بخشهای مختلف آن فایل هستند، git به صورت خودکار merge را انجام میدهد. یک نمونه رایج از زمانی که git نمیتواند merge خودکار انجام دهد، زمانی است که هر دو نسخه از پروژه یک خط خاص از یک فایل را به دو صورت مختلف تغییر داده باشند.
git تا جایی که ممکنه تغییرات همه رو با هم ترکیب میکنه اما یه جاهایی هست که چند نفر در برنچهای مختلف روی یک فایل تغییر ایجاد کردن و git نمیدونه چطور این تغییرات رو با هم ترکیب کنه. به این شرایط میگن conflict و خروج از این شرایط باید به صورت دستی انجام شه. یعنی خودمون باید تصمیم بگیریم که تغییراتِ فایلی که در هر دو برنچ تحت تاثیر قرار گرفته به چه شکل باشه. در گیت، conflict این طور نشون داده میشه.
|
|
rebase
rebase این امکان رو به ما میده که تغییرات رو طوری در branch مورد نظر اعمال کنیم که انگار هیچ تغییری توی مبدا اتفاق نیفتاده و به صورت خطی merge بشن و و دیگه نیازی به ساخت یک commit جداگانه نباشه. در حقیقت همونطور که از اسمش پیداست داره base برنچ رو (یا بخونید گره/جد مشترک) رو به وضعیت جدید تغییر میده. هدف اینکار تمیز نگه داشتن و خطی کردن تاریخچهی commitهاست.
عمل rebase سبب میشود تعدادی از کامیتهای برنچ اصلی تغییر کرده و id جدید بگیرند. در واقع این عمل تاریخچه برنچ را تغییر میدهد. پس در انجام این عمل محتاط باشید. در مجموع اکیدا توصیه میشود که در برنچهایی که به صورت گروهی روی آن کار میکنید، این عمل را انجام ندهید. بنابراین تلاش کنید تنها در پروژههایی که در سیستم محلی خودتان است یا در برنچهایی که فقط خودتان روی آنها کار میکنید از rebase استفاده کنید.
این سناریو رو در نظر بگیرید. در حال کار روی پروژه در برنچ main (بالا) هستیم. بعد از کامیت B، تصمیم اضافه کردن یک ویژگی جدید گرفته شده است. در branch جدید (پایین)، روی این ویژگی کار میکنیم (کامیتهای E و F)، همزمان، همتیمیهایمان تغییراتی را روی برنچ main قرار میدهند (کامیت C). ما نیاز به این تغییرات داریم. بنابرین با دریافت و merge کردن تغییرات، کامیت X به وجود میآید. همین روند تکرار میشود و کامیت Y به وجود میآید.
اما ما واقعا به کامیتهای X و Y نیازی نداشتیم و فقط برای اینکه تغییراتِ همتیمیهایمان را داشته باشیم، این commitها ایجاد شده اند. چه خوب میشد که در تاریخچهی commitها، این دو commit را نبینیم و تغییراتی که در branch خود دادهایم، در ادامهی تغییراتِ همتیمیهایمان باشد. در این حالت، merge کردن هم با استراتژیِ آسانترِ Fast-forward انجام خواهد شد. commitهای قرمز رنگ جدید هستند (hash متفاوتی دارند). کامیت H، همان کامیت E است. کامیت I، ترکیب کامیتهای F و X است. کامیت J، ترکیب کامیتهای G و Y است.
قصد داریم تغییرات برنچ feature در ادامهی برنچ main بیاید. ابتدا به برنچ feature میرویم، سپس دستور rebase را اجرا میکنیم.
|
|
log
گاهی نیازه که ببینیم در commitهای گذشته چه رخ داده و چه کسی و با چه توضیحی اون رو ایجاد کرده. گاهی نیاز به جستجو در commitها داریم تا مثلا بتونیم اونها رو بازیابی کنیم.
دستور اول، برای هر commitی که در branch فعلی انجام شده است، اطلاعاتی را نمایش میدهد. این اطلاعات عبارتند از:
- Commit: مقدار هش commit را نشان میدهد. این مقدار در واقع شناسه یکتای commit است (7 کاراکتر اول شناسهها هم با هم متفاوتاند).
- Author: اطلاعات مربوط به کسی که ایجاد کننده commit بوده را نمایش میدهد.
- Date: تاریخ و زمانی که این commit انجام شده است را نمایش میدهد.
- پیام مربوط به commit
دستور دوم، لاگهای یک branch خاص را نشان میدهد.
دستور سوم، لاگهای 5 کامیت آخر را نشان میدهد.
دستور چهارم، لاگهای commitهایی که توسط شخصی به نام Ali انجام شده است را نشان میدهد.
دستورات 5 و 6 و 7 و 8، لاگهای commitهایی که قبل از یک زمان خاص ایجاد شدهاند را نشان میدهد (برای دیدن commitهای از تاریخی به بعد نیز میتوانید از فلگ after مشابه before استفاده کنید).
دستور نهم، commitهایی که فایل main.py را تغییر دادهاند نشان میدهد.
دستور دهم، commitهایی را نمایش میدهد که در پیام آنها (که در زمان commit کردن با m- مشخص شدهاند)، عبارت AAA وجود داشته باشد.
|
|
گاهی بعد از گرفتن پروژه از ریپازیتوری remote، روی اون commitهای لوکال (در سیستم خودمون) انجام میدیم که هنوز به remote نفرستادهایم. git چطور متوجه میشه که آخرین commitِ دریافت شده از remote کدومه؟ متغیری به شکل remote/branch تعریف میکنه که آخرین commit دریافت شده از remote رو داره و با زدن دستور git log قابل دیدنه.
restore
گاهی میخواهیم فایل را به وضعیتی که در آخرین commit داشت برگردانیم و همهیِ تغییراتِ بعد از آخرین commit را حذف کنیم.
دستور اول، فایل test.txt را به وضعیتی که در آخرین commit داشت برمیگرداند و تغییراتِ انجام شده بعد از آخرین commit را حذف میکند.
دستور دوم، فایل test.txt را از stage خارج میکند اما تغییرات را حذف نمیکند.
دستور سوم، فایل test.txt را به وضعیتی که در اولین commit داشت برمیگرداند.
|
|
revert
دستور revert برای زمانی است که میخواهیم همه چیز به یک commit قبل بازگردد. git revert کامیت را حذف نمیکند، بلکه commit جدیدی میسازد که شامل برعکسِ تغییرات انجام شده است. این دستور به این صورت کار میکند که یک commit را میگیرد و یک commit جدید شامل معکوس تغییراتی که درون آن commit رخ داده میسازد. در واقع تغییری در تاریخچهی commitها به وجود نمیآورد و فقط یک commit اضافه میشود. (مناسب برای مواقعی که یک commit ناخواسته را روی remote فرستادهایم!)
دستور اول، تغییراتی که در کامیت abcdefgh رخ داده را برعکس میکند و با آنها یک commit جدید ایجاد میکند.
برای اینکه conflict رخ ندهد، بهتر است commitها به ترتیب revert شوند، مثلا اگر میخواهیم 3 کامیت به گذشته برویم و به ترتیب commit آخر، یکی مانده به آخر و دوتا مانده به آخر را revert کنیم، از دستور دوم استفاده میکنیم.
|
|
reset
برخلاف دستور revert برخی وقتها میخواهیم همه چیز حتی تاریخچه را نیز به commitی در گذشته برگردانیم و همه چیز را فراموش کنیم! برای این کار میتوان از دستور reset استفاده کرد. در واقع این دستور برخلاف revert، واقعا commitهای قبلی را پاک میکند.
دستور اول، با گرفتن یک commit علاوه بر پاک کردن تاریخچه و تغییر HEAD، تمامی فایلها را در هر مکانی به آن commit برمیگرداند. در صورتی که فایل تغییر کردهای در لوکال و یا فایل add شدهای به بخش staging داشته باشید، git آنها را در نظر نگرفته و آنها را از دست خواهید داد. اگر فایل جدیدی درست شده باشد که هنوز add نشده باشد یعنی untracked باشد این دستور تغییری در این فایل ایجاد نمیکند. ولی اگر این فایل را add کرده باشید (stage شده باشد ولی هنوز commit نشده باشد یا حتی commit هم شده باشد) دستور git reset –hard به آخرین commit این فایل را کلا حذف میکند. حتی در دایرکتوری لوکال نیز این فایل را نخواهید داشت.
دستور دوم، با گرفتن یک commit علاوه بر پاک کردن تاریخچه و تغییر HEAD، تمامی فایلها را در هر مکانی به جز working directory به آن commit برمیگرداند. در واقع در این حالت برخلاف حالت قبل، فایلهای تغییر کرده در لوکال را نگه میدارد اما در صورتی که فایل add شدهای به بخش staging داشته باشید، git آن را در نظر نگرفته و آن را از دست خواهید داد.
دستور سوم، با گرفتن یک commit تنها تاریخچه را حذف کرده و HEAD را به آن commit منتقل میکند. در واقع در این حالت، git فایلهای لوکال و add شده به بخش staging را برای شما نگه میدارد و تنها عملیات انتقال به commit موردنظر را انجام میدهد.
|
|
stash
فرض کنید در حال انجام تغییراتی در پروژه هستید و قبل از این که تغییراتتان کامل شود، متوجه میشوید که باید به branch دیگری بروید و مشکلی را در آنجا حل کنید. مشکلی که وجود دارد این است که اگر الان branchتان را عوض کنید، وضعیت فعلی پروژهتان از دست میرود و تغییرات به برنچ جدید میرود. همچنین چون هنوز تغییراتتان ناقص است، commit کردن آنها نیز کار خوبی نیست. در چنین شرایطی میتوانیم از دستور git stash استفاده کنیم. این دستور، یک کپی از حالت فعلی پروژهتان ذخیره میکند (stash را مثل صندوقچهای در نظر بگیرید که وضعیت فعلی پروژه موقتا وارد آن میشود) و به آخرین commit برمیگردد. بنابراین بعد از اجرای این دستور میتوانید به branch دیگر بروید و مشکل را حل کنید. سپس هر زمانی که خواستید میتوانید با اجرای دستور زیر حالت ذخیره شده پروژه را بازیابی کنید.
دستور اول، وضعیت فعلی پروژه را stash میکند.
دستور دوم، وضعیت فعلی پروژه را با پیام مورد نظر stash میکند.
دستور سوم، لیستی از stashها را نشان میدهد.
دستور چهارم، آخرین وضعیتِ پروژه که stash شده بود را بازیابی میکند.
دستور پنجم، nاُمین وضعیتِ پروژه که stash شده بود را بازیابی میکند.
دستور ششم، آخرین وضعیتِ پروژه که stash شده بود را بازیابی میکند و آن را از لیست stashها حذف میکند.
دستور هفتم، nاُمین وضعیتِ پروژه که stash شده بود را بازیابی میکند و آن را از لیست stashها حذف میکند.
دستور هشتم، همهی stashها را پاک میکند.
|
|
diff
گاهی میخواهیم تفاوت بین دو فایل، branch یا commit را ببینیم.
دستور اول، تغییرات جدید (stage نشده) را نشان میدهد (خطوط حذف شده با رنگ قرمز و خطوط اضافه شده با رنگ سبز نشان داده میشوند).
دستور دوم، تغییرات جدید (stage نشده) بین دو فایل file1 و file2 را نشان میدهد.
دستور سوم، تغییرات stage شده را نشان میدهد (تغییراتی که add شده اما commit نشده).
دستور چهارم، تغییرات انجام شده از زمان آخرین commit را نشان میدهد، هم stage شده و هم stage نشده.
دستور پنجم، تغییرات بین دو branch را نشان میدهد.
دستور ششم، تغییرات بین دو commit را نشان میدهد.
|
|
bisect
فرض کنید در حال کار رو پروژه هستیم که ناگهان متوجه میشویم قسمتی از آن دارای مشکل است و به شکل موقت یا دائم باید به حالت قبل برگردد. روش معمول این است که commitها را یکی یکی به عقب برویم تا commitی که در آن چنین مشکلی وجود ندارد را پیدا کنیم. اما git راه سریعتری پیشنهاد میدهد، bisect (بایسِکت).
با دستور git bisect start فرایند را شروع میکنیم.
با git bisect good old_commit_id آیدی یک commit قدیمیکه آن مشکل را ندارد به git معرفی میکنیم.
با git bisect bad new_commit_id آیدی یک commit جدید (مثلا commit آخر) که دارای مشکل است را به git معرفی میکنیم.
git هر بار commitی را به ما نشان میدهد و باید چک کنیم که آیا آن commit هم مشکل دارد یا خیر، اگر مشکل داشت git bisect bad و اگر نداشت git bisect good را وارد میکنیم.
با دستور git bisect reset فرایند را به اتمام میرسانیم.
|
|
cherry-pick
اگر یک commit را به cherry-pick بدهیم، تغییرات آن commit را طی commit جدیدی روی برنچ فعلی اعمال میکند.
|
|
مثلا فرض کنید شما و همتیمیتان روی دو branch مختلف از پروژه در حال کار کردن هستید. اگر متوجه شوید که بخشی از کدی که باید پیادهسازی کنید را همتیمیتان روی branch خودش هم پیاده کرده چه میکنید؟ طبیعتا دوباره خودتان پیادهسازی نمیکنید! در این مواقع میتوانید با استفاده از cherry-pick کد همتیمیتان را وارد branch خود کنید.
pull request
وقتی میخواهیم دو branch را با هم ادغام کنیم، از pull request استفاده میکنیم. در بیشتر پروژههای دنیای واقعی، اتصال دو branch فقط توسط یک نفر انجام نمیشود و فردی که میخواهد دو branch را به هم متصل کند، باید درخواست پول ریکوئست بدهد. با دادن این درخواست، پیامی به تعدادی از افراد گروه فرستاده میشود و آنها موظفند با بررسی کدهای اضافه شده و نحوهی رسیدگی کردن به کانفلیکتها، این merge را تایید کنند یا در صورتی که مشکلی وجود داشت، با کامنت آن مشکل را به فرد اصلی منتقل کنند تا او تغییرات مورد نظر را اعمال کند. تا هنگامیکه افراد گروه merge مورد نظر را تایید نکنند، این merge در پروژه اصلی انجام نمیشود. این ویژگی علاوه بر بهبود کیفیت و تمیزی کد، سبب میشود افراد بیشتری با هر تکه کد آشنا باشند و دید افراد به پروژه و نحوهی پیشرفت آن بهتر باشد.
جریان کار (workflow)
اینکه یک پروژه را چطور پیش ببریم، به عوامل مختلفی بستگی دارد. این نحوهی پیشرفتِ کار، در git هم منعکس میشود. در اینجا به چند workflowی معروف اشاره میکنیم.
مرکزی (centralized)
این جریان کار تنها از یک branch اصلی برای پیشرفت پروژه استفاده میکند. در واقع شرکت تنها از یک branch استفاده میکند و افراد روی آن هیچ branchی نمیزنند. در این حالت هر کسی روی سیستم خودش تغییرات مورد نظرش را روی این branch اعمال میکند. سپس pull کرده و در صورت خوردن کانفلیکت آن را برطرف میکند. سپس با دستور push کدهای جدیدش را روی سرور میفرستد.
برنچ ویژگی (feature branch)
در این نوع جریان کار یک branch اصلی داریم و برای ویژگیهای جدید branch زده میشود و کدها روی آن branch اضافه میشود. سپس با اتمام کار، این branch به کمک یک pull request و تضمین درستی کد، به branch اصلی merge میشود.
انشعابی (forking)
در این نوع جریان کار، از چند ریپازیتوری remote استفاده میکنیم. یکی از این ریپازیتوریها به عنوان پروژهی اصلی در نظر گرفته میشود و ریپازیتوریهای دیگر برای پیشرفت کد استفاده میشوند. در نهایت با کمک یک pull request میتوان ریپازیتوریهای کپی را به ریپازیتوری اصلی merge کرد. از ویژگیهای مثبت این جریان کار آن است که افراد توسعه دهنده مستقیما به کد اصلی دسترسی ندارند و بدین ترتیب امنیت کد اصلی بهتر حفظ میشود. به همین دلیل این نوع جریان کار در پروژههای منبع باز (open source) بسیار محبوب است.
جریان گیت (gitflow)
در این نوع جریان داده چند branch اصلی وجود دارد. یکی از این branchها که برنچ main است همواره پایدار بوده و هر commit آن میتواند به عنوان نسخهای از پروژه منتشر شود.
برای ساختن این جریان کار ابتدا از روی برنچ main برنچ اصلی مورد نظرمان (develop) را میسازیم. این branch مانند برنچ main همواره وجود خواهد داشت. حال برای پیشرفت کد و پیادهسازی ویژگیهای جدید، روی این برنچ، برنچ جدید زده و تغییرات مورد نظرمان را اعمال میکنیم و در نهایت آن را با برنچ develop مرج میکنیم. در هر لحظهای که بخواهیم نسخهی جدیدی از پروژه را روی main داشته باشیم، یک branch جدید مثلا با نام Release زده و کدهای روی برنچ develop را روی آن میبریم. این برنچ به هیچ عنوان برای پیشرفت کد استفاده نخواهد شد و تنها برای حل مشکلات همین نسخه استفاده میشود.
پس از رفع مشکلات احتمالی این نسخه، این برنچ را به main مرج میکنیم. دقت کنید که در صورت رفع باگ در برنچ Release، commit نهایی این برنچ باید به برنچ develop هم برده شود. حال اگر روی یکی از نسخههای روی برنچ main مشکلی پیدا شود، باید یک برنچ مثلا با نام Hotfix از روی آن نسخه زده شود و مشکل روی آن حل شود. دقت کنید در این حالت هم نتیجه نهایی باید بر روی develop برده شود. در تمام این مراحل تمام branchها به جز branchهای اصلی پس از merge شدن میتوانند حذف شوند.
توضیحات کامل در مورد gitflow رو میتونید در داکیومنتاش بخونید.
نمونهی جریان کار
یک flowی ساده میتواند به این صورت باشد.
taskها را به این شکل دستهبندی میکنیم:
- urgent: این عنوان معمولا وقتی بر روی یک task قرار میگیرد که یک مشکل و یا bug جدی در پروژه وجود دارد و میبایست هر چه سریعتر رفع گردد. هنگامی که یک تسک این برچسب را دارا میباشد، اولویت آن نسبت به تسکهای دیگر بالاتر است و باید سریعتر انجام شود.
- issue: این عنوان به taskهایی داده میشود که دارای مشکلاتی میباشد.
- bug: در واقع taskهایی که دارای این عنوان هستند، باگهایی هستند که در نسخه production وجود دارند و باید هرچه سریعتر حل شوند.
GitFlow را به این صورت در نظر میگیریم:
- برنچ main: این برنچ، برنچ اصلی پروژه است. این برنچ همیشه در حالت آماده باش (production) قرار دارد. هیچ کد تست نشدهای روی این برنچ قرار ندارد و فقط مدیر تیم میتواند روی این برنچ push کند.
- برنچ develop: روی این برنچ تمام تسکهای تمام و review شده قرار دارد که هنوز به حالت production در نیامدهاند. در واقع این برنچ حالت آماده باش (production) در زمان توسعه است.
- برنچهای feature: هر تسک یا فیچر، برنچ جدای خود را دارد که در GitFlow با نام feature شناخته میشود. هر feature جدید از روی برنچ develop ساخته میشود و پس از اتمام آن با develop ادغام (merge) میشود.
- برنچ release: بعد از آنکه تعداد فیچر مناسبی با برنچ develop مرج شد و خواستیم نسخه جدیدی را ریلیز کنیم، از این برنچ استفاده میکنیم. این برنچ نیز از روی develop ساخته میشود و بر روی آن میتوان آخرین تغییرات مورد نیاز قبل ریلیز (مانند آپدیت کردن ورژن) را اعمال کرد. پس اتمام آخرین کارها، این برنچ با برنچهای develop و main مرج میشود.
- برنچ hotfix: گاهی اوقات ممکن است که نسخهای که توسط کاربران استفاده میشود (production) باگهای اساسی و مهمی داشته باشد که باید فوراً رفع شود. برای حل این باگها از این برنچ استفاده میشود. این برنچ مستقیما از روی main ساخته میشود و پس از اتمام آن با main و develop مرج میشود.
استاندارد مورد توافق برای متن commitها به این صورت است (الگوی Semantic):
|
|
مقدار type باید برابر با یکی از آیتمهای زیر باشد:
- build: ایجاد تغییرات مرتبط (مثلا اضافه کردن dependenciesهای خارجی مثلا با npm)
- chore: اضافه کردن تغییراتی که برای همه مشخص نیست (مثلا تغییر دادن در فایل .gitignore)
- feat: اضافه کردن ویژگی جدید
- fix: رفع یک باگ
- docs: تغییرات مربوط به مستندات
- refactor: تغییری که نه یک ویژگی اضافه میکنه نه مشکلی رو حل میکنه (مثلا تغییرات در نامگذاری متغیرها)
- perf: تغییراتی که باعث بهبود کارایی میشه
- style: تغییراتی که مرتبط با تغییرات ظاهری هست
- test: اضافه کردن تست جدید یا تغییر در تستهای قبلی
scope (که البته اختیاری است)، محدودهای که تغییرات در آن رخ داده است و باید برابر با یکی از آیتمهای زیر باشد:
- نام فایل
- نام پکیج
- نام API
- نام الگوریتم و در کل هر نامی که برای یک توسعهدهندهی خارجی قابل فهم باشد که دقیقا به کجا اشاره دارد
subject توضیحی است که میگوید چه کاری انجام شده. قوانین نوشتن آن را مشابه قوانین نوشتن commit در نظر میگیریم.
در مورد قسمت body:
- هر خط آن نباید بیش از 72 کاراکتر باشد
- به صورت امری و در زمان حال نوشته شود
- به سه پرسش «چرا»، «چگونه» و «کجا» پاسخ دهد
- میتوان در صورت نیاز، قبل و بعد از تغییرات را مقایسه کرده و به کامیتهای دیگر (توسط hash) ارجاع داد
- میتوان از شمارهگذاری برای ارجاع استفاده کرد. ارجاعات در footer آورده میشود
در مورد قسمت footer (که البته اختیاری است):
- ارجاعاتی که در متن body شمارهگذاری شدهاند، در اینجا آورده میشوند
- میتوان با کلیدواژهی Closes به همراه # به issue ها ارجاع داد و گفت که در نتیجهی این کامیت چه issue ای رفع شده است
- میتوان با کلیدواژهی Trello به یک کارت در ترلو ارجاع داد
منابع
quera mjafar @sorousht @golemCourse atlassian @gitscm @rezakamalifard