มาเรียนรู้ Git แบบง่ายๆกันเถอะ

นี่ก็ปี 2017 แล้วนะ ยังไม่ใช้ Git อีกหรอ?

เนื่องจากทุกวันนี้ผมใช้ Git ในการทำงานอยู่แล้ว และบางครั้งก็ต้องสอนเรื่องนี้ให้กับสมาชิกใหม่ภายในทีม ก็เลยตัดสินใจเขียนบทความนี้ขึ้นมาซะเลย เพื่อที่จะได้แชร์ความรู้ให้กับ Developer ที่สนใจใน Git แต่ยังไม่เข้าใจหรือไม่รู้ว่าจะทำความเข้าใจยังไงดี เพราะทุกวันนี้โลกของ Developer นั้นไปไวเหลือเกิน หลายๆอย่างถ้าเราไม่รู้จักหรือใช้ไม่เป็นก็อาจจะทำให้พลาดโอกาสดีๆในเส้นทางนี้ก็เป็นได้

เนื้อหาของ Git ในบทความนี้ จะเน้นสำหรับสายโค้ดเป็นหลักนะจ๊ะ

มาพูดถึง Version Control Systems (VCS) กันก่อน

ในยุคนี้ต้องบอกเลยว่า Version Control ถือว่าเป็นหนึ่งในคุณสมบัติพื้นฐานของ Developer ก็ว่าได้ โดยเฉพาะอย่างยิ่งบริษัทที่ต้องทำงานร่วมกันเป็นทีม (ถึงจะพัฒนาคนเดียว ก็ควรจะใช้เหมือนกันนะ)

แล้ว Version Control มันสำคัญยังไง?

ลองนึกดูว่าถ้าคุณต้องพัฒนาโปรเจคขนาดใหญ่ที่มี Developer 4 คนที่กำลังมะรุมมะตุ้มโค้ดในโปรเจคนี้อยู่ คุณจะใช้วิธีไหนเพื่อเอาโค้ดที่แต่ละคนเขียนมารวมเข้าด้วยกันในโปรเจค

4 Developer กับ 4 Feature ระดับเทพ

วิธีสุดเก่าแก่ที่ใช้กันก็คือ — ก๊อปโปรเจคจากแต่ละคนมารวมไว้ในเครื่องเดียวกัน แล้วนั่งรวมหัวกัน มีคนนึงทำหน้าที่เปิดโค้ดของแต่ละคนขึ้นมา สมมติว่าคนๆนั้นคือนาย A และนาย B เป็นคนเขียนโค้ดที่กำลังจะรวมไว้ในโปรเจคเดียวกัน นาย A ก็เลยต้องถามนาย B ว่าเขียนโค้ดตรงไหนเพิ่มบ้าง แล้วค่อยเอาไปแปะรวมไว้ในโปรเจคหลัก

ปัญหาเก่าๆที่เกิดขึ้นอยู่ประจำก็คือ โค้ดที่นาย B ไปแก้ไข ดันไปทับซ้อนกับนาย C เพราะนาย C ก็แก้ไขโค้ดตรงจุดนั้นเหมือนกัน กลายเป็นว่านาย B ก็ต้องไปเรียกนาย C มานั่งคุยด้วย เพื่อบอกให้นาย A แก้ไขให้โค้ดของนาย B และนาย C ทำงานร่วมกันได้

รวมโค้ดของนาย A และ B เข้าด้วยกันก่อน ส่วนนาย C กับ D นั่งรอยาวๆไปนะ

นี่ยังไม่รวมไปถึงกรณีที่นาย B จำไม่ได้ว่าตัวเองแก้ไขโค้ดตรงจุดไหนไปบ้าง เพราะฟีเจอร์ที่ทำนั้นใช้เวลาหลายวันและเขียนโค้ดหลายบรรทัด กลายเป็นว่าต้องนั่งรวมหัวกันไล่ดูโค้ดว่าพลาดตรงไหนไปหรือป่าว ซึ่งรวมไปถึงปัญหาอื่นๆที่จะตามมาอีกมากมาย

แค่คิดตามก็ฟังดูวุ่นวายแล้วใช่มั้ยล่ะ?

นั่นล่ะครับ ที่ทำให้เกิดสิ่งที่เรียกว่า Version Control ขึ้นมาเพื่อควบคุมการเปลี่ยนแปลงของโค้ดในโปรเจค โดยประโยชน์ที่เห็นได้ชัดของ Version Control จะมีดังนี้

  • เก็บประวัติการแก้ไขโค้ดไว้ทุกครั้ง และรู้ได้ว่าโค้ดตรงไหนใครเป็นคนเพิ่มเข้ามาหรือแก้ไข
  • ช่วยรวมโค้ดจากหลายๆคนเข้าด้วยกันให้ง่ายขึ้น ดูได้ว่าโค้ดเดิมคืออะไร และแก้ไขเป็นอะไร
  • เมื่อเกิดปัญหาก็สามารถติดตามดูประวัติการแก้ไขโค้ดในแต่ละไฟล์แต่ละบรรทัดได้ง่าย
  • ช่วยให้สามารถจัดการโปรเจคได้อย่างเป็นระบบ ไม่แก้โค้ดสะเปะสะปะ มองโค้ดแต่ละส่วนเป็นฟีเจอร์ ไม่เขียนโค้ดข้ามฟีเจอร์ไปมาในโค้ดชุดเดียวกัน
  • เป็น Backup ไปในตัว ไม่ต้องกลัวเวลาโค้ดมีปัญหาแล้วต้อง Rollback กลับไปใช้โค้ดชุดเก่า และใช้พื้นที่ในการเก็บข้อมูลน้อยเมื่อเทียบกับการ Backup แบบเก็บทั้งโปรเจคไว้ทุกครั้งที่ทำการ Backup
  • สามารถ Track การทำงานของทุกคนภายในทีมได้จาก History

โดย Git นั้นเป็นหนึ่งใน Version Control แบบ Distributed Version Control Systems ที่นิยมใช้งานกันในปัจจุบัน

Git ทำงานยังไง?

