@anxin233/gitviz 1.0.0
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/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/README.zh-CN.md +183 -0
- package/dist/cli/index.js +67 -0
- package/dist/cli.js +67 -0
- package/dist/core/analyzer.js +99 -0
- package/dist/core/git-parser.js +53 -0
- package/dist/core/types.js +1 -0
- package/dist/visualizers/html-generator.js +292 -0
- package/package.json +59 -0
- package/scripts/build.js +28 -0
- package/scripts/bundle.js +47 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to GitViz will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-02-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release of GitViz
|
|
12
|
+
- Interactive commit timeline visualization
|
|
13
|
+
- Top contributors analysis with commit and line counts
|
|
14
|
+
- File change heatmap showing most frequently modified files
|
|
15
|
+
- CLI with `analyze` and `quick` commands
|
|
16
|
+
- HTML export functionality with embedded D3.js visualizations
|
|
17
|
+
- Support for custom output paths and commit limits
|
|
18
|
+
- Beautiful gradient UI with responsive design
|
|
19
|
+
- Hover tooltips for detailed information
|
|
20
|
+
- Repository statistics summary
|
|
21
|
+
- Cross-platform support (Windows, macOS, Linux)
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
- Fast Git history parsing using simple-git
|
|
25
|
+
- Intelligent data aggregation and analysis
|
|
26
|
+
- Standalone HTML reports (no external dependencies)
|
|
27
|
+
- Configurable commit limits for large repositories
|
|
28
|
+
- Automatic repository detection
|
|
29
|
+
- Color-coded visualizations with smooth transitions
|
|
30
|
+
|
|
31
|
+
### Technical
|
|
32
|
+
- Built with TypeScript for type safety
|
|
33
|
+
- Powered by Bun for optimal performance
|
|
34
|
+
- Uses D3.js v7 for data visualization
|
|
35
|
+
- Modular architecture for easy extension
|
|
36
|
+
- Clean separation of concerns (parser, analyzer, visualizer)
|
|
37
|
+
|
|
38
|
+
## [Unreleased]
|
|
39
|
+
|
|
40
|
+
### Planned
|
|
41
|
+
- Branch comparison feature
|
|
42
|
+
- Network graph visualization
|
|
43
|
+
- Export to PDF and PNG formats
|
|
44
|
+
- Custom theme support
|
|
45
|
+
- GitHub/GitLab API integration
|
|
46
|
+
- Real-time repository monitoring
|
|
47
|
+
- Plugin system for custom analyzers
|
|
48
|
+
- Multi-language support
|
|
49
|
+
- Performance optimizations for very large repositories
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GitViz Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# 🎨 GitViz
|
|
2
|
+
|
|
3
|
+
> Transform your Git history into stunning visual stories
|
|
4
|
+
|
|
5
|
+
English | [简体中文](README.zh-CN.md)
|
|
6
|
+
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://bun.sh/)
|
|
10
|
+
[](https://github.com/anxin233/gitviz)
|
|
11
|
+
[](https://github.com/anxin233/gitviz/issues)
|
|
12
|
+
[](CONTRIBUTING.md)
|
|
13
|
+
|
|
14
|
+
GitViz is a powerful CLI tool that analyzes your Git repositories and generates beautiful, interactive visualizations. Understand your project's history, contributor patterns, and code evolution at a glance.
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
[📖 Quick Start](QUICKSTART.md) • [🎨 Demo](DEMO.md) • [📚 Examples](EXAMPLES.md) • [🤝 Contributing](CONTRIBUTING.md)
|
|
19
|
+
|
|
20
|
+
## ✨ Features
|
|
21
|
+
|
|
22
|
+
- 📊 **Interactive Timeline** - Visualize commit activity over time
|
|
23
|
+
- 👥 **Contributor Analytics** - See who's contributing and how much
|
|
24
|
+
- 🔥 **File Heatmap** - Identify hotspots in your codebase
|
|
25
|
+
- 🚀 **Lightning Fast** - Powered by Bun for maximum performance
|
|
26
|
+
- 📦 **Zero Config** - Works out of the box with any Git repository
|
|
27
|
+
- 🎨 **Beautiful Output** - Export as standalone HTML files
|
|
28
|
+
- 💻 **Cross-Platform** - Works on macOS, Linux, and Windows
|
|
29
|
+
|
|
30
|
+
## 🚀 Quick Start
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Using npm
|
|
36
|
+
npm install -g @anxin233/gitviz
|
|
37
|
+
|
|
38
|
+
# Using Bun (recommended)
|
|
39
|
+
bun install -g @anxin233/gitviz
|
|
40
|
+
|
|
41
|
+
# Using yarn
|
|
42
|
+
yarn global add @anxin233/gitviz
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Analyze current repository
|
|
49
|
+
gitviz analyze
|
|
50
|
+
|
|
51
|
+
# Analyze specific repository
|
|
52
|
+
gitviz analyze --path /path/to/repo
|
|
53
|
+
|
|
54
|
+
# Customize output
|
|
55
|
+
gitviz analyze --output my-report.html --limit 500
|
|
56
|
+
|
|
57
|
+
# Quick analysis
|
|
58
|
+
gitviz quick
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 📊 What You Get
|
|
62
|
+
|
|
63
|
+
GitViz generates a comprehensive HTML report with:
|
|
64
|
+
|
|
65
|
+
### 1. Commit Timeline
|
|
66
|
+
See your project's activity patterns over time. Identify busy periods, quiet phases, and development trends.
|
|
67
|
+
|
|
68
|
+
### 2. Top Contributors
|
|
69
|
+
Understand who's driving your project forward. See commit counts and lines of code contributed.
|
|
70
|
+
|
|
71
|
+
### 3. File Change Heatmap
|
|
72
|
+
Discover which files change most frequently. Perfect for identifying technical debt and refactoring opportunities.
|
|
73
|
+
|
|
74
|
+
## 🎯 Use Cases
|
|
75
|
+
|
|
76
|
+
- **Project Health Checks** - Quickly assess repository activity
|
|
77
|
+
- **Team Analytics** - Understand contribution patterns
|
|
78
|
+
- **Documentation** - Add visual insights to your README
|
|
79
|
+
- **Code Reviews** - Identify frequently changed files
|
|
80
|
+
- **Onboarding** - Help new team members understand project history
|
|
81
|
+
|
|
82
|
+
## 🛠️ CLI Options
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
gitviz analyze [options]
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
-p, --path <path> Path to Git repository (default: ".")
|
|
89
|
+
-o, --output <file> Output HTML file (default: "gitviz-report.html")
|
|
90
|
+
-l, --limit <number> Limit number of commits (default: "1000")
|
|
91
|
+
-h, --help Display help information
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 📖 Examples
|
|
95
|
+
|
|
96
|
+
### Basic Analysis
|
|
97
|
+
```bash
|
|
98
|
+
gitviz analyze
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Analyze Large Repository
|
|
102
|
+
```bash
|
|
103
|
+
gitviz analyze --limit 5000 --output full-history.html
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Analyze Multiple Projects
|
|
107
|
+
```bash
|
|
108
|
+
gitviz analyze --path ~/projects/app1 --output app1-viz.html
|
|
109
|
+
gitviz analyze --path ~/projects/app2 --output app2-viz.html
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 🏗️ How It Works
|
|
113
|
+
|
|
114
|
+
1. **Parse** - GitViz reads your Git history using `simple-git`
|
|
115
|
+
2. **Analyze** - Processes commits, contributors, and file changes
|
|
116
|
+
3. **Visualize** - Generates interactive D3.js charts
|
|
117
|
+
4. **Export** - Creates a standalone HTML file you can share
|
|
118
|
+
|
|
119
|
+
## 🤝 Contributing
|
|
120
|
+
|
|
121
|
+
Contributions are welcome! Here's how you can help:
|
|
122
|
+
|
|
123
|
+
1. Fork the repository
|
|
124
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
125
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
126
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
127
|
+
5. Open a Pull Request
|
|
128
|
+
|
|
129
|
+
## 📝 Development
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Clone the repository
|
|
133
|
+
git clone https://github.com/anxin233/gitviz.git
|
|
134
|
+
cd gitviz
|
|
135
|
+
|
|
136
|
+
# Install dependencies
|
|
137
|
+
bun install
|
|
138
|
+
|
|
139
|
+
# Run in development mode
|
|
140
|
+
bun run dev analyze
|
|
141
|
+
|
|
142
|
+
# Build
|
|
143
|
+
bun run build
|
|
144
|
+
|
|
145
|
+
# Type check
|
|
146
|
+
bun run type-check
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 🐛 Known Issues
|
|
150
|
+
|
|
151
|
+
- Very large repositories (>10k commits) may take a few seconds to process
|
|
152
|
+
- Binary files are included in change statistics
|
|
153
|
+
|
|
154
|
+
## 🗺️ Roadmap
|
|
155
|
+
|
|
156
|
+
- [ ] Add more visualization types (network graphs, code frequency)
|
|
157
|
+
- [ ] Support for multiple branches comparison
|
|
158
|
+
- [ ] Export to PDF and PNG
|
|
159
|
+
- [ ] Real-time mode for live repositories
|
|
160
|
+
- [ ] Integration with GitHub/GitLab APIs
|
|
161
|
+
- [ ] Custom themes and color schemes
|
|
162
|
+
- [ ] Plugin system for custom analyzers
|
|
163
|
+
|
|
164
|
+
## 📄 License
|
|
165
|
+
|
|
166
|
+
MIT © 2026 GitViz Contributors
|
|
167
|
+
|
|
168
|
+
## 🌟 Show Your Support
|
|
169
|
+
|
|
170
|
+
If you find GitViz useful, please consider:
|
|
171
|
+
- ⭐ Starring the repository
|
|
172
|
+
- 🐦 Sharing on social media
|
|
173
|
+
- 🐛 Reporting bugs
|
|
174
|
+
- 💡 Suggesting new features
|
|
175
|
+
|
|
176
|
+
## 📬 Contact
|
|
177
|
+
|
|
178
|
+
- Issues: [GitHub Issues](https://github.com/anxin233/gitviz/issues)
|
|
179
|
+
- Discussions: [GitHub Discussions](https://github.com/anxin233/gitviz/discussions)
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
Made with ❤️ by developers, for developers
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# 🎨 GitViz
|
|
2
|
+
|
|
3
|
+
> 将你的 Git 历史转换为令人惊艳的可视化故事
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://bun.sh/)
|
|
8
|
+
[](https://github.com/anxin233/gitviz)
|
|
9
|
+
[](https://github.com/anxin233/gitviz/issues)
|
|
10
|
+
[](CONTRIBUTING.md)
|
|
11
|
+
|
|
12
|
+
[English](README.md) | 简体中文
|
|
13
|
+
|
|
14
|
+
GitViz 是一个强大的命令行工具,可以分析你的 Git 仓库并生成精美的交互式可视化图表。一目了然地理解项目历史、贡献者模式和代码演化过程。
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
[📖 快速开始](QUICKSTART.md) • [🎨 演示](DEMO.md) • [📚 示例](EXAMPLES.md) • [🤝 贡献指南](CONTRIBUTING.md)
|
|
19
|
+
|
|
20
|
+
## ✨ 特性
|
|
21
|
+
|
|
22
|
+
- 📊 **交互式时间线** - 可视化提交活动随时间的变化
|
|
23
|
+
- 👥 **贡献者分析** - 查看谁在贡献以及贡献了多少
|
|
24
|
+
- 🔥 **文件热力图** - 识别代码库中的热点区域
|
|
25
|
+
- 🚀 **极速性能** - 基于 Bun 实现最佳性能
|
|
26
|
+
- 📦 **零配置** - 开箱即用,适用于任何 Git 仓库
|
|
27
|
+
- 🎨 **精美输出** - 导出为独立的 HTML 文件
|
|
28
|
+
- 💻 **跨平台** - 支持 macOS、Linux 和 Windows
|
|
29
|
+
|
|
30
|
+
## 🚀 快速开始
|
|
31
|
+
|
|
32
|
+
### 安装
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 使用 npm
|
|
36
|
+
npm install -g @anxin233/gitviz
|
|
37
|
+
|
|
38
|
+
# 使用 Bun(推荐)
|
|
39
|
+
bun install -g @anxin233/gitviz
|
|
40
|
+
|
|
41
|
+
# 使用 yarn
|
|
42
|
+
yarn global add @anxin233/gitviz
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 使用方法
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# 分析当前仓库
|
|
49
|
+
gitviz analyze
|
|
50
|
+
|
|
51
|
+
# 分析指定仓库
|
|
52
|
+
gitviz analyze --path /path/to/repo
|
|
53
|
+
|
|
54
|
+
# 自定义输出
|
|
55
|
+
gitviz analyze --output my-report.html --limit 500
|
|
56
|
+
|
|
57
|
+
# 快速分析
|
|
58
|
+
gitviz quick
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 📊 你将获得什么
|
|
62
|
+
|
|
63
|
+
GitViz 会生成一份全面的 HTML 报告,包含:
|
|
64
|
+
|
|
65
|
+
### 1. 提交时间线
|
|
66
|
+
查看项目随时间的活动模式。识别繁忙期、平静期和开发趋势。
|
|
67
|
+
|
|
68
|
+
### 2. 顶级贡献者
|
|
69
|
+
了解谁在推动项目前进。查看提交次数和贡献的代码行数。
|
|
70
|
+
|
|
71
|
+
### 3. 文件变更热力图
|
|
72
|
+
发现哪些文件变更最频繁。非常适合识别技术债务和重构机会。
|
|
73
|
+
|
|
74
|
+
## 🎯 使用场景
|
|
75
|
+
|
|
76
|
+
- **项目健康检查** - 快速评估仓库活跃度
|
|
77
|
+
- **团队分析** - 理解贡献模式
|
|
78
|
+
- **文档编写** - 为 README 添加可视化洞察
|
|
79
|
+
- **代码审查** - 识别频繁变更的文件
|
|
80
|
+
- **新人入职** - 帮助新团队成员理解项目历史
|
|
81
|
+
|
|
82
|
+
## 🛠️ 命令行选项
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
gitviz analyze [选项]
|
|
86
|
+
|
|
87
|
+
选项:
|
|
88
|
+
-p, --path <path> Git 仓库路径(默认: ".")
|
|
89
|
+
-o, --output <file> 输出 HTML 文件(默认: "gitviz-report.html")
|
|
90
|
+
-l, --limit <number> 限制分析的提交数量(默认: "1000")
|
|
91
|
+
-h, --help 显示帮助信息
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 📖 示例
|
|
95
|
+
|
|
96
|
+
### 基础分析
|
|
97
|
+
```bash
|
|
98
|
+
gitviz analyze
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 分析大型仓库
|
|
102
|
+
```bash
|
|
103
|
+
gitviz analyze --limit 5000 --output full-history.html
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 分析多个项目
|
|
107
|
+
```bash
|
|
108
|
+
gitviz analyze --path ~/projects/app1 --output app1-viz.html
|
|
109
|
+
gitviz analyze --path ~/projects/app2 --output app2-viz.html
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 🏗️ 工作原理
|
|
113
|
+
|
|
114
|
+
1. **解析** - GitViz 使用 `simple-git` 读取 Git 历史
|
|
115
|
+
2. **分析** - 处理提交、贡献者和文件变更
|
|
116
|
+
3. **可视化** - 生成交互式 D3.js 图表
|
|
117
|
+
4. **导出** - 创建可分享的独立 HTML 文件
|
|
118
|
+
|
|
119
|
+
## 🤝 贡献
|
|
120
|
+
|
|
121
|
+
欢迎贡献!你可以这样帮助我们:
|
|
122
|
+
|
|
123
|
+
1. Fork 本仓库
|
|
124
|
+
2. 创建特性分支(`git checkout -b feature/amazing-feature`)
|
|
125
|
+
3. 提交你的更改(`git commit -m '添加某个很棒的特性'`)
|
|
126
|
+
4. 推送到分支(`git push origin feature/amazing-feature`)
|
|
127
|
+
5. 开启一个 Pull Request
|
|
128
|
+
|
|
129
|
+
## 📝 开发
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# 克隆仓库
|
|
133
|
+
git clone https://github.com/anxin233/gitviz.git
|
|
134
|
+
cd gitviz
|
|
135
|
+
|
|
136
|
+
# 安装依赖
|
|
137
|
+
bun install
|
|
138
|
+
|
|
139
|
+
# 开发模式运行
|
|
140
|
+
bun run dev analyze
|
|
141
|
+
|
|
142
|
+
# 构建
|
|
143
|
+
bun run build
|
|
144
|
+
|
|
145
|
+
# 类型检查
|
|
146
|
+
bun run type-check
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 🐛 已知问题
|
|
150
|
+
|
|
151
|
+
- 超大型仓库(>10k 提交)可能需要几秒钟处理
|
|
152
|
+
- 二进制文件会被包含在变更统计中
|
|
153
|
+
|
|
154
|
+
## 🗺️ 路线图
|
|
155
|
+
|
|
156
|
+
- [ ] 添加更多可视化类型(网络图、代码频率)
|
|
157
|
+
- [ ] 支持多分支对比
|
|
158
|
+
- [ ] 导出为 PDF 和 PNG 格式
|
|
159
|
+
- [ ] 实时模式监控活跃仓库
|
|
160
|
+
- [ ] 集成 GitHub/GitLab API
|
|
161
|
+
- [ ] 自定义主题和配色方案
|
|
162
|
+
- [ ] 插件系统支持自定义分析器
|
|
163
|
+
|
|
164
|
+
## 📄 许可证
|
|
165
|
+
|
|
166
|
+
MIT © 2026 GitViz 贡献者
|
|
167
|
+
|
|
168
|
+
## 🌟 支持我们
|
|
169
|
+
|
|
170
|
+
如果你觉得 GitViz 有用,请考虑:
|
|
171
|
+
- ⭐ 给仓库点个星
|
|
172
|
+
- 🐦 在社交媒体上分享
|
|
173
|
+
- 🐛 报告 bug
|
|
174
|
+
- 💡 提出新功能建议
|
|
175
|
+
|
|
176
|
+
## 📬 联系方式
|
|
177
|
+
|
|
178
|
+
- Issues: [GitHub Issues](https://github.com/anxin233/gitviz/issues)
|
|
179
|
+
- Discussions: [GitHub Discussions](https://github.com/anxin233/gitviz/discussions)
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
用 ❤️ 由开发者为开发者打造
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { writeFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { GitParser } from '../core/git-parser.js';
|
|
8
|
+
import { Analyzer } from '../core/analyzer.js';
|
|
9
|
+
import { generateHTML } from '../visualizers/html-generator.js';
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name('gitviz')
|
|
13
|
+
.description('🎨 Beautiful, interactive Git repository visualizations')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
program
|
|
16
|
+
.command('analyze')
|
|
17
|
+
.description('Analyze a Git repository and generate visualizations')
|
|
18
|
+
.option('-p, --path <path>', 'Path to Git repository', '.')
|
|
19
|
+
.option('-o, --output <file>', 'Output HTML file', 'gitviz-report.html')
|
|
20
|
+
.option('-l, --limit <number>', 'Limit number of commits to analyze', '1000')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
const spinner = ora('Initializing GitViz...').start();
|
|
23
|
+
try {
|
|
24
|
+
const parser = new GitParser(options.path);
|
|
25
|
+
spinner.text = 'Checking if directory is a Git repository...';
|
|
26
|
+
const isRepo = await parser.isGitRepository();
|
|
27
|
+
if (!isRepo) {
|
|
28
|
+
spinner.fail(chalk.red('Error: Not a Git repository'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
spinner.text = 'Parsing Git commits...';
|
|
32
|
+
const commits = await parser.parseCommits(parseInt(options.limit));
|
|
33
|
+
if (commits.length === 0) {
|
|
34
|
+
spinner.fail(chalk.red('Error: No commits found'));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
spinner.text = `Analyzing ${commits.length} commits...`;
|
|
38
|
+
const analyzer = new Analyzer();
|
|
39
|
+
const analysis = analyzer.analyze(commits);
|
|
40
|
+
spinner.text = 'Generating visualization data...';
|
|
41
|
+
const vizData = analyzer.generateVisualizationData(analysis);
|
|
42
|
+
spinner.text = 'Creating HTML report...';
|
|
43
|
+
const repoName = options.path === '.' ? 'Current Repository' : options.path;
|
|
44
|
+
const html = generateHTML(vizData, repoName);
|
|
45
|
+
const outputPath = join(process.cwd(), options.output);
|
|
46
|
+
writeFileSync(outputPath, html, 'utf-8');
|
|
47
|
+
spinner.succeed(chalk.green('✨ Visualization generated successfully!'));
|
|
48
|
+
console.log('\n' + chalk.bold('📊 Repository Statistics:'));
|
|
49
|
+
console.log(chalk.cyan(` Total Commits: ${analysis.totalCommits}`));
|
|
50
|
+
console.log(chalk.cyan(` Contributors: ${analysis.totalContributors}`));
|
|
51
|
+
console.log(chalk.cyan(` Files Changed: ${analysis.files.size}`));
|
|
52
|
+
console.log(chalk.cyan(` Date Range: ${analysis.dateRange.start.toLocaleDateString()} - ${analysis.dateRange.end.toLocaleDateString()}`));
|
|
53
|
+
console.log('\n' + chalk.bold(`📁 Output: ${outputPath}`));
|
|
54
|
+
console.log(chalk.gray(`\nOpen the file in your browser to view the interactive visualization.\n`));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
spinner.fail(chalk.red('Error: ' + error.message));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
program
|
|
62
|
+
.command('quick')
|
|
63
|
+
.description('Quick analysis of current directory')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
program.parse(['node', 'gitviz', 'analyze']);
|
|
66
|
+
});
|
|
67
|
+
program.parse();
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { writeFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { GitParser } from '../core/git-parser.js';
|
|
8
|
+
import { Analyzer } from '../core/analyzer.js';
|
|
9
|
+
import { generateHTML } from '../visualizers/html-generator.js';
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name('gitviz')
|
|
13
|
+
.description('🎨 Beautiful, interactive Git repository visualizations')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
program
|
|
16
|
+
.command('analyze')
|
|
17
|
+
.description('Analyze a Git repository and generate visualizations')
|
|
18
|
+
.option('-p, --path <path>', 'Path to Git repository', '.')
|
|
19
|
+
.option('-o, --output <file>', 'Output HTML file', 'gitviz-report.html')
|
|
20
|
+
.option('-l, --limit <number>', 'Limit number of commits to analyze', '1000')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
const spinner = ora('Initializing GitViz...').start();
|
|
23
|
+
try {
|
|
24
|
+
const parser = new GitParser(options.path);
|
|
25
|
+
spinner.text = 'Checking if directory is a Git repository...';
|
|
26
|
+
const isRepo = await parser.isGitRepository();
|
|
27
|
+
if (!isRepo) {
|
|
28
|
+
spinner.fail(chalk.red('Error: Not a Git repository'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
spinner.text = 'Parsing Git commits...';
|
|
32
|
+
const commits = await parser.parseCommits(parseInt(options.limit));
|
|
33
|
+
if (commits.length === 0) {
|
|
34
|
+
spinner.fail(chalk.red('Error: No commits found'));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
spinner.text = `Analyzing ${commits.length} commits...`;
|
|
38
|
+
const analyzer = new Analyzer();
|
|
39
|
+
const analysis = analyzer.analyze(commits);
|
|
40
|
+
spinner.text = 'Generating visualization data...';
|
|
41
|
+
const vizData = analyzer.generateVisualizationData(analysis);
|
|
42
|
+
spinner.text = 'Creating HTML report...';
|
|
43
|
+
const repoName = options.path === '.' ? 'Current Repository' : options.path;
|
|
44
|
+
const html = generateHTML(vizData, repoName);
|
|
45
|
+
const outputPath = join(process.cwd(), options.output);
|
|
46
|
+
writeFileSync(outputPath, html, 'utf-8');
|
|
47
|
+
spinner.succeed(chalk.green('✨ Visualization generated successfully!'));
|
|
48
|
+
console.log('\n' + chalk.bold('📊 Repository Statistics:'));
|
|
49
|
+
console.log(chalk.cyan(` Total Commits: ${analysis.totalCommits}`));
|
|
50
|
+
console.log(chalk.cyan(` Contributors: ${analysis.totalContributors}`));
|
|
51
|
+
console.log(chalk.cyan(` Files Changed: ${analysis.files.size}`));
|
|
52
|
+
console.log(chalk.cyan(` Date Range: ${analysis.dateRange.start.toLocaleDateString()} - ${analysis.dateRange.end.toLocaleDateString()}`));
|
|
53
|
+
console.log('\n' + chalk.bold(`📁 Output: ${outputPath}`));
|
|
54
|
+
console.log(chalk.gray(`\nOpen the file in your browser to view the interactive visualization.\n`));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
spinner.fail(chalk.red('Error: ' + error.message));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
program
|
|
62
|
+
.command('quick')
|
|
63
|
+
.description('Quick analysis of current directory')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
program.parse(['node', 'gitviz', 'analyze']);
|
|
66
|
+
});
|
|
67
|
+
program.parse();
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export class Analyzer {
|
|
2
|
+
analyze(commits) {
|
|
3
|
+
const contributors = new Map();
|
|
4
|
+
const files = new Map();
|
|
5
|
+
for (const commit of commits) {
|
|
6
|
+
const key = commit.email || commit.author;
|
|
7
|
+
if (!contributors.has(key)) {
|
|
8
|
+
contributors.set(key, {
|
|
9
|
+
name: commit.author,
|
|
10
|
+
email: commit.email,
|
|
11
|
+
commits: 0,
|
|
12
|
+
insertions: 0,
|
|
13
|
+
deletions: 0,
|
|
14
|
+
firstCommit: commit.date,
|
|
15
|
+
lastCommit: commit.date
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const contributor = contributors.get(key);
|
|
19
|
+
contributor.commits++;
|
|
20
|
+
contributor.insertions += commit.insertions;
|
|
21
|
+
contributor.deletions += commit.deletions;
|
|
22
|
+
contributor.lastCommit = commit.date;
|
|
23
|
+
for (const file of commit.files) {
|
|
24
|
+
if (!files.has(file)) {
|
|
25
|
+
files.set(file, {
|
|
26
|
+
path: file,
|
|
27
|
+
changes: 0,
|
|
28
|
+
lastModified: commit.date,
|
|
29
|
+
contributors: new Set()
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const fileChange = files.get(file);
|
|
33
|
+
fileChange.changes++;
|
|
34
|
+
fileChange.lastModified = commit.date;
|
|
35
|
+
fileChange.contributors.add(commit.author);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const dates = commits.map(c => c.date);
|
|
39
|
+
return {
|
|
40
|
+
commits,
|
|
41
|
+
contributors,
|
|
42
|
+
files,
|
|
43
|
+
totalCommits: commits.length,
|
|
44
|
+
totalContributors: contributors.size,
|
|
45
|
+
dateRange: {
|
|
46
|
+
start: new Date(Math.min(...dates.map(d => d.getTime()))),
|
|
47
|
+
end: new Date(Math.max(...dates.map(d => d.getTime())))
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
generateVisualizationData(analysis) {
|
|
52
|
+
return {
|
|
53
|
+
timeline: this.generateTimelineData(analysis.commits),
|
|
54
|
+
contributors: this.generateContributorData(analysis.contributors),
|
|
55
|
+
heatmap: this.generateHeatmapData(analysis.files)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
generateTimelineData(commits) {
|
|
59
|
+
const dailyStats = new Map();
|
|
60
|
+
for (const commit of commits) {
|
|
61
|
+
const dateKey = commit.date.toISOString().split('T')[0];
|
|
62
|
+
if (!dailyStats.has(dateKey)) {
|
|
63
|
+
dailyStats.set(dateKey, { commits: 0, insertions: 0, deletions: 0 });
|
|
64
|
+
}
|
|
65
|
+
const stats = dailyStats.get(dateKey);
|
|
66
|
+
stats.commits++;
|
|
67
|
+
stats.insertions += commit.insertions;
|
|
68
|
+
stats.deletions += commit.deletions;
|
|
69
|
+
}
|
|
70
|
+
return Array.from(dailyStats.entries())
|
|
71
|
+
.map(([date, stats]) => ({
|
|
72
|
+
date,
|
|
73
|
+
...stats
|
|
74
|
+
}))
|
|
75
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
76
|
+
}
|
|
77
|
+
generateContributorData(contributors) {
|
|
78
|
+
return Array.from(contributors.values())
|
|
79
|
+
.map(c => ({
|
|
80
|
+
name: c.name,
|
|
81
|
+
commits: c.commits,
|
|
82
|
+
lines: c.insertions + c.deletions
|
|
83
|
+
}))
|
|
84
|
+
.sort((a, b) => b.commits - a.commits)
|
|
85
|
+
.slice(0, 20);
|
|
86
|
+
}
|
|
87
|
+
generateHeatmapData(files) {
|
|
88
|
+
const fileArray = Array.from(files.values());
|
|
89
|
+
const maxChanges = Math.max(...fileArray.map(f => f.changes));
|
|
90
|
+
return fileArray
|
|
91
|
+
.map(f => ({
|
|
92
|
+
file: f.path,
|
|
93
|
+
changes: f.changes,
|
|
94
|
+
heat: f.changes / maxChanges
|
|
95
|
+
}))
|
|
96
|
+
.sort((a, b) => b.changes - a.changes)
|
|
97
|
+
.slice(0, 50);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import simpleGit from 'simple-git';
|
|
2
|
+
export class GitParser {
|
|
3
|
+
git;
|
|
4
|
+
repoPath;
|
|
5
|
+
constructor(repoPath = '.') {
|
|
6
|
+
this.repoPath = repoPath;
|
|
7
|
+
this.git = simpleGit(repoPath);
|
|
8
|
+
}
|
|
9
|
+
async isGitRepository() {
|
|
10
|
+
try {
|
|
11
|
+
await this.git.status();
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async parseCommits(limit = 1000) {
|
|
19
|
+
const log = await this.git.log({
|
|
20
|
+
maxCount: limit,
|
|
21
|
+
'--numstat': null,
|
|
22
|
+
'--pretty': 'format:%H|%an|%ae|%ai|%s'
|
|
23
|
+
});
|
|
24
|
+
const commits = [];
|
|
25
|
+
for (const commit of log.all) {
|
|
26
|
+
const diffSummary = await this.git.diffSummary([`${commit.hash}^`, commit.hash]);
|
|
27
|
+
commits.push({
|
|
28
|
+
hash: commit.hash,
|
|
29
|
+
author: commit.author_name || 'Unknown',
|
|
30
|
+
email: commit.author_email || '',
|
|
31
|
+
date: new Date(commit.date),
|
|
32
|
+
message: commit.message,
|
|
33
|
+
files: diffSummary.files.map(f => f.file),
|
|
34
|
+
insertions: diffSummary.insertions,
|
|
35
|
+
deletions: diffSummary.deletions
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return commits;
|
|
39
|
+
}
|
|
40
|
+
async getBranchName() {
|
|
41
|
+
const branch = await this.git.branch();
|
|
42
|
+
return branch.current;
|
|
43
|
+
}
|
|
44
|
+
async getRemoteUrl() {
|
|
45
|
+
try {
|
|
46
|
+
const remotes = await this.git.getRemotes(true);
|
|
47
|
+
return remotes[0]?.refs?.fetch || null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
export function generateHTML(data, repoName) {
|
|
2
|
+
return `<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>GitViz - ${repoName}</title>
|
|
8
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
11
|
+
body {
|
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
13
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
14
|
+
color: #333;
|
|
15
|
+
padding: 2rem;
|
|
16
|
+
}
|
|
17
|
+
.container {
|
|
18
|
+
max-width: 1400px;
|
|
19
|
+
margin: 0 auto;
|
|
20
|
+
background: white;
|
|
21
|
+
border-radius: 20px;
|
|
22
|
+
padding: 3rem;
|
|
23
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
24
|
+
}
|
|
25
|
+
h1 {
|
|
26
|
+
font-size: 3rem;
|
|
27
|
+
margin-bottom: 0.5rem;
|
|
28
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
29
|
+
-webkit-background-clip: text;
|
|
30
|
+
-webkit-text-fill-color: transparent;
|
|
31
|
+
background-clip: text;
|
|
32
|
+
}
|
|
33
|
+
.subtitle {
|
|
34
|
+
color: #666;
|
|
35
|
+
font-size: 1.2rem;
|
|
36
|
+
margin-bottom: 3rem;
|
|
37
|
+
}
|
|
38
|
+
.section {
|
|
39
|
+
margin-bottom: 4rem;
|
|
40
|
+
}
|
|
41
|
+
.section h2 {
|
|
42
|
+
font-size: 1.8rem;
|
|
43
|
+
margin-bottom: 1.5rem;
|
|
44
|
+
color: #333;
|
|
45
|
+
}
|
|
46
|
+
.chart {
|
|
47
|
+
background: #f8f9fa;
|
|
48
|
+
border-radius: 12px;
|
|
49
|
+
padding: 2rem;
|
|
50
|
+
}
|
|
51
|
+
.bar { fill: #667eea; transition: fill 0.3s; }
|
|
52
|
+
.bar:hover { fill: #764ba2; }
|
|
53
|
+
.axis { font-size: 12px; }
|
|
54
|
+
.axis path, .axis line { stroke: #ddd; }
|
|
55
|
+
.tooltip {
|
|
56
|
+
position: absolute;
|
|
57
|
+
background: rgba(0,0,0,0.8);
|
|
58
|
+
color: white;
|
|
59
|
+
padding: 8px 12px;
|
|
60
|
+
border-radius: 6px;
|
|
61
|
+
font-size: 14px;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
opacity: 0;
|
|
64
|
+
transition: opacity 0.3s;
|
|
65
|
+
}
|
|
66
|
+
.stats {
|
|
67
|
+
display: grid;
|
|
68
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
69
|
+
gap: 1.5rem;
|
|
70
|
+
margin-bottom: 3rem;
|
|
71
|
+
}
|
|
72
|
+
.stat-card {
|
|
73
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
74
|
+
color: white;
|
|
75
|
+
padding: 1.5rem;
|
|
76
|
+
border-radius: 12px;
|
|
77
|
+
text-align: center;
|
|
78
|
+
}
|
|
79
|
+
.stat-value {
|
|
80
|
+
font-size: 2.5rem;
|
|
81
|
+
font-weight: bold;
|
|
82
|
+
margin-bottom: 0.5rem;
|
|
83
|
+
}
|
|
84
|
+
.stat-label {
|
|
85
|
+
font-size: 1rem;
|
|
86
|
+
opacity: 0.9;
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
89
|
+
</head>
|
|
90
|
+
<body>
|
|
91
|
+
<div class="container">
|
|
92
|
+
<h1>📊 GitViz</h1>
|
|
93
|
+
<p class="subtitle">Repository: ${repoName}</p>
|
|
94
|
+
|
|
95
|
+
<div class="stats">
|
|
96
|
+
<div class="stat-card">
|
|
97
|
+
<div class="stat-value">${data.timeline.reduce((sum, d) => sum + d.commits, 0)}</div>
|
|
98
|
+
<div class="stat-label">Total Commits</div>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="stat-card">
|
|
101
|
+
<div class="stat-value">${data.contributors.length}</div>
|
|
102
|
+
<div class="stat-label">Contributors</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="stat-card">
|
|
105
|
+
<div class="stat-value">${data.heatmap.length}</div>
|
|
106
|
+
<div class="stat-label">Files Changed</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="section">
|
|
111
|
+
<h2>📈 Commit Timeline</h2>
|
|
112
|
+
<div class="chart" id="timeline"></div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="section">
|
|
116
|
+
<h2>👥 Top Contributors</h2>
|
|
117
|
+
<div class="chart" id="contributors"></div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="section">
|
|
121
|
+
<h2>🔥 File Change Heatmap</h2>
|
|
122
|
+
<div class="chart" id="heatmap"></div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="tooltip" id="tooltip"></div>
|
|
127
|
+
|
|
128
|
+
<script>
|
|
129
|
+
const data = ${JSON.stringify(data)};
|
|
130
|
+
|
|
131
|
+
// Timeline Chart
|
|
132
|
+
{
|
|
133
|
+
const margin = {top: 20, right: 30, bottom: 40, left: 50};
|
|
134
|
+
const width = 1200 - margin.left - margin.right;
|
|
135
|
+
const height = 300 - margin.top - margin.bottom;
|
|
136
|
+
|
|
137
|
+
const svg = d3.select("#timeline")
|
|
138
|
+
.append("svg")
|
|
139
|
+
.attr("width", width + margin.left + margin.right)
|
|
140
|
+
.attr("height", height + margin.top + margin.bottom)
|
|
141
|
+
.append("g")
|
|
142
|
+
.attr("transform", \`translate(\${margin.left},\${margin.top})\`);
|
|
143
|
+
|
|
144
|
+
const x = d3.scaleBand()
|
|
145
|
+
.domain(data.timeline.map(d => d.date))
|
|
146
|
+
.range([0, width])
|
|
147
|
+
.padding(0.1);
|
|
148
|
+
|
|
149
|
+
const y = d3.scaleLinear()
|
|
150
|
+
.domain([0, d3.max(data.timeline, d => d.commits)])
|
|
151
|
+
.range([height, 0]);
|
|
152
|
+
|
|
153
|
+
svg.append("g")
|
|
154
|
+
.attr("class", "axis")
|
|
155
|
+
.attr("transform", \`translate(0,\${height})\`)
|
|
156
|
+
.call(d3.axisBottom(x).tickValues(x.domain().filter((d, i) => i % Math.ceil(data.timeline.length / 10) === 0)));
|
|
157
|
+
|
|
158
|
+
svg.append("g")
|
|
159
|
+
.attr("class", "axis")
|
|
160
|
+
.call(d3.axisLeft(y));
|
|
161
|
+
|
|
162
|
+
svg.selectAll(".bar")
|
|
163
|
+
.data(data.timeline)
|
|
164
|
+
.enter()
|
|
165
|
+
.append("rect")
|
|
166
|
+
.attr("class", "bar")
|
|
167
|
+
.attr("x", d => x(d.date))
|
|
168
|
+
.attr("y", d => y(d.commits))
|
|
169
|
+
.attr("width", x.bandwidth())
|
|
170
|
+
.attr("height", d => height - y(d.commits))
|
|
171
|
+
.on("mouseover", function(event, d) {
|
|
172
|
+
d3.select("#tooltip")
|
|
173
|
+
.style("opacity", 1)
|
|
174
|
+
.html(\`<strong>\${d.date}</strong><br/>Commits: \${d.commits}<br/>+\${d.insertions} -\${d.deletions}\`)
|
|
175
|
+
.style("left", (event.pageX + 10) + "px")
|
|
176
|
+
.style("top", (event.pageY - 10) + "px");
|
|
177
|
+
})
|
|
178
|
+
.on("mouseout", function() {
|
|
179
|
+
d3.select("#tooltip").style("opacity", 0);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Contributors Chart
|
|
184
|
+
{
|
|
185
|
+
const margin = {top: 20, right: 30, bottom: 100, left: 50};
|
|
186
|
+
const width = 1200 - margin.left - margin.right;
|
|
187
|
+
const height = 400 - margin.top - margin.bottom;
|
|
188
|
+
|
|
189
|
+
const svg = d3.select("#contributors")
|
|
190
|
+
.append("svg")
|
|
191
|
+
.attr("width", width + margin.left + margin.right)
|
|
192
|
+
.attr("height", height + margin.top + margin.bottom)
|
|
193
|
+
.append("g")
|
|
194
|
+
.attr("transform", \`translate(\${margin.left},\${margin.top})\`);
|
|
195
|
+
|
|
196
|
+
const x = d3.scaleBand()
|
|
197
|
+
.domain(data.contributors.map(d => d.name))
|
|
198
|
+
.range([0, width])
|
|
199
|
+
.padding(0.2);
|
|
200
|
+
|
|
201
|
+
const y = d3.scaleLinear()
|
|
202
|
+
.domain([0, d3.max(data.contributors, d => d.commits)])
|
|
203
|
+
.range([height, 0]);
|
|
204
|
+
|
|
205
|
+
svg.append("g")
|
|
206
|
+
.attr("class", "axis")
|
|
207
|
+
.attr("transform", \`translate(0,\${height})\`)
|
|
208
|
+
.call(d3.axisBottom(x))
|
|
209
|
+
.selectAll("text")
|
|
210
|
+
.attr("transform", "rotate(-45)")
|
|
211
|
+
.style("text-anchor", "end");
|
|
212
|
+
|
|
213
|
+
svg.append("g")
|
|
214
|
+
.attr("class", "axis")
|
|
215
|
+
.call(d3.axisLeft(y));
|
|
216
|
+
|
|
217
|
+
svg.selectAll(".bar")
|
|
218
|
+
.data(data.contributors)
|
|
219
|
+
.enter()
|
|
220
|
+
.append("rect")
|
|
221
|
+
.attr("class", "bar")
|
|
222
|
+
.attr("x", d => x(d.name))
|
|
223
|
+
.attr("y", d => y(d.commits))
|
|
224
|
+
.attr("width", x.bandwidth())
|
|
225
|
+
.attr("height", d => height - y(d.commits))
|
|
226
|
+
.on("mouseover", function(event, d) {
|
|
227
|
+
d3.select("#tooltip")
|
|
228
|
+
.style("opacity", 1)
|
|
229
|
+
.html(\`<strong>\${d.name}</strong><br/>Commits: \${d.commits}<br/>Lines: \${d.lines}\`)
|
|
230
|
+
.style("left", (event.pageX + 10) + "px")
|
|
231
|
+
.style("top", (event.pageY - 10) + "px");
|
|
232
|
+
})
|
|
233
|
+
.on("mouseout", function() {
|
|
234
|
+
d3.select("#tooltip").style("opacity", 0);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Heatmap
|
|
239
|
+
{
|
|
240
|
+
const margin = {top: 20, right: 30, bottom: 20, left: 300};
|
|
241
|
+
const width = 1200 - margin.left - margin.right;
|
|
242
|
+
const height = Math.min(800, data.heatmap.length * 20);
|
|
243
|
+
|
|
244
|
+
const svg = d3.select("#heatmap")
|
|
245
|
+
.append("svg")
|
|
246
|
+
.attr("width", width + margin.left + margin.right)
|
|
247
|
+
.attr("height", height + margin.top + margin.bottom)
|
|
248
|
+
.append("g")
|
|
249
|
+
.attr("transform", \`translate(\${margin.left},\${margin.top})\`);
|
|
250
|
+
|
|
251
|
+
const y = d3.scaleBand()
|
|
252
|
+
.domain(data.heatmap.map(d => d.file))
|
|
253
|
+
.range([0, height])
|
|
254
|
+
.padding(0.1);
|
|
255
|
+
|
|
256
|
+
const colorScale = d3.scaleSequential(d3.interpolateReds)
|
|
257
|
+
.domain([0, 1]);
|
|
258
|
+
|
|
259
|
+
svg.selectAll("rect")
|
|
260
|
+
.data(data.heatmap)
|
|
261
|
+
.enter()
|
|
262
|
+
.append("rect")
|
|
263
|
+
.attr("y", d => y(d.file))
|
|
264
|
+
.attr("width", width)
|
|
265
|
+
.attr("height", y.bandwidth())
|
|
266
|
+
.attr("fill", d => colorScale(d.heat))
|
|
267
|
+
.on("mouseover", function(event, d) {
|
|
268
|
+
d3.select("#tooltip")
|
|
269
|
+
.style("opacity", 1)
|
|
270
|
+
.html(\`<strong>\${d.file}</strong><br/>Changes: \${d.changes}\`)
|
|
271
|
+
.style("left", (event.pageX + 10) + "px")
|
|
272
|
+
.style("top", (event.pageY - 10) + "px");
|
|
273
|
+
})
|
|
274
|
+
.on("mouseout", function() {
|
|
275
|
+
d3.select("#tooltip").style("opacity", 0);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
svg.selectAll("text")
|
|
279
|
+
.data(data.heatmap)
|
|
280
|
+
.enter()
|
|
281
|
+
.append("text")
|
|
282
|
+
.attr("x", -10)
|
|
283
|
+
.attr("y", d => y(d.file) + y.bandwidth() / 2)
|
|
284
|
+
.attr("dy", "0.35em")
|
|
285
|
+
.attr("text-anchor", "end")
|
|
286
|
+
.attr("font-size", "12px")
|
|
287
|
+
.text(d => d.file);
|
|
288
|
+
}
|
|
289
|
+
</script>
|
|
290
|
+
</body>
|
|
291
|
+
</html>`;
|
|
292
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anxin233/gitviz",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "🎨 Beautiful, interactive Git repository visualizations - Transform your Git history into stunning visual stories",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gitviz": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node --loader ts-node/esm src/cli/index.ts",
|
|
11
|
+
"build": "tsc && node scripts/bundle.js",
|
|
12
|
+
"build:bun": "bun build src/cli/index.ts --outfile dist/cli.js --target node --minify",
|
|
13
|
+
"build:web": "cd src/web && vite build",
|
|
14
|
+
"preview": "cd src/web && vite preview",
|
|
15
|
+
"type-check": "tsc --noEmit",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"git",
|
|
20
|
+
"visualization",
|
|
21
|
+
"analytics",
|
|
22
|
+
"cli",
|
|
23
|
+
"developer-tools",
|
|
24
|
+
"git-history",
|
|
25
|
+
"d3",
|
|
26
|
+
"charts"
|
|
27
|
+
],
|
|
28
|
+
"author": "anxin233",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/anxin233/gitviz.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/anxin233/gitviz#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/anxin233/gitviz/issues"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"commander": "^12.0.0",
|
|
40
|
+
"simple-git": "^3.22.0",
|
|
41
|
+
"chalk": "^5.3.0",
|
|
42
|
+
"ora": "^8.0.1",
|
|
43
|
+
"d3": "^7.9.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.11.0",
|
|
47
|
+
"@types/d3": "^7.4.3",
|
|
48
|
+
"typescript": "^5.3.3",
|
|
49
|
+
"vite": "^5.0.12",
|
|
50
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
51
|
+
"react": "^18.2.0",
|
|
52
|
+
"react-dom": "^18.2.0",
|
|
53
|
+
"@types/react": "^18.2.48",
|
|
54
|
+
"@types/react-dom": "^18.2.18"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/scripts/build.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const rootDir = join(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
// 创建 dist 目录
|
|
9
|
+
mkdirSync(join(rootDir, 'dist'), { recursive: true });
|
|
10
|
+
|
|
11
|
+
// 读取编译后的文件
|
|
12
|
+
const cliPath = join(rootDir, 'dist', 'cli', 'index.js');
|
|
13
|
+
const outputPath = join(rootDir, 'dist', 'cli.js');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const content = readFileSync(cliPath, 'utf-8');
|
|
17
|
+
|
|
18
|
+
// 添加 shebang
|
|
19
|
+
const finalContent = '#!/usr/bin/env node\n' + content;
|
|
20
|
+
|
|
21
|
+
// 写入最终文件
|
|
22
|
+
writeFileSync(outputPath, finalContent, 'utf-8');
|
|
23
|
+
|
|
24
|
+
console.log('✅ Build successful: dist/cli.js');
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('❌ Build failed:', error.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, cpSync } from 'fs';
|
|
2
|
+
import { join, dirname, relative } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const rootDir = join(__dirname, '..');
|
|
7
|
+
const distDir = join(rootDir, 'dist');
|
|
8
|
+
|
|
9
|
+
console.log('📦 Bundling CLI...');
|
|
10
|
+
|
|
11
|
+
// 读取所有编译后的文件并合并
|
|
12
|
+
function getAllFiles(dir, fileList = []) {
|
|
13
|
+
const files = readdirSync(dir);
|
|
14
|
+
files.forEach(file => {
|
|
15
|
+
const filePath = join(dir, file);
|
|
16
|
+
if (statSync(filePath).isDirectory()) {
|
|
17
|
+
getAllFiles(filePath, fileList);
|
|
18
|
+
} else if (file.endsWith('.js')) {
|
|
19
|
+
fileList.push(filePath);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return fileList;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// 简单方案:直接复制 cli/index.js 作为入口
|
|
27
|
+
const cliIndexPath = join(distDir, 'cli', 'index.js');
|
|
28
|
+
const outputPath = join(distDir, 'cli.js');
|
|
29
|
+
|
|
30
|
+
if (!statSync(cliIndexPath).isFile()) {
|
|
31
|
+
throw new Error('CLI index.js not found');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let content = readFileSync(cliIndexPath, 'utf-8');
|
|
35
|
+
|
|
36
|
+
// 确保有 shebang
|
|
37
|
+
if (!content.startsWith('#!')) {
|
|
38
|
+
content = '#!/usr/bin/env node\n' + content;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
writeFileSync(outputPath, content, 'utf-8');
|
|
42
|
+
|
|
43
|
+
console.log('✅ Bundle created: dist/cli.js');
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('❌ Bundle failed:', error.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|