UNPKG

cloc-graph

Version:

Track lines of code over time by language with visualization

217 lines (216 loc) 10 kB
#!/usr/bin/env node "use strict"; /** * 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); });