Git - חלק 6

בחלק השישי של סדרת Git אסביר כיצד למחוק ולשחק עם ההיסטוריה של ה repo, קצת על עבודת צוות במערכות שונות, ולבסוף על פקודה חדשה הנקראת cherry-pick.

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

 

מחיקת היסטוריה

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

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

אז דבר ראשון שנעשה לפני שאנחנו משכתבים את ההיסטוריה, זה גיבוי. נכתוב לדוגמה ‘git clone proName proName-filter’, ויש לנו גיבוי למקרה שאנחנו בטעות מוחקים commits שלא התכוונו. כדי לשכתב את ההיסטוריה ניעזר בפקודת filter-branch עם האופציה tree-filter כך ‘<git filter-branch –tree-filter <command’, גיט תוציא את כל ה commits מהתיקייה שלנו, לתיקייה אחרת, תריץ עליה את הפקודה () ולבסוף תבצע commit מחדש לקוד.

במקום הניתן לכתוב כל פקודה של ה shell (מטעפת). אז בוב יעבור ל repo המשוכפל עם הפקודה ‘cd proName-filter’ ויכתוב ‘git filter-branch –tree-filter ‘rm -f passwords.txt’ – –all’. הפקודה תעבור על כל commit ותסיר כל קובץ בשם “passowrds.txt”. מה שה “all– –” בפקודה עושה זה להריץ את הפקודה על כל ה commits בכל הענפים. בנוסף, במקום “all” ניתן לכתוב “HEAD” אם אנחנו רוצים שהפילטר הנ”ל ירוץ רק בענף הנוכחי.

אז מה הסיפור של ה “f-“ בפקודה? אם לא נכתוב אותה, ב commits בהם הקובץ passwords.txt לא יהיה פקודת המחיקה תיכשל, ואז כל פקודת ה git תיכשל. וה “f-“ מוודא שגם אם הקובץ לא קיים, הפקודה לא תחזיר כישלון, ואז פקודת הפילטר שלנו תמשיך לרוץ.

כמו שאתם מדמיינים (או ראיתם אם כבר הרצתם את הפקודה), בדיקת כל commits והרצת פקודת המחיקה מולו ואז ביצוע חוזר של ה commit, לוקח די הרבה זמן, במיוחד אם הפרויקט גדול. לכן ישנה אפשרות נוספת בה אנחנו יכולים להשתמש והיא נקראת index-filter, ונכתבת כך ‘<git filter-branch –index-filter <command’. גיט תריץ את הפקודה מול כל commit, אך היא לא תוציא כל קובץ וקובץ מחוץ ל repo. זה אומר שהפקודה שלנו ‘rm -f passwords.txt’ לא תעבוד, לכן מה שאנחנו צריכים לעשות במקרה הזה הוא לכתוב פקודת גיט ולא פקודת מעטפת, במקרה הזה הפקודה ‘‘git filter-branch –index-filter ‘git rm –cached –ignore-unmatch passwords.txt’ תתאים. מה שה “ignore-unmatch–” הפקודה עושה בדיוק מה ש “f-“ עושה בפקודת המעטפת.

חשוב לציין לציין, שאם בוב ינסה להריץ את הפקודה פעם שנייה הוא יתקל בשגיאה. מכיוון ש git משאירה גיבוי של כל העץ בתיקיית גיט (git.). אז מה שצריך לעשות אם אנחנו רוצים להריץ את הפקודה בפעם השניה הוא לוודא שאנחנו מפרטים את האופציה “f-“ כך ‘‘git filter-branch -f –tree-filter ‘rm -f passwords.txt’ – –all’. ה “f-“ בין ה filter-branch ל tree-filter מייצגת את המילה “force” שמכריחה אותו לעלות על הגיבוי (שאחרת הוא יתן הודעת שגיאה שקיים גיבוי קודם).

כשאנחנו מוחקים מידע מההיסטוריה, יכול להיות מצב בו חלק מה commits שלנו ישארו ריקים. מה שלא טוב, הרי למה שנרצה להשאיר commit אם הוא לא עושה כלום לקוד שלנו. אז איך אפשר להיפטר מה commit הריק? אפשר לכתוב ‘git filter-branch -f –prune-empty – –all’ שתעבור על כל ה commits ותמחק commits ריקים, כאלו שלא משנים כלום בקוד. ניתן גם לשלב אותה ישירות בפקודה הראשית ‘git filter-branch –tree-filter ‘rm -f passwords.txt’ –prune-empty – –all’ שעל הדרך תיפטר מכל ה commits הריקים.

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

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

