git-history-ui 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.
Files changed (64) hide show
  1. package/.eslintrc.js +21 -0
  2. package/README.md +304 -0
  3. package/demo.js +45 -0
  4. package/dist/__tests__/gitService.test.d.ts +2 -0
  5. package/dist/__tests__/gitService.test.d.ts.map +1 -0
  6. package/dist/__tests__/gitService.test.js +32 -0
  7. package/dist/__tests__/gitService.test.js.map +1 -0
  8. package/dist/backend/dev-server.d.ts +2 -0
  9. package/dist/backend/dev-server.d.ts.map +1 -0
  10. package/dist/backend/dev-server.js +16 -0
  11. package/dist/backend/dev-server.js.map +1 -0
  12. package/dist/backend/gitService.d.ts +45 -0
  13. package/dist/backend/gitService.d.ts.map +1 -0
  14. package/dist/backend/gitService.js +239 -0
  15. package/dist/backend/gitService.js.map +1 -0
  16. package/dist/backend/server.d.ts +9 -0
  17. package/dist/backend/server.d.ts.map +1 -0
  18. package/dist/backend/server.js +118 -0
  19. package/dist/backend/server.js.map +1 -0
  20. package/dist/cli.d.ts +3 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +49 -0
  23. package/dist/cli.js.map +1 -0
  24. package/frontend/.editorconfig +17 -0
  25. package/frontend/.vscode/extensions.json +4 -0
  26. package/frontend/.vscode/launch.json +20 -0
  27. package/frontend/.vscode/tasks.json +42 -0
  28. package/frontend/README.md +59 -0
  29. package/frontend/angular.json +99 -0
  30. package/frontend/package-lock.json +10566 -0
  31. package/frontend/package.json +55 -0
  32. package/frontend/proxy.conf.json +7 -0
  33. package/frontend/public/favicon.ico +0 -0
  34. package/frontend/src/app/app.component.ts +598 -0
  35. package/frontend/src/app/app.config.ts +12 -0
  36. package/frontend/src/app/app.css +0 -0
  37. package/frontend/src/app/app.html +342 -0
  38. package/frontend/src/app/app.routes.ts +3 -0
  39. package/frontend/src/app/app.spec.ts +23 -0
  40. package/frontend/src/app/app.ts +12 -0
  41. package/frontend/src/app/components/color-palette-selector/color-palette-selector.component.ts +137 -0
  42. package/frontend/src/app/components/commit-detail/commit-detail.component.ts +327 -0
  43. package/frontend/src/app/components/commit-graph/commit-graph.component.ts +294 -0
  44. package/frontend/src/app/components/commit-list/commit-list.component.ts +199 -0
  45. package/frontend/src/app/components/diff-viewer/diff-viewer.component.ts +311 -0
  46. package/frontend/src/app/models/color-palette.models.ts +229 -0
  47. package/frontend/src/app/models/git.models.ts +39 -0
  48. package/frontend/src/app/services/git.service.ts +43 -0
  49. package/frontend/src/index.html +13 -0
  50. package/frontend/src/main.ts +6 -0
  51. package/frontend/src/styles.css +397 -0
  52. package/frontend/tsconfig.app.json +15 -0
  53. package/frontend/tsconfig.json +34 -0
  54. package/frontend/tsconfig.spec.json +14 -0
  55. package/jest.config.js +13 -0
  56. package/package.json +70 -0
  57. package/public/app.js +403 -0
  58. package/public/index.html +172 -0
  59. package/src/__tests__/gitService.test.ts +35 -0
  60. package/src/backend/dev-server.ts +14 -0
  61. package/src/backend/gitService.ts +277 -0
  62. package/src/backend/server.ts +132 -0
  63. package/src/cli.ts +56 -0
  64. package/tsconfig.json +25 -0
