مقدمه ای بر کنترل نسخه

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

در عین اینکه مجموعه ابزارهای git ویژگیهای کاملی را ارایه میدهند یکی از مفیدترین ویژگیهای git انعطاف پذیری آن است. 

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

در این راهنما ما ایده hooks را بررسی میکنیم و  کدی را نشان خواهیم داد که به شما در اتوماتیک کردن کارها در سیستم منحصر به خودتان کمک خواهد کرد.

ما از یک سرور ubuntu 14.04 در این راهنما استفاده خواهیم کرد اما هر سیستمی که بتواند گیت را اجرا نماید باید به روش مشابه بتواند کار کند.

پیش نیازها

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

ایده اولیه هوک های گیت ( git hooks )

هوک های گیت مفهوم ساده ای است که برای برطرف کردن یک نیاز به وجود آمده است. هنگام توسعه نرم افزار در یک پروژه اشتراکی، نگهداری استانداردهای راهنمای استایل و یا هنگام پیاده سازی نرم افزار ، همیشه کارهای تکراری وجود دارند که میخواهید هنگام یک عمل دیگر بصورت اتوماتیک اجرا شوند.

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

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

اسکات چاکن در کتاب خود ( Pro Git ) سعی کرده است هوکها را بصورت زیر دسته بندی کند:

هوکهای سمت کلاینت : هوکهایی هستند که در کامپیوتر کامیت ( commit ) کننده ها اجرا میشوند. این هوکها خود به دسته های زیر تقسیم میشوند:

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

هوکهای ایمیل: این دسته از هوکها شامل اعمالی میشوند که در هنگام کار با پچهای ایمیل شده اجرا میگردند. پروژه هایی مثل کرنل لینوکس پچ ها را با استفاده از سیستم ایمیل ثبت و بررسی میکنند. این هوکها شبیه هوکهای کامیت هستند اما میتوانند به وسیله پشتبانهایی که مسول بکارگیری کدهای ثبت شده هستند به کار گرفته شوند.

هوکهای دیگر: هوکهای سمت کلاینت دیگر شامل هوکهای اجرا، ادغام، نوشتن مجدد و پاک کردن مخزن و ... میشوند.

هوکهای سمت سرور: این هوکها در سرورهایی که تغییرات ( pushes ) دریافت میکنند استفاده میشوند. عموما میتواند مخزن اصلی گیت برای یک پروژه باشد. این هوکها نیز شامل دسته هایی میشوند:

پیش دریافت و پس دریافت: این هوکها در سروری که یک push را دریافت میکند اجرا میشوند تا اعمالی نظیر چک کردن پروژه و پیاده سازی بعد از push را انجام دهند.

آپدیت: این هوکها مثل هوکهای قبل از دریافت هستند ولی برپایه یک شاخه-به-شاخه انجام میشوند تا کدی را قبل از اینکه شاخه ای پذیرفته شود اجرا کنند.

این دسته بندی ها برای به دست آورن ایده کلی از رویدادهایی که شما میتوانید یک هوک را برای آنها تنظیم نمایید خوب هستند. اما برای فهمیدن نحوه عمل این ایتمها این است که آنها را آزمایش کنید تا راه حل مورد نظر خود را بیابید.

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

در کل هوکهایی که در دسترس هستند عبارتند از:

