repo-wrapped 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/dist/cli.js +24 -0
- package/dist/commands/generate.js +95 -0
- package/dist/commands/index.js +24 -0
- package/dist/constants/chronotypes.js +23 -0
- package/dist/constants/colors.js +18 -0
- package/dist/constants/index.js +18 -0
- package/dist/formatters/index.js +17 -0
- package/dist/formatters/timeFormatter.js +29 -0
- package/dist/generators/html/scripts/export.js +125 -0
- package/dist/generators/html/scripts/knowledge.js +120 -0
- package/dist/generators/html/scripts/modal.js +68 -0
- package/dist/generators/html/scripts/navigation.js +156 -0
- package/dist/generators/html/scripts/tabs.js +18 -0
- package/dist/generators/html/scripts/tooltip.js +21 -0
- package/dist/generators/html/styles/achievements.css +387 -0
- package/dist/generators/html/styles/base.css +818 -0
- package/dist/generators/html/styles/components.css +1391 -0
- package/dist/generators/html/styles/knowledge.css +221 -0
- package/dist/generators/html/templates/achievementsSection.js +156 -0
- package/dist/generators/html/templates/commitQualitySection.js +89 -0
- package/dist/generators/html/templates/contributionGraph.js +73 -0
- package/dist/generators/html/templates/impactSection.js +117 -0
- package/dist/generators/html/templates/knowledgeSection.js +226 -0
- package/dist/generators/html/templates/streakSection.js +42 -0
- package/dist/generators/html/templates/timePatternsSection.js +110 -0
- package/dist/generators/html/utils/colorUtils.js +21 -0
- package/dist/generators/html/utils/commitMapBuilder.js +24 -0
- package/dist/generators/html/utils/dateRangeCalculator.js +57 -0
- package/dist/generators/html/utils/developerStatsCalculator.js +29 -0
- package/dist/generators/html/utils/scriptLoader.js +16 -0
- package/dist/generators/html/utils/styleLoader.js +18 -0
- package/dist/generators/html/utils/weekGrouper.js +28 -0
- package/dist/index.js +77 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/achievementDefinitions.js +433 -0
- package/dist/utils/achievementEngine.js +170 -0
- package/dist/utils/commitQualityAnalyzer.js +368 -0
- package/dist/utils/fileHotspotAnalyzer.js +270 -0
- package/dist/utils/gitParser.js +125 -0
- package/dist/utils/htmlGenerator.js +449 -0
- package/dist/utils/impactAnalyzer.js +248 -0
- package/dist/utils/knowledgeDistributionAnalyzer.js +374 -0
- package/dist/utils/matrixGenerator.js +350 -0
- package/dist/utils/slideGenerator.js +313 -0
- package/dist/utils/streakCalculator.js +135 -0
- package/dist/utils/timePatternAnalyzer.js +305 -0
- package/dist/utils/wrappedDisplay.js +115 -0
- package/dist/utils/wrappedGenerator.js +377 -0
- package/dist/utils/wrappedHtmlGenerator.js +552 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# repo-wrapped
|
|
2
|
+
|
|
3
|
+
A CLI tool for generating Git repository analytics and visualizations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g repo-wrapped
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Generate Visualization
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Current directory, current year
|
|
17
|
+
repo-wrapped generate
|
|
18
|
+
|
|
19
|
+
# Specific repository with HTML output
|
|
20
|
+
repo-wrapped generate /path/to/repo --html
|
|
21
|
+
|
|
22
|
+
# Analyze entire repository lifetime
|
|
23
|
+
repo-wrapped generate . --all --html
|
|
24
|
+
|
|
25
|
+
# Deep analysis with file-level knowledge distribution
|
|
26
|
+
repo-wrapped generate . --html --deep-analysis
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Options
|
|
30
|
+
|
|
31
|
+
| Flag | Description |
|
|
32
|
+
|------|-------------|
|
|
33
|
+
| `-y, --year <year>` | Year to analyze (default: current year) |
|
|
34
|
+
| `-m, --months <months>` | Number of months to show (1-12, default: 12) |
|
|
35
|
+
| `-a, --all` | Analyze entire repository lifetime |
|
|
36
|
+
| `--html` | Generate HTML report and open in browser |
|
|
37
|
+
| `--body-check` | Include commit body in quality scoring |
|
|
38
|
+
| `--deep-analysis` | Enable file-level analysis for knowledge distribution |
|
|
39
|
+
|
|
40
|
+
### Year in Code Summary
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
repo-wrapped wrapped 2024 . --html
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## HTML Report Sections
|
|
47
|
+
|
|
48
|
+
- **Contribution Graph** - Interactive commit heatmap
|
|
49
|
+
- **Streak Analytics** - Current and longest streaks, active day percentage
|
|
50
|
+
- **Time Patterns** - Hourly/daily distribution, chronotype detection, burnout risk
|
|
51
|
+
- **Commit Quality** - Message quality scoring, conventional commits adherence
|
|
52
|
+
- **File Hotspots** - Most changed files, technical debt indicators
|
|
53
|
+
- **Achievements** - Progress-based badges and milestones
|
|
54
|
+
- **Impact Analysis** - Lines changed, churn metrics
|
|
55
|
+
- **Knowledge Distribution** - Bus factor risk, ownership analysis
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install
|
|
61
|
+
npm run build
|
|
62
|
+
npm link
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Project Structure
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
src/
|
|
69
|
+
├── index.ts # CLI entry point
|
|
70
|
+
├── commands/
|
|
71
|
+
│ └── generate.ts # Generate command
|
|
72
|
+
├── types/
|
|
73
|
+
│ └── index.ts # TypeScript interfaces
|
|
74
|
+
├── generators/
|
|
75
|
+
│ └── html/
|
|
76
|
+
│ ├── scripts/ # Client-side JS
|
|
77
|
+
│ ├── styles/ # CSS files
|
|
78
|
+
│ ├── templates/ # HTML section builders
|
|
79
|
+
│ └── utils/ # Build utilities
|
|
80
|
+
└── utils/
|
|
81
|
+
├── gitParser.ts # Git log parsing
|
|
82
|
+
├── htmlGenerator.ts # HTML report assembly
|
|
83
|
+
├── streakCalculator.ts # Streak analytics
|
|
84
|
+
├── timePatternAnalyzer.ts
|
|
85
|
+
├── commitQualityAnalyzer.ts
|
|
86
|
+
├── fileHotspotAnalyzer.ts
|
|
87
|
+
├── achievementEngine.ts
|
|
88
|
+
├── impactAnalyzer.ts
|
|
89
|
+
└── knowledgeDistributionAnalyzer.ts
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const commander_1 = require("commander");
|
|
4
|
+
const gitParser_1 = require("./utils/gitParser");
|
|
5
|
+
const matrixGenerator_1 = require("./utils/matrixGenerator");
|
|
6
|
+
const program = new commander_1.Command();
|
|
7
|
+
program
|
|
8
|
+
.command('generate-matrix <repoPath>')
|
|
9
|
+
.description('Generate a GitHub-like commit matrix for the specified repository')
|
|
10
|
+
.option('-y, --year <year>', 'Year to generate matrix for', String(new Date().getFullYear()))
|
|
11
|
+
.option('-m, --month <month>', 'Month to generate matrix for (1-12)', String(new Date().getMonth() + 1))
|
|
12
|
+
.action(async (repoPath, options) => {
|
|
13
|
+
try {
|
|
14
|
+
const commits = (0, gitParser_1.parseGitCommits)(repoPath);
|
|
15
|
+
const year = parseInt(options.year);
|
|
16
|
+
const month = parseInt(options.month);
|
|
17
|
+
const matrix = (0, matrixGenerator_1.generateCommitMatrix)(commits, year, month, repoPath);
|
|
18
|
+
console.log(matrix);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error('Error generating commit matrix:', error);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.generateMatrix = void 0;
|
|
30
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
31
|
+
const child_process_1 = require("child_process");
|
|
32
|
+
const fs = __importStar(require("fs"));
|
|
33
|
+
const ora_1 = __importDefault(require("ora"));
|
|
34
|
+
const os = __importStar(require("os"));
|
|
35
|
+
const path = __importStar(require("path"));
|
|
36
|
+
const gitParser_1 = require("../utils/gitParser");
|
|
37
|
+
const htmlGenerator_1 = require("../utils/htmlGenerator");
|
|
38
|
+
const matrixGenerator_1 = require("../utils/matrixGenerator");
|
|
39
|
+
async function generateMatrix(repoPath, options) {
|
|
40
|
+
const spinner = (0, ora_1.default)('Analyzing git repository...').start();
|
|
41
|
+
try {
|
|
42
|
+
const absolutePath = path.resolve(repoPath);
|
|
43
|
+
const year = parseInt(options.year);
|
|
44
|
+
const monthsToShow = parseInt(options.months);
|
|
45
|
+
if (!options.all && (monthsToShow < 1 || monthsToShow > 12)) {
|
|
46
|
+
spinner.fail(chalk_1.default.red('Invalid months value. Please provide a value between 1 and 12.'));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
spinner.text = 'Fetching commit history...';
|
|
50
|
+
const commits = (0, gitParser_1.parseGitCommits)(absolutePath);
|
|
51
|
+
if (commits.length === 0) {
|
|
52
|
+
spinner.warn(chalk_1.default.yellow('No commits found in the repository.'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
spinner.text = 'Generating visualization...';
|
|
56
|
+
if (options.html) {
|
|
57
|
+
// Generate HTML output
|
|
58
|
+
const repoName = (0, gitParser_1.getRepositoryName)(absolutePath);
|
|
59
|
+
const repoUrl = (0, gitParser_1.getRepositoryUrl)(absolutePath);
|
|
60
|
+
const skipBodyCheck = !options.bodyCheck;
|
|
61
|
+
const deepAnalysis = options.deepAnalysis ?? false;
|
|
62
|
+
const html = (0, htmlGenerator_1.generateHTML)(commits, year, monthsToShow, absolutePath, repoName, repoUrl, skipBodyCheck, options.all, deepAnalysis);
|
|
63
|
+
const outputPath = path.join(os.tmpdir(), `git-wrapped-${Date.now()}.html`);
|
|
64
|
+
fs.writeFileSync(outputPath, html);
|
|
65
|
+
spinner.succeed(chalk_1.default.green('HTML file generated successfully!'));
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(chalk_1.default.cyan('Opening in browser...'));
|
|
68
|
+
console.log(chalk_1.default.dim(`File: ${outputPath}`));
|
|
69
|
+
// Open in default browser
|
|
70
|
+
const command = process.platform === 'win32'
|
|
71
|
+
? `start "" "${outputPath}"`
|
|
72
|
+
: process.platform === 'darwin'
|
|
73
|
+
? `open "${outputPath}"`
|
|
74
|
+
: `xdg-open "${outputPath}"`;
|
|
75
|
+
(0, child_process_1.exec)(command);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Generate terminal output
|
|
79
|
+
const repoName = (0, gitParser_1.getRepositoryName)(absolutePath);
|
|
80
|
+
const repoUrl = (0, gitParser_1.getRepositoryUrl)(absolutePath);
|
|
81
|
+
const matrix = (0, matrixGenerator_1.generateCommitMatrix)(commits, year, monthsToShow, absolutePath, repoName, repoUrl);
|
|
82
|
+
spinner.succeed(chalk_1.default.green('✓ Analysis complete!'));
|
|
83
|
+
console.log(matrix);
|
|
84
|
+
console.log();
|
|
85
|
+
console.log(chalk_1.default.cyan('💡 Tip:'), chalk_1.default.dim('Run with'), chalk_1.default.white('--html'), chalk_1.default.dim('to see interactive details and per-developer analytics'));
|
|
86
|
+
console.log(chalk_1.default.dim(' Example:'), chalk_1.default.white(`git-wrapped generate ${repoPath} --html`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
spinner.fail(chalk_1.default.red('Failed to generate visualization'));
|
|
91
|
+
console.error(chalk_1.default.red('\nError:'), error.message);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.generateMatrix = generateMatrix;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const commander_1 = require("commander");
|
|
4
|
+
const gitParser_1 = require("../utils/gitParser");
|
|
5
|
+
const matrixGenerator_1 = require("../utils/matrixGenerator");
|
|
6
|
+
const program = new commander_1.Command();
|
|
7
|
+
program
|
|
8
|
+
.command('generate-matrix <repoPath>')
|
|
9
|
+
.description('Generate a GitHub-like commit visualization for the specified repository')
|
|
10
|
+
.option('-y, --year <year>', 'Year to generate visualization for', String(new Date().getFullYear()))
|
|
11
|
+
.option('-m, --month <month>', 'Month to generate visualization for (1-12)', String(new Date().getMonth() + 1))
|
|
12
|
+
.action(async (repoPath, options) => {
|
|
13
|
+
try {
|
|
14
|
+
const commits = (0, gitParser_1.parseGitCommits)(repoPath);
|
|
15
|
+
const year = parseInt(options.year);
|
|
16
|
+
const month = parseInt(options.month);
|
|
17
|
+
const matrix = (0, matrixGenerator_1.generateCommitMatrix)(commits, year, month, repoPath);
|
|
18
|
+
console.log(matrix);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error('Error generating commit visualization:', error);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
exports.default = program;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ANALYSIS_CHRONOTYPE_EMOJIS = exports.WRAPPED_CHRONOTYPE_LABELS = exports.WRAPPED_CHRONOTYPE_EMOJIS = void 0;
|
|
4
|
+
// Chronotype definitions for YearInCodeSummary (morning/day/evening/night)
|
|
5
|
+
exports.WRAPPED_CHRONOTYPE_EMOJIS = {
|
|
6
|
+
morning: '☀️',
|
|
7
|
+
day: '📅',
|
|
8
|
+
evening: '🌆',
|
|
9
|
+
night: '🌙'
|
|
10
|
+
};
|
|
11
|
+
exports.WRAPPED_CHRONOTYPE_LABELS = {
|
|
12
|
+
morning: 'Morning Person',
|
|
13
|
+
day: 'Daytime Coder',
|
|
14
|
+
evening: 'Evening Developer',
|
|
15
|
+
night: 'Night Owl'
|
|
16
|
+
};
|
|
17
|
+
// Chronotype definitions for TimePattern analysis (early-bird/night-owl/balanced/vampire)
|
|
18
|
+
exports.ANALYSIS_CHRONOTYPE_EMOJIS = {
|
|
19
|
+
'early-bird': '🐦',
|
|
20
|
+
'night-owl': '🦉',
|
|
21
|
+
'vampire': '🧛',
|
|
22
|
+
'balanced': '⚖️'
|
|
23
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONTRIBUTION_LEVELS = exports.GITHUB_COLORS = void 0;
|
|
4
|
+
// GitHub contribution colors
|
|
5
|
+
exports.GITHUB_COLORS = {
|
|
6
|
+
EMPTY: 'rgba(235, 237, 240, 0.1)',
|
|
7
|
+
LEVEL_1: '#9be9a8',
|
|
8
|
+
LEVEL_2: '#40c463',
|
|
9
|
+
LEVEL_3: '#30a14e',
|
|
10
|
+
LEVEL_4: '#216e39',
|
|
11
|
+
BORDER: '#21262d'
|
|
12
|
+
};
|
|
13
|
+
// Contribution thresholds
|
|
14
|
+
exports.CONTRIBUTION_LEVELS = {
|
|
15
|
+
LEVEL_1: 3,
|
|
16
|
+
LEVEL_2: 6,
|
|
17
|
+
LEVEL_3: 9
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./colors"), exports);
|
|
18
|
+
__exportStar(require("./chronotypes"), exports);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./timeFormatter"), exports);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatHourShort = exports.formatHour = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Formats an hour (0-23) to a human-readable AM/PM string
|
|
6
|
+
*/
|
|
7
|
+
function formatHour(hour) {
|
|
8
|
+
if (hour === 0)
|
|
9
|
+
return '12:00 AM';
|
|
10
|
+
if (hour < 12)
|
|
11
|
+
return `${hour}:00 AM`;
|
|
12
|
+
if (hour === 12)
|
|
13
|
+
return '12:00 PM';
|
|
14
|
+
return `${hour - 12}:00 PM`;
|
|
15
|
+
}
|
|
16
|
+
exports.formatHour = formatHour;
|
|
17
|
+
/**
|
|
18
|
+
* Formats an hour (0-23) to a short AM/PM string (without :00)
|
|
19
|
+
*/
|
|
20
|
+
function formatHourShort(hour) {
|
|
21
|
+
if (hour === 0)
|
|
22
|
+
return '12 AM';
|
|
23
|
+
if (hour < 12)
|
|
24
|
+
return `${hour} AM`;
|
|
25
|
+
if (hour === 12)
|
|
26
|
+
return '12 PM';
|
|
27
|
+
return `${hour - 12} PM`;
|
|
28
|
+
}
|
|
29
|
+
exports.formatHourShort = formatHourShort;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Export functionality for Git Wrapped reports
|
|
2
|
+
|
|
3
|
+
function initExport() {
|
|
4
|
+
document.querySelectorAll('[data-export]').forEach(btn => {
|
|
5
|
+
btn.addEventListener('click', (e) => {
|
|
6
|
+
const format = e.currentTarget.dataset.export;
|
|
7
|
+
exportData(format);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function exportData(format) {
|
|
13
|
+
const data = window.__GITWRAPPED_DATA__;
|
|
14
|
+
|
|
15
|
+
if (!data) {
|
|
16
|
+
console.warn('No export data available');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const repoName = (data.repository || 'report').replace(/[^a-z0-9]/gi, '-').toLowerCase();
|
|
21
|
+
const timestamp = new Date().toISOString().slice(0, 10);
|
|
22
|
+
|
|
23
|
+
if (format === 'json') {
|
|
24
|
+
downloadFile(
|
|
25
|
+
JSON.stringify(data, null, 2),
|
|
26
|
+
`git-wrapped-${repoName}-${timestamp}.json`,
|
|
27
|
+
'application/json'
|
|
28
|
+
);
|
|
29
|
+
} else if (format === 'csv') {
|
|
30
|
+
const csv = convertToCSV(data);
|
|
31
|
+
downloadFile(csv, `git-wrapped-${repoName}-${timestamp}.csv`, 'text/csv');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function convertToCSV(data) {
|
|
36
|
+
const sections = [];
|
|
37
|
+
|
|
38
|
+
// Section 1: Summary Metrics
|
|
39
|
+
sections.push('# Summary Metrics');
|
|
40
|
+
sections.push('Metric,Value');
|
|
41
|
+
sections.push(`Repository,${escapeCSV(data.repository || '')}`);
|
|
42
|
+
sections.push(`Period Start,${data.period?.start || ''}`);
|
|
43
|
+
sections.push(`Period End,${data.period?.end || ''}`);
|
|
44
|
+
sections.push(`Author Filter,${escapeCSV(data.author || 'All')}`);
|
|
45
|
+
sections.push(`Total Commits,${data.summary?.totalCommits || 0}`);
|
|
46
|
+
sections.push(`Total Contributors,${data.summary?.totalContributors || 0}`);
|
|
47
|
+
sections.push(`Active Days,${data.summary?.activeDays || 0}`);
|
|
48
|
+
|
|
49
|
+
// Section 2: Streak Data
|
|
50
|
+
if (data.streaks) {
|
|
51
|
+
sections.push('');
|
|
52
|
+
sections.push('# Streak Analysis');
|
|
53
|
+
sections.push('Metric,Value');
|
|
54
|
+
sections.push(`Current Streak (days),${data.streaks.currentStreak || 0}`);
|
|
55
|
+
sections.push(`Longest Streak (days),${data.streaks.longestStreak || 0}`);
|
|
56
|
+
sections.push(`Total Active Days,${data.streaks.totalActiveDays || 0}`);
|
|
57
|
+
sections.push(`Active Day Percentage,${(data.streaks.activeDayPercentage || 0).toFixed(1)}%`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Section 3: Quality Metrics
|
|
61
|
+
if (data.quality) {
|
|
62
|
+
sections.push('');
|
|
63
|
+
sections.push('# Commit Quality');
|
|
64
|
+
sections.push('Metric,Value');
|
|
65
|
+
sections.push(`Overall Score,${(data.quality.overallScore || 0).toFixed(1)}`);
|
|
66
|
+
sections.push(`Conventional Commits %,${(data.quality.conventionalAdherence || 0).toFixed(1)}%`);
|
|
67
|
+
sections.push(`Avg Subject Length,${(data.quality.avgSubjectLength || 0).toFixed(0)} chars`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Section 4: Time Patterns
|
|
71
|
+
if (data.timePattern) {
|
|
72
|
+
sections.push('');
|
|
73
|
+
sections.push('# Time Patterns');
|
|
74
|
+
sections.push('Metric,Value');
|
|
75
|
+
sections.push(`Chronotype,${data.timePattern.chronotype || 'Unknown'}`);
|
|
76
|
+
sections.push(`Peak Hour,${data.timePattern.peakHour || 0}:00`);
|
|
77
|
+
sections.push(`Consistency Score,${(data.timePattern.consistency || 0).toFixed(1)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Section 5: Developer Stats
|
|
81
|
+
if (data.developerStats && data.developerStats.length > 0) {
|
|
82
|
+
sections.push('');
|
|
83
|
+
sections.push('# Developer Statistics');
|
|
84
|
+
sections.push('Developer,Commits,First Commit,Last Commit');
|
|
85
|
+
data.developerStats.forEach(dev => {
|
|
86
|
+
sections.push(`${escapeCSV(dev.name)},${dev.commits},${dev.firstCommit || ''},${dev.lastCommit || ''}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Section 6: Recent Commits (last 100)
|
|
91
|
+
if (data.commits && data.commits.length > 0) {
|
|
92
|
+
sections.push('');
|
|
93
|
+
sections.push('# Commits (last 100)');
|
|
94
|
+
sections.push('Date,Author,Message,Hash');
|
|
95
|
+
data.commits.slice(0, 100).forEach(c => {
|
|
96
|
+
sections.push(`${c.date},${escapeCSV(c.author)},${escapeCSV(c.message)},${c.hash || ''}`);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return sections.join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function escapeCSV(str) {
|
|
104
|
+
if (str == null) return '';
|
|
105
|
+
const s = String(str);
|
|
106
|
+
if (s.includes(',') || s.includes('"') || s.includes('\n') || s.includes('\r')) {
|
|
107
|
+
return '"' + s.replace(/"/g, '""') + '"';
|
|
108
|
+
}
|
|
109
|
+
return s;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function downloadFile(content, filename, mimeType) {
|
|
113
|
+
const blob = new Blob([content], { type: mimeType });
|
|
114
|
+
const url = URL.createObjectURL(blob);
|
|
115
|
+
const a = document.createElement('a');
|
|
116
|
+
a.href = url;
|
|
117
|
+
a.download = filename;
|
|
118
|
+
document.body.appendChild(a);
|
|
119
|
+
a.click();
|
|
120
|
+
document.body.removeChild(a);
|
|
121
|
+
URL.revokeObjectURL(url);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Initialize when DOM is ready
|
|
125
|
+
document.addEventListener('DOMContentLoaded', initExport);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Distribution - Expandable Row Interactivity
|
|
3
|
+
* Handles accordion-style expand/collapse for directory deep-dive analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initialize expandable rows in the knowledge table
|
|
11
|
+
*/
|
|
12
|
+
function initExpandableRows() {
|
|
13
|
+
const expandableRows = document.querySelectorAll('.knowledge-table .expandable-row');
|
|
14
|
+
|
|
15
|
+
expandableRows.forEach(row => {
|
|
16
|
+
row.addEventListener('click', handleRowExpand);
|
|
17
|
+
row.addEventListener('keydown', handleRowKeydown);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handle click on expandable row
|
|
23
|
+
*/
|
|
24
|
+
function handleRowExpand(event) {
|
|
25
|
+
const row = event.currentTarget;
|
|
26
|
+
const rowId = row.getAttribute('data-row-id');
|
|
27
|
+
if (!rowId) return;
|
|
28
|
+
|
|
29
|
+
const contentRow = document.getElementById(`${rowId}-content`);
|
|
30
|
+
if (!contentRow) return;
|
|
31
|
+
|
|
32
|
+
const isExpanded = row.getAttribute('aria-expanded') === 'true';
|
|
33
|
+
|
|
34
|
+
if (isExpanded) {
|
|
35
|
+
collapseRow(row, contentRow);
|
|
36
|
+
} else {
|
|
37
|
+
expandRow(row, contentRow);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Handle keyboard navigation on expandable rows
|
|
43
|
+
*/
|
|
44
|
+
function handleRowKeydown(event) {
|
|
45
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
handleRowExpand(event);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Expand a directory row to show details
|
|
53
|
+
*/
|
|
54
|
+
function expandRow(row, contentRow) {
|
|
55
|
+
row.setAttribute('aria-expanded', 'true');
|
|
56
|
+
row.classList.add('expanded');
|
|
57
|
+
contentRow.classList.remove('hidden');
|
|
58
|
+
|
|
59
|
+
// Animate expand icon
|
|
60
|
+
const icon = row.querySelector('.expand-icon');
|
|
61
|
+
if (icon) {
|
|
62
|
+
icon.textContent = '▼';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Animate the content panel
|
|
66
|
+
const panel = contentRow.querySelector('.expansion-panel');
|
|
67
|
+
if (panel) {
|
|
68
|
+
panel.style.maxHeight = panel.scrollHeight + 'px';
|
|
69
|
+
panel.style.opacity = '1';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Collapse a directory row to hide details
|
|
75
|
+
*/
|
|
76
|
+
function collapseRow(row, contentRow) {
|
|
77
|
+
row.setAttribute('aria-expanded', 'false');
|
|
78
|
+
row.classList.remove('expanded');
|
|
79
|
+
contentRow.classList.add('hidden');
|
|
80
|
+
|
|
81
|
+
// Animate expand icon
|
|
82
|
+
const icon = row.querySelector('.expand-icon');
|
|
83
|
+
if (icon) {
|
|
84
|
+
icon.textContent = '▶';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Animate the content panel
|
|
88
|
+
const panel = contentRow.querySelector('.expansion-panel');
|
|
89
|
+
if (panel) {
|
|
90
|
+
panel.style.maxHeight = '0';
|
|
91
|
+
panel.style.opacity = '0';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Collapse all expanded rows
|
|
97
|
+
*/
|
|
98
|
+
function collapseAllRows() {
|
|
99
|
+
const expandedRows = document.querySelectorAll('.knowledge-table .expandable-row.expanded');
|
|
100
|
+
expandedRows.forEach(row => {
|
|
101
|
+
const rowId = row.getAttribute('data-row-id');
|
|
102
|
+
const contentRow = document.getElementById(`${rowId}-content`);
|
|
103
|
+
if (contentRow) {
|
|
104
|
+
collapseRow(row, contentRow);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Initialize when DOM is ready
|
|
110
|
+
if (document.readyState === 'loading') {
|
|
111
|
+
document.addEventListener('DOMContentLoaded', initExpandableRows);
|
|
112
|
+
} else {
|
|
113
|
+
initExpandableRows();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Expose function for external use (e.g., collapse all when switching tabs)
|
|
117
|
+
window.knowledgeModule = {
|
|
118
|
+
collapseAllRows: collapseAllRows
|
|
119
|
+
};
|
|
120
|
+
})();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Modal functionality for commit details
|
|
2
|
+
const modal = document.getElementById('modal');
|
|
3
|
+
const modalTitle = document.getElementById('modal-title');
|
|
4
|
+
const modalClose = document.getElementById('modal-close');
|
|
5
|
+
const commitList = document.getElementById('commit-list');
|
|
6
|
+
|
|
7
|
+
// Helper function to escape HTML
|
|
8
|
+
function escapeHtml(text) {
|
|
9
|
+
const div = document.createElement('div');
|
|
10
|
+
div.textContent = text;
|
|
11
|
+
return div.innerHTML;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Modal click handlers
|
|
15
|
+
const clickableDays = document.querySelectorAll('.day.clickable');
|
|
16
|
+
clickableDays.forEach(day => {
|
|
17
|
+
day.addEventListener('click', () => {
|
|
18
|
+
const details = day.getAttribute('data-details');
|
|
19
|
+
const dateStr = day.getAttribute('data-date');
|
|
20
|
+
|
|
21
|
+
if (details && dateStr) {
|
|
22
|
+
try {
|
|
23
|
+
const commits = JSON.parse(details);
|
|
24
|
+
const date = new Date(dateStr);
|
|
25
|
+
const formattedDate = date.toLocaleDateString('en-US', {
|
|
26
|
+
weekday: 'long',
|
|
27
|
+
year: 'numeric',
|
|
28
|
+
month: 'long',
|
|
29
|
+
day: 'numeric'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
modalTitle.textContent = `${commits.length} contribution${commits.length !== 1 ? 's' : ''} on ${formattedDate}`;
|
|
33
|
+
|
|
34
|
+
// Build commit list
|
|
35
|
+
commitList.innerHTML = commits.map(commit => `
|
|
36
|
+
<div class="commit-item">
|
|
37
|
+
<div class="commit-message">${escapeHtml(commit.message)}</div>
|
|
38
|
+
<div class="commit-meta">
|
|
39
|
+
<span>by <strong>${escapeHtml(commit.author)}</strong></span>
|
|
40
|
+
<span class="commit-sha">${commit.hash.substring(0, 7)}</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
`).join('');
|
|
44
|
+
|
|
45
|
+
modal.classList.add('active');
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error('Failed to parse commit details:', e);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Close modal handlers
|
|
54
|
+
modalClose.addEventListener('click', () => {
|
|
55
|
+
modal.classList.remove('active');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
modal.addEventListener('click', (e) => {
|
|
59
|
+
if (e.target === modal) {
|
|
60
|
+
modal.classList.remove('active');
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
document.addEventListener('keydown', (e) => {
|
|
65
|
+
if (e.key === 'Escape' && modal.classList.contains('active')) {
|
|
66
|
+
modal.classList.remove('active');
|
|
67
|
+
}
|
|
68
|
+
});
|