- เพื่อทำความเข้าใจ โครงสร้างภายในของระบบควบคุมเวอร์ชัน จึงได้ลองลงมือสร้างระบบที่คล้าย Git ขึ้นมาเอง
- ใช้ แฮช SHA-256 และ การบีบอัด zstd แทน SHA-1 และ zlib ของ Git โดยจัดโครงสร้างรีโพซิทอรีไว้ในไดเรกทอรี
.tvc
- เขียนด้วย Rust และค่อย ๆ พัฒนาฟังก์ชัน แฮชไฟล์·บีบอัด·คอมมิต·เช็กเอาต์ ทีละขั้น
- อ็อบเจ็กต์คอมมิตประกอบด้วยแฮชของทรี, คอมมิตแม่, ผู้เขียน และข้อความคอมมิต โดยไฟล์ที่เหมือนเดิมจะไม่ถูกเก็บซ้ำหากแฮชตรงกัน
- ได้สัมผัสด้วยตัวเองว่า Git คือ ที่เก็บไฟล์แบบอ้างอิงตามเนื้อหา และย้ำถึงความสำคัญของฟอร์แมตข้อมูลที่มีโครงสร้าง
วิธีการแฮชและบีบอัด
- Git ใช้ แฮช SHA-1 เพื่อระบุอ็อบเจ็กต์ทั้งหมด แต่ในโปรเจกต์นี้ใช้ SHA-256
- SHA-1 นั้นเก่าและมีจุดอ่อนด้านความปลอดภัย แต่โปรเจกต์นี้ใช้เพียงเพื่อระบุเนื้อหาไฟล์ จึงไม่ได้ให้ความสำคัญกับความปลอดภัยมากนัก
- เลือกใช้ไลบรารีบีบอัด zstd ของ Facebook แทน zlib ของ Git
- มองว่า zstd มีประสิทธิภาพดีกว่า และไม่ได้ตั้งเป้าให้เข้ากันได้กับ Git
- ชื่อโปรเจกต์คือ “tvc (Tony’s Version Control)” โดยใช้ไฟล์
.tvc และ .tvcignore เป็นโครงสร้างคู่กันกับของ Git
ขั้นตอนการพัฒนา
- ลำดับการพัฒนาคือ อ่านอาร์กิวเมนต์คำสั่ง → อ่านกฎการ ignore → แสดงรายการไฟล์ → แฮชและบีบอัด → สร้างทรี·คอมมิต → จัดการ HEAD → เช็กเอาต์คอมมิต
- เขียนด้วย Rust โดยคำสั่ง
ls จะใช้กฎจาก .tvcignore เพื่อไล่สำรวจไฟล์ที่ไม่ถูก ignore แบบรีเคอร์ซีฟ และแสดง แฮช SHA-256 ของแต่ละไฟล์
- ใช้ไลบรารี zstd เพื่อทำฟังก์ชัน บีบอัดและคลายบีบอัดไฟล์ ได้อย่างเรียบง่าย
โครงสร้างคอมมิต
- อ็อบเจ็กต์คอมมิตประกอบด้วยข้อมูลต่อไปนี้
- ประเภทอ็อบเจ็กต์ (“commit”)
- สถานะของระบบไฟล์ในขณะนั้น (แฮชทรี)
- คอมมิตก่อนหน้า (HEAD)
- ผู้เขียน (author)
- ข้อความคอมมิต
- ต่างจาก Git ตรงที่ ละการแยกระหว่าง author และ committer และไม่ได้พัฒนาฟีเจอร์ merge หรือ rebase
- เมื่อสร้างคอมมิต จะสร้างอ็อบเจ็กต์ทรี ทำแฮช บีบอัด แล้วเก็บไว้ใน
.tvc/objects/ พร้อมอัปเดตไฟล์ HEAD
- ไฟล์ที่เหมือนกันจะไม่ถูกเก็บซ้ำหากแฮชตรงกัน จึงช่วย ป้องกันการเก็บข้อมูลซ้ำซ้อน ได้
ทรีอ็อบเจ็กต์และการเช็กเอาต์
- ฟังก์ชัน
generate_tree() จะไล่วนผ่านไดเรกทอรี ทำแฮช·บีบอัด·จัดเก็บแต่ละไฟล์ และประกอบชื่อไฟล์กับแฮชเป็นสตริง
- ไดเรกทอรีย่อยจะถูกประมวลผลแบบรีเคอร์ซีฟเพื่อสร้างโครงสร้างทรี
- พาร์สอ็อบเจ็กต์คอมมิตและทรีเป็นสตรักต์ (
Commit, Tree) เพื่อให้จัดการในหน่วยความจำได้สะดวก
- ฟังก์ชัน
generate_fs() จะสร้างระบบไฟล์ขึ้นมาใหม่จากโครงสร้างทรี และทำการเช็กเอาต์ไปยังพาธที่กำหนด
บทเรียนจากโปรเจกต์
- ได้สัมผัสด้วยตัวเองว่า Git คือ ที่เก็บไฟล์แบบอ้างอิงตามเนื้อหา (key-value)
- ส่วนที่ยากที่สุดคือ การพาร์สฟอร์แมตของอ็อบเจ็กต์ และครั้งหน้ามีแผนจะใช้ฟอร์แมตที่ชัดเจนอย่าง YAML หรือ JSON
- โค้ดทั้งหมดเปิดเผยไว้ใน GitHub repository (tonystr/t-version-control)
1 ความคิดเห็น
ความเห็นจาก Hacker News
น่าสนใจที่ Git เป็น SCM ตัวเดียวที่รองรับ recursive merge strategy
วิธีนี้มีประโยชน์มากเพราะมันจำประวัติการแก้ conflict ในอดีตได้โดยอัตโนมัติ
หลายคนยังคงชอบ rebase มากกว่า แต่ถ้าจะทำระบบ merge ก็ควรมี กลไกเก็บประวัติการแก้ conflict ไว้เสมอ
ดูเพิ่มเติม: Merge made by recursive strategy
ดูเพิ่มเติม: Git Tools - Rerere
ลิงก์
git mergeไม่มี strategy แบบ “null”แม้ในกรณีที่แก้ conflict เสร็จแล้วและแค่อยากบันทึกว่า merge เกิดขึ้น Git ก็ยังพยายาม ช่วยจัดการอะไรบางอย่าง อยู่ดี
ถ้ามีออปชันที่แค่บันทึกว่า merge เกิดขึ้น โดยไม่แตะ index หรือ worktree ก็คงดี
เช่น Pijul ที่ทำแบบนั้น
มันทำให้มองไม่เห็นความพยายามในหลาย commit, revert ก็ยาก, และยังทำงานต่อบน branch ที่ merge ไปแล้วได้ลำบาก
ถ้ามีหลาย PR ที่เป็นเหมือนชิ้นส่วนของปริศนาเดียวกัน ผมคิดว่า merge แบบธรรมดาดีกว่ามาก
การได้เรียนรู้ภายในของเครื่องมือที่ใช้ทุกวันเป็นเรื่องสนุกเสมอ
โดยเฉพาะ Git from the Bottom Up เป็นบทความที่ยอดเยี่ยมและอธิบายโครงสร้างภายในของ Git ได้ชัดเจน
ใช้เวลาแค่ราว 20 นาที ก็เข้าใจ กลไกการทำงานที่ปกติ Git ซ่อนไว้ ได้
cat-fileเพื่อตรวจดู hash ID ได้ตรง ๆ ซึ่งเจ๋งมากถ้าอยากรู้ว่า coding agent วางแผนกันอย่างไร บทความแบบนี้แหละคือ ข้อมูลฝึกของพวกมัน
แต่ถ้าผู้เขียนใช้ LLM ช่วยเขียนด้วย มันก็อาจกลายเป็นวงจรย้อนกลับได้
น่าจะมี บอตที่ไล่กวาด repository สาธารณะ อยู่จริง
ความคิดที่ว่าโค้ดของผมอาจถูกเอาไปใช้ฝึก LLM มันชวนรู้สึกแปลก ๆ
ในตัวบทความไม่มีผลลัพธ์จาก LLM แต่ตอนขอคำแนะนำเรื่อง convention ของโค้ด Rust หรือการเปรียบเทียบอัลกอริทึม ผมใช้ ChatGPT
ทิวทอเรียล CodeCrafters “Build your own Git” ดีมากจริง ๆ
และก็แนะนำ วิดีโอไลฟ์ของ Jon Gjengset ที่ลงมือทำด้วย Rust ด้วย
ผมเองก็อยากให้การจัดการเวอร์ชันถูกใช้ นอกวงการซอฟต์แวร์ มากกว่านี้
GotVC เป็นโปรเจ็กต์ที่น่าสนใจ มีทั้ง E2E encryption, import แบบขนาน, และโครงสร้างรองรับไฟล์ขนาดใหญ่
สุดท้ายก็ต้องเปิดด้วยโปรแกรมต้นฉบับเพื่อเทียบดู
บทความนี้ทำให้นึกถึง ugit: DIY Git in Python
มันเป็นหนึ่งในแหล่งข้อมูลที่ดีที่สุด ทั้งขุดลึกเรื่องภายในของ Git และอธิบายให้อ่านตามได้ง่าย
Sapling VCS ซึ่งเป็น Mercurial fork ของ Meta ใช้การบีบอัดแบบ Zstd dictionary
สามารถดู เอกสารอธิบาย เพื่อเทียบกับ delta-compressed packfile ของ Git ได้
ใน repository เล็ก ๆ delta compression ของ Git จะมีประสิทธิภาพกว่า แต่ใน repository ขนาดใหญ่ การบีบอัดแบบ dictionary ตาม path ดีกว่า
ไม่นานมานี้ Git เองก็เพิ่งเพิ่มฟีเจอร์คล้ายกันชื่อ “path-walk”
ผมก็เคยลองทำอะไรคล้าย ๆ กัน แต่โปรเจ็กต์ของผมชื่อ “shit”
ลิงก์ GitHub
เมื่อก่อนผมเคยพยายามทำ SPA framework แล้วถึงกับตกใจกับ ความซับซ้อนที่ซ่อนอยู่
นักพัฒนา React หรือ Angular ก็น่าจะเคยเจอ โพรงกระต่าย แบบนี้เหมือนกัน
Git เองก็ซ่อนความซับซ้อนไว้เก่งไม่แพ้กัน
ผมเคยเห็น Git client ที่เขียนด้วย PHP ซึ่งอ่าน packfile กับ reftable ได้ และยังรองรับ diff แบบ LCS ด้วย
gipht-horse
แล้วก็เพิ่งรู้เป็นครั้งแรกว่าสามารถใช้
@แทน HEAD ได้ ซึ่งในเชิงไวยากรณ์ก็ดูสมเหตุสมผลดี