Git - חלק 4

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

זוכרים שבחלק הקודם דיברנו על merge commit ואמרנו שהוא לא הכי טוב לפרויקט שיש בהם הרבה שיתוף פעולה (יש מיזוג מענפים מקומיים, מענפים מרוחקים וזה די מטנף את הלוג). הבטחתי שנדבר על דבר הנקרא rebase, והגיע הזמן לקיים את ההבטחה.

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

אז במקום שבוב יעשה ‘git pull’ ואז ‘git push’, נעשה fetch ואז rebase. אז אם אתם זוכרים, כשאנו כותבים ‘git fetch’ המערכת ניגשת ל GitHub ומושכת את השינויים (אך לא ממזגת אותם), ואחרי השלב הזה התמונה נראית בדיוק כמו שהראתי ב”שלב 1” בחלק 3 בתרחיש הראשון (לנוחות שמתי אותה בצד שמאל לצד הפקודה).

לאחר מכן נכתוב את הפקודה ‘git rebase’. והיא תבצע 3 דברים:

המערכת תעביר את כל השינויים לענף master, אז זה יקח את ה commit של בוב ויעביר אותו לאזור זמני לעת עתה.
המערכת תריץ את כל ה commit שנמצאים ב origin/master. מה שאומר שכעת ה commit שהיה ב origin/master הועבר לענף master.
ולבסוף, הוא יריץ את כל ה commit שבאזור הזמני (אם עשינו כמה, יהיו כמה), על גבי mater, אחת אחרי השני, ככה שה commit שלנו יהיה אחרון.
כמו ששמתם לב אין כאן merge commit, זה רק commit אחד אחרי השני, אחרי השלישי.

עכשיו, בואו נדבר על rebase בענפים מקומיים. נניח ויש לנו פרויקט בו 2 ענפים, master ו admin. על כל אחד מהם יש שתי commit וכעת יש למזג ביניהם, אך במקום למזג בדרך הישנה עם merge, נבצע rebase ל commit שב admin. אז קודם כל נעבור לענף admin עם הפקודה ‘git checkout admin’ ונריץ ‘git rebase master’. זה יגרום קודם כל להרצה של ה commit ב master, ורק לאחר מכן ב admin. כעת ב master ימשיכו להיות השתיים ה commit שהיו, אך ב admin יהיו ארבעה commit, השניים הראשונים הם ה commit מ master, ואחריהם עוד שתי commit, אלה שהיו ב admin עוד קודם לכן.

כעת, הצעד הבא הוא לחזור ל master בעזרת הפקודה ‘git checkout master’ ולבצע מיזוג מ admin בחזרה ל master עם הפקודה ‘git merge admin’. כל מה שהמיזוג הזה יעשה הוא ה fast forward שדיברנו עליו בעבר (מיזוג פשוט), וסיימנו.

 

מה אם ישנם קונפליקטים?

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

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

אז בשלב הראשון, בוב מבצע ‘git fetch’ (המערכת יוצרת ענף חדש בשם origin/master עם ה commit מ GitHub, אלה של אליס), ובשלב השני הוא מבצע ‘git rebase’ (המערכת תיקח את ה commit של בוב, אלה מענף master, ותעביר אותם לאזור זמני, לאחר מכן המערכת תריץ את כל ה commit שבענף origin/master, ז”א שכעת ה commit של אליס יהיו בענף הראשי של בוב - master, ולבסוף המערכת תריץ את כל ה commit באזור הזמני, כדי להחזיר אותם לענף master). השלב האחרון ב rebase, בו המערכת מריצה את ה commit מאזור הזמני מוביל לקונפליקט.

בצד שמאל ישנה תמונת מצב של מה שנראה כשהפקודה rebase תיתקל בקונפליקט. ניתן לראות שהמערכת מבקשת מאיתנו לתקן את הקונפליקט בקובץ readme.txt, ולאחר שנפתור את הקונפליקט להריץ ‘git rebase -continue’ על מנת להמשיך. אך אם אנחנו רוצים לבטל לדלג על הפאצ’ נכתוב ‘git rebase -skip’ ואם אנחנו רוצים לבטל אותו, כלומר לגלול אחורה ולהעמיד פנים שלא עשינו rebase מעולם, נכתוב ‘git reabse -abort’.

אם נכתוב כעת ‘git status’ נראה שאנחנו לא על אף ענף ספציפי, אנחנו באמצע ביצוע rebase. אז לאחר שנערוך את ה readme, נכתוב ‘git add README.txt’ ולאחר מכן ‘git rebase -continue’ והמערכת תמשיך כרגיל במיזוג ה commitים. כעת, אם נקליד ‘git log’ ונציץ בתיאור ה commit, זה יראה כמו התמונה משמאל, ושוב, ניתן לראות שאין כל הודעת merge conflict, אלא כל ה commits אחד אחרי השני, מרגיש נקי יותר, לא?

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

 

