divan/api/src/git.ts
Anas Rashid 93825fe5d9 Publishing commits to divan-data git (#34); public names only; 'last edited' on poems
- api/src/git.ts: each published version is a commit in the divan-data checkout (queued, one at a
  time): the moderator as author by public name with a placeholder email, the summary and version
  in the message, Reviewed-by / Approved-by / Divan-Revision / Divan-Version trailers;
  DIVAN_GIT_PUSH=1 pushes. The commit id is kept with the revision.
- Privacy fix: published files and commits carry public names (profile name, else 'موڈریٹر <id>'),
  never email addresses (divan-data is public).
- Poem pages: 'دیوان کا ورژن …' with who edited, reviewed and published, the date, and a link to
  the commit diff (DIVAN_DATA_COMMIT_URL).
- deploy/sync.sh: pull --rebase so publishing commits are kept.
- Admins are super moderators (documented).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:37:39 +02:00

32 lines
2.0 KiB
TypeScript

// Publishing to git (#34): each published version is a commit in the divan-data checkout, so the public git
// history is the record of every change. Commits run one at a time (ponytail: in-process queue; one API process).
// Moderators appear by their public name with a placeholder email, never their real address.
// DIVAN_GIT_PUSH=1 push after each commit (else the daily sync or a release pushes)
// DIVAN_GIT_EMAIL_DOMAIN domain of the placeholder emails (default users.noreply.divan)
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const run = promisify(execFile);
const git = (dir: string, ...args: string[]) => run('git', ['-C', dir, ...args], { maxBuffer: 16 * 1024 * 1024 });
let queue: Promise<unknown> = Promise.resolve();
export type Person = { id: number | string; name: string };
// a moderator as a git identity: public name, placeholder email
export const identity = (p: Person) => `${p.name.replace(/[<>\n]/g, '')} <moderator-${p.id}@${process.env.DIVAN_GIT_EMAIL_DOMAIN ?? 'users.noreply.divan'}>`;
// commit these files (paths relative to dir) as the author; returns the commit id, or null if dir is not a git
// checkout (publishing still works, there is just no commit)
export function commit(dir: string, files: string[], author: Person, message: string): Promise<string | null> {
const job = queue.then(async () => {
try { await git(dir, 'rev-parse', '--is-inside-work-tree'); } catch { return null; }
await git(dir, 'add', '--', ...files);
await git(dir, '-c', 'user.name=Divan', '-c', `user.email=publish@${process.env.DIVAN_GIT_EMAIL_DOMAIN ?? 'users.noreply.divan'}`,
'commit', '--quiet', `--author=${identity(author)}`, '-m', message, '--', ...files);
const sha = (await git(dir, 'rev-parse', 'HEAD')).stdout.trim();
if (process.env.DIVAN_GIT_PUSH === '1') await git(dir, 'push', '--quiet').catch((e) => console.error('divan-data push failed:', e.message));
return sha;
});
queue = job.catch(() => {});
return job;
}