# Git Merge Master

> เตรียม local repo (ไม่ว่าจะเป็น App ในกลุ่ม delphi group-insurance หรือ repo ทั่วไปที่ clone ผ่าน /git-clone) ให้พร้อม merge ticket/N (Gitblit) เข้า branch master โดยใช้ explicit merge commit ตาม convention ที่ยืนยันแล้ว (ห้าม plain fast-forward) แล้วส่งต่อให้ /git-commit-push ทำ push จริง. รับได้ทั้งชื่อ App ในกลุ่ม delphi group-insurance (ค้นหา Repo URL ผ่าน /git-clone-grouplife) หรือ Repo URL ตรงๆ (สำหรับ repo อื่นที่ clone ผ่าน /git-clone) พร้อม path code ในเครื่องและเลข Redmine ไว้ดึง subject จริงมาประกอบ merge commit message. Use when user says "/git-merge-master" หรือต้องการเตรียม merge ticket เข้า master.

- Skill: `l3rowniecakez/git-merge-master` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add l3rowniecakez/git-merge-master`
- Raw SKILL.md: https://api.skillmd.com/api/skills/l3rowniecakez/git-merge-master/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: l3rowniecakez (https://skillmd.com/u/l3rowniecakez)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/l3rowniecakez/git-merge-master

---


# /git-merge-master — เตรียม merge ticket/N เข้า master (Gitblit)

Sibling ของ [[git-clone-grouplife]] (ใช้ script `search_app.py` ตัวเดียวกันหา repo
จากชื่อ App — เฉพาะกรณี App อยู่ในกลุ่ม delphi group-insurance), [[git-clone]]
(สำหรับ repo ทั่วไปอื่น ๆ ที่ต้องใส่ Repo URL ตรง ๆ แทนชื่อ App) และ
[[git-commit-push]] (ใช้ทำ push จริงในขั้นสุดท้าย) — งานของ skill
นี้คือเตรียม local repo ให้พร้อม push ขึ้น master แบบถูกต้องตาม convention ที่
ยืนยันแล้วใน [[reference_claim_dataextraction_gitblit]]: **ห้าม plain
fast-forward ticket/N เข้า master เด็ดขาด** ต้องเป็น explicit merge commit
รูปแบบ `Merged #N "<subject>"` เท่านั้น ไม่งั้น Gitblit ticket UI จะไม่ขึ้นสถานะ
"Merged" ให้ถูกต้อง (เคย fast-forward เฉยๆ มาแล้วครั้งหนึ่งกับ repo อื่น ผลคือ
ticket UI ไม่อัพเดทสถานะ ผู้ใช้ปฏิเสธผลลัพธ์นั้น)

**กฎเหล็ก**: skill นี้จบงานที่เตรียม merge commit ให้พร้อมบน local เท่านั้น — การ
push จริงต้องส่งต่อให้ [[git-commit-push]] ทำ (ซึ่งจะมี warning สีแดง + confirm
สองชั้นของมันเองเมื่อ target = master) **ห้าม push เองตรงนี้โดยไม่ผ่านขั้นตอนนั้น**

## Step 0: Init

```bash
date "+🕐 %H:%M %Z (%A %d %B %Y)"
```

---

## Step 1 — รับครบ 4 อย่าง + ตรวจสอบ path local

รับ argument ตามลำดับ `<ชื่อ App หรือ Repo URL> <Path code ในเครื่อง> <เลข ticket>
<เลข Redmine>` — ข้อไหนขาดให้ถามเพิ่ม ห้ามเดาแทน:

