mc-gitpulse 1.0.1 → 1.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 CHANGED
@@ -48,6 +48,11 @@
48
48
  ## Demo
49
49
  ![GitPulse Demo on ollama repo](https://raw.githubusercontent.com/mchinnappan100/npmjs-images/main/gitpluse/gitpulse-demo-1.gif)
50
50
 
51
+ ## Screenshot
52
+
53
+ ![Git Diff on postgres repo](https://raw.githubusercontent.com/mchinnappan100/npmjs-images/main/gitpluse/gitpulse-demo-2.png)
54
+
55
+
51
56
  ## 📦 Installation
52
57
 
53
58
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mc-gitpulse",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "The ultimate Git UI - CLI + Web interface with advanced repository insights",
5
5
  "main": "index.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -3,12 +3,11 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>GitPulse - Ultimate Git UI</title>
6
+ <title>GitPulse - Ultimate Git UI</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <link rel="icon" type="image/x-icon"
9
9
  href="https://mohan-chinnappan-n5.github.io/dfv/img/mc_favIcon.ico" />
10
-
11
-
10
+
12
11
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
13
12
  <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
14
13
  <style>
@@ -61,16 +60,25 @@
61
60
  transform: translateY(-2px);
62
61
  }
63
62
 
64
- .split-pane {
63
+ .file-tree-container {
65
64
  display: flex;
66
- height: 600px;
67
- gap: 1rem;
65
+ height: 650px;
66
+ gap: 0;
67
+ position: relative;
68
68
  }
69
69
 
70
70
  .split-left {
71
- flex: 0 0 400px;
71
+ flex: 0 0 350px;
72
72
  overflow-y: auto;
73
- border-right: 1px solid #374151;
73
+ min-width: 200px;
74
+ max-width: 600px;
75
+ }
76
+
77
+ .file-history-sidebar {
78
+ flex: 0 0 280px;
79
+ overflow-y: auto;
80
+ min-width: 200px;
81
+ max-width: 500px;
74
82
  }
75
83
 
76
84
  .split-right {
@@ -78,15 +86,85 @@
78
86
  overflow: hidden;
79
87
  display: flex;
80
88
  flex-direction: column;
89
+ min-width: 400px;
81
90
  }
82
91
 
83
- #monaco-editor {
92
+ .splitter {
93
+ width: 4px;
94
+ background: #374151;
95
+ cursor: col-resize;
96
+ position: relative;
97
+ transition: background 0.2s;
98
+ flex-shrink: 0;
99
+ }
100
+
101
+ .splitter:hover {
102
+ background: #3b82f6;
103
+ }
104
+
105
+ .splitter:active {
106
+ background: #2563eb;
107
+ }
108
+
109
+ .splitter::before {
110
+ content: '';
111
+ position: absolute;
112
+ top: 50%;
113
+ left: 50%;
114
+ transform: translate(-50%, -50%);
115
+ width: 20px;
116
+ height: 40px;
117
+ background: transparent;
118
+ }
119
+
120
+ #monaco-editor-container {
121
+ flex: 1;
122
+ position: relative;
123
+ }
124
+
125
+ #monaco-editor, #monaco-diff-editor {
84
126
  height: 100%;
85
127
  border: 1px solid #374151;
86
128
  border-radius: 0.5rem;
87
129
  overflow: hidden;
88
130
  }
89
131
 
