gh-here 1.0.2 → 1.0.5
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/.claude/settings.local.json +16 -10
- package/README.md +6 -3
- package/bin/gh-here.js +49 -1258
- package/lib/file-utils.js +264 -0
- package/lib/git.js +207 -0
- package/lib/gitignore.js +91 -0
- package/lib/renderers.js +569 -0
- package/lib/server.js +391 -0
- package/package.json +1 -1
- package/public/app.js +692 -129
- package/public/styles.css +414 -44
- package/tests/draftManager.test.js +241 -0
- package/tests/httpService.test.js +268 -0
- package/tests/languageDetection.test.js +145 -0
- package/tests/pathUtils.test.js +136 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const octicons = require('@primer/octicons');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* File utilities module
|
|
6
|
+
* Handles file icons, language detection, and formatting
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function getFileIcon(filename) {
|
|
10
|
+
const ext = path.extname(filename).toLowerCase();
|
|
11
|
+
const name = filename.toLowerCase();
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Configuration files
|
|
15
|
+
if (name === 'package.json' || name === 'composer.json') {
|
|
16
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
|
|
17
|
+
}
|
|
18
|
+
if (name === 'tsconfig.json' || name === 'jsconfig.json') {
|
|
19
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
20
|
+
}
|
|
21
|
+
if (name === '.eslintrc' || name === '.eslintrc.json' || name === '.eslintrc.js' || name === '.eslintrc.yml') {
|
|
22
|
+
return octicons.gear?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
|
|
23
|
+
}
|
|
24
|
+
if (name === '.prettierrc' || name === 'prettier.config.js' || name === '.prettierrc.json') {
|
|
25
|
+
return octicons.gear?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
|
|
26
|
+
}
|
|
27
|
+
if (name === 'webpack.config.js' || name === 'vite.config.js' || name === 'rollup.config.js' || name === 'next.config.js' || name === 'nuxt.config.js' || name === 'svelte.config.js') {
|
|
28
|
+
return octicons.gear?.toSVG({ class: 'octicon-file text-orange' }) || octicons.file.toSVG({ class: 'octicon-file text-orange' });
|
|
29
|
+
}
|
|
30
|
+
if (name === 'tailwind.config.js' || name === 'postcss.config.js' || name === 'babel.config.js' || name === '.babelrc') {
|
|
31
|
+
return octicons.gear?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Docker files
|
|
35
|
+
if (name === 'dockerfile' || name === 'dockerfile.dev' || name === '.dockerignore') {
|
|
36
|
+
return octicons.container?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
|
|
37
|
+
}
|
|
38
|
+
if (name === 'docker-compose.yml' || name === 'docker-compose.yaml') {
|
|
39
|
+
return octicons.container?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Git files
|
|
43
|
+
if (name === '.gitignore' || name === '.gitattributes' || name === '.gitmodules') {
|
|
44
|
+
return octicons['git-branch']?.toSVG({ class: 'octicon-file text-orange' }) || octicons.file.toSVG({ class: 'octicon-file text-orange' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Documentation
|
|
48
|
+
if (name.startsWith('readme') || name === 'changelog.md' || name === 'history.md') {
|
|
49
|
+
return octicons.book.toSVG({ class: 'octicon-file text-blue' });
|
|
50
|
+
}
|
|
51
|
+
if (name === 'license' || name === 'license.txt' || name === 'license.md') {
|
|
52
|
+
return octicons.law?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Build files
|
|
56
|
+
if (name === 'makefile' || name === 'makefile.am' || name === 'cmakelists.txt') {
|
|
57
|
+
return octicons.tools?.toSVG({ class: 'octicon-file text-gray' }) || octicons.file.toSVG({ class: 'octicon-file text-gray' });
|
|
58
|
+
}
|
|
59
|
+
if (name.endsWith('.lock') || name === 'yarn.lock' || name === 'package-lock.json' || name === 'pipfile.lock') {
|
|
60
|
+
return octicons.lock?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// CI/CD files
|
|
64
|
+
if (name === '.travis.yml' || name === '.circleci' || name.startsWith('.github')) {
|
|
65
|
+
return octicons.gear?.toSVG({ class: 'octicon-file text-green' }) || octicons.file.toSVG({ class: 'octicon-file text-green' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Environment files
|
|
69
|
+
if (name === '.env' || name === '.env.local' || name.startsWith('.env.')) {
|
|
70
|
+
return octicons.key?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Extension-based icons
|
|
74
|
+
switch (ext) {
|
|
75
|
+
case '.js':
|
|
76
|
+
case '.mjs':
|
|
77
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-yellow' });
|
|
78
|
+
case '.jsx':
|
|
79
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
80
|
+
case '.ts':
|
|
81
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
82
|
+
case '.tsx':
|
|
83
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
84
|
+
case '.vue':
|
|
85
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
|
|
86
|
+
case '.svelte':
|
|
87
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
88
|
+
case '.py':
|
|
89
|
+
case '.pyx':
|
|
90
|
+
case '.pyi':
|
|
91
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
92
|
+
case '.java':
|
|
93
|
+
case '.class':
|
|
94
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
|
|
95
|
+
case '.c':
|
|
96
|
+
case '.h':
|
|
97
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
98
|
+
case '.cpp':
|
|
99
|
+
case '.cxx':
|
|
100
|
+
case '.cc':
|
|
101
|
+
case '.hpp':
|
|
102
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
103
|
+
case '.cs':
|
|
104
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
|
|
105
|
+
case '.go':
|
|
106
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
107
|
+
case '.rs':
|
|
108
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
109
|
+
case '.php':
|
|
110
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
|
|
111
|
+
case '.rb':
|
|
112
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
|
|
113
|
+
case '.swift':
|
|
114
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
115
|
+
case '.kt':
|
|
116
|
+
case '.kts':
|
|
117
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
|
|
118
|
+
case '.dart':
|
|
119
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
120
|
+
case '.scala':
|
|
121
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
|
|
122
|
+
case '.clj':
|
|
123
|
+
case '.cljs':
|
|
124
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
|
|
125
|
+
case '.hs':
|
|
126
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
|
|
127
|
+
case '.elm':
|
|
128
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
129
|
+
case '.r':
|
|
130
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
131
|
+
case '.html':
|
|
132
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
133
|
+
case '.css':
|
|
134
|
+
case '.scss':
|
|
135
|
+
case '.sass':
|
|
136
|
+
case '.less':
|
|
137
|
+
return octicons.paintbrush?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
|
|
138
|
+
case '.json':
|
|
139
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-yellow' });
|
|
140
|
+
case '.xml':
|
|
141
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
142
|
+
case '.yml':
|
|
143
|
+
case '.yaml':
|
|
144
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
|
|
145
|
+
case '.md':
|
|
146
|
+
case '.markdown':
|
|
147
|
+
return octicons.book.toSVG({ class: 'octicon-file text-blue' });
|
|
148
|
+
case '.txt':
|
|
149
|
+
return octicons['file-text']?.toSVG({ class: 'octicon-file text-gray' }) || octicons.file.toSVG({ class: 'octicon-file text-gray' });
|
|
150
|
+
case '.pdf':
|
|
151
|
+
return octicons['file-binary']?.toSVG({ class: 'octicon-file text-red' }) || octicons.file.toSVG({ class: 'octicon-file text-red' });
|
|
152
|
+
case '.png':
|
|
153
|
+
case '.jpg':
|
|
154
|
+
case '.jpeg':
|
|
155
|
+
case '.gif':
|
|
156
|
+
case '.svg':
|
|
157
|
+
case '.webp':
|
|
158
|
+
return octicons['file-media']?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
|
|
159
|
+
case '.mp4':
|
|
160
|
+
case '.mov':
|
|
161
|
+
case '.avi':
|
|
162
|
+
case '.mkv':
|
|
163
|
+
return octicons['device-camera-video']?.toSVG({ class: 'octicon-file text-red' }) || octicons.file.toSVG({ class: 'octicon-file text-red' });
|
|
164
|
+
case '.mp3':
|
|
165
|
+
case '.wav':
|
|
166
|
+
case '.flac':
|
|
167
|
+
return octicons.unmute?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
|
|
168
|
+
case '.zip':
|
|
169
|
+
case '.tar':
|
|
170
|
+
case '.gz':
|
|
171
|
+
case '.rar':
|
|
172
|
+
case '.7z':
|
|
173
|
+
return octicons['file-zip']?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
|
|
174
|
+
case '.sh':
|
|
175
|
+
case '.bash':
|
|
176
|
+
case '.zsh':
|
|
177
|
+
case '.fish':
|
|
178
|
+
return octicons.terminal?.toSVG({ class: 'octicon-file text-green' }) || octicons.file.toSVG({ class: 'octicon-file text-green' });
|
|
179
|
+
case '.sql':
|
|
180
|
+
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
181
|
+
default:
|
|
182
|
+
return octicons.file.toSVG({ class: 'octicon-file text-gray' });
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return octicons.file.toSVG({ class: 'octicon-file text-gray' });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getLanguageFromExtension(ext) {
|
|
190
|
+
const langMap = {
|
|
191
|
+
'js': 'javascript',
|
|
192
|
+
'jsx': 'javascript',
|
|
193
|
+
'ts': 'typescript',
|
|
194
|
+
'tsx': 'typescript',
|
|
195
|
+
'py': 'python',
|
|
196
|
+
'java': 'java',
|
|
197
|
+
'html': 'html',
|
|
198
|
+
'css': 'css',
|
|
199
|
+
'scss': 'scss',
|
|
200
|
+
'sass': 'sass',
|
|
201
|
+
'json': 'json',
|
|
202
|
+
'xml': 'xml',
|
|
203
|
+
'yaml': 'yaml',
|
|
204
|
+
'yml': 'yaml',
|
|
205
|
+
'sh': 'bash',
|
|
206
|
+
'bash': 'bash',
|
|
207
|
+
'zsh': 'bash',
|
|
208
|
+
'go': 'go',
|
|
209
|
+
'rs': 'rust',
|
|
210
|
+
'cpp': 'cpp',
|
|
211
|
+
'cxx': 'cpp',
|
|
212
|
+
'cc': 'cpp',
|
|
213
|
+
'c': 'c',
|
|
214
|
+
'h': 'c',
|
|
215
|
+
'hpp': 'cpp',
|
|
216
|
+
'php': 'php',
|
|
217
|
+
'rb': 'ruby',
|
|
218
|
+
'swift': 'swift',
|
|
219
|
+
'kt': 'kotlin',
|
|
220
|
+
'dart': 'dart',
|
|
221
|
+
'r': 'r',
|
|
222
|
+
'sql': 'sql',
|
|
223
|
+
'dockerfile': 'dockerfile',
|
|
224
|
+
'md': 'markdown',
|
|
225
|
+
'markdown': 'markdown',
|
|
226
|
+
'vue': 'vue',
|
|
227
|
+
'svelte': 'svelte'
|
|
228
|
+
};
|
|
229
|
+
return langMap[ext];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getLanguageColor(language) {
|
|
233
|
+
const colors = {
|
|
234
|
+
javascript: '#f1e05a',
|
|
235
|
+
typescript: '#2b7489',
|
|
236
|
+
python: '#3572A5',
|
|
237
|
+
java: '#b07219',
|
|
238
|
+
html: '#e34c26',
|
|
239
|
+
css: '#563d7c',
|
|
240
|
+
json: '#292929',
|
|
241
|
+
markdown: '#083fa1',
|
|
242
|
+
go: '#00ADD8',
|
|
243
|
+
rust: '#dea584',
|
|
244
|
+
php: '#4F5D95',
|
|
245
|
+
ruby: '#701516',
|
|
246
|
+
other: '#cccccc'
|
|
247
|
+
};
|
|
248
|
+
return colors[language] || colors.other;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatBytes(bytes) {
|
|
252
|
+
if (bytes === 0) return '0 B';
|
|
253
|
+
const k = 1024;
|
|
254
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
255
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
256
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
getFileIcon,
|
|
261
|
+
getLanguageFromExtension,
|
|
262
|
+
getLanguageColor,
|
|
263
|
+
formatBytes
|
|
264
|
+
};
|
package/lib/git.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const octicons = require('@primer/octicons');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Git operations module
|
|
8
|
+
* Handles git status, commits, diffs, and branch operations
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Check if current directory or any parent is a git repository
|
|
12
|
+
function findGitRepo(dir) {
|
|
13
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
const parentDir = path.dirname(dir);
|
|
17
|
+
if (parentDir === dir) {
|
|
18
|
+
return null; // Reached root directory
|
|
19
|
+
}
|
|
20
|
+
return findGitRepo(parentDir);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Git status icon helpers
|
|
24
|
+
function getGitStatusIcon(status) {
|
|
25
|
+
switch (status.trim()) {
|
|
26
|
+
case 'M': return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
|
|
27
|
+
case 'A': return octicons['plus'].toSVG({ class: 'git-status-icon' });
|
|
28
|
+
case 'D': return octicons['dash'].toSVG({ class: 'git-status-icon' });
|
|
29
|
+
case 'R': return octicons['arrow-right'].toSVG({ class: 'git-status-icon' });
|
|
30
|
+
case '??': return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
|
|
31
|
+
case 'MM':
|
|
32
|
+
case 'AM':
|
|
33
|
+
case 'AD': return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
|
|
34
|
+
default: return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getGitStatusDescription(status) {
|
|
39
|
+
switch (status.trim()) {
|
|
40
|
+
case 'M': return 'Modified';
|
|
41
|
+
case 'A': return 'Added';
|
|
42
|
+
case 'D': return 'Deleted';
|
|
43
|
+
case 'R': return 'Renamed';
|
|
44
|
+
case '??': return 'Untracked';
|
|
45
|
+
case 'MM': return 'Modified (staged and unstaged)';
|
|
46
|
+
case 'AM': return 'Added (modified)';
|
|
47
|
+
case 'AD': return 'Added (deleted)';
|
|
48
|
+
default: return `Git status: ${status}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Get git status for files
|
|
53
|
+
function getGitStatus(gitRepoRoot) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
if (!gitRepoRoot) {
|
|
56
|
+
resolve({});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
exec('git status --porcelain', { cwd: gitRepoRoot }, (error, stdout) => {
|
|
61
|
+
if (error) {
|
|
62
|
+
resolve({});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const statusMap = {};
|
|
67
|
+
const lines = stdout.trim().split('\n').filter(line => line);
|
|
68
|
+
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const status = line.substring(0, 2);
|
|
71
|
+
// Git status format: XY filename - find first non-space character after position 2
|
|
72
|
+
const filePath = line.substring(2).replace(/^\s+/, '');
|
|
73
|
+
const absolutePath = path.resolve(gitRepoRoot, filePath);
|
|
74
|
+
statusMap[absolutePath] = {
|
|
75
|
+
status: status.trim(),
|
|
76
|
+
staged: status[0] !== ' ' && status[0] !== '?',
|
|
77
|
+
modified: status[1] !== ' ',
|
|
78
|
+
untracked: status === '??'
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// If this is an untracked directory, mark all files within it as untracked too
|
|
82
|
+
if (status === '??' && fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
|
|
83
|
+
try {
|
|
84
|
+
const markFilesInDirectory = (dirPath) => {
|
|
85
|
+
const entries = fs.readdirSync(dirPath);
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const entryPath = path.join(dirPath, entry);
|
|
88
|
+
const entryStat = fs.statSync(entryPath);
|
|
89
|
+
|
|
90
|
+
statusMap[entryPath] = {
|
|
91
|
+
status: '??',
|
|
92
|
+
staged: false,
|
|
93
|
+
modified: false,
|
|
94
|
+
untracked: true
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Recursively handle subdirectories
|
|
98
|
+
if (entryStat.isDirectory()) {
|
|
99
|
+
markFilesInDirectory(entryPath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
markFilesInDirectory(absolutePath);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// Ignore errors reading directory contents
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
resolve(statusMap);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Get git branch info
|
|
116
|
+
function getGitBranch(gitRepoRoot) {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
if (!gitRepoRoot) {
|
|
119
|
+
resolve(null);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
exec('git branch --show-current', { cwd: gitRepoRoot }, (error, stdout) => {
|
|
124
|
+
if (error) {
|
|
125
|
+
resolve('main');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
resolve(stdout.trim() || 'main');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Commit operations
|
|
134
|
+
async function commitAllChanges(gitRepoRoot, message) {
|
|
135
|
+
const util = require('util');
|
|
136
|
+
const execAsync = util.promisify(exec);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
// Add all changes
|
|
140
|
+
await execAsync('git add -A', { cwd: gitRepoRoot });
|
|
141
|
+
|
|
142
|
+
// Commit with message
|
|
143
|
+
const escapedMessage = message.replace(/"/g, '\\"');
|
|
144
|
+
await execAsync(`git commit -m "${escapedMessage}"`, { cwd: gitRepoRoot });
|
|
145
|
+
|
|
146
|
+
return { success: true, message: 'Changes committed successfully' };
|
|
147
|
+
} catch (gitError) {
|
|
148
|
+
throw new Error(gitError.message || 'Git commit failed');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function commitSelectedFiles(gitRepoRoot, message, files) {
|
|
153
|
+
const util = require('util');
|
|
154
|
+
const execAsync = util.promisify(exec);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Add selected files one by one
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
const escapedFile = file.replace(/"/g, '\\"');
|
|
160
|
+
await execAsync(`git add "${escapedFile}"`, { cwd: gitRepoRoot });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Commit with message
|
|
164
|
+
const escapedMessage = message.replace(/"/g, '\\"');
|
|
165
|
+
await execAsync(`git commit -m "${escapedMessage}"`, { cwd: gitRepoRoot });
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
message: `${files.length} file${files.length === 1 ? '' : 's'} committed successfully`
|
|
170
|
+
};
|
|
171
|
+
} catch (gitError) {
|
|
172
|
+
throw new Error(gitError.message || 'Git commit failed');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Get git diff for a specific file
|
|
177
|
+
function getGitDiff(gitRepoRoot, filePath, staged = false) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const diffCommand = staged ?
|
|
180
|
+
`git diff --cached "${filePath}"` :
|
|
181
|
+
`git diff "${filePath}"`;
|
|
182
|
+
|
|
183
|
+
exec(diffCommand, { cwd: gitRepoRoot }, (error, stdout, stderr) => {
|
|
184
|
+
if (error) {
|
|
185
|
+
return reject(new Error(error.message));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
resolve({
|
|
189
|
+
success: true,
|
|
190
|
+
diff: stdout,
|
|
191
|
+
staged: staged,
|
|
192
|
+
filePath: filePath
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
findGitRepo,
|
|
200
|
+
getGitStatusIcon,
|
|
201
|
+
getGitStatusDescription,
|
|
202
|
+
getGitStatus,
|
|
203
|
+
getGitBranch,
|
|
204
|
+
commitAllChanges,
|
|
205
|
+
commitSelectedFiles,
|
|
206
|
+
getGitDiff
|
|
207
|
+
};
|
package/lib/gitignore.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Gitignore handling module
|
|
6
|
+
* Parses .gitignore files and filters files/directories
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Cache for gitignore rules
|
|
10
|
+
let gitignoreCache = null;
|
|
11
|
+
let gitignoreCacheTime = 0;
|
|
12
|
+
|
|
13
|
+
function parseGitignore(gitignorePath) {
|
|
14
|
+
try {
|
|
15
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
20
|
+
return content
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map(line => line.trim())
|
|
23
|
+
.filter(line => line && !line.startsWith('#'))
|
|
24
|
+
.map(pattern => {
|
|
25
|
+
// Convert gitignore patterns to regex-like matching
|
|
26
|
+
if (pattern.endsWith('/')) {
|
|
27
|
+
// Directory pattern
|
|
28
|
+
return { pattern: pattern.slice(0, -1), isDirectory: true };
|
|
29
|
+
}
|
|
30
|
+
return { pattern, isDirectory: false };
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isIgnoredByGitignore(filePath, gitignoreRules, workingDir, isDirectory = false) {
|
|
38
|
+
if (!gitignoreRules || gitignoreRules.length === 0) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const relativePath = path.relative(workingDir, filePath).replace(/\\/g, '/');
|
|
43
|
+
const pathParts = relativePath.split('/');
|
|
44
|
+
|
|
45
|
+
for (const rule of gitignoreRules) {
|
|
46
|
+
const { pattern, isDirectory: ruleIsDirectory } = rule;
|
|
47
|
+
|
|
48
|
+
// Skip directory rules for files and vice versa (unless rule applies to both)
|
|
49
|
+
if (ruleIsDirectory && !isDirectory) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Simple pattern matching (this is a basic implementation)
|
|
54
|
+
if (pattern.includes('*')) {
|
|
55
|
+
// Wildcard matching
|
|
56
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
57
|
+
if (regex.test(relativePath) || pathParts.some(part => regex.test(part))) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// Exact matching
|
|
62
|
+
if (relativePath === pattern ||
|
|
63
|
+
relativePath.startsWith(pattern + '/') ||
|
|
64
|
+
pathParts.includes(pattern)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getGitignoreRules(workingDir) {
|
|
74
|
+
const gitignorePath = path.join(workingDir, '.gitignore');
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
|
|
77
|
+
// Cache for 5 seconds to avoid excessive file reads
|
|
78
|
+
if (gitignoreCache && (now - gitignoreCacheTime) < 5000) {
|
|
79
|
+
return gitignoreCache;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
gitignoreCache = parseGitignore(gitignorePath);
|
|
83
|
+
gitignoreCacheTime = now;
|
|
84
|
+
return gitignoreCache;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
parseGitignore,
|
|
89
|
+
isIgnoredByGitignore,
|
|
90
|
+
getGitignoreRules
|
|
91
|
+
};
|