Git - חלק 3

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

 

תרחיש ראשון

נניח שאליס דחפה את הפרויקט שלה ל GitHub, ובוב רוצה עותק של הפרויקט, הוא ישבט אותו עם הפקודה <git clone <URL. לאחר מכן אליס תבצע כמה שינויים בפרויקט (נניח שהוסיפה קובץ חדש בשם words.txt עם הפקודה git add --all, לאחר מכן ביצעה לו commit עם הפקודה "git commit -m "Add words.txt ולבסוף דחפה את השינויים ל GitHub עם הפקודה git push). כעת נעבור לבוב, בוב עבד בזמן שאליס עשתה את השינויים שלה, ועשה שינויים משלו (שינויים בקובץ קיים, שמהערכת כבר עוקבת אחריו, וביצע commit עם הפקודה "git commit -m -a "Update the readme). אז כעת יש לנו את ה commit של אליס שכבר עודכן ב GitHub ואת ה commit של בוב, שיושב ב repo המקומי שלו (במחשב שלו), והם שונים (ניתן לראות בתמונה משמאל ייצוג וויזואלי שהכנתי לענפים/צירי הזמן של אליס ובוב).

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

nirgn $ git push
To http://github.com/example/test.git
![rejected] master -&gt; master (non-fast-forward)
error:failed to push some refs to 'http://github.com/example/test.git'
hint: Update were rejected because the tip of your current branch is behind
hint: it's remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
First Step

לכן בוב יצטרך לסנכרן את הפרויקט שלו עם הפקודה git pull ורק אז לדחוף את הפרויקט (עם ה commit) שלו ל GitHub עם הפקודה git push. אבל האם סנכרון הפרויקט שלו לא פוגע בשינויים שביצע? לא. בואו נראה מה קורה מתחת למכסה המנוע כשאנו מבצעים git push:

1.קודם כל, המערכת מביאה את ה repo המקומי שלנו לאותה הנקודה ב repo המרוחק (ז”א לוקחת את ה commit של אליס ומביא אותו לענף של בוב). חשוב לציין כי זה רק מביא את ה commit, ולא באמת מעדכן את הקוד המקומי שלנו. אם נרחיב את נקודת המבט בענף של בוב, נראה (כמו בתמונה משמאל) שבעצם יתפצל מה commit הזה ענף חדש, בשם origin/master שמייצג את ה repo המרוחק שלנו. בנוסף, הצעד הראשון זהה לכתיבת הפקודה git fetch.

nirgn $ git fetch
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 0), reused 4 (delta 0)
Unpacking objects: 100% (4/4), done.
From http://github.com/nirgn975/test.git
	f35f2f1..71a4650 master -&gt; origin/master
Step Three

2.בצעד השני המערכת ממזגת את הענף origin/master עם הענף master (כמו להריץ את הפקודה git merge origin/master). אם אתם זוכרים, בפוסט הקודם כשביצענו איחוד בין 2 ענפים עם commit שונה בכל אחד מהם, המערכת ישר הקפיצה לנו עורך בו היינו צריכים ליצור commit מאוחד. לאחר שיצאנו מהעורך, המערכת הדפיסה לנו טקסט ובאחת השורות נכתב כי בוצע “מיזוג רקורסיבי” (השורה: Merge made by the ‘recursive’ strategy).

3.אם נסתכל כעת בציר הזמן (תמונה משמאל) נראה כי ישנו commit חדש, שמו יהיה “merge commit”. תשימו לב כי הענף origin/master עדיין לא מודע לשינויים של בוב ול merge commit.

Step Four

4.בשלב הבא המערכת תבצע git push ובשלב הזה origin/master יצביע ל master (ולכן ידע על השינויים וה merge commit), והכל יעודכן.

 

כעת, אם נחזור קצת אחורה, לאחר שכתבנו את הפקודות git pull ו git push, נבדוק את הלוג (עם הפקודה git log) ונראה את ה merge commit.

