diffwatch 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/README.md +92 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +230 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/diff-formatter.d.ts +1 -0
- package/dist/utils/diff-formatter.js +98 -0
- package/dist/utils/diff-formatter.js.map +1 -0
- package/dist/utils/git.d.ts +12 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +96 -0
- package/dist/utils/git.js.map +1 -0
- package/jest.config.js +11 -0
- package/package.json +40 -0
- package/screenshot.jpg +0 -0
- package/src/index.ts +233 -0
- package/src/utils/diff-formatter.ts +70 -0
- package/src/utils/git.ts +92 -0
- package/tests/git.test.d.ts +2 -0
- package/tests/git.test.d.ts.map +1 -0
- package/tests/git.test.js.map +1 -0
- package/tests/git.test.ts +101 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# diffwatch
|
|
2
|
+
|
|
3
|
+
A CLI-based Node.js application that provides a real-time Terminal User Interface (TUI) for monitoring Git repository changes. Watch file statuses and view diffs in a split-pane interface without leaving your terminal.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Real-time Monitoring**: Automatically refreshes every 5 seconds to show the latest Git status
|
|
10
|
+
- **Split-pane TUI**: Left pane displays files with color-coded status, right pane shows unified diffs
|
|
11
|
+
- **Color-coded Status**: Green for added, Red for deleted, Blue for modified, White for untracked
|
|
12
|
+
- **Keyboard Navigation**: Use arrow keys to navigate files, Tab to switch panes
|
|
13
|
+
- **Git-only Operation**: Works exclusively in Git repositories
|
|
14
|
+
- **Cross-platform**: Built with Node.js and TypeScript
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
### Global Installation (Recommended)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g diffwatch
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Local Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git clone <repository-url>
|
|
28
|
+
cd diffwatch
|
|
29
|
+
npm install
|
|
30
|
+
npm run build
|
|
31
|
+
npm link
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Navigate to any Git repository and run:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
diffwatch
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The application will:
|
|
43
|
+
1. Check if the current directory is a Git repository
|
|
44
|
+
2. Display a TUI with two panes
|
|
45
|
+
3. Automatically refresh every 5 seconds
|
|
46
|
+
|
|
47
|
+
### Controls
|
|
48
|
+
|
|
49
|
+
- **Arrow Keys / Tab**: Navigate through the file list and scroll in diff pane
|
|
50
|
+
- **Enter**: Open selected file in default editor
|
|
51
|
+
- **Escape/Q/Ctrl+C**: Exit the application
|
|
52
|
+
|
|
53
|
+
## Prerequisites
|
|
54
|
+
|
|
55
|
+
- Node.js 16 or higher
|
|
56
|
+
- Git installed and accessible in PATH
|
|
57
|
+
- A terminal that supports TUI applications (most modern terminals)
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
### Setup
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install
|
|
65
|
+
npm run build
|
|
66
|
+
npm run dev # Run in development mode
|
|
67
|
+
npm test # Run tests
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Project Structure
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
src/
|
|
74
|
+
├── index.ts # Main application entry point
|
|
75
|
+
└── utils/
|
|
76
|
+
└── git.ts # Git operations and status handling
|
|
77
|
+
tests/
|
|
78
|
+
├── git.test.ts # Unit tests for GitHandler
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Building
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm run build # Compile TypeScript to JavaScript
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Testing
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm test # Run Jest test suite
|
|
91
|
+
```
|
|
92
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
4
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
5
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
6
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
7
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
8
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
9
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const blessed = require('neo-neo-blessed');
|
|
17
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const git_1 = require("./utils/git");
|
|
20
|
+
const diff_formatter_1 = require("./utils/diff-formatter");
|
|
21
|
+
function main() {
|
|
22
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
const gitHandler = new git_1.GitHandler();
|
|
24
|
+
if (!(yield gitHandler.isRepo())) {
|
|
25
|
+
console.log(chalk_1.default.red('Error: Current directory is not a git repository.'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const screen = blessed.screen({
|
|
29
|
+
smartCSR: true,
|
|
30
|
+
title: 'diffwatch',
|
|
31
|
+
});
|
|
32
|
+
const fileList = blessed.list({
|
|
33
|
+
top: 0,
|
|
34
|
+
left: 0,
|
|
35
|
+
width: '30%',
|
|
36
|
+
height: '100%',
|
|
37
|
+
label: ' Files (0) ',
|
|
38
|
+
keys: true,
|
|
39
|
+
vi: true,
|
|
40
|
+
mouse: true,
|
|
41
|
+
tags: true,
|
|
42
|
+
scrollbar: {
|
|
43
|
+
ch: ' ',
|
|
44
|
+
track: { bg: 'white' },
|
|
45
|
+
style: { bg: 'blue' },
|
|
46
|
+
},
|
|
47
|
+
style: {
|
|
48
|
+
selected: { fg: 'black', bg: 'white' },
|
|
49
|
+
border: { fg: 'white' },
|
|
50
|
+
},
|
|
51
|
+
border: { type: 'line' },
|
|
52
|
+
});
|
|
53
|
+
const diffView = blessed.scrollabletext({
|
|
54
|
+
top: 0,
|
|
55
|
+
left: '30%',
|
|
56
|
+
width: '70%',
|
|
57
|
+
height: '100%',
|
|
58
|
+
label: ' Diff () ',
|
|
59
|
+
keys: true,
|
|
60
|
+
vi: true,
|
|
61
|
+
mouse: true,
|
|
62
|
+
scrollbar: {
|
|
63
|
+
ch: ' ',
|
|
64
|
+
track: { bg: 'white' },
|
|
65
|
+
style: { bg: 'blue' },
|
|
66
|
+
},
|
|
67
|
+
style: {
|
|
68
|
+
border: { fg: 'white' },
|
|
69
|
+
},
|
|
70
|
+
border: { type: 'line' },
|
|
71
|
+
tags: false,
|
|
72
|
+
});
|
|
73
|
+
screen.append(fileList);
|
|
74
|
+
screen.append(diffView);
|
|
75
|
+
const updateBorders = () => {
|
|
76
|
+
fileList.style.border.fg = screen.focused === fileList ? 'yellow' : 'white';
|
|
77
|
+
diffView.style.border.fg = screen.focused === diffView ? 'yellow' : 'white';
|
|
78
|
+
screen.render();
|
|
79
|
+
};
|
|
80
|
+
let currentFiles = [];
|
|
81
|
+
let lastSelectedPath = null;
|
|
82
|
+
let diffUpdateTimeout = null;
|
|
83
|
+
const scheduleDiffUpdate = () => {
|
|
84
|
+
if (diffUpdateTimeout)
|
|
85
|
+
clearTimeout(diffUpdateTimeout);
|
|
86
|
+
diffUpdateTimeout = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
|
|
87
|
+
yield updateDiff();
|
|
88
|
+
}), 150); // 150ms debounce
|
|
89
|
+
};
|
|
90
|
+
const openInEditor = (filePath) => {
|
|
91
|
+
try {
|
|
92
|
+
if (process.platform === 'win32') {
|
|
93
|
+
// On Windows, use 'start' to open with default program
|
|
94
|
+
(0, child_process_1.spawn)('cmd', ['/c', 'start', '', filePath], { stdio: 'ignore', detached: true }).unref();
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// On Unix-like systems, try EDITOR, fallback to xdg-open
|
|
98
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'xdg-open';
|
|
99
|
+
(0, child_process_1.spawn)(editor, [filePath], { stdio: 'ignore', detached: true }).unref();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error(`Failed to open ${filePath}: ${error}`);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const updateDiff = () => __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
const selectedIndex = fileList.selected;
|
|
108
|
+
const selectedFile = currentFiles[selectedIndex];
|
|
109
|
+
if (selectedFile) {
|
|
110
|
+
const diff = yield gitHandler.getDiff(selectedFile.path);
|
|
111
|
+
const formattedDiff = (0, diff_formatter_1.formatDiffWithDiff2Html)(diff);
|
|
112
|
+
const newLabel = ` Diff (${selectedFile.path}) `;
|
|
113
|
+
const currentContent = diffView.content;
|
|
114
|
+
const currentLabel = diffView.label;
|
|
115
|
+
// Only update if content or label changed to reduce flickering
|
|
116
|
+
if (formattedDiff !== currentContent || newLabel !== currentLabel) {
|
|
117
|
+
const savedScroll = diffView.scrollTop;
|
|
118
|
+
const isNewFile = selectedFile.path !== lastSelectedPath;
|
|
119
|
+
diffView.setContent(formattedDiff);
|
|
120
|
+
diffView.setLabel(newLabel);
|
|
121
|
+
if (isNewFile) {
|
|
122
|
+
diffView.scrollTo(0);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
diffView.scrollTop = savedScroll;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
lastSelectedPath = selectedFile.path;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const newContent = 'Select a file to view diff.';
|
|
132
|
+
const newLabel = ' Diff () ';
|
|
133
|
+
if (diffView.content !== newContent || diffView.label !== newLabel) {
|
|
134
|
+
diffView.setContent(newContent);
|
|
135
|
+
diffView.setLabel(newLabel);
|
|
136
|
+
diffView.scrollTo(0);
|
|
137
|
+
}
|
|
138
|
+
lastSelectedPath = null;
|
|
139
|
+
}
|
|
140
|
+
screen.render();
|
|
141
|
+
});
|
|
142
|
+
const updateFileList = () => __awaiter(this, void 0, void 0, function* () {
|
|
143
|
+
var _a;
|
|
144
|
+
// Preserve selected file path and scroll positions
|
|
145
|
+
const selectedPath = (_a = currentFiles[fileList.selected]) === null || _a === void 0 ? void 0 : _a.path;
|
|
146
|
+
const fileListScroll = fileList.scroll;
|
|
147
|
+
const diffScroll = diffView.scrollTop;
|
|
148
|
+
const files = yield gitHandler.getStatus();
|
|
149
|
+
currentFiles = files;
|
|
150
|
+
const items = files.map(f => {
|
|
151
|
+
let color = '{white-fg}';
|
|
152
|
+
if (f.status === 'added')
|
|
153
|
+
color = '{green-fg}';
|
|
154
|
+
else if (f.status === 'deleted')
|
|
155
|
+
color = '{red-fg}';
|
|
156
|
+
else if (f.status === 'modified')
|
|
157
|
+
color = '{blue-fg}';
|
|
158
|
+
else if (f.status === 'unstaged')
|
|
159
|
+
color = '{white-fg}';
|
|
160
|
+
return `${color}${f.path}{/}`;
|
|
161
|
+
});
|
|
162
|
+
fileList.setItems(items);
|
|
163
|
+
fileList.setLabel(` Files (${files.length}) `);
|
|
164
|
+
if (items.length > 0) {
|
|
165
|
+
// Restore selection by path if possible
|
|
166
|
+
const newSelectedIndex = selectedPath ? currentFiles.findIndex(f => f.path === selectedPath) : -1;
|
|
167
|
+
fileList.select(newSelectedIndex >= 0 ? newSelectedIndex : 0);
|
|
168
|
+
// Cancel any pending diff update and update immediately
|
|
169
|
+
if (diffUpdateTimeout) {
|
|
170
|
+
clearTimeout(diffUpdateTimeout);
|
|
171
|
+
diffUpdateTimeout = null;
|
|
172
|
+
}
|
|
173
|
+
yield updateDiff();
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
diffView.setContent('No changes detected.');
|
|
177
|
+
diffView.setLabel(' Diff () ');
|
|
178
|
+
}
|
|
179
|
+
// Restore scroll positions
|
|
180
|
+
fileList.scroll = fileListScroll;
|
|
181
|
+
diffView.scrollTop = diffScroll;
|
|
182
|
+
screen.render();
|
|
183
|
+
});
|
|
184
|
+
fileList.on('select item', () => {
|
|
185
|
+
scheduleDiffUpdate();
|
|
186
|
+
});
|
|
187
|
+
fileList.key(['up', 'down'], () => {
|
|
188
|
+
scheduleDiffUpdate();
|
|
189
|
+
});
|
|
190
|
+
screen.key(['escape', 'q', 'C-c'], () => {
|
|
191
|
+
screen.destroy();
|
|
192
|
+
process.exit(0);
|
|
193
|
+
});
|
|
194
|
+
screen.key(['tab'], () => {
|
|
195
|
+
if (screen.focused === fileList) {
|
|
196
|
+
diffView.focus();
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
fileList.focus();
|
|
200
|
+
}
|
|
201
|
+
updateBorders();
|
|
202
|
+
});
|
|
203
|
+
screen.key(['left'], () => {
|
|
204
|
+
fileList.focus();
|
|
205
|
+
updateBorders();
|
|
206
|
+
});
|
|
207
|
+
screen.key(['right'], () => {
|
|
208
|
+
diffView.focus();
|
|
209
|
+
updateBorders();
|
|
210
|
+
});
|
|
211
|
+
screen.key(['enter'], () => {
|
|
212
|
+
const selectedIndex = fileList.selected;
|
|
213
|
+
const selectedFile = currentFiles[selectedIndex];
|
|
214
|
+
if (selectedFile) {
|
|
215
|
+
openInEditor(selectedFile.path);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
219
|
+
yield updateFileList();
|
|
220
|
+
}), 5000);
|
|
221
|
+
yield updateFileList();
|
|
222
|
+
fileList.focus();
|
|
223
|
+
updateBorders();
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
main().catch(err => {
|
|
227
|
+
console.error(err);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
});
|
|
230
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatDiffWithDiff2Html(diffString: string): string;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.formatDiffWithDiff2Html = formatDiffWithDiff2Html;
|
|
37
|
+
const cheerio = __importStar(require("cheerio"));
|
|
38
|
+
const Diff2Html = require('diff2html');
|
|
39
|
+
function formatDiffWithDiff2Html(diffString) {
|
|
40
|
+
if (!diffString || diffString.trim() === '') {
|
|
41
|
+
return 'No changes detected.';
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const html = Diff2Html.html(diffString, {
|
|
45
|
+
drawFileList: false,
|
|
46
|
+
matching: 'lines',
|
|
47
|
+
outputFormat: 'line-by-line',
|
|
48
|
+
colorScheme: 'dark',
|
|
49
|
+
});
|
|
50
|
+
const $ = cheerio.load(html);
|
|
51
|
+
let blessedText = '';
|
|
52
|
+
$('.d2h-diff-tbody tr').each((_, row) => {
|
|
53
|
+
const $row = $(row);
|
|
54
|
+
if ($row.find('.d2h-code-line').length > 0) {
|
|
55
|
+
const $codeCell = $row.find('td.d2h-code-linenumber');
|
|
56
|
+
const isAdded = $row.find('td.d2h-ins').length > 0;
|
|
57
|
+
const isDeleted = $row.find('td.d2h-del').length > 0;
|
|
58
|
+
const $lineContent = $row.find('.d2h-code-line-ctn');
|
|
59
|
+
const $linePrefix = $row.find('.d2h-code-line-prefix');
|
|
60
|
+
const $lineWrapper = $row.find('.d2h-code-line');
|
|
61
|
+
let prefix = '';
|
|
62
|
+
let content = '';
|
|
63
|
+
if ($linePrefix.length > 0) {
|
|
64
|
+
prefix = $linePrefix.text();
|
|
65
|
+
}
|
|
66
|
+
if ($lineContent.length > 0) {
|
|
67
|
+
content = $lineContent.text();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
content = $lineWrapper.text().trim();
|
|
71
|
+
}
|
|
72
|
+
const fullLine = prefix + content;
|
|
73
|
+
if (isAdded) {
|
|
74
|
+
blessedText += `\x1b[32m${fullLine}\x1b[0m\n`;
|
|
75
|
+
}
|
|
76
|
+
else if (isDeleted) {
|
|
77
|
+
blessedText += `\x1b[31m${fullLine}\x1b[0m\n`;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
blessedText += `\x1b[37m${fullLine}\x1b[0m\n`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if ($row.find('.d2h-info').length > 0) {
|
|
84
|
+
const hunkText = $row.find('.d2h-info').text().trim();
|
|
85
|
+
blessedText += `\x1b[36m${hunkText}\x1b[0m\n`;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
$('.d2h-file-header').each((_, header) => {
|
|
89
|
+
const fileText = $(header).text().trim();
|
|
90
|
+
blessedText += `\x1b[33m${fileText}\x1b[0m\n`;
|
|
91
|
+
});
|
|
92
|
+
return blessedText.trim() || 'No changes detected.';
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return `Error formatting diff: ${error}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=diff-formatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-formatter.js","sourceRoot":"","sources":["../../src/utils/diff-formatter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface FileStatus {
|
|
2
|
+
path: string;
|
|
3
|
+
status: 'modified' | 'added' | 'deleted' | 'unstaged' | 'unknown';
|
|
4
|
+
mtime?: Date;
|
|
5
|
+
}
|
|
6
|
+
export declare class GitHandler {
|
|
7
|
+
private git;
|
|
8
|
+
constructor(workingDir?: string);
|
|
9
|
+
isRepo(): Promise<boolean>;
|
|
10
|
+
getStatus(): Promise<FileStatus[]>;
|
|
11
|
+
getDiff(filePath: string): Promise<string>;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;CACnE;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,GAAG,CAAY;gBAEX,UAAU,GAAE,MAAsB;IAIxC,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAQ1B,SAAS,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IA8BlC,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAiBjD"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.GitHandler = void 0;
|
|
16
|
+
const simple_git_1 = require("simple-git");
|
|
17
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
18
|
+
class GitHandler {
|
|
19
|
+
constructor(workingDir = process.cwd()) {
|
|
20
|
+
this.git = (0, simple_git_1.simpleGit)(workingDir);
|
|
21
|
+
}
|
|
22
|
+
isRepo() {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
try {
|
|
25
|
+
return yield this.git.checkIsRepo();
|
|
26
|
+
}
|
|
27
|
+
catch (_a) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
getStatus() {
|
|
33
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
const status = yield this.git.status();
|
|
35
|
+
const files = [];
|
|
36
|
+
status.modified.forEach(path => {
|
|
37
|
+
files.push({ path, status: 'modified' });
|
|
38
|
+
});
|
|
39
|
+
status.deleted.forEach(path => {
|
|
40
|
+
files.push({ path, status: 'deleted' });
|
|
41
|
+
});
|
|
42
|
+
status.created.forEach(path => {
|
|
43
|
+
files.push({ path, status: 'added' });
|
|
44
|
+
});
|
|
45
|
+
status.not_added.forEach(path => {
|
|
46
|
+
files.push({ path, status: 'unstaged' });
|
|
47
|
+
});
|
|
48
|
+
status.renamed.forEach(r => {
|
|
49
|
+
files.push({ path: r.to, status: 'added' });
|
|
50
|
+
});
|
|
51
|
+
const uniqueFiles = new Map();
|
|
52
|
+
files.forEach(f => uniqueFiles.set(f.path, f));
|
|
53
|
+
// Add last modified time for sorting
|
|
54
|
+
const fileArray = Array.from(uniqueFiles.values());
|
|
55
|
+
yield Promise.all(fileArray.map((f) => __awaiter(this, void 0, void 0, function* () {
|
|
56
|
+
try {
|
|
57
|
+
const stat = yield promises_1.default.stat(f.path);
|
|
58
|
+
f.mtime = stat.mtime;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// For deleted or inaccessible files, use epoch time
|
|
62
|
+
f.mtime = new Date(0);
|
|
63
|
+
}
|
|
64
|
+
})));
|
|
65
|
+
// Sort by last modified descending, then by filename
|
|
66
|
+
return fileArray.sort((a, b) => {
|
|
67
|
+
const mtimeA = a.mtime || new Date(0);
|
|
68
|
+
const mtimeB = b.mtime || new Date(0);
|
|
69
|
+
const timeDiff = mtimeB.getTime() - mtimeA.getTime();
|
|
70
|
+
if (timeDiff !== 0) {
|
|
71
|
+
return timeDiff;
|
|
72
|
+
}
|
|
73
|
+
return a.path.localeCompare(b.path);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
getDiff(filePath) {
|
|
78
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
try {
|
|
80
|
+
const isUntracked = (yield this.git.status()).not_added.includes(filePath);
|
|
81
|
+
if (isUntracked) {
|
|
82
|
+
return yield this.git.raw(['diff', '--no-index', '--', '/dev/null', filePath]).catch(() => {
|
|
83
|
+
return `New file: ${filePath}`;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const diff = yield this.git.diff(['HEAD', '--', filePath]);
|
|
87
|
+
return diff || 'No changes or file is new.';
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return `Error getting diff: ${error}`;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.GitHandler = GitHandler;
|
|
96
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"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"}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "diffwatch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An app for watching git repository file changes.",
|
|
5
|
+
"author": "Sarfraz Ahmed <sarfraznawaz2005@gmail.com>",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"diffwatch": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "ts-node src/index.ts",
|
|
14
|
+
"test": "jest"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"diff",
|
|
18
|
+
"watch",
|
|
19
|
+
"file",
|
|
20
|
+
"comparison",
|
|
21
|
+
"utility"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "commonjs",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"chalk": "^4.1.2",
|
|
27
|
+
"cheerio": "^1.1.2",
|
|
28
|
+
"diff2html": "^3.4.55",
|
|
29
|
+
"neo-neo-blessed": "^0.7.1",
|
|
30
|
+
"simple-git": "^3.30.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/jest": "^30.0.0",
|
|
34
|
+
"@types/node": "^25.0.8",
|
|
35
|
+
"jest": "^30.2.0",
|
|
36
|
+
"ts-jest": "^29.4.6",
|
|
37
|
+
"ts-node": "^10.9.2",
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/screenshot.jpg
ADDED
|
Binary file
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const blessed = require('neo-neo-blessed');
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { GitHandler, FileStatus } from './utils/git';
|
|
6
|
+
import { formatDiffWithDiff2Html } from './utils/diff-formatter';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const gitHandler = new GitHandler();
|
|
10
|
+
|
|
11
|
+
if (!(await gitHandler.isRepo())) {
|
|
12
|
+
console.log(chalk.red('Error: Current directory is not a git repository.'));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const screen = blessed.screen({
|
|
17
|
+
smartCSR: true,
|
|
18
|
+
title: 'diffwatch',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const fileList = blessed.list({
|
|
22
|
+
top: 0,
|
|
23
|
+
left: 0,
|
|
24
|
+
width: '30%',
|
|
25
|
+
height: '100%',
|
|
26
|
+
label: ' Files (0) ',
|
|
27
|
+
keys: true,
|
|
28
|
+
vi: true,
|
|
29
|
+
mouse: true,
|
|
30
|
+
tags: true,
|
|
31
|
+
scrollbar: {
|
|
32
|
+
ch: ' ',
|
|
33
|
+
track: { bg: 'white' },
|
|
34
|
+
style: { bg: 'blue' },
|
|
35
|
+
},
|
|
36
|
+
style: {
|
|
37
|
+
selected: { fg: 'black', bg: 'white' },
|
|
38
|
+
border: { fg: 'white' },
|
|
39
|
+
},
|
|
40
|
+
border: { type: 'line' },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const diffView = blessed.scrollabletext({
|
|
44
|
+
top: 0,
|
|
45
|
+
left: '30%',
|
|
46
|
+
width: '70%',
|
|
47
|
+
height: '100%',
|
|
48
|
+
label: ' Diff () ',
|
|
49
|
+
keys: true,
|
|
50
|
+
vi: true,
|
|
51
|
+
mouse: true,
|
|
52
|
+
scrollbar: {
|
|
53
|
+
ch: ' ',
|
|
54
|
+
track: { bg: 'white' },
|
|
55
|
+
style: { bg: 'blue' },
|
|
56
|
+
},
|
|
57
|
+
style: {
|
|
58
|
+
border: { fg: 'white' },
|
|
59
|
+
},
|
|
60
|
+
border: { type: 'line' },
|
|
61
|
+
tags: false,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
screen.append(fileList);
|
|
65
|
+
screen.append(diffView);
|
|
66
|
+
|
|
67
|
+
const updateBorders = () => {
|
|
68
|
+
fileList.style.border.fg = screen.focused === fileList ? 'yellow' : 'white';
|
|
69
|
+
diffView.style.border.fg = screen.focused === diffView ? 'yellow' : 'white';
|
|
70
|
+
screen.render();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let currentFiles: FileStatus[] = [];
|
|
74
|
+
let lastSelectedPath: string | null = null;
|
|
75
|
+
let diffUpdateTimeout: NodeJS.Timeout | null = null;
|
|
76
|
+
|
|
77
|
+
const scheduleDiffUpdate = () => {
|
|
78
|
+
if (diffUpdateTimeout) clearTimeout(diffUpdateTimeout);
|
|
79
|
+
diffUpdateTimeout = setTimeout(async () => {
|
|
80
|
+
await updateDiff();
|
|
81
|
+
}, 150); // 150ms debounce
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const openInEditor = (filePath: string) => {
|
|
85
|
+
try {
|
|
86
|
+
if (process.platform === 'win32') {
|
|
87
|
+
// On Windows, use 'start' to open with default program
|
|
88
|
+
spawn('cmd', ['/c', 'start', '', filePath], { stdio: 'ignore', detached: true }).unref();
|
|
89
|
+
} else {
|
|
90
|
+
// On Unix-like systems, try EDITOR, fallback to xdg-open
|
|
91
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'xdg-open';
|
|
92
|
+
spawn(editor, [filePath], { stdio: 'ignore', detached: true }).unref();
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`Failed to open ${filePath}: ${error}`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const updateDiff = async () => {
|
|
100
|
+
const selectedIndex = fileList.selected;
|
|
101
|
+
const selectedFile = currentFiles[selectedIndex];
|
|
102
|
+
if (selectedFile) {
|
|
103
|
+
const diff = await gitHandler.getDiff(selectedFile.path);
|
|
104
|
+
const formattedDiff = formatDiffWithDiff2Html(diff);
|
|
105
|
+
const newLabel = ` Diff (${selectedFile.path}) `;
|
|
106
|
+
const currentContent = diffView.content;
|
|
107
|
+
const currentLabel = diffView.label;
|
|
108
|
+
|
|
109
|
+
// Only update if content or label changed to reduce flickering
|
|
110
|
+
if (formattedDiff !== currentContent || newLabel !== currentLabel) {
|
|
111
|
+
const savedScroll = diffView.scrollTop;
|
|
112
|
+
const isNewFile = selectedFile.path !== lastSelectedPath;
|
|
113
|
+
|
|
114
|
+
diffView.setContent(formattedDiff);
|
|
115
|
+
diffView.setLabel(newLabel);
|
|
116
|
+
|
|
117
|
+
if (isNewFile) {
|
|
118
|
+
diffView.scrollTo(0);
|
|
119
|
+
} else {
|
|
120
|
+
diffView.scrollTop = savedScroll;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
lastSelectedPath = selectedFile.path;
|
|
124
|
+
} else {
|
|
125
|
+
const newContent = 'Select a file to view diff.';
|
|
126
|
+
const newLabel = ' Diff () ';
|
|
127
|
+
if (diffView.content !== newContent || diffView.label !== newLabel) {
|
|
128
|
+
diffView.setContent(newContent);
|
|
129
|
+
diffView.setLabel(newLabel);
|
|
130
|
+
diffView.scrollTo(0);
|
|
131
|
+
}
|
|
132
|
+
lastSelectedPath = null;
|
|
133
|
+
}
|
|
134
|
+
screen.render();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const updateFileList = async () => {
|
|
138
|
+
// Preserve selected file path and scroll positions
|
|
139
|
+
const selectedPath = currentFiles[fileList.selected]?.path;
|
|
140
|
+
const fileListScroll = fileList.scroll;
|
|
141
|
+
const diffScroll = diffView.scrollTop;
|
|
142
|
+
|
|
143
|
+
const files = await gitHandler.getStatus();
|
|
144
|
+
currentFiles = files;
|
|
145
|
+
|
|
146
|
+
const items = files.map(f => {
|
|
147
|
+
let color = '{white-fg}';
|
|
148
|
+
if (f.status === 'added') color = '{green-fg}';
|
|
149
|
+
else if (f.status === 'deleted') color = '{red-fg}';
|
|
150
|
+
else if (f.status === 'modified') color = '{blue-fg}';
|
|
151
|
+
else if (f.status === 'unstaged') color = '{white-fg}';
|
|
152
|
+
|
|
153
|
+
return `${color}${f.path}{/}`;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
fileList.setItems(items);
|
|
157
|
+
fileList.setLabel(` Files (${files.length}) `);
|
|
158
|
+
|
|
159
|
+
if (items.length > 0) {
|
|
160
|
+
// Restore selection by path if possible
|
|
161
|
+
const newSelectedIndex = selectedPath ? currentFiles.findIndex(f => f.path === selectedPath) : -1;
|
|
162
|
+
fileList.select(newSelectedIndex >= 0 ? newSelectedIndex : 0);
|
|
163
|
+
// Cancel any pending diff update and update immediately
|
|
164
|
+
if (diffUpdateTimeout) {
|
|
165
|
+
clearTimeout(diffUpdateTimeout);
|
|
166
|
+
diffUpdateTimeout = null;
|
|
167
|
+
}
|
|
168
|
+
await updateDiff();
|
|
169
|
+
} else {
|
|
170
|
+
diffView.setContent('No changes detected.');
|
|
171
|
+
diffView.setLabel(' Diff () ');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Restore scroll positions
|
|
175
|
+
fileList.scroll = fileListScroll;
|
|
176
|
+
diffView.scrollTop = diffScroll;
|
|
177
|
+
|
|
178
|
+
screen.render();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
fileList.on('select item', () => {
|
|
182
|
+
scheduleDiffUpdate();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
fileList.key(['up', 'down'], () => {
|
|
186
|
+
scheduleDiffUpdate();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
screen.key(['escape', 'q', 'C-c'], () => {
|
|
190
|
+
screen.destroy();
|
|
191
|
+
process.exit(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
screen.key(['tab'], () => {
|
|
195
|
+
if (screen.focused === fileList) {
|
|
196
|
+
diffView.focus();
|
|
197
|
+
} else {
|
|
198
|
+
fileList.focus();
|
|
199
|
+
}
|
|
200
|
+
updateBorders();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
screen.key(['left'], () => {
|
|
204
|
+
fileList.focus();
|
|
205
|
+
updateBorders();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
screen.key(['right'], () => {
|
|
209
|
+
diffView.focus();
|
|
210
|
+
updateBorders();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
screen.key(['enter'], () => {
|
|
214
|
+
const selectedIndex = fileList.selected;
|
|
215
|
+
const selectedFile = currentFiles[selectedIndex];
|
|
216
|
+
if (selectedFile) {
|
|
217
|
+
openInEditor(selectedFile.path);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
setInterval(async () => {
|
|
222
|
+
await updateFileList();
|
|
223
|
+
}, 5000);
|
|
224
|
+
|
|
225
|
+
await updateFileList();
|
|
226
|
+
fileList.focus();
|
|
227
|
+
updateBorders();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
main().catch(err => {
|
|
231
|
+
console.error(err);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as cheerio from 'cheerio';
|
|
2
|
+
const Diff2Html = require('diff2html');
|
|
3
|
+
|
|
4
|
+
export function formatDiffWithDiff2Html(diffString: string): string {
|
|
5
|
+
if (!diffString || diffString.trim() === '') {
|
|
6
|
+
return 'No changes detected.';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const html = Diff2Html.html(diffString, {
|
|
11
|
+
drawFileList: false,
|
|
12
|
+
matching: 'lines',
|
|
13
|
+
outputFormat: 'line-by-line',
|
|
14
|
+
colorScheme: 'dark',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const $ = cheerio.load(html);
|
|
18
|
+
|
|
19
|
+
let blessedText = '';
|
|
20
|
+
|
|
21
|
+
$('.d2h-diff-tbody tr').each((_, row) => {
|
|
22
|
+
const $row = $(row);
|
|
23
|
+
|
|
24
|
+
if ($row.find('.d2h-code-line').length > 0) {
|
|
25
|
+
const $codeCell = $row.find('td.d2h-code-linenumber');
|
|
26
|
+
const isAdded = $row.find('td.d2h-ins').length > 0;
|
|
27
|
+
const isDeleted = $row.find('td.d2h-del').length > 0;
|
|
28
|
+
const $lineContent = $row.find('.d2h-code-line-ctn');
|
|
29
|
+
const $linePrefix = $row.find('.d2h-code-line-prefix');
|
|
30
|
+
const $lineWrapper = $row.find('.d2h-code-line');
|
|
31
|
+
|
|
32
|
+
let prefix = '';
|
|
33
|
+
let content = '';
|
|
34
|
+
|
|
35
|
+
if ($linePrefix.length > 0) {
|
|
36
|
+
prefix = $linePrefix.text();
|
|
37
|
+
}
|
|
38
|
+
if ($lineContent.length > 0) {
|
|
39
|
+
content = $lineContent.text();
|
|
40
|
+
} else {
|
|
41
|
+
content = $lineWrapper.text().trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fullLine = prefix + content;
|
|
45
|
+
|
|
46
|
+
if (isAdded) {
|
|
47
|
+
blessedText += `\x1b[32m${fullLine}\x1b[0m\n`;
|
|
48
|
+
} else if (isDeleted) {
|
|
49
|
+
blessedText += `\x1b[31m${fullLine}\x1b[0m\n`;
|
|
50
|
+
} else {
|
|
51
|
+
blessedText += `\x1b[37m${fullLine}\x1b[0m\n`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if ($row.find('.d2h-info').length > 0) {
|
|
56
|
+
const hunkText = $row.find('.d2h-info').text().trim();
|
|
57
|
+
blessedText += `\x1b[36m${hunkText}\x1b[0m\n`;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
$('.d2h-file-header').each((_, header) => {
|
|
62
|
+
const fileText = $(header).text().trim();
|
|
63
|
+
blessedText += `\x1b[33m${fileText}\x1b[0m\n`;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return blessedText.trim() || 'No changes detected.';
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return `Error formatting diff: ${error}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/utils/git.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { simpleGit, SimpleGit, StatusResult } from 'simple-git';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
export interface FileStatus {
|
|
5
|
+
path: string;
|
|
6
|
+
status: 'modified' | 'added' | 'deleted' | 'unstaged' | 'unknown';
|
|
7
|
+
mtime?: Date;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class GitHandler {
|
|
11
|
+
private git: SimpleGit;
|
|
12
|
+
|
|
13
|
+
constructor(workingDir: string = process.cwd()) {
|
|
14
|
+
this.git = simpleGit(workingDir);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async isRepo(): Promise<boolean> {
|
|
18
|
+
try {
|
|
19
|
+
return await this.git.checkIsRepo();
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async getStatus(): Promise<FileStatus[]> {
|
|
26
|
+
const status: StatusResult = await this.git.status();
|
|
27
|
+
const files: FileStatus[] = [];
|
|
28
|
+
|
|
29
|
+
status.modified.forEach(path => {
|
|
30
|
+
files.push({ path, status: 'modified' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
status.deleted.forEach(path => {
|
|
34
|
+
files.push({ path, status: 'deleted' });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
status.created.forEach(path => {
|
|
38
|
+
files.push({ path, status: 'added' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
status.not_added.forEach(path => {
|
|
42
|
+
files.push({ path, status: 'unstaged' });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
status.renamed.forEach(r => {
|
|
46
|
+
files.push({ path: r.to, status: 'added' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const uniqueFiles = new Map<string, FileStatus>();
|
|
50
|
+
files.forEach(f => uniqueFiles.set(f.path, f));
|
|
51
|
+
|
|
52
|
+
// Add last modified time for sorting
|
|
53
|
+
const fileArray = Array.from(uniqueFiles.values());
|
|
54
|
+
await Promise.all(fileArray.map(async (f) => {
|
|
55
|
+
try {
|
|
56
|
+
const stat = await fs.stat(f.path);
|
|
57
|
+
f.mtime = stat.mtime;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// For deleted or inaccessible files, use epoch time
|
|
60
|
+
f.mtime = new Date(0);
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// Sort by last modified descending, then by filename
|
|
65
|
+
return fileArray.sort((a, b) => {
|
|
66
|
+
const mtimeA = a.mtime || new Date(0);
|
|
67
|
+
const mtimeB = b.mtime || new Date(0);
|
|
68
|
+
const timeDiff = mtimeB.getTime() - mtimeA.getTime();
|
|
69
|
+
if (timeDiff !== 0) {
|
|
70
|
+
return timeDiff;
|
|
71
|
+
}
|
|
72
|
+
return a.path.localeCompare(b.path);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getDiff(filePath: string): Promise<string> {
|
|
77
|
+
try {
|
|
78
|
+
const isUntracked = (await this.git.status()).not_added.includes(filePath);
|
|
79
|
+
|
|
80
|
+
if (isUntracked) {
|
|
81
|
+
return await this.git.raw(['diff', '--no-index', '--', '/dev/null', filePath]).catch(() => {
|
|
82
|
+
return `New file: ${filePath}`;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const diff = await this.git.diff(['HEAD', '--', filePath]);
|
|
87
|
+
return diff || 'No changes or file is new.';
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return `Error getting diff: ${error}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.test.d.ts","sourceRoot":"","sources":["git.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.test.js","sourceRoot":"","sources":["git.test.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,0CAA8C;AAC9C,2CAAuC;AAEvC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAExB,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,IAAI,UAAsB,CAAC;IAC3B,IAAI,OAAY,CAAC;IAEjB,UAAU,CAAC,GAAG,EAAE;QACd,OAAO,GAAG;YACR,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE;YACtB,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE;YACjB,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE;YACf,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE;SAChB,CAAC;QACD,sBAAuB,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;QAClD,UAAU,GAAG,IAAI,gBAAU,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAS,EAAE;QACzD,OAAO,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAS,EAAE;QAC9D,OAAO,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;QAC/D,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAS,EAAE;QACnD,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC;YAC/B,QAAQ,EAAE,CAAC,UAAU,CAAC;YACtB,OAAO,EAAE,CAAC,UAAU,CAAC;YACrB,OAAO,EAAE,CAAC,UAAU,CAAC;YACrB,SAAS,EAAE,CAAC,UAAU,CAAC;YACvB,OAAO,EAAE,EAAE;SACZ,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;QACxE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QACvE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAS,EAAE;QAC5C,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,8BAA8B,CAAC,CAAC;QAC/D,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAClD,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;IACpD,CAAC,CAAA,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
jest.mock('simple-git', () => ({
|
|
2
|
+
simpleGit: jest.fn(),
|
|
3
|
+
}));
|
|
4
|
+
jest.mock('fs/promises', () => ({
|
|
5
|
+
stat: jest.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import { simpleGit } from 'simple-git';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import { GitHandler } from '../src/utils/git';
|
|
11
|
+
|
|
12
|
+
describe('GitHandler', () => {
|
|
13
|
+
let gitHandler: GitHandler;
|
|
14
|
+
let mockGit: any;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockGit = {
|
|
18
|
+
checkIsRepo: jest.fn(),
|
|
19
|
+
status: jest.fn(),
|
|
20
|
+
diff: jest.fn(),
|
|
21
|
+
show: jest.fn(),
|
|
22
|
+
};
|
|
23
|
+
(simpleGit as jest.Mock).mockReturnValue(mockGit);
|
|
24
|
+
(fs.stat as jest.Mock).mockResolvedValue({ mtime: new Date() });
|
|
25
|
+
gitHandler = new GitHandler();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return true if directory is a repo', async () => {
|
|
29
|
+
mockGit.checkIsRepo.mockResolvedValue(true);
|
|
30
|
+
const result = await gitHandler.isRepo();
|
|
31
|
+
expect(result).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return false if directory is not a repo', async () => {
|
|
35
|
+
mockGit.checkIsRepo.mockRejectedValue(new Error('not a repo'));
|
|
36
|
+
const result = await gitHandler.isRepo();
|
|
37
|
+
expect(result).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return file status correctly', async () => {
|
|
41
|
+
mockGit.status.mockResolvedValue({
|
|
42
|
+
modified: ['file1.ts'],
|
|
43
|
+
deleted: ['file2.ts'],
|
|
44
|
+
created: ['file3.ts'],
|
|
45
|
+
not_added: ['file4.ts'],
|
|
46
|
+
renamed: [],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const status = await gitHandler.getStatus();
|
|
50
|
+
expect(status).toEqual(expect.arrayContaining([
|
|
51
|
+
expect.objectContaining({ path: 'file1.ts', status: 'modified' }),
|
|
52
|
+
expect.objectContaining({ path: 'file2.ts', status: 'deleted' }),
|
|
53
|
+
expect.objectContaining({ path: 'file3.ts', status: 'added' }),
|
|
54
|
+
expect.objectContaining({ path: 'file4.ts', status: 'unstaged' }),
|
|
55
|
+
]));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return diff correctly', async () => {
|
|
59
|
+
mockGit.status.mockResolvedValue({ not_added: [] });
|
|
60
|
+
mockGit.diff.mockResolvedValue('+ added line\n- removed line');
|
|
61
|
+
const diff = await gitHandler.getDiff('file1.ts');
|
|
62
|
+
expect(diff).toBe('+ added line\n- removed line');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should sort files by last modified descending then filename', async () => {
|
|
66
|
+
// Mock fs.stat to return different mtimes
|
|
67
|
+
const mockStat = jest.fn();
|
|
68
|
+
(fs.stat as jest.Mock) = mockStat;
|
|
69
|
+
mockStat.mockImplementation((path: string) => {
|
|
70
|
+
const mtimes: Record<string, Date> = {
|
|
71
|
+
'z-file.ts': new Date('2024-01-03'),
|
|
72
|
+
'a-file.ts': new Date('2024-01-01'),
|
|
73
|
+
'z-deleted.ts': new Date('2024-01-02'),
|
|
74
|
+
'z-added.ts': new Date('2024-01-04'),
|
|
75
|
+
'a-added.ts': new Date('2024-01-05'),
|
|
76
|
+
'z-unstaged.ts': new Date('2024-01-06'),
|
|
77
|
+
'a-unstaged.ts': new Date('2024-01-07'),
|
|
78
|
+
};
|
|
79
|
+
return Promise.resolve({ mtime: mtimes[path] || new Date(0) });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
mockGit.status.mockResolvedValue({
|
|
83
|
+
modified: ['z-file.ts', 'a-file.ts'],
|
|
84
|
+
deleted: ['z-deleted.ts'],
|
|
85
|
+
created: ['z-added.ts', 'a-added.ts'],
|
|
86
|
+
not_added: ['z-unstaged.ts', 'a-unstaged.ts'],
|
|
87
|
+
renamed: [],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const status = await gitHandler.getStatus();
|
|
91
|
+
expect(status.map(f => f.path)).toEqual([
|
|
92
|
+
'a-unstaged.ts', // newest
|
|
93
|
+
'z-unstaged.ts',
|
|
94
|
+
'a-added.ts',
|
|
95
|
+
'z-added.ts',
|
|
96
|
+
'z-file.ts',
|
|
97
|
+
'z-deleted.ts',
|
|
98
|
+
'a-file.ts', // oldest
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES6",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"declaration": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist", "sample-tui", "tests"]
|
|
16
|
+
}
|