git push: วิธีส่งการเปลี่ยนแปลงไปยังรีโพซิทอรีรีโมต
git push ส่งคอมมิตในเครื่องของบรานช์ปัจจุบันที่ยังไม่มีบนเซิร์ฟเวอร์ไปยังรีโมต (มักชื่อ origin) และเลื่อนพอยน์เตอร์ของบรานช์บนเซิร์ฟเวอร์ การเผยแพร่บรานช์ครั้งแรกทำด้วย git push -u origin ชื่อบรานช์ หลังจากนั้นใช้ git push ได้เลย หากมีคอมมิตของคนอื่นบนเซิร์ฟเวอร์ ให้ดึงมาด้วย git pull ก่อน ไม่ใช่ force push
git push ทำอะไร
คำสั่ง git push นำคอมมิตจากบรานช์ท้องถิ่นที่ยังไม่อยู่บนเซิร์ฟเวอร์ ส่งไปยังรีโพซิทอรีรีโมต (ชื่อปกติคือ origin) และเลื่อนพอยน์เตอร์ของบรานช์บนนั้นไปยังคอมมิตล่าสุดของคุณ ไฟล์ในไดเรกทอรีทำงานและการเปลี่ยนแปลงที่ยังไม่คอมมิตไม่ได้ถูกส่งไปไหน ส่งไปเพียงประวัติ คือคอมมิต
สิ่งสำคัญคือ push ไม่ใช่ "การซิงก์" มันทำงานทางเดียว จากรีโพซิทอรีท้องถิ่นไปยังรีโมต หากต้องการรับการเปลี่ยนแปลงของคนอื่น ต้องใช้ git fetch หรือ git pull หากสับสนทิศทางนี้จะเกิดปัญหาเกือบทั้งหมดที่มือใหม่มักถามผู้ช่วยสอน
อีกกฎของ Git: โดยค่าเริ่มต้น push อนุญาตเฉพาะการเลื่อนไปข้างหน้า (fast-forward) เซิร์ฟเวอร์จะรับคอมมิตของคุณหากมันต่อจากประวัติที่มีอยู่แล้วบนเซิร์ฟเวอร์ หากมีคนส่งคอมมิตของเขาก่อนหน้าคุณ push ของคุณจะถูกปฏิเสธ ซึ่งเป็นการป้องกัน ไม่ใช่ข้อผิดพลาด
รูปแบบคำสั่ง
git push [<remote>] [<บรานช์ท้องถิ่น>[:<บรานช์รีโมต>]]
เมื่อเรียก git push โดยไม่มีอาร์กิวเมนต์ Git จะใช้บรานช์ปัจจุบันและส่งไปยังบรานช์ที่ตั้งเป็น upstream (ที่ติดตามอยู่) รูปแบบเต็มมีประโยชน์เมื่อยังไม่ได้ตั้ง upstream หรือต้องการส่งบรานช์ภายใต้ชื่ออื่น:
git push origin main # ส่ง main ท้องถิ่นไปยัง origin/main
git push origin feature/login # ส่งบรานช์ feature/login
git push origin fix:hotfix-123 # ส่ง fix ท้องถิ่นเป็น hotfix-123 บนเซิร์ฟเวอร์
git push origin HEAD # ส่งบรานช์ปัจจุบันภายใต้ชื่อเดียวกัน
การส่งการเปลี่ยนแปลงครั้งแรก
เส้นทางทั่วไปจากโปรเจกต์ใหม่ไปยังโค้ดบน GitHub หรือ GitLab มีลักษณะนี้ ก่อนอื่นสร้างรีโพซิทอรีว่างบนเซิร์ฟเวอร์ จากนั้นเชื่อมกับรีโพซิทอรีในเครื่องและส่งบรานช์
git init
git add .
git commit -m "คอมมิตแรก"
git branch -M main
git remote add origin git@github.com:user/project.git
git push -u origin main
แฟล็ก -u (รูปเต็ม --set-upstream) เชื่อมบรานช์ main ท้องถิ่นกับ origin/main หลังจากนั้นพิมพ์แค่ git push และ git pull ได้ และ git status จะเริ่มแสดงว่าคุณนำหน้าหรือตามหลังเซิร์ฟเวอร์กี่คอมมิต
ตรวจว่า remote ชี้ไปที่ใดได้ด้วย git remote -v หากที่อยู่ผิด แก้ด้วย git remote set-url origin <ที่อยู่ใหม่> แทนการลบและสร้างรีโพซิทอรีใหม่
การส่งบรานช์ใหม่
คุณสร้างบรานช์สำหรับงาน ทำคอมมิต และพิมพ์ git push Git จะตอบว่า:
fatal: The current branch feature/login has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin feature/login
นี่ไม่ใช่ข้อผิดพลาด แต่เป็นคำแนะนำ บรานช์ยังไม่มีคู่บนเซิร์ฟเวอร์ ให้รันคำสั่งที่แนะนำหรือแบบสั้น git push -u origin feature/login หากต้องการให้ Git ทำอัตโนมัติกับทุกบรานช์ใหม่ เปิดการตั้งค่า (มีตั้งแต่ Git 2.37):
git config --global push.autoSetupRemote true
ทำไม git push ถูกปฏิเสธ: rejected, non-fast-forward
สถานการณ์ที่พบบ่อยที่สุดในการทำงานเป็นทีม:
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:user/project.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.
เพื่อนร่วมงานส่งคอมมิตของเขาก่อนคุณ ประวัติของคุณและเซิร์ฟเวอร์แยกออกจากกัน หาก Git ยอมรับ push ของคุณ คอมมิตของเพื่อนจะหายไป วิธีที่ถูกต้องคือดึงการเปลี่ยนแปลงลงมาก่อนและนำคอมมิตของคุณไปวางต่อท้าย:
git pull --rebase # ดึงคอมมิตของคนอื่นและจัดคอมมิตของเราไว้ด้านบน
# หากขัดแย้ง: แก้ไฟล์ git add แล้ว git rebase --continue
git push
แทน --rebase จะใช้ git pull ธรรมดาก็ได้ ซึ่งจะเกิดคอมมิต merge ทั้งสองวิธีถูกต้อง ทีมส่วนใหญ่นิยม rebase สำหรับคอมมิตท้องถิ่นของตัวเอง เพื่อให้ประวัติเป็นเส้นตรง สิ่งสำคัญคืออย่า "แก้" การปฏิเสธด้วย --force
git push --force และ --force-with-lease
การ push แบบบังคับจำเป็นเมื่อคุณเขียนประวัติของบรานช์ตัวเองใหม่โดยตั้งใจ เช่น ใช้ git commit --amend, git rebase หรือรวมคอมมิตก่อนรีวิว push ปกติจะไม่รับบรานช์แบบนี้ เพราะคอมมิตเก่าบนเซิร์ฟเวอร์ไม่ใช่บรรพบุรุษของคอมมิตใหม่อีกต่อไป
git push --force-with-lease
--force-with-lease จะเขียนทับบรานช์บนเซิร์ฟเวอร์ก็ต่อเมื่อมันอยู่ในตำแหน่งที่ Git ท้องถิ่นเคยเห็นล่าสุด (ผ่านการอ้างอิง origin/<บรานช์>) หากเพื่อนร่วมงานส่งอะไรเข้ามาแล้ว คำสั่งจะปฏิเสธ และงานของเขาจะไม่ถูกลบ ส่วน --force (-f) ไม่มีการตรวจสอบนี้และเขียนทับทุกอย่างโดยเงียบ
มีข้อควรระวัง: หากรัน git fetch ก่อน การอ้างอิง origin/<บรานช์> จะอัปเดต และการป้องกันจะไม่ทำงาน เพราะ Git จะคิดว่าคุณ "เห็น" คอมมิตของคนอื่นแล้ว ดังนั้นอย่า fetch ก่อน force push โดยไม่ดูว่ามีอะไรเข้ามา
| คำสั่ง | ทำอะไร | เมื่อไรควรใช้ |
|---|---|---|
git push | เฉพาะการเลื่อนไปข้างหน้า | ค่าเริ่มต้นเสมอ |
git push --force-with-lease | เขียนทับพร้อมการตรวจสอบ | บรานช์ของตัวเองหลัง rebase หรือ amend |
git push --force | เขียนทับโดยไม่มีการตรวจสอบ | แทบไม่เคย และห้ามใช้กับบรานช์ร่วม |
ห้าม force push ไปยังบรานช์ร่วม (main, develop) เพราะทุกคนที่ดึงประวัติเก่าไปแล้วจะไม่ตรงกับเซิร์ฟเวอร์ โดยปกติบรานช์เหล่านี้ยังถูกป้องกันด้วยการตั้งค่าของรีโพซิทอรี และเซิร์ฟเวอร์จะตอบกลับด้วย protected branch hook declined
การส่งแท็ก
git push ธรรมดาไม่ส่งแท็ก ต้องระบุอย่างชัดเจน:
git tag -a v1.2.0 -m "รีลีส 1.2.0"
git push origin v1.2.0 # แท็กเดียว
git push --tags # แท็กท้องถิ่นทั้งหมด
git push --follow-tags # คอมมิตและแท็กแบบ annotated ที่ชี้ไปยังคอมมิตเหล่านั้น
--follow-tags สะดวกกว่า --tags เพราะจะไม่ส่งแท็กแบบเบาที่อยู่ในเครื่องโดยบังเอิญ ส่งเฉพาะแท็กแบบ annotated และเฉพาะที่เข้าถึงได้จากคอมมิตที่กำลังส่งเท่านั้น
การลบบรานช์บนเซิร์ฟเวอร์
การลบก็ทำผ่าน push เช่นกัน:
git push origin --delete feature/login
# รูปแบบเก่า ทำงานเหมือนกัน:
git push origin :feature/login
บรานช์ท้องถิ่นยังคงอยู่ ลบได้ด้วย git branch -d feature/login และเพื่อนร่วมงานจะยังเห็นการอ้างอิง origin/feature/login จนกว่าจะรัน git fetch --prune
แฟล็กที่มีประโยชน์ของ git push
| แฟล็ก | หน้าที่ |
|---|---|
-u, --set-upstream | จำบรานช์รีโมตเป็น upstream |
-n, --dry-run | แสดงสิ่งที่จะถูกส่ง โดยไม่ส่งจริง |
--all | ส่งทุกบรานช์ท้องถิ่น |
--tags / --follow-tags | ส่งแท็ก |
-d, --delete | ลบบรานช์หรือแท็กบนเซิร์ฟเวอร์ |
--force-with-lease | เขียนประวัติทับอย่างปลอดภัย |
-v, --verbose | ผลลัพธ์แบบละเอียด |
--no-verify | ข้าม hook pre-push (ใช้อย่างรู้ตัว) |
ข้อผิดพลาดที่พบบ่อยของ git push และวิธีแก้
Permission denied (publickey)
เซิร์ฟเวอร์ไม่ยอมรับคีย์ SSH ของคุณ ตรวจว่าคีย์มีอยู่ (ls ~/.ssh) ส่วนสาธารณะถูกเพิ่มในบัญชีแล้ว และการเชื่อมต่อใช้ได้: ssh -T git@github.com หาก remote ใช้ HTTPS Git จะถามชื่อผู้ใช้และโทเคนแทนคีย์ ดูได้จาก git remote -v
Authentication failed ผ่าน HTTPS
GitHub และบริการอื่นหลายแห่งไม่รับรหัสผ่านบัญชีผ่าน HTTPS มาสักพักแล้ว ต้องใช้ Personal Access Token หรือเปลี่ยนเป็น SSH ช่วยจัดเก็บโทเคนได้ด้วย credential helper: git config --global credential.helper store (เก็บแบบข้อความธรรมดา) หรือตัวจัดการข้อมูลรับรองของระบบปฏิบัติการ
src refspec main does not match any
ไม่มีบรานช์ชื่อนี้ในเครื่อง ส่วนใหญ่รีโพซิทอรีว่างยังไม่มีคอมมิตเลย หรือบรานช์ชื่อ master ตรวจด้วย git branch แล้วทำคอมมิต หรือเปลี่ยนชื่อบรานช์: git branch -M main
Everything up-to-date ทั้งที่มีการเปลี่ยนแปลง
คุณลืมคอมมิต git push ส่งคอมมิต ไม่ใช่ไฟล์ รัน git status แล้ว git add และ git commit หรืออีกกรณีหนึ่งคือคุณอยู่ในบรานช์ที่ไม่ใช่บรานช์ที่คุณคาดว่าจะเห็นบนเซิร์ฟเวอร์
ไฟล์ใหญ่เกินไป
บริการจำกัดขนาดไฟล์ (บน GitHub คือ 100 MB) หากไฟล์นั้นเข้าไปในประวัติแล้ว การลบด้วยคอมมิตใหม่ไม่พอ ต้องเขียนประวัติใหม่ หรือนำไฟล์ออกจากคอมมิตที่ยังไม่ได้ส่ง (git reset --soft แล้วคอมมิตใหม่) สำหรับไฟล์ไบนารีขนาดใหญ่ใช้ Git LFS
วงจรการทำงานกับ git push
git switch -c feature/xสร้างบรานช์สำหรับงาน- ทำคอมมิตเป็นขั้นตอนเล็ก ๆ ที่มีเหตุผล
git fetchและgit rebase origin/mainเพื่อดึงบรานช์หลักที่ทันสมัยมาgit push -u origin feature/xเผยแพร่บรานช์- เปิด pull request หลังรีวิวเพิ่มคอมมิตและรัน
git pushปกติ - หากต้องเขียนประวัติใหม่ ใช้
git push --force-with-lease
ก่อนส่งควรดูว่าจะมีอะไรขึ้นเซิร์ฟเวอร์: git log origin/main..HEAD --oneline แสดงคอมมิตที่ยังไม่มีบนเซิร์ฟเวอร์
ตั้งค่าครั้งเดียวที่ควรทำ
การตั้งค่าส่วนกลางไม่กี่อย่างช่วยป้องกันความประหลาดใจเกือบทั้งหมดเมื่อส่งงาน push.default กำหนดว่า git push ที่ไม่มีอาร์กิวเมนต์จะทำอะไร ค่าเริ่มต้นในเวอร์ชันใหม่คือ simple ซึ่งส่งเฉพาะบรานช์ปัจจุบันไปยังบรานช์ที่มีชื่อเดียวกันเป็น upstream โดยปกติไม่ต้องเปลี่ยน แต่ควรตรวจว่าในคอนฟิกเก่าไม่มี matching ที่ส่งทุกบรานช์ที่ชื่อตรงกันพร้อมกัน
git config --global push.default simple
git config --global push.autoSetupRemote true
git config --global push.followTags true
git config --global pull.rebase true
push.followTags ทำให้ --follow-tags เป็นพฤติกรรมเริ่มต้น ส่วน pull.rebase ช่วยลดคอมมิต merge ที่ไม่จำเป็น เมื่อ push ถูกปฏิเสธและต้องดึงการเปลี่ยนแปลงของคนอื่นมาก่อน
Hook pre-push
ไฟล์ .git/hooks/pre-push ทำงานก่อนการส่ง หากสคริปต์จบด้วยรหัสที่ไม่ใช่ศูนย์ push จะไม่เกิดขึ้น ทีมมักรันเทสต์หรือลินเตอร์ในนั้น เพื่อไม่ให้โค้ดที่พังขึ้นเซิร์ฟเวอร์ การข้าม hook ด้วย --no-verify ควรทำเฉพาะเมื่อเข้าใจแล้วว่าทำไมมันถึงล้มเหลว
สรุป
git push เผยแพร่คอมมิตของคุณไปยังรีโพซิทอรีรีโมต และทำงานทางเดียวเท่านั้น การส่งบรานช์ครั้งแรกทำด้วย -u เมื่อถูกปฏิเสธให้ดึงการเปลี่ยนแปลงของคนอื่นมาก่อนด้วย pull และประวัติของบรานช์ตัวเองที่เขียนใหม่แล้วให้ส่งด้วย --force-with-lease เท่านั้น การทำงานกับรีโมตรีโพซิทอรี push pull และคีย์ SSH ถูกอธิบายและฝึกจริงในหลักสูตร Git
คำถามที่พบบ่อย
git push ส่งการเปลี่ยนแปลงที่ยังไม่คอมมิตหรือไม่?
ไม่ push ส่งเฉพาะคอมมิต การเปลี่ยนแปลงในไดเรกทอรีทำงานและดัชนียังอยู่ในเครื่องจนกว่าคุณจะคอมมิต
จะยกเลิก git push ได้อย่างไร?
ในบรานช์ของตัวเองลบคอมมิตในเครื่องด้วย git reset แล้วส่งด้วย git push --force-with-lease ในบรานช์ร่วมปลอดภัยกว่าด้วยการสร้างคอมมิตย้อนกลับผ่าน git revert แล้ว push ปกติ
ส่งเพียงคอมมิตเดียวได้หรือไม่?
ได้ โดยระบุแฮชของมัน เช่น git push origin abc1234:main จะส่งประวัติทั้งหมดจนถึงคอมมิตนั้นรวมด้วย แต่คอมมิตที่ใหม่กว่าจะไม่ถูกส่ง และไม่สามารถส่งคอมมิตโดยไม่มีบรรพบุรุษของมันได้
git push ต่างจาก git push origin main อย่างไร?
หากไม่มีอาร์กิวเมนต์ Git ใช้ upstream ที่ตั้งไว้ของบรานช์ปัจจุบัน ส่วนรูปแบบระบุชัดเจนจะระบุ remote และบรานช์โดยตรง และทำงานได้แม้ยังไม่ได้ตั้ง upstream
จะส่งไปยังสองรีโพซิทอรีพร้อมกันได้อย่างไร?
เพิ่ม remote ตัวที่สองแล้วส่งไปที่แต่ละชื่อ หรือเพิ่มที่อยู่สำหรับ push ของ origin สองแห่งด้วย git remote set-url --add --push origin ที่อยู่ (ทำให้ครบทั้งที่อยู่เดิมและที่อยู่ใหม่ ไม่เช่นนั้น push จะไปเพียงแห่งเดียว)
หัวข้อนี้สอนอย่างละเอียดพร้อมฝึกปฏิบัติในคอร์ส “Git с нуля до уверенного разработчика”
ไปที่คอร์ส