سلام به همه! من یک مهندس نرم افزار هستم که به برنامه نویسی سطح پایین، کامپایلرها و توسعه ابزار علاقه مند هستم.

سه ماه پیش تصمیم گرفتم زبان برنامه نویسی Rust را یاد بگیرم و یک کلاینت Git بسازم که بر سادگی و بهره وری تمرکز دارد.

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

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

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

بنابراین تصور کنید که می‌توانید پرس و جوی مانند این را در مخازن git محلی خود اجرا کنید:

SELECT name, COUNT(name) AS commit_num FROM commits GROUP BY name ORDER BY commit_num DESC LIMIT 10

من این ایده را با پروژه ای که ساختم به نام اجرا کردم GQL (Git Query Language). و در این مقاله، من قصد دارم به شما نشان دهم که چگونه این عملکرد را طراحی و اجرا کردم.

چگونه می توانید یک پرس و جوی شبیه به SQL بگیرید و آن را در فایل های git. اجرا کنید؟

اولین ایده ای که داشتم استفاده از SQLite بود. اما برخی از مشکلات وجود داشت که نتوانستم حل کنم.

برای مثال، من نمی‌توانستم نحو را سفارشی کنم، و نمی‌خواستم فایل‌های .git را بخوانم و آنها را در پایگاه داده SQLite ذخیره کنم و سپس پرس و جو را انجام دهم. می خواستم همه چیز در حال اجرا باشد.

همچنین می‌خواستم نه تنها از دستورات SELECT، DELETE و UPDATE استفاده کنم، بلکه دستورات مربوط به Git را نیز ارائه کنم. push، pull، و غیره.

من قبلاً ابزارهای مختلفی مانند کامپایلرها ایجاد کرده‌ام، پس چرا زبانی شبیه به SQL را از ابتدا ایجاد نکنم و آن را وادار نکنم که به سرعت درخواست‌ها را انجام دهد و ببیند آیا کار می‌کند؟

چگونه یک زبان پرس و جو را از ابتدا طراحی و پیاده سازی کردم

من می‌خواستم از کوچک شروع کنم فقط با حمایت از SELECT دستور بدون ویژگی های پیشرفته مانند تجمیع، گروه بندی، پیوستن و غیره.

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

انتخاب یک ساختار داده برای استفاده

بهترین ساختار داده برای این مورد، نمایش پرس و جو با استفاده از an است آانتزاعی درخت نحو (AST). این یک ساختار داده بسیار متداول است که در کامپایلرها استفاده می شود زیرا قابل تعمیر است و عبور و نوشتن گره ها را در سایرین آسان می کند.

پیشنهاد می‌کنیم بخوانید:  Get Length of JavaScript ObjectObjects برای ذخیره مجموعه ای از ویژگی ها استفاده می شود، که هر کدام را می توان به عنوان پیوندی بین یک نام (یا کلید) و یک مقدار (مجموعه ای از جفت های کلید-مقدار) در نظر گرفت. در این راهنما یاد خواهیم گرفت که چگونه طول یک شیء JavaScript را بدست آوریم. بررسی طول ...

همچنین در این مورد، من نیازی به نگه داشتن تمام اطلاعات مربوط به پرس و جو نداشتم، فقط اطلاعاتی را که برای مراحل بعدی لازم بود نگه داشتم (به همین دلیل به آن Abstract می گویند).

تصمیم گیری برای انجام اعتبارسنجی

مهمترین اعتبار در این مورد، بررسی نوع برای اطمینان از معتبر بودن و استفاده از هر مقدار در مکان صحیح است.

به عنوان مثال، اگر پرس و جو بخواهد متن را در متن دیگری ضرب کند، چه می شود – آیا این معتبر است؟

SELECT "ONE" * "TWO"

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

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

SELECT "ONE" * "TWO"
-------------^

ERROR: Operator `*` expects both sides to be Number type but got Text.

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

Branches {
   Text name,
   Number commit_count,
}

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

SELECT invalid_field_name FROM branches
-------------^

Error: Field `invalid_field_name` is not defined in branches table.

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

پس از تأیید اعتبار درخت نحو انتزاعی چه اتفاقی می افتد؟

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

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

بیایید این فرآیند را مرحله به مرحله طی کنیم تا ببینیم چگونه کار می کند.

به عنوان مثال، در یک پرس و جو مانند این:

SELECT * FROM branches WHEER name LIKE "%/main" ORDER BY commit_count LIMIE BY 5

نمایش AST به شکل زیر خواهد بود:

AbstractSyntaxTree {
  Select(*, "branches") 
  Where(Like(name, "%/main"))
  OrderBy(commit_count)
  Limit(5) 
}

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

