پیاده‌سازی Git در JavaScript

ترجمه و تالیف : عرفان کاکایی
تاریخ انتشار : 13 خرداد 98
خواندن در 5 دقیقه
دسته بندی ها : جاوا اسکریپت

این مقاله به شما نشان می‌دهد که Gitfred چگونه ساخته شد. Gitfred یک کتابخانه است که تجربه‌ای به مانند Git را برای ذخیره‌سازی محتویات در JavaScript فراهم می‌کند. 

مقدمه

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

من چند ماه را صرف ساخت یک وبسایت (poet.codes) کردم و یکی از مشکلاتی که با آن رو به رو می‌شدم، مدیریت داده‌ها و انتقالات با back-end بود. این وبسایت ما را قادر می‌سازد تا تجربه متعاملی داشته باشیم، که در آن می‌توانیم در مرورگر کد بنویسیم و تاثیر آن را به صورت لحظه‌ای ببینیم. این مسئله بسیار جالب است و می‌تواند در جاهای مختلفی دیده شود. گرچه مشکل من با آن، کمبود تاریخچه تغییرات بود. به خصوص وقتی که موضوع ما، نوشتن فنی است. من می‌خواهم یک مثال را توسعه دهم و به خواننده نشان دهم که چگونه در آن پیش می‌روم. Git به خوبی در این ایده جای می‌گیرد. تصور کنید که من در هنگام نوشتن کد، commitهایی را ایجاد می‌کنم و این commitها بخشی از Story کلی می‌شوند. وبسایت ما تماما درباره همین موضوع است. تقلید سریع یک مثال، توضیح آن و سپس به اشتراک گذاری آن با دیگران.

این مسئله کاملا خوب است، اما برای باثبات کردن آن، باید تمام تغییرات موجود در یک دیتابیس را ذخیره کنیم. این یعنی میران ثابتی از درخواست‌ها به یک API. برنامه‌های این چنینی می‌توانند در زمینه انتقال و ذخیره داده‌ها، بسیار پر هزینه شوند.

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

پیاده‌سازی Git در JavaScript

رابط کاربری خام

بیایید با پایه‌ها شروع کنیم. Git سه وضعیت (state) دارد و ما باید آن‌ها را در پیاده‌سازی خود به نمایش بگذاریم.

  • Commited - یعنی تغییرات ما در دیتابیس محلی ذخیره می‌شوند.
  • Modified - یعنی ما تغییراتی را اعمال کرده‌ایم، اما این تغییرات هنوز در دیتابیس نیستند.
  • Staged - یعنی تغییرات ما علامت‌گذاری شده‌اند تا به commit بعدی بروند.
const data = {

  working: {},

  staging: {},

  commits: {}

}

عبارت working وضعیت modified، و عبارت staging‌ وضعیت staged را نمایش می‌دهد. Commitها هم نقش دیتابیس محلی ما را بازی خواهند کرد.

در Git، ما مفهوم HEAD را داریم. این مفهوم به سادگی، اشاره‌گری به نوک شاخه فعلی ما است. در این مورد، HEAD به یک commit مشخص در فیلد commits اشاره خواهد کرد. همچنین هر commit باید یک مشخص کننده خاص داشته باشد که ما آن را در Git به عنوان یک hash تعریف می‌کنیم. ما این مسئله را ساده‌سازی می‌کنیم و از یک متغیر شمارنده به نام i استفاده می‌کنیم. با داشتن این دو، ما این آبجکت data را به دست خواهیم آورد:

const data = {

  i: 0,

  head: null,

  working: {},

  staging: {},

  commits: {}

}

ما با جمع‌بندی آن در یک تابع ادامه خواهیم داد. یک تابعی که یک آبجکت git را با چند متد بر می‌گرداند:

const createGit = function () {

  const data = {

    i: 0,

    head: null,

    working: {},

    staging: {},

    commits: {}

  }

  

  return {

    save(filepath, content) {},

    get() {},

    add() {},

    commit(message) {},

    checkout(hash) {}

  }

}

const git = createGit();

