הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

חדש באתר: קורס TypeScript

19/12/2022

כמו הרבה טכנולוגיות, גם TypeScript לא היתה בשבילי אהבה ממבט ראשון. "מי צריך את זה?" שאלתי בזלזול, הרי אני אף פעם לא טועה בטיפוסים, וממילא באגים "אמיתיים" טייפסקריפט לא יצליח לתפוס. החשדנות שלי כלפי טייפסקריפט רק גברה ככל שהתחלתי לעבוד איתה - מצאתי את עצמי כותב הגדרות טיפוסים מסורבלות, ובטוח שטייפסקריפט הוא עוד מזימה שמטרתה לבזבז לי את הזמן במקום שאוכל להתקדם בקוד.

אחרי יותר משנתיים של עבודה בטייפסקריפט, אני מבין טוב את הטעות.

טייפסקריפט נכתבה כדי לתת למפתחים כלי עבודה טוב יותר לכתוב אפליקציות JavaScript. היא לא הולכת להפוך את JavaScript ל Java או ל C#, והיא לא הולכת להכניס שיטות עבודה "של אנשי בקאנד" לעולם הפרונט-אנד. היא בסך הכל נותנת לכם כלים טובים יותר לכתוב את ה JavaScript שאתם כבר מכירים, כדי שתוכלו לקודד מהר יותר ועם פחות הסחות דעת. אלה כמה מהפיצ'רים שלא חשבתי שאני בכלל צריך, אבל היום לא יכול בלעדיהם:

  1. השלמה אוטומטית של import-ים, כך שאני לא צריך לעצור ולהיזכר מאיזה קובץ מגיעה כל פונקציה.

  2. ריפקטורינג אוטומטי שאפשר לסמוך עליו, כך שאני לא צריך להיתקע עם שמות לא רלוונטיים לפונקציות או משתנים.

  3. השלמה אוטומטית אפקטיבית, גם כשאני משתמש בספריית קוד חדשה שאני עדיין לא מכיר בעל פה את ה API שלה.

ואחרי שלמדתי לעבוד נכון עם טייפסקריפט, גיליתי שה"מס" שאני משלם - כלומר כתיבת הגדרות הטיפוסים - הוא כמעט אפס. בעבודה נכונה, ובשימוש נכון ביכולות של טייפסקריפט, הטיפוסים כמעט כותבים את עצמם.

בקורס ריכזתי מסקנות משנתיים של עבודה עם טייפסקריפט, גם על פרויקטים שהצליחו וגם על פרויקטים שהסתבכו. בניגוד לתיעוד, שפשוט מראה לכם מה אפשר לעשות עם השפה, אני לקחתי כל יכולת לתוך Context של דוגמה או דוגמאות ספציפיות, שמראות איך ובאיזה מצבים היכולת הזאת תעזור לנו ואיזה בעיות כל פיצ'ר בא לפתור.

בין אם אתם צריכים להיכנס לפרויקט טייפסקריפט וחוששים לעשות טעויות, או אם אתם כבר עובדים בטייפסקריפט ומרגישים שהשפה לא נותנת לכם ערך מספיק גבוה, או אפילו אם אתם כבר אוהבים טייפסקריפט ורוצים לראות איך להשתמש בה בצורה יותר יעילה - הקורס הזה בשבילכם. זה הלינק עם סילבוס מלא, כל הפרטים וכמובן אם אתם מנויים יכולים ומוזמנים להתחיל לצפות כבר היום:

https://www.tocode.co.il/bundles/typescript

למה כל כך מסובך?

18/12/2022

ארבע סיבות מרכזיות בגללן אנחנו כותבים קוד מסובך:

  1. אנחנו עדיין לא מספיק מכירים את הטכנולוגיה או הבעיה כדי לכתוב פיתרון אלגנטי.

  2. אנחנו עדיין לא מבינים מספיק את הבעיה כדי להתמקד ב Use Cases האמיתיים שלה, ומתעקשים על מימוש שיתמודד עם מקרי קצה שאף פעם לא יקרו.

  3. הטכנולוגיה עדיין לא מספיק טובה כדי שנוכל לכתוב פיתרון אלגנטי.

  4. הבעיה באמת מסובכת ודורשת טיפול באינסוף מקרי קצה.