จริงๆมัน็คือการทำงานแบบ Distributed Version Control Systems นั่นแหละ (อยากรู้รายละเอียดมากกว่านี้ก็สามารถเอา Keyword นี้ไปหาข้อมูลเพิ่มเติมได้เลย)

https://git-scm.com/images/branching-illustration@2x.png

ให้ลองนึกถึงภาพว่ามี Server กลางตัวหนึ่งที่คอยเก็บข้อมูลจากผู้ใช้แต่ละเครื่องก่อนนะครับ

ให้นึกภาพแบบนี้ก่อน

Server กลางผมเรียกมันว่า Remote 1 ส่วน Developer ที่ทำโปรเจคนี้อยู่จะเรียกว่า Computer 1 และ 2 ซึ่งแต่ละคนก็ทำคนละ Feature อยู่ ซึ่ง Feature ของแต่ละคนนั้นก็จะถูกเก็บไว้ที่ Remote 1

อาจจะดูเหมือนว่ามันเป็นระบบ Server กลางที่คอย Backup ข้อมูลของทุกคนอยู่ตลอดเวลา แต่ในความเป็นจริง Git นั้นมีอะไรมากกว่านี้อีกเยอะ

Git ออกแบบมาให้ทำงานกระจายแบบไม่มีศูนย์กลาง ทุกเครื่องทำงานเป็น VCS ด้วยตัวเองได้

นั่นหมายความว่าไม่จำเป็นต้องมี Server กลางก็ได้ สามารถใช้เครื่องส่วนตัวทำเป็น VCS ได้เลย แต่ถ้าต้องทำงานร่วมกันหลายๆเครื่อง ก็ต้องใช้ Server เป็นตัวกลางในการรวมข้อมูล

แต่ละเครื่องทำงานเป็น VCS อยู่แล้ว

ซึ่งจะทำให้ข้อมูลของเราไม่ผูกขาดกับ Server จนเกินไป ในเวลาที่ Server กลางมีปัญหาหรือว่าทำงานแบบ Offline เราก็ยังคงทำงานได้อยู่ โดยใช้ข้อมูลจาก VCS ภายในเครื่องตัวเอง พอเชื่อมต่อกับ Server กลางก็ค่อย Sync ข้อมูลทีหลังได้

และเมื่อทุกๆเครื่องทำงานเป็น VCS อยู่แล้ว จึงทำให้เราสามารถมี Server กลางมากกว่า 1 ตัวได้เช่นกัน (แต่ไม่ค่อยเจอการใช้งานแบบนี้มากนัก)

สามารถใช้ Server หลายตัวทำเป็น VCS ได้เหมือนกันนะ

Git จะ Sync ข้อมูลเมื่อเราสั่งเท่านั้น

ขั้นตอนการ Sync ข้อมูลของ Git เดี๋ยวจะพูดถึงในภายหลัง ขอเน้นไปที่ภาพรวมของการทำงานก่อน

เวลาที่เราอยากจะ Sync ข้อมูลจาก Remote มาลงเครื่อง หรือจากเครื่องส่งขึ้นไปยัง Remote ก็จะต้องเป็นคนจัดการเอง ไม่ได้ทำการ Sync ให้โดยอัตโนมัติ เพราะว่ามันจะมีเรื่องจุกจิกที่เราต้องเป็นคนตัดสินใจและเลือกเอง ไม่สามารถให้ตัว Git จัดการให้อัตโนมัติได้

ยกตัวอย่างเช่น โค้ดจากนักพัฒนา 2 คนที่ดันไปแก้ไขที่บรรทัดเดียวกัน แต่คำสั่งต่างกัน และจุดประสงค์ของโค้ดต่างกัน แล้ว Git จะไปตรัสรู้ได้ยังไงล่ะว่าต้องเอาโค้ดแบบไหน?

ใครไวกว่าก็สบายไป

และนั่นก็คือหน้าที่ของนักพัฒนาที่จะต้องเป็นคนตัดสินใจหรือแก้ไขโค้ดนั้นเองครับ โดยคนที่ Sync ข้อมูลขึ้น Remote ก่อนจะไม่ต้องทำอะไร และคนที่ Sync ข้อมูลทีหลังจะต้องเป็นคนจัดการเอง ซึ่งสามารถตัดสินใจได้ว่าจะใช้โค้ดของคนแรก หรือจะใช้โค้ดของตัวเอง หรือว่าจะแก้ไขโค้ดเพื่อให้รองรับโค้ดของทั้งสองคนก็ทำได้เช่นกัน

ต้องดึงโค้ดมารวมกับโค้ดของตัวเองให้เรียบร้อยก่อน ถึงจะ Sync ขึ้น Remote ได้

ดังนั้น Git จะไม่ได้ทำงานอัตโนมัติโดยสมบูรณ์แบบ เพราะมันยังมีเรื่องละเอียดละอ่อนอีกหลายๆอย่างที่ผู้ใช้อย่างเราๆต้องเข้าใจเพื่อที่จะได้จัดการกับโค้ดในโปรเจคได้อย่างเหมาะสม

การเรียกใช้งาน Git

มีอยู่ 2 แบบหลักๆก็คือ Command Line กับ GUI

  • Command Line : พิมพ์คำสั่งของ Git จาก Terminal หรือ Command Prompt โดยตรงนั่นเอง
สายโหดก็จัดแบบ Command Line กันไป
ขี้เกียจพิมพ์ก็ใช้ Git GUI อย่าง SourceTree เอา

และ IDE บางตัวก็มี Git GUI ให้ในตัวเลยนะ ไม่ต้องไปลงเพิ่มให้เสียเวลา

Android Studio ก็มี
Xcode ก็มา

Server สำหรับให้บริการ Git (Version Control Repository Hosting Service)

การใช้ Git ให้เกิดประโยชน์ที่สุดก็คงต้องมี Server ที่ทำหน้าที่เป็น VCS ด้วย ซึ่งในปัจจุบันนี้ก็มีทางเลือกมากมายไม่ว่าจะใช้บริการจากเว็ปต่างๆหรือตั้ง Server ขึ้นมาเอง

ในกรณีเริ่มต้นผมขอแนะนำให้ใช้ Server ที่ให้บริการ Git ไปเลย เพราะเราไม่ต้องจัดการอะไรมาก แค่สมัครก็เข้าไปใช้งานได้เลย โดยเว็ปไซต์ยอดนิยมก็จะมีดังนี้