1. **App/Repo URL** รับได้ 2 แบบ — **เช็ครูปแบบก่อนตัดสินใจว่าจะทำแบบไหน อย่าเดา**:
   - **เป็น Repo URL อยู่แล้ว** (ขึ้นต้นด้วย `ssh://`, `git@`, หรือมี `.git` ต่อท้าย
     อย่างชัดเจน เช่น `ssh://<gitblit_user>@10.100.2.187:29418/delphi/xxx.git` หรือ
     `git@10.100.2.187:delphi/xxx.git`): **ใช้ตรง ๆ ได้เลย ข้ามการค้นหาไปเลย** — ไม่
     ต้องเรียก `search_app.py`
   - **เป็นชื่อ App** (คำธรรมดา ไม่ใช่ URL — พิมพ์บางส่วนก็ได้): ค้นด้วย script
     เดียวกับที่ [[git-clone-grouplife]] ใช้:
     ```bash
     ~/.config/redmine-summary-to-email/venv/bin/python3 \
       ~/.claude/skills/git-clone-grouplife/scripts/search_app.py "<คำค้น>"
     ```
     - **0 ผลลัพธ์**: อาจเพราะ repo นี้ไม่ใช่ App ในกลุ่ม delphi group-insurance เลย
       (เช่น repo ที่ clone มาผ่าน [[git-clone]] แทน) — แจ้ง user ว่าไม่พบใน
       tracking sheet แล้วขอ **Repo URL ตรง ๆ** มาแทนชื่อ App ห้ามค้นซ้ำเดา ๆ
     - **มากกว่า 1 ผลลัพธ์**: จัดการเหมือน [[git-clone-grouplife]] เป๊ะ (โชว์ทั้งหมด
       ให้เลือกเจาะจง — ห้ามเดาว่าอันไหนที่ต้องการ)
     - **1 ผลลัพธ์**: ยืนยันชื่อเต็มสั้น ๆ กับ user แล้วเก็บค่า `repo` ไว้ (แทน
       placeholder `<gitblit_user>` ด้วย username จริงจาก
       `~/.config/gitblit-web/credentials.json` เช่นเดียวกับที่
       [[git-clone-grouplife]] ทำ)
2. **Path code ในเครื่อง** — path ที่ clone ไว้แล้วบนเครื่อง (path ที่มีการแก้ไข/
   commit ค้างอยู่บน branch `ticket/<N>` แล้ว — ปกติมาจากที่เคยใช้
   [[git-clone-grouplife]] หรือ [[git-clone]] + [[git-new-branch]] +
   [[git-commit-push]] ทำไว้ก่อนหน้าในงานเดียวกัน) รับมา
   เป็น argument ตรง ๆ ได้เลย ไม่ต้องถามซ้ำถ้า user ใส่มาแล้ว แต่**ต้องตรวจสอบก่อน
   แตะอะไรเสมอ**:
   ```bash
   git -C "<path>" rev-parse --is-inside-work-tree
   git -C "<path>" branch --show-current
   git -C "<path>" remote get-url origin
   ```
   - ถ้า path ไม่มีอยู่จริง หรือไม่ใช่ git repo จริง หรือ remote ไม่ตรงกับ repo ที่
     หาได้ในข้อ 1 ให้หยุดแจ้ง user ทันที ห้ามเดาต่อ
   - ถ้า current branch ไม่ใช่ `ticket/<N>` (N จากข้อ 3) ให้เตือน user และถามยืนยัน
     ก่อนว่าจะดำเนินการต่อกับ branch ปัจจุบันนี้จริงไหม (เผื่อ user ตั้งใจจะ merge
     branch อื่น)
3. **เลข ticket** (Gitblit) ที่จะ merge (เช่น `15`) — ต้องได้มาจาก user ตรง ๆ ห้ามเดา
4. **เลข Redmine (RM)** ที่ ticket นี้ผูกอยู่ (เช่น `78856`) — ใช้ดึง subject จริงมา
   ประกอบ merge commit message ใน Step 3 ห้ามเดาหรือข้าม แม้ user จะไม่ได้ใส่มาก็ต้อง
   ถามเพิ่ม (ไม่มี fallback แบบเดา — ถ้า fetch จาก Redmine ไม่สำเร็จค่อย fallback ไปใช้
   subject จาก git log แทนตาม Step 3)

---

## Step 2 — Fetch + sync local `master` ให้ตรง origin/master ล่าสุดก่อน

```bash
git -C "<path>" fetch origin master ticket/<N>
git -C "<path>" checkout master 2>/dev/null || git -C "<path>" checkout -B master origin/master
git -C "<path>" log origin/master..master   # เช็คก่อนว่า local master มี commit ค้างที่ยังไม่ push อยู่หรือเปล่า
```