כשאתם חוזרים לקוד אחרי חצי שנה או שנה אתם מקבלים הזדמנות לתקן קוד מסובך שנכתב בגלל שלושת הסיבות הראשונות. אז כן אני מכיר את גישת ה "עובד אל תיגע" שהרבה אנשים מיישמים כלפי קוד מסובך, אבל לא מסכים איתה. שלוש מתוך ארבע סיבות לסיבוך כנראה כבר לא רלוונטיות בקוד שנכתב לפני שנה. היום יכול להיות הזדמנות טובה לפשט אותו.

[טייפסקריפט] דוגמה טובה לשימוש ב Function Overloading

17/12/2022

פיצ'ר Function Overloading של טייפסקריפט היה יכול להיות ממש מוצלח - הוא היה יכול להבין לפי הטיפוסים של הפרמטרים בזמן קומפילציה לאיזו גירסה של פונקציה התכוונו ולהפעיל אותה, במקום להכריח אותנו לכתוב גירסה כללית של הפונקציה בסגנון JavaScript. אבל למרות המימוש הקצת לוקה בחסר שלו, הוא עדיין יכול לעזור לנו בחיים האמיתיים לכתוב פונקציות עזר שיהיה כיף להפעיל. אם רק נקפיד לעבוד נכון.

המשך קריאה

היום למדתי: הפקודה readarray ב bash

16/12/2022

ל bash יש את הפקודה read שאמורה לקרוא קלט למשתנה, ולדעתי היא הסיבה המרכזית שאנשים לא אוהבים bash. היא תעבוד טוב כשניקח מילה בודדת למשתנה:

ubuntu@primary:~$ read name < <(echo ynon)
ubuntu@primary:~$ echo "$name"
ynon

אבל רק תנסו להעביר משהו שהיא לא אוהבת ואיבדתם אותה. הנה לדוגמה read מוחקת את הרווחים מתחילת השורה, סתם כי מתחשק לה:

ubuntu@primary:~$ read name < <(echo "   one")
ubuntu@primary:~$ echo "$name"
one

ואם בפלט יש כמה שורות, read פשוט תזרוק את כל מה שבא אחרי השורה הראשונה:

ubuntu@primary:~$ read name < <(cowsay hello)
ubuntu@primary:~$ echo "$name"
_______

ונכון הכל אפשר לפתור, אבל כשהכלים טובים יותר גם החיים פשוטים יותר. הפקודה readarray היא גירסה משודרגת של read שנועדה לקרוא אוסף של שורות לתוך מערך, וגם לטפל טוב יותר בכל סוגי הקלט. כך תיראה הדוגמה השניה עם הרווח בהתחלה עם readarray:

ubuntu@primary:~$ readarray lines < <(echo "   one")
ubuntu@primary:~$ echo "$lines"
   one

וזאת הדוגמה השלישית, שמדפיסה הפעם את כל השורות:

ubuntu@primary:~$ readarray lines < <(cowsay hello)
ubuntu@primary:~$ echo "${lines[@]}"
 _______
 < hello >
  -------
         \   ^__^
          \  (oo)\_______
             (__)\       )\/\
                 ||----w |
                 ||     ||

יותר מזה, בגלל שהפלט נשמר בתור מערך אפשר לגשת לכל שורה ממנו למשל:

echo "${lines[1]}"

ל readarray יש די הרבה אפשרויות להתאמה כולל המתג -t שמוחק את תו ירידת השורה מכל שורת קלט, המתג -n שמאפשר להגביל את מספר השורות שנקרא, המתג -s שמאפשר לדלג על השורות הראשונות.

נ.ב. בטוח שמתם לב לקריאה המשונה עם כל החצים שמאלה. גם אני כמוכם הייתי שמח לכתוב:

ubuntu@primary:~$ cowsay hello | readarray lines

אבל זה לא יעבוד. סימן | פותח Sub Shell, והמערך lines יוגדר באותו Shell פנימי. אם נפעיל פקודה כזאת ואחריה ננסה להדפיס את lines ב Shell שלנו נקבל מערך ריק.

[ריאקט] אפקט לחישוב אסינכרוני הוא Anti Pattern