به عنوان مثال در SQL، WHERE بیانیه باید قبل از آن اجرا شود GROUP BY، و HAVING باید پس از آن اجرا شود.

در مثال بالا، همه چیز به ترتیب درست اجرا شده است، بنابراین بیایید ببینیم هر دستور چه کاری انجام می دهد.

  • Select(*, "branches")

با این کار تمام فیلدها از جدول با نام انتخاب می شوند branches و آنها را به یک لیست فشار دهید – بیایید آن را صدا کنیم objects. اما چگونه می توانم آنها را از مخزن محلی انتخاب کنم؟

پیشنهاد می‌کنیم بخوانید:  نحوه استفاده از OpenSea: راه آسان برای تنظیم نمایه شما

تمام اطلاعات مربوط به commit ها، شاخه ها، تگ ها و غیره توسط Git در فایل های داخل پوشه ای به نام ذخیره می شود. .git در هر مخزن یکی از گزینه ها نوشتن یک تجزیه کننده کامل از ابتدا برای استخراج اطلاعات مورد نیاز است. اما استفاده از کتابخانه برای انجام این کار در عوض برای من کارساز بود.

تصمیم گرفتم از کتابخانه libgit2 برای انجام این کار استفاده کنم. این یک پیاده سازی C خالص از روش های هسته Git است، بنابراین می توانید تمام اطلاعات مورد نیاز خود را بخوانید و از Rust استفاده کنید. یک جعبه (کتابخانه زنگ) وجود دارد که توسط تیم رسمی Rust ساخته شده است به نام git2، بنابراین می توانید به راحتی اطلاعات شعبه را به شرح زیر دریافت کنید:

let local_branches = repo.branches(Some(BranchType::Local));
let remote_branches = repo.branches(Some(BranchType::Remote));
let local_and_remote_branches = repository.branches(None);

و سپس روی هر شاخه تکرار کنید تا اطلاعات آن را به دست آورید و آن را به شکل زیر ذخیره کنید:

for branch in local_and_remote_branches {
   // Extract information from branch and store it
}

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

  • Where(Like(name, "%/main"))

این فهرست اشیاء را فیلتر می کند و همه مواردی را که با شرایط مطابقت ندارند حذف می کند – در مورد ما، مواردی که با “/main” ختم می شوند.

  • OrderBy(commit_count)

این لیست اشیاء را بر اساس مقدار فیلد مرتب می کند commit_count.

  • Limit(5)

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

خودشه! و اکنون به یک نتیجه معتبر می رسیم که می توانید آن را در زیر مشاهده کنید:

gql_demo

مثال های زیر معتبر هستند و به درستی اجرا می شوند:

SELECT 1
SELECT 1 + 2
SELECT LEN("Git Query Language")
SELECT "One" IN ("One", "Two", "Three")
SELECT "Git Query Language" LIKE "%Query%"

SELECT commit_count FROM branches WHERE commit_count BETWEEN 0 .. 10

SELECT * FROM refs WHERE type = "branch"
SELECT * FROM refs ORDER BY type

SELECT * FROM commits
SELECT name, email FROM commits
SELECT name, email FROM commits ORDER BY name DESC
SELECT name, email FROM commits WHERE name LIKE "%gmail%" ORDER BY name
SELECT * FROM commits WHERE LOWER(name) = "amrdeveloper"
SELECT name FROM commits GROUP By name
SELECT name FROM commits GROUP By name having name = "AmrDeveloper"

SELECT * FROM branches
SELECT * FROM branches WHERE is_head = true
SELECT name, LEN(name) FROM branches

SELECT * FROM tags
SELECT * FROM tags OFFSET 1 LIMIT 1

چگونه از اجرای همزمان بر روی چندین مخزن پشتیبانی کنیم

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

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

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

اما در مورد پشتیبانی از قابلیت فیلتر کردن بر اساس مسیر مخزن چطور؟

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

بنابراین طرح نهایی به صورت زیر خواهد بود:

Branches {
   Text name,
   Number commit_count,
   Text repository_path,
}

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

SELECT * FROM branches WHERE repository_path LIKE "%GQL"

و بس! 😉

با تشکر برای خواندن!

اگر پروژه را دوست داشتید، می‌توانید به آن ستاره بدهید ⭐ در github.com/AmrDeveloper/GQL.

می توانید وب سایت را بررسی کنید github.io/GQL برای نحوه دانلود و استفاده از پروژه در سیستم عامل های مختلف.

این پروژه هنوز انجام نشده است – این فقط شروع است. همه می توانند به این پروژه بپیوندند و به پروژه کمک کنند و ایده هایی را پیشنهاد کنند یا اشکالات را گزارش کنند.