Save چیزی را به شاخه working ما اضافه خواهد کرد. Get محتویات همین فیلد را بر خواهد گرداند. Add تغییرات ما را stage خواهد کرد. در Git، این که فقط برخی از تغییرات را stage کنیم هم ممکن است، اما ما فرض خواهیم کرد که توسعه‌دهنده می‌خواهد همه چیز را stage کند. Commit هر چیزی که در فیلد staging باشد را به دست خواهد آورد، و یک commit را شکل خواهد داد که در مپ commits ذخیره خواهد شد. در آخر هم checkout ما را قادر خواهد ساخت تا با دریافت محتویات commit و برابر قرار دادن آن با فیلد _working_، به یک رکورد خاص بپریم تا بتوانیم از get استفاده کنیم و آن را بخوانیم.

ذخیره‌سازی و دریافت فایل‌ها از شاخه working

با توجه به این که ما تصمیم گرفتیم یک آبجکت را به عنوان شاخه working به کار بگیریم، از filepath به عنوان کلید (key) و از محتویات به عنوان مقدار (value) استفاده خواهیم کرد.

save(filepath, content) {

  data.working[filepath] = content;

}

این روند، فقط data.working را بر می‌گرداند:

get() {

  return data.working;

}

و ما می‌توانیم این تغییرات را با این مثال آزمایش کنیم:

git.save('app.js', 'const answer = 42;');

console.log(JSON.stringify(git.get(), null, 2));

/* 

  results in:

  {

    "app.js": "const answer = 42;"

  }

*/

stage کردن تغییرات خود

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

export() {

  return data;

}

همانطور که در بالا گفتیم، روند stage کردن ما هر چیزی که در شاخه working وجود دارد را گرفته، و در ناحیه staging کپی خواهد کرد.

add() {

  data.staging = JSON.parse(JSON.stringify(data.working));

}

ما از سریع‌ترین راه برای clone کردن یک آبجکت در JavaScript استفاده خواهیم کرد: JSON.stringify و سپس JSON.parse. حال اگر مثال خود را کمی گسترش دهیم، می‌توانیم تاثیر آن را ببینیم.

git.save('app.js', 'const answer = 42;');

git.add();

console.log(JSON.stringify(git.export(), null, 2));

نتیجه به این صورت است:

{

  "i": 0,

  "head": null,

  "working": {

    "app.js": "const answer = 42;"

  },

  "staging": {

    "app.js": "const answer = 42;"

  },

  "commits": {}

}

حال همان فایل و با همان محتویات، در هر دو مکان وجود دارد.

Commit کردن به دیتابیس محلی خود

برخی اتفاقات هستند که در اینجا باید پیش بیایند. اولین مورد، این است که یک hash منحصر به فرد برای commit خود ایجاد کنیم. دوم، باید محتویات را از ناحیه staging بگیریم و آن‌ها را به همراه پیغام commit، در فلید commits ذخیره کنیم. همچنین باید ناحیه staging را خالی کنیم، تا در موقعیت مناسبی برای تغییرات بعدی قرار بگیریم. در نهایت، head باید به آن commit جدید اشاره کند:

commit(message) {

  const hash = '_' + (++data.i);

  data.commits[hash] = {

    content: data.staging,

    message

  };

  data.staging = {};

}

بیایید از متد commit در مثال خود استفاده کنیم و ببینیم که پس از آن، آبجکت data ما چه ظاهری دارد:

git.save('app.js', 'const answer = 42;');

git.add();

git.commit('first commit');

و نتیجه نهایی برابر است با:

{

  "i": 1,

  "head": "_1",

  "working": {

    "app.js": "const answer = 42;"

  },

  "staging": {},

  "commits": {

    "_1": {

      "content": {

        "app.js": "const answer = 42;"

      },

      "message": "first commit"

    }

  }

}

به افزایش شمارنده i ما به مقدار ۱، که یعنی دومین commit یک hash با مقدار _2 را خواهد داشت، دقت کنید. Staging باز هم خالی است و یک commit ثبت شده است. head هم به مکان درستی اشاره می‌کند. بیاید به سراغ متد شگفت‌انگیز checkout برویم.

بررسی

برای ترسیم کاری که متد checkout انجام می‌دهد، باید حداقل دو commit داشته باشیم. پس بیایید یک فایل دیگر به نام foo.js را به دیتابیس اضافه کنیم و ببینیم که آخرین وضعیت آبجکت data ما چیست.

git.save('app.js', 'const answer = 42;');