15/12/2022

אם קומפוננטת ריאקט מקבלת ערך ב prop וצריכה להציג מידע מחושב מהערך הזה, כולנו יודעים לא לשמור את תוצאת החישוב ב State נפרד ופשוט לחשב אותו כל Render, לדוגמה:

function Twice({ x }) {
    return <p>{x} * 2 = {x * 2}</p>
}

אבל כשמדובר בחישוב אסינכרוני אנחנו נוטים להתבלבל - נזכרים שיש useEffect, חושבים שכדאי לחשב את הערך באיזה אפקט, לשמור אותו בסטייט נפרד ואז לרנדר מחדש כשיהיה מוכן. משהו כזה:

// Don't Do This:
function DataFromApi({ id }) {
    const [name, setName] = useState(null);

    useEffect(() => {
        fetch(`/api/product/${id}`)
            .then(p => p.json())
            .then(p => setName(p.name));
    }, [id]);

    if (!name) {
        return <p>Loading...</p>
    }

    return <p>Product Name = {name}</p>
}

אבל זאת Anti Pattern. אלה הבעיות המרכזיות עם הגישה:

  1. אם הקומפוננטה יוצאת מהמסך וחוזרת, הערך שלכם נמחק וצריך להביא אותו מחדש. הסטייט הוא לא מקום מספיק "יציב" כדי לשמור בו מידע שמגיע מהרשת (או מידע שהתאמצתם לחשב באופן כללי).

  2. יש לנו נטיה לטעות במערך ה Dependencies של useEffect לעתים הרבה יותר קרובות ממה שנדמה לכם, וטעויות כאלה מאוד קשות לזיהוי. סיכוי טוב שהחישוב שלכם ירוץ הרבה יותר פעמים ממה שאתם באמת צריכים.

כשאנחנו רוצים לחשב מידע בצורה אסינכרונית ולהשתמש בתוצאה בקומפוננטה, הרבה יותר מומלץ להפריד בין "אני רוצה לחשב את המידע הזה" לבין "אני צריך להוציא את המידע שחושב מהמחסן כדי להציג אותו". הקומפוננטה אחראית רק על החלק השני, אבל את התחלת החישוב ושמירת התוצאות כדאי לבצע מחוץ לעץ. רידאקס מספק פיתרון טוב באופן כללי, וכשמדובר בתקשורת ספריות כמו swr או react-query מציעות פיתרון טוב תוך שימוש ב Context.

[טייפסקריפט] ואז Parameters הציל את היום

14/12/2022

ספריית התקשורת האהובה עליי בריאקט היא swr. היא יותר קטנה מ react-query ועושה בדיוק מה שאני צריך, אבל יש לה רק בעיה אחת: בשביל להשתמש בה צריך להגדיר לה פונקציית תקשורת. אני כל פעם שוכח מחדש את התחביר וחיפוש בגוגל מביא אותי לאותה תוצאה:

const fetcher = url => fetch(url).then(r => r.json())

אבל איך מתרגמים את זה לטייפסקריפט?

אינטואיציה ראשונה תהיה להגדיר את url בתור string, אבל זה לא מרגיש לגמרי נכון. הפונקציה fetch יכולה לקבל גם אוביקט של פרמטרים, ו swr גם תומך בזה. במלים אחרות מבחינת swr אפשר לכתוב בקומפוננטה:

const { data, error } = useSWR({ url: '...', method: 'POST' });

וזה אמור לעבוד ולשלוח בקשת POST. אם אנחנו מכריחים את ה url להיות רק string אנחנו מבטלים שימוש די סטנדרטי בספריה.

מה שאנחנו באמת רוצים כאן זה להגדיר את fetcher שיקבל בדיוק את הפרמטרים ש fetch מקבלת. ואיך כותבים את זה? תשמחו לשמוע שבטייפסקריפט יש Utility Type שנקרא Parameters. זה עובד ככה:

const fetcher = (...args: Parameters<typeof fetch>) => fetch(...args).then(res => res.json())

עכשיו הפונקציה fetcher מקבלת בדיוק את הפרמטרים ש fetch של JavaScript מצפה לקבל, ואנחנו נהנים מכל ההשלמות האוטומטיות והוולידציה שטייפסקריפט יודעת להציע גם כשמפעילים אותה.

