Merge pull request 'chore(git): Gitea PR 헬퍼 — UTF-8 안전 생성/수정/머지 도구' (#42) from chore/gitea-pr-tool into main
This commit was merged in pull request #42.
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