ซึ่งเว็ปเหล่านี้จะมีให้บริการทั้งแบบ Public (ใครๆก็เข้ามาดูโค้ดของเราได้)และ Private (เฉพาะคนที่เราอนุญาตเท่านั้น) ถ้าไม่ได้ซีเรียสอะไรมากและอยากทำเป็น Open Source อยู่แล้ว ก็แนะนำ GitHub ครับ

GitHub บ้านหลังแรกสำหรับผู้ใช้ Git ทุกๆคน

คำต่างๆที่จะต้องรู้จักเมื่อใช้งาน Git (ชุดที่ 1)

ขอแบ่งเป็นหลายๆชุด เพื่อที่จะได้อธิบายการทำงานทีละส่วนให้เข้าใจได้ง่ายขึ้น

  • Repository
  • Clone
  • Commit
  • Unstaged
  • Staged
  • Push
  • Pull
  • Fetch
  • Conflict
  • Merge Commit

Repository

เวลาที่เราพัฒนาโปรแกรมซักตัว เราจะต้องสร้างสิ่งที่เรียกว่า Project ซึ่งการสร้าง Project สำหรับใช้งาน Git จะเรียกว่า Repository ครับ

ซึ่ง Repository ของ Git จริงๆแล้วก็คือ Folder ที่ใช้เก็บข้อมูลนั่นเอง อยากจะเก็บอะไรไว้ในนั้นก็ยัดเข้าไปได้เลย (ในความเป็นจริง 1 Repository สามารถเก็บ Project เท่าไรก็ได้ตามที่ต้องการ แต่ส่วนใหญก็นิยมเก็บ Project 1 ตัวต่อ 1 Repository)