משחקים עם טייפסקריפט - יצירת טיפוס לאינדקס חוקי במערך

13/12/2022

ניקח מערך של פריטים קבועים, למשל מערך של צבעים:

const colors = ['red', 'blue', 'green', 'white'];

ועכשיו נרצה לכתוב פונקציה שמקבלת אינדקס של צבע מתוך המערך ומחזירה את שם הצבע. ניסיון ראשון עשוי להיות:

function getColorName(index: number) {
    return colors[index];
}

וזה עובד כמובן אבל אנחנו לא נהנים מבדיקת הטיפוסים - אני יכול להפעיל את הפונקציה על אינדקסים חוקיים או לא חוקיים והכל יעבוד:

// compiles OK
getColorName(1);

// also compiles OK - but 129 is not a valid index
getColorName(129);

אם אתם אנשים שאוהבים לכתוב כפול תוכלו לתקן את הפונקציה בעזרת איחוד טיפוסים ליטרליים:

function getColorName(index: 0|1|2|3) {
    return colors[index];
}

אבל זאת בעיה כי עכשיו כל פעם שנעדכן את המערך נצטרך לעדכן גם את הפונקציה. ברור שאפשר להוציא את הגדרת הטיפוס מהפונקציה ל Type Alias, אבל עדיין נצטרך לתחזק אותו בנפרד מהמערך.

כיוון יותר מאתגר שחוסך את הקוד הכפול יהיה לייצר דינמית את אותו איחוד. כלי עבודה ראשון שנצטרך הוא ה Utility Type הבא שהופך טיפוס מחרוזתי שמייצג מספר למספר עצמו, כלומר הופך את הטיפוס '2' לטיפוס 2:

type StringToNumber<T extends string, A extends any[] = []> =
  T extends keyof [0, ...A] ? A['length'] : StringToNumber<T, [0, ...A]>

הקוד קצת מוזר אבל בגדול זו פונקציה רקורסיבית שמחפשת את הערך האמיתי של המספר באמצעות גודל של מערך. היא כל פעם מנסה מערך קצת יותר גדול ובודקת אם המספר T הוא אינדקס חוקי באותו מערך, ורק כשמצאנו את המערך ש T הוא אינדקס חוקי בו אנחנו עוצרים ומחזירים את גודל המערך.

ומשתמשים בה כדי לייצר טיפוסים באופן הבא:

// Two == 2
type Two = StringToNumber<'2'>;

// Three == 3
type Three = StringToNumber<'3'>;

הסיפור רק משתפר כשאני מנסה להפוך למספר משהו שהוא בכלל לא מספר. הקוד הזה לא מתקמפל כי אין מערך ש hello הוא אינדקס חוקי בו:

type Error = StringToNumber<'hello'>;

עם השגיאה המופלאה:

Type instantiation is excessively deep and possibly infinite.(2589)

וזה מהמם כי אנחנו יכולים "לתפוס" את השגיאה ולהשתמש בה כדי לזרוק מפתחות מיותרים בעזרת Mapped Types. הרעיון הוא לקחת את כל המפתחות ב items, שזה כל האינדקסים (בתור מחרוזות) אבל גם המאפיין length וכל שאר הפונקציות של המערך, ולסנן מהם רק את אלה שאפשר להמיר למספר, ועל הדרך גם להמיר אותם למספרים. בסוף נפעיל keyof על התוצאה ונקבל את איחוד המספרים שחיפשנו. זה הקוד:

type ValidIndex = keyof {
    [i in keyof typeof items as (i extends string ? StringToNumber<i> : never)]: 2;
}

הערך 2 בסוף הוא שרירותי לגמרי, כי keyof יזרוק אותו. גם ה never לא חשוב כי הוא ייזרק, ומנגנון המיפוי באופן אוטומטי ישמיט את המפתחות שלא הצליחו. התוצאה:

// Compiles OK - as 2 is a valid index in the array
const valid: ValidIndex = 2;

// Compilation error - 10 is not a valid index
const invalid: ValidIndex = 10;

רגע, למה זה עובד?

12/12/2022

