דילוג לתוכן
Git וניהול גרסאות

git undo last commit: איך מבטלים commit אחרון ב־Git בלי לאבד עבודה

התרחיש מוכר: עשיתם commit, שנייה אחר כך ראיתם ששכחתם קובץ, כתבתם הודעה לא נכונה, או גרוע יותר — הכנסתם שינוי שלא היה אמור להיכנס. החיפוש git undo last commit מוביל להרבה תשובות קצרות, אבל ב־Git הפרטים הקטנים חשובים: האם כבר עשיתם push? האם רוצים לשמור את הקבצים? האם יש שינויים לא שמורים בתיקייה?

במאמר הזה נלך צעד־צעד, בלי קסמים ובלי פקודות מסוכנות שנזרקות לאוויר. המטרה היא להבין מתי משתמשים ב־git reset --soft, מתי ב־git reset --mixed, מתי ב־git reset --hard, ומתי דווקא git revert הוא הבחירה הנכונה. אם אתם בתחילת הדרך עם שורת הפקודה, שווה לפתוח במקביל גם את מדריך צעדים ראשונים כדי לוודא שהמונחים הבסיסיים יושבים טוב.

לפני שמבטלים: שלוש שאלות שחייבים לענות עליהן

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

  • האם ה־commit כבר נשלח לשרת? אם עשיתם git push, עדיף בדרך כלל להשתמש ב־git revert במקום לשכתב היסטוריה.
  • האם רוצים לשמור את העבודה בקבצים? ברוב המקרים כן. לכן --soft או --mixed בטוחים יותר מ־--hard.
  • האם יש שינויים שלא נכנסו ל־commit? אם כן, כדאי לשמור אותם בצד עם git stash או ליצור ענף גיבוי.

בדיקה בסיסית לפני כל פעולה:

bash
git status
git log --oneline -5

git status מראה אם יש שינויים לא שמורים, ו־git log --oneline -5 מציג את חמשת ה־commit האחרונים בצורה קצרה. אין כאן שינוי בקבצים, רק בדיקה. אם הפלט של git status אומר working tree clean, מצב העבודה נקי.

מילון קצר: מה ההבדל בין working tree, staging area ו־commit?

כדי להבין איך לבטל commit אחרון בלי לאבד עבודה, צריך להכיר שלוש שכבות:

שכבה מה היא אומרת בפועל פקודות נפוצות
working tree הקבצים כפי שהם נמצאים כרגע בתיקייה git status, עריכה בעורך טקסט
staging area השינויים שסימנתם לקראת ה־commit הבא git add, git restore --staged
commit history ההיסטוריה השמורה של הפרויקט git commit, git log, git reset

הפקודה git reset משנה בעיקר לאן מצביע HEAD, כלומר מה נחשב ה־commit הנוכחי. הדגלים --soft, --mixed ו־--hard קובעים מה יקרה לשתי השכבות האחרות.

git undo last commit: איזו פקודה לבחור?

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

מצב פקודה מומלצת האם העבודה נשמרת?
רוצים לבטל את ה־commit אבל להשאיר את השינויים מוכנים ל־commit חדש git reset --soft HEAD~1 כן, וגם נשאר ב־staging area
רוצים לבטל את ה־commit ולהשאיר את השינויים כקבצים ערוכים בלבד git reset --mixed HEAD~1 כן, אבל לא staged
רוצים למחוק לגמרי את ה־commit ואת השינויים שבו git reset --hard HEAD~1 לא. מסוכן אם לא בטוחים
ה־commit כבר נשלח לשרת או משותף עם אחרים git revert HEAD כן. נוצר commit חדש שמבטל את הקודם

אפשרות 1: לבטל commit אחרון ולשמור הכול ב־staging

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

bash
git reset --soft HEAD~1

אחרי הפקודה, ה־commit האחרון נעלם מההיסטוריה המקומית, אבל השינויים שלו נשארים מסומנים ל־commit. אפשר לבדוק זאת עם git status. לרוב תראו קבצים תחת Changes to be committed.

דוגמה מלאה להרצה בלינוקס או macOS עם bash

bash
#!/usr/bin/env bash
set -e

DEMO_DIR="$(mktemp -d)"
cd "$DEMO_DIR"

git init
git config user.name "Demo User"
git config user.email "[email protected]"

printf "first line\n" > notes.txt
git add notes.txt
git commit -m "first commit"

printf "second line\n" >> notes.txt
git add notes.txt
git commit -m "second commit"

echo "Before reset:"
git log --oneline -2

git reset --soft HEAD~1

echo "After reset:"
git log --oneline -2
git status --short

echo "Demo directory: $DEMO_DIR"

