6 คะแนน โดย GN⁺ 2023-11-03 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ต่อให้ใช้ Git มานาน ก็ยังสับสนได้ง่าย เพราะมีการ ใช้คำซ้ำและนำคำเดิมกลับมาใช้ใหม่ จำนวนมาก ทำให้การท่องจำคำสั่งอย่างเดียวไม่พอที่จะคาดเดาพฤติกรรมจริงได้
  • คำอย่าง HEAD, heads, detached HEAD, ours/theirs, ../... จะ เปลี่ยนความหมายไปตามบริบท แม้จะเป็นคำหรือสัญลักษณ์เดียวกัน
  • index, staged, cached ต่างก็เชื่อมโยงกับ .git/index แต่ reset, revert, restore แม้ชื่อจะคล้ายกัน กลับมีผลต่อ ประวัติคอมมิต, working tree, และ branch pointer ไม่เหมือนกัน
  • การที่ main track รีโมตอยู่ กับ remote-tracking branch อย่าง origin/main เป็นคนละเรื่องกัน และ checkout ก็รวมทั้งการสลับสาขาและการกู้คืนไฟล์ไว้ในคำสั่งเดียว
  • ถ้าอยากเข้าใจ Git จริง ๆ ต้องมอง โมเดลภายใน ที่สาขา, reference, commit, index และสถานะการติดตามรีโมตทำงานสัมพันธ์กัน ไม่ใช่มองแค่คำสั่งเป็นรายตัว

HEAD, branch, detached HEAD

  • ใน Git คำว่า heads หมายถึงสาขา และภายในจริง ๆ สาขาจะถูกเก็บไว้ในไดเรกทอรี .git/refs/heads
    • glossary ของ Git อย่างเป็นทางการแยก branch ว่าเป็นคอมมิตทั้งหมดบนเส้นนั้น และ head ว่าเป็นคอมมิตล่าสุด แต่ก็อาจมองได้ว่าเป็นสองวิธีในการดูสิ่งเดียวกัน
  • HEAD ชี้ไปยังสาขาปัจจุบัน และถูกเก็บไว้ใน .git/HEAD
    • ประโยคอย่าง “head คือสาขา และ HEAD คือสาขาปัจจุบัน” ก็สะท้อนความชวนงงของศัพท์ Git ได้ดี
  • detached HEAD state คือสถานะที่ HEAD ไม่ได้ชี้ไปยังสาขา แต่ชี้ไปยัง commit ID โดยตรง
    • ถ้า checkout ไปที่แท็ก ก็อาจเข้าสู่ detached HEAD ได้ เพราะแท็กไม่ใช่สาขา
    • ในสถานะนี้ git pull จะใช้งานไม่ได้ และ git push ก็จะใช้ไม่ได้เช่นกัน เว้นแต่จะใช้วิธีพิเศษ
    • ยังสามารถใช้ git commit, git merge, git rebase, git cherry-pick ได้ แต่คอมมิตใหม่จะไม่ผูกกับสาขาใด ทำให้ตามหาทีหลังได้ยาก
    • สามารถออกจาก detached HEAD ได้ด้วยการสร้างสาขาใหม่หรือสลับกลับไปยังสาขาเดิมที่มีอยู่

ours และ theirs ที่เปลี่ยนความหมายใน merge กับ rebase

  • ตอนแก้ conflict คำสั่ง git checkout --ours file.txt จะเลือกไฟล์ฝั่ง “ours” แต่ จุดอ้างอิงของ merge กับ rebase ไม่เหมือนกัน
  • ใน merge สาขาปัจจุบันคือ ours และสาขาที่กำลังถูก merge เข้ามาคือ theirs
    • ตัวอย่าง: ถ้า git checkout merge-into-ours แล้วรัน git merge from-theirs สาขาปัจจุบันจะเป็น ours
  • ใน rebase สาขาปัจจุบันคือ theirs และสาขาที่ใช้เป็นฐาน rebase คือ ours
    • ตัวอย่าง: ถ้า git checkout theirs แล้วรัน git rebase ours สาขาเป้าหมายจะเป็น ours
  • ความต่างนี้เกี่ยวข้องกับการที่ git rebase main ภายในจริง ๆ ทำงานโดยนำคอมมิตของสาขาปัจจุบันไป merge ซ้ำทีละตัวบนสำเนาของสาขา main
  • ourstheirs อธิบายคำว่า ours และ theirs แบบสั้น ๆ
    • ใน VSCode คำว่า “current change” กับ “incoming change” ก็ทำให้งงในลักษณะเดียวกันได้

ข้อความสถานะรีโมตและ fast-forward

  • Your branch is up to date with 'origin/main' ไม่ได้แปลว่า main ปัจจุบันตรงกับสถานะล่าสุดบนรีโมตเสมอไป
    • มันหมายความว่า local branch กับ origin/main ที่ได้มาจาก git fetch หรือ git pull ครั้งล่าสุดนั้นตรงกัน
    • ถ้า fetch ครั้งล่าสุดเกิดขึ้นเมื่อ 5 วันก่อน ก็เท่ากับ “อัปเดตตามข้อมูลเมื่อ 5 วันก่อน”
  • Git เก็บข้อมูลเวลาของ fetch ล่าสุดไว้ใน reflog แต่ข้อความสถานะไม่ได้แสดงเวลาว่าเป็นข้อมูลจาก “fetch ครั้งล่าสุด”
  • can be fast-forwarded หมายถึงรีโมต branch เดินหน้าไปไกลกว่า local branch และฝั่ง local ไม่มีคอมมิตเพิ่มเอง จึงสามารถ เลื่อน pointer ไปข้างหน้าอย่างเดียว ได้
    • ตัวอย่าง: ถ้า main เป็น A-B-C และ origin/main เป็น A-B-C-D-E หลัง git pull ทั้งคู่จะกลายเป็น A-B-C-D-E
    • กรณีนี้ไม่มีโอกาสเกิด merge conflict
  • ถ้า fast-forward ไม่ได้ แปลว่าสองสาขาแยกทางกันแล้ว
    • ตัวอย่าง: ถ้า main มีคอมมิต X ที่ origin/main ไม่มี และ origin/main มี D, E ที่ main ไม่มี ก็จะ fast-forward ไม่ได้
    • ตอนนั้น Git จะบอกว่าสองสาขาอยู่ในสถานะ diverged และต่างฝ่ายต่างมีจำนวนคอมมิตของตัวเอง

