כמה דברים שרציתם לדעת על גיט – אבל חששתם / התעצלתם לשאול

ראיתי שהפוסט האחרון שלי על גיט עורר כמה תגובות מעניינות – והחלטתי להוסיף המשך קצר.

גיט הוא כלי "הכרחי":

  • הוא הבחירה של רוב הארגונים / פרויקטים כיום בעולם התוכנה. כלי מאוד דומיננטי.
  • מעשית, לא ניתן לעבוד בלעדיו: כתבת קוד? אתה רוצה לקרוא קוד? – עליך להשתמש בגיט.
מתוך הצורך הקיומי לשימוש בגיט, קיימת רמת מיומנות בסיסית הנפוצה בקרב אנשי תוכנה.
נקרא לה "רמה 2".
רוב אנשי התוכנה נמצאים ברמה 2. הם יודעים לעבוד בצורה שוטפת עם גיט, וכבר לא נתקלים בבעיות רציניות איתו (אין כבר מצבי "Detached from head" שכיחים כמו בהתחלה), אבל עדיין יש עוד הרבה בגיט שאינו מוכר ואינו מובן.
למשל:
  • עניינים של ה internals – כיצד גיט ממומש, וכיצד הוא עובד מאחורי הקלעים.
  • כל מיני מונחים מוכרים, אבל לא מובנים, למשל: Fast-Forward, ReReRe, Rebase, או Bisect – היא רשימה מייצגת שאני נתקלתי בה, של מונחים שאנשים רבים שמעו – אבל לא יודעים באמת מה הם אומרים.
האם רמה 2 היא "מספיק טובה" בכדי לעבוד עם גיט?
המציאות מוכיחה שבהחלט אפשר לעבוד ברמה 2, לאורך שנים – ולהפיק תוכנה שימושית ומועילה (אולי אפילו מעולה).
מצד שני… קצת חבל. לשמוע מושגים ולא להבין מה הם אומרים?
לאורך שנים?
לי זה קצת חבל ולכן אנסה לתת הסברים קצרים אך משמעותיים על כמה מונחים שבחרתי שנמצאים בנקודה הזו: שמם מוכר – אך יש ערפל גדול לגבי המהות שלהם, ומה הם עושים. דיסאינפורמציה גדולה מאינפורמציה.
ייתכן גם שאני טועה לחלוטין, ורק קומץ סטודנטים, למדעי-הרוח, בשנה ראשונה לא מכירים את המונחים הללו 😊.

Rebase

rebase הוא דרך חלופית ל merge על מנת למזג עבודה בין branches.
יש הנחה ששמעתי הגורסת שמשתמשי SVN לשעבר, הרגילים לעבור על branch יחיד (להלן "trunk") – נוטים להשתמש ב rebase כי הוא מזכיר את הכלי הקודם שלהם. לא יודע.

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