132
+ #monaco-diff-editor {
133
+ position: absolute;
134
+ top: 0;
135
+ left: 0;
136
+ right: 0;
137
+ bottom: 0;
138
+ }
139
+
140
+ .commit-item {
141
+ padding: 0.5rem;
142
+ border-radius: 0.375rem;
143
+ border: 1px solid #374151;
144
+ cursor: pointer;
145
+ transition: all 0.2s;
146
+ }
147
+
148
+ .commit-item:hover {
149
+ background-color: rgba(59, 130, 246, 0.1);
150
+ border-color: #3b82f6;
151
+ }
152
+
153
+ .commit-item.selected {
154
+ background-color: rgba(59, 130, 246, 0.2);
155
+ border-color: #3b82f6;
156
+ }
157
+
158
+ .commit-item.selected-1 {
159
+ background-color: rgba(239, 68, 68, 0.2);
160
+ border-color: #ef4444;
161
+ }
162
+
163
+ .commit-item.selected-2 {
164
+ background-color: rgba(16, 185, 129, 0.2);
165
+ border-color: #10b981;
166
+ }
167
+
90
168
  .file-item {
91
169
  cursor: pointer;
92
170
  padding: 0.5rem;
@@ -351,7 +429,7 @@
351
429
  <div class="container mx-auto px-6 py-4">
352
430
  <div class="flex items-center justify-between">
353
431
  <div class="flex items-center space-x-4">
354
- <h1 class="text-2xl font-bold text-blue-400">⚡ GitPulse</h1>
432
+ <h1 class="text-2xl font-bold text-blue-400">⚡ GitPulse </h1>
355
433
  <span class="text-sm text-gray-400 hidden sm:inline">The Ultimate Git Repository Analyzer</span>
356
434
  </div>
357
435
  <div class="flex items-center space-x-4">
@@ -509,14 +587,44 @@
509
587
  <!-- File Tree Tab -->
510
588
  <div id="tree-tab" class="tab-content">
511
589
  <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
512
- <h3 class="text-xl font-bold mb-4">📂 File Tree</h3>
513
- <div class="split-pane">
590
+ <div class="flex items-center justify-between mb-4">
591
+ <h3 class="text-xl font-bold">📂 File Tree</h3>
592
+ <div class="flex items-center space-x-4">
593
+ <button id="view-mode-btn" onclick="toggleViewMode()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded transition text-sm">
594
+ 📊 View Mode: <span id="view-mode-text">Single</span>
595
+ </button>
596
+ <button id="clear-selection-btn" onclick="clearCommitSelection()" class="hidden px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded transition text-sm">
597
+ Clear Selection
598
+ </button>
599
+ </div>
600
+ </div>
601
+ <div class="file-tree-container">
514
602
  <div class="split-left bg-gray-900 rounded p-4">
515
603
  <div id="file-tree" class="font-mono text-sm"></div>
516
604
  </div>
605
+
606
+ <div class="splitter" id="splitter1"></div>
607
+
608
+ <!-- File History Sidebar -->
609
+ <div id="file-history-sidebar" class="file-history-sidebar bg-gray-900 rounded p-4">
610
+ <h4 class="text-sm font-semibold mb-3 text-blue-400">File History</h4>
611
+ <div id="file-history-list" class="space-y-2 text-sm">
612
+ <p class="text-gray-500 text-xs">Select a file to see its commit history</p>
613
+ </div>
614
+ </div>
615
+
616
+ <div class="splitter" id="splitter2"></div>
617
+
618
+ <!-- Editor Area -->
517
619
  <div class="split-right">
518
- <div id="file-info" class="mb-2 text-sm text-gray-400">Select a file to view</div>
519
- <div id="monaco-editor"></div>
620
+ <div id="file-info" class="mb-2 text-sm text-gray-400 flex items-center justify-between">
621
+ <span>Select a file to view</span>
622
+ <span id="diff-info" class="text-xs text-blue-400"></span>
623
+ </div>
624
+ <div id="monaco-editor-container">
625
+ <div id="monaco-editor"></div>
626
+ <div id="monaco-diff-editor" class="hidden"></div>
627
+ </div>
520
628
  </div>
521
629
  </div>
522
630
  </div>
@@ -580,8 +688,12 @@
580
688
  let ws;
581
689
  let currentData = null;
582
690
  let monacoEditor = null;
691
+ let monacoDiffEditor = null;
583
692
  let allFiles = [];
584
693
  let modalCallback = null;
694
+ let currentFilePath = null;
695
+ let selectedCommits = [];
696
+ let viewMode = 'single'; // 'single' or 'diff'
585
697
 
586
698
  // Modal Functions
587
699
  function showModal(options) {
@@ -712,12 +824,69 @@
712
824
  loadData();
713
825
  setupTabs();
714
826
  initMonaco();
827
+ initSplitters();
715
828
  });
716
829
 