הדוגמה יוצרת תיקייה זמנית, מאתחלת בה מאגר Git, מוסיפה שני commit ואז מבטלת את האחרון עם git reset --soft HEAD~1. בפלט של git log אחרי הביטול תראו רק את ה־first commit. בפלט של git status --short צפויה להופיע שורה בסגנון M notes.txt, כלומר הקובץ עדיין מסומן ל־commit.

אפשרות 2: לבטל commit אחרון אבל להשאיר את הקבצים ערוכים

git reset --mixed HEAD~1 הוא מצב ברירת המחדל של git reset. הוא מבטל את ה־commit, אבל מוציא את השינויים מה־staging area. בפועל, הקבצים נשארים בתיקייה כאילו ערכתם אותם ועדיין לא הרצתם git add.

bash
git reset --mixed HEAD~1

אפשר לכתוב גם:

bash
git reset HEAD~1

המשמעות זהה, אבל במאמר הדרכתי עדיף לכתוב --mixed במפורש כדי שלא יהיה ספק.

מתי זה שימושי?

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

אחרי הפקודה, הריצו:

bash
git status

אם הכול עבד כצפוי, תראו את הקבצים תחת Changes not staged for commit. מכאן אפשר להריץ git add רק על הקבצים הרצויים, ואז git commit מחדש.

אפשרות 3: למחוק commit אחרון ואת השינויים שבו

git reset --hard HEAD~1 הוא הכלי החריף. הוא מבטל את ה־commit האחרון וגם מחזיר את הקבצים למצב של ה־commit שלפניו. כלומר: שינויי הקוד שהיו ב־commit האחרון ייעלמו מתיקיית העבודה.

bash
git reset --hard HEAD~1

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

bash
git branch backup-before-reset

הענף backup-before-reset ישמור מצביע למצב הנוכחי. אם טעיתם, תוכלו לחזור אליו עם git switch backup-before-reset.

אפשרות 4: אם כבר עשיתם push — השתמשו ב־revert

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

bash
git revert HEAD

git revert לא מוחק היסטוריה. הוא משאיר תיעוד ברור: היה שינוי, ואז היה שינוי נוסף שביטל אותו.

דוגמה מלאה ל־revert בלינוקס או macOS עם bash

bash
#!/usr/bin/env bash
set -e

DEMO_DIR="$(mktemp -d)"
cd "$DEMO_DIR"

git init
git config user.name "Demo User"
git config user.email "[email protected]"

printf "enabled=false\n" > app.conf
git add app.conf
git commit -m "add default config"

printf "enabled=true\n" > app.conf
git add app.conf
git commit -m "enable feature"

git revert --no-edit HEAD

git log --oneline -3
printf "Current file content:\n"
cat app.conf

echo "Demo directory: $DEMO_DIR"

הדוגמה יוצרת קובץ הגדרות, משנה אותו, ואז מבטלת את ה־commit האחרון באמצעות git revert --no-edit HEAD. בפלט של git log --oneline -3 תראו שלושה commit: המקורי, השינוי, ו־Revert "enable feature". תוכן הקובץ הצפוי בסוף הוא enabled=false.

מה עושים אם יש שינויים לא שמורים?

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

bash
git status --short

אם יש שינויים שאתם לא רוצים לערבב עם הביטול, אפשר לשמור אותם זמנית:

bash
git stash push -u -m "work before undoing last commit"

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

bash
git stash pop

אם יש התנגשות, Git יסמן את הקבצים שדורשים טיפול. במקרה כזה מתקנים ידנית, מריצים git status, ואז ממשיכים עם git add ו־git commit לפי הצורך.

דוגמה מלאה ב־Windows PowerShell