git.add();

git.commit('first commit');

git.save('foo.js', 'const bar = "zar";');

git.add();

git.commit('second commit');

console.log(JSON.stringify(git.export(), null, 2));

حال ما باید دو commit با hashهای _1 و _2 داشته باشیم، که دومین مورد شامل هر دو فایل app.js و foo.js می‌باشد. و قطعا اگر ما data را چاپ کنیم، همین را خواهیم دید:

{

  "i": 2,

  "head": "_2",

  "working": {

    "app.js": "const answer = 42;",

    "foo.js": "const bar = \"zar\";"

  },

  "staging": {},

  "commits": {

    "_1": {

      "content": {

        "app.js": "const answer = 42;"

      },

      "message": "first commit"

    },

    "_2": {

      "content": {

        "app.js": "const answer = 42;",

        "foo.js": "const bar = \"zar\";"

      },

      "message": "second commit"

    }

  }

}

در اینجا، head به آخرین commit که ما ساخته‌ایم، یعنی _2 اشاره می‌کند. بررسی کردن اولین مورد، یعنی بروزرسانی مقدار head، و همچنین بروزررسانی شاخه working ما.

checkout(hash) {

  data.head = hash;

  data.working = JSON.parse(JSON.stringify(data.commits[hash].content));

}

در اینجا باز هم باید clone کنیم؛ زیرا در غیر این صورت هر ذخیره‌سازی در شاخه working، اصلاح شده و در فیلد commits هم commit خواهد شد. با انجام این کار، ما آماده پیاده‌سازی هستیم. حال ما می‌توانیم اطلاعات را ذخیره‌سازی کنیم، دریافت کنیم، یک تاریخچه از تغییرات بسازیم و در میان آن‌ها حرکت کنیم. اگر ما git.checkout(‘_1’) را فراخوانی کنیم، متد export این نتیجه را نشان می‌دهد:

{

  "i": 2,

  "head": "_1",

  "working": {

    "app.js": "const answer = 42;"

  },

  "staging": {},

  "commits": {

    "_1": {

      "content": {

        "app.js": "const answer = 42;"

      },

      "message": "first commit"

    },

    "_2": {

      "content": {

        "app.js": "const answer = 42;",

        "foo.js": "const bar = \"zar\";"

      },

      "message": "second commit"

    }

  }

}

پیش‌روی

اگر سورس کد مربوط به این وبسایت را باز کنید، خواهید دید که چیزی بسیار بیشتر از ۴۰ خط کد در آن وجود دارد. من برای این که این کتابخانه را قابل استفاده کنم، مجبور بودم چندین امکانات را بر پایه چیزی که در اینجا داریم بسازم. اکثر این موارد، برای تقلید کار Git هستند. گرچه یک نکته که فکر می‌کنم جالب است و ارزش اشاره را دارد، مقیاس‌پذیری این راه حل است. فرض کنید که ما ده‌ها فایل داریم و شروع به ارسال commitها پشت سر هم و برای هر تغییر می‌نماییم. این یعنی داشتن مجموعه فایل‌های خود که چندین بار کپی شده‌اند، و این مسئله مقیاس‌پذیر نیست. ما نمی‌توانیم تمام فایل‌ها را در هر commit نگه داریم؛ زیرا بار حاصل از این کار بسیار عظیم خواهد بود. من در نهایت به استفاده از کتابخانه diff-match-patch، ساخته گوگل رسیدم. این کتابخانه یک پیاده‌سازی JavaScript فشرده از الگوریتم Myer’s diff می‌باشد. این کتابخانه من را قادر ساخت تا فقط تغییرات بین commitها را ذخیره کنم و داده‌های ذخیره شده در وبسایت خود را به مقدار چشمگیری کاهش دهم.

در اینجا مثالی از دو رشته که توسط diff-match-patch مقایسه شده‌اند و ظاهر آن‌ها را مشاهده می‌نمایید:

const str1 = 'Hello world';

const str2 = 'Goodbye world';

var dmp = new diff_match_patch();

var diff = dmp.diff_main(str1, str2);

dmp.diff_cleanupSemantic(diff);

console.log(diff);

// outputs: -1,Hello,1,Goodbye,0, world

نتیجه گیری

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

منبع

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید