├── .gitignore ├── 01-introduction └── 01-chapter1.markdown ├── 02-git-basics └── 01-chapter2.markdown ├── 03-git-branching └── 01-chapter3.markdown ├── 04-git-server └── 01-chapter4.markdown ├── 05-distributed-git └── 01-chapter5.markdown ├── 06-git-tools └── 01-chapter6.markdown ├── 07-customizing-git └── 01-chapter7.markdown ├── 08-git-and-other-scms └── 01-chapter8.markdown ├── 09-git-internals └── 01-chapter9.markdown └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | progit.*.pdf 2 | progit.*.mobi 3 | progit.*.epub 4 | progit.*.html 5 | progit-*.epub 6 | *.swp 7 | latex/* 8 | !latex/makepdf 9 | !latex/README 10 | !latex/template.tex 11 | !latex/config.yml 12 | epub/temp 13 | *~ 14 | .#* 15 | figures/* 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /01-introduction/01-chapter1.markdown: -------------------------------------------------------------------------------- 1 | ## เริ่มต้นปฐมบทแห่ง Git ## 2 | บทนี้เป็นบทแห่งการเริ่มต้นที่จะบอกเล่าเรื่องราวที่มาที่ไปของสิ่งที่เรียกว่า version contol tools จากนั้นเราก็จะถูกดึง มาเข้าเรื่อง Git ว่าเราจะไปทำยังไงให้ Git มาอยู่ในเครื่องเราได้ ทำไงให้มันทำงานได้และเมื่อจบบทนี้เราจะได้รู้ว่า ทำไมต้อง Git นั่นสิทำไม? 3 | 4 | ## เกี่ยวกับ Version Control ## 5 | อะไรคือ version control แล้วทำไมต้องใช้ด้วย บางคนอาจบอกว่า “ก็จำได้ว่าทำอะไรไปมีไรป่ะ” แต่สุดท้ายผ่านไปสองเดือนก็ลืมหมด version control คือระบบที่บันทึกการเปลี่ยนแปลงทั้งหมดที่เกิดขึ้นกับไฟล์หรือชุดของไฟล์ที่เรากำลัง ทำงานกับมันอยู่และเมื่อมีการบันทึกการเปลี่ยนแปลง ทุกครั้งที่เกิดปัญหาแก้ไฟล์มั่วจำไม่ได้ เราก็สามารถย้อนกลับ ไปหาเวอร์ชั่นใดๆก่อนหน้าได้เสมอ โดยที่ตัวอย่างที่จะใช้ในหนังสือเล่มนี้จะเป็นไฟล์ที่ทรัพย์สินอันสำคัญเช่น source code จะถูกควบคุมโดย version control แต่อย่างไรก็ตามสำหรับการนำไปใช้งานจริงนั้นเราสามารถประยุกต์ใช้กับ ไฟล์ประเภทไหนก็ได้ 6 | เช่นถ้าคุณเป็น graphic designer และอยากจะเก็บเวอร์ชั่นของภาพที่เราทำไปให้หมดทุกอัน สิ่งที่ช่วยเรื่องเหล่านี้ ได้คือ Version Control System (VCS) มันคือสิ่งที่คุณ “ต้อง” ใช้เพราะมันจะช่วยให้เราถอยไปถอยมาได้ไม่ว่าจะเป็น ระดับไฟล์หรือระดับโปรเจค ถ้ามันเละมากแก้มั่วไปหมดก็สามารถถอยกลับไปได้ วื๊บๆ ดังนั้นไอ้พวกข้ออ้างไฟล์หาย ไฟล์พังเพื่อขอเลื่อนโปรเจคหรือส่งงานข้าจะใช้ไม่ได้อีกต่อไป -/\- 7 | 8 | 9 | ### Local Version Control Systems ### 10 | 11 | ย่อหน้าที่แล้วมีคำว่า VCS เยอะมากเอ๊ะแล้วจริงๆเขาทำด้วยอะไรกัน? คำตอบเริ่มจากทำกันบ้านๆก็ได้ไม่ต้องคิดมาก copy file ที่ต้องการไว้บ่อยๆจะแก้ก็ copy ทิ้งไว้ก่อนแล้วตั้งชื่อ directory ให้สอดคล้องกัน ทำไปเรื่อยๆนี่ไง VCS กราบส์ OTL ทำแบบนี้ก็ไม่ผิดแต่มันเยอะส์บางทีลืมบ้างอะไรบ้างก็บ้าบอกันได้เลยทีเดียวเพราะไม่รู้อันไหนแน่ที่ต้อง เอากลับมาใช้และเพื่อแก้ไขปัญหาเรื่องนี้โปรแกรมเมอร์ (อีกแล้วไม่ใช่ tester) ได้สร้าง VCS ที่มี database แปะมาด้วยสำหรับการเก็บประวัติการเปลี่ยนแปลงทั้งหลายทั้งปวง ดังรูป(ดู Figure 1-1). 12 | 13 | Insert 18333fig0101.png 14 | Figure 1-1. Local version control diagram. 15 | 16 | หนึ่งใน VCS ในตำนานที่ยังคงอยู่ค้ำฟ้ามาจนถึงปัจจุบันคือ rcs ยกตัวอย่างเช่นใน Mac OS X เองก็ยังคงมีคำสั่ง rcs ให้เราใช้งานได้หลังจากที่เราติดตั้ง Developer Tools ลงในเครื่องโดยที่เจ้า rcs นี้จะทำหน้าที่เก็บความแตกต่างของไฟล์หรือที่เรียกว่า patch sets เพื่อใช้สำหรับการสร้างไฟล์ขึ้นมาใหม่ในกรณีที่เกิด file lock หลังจากที่เราลง patch ใหม่ลงไปในเครื่องดังนั้น rcs ก็จะเป็นตัวอย่างที่ดีของการทำ local repository แต่ของแบบนี้ก็ดีถ้าเราทำอะไรคนเดียว 17 | 18 | ### Centralized Version Control Systems ### 19 | ในกรณีที่เราต้องการทำงานร่วมกับคนอื่นๆแล้วเราจะแบ่งปันโค้ดเทพของกันและกันได้อย่างไรนี่ไอ้ของแบบ Local Repo คงไม่เหมาะเท่าไหร่ ดังนั้นของใหม่ที่เกิดถัดมาคือ Centralized Version Control System (CVCSs) และตัวอย่างของระบบแบบนี้คือ CVS, Subversion และ Perforce นั่นเองซึ่งหัวใจหลักของการทำงานแบบนี้คือจะต้องมี server หนึ่งตัวที่รับหน้าที่เก็บของให้ทั้งหมด ทั้งไฟล์ที่เกิดการเปลี่ยนแปลงและจำนวน user ที่ check out ไฟล์จาก server และอย่างที่เรารู้ว่า Centralize Repo เป็นมาตรฐานของ VCS มานานหลายปี (ดู Figure 1-2). 20 | 21 | 22 | Insert 18333fig0102.png 23 | Figure 1-2. Centralized version control diagram. 24 | 25 | สำหรับการ setup ระบบแบบ Centralize เองก็มีข้อดีหลายอย่างมากที่ดีกว่า local VCSs ยกตัวอย่างเช่นทุกๆคนในทีมจะรู้ว่าคนอื่นๆในทีมที่เหลือกำลังทำอะไรอยู่ ส่วนผู้ที่ดูแลระบบก็สามารถจัดการกับสิทธิ์การทำงานของ user ทุกคนได้ซึ่งนี้ก็เป็นข้อดีที่เหนือกว่าการมานั่งกำหนดอะไรอะไรที่ local database ทีละเครื่องมากมายนัก 26 | 27 | 28 | แต่อย่างไรก็ตามการทำงานในลักษณะนี้เองก็มีข้อเสียที่น่าสนใจอยู่หนึ่งอย่างและดูเหมือนว่าจะร้ายแรงมากคือในกรณีที่ server พังไปสักชั่วโมงจะทำให้คนที่ทำงานอยู่ไม่สามารถส่งอะไรเข้ามา update ได้เลยและถ้าเอาให้หนักหน่อยในกรณีที่ไม่มีการสำรองข้อมูลไว้เราจะพบกับฝันร้ายขั้นร้ายแรงคือ เราจะเสีย history ทั้งหมดของระบบไปหมดเลย (แรงดีเนอะ) ดังนั้นนี่คือด้วนมืดของ Centrlaized Repo 29 | 30 | ### Distributed Version Control Systems ### 31 | 32 | หลังจากที่เราเห็นด้านมืดของ Centralize Rep ไปแล้วเราก็จะได้รู้จักกับสิ่งที่เรียกว่า Distributed Version Control system (DVCSs) ตัวอย่างของระบบแบบนี้คือ (Git, Mercurial, Bazaar หรือ Darcs) โดยที่ความแปลกของระบบแบบนี้คือแทนที่เครื่อง client จะทำการ check out เอา snapshot ล่าสุดไปไว้บนเครื่องมันจะทำสิ่งที่ยิ่งใหญ่กว่านั้นคือมันทำ full mirror ข้อมูลทั้งหมดของโปรเจคที่กำลังทำงานลงมาไว้ที่เครื่องเลยดังนั้นการทำงานแบบนี้จะแก้ปัญหาเรื่อง Server พังแล้วไม่สามารถย้อนกลับไปเอา history กลับคืนมาได้เพราะข้อมูลที่อยู่บนเครื่อง client ก็สามารถถูกส่งกลับขึ้นไปเพื่อ restore ระบบได้ทั้งหมดเหมือนกันดังนั้นการ check out ของ DVCS ก็คือการทำ Full Backup นั่นเองดังรูป (see Figure 1-3) 33 | 34 | Insert 18333fig0103.png 35 | Figure 1-3. Distributed version control diagram. 36 | 37 | นอกจากนี้แล้วระบบแบบนี้ยังถูกออกแบบให้มีความสามารถในการทำงานกับ remote repository ได้มากกว่าหนึ่งที่ด้วยนั่นหมายความว่าเราสามารถทำงานแจมกับพรรคพวกได้มากกว่าหนึงที่ในโปรเจคเดียวกัน ดังนั้นเราจะสามารถ setup workflow ได้หลายประเภทเพื่อให้รองรับการทำงานของเราและสิ่งนี้ทำไม่ได้ใน Centralize Repository นะจ๊ะย้ำ 38 | 39 | ## ตำนานของ Git ## 40 | 41 | เฉกเช่นสิ่งมหัศจรรย์ทั้งหลายแหล่ในโลก, Git เริ่มต้นมาจากดราม่าส์เล็กๆ ณ ชุมชนหนึ่งในดาวนี้ Linux kernel เป็น open source โปรเจคอันนึงที่มีขนาดใหญ่พอสมควร เกือบทั้งชีวิตของการ maintenance kernel ของ Linux นี้ (ช่วงปี 1991–2002) การเปลี่ยนแปลงต่างๆถูกส่งไปส่งมาในรูปแบบ patch และ zip files จนกระทั่งปี 2002 Linux kernel project ก็เริ่มใช้ DVCS ที่เรียกว่า BitKeeper 42 | 43 | ในปี 2005 ความสัมพันธ์ระหว่าง community ที่พัฒนา Linux kernel กับบริษัท commercial ที่พัฒนา BitKeeper ก็สะบั้นลง ไอ้ tool ที่เคยใช้ได้ฟรีมาตลอดก็จุ๊งไป ทำให้ community ที่พัฒนา Linux (โดยเฉพาะท่านเทพ Linus Torvalds ที่เป็นบิดาแห่ง Linux) ต้องพัฒนา tool ขึ้นมาใช้เอง โดยอิงจากประสบการณ์ที่ได้เรียนรู้ระหว่างใช้งาน BitKeeper ระบบใหม่มีเป้าหมายบางประการ ดังนี้: 44 | 45 | * ความเร็วส์ 46 | * design ที่ simple 47 | * ต้องพัฒนาไปพร้อมๆกันที่ละหลายๆคนได้ (ประมาณ 1000 branches) 48 | * distributed สุดๆ 49 | * ใช้กับโปรเจคใหญ่ๆอย่าง Linux kernel ได้ดี (ทั้งเรื่องความเร็วส์และขนาดของข้อมูล) 50 | 51 | ตั้งแต่ถูกคลอดมาในปี 2005, Git ก็พัฒนาและเติบโตจนเป็นของที่ใช้ง่าย แต่ก็ยังคงไว้ซึ่งข้อดีข้างต้นนี้ นั่นเร็วส์สุดยิด, เหมาะกับโปรเจคขนาดยักษ์ และพัฒนาไปพร้อมๆกันทีละหลายๆ branch อย่างน่าอัศจรรย์ (ดูได้ใน Chapter 3). 52 | 53 | ## เบสิคของ Git ## 54 | 55 | เอาล่ะ มาดูกันว่า Git in a nutshell เป็นยังไง? ส่วนนี้เป็นส่วนสำคัญที่ท่านจะต้องดูดไป เพราะเมื่อไหร่ที่ท่านเข้าใจแก่นแท้ของ Git และเข้าใจว่ามันทำงานยังไง เมื่อนั้น การใช้ Git อย่างมีประสิทธิภาพสูงสุดก็ไม่ยากละ ขณะที่เรียนรู้ Git พยายามลืม VCSs ตัวที่ผ่านๆมาให้หมด (เช่น Subversion และพวกพ้อง) เพราะมันจะป้องกันความงงที่จะเกิดขึ้นระหว่างใช้ Git ได้ Git มองและจำข้อมูลต่างกับระบบอื่นๆค่อนข้างเยอะ แม้ว่า UI มันจะดูคล้ายๆตัวอื่นก็ตาม ทำความเข้าใจความแตกต่าง แล้วเวลาใช้จะไม่งง 56 | 57 | ### Snapshots, ไม่ใช่ความเปลี่ยนแปลง ### 58 | 59 | หลักๆเลย Git ต่างกับ VCS อื่นๆ (Subversion และเพื่อนๆ) ตรงที่วิธีที่ Git มองข้อมูลที่มันเก็บ โดยคอนเซปแล้ว ระบบอื่นๆจะเก็บข้อมูลในรูปของ listของความเปลี่ยนแปลง ระบบเหล่านี้ (CVS, Subversion, Perforce, Bazaar ฯลฯ) คิดว่าข้อมูลที่มันเก็บคือ set ของ files และความเปลี่ยนแปลงที่เกิดขึ้นกับแต่ละ file ในเวลาที่ดำเนินไป ดังเช่นตัวอย่างในรูป Figure 1-4. 60 | 61 | Insert 18333fig0104.png 62 | Figure 1-4. Other systems tend to store data as changes to a base version of each file. 63 | 64 | Git ไม่ได้มองหรือจำข้อมูลที่มันเก็บอย่างนั้น ในทางกลับกัน Git มองข้อมูลมันเหมือนกับเป็น set ของ snapshots ของ filesystem ขนาดจิ๋ว ทุกๆครั้งที่คุณ commit หรือ save project ใน Git มันจะถ่ายรูปว่า files ของเราหน้าตาเป็นไง ณ บัดนั้น และเก็บ reference ไปยัง snapshot (รูปถ่าย) นั้น และเพื่อให้มีประสิทธิภาพ ถ้า file ไม่ถูกแก้ไข Git จะไม่จำ file นั้นๆซ้ำ แค่เก็บ link ไปยัง file เก่าที่เหมือนกันเป๊ะๆ ที่มันเคยจำไว้แล้วเฉยๆ Git มองข้อมูลท่มันเก็บเหมือนดังภาพ Figure 1-5. 65 | 66 | Insert 18333fig0105.png 67 | Figure 1-5. Git stores data as snapshots of the project over time. 68 | 69 | นี่ความความแตกต่างที่สำคัญระหว่าง Git และ VCSs อื่นๆเกือบทั่วโลก มันทำให้ Git ต้องคิดใหม่ ทำใหม่เกือบทุกๆอย่าง ขณะที่ระบบอื่นๆแค่ copy มาจากรุ่นก่อนๆ มันทำให้ Git เหมือนเป็น filesystem ขนาดจิ๋วที่มากับ tools อันทรงพลังที่สร้างขึ้นมาครอบมันมากกว่าที่จะเป็นแค่ VCS ธรรมดา เด๋วเราค่อยมาโชว์ของดีที่ได้จากการมองข้อมูลในลักษณะนี้ในหัวข้อ branching ใน Chapter 3 70 | 71 | ### เกือบทุก operation นั้นทำบนเครื่องตัวเอง ### 72 | 73 | ส่วนใหญ่แล้ว operation ใน Git ต้องการแค่ file และทรัพยากรบนเครื่องในการทำงาน ไม่จำเป็นต้องมีข้อมูลอื่นใดจากเครื่องอื่นๆใน network ถ้าคุณคุ้นเคยกับ CVCS ที่ operation ส่วนใหญ่ต้องทนกับความช้าของ network แล้วละก็ คุณจะรู้สึกราวกับว่าเทพแห่งความเร็วนั้นอวยพร Git ด้วยความเร็วส์ที่ไม่อายบรรยาย เพราะว่าคุณมี history ทั้งหมดของ project เก็บอยู่ที่นี่ในเครื่องของตัวเอง operation ทั้งหลายแหล่จึงรวดเร็วทันใจ 74 | 75 | ยกตัวอย่างเช่นการค้นหา history ของ project Git ไม่จำเป็นต้องวิ่งไป server เพื่อดึง history แล้วหิ้วกลับมาแสดงผลให้คุณ มันแค่อ่านจาก database บนเครื่องก็ได้แล้ว นั่นหมายความว่าคุณสามารถเห็น project history ได้ในอึดใจเดียว ถ้าอยากดูความเปลี่ยนแปลงที่เกิดขึ้นบน file ซักอันระหว่าง version ปัจจุบันกับเมื่อเดือนที่แล้ว Git สามารถค้นหา file เมื่อเดือนที่แล้วบนเครื่องแล้วคำนวนหาสิ่งที่เปลี่ยนแปลงไปให้ได้ทันที แทนที่จะต้องไปอ้อน server ที่อยู่ไกลๆให้คำนวนให้หรือไปขอ file เดือนก่อนจาก server แล้วค่อยเอามาคำนวน 76 | 77 | นั่นหมายความว่าคุณแทบจะไม่มีอะไรที่ทำไม่ได้ในกรณีที่ไม่ได้ต่อเนตหรือต่อ VPN ไม่ว่าจะกำลังนั่งรถ นั่งเรือ นั่งเครื่องบินอยู่ ถ้ามีอะไรกระจุ๊กกระจิ๊กอยากทำก็สามารถทำแล้ว commit เก็บไว้ได้อย่างสบายอารมณ์ ไว้ต่อเนตได้เมื่อไหร่ก็ค่อย upload ถ้าสมมติอยู่บ้านแล้วไม่สามารถ set VPN ได้ ก็ยังจะทำงานได้อยู่ ถ้าเป็นระบบอื่นๆ จะทำให้ได้แบบนี้แทบจะเป็นไปไม่ได้ ถึงได้ก็เลือดตาแทบกระเด็นหล่ะ เช่นสมมติใช้ Perforce คุณแทบจะทำอะไรไม่ได้เลยถ้าไม่ต่ออยู่กับ server หรือกรณี Subversion และ CVS ถึงจะแก้ file ได้ ก็ commit ใส่ database ไม่ได้ (เพราะไม่ได้ต่อกับ database) ถึงเรื่องแค่นี้จะดูเป็นเรื่องเล็ก แต่ถ้าลองได้ใช้จริงจะรู้ว่าชีวิตมันรู้สึกแตกต่างกันขนาดไหน 78 | 79 | ### Git นั้นเที่ยงธรรม ### 80 | 81 | ทุกอย่างใน Git ถูก checksum ก่อนจะถูก save และจะถูกอ้างอิงถึงด้วย checksum นั้นๆ นั่นหมายความว่าไม่มีทางที่จะมีข้อมูลใน file หรือ directory เปลี่ยนไปโดยที่ Git จะไม่รู้เรื่อง ความสามารถนี้ถูกฝังไว้ในแก่นลึกสุดใจของ Git และหล่อหลอมเข้ากับจิตวิญญาณของมัน คุณไม่มีวันเจอ file เสียหรือพังไประหว่างถ่ายโอนข้อมูลโดยที่ Git ตรวจจับไม่เจอแน่นอน 82 | 83 | กลไกที่ Git ใช้ในการทำ checksum เรียกว่า SHA-1 hash ซึ่งเป็นเลขฐาน 16 ยาว 40 ตัวอักษรที่ถูกคำนวนมาจากเนื้อหาภายใน file หรือโครงสร้าง directory ภายใน Git SHA-1 hash มีหน้าตาประมาณนี้: 84 | 85 | 24b9da6552252987aa493b52f8696cd6d3b00373 86 | 87 | คุณจะเห็นค่า hash เหล่านี้กระจายตัวอยู่ทั่ว Git มันถูกใช้เยอะมว้ากก เอาจริงๆแล้ว Git ไม่ได้เก็บข้อมูลเป็น file แต่เก็บลง database ซึ่งสามารถเข้าถึงข้อมูลได้ด้วยค่า hash ของเนื้อหาใน file 88 | 89 | ### โดยรวมแล้ว Git มีแต่เพิ่มข้อมูล ### 90 | 91 | เมื่อคุณทำ action ใดๆใน Git เกือบทุกอย่างจะเป็นการเพิ่มข้อมูลลงไปใน Git database มันยากมากที่จะทำอะไรลงไปแล้ว undo ไม่ได้ คล้ายกับ VCS อื่นๆคือคุณยังสามรถทำ file หาย หรือว่าทำของเละเทะได้ ถ้าคุณยังไม่ได้ commit แต่เมื่อไหร่ที่คุณ commit ไปใน Git ซัก snapshot นึงแล้ว มันแทบจะไม่มีวันหายเลย โดยเฉพาะอย่างยิ่งเวลาคุณคอย push database ของคุณไปใส่ repository อื่นอย่างสม่ำเสมอ 92 | 93 | การใช้ Git จึงกลายเป็นความบันเทิงเพราะเรารู้ว่าเราสามารถทำการทดลองอะไรก็ได้โดยไม่กลัวว่าจะทำอะไรพัง ไว้ค่อยมาเจาะลึกว่า Git เก็บข้อมูลยังไง และเวลาทำอะไรเละเทะไปแล้วจะกู้กลับมายังไงในบท “Under the Covers” ใน Chapter 9. 94 | 95 | ### State ทั้งสาม ### 96 | 97 | เอาล่ะ! ตั้งสมาธิให้ดี นี่คือแก่นของวรยุทธ์ที่จะต้องจำให้ขึ้นใจถ้าอยากให้การศึกษา Git เป็นไปอย่างราบรื่น Git มีสาม state หลักสำหรับ file ทุก file: committed, modified และ staged 98 | 99 | Committed แปลว่าข้อมูลถูกจัดเก็บอย่างปลอดภัยใน database บนเครื่อง, Modified แปลว่าคุณได้แก้ file ไปแต่ยังไม่ได้ commit มันใส่ database, Staged แปลว่าคุณได้ mark file ที่ถูก modify ใน version นี้ให้มันถูก commit ใน snapshot หน้า 100 | 101 | 3 state นี้ชักนำให้เกิด 3 ก๊กใน Git project นั่นคือ Git directory, working directory, และ staging area 102 | 103 | Insert 18333fig0106.png 104 | Figure 1-6. Working directory, staging area, and git directory. 105 | 106 | Git directory คือที่ที่ Git เก็บ metadata และ object database สำหรับ project ของคุณ นี่คือส่วนที่สำคัญที่สุดของ Git และมันคือสิ่งที่ถูก copy มาเวลาที่คุณ clone repository มาจาก computer เครื่องอื่น 107 | 108 | working directory คือ checkout อันหนึ่งๆของซัก version นึงของ project ซึ่ง file เหล่านี้จะถูกดึงออกมาจาก compressed database ใน Git directory และเอามาวางบน disk ให้คุณใช้หรือว่าแก้ไขมัน 109 | 110 | staging area คือ file ธรรมดาไม่ซับซ้อน ซึ่งจะอยู่ใน Git directory ของคุณ มันจะเก็บข้อมูลว่าอะไรบ้างที่จะถูกรวมไปใน commit ถัดไป บางคนก็เรียกมันว่า index แต่คนส่วนใหญ่จะเรียกมันว่า staging area 111 | 112 | โดยเบสิคแล้ว flow ของ Git จะดำเนินไปดังนี้: 113 | 114 | 1. คุณแก้ไข file ใน working directory 115 | 2. คุณ stage file เหล่านั้น (เพิ่ม snapshot ของ file เหล่านั้นใน staging area ของคุณ) 116 | 3. คุณ commit ซึ่งเป็นการเอา snapshot ของ file นั้นๆใน staging area มา save เก็บไว้ Git directory ตลอดกาล 117 | 118 | ถ้า file ซัก version นึงถูกเก็บลง git directory แล้ว file นั้นจะมีสถานะ committed ถ้ามันโดนแก้และถูก add เข้าไปใน staging area มันจะมีสถานะ staged ถ้ามันถูกแก้ไขเฉยๆไปจากตอนที่ถูก check out แต่ยังไม่เคยถูก stage มันจะมีสถานะ modified ใน Chapter 2 คุณจะเข้าใจ state ทั้ง 3 นี้เพิ่มขึ้น และได้เรียนรู้วิธีที่จะใช้ประโยชน์จากพวกมันและวิธีการลัดข้ามส่วน staged ไปเลย 119 | 120 | ## ติดตั้ง Git ## 121 | 122 | มาเริ่มใช้ Git กันเหอะ อันดับแรกเลยคือต้องติดตั้งมันก่อนซึ่งสามารถทำได้หลายหลาย แต่สองทางหลักๆคือ ติดตั้งจาก source เลยหรือไม่ก็ติดตั้ง package ที่เตรียมไว้สำหรับ platform ของคุณ 123 | 124 | ### ติดตั้งจาก Source ### 125 | 126 | ถ้าทำได้ ติดตั้งจาก source เลยก็ดี เพราะจะได้ version ล่าสุดซิงๆ แต่ละ version ที่ใหม่ขึ้นของ Git ชอบมี UI ใหม่ๆ เจ๋งๆมาให้ใช้ ฉะนั้นการใช้ version ล่าสุดมักจะเป็นหนทางที่แหล่มที่สุดถ้าสะดวกใจที่จะ compile จาก source อีกเหตุผลนึงคือ Linux distribution หลายๆอันมักจะมี package ที่ดึกดำบรรพ์ ฉะนั้น ถ้าคุณไม่ได้ใช้ distro ที่มันซิงมากๆหรือใช้ backports การ install จาก source น่าจะเป็นการเดิมพันที่คุ้มสุด 127 | 128 | จะ install Git ได้ คุณต้องมี library ต่างเหล่านี้ที่ Git depends on: curl, zlib, openssl, expat และ libiconv ยกตัวอย่างเช่น ถ้าคุณใช้ OS ที่มี yum (เช่น Fedora) หรือ apt-get (เช่นพวกตระกูล Debian ทั้งหลาย) คุณสามารถใช้ command ข้างล่างนี้เพื่อ install พวก dependencies ทั้งหลายได้: 129 | 130 | $ yum install curl-devel expat-devel gettext-devel \ 131 | openssl-devel zlib-devel 132 | 133 | $ apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \ 134 | libz-dev libssl-dev 135 | 136 | เมื่อคุณมี dependencies ที่จำเป็นครบแล้ว ก็เดินหน้าลุยไปเอา snapshot ล่าสุดจาก Git web site ได้เลย: 137 | 138 | http://git-scm.com/download 139 | 140 | หลังจากนั้นก็ compile และ install: 141 | 142 | $ tar -zxf git-1.7.2.2.tar.gz 143 | $ cd git-1.7.2.2 144 | $ make prefix=/usr/local all 145 | $ sudo make prefix=/usr/local install 146 | 147 | หลังจากขั้นนี้ คุณสามารถ download Git ผ่าน Git เองได้ด้วยเวลาอยาก update: 148 | 149 | $ git clone git://git.kernel.org/pub/scm/git/git.git 150 | 151 | ### ติดตั้งบน Linux ### 152 | 153 | ถ้าอยากติดตั้ง Git บน Linux ผ่าน installer คุณก็ทำได้ง่ายๆผ่าน package-management tool ที่มากับ distribution ของคุณ เช่นถ้าใช้ Fedora ก็ yum เอาตามนี้: 154 | 155 | $ yum install git-core 156 | 157 | หรือถ้าใช้ตระกูล Debian อย่าง Ubuntu ก็ apt-get เอา: 158 | 159 | $ apt-get install git-core 160 | 161 | ### ติดตั้งบน Mac ### 162 | 163 | มีทางง่ายๆ 2 ทางที่จะติดตั้ง Git บน Mac ที่ง่ายสุดคือใช้ graphical Git installer ซึ่งสามารถ download ได้จาก Google Code page (ตามรูป Figure 1-7): 164 | 165 | http://code.google.com/p/git-osx-installer 166 | 167 | Insert 18333fig0107.png 168 | Figure 1-7. Git OS X installer. 169 | 170 | อีกทางคือติดตั้ง Git ผ่าน MacPorts (`http://www.macports.org`) ถ้าลง MacPorts ไว้อยู่แล้ว ก็ติดตั้ง Git ตามนี้ 171 | 172 | $ sudo port install git-core +svn +doc +bash_completion +gitweb 173 | 174 | ไม่จำเป็นต้องลง extras ทั้งหมดก็ได้ แต่อย่างน้อยน่าจะลง +svn ไว้ในกรณีที่ต้องใช้ Git ร่วมกับ Subversion repository (ดูได้ใน Chapter 8). 175 | 176 | ### ติดตั้งบน Windows ### 177 | 178 | ติดตั้ง Git บน Windows นี่โคตรง่าย msysGit project ทำวิธีติดตั้งง่ายๆไว้ แค่ download file installer (exe) จาก Google Code page แล้วก็ run มัน: 179 | 180 | http://code.google.com/p/msysgit 181 | 182 | หลังจากติดตั้งแล้ว คุณจะมีทั้ง version command-line (รวมทั้ง SSH client ที่จะได้ใช้ภายหลัง) และ GUI 183 | 184 | ## ครั้งแรกของ Git (setup) ## 185 | 186 | หลังจากมี Git บนเครื่องแล้ว ต้อง customize Git environment ซักนิดก่อน ขั้นตอนนี้ทำแค่ครั้งเดียวพอ ถึง upgrade version ก็ไม่ต้องมานั่ง set ใหม่ ถ้าวันหลังอยากจะเปลี่ยนมันก็เปลี่ยนได้ตลอดเวลา แค่ run command ซ้ำแค่นั้นเอง 187 | 188 | Git มากับ tool ที่เรียกว่า git config เปิดช่องให้ get หรือ set ตัวแปร configuration ที่ควบคุมการหน้าตา, คำสั่ง และทำงานของ Git ได้ ตัวแปรเหล่านี้ถูกแบ่งเก็บใน 3 ที่ดังนี้: 189 | 190 | * file `/etc/gitconfig`: เก็บข้อมูล user ทุกคนในระบบรวมถึง repository ทั้งหลาย ถ้าคุณส่ง option `--system` ให้ `git config` มันจะอ่านหรือเขียนใน file นี้ตรงๆ 191 | * file `~/.gitconfig`: เป็น file เฉพาะของ user คุณเท่านั้น สามารถเจาะจงให้ Git เขียนหรืออ่าน file นี้ได้โดยการส่ง `--global` option ให้มัน 192 | * config file ใน git directory (ซึ่งอยู่ที่ `.git/config`) ของ repository ใดๆที่คุณใช้งาน: เฉพาะเจาะจงกับ repository นั้นๆ แต่ละ level คอยบังค่าที่ set ใน level ก่อนๆ ฉะนั้น ค่าที่ set ไว้ใน `.git/config` ก็จะบังค่าใน `/etc/gitconfig` 193 | 194 | บน Windows นั้น Git อ่านค่าใน file `.gitconfig` ใน `$HOME` directory (ส่วนใหญ่จะอยู่ที่ `C:\Documents and Settings\$USER`) มันดูค่าใน /etc/gitconfig ด้วยเหมือนกัน แต่ต้องอ้างอิงกับ MSys root ซึ่งจะอยู่ที่ที่คุณ ลง Git ไว้ตอนที่ run installer 195 | 196 | ### Identity ของคุณ ### 197 | 198 | อย่างแรกที่ควรทำหลังจากลง Git คือการ set user name และ e-mail address ของคุณ ที่มันจำเป็นเพราะทุกๆ commit ใน Git จะใช้ข้อมูลนี้และมันจะถูกสลักลงไปในแต่ละ commit ที่คุณส่ง: 199 | 200 | $ git config --global user.name "John Doe" 201 | $ git config --global user.email johndoe@example.com 202 | 203 | ย้ำอีกครั้ง คุณจำเป็นต้อง set ค่าเหล่านี้แค่ครั้งเดียวถ้าคุณใช้ option `--global` เพราะ Git จะใช้ข้อมูลนั้นสำหรับทุกๆ action ที่คุณใช้บน system นั้น ถ้าอยากทับค่านั้นด้วชื่อหรือ e-mail address อื่น สำหรับบาง project คุณก็ทำได้โดยการ run command แล้วไม่ต้องส่ง option `--global` เข้าไปเวลาคนทำ project นั้น 204 | 205 | ### Editor ของคุณ ### 206 | 207 | หลังจาก setup Identity ของคุณแล้ว คุณก็สามารถเลือก default text editor ที่จะถูกเลือกใช้เวลา Git ต้องการให้คุณพิมพ์ message ได้ โดยปรกติแล้ว Git จะใช้ default editor ของระบบคุณ ซึ่งส่วนใหญ่จะเป็น Vi หรือ Vim ถ้าคุณอยากใช้ตัวอื่นเช่น Emacs ก็ทำได้ตังนี้: 208 | 209 | $ git config --global core.editor emacs 210 | 211 | ### Diff Tool ของคุณ ### 212 | 213 | อีก option นึงที่คุณน่าจะอยาก config คือ default diff tool เพื่อใช้ในการแก้ conflict ตอน merge สมมติว่าคุณอยากใช้ vimdiff: 214 | 215 | $ git config --global merge.tool vimdiff 216 | 217 | Git รองรับ kdiff3, tkdiff, meld, xxdiff, emerge, vimdiff, gvimdiff, ecmerge และ opendiff คุณสามารถ setup custom tool อื่นๆได้ด้วย ไปตามอ่านใน Chapter 7 ละกันว่าทำยังไง 218 | 219 | ### Check Settings ของคุณ ### 220 | 221 | ถ้าอยาก check settings ของคุณ คุณสามารถใช้คำสั่ง `git config --list` เพื่อจะ list settings ทั้งหลายแหล่ที่ Git มี ณ ตอนนั้นมาให้ดูได้: 222 | 223 | $ git config --list 224 | user.name=Scott Chacon 225 | user.email=schacon@gmail.com 226 | color.status=auto 227 | color.branch=auto 228 | color.interactive=auto 229 | color.diff=auto 230 | ... 231 | 232 | บาง key อาจจะซ้ำเพราะ Git อ่านค่า key เดียวกันจาก file ต่างๆ (เช่นจาก `/etc/gitconfig` และ `~/.gitconfig` เป็นต้น) ในกรณีนั้น Git จะใช้ค่าสุดท้ายที่มันเห็น 233 | 234 | คุณตรวจสอบได้ด้วยว่า Git มันกำลังคิดว่าค่าของ key นั้นๆเป็นอะไรโดยพิมพ์ว่า `git config {key}`: 235 | 236 | $ git config user.name 237 | Scott Chacon 238 | 239 | ## ขอความช่วยเหลือ ## 240 | 241 | ถ้าต้องการความช่วยเหลือขณะใช้ Git มี 3 ทางที่จะดู help สำหรับ command ใดๆของ Git ใน manual page (manpage): 242 | 243 | $ git help 244 | $ git --help 245 | $ man git- 246 | 247 | เช่นถ้าอยากดู help สำหรับคำสั่ง config ใน manpage ก็แค่ 248 | 249 | $ git help config 250 | 251 | command แบบนี้มันเจ๋งตรงที่เรียกจากตรงไหนก็ได้แม้ว่าจะไม่ได้ต่อเนตก็ตาม 252 | ถ้า manpage ทั้งหลายและหนังสือเล่มนี้ยังเอาไม่อยู่และคุณต้องการคนช่วย ให้ลองไปที่ Freenode IRC (irc.freenode.net) ที่ห้อง `#git` หรือ `#github` ดู ห้องเหล่านี้โดยปรกติจะมีคนเป็นร้อยๆที่มีความรู้เรื่อง Git และชอบช่วยเหลือ 253 | 254 | ## สรุป ## 255 | 256 | น่าจะพอเห็นภาพคร่าวๆว่า Git คืออะไรและมันต่างกับ CVCS ที่คุณเคยใช้ๆมายังไง และตอนนี้คุณน่าจะมี Git version ที่พร้อมใช้งานและมีข้อมูลส่วนตัว setup เรียบร้อยอยู่บนเครื่องแล้ว ถึงเวลาตะลุย basic ของ Git ซะที! 257 | -------------------------------------------------------------------------------- /02-git-basics/01-chapter2.markdown: -------------------------------------------------------------------------------- 1 | # Git Basics # 2 | 3 | If you can read only one chapter to get going with Git, this is it. This chapter covers every basic command you need to do the vast majority of the things you’ll eventually spend your time doing with Git. By the end of the chapter, you should be able to configure and initialize a repository, begin and stop tracking files, and stage and commit changes. We’ll also show you how to set up Git to ignore certain files and file patterns, how to undo mistakes quickly and easily, how to browse the history of your project and view changes between commits, and how to push and pull from remote repositories. 4 | 5 | ## Getting a Git Repository ## 6 | 7 | You can get a Git project using two main approaches. The first takes an existing project or directory and imports it into Git. The second clones an existing Git repository from another server. 8 | 9 | ### Initializing a Repository in an Existing Directory ### 10 | 11 | If you’re starting to track an existing project in Git, you need to go to the project’s directory and type 12 | 13 | $ git init 14 | 15 | This creates a new subdirectory named .git that contains all of your necessary repository files — a Git repository skeleton. At this point, nothing in your project is tracked yet. (See Chapter 9 for more information about exactly what files are contained in the `.git` directory you just created.) 16 | 17 | If you want to start version-controlling existing files (as opposed to an empty directory), you should probably begin tracking those files and do an initial commit. You can accomplish that with a few git add commands that specify the files you want to track, followed by a commit: 18 | 19 | $ git add *.c 20 | $ git add README 21 | $ git commit -m 'initial project version' 22 | 23 | We’ll go over what these commands do in just a minute. At this point, you have a Git repository with tracked files and an initial commit. 24 | 25 | ### Cloning an Existing Repository ### 26 | 27 | If you want to get a copy of an existing Git repository — for example, a project you’d like to contribute to — the command you need is git clone. If you’re familiar with other VCS systems such as Subversion, you’ll notice that the command is clone and not checkout. This is an important distinction — Git receives a copy of nearly all data that the server has. Every version of every file for the history of the project is pulled down when you run `git clone`. In fact, if your server disk gets corrupted, you can use any of the clones on any client to set the server back to the state it was in when it was cloned (you may lose some server-side hooks and such, but all the versioned data would be there — see Chapter 4 for more details). 28 | 29 | You clone a repository with `git clone [url]`. For example, if you want to clone the Ruby Git library called Grit, you can do so like this: 30 | 31 | $ git clone git://github.com/schacon/grit.git 32 | 33 | That creates a directory named "grit", initializes a `.git` directory inside it, pulls down all the data for that repository, and checks out a working copy of the latest version. If you go into the new `grit` directory, you’ll see the project files in there, ready to be worked on or used. If you want to clone the repository into a directory named something other than grit, you can specify that as the next command-line option: 34 | 35 | $ git clone git://github.com/schacon/grit.git mygrit 36 | 37 | That command does the same thing as the previous one, but the target directory is called mygrit. 38 | 39 | Git has a number of different transfer protocols you can use. The previous example uses the `git://` protocol, but you may also see `http(s)://` or `user@server:/path.git`, which uses the SSH transfer protocol. Chapter 4 will introduce all of the available options the server can set up to access your Git repository and the pros and cons of each. 40 | 41 | ## Recording Changes to the Repository ## 42 | 43 | You have a bona fide Git repository and a checkout or working copy of the files for that project. You need to make some changes and commit snapshots of those changes into your repository each time the project reaches a state you want to record. 44 | 45 | Remember that each file in your working directory can be in one of two states: tracked or untracked. Tracked files are files that were in the last snapshot; they can be unmodified, modified, or staged. Untracked files are everything else - any files in your working directory that were not in your last snapshot and are not in your staging area. When you first clone a repository, all of your files will be tracked and unmodified because you just checked them out and haven’t edited anything. 46 | 47 | As you edit files, Git sees them as modified, because you’ve changed them since your last commit. You stage these modified files and then commit all your staged changes, and the cycle repeats. This lifecycle is illustrated in Figure 2-1. 48 | 49 | Insert 18333fig0201.png 50 | Figure 2-1. The lifecycle of the status of your files. 51 | 52 | ### Checking the Status of Your Files ### 53 | 54 | The main tool you use to determine which files are in which state is the git status command. If you run this command directly after a clone, you should see something like this: 55 | 56 | $ git status 57 | # On branch master 58 | nothing to commit (working directory clean) 59 | 60 | This means you have a clean working directory — in other words, there are no tracked and modified files. Git also doesn’t see any untracked files, or they would be listed here. Finally, the command tells you which branch you’re on. For now, that is always master, which is the default; you won’t worry about it here. The next chapter will go over branches and references in detail. 61 | 62 | Let’s say you add a new file to your project, a simple README file. If the file didn’t exist before, and you run `git status`, you see your untracked file like so: 63 | 64 | $ vim README 65 | $ git status 66 | # On branch master 67 | # Untracked files: 68 | # (use "git add ..." to include in what will be committed) 69 | # 70 | # README 71 | nothing added to commit but untracked files present (use "git add" to track) 72 | 73 | You can see that your new README file is untracked, because it’s under the “Untracked files” heading in your status output. Untracked basically means that Git sees a file you didn’t have in the previous snapshot (commit); Git won’t start including it in your commit snapshots until you explicitly tell it to do so. It does this so you don’t accidentally begin including generated binary files or other files that you did not mean to include. You do want to start including README, so let’s start tracking the file. 74 | 75 | ### Tracking New Files ### 76 | 77 | In order to begin tracking a new file, you use the command `git add`. To begin tracking the README file, you can run this: 78 | 79 | $ git add README 80 | 81 | If you run your status command again, you can see that your README file is now tracked and staged: 82 | 83 | $ git status 84 | # On branch master 85 | # Changes to be committed: 86 | # (use "git reset HEAD ..." to unstage) 87 | # 88 | # new file: README 89 | # 90 | 91 | You can tell that it’s staged because it’s under the “Changes to be committed” heading. If you commit at this point, the version of the file at the time you ran git add is what will be in the historical snapshot. You may recall that when you ran git init earlier, you then ran git add (files) — that was to begin tracking files in your directory. The git add command takes a path name for either a file or a directory; if it’s a directory, the command adds all the files in that directory recursively. 92 | 93 | ### Staging Modified Files ### 94 | 95 | Let’s change a file that was already tracked. If you change a previously tracked file called `benchmarks.rb` and then run your `status` command again, you get something that looks like this: 96 | 97 | $ git status 98 | # On branch master 99 | # Changes to be committed: 100 | # (use "git reset HEAD ..." to unstage) 101 | # 102 | # new file: README 103 | # 104 | # Changed but not updated: 105 | # (use "git add ..." to update what will be committed) 106 | # 107 | # modified: benchmarks.rb 108 | # 109 | 110 | The benchmarks.rb file appears under a section named “Changed but not updated” — which means that a file that is tracked has been modified in the working directory but not yet staged. To stage it, you run the `git add` command (it’s a multipurpose command — you use it to begin tracking new files, to stage files, and to do other things like marking merge-conflicted files as resolved). Let’s run `git add` now to stage the benchmarks.rb file, and then run `git status` again: 111 | 112 | $ git add benchmarks.rb 113 | $ git status 114 | # On branch master 115 | # Changes to be committed: 116 | # (use "git reset HEAD ..." to unstage) 117 | # 118 | # new file: README 119 | # modified: benchmarks.rb 120 | # 121 | 122 | Both files are staged and will go into your next commit. At this point, suppose you remember one little change that you want to make in benchmarks.rb before you commit it. You open it again and make that change, and you’re ready to commit. However, let’s run `git status` one more time: 123 | 124 | $ vim benchmarks.rb 125 | $ git status 126 | # On branch master 127 | # Changes to be committed: 128 | # (use "git reset HEAD ..." to unstage) 129 | # 130 | # new file: README 131 | # modified: benchmarks.rb 132 | # 133 | # Changed but not updated: 134 | # (use "git add ..." to update what will be committed) 135 | # 136 | # modified: benchmarks.rb 137 | # 138 | 139 | What the heck? Now benchmarks.rb is listed as both staged and unstaged. How is that possible? It turns out that Git stages a file exactly as it is when you run the git add command. If you commit now, the version of benchmarks.rb as it was when you last ran the git add command is how it will go into the commit, not the version of the file as it looks in your working directory when you run git commit. If you modify a file after you run `git add`, you have to run `git add` again to stage the latest version of the file: 140 | 141 | $ git add benchmarks.rb 142 | $ git status 143 | # On branch master 144 | # Changes to be committed: 145 | # (use "git reset HEAD ..." to unstage) 146 | # 147 | # new file: README 148 | # modified: benchmarks.rb 149 | # 150 | 151 | ### Ignoring Files ### 152 | 153 | Often, you’ll have a class of files that you don’t want Git to automatically add or even show you as being untracked. These are generally automatically generated files such as log files or files produced by your build system. In such cases, you can create a file listing patterns to match them named .gitignore. Here is an example .gitignore file: 154 | 155 | $ cat .gitignore 156 | *.[oa] 157 | *~ 158 | 159 | The first line tells Git to ignore any files ending in .o or .a — object and archive files that may be the product of building your code. The second line tells Git to ignore all files that end with a tilde (`~`), which is used by many text editors such as Emacs to mark temporary files. You may also include a log, tmp, or pid directory; automatically generated documentation; and so on. Setting up a .gitignore file before you get going is generally a good idea so you don’t accidentally commit files that you really don’t want in your Git repository. 160 | 161 | The rules for the patterns you can put in the .gitignore file are as follows: 162 | 163 | * Blank lines or lines starting with # are ignored. 164 | * Standard glob patterns work. 165 | * You can end patterns with a forward slash (`/`) to specify a directory. 166 | * You can negate a pattern by starting it with an exclamation point (`!`). 167 | 168 | Glob patterns are like simplified regular expressions that shells use. An asterisk (`*`) matches zero or more characters; `[abc]` matches any character inside the brackets (in this case a, b, or c); a question mark (`?`) matches a single character; and brackets enclosing characters separated by a hyphen(`[0-9]`) matches any character between them (in this case 0 through 9) . 169 | 170 | Here is another example .gitignore file: 171 | 172 | # a comment - this is ignored 173 | *.a # no .a files 174 | !lib.a # but do track lib.a, even though you're ignoring .a files above 175 | /TODO # only ignore the root TODO file, not subdir/TODO 176 | build/ # ignore all files in the build/ directory 177 | doc/*.txt # ignore doc/notes.txt, but not doc/server/arch.txt 178 | 179 | ### Viewing Your Staged and Unstaged Changes ### 180 | 181 | If the `git status` command is too vague for you — you want to know exactly what you changed, not just which files were changed — you can use the `git diff` command. We’ll cover `git diff` in more detail later; but you’ll probably use it most often to answer these two questions: What have you changed but not yet staged? And what have you staged that you are about to commit? Although `git status` answers those questions very generally, `git diff` shows you the exact lines added and removed — the patch, as it were. 182 | 183 | Let’s say you edit and stage the README file again and then edit the benchmarks.rb file without staging it. If you run your `status` command, you once again see something like this: 184 | 185 | $ git status 186 | # On branch master 187 | # Changes to be committed: 188 | # (use "git reset HEAD ..." to unstage) 189 | # 190 | # new file: README 191 | # 192 | # Changed but not updated: 193 | # (use "git add ..." to update what will be committed) 194 | # 195 | # modified: benchmarks.rb 196 | # 197 | 198 | To see what you’ve changed but not yet staged, type `git diff` with no other arguments: 199 | 200 | $ git diff 201 | diff --git a/benchmarks.rb b/benchmarks.rb 202 | index 3cb747f..da65585 100644 203 | --- a/benchmarks.rb 204 | +++ b/benchmarks.rb 205 | @@ -36,6 +36,10 @@ def main 206 | @commit.parents[0].parents[0].parents[0] 207 | end 208 | 209 | + run_code(x, 'commits 1') do 210 | + git.commits.size 211 | + end 212 | + 213 | run_code(x, 'commits 2') do 214 | log = git.commits('master', 15) 215 | log.size 216 | 217 | That command compares what is in your working directory with what is in your staging area. The result tells you the changes you’ve made that you haven’t yet staged. 218 | 219 | If you want to see what you’ve staged that will go into your next commit, you can use `git diff --cached`. (In Git versions 1.6.1 and later, you can also use `git diff --staged`, which may be easier to remember.) This command compares your staged changes to your last commit: 220 | 221 | $ git diff --cached 222 | diff --git a/README b/README 223 | new file mode 100644 224 | index 0000000..03902a1 225 | --- /dev/null 226 | +++ b/README2 227 | @@ -0,0 +1,5 @@ 228 | +grit 229 | + by Tom Preston-Werner, Chris Wanstrath 230 | + http://github.com/mojombo/grit 231 | + 232 | +Grit is a Ruby library for extracting information from a Git repository 233 | 234 | It’s important to note that `git diff` by itself doesn’t show all changes made since your last commit — only changes that are still unstaged. This can be confusing, because if you’ve staged all of your changes, `git diff` will give you no output. 235 | 236 | For another example, if you stage the benchmarks.rb file and then edit it, you can use `git diff` to see the changes in the file that are staged and the changes that are unstaged: 237 | 238 | $ git add benchmarks.rb 239 | $ echo '# test line' >> benchmarks.rb 240 | $ git status 241 | # On branch master 242 | # 243 | # Changes to be committed: 244 | # 245 | # modified: benchmarks.rb 246 | # 247 | # Changed but not updated: 248 | # 249 | # modified: benchmarks.rb 250 | # 251 | 252 | Now you can use `git diff` to see what is still unstaged 253 | 254 | $ git diff 255 | diff --git a/benchmarks.rb b/benchmarks.rb 256 | index e445e28..86b2f7c 100644 257 | --- a/benchmarks.rb 258 | +++ b/benchmarks.rb 259 | @@ -127,3 +127,4 @@ end 260 | main() 261 | 262 | ##pp Grit::GitRuby.cache_client.stats 263 | +# test line 264 | 265 | and `git diff --cached` to see what you’ve staged so far: 266 | 267 | $ git diff --cached 268 | diff --git a/benchmarks.rb b/benchmarks.rb 269 | index 3cb747f..e445e28 100644 270 | --- a/benchmarks.rb 271 | +++ b/benchmarks.rb 272 | @@ -36,6 +36,10 @@ def main 273 | @commit.parents[0].parents[0].parents[0] 274 | end 275 | 276 | + run_code(x, 'commits 1') do 277 | + git.commits.size 278 | + end 279 | + 280 | run_code(x, 'commits 2') do 281 | log = git.commits('master', 15) 282 | log.size 283 | 284 | ### Committing Your Changes ### 285 | 286 | Now that your staging area is set up the way you want it, you can commit your changes. Remember that anything that is still unstaged — any files you have created or modified that you haven’t run `git add` on since you edited them — won’t go into this commit. They will stay as modified files on your disk. 287 | In this case, the last time you ran `git status`, you saw that everything was staged, so you’re ready to commit your changes. The simplest way to commit is to type `git commit`: 288 | 289 | $ git commit 290 | 291 | Doing so launches your editor of choice. (This is set by your shell’s `$EDITOR` environment variable — usually vim or emacs, although you can configure it with whatever you want using the `git config --global core.editor` command as you saw in Chapter 1). 292 | 293 | The editor displays the following text (this example is a Vim screen): 294 | 295 | # Please enter the commit message for your changes. Lines starting 296 | # with '#' will be ignored, and an empty message aborts the commit. 297 | # On branch master 298 | # Changes to be committed: 299 | # (use "git reset HEAD ..." to unstage) 300 | # 301 | # new file: README 302 | # modified: benchmarks.rb 303 | ~ 304 | ~ 305 | ~ 306 | ".git/COMMIT_EDITMSG" 10L, 283C 307 | 308 | You can see that the default commit message contains the latest output of the `git status` command commented out and one empty line on top. You can remove these comments and type your commit message, or you can leave them there to help you remember what you’re committing. (For an even more explicit reminder of what you’ve modified, you can pass the `-v` option to `git commit`. Doing so also puts the diff of your change in the editor so you can see exactly what you did.) When you exit the editor, Git creates your commit with that commit message (with the comments and diff stripped out). 309 | 310 | Alternatively, you can type your commit message inline with the `commit` command by specifying it after a -m flag, like this: 311 | 312 | $ git commit -m "Story 182: Fix benchmarks for speed" 313 | [master]: created 463dc4f: "Fix benchmarks for speed" 314 | 2 files changed, 3 insertions(+), 0 deletions(-) 315 | create mode 100644 README 316 | 317 | Now you’ve created your first commit! You can see that the commit has given you some output about itself: which branch you committed to (master), what SHA-1 checksum the commit has (`463dc4f`), how many files were changed, and statistics about lines added and removed in the commit. 318 | 319 | Remember that the commit records the snapshot you set up in your staging area. Anything you didn’t stage is still sitting there modified; you can do another commit to add it to your history. Every time you perform a commit, you’re recording a snapshot of your project that you can revert to or compare to later. 320 | 321 | ### Skipping the Staging Area ### 322 | 323 | Although it can be amazingly useful for crafting commits exactly how you want them, the staging area is sometimes a bit more complex than you need in your workflow. If you want to skip the staging area, Git provides a simple shortcut. Providing the `-a` option to the `git commit` command makes Git automatically stage every file that is already tracked before doing the commit, letting you skip the `git add` part: 324 | 325 | $ git status 326 | # On branch master 327 | # 328 | # Changed but not updated: 329 | # 330 | # modified: benchmarks.rb 331 | # 332 | $ git commit -a -m 'added new benchmarks' 333 | [master 83e38c7] added new benchmarks 334 | 1 files changed, 5 insertions(+), 0 deletions(-) 335 | 336 | Notice how you don’t have to run `git add` on the benchmarks.rb file in this case before you commit. 337 | 338 | ### Removing Files ### 339 | 340 | To remove a file from Git, you have to remove it from your tracked files (more accurately, remove it from your staging area) and then commit. The `git rm` command does that and also removes the file from your working directory so you don’t see it as an untracked file next time around. 341 | 342 | If you simply remove the file from your working directory, it shows up under the “Changed but not updated” (that is, _unstaged_) area of your `git status` output: 343 | 344 | $ rm grit.gemspec 345 | $ git status 346 | # On branch master 347 | # 348 | # Changed but not updated: 349 | # (use "git add/rm ..." to update what will be committed) 350 | # 351 | # deleted: grit.gemspec 352 | # 353 | 354 | Then, if you run `git rm`, it stages the file’s removal: 355 | 356 | $ git rm grit.gemspec 357 | rm 'grit.gemspec' 358 | $ git status 359 | # On branch master 360 | # 361 | # Changes to be committed: 362 | # (use "git reset HEAD ..." to unstage) 363 | # 364 | # deleted: grit.gemspec 365 | # 366 | 367 | The next time you commit, the file will be gone and no longer tracked. If you modified the file and added it to the index already, you must force the removal with the `-f` option. This is a safety feature to prevent accidental removal of data that hasn’t yet been recorded in a snapshot and that can’t be recovered from Git. 368 | 369 | Another useful thing you may want to do is to keep the file in your working tree but remove it from your staging area. In other words, you may want to keep the file on your hard drive but not have Git track it anymore. This is particularly useful if you forgot to add something to your `.gitignore` file and accidentally added it, like a large log file or a bunch of `.a` compiled files. To do this, use the `--cached` option: 370 | 371 | $ git rm --cached readme.txt 372 | 373 | You can pass files, directories, and file-glob patterns to the `git rm` command. That means you can do things such as 374 | 375 | $ git rm log/\*.log 376 | 377 | Note the backslash (`\`) in front of the `*`. This is necessary because Git does its own filename expansion in addition to your shell’s filename expansion. This command removes all files that have the `.log` extension in the `log/` directory. Or, you can do something like this: 378 | 379 | $ git rm \*~ 380 | 381 | This command removes all files that end with `~`. 382 | 383 | ### Moving Files ### 384 | 385 | Unlike many other VCS systems, Git doesn’t explicitly track file movement. If you rename a file in Git, no metadata is stored in Git that tells it you renamed the file. However, Git is pretty smart about figuring that out after the fact — we’ll deal with detecting file movement a bit later. 386 | 387 | Thus it’s a bit confusing that Git has a `mv` command. If you want to rename a file in Git, you can run something like 388 | 389 | $ git mv file_from file_to 390 | 391 | and it works fine. In fact, if you run something like this and look at the status, you’ll see that Git considers it a renamed file: 392 | 393 | $ git mv README.txt README 394 | $ git status 395 | # On branch master 396 | # Your branch is ahead of 'origin/master' by 1 commit. 397 | # 398 | # Changes to be committed: 399 | # (use "git reset HEAD ..." to unstage) 400 | # 401 | # renamed: README.txt -> README 402 | # 403 | 404 | However, this is equivalent to running something like this: 405 | 406 | $ mv README.txt README 407 | $ git rm README.txt 408 | $ git add README 409 | 410 | Git figures out that it’s a rename implicitly, so it doesn’t matter if you rename a file that way or with the `mv` command. The only real difference is that `mv` is one command instead of three — it’s a convenience function. More important, you can use any tool you like to rename a file, and address the add/rm later, before you commit. 411 | 412 | ## Viewing the Commit History ## 413 | 414 | After you have created several commits, or if you have cloned a repository with an existing commit history, you’ll probably want to look back to see what has happened. The most basic and powerful tool to do this is the `git log` command. 415 | 416 | These examples use a very simple project called simplegit that I often use for demonstrations. To get the project, run 417 | 418 | git clone git://github.com/schacon/simplegit-progit.git 419 | 420 | When you run `git log` in this project, you should get output that looks something like this: 421 | 422 | $ git log 423 | commit ca82a6dff817ec66f44342007202690a93763949 424 | Author: Scott Chacon 425 | Date: Mon Mar 17 21:52:11 2008 -0700 426 | 427 | changed the version number 428 | 429 | commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 430 | Author: Scott Chacon 431 | Date: Sat Mar 15 16:40:33 2008 -0700 432 | 433 | removed unnecessary test code 434 | 435 | commit a11bef06a3f659402fe7563abf99ad00de2209e6 436 | Author: Scott Chacon 437 | Date: Sat Mar 15 10:31:28 2008 -0700 438 | 439 | first commit 440 | 441 | By default, with no arguments, `git log` lists the commits made in that repository in reverse chronological order. That is, the most recent commits show up first. As you can see, this command lists each commit with its SHA-1 checksum, the author’s name and e-mail, the date written, and the commit message. 442 | 443 | A huge number and variety of options to the `git log` command are available to show you exactly what you’re looking for. Here, we’ll show you some of the most-used options. 444 | 445 | One of the more helpful options is `-p`, which shows the diff introduced in each commit. You can also use `-2`, which limits the output to only the last two entries: 446 | 447 | $ git log -p -2 448 | commit ca82a6dff817ec66f44342007202690a93763949 449 | Author: Scott Chacon 450 | Date: Mon Mar 17 21:52:11 2008 -0700 451 | 452 | changed the version number 453 | 454 | diff --git a/Rakefile b/Rakefile 455 | index a874b73..8f94139 100644 456 | --- a/Rakefile 457 | +++ b/Rakefile 458 | @@ -5,7 +5,7 @@ require 'rake/gempackagetask' 459 | spec = Gem::Specification.new do |s| 460 | - s.version = "0.1.0" 461 | + s.version = "0.1.1" 462 | s.author = "Scott Chacon" 463 | 464 | commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 465 | Author: Scott Chacon 466 | Date: Sat Mar 15 16:40:33 2008 -0700 467 | 468 | removed unnecessary test code 469 | 470 | diff --git a/lib/simplegit.rb b/lib/simplegit.rb 471 | index a0a60ae..47c6340 100644 472 | --- a/lib/simplegit.rb 473 | +++ b/lib/simplegit.rb 474 | @@ -18,8 +18,3 @@ class SimpleGit 475 | end 476 | 477 | end 478 | - 479 | -if $0 == __FILE__ 480 | - git = SimpleGit.new 481 | - puts git.show 482 | -end 483 | \ No newline at end of file 484 | 485 | This option displays the same information but with a diff directly following each entry. This is very helpful for code review or to quickly browse what happened during a series of commits that a collaborator has added. 486 | You can also use a series of summarizing options with `git log`. For example, if you want to see some abbreviated stats for each commit, you can use the `--stat` option: 487 | 488 | $ git log --stat 489 | commit ca82a6dff817ec66f44342007202690a93763949 490 | Author: Scott Chacon 491 | Date: Mon Mar 17 21:52:11 2008 -0700 492 | 493 | changed the version number 494 | 495 | Rakefile | 2 +- 496 | 1 files changed, 1 insertions(+), 1 deletions(-) 497 | 498 | commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 499 | Author: Scott Chacon 500 | Date: Sat Mar 15 16:40:33 2008 -0700 501 | 502 | removed unnecessary test code 503 | 504 | lib/simplegit.rb | 5 ----- 505 | 1 files changed, 0 insertions(+), 5 deletions(-) 506 | 507 | commit a11bef06a3f659402fe7563abf99ad00de2209e6 508 | Author: Scott Chacon 509 | Date: Sat Mar 15 10:31:28 2008 -0700 510 | 511 | first commit 512 | 513 | README | 6 ++++++ 514 | Rakefile | 23 +++++++++++++++++++++++ 515 | lib/simplegit.rb | 25 +++++++++++++++++++++++++ 516 | 3 files changed, 54 insertions(+), 0 deletions(-) 517 | 518 | As you can see, the `--stat` option prints below each commit entry a list of modified files, how many files were changed, and how many lines in those files were added and removed. It also puts a summary of the information at the end. 519 | Another really useful option is `--pretty`. This option changes the log output to formats other than the default. A few prebuilt options are available for you to use. The `oneline` option prints each commit on a single line, which is useful if you’re looking at a lot of commits. In addition, the `short`, `full`, and `fuller` options show the output in roughly the same format but with less or more information, respectively: 520 | 521 | $ git log --pretty=oneline 522 | ca82a6dff817ec66f44342007202690a93763949 changed the version number 523 | 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test code 524 | a11bef06a3f659402fe7563abf99ad00de2209e6 first commit 525 | 526 | The most interesting option is `format`, which allows you to specify your own log output format. This is especially useful when you’re generating output for machine parsing — because you specify the format explicitly, you know it won’t change with updates to Git: 527 | 528 | $ git log --pretty=format:"%h - %an, %ar : %s" 529 | ca82a6d - Scott Chacon, 11 months ago : changed the version number 530 | 085bb3b - Scott Chacon, 11 months ago : removed unnecessary test code 531 | a11bef0 - Scott Chacon, 11 months ago : first commit 532 | 533 | Table 2-1 lists some of the more useful options that format takes. 534 | 535 | Option Description of Output 536 | %H Commit hash 537 | %h Abbreviated commit hash 538 | %T Tree hash 539 | %t Abbreviated tree hash 540 | %P Parent hashes 541 | %p Abbreviated parent hashes 542 | %an Author name 543 | %ae Author e-mail 544 | %ad Author date (format respects the –date= option) 545 | %ar Author date, relative 546 | %cn Committer name 547 | %ce Committer email 548 | %cd Committer date 549 | %cr Committer date, relative 550 | %s Subject 551 | 552 | You may be wondering what the difference is between _author_ and _committer_. The author is the person who originally wrote the work, whereas the committer is the person who last applied the work. So, if you send in a patch to a project and one of the core members applies the patch, both of you get credit — you as the author and the core member as the committer. We’ll cover this distinction a bit more in Chapter 5. 553 | 554 | The oneline and format options are particularly useful with another `log` option called `--graph`. This option adds a nice little ASCII graph showing your branch and merge history, which we can see our copy of the Grit project repository: 555 | 556 | $ git log --pretty=format:"%h %s" --graph 557 | * 2d3acf9 ignore errors from SIGCHLD on trap 558 | * 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit 559 | |\ 560 | | * 420eac9 Added a method for getting the current branch. 561 | * | 30e367c timeout code and tests 562 | * | 5a09431 add timeout protection to grit 563 | * | e1193f8 support for heads with slashes in them 564 | |/ 565 | * d6016bc require time for xmlschema 566 | * 11d191e Merge branch 'defunkt' into local 567 | 568 | Those are only some simple output-formatting options to `git log` — there are many more. Table 2-2 lists the options we’ve covered so far and some other common formatting options that may be useful, along with how they change the output of the log command. 569 | 570 | Option Description 571 | -p Show the patch introduced with each commit. 572 | --stat Show statistics for files modified in each commit. 573 | --shortstat Display only the changed/insertions/deletions line from the --stat command. 574 | --name-only Show the list of files modified after the commit information. 575 | --name-status Show the list of files affected with added/modified/deleted information as well. 576 | --abbrev-commit Show only the first few characters of the SHA-1 checksum instead of all 40. 577 | --relative-date Display the date in a relative format (for example, “2 weeks ago”) instead of using the full date format. 578 | --graph Display an ASCII graph of the branch and merge history beside the log output. 579 | --pretty Show commits in an alternate format. Options include oneline, short, full, fuller, and format (where you specify your own format). 580 | 581 | ### Limiting Log Output ### 582 | 583 | In addition to output-formatting options, git log takes a number of useful limiting options — that is, options that let you show only a subset of commits. You’ve seen one such option already — the `-2` option, which show only the last two commits. In fact, you can do `-`, where `n` is any integer to show the last `n` commits. In reality, you’re unlikely to use that often, because Git by default pipes all output through a pager so you see only one page of log output at a time. 584 | 585 | However, the time-limiting options such as `--since` and `--until` are very useful. For example, this command gets the list of commits made in the last two weeks: 586 | 587 | $ git log --since=2.weeks 588 | 589 | This command works with lots of formats — you can specify a specific date (“2008-01-15”) or a relative date such as “2 years 1 day 3 minutes ago”. 590 | 591 | You can also filter the list to commits that match some search criteria. The `--author` option allows you to filter on a specific author, and the `--grep` option lets you search for keywords in the commit messages. (Note that if you want to specify both author and grep options, you have to add `--all-match` or the command will match commits with either.) 592 | 593 | The last really useful option to pass to `git log` as a filter is a path. If you specify a directory or file name, you can limit the log output to commits that introduced a change to those files. This is always the last option and is generally preceded by double dashes (`--`) to separate the paths from the options. 594 | 595 | In Table 2-3 we’ll list these and a few other common options for your reference. 596 | 597 | Option Description 598 | -(n) Show only the last n commits 599 | --since, --after Limit the commits to those made after the specified date. 600 | --until, --before Limit the commits to those made before the specified date. 601 | --author Only show commits in which the author entry matches the specified string. 602 | --committer Only show commits in which the committer entry matches the specified string. 603 | 604 | For example, if you want to see which commits modifying test files in the Git source code history were committed by Junio Hamano and were not merges in the month of October 2008, you can run something like this: 605 | 606 | $ git log --pretty="%h - %s" --author=gitster --since="2008-10-01" \ 607 | --before="2008-11-01" --no-merges -- t/ 608 | 5610e3b - Fix testcase failure when extended attribute 609 | acd3b9e - Enhance hold_lock_file_for_{update,append}() 610 | f563754 - demonstrate breakage of detached checkout wi 611 | d1a43f2 - reset --hard/read-tree --reset -u: remove un 612 | 51a94af - Fix "checkout --track -b newbranch" on detac 613 | b0ad11e - pull: allow "git pull origin $something:$cur 614 | 615 | Of the nearly 20,000 commits in the Git source code history, this command shows the 6 that match those criteria. 616 | 617 | ### Using a GUI to Visualize History ### 618 | 619 | If you like to use a more graphical tool to visualize your commit history, you may want to take a look at a Tcl/Tk program called gitk that is distributed with Git. Gitk is basically a visual `git log` tool, and it accepts nearly all the filtering options that `git log` does. If you type gitk on the command line in your project, you should see something like Figure 2-2. 620 | 621 | Insert 18333fig0202.png 622 | Figure 2-2. The gitk history visualizer. 623 | 624 | You can see the commit history in the top half of the window along with a nice ancestry graph. The diff viewer in the bottom half of the window shows you the changes introduced at any commit you click. 625 | 626 | ## Undoing Things ## 627 | 628 | At any stage, you may want to undo something. Here, we’ll review a few basic tools for undoing changes that you’ve made. Be careful, because you can’t always undo some of these undos. This is one of the few areas in Git where you may lose some work if you do it wrong. 629 | 630 | ### Changing Your Last Commit ### 631 | 632 | One of the common undos takes place when you commit too early and possibly forget to add some files, or you mess up your commit message. If you want to try that commit again, you can run commit with the `--amend` option: 633 | 634 | $ git commit --amend 635 | 636 | This command takes your staging area and uses it for the commit. If you’ve made no changes since your last commit (for instance, you run this command immediately after your previous commit), then your snapshot will look exactly the same and all you’ll change is your commit message. 637 | 638 | The same commit-message editor fires up, but it already contains the message of your previous commit. You can edit the message the same as always, but it overwrites your previous commit. 639 | 640 | As an example, if you commit and then realize you forgot to stage the changes in a file you wanted to add to this commit, you can do something like this: 641 | 642 | $ git commit -m 'initial commit' 643 | $ git add forgotten_file 644 | $ git commit --amend 645 | 646 | All three of these commands end up with a single commit — the second commit replaces the results of the first. 647 | 648 | ### Unstaging a Staged File ### 649 | 650 | The next two sections demonstrate how to wrangle your staging area and working directory changes. The nice part is that the command you use to determine the state of those two areas also reminds you how to undo changes to them. For example, let’s say you’ve changed two files and want to commit them as two separate changes, but you accidentally type `git add *` and stage them both. How can you unstage one of the two? The `git status` command reminds you: 651 | 652 | $ git add . 653 | $ git status 654 | # On branch master 655 | # Changes to be committed: 656 | # (use "git reset HEAD ..." to unstage) 657 | # 658 | # modified: README.txt 659 | # modified: benchmarks.rb 660 | # 661 | 662 | Right below the “Changes to be committed” text, it says use `git reset HEAD ...` to unstage. So, let’s use that advice to unstage the benchmarks.rb file: 663 | 664 | $ git reset HEAD benchmarks.rb 665 | benchmarks.rb: locally modified 666 | $ git status 667 | # On branch master 668 | # Changes to be committed: 669 | # (use "git reset HEAD ..." to unstage) 670 | # 671 | # modified: README.txt 672 | # 673 | # Changed but not updated: 674 | # (use "git add ..." to update what will be committed) 675 | # (use "git checkout -- ..." to discard changes in working directory) 676 | # 677 | # modified: benchmarks.rb 678 | # 679 | 680 | The command is a bit strange, but it works. The benchmarks.rb file is modified but once again unstaged. 681 | 682 | ### Unmodifying a Modified File ### 683 | 684 | What if you realize that you don’t want to keep your changes to the benchmarks.rb file? How can you easily unmodify it — revert it back to what it looked like when you last committed (or initially cloned, or however you got it into your working directory)? Luckily, `git status` tells you how to do that, too. In the last example output, the unstaged area looks like this: 685 | 686 | # Changed but not updated: 687 | # (use "git add ..." to update what will be committed) 688 | # (use "git checkout -- ..." to discard changes in working directory) 689 | # 690 | # modified: benchmarks.rb 691 | # 692 | 693 | It tells you pretty explicitly how to discard the changes you’ve made (at least, the newer versions of Git, 1.6.1 and later, do this — if you have an older version, we highly recommend upgrading it to get some of these nicer usability features). Let’s do what it says: 694 | 695 | $ git checkout -- benchmarks.rb 696 | $ git status 697 | # On branch master 698 | # Changes to be committed: 699 | # (use "git reset HEAD ..." to unstage) 700 | # 701 | # modified: README.txt 702 | # 703 | 704 | You can see that the changes have been reverted. You should also realize that this is a dangerous command: any changes you made to that file are gone — you just copied another file over it. Don’t ever use this command unless you absolutely know that you don’t want the file. If you just need to get it out of the way, we’ll go over stashing and branching in the next chapter; these are generally better ways to go. 705 | 706 | Remember, anything that is committed in Git can almost always be recovered. Even commits that were on branches that were deleted or commits that were overwritten with an `--amend` commit can be recovered (see Chapter 9 for data recovery). However, anything you lose that was never committed is likely never to be seen again. 707 | 708 | ## Working with Remotes ## 709 | 710 | To be able to collaborate on any Git project, you need to know how to manage your remote repositories. Remote repositories are versions of your project that are hosted on the Internet or network somewhere. You can have several of them, each of which generally is either read-only or read/write for you. Collaborating with others involves managing these remote repositories and pushing and pulling data to and from them when you need to share work. 711 | Managing remote repositories includes knowing how to add remote repositories, remove remotes that are no longer valid, manage various remote branches and define them as being tracked or not, and more. In this section, we’ll cover these remote-management skills. 712 | 713 | ### Showing Your Remotes ### 714 | 715 | To see which remote servers you have configured, you can run the git remote command. It lists the shortnames of each remote handle you’ve specified. If you’ve cloned your repository, you should at least see origin — that is the default name Git gives to the server you cloned from: 716 | 717 | $ git clone git://github.com/schacon/ticgit.git 718 | Initialized empty Git repository in /private/tmp/ticgit/.git/ 719 | remote: Counting objects: 595, done. 720 | remote: Compressing objects: 100% (269/269), done. 721 | remote: Total 595 (delta 255), reused 589 (delta 253) 722 | Receiving objects: 100% (595/595), 73.31 KiB | 1 KiB/s, done. 723 | Resolving deltas: 100% (255/255), done. 724 | $ cd ticgit 725 | $ git remote 726 | origin 727 | 728 | You can also specify `-v`, which shows you the URL that Git has stored for the shortname to be expanded to: 729 | 730 | $ git remote -v 731 | origin git://github.com/schacon/ticgit.git 732 | 733 | If you have more than one remote, the command lists them all. For example, my Grit repository looks something like this. 734 | 735 | $ cd grit 736 | $ git remote -v 737 | bakkdoor git://github.com/bakkdoor/grit.git 738 | cho45 git://github.com/cho45/grit.git 739 | defunkt git://github.com/defunkt/grit.git 740 | koke git://github.com/koke/grit.git 741 | origin git@github.com:mojombo/grit.git 742 | 743 | This means we can pull contributions from any of these users pretty easily. But notice that only the origin remote is an SSH URL, so it’s the only one I can push to (we’ll cover why this is in Chapter 4). 744 | 745 | ### Adding Remote Repositories ### 746 | 747 | I’ve mentioned and given some demonstrations of adding remote repositories in previous sections, but here is how to do it explicitly. To add a new remote Git repository as a shortname you can reference easily, run `git remote add [shortname] [url]`: 748 | 749 | $ git remote 750 | origin 751 | $ git remote add pb git://github.com/paulboone/ticgit.git 752 | $ git remote -v 753 | origin git://github.com/schacon/ticgit.git 754 | pb git://github.com/paulboone/ticgit.git 755 | 756 | Now you can use the string pb on the command line in lieu of the whole URL. For example, if you want to fetch all the information that Paul has but that you don’t yet have in your repository, you can run git fetch pb: 757 | 758 | $ git fetch pb 759 | remote: Counting objects: 58, done. 760 | remote: Compressing objects: 100% (41/41), done. 761 | remote: Total 44 (delta 24), reused 1 (delta 0) 762 | Unpacking objects: 100% (44/44), done. 763 | From git://github.com/paulboone/ticgit 764 | * [new branch] master -> pb/master 765 | * [new branch] ticgit -> pb/ticgit 766 | 767 | Paul’s master branch is accessible locally as `pb/master` — you can merge it into one of your branches, or you can check out a local branch at that point if you want to inspect it. 768 | 769 | ### Fetching and Pulling from Your Remotes ### 770 | 771 | As you just saw, to get data from your remote projects, you can run: 772 | 773 | $ git fetch [remote-name] 774 | 775 | The command goes out to that remote project and pulls down all the data from that remote project that you don’t have yet. After you do this, you should have references to all the branches from that remote, which you can merge in or inspect at any time. (We’ll go over what branches are and how to use them in much more detail in Chapter 3.) 776 | 777 | If you clone a repository, the command automatically adds that remote repository under the name origin. So, `git fetch origin` fetches any new work that has been pushed to that server since you cloned (or last fetched from) it. It’s important to note that the fetch command pulls the data to your local repository — it doesn’t automatically merge it with any of your work or modify what you’re currently working on. You have to merge it manually into your work when you’re ready. 778 | 779 | If you have a branch set up to track a remote branch (see the next section and Chapter 3 for more information), you can use the `git pull` command to automatically fetch and then merge a remote branch into your current branch. This may be an easier or more comfortable workflow for you; and by default, the `git clone` command automatically sets up your local master branch to track the remote master branch on the server you cloned from (assuming the remote has a master branch). Running `git pull` generally fetches data from the server you originally cloned from and automatically tries to merge it into the code you’re currently working on. 780 | 781 | ### Pushing to Your Remotes ### 782 | 783 | When you have your project at a point that you want to share, you have to push it upstream. The command for this is simple: `git push [remote-name] [branch-name]`. If you want to push your master branch to your `origin` server (again, cloning generally sets up both of those names for you automatically), then you can run this to push your work back up to the server: 784 | 785 | $ git push origin master 786 | 787 | This command works only if you cloned from a server to which you have write access and if nobody has pushed in the meantime. If you and someone else clone at the same time and they push upstream and then you push upstream, your push will rightly be rejected. You’ll have to pull down their work first and incorporate it into yours before you’ll be allowed to push. See Chapter 3 for more detailed information on how to push to remote servers. 788 | 789 | ### Inspecting a Remote ### 790 | 791 | If you want to see more information about a particular remote, you can use the `git remote show [remote-name]` command. If you run this command with a particular shortname, such as `origin`, you get something like this: 792 | 793 | $ git remote show origin 794 | * remote origin 795 | URL: git://github.com/schacon/ticgit.git 796 | Remote branch merged with 'git pull' while on branch master 797 | master 798 | Tracked remote branches 799 | master 800 | ticgit 801 | 802 | It lists the URL for the remote repository as well as the tracking branch information. The command helpfully tells you that if you’re on the master branch and you run `git pull`, it will automatically merge in the master branch on the remote after it fetches all the remote references. It also lists all the remote references it has pulled down. 803 | 804 | That is a simple example you’re likely to encounter. When you’re using Git more heavily, however, you may see much more information from `git remote show`: 805 | 806 | $ git remote show origin 807 | * remote origin 808 | URL: git@github.com:defunkt/github.git 809 | Remote branch merged with 'git pull' while on branch issues 810 | issues 811 | Remote branch merged with 'git pull' while on branch master 812 | master 813 | New remote branches (next fetch will store in remotes/origin) 814 | caching 815 | Stale tracking branches (use 'git remote prune') 816 | libwalker 817 | walker2 818 | Tracked remote branches 819 | acl 820 | apiv2 821 | dashboard2 822 | issues 823 | master 824 | postgres 825 | Local branch pushed with 'git push' 826 | master:master 827 | 828 | This command shows which branch is automatically pushed when you run `git push` on certain branches. It also shows you which remote branches on the server you don’t yet have, which remote branches you have that have been removed from the server, and multiple branches that are automatically merged when you run `git pull`. 829 | 830 | ### Removing and Renaming Remotes ### 831 | 832 | If you want to rename a reference, in newer versions of Git you can run `git remote rename` to change a remote’s shortname. For instance, if you want to rename `pb` to `paul`, you can do so with `git remote rename`: 833 | 834 | $ git remote rename pb paul 835 | $ git remote 836 | origin 837 | paul 838 | 839 | It’s worth mentioning that this changes your remote branch names, too. What used to be referenced at `pb/master` is now at `paul/master`. 840 | 841 | If you want to remove a reference for some reason — you’ve moved the server or are no longer using a particular mirror, or perhaps a contributor isn’t contributing anymore — you can use `git remote rm`: 842 | 843 | $ git remote rm paul 844 | $ git remote 845 | origin 846 | 847 | ## Tagging ## 848 | 849 | Like most VCSs, Git has the ability to tag specific points in history as being important. Generally, people use this functionality to mark release points (v1.0, and so on). In this section, you’ll learn how to list the available tags, how to create new tags, and what the different types of tags are. 850 | 851 | ### Listing Your Tags ### 852 | 853 | Listing the available tags in Git is straightforward. Just type `git tag`: 854 | 855 | $ git tag 856 | v0.1 857 | v1.3 858 | 859 | This command lists the tags in alphabetical order; the order in which they appear has no real importance. 860 | 861 | You can also search for tags with a particular pattern. The Git source repo, for instance, contains more than 240 tags. If you’re only interested in looking at the 1.4.2 series, you can run this: 862 | 863 | $ git tag -l 'v1.4.2.*' 864 | v1.4.2.1 865 | v1.4.2.2 866 | v1.4.2.3 867 | v1.4.2.4 868 | 869 | ### Creating Tags ### 870 | 871 | Git uses two main types of tags: lightweight and annotated. A lightweight tag is very much like a branch that doesn’t change — it’s just a pointer to a specific commit. Annotated tags, however, are stored as full objects in the Git database. They’re checksummed; contain the tagger name, e-mail, and date; have a tagging message; and can be signed and verified with GNU Privacy Guard (GPG). It’s generally recommended that you create annotated tags so you can have all this information; but if you want a temporary tag or for some reason don’t want to keep the other information, lightweight tags are available too. 872 | 873 | ### Annotated Tags ### 874 | 875 | Creating an annotated tag in Git is simple. The easiest way is to specify `-a` when you run the `tag` command: 876 | 877 | $ git tag -a v1.4 -m 'my version 1.4' 878 | $ git tag 879 | v0.1 880 | v1.3 881 | v1.4 882 | 883 | The `-m` specifies a tagging message, which is stored with the tag. If you don’t specify a message for an annotated tag, Git launches your editor so you can type it in. 884 | 885 | You can see the tag data along with the commit that was tagged by using the `git show` command: 886 | 887 | $ git show v1.4 888 | tag v1.4 889 | Tagger: Scott Chacon 890 | Date: Mon Feb 9 14:45:11 2009 -0800 891 | 892 | my version 1.4 893 | commit 15027957951b64cf874c3557a0f3547bd83b3ff6 894 | Merge: 4a447f7... a6b4c97... 895 | Author: Scott Chacon 896 | Date: Sun Feb 8 19:02:46 2009 -0800 897 | 898 | Merge branch 'experiment' 899 | 900 | That shows the tagger information, the date the commit was tagged, and the annotation message before showing the commit information. 901 | 902 | ### Signed Tags ### 903 | 904 | You can also sign your tags with GPG, assuming you have a private key. All you have to do is use `-s` instead of `-a`: 905 | 906 | $ git tag -s v1.5 -m 'my signed 1.5 tag' 907 | You need a passphrase to unlock the secret key for 908 | user: "Scott Chacon " 909 | 1024-bit DSA key, ID F721C45A, created 2009-02-09 910 | 911 | If you run `git show` on that tag, you can see your GPG signature attached to it: 912 | 913 | $ git show v1.5 914 | tag v1.5 915 | Tagger: Scott Chacon 916 | Date: Mon Feb 9 15:22:20 2009 -0800 917 | 918 | my signed 1.5 tag 919 | -----BEGIN PGP SIGNATURE----- 920 | Version: GnuPG v1.4.8 (Darwin) 921 | 922 | iEYEABECAAYFAkmQurIACgkQON3DxfchxFr5cACeIMN+ZxLKggJQf0QYiQBwgySN 923 | Ki0An2JeAVUCAiJ7Ox6ZEtK+NvZAj82/ 924 | =WryJ 925 | -----END PGP SIGNATURE----- 926 | commit 15027957951b64cf874c3557a0f3547bd83b3ff6 927 | Merge: 4a447f7... a6b4c97... 928 | Author: Scott Chacon 929 | Date: Sun Feb 8 19:02:46 2009 -0800 930 | 931 | Merge branch 'experiment' 932 | 933 | A bit later, you’ll learn how to verify signed tags. 934 | 935 | ### Lightweight Tags ### 936 | 937 | Another way to tag commits is with a lightweight tag. This is basically the commit checksum stored in a file — no other information is kept. To create a lightweight tag, don’t supply the `-a`, `-s`, or `-m` option: 938 | 939 | $ git tag v1.4-lw 940 | $ git tag 941 | v0.1 942 | v1.3 943 | v1.4 944 | v1.4-lw 945 | v1.5 946 | 947 | This time, if you run `git show` on the tag, you don’t see the extra tag information. The command just shows the commit: 948 | 949 | $ git show v1.4-lw 950 | commit 15027957951b64cf874c3557a0f3547bd83b3ff6 951 | Merge: 4a447f7... a6b4c97... 952 | Author: Scott Chacon 953 | Date: Sun Feb 8 19:02:46 2009 -0800 954 | 955 | Merge branch 'experiment' 956 | 957 | ### Verifying Tags ### 958 | 959 | To verify a signed tag, you use `git tag -v [tag-name]`. This command uses GPG to verify the signature. You need the signer’s public key in your keyring for this to work properly: 960 | 961 | $ git tag -v v1.4.2.1 962 | object 883653babd8ee7ea23e6a5c392bb739348b1eb61 963 | type commit 964 | tag v1.4.2.1 965 | tagger Junio C Hamano 1158138501 -0700 966 | 967 | GIT 1.4.2.1 968 | 969 | Minor fixes since 1.4.2, including git-mv and git-http with alternates. 970 | gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A 971 | gpg: Good signature from "Junio C Hamano " 972 | gpg: aka "[jpeg image of size 1513]" 973 | Primary key fingerprint: 3565 2A26 2040 E066 C9A7 4A7D C0C6 D9A4 F311 9B9A 974 | 975 | If you don’t have the signer’s public key, you get something like this instead: 976 | 977 | gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A 978 | gpg: Can't check signature: public key not found 979 | error: could not verify the tag 'v1.4.2.1' 980 | 981 | ### Tagging Later ### 982 | 983 | You can also tag commits after you’ve moved past them. Suppose your commit history looks like this: 984 | 985 | $ git log --pretty=oneline 986 | 15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment' 987 | a6b4c97498bd301d84096da251c98a07c7723e65 beginning write support 988 | 0d52aaab4479697da7686c15f77a3d64d9165190 one more thing 989 | 6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment' 990 | 0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc added a commit function 991 | 4682c3261057305bdd616e23b64b0857d832627b added a todo file 992 | 166ae0c4d3f420721acbb115cc33848dfcc2121a started write support 993 | 9fceb02d0ae598e95dc970b74767f19372d61af8 updated rakefile 994 | 964f16d36dfccde844893cac5b347e7b3d44abbc commit the todo 995 | 8a5cbc430f1a9c3d00faaeffd07798508422908a updated readme 996 | 997 | Now, suppose you forgot to tag the project at v1.2, which was at the "updated rakefile" commit. You can add it after the fact. To tag that commit, you specify the commit checksum (or part of it) at the end of the command: 998 | 999 | $ git tag -a v1.2 9fceb02 1000 | 1001 | You can see that you’ve tagged the commit: 1002 | 1003 | $ git tag 1004 | v0.1 1005 | v1.2 1006 | v1.3 1007 | v1.4 1008 | v1.4-lw 1009 | v1.5 1010 | 1011 | $ git show v1.2 1012 | tag v1.2 1013 | Tagger: Scott Chacon 1014 | Date: Mon Feb 9 15:32:16 2009 -0800 1015 | 1016 | version 1.2 1017 | commit 9fceb02d0ae598e95dc970b74767f19372d61af8 1018 | Author: Magnus Chacon 1019 | Date: Sun Apr 27 20:43:35 2008 -0700 1020 | 1021 | updated rakefile 1022 | ... 1023 | 1024 | ### Sharing Tags ### 1025 | 1026 | By default, the `git push` command doesn’t transfer tags to remote servers. You will have to explicitly push tags to a shared server after you have created them. This process is just like sharing remote branches — you can run `git push origin [tagname]`. 1027 | 1028 | $ git push origin v1.5 1029 | Counting objects: 50, done. 1030 | Compressing objects: 100% (38/38), done. 1031 | Writing objects: 100% (44/44), 4.56 KiB, done. 1032 | Total 44 (delta 18), reused 8 (delta 1) 1033 | To git@github.com:schacon/simplegit.git 1034 | * [new tag] v1.5 -> v1.5 1035 | 1036 | If you have a lot of tags that you want to push up at once, you can also use the `--tags` option to the `git push` command. This will transfer all of your tags to the remote server that are not already there. 1037 | 1038 | $ git push origin --tags 1039 | Counting objects: 50, done. 1040 | Compressing objects: 100% (38/38), done. 1041 | Writing objects: 100% (44/44), 4.56 KiB, done. 1042 | Total 44 (delta 18), reused 8 (delta 1) 1043 | To git@github.com:schacon/simplegit.git 1044 | * [new tag] v0.1 -> v0.1 1045 | * [new tag] v1.2 -> v1.2 1046 | * [new tag] v1.4 -> v1.4 1047 | * [new tag] v1.4-lw -> v1.4-lw 1048 | * [new tag] v1.5 -> v1.5 1049 | 1050 | Now, when someone else clones or pulls from your repository, they will get all your tags as well. 1051 | 1052 | ## Tips and Tricks ## 1053 | 1054 | Before we finish this chapter on basic Git, a few little tips and tricks may make your Git experience a bit simpler, easier, or more familiar. Many people use Git without using any of these tips, and we won’t refer to them or assume you’ve used them later in the book; but you should probably know how to do them. 1055 | 1056 | ### Auto-Completion ### 1057 | 1058 | If you use the Bash shell, Git comes with a nice auto-completion script you can enable. Download the Git source code, and look in the `contrib/completion` directory; there should be a file called `git-completion.bash`. Copy this file to your home directory, and add this to your `.bashrc` file: 1059 | 1060 | source ~/.git-completion.bash 1061 | 1062 | If you want to set up Git to automatically have Bash shell completion for all users, copy this script to the `/opt/local/etc/bash_completion.d` directory on Mac systems or to the `/etc/bash_completion.d/` directory on Linux systems. This is a directory of scripts that Bash will automatically load to provide shell completions. 1063 | 1064 | If you’re using Windows with Git Bash, which is the default when installing Git on Windows with msysGit, auto-completion should be preconfigured. 1065 | 1066 | Press the Tab key when you’re writing a Git command, and it should return a set of suggestions for you to pick from: 1067 | 1068 | $ git co 1069 | commit config 1070 | 1071 | In this case, typing git co and then pressing the Tab key twice suggests commit and config. Adding `m` completes `git commit` automatically. 1072 | 1073 | This also works with options, which is probably more useful. For instance, if you’re running a `git log` command and can’t remember one of the options, you can start typing it and press Tab to see what matches: 1074 | 1075 | $ git log --s 1076 | --shortstat --since= --src-prefix= --stat --summary 1077 | 1078 | That’s a pretty nice trick and may save you some time and documentation reading. 1079 | 1080 | ### Git Aliases ### 1081 | 1082 | Git doesn’t infer your command if you type it in partially. If you don’t want to type the entire text of each of the Git commands, you can easily set up an alias for each command using `git config`. Here are a couple of examples you may want to set up: 1083 | 1084 | $ git config --global alias.co checkout 1085 | $ git config --global alias.br branch 1086 | $ git config --global alias.ci commit 1087 | $ git config --global alias.st status 1088 | 1089 | This means that, for example, instead of typing `git commit`, you just need to type `git ci`. As you go on using Git, you’ll probably use other commands frequently as well; in this case, don’t hesitate to create new aliases. 1090 | 1091 | This technique can also be very useful in creating commands that you think should exist. For example, to correct the usability problem you encountered with unstaging a file, you can add your own unstage alias to Git: 1092 | 1093 | $ git config --global alias.unstage 'reset HEAD --' 1094 | 1095 | This makes the following two commands equivalent: 1096 | 1097 | $ git unstage fileA 1098 | $ git reset HEAD fileA 1099 | 1100 | This seems a bit clearer. It’s also common to add a `last` command, like this: 1101 | 1102 | $ git config --global alias.last 'log -1 HEAD' 1103 | 1104 | This way, you can see the last commit easily: 1105 | 1106 | $ git last 1107 | commit 66938dae3329c7aebe598c2246a8e6af90d04646 1108 | Author: Josh Goebel 1109 | Date: Tue Aug 26 19:48:51 2008 +0800 1110 | 1111 | test for current head 1112 | 1113 | Signed-off-by: Scott Chacon 1114 | 1115 | As you can tell, Git simply replaces the new command with whatever you alias it for. However, maybe you want to run an external command, rather than a Git subcommand. In that case, you start the command with a `!` character. This is useful if you write your own tools that work with a Git repository. We can demonstrate by aliasing `git visual` to run `gitk`: 1116 | 1117 | $ git config --global alias.visual "!gitk" 1118 | 1119 | ## Summary ## 1120 | 1121 | At this point, you can do all the basic local Git operations — creating or cloning a repository, making changes, staging and committing those changes, and viewing the history of all the changes the repository has been through. Next, we’ll cover Git’s killer feature: its branching model. 1122 | -------------------------------------------------------------------------------- /03-git-branching/01-chapter3.markdown: -------------------------------------------------------------------------------- 1 | # การแตก branch ใน Git # 2 | 3 | เกือบทุก VCS support การแตก branch ทางใดซักทางหนึ่ง การแตก Branch หมายถึงคุณแยกตัวออกมาจาก main line ของการพัฒนาและทำงานต่อไปบนบนนั้นโดยไม่ไปยุ่งเกี่ยวกับ main line ในหลายๆ VCS การทำแบบนี้ค่อนข้างจะเปลือง ส่วนใหญ่จะเป็นการ copy ทั้ง directory ของ source code ซึ่งจะกินเวลานานมว๊ากกบน project ใหญ่ๆ 4 | 5 | หลายคนเรียกการแตก branch ใน Git เป็น “killer feature” และมันทำให้ Git โดดเด่นออกมาจาก VCS อื่นๆ ทำไมน่ะเหรอ? เพราะวิธีที่ Git แตก branch มันถูกโคตร การแตก branch ทำได้ในชั่วพริบตาและการ switch ไปๆมาๆระหว่าง branch ก็เร็วพอๆกัน ไม่เหมือน VCS ดาษๆทั่วไป Git ผลักดันกระบวนการทำงาน workflow ให้แตก branch และ merge บ่อยๆแบบที่ทำได้วันละหลายๆครั้ง การทำความเข้าใจและบรรลุ feature นี้จะทำให้ Git กลายเป็นเครื่องมือที่ทรงพลังและมีเอกลักษณ์และทำให้วิถีการทำงานของคุณเปลี่ยนไปเลย 6 | 7 | ## Branch คืออะไร ## 8 | 9 | เพื่อทำความเข้าใจวิธีที่ Git แตก branch เราต้องย้อนกลับมาดูว่า Git เก็บข้อมูลยังไง ตามที่คุณอาจจะจำได้จาก Chapter 1 ว่า Git ไม่ได้เก็บข้อมูลเป็นลำดับของความเปลี่ยนแปลงต่อเวลาแต่เก็บเป็นลำดับของ snapshot ต่อเวลา 10 | 11 | เวลาคุณ commit ใน Git นั้น Git จะเก็บเป็น object ของการ commit ซึ่งประกอบด้วย pointer ชี้ไปยัง snapshot ของ content ที่คุณ stage ไว้, metadata ของชื่อผู้แก้ไขและ message ที่บันทึกไว้ และ pointer ที่ชี้ไปยัง parent ลำดับถัดไปของ commit นั้นๆ (ซึ่งอาจจะไม่มีก็ได้ถ้าเป็น commit ครั้งแรก, อาจจะมีอันเดียวชี้ไปยัง parent ของ commit ปรกติทั่วไปหรืออาจจะมี parent หลายอันสำหรับ commit ที่เป็นผลจากการ merge หลายๆ branch เข้าด้วยกัน) 12 | 13 | ลองจินตนาการว่าคุณมี directory อันนึงที่มี file อยู่ข้างใน 3 files แล้วคุณก็ stage ทั้งหมดและ commit การ stage file จะสร้าง checksum ของแต่ละ file (ไอ้ SHA-1 hash ที่บอกไว้ใน Chapter 1 นั่นแหละ), แล้วบันทึก version ของ file นั้นๆใน Git repository (Git อ้างอิงพวกมันแบบ blobs) และเพิ่ม checksum นั้นลงไปใน staging area: 14 | 15 | $ git add README test.rb LICENSE 16 | $ git commit -m 'initial commit of my project' 17 | 18 | เมื่อคุณทำการ commit ด้วยคำสั่ง `git commit` Git จะคำนวน checksum ของแต่ละ subdirectory (ในกรณีนี้ก็มีแค่ root project directory) และบันทึกโครงสร้างของ directory ใน Git repository หลังจากนั้น Git ก็จะสร้าง commit object ที่มี metadata และ pointer ชี้ไปยังโครงสร้างของ root project เพื่อที่มันจะได้สร้าง snapshot นั้นขึ้นมาใหม่ได้เมื่อต้องการ 19 | 20 | Git repository ของคุณตอนนี้จะมี 5 objects: blob แต่ละ blob สำหรับ content ของแต่ละ file ใน 3 files นั้น, โครงสร้าง root directory ที่เก็บ list ของสิ่งของในนั้นและบันทึกว่า file ไหนถูกเก็บใส่ blob ไหน และ 1 commit ที่มี pointer อันนึงชี้ไปยังโครงสร้างของ root directory กับพวก metadata ของ commit นั้น ซึ่งหน้าตาของข้อมูลใน Git repository ของคุณก็มีคอนเซปประมาณรูป Figure 3-1. 21 | 22 | Insert 18333fig0301.png 23 | Figure 3-1. Single commit repository data. 24 | 25 | ถ้าคุณทำการแก้ไขใดๆ แล้ว commit ซ้ำอีกครั้ง commit อันถัดไปจะเก็บในรูป pointer ชี้ไปยัง commit ก่อนหน้า ทำไปอีก 2 commits history ของคุณน่าจะมีหน้าตาประมาณรูป Figure 3-2. 26 | 27 | Insert 18333fig0302.png 28 | Figure 3-2. Git object data for multiple commits. 29 | 30 | branch อันนึงใน Git เป็นแค่ pointer ฉบับกระเป๋าของ commits เหล่านี้ ชื่อโดย default ของ branch ใน Git คือ master ขณะที่คุณ commit ครั้งแรกส่งกำลังส่ง master branch อันนึงที่ points กลับไปยัง commit ก่อนหน้า ทุกครั้งที่คุณ commit มันก็ค่อยๆขยับไปๆโดยอัตโนมัติ 31 | 32 | Insert 18333fig0303.png 33 | Figure 3-3. Branch pointing into the commit data’s history. 34 | 35 | แล้วจะเกิดอะไรขึ้นถ้าคุณสร้าง branch ใหม่? ก็แค่สร้าง pointer อันใหม่เพื่อที่จะโยกย้ายไปมาตามใจ ยกตัวอย่างว่าคุณสร้าง branch ใหม่ชื่อว่า testing ซึ่งสามารถทำได้ด้วยคำสั่ง `git branch`: 36 | 37 | $ git branch testing 38 | 39 | มันจะสร้าง pointer อันใหม่ใน commit ปัจจุบันที่คุณอยู่ (ดูรูป Figure 3-4). 40 | 41 | Insert 18333fig0304.png 42 | Figure 3-4. Multiple branches pointing into the commit’s data history. 43 | 44 | แล้ว Git มันรู้ได้ไงว่าตอนนี้คุณอยู่ branch ไหน? เพราะมันแอบจำ pointer พิเศษที่ชื่อว่า HEAD จำไว้ว่า HEAD นี้ต่างกันกับ concept ของ HEAD ใน VCS อื่นๆที่คุณอาจจะเคยใช้มาเยอะมาก (ไม่ว่าจะเป็น Subversion หรือ CVS) ใน Git นี่คือ pointer ชี้ไปยัง local branch ที่คุณทำงานอยู่ อย่างในกรณีนี้ คุณยังอยู่บน master คำสั่ง git branch แค่สร้าง branch ใหม่ให้เฉยๆ มันเปล่า switch คุณไปยัง branch ใหม่นั้นแต่อย่างใด (ดูรูป Figure 3-5). 45 | 46 | Insert 18333fig0305.png 47 | Figure 3-5. HEAD file pointing to the branch you’re on. 48 | 49 | ถ้าจะ switch ไปยัง branch ใดๆที่มีอยู่ คุณก็แค่สั่ง `git checkout` ลอง switch ไปยัง testing branch อันใหม่กัน: 50 | 51 | $ git checkout testing 52 | 53 | คำสั่งนี้จะย้าย HEAD ให้ไปชี้ที่ testing branch (ดังรูป Figure 3-6). 54 | 55 | Insert 18333fig0306.png 56 | Figure 3-6. HEAD points to another branch when you switch branches. 57 | 58 | แล้วมันสำคัญยังไงอ่ะ? อ่ะ มาดู commit ถัดมากัน: 59 | 60 | $ vim test.rb 61 | $ git commit -a -m 'made a change' 62 | 63 | รูป Figure 3-7 จะโชว์ผลให้ดู 64 | 65 | Insert 18333fig0307.png 66 | Figure 3-7. The branch that HEAD points to moves forward with each commit. 67 | 68 | น่าสนใจนะเนี่ย เพราะตอนนี้ testing branch ของคุณขยับไปข้างหน้า แต่ master branch ยังคงชี้ไปยัง commit ที่คุณอยู่ก่อนหน้านี้ตอนที่ switch branch ด้วยคำสั่ง `git checkout` อ่ะ ลอง switch กลับไป master branch กัน: 69 | 70 | $ git checkout master 71 | 72 | มาดูผลในรูป Figure 3-8 73 | 74 | Insert 18333fig0308.png 75 | Figure 3-8. HEAD moves to another branch on a checkout. 76 | 77 | คำสั่งนั้นทำสองอย่าง มันขยับ HEAD pointer กลับไปยัง master branch แต่มัน revert file ทั้งหลายใน working directory ของคุณกลับไปยัง snapshot ที่ master ชี้อยู่ ซึ่งหมายความว่าความเปลี่ยนแปลงทั้งหลายที่คุณแก้ไปตั้งแต่จุดนี้ถูกแยกออกไปจาก version เก่าของ project สรุปคือมัน rewind งานที่คุณทำไปบน testing branch กลับมาชั่วคราวเพื่อที่คุณจะได้ลองแก้ไปทางอื่นได้ 78 | 79 | มาลองแก้ file ซักนิดแล้ว commit อีกทีดู: 80 | 81 | $ vim test.rb 82 | $ git commit -a -m 'made other changes' 83 | 84 | ตอนนี้ project history ของคุณถูกแยกออก (ดู Figure 3-9) คุณสร้าง branch ใหม่และ switch ไป, ทำงานไปบนนั้น, แล้ว switch กลับมาที่ branch หลัก แล้วทำงานอื่นลงไป ความเปลี่ยนแปลงทั้งสองสายถูกตัดขาดจากกันใน branch ทั้งสอง คุณสามารถโดดกลับไปกลับมาระหว่างสอง branches ได้แล้วค่อย merge มันเข้าด้วยกันเมื่อคุณพร้อม ซึ่งทั้งหมดนั่นทำได้ด้วยคำสั่งง่ายอย่าง `branch` และ `checkout` 85 | 86 | Insert 18333fig0309.png 87 | Figure 3-9. The branch histories have diverged. 88 | 89 | เนื่องจาก branch ใน Git จริงๆแล้วเป็นแค่ file ธรรมดาๆที่เก็บตัวหนังสือ 40 อักษรที่เป็น SHA-1 checksum ของ commit ที่มันชี้ไปหา การสร้างหรือทำลาย branch เลยถูกอย่างกะขี้ สร้าง branch ใหม่ทั้งง่ายและเร็วส์ปานการเขียน 41 bytes ลงไปใน file (40 อักษรกะ newline อีกตัว) 90 | 91 | มันเลยแตกต่างกับการแตก branch ใน VCS tool ทั่วไปราวฟ้ากะเหว เพราะปรกติต้องนั่ง copy ทุก file ใน project ใส่ใน directory ใหม่ซึ่งกินเวลาหลายวิ หรืออาจจะเป็นนาทีขึ้นอยู่กับความอ้วนของ project ขณะที่ Git ตดไม่ทันหายเหม็นก็ทำเสร็จละ นอกจากนี้ เนื่องจากเราจำ parent ไว้ในทุกๆ commit เวลาต้องหาต้นตอ version เวลาจะ merge ก็ทำได้โดยอัตโนมัติและง่ายด้วย features เหล่านี้เติมความกล้าให้ developer สร้างและใช้ branches ทั้งหลายเยอะขึ้น 92 | 93 | มาดูกันว่าทำไมคุณก็ควรจะทำ 94 | 95 | ## เบสิคการแตก Branch และการ Merge ## 96 | 97 | มาดูตัวอย่างง่ายๆของการแตก branch และการ merge ด้วย workflow ที่คุณน่าจะใช้ในชีวิตประจำวัน คุณจะทำตามนี้: 98 | 99 | 1. ทำงานบน web site. 100 | 2. แตก branch สำหรับ story ใหม่ที่คุณกำลังทำ 101 | 3. ทำงานใน branch นั้น 102 | 103 | จังหวะนี้เอง มีโทรศัพท์เข้ามาบอกว่ามี issue ร้อนที่ต้องรีบ hotfix ด่วน! คุณก็ทำตามนี้: 104 | 105 | 1. กลับไปยัง production branch 106 | 2. แตก branch สำหรับทำ hotfix. 107 | 3. หลังจาก test แล้ว ก็ merge hotfix branch แล้ว push ขึ้นไปยัง production 108 | 4. switch กลับไป story ตอนแรก แล้วทำงานต่ออย่างสบายอารมณ์ 109 | 110 | ### เบสิคการแตก Branch ### 111 | 112 | ก่อนอื่น สมมติว่าคุณกำลังทำงานบน project คุณ และ commit เข้าไปหลายทีละ (ตาม Figure 3-10). 113 | 114 | Insert 18333fig0310.png 115 | Figure 3-10. A short and simple commit history. 116 | 117 | คุณตัดสินใจว่า เอาล่ะ วันนี้ทำ issue #53 ดีกว่า (ไม่ว่าระบบ issue-tracking ที่บริษัทคุณใช้จะเป็นอะไร) เอาจริงๆแล้ว Git ไม่ได้ผูกติดกับ issue-tracking แต่อย่างใด แต่เนื่องจาก issue #53 มันเป็นเรื่องเป็นราวของมันคุณก็เลยอยากแตก branch แยกออกไปทำ การสร้าง branch ใหม่และ switch ไปในจังหวะเดียวสามารถทำได้ด้วยคำสั่ง `git checkout` แล้วเสริม `-b` เข้าไป: 118 | 119 | $ git checkout -b iss53 120 | Switched to a new branch "iss53" 121 | 122 | นี่คือท่าลัดของ: 123 | 124 | $ git branch iss53 125 | $ git checkout iss53 126 | 127 | รูป 3-11 จะโชว์ผลให้ดู 128 | 129 | Insert 18333fig0311.png 130 | Figure 3-11. Creating a new branch pointer. 131 | 132 | คุณทำงานไปบน web site ของคุณและ commit ไปนิดหน่อย ซึ่งระหว่างนั้นก็เป็นการผลัก branch `iss53` ไปข้างหน้า เพราะคุณ checkout มันออกมา (แปลว่า HEAD ของคุณชี้ไปหามัน ดังรูป Figure 3-12): 133 | 134 | $ vim index.html 135 | $ git commit -a -m 'added a new footer [issue 53]' 136 | 137 | Insert 18333fig0312.png 138 | Figure 3-12. The iss53 branch has moved forward with your work. 139 | 140 | แล้วโทรศัพท์ก็มาบอกว่า web site มีงานเข้า และคุณต้องซ่อมมันด่วน เพราะ Git คุณไม่จำเป็นต้อง deploy fix ของคุณไปกับความเปลี่ยนแปลงใน `iss53` และคุณก็ไม่ต้องเปลืองแรงแก้ code กลับมาเป็นเหมือนเดิม ก่อนที่จะเริ่ม fix อะไรที่อยู่บน production ทั้งหมดที่ต้องทำก็แค่ switch กลับไปยัง master branch 141 | 142 | อย่างไรก็ตาม ก่อนทำแบบนั้น จำไว้ว่า working directory หรือ staging area ของคุณมีความเปลี่ยนแปลงที่ยังไม่โดน commit ซึ่ง conflict กับ branch ที่คุณกำลัง checkout Git ก็เลยไม่ปล่อยให้คุณ switch branches ถ้าจะให้ดี คุณควรจะมีสถานะการทำงาน cleanๆ ก่อนที่จะ switch branches จริงๆมันก็มีท่ายากที่เอาไว้แก้สถานการณ์นี้เหมือนกันนะ (คือการ stash เข้าไปก่อนแล้วค่อย commit amending ตาม) แต่ค่อยมาว่ากันมีหลัง สำหรับตอนนี้ แค่ commit ความเปลี่ยนแปลงทั้งหมดเข้าไปก่อนละกัน จะได้ switch กลับไป master branch ได้: 143 | 144 | $ git checkout master 145 | Switched to branch "master" 146 | 147 | ณ จุดนี้ working directory ของ project คุณจะเหมือนกับก่อนหน้าที่คุณเริ่มทำงานบน issue #53 เป๊ะๆ และคุณก็สามารถรวมสมาธิไปที่ hotfix ได้ ตรงนี้คือตรงที่ต้องจำให้ขึ้นใจ: Git จะ reset working directory ของคุณให้เหมือนกับ snapshot ของ commit ที่ branch ที่คุณ check out ชี้ไป มันจะเพิ่ม ลบ หรือแก้ file โดยอัตโนมัติจนมั่นใจว่า working copy ของคุณเหมือนกับ commit สุดท้ายบน branch นั้น 148 | 149 | หลังจากนั้น คุณมี hotfix ที่รอให้ทำ มาสร้าง branch เพื่อทำ hotfix จนกว่ามันจะเสร็จกัน(ดู Figure 3-13): 150 | 151 | $ git checkout -b 'hotfix' 152 | Switched to a new branch "hotfix" 153 | $ vim index.html 154 | $ git commit -a -m 'fixed the broken email address' 155 | [hotfix]: created 3a0874c: "fixed the broken email address" 156 | 1 files changed, 0 insertions(+), 1 deletions(-) 157 | 158 | Insert 18333fig0313.png 159 | Figure 3-13. hotfix branch based back at your master branch point. 160 | 161 | คุณสามารถ run tests เพื่อให้มั่นใจว่า hotfix มันทำงานได้ดั่งใจและ merge มันกลับเข้า master branch เพื่อ deploy ใส่ production ซึ่งสามารถทำได้ด้วยคำสั่ง `git merge`: 162 | 163 | $ git checkout master 164 | $ git merge hotfix 165 | Updating f42c576..3a0874c 166 | Fast forward 167 | README | 1 - 168 | 1 files changed, 0 insertions(+), 1 deletions(-) 169 | 170 | คุณจะเห็นคำว่า "Fast forward" ใน merge นั้น เพราะ commit ที่ถูกชี้โดย branch ที่คุณ merge มันเป็น upstream ของ commit ที่คุณอยู่โดยตรง Git ก็เลยขยับ pointer ไปข้างหน้า พูดอีกนัยหนึ่งก็คือ เวลาที่คุณพยายามจะ merge commit ซักอันเข้ากับ commit ที่สามารถไปถึงได้โดยการตาม history ของ commit อันแรก Git จะทำให้ทุกอย่างง่ายขึ้นโดยการขยับ pointer ไปข้างหน้าเพราะมันไม่มีงานที่ถูกแยกออกไปให้ merge สิ่งนี้เรียกว่า "fast forward". 171 | 172 | ความเปลี่ยนแปลงของคุณตอนนี้อยู่ใน snapshot ของ commit ที่ `master` branch ชี้ไป และคุณก็สามารถ deploy ความเปลี่ยนแปลงนี้ได้ (ดัง Figure 3-14). 173 | 174 | Insert 18333fig0314.png 175 | Figure 3-14. Your master branch points to the same place as your hotfix branch after the merge. 176 | 177 | หลังจาก fix ที่โคตรสำคัญถูก deployed คุณก็พร้อมที่จะ switch กลับไปยังงานที่คุณทำค้างไว้ก่อนถูกขัดจังหวะ อย่างไรก็ตาม สิ่งที่คุณจะทำก่อนคือการ delete branch `hotfix` เพราะว่าคุณไม่ต้องใช้มันอีกแล้ว เพราะไอ้ `master` branch มันชี้ไปที่จุดเดียวกันแล้ว คุณสามารถ delete มันด้วย option `-d` ของ `git branch`: 178 | 179 | $ git branch -d hotfix 180 | Deleted branch hotfix (3a0874c). 181 | 182 | เอาล่ะ ตอนนี้ switch กลับไป branch issue #53 ที่ทำค้างไว้ได้ละ แล้วก็ทำงานต่อตามสบาย (ดังรูป Figure 3-15): 183 | 184 | $ git checkout iss53 185 | Switched to branch "iss53" 186 | $ vim index.html 187 | $ git commit -a -m 'finished the new footer [issue 53]' 188 | [iss53]: created ad82d7a: "finished the new footer [issue 53]" 189 | 1 files changed, 1 insertions(+), 0 deletions(-) 190 | 191 | Insert 18333fig0315.png 192 | Figure 3-15. Your iss53 branch can move forward independently. 193 | 194 | ถึงงานที่ทำไว้ใน `hotfix` branch จะไม่อยู่ใน files ใน `iss53` branch ก็ช่างหัวมัน ถ้าต้องดึงมันเข้ามา คุณก็สามารถ merge `master` branch ของคุณใส่ใน `iss53` branch ได้ด้วยคำสั่ง `git merge master` หรือว่าจะรอ integrate ความเปลี่ยนแปลงพวกนั้นตอนจะรวม `iss53` branch กลับเข้า `master` ทีหลังก็ได้ 195 | 196 | ### เบสิคการ Merge ### 197 | 198 | สมมติว่าคุณมั่นแล้วว่า issue #53 นี่เนียนแล้วและพร้อมที่จะ merge มันเข้า `master` branch คุณก็ merge branch `iss53` (เหมือนกะตอนที่ทำ branch `hotfix` ก่อนหน้าอ่ะแหละ) ที่ต้องทำทั้งหมดก็แค่ check out branch ที่อยากจะ merge เข้าไปใส่ และสั่ง command `git merge`: 199 | 200 | $ git checkout master 201 | $ git merge iss53 202 | Merge made by recursive. 203 | README | 1 + 204 | 1 files changed, 1 insertions(+), 0 deletions(-) 205 | 206 | อันนี้อาจจะดูต่างกะตอน merge `hotfix` ก่อนหน้านิดส์นึง เพราะครั้งนี้ history การเปลี่ยนแปลงมันถูกแยกออกไปจากจุดก่อนหน้า นั่นเพราะว่า commit ของ branch ที่คุณกะลังอยู่ไม่ได้เป็นรากเหง้าโดยตรงของ branch ที่กำลังจะ merge เข้ามา Git เลยต้องออกแรง อย่างในกรณีนี้ Git ก็ทำ การ merge 3 ทางง่ายๆ โดยใช้ 2 snapshots ที่อยู่ที่ปลายทั้ง 2 ข้างของแต่ละ branch และรากเหง้าของทั้งสองที่เหมือนกัน Figure 3-16 จะ highlight 3 snapshots ที่ Git ใช้ในการ merge กรณีนี้ให้ดู 207 | 208 | Insert 18333fig0316.png 209 | Figure 3-16. Git automatically identifies the best common-ancestor merge base for branch merging. 210 | 211 | แทนที่จะแค่ขยับ pointer ของ branch ไปข้างหน้า Git สร้าง snapshot อันที่ที่เป็นผลจากการ merge 3 ทางนี้ และสร้าง commit อันใหม่ที่ชี้ไปยัง snapshot นั้นโดยอัตโนมัติ (see Figure 3-17) ปรกติเราเรียกท่านี้ว่า merge commit และความพิเศษของมันคือ มันมีแม่มากกว่า 1 อัน 212 | 213 | อย่างนึงที่อยากจะอวดคือ Git ไปคุ้ยหารากเหง้าที่ซ้ำกันของทั้งสองกิ่งให้เพื่อที่จะใช้เป็น merge base ซึ่ง CVS อื่นหรือ Subversion (ก่อน version 1.5) ไม่มีปัญญา และ developer ที่จะ merge ต้องไปคุ้ยเองว่า merge base ที่ดีที่สุดคืออันไหน ด้วยเหตุนี้การ merge ใน Git ก็เลยง่ายกว่าระบบอื่นๆมว้ากกกส์ 214 | 215 | Insert 18333fig0317.png 216 | Figure 3-17. Git automatically creates a new commit object that contains the merged work. 217 | 218 | เอาล่ะ หลังจากงานคุณถูก merge เข้าไปเรียบร้อยแล้ว คุณก็ไม่จำเป็นต้องเลี้ยง branch `iss53` ให้เสียข้าวสุก ลบแม่มเลย แล้วก็ไปปิด ticket ในระบบ ticket-tracking 219 | 220 | $ git branch -d iss53 221 | 222 | ### เบสิคของ Merge Conflicts ### 223 | 224 | ในบางเวลาที่ไม่เป็นใจ ถ้าคุณแก้ส่วนเดียวกันใน file เดียวกันไปคนละทิศคนละทางบน 2 branch ที่แตกต่างกัน เวลาคุณ merge มันเข้าด้วยกัน Git ก็ไม่รู้จะ merge มันเข้ามารวมกันเนียนๆได้ไง ถ้า fix ที่คุณทำไปบน issue #53 มีการแก้ส่วนเดียวกันบน file เดียวกันกับ `hotfix` คุณจะเจอ merge conflict ซึ่งมีหน้าตาประมาณนี้ 225 | 226 | $ git merge iss53 227 | Auto-merging index.html 228 | CONFLICT (content): Merge conflict in index.html 229 | Automatic merge failed; fix conflicts and then commit the result. 230 | 231 | Git ไม่ได้สร้าง merge commit อันใหม่ให้อัตโนมัติ มันกด pause เพื่อหยุดให้คุณ resolve merge conflict ถ้าคุณอยากดูว่า file ไหนบ้างที่ยังไม่ถูก merge ณ เวลาใดๆหลังจากเกิด merge conflict ก็สามารถดูได้ด้วยคำสั่ง `git status`: 232 | 233 | [master*]$ git status 234 | index.html: needs merge 235 | # On branch master 236 | # Changed but not updated: 237 | # (use "git add ..." to update what will be committed) 238 | # (use "git checkout -- ..." to discard changes in working directory) 239 | # 240 | # unmerged: index.html 241 | # 242 | 243 | อะไรก็ตามที่มี merge conflict และยังไม่ถูก resolve จะถูก list ออกมาว่า unmerged Git จะเติม conflict-resolution markers ลงไปใน files ที่มี conflicts เพื่อคุณจะได้เปิดมันและ resolve conflicts เหล่านั้นได้ file ของคุณจะมี section ที่หน้าตาประมาณนี้ 244 | 245 | <<<<<<< HEAD:index.html 246 | 247 | ======= 248 | 251 | >>>>>>> iss53:index.html 252 | 253 | จากรูป version ที่ HEAD (ซึ่งก็คือ master branch เพราะว่านั่นคือที่ที่คุณ check out ออกมาตอนคุณ run merge command) จะเป็นส่วนบนสุดของ block นั้น (ไอ้ที่อยู่บน `=======` น่ะ) ขณะที่ version ที่อยู่ใน `iss53` branch จะอยู่ในส่วนล่าง ในการที่จะ resolve conflict คุณก็ต้องเลือกซักส่วน หรือไม่ก็ merge มันเข้าด้วยกันเอง ยกตัวอย่างเช่น คุณอาจจะ resolve conflict อันนี้โดยการแก้ทั้ง block ให้เป็นอย่างข้างล่าง 254 | 255 | 258 | 259 | resolution (การซ่อม) อันนี้เอามาจากทั้งสองส่วนอย่างละนิดอย่างละหน่อย และผมก็ได้ลบไอ้พวก `<<<<<<<`, `=======` และ `>>>>>>>` ออกไป หลังจากคุณ resolved แต่ละ section ใน file ที่มี conflict แล้ว run `git add` บนแต่ละ file เพื่อระบุว่ามันถูก resolved การ Stage file เป็นการระบุว่ามันถูก resolved แล้วใน Git 260 | ถ้าอยากใช้ graphical tool เพื่อ resolve issues เหล่านี้ ก็ run `git mergetool` ซึ่งจะเปิด visual merge tool แหล่มๆขึ้นมาและเรียงแต่ละ conflicts ขึ้นมาให้คุณแก้: 261 | 262 | $ git mergetool 263 | merge tool candidates: kdiff3 tkdiff xxdiff meld gvimdiff opendiff emerge vimdiff 264 | Merging the files: index.html 265 | 266 | Normal merge conflict for 'index.html': 267 | {local}: modified 268 | {remote}: modified 269 | Hit return to start merge resolution tool (opendiff): 270 | 271 | ถ้า merge tool ที่ถูกเลือกขึ้นมาโดย default นั้นไม่โดนใจ (Git เลือก `opendiff` ให้ผมในกรณีนี้เพราะผม run command บน Mac) คุณก็ดูว่า supported tools ที่ list อยู่ข้างบนหลังจากคำว่า “merge tool candidates” แล้วพิมพ์ชื่อ tool ที่อยากใช้ลงไป ใน Chapter 7 เราจะมาถกกันว่าคุณจะเปลี่ยน default value เหล่านี้ใน environment คุณได้ยังไง 272 | 273 | หลังจากปิด merge tool ไป Git ก็จะถามคุณว่า merge สำเร็จมั้ย? ถ้าคุณตอบไปว่าชิววว มันก็จะ stages file นั้นเพื่อระบุว่ามันถูก resolved แล้วให้คุณ 274 | 275 | ลอง run `git status` อีกทีเพื่อเช็คดูว่าไม่เหลือ conflicts ให้ resolve แล้วนะ 276 | 277 | $ git status 278 | # On branch master 279 | # Changes to be committed: 280 | # (use "git reset HEAD ..." to unstage) 281 | # 282 | # modified: index.html 283 | # 284 | 285 | โอเค แหล่ม เช็คอีกทีให้มั่นใจว่าทุกอย่างที่มี conflicts ถูก staged แล้ว แล้วก็พิมพ์ `git commit` เพื่อจบการ merge commit โดยปรกติแล้ว commit message ตาม default จะหน้าตาประมาณนี้ 286 | 287 | Merge branch 'iss53' 288 | 289 | Conflicts: 290 | index.html 291 | # 292 | # It looks like you may be committing a MERGE. 293 | # If this is not correct, please remove the file 294 | # .git/MERGE_HEAD 295 | # and try again. 296 | # 297 | 298 | คุณมาสามารถแก้เติมรายละเอียดลงไปว่าคุณ resolve merge อันนั้นได้ไงลงไปใน message ได้ถ้ารู้สึกว่ามันจะช่วยให้เพื่อนๆเข้าใจ merge นี้ง่ายขึ้นเวลาต้องกลับมาดูมันใหม่ในอนาคต (เช่นอธิบายว่าไอ้ที่ทำไปแบบนี้ ทำไปเพราะอะไร) แต่ถ้ามันชัดอยู่แล้วก็ไม่ต้อง 299 | 300 | ## การบริหาร Branch ## 301 | 302 | เอาล่ะ หลังจากที่คุณ created, merged และ deleted บาง branches แล้ว มาดู tools ที่ใช้ทำ branch-management (บริหาร branche) บ้าง เพราะอาจจะต้องใช้เวลาที่คุณทำ branches เยอะๆ 303 | 304 | ไอ้ command `git branch` ทำได้มากกว่าแค่ create หรือ delete branches ถ้าคุณ run มันโดยไม่ใส่ arguments คุณจะได้ list ของ branches ปัจจุบันที่คุณมีอยู่: 305 | 306 | $ git branch 307 | iss53 308 | * master 309 | testing 310 | 311 | สังเกตุไอ้ `*` ข้างหน้า `master` branch มันจะคอยปะอยู่หน้า branch ที่คุณกำลัง checked out อยู่ในปัจจุบัน ซึ่งแปลว่า ถ้า commit ตอนนี้ `master` branch จะถูกขยับไปข้างหน้าด้วยงานที่เพิ่มมาใหม่ การจะดู commit สุดท้ายของแต่ละ branch คุณก็ run `git branch -v`: 312 | 313 | $ git branch -v 314 | iss53 93b412c fix javascript issue 315 | * master 7a98805 Merge branch 'iss53' 316 | testing 782fd34 add scott to the author list in the readmes 317 | 318 | อีกทางหนึ่งที่ใช้ในการเช็ค state ของ branches คือการ list ดูว่ามี branch ไหนบ้างที่เคยถูก merge และ branch ไหนบ้างที่ยังไม่เคยถูก merge เข้ามายัง branch ที่คุณอยู่ option เทพที่ชื่อว่า `--merged` และ `--no-merged` ถูกเพิ่มเข้ามาใน Git ตั้งแต่ version 1.5.6 เพื่อประการนี้ การจะดูว่า branches ไหนบ้างที่เคยถูก merged เข้ามายัง branch ที่คุณอยู่ สามารถทำได้โดย run `git branch --merged` 319 | 320 | $ git branch --merged 321 | iss53 322 | * master 323 | 324 | เพราะคุณเคย merged branch `iss53` เข้ามาแล้วเมื่อตะกี้ คุณก็เลยเห็นมันอยู่ใน list ปรกติแล้ว branches ต่างๆใน list นี้ (ยกเว้นอันที่กา `*` ไว้ข้างหน้านะ) สามารถ delete ทิ้งได้ตามสบายด้วยคำสั่ง `git branch -d` เพราะคุณได้ย้ายงานที่ทำในนั้นไปไว้ที่ branch อื่นแล้ว เพราะฉะนั้นลบไปก็ไม่มีงานหาย 325 | 326 | การจะดูว่า branches ไหนบ้างที่มีงานที่ยังไม่ได้ถูก merged เข้ามาก็สามารถทำได้โดยการ run `git branch --no-merged`: 327 | 328 | $ git branch --no-merged 329 | testing 330 | 331 | คำสั่งนี้แสดงอีก branch นึง เพราะว่ามันมีงานที่คุณยังไม่เคย merged เข้ามา ถ้าคุณลอง delete มันด้วยคำสั่ง `git branch -d` มันก็จะ fail 332 | 333 | $ git branch -d testing 334 | error: The branch 'testing' is not an ancestor of your current HEAD. 335 | If you are sure you want to delete it, run 'git branch -D testing'. 336 | 337 | ถ้าอยากจะ delete branch นั้นไปพร้อมๆกับงานที่ทำอยู่ในนั้นจริงๆ ก็สามารถบังคับให้มันลบได้ด้วย option `-D` ดังที่ help message บอกไว้ข้างบน 338 | 339 | ## Workflows ของการแตก Branch ## 340 | 341 | เอาหล่ะ หลังจากคุณมีเบสิคของการแตก branch และการ merge กลับเข้ามาติดตัวแล้ว มาดูกันว่าคุณสามารถ (และสมควร) ที่จะทำอะไรกับมันบ้าง? ใน section นี้ เราจะโชว์ workflows ทั่วไปว่าการแตก branch อันรวดเร็วส์นี้สามารถทำอะไรได้บ้าง คุณจะได้ตัดสินใจได้ว่าควรที่จะเอามันไปประยุกต์ใช้กับกระบวนการพัฒนาที่คุณใช้อยู่ไหม 342 | 343 | ### Branch ที่อายุยืนนาน ### 344 | 345 | เพราะว่า Git มีท่า merge 3 ทาง การ merge จาก branch หนึ่งไปยังอีก branch หลายๆครั้งเป็นระยะเวลานานๆก็กลายเป็นเรื่องชิวๆไปโดยปริยาย นั่นแปลว่าคุณสามารถเปิดหลายๆ branches ทิ้งไว้สำหรับ stages ต่างๆในกระบวนการพัฒนาที่คุณใช้อยู่ และคุณก็สามารถ merge จากบาง branch ไปยัง branch อื่นๆได้อย่างสม่ำเสมอ 346 | 347 | นักพัฒนาที่ใช้ Git หลายคนมี workflow ที่สนับสนุนวิธีแบบนี้ ยกตัวอย่างเช่น มี code stable สุดยิดเก็บไว้ใน `master` branch (ส่วนใหญ่จะเป็น code ที่ถูก release ไปแล้วเตรียมจะออก) แล้วพวกเขาก็มีอีก branch หนึ่ง เปิดควบคู่กันไปโดยตั้งชื่อมันว่า develop หรือ next แล้วก็ทำงานกันบนนี้ ไม่ก็ใช้เพื่อ test ความเสถียรของระบบ (ซึ่ง branch นี้ไม่จำเป็นต้อง stable ตลอดเวลา) แล้วเมื่อไหร่ก็ตามที่มันเริ่ม stable เขาก็ merge มันเข้า `master` ไอ้ develop branch นี้จะเป็นที่ไว้ pull พวก topic branches (พวก branch อายุสั้นๆอย่าง `iss53` branch อันก่อนหน้านี้) เข้ามาเมื่อมันเสร็จเพื่อเช็คว่ามัน pass ทุกๆ tests และไม่มี bugs ใหม่ๆโผล่เข้ามา 348 | 349 | ในชีวิตจริงนั้น ถ้ามาดู pointers ที่ค่อยๆขยับขึ้นตามสายของ commits ของคุณ พวก branches ที่ stable จะอยู่ล่างๆใน commit history และพวก branches ที่เก็บของใหม่ๆแรงๆจะอยู่บนใน history (ดู Figure 3-18). 350 | 351 | Insert 18333fig0318.png 352 | Figure 3-18. More stable branches are generally farther down the commit history. 353 | 354 | ถ้าจะเปรียบให้ง่าย ก็ลองมองมันเป็นยุ้งข้าวที่เก็บ commits ต่างๆ โดย commit จะค่อยๆถูกขยับไปในยุ้งที่เสถียรขึ้นเมื่อมันถูก tested เรียบร้อย (ดู Figure 3-19). 355 | 356 | Insert 18333fig0319.png 357 | Figure 3-19. It may be helpful to think of your branches as silos. 358 | 359 | คุณสามารถแยกแบบนี้ซ้ำๆเป็นกี่ระดับความเสถียรก็ได้ สำหรับ projects ใหญ่ๆบาง projects จะมี branch `proposed` หรือ `pu` (proposed updates) ซึ่งเป็น integrated branches ที่ยังไม่พร้อมจะเอาไปลง branch `next` หรือ `master` สรุปแล้ว ไอเดียคือ branches ต่างๆจะถูกแยกให้มีระดับความเสถียรไม่เท่ากัน เมื่อไหร่ที่มันเสถียรขึ้น ก็จะถูก merge ไปยัง branch ระดับที่สูงขึ้น 360 | ย้ำอีกที การมี long-running branches หลายๆอันนั้นไม่จำเป็น อย่างไรก็ตาม มันมักจะมีประโยชน์เวลาที่คุณกำลังทำ project ใหญ่ๆที่ซับซ้อนมากๆ 361 | 362 | ### Topic Branches ### 363 | 364 | ส่วน topic branches เป็นคนละเรื่องนะจ๊ะ เพราะมันมีประโยชน์กับ projects ทุก size topic branch นั้นคือ branch ที่มีอายุสั้นๆ short-lived ที่คุณสร้างและใช้สำหรับ feature ใดๆซักอัน branche แบบนี้เป็นอะไรที่คุณน่าจะไม่เคยทำมาก่อนกับ VCS อื่นๆเพราะโดยปรกติแล้วการ create และ merge branches มันเปลืองพลังงานมาก แต่ใน Git การ create branch, switch branch ทำงาน, merge branch และ delete branches เป็นเรื่องธรรมดาที่ทำกันได้ทุกวัน (อวดอีกแล้ว :P) 365 | 366 | อย่างที่คุณเห็นตัวอย่างไปแล้วใน branch `iss53` และ `hotfix` ที่คุณ create ขึ้นมา, commit ลงไปและก็ได้ delete พวกมันทันทีหลังจาก merge พวกมันเข้า branch หลัก เทคนิคแบบนี้จะทำให้คุณ context-switch ได้อย่างเฉียบขาดและรวดเร็วส์ (เพราะงานที่คุณทำถูกแยกออกมาอยู่ในยุ้งของตัวเองโดยที่ความเปลี่ยนแปลงทั้งหมดที่คุณสร้างมันเกี่ยวข้องกับ topic นั้นๆโดยตรง) การจะติดตามว่าอะไรเปลี่ยนไปยังไงก็ง่ายไม่ว่าจะเป็นตอนทำ code review หรืออะไรต่างๆ คุณจะเก็บความเปลี่ยนแปลงที่เกิดขึ้นไว้ใน branch นั้นเป็นหลายนาที, หลายวัน หรือหลายเดือนก็ตามใจ แล้วค่อย merge มันเข้ามาเมื่อมันเสร็จโดยที่ไม่เกี่ยวว่ามันจะถูกสร้างหรือทำเมื่อไหร่ 367 | 368 | ลองจินตนาการว่าคุณกำลังทำงาน (บน `master`), แล้วแตก branch ออกไปสำหรับ issue ซักอัน (สมมติชื่อ `iss91`) แล้วก็ทำงานบนนั้นไปซักแป๊ปแล้วแตก branch ออกไปอีกอันเพื่อลองแก้ปัญหาเดิมด้วยวิธีใหม่ (ชื่อ `iss91v2`) แล้วกลับไปที่ master branch และทำงานบนนั้นไปซักแป๊ปแล้วแตก branch ออกไปเพื่อลองไอเดียอะไรซักอย่างที่ไม่รู้ว่าเจ๋งป่าว (ชื่อ branch `dumbidea`) ตอนนี้ commit history ของคุณจะมีหน้าตาประมาณรูป Figure 3-20. 369 | 370 | Insert 18333fig0320.png 371 | Figure 3-20. Your commit history with multiple topic branches. 372 | 373 | ทีนี้, สมมติวันคุณตัดสินใจละ ว่าคุณชอบวิธีที่สองที่คุณทำสำหรับ issue มากกว่า (`iss91v2`) แล้วคุณก็โชว์ branch `dumbidea` ให้เพื่อนดูแล้วผลปรากฏว่ามันแหล่มมาก คุณก็สามารถโยนไอ้ branch `iss91` อันแรกทิ้ง (ทำให้ commits C5 and C6 หายไป) และ merge อีกสองอันเข้ามา ทำให้ history หน้าตาเหมือนรูป Figure 3-21. 374 | 375 | Insert 18333fig0321.png 376 | Figure 3-21. Your history after merging in dumbidea and iss91v2. 377 | 378 | ประเด็นสำคัญอันหนึ่งที่อยากจะเน้นคือขณะที่คุณทำไอ้ทุกอย่างที่เล่ามา branches เหล่านี้อยู่บน local ทั้งนั้น ไม่ว่าจะเป็นตอน branch ตอน merging ทุกอย่างเกิดขึ้นบน Git repository ของคุณเท่านั้น (ไม่มีการติดต่อกับ server เลยนะ) 379 | 380 | ## Remote Branches ## 381 | 382 | Remote branches คือคำที่ใช้อ้างถึงสถานะของ branches ที่ remote repositories พวกมันคือ local branches ที่คุณย้ายที่มันไม่ได้เพราะมันจะย้ายเองโดยอัตโนมัติเมื่อคุณติดต่อกับ any network พวก Remote branches จะทำตัวเหมือน bookmarks ที่คอยเตือนว่า branches ทั้งหลายบน remote repositories อยู่ตรงไหนตอนที่คุณ connected กับ remote ครั้งสุดท้าย 383 | 384 | รูปแบบพวกมันคือ `(remote)/(branch)` ตัวอย่างเช่น ถ้าคุณอยากเห็นว่า branch `master` เมื่อครั้งสุดท้ายเมื่อคุณติดต่อกับ `origin` remote หน้าตาเป็นยังไง คุณก็ดูได้ที่ branch `origin/master` ถ้าคุณกำลังทำซัก issue นึงกับเพื่อนๆแล้วเพื่อน push branch ที่ชื่อ `iss53` ขึ้นมา (คุณอาจจะมี local `iss53` branch อยู่แล้ว แต่ branch บน server จะชี้ไปยัง commit ที่ `origin/iss53`) 385 | 386 | ฟังแล้วอาจจะยังงงๆ มาดูตัวอย่างกันดีกว่า สมมติว่าคุณมี Git server อยู่ใน network ของคุณชื่อ `git.ourcompany.com` ถ้าคุณ clone จากที่นี่ Git จะตั้งชื่อมันว่า `origin` ให้คุณโดยอัตโนมัติ, ดึงข้อมูลทั้งหมดของมันลงมา, แล้วสร้าง pointer ไปยัง `master` branch ของมัน, แล้วตั้งชื่อว่า `origin/master` บนเครื่องของคุณ โดยที่คุณจะไม่สามารถย้ายที่มันได้ นอกจากนี้ Git ก็จะให้ `master` branch ส่วนตัวกับคุณ โดยมันจะเริ่มต้นจากที่เดียวกับ `master` branch ของ origin เพื่อเป็นจุดเริ่มต้นในการทำงานให้กับคุณ (ดู Figure 3-22). 387 | 388 | Insert 18333fig0322.png 389 | Figure 3-22. A Git clone gives you your own master branch and origin/master pointing to origin’s master branch. 390 | 391 | ถ้าคุณทำงานอะไรซักอย่างไปบน master branch บนเครื่องคุณ โดยระหว่างนั้นมีใครซักคน push ของใส่ `git.ourcompany.com` และ update master branch บนนั้น histories ของคุณก็ยังค่อยๆขยับไปข้างหน้าตามเรื่องตามราวของมัน ตราบใดที่คุณยังไม่ติดต่อกับ origin server ไอ้ pointer `origin/master` ของคุณก็จะไม่ขยับไปไหน (ดู Figure 3-23). 392 | 393 | Insert 18333fig0323.png 394 | Figure 3-23. Working locally and having someone push to your remote server makes each history move forward differently. 395 | 396 | เพื่อที่จะ synchronize งานที่คุณทำ คุณก็จะ run command `git fetch origin` โดย command นี้จะไปหาว่า server origin อยู่ไหน (ในกรณีนี้คือ `git.ourcompany.com`) แล้ว fetches ข้อมูลทั้งหมดที่คุณยังไม่มีจากมัน, updates database บนเครื่องคุณ, ขยับ pointer `origin/master` ไปยังที่ใหม่ที่ up-to-date กว่าเดิม (ดู Figure 3-24). 397 | 398 | Insert 18333fig0324.png 399 | Figure 3-24. The git fetch command updates your remote references. 400 | 401 | เพื่อจะทำให้เห็นภาพการมี remote servers หลายๆอันและดูว่า remote branches สำหรับ remote projects เหล่านั้นหน้าตาเป็นยังไง, มาลองสมมติกันว่าคุณมี Git server อีกอันนึงที่อีก sprint team นึงใช้ develop โดยเฉพาะ server อันนี้อยู่ที่ `git.team1.ourcompany.com` คุณสามารถเพิ่มมันเข้าไปเป็น remote reference อันใหม่ของ project ที่คุณกำลังทำงานอยู่โดยการ run command `git remote add` อย่างที่เราเคยเล่าไว้ใน Chapter 2 ตั้งชื่อ remote นี้ว่า `teamone` ซึ่งจะกลายเป็นชื่อย่อสำหรับ URL อันนั้นทั้งอัน (ดู Figure 3-25). 402 | 403 | Insert 18333fig0325.png 404 | Figure 3-25. Adding another server as a remote. 405 | 406 | ตอนนี้คุณสามารถ run `git fetch teamone` เพื่อ fetch ทุกอย่างที่ remote `teamone` server มีแต่คุณยังไม่มี เนื่องจาก server นั้นเป็นแค่ subset ของข้อมูลที่คุณมีบน `origin` server ในตอนนี้ Git ก็จะไม่ fetch ข้อมูลอะไร แค่ตั้ง remote branch ชื่อ `teamone/master` ชี้ไปยัง commit ที่ `teamone` มีอยู่ใน `master` branch ของมัน (ดู Figure 3-26). 407 | 408 | Insert 18333fig0326.png 409 | Figure 3-26. You get a reference to teamone’s master branch position locally. 410 | 411 | ### การ Push ### 412 | 413 | เมื่อใดที่คุณอยากจะ share branch ซักอันให้กับชาวโลก คุณต้อง push มันขึ้นไปยัง remote ซักอันที่คุณมี write access เนื่องจากพวก branches ต่างๆบนเครื่องคุณมันไม่ได้ synchronize กับ remotes ที่คุณติดต่อโดยอัตโนมัติ คุณต้องเป็นคน push พวกมันขึ้นไปเองถ้าคุณอยากจะ share ทำให้คุณสามารถใช้ private branches ทำงานที่คุณเขินไม่อยากจะ share และ push เฉพาะ topic branches ที่คุณต้องการความร่วมมือขึ้นไป 414 | 415 | ถ้าคุณมี branch ซักอันชื่อ `serverfix` ที่คุณอยากจะทำงานร่วมกันกับเพื่อนๆ คุณสามารถ push มันขึ้นไปด้วยวิธีเดียวกันกับที่คุณ push branch อันแรกเลย นั่นคือ run `git push (remote) (branch)`: 416 | 417 | $ git push origin serverfix 418 | Counting objects: 20, done. 419 | Compressing objects: 100% (14/14), done. 420 | Writing objects: 100% (15/15), 1.74 KiB, done. 421 | Total 15 (delta 5), reused 0 (delta 0) 422 | To git@github.com:schacon/simplegit.git 423 | * [new branch] serverfix -> serverfix 424 | 425 | อันนี้เป็น shortcut นิดส์ๆ โดย Git จะขยายชื่อ branch `serverfix` ออกเป็น `refs/heads/serverfix:refs/heads/serverfix` โดยอัตโนมัติ ซึ่งแปลว่า “เอา serverfix ที่เป็น local branch คนเครื่องฉันไป push ใส่ serverfix บน remote ให้ที” เด๋วเราค่อยมาว่ากันในรายละเอียดของส่วน `refs/heads/` ใน Chapter 9 ตอนนี้ทำเป็นลืมๆมันไปก่อน แน่นอนว่าคุณสามารถทำ `git push origin serverfix:serverfix` ก็ได้ เพราะมันจะได้ผลออกมาเหมือนกัน (เพราะมันแปลว่า “เอา serverfix ของฉันไปทำเป็น serverfix ของ remote ซะ”) คุณสามารถใช้ format เพื่อ push local branch ซักอันไปยัง remote branch ซึ่งมีชื่อต่างกันได้ ถ้าคุณไม่อยากให้มันมีชื่อว่า `serverfix` บน remote คุณก้ run `git push origin serverfix:awesomebranch` แทนเพื่อที่จะ push `serverfix` branch บนเครื่องไปเป็น `awesomebranch` branch บน remote project 426 | 427 | ครั้งต่อไปที่เพื่อนคุณซักคน fetch ของจาก server เค้าจะได้ reference อันนึงที่ชี้ไปยัง `serverfix` version บน server ในรูปแบบ remote branch ชื่อ `origin/serverfix`: 428 | 429 | $ git fetch origin 430 | remote: Counting objects: 20, done. 431 | remote: Compressing objects: 100% (14/14), done. 432 | remote: Total 15 (delta 5), reused 0 (delta 0) 433 | Unpacking objects: 100% (15/15), done. 434 | From git@github.com:schacon/simplegit 435 | * [new branch] serverfix -> origin/serverfix 436 | 437 | สิ่งสำคัญที่คุณจำให้ขึ้นใจคือเมื่อใดก็ตามที่คุณ fetch remote branches มาใหม่ คุณไม่ได้มี local copy ของมันโดยอัตโนมัติ อย่างในกรณีนี้ คุณไม่ได้มี `serverfix` branch บนเครื่อง คุณมีแค่ pointer ชื่อ `origin/serverfix` ที่คุณแก้ไขมันไม่ได้ 438 | 439 | การจะ merge งานจากนี้เข้าไปใน working branch ของคุณ คุณสามารถ run `git merge origin/serverfix` แต่ถ้าคุณอยากจจะมี `serverfix` branch เป็นของตัวเอง คุณก็สามารถ base มันออกมาจาก remote branch ได้: 440 | 441 | $ git checkout -b serverfix origin/serverfix 442 | Branch serverfix set up to track remote branch refs/remotes/origin/serverfix. 443 | Switched to a new branch "serverfix" 444 | 445 | แบบนี้คุณก็จะได้ local branch บนเครื่องที่คุณสามารถทำงานได้โดยมันจะเริ่มต้นจากจุดที่ `origin/serverfix` อยู่ 446 | 447 | ### การติดตาม Branches ### 448 | 449 | การ check out local branch ซักอันจาก remote branch จะสร้างสิ่งที่เรียกว่า _tracking branch_ ให้โดยอัตโนมัติ ไอ้พวก tracking branches เนี่ยคือ local branches ที่สัมพันธ์โดยตรงกับ remote branch เมื่อไหร่ที่คุณพิมพ์ `git push` ขณะอยู่บน tracking branch Git จะรู้โดยอัตโนมัติว่าจะต้อง push ใส่ server อะไร branch ไหน นอกจากนี้ การ run `git pull` ขณะอยู่บน branches แบบนี้ก็จะ fetches remote references ทั้งหมดและทำการ merge remote branch ที่เกี่ยวข้องให้โดยอัตโนมัติ 450 | 451 | ตอนที่คุณ clone repository ซักอัน, มันจะสร้าง `master` branch ให้โดยอัตโนมัติเพื่อติดตาม `origin/master` ทำให้ `git push` และ `git pull` work ตั้งแต่แรก อย่างไรก็ตาม คุณสามารถ set up tracking branches อื่นๆ (นอกเหนือจาก server `origin` และ branch `master`) ได้ถ้าต้องการ การ run `git checkout -b [branch] [remotename]/[branch]` ที่เห็นไปตะกี้ก็เป็นตัวอย่างนึง ถ้าคุณมี Git version 1.6.2 ขึ้นไป คุณจะสามารถใช้ option ลัด `--track` ตามตัวอย่างข้างล่าง: 452 | 453 | $ git checkout --track origin/serverfix 454 | Branch serverfix set up to track remote branch refs/remotes/origin/serverfix. 455 | Switched to a new branch "serverfix" 456 | 457 | เพื่อ set up local branch ด้วยชื่อที่ต่างกันกับ remote branch หรือไม่ก็ใช้ version แรก แต่ใส่ชื่อ local branch ให้ต่างออกไป: 458 | 459 | $ git checkout -b sf origin/serverfix 460 | Branch sf set up to track remote branch refs/remotes/origin/serverfix. 461 | Switched to a new branch "sf" 462 | 463 | ตอนนี้ local branch ที่ชื่อ sf จะ push และ pull จาก origin/serverfix โดยอัตโนมัติ 464 | 465 | ### การลบ Remote Branches ### 466 | 467 | สมมติว่าคุณจะทิ้ง remote branch ยกตัวอย่างเช่น คุณและเพื่อนๆทำ feature อันนึงเสร็จและได้ merged มันเข้าไปยัง remote `master` branch (หรือ branch ลิงอะไรก็ช่างที่เอาไว้เก็บ code ที่ stable อ่ะ) คุณสามารถ delete remote branch ได้อย่างงงๆด้วยคำสั่ง `git push [remotename] :[branch]` สมมติว่าอยากจะลบ `serverfix` branch จาก server ก็ run คำสั่งดังนี้: 468 | 469 | $ git push origin :serverfix 470 | To git@github.com:schacon/simplegit.git 471 | - [deleted] serverfix 472 | 473 | ตูม! ไม่เหลือซากของ branch บน server ละ พับมุมหน้านี้ไว้นะ เพราะคุณจะต้องใช้ command นี้แน่ๆ และคุณต้องลืม syntax ของคำสั่งแน่นอน ฟันธง! วิธีจำคำสั่งนี้คือพยายามนึกถึง syntax ของ `git push [remotename] [localbranch]:[remotebranch]` ที่ผ่านมาก่อนหน้านี้ไว้ ถ้าคุณละไอ้ส่วน `[localbranch]` ไว้ แปลว่าคุณกำลังบอกว่า “ไม่ต้องเอาอะไรจาก local ไปเลย แล้วทำให้มันเป็น `[remotebranch]` (ลบนั่นแหละ)” 474 | 475 | ## การ Rebase ## 476 | 477 | Git มีสองวิธีหลักๆในการ integrate changes จาก branch นึงไปยังอีก branch นึง นั่งคือการ `merge` กับการ `rebase` ใน section นี้คุณจะได้เรียนรู้ว่าการ rebase คืออะไร, ทำยังไง, ทำไมมันถึงน่าตื่นตาตื่นใจ, และในกรณีไหนที่คุณไม่อยากจะใช้มัน 478 | 479 | ### เบสิค Rebase ### 480 | 481 | ถ้าคุณย้อนกลับไปดูตัวอย่างก่อนหน้าใน section Merge (ดู Figure 3-27) คุณจะเห็นว่าคุณได้คุณได้แยกงานคุณออกมาและสร้าง commits บน 2 branches ที่แตกต่างกัน 482 | 483 | Insert 18333fig0327.png 484 | Figure 3-27. Your initial diverged commit history. 485 | 486 | วิธีที่ง่ายที่สุดที่จะ integrate มันเข้าด้วยกันคือการ `merge` (อย่างที่เห็นก่อนหน้านี้) มันจะทำการ merge สามทาง ระหว่าง snapshots ล่าสุดของทั้งสอง branch (C3 และ C4) และบรรพบุรุษล่าสุดที่ทั้งสอง branch share กัน (C2), สร้าง snapshot อันใหม่ (และ commit), ดัง Figure 3-28. 487 | 488 | Insert 18333fig0328.png 489 | Figure 3-28. Merging a branch to integrate the diverged work history. 490 | 491 | นอกจากนี้ยังมีอีกทาง นั่นคือไปเอา patch ของ change ที่เกิดขึ้นใน C3 แล้วเอามา apply บน C4 สำหรับ Git วิธีนี้เรียกว่า _rebasing_ ด้วย command `rebase` คุณสามารถเอา changes ที่ถูก committed บน branch นึงไป replay บนอีก branch ได้ 492 | 493 | อย่างในตัวอย่างนี้ คุณก็จะต้อง run คำสั่งต่อไปนี้: 494 | 495 | $ git checkout experiment 496 | $ git rebase master 497 | First, rewinding head to replay your work on top of it... 498 | Applying: added staged command 499 | 500 | วิธีการทำงานของมันคือย้ายไปยังบรรพบุรุษที่แชร์กันระหว่างสอง branches (ระหว่างอันที่คุณกำลังอยู่และอันที่คุณจะ rebasing เข้าไป), หา diff (ความเปลี่ยนแปลง) ที่เกิดขึ้นในแต่ละ commit ของ branch ที่คุณกำลังอยู่, save diffs เหล่านั้นใส่ใน temporary files, reset branch ปัจจุบันให้เป็นเหมือน commit ของ branch ที่คุณกำลังจะ rebase ไป, และสุดท้าย apply แต่ละ change ตามลำดับ Figure 3-29 จะแสดงให้เห็นว่ามันดำเนินไปยังไง 501 | 502 | Insert 18333fig0329.png 503 | Figure 3-29. Rebasing the change introduced in C3 onto C4. 504 | 505 | ณ บัดนาว คุณสามารถกลับไปยัง master branch แล้วทำ fast-forward merge ได้ (ดังรูป Figure 3-30). 506 | 507 | Insert 18333fig0330.png 508 | Figure 3-30. Fast-forwarding the master branch. 509 | 510 | ตอนนี้ snapshot ที่ถูกชี้โดย C3' หน้าตาเหมือนกันกับอันที่ถูกชี้ว่า C5 ในตัวอย่างการ merge เป๊ะๆ ไม่มีความแตกต่างใดๆในผลของการ integrate แต่การ rebase ทำให้ history สะอาดกว่า ถ้าคุณลองดู log ของ branch ที่ถูก rebase มา จะเห็นว่า history มันเป็นเส้นตรงราวกับว่างานทั้งหมดเกิดขึ้นตามลำดับแม้ว่าตอนแรกมันจะเกิดขึ้นควบคู่กันก็ตาม 511 | 512 | บ่อยครั้ง คุณจะเลือก rebase เพื่อให้ commits เรียงกันอย่างสวยงามส์บน remote branch ยกตัวอย่างเช่นใน project ที่คุณเข้าไปแจมแต่ไม่ได้ maintain เอง กรณีแบบนี้ คุณจะทำงานของคุณบน branch ซักอัน แล้ว rebase งานของคุณไปยัง `origin/master` เวลาที่คุณพร้อมจะ submit patches ของคุณลงบน main project ด้วยวิธีนี้ คนที่ maintain ก็ไม่ต้องทำการ integration ใดๆ แค่ fast-forward หรือ apply cleanๆ 513 | 514 | สังเกตุดู snapshot ที่ถูกชี้โดย commit สุดท้ายที่คุณมี ไม่ว่าจะเป็นกรณี rebase หรือ merge ก็ตาม มันเป็น snapshot เดียวกัน มีแค่ history เท่านั้นที่แตกต่าง การ Rebase จะ replay changes จากสายงานหนึ่งไปยังอีกอันตามลำดับที่มันถูกเปลี่ยนแปลง ขณะที่การ merge จะเอาเอาทั้งสองปลาย merge เข้าด้วยกัน 515 | 516 | ### Rebase ระดับเมพ ### 517 | 518 | คุณสามารถให้ rebase ของคุณ replay บนอย่างอื่นนอกจาก rebase branch ได้ด้วยนะ สมมติ history หน้าตาเหมือน Figure 3-31 คุณแตก topic branch ชื่อ (`server`) เพื่อเพิ่ม functionality บางอย่างบน server-side ของ project คุณ แล้วคุณก็ commit หลังจากนั้น, คุณแตก branch ออกไปอีกเพื่อทำ changes สำหรับ client-side (ชื่อ `client`) แล้วก็ commit อีกไม่กี่ที สุดท้าย คุณกลับมาที่ server branch ของคุณแล้ว commit ไปอีก 2-3 ที 519 | 520 | Insert 18333fig0331.png 521 | Figure 3-31. A history with a topic branch off another topic branch. 522 | 523 | สมมติว่าคุณตัดสินใจที่จะ merge changes ของ client-side เข้าไปใน mainline เพื่อที่จะ release, แต่คุณดันอยากเก็บ changes ของ server-side เอาไว้ก่อนจนกว่ามันจะโดน tested อีกซักหน่อย คุณสามารถเอา changes บน client ที่ไม่อยู่บน server (C8 และ C9) แล้ว reply มันบน master branch ได้โดยใช้ option `--onto` ของ `git rebase`: 524 | 525 | $ git rebase --onto master server client 526 | 527 | ซึ่งเป็นการบอกว่า, “check out client branch ซะ, แล้วหา patches จากบรรพบุรุษที่แชร์กันระหว่าง branch `client` กับ `server`, แล้ว replay มันไปบน `master`” ถึงจะฟังดูงงๆ แต่ผลลัพธ์ (ดังรูป Figure 3-32) ค่อนข้างแหล่มทีเดียว 528 | 529 | Insert 18333fig0332.png 530 | Figure 3-32. Rebasing a topic branch off another topic branch. 531 | 532 | คราวนี้คุณก็ fast-forward master branch ของคุณ (ดังรูป Figure 3-33): 533 | 534 | $ git checkout master 535 | $ git merge client 536 | 537 | Insert 18333fig0333.png 538 | Figure 3-33. Fast-forwarding your master branch to include the client branch changes. 539 | 540 | ทีนี้สมมติว่าคุณตัดสินใจที่จะ pull server branch เข้ามาด้วยเช่นกัน คุณก็ rebase server branch ไปบน the master branch โดยไม่ต้อง check out มันออกมาเลยโดยการ run `git rebase [basebranch] [topicbranch]` ซึ่งจะทำการ check out topic branch (ซึ่งในกรณีนี้ชื่อ `server`) ให้คุณ แล้ว replay มันไปบน base branch (`master`): 541 | 542 | $ git rebase master server 543 | 544 | การทำแบบนี้จะ replays งานใน `server` ลงบนงานใน `master` ดัง Figure 3-34. 545 | 546 | Insert 18333fig0334.png 547 | Figure 3-34. Rebasing your server branch on top of your master branch. 548 | 549 | หลังจากนั้นคุณก็ fast-forward branch หลัก (`master`): 550 | 551 | $ git checkout master 552 | $ git merge server 553 | 554 | ตอนนี้ก็ลบ branches `client` กับ `server` ได้ละ เพราะว่างานทั้งหมดถูก integrated เรียบร้อยเลยไม่รู้จะเก็บไว้ทำซากอะไร ผลที่ได้คือ history ทั้งยวงจะหน้าตาประมาณ Figure 3-35: 555 | 556 | $ git branch -d client 557 | $ git branch -d server 558 | 559 | Insert 18333fig0335.png 560 | Figure 3-35. Final commit history. 561 | 562 | ### ภยันตรายของการ Rebase ### 563 | 564 | อาาา ความสุขสันต์ของการ rebase ไม่ได้มาฟรีๆ มันมีข้อเสียด้วยซึ่งสามารถสรุปได้เป็นประโยคสั้นๆบรรทัดเดียวว่า: 565 | 566 | **อย่าได้ rebase commits ที่คุณเคย pushed ไปยัง public repository .. มันเป็นบาป** 567 | 568 | ตราบใดที่คุณทำตามคำแนะนำนี้ ชีวิตจะสดใส แต่ถ้าไม่ทำตาม ชาวประชาจะประนามคุณ เพื่อนๆและครอบครัวจะเหยียดหยามคุณ (จากผู้แปล: เชื่อเหอะ juacompe โดนมาแล้วตอนแปล progit เนี่ยแหละ T-T) 569 | 570 | ตอนที่คุณ rebase ของ คุณกำลังทิ้ง commits ที่มีอยู่ แล้วสร้างอันใหม่ที่ค่อนข้างเหมือนแต่ก็แตกต่าง ถ้าคุณ push commits ไปไว้ที่ไหนซักที่ แล้วคนอื่นๆ pull มันออกไปแล้วทำงานต่อยอดไปบนนั้น และแล้วคุณก็เขียนทับ commits เหล่านั้นด้วย `git rebase` แล้ว push มันกลับขึ้นไปอีกที, บรรดาเพื่อนร่วมงานจะต้อง merge งานทั้งหมดที่ทำมาเข้ามาใหม่ แล้วนรกก็จะเริ่มต้นขึ้นเมื่อคุณดึงงานของพวกเพื่อนๆเข้ามา 571 | 572 | ลองดูตัวอย่างกันว่าการ rebase งานที่เคยเปิดให้เป็น public แล้วมันเกิดปัญหายังไง สมมติว่าคุณ clone จาก server กลางซักตัวแล้วทำงานไปบนนั้น history ของ commits ก็จะหน้าตาประมาณ Figure 3-36. 573 | 574 | Insert 18333fig0336.png 575 | Figure 3-36. Clone a repository, and base some work on it. 576 | 577 | ทีนี้, ใครอีกคนก็ทำงานต่อแล้วก็ merge เข้ามา, แล้วก็ pushes งานนั้นเข้า server กลาง คุณก็ fetch งานพวกนั้นเข้ามาแล้วก็ merge remote branch อันใหม่เข้ามากับงานของคุณ, ทำให้ history ของคุณหน้าตาประมาณ Figure 3-37. 578 | 579 | Insert 18333fig0337.png 580 | Figure 3-37. Fetch more commits, and merge them into your work. 581 | 582 | หลังจากนั้น, ไอ้หมอนั่นที่ pushed งานที่ถูก merged ขึ้นมาก็ตัดสินใจที่จะย้อนกลับไปแล้ว rebase งานของมันแทน ซึ่งไอ้หมดนั่นก็จะสั่ง `git push --force` เพื่อทับ history บน server หลังจากนั้นคุณก็ fetch ของมาจาก server แล้วก็ได้ commits ใหม่ติดมา 583 | 584 | Insert 18333fig0338.png 585 | Figure 3-38. Someone pushes rebased commits, abandoning commits you’ve based your work on. 586 | 587 | ณ จุดนี้ คุณต้อง merge งานนี้เข้ามาใหม่อีกที แม้ว่าจะเคยทำไปทีนึงแล้วก็ตาม การ Rebase แก้ไขไอ้ SHA-1 hashes ของ commits เหล่านี้ทำให้ Git มองมันเป็นเหมือน commits ใหม่, ทั้งๆที่จริงๆแล้วคุณมีงานใน C4 อยู่ใน history ของคุณอยู่แล้ว (ดู Figure 3-39). 588 | 589 | Insert 18333fig0339.png 590 | Figure 3-39. You merge in the same work again into a new merge commit. 591 | 592 | ยังไงคุณจะต้อง merge ได้งานใหม่นี่เข้ามาซักวันแหละเพื่อจะได้ update code ตามเพื่อนๆได้ ซึ่งหลังจากทำแล้ว commit history ของคุณจะมีทั้ง commits C4 และ C4' ซึ่งมี SHA-1 hashes ต่างกันแต่เป็นงานเดียวกันและมี commit message เหมือนกัน ถ้าคุณ run `git log` ตอนที่ history คุณเป็นแบบนี้ คุณจะเห็นสอง commits ที่มี author date และ message เหมือนกันเป๊ะ แล้วมันจะชวนมึนมาก ยิ่งกว่านั้นคือถ้าคุณ push history นี้กลับไปยัง server คุณจะยัดพวก commits ที่ถูก rebased มาซ้ำเข้าไปที่ server กลาง ซึ่งจะไปทำให้คนอื่นๆมึนกันต่อ 593 | 594 | ถ้าคุณใช้การ rebase เฉพาะการเก็บกวาดและจัดระเบียบ commits ก่อนที่คุณจะ push มันและ ถ้าคุณ rebase เฉพาะแต่ commits ที่ไม่เคยถูกเปิดเผยให้ชาวประชา ปัญหาก็จะไม่เกิด แต่ถ้าคุณ rebase commits ที่เคยถูก pushed ออกไปให้สาธารณะชนแล้ว คนอื่นๆอาจจะทำงานต่อยอดไปจาก commits เหล่านั้น แล้วงานจะเข้า 595 | 596 | ## สรุป ## 597 | 598 | เราได้ผ่านเบสิคการแตก branch และการ merge ใน Git ไปแล้ว ตอนนี้การสร้าง branch ใหม่และย้ายไปทำงานบนนั้น, หรือว่าย้ายกลับไปกลับมา หรือว่า merge local branches เข้าด้วยกันน่าจะเป็นเรื่องชิวๆละ นอกจากนี้คุณน่าจะสามารถ share branches ของตัวเองโดยการ push มันขึ้นไปบน shared server, หรือว่าทำงานร่วมกับเพื่อนบน branches ที่ถูก shared หรือจะเป็นการ rebase branches ของคุณก่อนจะแชร์มัน 599 | -------------------------------------------------------------------------------- /07-customizing-git/01-chapter7.markdown: -------------------------------------------------------------------------------- 1 | # ปรับแต่ง Gitb # 2 | 3 | ที่ผ่านมาผมเน้นพื้นฐานการทำงานของ Git รวมถึงการใช้งานมัน และได้นำเสนอเครื่องมือหลายตัวที่ช่วยให้คุณสามารถใช้งาน Git ได้ง่าย และมีประสิทธิภาพมากขึ้น สำหรับบทนี้ผมจะพูดถึงเรื่องวิธีการปรับแต่ง Git ให้ทำงานตามสมัยนิยม โดยเน้นที่การปรับแต่งสำคัญๆ เป็นหลัก ซึ่งทำให้เราสามารถทำให้ Git ทำงานได้ในแบบที่เราอยากได้ บริษัทอยากได้ หรือแบบที่กลุ่มของเราอยากได้ 4 | 5 | ## ตั้งค่าให้ Git ## 6 | 7 | เราจะได้เห็นในบทที่ 1 ว่าเราสามารถปรับแต่งค่าของ Git ได้ด้วยคำสั่ง git config อย่างแรกที่เรากำหนดได้คือ ชื่อของเรา และ e-mail ของเรา 8 | 9 | $ git config --global user.name "John Doe" 10 | $ git config --global user.email johndoe@example.com 11 | 12 | หลังจากนี้เราจะได้เรียนรู้วิธีการปรับแต่ง Git เพื่อให้ตรงกับความต้องการของเรามากที่สุด 13 | 14 | ในบทแรกเราได้เราได้เรียนรู้การปรับแต่ง Git เบื้องต้นไปแล้วแต่ผมจะสรุปมันให้ฟังอีกที เราสามารถกำหนดลักษณะการทำงานของ Git ได้โดยการแก้ไขไฟล์สำหรับตั้งค่าเบื้องต้น โดยไฟล์แรกที่ Git จะไปอ่านคือไฟล์ `/etc/gitconfig` ซึ่งเก็บค่าเบื้องต้นสำหรับผู้ใช้ทุกคนบนเครื่องและสำหรับทุก Repositories ถ้าเราใส่คำว่า `--system` ตอนที่เรียก `git config` มันจะไปอ่านและเขียนที่ไฟล์นี้ 15 | 16 | ไฟล์ต่อไปที่ Git เข้าไปดูคือ `~/.gitconfig` ซึ่งใช้สำหรับปรับแต่งของผู้ใช้แต่ละคนบนเครื่อง เราสามารถแก้ไขไฟล์นี้โดยการใส่คำว่า `--global` เข้าไป 17 | 18 | สุดท้าย Git จะไปดูไฟล์ตั้งค่าในแต่ละ Git directory (`.git/config`) ที่เรากำลังใช้งานอยู่ ค่าเหล่านี้จะมีผลเฉพาะใน reposiroty ที่ไฟล์นั้นอยู่เท่านั้น 19 | 20 | ค่าที่ถูกตั้งอยู่ใกล้กับ repository จะทั้บค่าที่อยู่ไกลออกไปเช่น การตั้งค่าที่ `.git/config` จะทับค่าที่อยู่ใน `/etc/gitconfig` เป็นต้น เราสามารถเข้าไปแก้ไขค่าในไฟล์ได้โดยตรง แต่โดยปกติแล้วเราสามารถตั้งค่าต่างๆ ได้ผ่านคำสั่ง `git config` อยู่แล้ว 21 | 22 | 23 | ### พื้นฐานการปรับแต่งบนเครื่องตัวเอง ### 24 | 25 | การปรับแต่งใน Git มีอยู่สองประเภทคือ ปรับแต่งบนเครื่องตัวเอง (cliend side) และ ปรับแต่งที่ฝั่งเซิฟเวอร์ (server side) โดยส่วนมาแล้วจะเน้นการปรับแต่งบนเครื่องตัวเองมากกว่า เพราะมักจะเอาไว้ใช้เองไม่มีผลกับคนอื่น อย่างไรก็ตามค่าที่สามารถปรับแต่งได้มีอยู่มหาศาล แต่ที่จะพูดในบทนี้จะเป็นค่าที่ใช้กันบ่อยๆ หรือค่าที่จำเป็นสำหรับ workflow เป็นหลัก แม้ว่าในหลายๆ ค่าจะมีประโยชน์ในกรณีที่เราใช้ตัวล่าสุดแต่คงไม่ได้เอามาใส่ในเล่มนี้ ถ้าต้องการรู้ว่า Git ของคุณมีอะไรให้ปรับแต่งได้บ้าง ให้ลองสั่ง 26 | 27 | $ git config --help 28 | 29 | คู่มือการใช้งานที่ได้จาก `git config` จะแสดงรายการของค่าสำหรับปรับแต่พร้อมกับรายละเอียดอีกเล็กน้อย 30 | 31 | #### core.editor #### 32 | 33 | ถ้าไม่ได้ตั้งค่าอะไร Git จะเปิด text editor ที่กำหนดเป็นค่าบริยายในเครื่อง หรือไม่ก็ใช้ Vi เป็นตัวแก้ไขข้อความที่จะ commit หรือ tag ถ้าต้องการจะแก้ไขไปใช้ text editor ตัวอื่นให้ใช้คำสั่ง `core.editor` ในการตั้งค่า 34 | 35 | $ git config --global core.editor emacs 36 | 37 | หลังจากนี้ ไม่ว่า shell ของเราจะจะตั้งค่าปริยายสำหรับแก้ไขไฟล์เป็นอะไร โปรแกรม Git จะไปเปิด Emacs สำหรับแก้ไขข้อความเสมอ 38 | 39 | #### commit.template #### 40 | 41 | เราเรากำหนดค่านี้ไปที่ไฟล์บนเครื่อง Git จะใช้ไฟล์นี้เป็นข้อความตั้งต้นสำหรับการ commit ตัวอย่างเช่น เราสร้างไฟล์ `$HOME/.gitmessage.txt` ที่ออกมาหน้าตาแบบนี้ 42 | 43 | subject line 44 | 45 | what happened 46 | 47 | [ticket: X] 48 | 49 | วิธีการบอก Git ให้รู้ว่าเราต้องการให้ข้อความในไฟล์นี้มาแสดงใน text editor ตอนที่สั่ง `git commit` ให้สั่ง `commit.template` ดังนี้ 50 | 51 | $ git config --global commit.template $HOME/.gitmessage.txt 52 | $ git commit 53 | 54 | แล้วโปรแกรม text editor จะเปิดข้อความดังนี้ ทุกครั้งที่เราสั่ง commit 55 | 56 | subject line 57 | 58 | what happened 59 | 60 | [ticket: X] 61 | # Please enter the commit message for your changes. Lines starting 62 | # with '#' will be ignored, and an empty message aborts the commit. 63 | # On branch master 64 | # Changes to be committed: 65 | # (use "git reset HEAD ..." to unstage) 66 | # 67 | # modified: lib/test.rb 68 | # 69 | ~ 70 | ~ 71 | ".git/COMMIT_EDITMSG" 14L, 297C 72 | 73 | ต่อไปถ้าเราต้องการกำหนดนโยบายสำหรับข้อความที่จะ commit ให้เราใส่ไฟล์ต้นแบบไว้ในเครื่องแล้วกำหนดให้ Git ใช้เป็นค่าบริยาย จะช่วยให้การเปลี่ยนแปลงรูปแบบข้อความ commit ในทีมได้ง่ายขึ้น 74 | 75 | #### core.pager #### 76 | 77 | คำสั่ง core.pager ใช้สำหรับบอก Git ให้แสดงผลเป็นหน้าๆ สำหรับข้อมูลยาวๆ เช่น `log` และ `diff` เป็นต้น เราสามารถตั้งค่าเป็น `more` หรืออื่นๆ ตามที่ชอบ (ค่าบริยายคือ `less`) หรือเราจะปิดการแสดงผลเป็นหน้าไปเลยก็ได้ โดยการกำหนดค่าว่างให้มัน 78 | 79 | $ git config --global core.pager '' 80 | 81 | ถ้าเราสั่งคำสั่งข้างต้น ต่อไป Git จะแสดงข้อมูลทั้งหมดแบบไม่มีการแบ่งหน้า โดยไม่สนใจว่าข้อมูลจะยาวแค่ไหน 82 | 83 | #### user.signingkey #### 84 | 85 | ถ้าเราสร้าง signed annotated tag ขึ้นมา (อย่างที่คุยไว้ในบทที่ 2) การกำหนด GPG signing key สามารถทำได้เหมือนการกำหนดค่าอื่นๆ ตามนี้ 86 | 87 | $ git config --global user.signingkey 88 | 89 | ตอนนี้ เราสามารถใส่ tags โดยไม่ต้องมีกำหนด key ทุกครั้งที่เราใส่ tag ด้วยคำสั่ง `git tag` 90 | 91 | $ git tag -s 92 | 93 | #### core.excludesfile #### 94 | 95 | เราสามารถกำหนดรูปแบบของไฟล์ไว้ใน `.gitignore` เพื่อให้ Git ไม่สนใจไฟล์เหล่านั้นตอนที่เราสั่ง `git add` หรือตอนที่ตรวจสอบสถานะของโครงการ อย่างที่เคยคุยไว้ในบทที่สอง อย่างไรก็ตามถ้าเราต้องการให้มีไฟล์นอกโครงการของเรา ทำหน้าที่เก็บค่าเหล่านี้ หรือค่าอื่นๆ เราสามารถใช้คำสั่ง `core.excludesfile` ได้ โดยชี้คำสั่งนี้ไปที่ path ของไฟล์ที่มีเนื้อหาแบบเดียวกับ `.gitignore` ไฟล์ 96 | 97 | #### help.autocorrect #### 98 | 99 | คำสั่งนี้ใช้ได้เฉพาะใน Git 1.6.1 หรือเวอร์ชั่นที่สูงกว่า ถ้าเราพิมพ์คำสั่งผิดโปรแกรม Git 1.6 จะแสดงข้อความประมาณนี้ 100 | 101 | $ git com 102 | git: 'com' is not a git-command. See 'git --help'. 103 | 104 | Did you mean this? 105 | commit 106 | 107 | ถ้าเรากำหนด `help.autocorrect` เป็น 1 โปรแกรม Git จะแก้ไขข้อความแล้วสั่งให้โปรแกรมทำงานทันที ในกรณีที่ระบบเดาออกมาได้แค่ 1 รูปแบบ 108 | 109 | ### ใส่สีให้ Git ### 110 | 111 | Git สามารถแสดงสีบน terminal ได้ ซึ่งจะช่วยให้เราดูค่าต่างๆ ได้ง่ายและเร็วขึ้น โดยคำสั่งสำหรับกำหนดค่าสีมีดังนี้ 112 | 113 | #### color.ui #### 114 | 115 | Git จะพยายามใส่สีให้เราในทุกๆ ค่าที่ออกมาถ้าเราต้องการ เราสามารถกำหนดไปในรายละเอียดเกี่ยวกับสีและรูปแบบที่จะออกมาได้ แต่ก่อนอื่นต้องเราต้องเปิดให้ Git รู้ว่าเราต้องการให้มีสีก่อน โดยใช้คำสั่ง `color.ui` ดังนี้ 116 | 117 | $ git config --global color.ui true 118 | 119 | เมื่อค่าถูกกำหนด โปรแกรม Git จะใส่สีให้กับทุกค่าที่ออกมาใน terminal ทันที คำสั่งนี้ถูกใส่เข้ามาใน Git version 1.5.5 ถ้าเราใช้เวอร์ชั่นก่อนหน้านี้ เราจะต้องกำหนดค่าสีทีละค่าด้วยตัวเอง 120 | 121 | You'll rarely want `color.ui = always`. In most scenarios, if you want color codes in your redirected output, you can instead pass a `--color` flag to the Git command to force it to use color codes. The `color.ui = true` setting is almost always what you'll want to use. 122 | 123 | #### `color.*` #### 124 | 125 | ถ้าเราต้องการกำหนดลึกลงไปว่าคำสั่งใดบ้างที่ต้องการให้มีสี และมีสีอย่างไร เราสามารถกำหนดสีโดยใช้ verb-specific coloring setting ได้ โดยสามารถใช้ `true` `false` หรือ `always` กับคำสั่งเหล่านี้ 126 | 127 | color.branch 128 | color.diff 129 | color.interactive 130 | color.status 131 | 132 | ถ้ายังไม่สะใจ แต่ละคำสั่งสามารถกำหนดย่อยลงไปได้อีก โดยเราสามารถกำหนดสีให้กับแต่ละส่วนที่ออกมา เช่น ถ้าเราต้องการกำหนดให้ meta information ที่อยู่ใน diff มีสีตัวอักษรเป็นสีฟ้า มีสีพื้นหลังเป็นสีดำ และเป็นตัวหนา เราสามารถสั่งได้ดังนี้ 133 | 134 | $ git config --global color.diff.meta "blue black bold" 135 | 136 | เราสามารถกำหนดสีด้วยคำว่า normal black red green yellow blue magenta cyan หรือ white ถ้าเราต้องการกำหนดค่าอย่าง ตัวหนา แบบในตัวอย่างก่อนหน้านี้ เราสามารถใช้คำว่า bold dim ul blink และ reverse ได้ 137 | 138 | ดูเพิ่มเติมได้ใน `git config` manpage สำหรับค่าอื่นๆ ถ้าต้องการจะทำ 139 | 140 | ### ทำการ Merge และ Diff จากโปรแกรมภายนอก ### 141 | 142 | แม้ว่า Git จะมีระบบ diff (ที่เราใช้กันในบทก่อนๆ) เอาไว้แล้วก็ตาม แต่ถ้าเราต้องการใช้โปรแกรมอื่นในการตรวจสอบ diff ก็สามารถทำได้ เช่นเดียวกันเราสามารถใช้เครื่องมือที่มี GUI สำหรับการ merge และแก้ไข conflict แทนการใช้ merge ที่มีมากับ Git ก็ได้ ผมจะยกตัวอย่างการตั้งค่าเพื่อใช้ Perforce Visual Merge Tools (P4Merge) สำหรับ diffs และ merge เพราะมันมีหน้าตาที่ดูดี ที่สำคัญคือมันฟรี 143 | 144 | ถ้าต้องการทดลอง (P4Merge รองรับระบบปฏิบัติการหลักๆ) ก็สามารถทำตามได้เลย ผมจะใช้ path และ names โดยอ้างอิงจาก Mac และ Linux ส่วนใครที่ใช้ Windows ก็ให้เปลี่ยน `/usr/local/bin` ไปตาม path ของระบบที่ใช้อยู่นะครับ 145 | 146 | ขั้นแรกให้ Download P4Merge จากที่นี่ 147 | 148 | http://www.perforce.com/perforce/downloads/component.html 149 | 150 | เริ่มต้น เราจะกำหนดให้ external wrapper script ไปยังคำสั่งที่ต้องการ (ตัวอย่างจะเป็น path ของ Mac แต่ถ้าเป็นระบบอื่นก็ให้ลองหาคำสั่ง p4merge ในเครื่องดู) สร้าง merge wrapper ในชื่อ extMerge ให้มันไปเรียกคำสั่งที่เราต้องการดังนี้ 151 | 152 | $ cat /usr/local/bin/extMerge 153 | #!/bin/sh 154 | /Applications/p4merge.app/Contents/MacOS/p4merge $* 155 | 156 | สำหรับตัว diff wrapper เราจะได้รับ arguments ทั้งหมด 7 ตัว โดยเราจะส่ง 2 ตัวต่อไปยัง diff script ทั้งนี้ค่าปริยาย Git จะส่ง argument ไปยังโปรแกรม diff เป็นดังนี้ 157 | 158 | path old-file old-hex old-mode new-file new-hex new-mode 159 | 160 | เนื่องจากเราต้องการแค่ argument `old-file` และ `new-file` ดังนั้นเราจะให้ wrapper ส่งต่อไปเฉพาะที่เราต้องการดังนี้ 161 | 162 | $ cat /usr/local/bin/extDiff 163 | #!/bin/sh 164 | [ $# -eq 7 ] && /usr/local/bin/extMerge "$2" "$5" 165 | 166 | จากนั้นเราต้องเปลี่ยนให้ไฟล์ wrapper ทั้งสองตัวของเราเป็นไฟล์ที่สามารถ execute ได้ 167 | 168 | $ sudo chmod +x /usr/local/bin/extMerge 169 | $ sudo chmod +x /usr/local/bin/extDiff 170 | 171 | เมื่อทุกอย่างพร้อม เราจะสามารถกำหนดให้ Git ไปใช้ mearge resolution และ diff tools ของเราได้แล้ว เริ่มต้นเราบอก Git ก่อนว่าเราใช้ `merge.too` ตัวไหน จากนั้นก็บอกวิธีการเรียกใช้โดย `mergetool.extMerge.cmd` จากนั้นก็บอก Git ว่าเมื่อโปรแกรมทำงานเสร็จแล้วให้สนใจความสำเร็จหรือล้มเหลวของการ merge หรือไม่ โดยใช้คำสั่ง `mergetool.trustExitCode` เมื่อทำ merge เสร็จแล้วก็มากำหนดค่าให้ diff บ้างโดยใช้คำสั่ง `diff.external` ดังนี้ 172 | 173 | $ git config --global merge.tool extMerge 174 | $ git config --global mergetool.extMerge.cmd \ 175 | 'extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"' 176 | $ git config --global mergetool.trustExitCode false 177 | $ git config --global diff.external extDiff 178 | 179 | หรือจะใช้วิธีแก้ไขไฟล์ `~/.gitconfig` แล้วเพิ่มบรรทัดนี้ลงไป 180 | 181 | [merge] 182 | tool = extMerge 183 | [mergetool "extMerge"] 184 | cmd = extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED" 185 | trustExitCode = false 186 | [diff] 187 | external = extDiff 188 | 189 | หลังจากกำหนดค่าเสร็จแล้ว ถ้าเราลองสั่ง diff ตามนี้ 190 | 191 | $ git diff 32d1776b1^ 32d1776b1 192 | 193 | แทนที่จะได้ผลลัพทธ์เป็น diff จาก command line โปรแกรม Git จะไปเปิดโปรแกรม P4Merge ขึ้นมาแทนซึ่งได้ผลดังรูป 7-1 194 | 195 | Insert 18333fig0701.png 196 | รูป 7-1. P4Merge. 197 | 198 | ถ้าเราลอง merge สอง branches แล้วพบว่าเกิด conflic ขึ้น เราสามารถทดลองสั่ง `git mergetool` ดู โปรแกรม Git จะสั่ง P4Merge ให้ทำงานเพื่อให้เราแก้ไข conflic ผ่าน GUI 199 | 200 | การทำ wrapper มีข้อดีตรงที่เราสามารถปรับเปลี่ยนเครื่องมือในการ diff และ merge ได้ง่ายขึ้น ตัวอย่างเช่น เมื่อต้องการเปลี่ยนเครื่องมือ `extDiff` และ `extMerge` ไปใช้ KDiff3 สิ่งที่เราต้องทำก็แค่แก้ไขไฟล์ `extMerge` เท่านั้น 201 | 202 | $ cat /usr/local/bin/extMerge 203 | #!/bin/sh 204 | /Applications/kdiff3.app/Contents/MacOS/kdiff3 $* 205 | 206 | เท่านี้ Git ก็จะไปใช้โปรแกรม KDiff3 ในการแสดงผล diff และ merge conflict แทน 207 | 208 | Git ได้เตรียมวิธีง่ายๆ ให้เราสามารถเปลี่ยนไปใช้โปรแกรม merge-resolution บางตัวได้โดยไม่ต้องตั้งคำสั่งสำหรับเรียกใช้เลย เราสามารถตั้ง merge ให้ไปเรียกใช้ kDiff3 opendiff tkdiff meld xxdiff emerge vimdiff หรือ gvimdiff ได้ในบรรทัดเดียว และในกรณีที่เราไม่ต้องการใช้ kDiff3 ในการทำ diff แต่ต้องการใช้สำหรับการ merge-resolution เราสามารถสั่งได้ตามนี้ 209 | 210 | $ git config --global merge.tool kdiff3 211 | 212 | ถ้าเราใช้คำสั่งข้างต้นแทนการสร้างไฟล์ `extMerge` และ `extDiff` โปรแกรม Git จะใช้โปรแกรม kDiff3 สำหรับการ merge-resolution และใช้โปรแกรม diff ที่มาพร้อมกับ Git สำหรับการทำ diff 213 | 214 | ### การจัดการตัวอักษรและช่องว่าง ### 215 | 216 | เป็นเรื่องที่น่ารำคัญมากๆ เมื่อเราไล่ดูการแก้ไข code แล้วปรากฏว่าแทนที่เราจะเห็นเฉพาะส่วนเปลี่นแปลงที่มีผลกับโปรแกรม เรากลับเห็นจุดบรรทัดที่เปลี่ยนแปลงแค่ช่องว่างหลังบรรทัด โดยเฉพาะตอนที่เราไม่ได้ทำงานคนเดียว หรือทำงานข้ามระบบปฏิบัติการ มันมีโอกาสมากที่จะทำผิดพลาดเพราะ text editor ไม่ได้แสดงให้เราเห็นว่ามีช่องว่างอยู่ท้ายบรรทัดนั้นๆ หรือบางทีอาจจะแก้รหัส end-of-line ไปตามแต่ละระบบปฏิบัติการ โปรแกรม Git มีค่าที่เราตั้งได้หลายตัวเพื่อจัดการปัญหานี้ 217 | 218 | #### core.autocrlf #### 219 | 220 | ถ้าคุณทำงานบนระบบ Windows หรือทำงานบนระบบอื่นๆ แต่ทำงานร่วมกับคนที่เขียนโปรแกรมบน Windows คุณน่าจะเคยเจอปัญหา ระหัส end-of-line มาบ้าง นั่นเป็นเพราะ Windows ใช้ทั้ง carriage-return และใช้ linefeed สำหรับขึ้นบรรทัดใหม่ในไฟล์ แต่ใน Mac และ Linux ใช้เฉพาะ linefeed เท่านั้น ดูเหมือนจะเป็นเรื่องเล็กน้อยนะครับ แต่มันน่ารำคาญสุดๆ 221 | 222 | Git สามารถจัดการมันได้ ด้วยการเปลี่ยน carriage-return เป็น linefeed ให้อัตโนมัติตอนที่เราสั่ง commit และเปลี่ยนกลับให้เมื่อ checks out กลับมา เราสามารถเปิดใช้ความสามารถนี้ด้วยคำสั่ง `core.autocrlf` ถ้าอยู่บนระบบ Windows กำหนดให้ค่าเป็น `true` โปรแกรม Git จะแปลง linefeed เป็น carriage-return ให้ตอนที่เรา check out 223 | 224 | 225 | $ git config --global core.autocrlf true 226 | 227 | แต่ถ้าเราอยู่บน Linux หรือ Mac ที่ต้องการใช้ linefeed สำหรับจบบรรทัด เราไม่ต้องการให้ Git มา convert ให้แล้ว แต่อย่างไรก็ตามถ้าเราทำงานกับ Windows เราอาจจะบังเอิญไปเอาไฟล์ที่จบด้วย carriage-return มาใส่ในโครงการของเราเข้า วิธีการแก้ไขก็คือเราต้องบอกให้ Git เปลี่ยน carriage-return ไปเป็น linefeed ให้ตอนที่ commit แต่ไม่ต้องเปลี่ยนตอนที่ checkout ด้วยคำสั่ง `core.autocrlf` 228 | 229 | $ git config --global core.autocrlf input 230 | 231 | พอทำแบบนี้เราจะได้ carriage-return ลงท้ายบรรทัดสำหรับ Windows และได้ linefeed ลงท้ายสำหรับ Mac Linux และใน repository ครับ 232 | 233 | แต่ถ้าเราเป็นนักพัฒนาบน Windows และโปรเจคของเราเป็น Windows เท่านั้น เราสามารถปิดความสามารถนี้ทิ้งไปได้เลย โดยใช้คำสั่ง 234 | 235 | $ git config --global core.autocrlf false 236 | 237 | #### core.whitespace #### 238 | 239 | Git มาพร้อมกับความสามารถในการจัดการปัญหาช่องว่างทั้ง 4 ประการ โดยสองประการแรกจะถูกเปิดโดยอัตโนมัติ แต่จะปิดไปก็ได้ ส่วนอีกสองอันถ้าต้องการเปิดก็สามารถทำได้เช่นกัน 240 | 241 | สองคำสั่งแรกที่เปิดโดยอัตโนมัติคือ `trailing-space` กับหลับจัดการปัญหาช่องว่างหลังบรรทัด และอีกคำสั่งคือ `space-before-tab` สำหรับจากการช่องว่างก่อน tab ในตอนต้นของบรรทัด 242 | 243 | อีกสองคำสั่งที่ปิดเอาไว้แต่สามารถเปิดได้ คือ `indent-with-non-tab` สำหรับจัดการการใช้ช่องว่างขนาดเท่ากับแปดตัวอักษรหรือมากกว่า แทนที่จะใช้ tab และคำสั่งสุดท้ายคือ `cr-at-eol` ใช้สำหรับบอก Git ว่าไม่ต้องไปยุ่งกับ carriage-return ที่ท้ายบรรทัด 244 | 245 | เราสามารถบอก Git ให้ปิดหรือเปิดความสามารถเหล่านี้ได้ โดยการกำหนดค่าให้กับ `core.whitespace` โดยใส่คำสั่งที่ต้องการให้เปิด แล้วคั่นด้วย comma ถ้าตัวไหนที่ไม่ได้ใส่ไว้ Git จะเข้าใจว่าให้ปิดความสามารถนั่นๆ ไป หรืออีกทางหนึ่งคือให้ใส่เครื่องหมายลบ `-` เอาไว้หน้าคำสั่งนั้นๆ ตัวอย่างเช่นถ้าตอ้งการเปิดความสามารถทั้งหมด ยกเว้น `cr-at-eol` ให้สั่งตามนี้ 246 | 247 | $ git config --global core.whitespace \ 248 | trailing-space,space-before-tab,indent-with-non-tab 249 | 250 | Git จะช่วยจัดการปัญหาเหล่านี้ให้ตอนที่เราสั่ง `git diff` และจะใส่สีไว้ให้เห็นชัดๆ เพื่อเราจะได้แก้ไขก่อนที่จะ commit เข้าไป นอกจากนี้มันยังช่วยจัดการเรื่อง apple patches ตอนสั่ง `git apply` ด้วย เมื่อเราสั่ง apply patches เราสามารถให้ Git ช่วยเตื่อนได้ถ้าพบปัญหาเรื่องช่องว่าง เช่น 251 | 252 | $ git apply --whitespace=warn 253 | 254 | หรือเราสามารถบอกให้ Git แก้ข้อมูลให้เลย ก่อนที่จะทำการ patch ก็ได้ 255 | 256 | $ git apply --whitespace=fix 257 | 258 | ความสามารถนี้สามารถนำปประยุกต์ใช้กับ git rebase ได้เช่นเดียวกัน ตัวอย่างเช่น ถ้าเราเผลอ commit ช่องว่างที่ไม่เหมาะสมเข้าไป จึงไม่อยาก push ขึ้นไปที่ server เราสามารถสั่ง `rebase` ด้วยคำสั่ง `--whitespace=fix` แล้ว Git จะไปจัดการกับปัญหาช่องว่างให้เราเอง 259 | 260 | 261 | ### การปรับแต่งบนเครื่อง Server ### 262 | 263 | Git บนฝั่ง Server ไม่ค่อยมีอะไรให้ทำเท่าไหรครับ แต่มีหลายอย่างที่รู้ไว้ใช่ว่าใส่บ่าแบกหามครับ 264 | 265 | #### receive.fsckObjects #### 266 | 267 | ตามปกติ Git จะไม่มีระบบตรวจสอบความเรียบร้อยของแต่ละไฟล์ระหว่างที่ push ขึ้นมาบน Server ถึงแม้ว่า Git จะสามารถตรวจสอบด้วยวิธี SHA-1 checksum ได้ แต่มันทำไม่ได้ทำในทุกๆ การ push เพราะว่ามันจะทำให้เสียเวลามากๆ ถ้าทุกครับที่ push เราต้องตรวจสอบมันทุกไฟล์ ยิ่งถ้า repository หรือ push มีไฟล์จำนวนมาก แต่ถ้าเราต้องการให้ Git มันตรวจสอบทุกๆ ไฟล์ในทุกๆ ครั้งที่ push เราก็สามารถบีบคอให้ Git มันทำให้ได้ ผ่านการกำหนด `receive.fsckObjects` ให้เป็น true 268 | 269 | $ git config --system receive.fsckObjects true 270 | 271 | ที่นี่ Git จะทำการตรวจสอบทุกๆ ไฟล์ ก่อนที่จะรับเข้าไป repository เพื่อให้แน่ใจว่าความผิดพลาดที่ client จะไม่สงผลกระทบต่อข้อมูลของเรา 272 | 273 | #### receive.denyNonFastForwards #### 274 | 275 | ถ้าเรา rebase commits ที่เรา push ขึ้นไปแล้ว จากนั้นก็ push มันขึ้นไปอีกที หรือไม่เราก็พยายาม push commit ที่ไม่มีจุดเชื่อมต่อกับ commit บน server ตามปกติทั้งสองกรณี Git จะไม่อนุญาติให้เราทำ ซึ่งนี้เป็นสิ่งที่ดี แต่ในบางกรณีถ้าคุณเข้าใจระบบเป็นอย่างดี และแน่ในว่าต้องการทำอย่างนั้นจริงๆ เราก็สามารถใช้ `-f` เพื่อบีบคือให้ Git ยอม push ให้เรา 276 | 277 | เพื่อไม่ให้ใครมา force-update ได้ เราสามารถสั่งห้ามเด็จขาดโดยกำหนดให้ `receive.denyNonFastForwards` มีค่าเป็น true 278 | 279 | $ git config --system receive.denyNonFastForwards true 280 | 281 | อีกวิีธีที่สามารถทำได้บนฝั่ง server คือการใช้ receive hooks ซึ่งจะได้ดูกันต่อไป สำหรับวิธีนั้นจะค่อนข้างซับซ้อนเหมือนการทำ non-fast-forwards ที่เจาะจงกลุ่มผู้ใช้ 282 | 283 | #### receive.denyDeletes #### 284 | 285 | ผู้ใช้บางคนใช้วิธีขี้โกง เมื่อเห็นว่าเรามีข้อกำหนด `denyNonFastForwards` ก็จะใช้วิธีลบ branch ทิ้ง แล้วก็ push ขึ้นมาอีกทีพร้อมจุดเชื่อมต่อใหม่ สำหรับ Git เวอร์ชั่นใหม่ (สูงกว่าเวอร์ชั่น 1.6.1) เราสามารถตั้งค่า `receive.denyDeletes` เป็น true ได้ 286 | 287 | $ git config --system receive.denyDeletes true 288 | 289 | วิธีการนี้จะทำให้ Git ไม่ยอมให้ลบ branch หรือ tag ผ่านการ push ที่ผั่ง client ถ้าต้องการลบจริงๆ จะต้องลบที่ฝั่ง server ด้วยตัวเอง จริงๆ มีวิธีจัดการกับปัญหานี้อีกแบบด้วย ACLs ่ซึ่งเราจะอธิบายกันตอนท้ายๆ บทนี้ 290 | 291 | ## Git Attributes ## 292 | 293 | Git สามารถใช้ความสามารถนี้ได้เฉพาะกับ directory หรือ กับไฟล์ เท่านั้น เราเรียกการกำหนด path-specific ว่า Git attributes ซึ่งสามาถกำหนดไว้ในไฟล์ `.gitattributes` ที่อยู่ใน repository ของเรา (ปกติแล้วจะใส่ไว้ใน root ของ project) หรือใส่ไว้ในไฟล์ `.git/info/attributes` ถ้าไม่ต้องการให้ไฟล์ attributes ถูกส่งขึ้นไปบน server ด้วย 294 | 295 | การใช้ attributes จะทำให้เราเหมือนว่าสามารถกำหนดวิธีในการ merge สำหรับไฟล์แต่ละแบบได้ หรือสำหรับแต่ละ directory ใน project ของเราได้ เราสามารถสอน Git ได้ว่าไฟล์แต่ละแบบจะต้อง diff กันอย่างไร หรือจะต้องนำไฟล์นั้นๆ ไปทำอะไรก่อนแล้วค่อยนำค่ามา diff สำหรับในส่วนนี้เราจะใช้ตัวอย่างเพื่อแสดงให้เห็นวิธีการใช้งานคำสั่งบางส่วนของ attributes 296 | 297 | ### Binary Files ### 298 | 299 | เทคนิคเท่ๆ ที่ได้จากการใช้งาน Git attribute คือเราสามารถบอก Git ได้ว่าไฟล์ไหนคือ binary (ในหลายกรณีระบบอื่นๆ จะไม่สามารถแยกได้) และสามารถบอกได้ด้วยว่าจะจัดการกับไฟล์รูปแบบนั้นได้อย่างไร ตัวอย่างเช่น text file ที่คอมพิวเตอร์เป็นคนสร้างขึ้นมา ถึง diff ออกมาก็อ่านหรือแก้ไขไม่ได้ ไฟล์พวกนี้ก็ควรถูกจัดการแบบไฟล์ binary ในขณะที่ไฟล์ binary บางไฟล์เราสามารถบอกความแตกต่างของแต่ละไฟล์ได้ เราก็ควรจะบอกให้ Git ช่วยจัดการให้ 300 | 301 | #### ระบุไฟล์ประเภท Binary Files #### 302 | 303 | ไฟล์บางไฟล์อาจจะดูเหมือน text files แต่จริงๆ แล้วมันถูกสร้างขึ้นมาเพื่อให้จัดการมันแบบข้อมูล ิbinary ตัวอย่างเช่น project ที่ถูกสร้างด้วย XCode บน Mac จะมีไฟล์ที่ชื่อ `.pbxproj` ซึ่งด้านในเก็บข้อมูลแบบ JSON (plain text javascript data format) ซึ่งคนที่สร้างคือตัว IDE เอง สำหรับกำหนดค่า setting ในโปรแกรม ในทางเทคนิคแล้วนี่ถือว่าเป็น text file เพราะข้อมูลถูกเก็บด้วย ASCII และสามารถอ่านได้ แต่เราไม่จัดการไฟล์นี้แบบ text file เรามองไฟล์นี้เป็นเหมือน database ขนาดเล็ก เราไม่สามารถ merge ไฟล์นี้ได้ถ้ามีคนสองคนแก้มัน และการใช้ diff ก็ไม่ได้ช่วยอะไรมากนัก หมายวามว่าไฟล์นี้ควรถูกจัดการด้วยคอมพิวเตอร์ หรือเรียกว่าเราควรจัดการไฟล์นี้แบบ binary file 304 | 305 | การบอก Git ให้จัดการไฟล์ `pbxproj` แบบไฟล์ที่มีข้อมูลเป็น binary สามารถทำได้โด้ยการเพิ่มบรรทัดนี้ลงใน `.gitattributes` 306 | 307 | *.pbxproj -crlf -diff 308 | 309 | หลังจากนี้ Git จะไม่พยายามแก้ปัญหา carriage-return ให้เรา หรือไม่พยายามดูความแตกต่างในไฟล์ เมื่อเราสั่ง git diff แต่ใน Git เวอร์ชัน 1.6 เราสามารถใช้คำสั่งสั้นๆ แทนการใช้ `-crlf -diff` ได้ ด้วยคำสั่งนี้ 310 | 311 | *.pbxproj binary 312 | 313 | #### Diffing Binary Files #### 314 | 315 | ใน Git เวอร์ชัน 1.6 เราสามารถใช้ Git attributes ในการจัดการเปรียบเทียบ binary ไฟล์ได้ วิธีการคือเราต้องบอก Git ว่าจะแปลง binary data ให้เป็น text แบบไหน ก่อนที่จะใช้เปรียบเทียบโดยใช้ diff 316 | 317 | เพราะว่านี้เรื่องที่เท่มากๆ แต่คนทั่วไปกลับไม่ค่อยรู้จักกัน ดังนั้นผมจะลงไปในรายละเอียดให้มากหน่อย เริ่มต้นจากเราจะลองใช้เทคนิคนี้แก้ปัญหาในการทำ version-controlling ให้ Word document กัน แม้ว่าเราจะรู้กันว่าใช้ Word เป็น text editer นั้นสยองเอามากๆ แต่ทุกคนก็ใช้กัน แล้วถ้าเราต้องการทำ version-control กับเอกสาร Word ล่ะ แน่นอนว่าเราสามารถส่งไฟล์ Word ขึ้น Git ได้ แต่มันจะดีหรือถ้าเราสั่ง `git diff` แล้วมันแสดงแบบนี้ 318 | 319 | $ git diff 320 | diff --git a/chapter1.doc b/chapter1.doc 321 | index 88839c4..4afcb7c 100644 322 | Binary files a/chapter1.doc and b/chapter1.doc differ 323 | 324 | เราไม่สามารถเปรียบเทียบไฟล์สองเวอร์ชั้นได้ตรงๆ ยกเว้นจะ download มาทั้งสองตัวแล้วเปิดเทียบกันด้วยตัวเอง ทีนี้ลองมาดูว่า Git attribute จะช่วยเราได้อย่างไร เริ่มจากทดลองใส่บรรทัดต่อไปนี้ลงในไฟล์ `.gitattributes` 325 | 326 | *.doc diff=word 327 | 328 | บรรทัดนี้จะบอก Git ว่าไฟล์ที่ตรงกับรูปแบบ (.doc) ให้ใช้ "word" เป็นตัวกรองตอนที่ต้องการใช้ diff ในการดูความเปลี่ยนแปลง ไม่ต้องสงสัยว่า Git ทำ "word" ให้ เราต้องทำมันขึ้นมาเองครับ สิ่งที่ต้องทำคือใช้โปรแกรม `string` ในการแปลงเอกสาร Word ให้เป็น text file ที่อ่านได้ ซึ่งช่วยให้ diff สามารถทำการเปรียบเทียบได้ไปด้วย 329 | 330 | $ git config diff.word.textconv strings 331 | 332 | ตอนนี้ Git รู้แล้วว่าถ้าเราพยายาจะเปรียบเทียบไฟล์ที่ลงท้ายด้วย `.doc` สองเวอร์ชัน จะต้องเปรียบเทียบโดยใช้ "word" ในการกรอง โดยมันจะไปใช้โปรแกรม `strings` อีกที วิธีการนี้จะทำให้เราได้ text file อย่างดี สำหรับให้ Git เอาไปใช้ในการเปรียบเทียบ 333 | 334 | ตัวอย่างต่อไปนี้ ผมจะใส่บทแรกฉบับภาษาอังกฤษของหนังสือบทนี้ลงใน Git จากนั้นจะใส่ข้อความเล็กน้อยลงแทรกลงไป หลังจากบันทึก ผมจะสั่ง `git diff` เพื่อดูความเปลี่ยนแปลง 335 | 336 | $ git diff 337 | diff --git a/chapter1.doc b/chapter1.doc 338 | index c1c8a0a..b93c9e4 100644 339 | --- a/chapter1.doc 340 | +++ b/chapter1.doc 341 | @@ -8,7 +8,8 @@ re going to cover Version Control Systems (VCS) and Git basics 342 | re going to cover how to get it and set it up for the first time if you don 343 | t already have it on your system. 344 | In Chapter Two we will go over basic Git usage - how to use Git for the 80% 345 | -s going on, modify stuff and contribute changes. If the book spontaneously 346 | +s going on, modify stuff and contribute changes. If the book spontaneously 347 | +Let's see if this works. 348 | 349 | Git สามารถตรวจสอบความเปลี่ยนแปลงได้อย่างชัดเจน ประโยคที่ผมใส่ลงไปคือ "Let's see if this works" ซึ่งผลออกมาถูกต้อง แต่ก็ไม่ได้ถูกต้อง 100% นะครับ ยังมีประโยคที่จริงๆ ไม่ได้แก้ไขอะไรโผล่ออกมาด้วย คงต้องรอคนทำ Word-to-plain-text ดีๆ ออกมาก่อน อย่างไรก็ตามเจ้า `strings` ก็สามารถใช้ได้ทั้งน Mac และ Linux และยังสามารถแปลง binary file ได้หลาย format อีกด้วย ถือว่าใช้ได้ดีทีเดียว 350 | 351 | ปัญหาที่น่าสนใจอีกอย่าคือการ diff ไฟล์รูปภาพ แต่เราคงไปเทียบรูปภาพกันตรงๆ ได้ยาก ที่พอทำได้คือใช้ Metadata พวก EXIF ที่ติดมากับภาพในการเปรียบเทียบ เราจะใช้โปรแกรม `exiftool` มาช่วยดึง metadata ออกมาจากรูปภาพ อย่างน้อยเราก็จะได้เห็นของเก่าเที่ยบกับของใหม่บ้าง 352 | 353 | $ echo '*.png diff=exif' >> .gitattributes 354 | $ git config diff.exif.textconv exiftool 355 | 356 | ทดลองแทนที่รูปภาพเดิมด้วยภาพใหม่ จากนั้นสั่ง `git diff` แล้วดูผลลัพธ์ 357 | 358 | diff --git a/image.png b/image.png 359 | index 88839c4..4afcb7c 100644 360 | --- a/image.png 361 | +++ b/image.png 362 | @@ -1,12 +1,12 @@ 363 | ExifTool Version Number : 7.74 364 | -File Size : 70 kB 365 | -File Modification Date/Time : 2009:04:21 07:02:45-07:00 366 | +File Size : 94 kB 367 | +File Modification Date/Time : 2009:04:21 07:02:43-07:00 368 | File Type : PNG 369 | MIME Type : image/png 370 | -Image Width : 1058 371 | -Image Height : 889 372 | +Image Width : 1056 373 | +Image Height : 827 374 | Bit Depth : 8 375 | Color Type : RGB with Alpha 376 | 377 | เราจะเห็นได้ชัดเจนว่าภาพมีการเปลี่ยนแปลงขนาด ความกว้างยาว เทียบกันทั้งภาพเก่าและภาพใหม่ 378 | 379 | ### Keyword Expansion ### 380 | 381 | ความสามารถในการทำ Keyword expansion เหมือนของ SVN- หรือ CVS-style นั้นถูกเรียกร้องโดยผู้ที่เคยใช้มาก่อน แต่ปัญหาคือระบบของ Git ไม่อนุญาติให้เราแก้ไขไฟล์ด้วยข้อมูลที่มากับการ commit หลังจากที่เราทำการ commit ไปแล้วเพราะ Git ได้ checksums ไปแล้ว อย่างไรก็ตาม เราสามารถใช้วิธีแทรก text เข้าไปใน file ตอนที่ checkout ก็ได้ จากนั้นก็ลบมันออกก่อนที่จะ commit เข้าไป โดยใช้เทคนิคของ Git attribute ในการทำ 382 | 383 | ขั้นแรกทดลอง ใส่ SHA-1 checksum เข้าไปแทนที่ `$Id$` ในไฟล์แบบอัตโนมัติ ถ้าเรากำหนดค่านี้เข้าไปในไฟล์หนึ่งหรือหลายไฟล์ ตอนที่เรา checkout ิbranch นี้ออกมา ตัว Git จะแทนที่ `$Id$` ด้วย SHA-1 สิ่งสำคัญคือต้องไม่ลืมว่า เราไม่ได้ commit SHA เข้าไปแต่ส่งเข้าไปเฉพาะ blob (Binary Large OBjects) ของมัน 384 | 385 | $ echo '*.txt ident' >> .gitattributes 386 | $ echo '$Id$' > test.txt 387 | 388 | หลังจากนี้ทุกครั้งที่ check out โปรแกรม Git จะใส่ SHA of the blob เข้าไป 389 | 390 | $ rm text.txt 391 | $ git checkout -- text.txt 392 | $ cat test.txt 393 | $Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $ 394 | 395 | จะเห็นว่าสิ่งที่ได้มาเราใช้ได้ค่อนข้างจำกัด ถ้าเราใช้ระบบ keword ใน CVS หรือ Subversion เราสามารถใส่ datestamp ลงไปได้เลย เทียบกับ SHA จะไม่ได้ช่วยอะไรมากนัก เพราะเราคงบอกอะไรไม่ได้ว่า SHA ตัวนี้เก่าหรือใหม่กว่าอีกตัวนึง 396 | 397 | สรุปได้ว่าเราควรเขียน filter ขึ้นมาเองสำหรับการแทนค่าอะไรก็ตามในไฟล์ตอนที่ commit/checkout โดยอาศัย "clean" และ "smudge" ในไฟล์ `.gitattributes` เราสามารถระบุส่วนที่ต้องการแก้ไขลงไป จากนั้น script จะทำการแก้ไขไฟล์ก่อน checkout ("smudge" ตามรูป 7-2) และก่อน commit ("clean", ตามรูป 7-3) เราสามารถเล่นอะไรหลายๆ อย่างได้ด้วยวิธีการนี้ 398 | 399 | Insert 18333fig0702.png 400 | Figure 7-2. The "smudge" filter is run on checkout. 401 | 402 | Insert 18333fig0703.png 403 | Figure 7-3. The "clean" filter is run when files are staged. 404 | 405 | ตัวอย่างต่อไปเป็นการส่ง C source code เข้าโปรแกรม `indent` ก่อนที่จะ commit โดยเราจะเพิ่ม filter attribute ไปในไฟล์ `.gitattributes` เพื่อคัด `*.c` มาใส่โปรแกรม "indent" 406 | 407 | *.c filter=indent 408 | 409 | จากนั้น ให้กำหนด "indent" สำหรับ smudge และ clean 410 | 411 | $ git config --global filter.indent.clean indent 412 | $ git config --global filter.indent.smudge cat 413 | 414 | ตามตัวอย่างข้างต้น เมื่อเรา commit ไฟล์ที่เป็น `*.c` โปรแกรม Git จะส่งไฟล์นั้นไปให้โปรแกรม `indent` ก่อนที่จะ commit เข้าไป และจะส่งมันให้กับโปรแกรม `cat` ก่อนที่จะ checkout ทั้งนี้โปรแกรม `cat` จะไม่ได้เพิ่มหรือลดอะไรในไฟล์ถ้าส่งข้อมูลอะไรให้ มันก็จะส่งกลับออกมาแบบเดียวกัน ถ้าทำแบบนี้จะทำให้ C source code ของเราถูกจัด indent ทุกครั้งก่อนที่จะ commit เข้าไป 415 | 416 | ตัวอย่างอีกอันที่น่าสนใจคือการเลียนแบบ `$Date$` keyword expansion วิธีการคือเราจะเขียน scrite ขึ้นมาตัวนึงสำหรับเลือกไฟล์ที่เราต้องการแล้วนำไฟล์นั้นมาแก้ไข เพื่อใส่วันที่ commit ล่าสุดของโครงการลงไป เราจะใช้ script ruby ต่อไปนี้ 417 | 418 | #! /usr/bin/env ruby 419 | data = STDIN.read 420 | last_date = `git log --pretty=format:"%ad" -1` 421 | puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$') 422 | 423 | สิ่งที่ script ตัวนี้ทำคือ เอาค่าวันที่สุดท้ายที่ commit จากคำสั่ง `git log` จากนั้นนำไปแทนที่ `$Date$` ในทุกไฟล์ที่เข้ามา หลังจากทำเสร็จแล้วก็ให้มันแสดงออกมา ซึ่ง Git จะนำค่าที่ได้ไปใช้ต่อ จริงๆ แล้วเราจะใช้ภาษาอะไรก็ได้แล้วแต่ถนัด สมติว่าเราตั้งชื่อไฟล์ว่า `expand_date` จากนั้นก็กำหนดค่า smudge ให้ไปชี้ที่ `expand_date` เพื่อให้แก้ไขค่าตอนที่สั่ง checkout และตอนที่เรา commit ก็ให้เปลี่ยนค่าที่เราใส่ลงไปให้กลับมาเป็น $Date$ เหมือนเดิมด้วยภาษา perl 424 | 425 | $ git config filter.dater.smudge expand_date 426 | $ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"' 427 | 428 | คำสั่งภาษา perl ที่เห็นนี้ จะเปลี่ยนค่าที่อยู่ใน `$Date$` ให้กลับมาเหมือนตอนเริ่มต้นอีกครั้ง เอาละตอนนี้ตัวจัดการก็สมบูรณ์แล้ว เราสามารถทดสอบได้ด้วยการใส่ไฟล์ที่มีคำว่า $Date$ ลงใน code ของเรา 429 | 430 | $ echo '# $Date$' > date_test.txt 431 | $ echo 'date*.txt filter=dater' >> .gitattributes 432 | 433 | หลังจากที่เรา commit ค่าที่เราใส่ลงไป แล้วอลง check out ออกมาดู เราจะพบว่า keyword ของเราเปลี่ยนเป็นวันเวลาเรียบร้อยแล้ว 434 | 435 | $ git add date_test.txt .gitattributes 436 | $ git commit -m "Testing date expansion in Git" 437 | $ rm date_test.txt 438 | $ git checkout date_test.txt 439 | $ cat date_test.txt 440 | # $Date: Tue Apr 21 07:26:52 2009 -0700$ 441 | 442 | จะเห็นว่าเทคนิคนี้สามารถนำไปประยุกต์ใช้ได้มากมายในโปรแกรมของเรา แต่ที่ต้องระวังให้มากคือแม้ว่าไฟล์ `.gitattributes` จะถูกส่งขึ้น Git ไปกับ source code ของเราแต่ตัว driver (ในกรณีนี้คือ `dater`)ไม่ได้ถูกส่งไปด้วย ดังนั้นการ setup นี้จึงไม่ได้ทำงานได้ทุกที่ ดังนั้นถ้าเราเป็นคนออกแบบ filter เราจะต้องออกแบบให้แม้ว่า filter จะไม่ทำงาน ตัวโครงการก็ต้องสามารถทำงานได้ต่อไป 443 | 444 | ### Exporting Your Repository ### 445 | 446 | เราสามารถตั้งค่าใน Git attribute เพื่อกำหนดรูปแบบเท่ๆ ในการ export project ของเราได้ (export เป็น zip หรือ tar.gz) 447 | 448 | #### export-ignore #### 449 | 450 | เราสามารถกำหนดได้ว่าจะไม่ export ไฟล์หรือ directory ใดบ้าง โดยกำหนดไปที่ค่า `export-ignore` ตัวอย่างเช่น ถ้าเราไม่ต้องการ export directory `test/` เพราะมันดูไม่เหมาะสมตอนที่คนต้องการ export เป็น tar/zip ไปใช้ เราสามารถกำหนดในไฟล์ Git attributes ดังนี้ 451 | 452 | test/ export-ignore 453 | 454 | เอาล่ะ ครั้งหน้าตอนที่สั่ง git archive เพื่อสร้าง tarball เราจะไม่เห็น directory ที่กำหนดไว้ในผลลัพธ์ 455 | 456 | #### export-subst #### 457 | 458 | อีกอย่างที่เราสามารถทำได้ตอนที่สั่ง archive คือการทำ simple keyword substitution โปรแกรม Git ให้เราสามารถใส่ `$format:$` ไว้ในไฟล์ไหนก็ได้ ด้วย `--pretty=format` (ส่วนมากเราจะเห็นในบนที่สองแล้ว) ถ้าเราต้องการใส่ไฟล์ `LAST_COMMIT` ไว้ใน archive พร้อมกับวันที่เรา commit ครั้งสุดท้าย ตอนที่สั่ง `git archive` เราสามารถทำได้โดย 459 | 460 | $ echo 'Last commit date: $Format:%cd$' > LAST_COMMIT 461 | $ echo "LAST_COMMIT export-subst" >> .gitattributes 462 | $ git add LAST_COMMIT .gitattributes 463 | $ git commit -am 'adding LAST_COMMIT file for archives' 464 | 465 | ทีนี้เมื่อเราสั่ง `git archive` ข้อมูลที่อยู่ในไฟล์ ตอนที่ผู้ใช้แตก archive ออกมาจะเป็นดังนี้ 466 | 467 | $ cat LAST_COMMIT 468 | Last commit date: $Format:Tue Apr 21 08:38:48 2009 -0700$ 469 | 470 | ### Merge Strategies ### 471 | 472 | เราสามารถใช้ Git attritube ในการบอก Git ว่าเราจะใช้เทคนิคอะไรในการ Merge (Merge Strategies) ในแต่ละไฟล์ของโครงการ ตัวอย่างที่ใช้บ่อยๆ เช่นเราจะบอก git ว่าบางไฟล์ ถ้าเจอ conflicts ไม่ต้องทำการ merge แต่ให้เลือกใช้ไฟล์ใดไฟล์หนึ่งไปเลย 473 | 474 | การกำหนด merge strategies จะค่อนข้างใช้บ่อยในโครงการที่แตกออกมาจากโครงการอื่นเพราะมีความต้องการเฉพาะ แต่เราตอ้งการ merget กลับไปให้โครงการหลัก ทำให้เราไม่ต้องการ merge ในบางไฟล์ ยกตัวอย่างเช่นไฟล์ database.xml ซึ่งจะต่างกันในแต่ละ branch และเราไม่้ต้องการ Merge มันเข้าดัวยกัน เราสามารถกำหนดค่าได้ดังนี้ 475 | 476 | database.xml merge=ours 477 | 478 | เมื่อเรา merge ไปยัง branch อื่น แทนที่จะเกิด conflicts ในไฟล์ database.xml เราจะเห็นข้อความดังนี้ 479 | 480 | $ git merge topic 481 | Auto-merging database.xml 482 | Merge made by recursive. 483 | 484 | ในกรณีนี้ database.xml จะไม่เคยเปลี่ยนไปจากต้นฉบับเลย 485 | 486 | ## Git Hooks ## 487 | 488 | เหมือนกับ Version Control System อื่นๆ Git เปิดช่องให้เราเขียน Script เพื่อเข้าไปปรับแต่งการทำงานได้ การปรับแต่งจะมีอยู่สองกลุ่มคือ การปรับแต่งที่ client หรือการปรับแต่งที่เครื่องของเรา อีกแบบคือการปรับแต่งที่ server การปรับแต่งที่ฝั่ง client จะเข้าไปจัดการคำสั่งอย่างการ commit และการ merge ส่วนการจัดการในฝั่ง server จะเข้าไปจัดการคำสั่งอย่างการ receive push เป็นต้น เราสามารถ hook ได้ในหลายกรณี ซึ่งจะได้เรียนกันต่อไป 489 | 490 | ### Installing a Hook ### 491 | 492 | ไฟล์สำหรับการ hook จะอยู่ใน folder `hooks` ซึ่งอยู่ใน repository ของเรา โดยส่วนมากจะอยู่ใน `.git/hooks` ถ้าเปิดดูเราจะเห็นไฟล์ตัวอย่างหลายตัวอยู่ในนั้น เราควรจะลองเปิดอ่านดูเพราะเป็นตัวอย่างที่มีประโยชน์ มีคำอธิบายอยู่ในนั้นด้วย ตัวอย่างทั้งหมดเขียนด้วย shell script บางตัวเป็น perl แต่จริงๆ เราจะใช้อะไรเขียนก็ได้ จะเป็น python หรืออะไรก็ได้ที่ชอบ สำหรับ Git หลัง version 1.6 ไฟล์ที่อยู่ใน hooks จะมีชื่อต่อท้ายด้วย .sample ซึ่งเราต้องตัดออกก่อนจะนำไปใช้ ส่วน Git ก่อน version 1.6 จะมีชื่อถูกต้องอยู่แล้ว แต่มันจะยังไม่ทำงาน 493 | 494 | การเปิดใช้ hook script ทำได้โดยการใส่ไฟล์ไว้ใน `hooks` folder ของ repository ของเรา ตั้งชื่อให้ตรงตามตัวอย่าง ส่วนมันจะถูกเรียกตอนไหนเดี๋ยวเราค่อยมาว่ากันต่อ 495 | 496 | ### Client-Side Hooks ### 497 | 498 | เราสามารถเล่นกับ client-side hook ได้มากทีเดียว ในบทนี้เราจะแยกอธิบายดังนี้ committing-workflow hooks, e-mail-workflow scripts และ ส่วนที่เหลือ 499 | 500 | #### Committing-Workflow Hooks #### 501 | 502 | มี hook อยู่สี่ตัวสำหรับส่วนของ committing process โดย `pre-commit` จะถูกเรียกเป็นตัวแรก โดยมันจะถูกเรียกก่อนที่เราจะใส่ commit message ส่วนมากจะใช้ตรวจสอบว่าพร้อมจะ commit หรือยัง บางทีเราอาจจะลืมบางอย่าง หรือให้แน่ใจว่าเรา run test แล้ว หรืออะไรก็ตามที่เราต้องการตรวจสอบ code ของเราก่อน ถ้า hook ของเราจบด้วย non-zero จะเป็นการยกเลิกการ commit อย่างไรก็ตามถ้าเราต้องการจะ commit โดยไม่ต้องตรวจสอบ ก็ให้สั่ง `git commit --no-verify` เราสามารถตรวจสอบ code style ว่าถูกต้องหรือไม่ สามารถจัดการ whitespace ก่อน commit (ลองดูในตัวอย่าง) หรืออาจจะใช้ตรวจสอบความถูกต้องของคำอธิบายประกอบ methods ที่สร้างขึ้นมาใหม่ก็ได้ 503 | 504 | อันต่อมาคือ `prepare-commit-msg` จะถูกเรียกให้ทำงานก่อน commit message editor จะขึ้นมา แต่ก็จะทำงานหลังจากมีการสร้าง default message แล้ว มันช่วยให้เราเข้าไปแก้ไข default message ได้ ก่อนที่เราจะแก้ไข โดยจะมี option ให้เราเล่นมากกว่า hook แรก เช่น path ที่อยู่ของไฟล์ที่ใส่ commit message, ชนิดของการ commit และ commit SHA-1 ถ้าเป็น amended commit เป็นต้น โดยทั่วไป hook นี้จะไม่ค่อยมีประโยชน์สำหรับการ commit โดยทั่วไป แต่มันจะดีมากกๆ ถ้าใช้กับ commit ที่มี default message ถูกสร้างขึ้นมาอัตโนมัติ เช่น กรณีที่เป็น templat commit message หรือ merge commits หรือ squashed commits และ amended commit เป็นต้น เราอาจจะใช้มันร่วมกับ commit template เพื่อใส่ข้อมูลเพิ่มเติมก็ได้ 505 | 506 | `commit-msg` ้hook จะถูกเรียกหลังจากแก้ไข commit message แล้ว โดยจะมี parameter แค่ตัวเดียวคือ path ไปยัง temporary file ที่ใส่ commit message ไว้ ถ้า script นี้จบด้วย non-zero โปรแกรม Git จะยกเลิกกระบวนการ commit ทันที ดังนั้นเราจึงมักใช้มันในการตรวจสอบ commit message ก่อนที่จะอนุญาติให้ดำเนินการ commit ต่อไป ในส่วนสุดท้ายของบทนี้จะมีตัวอย่างการใช้งาน hook นี้ให้ดูอีกที 507 | 508 | หลังจากที่กระบวนการ commit เสร็จเรียบร้อยแล้ว `post-commit` จะถูกเรียกขึ้นมา script ตัวนี้จะไม่ได้รับ parameter ใดๆ แต่ถ้าเราอยากดู commit สุดท้ายก็แค่สั่ง `git log -1 HEAD` ส่วนมากแล้ว script นี้จะเอาไว้แจ้งเตือนเป็นหลัก 509 | 510 | สำหรับ script committing-workflow ในผั่ง client เราสามารถใช้มันในการควบคุมให้ code หรืออะไรก็ตามให้เป็นไปตาม policy ที่กำหนดไว้ได้ อย่างไรก็ตามควรจำไว้ว่า script ที่สร้างขึ้นจะไม่ถูกนำไปด้วย ในกรณีของการ clone ดังนั้นถ้าต้องการควบคุมให้เป็นไปตาม policy เราควรจะกำหนดไว้ที่ผั่ง server ว่าให้บอกปัดทุก push ของ commits ที่ไม่เป็นไปตาม policy ที่วางเอาไว้ และยกให้เป็นเรื่องของ developer ว่าจะกำหนด script ตัวนี้ไว้ที่ผั่ง client หรือไม่ จะเห็นว่า script ตัวนี้จะเอาไว้ช่วยงานกับผู้พัฒนาเป็นหลัก ดังนั้นการดูแติดตั้งจึงควรเป็นเรื่องของตัวผู้พัฒนา และเขาก็มีสิทธิ์ที่จะปรับแต่งแก้ไขด้วยตัวเองตามความต้องการ 511 | 512 | #### E-mail Workflow Hooks #### 513 | 514 | Git เปิดให้เรา hook ได้สามที่สำหรับการทำ e-mail-base workflow โดย hook ของเราจะถูกเรียกโดยคำสั่ง `git am` ดังนั้นถ้าใครไม่ได้ใช้คำสั่งนี้ใน workflow ก็สามารถอ่านข้ามส่วนนี้ไปได้เลย แต่ใครที่ยังส่ง patches ด้วย e-mail และใช้คำสั่ง `git fotmat-patch` ขอให้ลองอ่านดู บางทีอาจจะช่วยคุณได้มากทีเดียว 515 | 516 | hook ตัวแรกคือ `applypatch-msg` จะรับหนึ่ง argument คือชื่อของ temporary file ที่เก็บ commit message ในกรณีนี้ Git จะยกเลิก patch นี้ถ้าเราจบ script ด้วย non-zero เราสามารถใช้ hook ตัวนี้สำหรับตรวจสอบ commit message ว่าถูกต้องตามรูปแบบที่กำหนดหรือไม่ หรืออาจจะใช้ในการปรับแต่งข้อความก่อนก็ได้ 517 | 518 | hook ตัวต่อไปจะทำงานเมื่อ apply patches ผ่าน `git am` หรือก็คือ `pre-applypatch` โดยไม่รับ argument ใดๆ และทำงานหลังจากที่ patch ถูก apply ไปแล้ว ดังนั้นเราจะใช้มันในการตรวจสอบ snapshot ก่อนที่จะทำการ commit เช่น เราสามารถสั่ง run test ก่อนได้ ถ้าไม่ผ่านก็ให้จบ script ด้วย non-zero จะทำให้ patch นั้นไม่ถูก commit เข้าไป 519 | 520 | hook ตัวสุดท้ายจะทำงานระหว่างที่คำสั่ง `git am` หรือ `post-applypatch` กำลังทำงานอยู่ เราสามารถใช้มันเพื่อแจ้งให้คนในกลุ่ม หรือเจ้าของ patch ทราบว่าเรากำลัง pull สิ่งที่คุณทำอยู่ เป็นต้น เราไม่สามารถใช้ hook ตัวนี้ในการหยุด patching process ได้ 521 | 522 | #### Client Hooks อื่นๆ #### 523 | 524 | ไฟล์ `pre-rebase` ้เป็น hook ที่ทำงานก่อนที่ rebase จะทำงาน และเราสามารถหยุด process ได้โดยการจบ script ด้วย non-zero เราสามารถใช้ hook ตัวนี้เพื่อไม่ให้เกิดการ rebase commit ใดๆ ที่ถูก push ไปแล้ว ดูตัวอย่างวิธีการเขียนได้ในไฟล์ตัวอย่าง `pre-rebase` ใน hooks directory อย่างไรก็ตามตัว script จะใช้คำว่า "next" เป็นชื่อของ branch ที่เราจะใช้ในการ publish ถ้าต้องการใช้ branch ชื่ออื่น ก็ให้เข้าไปเปลี่ยนเป็นชื่อ branch ที่เราคิดว่าจะใช้ในการ publish 525 | 526 | หลังจากที่คำสั่ง `git checkout` ทำงานเสร็จแล้ว hook ที่ชื่อ `post-checkout` จะเริ่มทำงาน เราสามารถใช้ hook นี้ในการตั้งค่าให้ working directory ของเราได้ นั่นหมายความว่าเราสามารถจัดการโยกย้ายไฟล์ binary ขนาดใหญ่ๆ ที่เราไม่ต้องการให้อยู่ใน code เข้ามาได้ หรือจะเป็นพวก auto-generating document หรืออะไรได้ตามแต่จะสะดวก 527 | 528 | สุดท้ายคือ `post-merge` hook ซึ่งจะทำงานเมื่อ `merge` เสร็จ เราสามารถใช้มันเพื่อดึงข้อมูลซึ่ง Git ไม่ได้ track เอาไว้ กลับมาได้ เช่น permission data เป็นต้น นอกจากนั้นเรายังสามารถใช้ hook ตัวนี้ในการ copy สิ่งที่อยู่นอก working directory เข้ามาแบบเดียวกับ `post-checkout` ได้ด้วย 529 | 530 | ### Server-Side Hooks ### 531 | 532 | นอกจากการใช้ client-side แล้ว เรายังสามารถใช้ server-side ้hook เพื่่อช่วยให้ system admin สามารถควบคุม policy ใน project ได้ด้วย script ที่ใช้จะทำงานก่อนและหลังการ push มาที่ server และสำหรับ pre hook เราสามารถสั่งหยุดการ push ได้ด้วยการจบ script ด้วย non-zero และทำการส่ง error message กลับไปให้กับ client 533 | 534 | #### pre-receive and post-receive #### 535 | 536 | script ตัวแรกที่ถูกสั่งให้ทำงานหลังจากที่ server ได้รับ push จาก client คือ `pre-receive` มันจะรับ list ของ references ที่ส่งผ่าน push เข้ามาจาก stdin ถ้า script จบด้วย non-zero ก็จะไม่มีอะไรผ่านเข้ามาใน repository เราสามารถใช้ hook ตัวนี้เพื่อให้แน่ใจว่าไม่มี update ตัวไหนที่เป็น non-fast-forward หรือ ใช้มันสำหรับตรวจสอบว่าผู้ใช้ทำอะไร เช่น create, delete หรือ push access หรือสามารถดูไฟล์ทุกตัวที่ถูกแก้ไขผ่านทาง push 537 | 538 | อีกตัวคือ `post-receive` ้hook ซึ่งจะถูกสั่งให้ทำงานหลังจากที่ push ทำงานเสร็จแล้ว เราสามาถใช้มันในการ update service อื่นๆ หรือส่งข้อความให้กับผู้ใช้ก็ได้ ตัว hook จะได้รับข้อมูลเช่นเดียวกับ `pre-recevie` ้hook ตัวอย่างการใช้งานเช่น ส่ง e-mail ของการแก้ไข หรือ สั่งให้ continuous integration server ทำงาน หรือสั่ง update ticket-tracking system หรือเราสามารถดู commit messages เพื่อจัดการ เปิด ปิด หรือแก้ไข ticket ได้ ้script นี้ไม่สามารถหยุดกระบวนการ push ได้ แต่ผู้ใช้จะไม่สามารถ disconnect ได้ จนกว่ากระบวนการจะสิ้นสุด ดังนั้นถ้าจะทำอะไรก็ต้องคิดด้วยว่าจะใช้เวลานานหรือเปล่า 539 | 540 | #### update #### 541 | 542 | update script จะคล้ายๆ กับ `pre-receive` script ยกเว้นแต่ว่ามันทำงานแค่ครั้งเดียวในแต่ละ branch เมื่อคน push สั่ง update ดังนั้นถ้าคน push พยายามจะ push ทีละหลายๆ branch เจ้า `pre-receive` จะทำงานแค่ครั้งเดียว ในขณะที่ update จะทำงาน branch ละหนึ่งครั้ง และแทนที่จะอ่านข้อมูลจาก stdin ตัว script จะรับสาม argument คือ ชื่อของ branch, SHA-1 ที่อ้างอืงมาก่อนที่จะ push และสุดท้าย SHA-1 ที่ user ใช้ในการ push ทั้งนี้ถ้า update script ถูกจบด้วย non-zero จะมีเฉพาะ reference นั้นๆ ที่ถูก reject ส่วน reference อื่นๆ จะยังคง update ต่อไป 543 | 544 | ## An Example Git-Enforced Policy ## 545 | 546 | ในส่วนนี้ เราจะได้ลองใช้งานกันจริงๆ โดยอาศัยสิ่งที่เราเรียนผ่านมา เพื่อให้เป็นไปตาม workflow ที่กำหนด อันดับแรกคือกำหนดรูปแบบของ commit message บังคับให้เฉพาะ fast-forward-only เท่านั้นที่จะ push และ อนุญาติให้เฉพาะผู้ใช้บางคนเท่านั้นที่จะแก้ไขในบาง subdirectory ได้ เราจะได้ลองสร้าง client script เพื่อช่วยให้นักพัฒนารู้ล่วงหน้าว่าสิ่งที่กำลังจะ push จะถูกตีกลับมา และเราจะได้เขียน script ฝั่ง server เพื่อบังคับใช้ policy ที่เรากำหนดขึ้นมา 547 | 548 | ในตัวอย่างจะใช้ภาษา Ruby เพราะผู้เขียนชอบภาษานี้และคิดว่าเป็นภาษาที่อ่านง่ายถึงแม้ว่าจะไม่เคยเขียนภาษานี้มาก่อนก็ตาม แต่ถ้าดูตัวอย่างจากใน hooks ที่ Git มีมาให้ ในนั้นจะเขียนด้วยภาษา Perl หรือ Basic shell script ซึ่งเราสามารถดูตามได้ 549 | 550 | ### Server-Side Hook ### 551 | 552 | การทำ server-side hook จะทำที่ไฟล์ใน hooks directory โดยไฟล์ update จะถูกเรียกทำงาน branch ละหนึ่งครั้งในกรณีที่ผู้ใช้ push ขึ้นมาทีละหลาย branch โดยจะรับ parameter เป็นค่าอ้างอิงไปยัง revision เก่าของ branch นั้น และรับ revision ใหม่ที่ถูก push ขึ้นมา เรายังสามารถเข้าถึงข้อมูลผู้ใช้ขณะที่ผู้ใช้ push ได้ด้วยถ้าผู้ใช้ push ด้วย SSH แล้วถ้าเราอนุญาติให้ทุกคนเชื่อมต่อเข้ามาด้วย single user (like "git") ด้วย public-key authentication เราจะสามารถรู้ได้ด้วยว่าผู้ใช้คนไหนเชิ่มต่อมาด้วย public key และใช้ในการกำหนดลักษณะการรับ push ของ user คนนั้นๆ ในที่นี้เราสมติว่าชื่อผู้ใช้อยู่ใน `$USER` ดังนั้นเราจะเริ่มต้น script ด้วยการรวบรวมข้อมูลที่เราต้องการขึ้นมาก่อน 553 | 554 | #!/usr/bin/env ruby 555 | 556 | $refname = ARGV[0] 557 | $oldrev = ARGV[1] 558 | $newrev = ARGV[2] 559 | $user = ENV['USER'] 560 | 561 | puts "Enforcing Policies... \n(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})" 562 | 563 | ในตัวอย่างจะใช้ Global variable เพื่อให้เข้าใจง่าย ถ้าในการทำงานจริง ก็ลองแก้กันดูนะครับ 564 | 565 | #### Enforcing a Specific Commit-Message Format #### 566 | 567 | ด่านแรกของเราคือการบังคับให้ทุก commit message จะต้องใช้รูปแบบตามที่เรากำหนด สมติว่าทุก message จะต้องมีข้อความประมาณว่า "ref: 1234" เพราะเราต้องการให้ทุก commit จะต้องเชื่อมโยงไปยังระบบออก ticket สิ่งที่เราต้องทำคือการอ่านทุก commit ที่ถูกส่งเข้ามาแล้วดูว่าข้อความที่อยู่ใน commit message นั้นเป็นไปตามที่กำหนดหรือไม่ ถ้าไม่ใช่ก็ทำการจบ script ด้วย non-zero เพื่อยกเลิกการ push นั้น 568 | 569 | เราต้องดึงเอารายการค่า SHA-1 ออกมาจาก commit ที่ถูก push ขึ้นมา โดยใช้ตัวแปร `$newrev` และ `$oldrev` และคำสั่ง `git rev-list` สิ่งที่ได้จากคำสั่งนี้จะเหมือน `git log` แต่จะมีเฉพาะค่า SHA-1 เท่านั้น ไม่มีข้อมูลอื่น ดังนั้น ถ้าต้องการค่า SHA ทั้งหมดเราก็จะบอกลงไปว่าเราต้องการตั้งแต่ตัวอ้างอิงไหนถึงตัวไหน ดังนี้ 570 | 571 | $ git rev-list 538c33..d14fc7 572 | d14fc7c847ab946ec39590d87783c69b031bdfb7 573 | 9f585da4401b0a3999e84113824d15245c13f0be 574 | 234071a1be950e2a8d078e6141f5cd20c1e61ad3 575 | dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a 576 | 17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475 577 | 578 | เราจะใช้รายการ SHA-1 นี้ ไปดึงเอา message ออกมาทีละอัน จากนั้นก็ทดสอบความถูกต้องด้วย regular expression เพื่อดูว่าตรงกับรูปแบบที่เราวางไว้หรือเปล่า 579 | 580 | ตอนนี้เราอาจจะสงสัยว่าจะไปเอา commit message มาได้อย่างไร เราสามารถทำได้โดยใช้คำสั่ง `git cat-file` ไว้จะอธิบายอย่างละเอียดในบทที่ 9 อีกที สำหรับตอนนี้เราจะใช้คำสั่งนี้แบบนี้ครับ 581 | 582 | $ git cat-file commit ca82a6 583 | tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf 584 | parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 585 | author Scott Chacon 1205815931 -0700 586 | committer Scott Chacon 1240030591 -0700 587 | 588 | changed the version number 589 | 590 | เทคนิคการดึง commit message ออกจากจากข้อความทั้งหมดที่ได้จาก SHA-1 จะทำการการวิ่งไปหาบรรทัดว่างจากนั้นถือว่าทุกอย่างที่อยู่ใต้บรรทัดนั้นเป็น commit message เราสามารถใช้คำสั่ง `sed` ของ UNIX มาช่วยดึงได้ดังนี้ 591 | 592 | $ git cat-file commit ca82a6 | sed '1,/^$/d' 593 | changed the version number 594 | 595 | เราจะใช้ท่าข้างต้นในการดึงเอา commit message ออกมาตรวจที่ละตัว ถ้า commit message ที่เค้าพยายาม push ทั้งหมดมีตัวใดตัวหนึ่งไม่ตรงกับเงื่อนไขที่วางเอาไว้ เราก็จะจบ script เพิ่อยกเลิกการ push โดยการจบไฟล์ด้วย non-zero ดังนี้ 596 | 597 | $regex = /\[ref: (\d+)\]/ 598 | 599 | # enforced custom commit message format 600 | def check_message_format 601 | missed_revs = `git rev-list #{$oldrev}..#{$newrev}`.split("\n") 602 | missed_revs.each do |rev| 603 | message = `git cat-file commit #{rev} | sed '1,/^$/d'` 604 | if !$regex.match(message) 605 | puts "[POLICY] Your message is not formatted correctly" 606 | exit 1 607 | end 608 | end 609 | end 610 | check_message_format 611 | 612 | เอา code ข้างต้นใส่ไว้ใน `update` script จากนั้น commit message ใดที่ไม่เข้ากับเงื่อนไขจะถูกยกเลิกการ push ทุกครั้ง 613 | 614 | #### Enforcing a User-Based ACL System #### 615 | 616 | สมติว่าเราต้องการกำหนด access control list (ACL) เพื่อระบุว่าผู้ใช้คนไหนสามารถแก้ไขไฟล์ใน part ไหนได้บ้าง ผู้ใช้บางคนสามารถเข้าถึงได้ทั้งหมด และมีบางคนที่ทำได้เฉพาะการ push ไปยัง directory ที่กำหนด หรือไฟล์ที่กำหนดเท่านั้น การที่จะตั้งเงื่อนไขแบบนี้ได้เราจะต้องกำหนดไว้ในไฟล์ชื่อ `acl` ซึ่งอยู่ใน git repository บน server จากนั้นเราต้องแก้ไขไฟล์ `update` เพื่อให้มันทำตามกฏที่เราตั้งขึ้น โดยดูว่าไฟล์แต่ะลไฟล์ที่ commit ไว้สามารถ push ได้ตามสิทธิ์ของ user คนนั้นๆ หรือไม่ 617 | 618 | สิ่งแรกที่เราต้องทำการการแก้ไฟล์ ACL รูปแบบการเขียนจะคล้ายกับ ACL ของ CVS โดยแต่ละบรรทัดจะขึ้นต้นด้วย `avail` หรือ `unavail` จากนั้นตามด้วยชื่อผู้ใช้คั่นด้วย comma-delimited และใส่ตัวสุดท้ายเป็น path ที่อนุญาติหรือไม่อนุญาติให้แก้ไข (ถ้าเป็นช่องว่าง หมายถึง open access) จานั้นปิดหัวท้ายของผู้ใช้ด้วย `|` ยกเว้นบรรทัดที่เราต้องการให้ open access 619 | 620 | ในกรณีของเรา จะมีผู้ใช้บางคนเป็น administrator บางคนเป็นคนเขียนเอกสารจะเข้าถึงได้เฉพาะ directory `doc` และมีหนึ่งคนที่เข้าถึง directory `lib` และ `tests` ซึ่งทำให้ ACL ของเราเป็นดังนี้ 621 | 622 | avail|nickh,pjhyett,defunkt,tpw 623 | avail|usinclair,cdickens,ebronte|doc 624 | avail|schacon|lib 625 | avail|schacon|tests 626 | 627 | เราเริ่มต้นด้วยการอ่านข้อมูลเข้ามาเป็นรูปแบบที่เราใช้งานได้ก่อน ในกรณีนี้เราจะมีแค่ `avail` เท่านั้น ในตัวอย่างต่อไปนี้เราจะรวบรวมเป็น associative array โดยให้ key เป็นชื่อของ user และมี value เป็น array ของ path ที่ผู้ใช้คนนั้นสามารถแก้ไขได้ 628 | 629 | def get_acl_access_data(acl_file) 630 | # read in ACL data 631 | acl_file = File.read(acl_file).split("\n").reject { |line| line == '' } 632 | access = {} 633 | acl_file.each do |line| 634 | avail, users, path = line.split('|') 635 | next unless avail == 'avail' 636 | users.split(',').each do |user| 637 | access[user] ||= [] 638 | access[user] << path 639 | end 640 | end 641 | access 642 | end 643 | 644 | ผลลัพท์ที่ได้จากการอ่านไฟล์ ACL ด้วยคำสั่ง `get_acl_access_data` จะได้เป็นข้อมูลรูปแบบดังนี้ 645 | 646 | {"defunkt"=>[nil], 647 | "tpw"=>[nil], 648 | "nickh"=>[nil], 649 | "pjhyett"=>[nil], 650 | "schacon"=>["lib", "tests"], 651 | "cdickens"=>["doc"], 652 | "usinclair"=>["doc"], 653 | "ebronte"=>["doc"]} 654 | 655 | เอาล่ะ ตอนนี้เราได้สิทธิในการเข้าถึงของผู้ใช้แต่ละคนแล้ว ต่อไปเราต้องดูว่า paths ของ commit ที่ push เข้ามานั้น เป็นไปตามสิทธิของผู้ใช้ ตามเงื่อนไขที่เราวางเอาไว้หรือเปล่า 656 | 657 | เราสามารถทำได้ง่ายๆ โดยดูไฟล์ที่ถูกแก้ไขใน commit แต่ละตัวด้วย option `--name-only` ในคำสั่ง `git log` (ซึ่งได้พูดไว้คร่าวๆ ในบทที่ 2) 658 | 659 | $ git log -1 --name-only --pretty=format:'' 9f585d 660 | 661 | README 662 | lib/test.rb 663 | 664 | ถ้าเราใช้ ACL structure ที่ได้รับจาก `get_acl_access_data` แล้วเอามาเทียบกับรายชื่อของไฟล์ใน commit นั้นๆ เราจะสามารถบอกได้ว่าผู้ใช้แก้ไขไฟล์ตามสิทธิ์ของตนเองหรือไม่ 665 | 666 | # only allows certain users to modify certain subdirectories in a project 667 | def check_directory_perms 668 | access = get_acl_access_data('acl') 669 | 670 | # see if anyone is trying to push something they can't 671 | new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n") 672 | new_commits.each do |rev| 673 | files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n") 674 | files_modified.each do |path| 675 | next if path.size == 0 676 | has_file_access = false 677 | access[$user].each do |access_path| 678 | if !access_path # user has access to everything 679 | || (path.index(access_path) == 0) # access to this path 680 | has_file_access = true 681 | end 682 | end 683 | if !has_file_access 684 | puts "[POLICY] You do not have access to push to #{path}" 685 | exit 1 686 | end 687 | end 688 | end 689 | end 690 | 691 | check_directory_perms 692 | 693 | Code ส่วนใหญ่สามารถอ่านตามได้ง่าย เราสามารถแสดงรายการ commit ที่ถูกส่งมาให้ server ด้วยคำสั่ง `git rev-list` จากนั้นก็ไล่ดูไปที่ละอัน เราจะเห็นว่าไฟล์ไหนบ้างที่ถูกแก้ไข จากนั้นก็ตรวจสอบให้แน่ใจว่าใครเป็นคน push ขึ้นมา แล้วดูว่าเค้ามีสิทธิ์หรือไม่ ใครที่เป็นนักพัฒฯา Ruิัby อาจจะงงๆ กับคำสั่ง `path.index(access_path) == 0` ซึ่งจะ return true ถ้า path เริ่มต้นด้วย `access_path` นี่จะทำให้แน่ใจว่า `access_path` ไม่ได้มีแค่ตัวเดียวที่ตรง แต่ต้องตรงทุก path 694 | 695 | ตอนนี้ผู้ใช้ก็ไม่สามารถ push commit ที่ไม่ตรงกับรูปแบบที่เรากำหนดไว้ได้ และไม่สามารถแก้ไขที่อยุ่นอกสิทธิ์ที่เราออกแบบไว้ได้ด้วย 696 | 697 | #### Enforcing Fast-Forward-Only Pushes #### 698 | 699 | ตอนนี้ก็เหลือแค่บังคับให้ fast-forward-only เท่านั้นที่ push เข้ามาได้ สำหรับ Git ตั้งแต่ version 1.6 ขึ้นไป เราจะสามารถใช้ `recive.denyDeletes` และ `receive.denyNonFastForwards` ใน setting ได้ แต่ในตัวอย่างนี้จะเอาไว้ใช้กับ Git version เก่าๆ ได้ หรือไม่เราก็สามารถดัดแปลงไปใช้อย่างอื่นได้อีกในอนาคต 700 | 701 | เทคนิคในการตรวจสอบคือให้เราหาว่ามีซัก commit ที่เชื่อมไปยัง revision เก่าได้หรือไม่ ถ้าไม่มีก็แสดงว่าเป็น fast-forward push ถ้าไ่ม่ใช้ก็จะยกเลิก 702 | 703 | # enforces fast-forward only pushes 704 | def check_fast_forward 705 | missed_refs = `git rev-list #{$newrev}..#{$oldrev}` 706 | missed_ref_count = missed_refs.split("\n").size 707 | if missed_ref_count > 0 708 | puts "[POLICY] Cannot push a non fast-forward reference" 709 | exit 1 710 | end 711 | end 712 | 713 | check_fast_forward 714 | 715 | ทีนี้ทุกอย่างก็ครบแล้ว ถ้าเราทดลองสั่ง `chmod u+x .git/hooks/update` ซึ่งเป็น file ที่ code ทั้งหมดของเราใส่เอาไว้ จากนั้นก็ทดลอง push ืcode ที่เป็น non-fast-forwarded เข้าไป เราจะได้ค่าดังนี้กลับมา 716 | 717 | $ git push -f origin master 718 | Counting objects: 5, done. 719 | Compressing objects: 100% (3/3), done. 720 | Writing objects: 100% (3/3), 323 bytes, done. 721 | Total 3 (delta 1), reused 0 (delta 0) 722 | Unpacking objects: 100% (3/3), done. 723 | Enforcing Policies... 724 | (refs/heads/master) (8338c5) (c5b616) 725 | [POLICY] Cannot push a non-fast-forward reference 726 | error: hooks/update exited with error code 1 727 | error: hook declined to update refs/heads/master 728 | To git@gitserver:project.git 729 | ! [remote rejected] master -> master (hook declined) 730 | error: failed to push some refs to 'git@gitserver:project.git' 731 | 732 | เรามาลองดูค่าที่ส่งกลับมาโดยเริ่มจะวินาทีที่ hook เริ่มทำงาน 733 | 734 | Enforcing Policies... 735 | (refs/heads/master) (fb8c72) (c56860) 736 | 737 | จะเห็นว่าเราทำการแสดงค่าออกมาทาง stdout ตามที่เราสั่งไว้ ดังนั้นต้องจำไว้ว่า อะไรก็ตามที่เราสั่งเขียนออกทาง stdout จะถูกส่งต่อไปยังเครื่อง client 738 | 739 | ต่อไปคือ error message 740 | 741 | [POLICY] Cannot push a non fast-forward reference 742 | error: hooks/update exited with error code 1 743 | error: hook declined to update refs/heads/master 744 | 745 | บรรทัดแรกเราเป็นคนเขียนขึ้นมาเอง แต่สองบรรทัดต่อมา Git เป็นคนเขียน ว่า update script ได้ถูกยกเลิกพราะจบด้วย non-zero และ push ของผู้ใช้ก็ถูกยกเลิกไปด้วย โดยแสดงเป็น error ดังนี้ 746 | 747 | To git@gitserver:project.git 748 | ! [remote rejected] master -> master (hook declined) 749 | error: failed to push some refs to 'git@gitserver:project.git' 750 | 751 | เราจะเห็นจาก rejected message ว่าแต่ละ reference ที่ส่งเข้าไปใน hook นั้นถูกยกเลิกเพราะเหตุผลอะไร 752 | 753 | นอกจากนี้ ถ้า ref market ที่เรากำหนดให้ใส่ใน commit ดันไม่อยู๋ใน commit เราจะได้รับ message ดังนี้ 754 | 755 | [POLICY] Your message is not formatted correctly 756 | 757 | หรือถ้าผู้ใช้พยายามจะเข้าไปแก้ไขไฟล์ที่ตัวเองไม่มีสิทธิ์แก้ แล้วสั่ง push ขึ้นมาก็จะได้รับ error ดังนี้ 758 | 759 | [POLICY] You do not have access to push to lib/test.rb 760 | 761 | ครบแล้วครับ ตราบเท่าที่ `update` ของเรายังทำงานอยู่ repository ของเราจะไม่เคย reword และไม่เคยรับ commit ที่ไม่ถูกต้องตาม pattart และผู้ใช้ก็จะปลอดภัย 762 | 763 | ### Client-Side Hooks ### 764 | 765 | สิ่งที่เลวร้ายมากสำหรับนักพัฒนาใน workflow ของเราคือ เวลาที่เค้า commit เข้ามาแล้วโดน reject หลังจากนั้นต้องใช้เวลานานมากเพื่อแก้ไขประวัติของการ commit ให้ถูกต้อง 766 | 767 | วิธีที่จะป้องกันความเลวร้ายอันนี้คือ การสร้าง client-side ้hook ให้กับนักพัฒนา ซึ่งจะจำลองตัวเองเหมือน server แล้วเตือนผู้ใช้ว่า commit ที่เขียนเข้ามาถูกต้องตามเงื่อนไขหรือไม่ ตัวนี้จะช่วยบอกนักพัฒนาตัวแต่ตอนที่ commit ว่ามีอะไรผิดพลาดบ้างแทนที่จะรอให้ผ่านไปหลาย commit ซึ่งตอนนั้นก็แก้ยากแล้ว แต่อย่างหนึ่งที่ทำไม่ได้คือเราไม่สามารถผูก client-side ้hook ไว้กับ project ได้ เราต้องให้ผู้ใช้ download ไปเองหรือไม่ก็ทำ repository สำหรับเก็บแยกไว้ต่างหาก 768 | 769 | เราจะเริ่มต้นด้วยการตรวจสอบ commit message ทุกอันก่อนที่จะถูก push ขึ้นไปบน server โดยเขียน script ไว้ใน hook `commit-msg` จากนั้นอ่านทีละ message แล้วเปรียบเทียบกับ pattern ด้วยวิธีการเดียวกับ server ถ้าเกิดว่าไม่ตรงก็ให้ Git ทำการยกเลิก commit นั้นเลย 770 | 771 | #!/usr/bin/env ruby 772 | message_file = ARGV[0] 773 | message = File.read(message_file) 774 | 775 | $regex = /\[ref: (\d+)\]/ 776 | 777 | if !$regex.match(message) 778 | puts "[POLICY] Your message is not formatted correctly" 779 | exit 1 780 | end 781 | 782 | ถ้าเราเอา script ไว้ถูกที่ (ใน `.git/hooks/commit-msg`) และทำให้มัน excutable แล้ว เมื่อเรา commit message ที่ไม่ตรงตามรูปแบบที่วางเอาไว้ เราจะได้รับข้อความดังนี้ 783 | 784 | $ git commit -am 'test' 785 | [POLICY] Your message is not formatted correctly 786 | 787 | แต่ถ้า commit message ของเราเขียนมาอย่างถูกต้อง Git ก็จะให้ commit นั้นผ่านเข้าไปได้ และแสดงข้อความดังนี้ 788 | 789 | $ git commit -am 'test [ref: 132]' 790 | [master e05c914] test [ref: 132] 791 | 1 files changed, 1 insertions(+), 0 deletions(-) 792 | 793 | ต่อไปคือเราต้องการแน่ใจว่าไฟล์ที่ถูกแก้ไขนั้น ผู้แก้มีสิทธิในการแก้ไขตามที่ ACL กำหนดไว้ ถ้าโปรแกรมของเรามีไฟล์ ACL อยู่ใน `.git` เรียบร้อยแล้ว เราก็เขียนแก้ `pre-commit` ได้เลย 794 | 795 | #!/usr/bin/env ruby 796 | 797 | $user = ENV['USER'] 798 | 799 | # [ insert acl_access_data method from above ] 800 | 801 | # only allows certain users to modify certain subdirectories in a project 802 | def check_directory_perms 803 | access = get_acl_access_data('.git/acl') 804 | 805 | files_modified = `git diff-index --cached --name-only HEAD`.split("\n") 806 | files_modified.each do |path| 807 | next if path.size == 0 808 | has_file_access = false 809 | access[$user].each do |access_path| 810 | if !access_path || (path.index(access_path) == 0) 811 | has_file_access = true 812 | end 813 | if !has_file_access 814 | puts "[POLICY] You do not have access to push to #{path}" 815 | exit 1 816 | end 817 | end 818 | end 819 | 820 | check_directory_perms 821 | 822 | script ชุดนี้จะหน้าตาเหมือนของ server-side script แต่มีสองส่วนที่แตกต่างกัน อันแรกคือไฟล์ ACL จะอยู่คนละที่กัน เพราะ script จะทำงานที่ working directory ไม่ได้ดูที่ Git directory ดังนั้นเราต้องเปลี่ยนที่อยู่ของไฟล์ ACL จาก 823 | 824 | access = get_acl_access_data('acl') 825 | 826 | ไปเป็น 827 | 828 | access = get_acl_access_data('.git/acl') 829 | 830 | ความแตกต่างอย่างที่สองคือ วิธีการดึง list ของไฟล์ที่ถูกแก้ไข ในฝั่ง server เราจะใช้วิธีดู log ของการ commit แต่ถในกรณีของฝั่ง client ตัว commit จะยังไม่ได้ถูกบันทึกลงไป ดังนั้นเราจะต้องดึง list ของไฟล์จาก ที่ผู้ใช้ใส่เข้ามาโดยแทนที่บรรทัดต่อไปนี้ 831 | 832 | files_modified = `git log -1 --name-only --pretty=format:'' #{ref}` 833 | 834 | ด้วยบรรทัดนี้ 835 | 836 | files_modified = `git diff-index --cached --name-only HEAD` 837 | 838 | จะมีส่วนที่แตกต่างกันแค่สองส่วนนี้เท่านั้น นอกนั้น client กับ server จะเหมือนกัน มีอีกเล็กน้อยที่ต้องระวังคือผมคาดว่าเราจะใช้ user ที่ผั่ง client กับ server เป็นคนเดียวกัน แต่ถ้าไม่ใช่เราอาจจะต้องกำหนดค่า `$user` ขึ้นมาเอง 839 | 840 | อย่างสุดท้ายที่เราต้องดูคือ เราต้องตรวจสอบว่านักพัฒนาไม่ได้ push non-fast-forward เข้ามา ซึ่งเราไม่สามารถใช้ท่าธรรมดาได้ วิธีที่จะรู้ว่า reference นั้นไม่ใช่ fast-forward เราจะต้อง rebase commit ที่ถูก pus้h ขึ้นไแแล้ว หรือไม่ก็ทดลอง push local branch อื่นๆ ขึ้นไปที่ remote branch เดียวกัน 841 | 842 | เพราะเราต้องพึ่ง server ในการบอกว่า push นั้นเป็น non-fast-forward หรือไม่ โดยให้ตัว hook ที่ฝั่ง server ทำหน้าที่ตรวจสอบให้ ่เดี๋ยวเราจะทดลองสั่ง rebase commit ที่เราเคย push ขึ้นไปแล้ว เพื่อทดสอบ hook ของเรา 843 | 844 | Because the server will tell you that you can't push a non-fast-forward anyway, and the hook prevents forced pushes, the only accidental thing you can try to catch is rebasing commits that have already been pushed. 845 | 846 | ต่อไปนี้เป็นตัวอย่างของการทำ pre-rebase script สำหรับการตรวจสอบกรณีนี้ ตัวอย่างจะดึงรายการ commit ที่เราตอ้งการแก้ไข และตรวจสอบว่าแต่ละตัวมีการ push ขึ้น remote ไปบ้างแล้วหรือไม่ ถ้ามีตัวใดตัวหนึ่งที่มีอยู่แล้วเราจะทำการยกเลิกการ rebase ทันที 847 | 848 | #!/usr/bin/env ruby 849 | 850 | base_branch = ARGV[0] 851 | if ARGV[1] 852 | topic_branch = ARGV[1] 853 | else 854 | topic_branch = "HEAD" 855 | end 856 | 857 | target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("\n") 858 | remote_refs = `git branch -r`.split("\n").map { |r| r.strip } 859 | 860 | target_shas.each do |sha| 861 | remote_refs.each do |remote_ref| 862 | shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}` 863 | if shas_pushed.split("\n").include?(sha) 864 | puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}" 865 | exit 1 866 | end 867 | end 868 | end 869 | 870 | ใน script จะมีคำสั่งที่ไม่มีใน Revision Selection ้ของบทที่ 6 คือเราสามารถดึง list ของ commit ที่ถูก pust ขึ้นไปแล้วได้โดยการสั่ง 871 | 872 | git rev-list ^#{sha}^@ refs/remotes/#{remote_ref} 873 | 874 | ค่า `SHA^@` เป็นตัวแทน parents ทุกตัวของแต่ละ commit เราพยายามมองหาว่าแต่ละ commit ที่เชื่อมโยงกับ commit สุดท้ายบน server และดูว่ามันเชื่อมโยงไปยัง parent ของ SHA ตัวใดตัวหนึ่งที่เราพยายามจะ push หรือไม่ ถ้ามีก็แสดงว่าเป็น fast-forward 875 | 876 | ปัญหาของวิธีการนี้คือมันช้ามาก และดูเหมือนไม่ค่อยมีประโยชน์ เพราะถ้าเราไม่ได้พยายามจะ force push ด้วย `-f` แล้ว ทุกครั้งที่เรา พยายามจะ push ตัว server ก็จะเตือนเราเองและทางแก้ก็ไม่ยาก แต่อย่างไรก็ตามตัวอย่างนี้ก็น่าสนใจ และน่าจะสามารถช่วยให้เราแก้ปํญหาการ rebase ผิดแล้วต้องย้อนกลับมาแก้ทีหลัง 877 | 878 | ## Summary ## 879 | 880 | ถึงต้อนนี้เราได้เรียนรู้เทคนิคส่วนใหญ่ในการปรับแต่ Git แล้วทั้งฝั่ง client และ server เพื่อให้ Git ทำงานได้ตรงกับ workflow ของโครงการ 881 | ทั้งการปรับแต่งจาก configuration setting, file-base attributes, hooks และ ได้ทดลองทำ policy-enforceing ที่ฝั่ง server หลังจากนี้ 882 | เราจะสามารถสร้าง Git ในฝันของเราได้แล้ว 883 | -------------------------------------------------------------------------------- /08-git-and-other-scms/01-chapter8.markdown: -------------------------------------------------------------------------------- 1 | # Git และระบบอื่นๆ # 2 | 3 | ทุกอย่างมันยังไม่สมบูรณ์ไปซะหมดหรอก เพราะการที่เราต้องการเปลี่ยนทุกๆ โปรเจคที่มีใช้งานอยู่ส่วนร่วมอยู่ให้เป็น Git ได้ในทันทีคงทำไม่ได้ เพราะบางครั้งเรายังยึดติดกับโปรเจคที่ใช้ VCS (Versions Control System) อื่นๆ ซึ่งหลายครั้งมันคือ Subversion คุณเองจะสามารถใช้ส่วนแรกของบทนี้สำหรับเรียนรู้เกี่ยวกับ `git svn` เป็นเครื่องมื่อที่ช่วยให้เราสามารถใช้งานร่วมกับ Subversion ได้ 4 | 5 | บางครั้ง หากต้องการที่จะแปลงโปรเจคของคุณที่มีอยู่แล้วให้มาใช้งานร่วมกับ Git ซึ่งส่วนที่ 2 ของบทนี้จะกพูดถึงการย้ายโปรเจคที่มีอยู่เดิมไปยัง Git ซึ่งก็เลี่ยงไม่ได้ที่จะต้องเริ่มต้นด้วย Subversion และสุดท้ายคือการย้ายโปรเจคด้วยสคริปต์สำหรับนำเข้าโปรเจคที่สร้างขึ้นเอง สำหรับกรณีการนำเข้าโปรเจคที่ไม่เป็นไปตามมาตรฐาน 6 | 7 | ## Git และ Subversion ## 8 | 9 | ขณะนี้ การพัฒนาโปรเจคโอเพ่นซอร์ซส่วนใหญ่รวมไปถึงโปรเจคภายในบริษัทจำนวนมากต่างก็ใช้ Subversion เพื่อช่วยจัดการซอร์ซโค้ด โดยที่ Subversion เป็น VCS แบบโอเพ่นซอร์ซและใช้มากันมายาวนานเกือบ 10 ปีแล้ว Subversion นั้นคล้ายกับ CVS ในหลายด้าน โดยที่ CVS เองเคยเป็นพี่ใหญ่ในวงการ source-control มาก่อน Subversion 10 | 11 | คุณลักษณะเด่นอย่างหนึ่งของ Git ก็คือ Bidirectional Bridge ไปยัง Subversion เรียกว่า git svn ซึ่งเครื่องมือนี้จะทำให้เราใช้ Git แบบไคลเอนต์ (Client) ร่วมกับเซิร์ฟเวอร์ที่เป็น Subversion ได้ ในขณะเดียวกันก็ยังใช้คุณสมบัติต่างๆ แบบโลคอล ของ Git ได้ทั้งหมด และส่งโค้ดกลับไปยังเซิร์ฟเวอร์ Subversion ได้เช่นเดียวกันกับการใช้งานไคลเอนต์ที่เป็น Subversion นั่นย่อมหมายความว่าเราสามารถสร้าง branch และ merge ภายในเครื่องของเราเองได้ ใช้งาน Staging area, rebase และ cherry-picking รวมไปถึงความสามารถอื่นๆ ในขณะที่เพื่อนๆ ร่วมโปรเจคคุณยังคงก้มหน้าก้มตาใช้งานของเก่าคร่ำคร่ากันต่อไป ซึ่งวิธีนี้เป็นวิธีการที่ดีสำหรับ(ลอง/แอบ)ใช้ Git ในสภาพแวดล้อมของหน่วยงาน และยังช่วยให้นักพัฒนาในกลุ่มทำงานได้อย่างมีประสิทธิภาพมากยิ่งขึ้น ระหว่างที่เราโน้มน้าวเพื่อที่จะได้ทรัพยากรที่พร้อมสำหรับการเปลี่ยนไปยัง Git อย่างสมบูรณ์แบบ 12 | 13 | ### git svn ### 14 | 15 | คำสั่งพื้นฐานใน Git สำหรับคำสั่งใช้เชื่อมการทำงานกับ Subversion ทั้งหมดจะเริ่มต้นด้วย `git svn` แล้วต่อท้ายด้วยคำสั่งอีกไม่กี่คำสั่ง ซึ่งจะได้เรียนคำสั่งที่ใช้งานโดยทั่วไปผ่านขั้นตอนการทำงานเล็กน้อย 16 | 17 | สิ่งสำคัญอย่างหนึ่งที่ต้องจำไว้ว่าเรากำลังใช้ `git svn` เพื่อทำงานร่วมกับ Subversion ซึ่งเป็นระบบที่เก่าคร่ำคร่ากว่า Git อย่างไรก็ตามเรายังสร้าง branch และ merge ภายในเครื่องได้ ซึ่งโดยทั่วไปแล้วเป็นเรื่องดีที่จะเก็บประวัติการทำงานไว้ภายในเครื่องไปเรื่อยๆ ด้วยการ rebase ไปเรื่อยๆ และยังเป็นการเลี่ยงการเกิดผลกระทบต่อ Git repository ที่ใช้งานอยู่ด้วย 18 | 19 | ไม่ต้องเขียนประวัติการใช้งานใหม่และโยนกลับเข้าไปอีกครั้ง **and don’t push to a parallel Git repository to collaborate with fellow Git developers at the same time** Subversion สร้างประวัติการทำงานต่อเนื่องกันไปเรื่อยๆ นั่นก็ทำให้สับสนได้ง่าย ถ้าทำงานร่วมกันเป็นทีม มีบางส่วนใช้ SVN รวมไปถึง Git ก็มั่นใจได้เลยว่าทุกคนสามารถใช้งานเซิร์ฟเวอร์ Subversion ร่วมกันได้ ทำตามเลยครับ แล้วชีวิตจะง่ายขึ้นเยอะ 20 | 21 | ### จงเตรียมพร้อม ### 22 | 23 | สำหรับการทดลองความสามารถนี้ สิ่งที่ขาดไม่ได้ คือ SVN repository ที่สามารถเข้าไปเขียนข้อมูลได้ เพราะถ้าต้องการจะทำตามตัวอย่างด้านล่าง ก็ต้องทำให้ข้อมูลที่คัดลอกไปจาก repository เขียนเพิ่มเ่ข้าไปได้ก่อน เพื่อให้ชีวิตง่ายขึ้นก็ให้เรียกใช้เครื่องมือชื่อ `svnsync` ที่ติดมากับ Subversion ราวๆ เวอร์ชั่น 1.4 เป็นต้นมา สำหรับตัวอย่างอย่างต่อไปนี้ ได้สร้าง Subversion repository ไว้บน Google code เรียนร้อยแล้ว ซึ่งเป็นส่วนหนึ่งของโปรเจค `protobuf` ซึ่งเป็นเครื่องมือเข้ารหัสโครงสร้างข้อมูลสำหรับส่งผ่านเครือข่าย 24 | 25 | จากนี้เป็นต้นไป สิ่งที่ต้องทำเป็นอันดับแรกคือสร้าง Subversion repository ภายในเครื่องซะก่อน: 26 | 27 | $ mkdir /tmp/test-svn 28 | $ svnadmin create /tmp/test-svn 29 | 30 | ถัดไปก็เปิดให้ผู้ใช้ทุกคนเปลี่ยน revprops ได้ ซึ่งวิธีการที่ง่ายที่สุดคือเพิ่มสคริปต์ pre-revprop-change ซึ่ง exists 0 เสมอ: 31 | 32 | $ cat /tmp/test-svn/hooks/pre-revprop-change 33 | #!/bin/sh 34 | exit 0; 35 | $ chmod +x /tmp/test-svn/hooks/pre-revprop-change 36 | 37 | เท่านี้ก็ซิงก์โปรเจคที่สร้างขึ้นภายในเครื่องกับ repository (ซึ่งในที่นี้คือ Google code) ได้แล้วด้วยการเรียกใช้คำสั่ง `svnsync init` 38 | 39 | $ svnsync init file:///tmp/test-svn http://progit-example.googlecode.com/svn/ 40 | 41 | วิธีการกำหนดค่าต่างๆ ด้านบนเพื่อใช้ซิงก์ข้อมูล เมื่อเสร็จเรียบร้อยแล้วก็เริ่ม clone โค้ดออกมาโดยใช้คำสั่ง 42 | 43 | $ svnsync sync file:///tmp/test-svn 44 | Committed revision 1. 45 | Copied properties for revision 1. 46 | Committed revision 2. 47 | Copied properties for revision 2. 48 | Committed revision 3. 49 | ... 50 | 51 | แม้ว่าคำสั่งด้านบนจะใช้เวลาไม่กี่นาที แต่หากต้องการคัดลอก repository ต้นฉบับ ไปยัง repository อื่นแทนที่จะเป็น repository ที่อยู่ในเครื่อง กระบวนการนี้จะใช้เวลาอยู่ราวๆ 1 ชั่วโมง ถึงแม้ว่าจะมีรายการ commit ไม่ถึง 100 รายการก็ตาม เพราะในขณะที่ Subversion ดึงแต่ละ revision ออกมาก ก็จะโยนเข้าไปไว้ใน repository อีกอันหนึ่งไปด้วย มันดูเป็นวิธีที่น่าตลกไปหน่อย แต่ว่ามันก็เป็นวิธีการที่ง่ายที่สุดสำหรับการทำงานแบบนี้ 52 | 53 | ### เริ่มกันสักที ### 54 | 55 | จากหัวข้อที่แล้ว เราก็มี repository ของ Subversion ที่สามารถเขียนข้อมูลลงไปได้เรียบร้อยแล้ว ถัดไปก็จะลองทำตามขั้นตอนการทำงานทั่วไปดูก่อน เริ่มต้นด้วยคำสั่ง `git svn clone` ใช้นำรายการต่างๆ จาก repository ที่เป็น Subversion เข้าสู่ repository ของ Git ต้องจำไว้ว่าสำหรับการนำข้อมูลจากเซิร์ฟเวอร์ Subversion จริงๆ ควรเปลี่ยนจาก `file:///tmp/test-svn` ให้เป็น URL ของ repository ที่ทำงานด้วยซะก่อน: 56 | 57 | $ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags 58 | Initialized empty Git repository in /Users/schacon/projects/testsvnsync/svn/.git/ 59 | r1 = b4e387bc68740b5af56c2a5faf4003ae42bd135c (trunk) 60 | A m4/acx_pthread.m4 61 | A m4/stl_hash.m4 62 | ... 63 | r75 = d1957f3b307922124eec6314e15bcda59e3d9610 (trunk) 64 | Found possible branch point: file:///tmp/test-svn/trunk => \ 65 | file:///tmp/test-svn /branches/my-calc-branch, 75 66 | Found branch parent: (my-calc-branch) d1957f3b307922124eec6314e15bcda59e3d9610 67 | Following parent with do_switch 68 | Successfully followed parent 69 | r76 = 8624824ecc0badd73f40ea2f01fce51894189b01 (my-calc-branch) 70 | Checked out HEAD: 71 | file:///tmp/test-svn/branches/my-calc-branch r76 72 | 73 | คำสั่งด้านบนจะคล้ายกันการทำงานของคำสั่ง `git svn init` ต่อด้วย `git svn fetch` ไปยัง URL ที่ระบุโคยจะใช้เวลาทำงานสักครู่ แต้ด้วยโปรเจคที่สร้างขึ้นมีรายการ commit ไปเพียงแค่ 75 รายการ และขนาดโดยรวมของโค้ดไม่ใหญ่มาก ดังนั้นไม่น่าจะใช้เวลาไม่กี่นาทีเท่านั้น แต่อย่างไรก็ตาม Git จะ check out ออกมาครั้งละรายการ และ commit เข้าไปยัง repository ของ Git ดังนั้นสำหรับโปรเจคที่มีการายการ commit หลายร้อยหรือหลายพันรายการ จะใช้เวลาหลายชั่วโมงหรือหลายวันกว่าจะเสร็จ 74 | 75 | คำสั่ง `-T trunk -b branches -t tags` เป็นส่วนที่บอก Git ว่า repository ของ Subversion นี้เป็นไปตามโครงสร้างการแตก branch และการกำหนด tag แบบทั่วไป แต่ในกรณีที่ตั้งชื่อ trunk, branch หรือ tag ที่ต่างออกไป ก็แค่เปลี่ยนค่าใน option ได้ เนื่องจากมันค่นอข้างจะเป็นเรื่องที่เจอกันได้เรื่อง เพราะฉะนั้นคำสั่งด้านบนจึงสามารถแทนที่ด้วย `-s` เป็นส่วนที่หมายถึง repository นี้ใช้เลย์เอาท์มาตรฐานของ subversion และกำหนดค่าให้กับ option ต่างๆ เช่นเดียวกับคำสั่งด้านบน จะได้คำสั่ง: 76 | 77 | $ git svn clone file:///tmp/test-svn -s 78 | 79 | เมื่อถึงขั้นตอนนี้จะได้ repository ของ Git ที่นำข้อมูล branch และ tag เข้ามาเก็บไว้เรียบร้อยแล้ว: 80 | 81 | $ git branch -a 82 | * master 83 | my-calc-branch 84 | tags/2.0.2 85 | tags/release-2.0.1 86 | tags/release-2.0.2 87 | tags/release-2.0.2rc1 88 | trunk 89 | 90 | เรื่องหนึ่งที่สำคัญคือเครื่องมือนี้จะกำหนด namesapce สำหรับอ้างถึงต่างกันออกไป เมื่อดึงข้อมูลออกมาจาก repository ของ Git จะได้ข้อมูลของ branch ทั้งหมดของ repository นั้นออกมาเป็นไว้ที่ repository ภายในเครื่อง ซึ่งเหมือนกับ `origin/[branch]` โดยที่ namespace จะถูกกำหนดด้วยชื่อของกลุ่มที่เราสนใจอยู่ (namespaced by the name of the remote) ซึ่ง `git svn` จะทึกทักเอาว่าเราต้องไม่มีกลุ่มข้อมูลที่เราสนใจอยู่หลายกลุ่มและนอกจากนั้นยังจะคิดไปเราบันทึกตำแหน่งที่ใช้อ้างถึง (references to points) ทั้งหมดไว้บนเซิร์ฟเวอร์แบบที่ไม่ได้กำหนด namespace ซึ่งถ้าใช้คำสั่ง `show-ref` ของ Git ก็จะเห็น ตำแหน่งทั้งหมดที่ใช้อ้างถึงได้: 91 | 92 | $ git show-ref 93 | 1cbd4904d9982f386d87f88fce1c24ad7c0f0471 refs/heads/master 94 | aee1ecc26318164f355a883f5d99cff0c852d3c4 refs/remotes/my-calc-branch 95 | 03d09b0e2aad427e34a6d50ff147128e76c0e0f5 refs/remotes/tags/2.0.2 96 | 50d02cc0adc9da4319eeba0900430ba219b9c376 refs/remotes/tags/release-2.0.1 97 | 4caaa711a50c77879a91b8b90380060f672745cb refs/remotes/tags/release-2.0.2 98 | 1c4cb508144c513ff1214c3488abe66dcb92916f refs/remotes/tags/release-2.0.2rc1 99 | 1cbd4904d9982f386d87f88fce1c24ad7c0f0471 refs/remotes/trunk 100 | 101 | ซึ่ง repository ของ Git จะเห็นในลักษณะ: 102 | 103 | $ git show-ref 104 | 83e38c7a0af325a9722f2fdc56b10188806d83a1 refs/heads/master 105 | 3e15e38c198baac84223acfc6224bb8b99ff2281 refs/remotes/gitserver/master 106 | 0a30dd3b0c795b80212ae723640d4e5d48cabdff refs/remotes/origin/master 107 | 25812380387fdd55f916652be4881c6f11600d6f refs/remotes/origin/testing 108 | 109 | เรามี 2 เซิร์ฟเวอร์ด้วยกัน เซิร์ฟเวอร์แรกชื่อ `gitserver` ที่มี `master` เป็น branch และอีกเซิร์ฟเวอร์หนึ่งชื่อ `origin` มี `master` และ `testing` เป็น branch แยกกันอยู่ภายใน 110 | 111 | ต้องจำไว้อย่างหนึ่งว่าวิธีการในตัวอย่างของการอ้างไปยังกลุ่มข้อมูลที่เราสนใจอาศัยข้อมูลที่ได้จาก `git svn` ซึ่ง tag ที่เพิ่มเข้ามาเพื่อทำหน้าที่ branch ไม่ใช่ tag ของ Git จริงๆ โดยที่ Subversion จะนำเข้าข้อมูลโดยตั้งชื่อ tag ภายใต้ branch 112 | 113 | ### กลับสู่ Subversion ### 114 | 115 | ถึงตอนนี้ก็มี repository ที่พร้อมใช้งานแล้ว ตอนนี้ก็สามารถแก้ไขหรือเปลี่ยนแปลงข้อมูลตามที่ต้องการในโปรเจคและ commit กลับไปยัง upstream ซึ่งทั้งหมดใช้ Git ในลักษณะของ SVN Client ซึ่งแน่นอนว่าการทำงานทั้งหมดที่พูดถึงยังไม่ปรากฎบนเซิร์ฟเวอร์ Subversion มีอยู่เพียงแค่ภายในเครื่องเท่านั้น: 116 | 117 | $ git commit -am 'Adding git-svn instructions to the README' 118 | [master 97031e5] Adding git-svn instructions to the README 119 | 1 files changed, 1 insertions(+), 1 deletions(-) 120 | 121 | ถัดไปก็ต้องโยนข้อมูลการเปลี่ยนกลับไปยัง upstream สังเกตว่าวิธีการต่อไปนี้จะเป็นการเปลี่ยนแปลงวิธีที่เคยใช้งาน Subversion เพราะใน repository ของ Git สามารถ commit โค้ดได้ภายในเครื่องแบบออฟไลน์ จากนั้นจะส่งข้อมูลทั้งหมดกลับไปยังเซอิร์เวอร์ Subversion กลับไปในครั้งเดียว โดยการโยนข้อมูลกลับไปยังเซิร์ฟเวอร์ Subversion ก็ทำได้โดยใช้คำสั่ง `git svn dcommit`: 122 | 123 | $ git svn dcommit 124 | Committing to file:///tmp/test-svn/trunk ... 125 | M README.txt 126 | Committed r79 127 | M README.txt 128 | r79 = 938b1a547c2cc92033b74d32030e86468294a5c8 (trunk) 129 | No changes between current HEAD and refs/remotes/trunk 130 | Resetting to the latest refs/remotes/trunk 131 | 132 | คำสั่งด้านบนคือการส่งข้อมูลกลับไปยัง Subversion และนำไปวางไว้เป็นเวอร์ชั่นล่าสุดของ repository (HEAD) พร้อมกับรายการ commit ต่างๆ กลับไปพร้อมกัน จนไปถึงปรับปรุงค่าสำหรับบ่งชี้ (unique identifier) บน Git ภายในเครื่องใหม่ด้วย ขึ้นตอนนี้สำคัญมากเพราะนั้นหมายถึงค่า SHA-1 สำหรับแต่ละการ commit จะเปลี่ยนไป สาเหตุส่วนหนึ่งก็เป็นเพราะการทำงานบน Git ในลักษณะของไคลเอนท์กับโปรเจคมีเซิร์ฟเวอร์เป็น Subversion ไม่ใช่ความคิดที่ดีเท่าไหร่นัก ถ้าดูรายการ commit ล่าสุด จะเห็นว่า `git-svn-id` ได้ถูกเพิ่มเข้ามาแล้ว: 133 | 134 | $ git log -1 135 | commit 938b1a547c2cc92033b74d32030e86468294a5c8 136 | Author: schacon 137 | Date: Sat May 2 22:06:44 2009 +0000 138 | 139 | Adding git-svn instructions to the README 140 | 141 | git-svn-id: file:///tmp/test-svn/trunk@79 4c93b258-373f-11de-be05-5f7a86268029 142 | 143 | สังเกตว่าค่า checksum ของ SHA จะเริ่มต้นด้วย `97031e5` เป็นลำดับแรก เมื่อ commit ไปแล้ว ตอนนี้ก็กลายเป็น `938b1a5` และในกรณีที่ต้องการ push ไปทั้งเซิร์ฟเวอร์ Git และเซิร์ฟเวอร์ Subversion เราต้อง push (`dcommit`) ไปยังเซิร์ฟเวอร์ Subversion ก่อนเป็นลำดับแรก เพราะนั่นจะเป็นการเปลี่ยนข้อมูลที่ commits ของเรา 144 | 145 | ### การดึงข้อมูลเปลี่ยนแปลงใหม่ ### 146 | 147 | ในกรณีที่กำลังทำงานร่วมกันกับเพื่อนหลายคน ซึ่งในบางครั้งเพื่อนในกลุ่มเรา push ข้อมูลใหม่ขึ้นมา และอีกคนหนึ่งก็พยายามที่จะ push ข้อมูลของเขาขึ้นมาเพื่อแก้ conflict ที่เกิดขึ้น (conflict ทีเกิดขึ้นคือข้อมูลที่ต่างกันระหว่างคนแรกและคนที่สอง) การเปลี่ยนแปลงของคนที่ 2 นั้นจะถูกปฏิเสธ (reject) จนกว่าจะ merge ข้อมูลในส่วนที่เกิด conflict ซะก่อน ซึ่งใน `git svn` จะหน้าตาเป็นแบบนี้: 148 | 149 | $ git svn dcommit 150 | Committing to file:///tmp/test-svn/trunk ... 151 | Merge conflict during commit: Your file or directory 'README.txt' is probably \ 152 | out-of-date: resource out of date; try updating at /Users/schacon/libexec/git-\ 153 | core/git-svn line 482 154 | 155 | เพื่อแก้ไขปัญหาในลักษณะนี้ ทำได้โดยการใช้คำสั่ง `git svn rebase` ซึ่งจะดึงข้อมูลการเปลี่ยนแปลงทั้งหมดบนเซิร์ฟเวอร์ที่เราไม่มีข้อมูลอยู่และ rebase งานของเราให้เป็นเวอร์ชั่นล่าสุดล่าสุดเช่นเดียวกันกับบนเซิร์ฟเวอร์: 156 | 157 | $ git svn rebase 158 | M README.txt 159 | r80 = ff829ab914e8775c7c025d741beb3d523ee30bc4 (trunk) 160 | First, rewinding head to replay your work on top of it... 161 | Applying: first user change 162 | 163 | ตอนนี้งานทั้งหมดที่มีอยู่ภายในเครื่องก็เช่นเดียวกันกับไฟล์เวอร์ชั่นล่าสุดที่อยู่บนเซิร์ฟเวอร์แล้ว ดังนั้นก็ปิดงานด้วยคำสั่ง `dcommit`: 164 | 165 | $ git svn dcommit 166 | Committing to file:///tmp/test-svn/trunk ... 167 | M README.txt 168 | Committed r81 169 | M README.txt 170 | r81 = 456cbe6337abe49154db70106d1836bc1332deed (trunk) 171 | No changes between current HEAD and refs/remotes/trunk 172 | Resetting to the latest refs/remotes/trunk 173 | 174 | จำไว้ให้ว่าไม่เหมือน Git ที่จะบังคับให้ merge งานกับ upstream ก่อนที่จะ push กลับเข้าไป ซึ่ง `git svn` ช่วยเราได้ในกรณีที่เกิด conflict ถ้ามีใครสักคนโยนข้อมูลการเปลี่ยนแปลงของไฟล์สักไฟล์ และต่อมาเราก็โยนข้อมูลการเปลี่ยนแปลงของไฟล์อีกไฟล์หนึ่งกลับไป `dcommit` ก็ยังทำงานได้ไม่มีปัญหา: 175 | 176 | $ git svn dcommit 177 | Committing to file:///tmp/test-svn/trunk ... 178 | M configure.ac 179 | Committed r84 180 | M autogen.sh 181 | r83 = 8aa54a74d452f82eee10076ab2584c1fc424853b (trunk) 182 | M configure.ac 183 | r84 = cdbac939211ccb18aa744e581e46563af5d962d0 (trunk) 184 | W: d2f23b80f67aaaa1f6f5aaef48fce3263ac71a92 and refs/remotes/trunk differ, \ 185 | using rebase: 186 | :100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 \ 187 | 015e4c98c482f0fa71e4d5434338014530b37fa6 M autogen.sh 188 | First, rewinding head to replay your work on top of it... 189 | Nothing to do. 190 | 191 | อีกอย่างหนึ่งที่ต้องจำคือ ด้วยเหตุที่ว่าผลลัพธ์คือสถานะของโปรเจคที่ไม่ได้เกิดขึ้นเช่นเดียวกันกับเครื่องคอมพิวเตอร์ทุกเครื่องเมื่อเรา push ไปแล้ว ถ้าการเปลี่ยนแปลงดังกล่าวขัดแย้งกันแต่ไม่เกิด conflict ปัญหาที่แก้ยากก็จะตามมาภายหลัง ซึ่งนี่จะต่างจากเซิร์ฟเวอร์ที่เป็น Git เพราะ Git เราสามารถทดสอบสถานะต่างๆ บนไคลเอนต์ได้อย่างเต็มที่ก่อนที่จะโยนข้อมูลกลับไปยังเซิร์ฟเวอร์ ในทางตรงกันข้ามสำหรับ SVN แล้ว เราไม่สามารถมั่นใจได้เลยว่าสถานะก่อนและหลังการ commit จะเหมือนกันทุกอย่าง 192 | 193 | เราสามารถใช้คำสั่งเพื่อดึงรายการเปลี่ยนแปลงจากเซิร์ฟเวอร์ Subversion ถึงแม้จะยังไม่ต้องการจะ commit ก็ตาม โดยใช้คำสั่ง `git svn fetch` เพื่อดึงเอาข้อมูลใหม่บนเซิร์ฟเวอร์ลงมา แต่สำหรับ `git svn rebase` จะไม่มีดึงข้อมูลใหม่ลงมาหรืออัพเดตข้อมูลของรายการ commit ที่อยู่ภายในเครื่องเลย: 194 | 195 | $ git svn rebase 196 | M generate_descriptor_proto.sh 197 | r82 = bd16df9173e424c6f52c337ab6efa7f7643282f1 (trunk) 198 | First, rewinding head to replay your work on top of it... 199 | Fast-forwarded master to refs/remotes/trunk. 200 | 201 | เมื่อสั่ง `git svn rebase` ให้ทำงานในแต่ละครั้งต้องแน่ใจว่าโค้ดของเราเป็นโค้ดเวอร์ชั่นล่าสุดเสมอ แต่ถ้ามีการเปลี่ยนแปลงเกิดขึ้นภายในเครื่อง ก็ให้ commit การเปลี่ยนแปลงนั้นไปก่อนชั่วคราวหรือ stash ส่วนนั้นออกไปก่อนที่จะใช้คำสั่ง `git svn rebase` เพราะไม่เช่นนั้นแล้วคำสั่งนี้ก็จะหยุดทำงานเมื่อเห็นว่าเกิดข้อผิดพลาดของการ merge เกิดขึ้นอยู่ดี: 202 | 203 | ### ปัญหาการสร้าง Branch ของ Git ### 204 | 205 | พอเราเริ่มคุ้นเคยกับขั้นตอนการทำงานของ Git แล้ว เรื่องถัดไปก็น่าจะลองทำ branch เป็นเรื่องต่อไป แก้ไขข้อมูลที่แตก branch ออกไป และ merge เข้าหากัน ในกรณีที่ push ข้อมูลเข้าไปยังเซิร์ฟเวอร์ที่เป็น Subversion ผ่าน git svn ก็น่าจะเคยอยาก rebase งานให้อยู่บน branch เดียวกันไปแทนที่จะ merge ข้อมูลใน branch เข้าหากัน ด้วยเหตุผลที่ให้ความสำคัญกับการ rebase นั่นก็เพราะว่า Subversion จะเก็บประวัติการทำงานต่อกันไปเรื่อยๆ และยังไม่ได้เกี่ยวข้องเรื่อง merge เหมือนกับที่ Git ทำ ดังนั้น git svn ก็เลยทำตัวในลักษณะเดียวกันกับต้นฉบับ เมื่อมีการแปลง snapshots ให้กลายเป็นรายการ commit ของ Subversion 206 | 207 | สมมติว่าประวัติการทำงานของเราหน้าตาเป็นแบบนี้ คือ เราสร้าง branch ชื่อ `experiment` และ commit ไป 2 รายการ จากนั้นก็ merge กลับไปยัง `master` เมื่อสั่ง `dcommit` ก็จะได้ผลลัพธ์ดังนี้: 208 | 209 | $ git svn dcommit 210 | Committing to file:///tmp/test-svn/trunk ... 211 | M CHANGES.txt 212 | Committed r85 213 | M CHANGES.txt 214 | r85 = 4bfebeec434d156c36f2bcd18f4e3d97dc3269a2 (trunk) 215 | No changes between current HEAD and refs/remotes/trunk 216 | Resetting to the latest refs/remotes/trunk 217 | COPYING.txt: locally modified 218 | INSTALL.txt: locally modified 219 | M COPYING.txt 220 | M INSTALL.txt 221 | Committed r86 222 | M INSTALL.txt 223 | M COPYING.txt 224 | r86 = 2647f6b86ccfcaad4ec58c520e369ec81f7c283c (trunk) 225 | No changes between current HEAD and refs/remotes/trunk 226 | Resetting to the latest refs/remotes/trunk 227 | 228 | เมื่อ `dcommit` ทำงานบน branch พร้อมกับ merge รายการต่างๆ เรียบร้อย เว้นแต่ว่าถ้าลองดูในรายการประวัติของโปรเจคที่เป็น Git ของเรา จะพบว่ามันไม่มีการเขียนข้อมูลรายการ commit ลงไปใน `experiment` แต่ข้อมูลการเปลี่ยนแปลงทั้งหมดไปปรากฎบน SVN ในลักษณะของการ merge commit แทน 229 | 230 | หากคนอื่น clone โปรเจคนี้ออกไป สิ่งที่เขาจะเห็นคือการ merge commit ที่ถูกรวมเข้าด้วยกันแล้วแทน โดยจะไม่เห็นรายละเอียดว่ามีข้อมูลใดบ้างที่ commit เช่น ที่มาของรายการนี้ หรือ วันเวลาที่ commit 231 | 232 | ### การสร้าง Branch ใน Subversion ### 233 | 234 | การสร้าง branch ใน Subversion นั้นไม่ได้เหมือนกับการทำ branch ใน Git นัก แต่ถ้าทำเป็นลืมๆ หรือข้ามการทำ branch ไปได้ก็จะเป็นเรื่องดี แต่อย่างไรก็ตามเราก็ยังคงสามารถสร้าง branch และ commit เข้าไปใน Subversion ด้วยการใช้งาน git svn อยู่ดี 235 | 236 | #### การสร้าง Branch ใหม่ใน SVN #### 237 | 238 | สำหรับการสร้าง branch ใน Subversion ก็ทำได้ด้วยการใช้คำสั่ง `git svn branch [branchname]`: 239 | 240 | $ git svn branch opera 241 | Copying file:///tmp/test-svn/trunk at r87 to file:///tmp/test-svn/branches/opera... 242 | Found possible branch point: file:///tmp/test-svn/trunk => \ 243 | file:///tmp/test-svn/branches/opera, 87 244 | Found branch parent: (opera) 1f6bfe471083cbca06ac8d4176f7ad4de0d62e5f 245 | Following parent with do_switch 246 | Successfully followed parent 247 | r89 = 9b6fe0b90c5c9adf9165f700897518dbc54a7cbf (opera) 248 | 249 | คำสั่งนี้จะเหมือนกับคำสั่ง `svn copy trunk branches/opera` ใน Subversion รวมไปถึงการทำงานบนเซิร์ฟเวอร์ Subversion แต่เรื่องสำคัญเรื่องหนึ่งคือเราไม่ได้ check out จาก branch นั้น เพราะเมื่อเรา commit กลับไปตอนนี้ รายการ commit ทั้งหมดจะไปอยู่ใน `trunk` บนเซิร์ฟเวอร์ ไม่ใช่ `opera` 250 | 251 | ### เปลี่ยน Active Branch ### 252 | 253 | Git คิดถึงว่า เมื่อใช้ dcommits แล้ว จะไปอยู่ที่ branch ไหนด้วยการเข้าไปดูข้อมูลในประวัติการทำงานของ branch ใดๆ ที่อยู่ใน Subversion ของคุณ ซึ่งก็น่าจะมีอยู่เพียงรายการเดียวและน่าจะเป็นรายการสุดท้ายจาก `git-svn-id` ในประวัติการทำงานของ branch ปัจจุบัน 254 | 255 | ซึ่งถ้าต้องการทำพร้อมกันหลาย branch ก็ทำได้ด้วยการกำหนดให้ branch ทั้งหมดภายในเครื่องที่ต้องการทำงานพร้อมกันนั้น `dcommit` ไปยัง branch บน Subversion ด้วยการเริ่มต้นด้วยการนำข้อมูลการ commit ของ branch ใน Subversion ที่ต้องการทั้งหมดก่อน เช่น ถ้าต้องการ branch ชื่อ `opera` ที่เราทำงานแยกออกมาได้ ก็ทำได้ด้วยใช้คำสั่ง: 256 | 257 | $ git branch opera remotes/opera 258 | 259 | ขณะนี้ หากเราต้องการ merge ข้อมูลของ branch ชื่อ `opera` กลับไปยัง `trunk` (`master` branch) ก็ทำได้ด้วยคำสั่ง `git merge` เท่านั้น แต่ว่าต้องใส่คำอธิบายการ commit (commit message) เข้าไปด้วย (ระบุด้วยตัวเลือก `-m`) หรือไม่อย่างนั้นแล้วข้อความนั้นจะเป็น "Merge branch opera" แทนที่จะเป็นข้อความที่อธิบายได้ดีกว่านั้น 260 | 261 | ถึงแม้ว่าเราจะใช้ `git merge` และการ merge ก็ดูท่าจะง่ายมากเมื่อเทียบกับ Subversion (นั่นเพราะ Git จะตรวจสอบความเข้ากันได้สำหรับการ merge ให้อัตโนมัติ) แต่นี่ไม่ใช่สำหรับการ merge ของ Git เราต้อง push ข้อมูลเหล่านี้กลับไปยังเซิร์ฟเวอร์ Subversion นั่นทำให้ไม่สามารถควบคุมการ commit ที่แย่งออกไปหลายโปรเจคได้ ดังนั้น หลังจากส่งข้อมูลกลับไปแล้ว ก็จะเหมือนกับว่า commit นั้นโดนบีบอัดเข้าไปกับงานที่หมดของ branch อื่นๆ ภายใต้ commit เดียว และเมื่อใดที่เกิดการควบรวม branch หนึ่งไปยังอีก branch หนึ่งแล้ว มันต้องใช้พลังเยอะมากทีเดียวถ้าต้องการที่จะกลับไปทำงานต่อที่ branch แรกนั้น แต่สำหรับ Git แล้ว มันเด็กมาก คำสั่ง `dcommit` นั้นใช้สำหรับลบข้อมูลใดๆ ก็ตามที่บอกว่า branch ใดที่ถูก merge เข้ามา ดังนั้นการคำนวณเพื่อหาข้อมูลก่อนหน้าที่ merge มานั้นผิดพลาดไป ซึ่งคำสั่ง dcommit จะทำให้ผลลัพธ์ได้ที่จาก `git merge` เหมือนกับผลลัพธ์ที่ได้จาก `git merge --squash` โชคร้ายหน่อยที่ไมมีวิธีการอื่นเลยเพื่อหลีกเลี่ยงสถานะการณ์นี้เนื่องจาก Subversion ไม่สามารถเก็บข้อมูลได้ ดังนั้นเราก็คงต้องทำงานด้วยวิธีพิกลพิการ อันเนื่องมาจากข้อจำกัดหลายด้านของระบบซึ่งมันจะเป็นอยู่แบบนี้ต่อไปตราบใดที่เรายังคงใช้งานเซิร์ฟเวอร์ที่เป็น Subversion เช่นนี้การหลีกเลี่ยง issue นี้ก็คือ เราต้องลบ local branch (ในกรณีนี้คือ `opear`) หลังจากเรา mege มันเข้าไปยัง remositor แล้ว 262 | 263 | ### คำสั่งต่างๆ ของ Subversion ### 264 | 265 | `git svn` เป็นชุดเครื่องมือที่เตรียมคำสั่งต่างๆ สำหรับอำนวยความสะดวกสำหรับการย้ายมายัง Git ด้วยการเตรียมฟังก์ชันการใช้งานต่างๆ คล้ายกันกับที่มีใน Subversion ซึ่งบางคำสั่งก็เตรียมไว้เช่นเดียวกันกับที่ Subversion ใช้ 266 | 267 | #### รูปแบบการเก็บประวัติของ SVN #### 268 | 269 | ถ้าใช้ Subversion มาก่อน และได้ลองเปิดดูประวัติการทำงานแบบ SVN output style ซึ่งตอนนี้ถ้าต้องการดูประวิตการทำงานรูบแบบเดียวกันนั้นก็ให้ใช้คำสั่ง `git svn log`: 270 | 271 | $ git svn log 272 | ------------------------------------------------------------------------ 273 | r87 | schacon | 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009) | 2 lines 274 | 275 | autogen change 276 | 277 | ------------------------------------------------------------------------ 278 | r86 | schacon | 2009-05-02 16:00:21 -0700 (Sat, 02 May 2009) | 2 lines 279 | 280 | Merge branch 'experiment' 281 | 282 | ------------------------------------------------------------------------ 283 | r85 | schacon | 2009-05-02 16:00:09 -0700 (Sat, 02 May 2009) | 2 lines 284 | 285 | updated the changelog 286 | 287 | You should know two important things about `git svn log`. First, it works offline, unlike the real `svn log` command, which asks the Subversion server for the data. Second, it only shows you commits that have been committed up to the Subversion server. Local Git commits that you haven’t dcommited don’t show up; neither do commits that people have made to the Subversion server in the meantime. It’s more like the last known state of the commits on the Subversion server. 288 | สำหรับคำสัง `git svn log` นี้มี 2 เรื่องสำคัญที่ต้องรู้ไว้ คือ เรื่องแรก คำสั่งนี้จะทำงานแบบออฟไลน์ ซึ่งไม่เหมือนกับคำสั่ง `svn log` จริงๆ ที่จะไปถามเซิร์ฟเวอร์ Subversion เพื่อขอข้อมูล และเรื่องที่สองคือ คำสั่งนี้จะแสดงเฉพาะรายการทำงานที่ commit ไปยังเซิร์ฟเวอร์ Subversion แต่ประวัติการ commit ด้วย Git ซึ่งยังไม่ได้ใช้คำสั่ง dcommited จะไม่ได้ได้เอามาแสดง 289 | 290 | #### หมายเหตุใน SVN #### 291 | 292 | `git svn log` จำลองการทำงานหลายอย่างของ `svn log` ให้ทำงานได้แบบออฟไลน์ ซึ่งถ้าหากต้องการผลลัพธ์เช่นเดียวกันกับ `svn annotate` ให้ใช้คำสั่ง `git svn blame [FILE]` โดยผลลัพธ์ที่ได้ออกมาคือ: 293 | 294 | $ git svn blame README.txt 295 | 2 temporal Protocol Buffers - Google's data interchange format 296 | 2 temporal Copyright 2008 Google Inc. 297 | 2 temporal http://code.google.com/apis/protocolbuffers/ 298 | 2 temporal 299 | 22 temporal C++ Installation - Unix 300 | 22 temporal ======================= 301 | 2 temporal 302 | 79 schacon Committing in git-svn. 303 | 78 schacon 304 | 2 temporal To build and install the C++ Protocol Buffer runtime and the Protocol 305 | 2 temporal Buffer compiler (protoc) execute the following: 306 | 2 temporal 307 | 308 | เหมือนกับที่ผ่านมา ผลัพธ์ด้านบนจะไม่แสดงข้อมูลการ commit ที่ทำด้วย Git ไปแล้วในเครื่องหรือส่งไปยังเซิร์ฟเวอร์ Subversion ระหว่างนั้นแล้ว 309 | 310 | #### ข้อมูลเซิร์ฟเวอร์ SVN #### 311 | 312 | ถ้าต้องการผลลัพธ์ที่เหมือนกันกับคำสั่ง `svn info` ก็ให้ใช้คำสั่ง `git svn info`: 313 | 314 | $ git svn info 315 | Path: . 316 | URL: https://schacon-test.googlecode.com/svn/trunk 317 | Repository Root: https://schacon-test.googlecode.com/svn 318 | Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029 319 | Revision: 87 320 | Node Kind: directory 321 | Schedule: normal 322 | Last Changed Author: schacon 323 | Last Changed Rev: 87 324 | Last Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009) 325 | 326 | เช่นเดียวกันกับคำสั่ง `blame` และ `log` ที่จะทำงานแบบออฟไลน์เท่านั้นและอัพเดตแค่ล่าสุดที่ติดต่อกับเซิร์ฟเวอร์ Subversion 327 | 328 | 329 | #### อย่าไปสนเรื่องที่ Subversion ไม่สนใจ #### 330 | 331 | ถ้าหากเรา clone ข้อมูลจาก repository ของ Subversion ที่มีการกำหนดค่า `svn:ignore` ภายในโปรเจคนั้น ดังนั้นเรื่องหนึ่งที่เราต้องการคือกำหนดค่าในไฟล์ `.gitignore` ทุกไฟล์ให้เหมือนกันก็เพื่อไม่ให้เราต้อง commit ไฟล์ต่างๆ ที่ไม่ต้องการ ซึ่ง `git svn` ได้เตรียมคำสั่งไว้ 2 คำสั่งเพื่อจัดการปัญหานี้ คำสั่งแรกคือ `git svn create-ignore` ที่จะกำหนดค่าต่างๆ ของไฟล์ `.gitignore` ให้อัตโนมัติ ดังนั้นเมื่อส่งรายการ commit ครั้งต่อไปก็จะนำเอาข้อมูลดังกล่าวมาใช้งานได้ทันที 332 | 333 | คำสั่งอีกคำสั่งหนึ่งก็คือ `git svn show-ignore` ซึ่งจะแสดงข้อมูลที่ต้องนำเอาไปใส่ในไฟล์ `.gitignore` บนหน้าจอ แต่เราก็สามารถเปลี่ยนการแสดงบนให้ไปยังไฟล์สำหรับใช้แยกไฟล์ที่ไม่สนใจของโปรเจได้ดังนี้: 334 | 335 | $ git svn show-ignore > .git/info/exclude 336 | 337 | ด้วยวิธีการนี้เอง ทำให้โปรเจคมีไฟล์ `.gitignor` อยู่อย่างเกลื่อนกลาด ซึ่งวิธีนี้เป็นตัวเลือกที่ดี หากเราเป็นเพียงคนเดียวในทีมที่ใช้ Git ร่วมกันกับ Subversion และเพื่อนร่วมทีมคนอื่นก็คงไม่อยากได้ไฟล์ `.gitignore` มาอยู่ในโปเจค 338 | 339 | ### Git-Svn Summary ### 340 | 341 | The `git svn` tools are useful if you’re stuck with a Subversion server for now or are otherwise in a development environment that necessitates running a Subversion server. You should consider it crippled Git, however, or you’ll hit issues in translation that may confuse you and your collaborators. To stay out of trouble, try to follow these guidelines: 342 | 343 | * Keep a linear Git history that doesn’t contain merge commits made by `git merge`. Rebase any work you do outside of your mainline branch back onto it; don’t merge it in. 344 | * Don’t set up and collaborate on a separate Git server. Possibly have one to speed up clones for new developers, but don’t push anything to it that doesn’t have a `git-svn-id` entry. You may even want to add a `pre-receive` hook that checks each commit message for a `git-svn-id` and rejects pushes that contain commits without it. 345 | 346 | If you follow those guidelines, working with a Subversion server can be more bearable. However, if it’s possible to move to a real Git server, doing so can gain your team a lot more. 347 | 348 | ## Migrating to Git ## 349 | 350 | If you have an existing codebase in another VCS but you’ve decided to start using Git, you must migrate your project one way or another. This section goes over some importers that are included with Git for common systems and then demonstrates how to develop your own custom importer. 351 | 352 | ### Importing ### 353 | 354 | You’ll learn how to import data from two of the bigger professionally used SCM systems — Subversion and Perforce — both because they make up the majority of users I hear of who are currently switching, and because high-quality tools for both systems are distributed with Git. 355 | 356 | ### Subversion ### 357 | 358 | If you read the previous section about using `git svn`, you can easily use those instructions to `git svn clone` a repository; then, stop using the Subversion server, push to a new Git server, and start using that. If you want the history, you can accomplish that as quickly as you can pull the data out of the Subversion server (which may take a while). 359 | 360 | However, the import isn’t perfect; and because it will take so long, you may as well do it right. The first problem is the author information. In Subversion, each person committing has a user on the system who is recorded in the commit information. The examples in the previous section show `schacon` in some places, such as the `blame` output and the `git svn log`. If you want to map this to better Git author data, you need a mapping from the Subversion users to the Git authors. Create a file called `users.txt` that has this mapping in a format like this: 361 | 362 | schacon = Scott Chacon 363 | selse = Someo Nelse 364 | 365 | To get a list of the author names that SVN uses, you can run this: 366 | 367 | $ svn log --xml | grep author | sort -u | perl -pe 's/.>(.?)<./$1 = /' 368 | 369 | That gives you the log output in XML format — you can look for the authors, create a unique list, and then strip out the XML. (Obviously this only works on a machine with `grep`, `sort`, and `perl` installed.) Then, redirect that output into your users.txt file so you can add the equivalent Git user data next to each entry. 370 | 371 | You can provide this file to `git svn` to help it map the author data more accurately. You can also tell `git svn` not to include the metadata that Subversion normally imports, by passing `--no-metadata` to the `clone` or `init` command. This makes your `import` command look like this: 372 | 373 | $ git-svn clone http://my-project.googlecode.com/svn/ \ 374 | --authors-file=users.txt --no-metadata -s my_project 375 | 376 | Now you should have a nicer Subversion import in your `my_project` directory. Instead of commits that look like this 377 | 378 | commit 37efa680e8473b615de980fa935944215428a35a 379 | Author: schacon 380 | Date: Sun May 3 00:12:22 2009 +0000 381 | 382 | fixed install - go to trunk 383 | 384 | git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de- 385 | be05-5f7a86268029 386 | they look like this: 387 | 388 | commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2 389 | Author: Scott Chacon 390 | Date: Sun May 3 00:12:22 2009 +0000 391 | 392 | fixed install - go to trunk 393 | 394 | Not only does the Author field look a lot better, but the `git-svn-id` is no longer there, either. 395 | 396 | You need to do a bit of `post-import` cleanup. For one thing, you should clean up the weird references that `git svn` set up. First you’ll move the tags so they’re actual tags rather than strange remote branches, and then you’ll move the rest of the branches so they’re local. 397 | 398 | To move the tags to be proper Git tags, run 399 | 400 | $ cp -Rf .git/refs/remotes/tags/* .git/refs/tags/ 401 | $ rm -Rf .git/refs/remotes/tags 402 | 403 | This takes the references that were remote branches that started with `tag/` and makes them real (lightweight) tags. 404 | 405 | Next, move the rest of the references under `refs/remotes` to be local branches: 406 | 407 | $ cp -Rf .git/refs/remotes/* .git/refs/heads/ 408 | $ rm -Rf .git/refs/remotes 409 | 410 | Now all the old branches are real Git branches and all the old tags are real Git tags. The last thing to do is add your new Git server as a remote and push to it. Here is an example of adding your server as a remote: 411 | 412 | $ git remote add origin git@my-git-server:myrepository.git 413 | 414 | Because you want all your branches and tags to go up, you can now run this: 415 | 416 | $ git push origin --all 417 | 418 | All your branches and tags should be on your new Git server in a nice, clean import. 419 | 420 | ### Perforce ### 421 | 422 | The next system you’ll look at importing from is Perforce. A Perforce importer is also distributed with Git, but only in the `contrib` section of the source code — it isn’t available by default like `git svn`. To run it, you must get the Git source code, which you can download from git.kernel.org: 423 | 424 | $ git clone git://git.kernel.org/pub/scm/git/git.git 425 | $ cd git/contrib/fast-import 426 | 427 | In this `fast-import` directory, you should find an executable Python script named `git-p4`. You must have Python and the `p4` tool installed on your machine for this import to work. For example, you’ll import the Jam project from the Perforce Public Depot. To set up your client, you must export the P4PORT environment variable to point to the Perforce depot: 428 | 429 | $ export P4PORT=public.perforce.com:1666 430 | 431 | Run the `git-p4 clone` command to import the Jam project from the Perforce server, supplying the depot and project path and the path into which you want to import the project: 432 | 433 | $ git-p4 clone //public/jam/src@all /opt/p4import 434 | Importing from //public/jam/src@all into /opt/p4import 435 | Reinitialized existing Git repository in /opt/p4import/.git/ 436 | Import destination: refs/remotes/p4/master 437 | Importing revision 4409 (100%) 438 | 439 | If you go to the `/opt/p4import` directory and run `git log`, you can see your imported work: 440 | 441 | $ git log -2 442 | commit 1fd4ec126171790efd2db83548b85b1bbbc07dc2 443 | Author: Perforce staff 444 | Date: Thu Aug 19 10:18:45 2004 -0800 445 | 446 | Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into 447 | the main part of the document. Built new tar/zip balls. 448 | 449 | Only 16 months later. 450 | 451 | [git-p4: depot-paths = "//public/jam/src/": change = 4409] 452 | 453 | commit ca8870db541a23ed867f38847eda65bf4363371d 454 | Author: Richard Geiger 455 | Date: Tue Apr 22 20:51:34 2003 -0800 456 | 457 | Update derived jamgram.c 458 | 459 | [git-p4: depot-paths = "//public/jam/src/": change = 3108] 460 | 461 | You can see the `git-p4` identifier in each commit. It’s fine to keep that identifier there, in case you need to reference the Perforce change number later. However, if you’d like to remove the identifier, now is the time to do so — before you start doing work on the new repository. You can use `git filter-branch` to remove the identifier strings en masse: 462 | 463 | $ git filter-branch --msg-filter ' 464 | sed -e "/^\[git-p4:/d" 465 | ' 466 | Rewrite 1fd4ec126171790efd2db83548b85b1bbbc07dc2 (123/123) 467 | Ref 'refs/heads/master' was rewritten 468 | 469 | If you run `git log`, you can see that all the SHA-1 checksums for the commits have changed, but the `git-p4` strings are no longer in the commit messages: 470 | 471 | $ git log -2 472 | commit 10a16d60cffca14d454a15c6164378f4082bc5b0 473 | Author: Perforce staff 474 | Date: Thu Aug 19 10:18:45 2004 -0800 475 | 476 | Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into 477 | the main part of the document. Built new tar/zip balls. 478 | 479 | Only 16 months later. 480 | 481 | commit 2b6c6db311dd76c34c66ec1c40a49405e6b527b2 482 | Author: Richard Geiger 483 | Date: Tue Apr 22 20:51:34 2003 -0800 484 | 485 | Update derived jamgram.c 486 | 487 | Your import is ready to push up to your new Git server. 488 | 489 | ### A Custom Importer ### 490 | 491 | If your system isn’t Subversion or Perforce, you should look for an importer online — quality importers are available for CVS, Clear Case, Visual Source Safe, even a directory of archives. If none of these tools works for you, you have a rarer tool, or you otherwise need a more custom importing process, you should use `git fast-import`. This command reads simple instructions from stdin to write specific Git data. It’s much easier to create Git objects this way than to run the raw Git commands or try to write the raw objects (see Chapter 9 for more information). This way, you can write an import script that reads the necessary information out of the system you’re importing from and prints straightforward instructions to stdout. You can then run this program and pipe its output through `git fast-import`. 492 | 493 | To quickly demonstrate, you’ll write a simple importer. Suppose you work in current, you back up your project by occasionally copying the directory into a time-stamped `back_YYYY_MM_DD` backup directory, and you want to import this into Git. Your directory structure looks like this: 494 | 495 | $ ls /opt/import_from 496 | back_2009_01_02 497 | back_2009_01_04 498 | back_2009_01_14 499 | back_2009_02_03 500 | current 501 | 502 | In order to import a Git directory, you need to review how Git stores its data. As you may remember, Git is fundamentally a linked list of commit objects that point to a snapshot of content. All you have to do is tell `fast-import` what the content snapshots are, what commit data points to them, and the order they go in. Your strategy will be to go through the snapshots one at a time and create commits with the contents of each directory, linking each commit back to the previous one. 503 | 504 | As you did in the "An Example Git Enforced Policy" section of Chapter 7, we’ll write this in Ruby, because it’s what I generally work with and it tends to be easy to read. You can write this example pretty easily in anything you’re familiar with — it just needs to print the appropriate information to stdout. And, if you are running on Windows, this means you'll need to take special care to not introduce carriage returns at the end your lines — git fast-import is very particular about just wanting line feeds (LF) not the carriage return line feeds (CRLF) that Windows uses. 505 | 506 | To begin, you’ll change into the target directory and identify every subdirectory, each of which is a snapshot that you want to import as a commit. You’ll change into each subdirectory and print the commands necessary to export it. Your basic main loop looks like this: 507 | 508 | last_mark = nil 509 | 510 | # loop through the directories 511 | Dir.chdir(ARGV[0]) do 512 | Dir.glob("*").each do |dir| 513 | next if File.file?(dir) 514 | 515 | # move into the target directory 516 | Dir.chdir(dir) do 517 | last_mark = print_export(dir, last_mark) 518 | end 519 | end 520 | end 521 | 522 | You run `print_export` inside each directory, which takes the manifest and mark of the previous snapshot and returns the manifest and mark of this one; that way, you can link them properly. "Mark" is the `fast-import` term for an identifier you give to a commit; as you create commits, you give each one a mark that you can use to link to it from other commits. So, the first thing to do in your `print_export` method is generate a mark from the directory name: 523 | 524 | mark = convert_dir_to_mark(dir) 525 | 526 | You’ll do this by creating an array of directories and using the index value as the mark, because a mark must be an integer. Your method looks like this: 527 | 528 | $marks = [] 529 | def convert_dir_to_mark(dir) 530 | if !$marks.include?(dir) 531 | $marks << dir 532 | end 533 | ($marks.index(dir) + 1).to_s 534 | end 535 | 536 | Now that you have an integer representation of your commit, you need a date for the commit metadata. Because the date is expressed in the name of the directory, you’ll parse it out. The next line in your `print_export` file is 537 | 538 | date = convert_dir_to_date(dir) 539 | 540 | where `convert_dir_to_date` is defined as 541 | 542 | def convert_dir_to_date(dir) 543 | if dir == 'current' 544 | return Time.now().to_i 545 | else 546 | dir = dir.gsub('back_', '') 547 | (year, month, day) = dir.split('_') 548 | return Time.local(year, month, day).to_i 549 | end 550 | end 551 | 552 | That returns an integer value for the date of each directory. The last piece of meta-information you need for each commit is the committer data, which you hardcode in a global variable: 553 | 554 | $author = 'Scott Chacon ' 555 | 556 | Now you’re ready to begin printing out the commit data for your importer. The initial information states that you’re defining a commit object and what branch it’s on, followed by the mark you’ve generated, the committer information and commit message, and then the previous commit, if any. The code looks like this: 557 | 558 | # print the import information 559 | puts 'commit refs/heads/master' 560 | puts 'mark :' + mark 561 | puts "committer #{$author} #{date} -0700" 562 | export_data('imported from ' + dir) 563 | puts 'from :' + last_mark if last_mark 564 | 565 | You hardcode the time zone (-0700) because doing so is easy. If you’re importing from another system, you must specify the time zone as an offset. 566 | The commit message must be expressed in a special format: 567 | 568 | data (size)\n(contents) 569 | 570 | The format consists of the word data, the size of the data to be read, a newline, and finally the data. Because you need to use the same format to specify the file contents later, you create a helper method, `export_data`: 571 | 572 | def export_data(string) 573 | print "data #{string.size}\n#{string}" 574 | end 575 | 576 | All that’s left is to specify the file contents for each snapshot. This is easy, because you have each one in a directory — you can print out the `deleteall` command followed by the contents of each file in the directory. Git will then record each snapshot appropriately: 577 | 578 | puts 'deleteall' 579 | Dir.glob("**/*").each do |file| 580 | next if !File.file?(file) 581 | inline_data(file) 582 | end 583 | 584 | Note: Because many systems think of their revisions as changes from one commit to another, fast-import can also take commands with each commit to specify which files have been added, removed, or modified and what the new contents are. You could calculate the differences between snapshots and provide only this data, but doing so is more complex — you may as well give Git all the data and let it figure it out. If this is better suited to your data, check the `fast-import` man page for details about how to provide your data in this manner. 585 | 586 | The format for listing the new file contents or specifying a modified file with the new contents is as follows: 587 | 588 | M 644 inline path/to/file 589 | data (size) 590 | (file contents) 591 | 592 | Here, 644 is the mode (if you have executable files, you need to detect and specify 755 instead), and inline says you’ll list the contents immediately after this line. Your `inline_data` method looks like this: 593 | 594 | def inline_data(file, code = 'M', mode = '644') 595 | content = File.read(file) 596 | puts "#{code} #{mode} inline #{file}" 597 | export_data(content) 598 | end 599 | 600 | You reuse the `export_data` method you defined earlier, because it’s the same as the way you specified your commit message data. 601 | 602 | The last thing you need to do is to return the current mark so it can be passed to the next iteration: 603 | 604 | return mark 605 | 606 | NOTE: If you are running on Windows you'll need to make sure that you add one extra step. As metioned before, Windows uses CRLF for new line characters while git fast-import expects only LF. To get around this problem and make git fast-import happy, you need to tell ruby to use LF instead of CRLF: 607 | 608 | $stdout.binmode 609 | 610 | That’s it. If you run this script, you’ll get content that looks something like this: 611 | 612 | $ ruby import.rb /opt/import_from 613 | commit refs/heads/master 614 | mark :1 615 | committer Scott Chacon 1230883200 -0700 616 | data 29 617 | imported from back_2009_01_02deleteall 618 | M 644 inline file.rb 619 | data 12 620 | version two 621 | commit refs/heads/master 622 | mark :2 623 | committer Scott Chacon 1231056000 -0700 624 | data 29 625 | imported from back_2009_01_04from :1 626 | deleteall 627 | M 644 inline file.rb 628 | data 14 629 | version three 630 | M 644 inline new.rb 631 | data 16 632 | new version one 633 | (...) 634 | 635 | To run the importer, pipe this output through `git fast-import` while in the Git directory you want to import into. You can create a new directory and then run `git init` in it for a starting point, and then run your script: 636 | 637 | $ git init 638 | Initialized empty Git repository in /opt/import_to/.git/ 639 | $ ruby import.rb /opt/import_from | git fast-import 640 | git-fast-import statistics: 641 | --------------------------------------------------------------------- 642 | Alloc'd objects: 5000 643 | Total objects: 18 ( 1 duplicates ) 644 | blobs : 7 ( 1 duplicates 0 deltas) 645 | trees : 6 ( 0 duplicates 1 deltas) 646 | commits: 5 ( 0 duplicates 0 deltas) 647 | tags : 0 ( 0 duplicates 0 deltas) 648 | Total branches: 1 ( 1 loads ) 649 | marks: 1024 ( 5 unique ) 650 | atoms: 3 651 | Memory total: 2255 KiB 652 | pools: 2098 KiB 653 | objects: 156 KiB 654 | --------------------------------------------------------------------- 655 | pack_report: getpagesize() = 4096 656 | pack_report: core.packedGitWindowSize = 33554432 657 | pack_report: core.packedGitLimit = 268435456 658 | pack_report: pack_used_ctr = 9 659 | pack_report: pack_mmap_calls = 5 660 | pack_report: pack_open_windows = 1 / 1 661 | pack_report: pack_mapped = 1356 / 1356 662 | --------------------------------------------------------------------- 663 | 664 | As you can see, when it completes successfully, it gives you a bunch of statistics about what it accomplished. In this case, you imported 18 objects total for 5 commits into 1 branch. Now, you can run `git log` to see your new history: 665 | 666 | $ git log -2 667 | commit 10bfe7d22ce15ee25b60a824c8982157ca593d41 668 | Author: Scott Chacon 669 | Date: Sun May 3 12:57:39 2009 -0700 670 | 671 | imported from current 672 | 673 | commit 7e519590de754d079dd73b44d695a42c9d2df452 674 | Author: Scott Chacon 675 | Date: Tue Feb 3 01:00:00 2009 -0700 676 | 677 | imported from back_2009_02_03 678 | 679 | There you go — a nice, clean Git repository. It’s important to note that nothing is checked out — you don’t have any files in your working directory at first. To get them, you must reset your branch to where `master` is now: 680 | 681 | $ ls 682 | $ git reset --hard master 683 | HEAD is now at 10bfe7d imported from current 684 | $ ls 685 | file.rb lib 686 | 687 | You can do a lot more with the `fast-import` tool — handle different modes, binary data, multiple branches and merging, tags, progress indicators, and more. A number of examples of more complex scenarios are available in the `contrib/fast-import` directory of the Git source code; one of the better ones is the `git-p4` script I just covered. 688 | 689 | ## Summary ## 690 | 691 | You should feel comfortable using Git with Subversion or importing nearly any existing repository into a new Git one without losing data. The next chapter will cover the raw internals of Git so you can craft every single byte, if need be. 692 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Pro Git Grean Edition 2 | ===================== 3 | 4 | Repository นี้เป็นคลังกลางของการแปลงหนังสือ Pro Git [(ต้นฉบับภาษาอังกฤษ)](https://github.com/progit/progit) เป้าหมายคือการส่งเสริมการเรียนรู้เทคโนโลยีการพัฒนาซอฟต์แวร์เป็นภาษาไทย 5 | 6 | ทีมงานผู้แปลมีความตั้งใจที่จะแปลหนังสือเล่มนี้ด้วยสำนวนที่ไม่เป็นทางการ พร้อมทั้งตั้งใจว่าอยากให้ผู้ที่อยากเริ่มเรียน Git สามารถเรียนรู้ผ่านสำนวนการแปลที่สนุกสนานและหวังว่าจะช่วยให้ผู้อ่านสามารถเรียนรู้ Git ได้ดียิ่งขึ้น 7 | 8 | สัญญาอนุญาต 9 | ===================== 10 | 11 | หนังสือเล่มนี้อยู่ภายใต้สัญญาอนุญาต[ครีเอทีฟคอมมอนส์ แสดงที่มา-ไม่ใช่เพื่อการ-อนุญาตแบบเดียวกัน 3.0 ประเทศไทย (CC BY-NC-SA 3.0)](http://creativecommons.org/licenses/by-nc-sa/3.0/th/) --------------------------------------------------------------------------------