Chapters ▾ 2nd Edition

9.2 گیت و سیستم‌های دیگر (Git and Other Systems) - مهاجرت به گیت (Migrating to Git)

مهاجرت به گیت (Migrating to Git)

اگر کدبیس موجودی در یک سیستم کنترل نسخهٔ دیگر دارید ولی تصمیم گرفته‌اید از گیت استفاده کنید، باید پروژهٔ خود را به‌نوعی مهاجرت دهید. این بخش برخی واردکننده‌ها (importer) برای سیستم‌های رایج را بررسی می‌کند و سپس نشان می‌دهد چگونه یک واردکنندهٔ سفارشی بسازید. شما یاد می‌گیرید چگونه داده‌ها را از چند سیستم مدیریت پیکربندی (SCM) بزرگتر که به‌طور حرفه‌ای استفاده می‌شوند وارد کنید، زیرا این‌ها اکثریت کسانی را تشکیل می‌دهند که در حال مهاجرت‌اند و همچنین ابزارهای باکیفیت برای آن‌ها به‌راحتی در دسترس است.

سیستم کنترل نسخه ساب‌ورژن (Subversion)

اگر بخش قبلی درباره استفاده از git svn را خوانده باشید، می‌توانید به‌راحتی از همان دستورالعمل‌ها برای انجام git svn clone یک مخزن استفاده کنید؛ سپس استفاده از سرور Subversion را متوقف کرده، به یک سرور Git جدید منتقل شوید و شروع به استفاده از آن کنید. اگر می‌خواهید تاریخچه‌ی تغییرات حفظ شود، می‌توانید این کار را به سرعتی انجام دهید که داده‌ها را از سرور Subversion بیرون بکشید (که ممکن است مدتی طول بکشد).

با این حال، واردسازی کامل نیست؛ و چون این کار طول می‌کشد، بهتر است آن را درست انجام دهید. اولین مشکل مربوط به اطلاعات نویسنده است. در Subversion، هر کسی که کامیت می‌کند، یک کاربر روی سیستم دارد که در اطلاعات کامیت ثبت می‌شود. مثال‌های بخش قبلی، schacon را در برخی مکان‌ها نشان می‌دهند، مانند خروجی blame و git svn log. اگر می‌خواهید این اطلاعات به داده‌های نویسنده‌ی بهتر در Git نگاشت شود، نیاز به یک نگاشت از کاربران Subversion به نویسندگان Git دارید. یک فایل به نام users.txt ایجاد کنید که این نگاشت را با فرمت زیر داشته باشد:

schacon = Scott Chacon <schacon@geemail.com>
selse = Someo Nelse <selse@geemail.com>

برای دریافت فهرستی از نام نویسندگانی که SVN استفاده می‌کند، می‌توانید این را اجرا کنید:

$ svn log --xml --quiet | grep author | sort -u | \
  perl -pe 's/.*>(.*?)<.*/$1 = /'

این خروجی را در قالب XML تولید می‌کند، سپس فقط خطوط حاوی اطلاعات نویسنده را نگه می‌دارد، تکراری‌ها را حذف می‌کند و تگ‌های XML را پاک می‌کند. واضح است که این تنها روی سیستمی کار می‌کند که grep، sort و perl نصب باشند. سپس خروجی را به فایل users.txt هدایت کنید تا بتوانید داده معادل کاربر Git را در کنار هر ورودی اضافه کنید.

یادداشت

اگر این را روی یک ماشین ویندوزی امتحان می‌کنید، در این نقطه با مشکل مواجه خواهید شد. مایکروسافت راهنمایی‌ها و نمونه‌های خوبی را در https://learn.microsoft.com/en-us/azure/devops/repos/git/perform-migration-from-svn-to-git ارائه کرده است.

می‌توانید این فایل را به git svn بدهید تا در نگاشت دقیق‌تر داده‌های نویسنده کمک کند. همچنین می‌توانید به git svn بگویید متادیتایی را که Subversion معمولاً وارد می‌کند، شامل نکند، با ارسال --no-metadata به دستور clone یا init. متادیتا شامل git-svn-id در هر پیام کمیت است که Git هنگام واردسازی تولید می‌کند. این می‌تواند لاگ Git شما را حجیم کند و کمی نامشخص سازد.