בכל הפריימוורקים ושפות התכנות שיצא לי לראות, היה פער בין כמות השעות הנדרשת בשביל להגיע לקוד שעובד לכמות השעות הנדרשת בשביל להבין למה הוא עובד.

ברוב המקרים גם אחרי שחשבתי שאני מבין למה הקוד עובד, היה צריך עוד כמה שעות של לימוד והשקעה כדי להבין שבעצם לא הבנתי נכון בפעם הראשונה, ולדייק את ההבנה. לפעמים רק כשהגעתי לדבר עם אנשים על הקוד או להסביר אותו בכתב פתאום הבנתי שפספסתי כמה נקודות חשובות.

וזה מביא אותנו לכמה בעיות-

  1. ברוב התעשיה יש לחץ להגיע לפיצ'רים עובדים מהר. משפט כמו "בוס הקוד עובד פיקס ועכשיו אני יושב לקרוא את כל התיעוד כדי להבין למה זה עובד כל כך טוב" לא מתקבל בהבנה.

  2. גם המתכנתים עצמם לא מתלהבים להמשיך לעבוד על קוד שעובד, כי אנחנו לא בטוחים מה זה אומר "להבין את הקוד עד הסוף". רוב הזמן אחרי שקוד עובד אנחנו מרגישים שאנחנו מבינים אותו די טוב.

  3. לאורך זמן, קוד שאנחנו לא מבינים מייצר תופעות לוואי ובאגים שיהיה לנו קשה למצוא. המחשב לא מתעלם מכלום, אפילו מהנקודות שאנחנו לא ראינו.

הפיתרון? לדעתי הפיתרון היחיד הוא לשבור דברים אחרי שכתבתם אותם, ולא לדווח על פיצ'ר מוכן עד שסיימתם לכתוב עבורו בדיקות ולחקור את כל ההשלכות של אותו קוד. הכי חשוב לא להשאיר דברים ב"זה עובד ואני אחזור לעשות ריפקטורינג ולתקן אחר כך". האחר כך לא מגיע אף פעם, ודווקא הריפקטורינג הולך לחשוף עוד כמה באגים שמסתתרים שם. זה לא גמור כשזה עובד, זה גמור כשזה גמור.

פיתרון Advent Of Code 2022 יום 6 ביוניקס

11/12/2022

אחרי שאלה קצת מעייפת על מגדלים חזרנו לשאלות שאפשר לפתור בשורה אחת של יוניקס, והשאלה של יום 6 היתה הזדמנות מצוינת להיזכר בלולאות ומשתנים של Bash. המשימה הפעם היא לקחת מחרוזת תווים ולמצוא את האינדקס של התו הראשון שארבעת התווים שלפניו שונים אחד מהשני. הדוגמה היתה המחרוזת הזו:

mjqjpqmgbljsphdztnvjfqwrcgsmlb

ואנחנו רואים שבקריאה משמאל לימין הפעם הראשונה שמופיעים 4 תווים שונים היא התווים jqpm ולכן האינדקס של התו שאחריהם (האות g) הוא 7.

בשביל למצוא אותו אפשר לתת ל bash לעבוד:

  1. נרוץ בלולאה עם משתנה לולאה i שמתחיל מ-1.
  2. לכל ערך של i נחתוך מהמחרוזת את 4 התווים החל מהמקום ה i בעזרת cut.
  3. נשלח את egrep לחפש תו כפול במחרוזת. אם הוא לא מצא אפשר לצאת מהלולאה ולהדפיס את i+4-1

בהנחה שהקלט נשמר בקובץ input.txt תוכנית ה bash המתאימה היא:

#!/bin/bash

i=1
while cat input.txt | cut -c $i- | cut -c -4 | egrep '(\w).*\1' >& /dev/null
do
    (( i++ ))
done

echo $(( i + 4 - 1))

או בשורה אחת:

i=1; while cat input.txt| cut -c $i- | cut -c -4 | egrep  '(\w).*\1' >& /dev/null ; do; (( i ++ )); done; echo $(( i + 4 - 1))