שאלות: אופס, בטעות ביצעתם commit לסיסמה הראשית של שרת החברה. אתם צריכים למחוק את זה מההיסטוריה. קודם כל צרו עותק של ה repo שלכם (נקרא לה bla) ניתן לקרוא לעותק איך שתרצו אך למען הסדר הטוב קראו לה blabla. הסיסמה שביצעתם לה commit נמצאת בקובץ master_password.txt, השתמשו ב filter-branch ומחקו אותה מכל ה commits (בהנחה וכבר עברתם ל repo המשובט). ה repo כל כך גדול, השימוש ב filter-branch יקח כל היום. במקום, השתמשו ב index-filter להסיר את הקובץ של הסיסמה (master_password.txt). הרגע קלטתם שיש קובץ בשם master_username.txt שאצריך להסיר גם אותו, רק כדי שנהיה בטוחים. הסירו אותו (אל תשכחו ש git יצרה גיבוי של העץ). מצוין, רק שהבנתם שחלק מה commits שלכם כבר לא מכילים כל שינוי. מחקו אותם..

תשובות: הפקודה: ‘git clone bla blabla’. הפקודה: ‘git filter-branch –tree-filter ‘rm -f master_password.txt’ – –all’. הפקודה: ‘git filter-branch –index-filter ‘git rm –cached –ignore-unmatch master_password.txt’. בגלל שבהסבר של הפקודה, למעלה, נתתי את הפקודה עם tree-filter, כעת אכתוב אותה עם Index-filter. להלן הפקודה: ‘‘git filter-branch -f –index-filter ‘git rm –cached –ignore-unmatch master_username.txt’. הפקודה: ‘git filter-branch -f –prune-empty – –all’.

סיכום ביניים

אז נזכרנו שוב כיצד לשכפל repo. ולמדנו כיצד לשכתב את ההיסטוריה של ה commits ע”י מחיקה של קבצים שבוצעו להם commits בשתי דרכים שונות. ראינו כיצד המחיקה עובדת, שמנו לב שהמערכת יוצרת גיבוי של העץ לאחר המחיקה, דבר שמקשה על מחיקה נוספת (פעם שנייה ואילך), ולבסוף ניקינו את העץ מ commits ריקים.

עבודת צוות