830
+ function initSplitters() {
831
+ const splitter1 = document.getElementById('splitter1');
832
+ const splitter2 = document.getElementById('splitter2');
833
+ const leftPane = document.querySelector('.split-left');
834
+ const middlePane = document.querySelector('.file-history-sidebar');
835
+
836
+ let isResizing = false;
837
+ let currentSplitter = null;
838
+
839
+ function startResize(splitter, e) {
840
+ isResizing = true;
841
+ currentSplitter = splitter;
842
+ document.body.style.cursor = 'col-resize';
843
+ document.body.style.userSelect = 'none';
844
+ }
845
+
846
+ function stopResize() {
847
+ isResizing = false;
848
+ currentSplitter = null;
849
+ document.body.style.cursor = '';
850
+ document.body.style.userSelect = '';
851
+ }
852
+
853
+ function resize(e) {
854
+ if (!isResizing || !currentSplitter) return;
855
+
856
+ const container = document.querySelector('.file-tree-container');
857
+ const containerRect = container.getBoundingClientRect();
858
+ const offsetX = e.clientX - containerRect.left;
859
+
860
+ if (currentSplitter === splitter1) {
861
+ // Resizing left pane
862
+ const newWidth = Math.max(200, Math.min(600, offsetX));
863
+ leftPane.style.flexBasis = newWidth + 'px';
864
+ } else if (currentSplitter === splitter2) {
865
+ // Resizing middle pane
866
+ const leftWidth = leftPane.getBoundingClientRect().width;
867
+ const splitter1Width = splitter1.getBoundingClientRect().width;
868
+ const newWidth = Math.max(200, Math.min(500, offsetX - leftWidth - splitter1Width));
869
+ middlePane.style.flexBasis = newWidth + 'px';
870
+ }
871
+ }
872
+
873
+ if (splitter1) {
874
+ splitter1.addEventListener('mousedown', (e) => startResize(splitter1, e));
875
+ }
876
+
877
+ if (splitter2) {
878
+ splitter2.addEventListener('mousedown', (e) => startResize(splitter2, e));
879
+ }
880
+
881
+ document.addEventListener('mousemove', resize);
882
+ document.addEventListener('mouseup', stopResize);
883
+ }
884
+
717
885
  function initMonaco() {
718
886
  require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
719
887
 
720
888
  require(['vs/editor/editor.main'], function() {
889
+ // Single file editor
721
890
  monacoEditor = monaco.editor.create(document.getElementById('monaco-editor'), {
722
891
  value: '// Select a file from the tree to view its contents',
723
892
  language: 'javascript',
@@ -730,12 +899,27 @@
730
899
  scrollBeyondLastLine: false,
731
900
  wordWrap: 'on'
732
901
  });
902
+
903
+ // Diff editor
904
+ monacoDiffEditor = monaco.editor.createDiffEditor(document.getElementById('monaco-diff-editor'), {
905
+ theme: 'vs-dark',
906
+ automaticLayout: true,
907
+ readOnly: true,
908
+ renderSideBySide: true,
909
+ minimap: { enabled: true },
910
+ fontSize: 14,
911
+ lineNumbers: 'on',
912
+ scrollBeyondLastLine: false,
913
+ wordWrap: 'on'
914
+ });
733
915
  });
734
916
  }
735
917
 