Hook Name Invoked By Description Parameters (Number and Description)
applypatch-msg git am Can edit the commit message file and is often used to verify or actively format a patch's message to a project's standards. A non-zero exit status aborts the commit. (1) name of the file containing the proposed commit message
pre-applypatch git am This is actually called after the patch is applied, but before the changes are committed. Exiting with a non-zero status will leave the changes in an uncommitted state. Can be used to check the state of the tree before actually committing the changes. (none)
post-applypatch git am This hook is run after the patch is applied and committed. Because of this, it cannot abort the process, and is mainly used for creating notifications. (none)
pre-commit git commit This hook is called before obtaining the proposed commit message. Exiting with anything other than zero will abort the commit. It is used to check the commit itself (rather than the message). (none)
prepare-commit-msg git commit Called after receiving the default commit message, just prior to firing up the commit message editor. A non-zero exit aborts the commit. This is used to edit the message in a way that cannot be suppressed. (1 to 3) Name of the file with the commit message, the source of the commit message (message, template, merge, squash, or commit), and the commit SHA-1 (when operating on an existing commit).
commit-msg git commit Can be used to adjust the message after it has been edited in order to ensure conformity to a standard or to reject based on any criteria. It can abort the commit if it exits with a non-zero value. (1) The file that holds the proposed message.
post-commit git commit Called after the actual commit is made. Because of this, it cannot disrupt the commit. It is mainly used to allow notifications. (none)
pre-rebase git rebase Called when rebasing a branch. Mainly used to halt the rebase if it is not desirable. (1 or 2) The upstream from where it was forked, the branch being rebased (not set when rebasing current)
post-checkout git checkout and git clone Run when a checkout is called after updating the worktree or after git clone. It is mainly used to verify conditions, display differences, and configure the environment if necessary. (3) Ref of the previous HEAD, ref of the new HEAD, flag indicating whether it was a branch checkout (1) or a file checkout (0)
post-merge git merge or git pull Called after a merge. Because of this, it cannot abort a merge. Can be used to save or apply permissions or other kinds of data that git does not handle. (1) Flag indicating whether the merge was a squash.
pre-push git push Called prior to a push to a remote. In addition to the parameters, additional information, separated by a space is passed in through stdin in the form of "<local ref> <local sha1> <remote ref> <remote sha1>". Parsing the input can get you additional information that you can use to check. For instance, if the local sha1 is 40 zeros long, the push is a delete and if the remote sha1 is 40 zeros, it is a new branch. This can be used to do many comparisons of the pushed ref to what is currently there. A non-zero exit status aborts the push. (2) Name of the destination remote, location of the destination remote
pre-receive git-receive-pack on the remote repo This is called on the remote repo just before updating the pushed refs. A non-zero status will abort the process. Although it receives no parameters, it is passed a string through stdin in the form of "<old-value> <new-value> <ref-name>" for each ref. (none)
update git-receive-pack on the remote repo This is run on the remote repo once for each ref being pushed instead of once for each push. A non-zero status will abort the process. This can be used to make sure all commits are only fast-forward, for instance. (3) The name of the ref being updated, the old object name, the new object name
post-receive git-receive-pack on the remote repo This is run on the remote when pushing after the all refs have been updated. It does not take parameters, but receives info through stdin in the form of "<old-value> <new-value> <ref-name>". Because it is called after the updates, it cannot abort the process. (none)
post-update git-receive-pack on the remote repo This is run only once after all of the refs have been pushed. It is similar to the post-receive hook in that regard, but does not receive the old or new values. It is used mostly to implement notifications for the pushed refs. (?) A parameter for each of the pushed refs containing its name
pre-auto-gc git gc --auto Is used to do some checks before automatically cleaning repos. (none)
post-rewrite git commit --amend, git-rebase This is called when git commands are rewriting already committed data. In addition to the parameters, it receives strings in stdin in the form of "<old-sha1> <new-sha1>". (1) Name of the command that invoked it (amend or rebase)

حال که اطلاعات کلی را به دست آوردید میتوانیم به شما نحوه پیاده سازی چند سناریو را نشان دهیم.

راه اندازی یک مخزن

برای شروع، یک مخزن خالی جدید در پوشه home ایجاد میکنیم و نام آن را  proj انتخاب میکنیم.

 

mkdir ~/proj
cd ~/proj
git init

 

Initialized empty Git repository in /home/demo/proj/.git/ 

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

 

cd .git
ls -F
 branches/ config description HEAD hooks/ info/ objects/ refs/ 

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

 

cd hooks
ls -l 
  total 40 -rwxrwxr-x 1 demo demo 452 Aug 8 16:50 applypatch-msg.sample
-rwxrwxr-x 1 demo demo 896 Aug 8 16:50 commit-msg.sample
-rwxrwxr-x 1 demo demo 189 Aug 8 16:50 post-update.sample
-rwxrwxr-x 1 demo demo 398 Aug 8 16:50 pre-applypatch.sample
-rwxrwxr-x 1 demo demo 1642 Aug 8 16:50 pre-commit.sample
-rwxrwxr-x 1 demo demo 1239 Aug 8 16:50 prepare-commit-msg.sample
-rwxrwxr-x 1 demo demo 1352 Aug 8 16:50 pre-push.sample
-rwxrwxr-x 1 demo demo 4898 Aug 8 16:50 pre-rebase.sample
-rwxrwxr-x 1 demo demo 3611 Aug 8 16:50 update.sample

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

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

