• בלוג
  • עמוד 203
  • כל מה שרציתם לדעת על Git Rebase ולא ידעתם את מי לשאול

כל מה שרציתם לדעת על Git Rebase ולא ידעתם את מי לשאול

13/03/2019
git

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

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

1. מה זה diff? ומה זה patch?

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

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

$ git log --oneline
e3a19d8 (HEAD -> master) put code in function
5eedde1 initial commit

אני יכול להפעיל diff כדי לראות את ההבדלים בין שני הקומיטים:

$ git diff 5eedde1 e3a19d8
diff --git a/one.rb b/one.rb
index 1dad1b0..a21882d 100644
--- a/one.rb
+++ b/one.rb
@@ -1 +1,5 @@
-puts "one"
+def one
+  return "one"
+end
+
+puts one

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

נכתוב את ההבדלים לקובץ בשם fix.patch:

$ git diff 5eedde1 e3a19d8 > fix.patch

נחזור לגירסת הפרויקט של "לפני השינויים":

$ git reset --hard HEAD~

נוודא שהקובץ one.rb מופיע בגירסא המקורית שלו:

$ cat one.rb 
puts "one"

ונריץ את השינויים עם patch:

$ patch < fix.patch 
$ cat one.rb 
def one
  return "one"
end

puts one

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

$ git commit -a -m 'moved to function v2'
$ git log --oneline
ca6b800 (HEAD -> master) moved to function v2
5eedde1 initial commit

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

$ git diff e3a19d8 ca6b800

2. אוקיי אז מה זה ריבייס?

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

  1. אנחנו רוצים לקחת את כל הקומיטים בענף A שלא נמצאים בענף B.

  2. לכל אחד מהקומיטים האלה (החל מהראשון ולפי הסדר):

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

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

אני מזכיר שהפרויקט שלי נראה ככה:

$ git log --oneline
ca6b800 (HEAD -> master) moved to function v2
5eedde1 initial commit

אני יוצר ממנו ענף חדש לצורך פיתוח פיצ'ר מדליק וממשיך את פיתוח הפרויקט עד שמגיע ללוג הבא:

$ git log --oneline --graph master
* 9dae7c6 (HEAD -> master) moved two code to function
* ca6b800 moved to function v2
* 5eedde1 initial commit

$ git log --oneline --graph fr1
* 52361f5 (fr1) FIXED with require_relative
* 56e6f34 trying to use my lib
* 8938716 removed function def
* 69a9120 add lib
* ca6b800 moved to function v2
* 5eedde1 initial commit

אנחנו רואים ששני הבראנצ'ים נפרדו בקומיט ca6b800:

$ git merge-base master fr1
ca6b800f16ba27e424ee6586e33a25da32da2143

ואם עכשיו נפעיל merge נקבל Merge Commit חדש בענף ה master. אבל לא באנו לדבר כאן על merge אלא על rebase.

פעולת ריבייס של ענף fr1 ל master תיקח כל הקומיטים שחדשים יותר מ ca6b800, ואחד אחרי השני היא תחשב את ההבדל בינם לבין קומיט 9dae7c6 (שהוא החדש ביותר ב master) ואז תפעיל את ה patch כדי "לקדם" את master לכיוון fr1.

ככה זה נראה:

$ git checkout fr1
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: add lib
Applying: removed function def
Applying: trying to use my lib
Applying: FIXED with require_relative

$ git log --oneline
32c7fe8 (HEAD -> fr1) FIXED with require_relative
85258e9 trying to use my lib
cc2f449 removed function def
1ab57ee add lib
9dae7c6 (master) moved two code to function
ca6b800 moved to function v2
5eedde1 initial commit

שימו לב שכל מזהי הקומיטים השתנו. לדוגמא הקומיט האחרון בבראנצ' היה פעם 52361f5 ועכשיו הפך ל 32c7fe8. בניגוד לשינוי הידני שעשינו בהתחלה, הפעם אם נסתכל על ההבדל בין שני הקומיטים נראה שהקומיט החדש יותר כולל גם את השינויים מקומיט 9dae7c6:

$ git diff 32c7fe8 52361f5
diff --git a/two.rb b/two.rb
index ea4b92f..cc64ad1 100644
--- a/two.rb
+++ b/two.rb
@@ -1,6 +1 @@
-# this is my second file
-def two
-  "two"
-end
-
-puts two
+puts "two"

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

3. איך ליצור היסטוריה לינארית עם Rebase

עכשיו שהבנו מה זה ריבייס אפשר להתחיל לדבר על מה עושים איתו. הדבר הראשון והקל הוא יצירת היסטוריה לינארית לפרויקט. לפעמים אנחנו אוהבים לראות את ה Feature Branches שלנו בלוג ולהיזכר מה היה ואיך יצרנו אותם. אבל גם הרבה פעמים היינו מעדיפים לראות את ההיסטוריה בלעדיהם. כשזה קורה נוכל להשתמש בריבייס כמו שראינו כדי למזג ענף בלי ליצור Merge Commit. נמשיך את הדוגמא שלנו ל merge ותראו מה מקבלים:

$ git checkout master
$ git merge fr1
$ git branch -d fr1
$ git log --oneline

32c7fe8 (HEAD -> master) FIXED with require_relative
85258e9 trying to use my lib
cc2f449 removed function def
1ab57ee add lib
9dae7c6 moved two code to function
ca6b800 moved to function v2
5eedde1 initial commit

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

4. איך למזג Feature-Branch שיצא מ Feature-Branch אחר

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

$ git log --oneline
ed7f8f1 (HEAD -> fr2) add words file
59f413d adding readme
c0b632d (fr1) started integrating lib
32c7fe8 (master) FIXED with require_relative
85258e9 trying to use my lib
cc2f449 removed function def
1ab57ee add lib
9dae7c6 moved two code to function
ca6b800 moved to function v2
5eedde1 initial commit

מה אם נרצה לשלב את הענף fr2 לתוך ענף master? טוב אנחנו יודעים שאפשר להשתמש ב merge בשביל זה, אבל אז נקבל גם את השינויים מקומיט c0b632d. צריך להגיד - ברוב המקרים זה הדבר ההגיוני לעשות, כי קומיטים 59f413d ו-ed7f8f1 מתבססים עליו. מצד שני במקרה שלנו השינויים שהוכנסו בקומיט c0b632d הם לא הכרחיים בשביל להפעיל את השינויים של fr2.

בעזרת ריבייס נוכל למזג רק את קומיטים 59f413d ו-ed7f8f1 ולדלג על קומיט c0b632d. זה יראה ככה:

$ git rebase --onto master fr1 fr2
First, rewinding head to replay your work on top of it...
Applying: adding readme
Applying: add words file

אנחנו רואים את שני הקומיטים של fr2 מופעלים על master. אחרי שסיימנו הפרויקט נראה כך:

$ git log --oneline
f86a640 (HEAD -> fr2) add words file
a58dfdf adding readme
32c7fe8 (master) FIXED with require_relative
85258e9 trying to use my lib
cc2f449 removed function def
1ab57ee add lib
9dae7c6 moved two code to function
ca6b800 moved to function v2
5eedde1 initial commit

עכשיו זה כאילו התחלנו את fr2 מתוך master ולא מתוך fr1.

5. איך "למחוק" קומיטים עם ריבייס אינטרקטיבי

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

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

$ git rebase -i master

גיט פותח עורך טקסט עם השורות:

pick a58dfdf adding readme
pick f86a640 add words file

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

pick a58dfdf adding readme
f f85a640 add words file

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

$ git log --oneline
aa737d2 (HEAD -> fr2) adding readme
32c7fe8 (master) FIXED with require_relative
85258e9 trying to use my lib
cc2f449 removed function def
1ab57ee add lib
9dae7c6 moved two code to function
ca6b800 moved to function v2
5eedde1 initial commit

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

$ git diff HEAD~
diff --git a/lib/README.txt b/lib/README.txt
new file mode 100644
index 0000000..197ebfb
--- /dev/null
+++ b/lib/README.txt
@@ -0,0 +1,3 @@
+# My lib folder
+
+This is a folder in which I keep all sorts of cool stuff
diff --git a/lib/words.rb b/lib/words.rb
new file mode 100644
index 0000000..5548e69
--- /dev/null
+++ b/lib/words.rb
@@ -0,0 +1,7 @@
+def hello
+  "hello"
+end
+
+def world
+  "world"
+end

6. ריבייס ומאגרים מרוחקים

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

  1. אתם לוקחים פרויקט ממאגר מרוחק ומוסיפים קומיטים משלכם.

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

  3. החברה דוחפת את השינויים לשרת המרוחק.

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

כשאתם מזהים סיטואציה כזאת הייתם רוצים להגיד לגיט להתאמץ ולזהות קומיטים כאלה ש"שוכתבו" באמצעות ריבייס. המתג --rebase לפקודת pull עושה בדיוק את זה וזה אומר שאם מישהו כן עשה ריבייס והעלה את הקומיטים המשוכתבים למאגר המרכזי תצטרכו להפעיל:

$ git pull --rebase

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

7. בקיצור, מתי נשתמש בריבייס ומתי ב Merge?

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

  1. נשתמש בריבייס כשנרצה לקבל היסטוריה לינארית (בלי לראות גרף בלוג), אפילו שבמציאות יצרנו Feature Branch ועבדנו עליו.

  2. נשתמש בריבייס כשנרצה לתת הרגשה שה Feature Branch שלנו לקח רק קומיט יחיד, כדי שיהיה יותר קל לדבר על ה Pull Request שניצור ממנו.

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

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

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

https://www.tocode.co.il/workshops/69