אותו קוד, אותם שינויים – שיטת מיזוגים שונה (בד"כ ה rebase יהיה ארוך יותר)

Merge – מציג את המצב המסובך כפי שהוא.
Rebase – יוצר מציאות קלה יותר למעקב – אך יש מחירים לייצר אותה.

נניח שיש לנו branch עם 6 commits שאנחנו רוצים למזג ל master.
כאשר אנו משתמשים ב merge – זו פעולה אחת, עם סיכוי מסוים לקונפליקטים. הקונפליקטים נוצרים ע"פ המצב הסופי של ה branch שלי מול המצב הסופי של ה master.

rebase הוא מורכב יותר: הוא ייקח את ה branch שלי, commit אחר commit, ויטמיע אותם בראש ה master branch כאילו רק עכשיו כתבתי אותם. אחד אחרי השני.

הסיכוי לקונפליקטים שצריך לפתור גדל משמעותית: ייתכן שכתבתי קוד ב commit מס\'3 שיצור קונפליקט, אבל הסרתי את הקוד הזה ב commit מס\'5 (המאוחר יותר ב branch).
אם הייתי עובד עם merge לא הייתי צריך להתעסק עם הקונפליקט (הזמני) הזה -הקוד הסופי אינו מכיל אותו.
ב rebase אני צריך לפתור אותו. אם ב commit מס\'4 היה שינוי נוסף בקוד הזה – ייתכן וייווצר קונפליקט נוסף שיהיה עלי לפתור.

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

מתי זה משתלם?
עבור רוב הפרויקטים זה לא משתלם. בפרויקטים בהם עוברים על ההיסטוריה פעמים רבות ומנסים להבין אותה – זה עשוי להשתלם.
Rebase נפוץ בשימוש, יחסית, בפרויקטי Open Source מרובי תורמים זרים – כלומר: תורמים שלא מכירים זה את זה ולכן התקשורת ביניהם היא פחות יעילה. במקרים האלו – היסטוריה נקיה יכולה לחסוך הרבה בעיות תקשורת, ולהצדיק את המחיר הנוסף בביצוע rebase.

נ.ב: בעוד את פקודת git merge מפעילים מתוך branch היעד (אליו רוצים למזג), את פקודת git rebase מפעילים מתוך branch המקור – ה branch אשר את תוכנו רוצים למזג/"להרכיב" על branch אחר.

Fast Forward

אין סיכוי שלא נתקלתם במונח Fast Forward (להלן FF) בעבודה עם גיט.

"טוב, קרה פה משהו מהיר. נראה שאין בעיות. יופי – נמשיך הלאה!" – היא התגובה המקובלת להודעה של גיט שבוצע FF.

בואו נבין טיפה יותר:
בעצם FF הוא אופטימיזציה של גיט על מנת לפשט את ההיסטוריה.

כאשר יש לי branch (למשל: feature branch) שאני רוצה למזג (למשל: ל master) אבל ה master לא השתנה מאז שהתפצלתי ל feature branch – אפשר לפשט את הדברים.

merge בשלב כזה ל master הוא כאילו הוספתי את ה commits שלי ,לא ל feature branch – אלא ישירות ל master.
התוצאה הרי הייתה זהה.

יש פה גם עניין של חיסכון ברמת המימוש הפנימי של גיט: בוודאי שמעתם ש branch הוא בעצם רק pointer.
על מנת לבצע FF כל מה שגיט צריך הוא להפנות את המצביע (branch) בשם "master" להצביע לנקודה של המצביע "feature branch".

אם מסיבה כלשהי חשוב לכם להדגיש את ההיסטוריה כפי שקרתה – אתם יכולים להורות ל git לבצע merge ללא FF.
זה כל הסיפור.

Git Revert

את הפקודה הזו כדאי להכיר כי היא מאוד שימושית ברגעים מסוימים. אני לא בטוח כמה אנשים מודעים אליה. הרבה פעמים אנשים עם ניסיון בכלי version control אחרים נוטים להתבלבל ולחשוב ש git revert עושה מה שבעצם git reset עושה.

הפקודה git revert HEAD~2 (אנו מכוונים ל commit X, הרי הוא Head פחות 2 צעדים אחורה) – מנסה ליצור commit חדש המסומן כ X- אשר מהווה את ההופכי של X ומבטל את כל הפעולות שנעשו.
לאחר ש commit -X ייווצר – כל התוספות של y ו z – עדיין יהיו תקפות, לא ביטלנו אותן.

מתי הדבר שימושי? למשל כאשר יש בעיה בסביבת production או staging שאנו רוצים לפתור מהר, ואנו יודעים איזה commit אחראי לה. ניתן אח"כ לעשות revert ל X- – ולקבל בחזרה את השינויים של X למערכת.

אם יש התנגשות שגיט לא יודע לפתור (בד"כ הוא עושה עבודה יפה מאוד) – אזי הוא ייתן לכם לפתור את הקונפליקטים.
ניתן לקרוא ל git revert –abort (ממש כמו merge) – אם הסתבכתם בהתרה שלהם.

revert ניתן גם לעשות מתוך ה UI של github ולפתוח ממנו מיד Pull Request – מה שמאוד נוח.

מקור: https://www.slideshare.net/durdn/ninja-git-save-your-master

Git ReReRe (ידוע גם כ Re3)

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

השם הלא-שגרתי הוא קיצור לא Reuse Recorded Resolution או "שימוש-חוזר בפתרונות מוקלטים".

שימוש נפוץ בפקודה היא בסיטואציות בהן לא עובדים ב Continuous Delivery אלא ב long feature branches.
למשל: אני עובד על פיצ\'ר במשך שבועיים-שלושה, ורוצה כל יום למשוך שינויים מה master.

לרוע המזל אני נתקל יום אחרי יום באותו ה merge conflict כי אני עובד על קטע קוד שעובר שוב ושוב שינויים גם על גבי ה master.

תסריט נפוץ אחר הוא כאשר עובדים עם rebase, ואז ישנם הרבה קונפליקטים דומים. למשל: אני עושה rebase ל branch עם 10 commits המכיל 4-5 קונפליקטים דומים על אותו האזור בדיוק. אני רוצה שגיט ילמד איך פתרתי את הקונפליקט הפעם הראשונה – ו"יסתדר לבד" בפעמים הבאות.

רהרהרה היא גם פקודה וגם קונפיגורציה. אנו אומרים לגיט להקליט את ה מיזוגים שאנו עושים (בעקבות rebase, merge, cherry-pick וכדומה) – בכדי שישמש בהם כ reference ל conflict resolution אוטומטי בעתיד.

הפעלת הקונפיגורציה הבסיסית נראית כך:
git config –global rerere.enabled 1

בכל פעולה בה יש מיזוג של קוד (merge, rebase, ועוד) והיה קונפליקט שנפתר על ידי ידנית – גיט ישמור "fingerprint".
ה fingerprint הוא ספציפי לקובץ מסוים, ומכיל את הקונפליקט: מה היה לפני, בשני ה branches שביניהם יש קונפליקט – ואיך נראה הקוד לאחר שפתרתי את הקונפליקט.

מקור: https://readyspace.com.hk/rerere

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

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

  • שגיט יבצע טעות, ויבצע merge שמתאים ל pattern שהוא זיהה – איך אינו נכון.
  • אני ביצעתי טעות ולימדתי את גיט/רהרהרה פתרון שגוי – שהוא יחזור עליו עוד מספר פעמים עד שאזהה את הטעות.

האם גיט רהרהרה שווה את הסיכון? התשובה כנראה מאוד אינדיבידואלית.
רהרהרה מכיל גם מנגנוני תיקון, כמו הפקודה git rerere forget path_spec – המאפשרים לי לתקן למידה לא-טובה, אם זה גם אומר שעלי להשקיע עבודה נוספת / המנגנון לא פועל בצורה אוטומטית לגמרי.

סיכום

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

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

שיהיה בהצלחה!

לינקים רלוונטיים

Git Flight Rules – דוגמאות והסברים על מגוון פעולות בגיט.
מדריך לשימוש בגיט רהרהרה

ניקיונות גיט (פוסט קצר לפסח)

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

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

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

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

 

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

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


קצת רקע אישי הקשור לגיט:

  • אני עובד עם Github (כמו הרוב), מה שאומר שאני לא נוהג לבצע פעולות ב remote מתוך command line. 
  • אני עובד עם IntelliJ, מה שאומר שאני שחלק מהפעולות אני מעדיף לעשות ב GUI. בעיקר: 
    • git log, git blame, git diff
    • ל intelliJ יש גם יכולת שימושית בשם Git Shelf – יכולת מקבילה ל git stash המאפשרת לאחסן כמות לא מוגבלת של "stashes" (ה IDE פשוט מאכסון את ה diffs בתיקיה נפרדת).
  • אני עובד עם Ohh my Zsh מה שמאפשר לי כמה קיצורים ו autocomplete שלא נמצאים ב shell הסטנדרטי. זה כנראה לא משפיע הרבה על מה שאכתוב כאן – אקפיד להשתמש בשם הפקודה המלאה ולא בקיצורים.

 

 

 

ניקוי קבצים

 
אתחיל בתסריט של ניקוי קבצים:
עבדתם על משהו, ואז מישהו ביקש מכם code review על branch אחר ו/או מישהו ניגש אליכם ושאל איך כדאי לעשות משהו – והראתם לו כמה שינויים בקוד.
עכשיו אתם רוצים לחזור לעבודה, אבל יש לכם כמה קבצים ושינויים שאתם לא זקוקים להם. 
 
ראיתי שלא פעם אנשים מסתבכים עם הסיטואציה הזו וצריכים "לחשוב".
 
הנה כמה פעולות שימושיות "להשתחרר" מהמצב:

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

 
Test-Delete untracked files: git clean -n
Delete untracked files (not staging): git clean -f

clean היא פקודה שמטרתה לנקות קבצים שאינם באינדקס (קרי: לא tracked). הגרסה n- מציגה רשימה של קבצים למחיקה, ו f- מוחקת אותם בפועל.


גרסה מעט אגרסיבית, אבל יעילה לנקות את כל השינויים המקומיים:

. git add ואז
git reset –hard

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

הסבר קצר: לפקודה git reset יש 2 צורות:

  • הסרה של קובץ / path מהאינדקס. ההופכי ל git add. הווריאציה הזו פועלת כאשר מצוין path (למשל: .)
  • איפוס ה branch הנוכחי (HEAD) למצב של commit מסוים – כאשר לא מצוין path.
קצת מבלבל שיש 2 פעולות עם אופי שונה תחת אותה הפקודה!

בדוגמה לעיל אנחנו משתמשים בצורה השנייה.

לכאורה ניתן לבצע את פעולת "revert" מתוך ה IDE אבל בכמה מקרים (למשל: קבצים חדשים) – יידרשו עוד כמה צעדים ידניים. גישת ה add-reset היא המהירה ביותר (עד כמה שידוע לי).

 

התחרטתי!

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


התחלתי merge אבל הוא הסתבך לי

git merge --abort

מבטל את ה merge וחוזר לשלב שהיה לפניו.
בד"כ הפקודה הבאה תהיה שוב <git merge <some branch – אבל הפעם אנו יודעים טוב יותר מה מצפה לנו, ונעשה אותו בצורה נכונה יותר.

עשיתי commit ל master או ל branch אחר שלא התכוונתי
git reset HEAD~1

הנה עוד שימוש בווריאציה השנייה של git reset – איפוס ה HEAD ל commit מסוים. 
על הפקודה הזו ניתן לחשוב כמו HEAD = HEAD-1:

  • החזר את ה branch ל commit אחד אחורה (ניתן להחליף את המספר 1 בכל מספר אחר).
  • כל השינויים שנכללו ב commits ש"בוטלו" – יועברו ל working directory.
מכאן אני יכול לבצע:
git checkout desired_branch
. git add
"…" git commit -m

והנה כל השינויים נמצאים כ commit ב branch שאליו התכוונתי.


נ.ב.
אם אתם רוצים לחזור הרבה commits אחורה, וקצת מתבלבלים בספירה – אולי עדיף לעשות את הפעולה מתוך ה IDE.


בגדול, כל פעם ש git log מעורב – יש יתרון לממשקי GUI ע"פ ה command line (לטעמי).
בהפעלת הפעולה, IntelliJ פותח 4 אופציות. האופציה שציינתי נקראת "mixed" – אבל אתם יכולים לבחור בכל אופציה שנשמעת לכם הגיונית ורצויה.



לנקות branches


branches באים הולכים: נוצרים, נושאים שינויי קוד, ואז ממורג'ג'ים ל branch אחר – ואז אין צורך בהם.
אם אתם מיישמים Continuous Integration – רוב הסיכויים שייצרו כמה branches חדשים כל יום.
מכירים את המצב שיש לכם 5 או יותר branches מקומיים שאתם לא בטוחים מה המצב שלהם?
אתם רוצים לעשות push ולמרגג' את מה שנותן, למחוק אותם – ואז להתחיל עבודה חדשה.

מסובך? 

העצה הכי טובה שיש לי היא למחוק בזוגות, branches מקומיים ומרוחקים – מיד לאחר ביצוע merge ב github.

git branch -d branch_name :

  1. ימחק branch שהוא fully merged, ל repository המרוחק, או המקומי
  2. ימחק (עם warning) את ה branch אם הוא push ל remote (ואין לו עדכונים).
  3. לא ימחק אם אין remote tracking ו/או יש שינויים מקומיים שלא עודכנו ל remote.
זוהי פקודה בטוחה יחסית. אם המקרה השני קרה – אפשר לחזור ממנו בעזרת git checkout ל branch שנמחק. הקוד נמצא ב remote.
את ה branch ב Github – מוחקים בעזרת ה UI.

אפשר לאטמט את התהליך המקומי ולהפעיל פקודה כמו:
git branch | grep -v "master" | xargs git branch -d
עוברים על כל ה branches המקומיים
grep -v יוציא ה master מהרשימה (אותו לא נרצה למחוק)
xargs מפעילה את הפקודה שאחריה עם פרמטר של מה שמגיע מה pipe (כלומר: stdin). כלומר: תנסה למחוק את כל ה branches, מלבד master, בצורה "בטוחה".
עדיין צריך לשים לב ל warnings ולהחזיר (בעזרת checkout) את branches שלא התכוונו למחוק.
יהיו עדיין branches שהפקודה לא תמחק. ירשם error בנוסח "the branch … is not fully merged". אלו:
  • branches עם commits מקומיים – אולי שכחנו לעדכן ל remote? אולי ויתרנו על הקוד הזה?
  • branches ללא remote tracking (מעולם לא עשינו push ו/או הפעלנו git fetch –prune או פקודה דומה).
אז מה עושים עם ה branches שלא נמחקו עם git branch -d?
כאן יש עוד עבודה ידנית. עדיין לא מצאתי דרך פשוטה יותר:
  • להיכנס לכל branch
  • לבדוק את ה log לראות אילו commits קיימים
  • אם זה לא מספיק – לעשות diff ולראות מה השינויים בקבצים. האם נרצה אותה?
  • לדחוף (git push) ו/או למחק (git branch -D branch_name) את ה branch.
כרגיל, כאשר יש עבודה עם git log – יש יתרון גדול לעשות את העבודה ב GUI.

שווה אולי להזכיר שיש גם פקודות כגון:


git branch –merged branch_name

המספקת לנו רשימה של branches שממורג'ג'ים ל branch_name.
כאשר רוב ה merges נעשים ב remote (למשל: GitHub) – היא פחות שימושית

ניתן גם לבדוק אלו branches שיש להם remote tracking (להלן r-) מורג'ג'ו ל master המרוחק:

git branch -r –merged origin/master

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

טיפ קטן למשתמשי Ohh my Zsh:
לעתים המלצות ה autocomplete ל gco (קרי git checkout) כוללות כל מיני branches ישנים.

git fetch -p שזה בעצם git fetch –prune

מנקה את ה metadata על branches שהם כבר לא remote tracked – מה שמנקה הצעות autocomplete לא רלוונטיות, אל הניקוי הזה גם גורם לפקודת git branch -d לכישלונות כי היא לא יודעת מה מצב ה branch המרוחק.

הטיפ: לקרוא ל git fetch -p רק במצב שכל ה branches המקומיים נקיים או שאין לכם בכלל branches מקומיים. זה יחסוך לכם כאבי ראש.

 
טיפ קטן לסיום:

איך רואים ב IntelliJ השוואה כמו זו של Github?

ה compare של Github נראה קצר יותר וממוקד יותר מ compare ב IDE?
אתם מוצאים את עצמכם עושים git push רק בכדי ליהנות מה compare של Github ולצפות בהתקדמות שלכם בכתיבת קוד?

אתם יכולים לבצע השוואה דומה גם ב IntelliJ.

אני מניח שאתם נוהגים לעשות compare מתוך התפריט בפינה התחתונה של ה IDE:


ואז compare ל branch המוביל:


עד כאן טוב ויפה, אבל כנראה שהפעולה הבאה שלכם היא לבחור ב tab של files (מסומן ב x אדום):


התוצאה תהיה לראות את כל השינויים מול ה master – גם כאלו שלא אתם הכנסתם, אלא אתם גוררים עם ה branch. זה לא יעיל.

במקום זה, תבחרו את רשימת ה commits שלכם (1), ואז תקבלו בצד ימין את רשימת הקבצים בהם נעשו שינויים כחלק מה commits שלכם (2).

הנה עוד כמה settings ב view של ה compare שידמו יותר ל Github:



זה אולי יחסוך לחסוך לכם כמה גישות לגיטהאב + יש יתרון ממשי ל compare בתוך ה IDE. 
למשל: היכולת לבצע שינויים במקום על מה שלא נראה לכם.


זהו לסיום.
מקווה שהפוסט יהיה שימושי לכמה אנשים.

שיהיה בהצלחה!

 

לינקים רלוונטיים

https://zippyzsquirrel.github.io/squirrel-u/1_SquirrelU/4_GitHub/1_introToGitAndGitHub/ – מדריך הכולל גם פרטים כיצד לעבוד עם כלי ה git של IntelliJ.

להבין גיט (Git)

Git הוא כלי לניהול גרסאות (Version Control System) הפופולרי למדי בימים אלו.
בניגוד ל (Subversion (SVN או Perforce שהם כלים בעלי ניהול מרכזי, ל Git יש ניהול מבוזר.משמעות אחת היא שבמקום שיהיה איש Operations מאחורי הקלעים שינהל את שרת ה SVN ויחסוך חשיבה למתכנתים ברוב הסוגיות הקשורות ל Source Control – כעת על המפתחים לדאוג לנושא זה בעצמם (לפחות במידה מסוימת).

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

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

בסה"כ Git (וכלי ניהול גרסאות מבוזרים בכלל) נחשב לתומך בפיתוח Agile המאפשר גמישות לנהל releases שתוכנם נקבע בשלב מאוחר, לעבוד בשיתוף עם מפתחים אחרים – מבלי להפריע לשאר המפתחים, לבצע שינויים משמעותיים בבסיס הקוד, ועוד. במקרים רבים ארגונים מאמצים Git בעיקר בכדי "לזרז את ה Agile".

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

גיט הוא פשוט להיט!
מקור: סקר המפתחים של אקליפס 2013

קצת הגדרות

כשלומדים גרמנית (ניסיתי), מהר מאוד מזהים את הדמיון שלה לאנגלית:
Ich bin Lior, משמעו: I am Lior.
Das ist gut, משמעו:  This is good.

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

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

ה Commit Graph בגיט, מורכב מה commits השונים (ריבועים בציור למעלה) – כל אחד כולל snapshot של כל הפרויקט ברגע מסוים, וחצים – המייצגים קשר בין commits. למשל: B הוא בן של A: מישהו עבד על A, שינה אותו ואז ביצע commit ל B.

Branch בגיט מוגדר כשרשרת כל parent commits של ה tip (קצה) של ה Branch. כלומר:

  • ה release Branch מכיל את A, B, C, E, G, H.
  • ה master Branch מכיל את A, B, C, D, F, I, J, K. כן, commits יכולים להיות חברים ביותר מ Branch אחד.
  • ה topic Branch מכיל את A, B, F, I, L, M.

בד"כ נעבוד על ה Tip של ה Branch ולא על commits שנמצאים בהיסטוריה שלו.
ה Branch הראשון שנוצר ב Git Repository נקרא master, אולם הוא לא שונה במאומה מכל מכל branch אחר שתיצרו מאוחר יותר – זה רק שם ברירת-מחדל.

גיט מקומי וגיט מרוחק

יש טעם לעבוד על גיט מקומי-בלבד, נאמר: כאשר שאר הצוות שלכם עובד על SVN ואתם רוצים Repository שלכם שישמור את השינויים שעשיתם כל 5 עד 30 דקות של עבודה.
אם אתם לא רגילים לגיט, לבצע commit כל כמה דקות נשמע מוזר – אבל זה עניין שמתרגלים אליו :).

כמובן, שהייעוד העיקרי של גיט הוא עבודה בצוות. גיט מאפשר לשתף עבודה בין Repositories:

פעולת ה "clone" תשכפל את ה Repository המרוחק ותייצר Repository מקומי זהה על המחשב שלכם. זהה = מכיל את כל הקבצים, כל ה Branches וכל ההיסטוריה מרגע יצירת הפרויקט.

פעולת Push דוחפת (commit(s מה Repository שלכם למרוחק ו Pull מביאה (commit(s מה Repository המרוחק אליכם.

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

לגיט יש עקומת למידה לא-קלה

שכבת ה Persistence של גיט

שכבת אכסון הנתונים של גיט ידועה בשם The Object Store או Object Database. את התוכן שלה ניתן למצוא בפועל תחת הספרייה git/objects.(בעקבות פעולת git clone נוצרת תיקייה נסתרת בשם "git." ב path בו התבצעה הפעולה).
ה Object Store מאכסן 4 סוגים של אובייקטים. הנה הקשרים הלוגיים ביניהם:

האובייקט הבולט ביותר הוא כמובן אובייקט ה Commit, המשמש כ Aggregator לתת העץ של כל הספריות והקבצים באותו ה Commit.
ה Commit מצביע לעץ (Tree) אחד, שהוא ה Root Directory של תתי-הספריות (עצים) וקבצים (BLOBs) שהם חלק מה Commit.

הערת צד: בוודאי מטרידה אתכם השאלה כיצד ענף (Branch) יכול להכיל משהו (Commit) שמצדו מכיל הרבה עצים (Trees) – הרי "ענף" הוא חלק מה"עץ". יש לכך 2 תשובות:

  1. Branch הוא ענף של ה "Commit Graph"', בעוד עץ (Tree) הוא ספרייה במערכת הקבצים של ה Commit הנוכחי.
  2. זו אכן טרמינולוגיה מבלבלת – היה עדיף לקרוא ל Tree פשוט Directory או Folder, על אף הקונוטציה הלא-רצויה למערכת ההפעלה חלונות (רחמנא ליצלן).

כל BLOB ב Object Store מייצג קובץ, יהיה זה קובץ קוד (למשל java.) או קובץ נתונים אחר (למשל תמונה בפורמט png.). תוכן הקובץ עובר פונקציית Hash בשם SHA-1 המייצרת מספר בן 160bit המתאר את תוכן הקובץ. פונקציית SHA-1 היא פונקציית hash בעלת פיזור אחיד למדי (מקורה בעולם האבטחה) – ואנו מניחים שהיא מזהה תוכן של קובץ באופן ייחודי. ב UI של גיט נראה את ה hash בייצוג הקסדצימלי, למשל:

12cfb1862b23356523d88127fa5d5aeb333950

לעתים נראה קיצור של ה hash בצורת 7 הספרות הראשונות שלו – לרוב זהו ייצוג ייחודי דיו בכדי לזהות אובייקט בצורה אמינה (וגם: קל הרבה יותר להקלדה).
גיט מזהה כל קובץ ע"פ התוכן שלו (לא ע"פ שם הקובץ, סיומת או תאריך) – כלומר, ע"פ ה hash שלו. זאת אומרת ש Tree בעצם מכיל רשימת hashes של קבצים, המתארת את הקבצים בספרייה, ומייצא בעצמו hash של ערכים אלו. ה Commit מקבל ה hash המחושב מה hash של תיקיית ה root של הפרויקט + כמה שדות.

אם הקובץ(כלומר התוכן, בפנים) לא השתנה, אזי ה hash שלו יישאר זהה, וגיט לא תאכסן עותק חדש שלו ב Object Store. מספיק שביט אחד בקובץ השתנה (למשל: ריווחים בקוד) בכדי שעבור גיט זה יהיה אובייקט BLOB חדש לגמרי, בעל hash שונה לחלוטין שמאוחסן בשלמותו (ולא deltas).

הערת צד: שמירת עותקים שלמים של קבצים היא החלטת Design של גיט הכוללת Tradeoff בין נפח אכסון ליעילות. כלי ניהול גרסאות רבים אחרים בחרו את האופציה השנייה. שווה לציין שהיתירות (redundancy) במקרה זה מגבירה את האמינות. אם היינו מחזיקים רק diffs, ו diff מסוים בשרשרת היה נפגם – היינו מאבדים את כל המשך השרשרת מאותו הרגע. באופן לא-מפתיע למדי, ניתן לראות דמיון רב בין החלטות התכנון של גיט ל פילוסופיית התכנון של יוניקס.

באופן דומה, העצים הם hash של כל ה hashes של העצים / קבצים שהם מכילים. אם כל הקבצים בתת-עץ לא השתנו, אזי אובייקט ה Tree שמייצג אותם יישאר עם אותו ה hash וכן הלאה.
זיהוי אובייקטים ע"פ hash מאפשר לגיט לבצע השוואות מהירות בין אובייקטים ובין תתי-עצים של אובייקטים: פשוט יש להשוות בין שני hashes.

כמה properties חשובים על אובייקט ה Commit:


commit message

הערת טקסט המסבירה מה כולל ה commit.
parents
מצביעים ל commits אחרים, קשר המגדיר את ה Commit Graph.
  • אם ל commit יש יותר מהורה אחד (ניתן 3 או יותר) – אזי זהו merge בין ענפים (branches).
  • אם ל commit אין parents, אזי ה commit הזה הוא ה Root Commit (ראשית ה Repository) או orphan commit – מין אופציה מתקדמת בגיט לייצר branch חדש עם קוד שלא קשור לענפים הקיימים ב Repository.


committer vs. author

לרוב ה committer וה author יהיה אותו אדם, מלבד כמה מקרים בהם מישהו מבצע commit מחודש לקוד שמישהו אחר עשה לו commit בעבר.
דוגמה אחת לכך היא cherry-pick, היכולת לקחת שינויים של commit ולהעתיק רק אותם (כלומר: את השינויים) ל branch אחר. יכולת זו היא רבת-עוצמה כאשר רוצים להעביר בין ענפים תיקוני-באגים. יכולות אחרות הן filter-branch ("שכתוב היסטוריה") ו rebase (חיתוך ענף, והדבקה שלו במקום אחר). במקרים אלו יהיה ניתן להבחין בין האדם שביצע את הפעולה (committer) לבין ה committer של הקוד המקורי (author).

ה Index

תהליך העדכון קבצים ל Git Repository מתבצע בשני שלבים:

  1. עדכון ה Index (נקרא גם "Staging Area" או "Directory Cache") בעדכון.
  2. ביצוע העדכונים מתוך ה Index ל Repository.
תהליך דו-שלבי זה נוצר בכדי לאפשר למתכנת לבצע Review על השינויים שלו ולשלוט מה בדיוק נכנס ל Repository.

האינדקס יכול להיות מבלבל לפעמים: רבים מדמיינים אותו כ snapshot הנוכחי של הפרויקט – אבל זה לא המצב. ה snapshot הנוכחי של הפרויקט לא מנוהל ע"י גיט – גיט סורק את תיקיות הפרויקט ברגע שהוא צריך (למשל: בעת הפעלת פקודת git status) כדי לדעת מה השתנה.

האינדקס הוא מבנה נתונים נוסף של גיט, שמנותק ממצב הקבצים במערכת הקבצים (נקרא בעולם הגיט "Working tree)" ו/או מה Commit האחרון. הוא פשוט מילון של pathnames ו hashes של אובייקטים ב Object Store, כלומר .

כאשר אנו מבצעים פעולת git status (או git status –short), או רואים השוואה בין התיקייה המקומית והאינדקס.
בשלב הראשון גיט יציג לנו קבצים שהשתנו במערכת הקבצים, אבל לא מנוהלים ("tracking") ע"י האינדקס:

הערה: git stat הוא alias שיצרתי לפקודה git status –short. לא נראה לי הגיוני שהגרסה הקצרה של הפקודה, ארוכה יותר להקלדה מהגרסה הארוכה של הפקודה. לכן יצרתי ה alias באופן הבא:

git config –global alias.stat "status –short"

ברגע שנבצע פעולת git add לקבצים – הם ייווספו לאינדקס:

  • ה pathname יזכור איזה קובץ במעקב.
  • תוכן הקובץ, כפי שהוא באותו הרגע, ישמר כ BLOB ב Object Store וה hash של אותו BLOB יישמר באינדקס.
הנה:

כעת אני רואה שהקובץ התווסף לאינדקס (עמודה ירוקה, A הוא קיצור של Add).
כדי לראות אלו קבצים נמצאים באינדקס – פשוט הקלידו git ls-files.

כמה דקות לאחר מכן אני מבצע git stat ומקבל את המצב הבא:

מה זה המצב הדו-פרצופי הזה – מישהו יודע?
הקובץ השתנה או התווסף? מדוע אדום וירוק יחדיו?

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

אם נבצע git commit בשלב זה, העותק שהוספנו קודם לכן לאינדקס (ונשמר ב Object Store) יכנס ל Repository, בעוד השינויים שביצעו לאחר מכן – יישארו במערכת הקבצים.
לאחר ה commit, פעולת Git Stat תראה רק את השינוי שבדיסק (הקובץ יוותר במעקב):

עוד טיפ קטן:

  • פעולת <git rm <filename מסירה קובץ מהאינדקס (כלומר – הוא לא יהיה במעקב) ומהדיסק: זו הדרך להעיף קובץ שאינכם מעוניינים בו מהפרויקט. במילים אחרות: הוסף קובץ להסרה – לאינדקס. בכלל ש rm נשמע ההיפך מ add – זו דרך מצוינת להתבלבל.
  • לצורך הגנה, אם ניסיתם לבצע git rm על קובץ שנעשו בו שינויים – גיט יסרב ויציע לכם להוסיף cached– .לא פעם הקשבתי לעצתו בשמחה – ומחקתי לעצמי כמה שינויים.
  • פעולת <git reset <filename רק מסירה את הקובץ מהאינדקס – זו בעצם הפעולה ההופכית ל git add.
פתרון אפשרי לבלבול, יכול להיות להגדיר alias בשם remove, שיהיה ההופכי ל add:
git config –global alias.remove "reset"

לי זה עזר.

אם אתם רוצים לבצע היפוך ("revert" בשפת perforce) לשינוי בקובץ, עליכם פשוט לבצע <git checkout <filename. כפי שהזהרתי בהקדמה על השפה הגרמנית – פעולת git revert עושה משהו לגמרי אחר (והרסני).

ענפים

הפוסט מתארך, ולכן אסיים בנושא אחרון חשוב לפוסט זה: Branches.

מהו Branch?
כבר אמרנו ש Branch מוגדר כאוסף ה commits שניתן להגיע אליהם מה tip (הקצה) של ה Branch.
הגדרה נוספת ומשלימה הוא ש Branch הוא מצביע ל commit, ומנקודה זו מוגדר ה branch.

גיט מחזיק במבנה נתונים נוסף: refs (קיצור של References, נקרא גם Pointer לפעמים). יש בגיט 2 סוגי מצביעים:

  • מצביע ישיר, המצביע לאובייקט ב Object Store (לרוב זה יהיה commit או tag).
  • מצביע סימבולי (symbolic ref) המצביע למצביע אחר. משהו כמו symbolic link במערכת ההפעלה.
גיט משתמש במצביעים כדי לתת שמות / לסמן דברים, ובמקרה שלנו – branches. כל אחד מה branches הוא פשוט מצביע שכזה.
לעתים יווצר מצב בו ה Repository המרוחק ("origin") יכיל branches שלא קיימים אצלנו. פקודת git pull מייבאת את ה commits – אך לא את המצביעים. פקודת git fetch מייבאת את המצביעים מה Repository המרוחק, מה שיראה אצלנו כ branches ו tags.
מצביע מיוחד הוא מצביע ה HEAD המסמן את המיקום הנוכחי שלנו ב Commit Graph. כאשר אנו מחליפים branch לעבודה ע"י פקודת <git checkout <branch name אנו מחליפים את HEAD להצביע על commit ו/או branch אחר.
ייתכן מצב בו HEAD לא יצביע על tip של branch אלא על commit אחר בגרף. מצב זה יתרחש, למשל, בעקבות ביצוע פקודת <git checkout <commit's hash. לאחר פעולה זו גיט יספק הזהרה שאתם במצב של detached HEAD – כלומר HEAD לא מצביע על ה tip commit – ולכן אינו מצביע על branch.
מצב זה, למרות שנראה מוזר, הוא תקין וניתן לבצע בו commits – מה שיוביל אותנו למצב הבא:

commit E הוא לא חלק מה branch. ניתן לפתור מצב זה ע"י יצירת branch חדש מהנקודה הנוכחית ע"י
פקודת <git checkout -b <new branch name ואולי אח"כ לבצע merge בין ה branch החדש ל dev. אם לאחר זמן ממושך לא "תטפלו" ב commits שאינם שייכים ל branch כלשהו – גיט ינקה אותם בתהליך "ניקוי הזבל" שלו.

סיכום

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

שיהיה בהצלחה!

—–

קישורים רלוונטיים

GitGuys – מקור טוב להבנת המבנה של גיט:
http://www.gitguys.com/topics/

Git cheat sheet – רפרנס ויזואלי של הפקודות הנפוצות וההשפעות שלהן:
http://www.ndpsoftware.com/git-cheatsheet.html#loc=remote_repo;

UnGit – כלי ויזואלי שעוזר ללמוד גיט:
https://github.com/FredrikNoren/ungit

Learn Git Branching – אתר טוב שמציע "משחק" שיסייע לכם לעקוב אחר branching מתקדם בגיט – נחמד מאוד.
http://pcottle.github.io/learnGitBranching/

כלי UI טוב מהרגיל לגיט (שומר על השפה של גיט) – SourceTree:

"A shout out to developers of SourceTree – a nice GUI for git and hg. Useful even for a command-line fan like me. " — Martin Fowler

לינק מעניין: לינוס מציג את גיט ב 2007.

אוסף נחמד של מדריכי וידאו לגיט: http://training.github.com/resources/videos/