git-veil
Version:
A CLI tool for synchronizing development activities to a personal GitHub repository discreetly and confidentially.
299 lines (298 loc) • 14.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.pushCommits = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const child_process_1 = require("child_process");
const qrcode_1 = __importDefault(require("qrcode"));
const fileUtils_1 = require("../utils/fileUtils");
// Main function for pushing anonymized commits
function pushCommits(options) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
// Search for the GitVeil project root (to always use the correct config)
let projectRoot = path.dirname(((_a = require.main) === null || _a === void 0 ? void 0 : _a.filename) || process.argv[1]);
while (!fs.existsSync(path.join(projectRoot, "gitveil.config.json"))) {
const parent = path.dirname(projectRoot);
if (parent === projectRoot)
break;
projectRoot = parent;
}
const configPath = path.join(projectRoot, "gitveil.config.json");
let targetRepoPath = "";
let userName = "GitVeil";
let userEmail = "gitveil@example.com";
// Read the module config (always at the root)
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
targetRepoPath = config.targetRepoPath;
userName = config.name || userName;
userEmail = config.email || userEmail;
}
else {
console.error("gitveil.config.json not found.");
console.log("Please run `gitveil config --init` to create the configuration file.");
return;
}
console.log();
console.log(`> Mirror repo path: ${targetRepoPath}`);
// Check for the presence of README.md (serves as commit file)
const readmePath = path.join(targetRepoPath, "README.md");
let initialCounter = 0;
if (fs.existsSync(readmePath)) {
try {
const readmeContent = fs.readFileSync(readmePath, "utf-8");
const counterMatch = readmeContent.match(/Counter: (\d+)/);
if (counterMatch)
initialCounter = parseInt(counterMatch[1], 10);
}
catch (e) {
console.warn("Unable to read the initial Counter value in README.md");
}
}
else {
// Create an empty README.md if needed
fs.writeFileSync(readmePath, "");
console.log("📄 README.md created in targetRepoPath.");
}
// Memory-optimized reading of JSON files (avoids parsing multiple times)
const logsDir = path.join(projectRoot, "records-folder");
if (!fs.existsSync(logsDir)) {
console.error(`❌ Logs directory not found: ${logsDir}`);
return;
}
// Only read .json files
const files = fs.readdirSync(logsDir).filter((f) => f.endsWith(".json"));
const allDates = new Set();
for (const file of files) {
try {
const content = fs.readFileSync(path.join(logsDir, file), "utf-8");
const json = JSON.parse(content);
if (Array.isArray(json)) {
json.forEach((entry) => allDates.add(entry));
}
}
catch (e) {
console.warn(`⚠️ Error parsing file ${file}`);
}
}
// Initialize git repository only if needed
const gitDir = path.join(targetRepoPath, ".git");
if (!fs.existsSync(gitDir)) {
// If the repository doesn't exist, initialize it and configure the user
console.log(`🆕 Initializing git repo in ${targetRepoPath}`);
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" init`);
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" config user.name "${userName}"`);
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" config user.email "${userEmail}"`);
if (fs.existsSync(readmePath)) {
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" add README.md`);
const status = (0, child_process_1.execSync)(`git -C "${targetRepoPath}" status --porcelain`).toString();
if (status.trim()) {
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" commit -m "Initial commit"`);
console.log("✅ Initial commit created.");
}
}
}
else {
// If the repository exists, check user config
let currentName = "";
try {
currentName = (0, child_process_1.execSync)(`git -C "${targetRepoPath}" config user.name`)
.toString()
.trim();
}
catch (_b) { }
let currentEmail = "";
try {
currentEmail = (0, child_process_1.execSync)(`git -C "${targetRepoPath}" config user.email`)
.toString()
.trim();
}
catch (_c) { }
if (currentName !== userName)
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" config user.name "${userName}"`);
if (currentEmail !== userEmail)
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" config user.email "${userEmail}"`);
}
// Check if the repo has no commits and push an initial commit if needed
let hasCommit = false;
try {
const logResult = (0, child_process_1.execSync)(`git -C "${targetRepoPath}" log --oneline main`, {
stdio: "pipe",
}).toString();
if (logResult.trim())
hasCommit = true;
}
catch (e) { }
if (!hasCommit) {
if (!fs.existsSync(readmePath)) {
fs.writeFileSync(readmePath, "# GitVeil\n");
}
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" add README.md`);
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" commit -m "Initial commit"`);
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" push origin main --quiet`);
(0, child_process_1.execSync)(`git -C "${targetRepoPath}" config core.autocrlf input`);
console.log("> Initial commit created and pushed on main.");
}
// Get dates already present in git history (avoid duplicates)
let existingCommitDates = [];
try {
const gitLogCmd = `git -C "${targetRepoPath}" log --pretty=format:%ad --date=iso8601-strict`;
const stdout = (0, child_process_1.execSync)(gitLogCmd).toString();
existingCommitDates = stdout.split("\n").filter(Boolean);
}
catch (e) { }
// The counter must reflect the number of existing commits to avoid increment errors
let counter = existingCommitDates.length;
// Filter dates to commit (only new ones)
let newCommits = 0;
const toCommit = Array.from(allDates)
.sort()
.filter((date) => !existingCommitDates.includes(String(date).trim().replace(/\r|\n/g, "")));
const totalToCommit = toCommit.length;
if (allDates.size > 0) {
console.log(`📦 ${totalToCommit} commit(s) to create (not yet in history) out of ${allDates.size} extracted`);
console.log();
// Display a QR code to GitHub (small format)
const qrAscii = yield qrcode_1.default.toString("https://coff.ee/nicolastardieu", {
type: "terminal",
small: true,
});
console.log("If GitVeil has been valuable to you, please consider supporting its continued development");
console.log("Thank you for trusting GitVeil. Support the project here: https://coff.ee/nicolastardieu");
console.log();
console.log(qrAscii);
console.log();
}
if (totalToCommit === 0) {
// Nothing to do, clean up and exit
console.log();
console.log("Everything is up to date, nothing to push.");
(0, fileUtils_1.clearRecordsFolder)(logsDir);
return;
}
const gitCmdBase = `git -C "${targetRepoPath}"`;
let oldUserName = "";
let oldUserEmail = "";
try {
oldUserName = (0, child_process_1.execSync)(`${gitCmdBase} config user.name`).toString().trim();
}
catch (_d) { }
try {
oldUserEmail = (0, child_process_1.execSync)(`${gitCmdBase} config user.email`)
.toString()
.trim();
}
catch (_e) { }
// Synchronous loop for commits
const lastCommitIndex = toCommit.length - 1;
const commitCmds = [];
for (const [idx, dateRaw] of toCommit.entries()) {
const date = String(dateRaw).trim().replace(/\r|\n/g, "");
if (existingCommitDates.includes(date)) {
continue;
}
counter++;
newCommits++;
const commitEnv = Object.assign(Object.assign({}, process.env), { GIT_COMMITTER_NAME: userName, GIT_COMMITTER_EMAIL: userEmail });
if (idx !== lastCommitIndex) {
commitCmds.push({
cmd: `${gitCmdBase} commit --allow-empty --author="${userName} <${userEmail}>" -m "Commit #${counter} for ${date}" --date="${date}"`,
env: commitEnv,
isLast: false,
date,
idx,
});
}
else {
// Last commit: modify README.md
const content = [`Counter: ${counter}`, ""].join("\n");
fs.writeFileSync(readmePath, content);
commitCmds.push({
cmd: `${gitCmdBase} add -A`,
env: commitEnv,
isLast: false,
date,
idx,
});
commitCmds.push({
cmd: `${gitCmdBase} status --porcelain`,
env: commitEnv,
isLast: true,
date,
idx,
});
}
}
for (const commit of commitCmds) {
if (commit.isLast) {
const stdout = (0, child_process_1.execSync)(commit.cmd, { env: commit.env }).toString();
if (!stdout.trim())
continue;
const lastCommitCmd = `${gitCmdBase} commit --author="${userName} <${userEmail}>" -m "Commit #${counter} for ${commit.date}" --date="${commit.date}"`;
(0, child_process_1.execSync)(lastCommitCmd, { env: commit.env });
}
else {
(0, child_process_1.execSync)(commit.cmd, { env: commit.env });
}
process.stdout.write(`\r> Commit ${commit.idx + 1}/${totalToCommit} (${Math.round(((commit.idx + 1) / totalToCommit) * 100)}%)`);
}
// Restore original local git config
try {
if (oldUserName)
(0, child_process_1.execSync)(`${gitCmdBase} config user.name "${oldUserName}"`);
if (oldUserEmail)
(0, child_process_1.execSync)(`${gitCmdBase} config user.email "${oldUserEmail}"`);
}
catch (_f) { }
process.stdout.write("\n");
// Automatic push to remote repository (--quiet option to speed up)
try {
console.log();
console.log("> Pushing commits to remote repository... 🚀");
(0, child_process_1.execSync)(`${gitCmdBase} push origin main --quiet`);
console.log("> Push commits to remote completed ✅");
// Clean up temporary files after push
(0, fileUtils_1.clearRecordsFolder)(logsDir);
}
catch (err) {
console.error(`❌ Git push failed: ${err.message}`);
}
});
}
exports.pushCommits = pushCommits;