یادداشت

نیاز دارید متادیتا را نگه دارید وقتی می‌خواهید کمیت‌هایی را که در مخزن Git انجام شده‌اند، به مخزن SVN اصلی بازآینه (mirror) کنید. اگر نمی‌خواهید همگام‌سازی در لاگ کمیت شما ظاهر شود، می‌توانید پارامتر --no-metadata را حذف کنید.

این باعث می‌شود دستور import شما به این شکل باشد:

$ git svn clone http://my-project.googlecode.com/svn/ \
      --authors-file=users.txt --no-metadata --prefix "" -s my_project
$ cd my_project

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

commit 37efa680e8473b615de980fa935944215428a35a
Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

    git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de-
    be05-5f7a86268029

آن‌ها این‌گونه به نظر می‌رسند:

commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2
Author: Scott Chacon <schacon@geemail.com>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

نه تنها فیلد Author بسیار بهتر است، بلکه git-svn-id هم دیگر وجود ندارد.

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

برای منتقل کردن تگ‌ها به تگ‌های واقعی Git، اجرا کنید:

$ for t in $(git for-each-ref --format='%(refname:short)' refs/remotes/tags); do git tag ${t/tags\//} $t && git branch -D -r $t; done

این دستور، ارجاع‌هایی را که شاخه‌های ریموت بودند و با refs/remotes/tags/ شروع می‌شدند، گرفته و آن‌ها را به تگ‌های واقعی (سبک/lightweight) تبدیل می‌کند.

سپس، بقیه‌ی ارجاع‌ها زیر refs/remotes را به شاخه‌های محلی منتقل کنید:

$ for b in $(git for-each-ref --format='%(refname:short)' refs/remotes); do git branch $b refs/remotes/$b && git branch -D -r $b; done

ممکن است پیش بیاید که برخی شاخه‌های اضافه ببینید که با @xxx ختم شده‌اند (که xxx یک عدد است)، در حالی که در Subversion تنها یک شاخه مشاهده می‌کنید. این در واقع یک ویژگی Subversion به نام "peg-revisions" است، که معادلی نحوی (syntax) در Git ندارد. بنابراین، git svn به سادگی شماره نسخه SVN را به نام شاخه اضافه می‌کند، درست همان‌طور که اگر در SVN می‌خواستید به peg-revision آن شاخه اشاره کنید، می‌نوشتید. اگر دیگر نیازی به peg-revisions ندارید، به سادگی آن‌ها را حذف کنید:

$ for p in $(git for-each-ref --format='%(refname:short)' | grep @); do git branch -D $p; done

اکنون همه‌ی شاخه‌های قدیمی، شاخه‌های واقعی Git هستند و همه‌ی تگ‌های قدیمی، تگ‌های واقعی Git شده‌اند.

یک نکته‌ی آخر برای پاک‌سازی باقی مانده است. متأسفانه، git svn یک شاخه‌ی اضافه به نام trunk ایجاد می‌کند که به شاخه‌ی پیش‌فرض Subversion نگاشت می‌شود، اما ارجاع trunk به همان مکانی اشاره می‌کند که master اشاره دارد. از آنجا که master بیشتر با سبک Git همخوانی دارد، روش حذف این شاخه‌ی اضافه به صورت زیر است:

$ git branch -d trunk

آخرین کاری که باید انجام دهید، اضافه کردن سرور Git جدید خود به عنوان یک ریموت و انجام push به آن است. در اینجا یک مثال برای اضافه کردن سرور به عنوان ریموت آورده شده است:

$ git remote add origin git@my-git-server:myrepository.git

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

$ git push origin --all
$ git push origin --tags

تمام شاخه‌ها و تگ‌های شما باید در سرور Git جدیدتان قرار داشته باشند، به‌طوری که واردسازی به‌صورت مرتب و تمیز انجام شده باشد.

سیستم کنترل نسخه‌ی توزیع‌شده Mercurial (Mercurial)

از آنجا که Mercurial و Git مدل‌های نسبتاً مشابهی برای نمایش نسخه‌ها دارند و Git تا حدی انعطاف‌پذیرتر است، تبدیل یک مخزن از Mercurial به Git نسبتاً سرراست است و از ابزاری به نام "hg-fast-export" استفاده می‌کند که باید یک نسخه از آن را داشته باشید:

$ git clone https://github.com/frej/fast-export.git

اولین گام در تبدیل، گرفتن یک کلون کامل از مخزن Mercurial است که می‌خواهید تبدیل کنید:

$ hg clone <remote repo URL> /tmp/hg-repo

گام بعدی ایجاد یک فایل نگاشت نویسنده است. Mercurial در مورد مقداری که در فیلد نویسنده برای changesetها قرار می‌دهد کمی بزرگ‌سروتر از Git است، بنابراین این زمان خوبی است برای پاک‌سازی. تولید این فایل با یک فرمان تک‌خطی در شل bash انجام می‌شود:

$ cd /tmp/hg-repo
$ hg log | grep user: | sort | uniq | sed 's/user: *//' > ../authors

این کار چند ثانیه طول می‌کشد، بسته به طول تاریخچه پروژه‌تان، و پس از آن فایل /tmp/authors چیزی شبیه به این خواهد بود:

bob
bob@localhost
bob <bob@company.com>
bob jones <bob <AT> company <DOT> com>
Bob Jones <bob@company.com>
Joe Smith <joe@company.com>

در این مثال، یک فرد واحد (باب) changesetهایی را تحت چهار نام مختلف ایجاد کرده است؛ یکی از آن‌ها در واقع درست به نظر می‌رسد و یکی از آن‌ها برای یک commit در Git کاملاً نامعتبر خواهد بود. Hg-fast-export به ما اجازه می‌دهد این را با تبدیل هر خط به یک قاعده اصلاح کنیم: "<input>"="<output>"، که یک <input> را به یک <output> نگاشت می‌کند. درون رشته‌های <input> و <output>، تمام توالی‌های فرار (escape sequences) که توسط انکودینگ Python string_escape شناخته می‌شوند پشتیبانی می‌شوند. اگر فایل نگاشت نویسنده شامل یک <input> مطابق نباشد، آن نویسنده بدون تغییر به Git ارسال خواهد شد. اگر همه نام‌های کاربری درست به نظر برسند، اصلاً به این فایل نیازی نخواهیم داشت. در این مثال، می‌خواهیم فایل ما شبیه به این باشد:

"bob"="Bob Jones <bob@company.com>"
"bob@localhost"="Bob Jones <bob@company.com>"
"bob <bob@company.com>"="Bob Jones <bob@company.com>"
"bob jones <bob <AT> company <DOT> com>"="Bob Jones <bob@company.com>"

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

گام بعدی ایجاد مخزن جدید Git و اجرای اسکریپت export است:

$ git init /tmp/converted
$ cd /tmp/converted
$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors
 پرچم `-r` به `hg-fast-export` می‌گوید مخزن مرکوریال که می‌خواهیم تبدیل کنیم کجا قرار دارد، و پرچم `-A` به آن می‌گوید فایل نگاشت نویسنده‌ها کجا قرار دارد (فایل‌های نگاشت شاخه و تگ به‌ترتیب با پرچم‌های `-B` و `-T` مشخص می‌شوند).
اسکریپت تغییرمجموعه‌های مرکوریال را پارس کرده و آن‌ها را به اسکریپتی برای قابلیت `fast-import` گیت تبدیل می‌کند (که کمی بعد به‌طور مفصل درباره‌اش صحبت خواهیم کرد).
این کار کمی زمان می‌برد (اگرچه خیلی سریع‌تر از زمانی است که بخواهید از طریق شبکه انجامش دهید)، و خروجی نسبتاً پرحرف است:
$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors
Loaded 4 authors
master: Exporting full revision 1/22208 with 13/0/0 added/changed/removed files
master: Exporting simple delta revision 2/22208 with 1/1/0 added/changed/removed files
master: Exporting simple delta revision 3/22208 with 0/1/0 added/changed/removed files
[…]
master: Exporting simple delta revision 22206/22208 with 0/4/0 added/changed/removed files
master: Exporting simple delta revision 22207/22208 with 0/2/0 added/changed/removed files
master: Exporting thorough delta revision 22208/22208 with 3/213/0 added/changed/removed files
Exporting tag [0.4c] at [hg r9] [git :10]
Exporting tag [0.4d] at [hg r16] [git :17]
[…]
Exporting tag [3.1-rc] at [hg r21926] [git :21927]
Exporting tag [3.1] at [hg r21973] [git :21974]
Issued 22315 commands
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:     120000
Total objects:       115032 (    208171 duplicates                  )
      blobs  :        40504 (    205320 duplicates      26117 deltas of      39602 attempts)
      trees  :        52320 (      2851 duplicates      47467 deltas of      47599 attempts)
      commits:        22208 (         0 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:         109 (         2 loads     )
      marks:        1048576 (     22208 unique    )
      atoms:           1952
Memory total:          7860 KiB
       pools:          2235 KiB
     objects:          5625 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =      90430
pack_report: pack_mmap_calls          =      46771
pack_report: pack_open_windows        =          1 /          1
pack_report: pack_mapped              =  340852700 /  340852700
---------------------------------------------------------------------

$ git shortlog -sn
   369  Bob Jones
   365  Joe Smith

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

$ git remote add origin git@my-git-server:myrepository.git
$ git push origin --all

سیستم کنترل نسخه‌ی توزیع‌شده Perforce (Perforce)

سیستم بعدی که برای بررسی وارد کردن داده‌ها مورد توجه قرار می‌دهیم، Perforce است. همان‌طور که پیش‌تر اشاره شد، دو روش اصلی برای ارتباط بین Git و Perforce وجود دارد: git-p4 Perforce Git Fusion

ادغام Perforce و Git (Perforce Git Fusion)

Perforce Git Fusion این فرآیند را به‌طور چشمگیری ساده می‌کند. کافی است تنظیمات پروژه، نقشه‌های کاربری و شاخه‌ها را تنها با استفاده از یک فایل پیکربندی (همان‌طور که در بخش گیت فیوژن (Git Fusion) توضیح داده شده است) انجام دهید و سپس مخزن را کلون کنید.

Git Fusion یک مخزن گیت ایجاد می‌کند که درست مانند یک مخزن بومی Git عمل می‌کند. شما می‌توانید در صورت تمایل این مخزن را به یک میزبان Git بومی ارسال کنید. همچنین این امکان وجود دارد که حتی از Perforce به‌عنوان میزبان Git خود استفاده نمایید.

ابزار گیت برای تعامل با Perforce (Git-p4)

برای استفاده از git-p4 به‌عنوان ابزار واردات، ابتدا باید محیط خود را به‌درستی تنظیم کنید. به‌عنوان مثال، اگر بخواهید پروژه Jam را از Perforce Public Depot وارد کنید، لازم است متغیر محیطی P4PORT را به‌درستی تنظیم کنید تا به مخزن Perforce اشاره کند. این کار را می‌توانید با دستور زیر انجام دهید:

$ export P4PORT=public.perforce.com:1666
یادداشت

برای دنبال کردن این آموزش، نیاز به یک پایگاه داده Perforce دارید تا بتوانید به آن متصل شوید. ما برای مثال‌ها از پایگاه داده عمومی public.perforce.com استفاده می‌کنیم، اما شما می‌توانید از هر پایگاه داده‌ای که به آن دسترسی دارید بهره ببرید.

دستور git p4 clone را برای وارد کردن پروژه Jam از سرور Perforce اجرا کنید، با ارائه مسیر مخزن و پروژه و مسیری که می‌خواهید پروژه را در آن وارد کنید:

$ git-p4 clone //guest/perforce_software/jam@all p4import
Importing from //guest/perforce_software/jam@all into p4import
Initialized empty Git repository in /private/tmp/p4import/.git/
Import destination: refs/remotes/p4/master
Importing revision 9957 (100%)

این پروژه خاص تنها یک شاخه دارد، اما اگر پروژه شما چند شاخه دارد یا شاخه‌ها با نمای شاخه‌ها (یا فقط مجموعه‌ای از دایرکتوری‌ها) پیکربندی شده‌اند، می‌توانید از گزینه‌ی --detect-branches در دستور git p4 clone استفاده کنید تا تمام شاخه‌های پروژه وارد شوند. برای اطلاعات بیشتر در این زمینه، به بخش شاخه‌بندی (Branching) مراجعه کنید.

در این مرحله، تقریباً فرآیند وارد کردن پروژه به پایان رسیده است. با رفتن به دایرکتوری p4import و اجرای دستور git log می‌توانید تاریخچه و تغییرات وارد شده پروژه را مشاهده کنید.

$ git log -2
commit e5da1c909e5db3036475419f6379f2c73710c4e6
Author: giles <giles@giles@perforce.com>
Date:   Wed Feb 8 03:13:27 2012 -0800

    Correction to line 355; change </UL> to </OL>.

    [git-p4: depot-paths = "//public/jam/src/": change = 8068]

commit aa21359a0a135dda85c50a7f7cf249e4f7b8fd98
Author: kwirth <kwirth@perforce.com>
Date:   Tue Jul 7 01:35:51 2009 -0800

    Fix spelling error on Jam doc page (cummulative -> cumulative).

    [git-p4: depot-paths = "//public/jam/src/": change = 7304]

شما می‌توانید مشاهده کنید که git-p4 یک شناسه در هر پیغام commit باقی گذاشته است. نگهداری این شناسه مشکلی ندارد، در صورتی که بخواهید شماره تغییر Perforce را بعداً ارجاع دهید. اما اگر تمایل دارید شناسه را حذف کنید، اکنون زمان مناسبی برای این کار است – قبل از شروع به انجام کار روی مخزن جدید. شما می‌توانید از دستور git filter-branch برای حذف گروهی شناسه‌ها استفاده کنید:

$ git filter-branch --msg-filter 'sed -e "/^\[git-p4:/d"'
Rewrite e5da1c909e5db3036475419f6379f2c73710c4e6 (125/125)
Ref 'refs/heads/master' was rewritten

اگر دستور git log را اجرا کنید، می‌توانید مشاهده کنید که تمام چک‌سام‌های SHA-1 برای commitها تغییر کرده‌اند، اما رشته‌های git-p4 دیگر در پیغام‌های commit وجود ندارند:

$ git log -2
commit b17341801ed838d97f7800a54a6f9b95750839b7
Author: giles <giles@giles@perforce.com>
Date:   Wed Feb 8 03:13:27 2012 -0800

    Correction to line 355; change </UL> to </OL>.

commit 3e68c2e26cd89cb983eb52c024ecdfba1d6b3fff
Author: kwirth <kwirth@perforce.com>
Date:   Tue Jul 7 01:35:51 2009 -0800

    Fix spelling error on Jam doc page (cummulative -> cumulative).

واردات شما آماده است تا به سرور جدید Git منتقل شود.

یک واردکننده سفارشی (A Custom Importer)

اگر سیستم شما جزو موارد بالا نیست، باید به دنبال یک واردکننده در اینترنت بگردید — واردکننده‌های باکیفیت برای بسیاری از سیستم‌های دیگر هم موجود است، از جمله CVS، ClearCase، Visual SourceSafe، حتی یک فهرست از آرشیوها. اگر هیچ‌یک از این ابزارها برای شما کارساز نبود، یا ابزار شما نامأنوس‌تر است، یا به هر دلیل به فرایند وارد کردن سفارشی‌تری نیاز دارید، باید از git fast-import استفاده کنید. این دستورالعمل، دستورهای ساده‌ای را از stdin می‌خواند تا داده‌های مشخص گیت را بنویسد. ایجاد اشیای گیت به این روش بسیار آسان‌تر است تا اجرای دستورات خام گیت یا تلاش برای نوشتن اشیای خام (برای اطلاعات بیشتر به (Git Internals) مراجعه کنید). به این ترتیب می‌توانید یک اسکریپت واردکننده بنویسید که اطلاعات لازم را از سیستمی که می‌خواهید وارد کنید بخواند و دستورالعمل‌های ساده را به stdout چاپ کند. سپس می‌توانید این برنامه را اجرا کرده و خروجی آن را به git fast-import منتقل کنید.

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

$ ls /opt/import_from
back_2014_01_02
back_2014_01_04
back_2014_01_14
back_2014_02_03
current

برای وارد کردن یک دایرکتوری به گیت، باید مرور کنید که گیت داده‌هایش را چگونه ذخیره می‌کند. همان‌طور که احتمالاً به یاد دارید، گیت اساساً یک فهرست پیوندی از اشیای commit است که به یک تصویر (snapshot) از محتوا اشاره می‌کنند. تمام کاری که باید بکنید این است که به fast-import بگویید تصویربرداری‌های محتوا چه هستند، داده‌های commit چه چیزی را به آن‌ها اشاره می‌کنند، و ترتیب آن‌ها چیست. استراتژی شما این خواهد بود که تک‌تک تصاویر را بررسی کرده و commitهایی با محتوای هر دایرکتوری ایجاد کنید و هر commit را به commit قبلی پیوند دهید.

 همان‌طور که در <<_an_example_git_enforced_policy>> انجام دادیم، این را هم به روبی می‌نویسیم، چون همان زبانی است که معمولاً با آن کار می‌کنیم و خواندنش ساده است.
شما می‌توانید این مثال را به‌راحتی با هر زبانی که با آن آشنا هستید بنویسید — فقط باید اطلاعات مناسب را به stdout چاپ کند.
و اگر روی ویندوز اجرا می‌کنید، باید دقت ویژه‌ای کنید که در انتهای خطوط CR (carriage return) اضافه نکنید — git fast-import نسبت به خط‌ها بسیار دقیق است و فقط Line Feed (LF) را می‌پذیرد، نه ترکیب CRLF که ویندوز استفاده می‌کند.

برای شروع، به دایرکتوری مقصد می‌روید و هر زیرپوشه را شناسایی می‌کنید؛ هر کدام از این زیرپوشه‌ها یک شات‌نِپ است که می‌خواهید به‌عنوان یک commit وارد کنید. به هر زیرپوشه می‌روید و فرمان‌های لازم برای اکسپورت آن را چاپ می‌کنید. حلقهٔ اصلی پایه‌ای شما شبیه این است:

last_mark = nil

# loop through the directories
Dir.chdir(ARGV[0]) do
  Dir.glob("*").each do |dir|
    next if File.file?(dir)

    # move into the target directory
    Dir.chdir(dir) do
      last_mark = print_export(dir, last_mark)
    end
  end
end

شما در هر دایرکتوری تابع print_export را اجرا می‌کنید که manifest و mark شات‌نِپ قبلی را می‌گیرد و manifest و mark این شات‌نِپ را برمی‌گرداند؛ به این ترتیب می‌توانید آن‌ها را درست به هم پیوند دهید. «Mark» اصطلاح fast-import برای شناسه‌ای است که به یک commit می‌دهید؛ هرگاه commit می‌سازید، به هر کدام یک mark می‌دهید که بتوانید از سایر commitها به آن ارجاع دهید. پس اولین کاری که در متد print_export باید انجام دهید این است که از نام دایرکتوری یک mark بسازید:

mark = convert_dir_to_mark(dir)

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

$marks = []
def convert_dir_to_mark(dir)
  if !$marks.include?(dir)
    $marks << dir
  end
  ($marks.index(dir) + 1).to_s
end

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

date = convert_dir_to_date(dir)

که در آن convert_dir_to_date به‌صورت زیر تعریف شده است:

def convert_dir_to_date(dir)
  if dir == 'current'
    return Time.now().to_i
  else
    dir = dir.gsub('back_', '')
    (year, month, day) = dir.split('_')
    return Time.local(year, month, day).to_i
  end
end

این مقدار یک عدد صحیح برای تاریخ هر دایرکتوری برمی‌گرداند. آخرین تکهٔ اطلاعات متا که برای هر commit نیاز دارید، داده‌های committer است که آن را در یک متغیر سراسری به‌صورت ثابت قرار می‌دهید:

$author = 'John Doe <john@example.com>'

حالا آماده‌اید که چاپ داده‌های commit برای واردکننده‌تان را شروع کنید. اطلاعات اولیه بیان می‌کند که شما یک شیء commit را تعریف می‌کنید و روی کدام شاخه است، سپس مارکی که تولید کرده‌اید، اطلاعات committer و پیام commit و در ادامه commit قبلی، در صورت وجود، آمده است. کد به این شکل است:

# print the import information
puts 'commit refs/heads/master'
puts 'mark :' + mark
puts "committer #{$author} #{date} -0700"
export_data('imported from ' + dir)
puts 'from :' + last_mark if last_mark

شما ناحیه زمانی را به‌صورت ثابت (-0700) قرار می‌دهید چون این کار ساده است. اگر از سیستم دیگری وارد می‌کنید، باید ناحیه زمانی را به‌صورت آفست مشخص کنید. پیام commit باید به قالب ویژه‌ای بیان شود:

data (size)\n(contents)

قالب شامل واژه data، اندازه داده‌ای که باید خوانده شود، یک خط جدید و در نهایت خود داده است. از آنجا که بعداً هم باید از همان قالب برای مشخص کردن محتویات فایل‌ها استفاده کنید، یک متد کمکی به نام export_data ایجاد می‌کنید:

def export_data(string)
  print "data #{string.size}\n#{string}"
end

تنها کاری که باقی می‌ماند مشخص کردن محتویات فایل برای هر snapshot است. این کار آسان است، چون هر کدام را در یک دایرکتوری دارید — می‌توانید دستور deleteall را چاپ کنید و سپس محتوای هر فایل در دایرکتوری را خروجی دهید. سپس Git هر snapshot را به‌درستی ثبت خواهد کرد:

puts 'deleteall'
Dir.glob("**/*").each do |file|
  next if !File.file?(file)
  inline_data(file)
end

توجه: چون بسیاری از سیستم‌ها بازنگری‌هایشان را به‌عنوان تفاوت‌ها بین یک commit و commit بعدی می‌دانند، fast-import همچنین می‌تواند دستوراتی همراه هر commit بپذیرد که مشخص کند کدام فایل‌ها افزوده، حذف یا تغییر کرده‌اند و محتوای جدید چیست. می‌توانید اختلافات بین snapshotها را محاسبه کنید و تنها همین داده‌ها را ارائه دهید، اما انجام این کار پیچیده‌تر است — بهتر است تمام داده‌ها را به Git بدهید و بگذارید خودش آن‌ها را حل کند. اگر این روش برای داده‌های شما مناسب‌تر است، صفحه man برای fast-import را برای جزئیات نحوه ارائه داده‌ها به این شکل بررسی کنید.

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

M 644 inline path/to/file
data (size)
(file contents)

در اینجا، 644 حالت دسترسی است (اگر فایل‌های اجرایی دارید باید این موضوع را تشخیص دهید و 755 را مشخص کنید)، و inline می‌گوید محتوای فایل بلافاصله بعد از این خط فهرست خواهد شد. متد inline_data شما به این شکل است:

def inline_data(file, code = 'M', mode = '644')
  content = File.read(file)
  puts "#{code} #{mode} inline #{file}"
  export_data(content)
end

شما از متد export_data که قبلاً تعریف کرده‌اید استفاده می‌کنید، زیرا همانطور است که داده‌های پیام کامیت خود را مشخص کرده‌ بودید.

آخرین کاری که باید انجام دهید این است که نشانگر جاری (current mark) را بازگردانید تا بتوان آن را به تکرار بعدی منتقل کرد:

return mark
یادداشت

اگر روی ویندوز اجرا می‌کنید باید یک گام اضافی اضافه کنید. همان‌طور که پیش‌تر گفته شد، ویندوز از CRLF برای کاراکترهای پایان خط استفاده می‌کند در حالی که git fast-import تنها انتظار LF دارد. برای رفع این مشکل و راضی نگه داشتن git fast-import، باید به روبی بگویید که به جای CRLF از LF استفاده کند:

$stdout.binmode

همین بود. در اینجا اسکریپت به‌طور کامل آمده است:

#!/usr/bin/env ruby

$stdout.binmode
$author = "John Doe <john@example.com>"

$marks = []
def convert_dir_to_mark(dir)
    if !$marks.include?(dir)
        $marks << dir
    end
    ($marks.index(dir)+1).to_s
end

def convert_dir_to_date(dir)
    if dir == 'current'
        return Time.now().to_i
    else
        dir = dir.gsub('back_', '')
        (year, month, day) = dir.split('_')
        return Time.local(year, month, day).to_i
    end
end

def export_data(string)
    print "data #{string.size}\n#{string}"
end

def inline_data(file, code='M', mode='644')
    content = File.read(file)
    puts "#{code} #{mode} inline #{file}"
    export_data(content)
end

def print_export(dir, last_mark)
    date = convert_dir_to_date(dir)
    mark = convert_dir_to_mark(dir)

    puts 'commit refs/heads/master'
    puts "mark :#{mark}"
    puts "committer #{$author} #{date} -0700"
    export_data("imported from #{dir}")
    puts "from :#{last_mark}" if last_mark

    puts 'deleteall'
    Dir.glob("**/*").each do |file|
        next if !File.file?(file)
        inline_data(file)
    end
    mark
end

# Loop through the directories
last_mark = nil
Dir.chdir(ARGV[0]) do
    Dir.glob("*").each do |dir|
        next if File.file?(dir)

        # move into the target directory
        Dir.chdir(dir) do
            last_mark = print_export(dir, last_mark)
        end
    end
end

اگر این اسکریپت را اجرا کنید، محتوایی شبیه به این به دست خواهید آورد:

$ ruby import.rb /opt/import_from
commit refs/heads/master
mark :1
committer John Doe <john@example.com> 1388649600 -0700
data 29
imported from back_2014_01_02deleteall
M 644 inline README.md
data 28
# Hello

This is my readme.
commit refs/heads/master
mark :2
committer John Doe <john@example.com> 1388822400 -0700
data 29
imported from back_2014_01_04from :1
deleteall
M 644 inline main.rb
data 34
#!/bin/env ruby

puts "Hey there"
M 644 inline README.md
(...)

برای اجرای ایمپورتر، این خروجی را هنگام قرار داشتن در پوشه گیت که می‌خواهید در آن ایمپورت کنید، به git fast-import پایپ کنید. برای شروع می‌توانید یک پوشه جدید بسازید و در آن git init را اجرا کنید، سپس اسکریپت را اجرا نمایید:

$ git init
Initialized empty Git repository in /opt/import_to/.git/
$ ruby import.rb /opt/import_from | git fast-import
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:       5000
Total objects:           13 (         6 duplicates                  )
      blobs  :            5 (         4 duplicates          3 deltas of          5 attempts)
      trees  :            4 (         1 duplicates          0 deltas of          4 attempts)
      commits:            4 (         1 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:           1 (         1 loads     )
      marks:           1024 (         5 unique    )
      atoms:              2
Memory total:          2344 KiB
       pools:          2110 KiB
     objects:           234 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =         10
pack_report: pack_mmap_calls          =          5
pack_report: pack_open_windows        =          2 /          2
pack_report: pack_mapped              =       1457 /       1457
---------------------------------------------------------------------

همان‌طور که می‌بینید، وقتی با موفقیت انجام شد، آمار متعددی از آنچه انجام شده به شما می‌دهد. در این مورد، مجموعاً 13 شی برای 4 کامیت در 1 شاخه وارد شده است. اکنون می‌توانید با اجرای git log تاریخچه جدید خود را ببینید:

$ git log -2
commit 3caa046d4aac682a55867132ccdfbe0d3fdee498
Author: John Doe <john@example.com>
Date:   Tue Jul 29 19:39:04 2014 -0700

    imported from current

commit 4afc2b945d0d3c8cd00556fbe2e8224569dc9def
Author: John Doe <john@example.com>
Date:   Mon Feb 3 01:00:00 2014 -0700

    imported from back_2014_02_03

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

$ ls
$ git reset --hard master
HEAD is now at 3caa046 imported from current
$ ls
README.md main.rb

با ابزار fast-import می‌توانید کارهای بیشتری انجام دهید — حالت‌های مختلف، داده‌های باینری، شاخه‌ها و ادغام‌های متعدد، تگ‌ها، نمایش پیشرفت و موارد دیگر. چندین مثال از سناریوهای پیچیده‌تر در دایرکتوری contrib/fast-import در سورس کد گیت موجود است.

scroll-to-top