How To Build a Time Machine? Demystifying Git
Git לא מפחידה כי היא מסובכת, אלא כי הארכיטקטורה שלה נסתרת. אריאל שולמן מפרקת hashes, commits, branches ו-rebase כדי להחליף ניחוש בהבנה בצוות.
צפו בהרצאההסיפור שמאחורי ההרצאה
- Git היא מכונת זמן של תוכן: blobs ו-trees מייצגים קבצים ותיקיות, ו-commit מחזיק את ההיסטוריה שמאפשרת לשחזר מצב של repository.
- hash ב-Git הוא כתובת עקבית של תוכן. Git משתמשת ב-SHA-1 כדי לזהות ולחבר אובייקטים, לא כפתרון הצפנה.
- branch אינו סביבת עבודה קסומה אלא reference ל-commit האחרון על timeline. main היא קונבנציה, לא branch עם כוחות מיוחדים.
- merge הוא commit עם יותר מהורה אחד. fast-forward רק מקדם reference כשאין צורך ליצור commit חדש.
- rebase מייצרת עותקים של commits על בסיס חדש, ולכן לא עושים אותה על commits משותפים. fetch, pull ו-push הם פעולות על references מקומיים ומרוחקים.
Git לא מפחידה כי יש בה יותר מדי פקודות. היא מפחידה כש-run, merge או rebase מרגישים כמו קסם שחור, ואז כל פעולה נראית כמו הימור על ה-repository. בהרצאה הזו אריאל שולמן מפרקת את Git למבנה שלה: אובייקטים, references ושרשראות של commits. כשמבינים אותם, מכונת הזמן כבר לא מסתורית.
המטרה אינה לשנן plumbing commands או להוכיח שאפשר לעבוד בלי ממשק גרפי. אריאל יוצאת נגד המיתוס ש-command line היא תעודת הבנה. אפשר לדעת להקליד פקודה ועדיין לא לדעת מה יקרה לקוד, להיסטוריה או לחברי הצוות. ההבנה מתחילה במה שיש בתוך .git.
כי לדעת פקודות בקומנדליין ממש לא אומר שאתה יודע מה אתה עושה.
מכונת הזמן מתחילה ב-hash
Git מבוססת על SHA-1. אריאל מדגישה שהיא לא משתמשת בו כאן לצורך אבטחה, אלא כדי לתת לכל תוכן כתובת עקבית. אותו רצף טקסט יחזיר אותו hash בכל מקום ובכל זמן. זו התכונה שמאפשרת ל-Git לזהות אובייקטים ולחבר ביניהם בלי לתת להם שמות אנושיים.
ה-hash אינו קיצור דרך חזרה לטקסט המקורי. הוא identifier. Git שומרת את האובייקט בתיקיית .git/objects תחת הכתובת הזאת, ופקודות כמו git cat-file מאפשרות לראות מה נשמר. מה שנראה כמו מחרוזת אקראית ב-log הוא למעשה הציר שמחבר את התוכן, התיקיות וההיסטוריה.
Git משתמשת בחיסכון הזה היטב. אם אותו תוכן חוזר שוב, אין צורך לשמור עותק מלא נוסף רק כי הוא מופיע ב-commit אחר. האובייקטים מפנים זה לזה דרך hashes. כך מכונת הזמן נשארת גם עקבית וגם קלה יחסית לשכפול.
Blob הוא תוכן, tree הוא context
אריאל מתחילה משני אובייקטים בסיסיים: blob ו-tree. blob מחזיק את התוכן של קובץ. tree מחזיק את המבנה: שמות, קבצים, תיקיות והקשרים ביניהם. במילים שלה, blob הוא content ו-tree הוא context.
כשעושים git add ואז commit, Git אינה מצלמת את כל ה-repository כקובץ ZIP חדש. היא בונה או ממחזרת blobs לתוכן, trees למבנה, ואז commit שמצביע אל ה-tree של אותו מצב. זה מסביר למה אפשר להגיע חזרה למצב מדויק של קוד: ה-commit מוביל ל-tree, וה-tree מוביל לקבצים כפי שהיו באותו רגע.
commit מכילה גם author, committer, message ומצביע ל-commit ההורה. לכן commit אינה רק “רשומת שינויים”. היא node בגרף שמספר גם מה היה המצב וגם מאיפה הוא הגיע.
Branch היא לא סביבת קסם
אחרי שיש רצף commits, צריך משהו שיחזיק את הקצה שלו. branch עושה בדיוק את זה: היא reference ל-commit האחרון ב-timeline. כל commit מצביע אל הקודם, וה-branch מצביעה אל החדש ביותר. זו שרשרת פשוטה.
אם אין reference לאובייקט, Git יכולה בסוף לאסוף אותו ב-garbage collection. branch מחזיקה את כל הרצף מאחוריה חי. לכן אריאל מציעה לחשוב עליה לא כעל “עותק של הקוד” אלא כעל context לטיימליין: הטיימליין הראשי, טיימליין של feature או טיימליין של ניסוי.
גם main אינה קסומה. השם אומר לצוות מהו הקו הראשי, אבל מבחינה טכנית היא branch בדיוק כמו אחרות. הבחנה זו מונעת טעויות תודעתיות: לא מבקשים מ-branch “להתמזג” כי היא ישות מיוחדת, אלא מזיזים ומחברים references אל commits.
Merge מוסיף הורה, fast-forward מזיז מצביע
כשרוצים להכניס עבודה מ-branch אחת לאחרת, Git יכולה ליצור merge commit. ההגדרה של אריאל חדה: זה commit עם יותר מהורה אחד. הוא מצביע להיסטוריה של שני הצדדים ולתוצאה המשותפת שלהם.
merge זה commit עם יותר מהורה אחד.
אבל לא תמיד צריך commit חדש. אם branch יעד כבר נמצאת מאחורי branch אחרת וכל התוכן שלה כבר כלול בה, Git יכולה פשוט לקדם את ה-reference קדימה. זה fast-forward. אין דרמה ואין merge commit חדש, כי אין שני קווי היסטוריה שצריך לחבר.
ההבדל הזה חשוב יותר מהפקודה עצמה. הוא מסביר למה לפעמים merge משנה רק את pointer ולפעמים נוצרת node חדשה. במקום לנחש לפי הפלט של Git, אפשר להסתכל על גרף ה-commits ולראות איזה מצב קיים.
Rebase אינה merge עם שם אחר
rebase מעבירה רצף commits לבית חדש. אם commit D הצביעה פעם ל-A ועכשיו אמורה להצביע ל-C, היא כבר לא אותה commit - ההורה הוא חלק מהתוכן שעליו נוצר ה-hash. לכן Git יוצרת commit חדשה עם אותו שינוי עקרוני אך עם hash והורה אחרים.
rebase זאת עריכה של ההיסטוריה.
זה שימושי ל-branch מקומית: אפשר לסדר commits, לעשות interactive squash ולהחזיר סדר לפני שילוב. אבל ב-branch משותפת זה יכול לשבור אחרים. מי שכבר בנה commit על ההיסטוריה המקורית יגלה פתאום timeline שונה. הכלל של אריאל ברור: לא עושים rebase על commits שחולקים עם הצוות.
זה לא איסור מוחלט, אלא tradeoff. merge משמרת את ההיסטוריה כפי שקרתה; rebase מציגה אותה מחדש. לשתיהן יש מחיר. הבעיה מתחילה כשעורכים היסטוריה בלי להבין שזו הפעולה.
HEAD, remote והסנכרון בין מכונות זמן
HEAD היא המיקום הנוכחי שלנו בתוך ההיסטוריה. כש-HEAD מצביעה ל-branch, היא מתקדמת איתה. כשעושים checkout ל-commit מסוימת, נכנסים ל-detached HEAD: אפשר לבחון מצב ישן וליצור commits, אבל בלי reference חדשה העבודה עלולה להישאר ללא מצביע ולהיעלם בהמשך.
בצוות אין רק timeline אחת. יש references מקומיות ו-references של origin. git fetch מעדכנת את מה שיודעים על origin ומביאה אובייקטים חסרים, בלי לשנות את ה-branch המקומית. git pull היא fetch ואחריה merge. git push מנסה לקדם את reference המרוחקת מהלוקלית, ורק כאשר ההיסטוריה מאפשרת fast-forward.
force push מפרה את ההגנה הזו. היא אומרת שה-remote תצביע עכשיו לאן שהלוקלי מצביעה גם אם היו שם commits של אחרים. לכן היא דומה בסיכון ל-rebase על עבודה משותפת: אפשר לעשות אותה, אבל צריך לדעת בדיוק מי יאבד את timeline שלו בדרך.
מה לקחת לפרויקט הבא
- לפני פקודת Git מלחיצה, ציירו מי מצביע לאן: HEAD, branches ו-commits.
- זכרו ש-commit שומרת מצב, הורה ומצביע ל-tree - לא רק diff שקשה לקרוא.
- בחרו merge או rebase לפי המשמעות של ההיסטוריה לצוות, לא לפי הרגל.
- השתמשו ב-fetch כדי להבין מה קורה ב-origin לפני שאתם משנים את העבודה המקומית.
- אל תעשו force push או rebase על branch משותפת בלי תיאום מפורש.
Git נשארת מכונת זמן גם כשיש בה conflicts. ההבדל הוא שאחרי שמבינים את האובייקטים וה-pointers, אפשר לחזור, להתקדם ולסנכרן בלי להחזיק אצבעות.
"כי לדעת פקודות בקומנדליין ממש לא אומר שאתה יודע מה אתה עושה."
- אריאל שולמן
"merge זה commit עם יותר מהורה אחד."
- אריאל שולמן
"rebase זאת עריכה של ההיסטוריה."
- אריאל שולמן
- 01:38 למה command line לא הופך אותך למבין Gitאריאל מנפצת את המיתוס ששליטה בפקודות מספיקה וממקמת את ההבנה של הארכיטקטורה כדרך להפסיק לפחד מ-merge, rebase וחזרה אחורה.
- 03:48 SHA-1: הכתובת של התוכןאותו רצף תוכן מייצר אותו hash בכל זמן ובכל מקום. Git נשענת עליו כדי לזהות אובייקטים ולחבר את ההיסטוריה שלה.
- 06:35 Blob, tree ו-commit בתוך .gitblob מייצג תוכן של קובץ, tree נותן לו context של תיקייה, ו-commit מכיל מצביע ל-tree לצד author, message והורה.
- 12:04 Branch הוא pointer לטיימלייןmain מצביעה ל-commit האחרון בשרשרת. branch מחזיקה reference שמונע מהקומיטים להישאר בלי מצביע ולהיאסף על ידי garbage collection.
- 21:13 Merge ו-fast-forwardmerge הוא commit עם שני הורים או יותר. כשכל התוכן כבר נכלל ב-branch היעד, Git מקדמת את ה-reference ב-fast-forward במקום לייצר אובייקט מיותר.
- 24:33 Rebase עורכת את ההיסטוריההעברת commits לבית חדש משנה את ההורה שלהם ולכן יוצרת commits חדשים. אריאל מסבירה למה הדבר מסוכן כש-branch משותף לצוות.
- 27:13 HEAD ו-detached HEADHEAD מייצגת את המיקום הנוכחי של מפתחת אחת בתוך מכונת הזמן. checkout ל-commit ישיר מאפשר ניסוי, אבל עבודה בלי reference עלולה ללכת לאיבוד.
- 29:07 Fetch, pull ו-push כשני timelines מסונכרניםfetch מקדמת את reference של origin, pull משלבת fetch עם merge, ו-push מנסה לקדם את ה-reference המרוחק רק כשההיסטוריה מיושרת.
מה כוסה בהרצאה
שאלות מההרצאה
מה ההבדל בין blob, tree ו-commit ב-Git?
לפי ההסבר של אריאל, blob מייצג את תוכן הקובץ, tree מייצג את ההקשר של קבצים ותיקיות, ו-commit מכיל מצביע ל-tree יחד עם פרטי commit ומצביע ל-commit ההורה.
מה branch באמת עושה ב-Git?
branch היא reference ל-commit האחרון ברצף. היא נותנת context לטיימליין ומחזיקה reference לכל ההיסטוריה שמאחוריה. main אינה שונה טכנית משאר ה-branches, אלא שם מקובל לראשי.
למה לא לעשות rebase על branch משותף?
rebase משנה את ההורה של commits ולכן יוצרת העתקים חדשים שלהם. אם אנשים אחרים כבר בנו עבודה על ה-commits המקוריים, ההיסטוריות מתפצלות ונוצר בלבול או אובדן עבודה.
מה ההבדל בין fetch, pull ו-push?
fetch מעדכנת את ה-reference המקומי של origin ומביאה אובייקטים חסרים בלי לשנות branch מקומי. pull היא fetch ואז merge, ו-push מקדמת את reference של origin מהלוקלי כאשר אפשר לבצע fast-forward.