736
918
  async function loadFileContent(filepath) {
737
919
  try {
738
- showToast(`Loading ${filepath.split('/').pop()}...`, 'info', 1500);
920
+ currentFilePath = filepath;
921
+ selectedCommits = [];
922
+ updateClearSelectionButton();
739
923
 
740
924
  const ref = currentData.branches.current;
741
925
  const response = await fetch(`/api/file/${ref}/${filepath}`);
@@ -744,18 +928,7 @@
744
928
  if (data.content) {
745
929
  // Detect language from file extension
746
930
  const ext = filepath.split('.').pop().toLowerCase();
747
- const languageMap = {
748
- js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
749
- py: 'python', java: 'java', cpp: 'cpp', c: 'c', cs: 'csharp',
750
- html: 'html', css: 'css', scss: 'scss', sass: 'sass',
751
- json: 'json', xml: 'xml', yaml: 'yaml', yml: 'yaml',
752
- md: 'markdown', sh: 'shell', bash: 'shell',
753
- sql: 'sql', php: 'php', rb: 'ruby', go: 'go',
754
- rs: 'rust', swift: 'swift', kt: 'kotlin',
755
- vue: 'html', svelte: 'html'
756
- };
757
-
758
- const language = languageMap[ext] || 'plaintext';
931
+ const language = getLanguageFromExtension(ext);
759
932
 
760
933
  monaco.editor.setModelLanguage(monacoEditor.getModel(), language);
761
934
  monacoEditor.setValue(data.content);
@@ -767,17 +940,272 @@
767
940
  </div>
768
941
  `;
769
942
 
770
- showToast('File loaded successfully', 'success', 2000);
943
+ const filename = filepath.split('/').pop();
944
+ showToast(`✓ ${filename}`, 'success', 2000);
945
+
946
+ // Load file history
947
+ loadFileHistory(filepath);
771
948
  }
772
949
  } catch (error) {
773
950
  monacoEditor.setValue(`// Error loading file: ${error.message}`);
774
951
  document.getElementById('file-info').innerHTML = `
775
952
  <span class="text-red-400">❌ Error loading ${escapeHtml(filepath)}</span>
776
953
  `;
777
- showToast(`Failed to load ${filepath.split('/').pop()}`, 'error', 3000);
954
+ const filename = filepath.split('/').pop();
955
+ showToast(`Failed: ${filename}`, 'error', 3000);
956
+ }
957
+ }
958
+
959
+ async function loadFileHistory(filepath) {
960
+ try {
961
+ const response = await fetch(`/api/file-history/${encodeURIComponent(filepath)}?limit=1000`);
962
+ const data = await response.json();
963
+
964
+ const historyList = document.getElementById('file-history-list');
965
+
966
+ if (data.commits.length === 0) {
967
+ historyList.innerHTML = '<p class="text-gray-500 text-xs">No commit history available</p>';
968
+ return;
969
+ }
970
+
971
+ historyList.innerHTML = `
972
+ <div class="text-xs text-gray-400 mb-2">Showing ${data.commits.length} commits</div>
973
+ ${data.commits.map((commit, index) => `
974
+ <div class="commit-item" data-commit="${commit.hash}" onclick="selectCommit('${commit.hash}', ${index})">
975
+ <div class="flex items-center justify-between mb-1">
976
+ <span class="font-mono text-xs text-yellow-400">${commit.shortHash}</span>
977
+ <span class="text-xs text-gray-500">${commit.relativeDate}</span>
978
+ </div>
979
+ <div class="text-xs text-gray-300 mb-1 truncate" title="${escapeHtml(commit.message)}">
980
+ ${escapeHtml(commit.message)}
981
+ </div>
982
+ <div class="text-xs text-gray-500 truncate">
983
+ ${escapeHtml(commit.author)}
984
+ </div>
985
+ </div>
986
+ `).join('')}
987
+ `;
988
+
989
+ } catch (error) {
990
+ console.error('Error loading file history:', error);
991
+ document.getElementById('file-history-list').innerHTML =
992
+ '<p class="text-red-400 text-xs">Failed to load history</p>';
778
993
  }
779
994
  }
780
995
 
996
+ function selectCommit(commitHash, index) {
997
+ if (viewMode === 'single') {
998
+ // In single mode, just view that commit
999
+ viewCommitVersion(commitHash);
1000
+ } else {
1001
+ // In diff mode, select up to 2 commits
1002
+ const commitItem = document.querySelector(`[data-commit="${commitHash}"]`);
1003
+
1004
+ if (selectedCommits.includes(commitHash)) {
1005
+ // Deselect
1006
+ selectedCommits = selectedCommits.filter(c => c !== commitHash);
1007
+ commitItem.classList.remove('selected-1', 'selected-2');
1008
+ } else {
1009
+ if (selectedCommits.length >= 2) {
1010
+ // Remove oldest selection
1011
+ const oldCommit = selectedCommits.shift();
1012
+ const oldItem = document.querySelector(`[data-commit="${oldCommit}"]`);
1013
+ if (oldItem) oldItem.classList.remove('selected-1', 'selected-2');
1014
+ }
1015
+ selectedCommits.push(commitHash);
1016
+ }
1017
+
1018
+ // Update visual selection
1019
+ document.querySelectorAll('.commit-item').forEach(item => {
1020
+ item.classList.remove('selected-1', 'selected-2');
1021
+ });
1022
+
1023
+ if (selectedCommits.length > 0) {
1024
+ const item1 = document.querySelector(`[data-commit="${selectedCommits[0]}"]`);
1025
+ if (item1) item1.classList.add('selected-1');
1026
+ }
1027
+
1028
+ if (selectedCommits.length > 1) {
1029
+ const item2 = document.querySelector(`[data-commit="${selectedCommits[1]}"]`);
1030
+ if (item2) item2.classList.add('selected-2');
1031
+
1032
+ // Show diff
1033
+ viewDiff(selectedCommits[0], selectedCommits[1]);
1034
+ }
1035
+
1036
+ updateClearSelectionButton();
1037
+ }
1038
+ }
1039
+
1040
+ async function viewCommitVersion(commitHash) {
1041
+ try {
1042
+ const response = await fetch(`/api/file/${commitHash}/${currentFilePath}`);
1043
+ const data = await response.json();
1044
+
1045
+ if (data.content) {
1046
+ const ext = currentFilePath.split('.').pop().toLowerCase();
1047
+ const language = getLanguageFromExtension(ext);
1048
+
1049
+ monaco.editor.setModelLanguage(monacoEditor.getModel(), language);
1050
+ monacoEditor.setValue(data.content);
1051
+
1052
+ const shortHash = commitHash.substring(0, 7);
1053
+ document.getElementById('file-info').innerHTML = `
1054
+ <div class="flex items-center justify-between">
1055
+ <span class="text-blue-400">📄 ${escapeHtml(currentFilePath)} @ ${shortHash}</span>
1056
+ <span class="text-gray-500">${language}</span>
1057
+ </div>
1058
+ `;
1059
+
1060
+ showToast(`Viewing @ ${shortHash}`, 'success', 2000);
1061
+ }
1062
+ } catch (error) {
1063
+ showToast('Failed to load commit version', 'error', 3000);
1064
+ }
1065
+ }
1066
+
1067
+ async function viewDiff(commit1, commit2) {
1068
+ try {
1069
+ // Check if Monaco is ready
1070
+ if (!monacoDiffEditor) {
1071
+ throw new Error('Monaco Diff Editor not initialized yet. Please wait a moment and try again.');
1072
+ }
1073
+
1074
+ const response = await fetch(
1075
+ `/api/file-diff/${encodeURIComponent(currentFilePath)}?commit1=${commit1}&commit2=${commit2}`
1076
+ );
1077
+
1078
+ if (!response.ok) {
1079
+ throw new Error(`HTTP error! status: ${response.status}`);
1080
+ }
1081
+
1082
+ const data = await response.json();
1083
+
1084
+ if (data.error) {
1085
+ throw new Error(data.error);
1086
+ }
1087
+
1088
+ const ext = currentFilePath.split('.').pop().toLowerCase();
1089
+ const language = getLanguageFromExtension(ext);
1090
+
1091
+ // Ensure monaco.editor is available
1092
+ if (typeof monaco === 'undefined' || !monaco.editor) {
1093
+ throw new Error('Monaco Editor not loaded yet. Please refresh and try again.');
1094
+ }
1095
+
1096
+ const originalModel = monaco.editor.createModel(data.content1, language);
1097
+ const modifiedModel = monaco.editor.createModel(data.content2, language);
1098
+
1099
+ monacoDiffEditor.setModel({
1100
+ original: originalModel,
1101
+ modified: modifiedModel
1102
+ });
1103
+
1104
+ // Switch to diff view
1105
+ const monacoEditorEl = document.getElementById('monaco-editor');
1106
+ const monacoDiffEditorEl = document.getElementById('monaco-diff-editor');
1107
+ const fileInfoEl = document.getElementById('file-info');
1108
+ const diffInfoEl = document.getElementById('diff-info');
1109
+
1110
+ if (monacoEditorEl) monacoEditorEl.classList.add('hidden');
1111
+ if (monacoDiffEditorEl) monacoDiffEditorEl.classList.remove('hidden');
1112
+
1113
+ const short1 = commit1.substring(0, 7);
1114
+ const short2 = commit2.substring(0, 7);
1115
+
1116
+ if (fileInfoEl) {
1117
+ fileInfoEl.innerHTML = `
1118
+ <div class="flex items-center justify-between">
1119
+ <span class="text-blue-400">📄 ${escapeHtml(currentFilePath)}</span>
1120
+ <span class="text-gray-500">${language}</span>
1121
+ </div>
1122
+ `;
1123
+ }
1124
+
1125
+ if (diffInfoEl) {
1126
+ diffInfoEl.innerHTML = `
1127
+ <span class="text-red-400">${short1}</span> ⟷ <span class="text-green-400">${short2}</span>
1128
+ `;
1129
+ }
1130
+
1131
+ // Only show success toast
1132
+ showToast(`Comparing ${short1} ⟷ ${short2}`, 'success', 2500);
1133
+
1134
+ } catch (error) {
1135
+ console.error('Diff loading error:', error);
1136
+ showToast(`Diff error: ${error.message}`, 'error', 4000);
1137
+ }
1138
+ }
1139
+
1140
+ function toggleViewMode() {
1141
+ viewMode = viewMode === 'single' ? 'diff' : 'single';
1142
+ document.getElementById('view-mode-text').textContent =
1143
+ viewMode === 'single' ? 'Single' : 'Diff';
1144
+
1145
+ clearCommitSelection();
1146
+
1147
+ showToast(
1148
+ viewMode === 'single'
1149
+ ? 'Single file view mode'
1150
+ : 'Diff mode: Select 2 commits to compare',
1151
+ 'info',
1152
+ 3000
1153
+ );
1154
+ }
1155
+
1156
+ function clearCommitSelection() {
1157
+ selectedCommits = [];
1158
+ document.querySelectorAll('.commit-item').forEach(item => {
1159
+ item.classList.remove('selected-1', 'selected-2');
1160
+ });
1161
+
1162
+ // Switch back to single editor
1163
+ document.getElementById('monaco-editor').classList.remove('hidden');
1164
+ document.getElementById('monaco-diff-editor').classList.add('hidden');
1165
+ document.getElementById('diff-info').innerHTML = '';
1166
+
1167
+ updateClearSelectionButton();
1168
+
1169
+ // Reload current file
1170
+ if (currentFilePath) {
1171
+ const ref = currentData.branches.current;
1172
+ fetch(`/api/file/${ref}/${currentFilePath}`)
1173
+ .then(res => res.json())
1174
+ .then(data => {
1175
+ if (data.content) {
1176
+ const ext = currentFilePath.split('.').pop().toLowerCase();
1177
+ const language = getLanguageFromExtension(ext);
1178
+ monaco.editor.setModelLanguage(monacoEditor.getModel(), language);
1179
+ monacoEditor.setValue(data.content);
1180
+ }
1181
+ });
1182
+ }
1183
+ }
1184
+
1185
+ function updateClearSelectionButton() {
1186
+ const btn = document.getElementById('clear-selection-btn');
1187
+ if (selectedCommits.length > 0 || viewMode === 'diff') {
1188
+ btn.classList.remove('hidden');
1189
+ } else {
1190
+ btn.classList.add('hidden');
1191
+ }
1192
+ }
1193
+
1194
+ function getLanguageFromExtension(ext) {
1195
+ const languageMap = {
1196
+ js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
1197
+ py: 'python', java: 'java', cpp: 'cpp', c: 'c', cs: 'csharp',
1198
+ html: 'html', css: 'css', scss: 'scss', sass: 'sass',
1199
+ json: 'json', xml: 'xml', yaml: 'yaml', yml: 'yaml',
1200
+ md: 'markdown', sh: 'shell', bash: 'shell',
1201
+ sql: 'sql', php: 'php', rb: 'ruby', go: 'go',
1202
+ rs: 'rust', swift: 'swift', kt: 'kotlin',
1203
+ vue: 'html', svelte: 'html'
1204
+ };
1205
+ return languageMap[ext] || 'plaintext';
1206
+ }
1207
+
1208
+
781
1209
  function initWebSocket() {
782
1210
  ws = new WebSocket(`ws://${window.location.host}`);
783
1211
 
@@ -795,7 +1223,7 @@
795
1223
  if (data.type === 'update') {
796
1224
  currentData = data.data;
797
1225
  renderData();
798
- showToast('Repository data updated', 'success', 2000);
1226
+ showToast(' Data refreshed', 'success', 2000);
799
1227
  }
800
1228
  };
801
1229
  }
