@tricoteuses/senat
Version:
Handle French Sénat's open data
218 lines (217 loc) • 7 kB
JavaScript
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();
}
}