ไวยากรณ์เลือก commit: ^, ~, .., ...

  • HEAD^ และ HEAD~ ต่างก็ชี้ไปยัง คอมมิตก่อนหน้า 1 ตัว
    • HEAD^^^, HEAD~~~, HEAD~3 ต่างก็หมายถึงคอมมิตก่อนหน้า 3 ตัว
  • ในคอมมิตแบบ merge ที่มี parent หลายตัว ความหมายของ HEAD^2, HEAD^3 จะต่างออกไป
    • HEAD^ คือ parent ตัวแรก
    • HEAD^2 คือ parent ตัวที่สอง
    • HEAD^3 คือ parent ตัวที่สาม
    • HEAD~3 หมายถึง parent ของ parent ของ parent จึงไม่เหมือน HEAD^3
  • git log main..test กับ git log main...test ก็ทำงานต่างกัน
    • main..test แสดงคอมมิตที่มีอยู่เฉพาะใน test
    • test..main แสดงคอมมิตที่มีอยู่เฉพาะใน main
    • main...test แสดงคอมมิตที่มีเฉพาะอยู่ฝั่งใดฝั่งหนึ่งของทั้งสองด้านรวมกัน
  • ใน git diff เครื่องหมาย .. และ ... ถูกตีความต่างจากใน git log
    • git diff test..main จะเปรียบเทียบโดยรวมทั้งการเปลี่ยนแปลงฝั่ง test และฝั่ง main
    • git diff test...main จะเทียบ common ancestor กับปลายฝั่ง main จึงแสดง diff เพียงด้านเดียว
  • ความสับสนของสัญลักษณ์จุดถูกอธิบายเพิ่มเติมใน pain in dots

reference, refspec, tree-ish

  • ใน Git คำว่า reference ถูกใช้ได้หลายความหมาย
    • สาขาและแท็กอย่าง main, v0.2
    • HEAD ที่ชี้ไปยังสาขาปัจจุบัน
    • นิพจน์อย่าง HEAD^^^ ที่ตีความเป็น commit ID ได้
  • เอกสารของ Git เรียกนิพจน์อย่าง HEAD^^^ แบบเคร่งครัดว่า “revision parameters” แต่ในการใช้งานทั่วไปก็มักถูกปฏิบัติเหมือน reference
  • symbolic reference หมายถึง reference ที่ชี้ไปยัง reference อื่น เช่น HEAD
    • HEAD เป็นแนวคิดศูนย์กลางที่ส่งผลมากต่อพฤติกรรมของคำสั่งหลักของ Git
  • การตั้งค่า remote ใน .git/config จะมี refspec อยู่ด้วย
    • ตัวอย่าง: +refs/heads/main:refs/remotes/origin/main
    • หลายกรณีก็ใช้ค่าที่ git clone หรือ git remote add สร้างให้เป็นค่าเริ่มต้นเลย
  • tree-ish หมายถึงสิ่งที่สามารถใช้แทน THING ใน git checkout THING . ได้
    • commit ID
    • reference ของคอมมิต เช่น main, HEAD^^, v0.3.2
    • ไดเรกทอรีย่อยภายในคอมมิต เช่น main:./docs
    • ในทางปฏิบัติมักเข้าใจแบบง่าย ๆ ได้ว่าเป็น “คอมมิตหรือ reference ที่ชี้ไปยังคอมมิต”

index, staged, cached

  • index, staged, cached ต่างก็เกี่ยวข้องกับ .git/index
    • .git/index คือไฟล์ที่ใช้ตอน stage การเปลี่ยนแปลงด้วย git add
  • มีหลายคำสั่งที่ใช้คำต่างกันแต่ชี้ไปยังสิ่งเดียวกัน
    • git diff --cached
    • git rm --cached
    • git diff --staged
    • .git/index
  • อย่างไรก็ตาม แฟลก --index กับ --cached ไม่ได้มีความหมายเหมือนกันเสมอไป
    • บทความ ของ Junio Hamano ซึ่งเป็น lead maintainer ของ Git อธิบายความต่างเชิงรายละเอียดไว้
  • ด้วยเหตุผลด้านประสิทธิภาพ index ยังเก็บรายการ untracked files ด้วย แต่โดยทั่วไปจะไม่ถือว่า staging area รวม untracked files อยู่ด้วย

reset, revert, restore

  • reset, revert, restore เป็นคำที่คล้ายกัน แต่ใน Git ส่งผลต่อคนละสิ่ง
  • git revert COMMIT จะสร้าง คอมมิตใหม่ บนสาขาปัจจุบันเพื่อทำงานตรงข้ามกับ COMMIT
    • ถ้า COMMIT เพิ่มมา 3 บรรทัด คอมมิตใหม่ก็จะลบ 3 บรรทัดนั้นออก
  • git reset --hard COMMIT จะบังคับย้อนสาขาปัจจุบันกลับไปยังจุดของ COMMIT
    • นี่เป็นการกระทำที่อันตรายมาก เพราะลบการเปลี่ยนแปลงใหม่หลัง COMMIT ทิ้ง
  • git restore --source=COMMIT PATH จะคืนไฟล์ใน PATH ให้กลับไปเป็นสภาพตอน COMMIT
    • มันไม่เปลี่ยนไฟล์อื่นหรือประวัติคอมมิต
  • git reset --hard กับ git restore . เมื่อใช้เดี่ยว ๆ มักให้ผลคล้ายกัน แต่ git reset --hard COMMIT กับ git restore --source COMMIT . นั้นทำงานต่างกันโดยสิ้นเชิง

track, remote-tracking branch, untracked files

  • Git ใช้คำว่า track อยู่ 3 แบบ
    • Untracked files: ใน git status หมายถึงไฟล์ที่ Git ยังไม่ได้จัดการและยังไม่รวมอยู่ในคอมมิต
    • remote-tracking branch อย่าง origin/main คือ local reference ที่เก็บ commit ID ที่ main บนรีโมต origin ชี้อยู่ ณ ตอน git pull หรือ git fetch ครั้งล่าสุด
    • local branch ยังสามารถถูกตั้งค่าให้ track remote branch ได้ เช่นในข้อความ “branch foo set up to track remote branch bar from origin”
  • การบอกว่า main เป็นสาขาที่ track รีโมต กับการบอกว่า origin/main เป็น remote-tracking branch นั้นเป็นคนละเรื่อง
  • main เป็นสาขาจริง จึงสามารถสร้างคอมมิตและ merge ได้
    • ถ้าตั้งค่าไว้ใน .git/config ให้ track main บนรีโมต ก็จะใช้ git pull และ git push ได้สะดวกขึ้น
  • origin/main แม้ชื่อจะมีคำว่า “remote-tracking branch” แต่ก็ไม่ใช่สาขาปกติ
    • คอมมิตใส่โดยตรงไม่ได้
    • จะถูกอัปเดตเฉพาะตอนดึงสถานะล่าสุดของ main จากรีโมตผ่าน git pull หรือ git fetch เท่านั้น

checkout, reflog, merge/rebase/cherry-pick

  • git checkout รวมงานคนละแบบไว้ในคำสั่งเดียว
    • git checkout BRANCH ใช้สลับสาขา
    • git checkout file.txt ใช้ทิ้งการเปลี่ยนแปลงแบบ unstaged ของ file.txt
  • Git พยายามลดความสับสนนี้ด้วยการแยกความสามารถออกเป็น git switch และ git restore แต่ checkout แบบเดิมก็ยังใช้ได้อยู่
  • reflog ย่อมาจาก reference log คือบันทึกว่า reference เคยชี้ไปที่อะไรในอดีต
    • reference เป็นคำกว้างที่รวม branch, tag และ HEAD
    • reflog ช่วยกู้สถานการณ์ Git ที่แย่มาก ๆ ได้ เช่นเผลอลบสาขาสำคัญไป
  • merge, rebase, cherry-pick เป็นวิธีรวมคอมมิตที่ต่างกัน
    • merge สร้างคอมมิตใหม่หนึ่งตัวเพื่อรวมสองสาขา
    • rebase คัดลอกคอมมิตของสาขาปัจจุบันไปวางทีละตัวบนสาขาเป้าหมาย
    • cherry-pick คล้าย rebase แต่ไวยากรณ์ต่างกัน และเป็นการคัดลอกคอมมิตมายังสาขาปัจจุบัน
  • git rebase --onto main otherbranch mybranch ใช้ได้ในกรณีที่ต้องการย้ายเฉพาะบางคอมมิตของ mybranch ไปไว้บน main
    • เช่น ในโครงสร้างตัวอย่าง ถ้าต้องการ rebase แค่ F, G ขึ้นไปบน main
    • เป็นไวยากรณ์ที่จำยากเพราะมีชื่อสาขาถึงสามชื่อ

commit และคำศัพท์ชวนงงอื่น ๆ

  • commit ใน Git ใช้ได้ทั้งเป็นคำกริยาและคำนาม
    • คำกริยา: “commit บ่อย ๆ”
    • คำนาม: commit ล่าสุดของ main
  • Git commit มองได้ 3 แบบ
    • เป็น snapshot ของสถานะปัจจุบันของทุกไฟล์
    • เป็น diff เมื่อเทียบกับ parent commit
    • เป็น history ที่รวมคอมมิตทั้งหมดก่อนหน้า
  • แต่ละคำสั่งก็ปฏิบัติต่อ commit ไม่เหมือนกัน
    • git show มอง commit แบบ diff
    • git log มอง commit แบบ history
    • git restore มอง commit แบบ snapshot
  • blob, tree, origin, upstream, downstream, ความสัมพันธ์ระหว่าง fetch กับ pull, รวมถึง stash, worktree, subtree, submodule ก็เป็นส่วนหนึ่งของศัพท์ Git ที่ชวนสับสน
  • ฝั่งคำศัพท์ของ GitHub ก็มีตัวอย่างที่ชวนงง เช่น “pull request”, “squash and merge”, “rebase and merge”

แกนหลัก 3 อย่างของความสับสนที่วนซ้ำ

  • คำศัพท์ Git ที่ชวนสับสนมากเป็นพิเศษ มักย่อได้เป็น 3 เรื่อง
    • head คือสาขา ส่วน HEAD คือสาขาปัจจุบัน
    • “remote tracking branch” กับ “branch that tracks a remote” เป็นคนละอย่างกัน
    • index, staged, cached ต่างก็ชี้ไปยังสิ่งเดียวกัน
  • บทความปี 2012 the most confusing git terminology ก็พูดถึงด้วยว่าคำศัพท์ Git เชื่อมโยงกับคำจาก CVS และ Subversion อย่างไร
  • ความสับสนของศัพท์ Git ไม่ได้เกิดเฉพาะฟีเจอร์แปลก ๆ แต่เกิดซ้ำอยู่ทั่วฟีเจอร์หลักของระบบ
  • โดยเฉพาะเวลาอธิบายเรื่องสาขา คำว่า “tracking” ถูกใช้ได้หลายแบบจนคนเพิ่งเริ่มมักแยกไม่ออก
  • คำอธิบายบางส่วนยังแตะถึงมุมที่ไม่ค่อยถูกใช้งานของ Git จึงมีการทิ้งข้อสังเกตไว้ว่าอาจมีข้อผิดพลาดได้บ้าง

