git rebase הוא אחד הפקודות הכי שימושיות והכי מבלבלות ב־Git: מצד אחד הוא נותן היסטוריה נקייה וליניארית, מצד שני שימוש לא זהיר בו יכול לשכתב היסטוריה שכבר שותפה עם אחרים. מולו עומד git merge, שנראה לפעמים “מלוכלך” יותר בגרף, אבל שומר את מה שקרה בדיוק כפי שקרה.
במאמר הזה נפרק את ההבדל בצורה מעשית: מתי משתמשים ב־rebase, מתי עדיף merge, איך זה נראה בפקודות אמיתיות, איך פותרים קונפליקטים, ומה כדאי לקבוע כמדיניות צוותית. אם אתם עדיין מחזקים את הבסיס בפקודות טרמינל, שווה לפתוח במקביל את מדריך צעדים ראשונים ולחזור לכאן עם סביבת Git מקומית מוכנה.
התשובה הקצרה: rebase לניקוי ענף עבודה, merge לשימור נקודת חיבור
בדרך כלל, הבחירה הפשוטה היא זו:
- עובדים על ענף פיצ׳ר אישי? השתמשו ב־
git rebaseכדי לעדכן אותו מולmainלפני פתיחת Pull Request או לפני מיזוג. - מכניסים פיצ׳ר גמור לענף משותף? השתמשו ב־
git merge, או ב־merge דרך מערכת ניהול הקוד, כדי לשמור נקודת חיבור ברורה. - ענף כבר משותף לכמה מפתחים? אל תעשו לו rebase בלי תיאום מפורש. זו הדרך הקצרה לבלבול, קומיטים כפולים ודחיפות שנדחות.
הכלל הזה לא מוחלט, אבל הוא עובד טוב ברוב הצוותים: rebase מסדר את הסיפור לפני הפרסום; merge מתעד את הפרסום עצמו.
מה צריך להכין מראש לפני שמנסים את הדוגמאות
הדוגמאות בהמשך מיועדות ל־Linux או macOS עם bash. הן יוצרות תיקיות זמניות, מגדירות משתמש Git מקומי בתוך הריפו בלבד, ולא נוגעות בהגדרות הגלובליות שלכם.
צריך לוודא שיש:
gitמותקן וזמין בטרמינל.bashפעיל.- כמה דקות להריץ פקודות ולהסתכל על הפלט.
אם אתם מגיעים מעולמות תכנות כמו פייתון, חשבו על Git כמו על לוג של שינויים בקוד: כל commit הוא מצב שמור, ו־branch הוא מצביע למסלול התקדמות מסוים. ההבדל בין rebase ל־merge הוא בעיקר בשאלה איך מחברים שני מסלולים כאלה.
המודל המנטלי: מה merge עושה ומה rebase עושה
git merge לוקח שני ענפים ויוצר commit חיבור, אם אי אפשר לבצע fast-forward. היסטוריית העבודה נשמרת כמו שהיא: רואים שהיה ענף, רואים שהיה פיתוח במקביל, ורואים מתי הוא חובר.
git rebase עושה משהו אחר: הוא לוקח את הקומיטים שלכם, “מרים” אותם מהבסיס הישן, ומניח אותם מחדש על בסיס חדש. התוצאה נראית כאילו התחלתם לעבוד אחרי השינויים האחרונים של main, גם אם בפועל התחלתם לפני.
| פעולה | מה קורה להיסטוריה | יתרון מרכזי | סיכון מרכזי |
|---|---|---|---|
merge |
נשמרת כפי שהייתה, כולל הסתעפות | שקיפות מלאה לגבי מה קרה ומתי | גרף עמוס ב־merge commits |
rebase |
קומיטים מועתקים לבסיס חדש עם hash חדש | היסטוריה ליניארית וקלה לקריאה | שכתוב היסטוריה שכבר אולי שותפה |
דוגמה 1: איך נראה merge בפועל
הדוגמה הבאה יוצרת ריפו מקומי, מוסיפה commit ראשון, יוצרת ענף פיצ׳ר, מוסיפה שינוי גם ב־main, ואז מבצעת merge עם commit חיבור מפורש.
#!/usr/bin/env bash
set -euo pipefail
# Create a temporary Git repository
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cd "$tmpdir"
git init -q
git branch -M main
git config user.name "Local Demo"
git config user.email "[email protected]"
# Create the initial commit
printf "line 1\n" > notes.txt
git add notes.txt
git commit -q -m "initial notes"
# Create a feature branch and commit a change
git switch -q -c feature
printf "feature line\n" >> notes.txt
git add notes.txt
git commit -q -m "add feature line"
# Add a different change on main
git switch -q main
printf "main line\n" > main.txt
git add main.txt
git commit -q -m "add main file"
# Merge the feature branch with an explicit merge commit
git merge --no-ff feature -m "merge feature into main" -q
git log --oneline --graph --all --decorateהפלט הצפוי ייראה בערך כך, עם מזהי commit שונים אצלכם:
* 1a2b3c4 (HEAD -> main) merge feature into main
|\
| * 5d6e7f8 (feature) add feature line
* | 9a0b1c2 add main file
|/
* 3d4e5f6 initial notesמה רואים כאן? יש פיצול ברור. הענף feature התקדם לכיוון אחד, main התקדם לכיוון אחר, ואז נוצר commit שמחבר ביניהם. זה מצוין כשחשוב לראות שפיצ׳ר מסוים נכנס כיחידה אחת.
איך עובד git rebase בפועל, ולמה ההיסטוריה נראית נקייה יותר
עכשיו נעשה תרחיש דומה, אבל במקום למזג את הפיצ׳ר לתוך main מיד, נעדכן את ענף הפיצ׳ר כך שיישב מעל main. לאחר מכן נבצע merge --ff-only, כלומר מיזוג שמותר רק אם אין צורך ב־merge commit.
#!/usr/bin/env bash
set -euo pipefail
# Create a temporary Git repository
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cd "$tmpdir"
git init -q
git branch -M main
git config user.name "Local Demo"
git config user.email "[email protected]"
# Create the initial commit
printf "line 1\n" > notes.txt
git add notes.txt
git commit -q -m "initial notes"
# Create a feature branch and commit a change
git switch -q -c feature
printf "feature line\n" >> notes.txt
git add notes.txt
git commit -q -m "add feature line"
# Add a different change on main
git switch -q main
printf "main line\n" > main.txt
git add main.txt
git commit -q -m "add main file"
# Move the feature commits on top of main
git switch -q feature
git rebase main
# Fast-forward main to the rebased feature
git switch -q main
git merge --ff-only feature
git log --oneline --graph --all --decorateהפלט הצפוי יהיה ליניארי יותר:
* 7f8a9b0 (HEAD -> main, feature) add feature line
* 2c3d4e5 add main file
* 6a7b8c9 initial notesשימו לב: ה־commit בשם add feature line קיבל hash חדש אחרי ה־rebase. זה לא אותו commit טכנית, גם אם התוכן וההודעה דומים. זו הסיבה שאסור לבצע rebase חופשי על ענפים שאנשים אחרים כבר משכו ועובדים עליהם.
מתי לבחור merge בצוות
merge מתאים במיוחד במקרים שבהם ההקשר חשוב. למשל: פיצ׳ר שהורכב מכמה קומיטים, תיקון באג שהגיע מענף נפרד, או עבודה שנעשתה במקביל על ידי יותר מאדם אחד.
בחרו merge כאשר:
- הענף כבר פורסם ומפתחים אחרים עובדים עליו.
- רוצים לראות בגרף מתי פיצ׳ר נכנס ל־
main. - יש חשיבות לשמירת ההיסטוריה האמיתית של העבודה.
- מערכת ניהול הקוד שלכם מייצרת merge commit ברור לכל Pull Request.
החיסרון הוא שבפרויקטים פעילים מאוד, גרף ההיסטוריה יכול להפוך לרועש. הרבה merge commits עם הודעות דומות כמו Merge branch main into feature מקשים על חיפוש המקור של שינוי.
מתי לבחור rebase בצוות
rebase מתאים בעיקר לניקוי ענף לפני שהוא נכנס לענף המרכזי. הוא עוזר לכם להציג את העבודה כאילו נעשתה אחרי העדכונים האחרונים, במקום להראות כל פעם שסנכרנתם מול main.
בחרו rebase כאשר:
- אתם עובדים על ענף אישי שעדיין לא משמש אחרים.
- אתם רוצים לפתור קונפליקטים אצלכם לפני פתיחת Pull Request.
- הצוות מעדיף היסטוריה ליניארית.
- אתם רוצים להימנע מ־merge commits מיותרים כמו “עדכון מול main”.
פקודת עבודה נפוצה היא:
git fetch origin
git switch feature
git rebase origin/mainהפקודות האלה מביאות את מצב השרת, עוברות לענף הפיצ׳ר, ומניחות את הקומיטים המקומיים שלכם מעל origin/main. אם הכול עובר בלי קונפליקטים, תקבלו ענף נקי ומעודכן.
דוגמה 3: קונפליקט בזמן rebase ואיך פותרים אותו
קונפליקטים הם לא סימן שעשיתם משהו לא נכון. הם פשוט אומרים ש־Git לא הצליח להחליט לבד איזה שינוי לשמור. הדוגמה הבאה יוצרת קונפליקט צפוי, מציגה אותו, פותרת אותו, וממשיכה את ה־rebase.
#!/usr/bin/env bash
set -euo pipefail
# Create a temporary Git repository
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cd "$tmpdir"
git init -q
git branch -M main
git config user.name "Local Demo"
git config user.email "[email protected]"
# Create the initial file
printf 'print("hello")\n' > app.py
git add app.py
git commit -q -m "initial app"
# Change the same line on the feature branch
git switch -q -c feature
printf 'print("hello from feature")\n' > app.py
git add app.py
git commit -q -m "feature greeting"
# Change the same line on main
git switch -q main
printf 'print("hello from main")\n' > app.py
git add app.py
git commit -q -m "main greeting"
# Start a rebase that will create a conflict
git switch -q feature
set +e
git rebase main
status=$?
set -e
echo "rebase exit status: $status"
echo "conflict markers:"
grep -n '<<<<<<<\|=======\|>>>>>>>' app.py || true
# Resolve the conflict by writing the intended final content
printf 'print("hello from main and feature")\n' > app.py
git add app.py
GIT_EDITOR=true git rebase --continue
echo "final file:"
cat app.py
echo "final history:"
git log --oneline --graph --decorate --allבמהלך הריצה תראו הודעות בסגנון:
CONFLICT (content): Merge conflict in app.py
error: could not apply ... feature greeting
rebase exit status: 1
conflict markers:
1:<<<<<<< HEAD
3:=======
5:>>>>>>> feature greeting
final file:
print("hello from main and feature")הנקודה החשובה: בזמן rebase פותרים קונפליקט, מריצים git add על הקובץ שתוקן, ואז ממשיכים עם git rebase --continue. אם החלטתם שהסתבכתם, אפשר לחזור אחורה עם git rebase --abort.
מדיניות צוותית מומלצת: פשוטה, כתובה, בלי הפתעות
הבעיה עם rebase ו־merge היא לא הפקודות עצמן, אלא חוסר תיאום. צוות טוב לא צריך דיון מחדש בכל Pull Request. מספיקים כמה כללים ברורים:
- לא עושים rebase ל־
main. ענף מרכזי צריך להיות יציב וצפוי. - מותר לעשות rebase לענף אישי, כל עוד אף אחד אחר לא עובד עליו.
- אם דחפתם ענף אחרי rebase, השתמשו ב־
git push --force-with-leaseולא ב־git push --force, ורק אם זה ענף שלכם. - לפני מיזוג, עדכנו את הענף מול
mainבאמצעות rebase או merge, לפי המדיניות שנקבעה. - אל תערבבו בלי סיבה: ענף מלא ב־merge commits ואז rebase אגרסיבי יהיה קשה יותר להבנה.
למי שעדיין מתרגל עבודה עם קבצים, קומיטים והרצה מקומית, כדאי לחזור מדי פעם ליסודות: צעדים ראשונים נותן הקשר טוב לעבודה בטרמינל, ו־פייתון יכול לעזור לתרגל פרויקטים קטנים שבהם Git באמת מתחיל להיות שימושי.
טעויות נפוצות והודעות שגיאה אמיתיות
error: cannot pull with rebase: You have unstaged changes.
זה קורה כשיש לכם שינויים פתוחים בקבצים ו־Git לא רוצה להתחיל rebase מעל מצב לא נקי. בדקו מה השתנה:
git statusאם השינוי מוכן, עשו commit. אם הוא זמני, השתמשו ב־git stash:
git stash push -m "work in progress"
git pull --rebase
git stash popאחרי stash pop ייתכנו קונפליקטים, אבל לפחות ה־rebase עצמו התחיל ממצב נקי.
CONFLICT (content): Merge conflict in file-name
זו הודעת קונפליקט רגילה. פתחו את הקובץ, חפשו את הסימונים <<<<<<<, =======, >>>>>>>, והשאירו את הגרסה הרצויה. לאחר מכן:
git add file-name
git rebase --continueב־merge הפקודה האחרונה תהיה לרוב git commit, אלא אם Git כבר הכין הודעת merge ואתם ממשיכים דרך הכלי שלכם.
fatal: Not possible to fast-forward, aborting.
הודעה זו מופיעה כשמריצים git merge --ff-only, אבל ההיסטוריה לא מאפשרת fast-forward. הפתרון תלוי במדיניות: או לבצע rebase לענף הפיצ׳ר מעל main, או לבצע merge רגיל וליצור merge commit.
fatal: invalid upstream 'origin/main'
Git לא מכיר את origin/main. ייתכן שלא הרצתם git fetch, ייתכן שהענף המרכזי נקרא אחרת, או שאין remote בשם origin. התחילו ב:
git remote -v
git branch -a
git fetch originלאחר מכן השתמשו בשם הענף שקיים בפועל.
error: failed to push some refs to ...
אחרי rebase, ה־hash של הקומיטים השתנה. אם הענף כבר נמצא בשרת, push רגיל עלול להידחות. בענף אישי בלבד, ואחרי שווידאתם שאינכם מוחקים עבודה של אחרים, השתמשו בזהירות ב:
git push --force-with-lease--force-with-lease בטוח יותר מ־--force כי הוא בודק שהענף המרוחק לא השתנה מאז שראיתם אותו לאחרונה.
שאלות שקוראים באמת שואלים
האם rebase מוחק קומיטים?
לא בדיוק. הוא יוצר קומיטים חדשים עם תוכן דומה או זהה, ומזיז את הענף אליהם. הקומיטים הישנים לא נעלמים מיד, ובמקרים רבים אפשר למצוא אותם דרך git reflog. אבל מבחינת ההיסטוריה הרגילה של הענף, הם הוחלפו.
האם merge פחות מקצועי מ־rebase?
לא. זו תפיסה שגויה. merge הוא כלי נכון כאשר רוצים לשמר היסטוריה אמיתית של הסתעפות וחיבור. rebase הוא כלי נכון כאשר רוצים להציג רצף עבודה נקי. מקצועיות היא לדעת מתי להשתמש בכל אחד.
מה עדיף לפני Pull Request?
בצוותים רבים עדיף לעדכן את ענף הפיצ׳ר מול main בעזרת git rebase origin/main, לפתור קונפליקטים מקומית, ואז לפתוח Pull Request נקי. אבל אם הצוות שלכם מעדיף merge commits גם בענפי פיצ׳ר, לכו עם המדיניות הקיימת.
סיכום: לא rebase מול merge, אלא בחירה לפי שלב העבודה
הדרך הבריאה לחשוב על זה היא לא “איזו פקודה טובה יותר”, אלא “באיזה שלב אני נמצא”. בזמן פיתוח אישי, git rebase עוזר לשמור על ענף מסודר ומעודכן. בזמן הכנסת עבודה לענף משותף, git merge נותן תיעוד ברור של החיבור.
אם אתם צריכים כלל אצבע אחד: עשו rebase לענפים פרטיים לפני שהם נכנסים לבדיקה, ועשו merge כשמחברים עבודה גמורה לענף משותף. מעל הכול, כתבו את הכלל הזה לצוות, היצמדו אליו, ואל תשכתבו היסטוריה שאנשים אחרים כבר בונים עליה.