- ถ้า `git log origin/master..master` มีผลลัพธ์ (แปลว่า local master มี commit ที่
  ยังไม่ได้ push ค้างอยู่ก่อนหน้า) **ห้าม reset ทับเงียบ ๆ** ให้หยุดแจ้ง user ก่อนว่า
  พบ commit ค้างอะไรบ้าง ให้ user ตัดสินใจ
- ถ้าไม่มี commit ค้าง (สะอาด/ตรงกับ origin อยู่แล้ว หรือ local master ยังไม่เคยมี
  มาก่อน) ถึงจะทำ:
  ```bash
  git -C "<path>" reset --hard origin/master
  ```

---

## Step 3 — Merge `ticket/<N>` เข้า master ด้วย explicit merge commit (ตาม convention)

ดึง subject มาประกอบข้อความ merge commit — ใช้ **subject จริงจาก Redmine (RM)**
เป็นหลัก เพราะสั้น ตรงประเด็น และตรงกับที่ user อ้างอิงงานจริง (ดู
[[reference_redmine_api_key]]):

```bash
curl -s -H "X-Redmine-API-Key: <key>" \
  "https://redmine.ochi.link/issues/<RM>.json" | python3 -c "import json,sys; print(json.load(sys.stdin)['issue']['subject'])"
```

- **fetch สำเร็จ**: ใช้ `SUBJECT="RM #<RM>: <subject จาก Redmine>"`
- **fetch ไม่สำเร็จ** (network/RM ไม่พบ ฯลฯ): fallback ไปใช้ subject ของ commit
  แรกสุดบน `ticket/<N>` แทน (นับจาก origin/master) แล้วแจ้ง user ว่า fallback แล้ว
  เพราะดึงจาก Redmine ไม่ได้:
  ```bash
  SUBJECT=$(git -C "<path>" log --reverse --format=%s origin/master..ticket/<N> | head -1)
  ```

จากนั้น merge แบบ **`--no-commit` ก่อนเสมอ** (ห้าม commit ทันทีในคำสั่งเดียว) เพื่อ
เปิดช่องให้เช็ค duplicate-code risk ก่อน commit จริง:

```bash
git -C "<path>" merge --no-commit --no-ff "ticket/<N>"
```

- **ถ้าเกิด merge conflict**: หยุดทันที ห้ามแก้ conflict เดาเอง — แจ้ง user ว่าไฟล์
  ไหน conflict บ้าง (`git -C "<path>" status --short`) แล้วให้ user ตัดสินใจ/แก้เอง
  ก่อน (เสนอ `git merge --abort` ถ้า user อยากยกเลิกแล้วเริ่มใหม่)

- **ถ้า merge สำเร็จแบบไม่มี conflict** — **ห้ามไปต่อ commit ทันที** ต้องเช็ค
  duplicate-code risk ก่อน: กรณีที่คนอื่นเอาโค้ดของเรา (หรือโค้ดที่ ticket/N นี้จะเพิ่ม)
  ไป merge เข้า master ไปแล้วก่อนหน้าโดยเราไม่รู้ (ผ่าน cherry-pick/merge อื่น/แก้มือ)
  git แบบ line-based merge จะไม่ฟ้อง conflict ถ้า diff hunk ขยับบรรทัดไปจนบริบทไม่
  overlap กับของเดิม — ผลคือโค้ดเบิ้ลใน source แต่ merge "สำเร็จ" เงียบ ๆ โปรแกรม error
  ทันทีหลัง deploy ให้รันตรวจก่อน commit เสมอ:
  ```bash
  python3 ~/.claude/skills/git-merge-master/scripts/check_duplicate_merge.py \
    "<path>" "ticket/<N>"
  ```
  - **ผลลัพธ์ `CLEAN`**: ไปต่อ commit ได้ตามปกติ (ด้านล่าง)
  - **ผลลัพธ์ `DUPLICATE_RISK_FOUND`**: **ห้าม commit ต่อโดยไม่ถาม** — โชว์ผลลัพธ์
    ที่ script พิมพ์ออกมาให้ user เห็นเต็ม ๆ (ไฟล์ไหน บรรทัดไหนที่ซ้ำ ซ้ำกี่ครั้ง)
    แล้วถาม user ตรง ๆ ว่าจะดำเนินการ commit merge นี้ต่อไปจริงไหม (อาจเป็น false
    positive เช่น boilerplate ที่ซ้ำโดยตั้งใจ) หรือจะ `git merge --abort` แล้วไป
    ตรวจโค้ดจริงก่อน — **ห้ามตัดสินใจแทน user เด็ดขาด** เพราะนี่คือจุดที่พลาดมาแล้ว
    ครั้งก่อน (merge ซ้ำโค้ดที่มีอยู่แล้ว โปรแกรม error ทันที)