اجازه دهید به دایرکتوری کاری خود برگردیم.

 

cd ../..

مثال اول: پیاده سازی به یک سرور وب لوکال با هوک پس-کامیت (post-commit)

مثال اول ما از هوک post-commit استفاده میکند تا به شما نشان دهد که چگونه به یک سرور لوکال بعد از انجام یک کامیت کد خود را انتقال دهیم. این هوک چیزی نیست که برای یک محیط واقعی در عمل استفاده شود اما به ما اجازه میدهد تا برخی ازموارد مهم که به ندرت مستند شده است و شما باید بدانید را تشریح نماییم.

ابتدا سرور آپاچی را نصب میکنیم.

 

sudo apt-get update
sudo apt-get install apache2 

برای اینکه بتوانیم اسکریپت خود را اجرا کنیم نیاز داریم که دسترسی نوشتن پوشه /var/www/html را داشته باشیم. این کار را میتوانیم با دستور زیر انجام دهید:

 

sudo chown -R yourusername:yougroupname /var/www/html 

حالا در پوشه پروژه خود یک فایل با نام index.html ایجاد میکنیم.

 

cd ~/proj
nano index.html 

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

<h1>Here is a title!</h1>

<p>Please deploy me!</p>

فایل جدید را به گیت اضافه میکنیم:

git add .

حال قبل از کامیت کردن هوک post-commit خود را راه اندازی میکنیم. این فایل را در پوشه .git/hooks پروژه ایجاد میکنیم.

vim .git/hooks/post-commit

قبل از اینکه به محتوای این فایل بپردازیم نیاز داریم کمی در مورد اینکه گیت چگونه متغیرهای محیطی را تنظیم میکند یاد بگیریم.

حاشیه ای بر متغیرهای محیطی و هوکهای گیت

قبل از اینکه اسکریپت خود را شروع کنیم نیاز داریم تا کمی در مورد متغیرهای محیطی که گیت در هنگام فراخوانی هوکها تنظیم میکند بدانیم. برای اینکه اسکریپت ما درست عمل کند نیاز داریم که تنظیمات متغیرهای محیطی که گیت در هنگام فراخوانی هوک  post-commit ست میکند را از بین ببریم.

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

اولین مشکل این است که اگر از متغیرهایی که گیت بصورت خودکار تنظیم میکند آگاه نباشید محیط اسکریپت نویسی شما بسیار غیر قابل پیش میشود. مشکل دوم این است که اکثر این متغیرها جایی در مستندات گیت وجود ندارند.

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

#!/bin/bash
echo Running $BASH_SOURCE
set | egrep GIT
echo PWD is $PWD

