git-filter-branch -
بازنویسی
شاخهها
git filter-branch [--setup <command>] [--subdirectory-filter <directory>]
[--env-filter <command>] [--tree-filter <command>]
[--index-filter <command>] [--parent-filter <command>]
[--msg-filter <command>] [--commit-filter <command>]
[--tag-name-filter <command>] [--prune-empty]
[--original <namespace>] [-d <directory>] [-f | --force]
[--state-branch <branch>] [--] [<rev-list-options>...]
دستور git filter-branch
دارای
دامهای
فراوانی
است که
میتوانند
منجر به
خرابیهای
غیرواضح در
بازنویسی
هدفمند
تاریخچه
شوند (و به
دلیل
کارایی
بسیار ضعیف
آن، زمان
اندکی برای
بررسی چنین
مشکلاتی
برایتان
باقی
میگذارد).
این مسائل
امنیتی و
عملکردی را
نمیتوان
با حفظ
سازگاری با
گذشته
اصلاح کرد و
به همین
دلیل،
استفاده از
آن توصیه
نمیشود.
لطفاً از یک
ابزار
جایگزین
برای فیلتر
کردن
تاریخچه
مانند git filter-repo[1]
استفاده
کنید. اگر
همچنان
نیاز به
استفاده از
git filter-branch دارید،
لطفاً بخش
«امنیت (SAFETY)» (و
بخش
«کارایی
(PERFORMANCE)») را به
دقت
بخوانید تا
با خطرات
پنهان filter-branch
آشنا شوید،
و سپس تا جای
ممکن و
هوشیارانه
از خطرات
ذکرشده در
آنجا
اجتناب
کنید.
به شما
امکان
میدهد تا
با
بازنویسی
شاخههای
ذکرشده در
<rev-list-options> و اعمال
فیلترهای
سفارشی روی
هر
بازنگری،
تاریخچه
بازنگریهای
گیت را
بازنویسی
کنید. این
فیلترها
میتوانند
هر درخت
(مانند حذف
یک فایل یا
اجرای یک
اسکریپت
بازنویسی perl
روی تمام
فایلها) یا
اطلاعات
مربوط به هر
کامیت را
تغییر دهند.
در غیر این
صورت، تمام
اطلاعات (از
جمله
زمانهای
اصلی کامیت
یا اطلاعات
ادغام) حفظ
خواهند شد.
این دستور
تنها
ارجاعهای
مثبت
ذکرشده در
خط فرمان را
بازنویسی
خواهد کرد
(برای مثال،
اگر a..b را
بدهید،
تنها b
بازنویسی
میشود). اگر
هیچ فیلتری
مشخص
نکنید،
کامیتها
بدون هیچ
تغییری
دوباره ثبت
میشوند که
در حالت
عادی هیچ
اثری
نخواهد
داشت. با این
وجود، این
کار ممکن
است در
آینده برای
جبران برخی
باگهای
گیت یا
موارد
مشابه مفید
باشد،
بنابراین
چنین
کاربردی
مجاز است.
نکته: این
دستور فایل
.git/info/grafts و
ارجاعهای
موجود در
فضای نام
refs/replace/ را
محترم
میشمارد.
اگر
پیوندها (grafts)
یا
ارجاعهای
جایگزینی
تعریف کرده
باشید،
اجرای این
دستور
آنها را
دائمی
خواهد کرد.
هشدار!
تاریخچه
بازنویسیشده
دارای
شناسههای
شیء
متفاوتی
برای تمامی
اشیاء
خواهد بود و
با شاخه
اصلی همگرا
نخواهد شد.
شما
نمیتوانید
به آسانی
شاخه
بازنویسیشده
را روی شاخه
اصلی ارسال
(push) و توزیع
کنید. اگر
پیامدهای
کامل آن را
نمیدانید
لطفاً از
این دستور
استفاده
نکنید، و در
هر صورت اگر
یک کامیت
ساده تکی
برای حل
مشکلتان
کفایت
میکند، از
بهکارگیری
آن
بپرهیزید.
(برای
اطلاعات
بیشتر
پیرامون
بازنویسی
تاریخچه
منتشرشده،
بخش "RECOVERING FROM UPSTREAM REBASE"
را در git-rebase(1)
ببینید.)
همیشه
بررسی کنید
که نسخه
بازنویسیشده
درست باشد:
ارجاعهای
اصلی، در
صورتی که با
ارجاعهای
بازنویسیشده
متفاوت
باشند، در
فضای نام
refs/original/ ذخیره
خواهند شد.
توجه
داشته
باشید که از
آنجا که این
عملیات از
نظر
ورودی/خروجی
(I/O) بسیار
سنگین است،
انتقال
مسیر پوشه
موقت به
خارج از
دیسک با
گزینه -d
(برای مثال
روی tmpfs) ایده
خوبی خواهد
بود. طبق
گزارشها،
افزایش
سرعت بسیار
چشمگیر
است.
فیلترها
به ترتیبی
که در زیر
آمده اعمال
میشوند.
آرگومان <command>
همیشه در
محیط پوسته
با استفاده
از دستور eval
ارزیابی
میشود (به
استثنای
فیلتر commit، به
دلایل فنی).
پیش از آن،
متغیر
محیطی $GIT_COMMIT
طوری تنظیم
میشود که
شامل شناسه
کامیت در
حال
بازنویسی
باشد.
همچنین GIT_AUTHOR_NAME،
GIT_AUTHOR_EMAIL، GIT_AUTHOR_DATE، GIT_COMMITTER_NAME،
GIT_COMMITTER_EMAIL و GIT_COMMITTER_DATE از
کامیت جاری
گرفته شده و
به
متغیرهای
محیطی صادر
میشوند تا
هویت
پدیدآورنده
و
ثبتکننده
کامیت
جایگزین که
توسط git-commit-tree(1) پس
از اجرای
فیلترها
ایجاد
میشود را
تحت تأثیر
قرار دهند.
اگر هر
گونه
ارزیابی <command>
وضعیت خروج
غیر صفر
برگرداند،
کل عملیات
متوقف
خواهد شد.
یک تابع map
در دسترس
است که یک
آرگومان
"شناسه sha1
اصلی" را
میگیرد و
اگر کامیت
قبلاً
بازنویسی
شده باشد
"شناسه sha1
بازنویسیشده"
و در غیر این
صورت
"شناسه sha1
اصلی" را
خروجی
میدهد؛
اگر فیلتر commit
شما چندین
کامیت
تولید کرده
باشد، تابع
map میتواند
چندین
شناسه را در
خطوط
جداگانه
برگرداند.
--setup <command>
این یک
فیلتر
واقعی که
برای هر
کامیت اجرا
شود نیست،
بلکه یک
راهاندازی
یکباره
درست پیش از
آغاز حلقه
است.
بنابراین
هنوز هیچ
متغیر ویژه
کامیت
تعریف نشده
است. توابع
یا
متغیرهای
تعریفشده
در اینجا را
میتوان در
مراحل
فیلتر بعدی
استفاده
کرد یا
تغییر داد
(به دلایل
فنی، به جز
فیلتر commit).
--subdirectory-filter <directory>
تنها
تاریخچهای
را بررسی
میکند که
زیرپوشه
دادهشده
را لمس کرده
باشد. نتیجه
شامل آن
پوشه (و تنها
همان) به
عنوان ریشه
پروژه
خواهد بود.
مستلزم بخش
موسوم به
«نگاشت
مجدد به
نیای والد»
است.
--env-filter <command>
اگر تنها
نیاز به
تغییر
محیطی
دارید که
کامیت در آن
انجام
خواهد شد،
این فیلتر
میتواند
استفاده
شود. به طور
خاص، ممکن
است
بخواهید
متغیرهای
محیطی
مربوط به
نام/رایانامه/زمان
پدیدآورنده/ثبتکننده
را
بازنویسی
کنید (برای
جزئیات
git-commit-tree(1)
را ببینید).
--tree-filter <command>
این
فیلتر برای
بازنویسی
درخت و
محتویات آن
است.
آرگومان در
پوسته
همراه با
تنظیم پوشه
کاری بر روی
ریشه درخت
تحویلگرفتهشده
ارزیابی
میشود. سپس
درخت جدید
همانگونه
که هست به
کار میرود
(فایلهای
جدید به طور
خودکار
اضافه
میشوند،
فایلهای
ناپدیدشده
به طور
خودکار حذف
میشوند - نه
فایلهای .gitignore
و نه هیچ
قانون
نادیدهگرفتن
دیگری هیچ
اثری
ندارند!).
--index-filter <command>
این
فیلتر برای
بازنویسی
نمایه (index) است.
این فیلتر
شبیه فیلتر tree
است اما
درخت را
تحویل (checkout)
نمیگیرد،
که این
موضوع آن را
بسیار
سریعتر
میکند. به
طور مکرر
همراه با
git rm
--cached --ignore-unmatch ...
استفاده
میشود،
بخش
مثالها در
زیر را
ببینید.
برای موارد
پیچیدهتر،
git-update-index(1) را
ببینید.
--parent-filter <command>
این
فیلتر برای
بازنویسی
فهرست والد
کامیت است.
رشته والد
را در ورودی
استاندارد
دریافت
خواهد کرد و
باید رشته
والد جدید
را در خروجی
استاندارد
ارائه دهد.
رشته والد
در قالبی
است که در
git-commit-tree(1) شرح
داده شده
است: خالی
برای کامیت
اولیه، "
-p parent"
برای یک
کامیت عادی
و "
-p parent1 -p parent2 -p parent3 ..."
برای یک
کامیت
ادغام.
--msg-filter <command>
این
فیلتر برای
بازنویسی
پیامهای
کامیت است.
آرگومان در
پوسته
همراه با
پیام کامیت
اصلی در
ورودی
استاندارد
ارزیابی
میشود؛
خروجی
استاندارد
آن به عنوان
پیام کامیت
جدید
استفاده
خواهد شد.
--commit-filter <command>
این
فیلتر برای
انجام خود
کامیت است.
اگر این
فیلتر مشخص
شود، به جای
دستور
git commit-tree
همراه با
آرگومانهایی
در قالب "<TREE_ID> [(
-p
<PARENT_COMMIT_ID>)...]" و پیام
لاگ در
ورودی
استاندارد
فراخوانی
خواهد شد.
انتظار
میرود
شناسه
کامیت در
خروجی
استاندارد
چاپ شود.
به عنوان
یک گسترش
ویژه،
فیلتر commit
ممکن است
چندین
شناسه
کامیت
تولید کند؛
در آن صورت،
فرزندان
بازنویسیشده
کامیت اصلی
همه آنها
را به عنوان
والد
خواهند
داشت.
میتوانید
از تابع
کمکی map و
همچنین
سایر توابع
کمکی در این
فیلتر
استفاده
کنید. برای
مثال،
فراخوانی
skip_commit "$@" کامیت
جاری را حذف
خواهد کرد
(اما نه
تغییرات آن
را! اگر چنان
چیزی
میخواهید،
در عوض از git rebase
استفاده
کنید).
همچنین
میتوانید
از git_commit_non_empty_tree "$@"
به جای git commit-tree
"$@" استفاده
کنید اگر
مایل
نیستید
کامیتهایی
با یک والد
تک که هیچ
تغییری در
درخت ایجاد
نمیکنند
را نگه
دارید.
--tag-name-filter <command>
این
فیلتر برای
بازنویسی
نام
برچسبها
است. هنگام
ارسال،
برای هر
ارجاع
برچسبی که
به یک شیء
بازنویسیشده
اشاره دارد
(یا به شیء
برچسبی که
به یک شیء
بازنویسیشده
اشاره
میکند)
فراخوانی
خواهد شد.
نام برچسب
اصلی از
طریق ورودی
استاندارد
فرستاده
میشود و
انتظار
میرود نام
برچسب جدید
در خروجی
استاندارد
دریافت شود.
برچسبهای
اصلی حذف
نمیشوند،
اما
میتوان
روی آنها
دوباره
نوشت؛ از
"--tag-name-filter cat" برای
بهروزرسانی
ساده
برچسبها
استفاده
کنید. در این
حالت،
بسیار
محتاط
باشید و
اطمینان
حاصل کنید
که از
برچسبهای
قدیمی نسخه
پشتیبان
تهیه
کردهاید
تا در صورتی
که تبدیل به
بیراهه
رفت، مشکلی
پیش نیاید.
بازنویسی
تقریباً
مناسب
اشیاء
برچسب
پشتیبانی
میشود. اگر
برچسب
دارای
پیامی
ضمیمهشده
باشد، یک
شیء برچسب
جدید با
همان پیام،
پدیدآورنده
و مهر زمانی
ایجاد
خواهد شد.
اگر برچسب
دارای یک
امضا باشد،
امضا حذف
خواهد شد؛
زیرا طبق
تعریف، حفظ
امضاها
غیرممکن
است. دلیل
اینکه این
کار
"تقریباً"
مناسب است
این است که
در حالت
ایدهآل
اگر برچسب
تغییر
نکرده باشد
(به همان شیء
اشاره کند،
همان نام را
داشته باشد
و...) باید هر
گونه امضا
را حفظ کند.
در عمل چنین
نیست و
امضاها
همواره حذف
خواهند شد؛
بنابراین
هوشیار
باشید.
همچنین هیچ
پشتیبانی
برای تغییر
پدیدآورنده
یا مهر
زمانی (یا در
واقع پیام
برچسب) وجود
ندارد.
برچسبهایی
که به
برچسبهای
دیگر اشاره
میکنند
طوری
بازنویسی
خواهند شد
که به کامیت
بنیادین
اشاره
نمایند.
--prune-empty
برخی
فیلترها
کامیتهای
خالی تولید
میکنند که
درخت را
دستنخورده
باقی
میگذارد.
این گزینه
به git-filter-branch دستور
میدهد تا
اگر چنین
کامیتهایی
دقیقاً یک
یا صفر والد
هرسنشده
دارند،
آنها را
حذف کند؛
بنابراین
کامیتهای
ادغام
دستنخورده
باقی
خواهند
ماند. این
گزینه
نمیتواند
همراه با
--commit-filter
استفاده
شود، اگرچه
میتوان با
استفاده از
تابع
ارائهشده
git_commit_non_empty_tree در یک
فیلتر commit به
همین نتیجه
دست یافت.
--original <namespace>
از این
گزینه برای
تنظیم فضای
نامی که
کامیتهای
اصلی در آن
ذخیره
میشوند
استفاده
کنید. مقدار
پیشفرض refs/original
است.
-d <directory>
از این
گزینه برای
تعیین مسیر
پوشه موقت
مورداستفاده
برای
بازنویسی
استفاده
کنید. هنگام
اعمال
فیلتر tree،
دستور
نیازمند
تحویل
گرفتن موقت
درخت در یک
پوشه است،
که در مورد
پروژههای
بزرگ ممکن
است فضای
قابلتوجهی
را مصرف کند.
به صورت
پیشفرض
این کار در
پوشه .git-rewrite/
انجام
میشود اما
میتوانید
این انتخاب
را با این
پارامتر
لغو کنید.
-f, --force
دستور git
filter-branch از شروع
به کار با یک
پوشه موقت
موجود یا
هنگامی که
از پیش
ارجاعهایی
با شروع refs/original/
وجود داشته
باشند
خودداری
میکند،
مگر اینکه
اجبار شود.
--state-branch <branch>
این
گزینه موجب
میشود تا
نگاشت از
اشیاء
قدیمی به
جدید در
زمان
راهاندازی
از شاخه
نامبرده
بارگیری
شده و هنگام
خروج به
عنوان یک
کامیت جدید
در آن شاخه
ذخیره شود،
که امکان
بازنویسی
افزایشی
درختهای
بزرگ را
فراهم
میسازد.
اگر <branch>
وجود
نداشته
باشد،
ایجاد
خواهد شد.
<rev-list options>...
آرگومانهایی
برای git rev-list.
تمامی
ارجاعهای
مثبت مشمول
این
گزینهها
بازنویسی
میشوند.
همچنین
میتوانید
گزینههایی
مانند --all را
مشخص کنید،
اما باید از
-- برای جدا
کردن آنها
از
گزینههای
git filter-branch
استفاده
کنید.
مستلزم بخش
موسوم به
«نگاشت
مجدد به
نیای والد»
است.
با
استفاده از
آرگومانهای
git-rev-list(1)، به
عنوان مثال
محدودکنندههای
مسیر،
میتوانید
مجموعه
بازنگریهایی
را که
بازنویسی
میشوند
محدود کنید.
با این حال،
ارجاعهای
مثبت در خط
فرمان
متمایز
هستند: ما
اجازه
نمیدهیم
که آنها
توسط چنین
محدودکنندههایی
کنار
گذاشته
شوند. برای
این منظور،
آنها در
عوض طوری
بازنویسی
میشوند که
به
نزدیکترین
نیای والدی
که کنار
گذاشته
نشده بود
اشاره
کنند.
در صورت
موفقیت،
وضعیت خروج
0 است. اگر
فیلتر
نتواند هیچ
کامیتی
برای
بازنویسی
پیدا کند،
وضعیت خروج
2 خواهد بود.
در هر خطای
دیگری،
وضعیت خروج
ممکن است هر
مقدار غیر
صفر دیگری
باشد.
فرض کنید
میخواهید
فایلی (حاوی
اطلاعات
محرمانه یا
نقض حق
تکثیر) را از
تمام
کامیتها
حذف کنید:
git filter-branch --tree-filter 'rm filename' HEAD
با این
حال، اگر
فایل در
درخت برخی
از
کامیتها
غایب باشد،
یک دستور
ساده rm filename
برای آن
درخت و
کامیت
ناموفق
خواهد بود.
بنابراین
ممکن است
بخواهید به
جای آن از rm
-f filename به
عنوان
اسکریپت
استفاده
کنید.
استفاده
از --index-filter
همراه با git rm
نسخهای به
مراتب
سریعتر را
به دست
میدهد.
مشابه
استفاده از
rm filename، دستور
git rm --cached filename
نیز اگر
فایل در
درخت یک
کامیت
موجود
نباشد، با
شکست مواجه
میشود. اگر
میخواهید
فایلی را
«کاملاً به
فراموشی
بسپارید»،
مهم نیست چه
زمانی وارد
تاریخچه
شده است،
بنابراین
ما --ignore-unmatch را
نیز
میافزاییم:
git filter-branch --index-filter 'git rm --cached --ignore-unmatch filename' HEAD
اکنون،
تاریخچه
بازنویسیشده
ذخیرهشده
در HEAD را در
اختیار
خواهید
داشت.
برای
بازنویسی
مخزن به
گونهای که
گویی foodir/
ریشه پروژه
آن بوده
است، و دور
انداختن
سایر
بخشهای
تاریخچه:
git filter-branch --subdirectory-filter foodir -- --all
بدین
ترتیب
میتوانید
برای مثال،
یک زیرپوشه
کتابخانه
را به مخزنی
مستقل برای
خود تبدیل
کنید. به --
که
گزینههای
filter-branch را از
گزینههای
revision جدا
میکند، و
به --all برای
بازنویسی
تمام
شاخهها و
برچسبها
دقت کنید.
برای
تعیین یک
کامیت (که
معمولاً در
رأس یک
تاریخچه
دیگر قرار
دارد) به
عنوان والد
کامیت
اولیه
کنونی، جهت
چسباندن
تاریخچه
دیگر در پشت
سر تاریخچه
جاری:
git filter-branch --parent-filter 'sed "s/^\$/-p <graft-id>/"' HEAD
(اگر رشته
والد خالی
باشد - که
هنگام
برخورد با
کامیت
اولیه رخ
میدهد - graftcommit
را به عنوان
والد بیفزا).
توجه داشته
باشید که
این دستور
فرض میکند
تاریخچه
دارای یک
ریشه واحد
است (یعنی
هیچ ادغامی
بدون اجداد
مشترک رخ
نداده است).
در غیر این
صورت، از
این دستور
استفاده
کنید:
git filter-branch --parent-filter \
'test $GIT_COMMIT = <commit-id> && echo "-p <graft-id>" || cat' HEAD
یا حتی
سادهتر:
git replace --graft $commit-id $graft-id
git filter-branch $graft-id..HEAD
برای حذف
کامیتهای
ثبتشده
توسط «Darl McBribe»
از
تاریخچه:
git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_NAME" = "Darl McBribe" ];
then
skip_commit "$@";
else
git commit-tree "$@";
fi' HEAD
تابع skip_commit
به صورت زیر
تعریف شده
است:
skip_commit()
{
shift;
while [ -n "$1" ];
do
shift;
map "$1";
shift;
done;
}
ترفند shift
ابتدا
شناسه tree و
سپس
پارامترهای
-p را دور
میاندازد.
توجه کنید
که این
ترفند
ادغامها
را به درستی
مدیریت
میکند!
چنانچه Darl یک
ادغام میان P1
و P2 را کامیت
کرده باشد،
این تغییر
به درستی
منتشر
خواهد شد و
تمام
فرزندان آن
ادغام، به
جای کامیت
ادغام،
تبدیل به
کامیتهای
ادغامی با
والدینی
متشکل از P1,P2
میشوند.
توجه
تغییرات
ایجادشده
توسط
کامیتها
که توسط
کامیتهای
پسین لغو (revert)
نشده
باشند،
همچنان در
شاخه
بازنویسیشده
باقی
خواهند
ماند. اگر
میخواهید
تغییرات
را همراه با
خود
کامیتها
به دور
بیندازید،
باید از
حالت
تعاملی (interactive)
دستور git rebase
استفاده
کنید.
میتوانید
پیامهای
گزارش
کامیت را با
استفاده از
--msg-filter
بازنویسی
کنید. برای
نمونه،
رشتههای git
svn-id در مخزنی
که توسط git svn
ایجاد شده
است را
میتوان به
این صورت
حذف نمود:
git filter-branch --msg-filter '
sed -e "/^git-svn-id:/d"
'
چنانچه
لازم است
خطوط Acked-by را
مثلاً به ۱۰
کامیت
پایانی (که
هیچکدام
ادغام
نیستند)
بیفزایید،
از این
دستور
استفاده
کنید:
git filter-branch --msg-filter '
cat &&
echo "Acked-by: Bugs Bunny <bunny@bugzilla.org>"
' HEAD~10..HEAD
گزینه --env-filter
را میتوان
برای تغییر
هویت
ثبتکننده
و/یا
پدیدآورنده
به کار برد.
برای
نمونه، اگر
متوجه شدید
که
کامیتهای
شما به دلیل
تنظیم
نادرست user.email
هویت
اشتباهی
دارند، پیش
از انتشار
پروژه
میتوانید
تصحیحی به
این صورت
انجام
دهید:
git filter-branch --env-filter '
if test "$GIT_AUTHOR_EMAIL" = "root@localhost"
then
GIT_AUTHOR_EMAIL=john@example.com
fi
if test "$GIT_COMMITTER_EMAIL" = "root@localhost"
then
GIT_COMMITTER_EMAIL=john@example.com
fi
' -- --all
برای
محدود کردن
بازنویسی
تنها به
بخشی از
تاریخچه،
یک بازه
بازنگری (revision range)
را علاوه بر
نام شاخه
جدید مشخص
کنید. نام
شاخه جدید
به
بالاترین
بازنگریای
اشاره
خواهد کرد
که یک git rev-list از
این بازه
چاپ
میکند.
این
تاریخچه را
در نظر
بگیرید:
D--E--F--G--H
/ /
A--B-----C
برای
بازنویسی
تنها
کامیتهای
D، E، F، G و H، اما
دستنخورده
گذاشتن A، B و
C، از این
دستور
استفاده
کنید:
git filter-branch ... C..H
برای
بازنویسی
کامیتهای
E، F، G و H، از
یکی از این
دستورات
استفاده
کنید:
git filter-branch ... C..H --not D
git filter-branch ... D..H --not C
برای
انتقال
تمام
ساختار
درخت به یک
زیرپوشه،
یا حذف آن از
آنجا:
git filter-branch --index-filter \
'git ls-files -s | sed "s-\t\"*-&newsubdir/-" |
GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
git update-index --index-info &&
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD
میتوان
از git-filter-branch برای
خلاص شدن از
زیرمجموعهای
از فایلها
استفاده
کرد،
معمولاً با
ترکیبی از
--index-filter و --subdirectory-filter.
افراد
انتظار
دارند مخزن
حاصل
کوچکتر از
نسخه اولیه
باشد، اما
برای
کوچکتر
کردن واقعی
آن به چند
مرحله دیگر
نیاز
دارید،
زیرا گیت
سخت تلاش
میکند
اشیاء شما
را تا زمانی
که صراحتاً
نگویید از
دست ندهد.
نخست مطمئن
شوید که:
•واقعاً
تمامی
گونههای
یک نام فایل
را حذف
کردهاید،
اگر یک شیء
داده (blob) در
طول عمر خود
جابجا شده
باشد. git log --name-only
--follow --all -- filename
میتواند
به شما در
یافتن
تغییر
نامها کمک
کند.
•واقعاً
تمامی
ارجاعها
را فیلتر
کردهاید:
هنگام
فراخوانی
git-filter-branch از --tag-name-filter cat
-- --all
استفاده
کنید.
سپس دو راه
برای رسیدن
به مخزنی
کوچکتر
وجود دارد.
راه
ایمنتر
شبیهسازی
(clone) است، که
نسخه اصلی
شما را
دستنخورده
نگه
میدارد.
•آن را
با
git clone file:///path/to/repo
کلون کنید.
مخزن
کلونشده
اشیاء
حذفشده را
نخواهد
داشت.
git-clone(1) را
ببینید.
(توجه داشته
باشید که
کلون کردن
با یک مسیر
معمولی فقط
همه چیز را
به صورت سخت
پیوند
میدهد!).
اگر
واقعاً به
هر دلیلی
تمایلی به
کلون کردن
آن ندارید،
به جای آن
موارد زیر
را بررسی
کنید (به
همین ترتیب).
این یک
رویکرد
بسیار مخرب
است،
بنابراین
یک نسخه
پشتیبان
تهیه کنید
یا به روش
کلون کردن
برگردید. به
شما هشدار
داده شده
است.
•ارجاعهای
اصلی
پشتیبانگیریشده
توسط git-filter-branch را
حذف کنید:
مانند git for-each-ref
--format="%(refname)" refs/original/ |
xargs -n 1 git update-ref -d.
•تمام
reflogها را با git
reflog expire --expire=now --all
منقضی
کنید.
•تمام
اشیاء
ارجاعنشده
را با
بازیافت
حافظه (garbage collect)
حذف کنید با
git gc --prune=now (یا
اگر git-gc شما
آنقدر
جدید نیست
که از
آرگومانهای
--prune
پشتیبانی
کند، به جای
آن از git repack -ad;
git prune
استفاده
کنید).
کارایی git-filter-branch
بسیار کُند
و لاکپشتی
است؛ طراحی
آن باعث
میشود که
یک
پیادهسازی
سازگار با
گذشته هرگز
نتواند
سریع باشد:
•در
ویرایش
فایلها،
git-filter-branch طبق
طراحی خود
تکتک
کامیتها
را دقیقاً
همانگونه
که در مخزن
اصلی وجود
داشتند
تحویل (checkout)
میگیرد.
اگر مخزن
شما دارای 10^5
فایل و 10^5
کامیت
باشد، اما
هر کامیت
تنها پنج
فایل را
تغییر دهد،
آنگاه git-filter-branch
شما را
وادار به
انجام 10^10
تغییر
میکند، با
وجود اینکه
(در بیشترین
حالت) تنها
5*10^5 شیء
حبابی (blob)
یکتا دارد.
•اگر
تلاش کنید
تقلب کنید و
سعی نمایید
کاری کنید
که git-filter-branch تنها
روی
فایلهای
تغییریافته
در یک کامیت
کار کند، دو
اتفاق
میافتد:
•هر زمان
که کاربر به
سادگی در
تلاش برای
تغییر نام
فایلها
باشد، با
مشکلاتی در
حذف
فایلها
مواجه
میشوید
(زیرا تلاش
برای حذف
فایلهایی
که وجود
ندارند
مانند
عملیات
بدون اثر به
نظر
میرسد؛
نگاشت مجدد
حذفها در
میان تغییر
نام
فایلها
زمانی که
تغییر
نامها از
طریق یک
پوسته
دلخواه
ارائهشده
توسط کاربر
رخ میدهد،
نیاز به
فریبکاری
و ترفندهای
پیچیده
دارد)
•حتی اگر
در این
فریبکاری
نگاشت
حذفها
برای تغییر
نام موفق
شوید، از
نظر فنی
همچنان
سازگاری با
گذشته را
نقض
میکنید
زیرا
کاربران
مجازند
فایلها را
به
روشهایی
فیلتر کنند
که به
توپولوژی
کامیتها
وابسته
است، به جای
اینکه
صرفاً بر
اساس محتوا
یا نام
فایلها
فیلتر
نمایند
(اگرچه این
مورد در عمل
دیده نشده
است).
•حتی اگر
نیازی به
ویرایش
فایلها
نداشته
باشید و فقط
بخواهید به
عنوان مثال
برخی را
تغییر نام
دهید یا حذف
کنید و
بنابراین
بتوانید از
تحویل
گرفتن
تکتک
فایلها
اجتناب
کنید (یعنی
بتوانید از
--index-filter استفاده
کنید)،
همچنان در
حال ارسال
قطعهکدهای
پوسته برای
فیلترهای
خود هستید.
این بدان
معنی است که
برای هر
کامیت،
باید یک
مخزن گیت
آماده
داشته
باشید که
این
فیلترها
بتوانند در
آن اجرا
شوند. این یک
بار
راهاندازی
قابلتوجه
است.
•علاوه
بر این،
چندین فایل
اضافی به
ازای هر
کامیت توسط
git-filter-branch ایجاد یا
بهروزرسانی
میشوند.
برخی از این
فایلها
برای
پشتیبانی
از توابع
کمکی
ارائهشده
توسط git-filter-branch
(مانند ()map)
هستند، در
حالی که
برخی دیگر
برای
ردیابی
وضعیت
داخلی به
کار
میروند
(اما
میتوانستند
توسط
فیلترهای
کاربر نیز
قابل
دسترسی
باشند؛ یکی
از
آزمایشهای
رگرسیون git-filter-branch
چنین کاری
میکند). این
در اصل به
منزله
استفاده از
سیستم فایل
به عنوان یک
سازوکار
ارتباط
بینفرآیندی
(IPC) میان git-filter-branch و
فیلترهای
ارائهشده
توسط کاربر
است.
دیسکها
معمولاً
سازوکار IPC
کُندی
هستند، و
نوشتن این
فایلها
همچنین در
عمل
نشاندهنده
یک نقطه
همگامسازی
اجباری
میان
فرآیندهای
مجزا است که
در هر کامیت
با آن مواجه
میشویم.
•دستورات
پوسته
ارائهشده
توسط کاربر
احتمالاً
شامل یک خط
لوله (pipeline) از
دستورات
خواهد بود
که منجر به
ایجاد
فرآیندهای
متعددی به
ازای هر
کامیت
میشود.
ایجاد و
اجرای یک
فرآیند
دیگر میان
سیستمعاملهای
گوناگون
زمان بسیار
متغیری
میبرد،
اما در هر
بستری نسبت
به
فراخوانی
یک تابع
بسیار کُند
است.
•خود git-filter-branch
با اسکریپت
پوسته
نوشته شده
است که تا
حدودی کُند
است. این
تنها مشکل
کارایی است
که
میتوانست
با حفظ
سازگاری با
گذشته
برطرف شود،
اما در
مقایسه با
مشکلات
پیشین که
ذاتی طراحی
git-filter-branch هستند،
زبان نگارش
خود ابزار
یک مسئله
نسبتاً
جزئی است.
•یادداشت
حاشیهای:
متأسفانه،
افراد
تمایل
دارند روی
جنبه
نوشتهشدن
با پوسته
تمرکز کنند
و مرتباً
بپرسند که
آیا
میتوان git-filter-branch
را به زبان
دیگری
بازنویسی
کرد تا
مشکلات
عملکردی
برطرف شود.
این کار نه
تنها
مشکلات
ذاتی
بزرگتر
طراحی را
نادیده
میگیرد،
بلکه بسیار
کمتر از
آنچه
انتظار
میرود
سودمند
خواهد بود:
اگر خود git-filter-branch
پوسته
نبود،
آنگاه
توابع کمکی
(()map، ()skip_commit و غیره)
و آرگومان
--setup دیگر
نمیتوانستند
یک بار در
ابتدای
برنامه
اجرا شوند،
بلکه در عوض
باید به
ابتدای هر
فیلتر
کاربر
افزوده
میشدند (و
بنابراین
با هر کامیت
از نو اجرا
میشدند).
ابزار git filter-repo[1]
جایگزینی
برای git-filter-branch است
که از این
مشکلات
عملکردی یا
مشکلات
امنیتی (که
در زیر ذکر
شده) رنج
نمیبرد.
برای کسانی
که
ابزارهای
موجود
مبتنی بر git-filter-branch
دارند، git filter-repo
همچنین filter-lamely[2]
را ارائه
میدهد که
یک جایگزین
مستقیم
برای git-filter-branch است
(با چند نکته
قابل توجه).
اگرچه filter-lamely از
تمام همان
مشکلات
امنیتی git-filter-branch
رنج
میبرد،
اما دستکم
کمی از
مشکلات
عملکردی را
التیام
میبخشد.
دستور git-filter-branch
پر از
دامهایی
است که منجر
به روشهای
مختلفی
برای خراب
کردن آسان
مخازن یا
رسیدن به
وضعیتی به
مراتب
آشفتهتر
از آنچه
شروع
کردهاید
میشود:
•شخصی
میتواند
مجموعهای
از
«فیلترهای
کارآمد و
آزمودهشده»
داشته باشد
که آنها را
مستند کرده
یا در
اختیار یک
همکار قرار
میدهد، و
سپس آن
همکار
آنها را
روی
سیستمعامل
دیگری اجرا
میکند که
همان
دستورات در
آنجا کار
نمیکنند
یا آزمایش
نشدهاند
(برخی از
مثالهای
موجود در
صفحه
راهنمای git-filter-branch
نیز تحت
تأثیر این
موضوع
هستند).
تفاوتهای
میان فضای
کاربری BSD و GNU
واقعاً
میتواند
دردسرساز
شود. اگر
خوششانس
باشید،
پیامهای
خطا ظاهر
میشوند.
اما به همان
اندازه
محتمل است
که دستورات
یا فیلتر
درخواستی
را انجام
ندهند، یا
با اعمال
تغییراتی
ناخواسته
به طور
خاموش
دادهها را
مخدوش کنند.
تغییر
ناخواسته
ممکن است
تنها بر چند
کامیت
تأثیر
بگذارد،
بنابراین
لزوماً
واضح نیز
نخواهد بود.
(این واقعیت
که مشکلات
لزوماً
آشکار
نخواهند
بود به این
معنی است که
احتمالاً
تا زمانی که
تاریخچه
بازنویسیشده
برای مدتی
طولانی
مورد
استفاده
قرار نگیرد
متوجه
آنها
نخواهید
شد، در این
نقطه توجیه
یک دگرگونی
سراسری
دیگر برای
یک
بازنویسی
جدید بسیار
دشوار
خواهد بود.)
•نام
فایلهای
حاوی فاصله
اغلب توسط
قطعهکدهای
پوسته به
نادرستی
مدیریت
میشوند
زیرا برای
خطوط لوله
پوسته مشکل
ایجاد
میکنند.
همه با find -print0، xargs
-0، git-ls-files -z و غیره
آشنا
نیستند. حتی
افرادی که
با این
موارد آشنا
هستند ممکن
است تصور
کنند چنین
فلگهایی
مرتبط
نیستند
زیرا شخص
دیگری قبل
از پیوستن
فرد مجری
فیلتر به
پروژه، نام
چنین
فایلهایی
را در مخزن
تغییر داده
است. و اغلب،
حتی کسانی
که با
مدیریت
آرگومانهای
دارای
فاصله آشنا
هستند ممکن
است صرفاً
به این دلیل
که در ذهنیت
فکر کردن به
هر چیزی که
احتمال
دارد خراب
شود
نیستند،
این کار را
انجام
ندهند.
•نام
فایلهای
غیر اسکی (non-ascii)
با وجود
اینکه در یک
پوشه مورد
نظر قرار
دارند
میتوانند
به صورت
خاموش حذف
شوند.
نگهداشتن
تنها
مسیرهای
مورد نظر
اغلب با
استفاده از
خطوط
لولهای
مانند git ls-files |
grep -v ^WANTED_DIR/ | xargs git rm
انجام
میشود.
دستور ls-files
تنها در
صورت نیاز
نام
فایلها را
نقلقول
میکند،
بنابراین
ممکن است
افراد
متوجه
نشوند که
یکی از
فایلها با
عبارت
باقاعده
مطابقت
نداشته است
(دستکم نه
تا زمانی که
خیلی دیر
شده باشد).
بله، کسی که
از core.quotePath اطلاع
دارد
میتواند
از این مشکل
جلوگیری
کند (مگر
اینکه
کاراکترهای
خاص دیگری
مانند \t، \n یا "
داشته
باشند)، و
افرادی که
از ls-files -z با چیزی
غیر از grep
استفاده
میکنند
میتوانند
از این مشکل
دوری کنند،
اما این
بدان معنا
نیست که
حتماً چنین
خواهند
کرد.
•به طور
مشابه،
هنگام
جابجایی
فایلها،
ممکن است
متوجه شوید
نام
فایلهای
دارای
کاراکترهای
غیراسکی یا
خاص سر از
پوشه
متفاوتی
درمیآورند،
پوشهای که
شامل
کاراکتر
علامت
نقلقول
جفت (double quote) است.
(این از نظر
فنی همان
مسئله
نقلقول در
بالا است،
اما شاید
روش جالب و
متفاوتی
باشد که
میتواند
به عنوان یک
مشکل خود را
نشان دهد و
نشان داده
است.)
•مخلوط
کردن
تصادفی
تاریخچه
قدیم و جدید
بسیار آسان
است. با هر
ابزاری
هنوز هم این
احتمال
وجود دارد،
اما git-filter-branch
تقریباً به
آن دعوت
میکند! در
صورت
خوششانسی،
تنها نکته
منفی این
است که
کاربران از
ندانستن
نحوه کوچک
کردن مخزن
خود و حذف
موارد
قدیمی
کلافه
میشوند. در
صورت
بدشانسی،
آنها
تاریخچه
قدیمی و
جدید را
ادغام
میکنند و
با چندین
«نسخه» از
هر کامیت
مواجه
میشوند،
که برخی از
آنها
دارای
فایلهای
ناخواسته
یا حساس
هستند و
برخی دیگر
ندارند. این
وضعیت به
چندین روش
مختلف رخ
میدهد:
•پیشفرض
بودن انجام
تنها
بازنویسی
جزئی
تاریخچه (--all
پیشفرض
نیست و
مثالهای
کمی آن را
نشان
میدهند)
•این
واقعیت که
هیچ
پاکسازی
خودکاری پس
از اجرا
وجود
ندارد
•این
واقعیت که
--tag-name-filter (هنگامی
که برای
تغییر نام
برچسبها
استفاده
میشود)
برچسبهای
قدیمی را
حذف
نمیکند،
بلکه تنها
برچسبهای
جدید را با
نام جدید
اضافه
میکند
•این
واقعیت که
اطلاعات
آموزشی کمی
برای آگاه
کردن
کاربران از
پیامدهای
بازنویسی و
نحوه
جلوگیری از
ترکیب
تاریخچه
قدیم و جدید
ارائه شده
است. برای
مثال، این
صفحه
راهنما
توضیح
میدهد که
کاربران
باید درک
کنند که
باید
تغییرات
خود را برای
تمامی
شاخههایشان
بر فراز
تاریخچه
جدید
بازپایهریزی
(rebase) کنند (یا
حذف کرده و
دوباره
کلون
نمایند)،
اما این
تنها یکی از
چندین
دغدغهای
است که باید
در نظر
گرفته شود.
برای
جزئیات
بیشتر بخش
"DISCUSSION" در صفحه
راهنمای git filter-repo
را ببینید.
•برچسبهای
حاشیهنویسیشده
(annotated tags) ممکن است
به دلیل یکی
از این دو
مشکل، به
طور تصادفی
به
برچسبهای
سبکوزن (lightweight tags)
تبدیل شوند:
•شخصی
میتواند
بازنویسی
تاریخچه را
انجام دهد،
متوجه شود
که
خرابکاری
کرده است،
از
نسخههای
پشتیبان در
refs/original/ بازیابی
کند، و سپس
دستور git-filter-branch
خود را
دوباره
اجرا نماید.
(پشتیبان
موجود در refs/original/
یک پشتیبان
واقعی
نیست؛
ابتدا
ارجاعزدایی
برچسبها
را انجام
میدهد.)
•اجرای
git-filter-branch با --tags یا --all
در <rev-list-options>. برای
حفظ
برچسبهای
حاشیهنویسیشده
به همان
صورت
حاشیهنویسیشده،
باید از --tag-name-filter
استفاده
کنید (و
نباید در یک
بازنویسی
خرابشده
قبلی از refs/original/
بازیابی
کرده
باشید).
•هر پیام
کامیتی که
یک
رمزگذاری (encoding)
را مشخص کند
بر اثر
بازنویسی
خراب خواهد
شد؛ git-filter-branch
رمزگذاری
را نادیده
میگیرد،
بایتهای
اصلی را
میگیرد و
آن را بدون
اعلام
رمزگذاری
مناسب به commit-tree
تحویل
میدهد. (این
اتفاق چه از
--msg-filter استفاده
شود و چه
نشود رخ
میدهد.)
•پیامهای
کامیت (حتی
اگر همگی UTF-8
باشند) به
صورت
پیشفرض به
دلیل
بهروزرسانی
نشدن خراب
میشوند —
هر ارجاعی
به سایر
هشهای
کامیت در
پیامهای
کامیت
اکنون به
کامیتهایی
ارجاع
خواهد داد
که دیگر
وجود خارجی
ندارند.
•هیچ
امکاناتی
برای کمک به
کاربران در
پیدا کردن
ضایعات
ناخواستهای
که باید حذف
کنند وجود
ندارد، که
این بدان
معناست که
آنها
بسیار
بیشتر در
معرض
پاکسازیهای
ناقص یا
جزئی قرار
دارند که
گاهی اوقات
منجر به
سردرگمی و
تلف شدن وقت
افراد برای
درک آن
میشود.
(برای مثال،
افراد
معمولاً به
جای
پوشههای
بزرگ یا
پسوندها،
فقط به
دنبال
فایلهای
بزرگ برای
حذف
میگردند،
و هنگامی که
چنین
میکنند،
مدتی بعد
افرادی که
از مخزن
جدید
استفاده
میکنند و
تاریخچه را
مرور
میکنند،
متوجه یک
پوشه مربوط
به مصنوعات
ساخت
میشوند که
برخی
فایلها را
دارد و برخی
دیگر را
ندارد، یا
یک حافظه
پنهان
وابستگیها
(node_modules یا موارد
مشابه) که
هرگز
نمیتوانسته
کار کند
زیرا برخی
فایلهایش
گم شده است.)
•اگر --prune-empty
مشخص نشود،
فرآیند
فیلتر کردن
میتواند
انبوهی از
کامیتهای
خالی
گیجکننده
ایجاد کند
•اگر --prune-empty
مشخص شود،
کامیتهای
تعمداً
خالی از قبل
از عملیات
فیلتر کردن
نیز به جای
هرس کردن
صرفاً
کامیتهایی
که به دلیل
قوانین
فیلتر خالی
شدهاند،
هرس
میشوند.
•اگر --prune-empty
مشخص شود،
گاهی اوقات
کامیتهای
خالی جا
افتاده و به
هر حال باقی
میمانند
(یک باگ تا
حدی نادر،
اما رخ
میدهد...)
•یک
مسئله
جزئی، اما
کاربرانی
که هدفشان
بهروزرسانی
همه نامها
و
رایانامهها
در یک مخزن
است ممکن
است به سمت
--env-filter هدایت
شوند که
تنها
پدیدآورندگان
و
ثبتکنندگان
را
بهروزرسانی
میکند و
برچسبگذاران
(taggers) را جا
میاندازد.
•اگر
کاربر یک --tag-name-filter
ارائه دهد
که چندین
برچسب را به
یک نام
نگاشت کند،
هیچ هشدار
یا خطایی
داده
نمیشود؛
git-filter-branch به سادگی
روی هر
برچسب در یک
ترتیب از
پیش
تعریفشده
و
مستندنشده
بازنویسی
میکند که
در نهایت
تنها به یک
برچسب منجر
میشود. (یک
آزمون
رگرسیون git-filter-branch
به این
رفتار
تعجبآور
نیاز دارد.)
همچنین،
عملکرد
ضعیف git-filter-branch
اغلب منجر
به مسائل
امنیتی
میشود:
•رسیدن
به قطعهکد
صحیح پوسته
برای انجام
فیلتر مورد
نظرتان
گاهی اوقات
دشوار است
مگر اینکه
فقط در حال
انجام یک
تغییر جزئی
مانند حذف
دو فایل
باشید.
متأسفانه،
افراد اغلب
با امتحان
کردن متوجه
میشوند که
قطعهکد
درست است یا
غلط، اما
درستی یا
نادرستی
میتواند
بسته به
شرایط خاص
(فاصله در
نام
فایلها،
نام
فایلهای
غیر اسکی،
نامها یا
رایانامههای
نامتعارف
نویسنده،
مناطق
زمانی
نامعتبر،
وجود
پیوندها یا
اشیاء
جایگزین و...)
متغیر
باشد، به
این معنی که
ممکن است
مجبور شوند
مدت طولانی
صبر کنند،
با خطا
مواجه
شوند، و سپس
دوباره
شروع کنند.
عملکرد git-filter-branch
آنقدر بد
است که این
چرخه
عذابآور
است و زمان
موجود برای
بررسی دقیق
را کاهش
میدهد (صرف
نظر از
تأثیری که
بر صبر و
شکیبایی
فرد
انجامدهنده
بازنویسی
میگذارد
حتی اگر از
نظر فنی وقت
بیشتری
داشته باشد).
این مشکل
دوچندان
تشدید
میشود
زیرا
خطاهای
ناشی از
فیلترهای
معیوب ممکن
است برای
مدتی
طولانی
نشان داده
نشوند و/یا
در دریایی
از
خروجیها
گم شوند.
بدتر از آن،
فیلترهای
خراب اغلب
فقط منجر به
بازنویسیهای
نادرست
بیصدا
میشوند.
•علاوه
بر همه
اینها،
حتی زمانی
که کاربران
سرانجام
دستورات
کارآمدی
پیدا
میکنند،
طبیعتاً
میخواهند
آنها را به
اشتراک
بگذارند.
اما ممکن
است از این
موضوع
بیخبر
باشند که
مخزن آنها
برخی از
حالات خاص
را که مخزن
فرد دیگری
دارد،
نداشته است.
بنابراین،
هنگامی که
شخص دیگری
با یک مخزن
متفاوت
همان
دستورات را
اجرا
میکند،
دچار
مشکلات
بالا
میشود. یا
اینکه
کاربر
دستوراتی
را اجرا
میکند که
واقعاً
برای حالات
خاص بررسی
شده بودند،
اما
همانگونه
که در بالا
ذکر شد، آن
را روی
سیستمعامل
دیگری اجرا
میکند که
در آنجا کار
نمیکند.
- 1.
- git filter-repo
- 2.
- filter-lamely