cloc-graph
Version:
Track lines of code over time by language with visualization
217 lines (216 loc) • 10 kB
JavaScript
;
/**
* cloc-graph
*
* MIT License
* Copyright (c) 2025 Barthélemy Paléologue
*/
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 () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = __importStar(require("path"));
// Import fs-extra but don't use direct fs import to avoid linting error
const csv_writer_1 = require("csv-writer");
const cli_1 = require("./cli");
const gitService_1 = require("./services/gitService");
const clocService_1 = require("./services/clocService");
const chartService_1 = require("./services/chartService");
const dateUtils_1 = require("./utils/dateUtils");
const errorHandler_1 = require("./utils/errorHandler");
const logger_1 = require("./utils/logger");
const progressService_1 = require("./services/progressService");
// Register Chart.js components
require("chart.js/auto");
function main() {
return __awaiter(this, void 0, void 0, function* () {
try {
logger_1.logger.info("Starting cloc-graph analysis");
// Parse and validate command line options
const options = (0, cli_1.parseCommandLineArgs)();
(0, cli_1.validateOptions)(options);
const step = parseInt(options.step, 10);
const top = parseInt(options.top, 10);
const maxSamples = parseInt(options.maxSamples, 10);
const repoPath = path.resolve(options.path);
logger_1.logger.info(`Analyzing repository at ${repoPath}`);
// Change to the target directory
try {
process.chdir(repoPath);
}
catch (_e) {
throw new errorHandler_1.AppError(`Could not change to directory ${repoPath}`, errorHandler_1.ErrorTypes.FILE_SYSTEM_ERROR, 2);
}
// Initialize git repository
const git = yield (0, gitService_1.initializeGitRepo)(repoPath);
const seen = new Set();
const records = [];
const langs = new Set();
// Get commits with optional smart sampling
logger_1.logger.info("Retrieving commits from repository");
const targetCommits = yield (0, gitService_1.getCommits)(git, {
maxSamples,
smartSampling: options.smartSampling,
});
logger_1.logger.info(`Processing ${targetCommits.length} commits`);
// Create a progress bar instance
const progress = new progressService_1.ProgressService();
progress.start(targetCommits.length, {
title: "Processing commits",
});
// Iterate through commits
for (let idx = 0; idx < targetCommits.length; idx++) {
const commit = targetCommits[idx];
// Apply step filtering for commit granularity
if (options.granularity === "commits" &&
!options.smartSampling &&
(idx + 1) % step !== 0) {
// Update progress without affecting the total
progress.increment();
continue;
}
// Handle potentially undefined date property
const commitDate = commit.date ? new Date(commit.date) : new Date();
const dateStr = (0, dateUtils_1.formatDate)(commitDate);
// Filter by date range if specified
if (options.from && dateStr < options.from) {
// Update progress without affecting the total
progress.increment();
continue;
}
if (options.to && dateStr > options.to) {
// Update progress without affecting the total
progress.increment();
continue;
}
// Determine grouping key based on granularity
let key;
if (options.granularity === "daily") {
key = dateStr;
}
else if (options.granularity === "weekly") {
key = (0, dateUtils_1.getISOWeek)(commitDate);
}
else if (options.granularity === "monthly") {
key = (0, dateUtils_1.getYearMonth)(commitDate);
}
else {
key = `commit_${idx + 1}`;
}
// Skip if we've already processed this key (date/week/month/commit)
if (seen.has(key)) {
// Update progress without affecting the total
progress.increment();
continue;
}
seen.add(key);
// Run cloc on this commit and create a record
logger_1.logger.debug(`Processing commit ${commit.hash} from ${dateStr}`);
const data = (0, clocService_1.analyzeCommit)(commit.hash);
const record = (0, clocService_1.createRecord)(data, commitDate, langs);
records.push(record);
// Update progress
progress.increment();
// Stop if we've reached the maximum number of samples
if (records.length >= maxSamples) {
logger_1.logger.info(`Reached maximum sample limit of ${maxSamples}`);
break;
}
}
// Stop and finalize the progress bar
progress.stop();
if (records.length === 0) {
logger_1.logger.warn("No commit data was collected. Check your filters and options.");
return 0;
}
// Calculate language totals for top-N filtering
const langTotals = {};
for (const lang of langs) {
langTotals[lang] = records.reduce((sum, rec) => sum + (rec[lang] || 0), 0);
}
// Sort languages by total LOC
const sortedLangs = [...langs].sort((a, b) => langTotals[b] - langTotals[a]);
// Filter languages if top-N is specified
let filteredLangs = top > 0 ? sortedLangs.slice(0, top) : [...sortedLangs].sort();
// Exclude specified languages
if (options.exclude) {
const excludeLangs = new Set(options.exclude.split(",").map((lang) => lang.trim()));
filteredLangs = filteredLangs.filter((lang) => !excludeLangs.has(lang));
}
// Include only specified languages if --include option is used
if (options.include) {
const includeLangs = new Set(options.include.split(",").map((lang) => lang.trim()));
filteredLangs = filteredLangs.filter((lang) => includeLangs.has(lang));
}
logger_1.logger.info(`Analyzed ${filteredLangs.length} languages across ${records.length} data points`);
// Write CSV
const csvFilePath = "loc_over_time_by_lang.csv";
const csvWriter = (0, csv_writer_1.createObjectCsvWriter)({
path: csvFilePath,
header: [
{ id: "date", title: "date" },
...filteredLangs.map((lang) => ({ id: lang, title: lang })),
],
});
yield csvWriter.writeRecords(records);
logger_1.logger.info(`Wrote ${csvFilePath} (${records.length} rows, top ${filteredLangs.length} languages) from '${repoPath}'`);
// Generate chart
yield (0, chartService_1.generateChart)(records, filteredLangs);
logger_1.logger.info("Analysis completed successfully");
return 0;
}
catch (e) {
return (0, errorHandler_1.handleError)(e);
}
});
}
main()
.then((exitCode) => {
if (exitCode !== 0) {
process.exit(exitCode);
}
})
.catch((err) => {
logger_1.logger.error("Unhandled exception", { error: err.message });
process.exit(1);
});