در زیر مثالی از نتایج تست این اسکریپت آماده است. پوشه کار تست  /home/demo/test_hooks و مخزن گیت راه دور /home/demo/origin/test_hooks.git بوده است.

  • Hooks: applypatch-msg, pre-applypatch, post-applypatch
    • Environmental Variables:
    • GIT_AUTHOR_DATE='Mon, 11 Aug 2014 11:25:16 -0400'
    • GIT_AUTHOR_EMAIL=demo@example.com
    • GIT_AUTHOR_NAME='Demo User'
    • GIT_INTERNAL_GETTEXT_SH_SCHEME=gnu
    • GIT_REFLOG_ACTION=am
    • Working Directory: /home/demo/test_hooks
  • Hooks: pre-commit, prepare-commit-msg, commit-msg, post-commit
    • Environmental Variables:
    • GIT_AUTHOR_DATE='@1407774159 -0400'
    • GIT_AUTHOR_EMAIL=demo@example.com
    • GIT_AUTHOR_NAME='Demo User'
    • GIT_DIR=.git
    • GIT_EDITOR=:
    • GIT_INDEX_FILE=.git/index
    • GIT_PREFIX=
    • Working Directory: /home/demo/test_hooks
  • Hooks: pre-rebase
    • Environmental Variables:
    • GIT_INTERNAL_GETTEXT_SH_SCHEME=gnu
    • GIT_REFLOG_ACTION=rebase
    • Working Directory: /home/demo/test_hooks
  • Hooks: post-checkout
    • Environmental Variables:
    • GIT_DIR=.git
    • GIT_PREFIX=
    • Working Directory: /home/demo/test_hooks
  • Hooks: post-merge
    • Environmental Variables:
    • GITHEAD_4b407c...
    • GIT_DIR=.git
    • GIT_INTERNAL_GETTEXT_SH_SCHEME=gnu
    • GIT_PREFIX=
    • GIT_REFLOG_ACTION='pull other master'
    • Working Directory: /home/demo/test_hooks
  • Hooks: pre-push
    • Environmental Variables:
    • GIT_PREFIX=
    • Working Directory: /home/demo/test_hooks
  • Hooks: pre-receive, update, post-receive, post-update
    • Environmental Variables:
    • GIT_DIR=.
    • Working Directory: /home/demo/origin/test_hooks.git
  • Hooks: pre-auto-gc
    • (unknown because this is difficult to trigger reliably)
  • Hooks: post-rewrite
    • Environmental Variables:
    • GIT_AUTHOR_DATE='@1407773551 -0400'
    • GIT_AUTHOR_EMAIL=demo@example.com
    • GIT_AUTHOR_NAME='Demo User'
    • GIT_DIR=.git
    • GIT_PREFIX=
    • Working Directory: /home/demo/test_hooks

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

ادامه نوشتن اسکریپت

حال که ایده اولیه ای در مورد متغیرهای محیطی بدست آوردیم میتوانیم نوشتن اسکریپت خود را شروع کنیم.

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

#!/bin/bash

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

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

#!/bin/bash
git --work-tree=/var/www/html --git-dir=/home/demo/proj/.git checkout -f

در این نقطه تقریبا کارمان به پایان رسیده است. هر چند نیاز داریم که دقت بیشتری روی متغیرهای محیطی که در هنگام فراخوانی هوک post-commit ست میشوند داشته باشیم. به خصوص متغیر GIT_INDEX_FILE که با مقدار .git/index ست شده است.

این مسیر به دایرکتوری کاری که در این مورد  /var/www/html است مربوط میشود. از آنجا که index در آن پوشه وجود ندارد اگر در اسکریپتمان تغییراتی ایجاد نکنیم در هنگام اجرا متوقف خواهد شد. برای جلوگیری از چنین پیشامدی میتوانیم مقدار متغیر محیطی را بصورت دستی پاک کنیم که باعث میشود گیت در پوشه مخزن به دنبال index بگردد نه در پوشه در حال کار. این کار را باید قبل از خط  checkout انجام دهیم:

#!/bin/bash
unset GIT_INDEX_FILE
git --work-tree=/var/www/html --git-dir=/home/demo/proj/.git checkout -f

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

پس از ویرایش و انجام تغییرات آنها را ذخیره نمایید.

حال باید اسکریپتی را که نوشته ایم به یک فایل قابل اجراشدن تبدیل نماییم.  دستور زیر را اجرا نمایید:

chmod +x .git/hooks/post-commit

خب حالا آماده کامیت کردن تغییراتی که در مخزن شکل گرفته است هستیم. مطمئن شوید که به پوشه درست برگشته اید و سپس تغییرات را کامیت کنید.

cd ~/proj
git commit -m "here we go..."

خب حالا اگر نامه دامنه سرور خود یا آدرس آی پی آن را در مرورگرتان تایپ کنید باید صفحه  index.html را که ایجاد کرده اید ببینید.

http://server_domain_or_IP

Test index.html

همانطور که می بینید تغییرات اخیر بصورت اتوماتیک به محض کامیت روی پوشه روت وب سرور آپاچی کپی شده اند. میتوانیم تغییرات بیشتری نیز انجام دهیم تا ببینیم که در هر کامیت هوک ما درست کار میکند.

echo "<p>Here is a change.</p>" >> index.html
git add .
git commit -m "First change"

اگر مرور کر خود را refresh کنید میبیند که آخرین تغییرات نیز اعمال شده اند:

deploy changes

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