UNPKG

@tricoteuses/senat

Version:

Handle French Sénat's open data

218 lines (217 loc) 7 kB
import { execSync } from "node:child_process"; import fs from "fs-extra"; import path from "node:path"; const MAXBUFFER = 50 * 1024 * 1024; const GIT_LOCK_RETRY_DELAY_MS = 1000; const GIT_LOCK_RETRY_COUNT = 5; const GIT_LOCK_STALE_AFTER_MS = 2 * 60 * 1000; function sleep(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } function getIndexLockPath(repositoryDir) { return path.join(repositoryDir, ".git", "index.lock"); } function isIndexLockError(error) { const stderr = String(error?.stderr || ""); return /index\.lock': File exists\./.test(stderr); } function removeStaleIndexLock(repositoryDir) { const lockPath = getIndexLockPath(repositoryDir); if (!fs.existsSync(lockPath)) { return false; } const stats = fs.statSync(lockPath); const ageMs = Date.now() - stats.mtimeMs; if (ageMs < GIT_LOCK_STALE_AFTER_MS) { return false; } fs.removeSync(lockPath); return true; } function execGitWithIndexLockRecovery(command, repositoryDir, options) { let lockRemoved = false; for (let attempt = 1; attempt <= GIT_LOCK_RETRY_COUNT; attempt++) { try { execSync(command, { cwd: repositoryDir, ...options, }); return; } catch (error) { if (!isIndexLockError(error)) { throw error; } if (!lockRemoved && removeStaleIndexLock(repositoryDir)) { lockRemoved = true; continue; } if (attempt === GIT_LOCK_RETRY_COUNT) { throw error; } sleep(GIT_LOCK_RETRY_DELAY_MS); } } } export function initRepo(repositoryDir) { if (!fs.existsSync(path.join(repositoryDir, ".git"))) { fs.ensureDirSync(repositoryDir); execSync("git init", { cwd: repositoryDir, env: process.env, encoding: "utf-8", stdio: ["ignore", "ignore", "pipe"], }); } } export function commit(repositoryDir, message) { initRepo(repositoryDir); execGitWithIndexLockRecovery("git add .", repositoryDir, { env: process.env, encoding: "utf-8", stdio: ["ignore", "ignore", "pipe"], maxBuffer: MAXBUFFER, }); try { execSync(`git commit -m "${message}" --quiet`, { cwd: repositoryDir, env: process.env, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], }); return true; } catch (childProcess) { if (childProcess.stdout === null || !/nothing to commit|rien à valider/.test(childProcess.stdout)) { console.error(childProcess.output); throw childProcess; } return false; } } export function commitAndPush(repositoryDir, message, remotes) { let exitCode = 0; if (commit(repositoryDir, message)) { for (const remote of remotes || []) { try { execSync(`git push ${remote} master`, { cwd: repositoryDir, env: process.env, encoding: "utf-8", stdio: ["ignore", "ignore", "pipe"], }); } catch (childProcess) { // Don't stop when push fails. console.error(childProcess.output); exitCode = childProcess.status; } } } else { // There was nothing to commit. exitCode = 10; } return exitCode; } export function resetAndPull(gitDir) { execSync("git reset --hard origin/master", { cwd: gitDir, env: process.env, encoding: "utf-8", stdio: ["ignore", "ignore", "pipe"], }); execSync("git pull --rebase", { cwd: gitDir, env: process.env, encoding: "utf-8", stdio: ["ignore", "ignore", "pipe"], }); return true; } export function clone(gitGroupUrl, gitName, workingDir) { if (gitGroupUrl !== undefined) { execSync(`git clone ${gitGroupUrl}/${gitName}.git`, { cwd: workingDir, env: process.env, encoding: "utf-8", stdio: ["ignore", "ignore", "pipe"], }); } } export function run(repositoryDir, args, verbose) { try { if (verbose) console.log(`git -C ${repositoryDir} ${args}`); const output = execSync(`git ${args}`, { cwd: repositoryDir, maxBuffer: MAXBUFFER, }) .toString() .trim(); if (verbose) console.log(output); return output; } catch (childProcess) { for (const output of ["stdout", "stderr"]) console.error(`${output}: ${childProcess[output]}`); throw childProcess; } } export function test(repositoryDir, args, verbose) { try { if (verbose) console.log(`git -C ${repositoryDir} ${args}`); const output = execSync(`git ${args}`, { cwd: repositoryDir, stdio: ["ignore", "pipe", "pipe"], maxBuffer: MAXBUFFER, }) .toString() .trim(); if (verbose) console.log(output); return true; } catch (childProcess) { if (childProcess.status != 0) return false; throw childProcess; } } /** * Get the list of files that have changed since a specific commit in a git repository. * @param repositoryDir The directory of the git repository * @param sinceCommit The commit hash to compare against (e.g., "HEAD~1", "abc123", etc.) * @param options Options for filtering * @param options.diffFilter Git diff-filter string (default: "AMR"). * A=Added, M=Modified, D=Deleted, R=Renamed, C=Copied, T=Type changed, U=Unmerged * @returns A Map of file paths to their git status */ export function getChangedFilesSinceCommit(repositoryDir, sinceCommit, options = {}) { const { diffFilter } = options; try { // Using diff-filter: A = Added, M = Modified, R = Renamed, D = Deleted, etc. // Default to AMR (excludes deleted files to prevent loading errors) const filter = diffFilter ?? "AMR"; const output = run(repositoryDir, `diff --name-status --diff-filter=${filter} ${sinceCommit}`, false); const changedFiles = new Map(); for (const line of output.split("\n")) { if (line.trim().length === 0) continue; const parts = line.split("\t"); if (parts.length >= 2) { const status = parts[0].charAt(0); const path = parts[1]; changedFiles.set(path, status); } } return changedFiles; } catch (error) { console.error(`Error getting changed files since commit ${sinceCommit}:`, error); return new Map(); } }