หลังผ่านการเช็คแล้ว (CLEAN หรือ user ยืนยันให้ผ่านต่อ) ค่อย commit merge จริง:

```bash
git -C "<path>" commit -m "Merged #<N> \"$SUBJECT\" #ai-work"
```

- ข้อความ commit **ต้องเป๊ะตามฟอร์แมต** `Merged #<N> "<subject>"` เท่านั้น (อ้างอิง
  [[reference_claim_dataextraction_gitblit]] — ผิดฟอร์แมตแล้ว Gitblit ticket UI จะ
  ไม่ขึ้นสถานะ "Merged" ให้ถูกต้อง) — `<N>` ในฟอร์แมตนี้คือเลข **Gitblit ticket**
  เสมอ ไม่ใช่เลข Redmine (สองเลขนี้คนละตัวกัน อย่าสลับ)
- **`#ai-work` ต้องต่อท้ายเสมอ** ตาม [[feedback_tag_ai_work_comments]] — ต่อท้าย
  หลังปิด quote ของ subject แบบ `Merged #<N> "<subject>" #ai-work` (อยู่หลัง
  ส่วนที่ Gitblit parse เพื่อขึ้นสถานะ ticket จึงไม่กระทบ format ที่ต้องเป๊ะข้างบน)
  ห้ามลืมแม้ merge commit นี้จะดูเหมือนเป็น "auto-generated message" ก็ตาม
- Merge สำเร็จแล้ว **ห้าม push เองตรงนี้** — ไปต่อ Step 4

---

## Step 4 — ส่งต่อให้ [[git-commit-push]] ทำ push จริง

แจ้ง user สรุปว่า local `master` พร้อม push แล้ว (โชว์
`git -C "<path>" log -1 --oneline` ของ merge commit ที่เพิ่งสร้าง) แล้วถามว่าจะ
push เลยไหมด้วย [[git-commit-push]] — ระบุให้ชัดเจนไปด้วยว่า:

- Repo (SSH) = จาก Step 1, target branch = `master`
- Source path = path เดิมที่ใช้ตลอด flow นี้
- **หมายเหตุสำคัญที่ต้องส่งต่อให้ [[git-commit-push]] รู้ด้วย**: ตอนนี้ working
  tree สะอาดแล้ว มีแค่ merge commit ที่รอ push เท่านั้น (ไม่มีไฟล์แก้ไขค้างให้ stage) —
  ดังนั้นขั้นตอน `git add -A && git commit -m ...` ใน Step 5 ของ
  [[git-commit-push]] **ต้องข้าม** (ไม่มีอะไรให้ commit ซ้ำ ถ้าพยายาม commit จะ
  error "nothing to commit") ให้ push `HEAD:master` ตรง ๆ เลย
- [[git-commit-push]] จะมี warning สีแดง + confirm สองชั้นของมันเองอยู่แล้วเพราะ
  target คือ `master` — ให้ user ยืนยันผ่าน flow นั้นตามปกติ ห้ามข้าม

ถ้า user ไม่ต้องการ push ตอนนี้ ให้หยุดไว้แค่นี้ (local master merge ไว้แล้ว รอ push
ทีหลังได้ ไม่มีอะไรเสียหาย)

---

## Notes

- Skill นี้ไม่ log time หรือ comment กลับ Redmine อัตโนมัติ
- ไม่ลบ/แก้ branch `ticket/<N>` หลัง merge — Gitblit เก็บ ticket branch ไว้เป็น
  ประวัติ ไม่ต้อง cleanup เอง เว้นแต่ user สั่งเอง
- ถ้า repo ไหนไม่ได้ใช้ pattern `ticket/N` แบบ Gitblit skill นี้ไม่รองรับ — แจ้ง
  user แทนที่จะเดา flow merge อื่น

---

ARGUMENTS: $ARGUMENTS