@@ -838,8 +1266,6 @@
838
1266
  }
839
1267
 
840
1268
  function refreshData() {
841
- showToast('Refreshing repository data...', 'info', 2000);
842
-
843
1269
  if (ws && ws.readyState === WebSocket.OPEN) {
844
1270
  ws.send(JSON.stringify({ type: 'refresh' }));
845
1271
  } else {
@@ -895,12 +1321,35 @@
895
1321
 
896
1322
  async function loadGitGraph() {
897
1323
  try {
1324
+ const graphElement = document.getElementById('git-graph');
1325
+ if (!graphElement) return;
1326
+
1327
+ // Show loading state
1328
+ graphElement.innerHTML = '<div class="text-gray-400 py-4">Loading git graph...</div>';
1329
+
898
1330
  const limit = document.getElementById('graph-limit')?.value || 100;
899
1331
  const response = await fetch(`/api/graph?limit=${limit}`);
1332
+
1333
+ if (!response.ok) {
1334
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1335
+ }
1336
+
900
1337
  const graph = await response.json();
901
1338
 
902
- const graphElement = document.getElementById('git-graph');
903
- if (!graphElement) return;
1339
+ if (!graph || graph.length === 0) {
1340
+ graphElement.innerHTML = `
1341
+ <div class="text-yellow-400 py-4">
1342
+ <p class="mb-2">⚠️ No commits found in git graph</p>
1343
+ <p class="text-sm text-gray-400">This could mean:</p>
1344
+ <ul class="text-sm text-gray-400 list-disc list-inside ml-4 mt-2">
1345
+ <li>The repository is empty</li>
1346
+ <li>There are no commits yet</li>
1347
+ <li>There's an issue with git log</li>
1348
+ </ul>
1349
+ </div>
1350
+ `;
1351
+ return;
1352
+ }
904
1353
 
905
1354
  graphElement.innerHTML = graph.map(commit => {
906
1355
  const graphPart = commit.graph.replace(/\*/g, '<span class="text-yellow-400">●</span>')
@@ -918,11 +1367,22 @@
918
1367
  </div>
919
1368
  `;
920
1369
  }).join('');
1370
+
1371
+ console.log(`Git graph loaded: ${graph.length} commits`);
1372
+
921
1373
  } catch (error) {
922
1374
  console.error('Error loading git graph:', error);
923
1375
  const graphElement = document.getElementById('git-graph');
924
1376
  if (graphElement) {
925
- graphElement.innerHTML = '<div class="text-red-400">Error loading git graph</div>';
1377
+ graphElement.innerHTML = `
1378
+ <div class="text-red-400 py-4">
1379
+ <p class="mb-2">❌ Error loading git graph</p>
1380
+ <p class="text-sm">${error.message}</p>
1381
+ <button onclick="loadGitGraph()" class="mt-3 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm">
1382
+ Try Again
1383
+ </button>
1384
+ </div>
1385
+ `;
926
1386
  }
927
1387
  }
928
1388
  }
@@ -1358,4 +1818,4 @@
1358
1818
  }
1359
1819
  </script>
1360
1820
  </body>
1361
- </html>
1821
+ </html>
package/server.js CHANGED
@@ -2,9 +2,29 @@ const express = require('express');
2
2
  const path = require('path');
3
3
  const http = require('http');
4
4
  const WebSocket = require('ws');
5
- const {openResource} = require('open-resource');
6
5
  const chalk = require('chalk');
7
6
  const { GitAnalyzer } = require('./analyzer');
7
+ const { openResource } = require('open-resource');
8
+
9
+ function getRelativeTime(date) {
10
+ const now = new Date();
11
+ const diff = now - date;
12
+ const seconds = Math.floor(diff / 1000);
13
+ const minutes = Math.floor(seconds / 60);
14
+ const hours = Math.floor(minutes / 60);
15
+ const days = Math.floor(hours / 24);
16
+ const weeks = Math.floor(days / 7);
17
+ const months = Math.floor(days / 30);
18
+ const years = Math.floor(days / 365);
19
+
20
+ if (seconds < 60) return 'just now';
21
+ if (minutes < 60) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
22
+ if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
23
+ if (days < 7) return `${days} day${days > 1 ? 's' : ''} ago`;
24
+ if (weeks < 4) return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
25
+ if (months < 12) return `${months} month${months > 1 ? 's' : ''} ago`;
26
+ return `${years} year${years > 1 ? 's' : ''} ago`;
27
+ }
8
28
 
9
29
  async function startServer(repoPath, port, openBrowser = true) {
10
30
  const app = express();
@@ -39,9 +59,19 @@ async function startServer(repoPath, port, openBrowser = true) {
39
59
  app.get('/api/graph', async (req, res) => {
40
60
  try {
41
61
  const limit = parseInt(req.query.limit) || 100;
62
+ console.log(`Fetching git graph with limit: ${limit}`);
63
+
42
64
  const graph = await analyzer.getCommitGraph(limit);
65
+
66
+ console.log(`Git graph returned ${graph.length} commits`);
67
+
68
+ if (graph.length === 0) {
69
+ console.warn('Warning: Git graph is empty');
70
+ }
71
+
43
72
  res.json(graph);
44
73
  } catch (error) {
74
+ console.error('Error in /api/graph:', error);
45
75
  res.status(500).json({ error: error.message });
46
76
  }
47
77
  });
@@ -57,6 +87,70 @@ async function startServer(repoPath, port, openBrowser = true) {
57
87
  }
58
88
  });
59
89
 
90
+ app.get('/api/file-history/:filepath(*)', async (req, res) => {
91
+ try {
92
+ const filepath = req.params.filepath;
93
+ const limit = parseInt(req.query.limit) || 1000;
94
+
95
+ // Get file history with commit details
96
+ const log = await analyzer.git.log({
97
+ file: filepath,
98
+ '--max-count': limit
99
+ });
100
+
101
+ const commits = log.all.map(commit => ({
102
+ hash: commit.hash,
103
+ shortHash: commit.hash.substring(0, 7),
104
+ message: commit.message,
105
+ author: commit.author_name,
106
+ date: commit.date,
107
+ relativeDate: getRelativeTime(new Date(commit.date))
108
+ }));
109
+
110
+ res.json({ commits, filepath });
111
+ } catch (error) {
112
+ res.status(500).json({ error: error.message });
113
+ }
114
+ });
115
+
116
+ app.get('/api/file-diff/:filepath(*)', async (req, res) => {
117
+ try {
118
+ const filepath = req.params.filepath;
119
+ const commit1 = req.query.commit1;
120
+ const commit2 = req.query.commit2;
121
+
122
+ if (!commit1 || !commit2) {
123
+ return res.status(400).json({ error: 'Both commit1 and commit2 are required' });
124
+ }
125
+
126
+ // Get file content at both commits
127
+ let content1 = '';
128
+ let content2 = '';
129
+
130
+ try {
131
+ content1 = await analyzer.git.show([`${commit1}:${filepath}`]);
132
+ } catch (e) {
133
+ content1 = '// File does not exist at this commit';
134
+ }
135
+
136
+ try {
137
+ content2 = await analyzer.git.show([`${commit2}:${filepath}`]);
138
+ } catch (e) {
139
+ content2 = '// File does not exist at this commit';
140
+ }
141
+
142
+ res.json({
143
+ content1,
144
+ content2,
145
+ commit1,
146
+ commit2,
147
+ filepath
148
+ });
149
+ } catch (error) {
150
+ res.status(500).json({ error: error.message });
151
+ }
152
+ });
153
+
60
154
  app.post('/api/gc', async (req, res) => {
61
155
  try {
62
156
  await analyzer.git.raw(['gc', '--aggressive']);
@@ -104,4 +198,4 @@ async function startServer(repoPath, port, openBrowser = true) {
104
198
  return server;
105
199
  }
106
200
 
107
- module.exports = { startServer };
201
+ module.exports = { startServer };