אתה יודע מאוד עוזר לחשוב על גיט בתור גרף מכוון.
גרף מה?
גרף מכוון. גרף הוא אוסף של אוביקטים שכל שניים עשויים להיות מקושרים זה לזה, ומכוון אומר שהקישור בין שני אוביקטים הוא לכיוון אחד. תחשוב על מסלול של אוטובוס - אפשר לצייר כל תחנה על המסלול בתור עיגול על הדף, ואם האוטובוס נוסע מתחנה א לתחנה ב נצייר חץ בין שתיהן לפי כיוון הנסיעה.
אבל איך אוטובוס קשור לגיט?
כמו שהאוטובוס נוסע מתחנת המוצא לתחנת היעד, כך הקוד שלך מתקדם מנקודת ההתחלה (היום בו התחלת לעבוד על הפרויקט) עד נקודת היעד (היום בו החלטת לקבור את הפרויקט ולעבור לכתוב משהו חדש). וכן זה קצת שונה מתחנות אוטובוס כי האוטובוס מתחיל בנסיעה כשהוא יודע איך יראה כל המסלול, ואילו אנחנו מתחילים לקודד כשכל פעם אנחנו יכולים לראות רק את התחנה הבאה. אבל ההבדל היותר חשוב הוא כיוון הנסיעה.
מה הכוונה כיוון הנסיעה?
האוטובוס נוסע מתחנת המוצא לתחנת היעד, אז כשציירנו אותו בתור גרף כל פעם ציירנו חץ מהתחנה לתחנה הבאה. בשביל לקחת את האנלוגיה הזאת לגיט אפשר לדמיין שכל פעם שאנחנו עושים קומיט אנחנו בונים תחנת אוטובוס חדשה על המסלול, ובתחנה הזאת מחביאים בקופסה את כל הקבצים בפרויקט. בזמן בניית התחנה אנחנו לא יודעים מה תיהיה התחנה שאחריה (או אם אפילו תהיה אחת בכלל), אז על כל תחנה גיט כותב את הכתובת של התחנה שהיתה לפניה. בצורה כזאת כל פעם שאוטובוס עוצר בתחנה הוא תמיד יכול לנסוע לתחנה שהיתה לפניה, אבל בשום שלב הוא לא יודע מה הכתובת של התחנה הבאה.
עדיין לא הבנתי. איך הקומיט קשור לתחנת אוטובוס?
בוא ננסה את זה בקוד. אני יוצר פרויקט חדש עם גיט בתיקיה הנוכחית:
$ git init .
כותב שטויות באיזה קובץ ושומר אותו בקופסה - זאת התחנה הראשונה:
$ echo hello world > readme.txt
$ git add readme.txt
$ git commit -m 'initial commit'
אני יכול לראות את כל התחנות עם פקודת git log, אבל כרגע יש שם רק אחת:
$ git log --oneline
f812a16 initial commit
לתחנה האחת שלי אגב יש שם - f812a16. זה מזהה ייחודי של התחנה הזאת. נמשיך לבנות עוד כמה תחנות במסלול:
$ echo second stop > status.txt
$ git add .
$ git commit -m 'second stop'
[main b39bd7e] second stop
$ echo third stop > status.txt
$ git add .
$ git commit -m 'third stop'
[main 43ea893] third stop
$ echo fourth stop > status.txt
$ git add .
$ git commit -m 'fourth stop'
[main fb7fdb6] fourth stop
הלוג עכשיו מראה את כל התחנות:
$ git log --oneline
fb7fdb6 fourth stop
43ea893 third stop
b39bd7e second stop
f812a16 initial commit
אבל הדבר החשוב כאן הוא הכיוון: כל תחנה "מצביעה" על התחנה שבאה לפניה. אני יכול לראות את זה עם cat-file
:
$ git cat-file commit fb7fdb6
tree 2394fdb509358a49d452574536bec528d119ba06
parent 43ea893b8bda3420d7a794ef14a7246d94f88ced
author ynonp <ynonperek@gmail.com> 1670589664 +0200
committer ynonp <ynonperek@gmail.com> 1670589664 +0200
fourth stop
אחרי התווית parent אני רואה את שם התחנה השלישית, כי התחנה הרביעית "יודעת" שלפניה היתה התחנה השלישית (וכן השם שמופיע כאן הוא יותר ארוך מהשם בו אני משתמש, אבל זה אותו קומיט הקיצור הוא רק בשבילנו). אתה יכול גם להמשיך את המשחק ולראות שכל תחנה אחרת מכירה רק את התחנה שבאה לפניה. זאת הסיבה ש git log עשוי להיות כל כך מבלבל - ברירת המחדל שלו היא להראות את אותו גרף, החל מהתחנה בה אני נמצא ואחורה עד תחילת הפרויקט.
רגע רגע, מה הכוונה "התחנה בה אתה נמצא?" - אפשר לנסוע בין התחנות?
ברור. זאת כל הפואנטה. הפקודה git switch לוקחת אותך לתחנה אחרת. הנה דוגמה:
$ git switch --detach b39bd7e
HEAD is now at b39bd7e second stop
מבט על הפרויקט יראה לנו את הקבצים כמו שהם היו כשהיינו בתחנה השניה. אבל השוס האמיתי הוא הפלט של git log:
$ git log --oneline
b39bd7e second stop
f812a16 initial commit
התחנה השניה יכולה להסתכל רק "אחורה" לתחנה הראשונה, ולכן זה מה שאנחנו רואים בלוג. התחנות השלישית והרביעית אינן במסלול.
מה? מחקת לי את כל הפרויקט?? אז איך אני חוזר עכשיו לנקודת ההתחלה?
קח אוויר אחינו. רק בגלל שהאוטובוס נוסע ברוורס לא אומר שאנחנו אבודים. האמת שלנהג האוטובוס יש וויז בדיוק כמו לך. אם רק תתן לו כתובת של תחנה הוא ברגע יסע לשם. יותר מזה, גיט שומר את כל הכתובות של התחנות שהוא כבר ראה, כדי שלא נלך לאיבוד. תחשוב על זה כמו יומן מסע שגיט מנהל. סוג אחד של אינדקסים נקרא Branch-ים, שהם בעצם שמות יותר נוחים לתחנות. בגלל שהיומן כתוב בעיפרון, אפשר תמיד להוסיף עוד "רשומות", כלומר עוד שמות לתחנות שלנו, וגם להזיז את השמות בין תחנות. אני יכול לראות את כל השמות שגיט כתב עד עכשיו עם:
$ git branch -v
* (HEAD detached at b39bd7e) b39bd7e second stop
main fb7fdb6 fourth stop
ואני רואה את השם המיוחד HEAD
שתמיד מתיחס לתחנה בה האוטובוס נמצא כרגע, ואת השם main שמודבק למזהה התחנה fb7fdb6
. בוא נירגע ונקפיץ את האוטובוס חזרה ל main, שהיא כרגע התחנה הכי רחוקה במסלול:
$ git switch main
Previous HEAD position was b39bd7e second stop
Switched to branch 'main'
עכשיו לוג כבר מראה את כל המסלול כי התחלנו מהנקודה הרחוקה ביותר, וכל נקודה יכולה "לראות" את זו שבאה לפניה:
$ git log --oneline | cat
fb7fdb6 fourth stop
43ea893 third stop
b39bd7e second stop
f812a16 initial commit
שני הסוגים המרכזיים של "שמות" שגיט מחזיק הם branch ו tag, כשההבדל ביניהם הוא ש branch מתקדם באופן אוטומטי כשאני יוצר תחנות חדשות (בגלל זה למרות שלא התיחסתי אליו עד עכשיו, הבראנץ main הכיל את המזהה של התחנה הרחוקה ביותר), ו tag נשאר במקום בו יצרנו אותו. ואגב הפקודה git log
יודעת לקבל תחנת התחלה, כך שלא משנה איפה אתה בפרויקט תוכל להקליד git log main
ולראות את הלוג החל מתחנת main.
אני חושב שאני מתחיל להבין. אז בעצם האוטובוס הוא כמו תיקיית העבודה שלי. כשהוא נוסע בין תחנות הקבצים שאני רואה משתנים, ובכל תחנה הוא יכול לראות את הכתובת של התחנה הקודמת כדי לנסוע אליה. בשביל לנסוע קדימה בזמן הנהג פותח את ספר הכתובות שלו וכך יודע להגיע גם לתחנות רחוקות, והקבצים שלי אף פעם לא ילכו לאיבוד. נכון?
כמעט, האמת שבעוד שתחנות אי אפשר להזיז מרגע שנוצרו, את ספר הכתובות אפשר לשנות כל הזמן. הפקודה הבאה מוחקת את הכתובת של התחנה האחרונה בספר (main) ובמקומה רושמת את הכתובת של התחנה השניה ליד השם main:
git reset --hard b39bd7e
ננסה עכשיו את הטריקים שלמדנו ונופתע לגלות:
$ git log main --oneline| cat
b39bd7e second stop
f812a16 initial commit
$ git branch -v | cat
* main b39bd7e second stop
בספר הכתובות שלי השם main מודבק עכשיו ליד תחנה b39bd7e. התחנה fb7fdb6 עדיין נמצאת בשכונה, אבל בשביל להגיע אליה אני חייב להשתמש בכתובת המדויקת שלה. לפני שנשכח אני מוסיף אותה לספר הכתובות עם שם חדש:
$ git branch dev fb7fdb6
ועכשיו יש לי שתי כתובות בספר:
$ git branch -v
dev fb7fdb6 fourth stop
* main b39bd7e second stop
ואני שוב יכול לחזור לכתובת הרחוקה ביותר:
$ git switch dev
$ git log --oneline
fb7fdb6 fourth stop
43ea893 third stop
b39bd7e second stop
f812a16 initial commit
- נשמע מלחיץ. ואיך זה מסתדר עם merge-ים? ריבייסים? צ'רי פיק?
- כן צריך להיזהר עם גיט, אבל כשמבינים את המודל שלו הרבה יותר קל לצאת ממצבים מביכים. הייתי שמח להישאר ולקשקש אבל אני רואה את האוטובוס שלי מגיע. על מרג'ים, ריבייסים וכל השאר נצטרך להמשיך לדבר ביום אחר. אה, וכמעט שכחתי - יש בטוקוד קורס גיט נהדר שמסביר את הפקודות יחד עם המודל המנטלי שמתאים להן. נתראה בתחנה הבאה.