เวลาที่ผู้อ่านเปิดดูโค้ดต่างๆของนักพัฒนาบน GitHub (ยกตัวอย่างเช่น https://github.com/google/volley) ในแต่ละโปรเจคที่เราเห็นนั่นแหละครับ ที่เค้าเรียกว่า Repository

Clone

เวลาที่ผู้อ่านมี Repository อยู่บน Remote ซักแห่งอยู่แล้ว และต้องการ Sync มาลงเครื่องของเรา เราจะต้องทำสิ่งที่เรียกว่า Clone Repository หรือก็คือการก๊อป Repository จาก Remote มาลงเครื่องนั่นเอง

Commit

เวลาที่มีข้อมูลที่แก้ไขเสร็จแล้ว (โค้ดที่เขียนคำสั่งบางอย่างเสร็จแล้ว) แล้วอยากจะทำการ Backup เก็บไว้ใน VCS จะเรียกกันว่า Commit ครับ

ซึ่งการ Commit จะสามารถเลือกได้ว่าจะเอาไฟล์ไหนบ้าง (ไม่จำเป็นต้องเลือกทุกไฟล์)

เขียนโค้ดเสร็จชุดหนึ่งก็ทำการ Commit เก็บไว้ เขียนชุดต่อไปก็ Commit เก็บไว้อีก ทำเช่นนี้ไปเรื่อยๆ

ซึ่งเบื้องหลังของ Commit ก็คือในแต่ละครั้งที่ทำการ Commit มันจะจำแค่ว่ามีตรงไหนของข้อมูลที่ถูกเปลี่ยนแปลงไปเรื่อยๆ ดังนั้นในแต่ละ Commit มันจะไม่มีไฟล์ข้อมูลฉบับเต็ม แต่มันสามารถย้อนดู History ได้ว่ามีการแก้ไขอะไรบ้าง ทำให้รู้ว่าใน Commit นั้นๆ แต่ละไฟล์มีข้อมูลเป็นอย่างไร

แต่ละ Commit จะจำไว้ว่าไฟล์ไหนมีการแก้ไขอะไรบ้าง — ภาพจากใน SourceTree

และในการ Commit แต่ละครั้ง จะต้องใส่ Commit Message ด้วย เพื่ออธิบายรายละเอียดของข้อมูลใน Commit นั้นๆว่าเราทำอะไรไปบ้าง เพื่อที่มาดูในภายหลังจะได้อ่านจาก Commit Message ได้เลย ไม่ต้องไปนั่งกดดูเองทีละไฟล์ ดังนั้นการ Commit ที่ดีจึงควรใส่ใจกับ Commit Message ด้วยนะ

Unstaged และ Staged

ขอพูดถึง 2 คำนี้ไปพร้อมๆกันเลยละกัน

เวลาเราแก้ไขโค้ดหรือแก้ไขข้อมูล ไฟล์ที่ถูกแก้ไขจะอยู่ในสถานะ Unstaged และเวลาที่เราทำอะไรเสร็จเรียบร้อย แล้วอยากจะ Commit เก็บไว้ จะต้องเลือกไฟล์ที่ต้องการเพื่อย้ายเข้าสู่ในสถานะ Staged ก่อนถึงจะทำการ Commit ได้

ซึ่งสถานะ Unstaged และ Staged ทำให้เราสามารถเลือกเฉพาะบางไฟล์สำหรับ Commit ได้นั่นเอง จะได้ Commit เฉพาะไฟล์ที่เราเขียนเสร็จแล้ว

แก้ไขโค้ด (ไฟล์จะอยู่ใน Unstaged อัตโนมัติ) > เลือกไฟล์ที่ต้องการเข้า Staged > Commit

Push

เวลาที่มี Commit อยู่ในเครื่องและต้องการจะ Sync ขึ้นไปเก็บไว้ใน Remote จะเรียกขั้นตอนนี้ว่า Push

Push : ส่ง Commit จาก Client ขึ้นไปเก็บไว้ที่ Remote ด้วย

Pull

เวลา Sync จาก Remote เพื่อดึงข้อมูล Commit ใหม่ๆลงมาเก็บไว้ในเครื่องจะเรียกขั้นตอนนี้ว่า Pull

Pull : ดึง Commit ใหม่ๆจาก Remote มาเก็บไว้ใน Client ด้วย

Fetch

ในบางครั้งเราอาจจะไม่ต้องการ Pull ข้อมูลลงมาเก็บไว้ในเครื่องทันที แค่อยากเช็คสถานะของ Remote เฉยๆว่ามีใคร Push ข้อมูลใหม่ขึ้นไปที่ Remote หรือป่าว เราเรียกวิธีนี้ว่า Fetch ครับ ซึ่งการ Fetch จะทำให้เราสามารถอัพเดทและดู History ทั้งหมดที่อยู่บน Remote ได้ โดยไม่ต้องดึงข้อมูลลงมา

Fetch : อยากรู้ว่ามี Commit ใหม่ๆบน Remote มั้ย แต่ไม่ต้องดึงข้อมูล

ซึ่งจริงๆแล้วขั้นตอนการ Fetch จะถูกเรียกทุกครั้งที่ทำการ Pull ดังนั้นเราจึงสามารถเลือกได้ครับว่าจะ Pull เลย (เดี๋ยวมันก็ Fetch เพื่อเช็คเองอยู่ดี) หรือจะ Fetch ดูก่อนว่ามี Commit อะไรเพิ่มเข้ามา แล้วค่อย Pull ลงมา

Merge Commit

สมมติว่านาย A กับนาย B เขียนโค้ดด้วยกันอยู่ และทั้งคู่ก็เขียนโค้ดที่อยู่ในไฟล์เดียวกัน

นาย A เพิ่ม method2 ต่อท้าย method1 ส่วนนาย B ลบ method1 ออก แล้วเพิ่ม method3 กับ method4 เข้าไป

นาย B เขียนโค้ดเสร็จแล้ว ก็เลย Commit ไฟล์ที่ตัวเองเขียนเสร็จแล้ว Push ขึ้น Remote

นาย B ทำการ Push ข้อมูลขึ้น Remote

นาย A เขียนโค้ดเสร็จทีหลัง และจะ Push ขึ้น Remote แต่พบว่านาย B ได้ Push ขึ้นไปก่อนแล้ว จึง Push ขึ้นไปทันทีไม่ได้

“Push ก่อนได้เปรียบ” นาย B ไม่ได้กล่าวไว้

ดังนั้นสิ่งที่นาย A ต้องทำก่อนที่จะ Push ของตัวเองขึ้นไปได้ ก็คือจะต้อง Pull จาก Remote ลงมาใหม่ก่อนเพื่ออัพเดต Commit ที่นาย B ได้ Push ขึ้นไป ซึ่งเราเรียกขั้นตอนนี้ว่า Merge Commit นั่นเอง

โค้ดของนาย A และ B แก้ที่บรรทัดเดียวกัน แต่โค้ดไม่เหมือนกัน

ในกรณีที่โค้ดสามารถ Merge กันได้ปกติ ยกตัวอย่างเช่น

// โค้ดเดิม
void main() {
method1();
}
// โค้ดที่นาย A แก้ไข
void main() {
method2();
method1();
}
// โค้ดที่นาย B แก้ไข
void main() {
method1();
method3();
}

เวลา Merge เข้าด้วยกันจะได้ผลลัพธ์ออกมาเป็นแบบนี้

void main() {
method2(); // โค้ดของนาย A
method1();
method3(); // โค้ดของนาย B
}

ซึ่งนี่คือกรณีที่โค้ดสามารถ Merge รวมกันได้โดยไม่มีปัญหาอะไรครับ เมื่อ Merge เสร็จแล้ว มันก็จะกลายเป็น Commit ตัวหนึ่งที่ให้เราเก็บไว้และ Push ขึ้น Remote

เพิ่มเติม : จริงๆ Git สามารถ Auto Merge ได้เลย (กด Pull ปุ๊ป > Merge ไฟล์ปั๊ป > Commit ให้ทันที) แต่ผมก็แนะนำว่าให้ Manual Merge ดีกว่า เพื่อที่ว่าจะได้ Review Code ไปในตัวด้วย

แต่ทว่าโค้ดที่นาย A กับนาย B เขียนมันไม่ใช่แบบนั้นน่ะสิ

Conflict

ในขณะที่กำลังจะ Merge อยู่นั้น จะเห็นว่าโค้ดของนาย A และนาย B มันชนกัน ไปแก้โค้ดที่เดียวกัน ดังนั้น Git จะแจ้งว่าเกิด Conflict หรือก็คือโค้ดที่ทับซ้อนกันนั่นเอง

Conflict กันแน่นอน~

ซึ่งนาย A ก็ต้องแก้ Conflict นี้ให้เรียบร้อยซะ ถึงจะ Merge Commit แล้ว Push ขึ้น Remote ได้ ซึ่งตอนที่เกิด Conflict ขึ้น แล้วเปิดโค้ดดังกล่าวมาดู ก็จะเห็นเป็นแบบนี้

void main() {
<<<<<<< HEAD
method1();
method2();
=======
method3();
method4();
>>>>>>> 322d39a003e4d9...
}

ให้สังเกตรูปแบบของเครื่องหมาย <<<, === และ >>> ให้ดีๆ แล้วจะเห็นว่าจริงๆมันมีรูปแบบที่เข้าใจได้ง่ายมาก โดยที่

  • โค้ดที่อยู่ระหว่าง <<< และ === คือโค้ดของนาย A
  • โค้ดที่อยู่ระหว่าง === และ >>> คือโค้ดของนาย B ส่วนตัวเลขต่อท้ายคือหมายเลขของ Commit ที่ทำการ Merge

เพื่อให้โค้ดทำงานได้เหมาะสม นาย A ก็จะต้องเอาโค้ดที่ตัวเองแก้ไข ไปรวมกับโค้ดของนาย B ให้ทำงานได้

void main() {
method2();
method3();
method4();
}

เมื่อเสร็จแล้วก็ให้ Commit (ซึ่งจะเป็นการ Merge Commit) แล้วนาย A ก็จะสามารถ Push ขึ้น Remote ได้แล้ว (นาย B ก็จะต้อง Pull Commit ตัวนี้ทีหลัง และถ้าเผลอไปแก้ไขอะไรที่เดิมซ้ำก็อาจจะเกิด Conflict ได้)

Conflict ถือว่าเป็นเหตุการณ์ที่เกิดขึ้นได้เป็นปกติ โดยเฉพาะอย่างยิ่งโปรเจคใหญ่ๆที่มี Developer หลายๆคนช่วยกันรุมเขียนโค้ด ดังนั้นการแก้ Conflict จึงเป็นหนึ่งในพื้นฐานของการใช้งาน Git ที่นักพัฒนาต้องเข้าใจและจัดการกับมันได้ ไม่เช่นนั้นจะเกิดปัญหาอย่างเช่น เผลอไปลบโค้ดของเพื่อน โดยไม่สนใจอะไร เพื่อให้ Conflict หายไป เป็นต้น

เมื่อสรุปขั้นตอนของการแก้ Conflict จะเป็นแบบนี้

  • Pull จาก Remote ลงเครื่อง
  • Conflict เกิดขึ้น
  • แก้ไขโค้ดให้เหมาะสม
  • Merge Commit
  • Push ขึ้น Remote

สรุปวัฏจักรของ Git ในชุดแรก

เวลาพัฒนาโปรแกรมซักตัวก็จะมีขั้นตอนแบบนี้

  • เขียนโค้ด
  • เลือกไฟล์ที่ต้องการเข้า Staged
  • Commit
  • เขียนโค้ดต่อ
  • เลือกไฟล์ที่ต้องการเข้า Staged
  • Commit
  • วนลูปไปเรื่อยๆจนกว่างานจะเสร็จ

เวลาจะ Sync ข้อมูลไปที่ Remote ก็จะมีขั้นตอนแบบนี้

  • Fetch เพื่อเช็คว่ามีใคร Push อะไรขึ้น Remote หรือป่าว (ไม่จำเป็นต้องทำก็ได้)
  • ถ้ามีก็จะต้อง Pull โค้ดก่อน แล้ว Merge Commit ให้เรียบร้อย
  • Push ข้อมูลขึ้น Remote

เวลาจะ Sync ข้อมูลไปที่ Remote แบบมี Conflict ก็จะมีขั้นตอนแบบนี้

  • Fetch เพื่อเช็คว่ามีใคร Push อะไรขึ้น Remote หรือป่าว (ไม่จำเป็นต้องทำก็ได้)
  • Pull เพื่อดึงข้อมูลมาไว้ในเครื่อง
  • เกิด Conflict
  • แก้ไขโค้ดให้เหมาะสม
  • Merge Commit
  • Push ข้อมูลขึ้น Remote

เขียนโค้ดไปจนถึงเมื่อไรถึงจะต้อง Commit?

อันนี้น่าจะเป็นคำถามที่เกิดขึ้นบ่อยกับนักพัฒนามือใหม่ที่กำลังเริ่มต้นเรียนรู้และหัดใช้งาน Git กับโปรเจคของตัวเอง

ในการ Commit แต่ละครั้งถึงแม้ว่าจะไม่ได้มีใครมากำหนดมาตรฐาน แต่การ Commit ที่ดีควรทำให้สามารถไล่โค้ดทีหลังได้ง่าย สามารถทำงานร่วมกับคนอื่นได้ และต้องทำเหมือนเดิมทุกครั้งจนเป็นนิสัย ไม่ใช่ว่านึกออกเมื่อไรก็ค่อย Commit

สำหรับผม การ Commit ที่ดีต้องกระชับและเล็กมากพอที่จะไล่ดูใน History ได้ง่ายๆ เวลามีโปรเจคตัวหนึ่ง ผมจะแยกสิ่งที่ต้องทำก็จะแบ่งออกมาเป็น Feature แล้วก็แบ่งย่อยลงไปอีกให้เป็นระดับ Function ถึงจะเริ่มเขียนโค้ด

เมื่อทำในแต่ละ Function เสร็จก็จะ Commit ให้เรียบร้อย แล้วก็ทำ Function ต่อไป และวนแบบนี้ไปเรื่อยๆ จน Feature นั้นเสร็จ

ไม่แนะนำให้ทำในระดับ Feature เสร็จแล้วค่อย Commit เพราะการทำแบบนั้นคุณจะใช้ประโยชน์ Git ได้แค่การ Backup เท่านั้น แต่คุณจะไล่ดู History เวลาแก้ไขโค้ดได้ยาก และไม่สามารถย้อนข้อมูลกลับได้เลย เพราะมันจะย้อนกลับไปทั้งหมดเลย โค้ดที่เขียนไว้ทั้งหมดก็จะหายไปพร้อมๆกัน

และก็ไม่แนะนำให้ Commit โค้ดที่ยังเขียนไม่เสร็จเช่นกัน โค้ดที่เขียนไม่เสร็จ ณ ที่นี้หมายถึงโค้ดที่ไม่สามารถกด Run เพื่อทดสอบโปรแกรมได้ เพราะถ้าทำงานร่วมกับคนอื่น แล้วคนอื่น Pull ข้อมูลไป กลายเป็นว่าคนๆนั้นก็ Run โปรแกรมไม่ได้เช่นกัน ต้องเสียเวลานั่งแก้โค้ดของเราอีก

และอย่าลืมใส่ใจกับ Commit Message ด้วยนะ อย่าใส่อะไรแบบนี้เด็ดขาดล่ะ

  • “Edit code”
  • “Commit”
  • “Refactor”

เพราะมันจะไม่มีทางรู้เลยว่า Commit นั้นทำอะไรลงไป

ต้อง Push เมื่อไร?

นอกเหนือจากการ Commit แล้ว การ Push ก็เป็นอีกอย่างที่หลายๆคนสงสัยเช่นกัน

เนื่องจากการ Push นั้น ในบางครั้งก็ต้อง Pull แล้ว Merge Commit ให้เรียบร้อยก่อนถึงจะทำได้ ดังนั้นไม่แนะนำให้ Push ก่อนปิดคอมกลับบ้าน เพราะนั่นอาจจะทำให้คุณไม่ได้กลับบ้านก็เป็นได้ เนื่องจากถ้ามีคน Push ก่อนหน้า แล้วพอคุณ Pull มาก็พบว่ามัน Conflict ซะงั้น สุดท้ายแล้วคุณก็ต้องนั่งแก้ Conflict ชุดใหญ่จนไม่ได้กลับบ้านนั่นเอง ดังนั้นทางที่ดีควร Push ทุกครั้งที่มีโอกาสดีกว่า อย่างน้อยถ้ามันจะ Conflict คุณก็ได้แก้ตั้งแต่เนิ่นๆและไม่เยอะมากนัก ที่สำคัญมันจะช่วยให้ทุกคนในทีมรู้ Progress ด้วยกันได้จากสถานะการ Push ด้วยนะ

และนอกจากนี้ ในบางที่ที่มีการทำ Continuous Integration (CI) ที่จะดึง Commit จาก Remote ไปทำ Testing ถ้าเรา Push บ่อยๆแล้ว Test เกิด Failed ขึ้นมาก็จะทำให้รู้ตัวได้ไวและแก้ไขได้ทันที

คำต่างๆที่จะต้องรู้จักเมื่อใช้งาน Git (ชุดที่ 2)

เมื่อคุณรู้จักคำต่างๆของ Git ในชุดที่ 1 แล้ว นั่นหมายความว่าคุณสามารถใช้งาน Git เบื้องต้นได้แล้ว แต่นั่นก็ยังไม่พอ เพราะคำต่อไปนี้คือสิ่งที่ควรจะรู้จักและเข้าใจด้วยเช่นกัน

  • BranchPull Request
  • Merge Branch
  • Stash
  • Unstash
  • Git Flow
  • Tag
  • Fork
  • Pull Request

Branch

เวลาที่เรา Commit เรื่อยๆจนกลายเป็น History มันก็จะมีหน้าตาแบบนี้

แต่ละ Commit จะเรียงต่อกันเป็น Node จนกลายเป็นเส้นยาวๆ

การเรียงกันของ Commit จนเป็นเส้นยาวๆแบบนี้จะเรียกว่า Branch และในการใช้งาน Git จริงๆจะไม่ได้มีแค่ Branch เดียวเท่านั้นนะ

จะเกิดอะไรขึ้นถ้านาย A และนาย B (คู่นี้อีกละ) ทำโปรเจคเดียวกัน แต่ว่าทำคนละ Feature?

ถ้าเราทำงานกันแบบ Branch เดียว เราก็จะพบว่านาย A กับนาย B ต้องเสียเวลา Pull ข้อมูลของกันและกันบ่อยมาก ทั้งๆที่โค้ดของทั้งสองไม่ได้เกี่ยวข้องกันซักนิด

โค้ดไม่ได้เกี่ยวข้องกัน แต่ต้องมาอยู่บน Branch เดียวกัน

ซึ่ง Git ออกแบบมาให้สร้าง Branch แยกออกมาได้หลายเส้น ดังนั้นในกรณีดังกล่าวเราจึงควรแยก Branch ออกมาสำหรับนาย A และอีก Branch สำหรับนาย B

แยก Branch เสร็จแล้ว ก็ตัวใครตัวมันละ

พอทั้งสองคนเขียนโค้ดของตัวเองเสร็จแล้วก็ถึงเวลาของ…

Merge Branch

เมื่อใดก็ตามที่อยากจะจับ Branch คู่ใดมา Merge รวมกัน นั่นล่ะครับ Merge Branch

เมื่อรวมโค้ดของทั้งสองเข้าด้วยกัน ความมันส์ก็เกิดขึ้น (บั๊กกระจุยสิครับ)

แต่ในระหว่างการ Merge Branch ก็อาจจะมี Conflict เกิดขึ้น แต่เนื่องจากการแยก Branch มักจะใช้สำหรับกรณีที่เขียนโค้ดคนละส่วนกันอยู่แล้ว ดังนั้นตำแหน่งของโค้ดที่เกิด Conflict ก็จะมีเพียงบางจุดเท่านั้น และเมื่อแก้ไข Conflict จนเสร็จแล้ว Merge Branch ทั้งสองเข้าด้วยกันได้แล้ว มันก็จะได้ออกมาเป็น Merge Commit นั่นเอง

Stash และ Unstash

สมมติว่าโปรเจคตัวหนึ่งถูกแยกออกมาเป็น 2 Branch ด้วยกัน แล้วก็เป็นหน้าที่ของนาย A และนาย B ที่จะต้องรับผิดชอบเหมือนเคย

อะไร อะไร ก็นาย A กับนาย B ตลอด

แต่อยู่ดีๆก็เกิดเหตุด่วน เพราะนาย A ดันเขียนโค้ดไม่ทัน แล้ว Feature ของนาย A ต้องส่งให้ลูกค้าในวันพรุ่งนี้ จึงทำให้นาย B ที่เขียนโค้ดใน Feature ของตัวเองอยู่ ก็ต้องหยุดชะงักกลางคันเพื่อสลับไปช่วยนาย A เขียนให้เสร็จ

แต่อนิจจาชีวิตนักพัฒนา มีบางไฟล์ที่นาย B เขียนค้างไว้ยังไม่เสร็จ แถมจะ Commit เลยก็ทำไม่ได้ อีกทั้งยังต้องสลับไป Branch ของนาย A ด่วนด้วย

ดังนั้นนาย B จะต้องเอาไฟล์ที่อยู่ใน Unstaged ไปเก็บไว้ใน Stash ชั่วคราวก่อน จะได้สลับ Branch ไปช่วยนาย A เขียนโค้ดให้เสร็จทันเวลา

เก็บโค้ดที่เขียนยังไม่เสร็จลงใน Stash ไว้ชั่วคราวก่อน

พอนาย B ไปช่วยนาย A เขียนจนเสร็จแล้ว ก็กลับมา Branch ของตัวเองแล้ว Unstash เพื่อเอาไฟล์ที่เคยเขียนค้างไว้กลับมาทำต่อให้เสร็จ

พอกลับมาก็ Unstash ของเก่ามาเขียนต่อ

และนอกจากนี้ Stash ยังสามารถใช้ย้ายไฟล์ข้าม Branch ได้อีกด้วยนะ

Git Flow

ใน Development Process จริงๆนั้นมีอะไรอีกหลายๆอย่าง ไม่ว่าจะเป็นการพัฒนาหลายๆ Feature พร้อมๆกัน แต่เวลาขึ้น Production จะหยิบเฉพาะบาง Feature เท่านั้น ส่วน Feature ที่เหลือก็อาจจะพัฒนาต่อหรือปล่อยทิ้งไว้รอขึ้นในรอบหน้า รวมไปถึงขั้นตอนก่อนที่จะขึ้น Production ที่จะต้องให้ QA/Tester ทดสอบก่อน หรือการทำ Hotfix เพราะเจอบั๊กตัวใหญ่ใน Production ที่ต้องแก้ด่วน

ดังนั้นเราจึงสามารถใช้ความสามารถในการแยก Branch ของ Git มาช่วยแก้ปัญหานี้ได้ และเมื่อวิธีแบบนี้นิยมมากขึ้นเรื่อยๆก็ทำให้เกิดสิ่งที่เรียกว่า Git Flow ขึ้นมา

Git Flow คือหนึ่งในมาตรฐานที่กำหนดรูปแบบของการแตก Branch ให้เป็นระบบระเบียบเพื่อตอบโจทย์ใน Development Process โดยแบ่ง Branch ออกเป็น 5 กลุ่มใหญ่ๆดังนี้

  • master
  • develop
  • feature
  • release
  • hotfix

ขั้นตอนการเขียนโค้ดทั้งหมดจะอยู่ใน develop และ feature อยู่เสมอ โดยตอนขึ้นโครงโปรเจคในตอนเริ่มต้นอาจจะทำใน develop แต่เมื่อโค้ดพื้นฐานพร้อมแล้ว ก็จะแยก Branch ตาม Feature เพื่อเขียนโค้ด และนั่นก็คือ branch ที่ชื่อว่า feature นั่นเอง

develop มีตัวเดียว แต่ feature จะแยกเป็น Sub Branch โดยใช้ชื่อตาม Feature นั้นๆ

เมื่อพัฒนา Feature ตัวไหนเสร็จ และพร้อมจะ Release ก็จะ Merge เข้าไปใน develop เลย ส่วนตัวไหนที่ยังไม่พร้อม Release ก็จับดองเค็มทิ้งไว้อย่างนั้นแหละ

Merge Branch แบบนี้ ใน Git Flow จะเรียกว่า Finish Branch

เมื่อถึงเวลาอันสมควรที่จะ Release เพื่อเตรียมขึ้น Production แล้ว ก็จะแตก Branch ออกมาเป็น release โดยมี Sub Branch เป็นชื่อเวอร์ชันที่ต้องการ

เย้ ในที่สุดวันนี้ก็มาถึง T^T

ซึ่งการแตกเข้าสู่ release นี้มีไว้เพื่อให้ QA/Tester ทำการทดสอบโปรแกรมของเราให้ผ่านก่อนที่จะขึ้น Production นั่นเอง ดังนั้นถ้ามี Issue เกิดขึ้นระหว่างนี้ก็ต้องแก้ไขในนี้ให้เรียบร้อยซะ

เมื่อเล็ดรอดจากเงื้อมมือของ QA/Tester เรียบร้อยแล้ว ก็ถึงเวลาพร้อมที่จะเข้าสู่ master แล้วววววว

เอาโค้ดจากใน master ไปขึ้น Production ได้แล้ว เย้!

สำหรับการ Finish Branch ของ release จะต้อง Merge เข้า master และ develop ทุกครั้ง สาเหตุที่ต้อง Merge เข้า develop ด้วยก็เพราะว่ามันมีโค้ดที่แก้ Issue ในระหว่างการทดสอบของ QA/Tester เพิ่มเข้ามา

จะเห็นว่าที่ master นั้นจะเป็นโค้ดที่สมบูรณ์ที่สุด (ของเวอร์ชันนั้นๆ) และจะไม่มีการ Commit เข้าไปตรงๆเลย แต่จะมาจากการ Merge ของ release เท่านั้น ดังนั้น master จึงมีแต่โค้ดที่เสร็จสมบูรณ์ เวลาจะ Rollback แบบกระทันหันก็สามารถทำได้โดยไม่ต้องกังวลว่าจะมีปัญหาอะไรหรือป่าว

ในกรณีที่ตัว Production ตัวล่าสุดเกิด Critical Issue ขึ้นมา แต่ไม่ถึงกับต้อง Rollback ก็จะมี hotfix เพื่อเข้ามาแก้ปัญหานี้ให้โดยเฉพาะ

แก้บั๊กแบบด่วนจี๋

หน้าที่ของ hotfix ก็ตามชื่อของมันเลย มีไว้แก้บั๊กบน Production แบบด่วนจี๋ ก่อนที่จะโดนลูกค้าด่าไปมากกว่านี้ โดย hotfix จะมี Sub Branch เป็นชื่อเวอร์ชัน (นิยมเปลี่ยนเฉพาะเลขของ Patch Version)

เมื่อแก้ไขเสร็จแล้วก็จะ Merge เข้าสู่ master และ develop เหมือนกับ release เป๊ะๆ

และนี่ก็คือรูปแบบทั้งหมดของ Git Flow ที่ถูกออกแบบมาให้ตอบโจทย์ในการพัฒนาโปรเจคใหญ่ๆ โดยจะเห็นว่า Git Flow จะช่วยให้เขียนโค้ดไม่ซับซ้อนวุ่นวาย บริหารและจัดการ Feature ต่างๆได้ง่าย รองรับ Process ที่ต้องมีการผ่าน QA/Tester ก่อนจะขึ้นสู่ Production ด้วย และตอบโจทย์สำหรับการรับมือกับ Issue ต่างๆที่สามารถเกิดขึ้นได้ตลอดเวลา

เพิ่มเติม — Git Flow อาจจะดูวุ่นวายตอนที่สร้าง Branch ต่างๆตามที่กำหนดไว้ แต่ในความเป็นจริง ใน Git GUI หลายๆตัวนั้นมีตัวช่วยสร้าง Git Flow ให้ง่ายขึ้นด้วยนะ เพียงแค่กดๆ ตั้งชื่อ แล้วก็กดๆ

Git Flow ใน SourceTree ใช้โคตรง่ายอ่ะ บอกเลย

Tag

ถ้าดูจากที่ผมอธิบายใน Git Flow ก็จะเห็นว่าใน Master จะมี Tag อยู่ด้วย ซึ่ง Tag จะเป็นเหมือนตัวช่วยบอกว่าอันไหนอยู่ที่ Commit ไหน เพื่อที่ว่าจะได้ตามหาทีหลังได้ง่าย

หาได้ไม่ยากว่าเวอร์ชัน 1.0.7 อยู่ที่ Commit ไหน เพราะว่าใส่ Tag ไว้แล้ว

ในกรณีที่ใช้ Git Flow ก็จะมีการใส่ Tag ให้อัตโนมัติตามชื่อ Branch ที่กำหนดไว้ตอน Release และ Hotfix

และถ้าใช้ VCS ของผู้ให้บริการอย่าง GitHub ก็จะมีเมนูแสดง Source Code ของแต่ละ Tag ได้ทันที

เห็นแบบนี้ก็รู้ได้ทันทีว่า Release ล่าสุดของ React คือเวอร์ชัน 15.6.1

Fork

โดยปกติแล้ว คนที่สามารถเข้าไป Commit และ Push ใน Repository ได้จะต้องเป็นคนที่ได้รับอนุญาตเท่านั้น ถึงแม้ว่าจะเป็น Public Repository ก็ตาม

สมมติว่าผมไปใช้ Library ซักตัวหนึ่งที่มี Repository อยู่บน GitHub แล้ว Library ตัวนั้นดันมีบั๊กอยู่ แต่คนดูแลไม่ว่างมาแก้ไขให้ซักที ส่วนตัวผมก็รีบร้อนมากๆ อยากจะใช้ไวๆเพื่อให้งานเสร็จ ดังนั้นผมก็ต้องทำการ Fork Repository ตัวนั้นมาไว้ที่ GitHub ของผมแล้วทำการแก้ไขให้เรียบร้อยซะ

ซึ่งการ Fork ก็คือการคัดลอก Repository (ของคนอื่นที่อนุญาตให้เราเข้าไปดูได้) มาเก็บไว้เป็น Repository ของเรา ซึ่งทุกๆอย่างจะเหมือนกันทั้งหมด ต่างกันแค่ว่าเราเป็นเจ้าของ Repository ตัวที่แยกออกมาเท่านั้นเอง ดังนั้นผมจึงสามารถแก้ไขบั๊กตัวนั้นจากใน Repository ของผมได้ทันที

Pull Request

เมื่อผมแก้บั๊กใน Repository ที่ Fork มาจาก Library ของคนอื่นแล้ว แต่ผมก็พบว่าคนอื่นที่เอา Library ของคนๆนั้นไปก็ยังเจอบั๊กเหมือนผมอยู่ดี ก็เลยอยากจะช่วยแก้บั๊กนี้ใน Library ของเค้าซะ ดังนั้นผมจึงส่ง Pull Request ไปให้ต้นทางเพื่อให้เจ้าของ Library เห็น Commit ของผมที่แก้บั๊กตัวนั้น

เจ้าของ Library ที่เห็น Commit ของผมก็จะมีทางเลือกอยู่สองทาง

  • ช่างแม่ง ปล่อยทิ้งไว้
  • รับ Pull Request ของผมเพื่อรวม Commit ที่แก้บั๊กแล้ว เข้าไปใน Repository ของเขา

แต่จะมีเหตุผลอะไรที่จะไม่รับ Pull Request จากผมล่ะเนอะ? อุตส่าห์แก้บั๊กให้ (ถ้าไม่งอกบั๊กตัวอื่นเพิ่มขึ้นมา) เมื่อเค้ารับ Pull Request ปุ๊ป บั๊กใน Library ของเค้าก็ถูกแก้ไข ก็อาจจะต้องไป Release เป็นเวอร์ชันใหม่อีกที แต่ที่แน่ๆคือผมก็ได้กลายเป็นหนึ่งใน Contributor ของ Library ตัวนี้ซะแล้ว

และนี่ก็คือโลกของ Open Source นั่นเอง

สรุป

ไม่รู้จะสรุปยังไงดี เพราะเขียนยาวเหยียดมากกกกก

แต่ก็ต้องบอกเลยว่า Git เข้ามาเปลี่ยนชีวิตนักพัฒนาให้ดีขึ้นมาก แก้ปัญหาเดิมๆใน Development Process ที่เคยปวดหัวมานมนานให้กลายเป็นเรื่องง่าย จะทำอะไรก็เป็นระบบระเบียบ มีปัญหาเกิดขึ้นก็รู้ได้ว่าเพราะอะไรและฝีมือใคร

และที่สำคัญคือโลกของนักพัฒนาสามารถ Open Source กันได้ทุกวันนี้ ส่วนหนึ่งก็ต้องขอบคุณ Git นี่ล่ะครับที่ทำให้เป็นเรื่องง่าย ถึงแม้ว่าจะมี VCS มานานแล้วก็จริง แต่ด้วยความนิยมของ Git และ GitHub จึงทำให้เกิด Community ของชาว Open Source ได้อย่างแพร่หลายนั่นเอง

https://www.pexels.com/photo/beverage-black-coffee-business-chart-33972/

สุดท้ายนี้ นอกจากการใช้ Git เป็นแล้ว ผมก็แนะนำให้ทุกๆคนลองสร้างโปรเจคหรือ Library ซักตัวแล้วเปิด Public เป็น Open Source บน GitHub ดูครับ แล้วจะรู้ว่า Git นั้นยังทำอะไรได้อีกเยอะมากกกกกก เพราะยังมีอีกหลายๆอย่างที่ผมยังไม่ได้พูดถึง ไม่ว่าจะเป็น .gitignore, Reset, Rebase, Cherry Pick, File Diff และอื่นๆอีกมากมาย

ใช้ Git เถอะครับ เพื่อคุณภาพชีวิตที่ดีขึ้น

บทความเกี่ยวกับ Git ที่แนะนำให้อ่านต่อ

Lovely android developer who enjoys learning in android technology, habitual article writer about Android development for Android community in Thailand.

Lovely android developer who enjoys learning in android technology, habitual article writer about Android development for Android community in Thailand.