מערכות הפעלה שונות משתמשות לפעמים במפרידי שורה שונים. לינוקס ו OSX משתמשות ב LF (ראשי תיבות של LineFeed, ומיוצג לפעמים ע”י “n"), בעוד ווינדוס משתמשת ב CR (ראשי תיבות של Carriage Return) ואחריו LF (או “r" ואחריו “n"). כשיש חברי צוות שמשתמשים באיזו שהיא הפצת לינוקס או OSX, וחלק מחברי הצוות משתמשים ב windows מתעוררת בעיה כשיש להם מפרידי שורה בקובץ. אם לדוגמה נכתוב קוד במסמך בלינוקס, ואז נפתח אותו בווינדוס, כמו שניתן לראות בתמונה משמאל, ווינדוס לא יזהה את מפרידי השורה.

למזלנו גיט מגיע עם כמה אופציות המאפשרות לנו להתמודד עם התופעה. אם אתם על מערכת הפעלה דמויית יוניקס, לינוקס או OSX וכד’. הריצו את הפקודה ‘git config –global core.autocrlf input’ והמערכת תוודא שלכל קובץ שאתם מבצעים commit יש רק LF. בווינדוס כתבו ‘git config –global core.autocrlf true’ והמערכת תידע להפוך כל LF ל LF ואחריו CR, אך כשמשתמשי ווינדוס יבצעו commit המערכת תשמור את הקבצים רק עם LF.

אם אתם עובדים בצוות שמשתמש אך ורק בווינדוס ולא צריך לדאוג לזה, אתם יכולים לכתוב ‘git config core.autocrlf false’ והמערכת פשוט תשאיר את ה CR אחרי ה LF כשתבצעו commit.

בנוסף, אתם לא חייבים לסמוך על כולם שיקבעו את הסידורים האלו אצלם, במקום זה אתם יכולים ליצור קובץ “gitattributes.” והוא יושב בתיקיית האב (root) של הפרויקט. תוכן הקובץ יראה כמו בתמונה משמאל, כשמצד שמאל יהיו סוגי הקבצים ומצד שני (ימין) יהיו את הגדרות ההמרה (כמו בתמונה משמאל).

קודם כל, בואו נדבר על סוג קובץ, שנמצא בצד השמאלי בתוכן של הקובץ. אם יש * זה (הכלל אם תרצו) הולך להתאים לכל קובץ עם סיומת זו (ללא תלות בשם הקובץ). בצד הימני של המסמך יש לנו את הגדרות ההמרה, שמאפשר לנו לתאר כיצד קובץ ספציפי יטופל. במקרה הראשון שבו כל הקבצים מטופלים כ “text=auto” המערכת תבדוק האם הקובץ הוא קובץ טקסט, היא תמיר אותו באופן נכון. בשורה הבאה בה כתוב “text” המערכת תטפל בהם כקבצי טקסט ותמיר את הקבצים ל LF או LF ואז CR (בהתאם למערכת ההפעלה) כשתצאו מהקוד, אך תחזיר אותו לרק LF כשתחזרו אליו.

אם נרצה לתאר באופן ספציפי איך קבצי טקסט ימרו, נוכל להיעזר בהגדרות כמו “text eol=crlf” ו “text eol=lf” כש eol מייצג את End Of Line. אז בעצם אם תכתבו “text eol=crlf” המערכת תמיר לפורמט שבחרתם (במקרה הזה תוסיף CL) כל פעם שאתם יוצאים מהקוד, ותחזיר אותו ל LF כשאתם חוזרים לקוד (ככה שמשתמשי ווינדוס יכולו לראות את הקוד בצורה תקינה). וה “text eol=lf” פשוט תוודא שאין ולא יהיה CR בקוד שלכם. ולבסוף, אופציית ה “binary” תטפל בקובץ כקובץ בינארי רגיל כך שהמערכת לא תנסה לעשות כל שינוי/המרה שהיא בקובץ.

cherry-pick

בואו נניח לרגע שאנחנו עובדים בענף ה production שלנו, ואנחנו צריכים חתיכה של קוד שכתבנו בקובץ הנמצא בענף אחר, נקרא לו development, והקובץ הזה נמצא ב commit באמצע הרשימה של שאר ה commits בענף. מה שאנחנו צריכים לעשות נקרא cherry-pick, ז”א לאסוף אותו ולשים אותו בענף production. בצד שמאל ניסיתי ליצור תמונה שתמחיש באופן וויזואלי, מצד שמאל יש את המצב הקיים, ובצד ימין יש את הפעולה שאנחנו רוצים לבצע.

כדי לבצע את זה, דבר ראשון נעבור לענף production (אם היינו בענף אחר או אפילו רק בשביל לוודא שאנחנו בענף הנכון) עם הפקודה ‘git checkout production’. ואז נכתוב ‘git cherry-pick 53212e5’ (בהנחה כמובן שה SHA של ה commit הוא 53212e5). עכשיו אם נכתוב ‘git log’ נראה את ה commit כ commit האחרון (בענף production). אם תשימו לב, תראו ש SHA של ה commit בענף production אינו כמו ה commit בענף development, ה SHA השתנה, והוא השתנה אחרי ההעתקה בגלל שיש לו אבא אחר.

לפעמים, כדי לבצע דברים שונים כמו לתת תיאור שונה ל commit שאנחנו מעתיקים (מבצעים לו cherry-pick). במקרה כזה נשתמש בפקודה ‘git cherry-pick –edit 53212e5’, לאחר שנכתוב אותה יפתח לנו עורך טקסט שם נוכל לערוך את תיאור ה commit. ולפעמים אף נרצה לאסוף כמה commits ביחד ולמזג אותם ביחד כ commit אחד בענף שלנו. כדי לבצע את זה נכתוב ‘git cherry-pick –no-commit 53212e5 55ae374’, בעזרת האופציה “no-commit” משכנו את שתי ה commits המתוארים בפקודה והוספנו אותם לאזור ההיערכות אך לא ביצענו להם commit, ז”א שכל השינויים של שתי ה commits האלו, יחדיו, באזור ההיערכות. בהוספת השינויים לאזור ההיערכות אנחנו בעצם מוסיפים אותם ל HEAD הנוכחי שלנו, אז אם נכתוב ‘git status’ נראה את השינויים של שני ה commits. אז עכשיו נשאר רק לבצע commit לשינויים שבאזור ההיערכות, כ commit רגיל.

  • אופציית ה “no-commit” שימושית מאוד גם כשאנו רוצים לבחור commit אך לא להעתיקו כמו שהוא אלה לבצע בו כמה שינויים קטנים לפני ה commit.

כשאנחנו מבצעים cherry-pick חשוב מאוד לעקוב מאיפה ה commit הגיע. אחת הדרכים לעשות זאת היא להשתמש ב אופציית “x-“ כך לדוגמה ‘git cherry-pick -x 53212e5’. ה”x-“ תכניס לתוך תיאור ה commit את ה SHA של commit המקורי, כך שכשנכתוב ‘git log –oneline’ (ה oneline נועד לקיצור של ה log לשורה אחת למי שלא זוכר) נראה בתיאור ה commit החדש סוגריים בהם יהיה כתוב לנו שה commit נאסף (cherry picked) מה commit ואז את ה SHA שלו.

חשוב לציין שכשאנחנו מבצעים cherry-pick ל commit, כתוב ה commit מועבר גם הוא ל commit החדש, ז”א שהוא זה שביצע את ה commit החדש, אבל הרי הוא ביצע אותו בענף אחר, לכן כדאי גם לעקוב אחרי מי שביצע את ה cherry-pick ל commit. לכן כדי להוסיף את המשתמש שביצע את ה cherry-pick נוסיף “–signoff” לפקודה כך ‘git cherry-pick –signoff 53212e5’. וזה יוסיף לתיאור שורה של “:Signed-off-by”.

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

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

שאלות: החברה שלכם שכרה מפתח חדש שעובד על ווינדוס, ושמתם לב שלכמה קבצים חסרים מפרידי שורה. כתבו פקודה גלובאלית שתוודא שלכל קובץ שאתם מבצעים commit המערכת תהפוך את מפרידי השורה לצורה בה מפרידי השורה במערכת UNIX. מכיוון שאתם אנשים נחמדים, החלטתם ללכת לעזור למפתח החדש להפוך את כל מפרידי השורה שיעבדו על המערכת שלו. כתבו פקודה גלובאלית כך שהמערכת תהפוך את כל מפרידי השורה מהצורה בה הם מתבצעים ב UNIX לצורה בה הם מתבצעים ב Windows. אחרי שהגיעו עוד כמה מפתחים בווינדוס, החלטתם שמספיק והגיע הזמן להגדיר קובץ הגדרות (gitattributes.) שיטפל בכל הקבצים, ובאופן ספציפי יש לטפל בהבדל בין קבצי bat. של ווינדוס ל sh. של יוניקס. אתם עובדים על פיצ’ר חדש של מנורה, ומצאתם תיקון לבאג שגרם לנורה להבהב באופן רנדומלי. אתם ממש רוצה את ה commit של התיקון הזה בענף master אצלכם, אספו אותו בעזרת cherry-pick כשה SHA שלו הוא 3fbd473. הבחנתם שיש ל commit תיאור מעורפל, בצעו שוב cherry-pick והפעם ערכו את תיאור ה commit. נאלצתם לעשות עוד commit (עם SHA קוד b447335) שתיקן באג שגרם למנורה לדהות, והקוד SHA החדש (מה commit שעשיתם לו cherry-pick קודם לכן) הוא b59d285. מזגו את הקוד של ה commits האלו ביחד לאזור ההיערכות. התבצעו הרבה cherry-pick בזמן האחרון, בצעו cherry-pick ל commit הבא ושמרו בתיאור מאיפה הוא הגיע (SHA קוד bdf9578). החלטנו שזה רעיון טוב לשים את השם שלך על ה commit שאתה מבצע להם cherry-pick. התחילו עם ה commit הבא (SHA קוד bdf9578).

תשובות: הפקודה: ‘git config –global core.autocrlf input’. הפקודה: ‘git config –global core.autocrlf true’. תוכן הטקסט של קובץ ההגדרות נמצא בתמונה. הפקודה: ‘git cherry-pick 3fbd473’. הפקודה: ‘git cherry-pick –edit 3fbd473’. הפקודה: ‘git cherry-pick –no-commit b447335 b59d285’. הפקודה: ‘git cherry-pick -x bdf9578’. הפקודה: ‘git cherry-pick –signoff bdf9578’.

סיכום

למדנו כיצד לשכתב את ההיסטוריה של ה commits ע”י מחיקה של קבצים שבוצעו להם commits בשתי דרכים שונות (ראינו כיצד המחיקה עובדת, שמנו לב שהמערכת יוצרת גיבוי של העץ לאחר המחיקה, דבר שמקשה על מחיקה נוספת (פעם שנייה ואילך), ולבסוף ניקינו את העץ מ commits ריקים). לאחר מכן עסקנו בשיתוף הפעולה בצוותים על מערכות שונות, ובפרט במפרידי השורה של מערכות UNIX מול מערכות Windows. ראינו כיצד להתאים את מפרידי השורה בכל מערכת ולבסוף אף ליצור קובץ כללי שינהל את ההגדרות של הפרויקט. לבסוף, עברנו לנושא cherry-pick שבו העתקנו commits מענף לענף (ראינו איך ניתן להעתיק כמה commits ביחד, לערוך תיאור של commit, לראות מי ביצע את ה cherry-pick, לשמור מאיפה ה commit הגיע). אם ישנם שאלות, תיקונים, הבהרות וכ’ו אשמח לענות בתגובות.