chore(git): Gitea PR 헬퍼 — UTF-8 안전 생성/수정/머지
Windows 셸 인라인 curl -d 본문이 CP949로 전송되어 PR #34~41 한글이 깨진 사고 재발 방지. 제목/본문은 UTF-8 spec JSON 파일로만 받고 Node fetch가 전송. git credential(GCM)에서 토큰 자동 취득·401 재시도. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
75
tools/git/gitea-pr.mjs
Normal file
75
tools/git/gitea-pr.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
// Gitea PR 생성·수정·머지 헬퍼 — UTF-8 안전.
|
||||
//
|
||||
// ⚠️ 배경: Windows에서 bash 인라인 curl -d '{"title":"한글..."}' 은 셸 경계에서
|
||||
// CP949로 인코딩되어 Gitea에 한글이 깨져 올라간다(PR #34~41 사고).
|
||||
// 제목/본문은 반드시 UTF-8 파일(spec JSON)로 받고, Node fetch가 UTF-8로 전송한다.
|
||||
//
|
||||
// 사용법 (저장소 루트에서):
|
||||
// node tools/git/gitea-pr.mjs create <spec.json> # spec: { head, base?, title, body }
|
||||
// node tools/git/gitea-pr.mjs edit <번호> <spec.json> # spec: { title?, body? }
|
||||
// node tools/git/gitea-pr.mjs merge <번호>
|
||||
// node tools/git/gitea-pr.mjs show <번호>
|
||||
//
|
||||
// 토큰은 git credential fill(GCM)에서 읽는다. 인증 실패 시 한 번 재시도(토큰 갱신 직후 캐시 이슈).
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const HOST = 'gitea.gahusb.synology.me';
|
||||
const REPO = 'gahusb/maplecontest';
|
||||
const API = `https://${HOST}/api/v1/repos/${REPO}`;
|
||||
|
||||
function token() {
|
||||
const out = execSync('git credential fill', { input: `protocol=https\nhost=${HOST}\n\n`, encoding: 'utf8' });
|
||||
const m = out.match(/^password=(.+)$/m);
|
||||
if (!m) throw new Error('git credential에서 토큰을 찾지 못함');
|
||||
return m[1].trim();
|
||||
}
|
||||
|
||||
async function call(method, path, body) {
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
const res = await fetch(`${API}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token()}`,
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
body: body == null ? undefined : JSON.stringify(body),
|
||||
});
|
||||
if (res.status === 401 && attempt === 1) continue; // GCM 토큰 갱신 재시도
|
||||
const text = await res.text();
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
||||
return text ? JSON.parse(text) : null;
|
||||
}
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
const [cmd, a1, a2] = process.argv.slice(2);
|
||||
|
||||
if (cmd === 'create') {
|
||||
const spec = JSON.parse(readFileSync(a1, 'utf8'));
|
||||
if (!spec.head || !spec.title) throw new Error('spec에 head/title 필수');
|
||||
const pr = await call('POST', '/pulls', {
|
||||
head: spec.head,
|
||||
base: spec.base || 'main',
|
||||
title: spec.title,
|
||||
body: spec.body || '',
|
||||
});
|
||||
console.log(`PR #${pr.number} 생성: ${pr.html_url || pr.url}`);
|
||||
} else if (cmd === 'edit') {
|
||||
const spec = JSON.parse(readFileSync(a2, 'utf8'));
|
||||
const pr = await call('PATCH', `/pulls/${a1}`, {
|
||||
...(spec.title != null ? { title: spec.title } : {}),
|
||||
...(spec.body != null ? { body: spec.body } : {}),
|
||||
});
|
||||
console.log(`PR #${pr.number} 수정: ${pr.title}`);
|
||||
} else if (cmd === 'merge') {
|
||||
await call('POST', `/pulls/${a1}/merge`, { Do: 'merge' });
|
||||
console.log(`PR #${a1} 머지 완료`);
|
||||
} else if (cmd === 'show') {
|
||||
const pr = await call('GET', `/pulls/${a1}`);
|
||||
console.log(`#${pr.number} [${pr.state}${pr.merged ? '·merged' : ''}] ${pr.title}`);
|
||||
console.log(pr.body || '(본문 없음)');
|
||||
} else {
|
||||
console.error('사용법: node tools/git/gitea-pr.mjs <create <spec.json> | edit <번호> <spec.json> | merge <번호> | show <번호>>');
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user