powershell
$DemoDir = Join-Path $env:TEMP ("git-undo-demo-" + [Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $DemoDir | Out-Null
Set-Location $DemoDir

git init
git config user.name "Demo User"
git config user.email "[email protected]"

"one" | Set-Content -Path "file.txt"
git add file.txt
git commit -m "first commit"

"two" | Add-Content -Path "file.txt"
git add file.txt
git commit -m "second commit"

"temporary note" | Set-Content -Path "scratch.txt"

git stash push -u -m "temporary work"
git reset --mixed HEAD~1
git stash pop

git status --short
Write-Output "Demo directory: $DemoDir"

הדוגמה מריצה תהליך מלא ב־PowerShell: יצירת מאגר, שני commit, קובץ זמני שלא נכנס ל־Git, שמירה עם git stash, ביטול ה־commit האחרון באמצעות git reset --mixed HEAD~1, והחזרת השינויים הזמניים. בפלט הסופי צפוי להופיע M file.txt עבור השינוי מה־commit שבוטל, וייתכן גם ?? scratch.txt עבור הקובץ הזמני שהוחזר.

אוטומציה קטנה: לבדוק לפני שמבטלים

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

python
#!/usr/bin/env python3
import subprocess
import sys

def run_git(args):
    result = subprocess.run(
        ["git"] + args,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    return result.returncode, result.stdout.strip(), result.stderr.strip()

code, status, error = run_git(["status", "--short"])
if code != 0:
    print("This directory is not a Git repository or Git is not available.")
    print(error)
    sys.exit(1)

code, log, error = run_git(["log", "--oneline", "-3"])
if code != 0:
    print("No commits were found.")
    print(error)
    sys.exit(1)

print("Recent commits:")
print(log)
print()
print("Working tree status:")
print(status if status else "clean")
print()
print("Safe options:")
print("Keep changes staged: git reset --soft HEAD~1")
print("Keep changes unstaged: git reset --mixed HEAD~1")
print("Undo a pushed commit: git revert HEAD")

שמרו את הקובץ בשם check_git_undo.py והריצו אותו מתוך תיקיית מאגר Git עם python3 check_git_undo.py. אם המאגר נקי, הפלט יכלול clean. אם יש שינויים, הם יוצגו בפורמט הקצר של Git. הסקריפט משתמש רק בספרייה הסטנדרטית של Python 3 ואינו מבצע reset או revert בעצמו.

טעויות נפוצות והודעות שגיאה אמיתיות

fatal: ambiguous argument 'HEAD~1': unknown revision or path not in the working tree.

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

bash
git log --oneline

אם אין היסטוריה, אין מה לבטל עם HEAD~1. אם יש רק commit אחד ואתם רוצים לפרק אותו, אפשר להשתמש ב־git reset --soft --root, אבל עשו זאת בזהירות ורק במאגר מקומי.

Author identity unknown

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

bash
git config user.name "Your Name"
git config user.email "[email protected]"

לאחר מכן הריצו שוב את git commit. במאגרים אמיתיים השתמשו בפרטים המתאימים לכם, לא בערכי הדוגמה.

! [rejected] main -> main (non-fast-forward)

זה קורה כשעשיתם reset מקומי ואז ניסיתם push לענף שכבר מכיל את ה־commit שמחקתם מקומית. Git מסרב כי הדחיפה תשכתב היסטוריה מרוחקת.

הפתרון הבטוח ברוב הצוותים הוא לא לכפות דחיפה, אלא להחזיר את הענף למצב תקין וליצור revert. אם זו עבודה אישית לגמרי ואתם מבינים את המשמעות, קיימת אפשרות של git push --force-with-lease, אבל אין להריץ אותה כברירת מחדל.

error: commit ... is a merge but no -m option was given.

הודעה זו מופיעה כשמנסים להריץ git revert על merge commit. ביטול מיזוג דורש לציין איזה צד של המיזוג נחשב הקו הראשי. זה כבר מקרה מתקדם יותר, וכדאי לבדוק היטב את ההיסטוריה עם git log --graph --oneline לפני שפועלים.

איך לתקן הודעת commit אחרון בלי לבטל את הכול?

אם הבעיה היא רק הודעת ה־commit, אין צורך ב־reset. השתמשו ב־git commit --amend:

bash
git commit --amend -m "better commit message"

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

מה ההבדל בין reset לבין restore?

git restore מיועד בעיקר להחזרת קבצים או להוצאה שלהם מה־staging area. הוא לא הכלי המרכזי לביטול commit שלם. לדוגמה, אם אחרי git reset --mixed HEAD~1 יש קובץ staged בטעות, אפשר להשתמש ב־git restore --staged file.txt.

לעומת זאת, git reset עובד על מצביעי ההיסטוריה ועל מצב ה־index. לכן הוא מתאים לביטול commit מקומי, אבל דורש יותר זהירות.

תהליך מומלץ בפועל

  1. הריצו git status ובדקו אם יש שינויים לא שמורים.
  2. הריצו git log --oneline -5 כדי לוודא מה ה־commit האחרון.
  3. אם יש ספק, צרו גיבוי עם git branch backup-before-undo.
  4. אם ה־commit לא נשלח: בחרו --soft או --mixed.
  5. אם ה־commit נשלח: השתמשו ב־git revert HEAD.
  6. בדקו שוב עם git status ו־git log --oneline -5.

אם אתם עדיין מתלבטים, הבחירה השמרנית היא git reset --soft HEAD~1 עבור עבודה מקומית שלא נשלחה, ו־git revert HEAD עבור עבודה שכבר נשלחה. שתי האפשרויות שומרות את העבודה שלכם בצורה ברורה יחסית. ככל שתתרגלו יותר את ההפרדה בין תיקיית העבודה, ה־staging area וההיסטוריה, כך פעולות כמו ביטול commit אחרון ירגישו פחות מפחידות ויותר כמו כלי עבודה רגיל.

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