คำศัพท์ Git ที่ชวนสับสน
(jvns.ca)- ต่อให้ใช้ Git มานาน ก็ยังสับสนได้ง่าย เพราะมีการ ใช้คำซ้ำและนำคำเดิมกลับมาใช้ใหม่ จำนวนมาก ทำให้การท่องจำคำสั่งอย่างเดียวไม่พอที่จะคาดเดาพฤติกรรมจริงได้
- คำอย่าง
HEAD,heads, detached HEAD,ours/theirs,../...จะ เปลี่ยนความหมายไปตามบริบท แม้จะเป็นคำหรือสัญลักษณ์เดียวกัน index,staged,cachedต่างก็เชื่อมโยงกับ.git/indexแต่reset,revert,restoreแม้ชื่อจะคล้ายกัน กลับมีผลต่อ ประวัติคอมมิต, working tree, และ branch pointer ไม่เหมือนกัน- การที่
maintrack รีโมตอยู่ กับ 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- มีตัวอย่างที่เกี่ยวข้องใน เชลล์สคริปต์ที่ทำ
git rebaseด้วยgit merge
- มีตัวอย่างที่เกี่ยวข้องใน เชลล์สคริปต์ที่ทำ
- 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 วันก่อน”
- มันหมายความว่า local branch กับ
- 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แสดงคอมมิตที่มีอยู่เฉพาะในtesttest..mainแสดงคอมมิตที่มีอยู่เฉพาะในmainmain...testแสดงคอมมิตที่มีเฉพาะอยู่ฝั่งใดฝั่งหนึ่งของทั้งสองด้านรวมกัน
- ใน
git diffเครื่องหมาย..และ...ถูกตีความต่างจากในgit loggit diff test..mainจะเปรียบเทียบโดยรวมทั้งการเปลี่ยนแปลงฝั่งtestและฝั่งmaingit 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 อื่น เช่น
HEADHEADเป็นแนวคิดศูนย์กลางที่ส่งผลมากต่อพฤติกรรมของคำสั่งหลักของ 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 --cachedgit rm --cachedgit 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ให้ trackmainบนรีโมต ก็จะใช้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 ที่แย่มาก ๆ ได้ เช่นเผลอลบสาขาสำคัญไป
- reference เป็นคำกว้างที่รวม branch, tag และ
- merge, rebase, cherry-pick เป็นวิธีรวมคอมมิตที่ต่างกัน
- merge สร้างคอมมิตใหม่หนึ่งตัวเพื่อรวมสองสาขา
- rebase คัดลอกคอมมิตของสาขาปัจจุบันไปวางทีละตัวบนสาขาเป้าหมาย
- cherry-pick คล้าย rebase แต่ไวยากรณ์ต่างกัน และเป็นการคัดลอกคอมมิตมายังสาขาปัจจุบัน
git rebase --onto main otherbranch mybranchใช้ได้ในกรณีที่ต้องการย้ายเฉพาะบางคอมมิตของmybranchไปไว้บนmain- เช่น ในโครงสร้างตัวอย่าง ถ้าต้องการ rebase แค่
F,Gขึ้นไปบนmain - เป็นไวยากรณ์ที่จำยากเพราะมีชื่อสาขาถึงสามชื่อ
- เช่น ในโครงสร้างตัวอย่าง ถ้าต้องการ rebase แค่
commit และคำศัพท์ชวนงงอื่น ๆ
commitใน Git ใช้ได้ทั้งเป็นคำกริยาและคำนาม- คำกริยา: “commit บ่อย ๆ”
- คำนาม: commit ล่าสุดของ
main
- Git commit มองได้ 3 แบบ
- เป็น snapshot ของสถานะปัจจุบันของทุกไฟล์
- เป็น diff เมื่อเทียบกับ parent commit
- เป็น history ที่รวมคอมมิตทั้งหมดก่อนหน้า
- แต่ละคำสั่งก็ปฏิบัติต่อ commit ไม่เหมือนกัน
git showมอง commit แบบ diffgit logมอง commit แบบ historygit 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 ความคิดเห็น
ความคิดเห็นบน Hacker News
ชื่อแบรนช์,
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 ยอมรับความสับสนนี้ และกำลังทำคำสั่งที่มีอินเทอร์เฟซน่าแปลกใจน้อยลงและเรียบง่ายขึ้นreflogอีกจุดที่หลายคนใช้ Git แล้วไม่ค่อยนึกถึงคือ ref ของแบรนช์หรือแท็กจากรีโมตที่ fetch มาก็เป็น ref ทั้งหมดเช่นกัน ดังนั้นจึงอ้างอิงได้ทุกที่ที่ใช้ชื่อ ref ได้
ตัวอย่างเช่น ถ้าต้องการบอกว่า “ไม่สนว่าแบรนช์นี้มีอะไรอยู่ ไม่ต้อง fast-forward ไม่ต้อง merge ไม่ต้อง rebase แค่เขียนทับแบรนช์โลคัลด้วยเนื้อหาจากรีโมต” ก็ทำแบบนี้ได้:
git checkout foo,git reset --hard origin/fooHEADไม่ได้ชี้ตรงไปที่คอมมิต โดยปกติมันจะชี้ไปยัง พอยน์เตอร์แบรนช์ อย่าง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)ส่วน annotated tag สามารถเก็บข้อความ ไทม์สแตมป์ SHA ฯลฯ ได้ พวกนี้เป็นออบเจ็กต์ Git ที่สมบูรณ์ และทำงานค่อนข้างคล้ายออบเจ็กต์คอมมิต แต่โดยปกติก็ยังอ้างอิงไปยังออบเจ็กต์ Git อื่น ซึ่งมักเป็นคอมมิต
git rev-parse HEADแล้วคัดลอกสตริงนั้นเก็บไว้ในที่ปลอดภัยก็เป็นความคิดที่ดีมันทำให้อุ่นใจว่าได้ทำเครื่องหมายสถานะที่มั่นใจว่าปกติไว้ตรง ๆ แล้ว โดยทั่วไปจะกลับไปยัง สถานะปกติล่าสุด ได้ ไม่ใช่จุดที่เข้าไปในความโกลาหลแล้วหลายขั้น แน่นอนว่าภายใต้เงื่อนไขว่าไม่ได้ใช้
git gcหรือgit filter-branchอะไรทำนองนั้นอย่างไม่ระวังฟังเหมือนมุกแต่ไม่ใช่มุกเลย การบอกว่าการใช้ Git ต้องมีปริญญาวิทยาการคอมพิวเตอร์จริง ๆ ก็ไม่ได้ผิดนัก ผมเห็นด้วยว่าอาร์กิวเมนต์เริ่มต้นของ Git ทำให้ประสบการณ์ใช้งานไม่สอดคล้องกันมาก
git checkout .,git reset --hard/soft,git cleanupทั้งหมดดูคล้ายกันมาก แต่จริง ๆ แล้วต่างกันมาก ถึงอย่างนั้นผมก็ยังชอบ Git มากกว่า Perforce หรือ SVN อยู่ดี เมื่อก่อนผมชอบ Mercurial มากกว่านิดหน่อย แต่ตอนนี้ไม่แน่ใจแล้วยังเป็นแบบนั้นไหมgit reflogคือราชาและแสดงความจริงผมจำได้ว่าเคยอ่านที่ Linus Torvalds เขียนว่าเขาประหลาดใจที่ผู้คนเริ่มใช้ Git โดยตรงโดยไม่วางเลเยอร์ที่เป็นมิตรกว่านี้ทับไว้ ถ้าวงการนี้มีการทบทวนตัวเองได้ ก็คงยอมรับว่าการเลือก Git เป็นความผิดพลาด แล้วเปลี่ยนไปใช้ระบบควบคุมเวอร์ชันอื่น แทนที่จะเสียเวลาอย่างมหาศาลกับเครื่องมือที่มีหน้าที่แค่เก็บข้อความ
แน่นอนว่ายังทำให้ดีขึ้นได้ แต่ความเกลียดชังที่ Git ได้รับทำให้ผมแปลกใจเสมอ Git เป็นเครื่องมือที่ยอดเยี่ยม และล้ำหน้าเครื่องมือที่ทำมาสำหรับผู้ใช้ที่ไม่ใช่นักพัฒนาอย่าง Word หรือ Google Docs มาก
อาจเป็นเพราะผมแก่พอที่จะจำยุคที่ Subversion เป็นราชาได้
ทุกสัปดาห์ผมเห็นเพื่อนร่วมงานสับสนเพราะ Git เห็นโพสต์ขอความช่วยเหลือออนไลน์ และเห็นบทความที่พยายามอธิบายสิ่งที่ควรเรียบง่ายอีกครั้งแบบบทความนี้
ตลอดเวลาที่เขียนโค้ด ผมจำไม่ได้ว่ามีใครเสียเวลาหลายชั่วโมงไปกับการย้อนแก้ความยุ่งเหยิงที่ตัวเองสร้างไว้ใน SVN, TFVC, Perforce ฯลฯ
เครื่องมือมีอยู่เพื่อทำให้ชีวิตง่ายขึ้น ถ้าทำไม่ได้ ก็ไม่คุ้มที่จะเสียเวลาให้
ผมยังอยากให้มีความหลากหลายแบบนั้นอยู่ วัฒนธรรม Git แบบเดี่ยว ยังหมายความด้วยว่าอะไรก็ตามที่คิดจะมาแทน Git ต้องเริ่มจากการ implement Git ขึ้นมาใหม่ก่อน ดังนั้นเราจึงแทบจะถูกผูกติดกับ Git ไปตลอด เหมือน ASCII หรือปุ่ม Scroll Lock
ความกังวลหลักคือ GitHub ดีเกินไป จนกว่า GitHub จะแยก Git ออกจากเอนจินจัดการข้อความและอนุญาตให้ใช้เอนจินอื่น เราก็จะถูกผูกติดกับ Git
https://trolololo.xyz/github
https://news.ycombinator.com/item?id=38098109
เป็นศัพท์แบบ abstraction ที่รั่ว
cacheซึ่งเป็นศัพท์เชิงวิทยาการคอมพิวเตอร์ที่สุดแล้วต่อมา Junio ดูเหมือนจะเปลี่ยนเป็น
indexซึ่งเฉพาะเจาะจงกับระบบควบคุมเวอร์ชันแบบกระจายมากกว่า และผู้ใช้ส่วนใหญ่เรียกมันว่า staging area ตอนนี้วิวัฒนาการของศัพท์เหล่านี้กลายเป็นซากฟอสซิลค้างอยู่ใน UI และภายในของ Gitpullorigin/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 ที่ซับซ้อนมากไว้บนแกนหลักที่ค่อนข้างเรียบง่ายได้
masterในเครื่องทิ้งไปแล้ว โดยปกติคุณ push ไปmasterไม่ได้ หรือยังไงก็ไม่อยากทำงานบนmasterโดยตรงอยู่แล้ว ถ้าอย่างนั้นก็ไม่มีเหตุผลที่จะมี branchmasterในเครื่องที่แก้ไขได้ดังนั้นแค่อัปเดต
origin/masterก็พอ และfetchก็เพียงพอแล้วgit fetch && git pullรอวันที่จะเกษียณในอีก 3 ปี แล้วล้างมือจากขยะพัง ๆ นี่ให้ได้
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...
น่าเสียดายที่เพื่อให้เร็วขึ้น พวกเขา hardcode UI นั้นเข้าไป แต่ไม่ได้ปรับปรุงมัน
พอมองย้อนกลับไป ตอนเริ่มแรกมันคงสับสนสุด ๆ แต่ผมคงแกล้งทำเหมือนไม่เป็นแบบนั้นเพื่อให้ดูเท่
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” มากกว่า“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 และผลของคำสั่งต่าง ๆ
https://learngitbranching.js.org/
ผมได้เรียนรู้จากการใช้เครื่องมือนี้ 10 นาที มากกว่าประสบการณ์ Git ราว 10 ปีก่อนหน้านั้นเสียอีก แนะนำอย่างยิ่ง
“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 แบรนช์ของเราไปดู เพื่อพิจารณาการเปลี่ยนแปลงที่เราอยากมีส่วนร่วมกับโปรเจกต์
[0] https://git-scm.com/book/en/v2/Distributed-Git-Contributing-...