שלוש נקודות לקחת מהתוכנית:

  1. הרבה פעמים נוח לפצל תנאי לכמה תנאים ב Pipeline. במקרה שלנו בשביל לחתוך 4 תווים החל ממקום i אפשר לשבור את השיניים בשביל ליצור פקודת cut אחת, אבל יותר קל לפצל את זה לשתי פקודות cut אחת אחרי השניה, אחת מתחילה מהמקום ה i והשניה חותכת 4 תווים.

  2. מאוד נוח שב bash פקודת while מבצעת פקודה שהיא מקבלת שוב ושוב כל עוד הפקודה מצליחה.

  3. סוגריים עגולים כפולים ב bash "בורחים" מהעולם של bash לעולם חשבוני רגיל, ובגלל זה עדיף להשתמש בהם כשרוצים לבצע פעולות אריתמטיות על משתנים.

למרות שלסקריפטים יש בעיית תחזוקה וברוב המקרים יותר קל לכתוב חדש מאשר לעדכן קיים, הם עדיין כלי עבודה עוצמתי במיוחד למשימות קטנות, בזכות הפורטביליות, קלות הכתיבה והיכולת להשתמש בקלות בכל הכלים הקיימים ביוניקס.

גיט הוא בסך הכל גרף

10/12/2022

  • אתה יודע מאוד עוזר לחשוב על גיט בתור גרף מכוון.

  • גרף מה?

  • גרף מכוון. גרף הוא אוסף של אוביקטים שכל שניים עשויים להיות מקושרים זה לזה, ומכוון אומר שהקישור בין שני אוביקטים הוא לכיוון אחד. תחשוב על מסלול של אוטובוס - אפשר לצייר כל תחנה על המסלול בתור עיגול על הדף, ואם האוטובוס נוסע מתחנה א לתחנה ב נצייר חץ בין שתיהן לפי כיוון הנסיעה.

  • אבל איך אוטובוס קשור לגיט?

  • כמו שהאוטובוס נוסע מתחנת המוצא לתחנת היעד, כך הקוד שלך מתקדם מנקודת ההתחלה (היום בו התחלת לעבוד על הפרויקט) עד נקודת היעד (היום בו החלטת לקבור את הפרויקט ולעבור לכתוב משהו חדש). וכן זה קצת שונה מתחנות אוטובוס כי האוטובוס מתחיל בנסיעה כשהוא יודע איך יראה כל המסלול, ואילו אנחנו מתחילים לקודד כשכל פעם אנחנו יכולים לראות רק את התחנה הבאה. אבל ההבדל היותר חשוב הוא כיוון הנסיעה.

  • מה הכוונה כיוון הנסיעה?

  • האוטובוס נוסע מתחנת המוצא לתחנת היעד, אז כשציירנו אותו בתור גרף כל פעם ציירנו חץ מהתחנה לתחנה הבאה. בשביל לקחת את האנלוגיה הזאת לגיט אפשר לדמיין שכל פעם שאנחנו עושים קומיט אנחנו בונים תחנת אוטובוס חדשה על המסלול, ובתחנה הזאת מחביאים בקופסה את כל הקבצים בפרויקט. בזמן בניית התחנה אנחנו לא יודעים מה תיהיה התחנה שאחריה (או אם אפילו תהיה אחת בכלל), אז על כל תחנה גיט כותב את הכתובת של התחנה שהיתה לפניה. בצורה כזאת כל פעם שאוטובוס עוצר בתחנה הוא תמיד יכול לנסוע לתחנה שהיתה לפניה, אבל בשום שלב הוא לא יודע מה הכתובת של התחנה הבאה.

  • עדיין לא הבנתי. איך הקומיט קשור לתחנת אוטובוס?

  • בוא ננסה את זה בקוד. אני יוצר פרויקט חדש עם גיט בתיקיה הנוכחית:

$ 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-ים? ריבייסים? צ'רי פיק?
  • כן צריך להיזהר עם גיט, אבל כשמבינים את המודל שלו הרבה יותר קל לצאת ממצבים מביכים. הייתי שמח להישאר ולקשקש אבל אני רואה את האוטובוס שלי מגיע. על מרג'ים, ריבייסים וכל השאר נצטרך להמשיך לדבר ביום אחר. אה, וכמעט שכחתי - יש בטוקוד קורס גיט נהדר שמסביר את הפקודות יחד עם המודל המנטלי שמתאים להן. נתראה בתחנה הבאה.