שאלות ותשובות

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

שאלות:

  1. ביצעת כמה commit לענף בשם feature, אך ביצעתם עוד commit לאיזה באג בענף master. זה יהפוך את המיזוג למבולגן, עברו לענף feature כדי שתוכלו לבצע rebase של ה commit בו לענף master.
  2. אוקי אתם בענף feature. המטרה שלנו היא להיות מסוגלים למזג את הענף חזרה ל master ללא קונפליקטים או merge commit. בצעו rebase לענף הנוכחי (feature) ל master.
  3. לאחר שביצעתם rebase, הענף feature צריך להתמזג עם master בצורה נקייה. החליפו ענף חזרה ל master.
  4. אנחנו ב master ואנחנו יודעים ש feature יתמזג באופן חלק, מזגו אותו עם feature.
  5. שותף שלכם לצוות ביצע עבודה על ענף master ב origin (ה repo המרוחק). משכו את השינויים בלי למזג אותם, כדי שנוכל לבצע עליהם עבודה.
  6. עכשיו כשהמאגר (repo) המקומי שלכם מכיר את השינויים האחרונים ב origin/master, העבירו את ה commitים מ master לאחרי ה commitים מ origin/master.
  7. שותף שלכם לעבודה שוב דחף קוד לפניכם, משכו שוב את הקוד.
  8. עכשיו שוב, הריצו rebase כדי להעביר את ה commit שלכם אחרי אלה שמשכתם בסעיף 7.
  9. אופס! נראה שיש לכם קונפליקט, שניכם ביצעתם שינויים באותו הקובץ (index). להלן תמונה של תוכן הקובץ (השאירו רק את הקישור עם Cats ו Dog).
  10. כעת, סמנו למערכת (ל git) שהקונפליקט ב index.html טופל.
  11. עכשיו כשכל הקונפליקטים נפתרו, והקבצים שבהם היו קונפליקטים נוספו לאזור ההיערכות, המשיכו בתהליך ה rebase הנוכחי.

תשובות:

  1. הפקודה: git checkout feature.
  2. הפקודה: git rebase master.
  3. הפקודה: git checkout master.
  4. הפקודה: git merge feature.
  5. הפקודה: git fetch.
  6. הפקודה: git rebase origin/master.
  7. הפקודה: git fetch.
  8. הפקודה: git rebase origin/master.
  9. התמונה של תוכן הקובץ (index.html) אחרי העריכה.
  10. הפקודה: git add index.html.
  11. הפקודה: git rebase -continue.

 

סיכום ביניים

כמו שהבטחתי בחלק הקודם עברנו על פקודת ה rebase. ראינו כיצד היא עובדת, ויישמנו אותה בשני תרחישים שונים. הראשון פשוט, ללא קונפליקטים, והשני עם קונפליקטים (שינויים באותו הקובץ). אני מקווה שהצלחתי להבהיר ש rebase הוא לא תמיד הפתרון הטוב ביותר, הסברתי למה, ומתי merge commit כנראה יהיה טוב יותר.

עוד קצת על Log

בחלקים הקודמים השתמשנו לא מעט ב ‘git log’ כדי לראות את הרשימה של ה commit האחרונים שלנו. ה commit האלו כוללים לרוב את ה SHA, לאחר מכן את שם המחבר (והאימייל שלו), התאריך והתיאור שכתב לאותו ה commit. הפלט מפקודת ה log אינו הדבר הכי קל לקרוא לכן נעבור על כמה דרכים על מנת לשפר אותו.
הדבר הראשון שנעשה הוא להגדיר את הצבעים של ה UI ל “true” בעזרת הפקודה ‘git config -global color.ui true’, וזה יגרום ל SHA להיות מודגש בצבע (כל פעם). בנוסף, אתם יכולים לסדר את הפורמט שה log ידפס, אחרת. אם נכתוב
‘git log -pretty=oneline’ (במקום לכתוב ‘git log’) הפלט יציג commit אחד בכל שורה, שיכיל את ה SHA ואת התיאור של ה commit (בלי שם היוצר שלו, האיימיל והתאריך).

