לעילוי נשמת הלל מנחם ויגל יעקב למשפחת יניב, מהר-ברכה, שנירצחו על היותם יהודים (ה אדר התשפ"ג).
רוב הפקודות בגיט בטוחות לשימוש וגם אם טועים יש דרך לתקן. ישנן כמה פקודות מסוכנות שצריך להכיר לעומק כי לא תמיד יש אפשרות תיקון. git reset היא אחת מהן.
יש לה המון אפשרויות שכל אחת פועלת אחרת וכדאי להכיר מה בדיוק כל פקודה עושה לפני שמשתמשים בה.
המאמר הזה גם יעזור להבין טוב יותר איך git עובד ויכיר לנו מספר פקודות נוספות.
שלושת העצים
rafael@DELL-RAFAELJ MINGW64 /c/learn/git$ mkdir git_resetrafael@DELL-RAFAELJ MINGW64 /c/learn/git$ cd git_reset/rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset$ git init .Initialized empty Git repository in C:/learn/git/git_reset/.git/rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)$ touch a.txtrafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)$ git add a.txtrafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)$ git commit -m "initial commit"[master (root-commit) a358dfe] initial commit1 file changed, 0 insertions(+), 0 deletions(-)create mode 100644 a.txt
working directory
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ echo 'hello' > a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: a.txt no changes added to commit (use "git add" and/or "git commit -a")
staging index
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git ls-files -s 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a.txt
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git add a.txt warning: in the working copy of 'a.txt', LF will be replaced by CRLF the next time Git touches it rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git status On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: a.txt
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git ls-files -s 100644 ce013625030ba8dba906f756967f9e9ca394464a 0 a.txt
commit history
פקודת git commit מכניסה שינויים שישמרו כ-snapshot בעץ ה-commit history. ה-snapshot כולל גם את המצב של ה-staging index בזמן ה-commit. נדגים זאת:
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ git commit -m "insert some text into a.txt"
[master 043d64c] insert some text into a.txt
1 file changed, 1 insertion(+)
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ git status
On branch master
nothing to commit, working tree clean
ניתן לראות בעזרת git status שלאחר ה-commit אין שינויים באף אחד מהעצים.
אז מה בדיוק עושה פקודת git reset
האמת שהיא דומה לפקודת git checkout. פקודת checkout משנה את הפוינטר HEAD. ולעומתה, פקודת reset פועלת על הפוינטר HEAD וגם על הפוינטר של ה-branch הנוכחי.
דוגמה תסביר את זה הכי טוב:
בדוגמה זו יש רצף של commits ב-branch שנקרא main. כאשר גם ה-HEAD וגם המצביע של ה-branch מצביעים ל-commit שנקרא d. עכשיו נשווה את הפעולה של checkout מול reset.
אם נבצע git checkout b נקבל את המצב הבא:
רואים שרק המצביע HEAD השתנה. (המצב הזה נקרא deatached HEAD כיון שבמצב הזה HEAD לא מחובר לשום branch).
ואם נבצע את הפקודה git reset b נקבל את המצב הבא:
כאן רואים שפקודת reset משפיעה גם על HEAD וגם על המצביע של ה-branch.
ה-HEAD והפוינטר של ה-branch נקראים commit ref pointers. ופעולות עליהם הן פעולות בעץ ה-commit history.
ל-git reset יש 3 אפשרויות, hard, mixed, soft. והן קובעות איך ישתנו שני העצים האחרים.
בקצרה:
hard-- זו האפשרות הכי מסוכנת. הפעולה הזו גם משנה את הפוינטר ל-commit המבוקש. אבל היא גם מוחקת את השינויים שהיו על ה-staged tree ועל ה-working directory tree.
mixed-- זו אפשרות הדיפולט. היא משנה את הפוינטר ל-commit המבוקש. והיא מעבירה שינויים שהיו על ה-staged tree ל working directory tree. כאילו שלא עשינו להם git add.
soft-- זו האפשרות העדינה ביותר. היא משנה את הפוינטר ל-commit המבוקש. ולא נוגעת ב-staged tree וב-working directory tree.
הפקודה הדיפולטיבית
אם נשתמש בפקודה git reset בלי לציין שום דבר נוסף, זה יהיה זהה לפקודה git reset --mixed HEAD.
הפקודה הזו בעצם תגרום למצביע HEAD ולמצביע של ה-branch להצביע על HEAD. ותעביר שינויים שהיו על ה-staged tree ל working directory tree.
שימוש ב git reset --hard commit_sha1
האפשרות המסוכנת ביותר ועם זאת היא האפשרות שככל הנראה הכי הרבה משתמשים בה. דבר ראשון, כמו בכל שימוש של git reset הפוינטרים של HEAD ושל ה-branch הנוכחי עוברים להצביע על ה-commit_sha1 שאותו בחרנו בפקודה. בנוסף לזה, העצים staging index, working directory מאותחלים כך שגם הם יהיו על אותו commit_sha1 שבחרנו. בפקודה הזו אנחנו מקבלים סביבת עבודה נקיה משינויים. זו הסיבה שזו הפקודה שהכי בשימוש.
כל שינוי שנעשה על ה-working directory וכל שינוי שמחכה על ה-staging index ימחקו כדי להביא את העצים הללו למצב זהה לאותו commit_sha1 שבחרנו.
נדגים את זה בצורה הבאה, נכניס שינוי לעץ ה-working directory ולעץ ה-staging:
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ echo 'some text' > b.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git add b.txtrafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ echo 'adding some text' >> a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git status On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: b.txt Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: a.txt
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git reset --hard HEAD is now at 043d64c insert some text into a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git status On branch master nothing to commit, working tree clean
שימוש ב mixed--
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ echo 'some text' > b.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git add b.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ echo 'adding some text' >> a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git status On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: b.txt Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git reset --mixed Unstaged changes after reset: M a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: a.txt Untracked files: (use "git add <file>..." to include in what will be committed) b.txt no changes added to commit (use "git add" and/or "git commit -a")
שימוש ב soft--
האפשרות של soft משנה רק את המצביעים של ה-HEAD ושל ה-branch הנוכחי, ולא משנה בכלל את עץ ה-staging וה-working directory.
כדי להבין איך בדיוק soft פועל נשתמש בדוגמה אחרת. נתחיל עם קובץ a.txt ונכניס לתוכו שלוש שורות, כל שורה תהיה ב-commit נפרד:
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ echo 'one' >> a.txt
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ git commit -am "one"
warning: in the working copy of 'a.txt', LF will be replaced by CRLF the next time Git touches it
[master 3faafaf] one
1 file changed, 1 insertion(+)
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ echo 'two' >> a.txt
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git commit -am "two" warning: in the working copy of 'a.txt', LF will be replaced by CRLF the next time Git touches it [master 5dc4b70] two 1 file changed, 1 insertion(+) rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ echo 'three' >> a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git commit -am "three" warning: in the working copy of 'a.txt', LF will be replaced by CRLF the next time Git touches it [master d3308f2] three 1 file changed, 1 insertion(+)
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ cat a.txt
one
two
three
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ git log
commit d3308f222fed7811c93d868a3aed9aa5c4fe6736 (HEAD -> master)
Author: Rafael Jan <rafael.jan@inx.co>
Date: Fri Mar 3 00:55:23 2023 +0200
three
commit 5dc4b707348e31a12a934120b97e8d684277eee0
Author: Rafael Jan <rafael.jan@inx.co>
Date: Fri Mar 3 00:55:07 2023 +0200
two
commit 3faafaffec7c7078db091ac2e87ddf44f6279a4c
Author: Rafael Jan <rafael.jan@inx.co>
Date: Fri Mar 3 00:54:26 2023 +0200
one
commit a358dfe0f11dcf512be74a8fe83bf3efb0bb25dd
Author: Rafael Jan <rafael.jan@inx.co>
Date: Thu Feb 23 10:00:41 2023 +0200
initial commit
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ echo 'four' >> a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: a.txt no changes added to commit (use "git add" and/or "git commit -a")
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ git reset --soft 3faafaffec7c7078db091ac2e87ddf44f6279a4c
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git status On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: a.txt Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ cat a.txt one two three four
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git restore a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ cat a.txt one two three
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ git restore --staged a.txt
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: a.txt
no changes added to commit (use "git add" and/or "git commit -a")
rafae@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ cat a.txt one two three
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git restore a.txt rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ git status On branch master nothing to commit, working tree clean rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master) $ cat a.txt one
rafael@DELL-RAFAELJ MINGW64 /c/learn/git/git_reset (master)
$ git log
commit 3faafaffec7c7078db091ac2e87ddf44f6279a4c (HEAD -> master)
Author: Rafael Jan <rafael.jan@inx.co>
Date: Fri Mar 3 00:54:26 2023 +0200
one
commit a358dfe0f11dcf512be74a8fe83bf3efb0bb25dd
Author: Rafael Jan <rafael.jan@inx.co>
Date: Thu Feb 23 10:00:41 2023 +0200
initial commit
פעולת reset לעומת פעול revert
המסקנה מכל זה היא ש-revert היא הפעולה הנכונה לשימוש כאשר רוצים לעשות undo ל-commit ואילו פעולת reset היא הפעולה הנכונה לשימוש כשרוצים לבטל שינויים ב-staging וב-working directory.
מאוד לא מומלץ לעשות שימוש ב-reset על commit שכבר פורסם בפרוייקט שמשתמשים בו עוד אנשים, כיון שזה יכול לגרום לכך שמי שנמצא על אותו commit ינותק משאר העץ, ולא יוכל להסתנכרן עם העץ בהמשך. לכן כדאי תמיד להשתמש ב-revert במקרה כזה.
מצד שני, זה בסדר להשתמש ב-reset על commits שעדיין לא פורסמו אלא הם עדיין רק לוקאלים. למשל, אם עשיתי שינוי בקובץ ועשיתי לו commit. ואז עשיתי שוב שינוי באותו קובץ ושוב commit. ועכשיו אני רוצה לבטל את 2 ה-commits, כיון שעדיין לא פירמתי אותם עם push אני יכול בביטחה להשתמש ב-reset. כדי למחוק שתי commits אפשר פשוט להשתמש בכתיבה הנוחה הזו:
$ git reset --hard HEAD~2
סיכום
פעולת git reset היא אחת הפעולות המסוכנות שיש בגיט ואחת היחידות שיכולה לגרום לאיבוד מידע לחלוטין. השימוש בה הוא בעיקר לביטול שינויים בעץ ה-staging וה-working directory, או לביטול commits לוקאלים שעדיין לא פורסמו.
עבור commits שכבר פורסמו נשתמש תמיד בפקודת revert, שמבטלת את השינויים, תוך כדי יצירת commit נוסף וכך שומרת את כל ההיסטוריה.
אם אהבתם, תכתבו משהו למטה...
מקורות:
מבוסס בעיקר על המאמר הזה: https://www.atlassian.com/git/tutorials/undoing-changes/git-reset