nirgn $ git log
	commit ee47baaedcd54e1957f86bda1aaa1b8a136185da
	Merge: 87c5243 57501d5
	Author: Nir Galon <nirgn975@gmail.com> <!-------- Merge Commit !!!!

		Merge branch 'master' of https://github.com/nirgn975/test.git

	commit 87c5243d2266f05cd9fda8b1c9137f11b3fe6f31
	Author: Nir Galon &lt;nirgn975@gmail.com&gt;

		Update the readme.

	commit 57501d595b16e2d1198a9c04c547a5b1380a6618
	Author: Nir Galon &lt;nirgn975@gmail.com&gt;

		Add store and product models.</pre>

יש ברשת כמה דעות, ותוכלו בקלות למצוא אנשים שלא ממש מחבבים את ה merge commit, מכיוון שאם ישנו הרבה שיתוף פעולה בפרויקט חי (כזה שזז לעיתים קרובות ע”י כמה אנשים בו זמנית), תוכלו לראות אחוז נכבד מהשינויים בלוג כ merge commit, וזה די מטנף את הלוג. כרגע נשאיר את זה ככה, בהמשך נדבר על משהו שנקרא rebase, שזו אחת מהדרכים לעקוף את ה merge commit.

 

תרחיש שני

First Scenario

כעת, נניח שאליס ובוב עובדים שוב על אותו הקוד באותו הזמן, רק שהפעם הם עובדים על אותו הקובץ, נניח קובץ ה README. שוב, אליס ביצעה את השינויים ודחפה את ה commit ל repo המרוחק, GitHub, ובוב רק ביצע commit (נשאר באופן מקומי). כעת, כמו בתמונה הראשונה בתרחיש הראשון, יש לנו שתי commit, אחד ב GitHub והשני מקומי, ושניהם שונים אחד מהשני (בעוד אלה שלפניהם זהים).

מה יקרה הפעם אם בוב ידחוף את ה commit שלו לאותו ה repo המרוחק שמאוחסן ב GitHub? הפעם אנחנו מנוסים, נדלג על הטעויות שביצענו בתרחיש הקודם ובוב יבצע ישר git pull (זה מביא את ה repo המקומי לאותה הנקודה כמו ב repo המרוחק (מושך את ה commit של אליס) ואז הוא מנסה למזג את הענפים), אך כאן המיזוג יכשל, ישנו קונפליקט והמערכת כותבת לנו איפה הוא נמצא (ניתן לראות שבקובץ README.txt), לכן נהיה חייבים לערוך את קובץ ה README, מכיוון שהמערכת זורקת אותנו חזרה לשורת הפקודה.

אם נכתוב בשורת הפקודה git status, נראה שהמערכת אומרת לנו שבענף שלנו ובענף “origin/master” בוצע שינוי באותו קובץ, README.txt ולכן, שוב, אנחנו חייבים לערוך את הקובץ ורק אז לעשות merge commit. אם בנקודה הזאת נעבור ל README.txt משהו שנראה כמו diff, יהיה שם קטע ערוך ובו השורות קוד של בוב, חוצץ (בדמות ====), והשורות קוד של אליס (כל זה בין »» ל ««). ונצטרך לערוך את הקוד ידנית כדי לתקן אותו, נמחק את הקוד המיותר, וכשסיימנו נשמור ונכתוב בשורת הפקודה git commit -a (או git add -all) והמערכת תפתח את העורך, המערכת תכתוב לנו את ההודעה שתהיה ב commit (כי זהו merge commit) ואפילו תציג את הקבצים איתם היה קונפליקט ותיקנו, כשנשמור הענף יראה כמו בתמונה של שלב 4 בתרחיש הראשון, וכשנבצע git push ה origin/master יסתנכרן (יצביע) ל master.

 

שאלות ותשובות

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

שאלות:

  1. ביצעתם commit לעבודה שעשיתם, הגיע הזמן לשתף אותה, דחפו אותה לשרת של החברה שלכם (הניחו שזאת לא פעם ראשונה שאתם דוחפים קוד, לכן כבר שמרתם את כתובת ה repo המרוחק).
  2. אופס, הדחיפה שלכם נדחתה, נראה שחבר שלכם לצוות עשה עבודה ודחף אותה, וה repo שלכם כבר אינו מעודכן. סנכרנו את הענף שלכם.
  3. גיט מדווחת על קונפליקט עם השינויים של חברכם לצוות בקובץ index.html, תשמרו את השינויים שלכם, וכתבו תיאור של השינויים שלו כהערה (comment). להלן תמונה של תוכן הקובץ.
  4. הוסיפו את השינויים שעשיתם (בפתירת הקונפליקט שהיה ב index.html) ל commit.
  5. לבסוף, בצעו commit לשינויים במיזוג (אלה שביצעתם כדי לפתור את הקונפליקט), אל תשכחו להוסיף תיאור כדי שנדע על מה ה commit.