@@ -0,0 +1,172 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Git History UI</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://d3js.org/d3.v7.min.js"></script>
9
+ <script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
10
+ <style>
11
+ /* Ensure smooth transitions for dark mode */
12
+ html, body {
13
+ transition: background-color 0.2s ease, color 0.2s ease;
14
+ }
15
+
16
+ /* Dark mode specific styles */
17
+ .dark {
18
+ color-scheme: dark;
19
+ }
20
+
21
+ .dark body {
22
+ background-color: #111827;
23
+ color: #f9fafb;
24
+ }
25
+
26
+ .commit-node {
27
+ cursor: pointer;
28
+ transition: all 0.2s ease;
29
+ }
30
+ .commit-node:hover {
31
+ stroke-width: 3px;
32
+ }
33
+ .branch-line {
34
+ stroke-width: 2px;
35
+ }
36
+
37
+ /* SVG dark mode support */
38
+ .dark svg {
39
+ background-color: transparent;
40
+ }
41
+
42
+ /* Graph container dark mode */
43
+ .dark #commitGraph {
44
+ background-color: transparent;
45
+ }
46
+ .diff-line {
47
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
48
+ font-size: 12px;
49
+ line-height: 1.4;
50
+ }
51
+ .diff-line.added {
52
+ background-color: #dcfce7;
53
+ color: #166534;
54
+ }
55
+ .diff-line.removed {
56
+ background-color: #fee2e2;
57
+ color: #991b1b;
58
+ }
59
+ .dark .diff-line.added {
60
+ background-color: #14532d;
61
+ color: #bbf7d0;
62
+ }
63
+ .dark .diff-line.removed {
64
+ background-color: #7f1d1d;
65
+ color: #fecaca;
66
+ }
67
+
68
+ /* Ensure all elements transition smoothly */
69
+ * {
70
+ transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
71
+ }
72
+ </style>
73
+ </head>
74
+ <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
75
+ <div id="app" class="min-h-screen">
76
+ <!-- Header -->
77
+ <header class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 transition-colors duration-200">
78
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
79
+ <div class="flex justify-between items-center py-4">
80
+ <div class="flex items-center space-x-4">
81
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white transition-colors duration-200">
82
+ šŸ“Š Git History UI
83
+ </h1>
84
+ <div class="flex items-center space-x-2">
85
+ <button id="graphView" class="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200">
86
+ Graph View
87
+ </button>
88
+ <button id="listView" class="px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors duration-200">
89
+ List View
90
+ </button>
91
+ </div>
92
+ </div>
93
+ <div class="flex items-center space-x-4">
94
+ <button id="darkMode" class="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors duration-200">
95
+ šŸŒ™
96
+ </button>
97
+ <div class="relative">
98
+ <input type="text" id="search" placeholder="Search commits..."
99
+ class="w-64 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors duration-200">
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </header>
105
+
106
+ <!-- Filters -->
107
+ <div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 transition-colors duration-200">
108
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
109
+ <div class="flex items-center space-x-4">
110
+ <div class="flex items-center space-x-2">
111
+ <label class="text-sm font-medium text-gray-700 dark:text-gray-300 transition-colors duration-200">Author:</label>
112
+ <select id="authorFilter" class="text-sm border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-colors duration-200">
113
+ <option value="">All authors</option>
114
+ </select>
115
+ </div>
116
+ <div class="flex items-center space-x-2">
117
+ <label class="text-sm font-medium text-gray-700 dark:text-gray-300 transition-colors duration-200">Since:</label>
118
+ <select id="sinceFilter" class="text-sm border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-colors duration-200">
119
+ <option value="">All time</option>
120
+ </select>
121
+ </div>
122
+ <div class="flex items-center space-x-2">
123
+ <label class="text-sm font-medium text-gray-700 dark:text-gray-300 transition-colors duration-200">File:</label>
124
+ <input type="text" id="fileFilter" placeholder="Filter by file..."
125
+ class="text-sm border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-colors duration-200">
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <!-- Main Content -->
132
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 transition-colors duration-200">
133
+ <!-- Loading -->
134
+ <div id="loading" class="flex justify-center items-center py-12">
135
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
136
+ <span class="ml-3 text-gray-600 dark:text-gray-400 transition-colors duration-200">Loading commits...</span>
137
+ </div>
138
+
139
+ <!-- Graph View -->
140
+ <div id="graphView" class="hidden">
141
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 transition-colors duration-200">
142
+ <svg id="commitGraph" width="100%" height="600"></svg>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- List View -->
147
+ <div id="listView" class="space-y-4">
148
+ <div id="commitsList" class="space-y-2"></div>
149
+ </div>
150
+
151
+ <!-- Commit Detail Modal -->
152
+ <div id="commitModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
153
+ <div class="flex items-center justify-center min-h-screen p-4">
154
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-screen overflow-y-auto transition-colors duration-200">
155
+ <div class="flex justify-between items-center p-6 border-b border-gray-200 dark:border-gray-700">
156
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white transition-colors duration-200">Commit Details</h3>
157
+ <button id="closeModal" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors duration-200">
158
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
159
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
160
+ </svg>
161
+ </button>
162
+ </div>
163
+ <div id="commitDetails" class="p-6"></div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </main>
168
+ </div>
169
+
170
+ <script src="app.js"></script>
171
+ </body>
172
+ </html>
@@ -0,0 +1,35 @@
1
+ import { GitService } from '../backend/gitService';
2
+
3
+ describe('GitService', () => {
4
+ let gitService: GitService;
5
+
6
+ beforeEach(() => {
7
+ gitService = new GitService();
8
+ });
9
+
10
+ describe('getCommits', () => {
11
+ it('should return an array of commits', async () => {
12
+ const commits = await gitService.getCommits({ limit: 5 });
13
+ expect(Array.isArray(commits)).toBe(true);
14
+ });
15
+
16
+ it('should respect the limit parameter', async () => {
17
+ const commits = await gitService.getCommits({ limit: 3 });
18
+ expect(commits.length).toBeLessThanOrEqual(3);
19
+ });
20
+ });
21
+
22
+ describe('getTags', () => {
23
+ it('should return an array of tags', async () => {
24
+ const tags = await gitService.getTags();
25
+ expect(Array.isArray(tags)).toBe(true);
26
+ });
27
+ });
28
+
29
+ describe('getBranches', () => {
30
+ it('should return an array of branches', async () => {
31
+ const branches = await gitService.getBranches();
32
+ expect(Array.isArray(branches)).toBe(true);
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,14 @@
1
+ import { startServer } from './server';
2
+
3
+ async function main() {
4
+ try {
5
+ console.log('šŸš€ Starting Git History UI Development Server...');
6
+ await startServer(3000, 'localhost');
7
+ console.log('āœ… Development server running at http://localhost:3000');
8
+ } catch (error) {
9
+ console.error('āŒ Error starting development server:', error);
10
+ process.exit(1);
11
+ }
12
+ }
13
+
14
+ main();
@@ -0,0 +1,277 @@
1
+ import simpleGit, { SimpleGit, LogResult, DefaultLogFields } from 'simple-git';
2
+ import path from 'path';
3
+
4
+ export interface Commit {
5
+ hash: string;
6
+ author: string;
7
+ date: string;
8
+ message: string;
9
+ files: string[];
10
+ parents: string[];
11
+ branches: string[];
12
+ tags: string[];
13
+ }
14
+
15
+ export interface DiffFile {
16
+ file: string;
17
+ additions: number;
18
+ deletions: number;
19
+ changes: string;
20
+ }
21
+
22
+ export interface BlameLine {
23
+ line: number;
24
+ hash: string;
25
+ author: string;
26
+ date: string;
27
+ content: string;
28
+ }
29
+
30
+ export interface GitOptions {
31
+ file?: string;
32
+ since?: string;
33
+ author?: string;
34
+ limit?: number;
35
+ }
36
+
37
+ export class GitService {
38
+ private git: SimpleGit;
39
+
40
+ constructor() {
41
+ this.git = simpleGit();
42
+ }
43
+
44
+ async getCommits(options: GitOptions = {}): Promise<Commit[]> {
45
+ try {
46
+ const logOptions = {
47
+ maxCount: options.limit || 100
48
+ };
49
+
50
+ let log: LogResult<DefaultLogFields>;
51
+
52
+ if (options.file) {
53
+ log = await this.git.log({
54
+ ...logOptions,
55
+ file: options.file
56
+ });
57
+ } else if (options.since) {
58
+ log = await this.git.log({
59
+ ...logOptions,
60
+ from: options.since
61
+ });
62
+ } else if (options.author) {
63
+ log = await this.git.log({
64
+ ...logOptions,
65
+ author: options.author
66
+ });
67
+ } else {
68
+ log = await this.git.log(logOptions);
69
+ }
70
+
71
+ const commits: Commit[] = await Promise.all(
72
+ log.all.map(async (commit) => {
73
+ const [branches, tags] = await Promise.all([
74
+ this.getBranchesForCommit(commit.hash),
75
+ this.getTagsForCommit(commit.hash)
76
+ ]);
77
+
78
+ return {
79
+ hash: commit.hash,
80
+ author: commit.author_name,
81
+ date: commit.date,
82
+ message: commit.message,
83
+ files: await this.getFilesForCommit(commit.hash),
84
+ parents: [], // We'll get this from git show if needed
85
+ branches,
86
+ tags
87
+ };
88
+ })
89
+ );
90
+
91
+ return commits;
92
+ } catch (error) {
93
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
94
+ throw new Error(`Failed to get commits: ${errorMessage}`);
95
+ }
96
+ }
97
+
98
+ async getCommit(hash: string): Promise<Commit> {
99
+ try {
100
+ const log = await this.git.log({
101
+ from: hash,
102
+ to: hash,
103
+ maxCount: 1
104
+ });
105
+
106
+ if (log.all.length === 0) {
107
+ throw new Error('Commit not found');
108
+ }
109
+
110
+ const commit = log.all[0];
111
+ const [branches, tags] = await Promise.all([
112
+ this.getBranchesForCommit(hash),
113
+ this.getTagsForCommit(hash)
114
+ ]);
115
+
116
+ return {
117
+ hash: commit.hash,
118
+ author: commit.author_name,
119
+ date: commit.date,
120
+ message: commit.message,
121
+ files: await this.getFilesForCommit(hash),
122
+ parents: [], // We'll get this from git show if needed
123
+ branches,
124
+ tags
125
+ };
126
+ } catch (error) {
127
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
128
+ throw new Error(`Failed to get commit: ${errorMessage}`);
129
+ }
130
+ }
131
+
132
+ async getDiff(hash: string): Promise<DiffFile[]> {
133
+ try {
134
+ const diff = await this.git.diff([hash + '^', hash]);
135
+ return this.parseDiff(diff);
136
+ } catch (error) {
137
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
138
+ throw new Error(`Failed to get diff: ${errorMessage}`);
139
+ }
140
+ }
141
+
142
+ async getBlame(filePath: string): Promise<BlameLine[]> {
143
+ try {
144
+ const blame = await this.git.raw(['blame', '--porcelain', filePath]);
145
+ return this.parseBlame(blame);
146
+ } catch (error) {
147
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
148
+ throw new Error(`Failed to get blame: ${errorMessage}`);
149
+ }
150
+ }
151
+
152
+ async getTags(): Promise<string[]> {
153
+ try {
154
+ const tags = await this.git.tags();
155
+ return tags.all;
156
+ } catch (error) {
157
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
158
+ throw new Error(`Failed to get tags: ${errorMessage}`);
159
+ }
160
+ }
161
+
162
+ async getBranches(): Promise<string[]> {
163
+ try {
164
+ const branches = await this.git.branch();
165
+ return branches.all;
166
+ } catch (error) {
167
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
168
+ throw new Error(`Failed to get branches: ${errorMessage}`);
169
+ }
170
+ }
171
+
172
+ private async getFilesForCommit(hash: string): Promise<string[]> {
173
+ try {
174
+ const diff = await this.git.diff([hash + '^', hash, '--name-only']);
175
+ return diff.split('\n').filter(Boolean);
176
+ } catch (error) {
177
+ return [];
178
+ }
179
+ }
180
+
181
+ private async getBranchesForCommit(hash: string): Promise<string[]> {
182
+ try {
183
+ const branches = await this.git.branch(['--contains', hash]);
184
+ return branches.all;
185
+ } catch (error) {
186
+ return [];
187
+ }
188
+ }
189
+
190
+ private async getTagsForCommit(hash: string): Promise<string[]> {
191
+ try {
192
+ const tags = await this.git.raw(['tag', '--contains', hash]);
193
+ return tags.split('\n').filter(Boolean);
194
+ } catch (error) {
195
+ return [];
196
+ }
197
+ }
198
+
199
+ private parseDiff(diff: string): DiffFile[] {
200
+ const files: DiffFile[] = [];
201
+ const lines = diff.split('\n');
202
+ let currentFile: DiffFile | null = null;
203
+ let currentFileLines: string[] = [];
204
+
205
+ for (const line of lines) {
206
+ if (line.startsWith('diff --git')) {
207
+ if (currentFile) {
208
+ currentFile.changes = currentFileLines.join('\n');
209
+ files.push(currentFile);
210
+ }
211
+ const fileMatch = line.match(/b\/(.+)$/);
212
+ currentFile = {
213
+ file: fileMatch ? fileMatch[1] : '',
214
+ additions: 0,
215
+ deletions: 0,
216
+ changes: ''
217
+ };
218
+ currentFileLines = [];
219
+ } else if (line.startsWith('+') && !line.startsWith('+++')) {
220
+ if (currentFile) currentFile.additions++;
221
+ currentFileLines.push(line);
222
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
223
+ if (currentFile) currentFile.deletions++;
224
+ currentFileLines.push(line);
225
+ } else if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++') || line.trim() === '') {
226
+ // Include git diff headers and context lines
227
+ currentFileLines.push(line);
228
+ } else if (line.startsWith(' ')) {
229
+ // Context lines (unchanged)
230
+ currentFileLines.push(line);
231
+ }
232
+ }
233
+
234
+ if (currentFile) {
235
+ currentFile.changes = currentFileLines.join('\n');
236
+ files.push(currentFile);
237
+ }
238
+
239
+ return files;
240
+ }
241
+
242
+ private parseBlame(blame: string): BlameLine[] {
243
+ const lines: BlameLine[] = [];
244
+ const blameLines = blame.split('\n');
245
+ let currentLine: BlameLine | null = null;
246
+
247
+ for (const line of blameLines) {
248
+ if (line.startsWith('author ')) {
249
+ if (currentLine) {
250
+ lines.push(currentLine);
251
+ }
252
+ const hash = blameLines[blameLines.indexOf(line) - 1].split(' ')[0];
253
+ const author = line.substring(7);
254
+ const dateLine = blameLines[blameLines.indexOf(line) + 1];
255
+ const date = dateLine.startsWith('author-time ')
256
+ ? new Date(parseInt(dateLine.substring(12)) * 1000).toISOString()
257
+ : '';
258
+
259
+ currentLine = {
260
+ line: lines.length + 1,
261
+ hash,
262
+ author,
263
+ date,
264
+ content: ''
265
+ };
266
+ } else if (line.startsWith('\t') && currentLine) {
267
+ currentLine.content = line.substring(1);
268
+ }
269
+ }
270
+
271
+ if (currentLine) {
272
+ lines.push(currentLine);
273
+ }
274
+
275
+ return lines;
276
+ }
277
+ }
@@ -0,0 +1,132 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import { createServer } from 'http';
4
+ import { Server } from 'socket.io';
5
+ import path from 'path';
6
+ import { GitService } from './gitService';
7
+
8
+ export interface ServerOptions {
9
+ port?: number;
10
+ host?: string;
11
+ file?: string;
12
+ since?: string;
13
+ author?: string;
14
+ }
15
+
16
+ export async function startServer(
17
+ port: number = 3000,
18
+ host: string = 'localhost',
19
+ options: Partial<ServerOptions> = {}
20
+ ) {
21
+ const app = express();
22
+ const server = createServer(app);
23
+ const io = new Server(server, {
24
+ cors: {
25
+ origin: "*",
26
+ methods: ["GET", "POST"]
27
+ }
28
+ });
29
+
30
+ const gitService = new GitService();
31
+
32
+ // Middleware
33
+ app.use(cors());
34
+ app.use(express.json());
35
+
36
+ // Serve Angular build files
37
+ app.use(express.static(path.join(__dirname, '../../frontend/dist/frontend/browser')));
38
+
39
+ // Serve public folder as fallback
40
+ app.use(express.static(path.join(__dirname, '../../public')));
41
+
42
+ // API Routes
43
+ app.get('/api/commits', async (req, res) => {
44
+ try {
45
+ const { file, since, author, limit = '100' } = req.query;
46
+ const commits = await gitService.getCommits({
47
+ file: file as string,
48
+ since: since as string,
49
+ author: author as string,
50
+ limit: parseInt(limit as string)
51
+ });
52
+ res.json(commits);
53
+ } catch (error) {
54
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
55
+ res.status(500).json({ error: errorMessage });
56
+ }
57
+ });
58
+
59
+ app.get('/api/commit/:hash', async (req, res) => {
60
+ try {
61
+ const { hash } = req.params;
62
+ const commit = await gitService.getCommit(hash);
63
+ res.json(commit);
64
+ } catch (error) {
65
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
66
+ res.status(500).json({ error: errorMessage });
67
+ }
68
+ });
69
+
70
+ app.get('/api/diff/:hash', async (req, res) => {
71
+ try {
72
+ const { hash } = req.params;
73
+ const diff = await gitService.getDiff(hash);
74
+ res.json(diff);
75
+ } catch (error) {
76
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
77
+ res.status(500).json({ error: errorMessage });
78
+ }
79
+ });
80
+
81
+ app.get('/api/blame/:file', async (req, res) => {
82
+ try {
83
+ const { file } = req.params;
84
+ const blame = await gitService.getBlame(file);
85
+ res.json(blame);
86
+ } catch (error) {
87
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
88
+ res.status(500).json({ error: errorMessage });
89
+ }
90
+ });
91
+
92
+ app.get('/api/tags', async (req, res) => {
93
+ try {
94
+ const tags = await gitService.getTags();
95
+ res.json(tags);
96
+ } catch (error) {
97
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
98
+ res.status(500).json({ error: errorMessage });
99
+ }
100
+ });
101
+
102
+ app.get('/api/branches', async (req, res) => {
103
+ try {
104
+ const branches = await gitService.getBranches();
105
+ res.json(branches);
106
+ } catch (error) {
107
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
108
+ res.status(500).json({ error: errorMessage });
109
+ }
110
+ });
111
+
112
+ // Serve the main HTML file for Angular routing
113
+ app.get('*', (req, res) => {
114
+ res.sendFile(path.join(__dirname, '../../frontend/dist/frontend/browser/index.html'));
115
+ });
116
+
117
+ // Socket.IO for real-time updates
118
+ io.on('connection', (socket) => {
119
+ console.log('Client connected');
120
+
121
+ socket.on('disconnect', () => {
122
+ console.log('Client disconnected');
123
+ });
124
+ });
125
+
126
+ return new Promise<void>((resolve) => {
127
+ server.listen(port, host, () => {
128
+ console.log(`Server running on http://${host}:${port}`);
129
+ resolve();
130
+ });
131
+ });
132
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import open from 'open';
6
+ import { startServer } from './backend/server';
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('git-history-ui')
12
+ .description('Beautiful git history visualization in your browser')
13
+ .version('1.0.0');
14
+
15
+ program
16
+ .option('-p, --port <number>', 'port to run server on', '3000')
17
+ .option('-f, --file <path>', 'show history only for a specific file')
18
+ .option('-s, --since <ref>', 'show commits since a specific reference (e.g., v2.0.0)')
19
+ .option('-a, --author <name>', 'filter commits by author')
20
+ .option('--no-open', 'do not automatically open browser')
21
+ .option('--host <host>', 'host to bind to', 'localhost');
22
+
23
+ program.parse();
24
+
25
+ const options = program.opts();
26
+
27
+ async function main() {
28
+ try {
29
+ console.log(chalk.blue('šŸš€ Starting Git History UI...'));
30
+
31
+ const port = parseInt(options.port);
32
+ const serverUrl = `http://${options.host}:${port}`;
33
+
34
+ // Start the server
35
+ await startServer(port, options.host);
36
+
37
+ console.log(chalk.green(`āœ… Server running at ${serverUrl}`));
38
+
39
+ if (options.open !== false) {
40
+ console.log(chalk.yellow('🌐 Opening browser...'));
41
+ await open(serverUrl);
42
+ }
43
+
44
+ console.log(chalk.cyan('\nšŸ“ Usage:'));
45
+ console.log(chalk.white(' • Use the search bar to filter commits'));
46
+ console.log(chalk.white(' • Click on commits to view diffs'));
47
+ console.log(chalk.white(' • Use the graph view to see branch structure'));
48
+ console.log(chalk.white(' • Press Ctrl+C to stop the server'));
49
+
50
+ } catch (error) {
51
+ console.error(chalk.red('āŒ Error starting server:'), error);
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ main();