บทที่ 2 Git พื้นฐาน: commit, branch และการแก้ merge conflict
ภาพรวมของบท
บทนี้เปลี่ยนโฟลเดอร์ devops-lab01 จากบทที่ 1 ให้เป็น Git repository ที่มีประวัติจริง: commit แรก, branch, merge แบบ Branch-First, merge conflict ที่คุณแก้เองตามเอกสารทางการ, .gitignore กันไฟล์ลับ และปิดท้ายด้วย tag v0.1.0 ซึ่งเป็น milestone แรก (M1) ของโครงงานสะสมที่ใช้ต่อถึงวันนำเสนอ
ทำไมเรื่องนี้สำคัญ: ทุกบทหลังจากนี้ทำงานบน repository บทที่ 3 ผลักขึ้น GitHub ทำงานเป็นทีมผ่าน pull request บทที่ 5 ให้ CI ตรวจทุก commit ที่ push ขึ้นมา และบทที่ 8 ติด tag กับ image ตามเวอร์ชันของแอป ถ้าคุณไม่แน่ใจว่า commit ของคุณมีไฟล์อะไรข้างใน หรือแก้ conflict ไม่เป็น งานทุกบทถัดไปจะสะดุดที่จุดเดียวกันนี้
ความเชื่อมโยงกับบทอื่น: โฟลเดอร์ทำงานคือ ~/devops-lab01 จากบทที่ 1 ซึ่งมี app.log, app.log.bak, clean.log, hello.sh, .env และ check-errors.sh จาก Lab E สคริปต์นี้จะถูกย้ายไปอยู่ที่ scripts/check-errors.sh ใน repository และจะกลับมาเป็นขั้นตรวจใน CI ของบทที่ 5 สิ่งที่ได้จากบทนี้ถูกใช้ต่อดังนี้
| สิ่งที่ได้จากบทนี้ | ใช้ต่อในบท |
|---|---|
| repository ที่มีประวัติ commit, branch และ merge (M1) | 3 (ผลักขึ้น GitHub เป็นทีม) และทุกบทหลังจากนั้น |
scripts/check-errors.sh ใน repository | 5 (ขั้นตรวจใน CI) |
.gitignore และนิสัยกันไฟล์ลับออกจาก repository | 9 (ตรวจว่าไม่พบรหัสผ่าน) และ 10 (GitHub Secrets) |
tag v0.1.0 | 4 (ปรับเวอร์ชันเป็น 0.2.0) และ 8 (tag ของ image) |
การอ่านผล git log --oneline --graph | 3 (อ่านประวัติ pull request) และ 12 (อ่านประวัติทั้งโครงงาน) |
ผลลัพธ์การเรียนรู้
เมื่อจบบท คุณต้องทำสิ่งต่อไปนี้ได้ และแต่ละข้อมีงานที่ใช้วัดผล
| รหัส | ผลลัพธ์การเรียนรู้ | วัดจาก | CLO ของรายวิชา |
|---|---|---|---|
| 2.1 | สร้าง commit, branch และ merge ตามลำดับที่กำหนดใน repository ของตนเอง | งานข้อ 1 | CLO3 |
| 2.2 | แก้ merge conflict จนไฟล์ได้ผลตามที่กำหนด โดยใช้ข้อมูลจากเอกสาร git ทางการ | งานข้อ 2 | CLO3 |
| เตรียมพื้น | แยกไฟล์ที่ควรเก็บออกจากไฟล์ที่ไม่ควรเก็บด้วย .gitignore และปักหมุดเวอร์ชันด้วย tag | งานข้อ 3 | ยังไม่นับ CLO ใช้ต่อในบทที่ 3 และ 5 |
CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):
- CLO3 ใช้เครื่องมือดิจิทัลอย่างเหมาะสมเพื่อพัฒนาระบบ และสามารถเรียนรู้และปรับตัวด้วยตนเองจากแหล่งข้อมูลที่เชื่อถือได้
แนวคิด Single Source of Truth และ Branch-First ในส่วนที่ 1 เป็นพื้นความรู้ของวิชา ไม่ได้วัดเป็น CLO ในบทนี้ เพราะการเลือกและอธิบาย Git workflow ตามบริบทงานจะเริ่มวัดในบทที่ 3 (CLO1) บทนี้จึงจับคู่เฉพาะ CLO3 ซึ่งวัดการใช้เครื่องมือตามขั้นตอนที่กำหนดและการหาคำตอบจากเอกสารทางการ Learning Log ครั้งที่ 2 เก็บสะสมเป็น portfolio ที่ประเมินรวมในบทที่ 13
ก่อนเริ่ม: ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์
ความรู้พื้นฐาน: บทที่ 1 (ใช้ Terminal, แก้ไฟล์ข้อความ, คำสั่ง grep และโครงสร้างโฟลเดอร์ devops-lab01) ไม่ต้องเคยใช้ Git มาก่อน
สิ่งที่ต้องมี: เครื่อง Ubuntu ที่ติดตั้ง Git แล้ว (ตรวจด้วย git --version ถ้ายังไม่มี ติดตั้งด้วย sudo apt update แล้ว sudo apt install git) และโฟลเดอร์ ~/devops-lab01 จากบทที่ 1 ที่มีไฟล์ app.log, app.log.bak, clean.log, hello.sh, .env และ check-errors.sh (เวอร์ชันที่แก้ตาม Lab E.3) ถ้าโฟลเดอร์หาย ให้ทำ Lab C และ Lab E ของบทที่ 1 ใหม่ก่อน
รุ่นซอฟต์แวร์ที่ใช้ทดสอบ: ทุกคำสั่งและผลลัพธ์ในบทนี้ทดสอบแล้วบนรุ่นต่อไปนี้
| ซอฟต์แวร์ | รุ่นที่ทดสอบ |
|---|---|
| Git | 2.51.0.windows.1 (ทดสอบบน Windows 10 กับ Git Bash วันที่ 18 ต.ค. 2569) |
| Git ของผู้เรียน (Ubuntu 24.04) | 2.43.x — ยังไม่ได้ทดสอบซ้ำบน Ubuntu 24.04 และเครื่องห้องแลบจริง |
คำสั่งทั้งหมดเป็นคำสั่งพื้นฐานที่ใช้เหมือนกันทุกแพลตฟอร์ม (git switch มีตั้งแต่ Git 2.23) ส่วนที่ต่างกันได้คือข้อความเตือน เช่น เรื่อง CRLF ที่พบเฉพาะ Windows (ดู Lab A) และข้อความเกี่ยวกับชื่อ branch เริ่มต้นที่ปรากฏเมื่อยังไม่ได้ตั้งค่า
เวลาโดยประมาณ: ผู้เรียนที่ผ่านบทที่ 1 มาแล้วใช้เวลาส่วนปฏิบัติประมาณ 2–3 ชั่วโมง บวกเวลาศึกษาด้วยตนเอง
ค่าใช้จ่ายและข้อมูลลับ: บทนี้ทำบนเครื่องของคุณทั้งหมด ไม่มี Lab บนคลาวด์ จึงไม่มีค่าใช้จ่าย แต่มีสองเรื่องที่ต้องระวังตั้งแต่ต้น ข้อแรก user.name และ user.email ที่คุณตั้งใน Lab A จะติดไปกับทุก commit เมื่อผลัก repository ขึ้น GitHub ในบทที่ 3 ใครก็เห็นข้อมูลนี้ได้ จึงควรใช้ชื่อและอีเมลที่คุณสบายใจจะเปิดเผย ข้อที่สอง ไฟล์ลับอย่าง .env (มีรหัสผ่านสมมติ change-me จากบทที่ 1) ต้องไม่ติดเข้าไปใน repository เลย .gitignore ใน Lab D คือเครื่องมือแรกของวิชานี้ที่กันความลับออกจาก repository และแนวคิดนี้จะกลับมาอีกครั้งในบทที่ 9 (ตรวจว่าไม่พบรหัสผ่านใน repository) และบทที่ 10 (GitHub Secrets)
ส่วนที่ 1 บรรยาย
แต่ละหัวข้อมีตัวอย่างที่ยากขึ้นทีละระดับ และจบด้วยคำถามตรวจความเข้าใจ ให้ตอบในใจหรือคุยกับเพื่อนข้าง ๆ ก่อนไปหัวข้อถัดไป
2.1 ทำไมต้องมี version control และ Single Source of Truth
ถ้าคุณเคยตั้งชื่อไฟล์ว่า report.doc, report-v2.doc, report-final.doc และ report-final-จริงๆ.doc คุณก็เคยเจอปัญหาที่ version control มีไว้แก้แล้ว version control system (VCS) คือเครื่องมือที่บันทึกประวัติการเปลี่ยนแปลงของไฟล์ทั้งโครงการ ทำให้ย้อนดูว่าใครแก้อะไรเมื่อไร กลับไปใช้เวอร์ชันก่อนหน้าได้ และทำงานหลายทางพร้อมกันแล้วรวมกันได้ (Pro Git)
Git ต่างจาก VCS รุ่นเก่าตรงที่เวลาคุณ commit Git เก็บ "ภาพถ่าย" (snapshot) ของไฟล์ทั้งชุดในขณะนั้น ไม่ใช่เก็บเฉพาะส่วนต่างของแต่ละไฟล์ ตำรา Pro Git สรุปว่า Git คิดเรื่องข้อมูลของตัวเองเป็น "stream of snapshots" คือลำดับของภาพถ่ายทั้งชุด (Pro Git §1.3)
Single Source of Truth (SSOT) คือหลักที่ว่า ความจริงร่วมของทีมต้องมีที่เดียว สำหรับโค้ด ที่นั้นคือ repository ไม่ใช่เครื่องของใครคนใดคนหนึ่ง หรือไฟล์ zip ในกล่องจดหมาย เมื่อทุกคนดึงงานจากที่เดียว จะไม่มีคำถามว่า "เวอร์ชันล่าสุดอยู่ที่ไหน" ในบทนี้ repository ยังอยู่บนเครื่องของคุณคนเดียว แต่ทุกอย่างที่ฝึกวันนี้จะถูกยกขึ้น GitHub ให้เป็น SSOT ของทีมในบทที่ 3
ประวัติของ repository ยังเป็นหลักฐานที่ตรวจสอบย้อนได้ (audit trail) ซึ่งสำคัญกับ DevOps ตรงที่ pipeline ในบทที่ 5 จะทำงานกับ "commit" เป็นหน่วย ไม่ใช่กับ "ไฟล์"
ตัวอย่างระดับความยากที่เพิ่มขึ้น:
- ระดับ 1 (ไฟล์เดียว): นักเขียนคนเดียวเก็บทุกฉบับเอง ยังไม่ถึงกับพัง แต่ไล่เรียงว่าฉบับไหนเกิดก่อนหลังไม่ได้
- ระดับ 2 (หลายคน): สองคนแก้ไฟล์เดียวกันจากสำเนาคนละชุด เอามารวมกันด้วยมือ ต้องเทียบทีละบรรทัด
- ระดับ 3 (ส่งมอบต่อเนื่อง): ทีมส่งรุ่นใหม่ทุกสัปดาห์ ต้องรู้ทุกเวลาว่าโค้ดตรงไหนอยู่ในรุ่นไหน และกลับไปรุ่นก่อนได้เร็วแค่ไหน นี่คือบริบทของวิชานี้และของ DORA metrics จากบทที่ 1
คำถามตรวจความเข้าใจ 2.1: ทีมเก็บโค้ดล่าสุดไว้ในเครื่องของหัวหน้าและแจกสำเนาผ่านไดรฟ์แชร์ วันจันทร์ลูกทีมคนหนึ่งก็อปไปแก้ วันพุธหัวหน้าก็แก้ไฟล์เดิมบนเครื่องตัวเอง วันศุกร์เอาสองฉบับมารวมกันไม่ได้ บอกอย่างน้อย 2 อย่างที่ version control ช่วยแก้สถานการณ์นี้
2.2 working tree, staging และ commit
ไฟล์ในโครงการ Git อยู่ได้สามสถานะ ตามสามพื้นที่หลักของโครงการ (Pro Git §1.3 The Three States)
| พื้นที่ | คืออะไร | ไฟล์ในสถานะ |
|---|---|---|
| working tree | โฟลเดอร์จริงที่คุณแก้ไฟล์อยู่ | modified (แก้แล้ว ยังไม่บอก Git) |
| staging area (index) | ร่างที่คัดไว้ว่าจะเก็บอะไรในภาพถ่ายถัดไป | staged (คัดแล้วด้วย git add) |
| repository (.git) | ประวัติถาวรของภาพถ่ายทั้งหมด | committed (เก็บแล้วด้วย git commit) |
คำสั่งหลักสามตัวที่เดินไปตามสามพื้นที่นี้: git status ดูว่าไฟล์แต่ละตัวอยู่สถานะใด, git add ชื่อไฟล์ ยกไฟล์จาก working tree ขึ้น staging, และ git commit -m "ข้อความ" ถ่ายรูปสิ่งที่อยู่บน staging ลงประวัติ
ระดับ 1 อ่าน git status ออก: ผลแบ่งเป็นกลุ่น "Changes to be committed" คือ staged จะติดไปกับ commit ถัดไป, "Changes not staged for commit" คือ modified ยังไม่ได้ add, และ "Untracked files" คือไฟล์ใหม่ที่ Git ยังไม่รู้จัก
ระดับ 2 ยืนยันด้วยตา: คุณแก้ a.txt กับ b.txt แล้วสั่ง git add a.txt เวลา commit เฉพาะ a.txt ติดไป ในขณะที่ b.txt ยังเป็น modified รออยู่ (จะเห็นจริงใน Lab A) ใช้ git diff ดูการแก้ที่ยังไม่ได้ stage และ git diff --staged ดูสิ่งที่กำลังจะติดไปกับ commit
ระดับ 3 ทำไมต้องมี staging ทั้งที่ดูเหมือนขั้นตอนเกิน: เพราะมันให้คุณคัดว่าอะไรจะเข้าภาพถ่าย แทนที่จะ "เก็บทุกอย่างที่แก้วันนี้" การ commit เล็กและตรงประเด็นทำให้ทบทวนง่าย เมื่อถึงบทที่ 3 เพื่อนรีวิว pull request ของคุณทีละ commit คุณจะขอบคุณ staging นี้
คำถามตรวจความเข้าใจ 2.2: คุณแก้ A.txt กับ B.txt สั่ง git add A.txt ตามด้วย git commit -m "แก้ไข" เสร็จแล้วสั่ง git commit -m "อีกครั้ง" ทันทีโดยไม่ add อะไรเพิ่ม — B.txt อยู่สถานะใด และ commit รอบที่สองเกิดอะไรขึ้น
2.3 branch: แยกทางทำงานโดยไม่แตะงานของกัน
branch ใน Git ไม่ใช่สำเนาโฟลเดอร์ทั้งก้อน แต่เป็น "ตัวชี้" ที่ชี้ไปที่ commit หนึ่ง ตำรา Pro Git นิยามตรง ๆ ว่า "A branch in Git is simply a lightweight movable pointer to one of these commits" (Pro Git §3.1) การสร้าง branch จึงเร็วและถูกมาก สร้างได้ไม่จำกัด ไม่ต้องประหยัด
อีกตัวชี้ที่ต้องรู้คือ HEAD ซึ่งชี้ไปที่ branch ที่คุณ "กำลังอยู่" เมื่อคุณ commit ใหม่ ตัวชี้ของ branch ปัจจุบัน (รวมทั้ง HEAD) จะเลื่อนไปชี้ commit ใหม่นั้น ส่วน branch อื่นยังชี้ที่เดิม
คำสั่งหลัก: git branch ดูรายการ branch (ตัวที่มี * คือตัวที่อยู่), git switch -c ชื่อbranch สร้างใหม่แล้วสลับไปอยู่, git switch ชื่อbranch สลับกลับ ถ้าเจอคำสั่ง git checkout -b ในเอกสารเก่า นั่นคือวิธีเดิมที่ทำสิ่งเดียวกัน git switch เป็นคำสั่งแยกที่มาทีหลัง (Git 2.23) เพื่อให้อ่านออกว่า "สลับ" อย่างเดียว (git-switch)
- ระดับ 1: ดูรายการ branch และบอกได้ว่าตอนนี้อยู่ตัวไหน
- ระดับ 2: สร้าง
feature/usage, commit หนึ่งครั้งบนนั้น, สลับกลับmain— ไฟล์ในโฟลเดอร์กลับไปเป็นเหมือนก่อนแก้ เพราะ working tree ถูกอัปเดตตาม branch ที่สลับไป (git-switch) - ระดับ 3: จาก commit เดียวกัน สอง branch แยกทางไปคนละทาง กราฟของประวัติแยกเป็นสองเส้นแล้วมาบรรจบอีกครั้งตอน merge (หัวข้อถัดไป) นี่คือรูปที่คุณต้องอ่านออกในแบบฝึกหัดของบทนี้
คำถามตรวจความเข้าใจ 2.3: คุณอยู่บน branch feature/usage แล้ว commit 2 ครั้ง เพื่อนเปิดดู branch main ในเครื่องเดียวกัน — เพื่อนเห็น commit ทั้งสองอันนั้นไหม เพราะอะไร
2.4 merge และ Branch-First
merge คือการนำ commit ของ branch หนึ่งมารวมกับ branch ที่คุณกำลังอยู่ ด้วยคำสั่ง git merge ชื่อbranch (สั่งจาก branch ปลายทางเสมอ) ผลมีสองรูปแบบ (Pro Git §3.2)
| รูปแบบ | เกิดเมื่อ | ประวัติเป็นอย่างไร |
|---|---|---|
| fast-forward | branch ปลายทางไม่มี commit ใหม่เลยหลังแตกกิ่ง | Git แค่เลื่อนตัวชี้ไปหน้า ไม่เกิด merge commit กราฟเป็นเส้นตรง |
| three-way merge | ทั้งสองฝั่งมี commit ใหม่ของตัวเอง | Git เทียบสามจุด (ปลายสอง branch + บรรพบุรุษร่วม) แล้วสร้าง merge commit ที่มีพ่อแม่สองตัว กราฟเป็นสามเหลี่ยม |
เอกสารทางการของ git merge ระบุเรื่อง fast-forward ไว้ว่าในกรณีนั้น "a new commit is not needed ... instead, the HEAD (along with the index) is updated to point at the named commit, without creating an extra merge commit" และพฤติกรรมนี้ "can be suppressed with the --no-ff option" (git-merge) วิชานี้ใช้ git merge --no-ff เสมอ เพราะ merge commit ที่ปรากฏชัดทำให้เห็นในกราฟว่างานชิ้นไหนเคยมี branch ของตัวเอง — สะดวงทั้งตอนเรียน ตอนตรวจงาน และตรงกับ pull request ของบทที่ 3 ซึ่ง merge ผ่าน GitHub ก็สร้าง merge commit ให้เหมือนกัน
Branch-First คือกติกาการทำงานของวิชานี้: งานทุกชิ้นเริ่มที่การแตก branch ใหม่ ไม่แก้และ commit ตรง ๆ บน branch หลัก เหตุผลคือ branch หลัก (main) ต้องเชื่อถือได้เสมอ ทุกการเปลี่ยนแปลงเกิดบน branch ที่มีชื่อบอกว่าทำอะไร แล้วค่อยรวมกลับอย่างเป็นระบบ นี่คือแก่นของ GitHub flow ที่ทั้งทีมจะใช้จริงในบทที่ 3 ชื่อ branch ที่ใช้ในวิชานี้เป็น feature/ชื่องาน เช่น feature/usage
คำถามตรวจความเข้าใจ 2.4: คุณแตก branch feature/x จาก main แล้ว commit บน feature/x หนึ่งครั้ง โดย main ไม่มี commit ใหม่เลย — ถ้า merge แบบปกติจะได้รูปแบบใด และใครบางคนที่ดูกราฟจะรู้ไหมว่าเคยมี branch นี้
2.5 conflict เกิดเมื่อไร
Git รวมงานอัตโนมัติได้ดีเมื่อสองฝั่งแก้คนละบริเวณ ตำรา Pro Git ระบุเงื่อนไขของ conflict ว่า "If you changed the same part of the same file differently in the two branches you're merging, Git won't be able to merge them cleanly" (Pro Git §3.2) เมื่อเกิด conflict การ merge จะหยุดกลางทาง และ Git ทิ้งเครื่องหมายไว้ในไฟล์ให้คนตัดสินใจ
<<<<<<< HEAD
ข้อความฝั่งของคุณ (branch ที่คุณอยู่ตอนสั่ง merge)
=======
ข้อความฝั่งที่ merge เข้ามา
>>>>>>> ชื่อ branch ที่ mergeเอกสารทางการของ git merge อธิบายว่าเครื่องหมายสามตัวนี้ครอบ "พื้นที่ที่การเปลี่ยนแปลงที่ขัดกันเกิดขึ้น" โดยฝั่งก่อน ======= คือฝั่งของคุณ และหลังจากนั้นคืออีกฝั่ง (git-merge, HOW CONFLICTS ARE PRESENTED) เมื่อเจอ conflict คุณมีสองทางเท่านั้น (git-merge, HOW TO RESOLVE CONFLICTS)
- ยกเลิก:
git merge --abortทำให้กลับไปสถานะก่อนสั่ง merge ทุกอย่างเหมือนเดิม - แก้ให้จบ: แก้ไฟล์ให้เป็นผลที่ต้องการ (เลือกฝั่งหนึ่ง หรือรวมทั้งสองฝั่ง) ลบเครื่องหมายทั้งสามชนิด แล้ว
git add ไฟล์ตามด้วยgit commitเพื่อปิดการ merge
สิ่งสำคัญที่ต้องเข้าใจ: conflict ไม่ใช่ความผิดพลาดของเครื่อง และไม่ทำให้ไฟล์พัง Git แค่ไม่กล้าตัดสินใจแทนคุณว่าข้อความไหนถูกต้อง มันจึงหยุดรอคุณ ใน Lab C คุณจะสร้าง conflict จริงด้วยตัวเองและแก้ด้วยสองวิธีข้างบน
คำถามตรวจความเข้าใจ 2.5: จาก block conflict ด้านบน ฝั่งบน (เหนือ =======) มาจาก branch ใด และถ้าคุณตัดสินใจว่ายังไม่อยาก merge ตอนนี้ จะใช้คำสั่งใด
2.6 สาธิตก่อนเข้า Lab: เดินครบเส้นทางใน 10 นาที
ก่อนลงมือทำ Lab ผู้สอนจะสาธิตสดครบวงจรหนึ่งรอบบนโครงงานจำลอง ลำดับคือ: git init → ตั้งค่า → git add + git commit แรก → git switch -c แตก branch → แก้ไฟล์แล้ว commit → git switch main → git merge --no-ff → ทำ conflict ขึ้นมา → git merge --abort → merge ใหม่ → แก้ conflict → git add + git commit → git log --oneline --graph ดูประวัติ → git tag จบด้วยภาพเดียวกับที่คุณต้องส่งเป็นงานข้อ 1
ขณะดูสาธิต ให้จดสองอย่าง: ผลของ git status เปลี่ยนอย่างไรทีละขั้น และกราฟของ git log --oneline --graph งอกเป็นสามเหลี่ยมตอนไหน Lab A ถึง D จะพาคุณทำซ้ำทุกขั้นบนโครงงานจริงของคุณเอง
คำถามตรวจความเข้าใจ 2.6: ในสาธิตมีคำสั่งที่คุณใช้มาแล้วจากบทที่ 1 อยู่ด้วย คำสั่งใด และช่วยอะไรในบทนี้
ส่วนที่ 2 ปฏิบัติ
ทุก Lab มีสองช่วง ฝึกตามตัวอย่าง ให้พิมพ์คำสั่งตามและเทียบผล ส่วน ตัดสินใจเอง (B.5, C.6, D.4) ไม่มีคำสั่งให้ คุณต้องเลือกคำสั่งหรือตัดสินใจเอง
เลข hash ของ commit (เช่น 77b5783) เกิดใหม่ไม่ซ้ำกันทุกเครื่องและทุกครั้ง ชื่อผู้เขียนมาจาก user.name ที่คุณตั้ง และเวลาในผลลัพธ์ตามเครื่องคุณ ตัวอย่างในบทนี้ใช้ผู้ใช้ชื่อ student ให้ดูที่รูปแบบของผล ไม่ต้องตรงทุกตัวอักษร
ใน devops-lab01 มี .env ซึ่งเป็นไฟล์รหัสผ่าน (ค่าสมมติจากบทที่ 1) คำสั่ง git add . เพิ่มทุกไฟล์รวมทั้งไฟล์ลับเข้า staging ทันที จนกว่าจะมี .gitignore ใน Lab D ให้ระบุชื่อไฟล์ที่ต้องการ add เสมอ และตรวจด้วย git status ก่อน commit ทุกครั้ง
Lab A: ตั้งค่า Git และ commit แรกบนแอปตั้งต้น
สถานการณ์: หัวหน้าทีมบอกว่า ก่อนจะเข้าทีมในบทที่ 3 ทุกคนต้องมีโครงงานของตัวเองเป็น repository ที่มีประวัติ คุณจึงเปลี่ยนโฟลเดอร์ devops-lab01 จากบทที่ 1 ให้เป็น repository ของคุณเอง
เป้าหมาย: ตั้งค่า Git ครั้งแรกของเครื่อง, เตรียมโครงสร้างโฟลเดอร์, สร้าง commit แรก (ส่วนต้นของ LLO 2.1)
ฝึกตามตัวอย่าง
A.1 เตรียมโครงสร้างโครงงาน
ย้ายสคริปต์จากบทที่ 1 ไปอยู่ในโฟลเดอร์ scripts/ แล้วสร้าง README.md อธิบายโครงงาน
cd ~/devops-lab01
mkdir -p scripts
mv check-errors.sh scripts/check-errors.sh
cat > README.md <<'EOF'
# DevOps Lab 01
แบบฝึกหัดของรายวิชา 10306343 (DevOps) เริ่มจากบทที่ 1
## ไฟล์ในโครงงาน
- app.log ตัวอย่าง log จากบทที่ 1
- scripts/check-errors.sh สคริปต์ตรวจ ERROR จากบทที่ 1
- hello.sh สคริปต์ทักทายจาก Lab B ของบทที่ 1
EOFA.2 ตั้งค่า Git ครั้งแรกของเครื่อง
git --version
git config --global user.name "student"
git config --global user.email "[email protected]"
git config --global init.defaultBranch main
git config --global core.autocrlf falseแต่ละค่าทำงานดังนี้
| ค่า | ทำไมต้องตั้ง |
|---|---|
user.name / user.email | ประทับตัวตนลงทุก commit ถ้าไม่ตั้ง Git ปฏิเสธการ commit (ทดสอบแล้ว ขึ้น "Author identity unknown") ใช้ชื่อที่คุณสบายใจเปิดเผย เพราะจะปรากฏบน GitHub ตั้งแต่บทที่ 3 |
init.defaultBranch main | ให้ branch แรกของ repository ใหม่ชื่อ main ตรงกับที่วิชานี้และ GitHub ใช้ |
core.autocrlf false | ไม่เปลี่ยนปลายบรรทัดระหว่าง LF กับ CRLF ผู้ใช้ Windows ที่ไม่ตั้งค่านี้จะเจอข้อความเตือนตอน git add เช่น warning: in the working copy of 'README.md', LF will be replaced by CRLF ... (พบจริงตอนทดสอบบน Git for Windows) การปิดการแปลงทำให้ไฟล์ใน repository เหมือนกันทุกแพลตฟอร์ม บน Ubuntu ไม่จำเป็นแต่ตั้งไว้ไม่เสียหาย |
A.3 สร้าง repository และดูสถานะ
git init
git statusInitialized empty Git repository in /home/student/devops-lab01/.git/
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env
README.md
app.log
app.log.bak
clean.log
hello.sh
scripts/
nothing added to commit but untracked files present (use "git add" to track)คำสั่ง git init สร้างโฟลเดอร์ลับ .git/ ซึ่งเก็บทั้งประวัติ (repository ในความหมายของหัวข้อ 2.2) สังเกตว่า git status เห็น .env ด้วย — ไฟล์ลับยังไม่ถูกเก็บเข้าไปไหน แต่มันรออยู่ใน working tree และจะติดทันทีถ้าคุณสั่ง git add .
A.4 commit แรก
git add README.md
git status
git commit -m "commit แรก: เพิ่ม README อธิบายโครงงาน"
git log --oneline[main (root-commit) 77b5783] commit แรก: เพิ่ม README อธิบายโครงงาน
1 file changed, 9 insertions(+)
create mode 100644 README.md
77b5783 (HEAD -> main) commit แรก: เพิ่ม README อธิบายโครงงานcommit แรกมีเฉพาะ README.md ตามที่เรา add เท่านั้น ไฟล์อื่นยังเป็น untracked รอวันของตัวเองใน Lab D สัญลักษณ์ (HEAD -> main) ยืนยันว่า HEAD ชี้ branch main ซึ่งชี้ commit ใหม่นี้
จุดตรวจ A: git log --oneline แสดง commit หนึ่งแถวขึ้นต้นด้วย (root-commit) และ git status ยังแสดง untracked ที่เหลืออยู่ (ไม่มี .env ติดเข้าไปใน commit)
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
git commit ขึ้น Author identity unknown และ Please tell me who you are | ยังไม่ได้ตั้ง user.name / user.email | รันคำสั่ง config ใน A.2 ให้ครบแล้ว commit ใหม่ |
git init แล้วขึ้นข้อความยาวเกี่ยวกับ master และ init.defaultBranch | ใช้ Git ที่ยังไม่ได้ตั้ง defaultBranch (พบได้บน Ubuntu ที่ติดตั้งใหม่) | ตั้งตาม A.2 แล้วเปลี่ยนชื่อ branch ปัจจุบันด้วย git branch -m main |
warning: in the working copy of '...', LF will be replaced by CRLF ... ตอน git add บน Windows | Git for Windows ตั้ง core.autocrlf true ไว้ตั้งแต่ติดตั้ง จึงพยายามแปลงปลายบรรทัด | ตั้ง git config --global core.autocrlf false ตาม A.2 แล้ว add ใหม่ (ไม่เป็นอันตรายแม้ไม่แก้ แต่ผลจะต่างจากตัวอย่าง) |
fatal: not a git repository (or any of the parent directories) | รันคำสั่ง Git นอกโฟลเดอร์ที่ git init ไว้ | cd ~/devops-lab01 แล้วตรวจด้วย pwd |
Lab B: branch และ merge
สถานการณ์: คุณต้องเพิ่มเอกสารสองเรื่องใน README.md คือหัวข้อ "วิธีใช้สคริปต์" กับการปรับคำอธิบายโครงงาน ตามกติกา Branch-First คุณทำงานแต่ละชิ้นบน branch ของมัน แล้ว merge กลับ main ด้วย merge commit ที่เห็นชัดในกราฟ
เป้าหมาย: LLO 2.1 สร้าง commit, branch และ merge ตามลำดับที่กำหนด
ฝึกตามตัวอย่าง
B.1 แตก branch และทำงานบนนั้น
git switch -c feature/usageSwitched to a new branch 'feature/usage'เปิด README.md ด้วย editor ที่คุณถนัด (เช่น nano README.md) แล้วเพิ่มหัวข้อต่อท้ายไฟล์
## วิธีใช้สคริปต์
รันสคริปต์ตรวจ log ด้วยคำสั่ง
./scripts/check-errors.sh app.logแล้ว commit บน branch นี้
git add README.md
git commit -m "เพิ่มหัวข้อวิธีใช้สคริปต์ใน README"[feature/usage ae00216] เพิ่มหัวข้อวิธีใช้สคริปต์ใน README
1 file changed, 6 insertions(+)สังเกตว่าบรรทัดผลเปลี่ยนจาก [main ...] เป็น [feature/usage ...] — commit ตกอยู่บน branch ที่คุณอยู่เท่านั้น
B.2 กลับ main และแก้คนละบริเวณกับ branch
git switch mainSwitched to branch 'main'เปิด README.md อีกครั้ง จะพบว่าหัวข้อที่เพิ่งเขียนไปหายไปแล้ว เพราะ working tree ถูกคืนกลับมาตามสถานะของ main นี่คือพฤติกรรมปกติของ git switch (git-switch) ให้แก้บรรทัดที่สามของไฟล์ (บรรทัดคำอธิบายใต้ชื่อโครงงาน) จาก
แบบฝึกหัดของรายวิชา 10306343 (DevOps) เริ่มจากบทที่ 1เป็น
แบบฝึกหัดของรายวิชา 10306343 (DevOps) ต่อยอดต่อเนื่องทุกบทแล้ว commit
git add README.md
git commit -m "ปรับคำอธิบายโครงงานให้ตรงเป้าหมายของวิชา"[main 7d8f70b] ปรับคำอธิบายโครงงานให้ตรงเป้าหมายของวิชา
1 file changed, 1 insertion(+), 1 deletion(-)การแก้ครั้งนี้อยู่บรรทัดที่ 3 ส่วนของ branch feature/usage แตะเฉพาะท้ายไฟล์ สองบริเวณห่างกันพอที่ Git จะรวมให้เองได้ในขั้นตอนถัดไป (ถ้าแก้บริเวณเดียวกันจะเกิดอะไรขึ้น รอไว้ดูใน Lab C)
B.3 merge กลับด้วย --no-ff
git merge --no-ff feature/usage -m "รวม branch feature/usage: เพิ่มหัวข้อวิธีใช้"Auto-merging README.md
Merge made by the 'ort' strategy.
README.md | 6 ++++++
1 file changed, 6 insertions(+)Auto-merging README.md คือ Git รวมสองฝั่งของไฟล์นี้ให้เอง เพราะแก้คนละบริเวณ ดูประวัติทั้งหมดเป็นกราฟ
git log --oneline --graph --all* 9690908 (HEAD -> main) รวม branch feature/usage: เพิ่มหัวข้อวิธีใช้
|\
| * ae00216 (feature/usage) เพิ่มหัวข้อวิธีใช้สคริปต์ใน README
* | 7d8f70b ปรับคำอธิบายโครงงานให้ตรงเป้าหมายของวิชา
|/
* 77b5783 commit แรก: เพิ่ม README อธิบายโครงงานอ่านกราฟจากบนลงล่าง: จุด * คือ commit, เส้น |\ และ |/ คือการแยกและบรรจบของ branch, commit 9690908 ที่ผ่านจากสองเส้นคือ merge commit ที่มีพ่อแม่สองตัว และ (HEAD -> main) บอกว่าคุณอยู่บน main ซึ่งชี้ commit ล่าสุดนี้
B.4 ลบ branch ที่รวมแล้ว
git branch -d feature/usage
git branchDeleted branch feature/usage (was ae00216).
* maincommit ของ branch ที่รวมแล้วไม่หายไปไหน มันถูกเลี้ยงด้วย merge commit บน main การลบเป็นแค่การเก็บตัวชี้ที่ไม่ใช้แล้ว ตัวเลือก -d (ตัวเล็ก) จะปฏิเสธถ้า branch ยังไม่ได้ถูกรวม ซึ่งเป็นเครื่องกันพลาดให้คุณ
จุดตรวจ B.1–B.4: git log --oneline --graph แสดงรูปสามเหลี่ยมหนึ่งอัน (merge commit หนึ่งตัว) README.md มีทั้งคำอธิบายที่แก้บน main และหัวข้อวิธีใช้จาก branch และ git branch เหลือแค่ * main
ตัดสินใจเอง B.5 (LLO 2.1): ทำงานชิ้นที่สองด้วยตัวเองทั้งชิ้น โดยไม่ดูคำสั่งใน B.1–B.4: แตก branch ใหม่ชื่อ feature/goals เพิ่มหัวข้อใหม่ท้าย README.md
## ก้าวต่อไป
- บทที่ 3: ผลัก repository นี้ขึ้น GitHub แล้วทำงานเป็นทีมcommit, กลับ main, merge ด้วย --no-ff, ลบ branch แล้วดูกราฟ ข้อสังเกตที่ต้องตอบใน Learning Log: รอบนี้ main ไม่มี commit ใหม่เลยหลังแตก branch ถ้าไม่ใส่ --no-ff ผลจะต่างอย่างไร (ทดลองได้ แต่ให้จบด้วย merge commit ตามกติกาของวิชา)
ผลลัพธ์ที่คาดหวัง B.5: กราฟมีสามเหลี่ยมสองอันซ้อนกัน และ README.md มีหัวข้อก้าวต่อไป
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
สั่ง merge แล้วขึ้น Already up to date. | อยู่บน branch ที่จะ merge (ทิศกลับ) หรือ merge ซ้ำอันที่รวมไปแล้ว | merge ต้องสั่งจาก branch ปลายทาง ในบทนี้คืออยู่ main ก่อนสั่ง git merge feature/... ตรวจด้วย git branch ทุกครั้งก่อน merge |
| แก้ไฟล์และ commit ไปแล้ว ถึงคิดได้ว่าอยู่ branch ผิด | ไม่ดู branch ปัจจุบันก่อนเริ่มงาน | ถ้า commit ไปแล้ว ให้สร้าง branch ที่ต้องการตอนนี้ (git switch -c ชื่อ) แล้วคืน main ด้วย git switch main — commit จะอยู่กับ branch ใหม่ที่สร้าง |
| สั่ง merge แล้วจอขึ้นโปรแกรมแก้ข้อความรอ | merge commit ต้องการข้อความแต่ไม่ได้ใส่ -m | พิมพ์ :wq แล้วกด Enter (ใน vi) หรือกำหนด -m "ข้อความ" ตั้งแต่แรกตามที่บทสอน |
กราฟไม่เห็นเส้นแยกสองเส้น (|) | merge เกิดแบบ fast-forward เพราะไม่ใส่ --no-ff หรืออยู่ branch ผิด | ตรวจคำสั่งว่าเป็น git merge --no-ff ... -m "..." และอยู่ main |
merge แล้วเกิด CONFLICT ทั้งที่คิดว่าแก้คนละที่ | บริเวณที่สองฝั่งแก้อยู่ติดกันหรือทับกัน (เช่น ฝั่งหนึ่งแก้บรรทัดท้ายไฟล์ อีกฝั่งเพิ่งต่อท้ายไฟล์) | ถ้ายังไม่ถึง Lab C ให้ git merge --abort แล้วขยับจุดแก้ให้ห่างกัน หรืออ่าน Lab C แล้วแก้ไปเลย |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab C: conflict จำลอง
สถานการณ์: คุณกับเพื่อนร่วมทีมแก้หัวข้อเดียวกันพร้อมกันบนสอง branch ในทีมจริงจะเกิดจากสองคน ที่นี่คุณเล่นทั้งสองบทบาทเอง เพื่อให้เจอ conflict จริงและได้ฝึกแก้ตามคู่มือทางการ
เป้าหมาย: LLO 2.2 แก้ merge conflict จนไฟล์ได้ผลตามที่กำหนด โดยใช้ข้อมูลจากเอกสาร git ทางการ
ฝึกตามตัวอย่าง
C.1 ฝั่งแรก: เปลี่ยนชื่อโครงการบน branch ใหม่
git switch -c feature/titleเปิด README.md แก้บรรทัดแรกจาก # DevOps Lab 01 เป็น
# สมุดบันทึกการทดลอง DevOpscommit ด้วยข้อความ เปลี่ยนชื่อโครงการเป็นสมุดบันทึกการทดลอง
C.2 ฝั่งที่สอง: แก้บรรทัดเดิมบน main
git switch mainแก้บรรทัดแรกของ README.md (ซึ่งตอนนี้ยังเป็น # DevOps Lab 01 เพราะงานของ branch ยังไม่มาถึง) เป็น
# DevOps Lab 01 (10306343)commit ด้วยข้อความ ใส่รหัสรายวิชาในชื่อโครงการ
C.3 merge แล้วเจอ conflict จริง
git merge --no-ff feature/title -m "รวม branch feature/title"Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.Git บอกตรง ๆ ว่าผสมสองฝั่งของ README.md ไม่ได้ เพราะทั้งคู่แก้บรรทัดเดียวกันด้วยข้อความต่างกัน ดูสถานะและตัวไฟล์
git status
head README.mdOn branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: README.md<<<<<<< HEAD
# DevOps Lab 01 (10306343)
=======
# สมุดบันทึกการทดลอง DevOps
>>>>>>> feature/title
แบบฝึกหัดของรายวิชา 10306343 (DevOps) ต่อยอดต่อเนื่องทุกบทไฟล์ไม่พัง และไม่มีอะไรถูก commit ไปไหน Git แค่คั่นสองฝั่งไว้ให้คุณตัดสิน (หัวข้อ 2.5) ฝั่งบนคือของ main ที่คุณอยู่ ฝั่งล่างคือของ feature/title
C.4 ทางหนี: abort แล้ว merge ใหม่
ก่อนจะแก้ ให้ลองทางออกแรกจากเอกสารทางการ (git-merge) สักครั้งเพื่อรู้จักไว้
git merge --abort
git status --short
git merge --no-ff feature/title -m "รวม branch feature/title"คำสั่งแรกคืนทุกอย่างกลับไปก่อน merge (คำสั่งที่สองจึงเหลือแต่ untracked เดิม ๆ) แล้วเราสั่ง merge เข้ามาใหม่เพื่อกลับมาที่ conflict เหมือนเดิม ทางออกนี้ใช้เมื่อคุณยังไม่พร้อมแก้ เช่น อยากไปคุยกับเพื่อนร่วม branch ก่อน
C.5 แก้ conflict ให้จบและปิด merge
ตกลงกันเองหรือกับทีมว่าชื่อไหนถูก ข้อกำหนดของ Lab นี้คือใช้ฝั่ง main เปิด README.md แล้วแก้บรรทัดแรกให้เหลือเพียง
# DevOps Lab 01 (10306343)โดยลบเครื่องหมาย <<<<<<< HEAD, =======, >>>>>>> feature/title และข้อความฝั่ง branch ออกให้หมด จากนั้นตรวจว่าไม่มีเครื่องหมายค้างด้วย grep จากบทที่ 1
grep -n -E "^(<<<<<<<|=======|>>>>>>>)" README.md
echo "grep exit=$?"grep exit=1exit 1 คือ "ไม่พบ" ซึ่งเป็นผลที่ต้องการ (ระวัง: รูปแบบนี้ตรงกับบรรทัดที่ขึ้นต้นด้วยเครื่องหมายจริง ๆ ของ marker เท่านั้น ไม่ตรงกับเนื้อหาปกติของ README) แล้วปิดการ merge ตามขั้นที่เอกสารกำหนด: git add ไฟล์ที่แก้ แล้ว git commit
git add README.md
git commit -m "แก้ conflict ใน README.md: เลือกใช้ชื่อโครงการของฝั่ง main"
git branch -d feature/title
git log --oneline --graph | head -12[main 51fccf4] แก้ conflict ใน README.md: เลือกใช้ชื่อโครงการของฝั่ง main
Deleted branch feature/title (was 992146d).
* 51fccf4 (HEAD -> main) แก้ conflict ใน README.md: เลือกใช้ชื่อโครงการของฝั่ง main
|\
| * 992146d เปลี่ยนชื่อโครงการเป็นสมุดบันทึกการทดลอง
* | 229fb9a ใส่รหัสรายวิชาในชื่อโครงงาน
|/
* e47c37f รวม branch feature/goals: เพิ่มหัวข้อก้าวต่อไปสังเกตว่า commit ที่ปิด merge conflict มีสองพ่อแม่เหมือน merge commit ปกติ และทีมตรวจงานย้อนหลังได้ว่า commit นี้เกิดจากการแก้ conflict
จุดตรวจ C.1–C.5: grep ไม่เจอ marker (exit 1), บรรทัดแรกของ README.md เป็น # DevOps Lab 01 (10306343) และ git status กลับไปสะอาด (เหลือแต่ untracked เดิม)
ตัดสินใจเอง C.6 (LLO 2.2): จำลอง conflict อีกครั้งด้วยตัวเอง แต่รอบนี้ให้แก้ด้วยวิธี "รวมความหมายทั้งสองฝั่ง" แทนการเลือกฝั่งเดียว
- แตก branch ใหม่ (ตั้งชื่อเอง เช่น
feature/filelist) แล้วแก้หัวข้อ## ไฟล์ในโครงงานของREADME.mdให้สั้นลงเป็น## รายการไฟล์แล้ว commit - กลับ
mainแก้หัวข้อเดิมเป็น## ไฟล์ในโครงงาน (จากบทที่ 1)แล้ว commit - merge ให้เกิด conflict แล้วแก้โดยให้เหลือหัวข้อเดียวคือ
## รายการไฟล์จากบทที่ 1(รวมทั้งสองฝั่ง) ตรวจด้วยgrepว่าไม่เหลือ marker แล้ว commit ปิด merge - เปิดเอกสารทางการ git-merge หัวข้อ HOW TO RESOLVE CONFLICTS (และ/หรือ Pro Git §3.2) แล้วเขียนบันทึก 2–3 บรรทัดว่าอ่านหัวข้อใด และเข้าใจอะไรเพิ่มจากตอนทำ hands-on — บันทึกนี้ใช้ส่งงานข้อ 2
ผลลัพธ์ที่คาดหวัง C.6: README.md มีหัวข้อ ## รายการไฟล์จากบทที่ 1 หนึ่งเดียว ไม่มี marker เหลือ มี commit ปิด merge ตัวที่สองในประวัติ และมีบันทึกแหล่งทางการที่อ่าน
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
merge แล้วขึ้น Already up to date. ไม่มี conflict | สองฝั่งแก้คนละบริเวณ Git รวมให้แล้ว หรือขืน commit ฝั่งหนึ่งไม่ครบ | ตรวจว่าแก้บรรทัดเดียวกันจริง และ commit ครบทั้งสองฝั่งก่อน merge |
git commit ตอน conflict ขึ้น error: Committing is not possible because you have unmerged files | ยังไม่ได้ git add ไฟล์ที่แก้เสร็จ | แก้ไฟล์ให้จบก่อน แล้ว git add README.md แล้วจึง commit |
commit แล้วเปิดไฟล์อีกครั้งยังเห็น <<<<<<< | ลบเครื่องหมายหรือข้อความฝั่งหนึ่งไม่ครบ | ใช้ grep -n -E "^(<<<<<<<|=======|>>>>>>>)" README.md หาตำแหน่ง แล้วแก้และ commit การแก้ไขซ้ำเป็น commit ใหม่ |
| อยู่ๆ อยากย้อนกลับตอนกำลังแก้ conflict | ยังไม่พร้อม หรือแก้พัง | git merge --abort ใช้ได้เฉพาะก่อน commit ปิด merge เท่านั้น |
เผลอเรียก git add . ตอนแก้ conflict | ความชินมือ | ยังไม่พังถ้าทำตามบท (ไฟล์ลับจะถูก stage แต่ยังไม่ถูก commit) — git restore --staged .env คืนสถานะได้ และตรวจ git status ก่อน commit เสมอ |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab D: .gitignore, สคริปต์เข้า repository และ tag v0.1.0
สถานการณ์: ก่อนส่งมอบ repository ให้ทีมในบทที่ 3 คุณต้องแยกให้ออกว่าไฟล์ไหนควรเก็บเข้า repository ไฟล์ไหนต้องไม่เข้าเด็ดขาด แล้วปักหมุดสถานะสมบูรณ์ของบทนี้ด้วย tag เป็น milestone แรกของโครงงานสะสม
เป้าหมาย: แยกไฟล์ที่ควรเก็บ/ไม่ควรเก็บด้วย .gitignore และปักหมุดเวอร์ชันด้วย tag (เตรียมพื้นสำหรับบทที่ 3 และ 5 ไม่นับ CLO)
ฝึกตามตัวอย่าง
D.1 สำรวจสิ่งที่เหลืออยู่
git statusOn branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env
app.log
app.log.bak
clean.log
hello.sh
scripts/
nothing added to commit but untracked files present (use "git add" to track)จัดกลุ่มไฟล์ที่เหลือตามที่มาจากบทที่ 1
| ไฟล์ | ที่มา | ควรเข้า repository ไหม |
|---|---|---|
scripts/check-errors.sh | Lab E ของบท 1 (แก้ตาม E.3 แล้ว) | ใช่ จะเป็นขั้นตรวจใน CI บทที่ 5 |
hello.sh | Lab B ของบท 1 | ใช่ เป็นสคริปต์ใช้งานได้จริง |
app.log | Lab C ของบท 1 | ไม่ เป็นข้อมูลตัวอย่างที่สร้างใหม่ได้ |
app.log.bak | ไฟล์สำรองจาก sed -i.bak ในบท 1 | ไม่ ไฟล์สำรองชั่วคราว |
clean.log | ผลจาก Lab E ของบท 1 | ไม่ เป็นผลรันที่สร้างใหม่ได้ |
.env | ไฟล์รหัสผ่านสมมติจาก Lab B ของบท 1 | ห้าม ไฟล์ลับต้องไม่ติดเข้า repository เด็ดขาด |
หลักคิด: เก็บสิ่งที่ต้องใช้ทำงานต่อ (โค้ด สคริปต์ เอกสาร) ไม่เก็บสิ่งที่เครื่องสร้างใหม่ได้เอง (ผลรัน, log, ไฟล์สำรอง) และไม่เก็บความลับเด็ดขาด
D.2 เก็บสคริปต์ที่ใช้งานจริงเข้า repository
git add scripts/check-errors.sh hello.sh
git commit -m "เก็บสคริปต์ของบทที่ 1 เข้า repository"
git ls-files[main bfa5a36] เก็บสคริปต์ของบทที่ 1 เข้า repository
2 files changed, 14 insertions(+)
create mode 100644 hello.sh
create mode 100644 scripts/check-errors.sh
README.md
hello.sh
scripts/check-errors.shgit ls-files แสดงไฟล์ทั้งหมดที่อยู่ใน repository — ตอนนี้มีสามไฟล์ และไม่มี .env
D.3 รู้จักและเริ่ม .gitignore
ไฟล์ .gitignore เป็นรายการชื่อไฟล์ที่ Git จะทิ้งไว้ ไม่ตามเล่น หนึ่งรายการต่อบรรทัด ใช้ * แทนอะไรก็ได้ เช่น *.bak คือไฟล์ใด ๆ ที่ลงท้าย .bak เริ่มจากสองรายการแรก
printf 'app.log\n*.bak\n' > .gitignore
git status --short?? .env
?? .gitignore
?? clean.logapp.log กับ app.log.bak หายไปจากสายตาของ Git แล้ว เหลือ .env กับ clean.log ที่ยังไม่มีในรายการ (ตัว .gitignore เองจะถูกเก็บเข้า repository เหมือนไฟล์ปกติ เพื่อให้ทีมได้กติกาเดียวกัน)
ตัดสินใจเอง D.4 (เตรียมพื้น): เติม .gitignore ให้ครบด้วยตัวเอง แล้วปิดบทด้วย tag
- คิดว่า
clean.logควรใส่เป็นชื่อตรงหรือใช้แพเทิร์น*.logดี (ข้อดี-ข้อเสียต่างกันอย่างไร) และ.envต้องอยู่ในรายการเสมอ เพราะเหตุใด - ตรวจว่าครบด้วย
git check-ignore .env app.log app.log.bak clean.log— คำสั่งนี้พิมพ์เฉพาะไฟล์ที่ถูกกัน - commit
.gitignoreด้วยข้อความเพิ่ม .gitignore กันไฟล์ log และไฟล์ลับ - ปิดบทด้วย annotated tag และดูภาพรวมสุดท้าย:
git tag -a v0.1.0 -m "รุ่นสมบูรณ์ของบทที่ 2 (M1): repository, .gitignore, สคริปต์ตรวจ log"
git tag
git show v0.1.0 --stat | head -8
git log --oneline --graph --all
git statustag คือชื่อติดกับ commit หนึ่งเพื่อใช้อ้างอิงเวอร์ชัน แบบ annotated (สร้างด้วย -a) เก็บชื่อผู้ติด tag วันที่และข้อความเป็นวัตถุของตัวเอง เอกสารทางการแนะนำให้ใช้แบบนี้กับ release (git-tag) เมื่อ commit ใหม่เกิดขึ้นภายหลัง tag ยังชี้ commit เดิม — นี่คือสิ่งที่ทำให้ tag ใช้ย้อนกลับมา "รุ่น v0.1.0" ได้เสมอ และเป็นพื้นของการติด tag image ในบทที่ 8
สำหรับคุณ v0.1.0 คือ M1 (milestone 1) ของโครงงานสะสมของวิชา: repository เดี่ยวที่เกิดจากแอปตั้งต้น มีประวัติ commit, branch และ merge ครบ มี .gitignore กันไฟล์ลับ และมี scripts/check-errors.sh พร้อมใช้ แบบเดียวกับที่บทที่ 1 บอกคุณไว้ว่า check-errors.sh จะถูกเก็บเข้า repository และใช้ต่อในบทที่ 5 — ตอนนี้มันเป็นความจริงแล้ว บทที่ 3 ทั้งทีมจะเริ่มจาก repository แบบนี้ของทุกคน
ผลลัพธ์ที่คาดหวัง D.4: git check-ignore พิมพ์ครบสี่ไฟล์, git status จบด้วย nothing to commit, working tree clean, git tag แสดง v0.1.0 และกราฟสุดท้ายมีหน้าตาแบบนี้ (hash ของคุณต่างกัน)
* 90db269 (HEAD -> main, tag: v0.1.0) เพิ่ม .gitignore กันไฟล์ log และไฟล์ลับ
* bfa5a36 เก็บสคริปต์ของบทที่ 1 เข้า repository
* aa4337e แก้ conflict หัวข้อรายการไฟล์: รวมความหมายทั้งสองฝั่ง
|\
| * 57237c4 เปลี่ยนหัวข้อรายการไฟล์ให้สั้นลง
* | 86585a0 ระบุที่มาของรายการไฟล์
|/
* 51fccf4 แก้ conflict ใน README.md: เลือกใช้ชื่อโครงการของฝั่ง main
|\
| * 992146d เปลี่ยนชื่อโครงการเป็นสมุดบันทึกการทดลอง
* | 229fb9a ใส่รหัสรายวิชาในชื่อโครงงาน
|/
* e47c37f รวม branch feature/goals: เพิ่มหัวข้อก้าวต่อไป
|\
| * 8fbcd09 เพิ่มหัวข้อก้าวต่อไปใน README
|/
* 9690908 รวม branch feature/usage: เพิ่มหัวข้อวิธีใช้
|\
| * ae00216 เพิ่มหัวข้อวิธีใช้สคริปต์ใน README
* | 7d8f70b ปรับคำอธิบายโครงงานให้ตรงเป้าหมายของวิชา
|/
* 77b5783 commit แรก: เพิ่ม README อธิบายโครงงานภาพนี้คือหลักฐานหลักของงานข้อ 1 (M1) ให้ถ่ายรูปเก็บไว้ตอนนี้เลย
จุดตรวจ D: git ls-files ไม่มี .env และไฟล์ log, git status สะอาด, git tag มี v0.1.0
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
เพิ่มชื่อไฟล์ใน .gitignore แล้ว git status ยังแสดงไฟล์นั้น | ไฟล์ถูก git add และ commit ไปก่อนแล้ว .gitignore มีผลเฉพาะไฟล์ที่ยังไม่ถูก track | git rm --cached ชื่อไฟล์ เอาออกจาก staging ของ Git (ไฟล์จริงยังอยู่) แล้ว commit การเอาออก — ถ้าเป็นไฟล์ลับที่ติดไปแล้ว ให้แจ้งผู้สอนทันที |
git check-ignore .env ไม่พิมพ์อะไร | รายการใน .gitignore พิมพ์ผิด หรือมีช่องว่างนำหน้า | ตรวจไฟล์ด้วย cat .gitignore แต่ละบรรทัดต้องชิดซ้าย |
| tag ข้อความพิมพ์ผิด อยากลบทำใหม่ | พิมพ์เร็วไป | git tag -d v0.1.0 ลบ แล้วติดใหม่ (ทำได้เพราะยังไม่ได้ผลัก tag ขึ้นไหน) |
ใส่ *.log แล้วกังวลว่าจะกันไฟล์ที่ทีมต้องการ | แพเทิร์นกว้างไป | นี่คือการ trade-off ของข้อ 1 ใน D.4 ถ้าทีมต้องการเก็บ log บางชนิดไว้ ให้ระบุชื่อเฉพาะแทน หรือใช้ exception ใน .gitignore (นอกขอบเขตบทนี้ รีวิวกับทีมในบทที่ 3) |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
เก็บกวาดเมื่อจบ Lab
Lab ของบทนี้ไม่มีอะไรต้องลบ — สิ่งที่ได้คือ repository ของคุณเอง ให้ยืนยันสถานะส่งมอบก่อนจบวัน
git status
git log --oneline --graph --all | head -5
git tagสิ่งที่ต้องเห็น: nothing to commit, working tree clean, กราฟมี merge commit อย่างน้อยสองตัว (จาก Lab B และ B.5) และ commit แก้ conflict (จาก Lab C และ C.6) และมี tag v0.1.0 เก็บโฟลเดอร์ ~/devops-lab01 ทั้งโฟลเดอร์ไว้ บทที่ 3 จะผลักมันขึ้น GitHub และใช้ต่อทั้งทีมจนจบภาคเรียน
แบบฝึกหัด
ทำข้อ ก1–ก2 และ ข1 ในชั้นเรียนถ้ามีเวลาเหลือ ข้อที่เหลือทำในเวลาศึกษาด้วยตนเอง ทุกข้อระบุผลลัพธ์การเรียนรู้ที่ฝึก
ก. ความเข้าใจ
- ก1 (LLO 2.1) จากกราฟต่อไปนี้ (เก็บมาจาก Lab B ก่อนลบ branch) ตอบ 3 ข้อ
* 9690908 (HEAD -> main) รวม branch feature/usage: เพิ่มหัวข้อวิธีใช้
|\
| * ae00216 (feature/usage) เพิ่มหัวข้อวิธีใช้สคริปต์ใน README
* | 7d8f70b ปรับคำอธิบายโครงงานให้ตรงเป้าหมายของวิชา
|/
* 77b5783 commit แรก: เพิ่ม README อธิบายโครงงาน(1) commit ใดคือ merge commit และรู้ได้อย่างไร (2) commit ae00216 อยู่บน branch ใด (3) ผู้ใช้นี้กำลังอยู่บน branch ไหน และรู้ได้จากอะไร
- ก2 (LLO 2.1) ไฟล์หนึ่งถูกแก้แล้วแต่ยังไม่ได้
git add— ไฟล์นั้นอยู่พื้นที่ใดของสามพื้นที่ และมีสถานะว่าอะไร - ก3 (พื้นความรู้ ไม่นับ CLO) ทีมเก็บโค้ดไว้ 3 ที่ (เครื่องละคนและโฟลเดอร์แชร์หนึ่ง) อธิบาย 2–3 บรรทัดว่าปัญหาอะไรจะเกิดขึ้น และ SSOT ช่วยอย่างไร
- ก4 (เตรียมพื้น) ไฟล์ต่อไปนี้ไฟล์ใดบ้างที่ควรอยู่ใน
.gitignoreของโครงงานคล้ายของเรา:README.md,.env,clean.log,scripts/check-errors.sh,app.log.bak
ข. การประยุกต์
- ข1 (LLO 2.1) เขียนลำดับคำสั่งทั้งชุด (อย่างน้อย 7 คำสั่ง) สำหรับงานหนึ่งชิ้นตามกติกา Branch-First โดยไม่เปิดดูบท: เริ่มจากการอยู่บน
mainที่สะอาด จนได้ merge commit และ branch ถูกลบ - ข2 (LLO 2.2) ไฟล์ที่กำลัง conflict มีบริเวณหนึ่งเป็น
<<<<<<< HEAD
เวอร์ชัน: 1.5.0
=======
เวอร์ชัน: 1.6.0
>>>>>>> releaseทีมตกลงให้ใช้ 1.6.0 เขียนว่าบรรทัดใดบ้างที่ต้องลบ และผลลัพธ์สุดท้ายของบริเวณนี้เป็นอย่างไร
- ข3 (LLO 2.2) เพื่อนสั่ง merge แล้วเจอ conflict แต่กลัวว่าจะทำพังเลยไม่กล้าแตะอะไร แนะนำเพื่อน 2 ทางเลือกพร้อมเงื่อนไขว่าแต่ละทางใช้ได้ถึงเมื่อไร (อ้างจากเอกสารทางการ)
ค. การวิเคราะห์ปัญหา
- ค1 (LLO 2.1) เพื่อนอีกคนทำ Lab B เสร็จแล้วบอกว่า "merge สำเร็จ แต่กราฟเป็นเส้นตรง ไม่มีสามเหลี่ยมเหมือนของคุณ" อธิบายสาเหตุ และบอกว่าต้องทำอย่างไรกราฟจะเห็น merge commit
- ค2 (LLO 2.2) เพื่อนแก้ conflict จน commit ผ่านไปแล้ว แต่พอเปิดไฟล์ดูพบข้อความ
=======ปนอยู่กลางไฟล์ และแอปทำงานผิดปกติ บอกสาเหตุ คำสั่งที่ใช้ตรวจหาจุดผิดทั้งไฟล์ และวิธีแก้ให้เรียบร้อย - ค3 (เตรียมพื้น) เพื่อนใจร้อนสั่ง
git add .ตอนทำ Lab A จน.envติด commit แรกไปแล้ว จึงมาเพิ่ม.envใน.gitignoreทีหลัง (Lab D) —.envจะหายไปจากการถูก track หรือไม่ เพราะเหตุใด และควรแก้อย่างไร (คำใบ้:git ls-filesและgit rm --cached)
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
ส่วนที่ 3 ศึกษาด้วยตนเอง
- อ่าน Pro Git บทที่ 2 Git Basics และ บทที่ 3 Git Branching แล้วสรุปแบบของคุณเองอย่างละ 5 บรรทัดว่าแต่ละบทสอนอะไรที่เกินจากที่ทำใน Lab
- ทำแบบฝึกหัดที่เหลือให้ครบ และทำ Lab B ซ้ำหนึ่งรอบโดยไม่เปิดดูคำสั่ง (ใช้ branch ชื่อใหม่ แล้วลบทิ้งเมื่อเสร็จ)
- เขียนงานที่ต้องส่งให้ครบทั้ง 4 ข้อ
- เขียน Learning Log ครั้งที่ 2 ตามหัวข้อในงานข้อ 4
- (ไม่บังคับ) เล่นเกม Oh My Git! ซึ่งให้ฝึกอ่านกราฟ commit และเคลื่อน branch ด้วยภาพ ผ่านด่านแรก ๆ ช่วยให้อ่าน
git log --graphคล่องขึ้น - (ไม่บังคับ) อ่าน Pro Git §3.6 Rebasing — วิชานี้ใช้ merge อย่างเดียวในแกนหลัก rebase เป็นภาคผนวกสำหรับผู้ที่อยากรู้ เพราะยากสำหรับผู้เริ่มต้น ถ้าอ่านแล้วลองสรุปว่า rebase กับ merge ต่างกันตรงไหน
งานที่ต้องส่ง
กำหนดส่ง: ก่อนเรียนบทที่ 4 เพื่อเว้นบัฟเฟอร์ 1 บท เช่นเดียวกับบทที่ 1 ที่ให้ส่งก่อนบทที่ 3
| ข้อ | งาน | หลักฐาน | ผลลัพธ์ | CLO |
|---|---|---|---|---|
| 1 | M1 ประวัติ repository ของคุณ | ภาพหรือข้อความผล git log --oneline --graph --all จากจุดสิ้นสุด Lab D พร้อมผล git tag และ hash ของ commit ที่ tag v0.1.0 ชี้ | 2.1 | CLO3 |
| 2 | การแก้ merge conflict | ผล merge ที่เกิด conflict (มีบรรทัด CONFLICT) และไฟล์ตอนมี marker, บรรทัดแรก/หัวข้อของไฟล์หลังแก้, hash ของ commit ที่แก้ conflict (C.5 และ C.6) และบันทึกแหล่งข้อมูลทางการที่อ่าน (URL + หัวข้อ + สิ่งที่เข้าใจเพิ่ม) | 2.2 | CLO3 |
| 3 | .gitignore และ tag | เนื้อหา .gitignore พร้อมเหตุผลโดยละเอียดว่ากันอะไรบ้างเพราะอะไร, ผล git check-ignore และ git status ที่สะอาด, ผล git show v0.1.0 --stat | เตรียมพื้น | ยังไม่นับ CLO ใช้ต่อในบทที่ 3 และ 5 |
| 4 | Learning Log ครั้งที่ 2 | ตอบ 4 หัวข้อ: วันนี้เรียนอะไร · คำสั่งหรือสถานะของ Git ใดยังไม่เข้าใจ · หาคำตอบจากแหล่งใด (ใส่ลิงก์เอกสารทางการอย่างน้อย 1 แหล่ง) · ถ้าต้องสอนเพื่อน จะอธิบาย conflict markers สามตัวอย่างไร | — | สะสม portfolio ประเมินรวมบทที่ 13 |
เกณฑ์ให้คะแนน
| ข้อ | ผ่าน | ยังไม่ผ่าน |
|---|---|---|
| 1 | กราฟมีโครงสร้างครบตามที่กำหนด: commit แรก, merge commit อย่างน้อย 2 ตัวจาก branch จริง (Lab B และ B.5), commit แก้ conflict อย่างน้อย 2 ตัว (Lab C และ C.6) และ tag v0.1.0 ชี้ commit สุดท้าย (แก้แล้วส่งใหม่ได้ 1 ครั้ง) | ขาด merge commit หรือ commit แก้ conflict หรือไม่มี tag หลังส่งใหม่ |
| 2 | มีผลก่อนแก้และหลังแก้ครบ, ไม่มี marker เหลือ, ไฟล์ตรงข้อความที่กำหนด และมีบันทึกแหล่งทางการพร้อมหัวข้อ | เหลือ marker, ไฟล์ไม่ตรง หรือไม่มีแหล่งอ้างอิงทางการ |
| 3 | .env ถูกกัน, มีเหตุผลประกอบรายการ, status สะอาด และมี tag v0.1.0 | ขาดข้อใดข้อหนึ่งจากสี่อย่าง |
| 4 | ตอบครบ 4 หัวข้อ และมีลิงก์เอกสารทางการ | ขาดหัวข้อ หรือไม่มีแหล่งอ้างอิง |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
สรุปบทและสิ่งที่ส่งต่อไปยัง Capstone
- ประวัติของ Git เก็บเป็น snapshot ของไฟล์ทั้งชุด ไฟล์เดินผ่านสามพื้นที่: working tree (แก้) → staging (คัดด้วย
git add) → repository (เก็บด้วยgit commit) - branch คือตัวชี้ ๆ เดียวไปที่ commit สร้างถูกมาก ไม่ต้องกลัว งานทุกชิ้นของวิชานี้เริ่มบน branch ใหม่ (Branch-First) แล้ว merge กลับ
mainด้วย--no-ffเพื่อให้เห็น merge commit ในกราฟ - merge เป็น fast-forward เมื่อปลายทางไม่มี commit ใหม่ และเป็น three-way merge เมื่อสองฝั่งแยกทางกัน
- conflict เกิดเมื่อสองฝั่งแก้บริเวณเดียวกันต่างกัน Git หยุดรอคนตัดสิน แก้ด้วย
git merge --abortหรือแก้ไฟล์ →git add→git commitและตรวจว่าไม่เหลือ marker ด้วยgrep .gitignoreกันไฟล์ที่เครื่องสร้างใหม่ได้และไฟล์ลับออกจาก repository ใช้ได้เฉพาะไฟล์ที่ยังไม่ถูก track- tag annotated (
git tag -a) ปักหมุดชื่อไว้กับ commit เพื่ออ้างอิงเวอร์ชันv0.1.0ของคุณคือ M1
สิ่งที่ส่งต่อไปยังโครงงานและ Capstone
| สิ่งที่ได้ | ไปอยู่ที่ |
|---|---|
repository ของคุณที่มีประวัติ commit/branch/merge, .gitignore และ tag v0.1.0 (M1) | บทที่ 3 ผลักขึ้น GitHub แล้ว branch หลักของคุณจะกลายเป็น main บน GitHub ของทีม · ใช้ต่อทุกบทถึง Capstone |
scripts/check-errors.sh ใน repository | บทที่ 5 ใช้เป็นขั้นตรวจใน CI (M1 ถูกใช้ต่อในบทที่ 5) |
นิสัยกันไฟล์ลับด้วย .gitignore และการระบุชื่อไฟล์ตอน git add แทน git add . | บทที่ 9 ตรวจว่าไม่พบรหัสผ่านใน repository · บทที่ 10 ย้ายความลับไป GitHub Secrets |
| tag กับเวอร์ชัน | บทที่ 4 ปรับเลขเวอร์ชันเป็น 0.2.0 · บทที่ 8 ติด tag ให้ image ตามเวอร์ชัน |
| Learning Log ครั้งที่ 2 | portfolio ที่ส่งในบทที่ 13 |
สรุปคำสั่งในบทนี้
| คำสั่ง | ใช้ทำอะไร |
|---|---|
git init | สร้าง repository ในโฟลเดอร์ปัจจุบัน |
git config --global user.name / user.email / init.defaultBranch / core.autocrlf | ตั้งค่าระดับผู้ใช้ครั้งเดียว (ตัวตน, ชื่อ branch แรก, การแปลงปลายบรรทัด) |
git status, git status --short | ดูสถานะไฟล์ว่าอยู่พื้นที่ใด |
git add ชื่อไฟล์ | ยกไฟล์ขึ้น staging (ระบุชื่อเสมอ จนกว่าจะมี .gitignore) |
git commit -m "ข้อความ" | ถ่ายรูปสิ่งที่อยู่บน staging ลงประวัติ |
git log --oneline --graph --all | ดูประวัติเป็นกราฟทั้งหมด (ใช้ตรวจงานและส่ง M1) |
git diff, git diff --staged | ดูการแก้ที่ยังไม่ stage และสิ่งที่กำลังจะติด commit |
git switch -c ชื่อbranch, git switch ชื่อbranch | สร้าง branch ใหม่พร้อมสลับไป และสลับ branch |
git branch, git branch -d ชื่อbranch, git branch -m main | ดูรายการ, ลบ branch ที่รวมแล้ว, เปลี่ยนชื่อ branch ปัจจุบัน |
git merge --no-ff ชื่อbranch -m "..." | รวม branch เข้า branch ปัจจุบันโดยบังคับ merge commit |
git merge --abort | ยกเลิก merge ที่ค้าง conflict (ใช้ก่อน commit ปิดการ merge) |
git ls-files | ดูรายชื่อไฟล์ที่ถูก track ทั้งหมด (ตรวจว่าไฟล์ลับไม่ติดเข้ามา) |
git check-ignore ไฟล์... | ตรวจว่าไฟล์ใดถูก .gitignore กันอยู่ |
git tag -a v0.1.0 -m "...", git tag, git show v0.1.0 --stat | ติด annotated tag, ดูรายการ tag, ดูรายละเอียดของ tag |
grep -n -E "^(<<<<<<<|=======|>>>>>>>)" ไฟล์ | ตรวจว่าไม่เหลือ conflict marker (จากบทที่ 1) |