1 ความคิดเห็น

 
GN⁺ 2023-11-03
ความคิดเห็นบน Hacker News
  • คำศัพท์เหล่านี้ส่วนใหญ่จำยากสำหรับผม แต่สิ่งสำคัญที่ทำให้ทุกอย่างเข้าที่คือ หลายอย่างใน Git สุดท้ายแล้วเป็น พอยน์เตอร์ที่ชี้ไปยังคอมมิต
    ชื่อแบรนช์, HEAD, แท็ก ล้วนเป็นพอยน์เตอร์ HEAD ชี้ไปยังคอมมิตที่กำลังดูอยู่ และชื่อแบรนช์อย่าง my-feature ชี้ไปยังคอมมิตล่าสุดของแบรนช์นั้น ถ้าอยู่บน main แล้วรัน git checkout -b my-feature ทั้ง main, my-feature, HEAD จะชี้ไปยังคอมมิตล่าสุดของ main ทั้งหมด
    ทุกครั้งที่คอมมิตบน my-branch ทั้ง HEAD และ my-branch จะย้ายไปชี้คอมมิตใหม่ detached HEAD หมายถึงสถานะที่ไม่มีแบรนช์ใดชี้ไปยังคอมมิตที่ HEAD ชี้อยู่ ความต่างระหว่างแท็กกับแบรนช์คือ แท็กชี้ไปยังคอมมิตหนึ่ง ๆ และไม่ขยับ
    อีกเรื่องที่ทำให้สับสนคือเหตุผลที่หลายคำสั่งดูไม่สอดคล้องกัน เป็นเพราะ Git สมมติอาร์กิวเมนต์เริ่มต้นเอาไว้ git checkout file.txt เท่ากับ git checkout HEAD -- file.txt และเมื่ออยู่บน my-branch คำสั่ง git rebase main เท่ากับ git rebase main my-branch เพียงแต่รูปแบบหลังสามารถรันจากแบรนช์อื่นได้ด้วย
    สุดท้าย เมื่อทุกอย่างยุ่งเหยิง คำสั่งที่ช่วยให้หลุดออกจากสถานะแปลก ๆ ได้คือ git reflog มันจะแสดงคอมมิตทั้งหมดที่ HEAD เคยชี้ไป ถึงอย่างนั้นก็ดีใจที่ Git ยอมรับความสับสนนี้ และกำลังทำคำสั่งที่มีอินเทอร์เฟซน่าแปลกใจน้อยลงและเรียบง่ายขึ้น
    • ถ้าใช้ชื่อที่ Git ตั้งให้แนวคิดนี้ “พอยน์เตอร์คอมมิต” ก็คือ refs นั่นจึงเป็นที่มาของชื่อ reflog
      อีกจุดที่หลายคนใช้ Git แล้วไม่ค่อยนึกถึงคือ ref ของแบรนช์หรือแท็กจากรีโมตที่ fetch มาก็เป็น ref ทั้งหมดเช่นกัน ดังนั้นจึงอ้างอิงได้ทุกที่ที่ใช้ชื่อ ref ได้
      ตัวอย่างเช่น ถ้าต้องการบอกว่า “ไม่สนว่าแบรนช์นี้มีอะไรอยู่ ไม่ต้อง fast-forward ไม่ต้อง merge ไม่ต้อง rebase แค่เขียนทับแบรนช์โลคัลด้วยเนื้อหาจากรีโมต” ก็ทำแบบนี้ได้: git checkout foo, git reset --hard origin/foo
    • พูดให้แม่นคือ พอยน์เตอร์ HEAD ไม่ได้ชี้ตรงไปที่คอมมิต โดยปกติมันจะชี้ไปยัง พอยน์เตอร์แบรนช์ อย่าง my-branch แล้วพอยน์เตอร์แบรนช์นั้นจึงชี้ไปยังคอมมิต สถานะ detached HEAD เป็นข้อยกเว้น
      เมื่อคอมมิตบน my-branch ไม่ใช่ว่า HEAD และ my-branch ทั้งคู่ย้ายไปยังคอมมิตใหม่ แต่ HEAD ยังคงชี้ไปที่พอยน์เตอร์ my-branch และมีเพียง my-branch ที่ย้ายไปชี้คอมมิตใหม่ แน่นอนว่าถ้าตาม HEAD ผ่าน my-branch ไปยังคอมมิต ก็จะไปถึงคอมมิตใหม่
      detached HEAD คือสถานะที่ HEAD ชี้ตรงไปยังคอมมิตแทนที่จะชี้พอยน์เตอร์แบรนช์ แม้อยู่ในสถานะ detached HEAD ทั้ง HEAD และพอยน์เตอร์แบรนช์ก็อาจชี้ไปยังคอมมิตล่าสุดทั้งคู่ได้ ถ้าดูด้วย git log --decorate ปกติจะเห็นเป็น (HEAD -> my-branch) แต่ในกรณีนี้จะแสดงบนคอมมิตล่าสุดคล้าย (HEAD, my-branch)
    • คำอธิบายนี้ดี แต่ส่วนที่ทำให้สับสนมากขึ้นซึ่งเจอบ้างคือ แท็กมีสองชนิด แท็กส่วนใหญ่เป็น lightweight tag ที่ชี้ไปยังออบเจ็กต์คอมมิตอย่างที่ว่า และไม่มีอะไรมากกว่านั้น
      ส่วน annotated tag สามารถเก็บข้อความ ไทม์สแตมป์ SHA ฯลฯ ได้ พวกนี้เป็นออบเจ็กต์ Git ที่สมบูรณ์ และทำงานค่อนข้างคล้ายออบเจ็กต์คอมมิต แต่โดยปกติก็ยังอ้างอิงไปยังออบเจ็กต์ Git อื่น ซึ่งมักเป็นคอมมิต
    • ถ้าเป็นคนระวังมากเกินไปเหมือนผม ก่อนเริ่มขั้นตอนยุ่งยากที่มี conflict เยอะหรือมีการย้ายหลายครั้ง การรัน git rev-parse HEAD แล้วคัดลอกสตริงนั้นเก็บไว้ในที่ปลอดภัยก็เป็นความคิดที่ดี
      มันทำให้อุ่นใจว่าได้ทำเครื่องหมายสถานะที่มั่นใจว่าปกติไว้ตรง ๆ แล้ว โดยทั่วไปจะกลับไปยัง สถานะปกติล่าสุด ได้ ไม่ใช่จุดที่เข้าไปในความโกลาหลแล้วหลายขั้น แน่นอนว่าภายใต้เงื่อนไขว่าไม่ได้ใช้ git gc หรือ git filter-branch อะไรทำนองนั้นอย่างไม่ระวัง
    • คำอธิบายนี้ยังแสดงให้เห็นได้ดีว่า โครงสร้างต้นไม้ ภายใน Git เป็นสิ่งที่ครอบงำทุกอย่าง
      ฟังเหมือนมุกแต่ไม่ใช่มุกเลย การบอกว่าการใช้ Git ต้องมีปริญญาวิทยาการคอมพิวเตอร์จริง ๆ ก็ไม่ได้ผิดนัก ผมเห็นด้วยว่าอาร์กิวเมนต์เริ่มต้นของ Git ทำให้ประสบการณ์ใช้งานไม่สอดคล้องกันมาก
      git checkout ., git reset --hard/soft, git cleanup ทั้งหมดดูคล้ายกันมาก แต่จริง ๆ แล้วต่างกันมาก ถึงอย่างนั้นผมก็ยังชอบ Git มากกว่า Perforce หรือ SVN อยู่ดี เมื่อก่อนผมชอบ Mercurial มากกว่านิดหน่อย แต่ตอนนี้ไม่แน่ใจแล้วยังเป็นแบบนั้นไหม
      git reflog คือราชาและแสดงความจริง
  • ประมาณ 10 ปีแล้วที่ Git “ชนะ” สงครามระบบควบคุมเวอร์ชันจากกระแสเทคโนโลยีไร้เหตุผลอีกระลอกหนึ่ง แต่เรายังมีปัญหากับงานที่ควรจะเล็กน้อยอยู่
    ผมจำได้ว่าเคยอ่านที่ Linus Torvalds เขียนว่าเขาประหลาดใจที่ผู้คนเริ่มใช้ Git โดยตรงโดยไม่วางเลเยอร์ที่เป็นมิตรกว่านี้ทับไว้ ถ้าวงการนี้มีการทบทวนตัวเองได้ ก็คงยอมรับว่าการเลือก Git เป็นความผิดพลาด แล้วเปลี่ยนไปใช้ระบบควบคุมเวอร์ชันอื่น แทนที่จะเสียเวลาอย่างมหาศาลกับเครื่องมือที่มีหน้าที่แค่เก็บข้อความ
    • ผมไม่มองว่า Git เป็นเครื่องมือเก็บข้อความ มันเป็นเครื่องมือที่ประสานการเปลี่ยนแปลงที่หลายคนทำกับ “ข้อความ” เดียวกันได้ค่อนข้างละเอียดและเสถียร โดยไม่บล็อกความคืบหน้าของใคร ลองทำด้วย Word ดูแล้วจะเห็นต่างกันมาก
      แน่นอนว่ายังทำให้ดีขึ้นได้ แต่ความเกลียดชังที่ Git ได้รับทำให้ผมแปลกใจเสมอ Git เป็นเครื่องมือที่ยอดเยี่ยม และล้ำหน้าเครื่องมือที่ทำมาสำหรับผู้ใช้ที่ไม่ใช่นักพัฒนาอย่าง Word หรือ Google Docs มาก
      อาจเป็นเพราะผมแก่พอที่จะจำยุคที่ Subversion เป็นราชาได้
    • เห็นด้วยอย่างยิ่ง
      ทุกสัปดาห์ผมเห็นเพื่อนร่วมงานสับสนเพราะ Git เห็นโพสต์ขอความช่วยเหลือออนไลน์ และเห็นบทความที่พยายามอธิบายสิ่งที่ควรเรียบง่ายอีกครั้งแบบบทความนี้
      ตลอดเวลาที่เขียนโค้ด ผมจำไม่ได้ว่ามีใครเสียเวลาหลายชั่วโมงไปกับการย้อนแก้ความยุ่งเหยิงที่ตัวเองสร้างไว้ใน SVN, TFVC, Perforce ฯลฯ
      เครื่องมือมีอยู่เพื่อทำให้ชีวิตง่ายขึ้น ถ้าทำไม่ได้ ก็ไม่คุ้มที่จะเสียเวลาให้
    • แม้ในปี 2013 ก็ยังไม่ชัดเจนเลยว่า Git จะชนะ มีผู้ใช้ darcs, Mercurial, svn หรือแม้แต่ cvs อยู่ค่อนข้างมาก และเครื่องมือในยุคนั้นมักมีปลั๊กอินที่รองรับทั้งหมดเหล่านี้

