גיט: איך לבטל תיקון ישן אחרי שהבנת שזה לא היה רעיון טוב
פוסט זה כולל טיפ קצר על כלי עבודה בסביבת Linux. בשביל ללמוד יותר על עבודה בסביבת Linux ו Unix אני ממליץ לכם לבדוק את קורס Linux שיש לנו כאן באתר. הקורס כולל מעל 50 שיעורי וידאו והמון תרגול מעשי ומתאים גם למתחילים.
הבעיה המרכזית בממשק של גיט איננה שורת הפקודה, אלא העובדה שהוא זורק עלינו עולם מושגים שלם שלא תמיד מתחבר לעולם המושגים שלנו. אבל מי שיתאמץ להבין אותו יגלה שהרבה פעמים ההשקעה היתה שווה את זה, כמו בסיפור הבא על מחיקת תיקון ישן.
1. בואו נבנה בלוג
טוב אולי לא בלוג מלא אבל לפחות פונקציה קטנה שלוקחת פוסט ומחזירה URL לפוסט הזה. לפוסטים אצלנו אגב יש שם ומזהה, וה URL של כל פוסט מורכב מהתחילית blog ומהמזהה:
class Post
attr_accessor :id, :name
def initialize(id, name)
@id = id
@name = name
end
end
def url_for(post)
"/blog/#{post.id}"
end
p = Post.new(2, 'hello-world')
puts url_for(p)
הקוד בשפת רובי וכשמפעילים אותו מקבלים:
/blog/2
שהוא הקישור לפוסט שהמזהה שלו הוא 2.
נכניס את זה ל git repository כי פוסט על גיט והכל:
$ git init
$ git add .
$ git commit -m 'initial commit'
2. זמן עובר
ואתה חושב לעצמך שבטח גוגל כועס בגלל שה URL כולל מספרים ולא נראה קשור לתוכן הפוסט. אז אתה מחליט שהגיע הזמן לשנות את הפונקציה כך שתציג את ה name של כל פוסט במקום את ה id שלו. לא בעיה נכון? הנה הצעה לתיקון:
class Post
attr_accessor :id, :name
def initialize(id, name)
@id = id
@name = name
end
end
def url_for(post_name)
"/blog/#{post_name}"
end
p = Post.new(2, 'hello-world')
puts url_for(p.name)
ועל זה אפשר להגיע ״עקום אבל עובד״. כי ה URL עכשיו מתחיל להציג את שם הפוסט. ברור שהיה עדיף לשנות רק את תוכן הפונקציה במקום את הממשק שלה ואת כל הקוד שקורא לה, אבל בזמן אמת לא תמיד רואים את הפיתרון הברור.
מה שכן היית מספיק חכם להכניס גם את התיקון הזה ל git repository, כי פוסט על גיט וכו':
$ git add .
$ git commit -m 'Changed url_for to show friendly names'
3. הזמן עובר
ושמעת מחבר שטסטים זה הדבר הגדול הבא אז החלטת להוסיף טסט לתוכנית שנהיה בטוחים שה URL אכן מציג את השמות:
require 'minitest'
require 'minitest/rg'
class Post
attr_accessor :id, :name
def initialize(id, name)
@id = id
@name = name
end
end
def url_for(post_name)
"/blog/#{post_name}"
end
class PostTest < MiniTest::Test
def test_url_uses_names
p = Post.new(2, 'hello-world')
q = Post.new(4, 'git-tips')
assert_equal('/blog/hello-world', url_for(p.name))
assert_equal('/blog/git-tips', url_for(q.name))
end
end
Minitest.run
וסוף סוף אפשר להריץ את התוכנית ובמקום לראות URL ולשבור את הראש אם זה מה שהיה צריך להופיע נקבל:
Run options: --seed 28117
# Running:
.
Finished in 0.001478s, 676.5902 runs/s, 1353.1803 assertions/s.
1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
ומה עם הגיט? גם את השינוי הזה נוסיף לריפוזיטורי:
$ git add .
$ git commit -m 'Add tests'
4. ואז זה מגיע
אולי משהו בטסט, אולי באוויר, לא יודע. אבל פתאום הבנת שהתיקון ההוא ששינה את כל הממשק של הפונקציה url_for
במקום לשנות שורה אחת בתוך גוף הפונקציה היה ממש טפשי. וזה גם הזמן לשמוח ששמרנו הכל בגיט. כך נראה הלוג:
$ git log
commit 1c486c42f3a00d891188674cb99b0b383af9c2c2 (HEAD -> master)
Author: ynonp <ynonperek@gmail.com>
Date: Fri Jan 26 09:16:54 2018 +0200
Add tests
commit d66b476e4cd25415d599bf52d9118e48f31c2c89
Author: ynonp <ynonperek@gmail.com>
Date: Fri Jan 26 09:15:58 2018 +0200
Changed url_for to show friendly names
commit f8e7ef78ef360fadda02ff4a73d9b561b78bf4b1
Author: ynonp <ynonperek@gmail.com>
Date: Fri Jan 26 09:15:20 2018 +0200
initial commit
כל מה שצריך זה למחוק את הקומיט האמצעי! והנה הזדמנות לדבר על rebase. קודם הפקודה:
$ git rebase -p --onto d66b476^ d66b476
ובעברית זה אומר לגיט:
- קח את כל השינויים שקרו בין קומיט d66b476 ל head, כלומר את:
$ git log d66b476..head
commit 1c486c42f3a00d891188674cb99b0b383af9c2c2 (HEAD -> master)
Author: ynonp <ynonperek@gmail.com>
Date: Fri Jan 26 09:16:54 2018 +0200
Add tests
ושים אותם בצד.
עכשיו תבצע checkout לגירסא שהעברנו ב onto, שזו גירסא אחת לפני קומיט d66b476 (כלומר הגירסא הראשונה אצלנו בריפוזיטורי). זה מוביל למחיקה של כל השינויים שקרו אחרי גירסא זו מתיקיית העבודה.
עכשיו תתחיל לבצע את השינויים מ (1) לפי הסדר.
נפעיל ונראה מה קורה:
$ git rebase -p --onto d66b476^ d66b476
error: could not apply 1c486c4... Add tests
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
Could not pick 1c486c42f3a00d891188674cb99b0b383af9c2c2
גיט מספר לנו שיש קונפליקט: הקומיט שהוסיף את הבדיקות התבסס על קוד שכבר לא קיים ולכן גיט לא ידע מה לעשות עם השורות שהתנגשו. במקום להחליט הוא שם את כל השורות בקובץ ומבקש מאתנו לשבור את הראש מה הגירסא הנכונה. הקובץ demo.rb שלנו נראה עכשיו כך:
require 'minitest'
require 'minitest/rg'
class Post
attr_accessor :id, :name
def initialize(id, name)
@id = id
@name = name
end
end
def url_for(post)
"/blog/#{post.id}"
end
<<<<<<< HEAD
p = Post.new(2, 'hello-world')
puts url_for(p)
=======
class PostTest < MiniTest::Test
def test_url_uses_names
p = Post.new(2, 'hello-world')
q = Post.new(4, 'git-tips')
assert_equal('/blog/hello-world', url_for(p.name))
assert_equal('/blog/git-tips', url_for(q.name))
end
end
Minitest.run
>>>>>>> 1c486c4... Add tests
הפונקציה url_for
חזרה לגירסא הראשונה שלה שמקבלת post ומשתמשת ב id כדי לבנות את ה URL, ובנוסף יש לנו את שורות ההדפסה מהקומיט שלפני הקומיט שנמחק ואת קוד הבדיקות מהקומיט האחרון. נסדר את הקובץ ונקבל:
require 'minitest'
require 'minitest/rg'
class Post
attr_accessor :id, :name
def initialize(id, name)
@id = id
@name = name
end
end
def url_for(post)
"/blog/#{post.name}"
end
class PostTest < MiniTest::Test
def test_url_uses_names
p = Post.new(2, 'hello-world')
q = Post.new(4, 'git-tips')
assert_equal('/blog/hello-world', url_for(p))
assert_equal('/blog/git-tips', url_for(q))
end
end
Minitest.run
ועכשיו אפשר להמשיך את הריבייס:
$ git add demo.rb
$ git rebase --continue
אחרי כל השינויים הקובץ demo.rb כולל את הקוד המתוקן וצפיה בגיט לוג לא מזכירה את הטעות המביכה:
$ git log
commit 3df1c49e4d63ee8a5c67acdbcc880b83668bafb7 (HEAD -> master)
Author: ynonp <ynonperek@gmail.com>
Date: Fri Jan 26 09:16:54 2018 +0200
Add tests
commit f8e7ef78ef360fadda02ff4a73d9b561b78bf4b1
Author: ynonp <ynonperek@gmail.com>
Date: Fri Jan 26 09:15:20 2018 +0200
initial commit