אפשר אפילו לדייק עוד יותר ולפרט את פורמט הפלט בדיוק כמו שאנחנו רוצים שהוא יצא עם הפקודה ‘”-.”:git log -pretty=format’ (במקום הנקודות יש להכניס את המציין מקום של כל אחד לפי הסדר שנרצה. כל המצייני מקום נמצאים בתמונה משמאל). ניקח לדוגמה את הפקודה ‘“[git log -pretty=format:”%h %ad- %s [%an’ כשנכתוב אותה, הפלט יהיה בפורמט שמכיל קודם את ה SHA, לאחר מכן את התאריך שבו נוצר ה commit, מקף ואז תיאור של ה commit, ולבסוף את שם היוצר של ה commit בסוגריים מרובעים.

אם אתם רוצים לראות מה כל commit שינה בפלט של הלוג, ניתן להשתמש בפקודה ‘git log -oneline -p’ כשה p מסמנת את המילה Patch, וזה יראה לכם איזה שורות נמחקו ואיזה נוספו לקבצים ששונו בכל commit. הפקודה ‘git log -oneline -stat’ תראה לכם סיכום של כמה הוספות ומחיקות בוצעו בכל קובץ בכל commit. אך לא רק commit ניתן לראות בלוג, גם ענפים. בעזרת הפקודה ‘git log -oneline -graph’ נוכל לראות ייצוג גרפי של הענפים ושל ה commit שהיה בכל ענף (ה commit וסומן ע”י *).

לפעמים בפרויקטים גדולים וארוכים, לא יהיה שימושי להדפיס את כל הלוג כל פעם, אך ניתן לבחור מראש תאריכים כך שהפלט מהלוג יציג את ה commit שהיו אך ורק בין התאריכים הללו. לדוגמה ‘git log -until=1.minute.ago’ יציג את כל ה commit שהיו מההתחלה עד לפני דקה. בניגוד אליו ‘git log -since 1.day.ago’ יציג את כל ה commit שהיו לפניו יום אחד ועד הרגע (באותה מידה ניתן לכתוב ‘git log -since=1.hour.ago’ להציג את כל ה commit מהשעה האחרונה (ניתן כמובן להכניס כמות שעות שונה)) או אפילו לתחום את הזמן עם ‘git log -since=1.month.ago -until=2.weeks.ago’ ואפילו לתת תאריכים ספציפיים ‘got log -since=2000-01-01 -until=2011-10-21’.

כמו שראינו בעבר, ‘git diff’ יכול להיות מאוד שימושי לראות מה השתנה בקובץ כלשהו, ניתן לראות את השורות שהוסרו מול השורות שנמחקו, ו ‘git diff HEAD’ עושה בדיוק אותו הדבר כמו ‘git diff’ רק שהוא משווה את השינויים מהמצב הנוכחי ל commit האחרון (כי כתוב HEAD), אך אם נכתוב “^HEAD” נשווה את השינויים מהמצב הנוכחי ל commit שלפני האחרון. ונוכל לכתוב גם ‘^^git diff HEAD’ בשביל ה commit השני לפני האחרון, או כך ‘5~git diff HEAD’ כדי להגיע ללפני 5 commit. או בשביל להשוות שתי commit שונים (בניגוד לcommit מול המצב הנוכחי) נכתוב ‘git diff HEAD^..HEAD’ (כמובן שניתן להחליף את ה HEAD בכל דרך אחרת שתיארנו למעלה כדי להגיע ל commit אחר). בנוסף, זוכרים את ה SHA של כל commit? ניתן להשתמש גם בהם כדי להשוות בין שתי commit שונים, לדוגמה ‘git diff 4fb063f..f5a6ff9’. ואפילו נוכל להשתמש בענפים, לדוגמה ‘git diff master bird’ (השוואה של ענף master מול ענף בשם bird). ובדיוק כמו ב log נוכל להשוות בעזרת זמנים גם ב diff, לדוגמה ‘git diff -since=1.week.ago -until=1.minute.ago’.

לפעמים, במיוחד בפרויקטים גדולים וארוכים, ניתקל בשינוי שמישהו או אנחנו בעצמנו עשינו ואנחנו לא מצליחים להבין אותו. כלי שימושי מאוד במקרים כאלו הוא ‘git blame’. לדוגמה בקובץ index.html נכתוב ‘git blame index.html -date short’. ונקבל כפלט בדיוק את ה SHA של ה commitים שבוצעו בקובץ, מי ביצע אותם, באיזה תאריך, איזה שורות, ואת השינוי עצמו. בתמונה משמאל ניתן לראות בדיוק כיצד יראה הפלט.

 

מחיקת קובץ

נושא מהותי שלא דיברנו עליו עד כה הוא הסרה של קבצים מה repo. הסרה מתבצעת על ידי הפקודה ‘git rm README.txt’ (ניתן להסיר כל קובץ ותיקיות במקום README.txt). הפקודה תסיר את הקובץ ותוסיף את השינוי הזה (ההסרה) ל ‘git status’ כדי שזה לאחר מכן יתבצע ב commit, לכן לאחר מכן נכתוב ‘“git commit -m “Remove readme file’ והקובץ יעלם.

אך אם אנחנו כבר עוקבים אחרי הקובץ, אנחנו לא רק רוצים להסיר אותו ממערכת הקבצים שלנו, אלא גם להגיד ל git להפסיק לעקוב אחרי הקובץ הזה וכדי לעשות זאת נכתוב ‘git rm -cached index.html’ שבעצם יספיק לבדוק שינויים בקובץ, יפסיק לעקוב אחריו. אם עכשיו נכתוב ‘git status’ נראה ש git אומרת לנו שהקובץ נמחק, בדיוק כמו שהיא אמרה לנו כשבאמת מחקנו אותו, רק שהפעם הוא לא נמחק ממערכת הקבצים שלנו.

 

קונפיגורציות מתקדמות אחרות

לפעמים מגיע זמן בפרויקט בו נצטרך לעבוד על תיקייה ספציפית אך לא נרצה לכלול אותה ב repo ולשתף אותה עם שאר חברי הצוות שלך. אז ניצור תיקייה חדשה בשם experiments. כדי להכיל אותה עדיין בתיקייה של הפרויקט אך ש git לא תקרא אותה ותציק לנו להוסיף אותה לאזור ההיערכות כשנכתוב ‘git status’. נשים את תיקיית experiments בתיקייה exclude שנמצאת בנתיב git/info (זוכרים את התיקיה הבלתי ניראית הזאת? git.).

בנוסף, ניתן להגדיר ל git (בדיוק כמו שהגדרנו בהתחלה את שם המשתמש והאימייל שלנו) איך למזג קונפליטקים על ידי הפקודה ‘git config -global merge.tool meld’ (השתמשתי בדוגמה הזו בעורך בשם meld). ניתן גם להגדיר אימייל ספציפי ל repo עם הפקודה ‘git config user.email bla@gmail.com’ (כמובן הכניסו את האימייל האמיתי במקום bla@gmail.com).

למעלה ראינו כיצד ניתן להדפיס את הלוג בפורמט שונה, לבחירתנו, אך מה אם אנחנו רוצים באופן קבוע להדפיס בפורמט הזה, לכתוב כל פעם את הפורמט לא נשמע הגיוני, אך ניתן לשמור את הפורמט בכיוני ואז לכתוב את הכיוני, לדוגמה
‘“git config -global alias.lol “log -pretty=format:’%h %s [%an]’ -graph’ וכשנרצה להדפיס את הלוג בפורמט הנ”ל נכתוב ‘git lol’.

 

שאלות ותשובות

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

שאלות:

  1. כל כתובת האיימיל וה SHA האלו הופכים את הלוג למסובך, קשה להסתכל על זהו ולהבין מההודעות של ה commit דברים, נסו לראות את הלוג עם commit אחד בכל שורה.
  2. הבוס שלכם קורא לכם ורוצה לדעת איך התקדמתם מאז הישיבה האחרונה, כדי שיהיה לכם נוח הדפיסו תקציר בשורת הפקודה של השינויים הנוכחיים בקובץ הנוכחי.
  3. סיים לכתוב חלק בפרויקט שנמצא על ענף אחר בשם bla, אתם צריכים לכתוב לחברי הצוות מה השינויים ואתם לא רוצים לפספס דבר. השוו את ענף ה master מול ענף bla שבו נמצא הפרויקט כדי לראות מה השוני ביניהם.
  4. חברכם לצוות ביצע commit ואחריו גם אתם ביצעתם, הפרויקט כעת לא עולה, בדקו את השינויים שביצע חברכם ב commit שלפניכם.
  5. אוקי, ראיתם את השינויים אבל אתם עדיין לא מבינים מה חברכם לצוות ניסה להשיג. הציגו את השינויים ביחד עם הלוג כדי לנסות לראות מה קורה.
  6. טוב, אין לנו מושג מה הולך כאן ומה הוא ניסה לעשות, בואו נבדוק מי עשה את השינוי הזה בקובץ (index.html).
  7. הגדירו את האימייל admin@example.com ל repo.
  8. ‘git commit’ זאת לא פקודה מגניבה מספיק, צרו כינוי לפקודה בשם legendaryCommand שתבצע commit בכל פעם שתכתבו אותה.

תשובות:

  1. הפקודה: git log -pretty=oneline.
  2. הפקודה: git diff.
  3. הפקודה: git diff master bla.
  4. הפקודה: ^^git diff HEAD' או 'git diff HEAD~2.
  5. הפקודה: git log -p בפוסט כתבתי את הפקודה ביחד עם הצגתה בשורה אחת, אך לא חייב ופה הסרתי את “oneline”.
  6. הפקודה: git blame index.html.
  7. הפקודה: git config user.email admin@example.com.
  8. הפקודה: "git config -global alias.legendaryCommand "commit.

 

סיכום

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