ผมยังอยากให้มีความหลากหลายแบบนั้นอยู่ วัฒนธรรม Git แบบเดี่ยว ยังหมายความด้วยว่าอะไรก็ตามที่คิดจะมาแทน Git ต้องเริ่มจากการ implement Git ขึ้นมาใหม่ก่อน ดังนั้นเราจึงแทบจะถูกผูกติดกับ Git ไปตลอด เหมือน ASCII หรือปุ่ม Scroll Lock

  • Git ชนะด้วยเหตุผลที่ดี และดีกว่าสิ่งที่มาก่อนหน้าอย่างชัดเจน ตอนนี้การด่า Git อาจเป็นกระแสเหมือน jQuery แต่ตอนที่มันออกมา มันคือการปรับปรุงที่ชัดเจน
  • ผมเพิ่งเขียนบทความเมื่อเร็ว ๆ นี้เกี่ยวกับปัญหาคล้ายกัน ว่าทำไมเราอาจติดอยู่กับ Git
    ความกังวลหลักคือ GitHub ดีเกินไป จนกว่า GitHub จะแยก Git ออกจากเอนจินจัดการข้อความและอนุญาตให้ใช้เอนจินอื่น เราก็จะถูกผูกติดกับ Git
    https://trolololo.xyz/github
    https://news.ycombinator.com/item?id=38098109
  • ศัพท์ของ Git หลายคำจะสมเหตุสมผลก็ต่อเมื่อเข้าใจวิธีทำงานแล้ว แต่ถ้าไม่รู้รายละเอียด implementation หรือเอาไปวางเทียบกับศัพท์อื่น ๆ ก็แทบไม่สมเหตุสมผลเลย เป็นเหมือนเคสทางคลินิก
    เป็นศัพท์แบบ abstraction ที่รั่ว
    • ถูกบางส่วน แต่ก็เป็นเพราะส่วนต่าง ๆ ของ Git ถูกสร้างโดยคนต่างกันและตั้งชื่อแยกกันด้วย เดิมที Linus เรียกมันว่า cache ซึ่งเป็นศัพท์เชิงวิทยาการคอมพิวเตอร์ที่สุดแล้ว
      ต่อมา Junio ดูเหมือนจะเปลี่ยนเป็น index ซึ่งเฉพาะเจาะจงกับระบบควบคุมเวอร์ชันแบบกระจายมากกว่า และผู้ใช้ส่วนใหญ่เรียกมันว่า staging area ตอนนี้วิวัฒนาการของศัพท์เหล่านี้กลายเป็นซากฟอสซิลค้างอยู่ใน UI และภายในของ Git
    • ไม่ใช่ว่ามันสมเหตุสมผล แค่เป็นการตั้งชื่อที่ห่วยแตกเท่านั้น การตั้งชื่อที่ดีต้องใช้ความพยายาม แต่นักพัฒนา Git รุ่นแรก ๆ ไม่ได้ทำสิ่งนั้น
    • ถ้าลองจินตนาการว่าการใช้งานพื้นฐานต้องรู้เรื่องระบบปฏิบัติการ, IDE, และภายในของคอมไพเลอร์ นั่นก็คือการออกแบบ API ที่แย่โดยแท้
  • “Your branch is up to date with origin/master” คือคำโกหกที่ใหญ่ที่สุดที่ได้ยินทุกวัน ดังนั้นทุกครั้งผมก็แค่ pull
    • พูดแบบเคร่งครัดแล้วมันจริง origin/master ไม่ได้หมายถึง master ที่อยู่บน origin แต่หมายถึง commit ที่ origin/master เท่าที่รู้ล่าสุดชี้อยู่ ค่านี้จะถูกอัปเดตตอน fetch และ pull จะทำ fetch ให้อัตโนมัติ
      ถ้าดู ls -l .git/refs/remotes/origin/master จะเห็นว่า origin/master เป็นแค่ไฟล์ในระบบของเราเอง และยังดูได้ด้วยว่ามันเปลี่ยนเมื่อไร มันไม่ได้อัปเดตด้วยเวทมนตร์
      ถ้ามีการเปลี่ยนแปลงตอนรัน git fetch origin ทั้ง timestamp และเนื้อหาจะเปลี่ยน ตรวจสอบได้ด้วย cat .git/refs/remotes/origin/master
      พื้นฐานของ Git นั้นเรียบง่ายมาก โครงสร้างข้อมูลหลักและ operation ไม่กี่อย่างสามารถ implement ได้ในหนึ่งวัน การรู้เรื่องนี้มีค่ามาก ไม่รู้เกิดอะไรขึ้น Git ถึงสร้าง user interface ที่ซับซ้อนมากไว้บนแกนหลักที่ค่อนข้างเรียบง่ายได้
    • ข้อความนั้นแค่หมายความว่า Git จะไม่ต่ออินเทอร์เน็ตหรือ remote repository เพื่อตรวจอัปเดต เว้นแต่จะสั่งอย่างชัดเจน ผมคิดว่าคุณสมบัตินี้เองเป็นสิ่งที่ดี เพียงแต่ข้อความควรชัดเจนกว่านี้
    • น่าจะเป็นประมาณ “Your branch is up to date with the local copy of origin/master, last updated on XXXX-XX-XX:XX:XX:XX”
    • ผมลบ branch master ในเครื่องทิ้งไปแล้ว โดยปกติคุณ push ไป master ไม่ได้ หรือยังไงก็ไม่อยากทำงานบน master โดยตรงอยู่แล้ว ถ้าอย่างนั้นก็ไม่มีเหตุผลที่จะมี branch master ในเครื่องที่แก้ไขได้
      ดังนั้นแค่อัปเดต origin/master ก็พอ และ fetch ก็เพียงพอแล้ว
    • git fetch && git pull
      รอวันที่จะเกษียณในอีก 3 ปี แล้วล้างมือจากขยะพัง ๆ นี่ให้ได้
  • ผมใช้ Git มาตั้งแต่แทบจะช่วงแรก ๆ ที่มันออกมา แต่เพิ่งมารู้จัก porcelain เอาสัปดาห์นี้เอง มีโปรเจกต์หนึ่งที่ parse output ของ git status อยู่ พอใส่ flag --porcelain แล้วช่วยได้จริง ๆ มันสร้าง output ที่กระชับกว่าและ parse ด้วยโปรแกรมได้ง่ายกว่า
    เลยสงสัยว่ามีคำสั่งอีกกี่ตัวที่มี output ที่เครื่องอ่านได้ง่ายกว่า จนไปดูหน้า “Git Internals - Plumbing and Porcelain” ของ git-scm สรุปคือ Git ถูกเขียนขึ้นตั้งแต่แรกในฐานะชุดเครื่องมือสำหรับจัดการ version control มากกว่าจะเป็นระบบ version control ที่ขัดเกลาแล้ว
    คนจำนวนมากที่ใช้ Git ตั้งแต่แรก ๆ เรียนรู้งาน version control ผ่านคำสั่งระดับต่ำเหล่านี้ แล้วก็เผยแพร่ workflow นั้นไปยังคนอื่น นี่คือเลเยอร์ plumbing ต่อมา Git ได้สร้างเลเยอร์ที่ขัดเกลามากขึ้น และเรียกมันว่า porcelain
    ผมยังไม่เข้าใจทั้งหมดว่าคำสั่งไหนอยู่เลเยอร์ไหน แต่เรื่องนี้ช่วยให้เข้าใจ workflow ใหม่ ๆ ที่แนะนำกันในช่วงไม่กี่ปีที่ผ่านมา และทำให้ผมตัดสินใจได้ดีขึ้นเวลาจะเปลี่ยน workflow ของตัวเอง
    https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Po...
    • นี่เป็นสัญญาณชัดเจนว่า Linus พอใจกับ plumbing ซึ่งเป็นโครงสร้างภายใน แต่เห็นว่า porcelain ซึ่งเป็น UI ยังต้องปรับปรุง เดิมที porcelain ทั้งหมดเป็นสคริปต์ที่เรียก plumbing
      น่าเสียดายที่เพื่อให้เร็วขึ้น พวกเขา hardcode UI นั้นเข้าไป แต่ไม่ได้ปรับปรุงมัน
  • น้ำเสียงการเขียนของ Julia ดีมาก เธอเขียนเนื้อหาที่มีคุณค่ามากแม้กับนักพัฒนาระดับเก๋า ขณะเดียวกันก็รักษาโทนที่เป็นมิตรและไม่กีดกันคนที่เพิ่งเข้ามาในสายนี้
  • ผมคิดว่าตัวเองเป็น “โรคสต็อกโฮล์มจาก Git” คำศัพท์พวกนี้ไม่ได้ทำให้สับสนขนาดนั้น แต่คงเป็นเพราะผมแทบไม่ได้เรียนรู้ระบบอื่น และผูกติดกับ Git อย่างลึกซึ้งมาตั้งแต่ราวปี 2011
    พอมองย้อนกลับไป ตอนเริ่มแรกมันคงสับสนสุด ๆ แต่ผมคงแกล้งทำเหมือนไม่เป็นแบบนั้นเพื่อให้ดูเท่
    • หลังจากได้เห็น CLI ของระบบควบคุมเวอร์ชัน Fossil ผมถึงรู้ว่าตัวเองมีโรคสต็อกโฮล์ม มันใช้งานได้ตรงสัญชาตญาณมาก
    • ก่อนใช้ Git ผมเคยใช้ cvs กับ svn และเมื่อเทียบกับ svn แล้ว Git เข้าใจง่ายกว่ามาก
    • ระบบควบคุมเวอร์ชันอื่นที่ผมเคยใช้มีแค่ Plastic SCM แต่ไม่ว่าจะเป็นโรคสต็อกโฮล์มหรือไม่ Git ก็ดีกว่ามาก
  • สิ่งที่น่าดูประกอบ: Git staging area เป็นคำที่ทุกคนเห็นพ้องกันตามตัวอักษร