תשובות:

  1. הפקודה: git push.
  2. הפקודה: git pull.
  3. התמונה של תוכן הקובץ (index.html) אחרי העריכה.
  4. הפקודה: git commit -a או git add -all.
  5. הפקודה: "git commit -a -m "Merge changes.

 

סיכום ביניים

עברנו על שני תרחישים לשיתוף פעולה מורכב בין חברי צוות, ראינו בדיוק את התהליך שמתבצע כשאנו כותבים את הפקודה git pull</span>’`, ומה צריך לעשות כדי להתגבר על שינויים שבוצעו בו זמנית בקבצים שונים, ואף שינויים שבוצעו באותו הקובץ.

 

ענפים מרוחקים

בחלק הקודם דיברנו על ענפים מקומיים, כאלה שאנחנו יוצרים ב repo המקומי שלנו, אך מה לגבי ענפים בפרויקטים מרוחקים? (remote branch). יכול להיות מצב בו יהיה לנו ענף, נניח ענף admin, ואנחנו רוצים לאפשר לאנשים אחרים לעבוד עליו. אנחנו נצטרך לדחוף את הענף הזה כדי שאנשים אחרים גם יוכלו לגשת אליו, ולעבוד אליו עד שניתן למזגו עם master. בנוסף, אם אנחנו עובדים על ענף עם פרויקט די גדול, שימשך קצת זמן, בואו נגיד כמה ימים ומעלה, כנראה שנרצה לגבות אותו, שישמר בעוד מקום מעבר לרק ב repo המקומי שלנו. דחיפת הענף עונה גם על צורך זה ומאפשרת לגבות אותו, לדוגמה ב GitHub.

נניח שאליס רוצה לעבוד על אזור חדש באתר בשם profile. היא יצרה ענף חדש בשם זה בעזרת הפקודה git checkout -b profile (כדי ליצור את הענף ולעבור אליו בפקודה אחת), וכדי לדחוף את הענף ל GitHub כתבה git push origin profile. וזה בעצם מקשר בין הענף המקומי לענף המרוחק (המגובה ב GitHub) ומתחיל לעקוב אחריו.

$ git checkout -b prfile
  Switched to a new brnach ‘progile’

$ git push origin profile
  Counting objects: 10, done.
  Delta compression using up to 4 threads.
  Compressing objects: 100% (6/6), done.
  writing objects: 100% (6/6), 619 bytes, done.
  Total 6 (delta 2), reused 0 (delta 0)
  To http://github.com/nirgn975/test.git
    * [new branch]     profile -&gt; profile
GitHub Branches Tags

בואו נגיד שאליס עשתה כמה שינויים, הוסיפה דף בשם profile.html עם הפקודה git add profile.html, ביצעה לו commit בעזרת הפקודה "git commit -a -m "Create profile page, ואז נבצע push, כי זה ענף שאחריו המערכת כבר עוקבת (עם הפקודה git push) ולכן הוא יודע לדחוף את הענף profile המקומי לענף profile המרוחק. אם נקפוץ לאתר GitHub, נשים לב שבתוך הפרויקט שלנו ישנו כפתור בשם -branch”, כשנלחץ עליו תפתח לנו רשימה של כל הענפים (בתמונה משמאל ניתן לראות רק ענף אחד, בשם “gh-pages”), כשנלחץ על הענף נראה את תיאור ה commit האחרון ומתחתיו את הקבצים שנמצאים בענף).

$ git add profile.html
$ git commit -a -m "Create profile page"
  [profile 2a0dbf9] Create profile page
  1 file changed, 1 insertion(+)
  create mode 100644 profile.html

$ git push
  Counting objects: 4, done.
  Delta compression using up to 4 threads.
  Compressing objects: 100% (2/2), done.
  Writing objects: 100% (3/3), 302 bytes, done.
  Total 3 (delta 1), reused 0 (delta 0)
  To https://github.com/nirgn975/test.git
    786d7a1..2a0dbf9     profile -&gt; profile

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

כשבוב יבצע git pull המערכת תראה לו (בפלט) שישנו ענף מרוחק חדש בשם profile. אם הוא יכתוב git branch הוא לא יראה אותו כענף מקומי, אך אם הוא יכתוב git branch -r הוא יראה את הרשימה של כל הענפים המרוחקים (ואת profile בינהם). הוא יוכל לכתוב git checkout profile כדי להתחיל לעבוד על הענף, המערכת, באופן אוטומטי (כתוצאה מכתיבת הפקודה), תוריד את הענף, ותתחיל לעקוב אחריו כענף מרוחק. לכן מפה הוא יכול לבצע את העבודה שלו, וכשהוא מסיים לדחוף את העבודה לענף המרוחק באמצעות הפקודה ‘git push.

אחת הפקודות השימושיות ביותר לעבודה עם ענפים מרוחקים היא הפקודה git remote show, כשנכתוב git remote show origin (כש origin הוא השם של ה repo המרוחק), המערכת תראה לנו את כל הענפים המרוחקים בפרויקט, האם המערכת עוקבת אחריהם או לא, את הענפים המקומיים שלנו ולאיזה ענפים מרוחקים הם מסונכרנים (או מתמזגים), ולבסוף, זה יראה לנו למה הענפים המקומיים שלנו מוגדרים כשנבצע git push. בנוסף, כשאנו נכתוב את הפקודה, המערכת אפילו תיגש לשרת ותבדוק אם אחד מהענפים המקומיים שלנו אינו עדכני.

$ git remote show origin
  * remote origin
    Fetch URL: https://github.com/nirgn975/test.git
    Push  URL: https://github.com/nirgn975/test.git
    HEAD branch: master
    Remote branches:
      master  tracked
      profile tracked
    Local branches configured for 'git pull':
      master  merges with remote master
      profile merges with remote master
    Local refs configured for 'git push':
      master  pushes to master  (up to date)
      profile pushes to profile (local out of date)

 

הסרה של ענפים מרוחקים

ענפים מרוחקים, בדיוק כמו ענפים מקומיים, אינם כאן כדי להישאר לנצח. כדי למחוק ענף מרוחק נכתוב את הפקודה <git push origin:<branchName ז”א במקרה שלנו אם נרצה למחוק את profile נכתוב git push origin :profile. שימו Kב שהפקודה מוחקת אך ורק את הענף המרוחק, הענף המקומי עדיין יהיה קיים. בסעיף הקודם ראינו שכדי למחוק את הענף המקומי נכתוב git branch -d profile, אך אם נכתוב זאת כרגע, נקבל הודעת שגיאה, המערכת לא תתן לנו למחוק את הענף המקומי, למה? כי המחיקה היא של הענף בלבד, המערכת תציין כי ישנם commitים שלא מזגנו לשום מקום (אם באמת נרצה למחוק את הענף, כולל את ה commitים שהוא מכיל נכתוב git branch -D profile).

אז בואו נניח שבוב מחק את הענף. אבל הרי אליס היא זאת שיצרה אותו מלכתחילה. מה יקרה אם אליס תנסה לדחוף שינויים ל remote שלא קיים יותר? נניח שאליס עשתה כמה שינויים ומבצעת commit, לדוגמה "git commit -m -a "Add ability to change city ולאחר מכן היא מנסה לעשות git push כדי לעדכן את ה commit בענף המרוחק. git תחזיר תשובה “Everything up-to-date”, בגלל שהענף המרוחק לא קיים, העדכון מתבצע בענף המקומי בלבד ואינו עולה לשרת ב GitHub. כדי לבדוק מה הולך כאן כנראה שאליס תריץ את הפקודה git remote show origin ותראה שהענף המקומי שלה, profile, ישן (באנגלית: stale), מישהו מחק אותו. כדי למחוק את ההפניה המקומית הזאת היא תריץ git remote prune origin ו git תנקה את כל ההפניות והענפים הישנים.

 

Tags

טאגים (tags) הם בעצם התייחסות ספציפית ל commit ספציפי, זהוי דרך טובה לקפוץ למצב בו הקוד היה במצב כלשהו בזמן כלשהו, והרבה מהאנשים משתמשים בזה למספור של גרסאות שחרור. ז”א שאם הקוד שלנו במצב טוב, סיימנו פיצ’ר מרכזי וכד’, אולי נשייך אליו tag בשם v0.1 (או משהו כזה).

אז כדי ש git תדפיס לנו רשימה של כל ה טאגים הקיימים בפרוייקט ניתן לכתוב git tag. כדי לבדוק את אחד מהטאגים ולראות איך הקוד היה באותו הזמן נכתוב <git checkout <tagName, ז”א שאם נמשיך עם הדוגמה של מס’ הגרסה נכתוב git checkout v0.1. כדי להוסיף טאג חדש נכתוב <git tag -a <newTagName> -m <tagDescription, לדוגמה <git tag -a v0.2 - "Version 0.2, וכדי לדחוף את הטאגים שלנו נצטרך לכתוב git push -tags, אחרת הטאגים רק ישארו מקומיים.

עכשיו אם תעלו למעלה, לצילום מסך מ GitHub, תראו שליד ה “Branches” ישנו חוצץ בשם “Tags”, אם נלחץ עליו נראה את רשימת כל הטאגים ב repo. אם נלחץ על אחד מהטאגים הדף יטען מחדש ונראה את המצב בו הקבצים והקוד היו באותו הטאג.

 

שאלות ותשובות

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

שאלות:

  1. יצרתם ענף חדש בשם checkout לאזור חדש באתר קניות, פיצ’ר העגלה מוכן וביצעתם לו commit בענף checkout. כעת, פרסמו את הענף הנ”ל ב repo הראשי (בשם origin).
  2. חבר שלכם לצוות הוסיף ענף חדש, תמשכו אותו מה repo המרוחק.
  3. מצוין, משכתם את כל הענפים החדשים, אך חברכם לצוות לא אמר לכם מהו שם הענף החדש, הדפיסו רשימה של כל הענפים.
  4. הפרויקט של חבר שלכם בוטל, תמחקו את הענף (שם הענף blaBla, הקפידו למחוק רק את הענף המרוחק, לא המקומי).
  5. חכו שנייה, כבר משכתם את הענף ל repo המקומי שלכם. תבדקו איזה ענפים נמצאים אצלכם בפרויקט ולאיזה ענפים מרוחקים הם מסונכרנים.
  6. יש לכם עדיין התייחסות לענף blaBla שמחקנו מקודם, נקו את כל ההפניות והענפים הישנים.
  7. עכשיו כשהפרויקט בוטל, אנחנו רוצים לפרסם גרסה חדשה של האתר, אך איזה גרסה נכתוב? תוציאו רשימה של הטאגים כדי שתוכלו לראות את מס’ הגרסה האחרון.
  8. אה אוקי, הגרסה האחרונה הייתה v1.0.5, לכן הגרסה הזו תיהיה v1.0.6, צרו טאג לפיכך.
  9. דחפו את הטאג ל origin.
  10. טעות שלי, חזרו לקוד של הטאג v1.0.5.

תשובות:

  1. הפקודה git push origin checkout. בגלל שהמערכת אינה עוקבת אחר הענף (הוא ענף מקומי בלבד) אנחנו צריכים לפרט את שם ה repo שאליו אנחנו דוחפים (אליו מתבצע הקישור) ואת שם הענף.
  2. הפקודה git fetch תמשוך את כל הענפים החדשים.
  3. הפקודה git branch -r.
  4. הפקודה git push origin :blaBla.
  5. הפקודה git remote show origin.
  6. הפקודה git remote prune origin.
  7. הפקודה git tag.
  8. הפקודה "git tag -a v1.0.6 -m "diss tag.
  9. הפקודה git push -tags.
  10. הפקודה git checkout v1.0.5.

 

סיכום

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