mc-gitpulse 1.0.0 → 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 +8 -0
- package/package.json +1 -1
- package/public/index.html +496 -36
- package/server.js +96 -2
package/README.md
CHANGED
|
@@ -45,6 +45,14 @@
|
|
|
45
45
|
- **Real-time updates** - WebSocket-powered live data
|
|
46
46
|
- **Smooth animations** - Professional transitions and effects
|
|
47
47
|
|
|
48
|
+
## Demo
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
## Screenshot
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
|
|
48
56
|
## 📦 Installation
|
|
49
57
|
|
|
50
58
|
```bash
|
package/package.json
CHANGED
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
|
|
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
|
-
.
|
|
63
|
+
.file-tree-container {
|
|
65
64
|
display: flex;
|
|
66
|
-
height:
|
|
67
|
-
gap:
|
|
65
|
+
height: 650px;
|
|
66
|
+
gap: 0;
|
|
67
|
+
position: relative;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
.split-left {
|
|
71
|
-
flex: 0 0
|
|
71
|
+
flex: 0 0 350px;
|
|
72
72
|
overflow-y: auto;
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
513
|
-
|
|
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
|
|
519
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
903
|
-
|
|
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 =
|
|
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 };
|