https://news.ycombinator.com/item?id=28143078
“Your branch is up to date with ‘origin/main’” จริง ๆ แล้วชวนให้เข้าใจผิดอยู่บ้าง อาจตีความได้ว่าแบรนช์ main เป็นเวอร์ชันล่าสุดแล้ว แต่ความจริงไม่ใช่อย่างนั้น
แต่ก็ไม่จำเป็นต้องยึดติดกับคำว่า “ถ้าพูดอย่างเคร่งครัด” มากเกินไป เรื่องนี้ไม่ใช่ว่าเข้าข่าย ปัญหานายพลสองคน หรือ? ถ้าจับผิดกันจริง ๆ เราไม่มีทางรู้ได้ว่า “ล่าสุด” ณ วินาทีนี้หรือไม่ ต่อให้ fetch ก่อน status ก็เหมือนกัน
ถ้าอย่างนั้นความคาดหวังที่สมเหตุสมผลคืออะไร? ก็ประมาณว่า ref ที่อยู่ในฐานข้อมูลอ็อบเจกต์บนเครื่องของฉัน เท่ากับ ref ในฐานข้อมูลอ็อบเจกต์อีกแห่งที่อยู่ไกลออกไป ณ ครั้งสุดท้ายที่ตรวจสอบ
การเรียก status ง่าย ๆ จะไปทำ fetch ผ่านเครือข่าย หรือคอยเตือนหลักการพื้นฐานของเครื่องมือที่ใช้อยู่ทุกครั้งไม่ได้ ส่วนตัวผมคิดแบบนั้น แต่ที่น่าประหลาดคือคอมเมนต์จำนวนมากในคำตอบบน StackOverflow กลับมองว่าต้องมีทั้งสองอย่างนั้น
ข้อความอย่าง “is up to date with the origin’s main as of your last fetch 5 days ago” สมเหตุสมผลกว่าเล็กน้อย เพราะบอกแค่ว่าครั้งสุดท้ายที่ fetch ผ่านมากี่วันแล้ว
[1] https://stackoverflow.com/questions/27828404/why-does-git-st...
อีกอย่างหนึ่ง: ผมเคยคิดว่า ORIG_HEAD เกี่ยวกับ “origin’s head” เหมือน FETCH_HEAD แต่ไม่ใช่เลย “pseudoref” นี้ดูเหมือนใช้เป็นจุดบันทึกก่อนการเขียนประวัติใหม่ที่ซับซ้อนกว่าอย่าง rebase และถูกทำไว้ก่อนจะมี reflog น่าจะหมายถึง “original head” มากกว่า

  • ผมค่อนข้างมอง Git ในแง่ดี แต่เรื่องถ้อยคำของข้อความนั้นผมเห็นด้วยกับผู้เขียน อย่างไรก็ตาม ไม่เห็นด้วยกับทางเลือกที่เสนอมา
    “Your branch is up to date with ‘origin/main’” ในเชิงเทคนิคถือว่าถูก แต่คำว่า up to date ถ้าอ่านคร่าว ๆ จะสื่อเป็นนัยสองอย่าง: main เท่ากับ origin/main และ origin/main ก็เป็นเวอร์ชันล่าสุดด้วย กล่าวคือให้ความรู้สึกว่ามันถูกอัปเดตระหว่างคำสั่ง หรือถูกทำให้ทันสมัยโดยอัตโนมัติและเพิ่งซิงก์สำเร็จมาไม่นาน
    นี่ไม่ใช่สัญญาณให้เครื่องอ่านที่ต้องเป็นจริงภายในหนึ่งรอบสัญญาณ CPU แต่เป็นข้อความสถานะที่แสดงให้ผู้ใช้เห็น ไม่ต้องไปถึงทฤษฎีเครือข่าย การตีความแบบนั้นก็สมเหตุสมผลแล้ว
    “Up to date” ใกล้เคียงกับความหมายว่าตามทันในเชิงเวลา มากกว่าจะเป็นเรื่องตำแหน่งในเชิงพื้นที่ ตำแหน่งของแบรนช์โลคัลใกล้กับ “แบรนช์นี้ชี้ไปที่ไหน” ซึ่งเป็นเชิงพื้นที่ ส่วนสถานะของ remote ref ใกล้กับ “ฉันอัปเดต remote ref ครั้งล่าสุดเมื่อไร” ซึ่งเป็นเชิงเวลา แน่นอนว่านี่เป็นเรื่องอัตวิสัยมาก และทั้งสองอย่างก็อาจสื่อได้ทั้งสองความหมาย
    ถ้อยคำที่ดีกว่าอาจเป็น “Your branch matches origin/main”, “Your branch’s head is the same as origin/main”, “Your branch is pointing to the same commit as origin/main” อะไรทำนองนี้ เป็นการประนีประนอมระหว่างความชัดเจนกับความยืดยาว และอาจใส่ “remote ref last updated 5 days ago” ไว้ในวงเล็บตามข้อเสนอของผู้เขียนก็ได้
  • ผมไม่เห็นด้วยว่าการที่ status เรียก fetch ผ่านเครือข่ายเป็นเรื่องน่ารำคาญ ผมมองว่า งานที่ใช้เครือข่ายควรเป็นสิ่งที่สั่งอย่างชัดเจน ไม่ใช่เกิดขึ้นโดยนัย เวลารัน git status ผมอยากรู้สถานะของรีโพซิทอรีของผมที่อยู่ในระบบไฟล์
    ถ้าอยากรู้ว่ารีโมตอื่นเกิดอะไรขึ้น ผมก็ fetch เองแล้วค่อยเปรียบเทียบ
  • สิ่งที่คาดหวังคือได้ข้อมูลล่าสุด ณ เวลาที่เริ่มคำสั่ง status อาจมี race condition ระหว่างที่พิมพ์ข้อมูลลงเทอร์มินัล แต่ก็เป็นช่วงเวลาที่เล็กมาก
    โดยทั่วไปนี่หมายความว่าต้องการ fetch ปัญหานายพลสองคนใช้ได้กับฝั่งรีโมตเท่านั้น ถ้ารีโมตไม่ได้รับการยืนยันว่าคลไคลเอนต์ได้รับข้อมูลแล้ว มันก็สามารถส่งซ้ำต่อไปสักพักได้ นั่นไม่ใช่ปัญหาของไคลเอนต์
    ถ้าไคลเอนต์ไม่ได้รับข้อมูลจากเซิร์ฟเวอร์ พฤติกรรมที่ถูกต้องน่าจะเป็นค้างรออยู่แล้วแสดงข้อผิดพลาดหลัง timeout ซึ่ง fetch ปัจจุบันก็น่าจะทำแบบนั้นอยู่
  • ขั้นตอนที่มีค่าที่สุดในการเรียน Git คือการมีแบบจำลองทางความคิดที่ถูกต้อง ทุกอย่างคือ กราฟ
    เครื่องมือนี้ถูกแชร์บ่อย และก็เหมาะกับกรณีนี้ด้วย มันช่วยแสดงภาพโมเดลภายในของ Git และผลของคำสั่งต่าง ๆ
    https://learngitbranching.js.org/
    ผมได้เรียนรู้จากการใช้เครื่องมือนี้ 10 นาที มากกว่าประสบการณ์ Git ราว 10 ปีก่อนหน้านั้นเสียอีก แนะนำอย่างยิ่ง
  • ไม่อยากเสริมมากนัก แต่การที่คนของ Bitbucket สร้างคำว่า pull request ขึ้นมาก็เป็นปัญหาเหมือนกัน
    “pull” คือการรวมการเปลี่ยนแปลงจากรีโมตเข้ามาในรีโพซิทอรีโลคัล สิ่งที่ผู้ใช้ร้องขอจริง ๆ คือขอให้เซิร์ฟเวอร์รวมการเปลี่ยนแปลงรีโมตของตนเข้าไปในแบรนช์ใดแบรนช์หนึ่ง คำว่า “merge request” ของ GitLab จึงเป็นถ้อยคำที่ถูกต้อง
    ผมเคยเห็นคน fetch แล้วคอมมิตหายไป เพราะรีโพซิทอรีถอยหลังไปในเชิงเวลา ทำให้สิ่งที่เคยถูกซ่อนไว้ถูกซ่อนอีกครั้ง Git ซ่อนรายละเอียดแบบนั้นไว้ใน status ถ้าไม่ใช้ git-prompt หรือ powerline-shell ก็เหมือนทำงานอยู่ในความมืด
    • แนวคิดดั้งเดิมที่ยังเหลืออยู่ใน git-request-pull(1) คือเราทุกคนต่างมีรีโพซิทอรี Git ของตัวเองอยู่ที่ที่อย่าง kernel.org/git หรือ redhat.com/git แล้วส่งอีเมลขอให้อีกฝ่าย pull จากรีโพซิทอรีที่อยู่คนละเซิร์ฟเวอร์ องค์กร หรือโดเมนกัน
      GitHub ได้แรงบันดาลใจจากคำสั่ง request-pull ของ Git แต่ตีความใหม่ให้เป็นการ merge จากรีโพซิทอรี GitHub หนึ่งไปยังรีโพซิทอรี GitHub อีกแห่ง
    • จากมุมมองของเซิร์ฟเวอร์ ผู้ใช้คือรีโมต และเซิร์ฟเวอร์คือโลคัล ดังนั้นผู้ใช้จึงขอให้เซิร์ฟเวอร์ pull จากฝั่งของตน
      อย่างน้อยผมก็อ่านแบบนั้นมาตลอด
    • หนังสือ Git อธิบายที่มาของคำนี้และเหตุผลว่าทำไมมันจึงสมเหตุสมผล
      สรุปคือ มันไม่ใช่คำขอไปยังเซิร์ฟเวอร์ แต่เป็นคำขอไปยังอีกคนหนึ่ง เป็นการขอให้อีกฝ่าย pull แบรนช์ของเราไปดู เพื่อพิจารณาการเปลี่ยนแปลงที่เราอยากมีส่วนร่วมกับโปรเจกต์

[0] https://git-scm.com/book/en/v2/Distributed-Git-Contributing-...

  • คำว่า “pull request” เป็นคำที่ GitHub ทำให้แพร่หลาย และผมมองว่าเป็นคำที่ใช้ได้ดีทีเดียว ในทางปฏิบัติ การแยกว่าเป็นการ pull จาก repository อื่นจริง ๆ หรือเป็น “merge request” ภายใน repository เดียวกันนั้นไม่ได้มีอะไรน่าสนใจนัก “merge request” ให้ความรู้สึกเหมือนเป็นคำเฉพาะของ GitLab
  • เรื่องนี้เป็นสิ่งที่เข้าใจยากที่สุดเสมอมา รู้สึกว่าโดยธรรมชาติแล้วมันควรเป็น push request หรือ merge request ไม่ใช่ pull request ไม่ใช่หรือ