git-push-time 1.0.3 → 1.1.1
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/.github/workflows/npm-publish.yml +33 -0
- package/README.md +215 -134
- package/bin/index.js +337 -1004
- package/lib/commands/edit-time.js +141 -0
- package/lib/commands/fix.js +113 -0
- package/lib/commands/github.js +439 -0
- package/lib/commands/pull.js +89 -0
- package/lib/commands/push.js +673 -0
- package/lib/config.js +220 -0
- package/lib/fix.js +286 -260
- package/lib/git.js +492 -452
- package/lib/github.js +322 -0
- package/lib/prompt-helpers.js +62 -0
- package/lib/secrets.js +92 -87
- package/lib/ui.js +804 -728
- package/lib/utils.js +120 -106
- package/package.json +35 -32
package/bin/index.js
CHANGED
|
@@ -1,1004 +1,337 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
*/
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
process.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
step = 1;
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
// Bước 1: repo URL
|
|
342
|
-
if (step === 1) {
|
|
343
|
-
if (options.repo) { repoUrl = options.repo; step = 2; continue; }
|
|
344
|
-
if (!repoUrl) {
|
|
345
|
-
printSection('KẾT NỐI REPOSITORY');
|
|
346
|
-
const val = await inputWithBack({
|
|
347
|
-
message: 'Dán link Git Repository:',
|
|
348
|
-
validate: (value) => {
|
|
349
|
-
if (!value || !value.trim()) return 'Vui lòng nhập đường dẫn repository!';
|
|
350
|
-
if (!isValidGitUrl(value)) return 'URL chưa đúng (vd: https://github.com/user/repo.git)';
|
|
351
|
-
return true;
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
if (val === BACK) {
|
|
355
|
-
// Về bước chọn folder (hoặc về menu nếu có --dir + chạy từ menu)
|
|
356
|
-
if (options.dir) {
|
|
357
|
-
if (cameFromMenu) { await runMainMenuLoop(); continue; }
|
|
358
|
-
step = 1;
|
|
359
|
-
} else { step = 0; }
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
repoUrl = val;
|
|
363
|
-
}
|
|
364
|
-
if (!repoUrl || !isValidGitUrl(repoUrl)) {
|
|
365
|
-
printErrorBox('URL REPOSITORY KHÔNG HỢP LỆ', [`URL nhận được: '${repoUrl || '(trống)'}'`], 'Ví dụ đúng: https://github.com/username/repository.git');
|
|
366
|
-
const retry = await promptContinueOrBack('URL chưa đúng. Bạn muốn nhập lại hay quay lại?');
|
|
367
|
-
if (retry === 'exit') { printCancelled(); process.exit(0); }
|
|
368
|
-
repoUrl = options.repo;
|
|
369
|
-
if (retry === 'back') {
|
|
370
|
-
if (cameFromMenu) await runMainMenuLoop();
|
|
371
|
-
step = options.dir ? 1 : 0;
|
|
372
|
-
}
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
step = 2;
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
// Bước 2: commit message
|
|
379
|
-
if (step === 2) {
|
|
380
|
-
if (options.message) { commitMsg = options.message; step = 3; continue; }
|
|
381
|
-
if (!commitMsg) {
|
|
382
|
-
const val = await inputWithBack({ message: 'Thông điệp commit:', default: 'Initial commit' });
|
|
383
|
-
if (val === BACK) { step = 1; if (!options.repo) repoUrl = undefined; continue; }
|
|
384
|
-
commitMsg = val || 'Initial commit';
|
|
385
|
-
}
|
|
386
|
-
step = 3;
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
// Bước 3: thời gian
|
|
390
|
-
if (step === 3) {
|
|
391
|
-
if (options.date) { dateInput = options.date; step = 4; continue; }
|
|
392
|
-
if (!dateInput) {
|
|
393
|
-
printSection('THỜI GIAN COMMIT');
|
|
394
|
-
const dateOption = await selectWithBack({
|
|
395
|
-
message: 'Chọn thời gian cho commit:',
|
|
396
|
-
choices: [
|
|
397
|
-
{ name: '» Ngay bây giờ', value: 'now', description: 'Dùng thời gian hiện tại' },
|
|
398
|
-
{ name: '» Lùi thời gian', value: 'relative', description: 'VD: -2d, -5h, -30m, -1w' },
|
|
399
|
-
{ name: '» Ngày giờ cụ thể', value: 'custom', description: 'VD: 2024-01-15 14:30:00' }
|
|
400
|
-
]
|
|
401
|
-
});
|
|
402
|
-
if (dateOption === BACK) { step = 2; if (!options.message) commitMsg = undefined; continue; }
|
|
403
|
-
if (dateOption === 'now') {
|
|
404
|
-
dateInput = 'now';
|
|
405
|
-
} else if (dateOption === 'relative') {
|
|
406
|
-
const val = await inputWithBack({
|
|
407
|
-
message: 'Nhập thời gian lùi (VD: -2d, -5h, -30m, -1w):',
|
|
408
|
-
default: '-1d',
|
|
409
|
-
validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Thời gian không hợp lệ'; }
|
|
410
|
-
});
|
|
411
|
-
if (val === BACK) continue; // quay lại chọn kiểu thời gian
|
|
412
|
-
dateInput = val;
|
|
413
|
-
} else if (dateOption === 'custom') {
|
|
414
|
-
const val = await inputWithBack({
|
|
415
|
-
message: 'Nhập ngày giờ (YYYY-MM-DD HH:mm:ss):',
|
|
416
|
-
default: '2024-01-15 10:00:00',
|
|
417
|
-
validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Ngày giờ không hợp lệ'; }
|
|
418
|
-
});
|
|
419
|
-
if (val === BACK) continue;
|
|
420
|
-
dateInput = val;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
parsedDate = parseCustomDate(dateInput || 'now');
|
|
424
|
-
if (!parsedDate.valid) {
|
|
425
|
-
printErrorBox('THỜI GIAN KHÔNG HỢP LỆ', [parsedDate.error], 'VD: now, -2d, -5h, 2024-01-15 14:30:00');
|
|
426
|
-
const retry = await promptContinueOrBack('Thời gian chưa đúng. Bạn muốn nhập lại hay quay lại?');
|
|
427
|
-
if (retry === 'exit') { printCancelled(); process.exit(0); }
|
|
428
|
-
dateInput = options.date;
|
|
429
|
-
if (retry === 'back') { step = 2; if (!options.message) commitMsg = undefined; }
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
if (!dateInput) dateInput = 'now';
|
|
433
|
-
step = 4;
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
// Bước 4: branch
|
|
437
|
-
if (step === 4) {
|
|
438
|
-
if (options.branch) { branchName = options.branch; step = 5; continue; }
|
|
439
|
-
if (!branchName) {
|
|
440
|
-
const currentB = getCurrentBranch(targetDir);
|
|
441
|
-
const val = await inputWithBack({ message: 'Branch trên remote:', default: currentB || 'main' });
|
|
442
|
-
if (val === BACK) { step = 3; if (!options.date) dateInput = undefined; continue; }
|
|
443
|
-
branchName = val || currentB || 'main';
|
|
444
|
-
}
|
|
445
|
-
if (!branchName) branchName = 'main';
|
|
446
|
-
step = 5;
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
// Bước 5: xác nhận — có nút quay về từng bước trước
|
|
450
|
-
if (step === 5) {
|
|
451
|
-
if (!commitMsg) commitMsg = 'Initial commit';
|
|
452
|
-
if (!dateInput) dateInput = 'now';
|
|
453
|
-
if (!branchName) branchName = 'main';
|
|
454
|
-
parsedDate = parseCustomDate(dateInput);
|
|
455
|
-
printSummary({ repoUrl, commitMsg, dateDisplay: parsedDate.display, branch: branchName, force: options.force, sourceDir: targetDir });
|
|
456
|
-
const decision = await select({
|
|
457
|
-
message: 'Xác nhận đẩy lên Git ngay?',
|
|
458
|
-
choices: [
|
|
459
|
-
{ name: '» Tiếp tục đẩy', value: 'continue', description: 'Bắt đầu git init/add/commit/push' },
|
|
460
|
-
{ name: '« Quay lại sửa branch', value: 'back-branch', description: 'Về bước nhập branch' },
|
|
461
|
-
{ name: '« Quay lại sửa thời gian', value: 'back-date', description: 'Về bước chọn thời gian' },
|
|
462
|
-
{ name: '« Quay lại sửa message', value: 'back-msg', description: 'Về bước nhập message' },
|
|
463
|
-
{ name: '« Quay lại sửa repo URL', value: 'back-url', description: 'Về bước nhập URL' },
|
|
464
|
-
{ name: '« Quay lại sửa folder', value: 'back-folder', description: 'Về bước chọn folder' },
|
|
465
|
-
{ name: '« Quay lại menu chính', value: 'back-menu', description: 'Đổi sang Pull / Sửa thời gian / Fix' },
|
|
466
|
-
{ name: 'x Hủy', value: 'exit', description: 'Thoát, không làm gì' }
|
|
467
|
-
]
|
|
468
|
-
});
|
|
469
|
-
if (decision === 'exit') { printCancelled(); process.exit(0); }
|
|
470
|
-
if (decision === 'continue') break;
|
|
471
|
-
if (decision === 'back-branch') { if (!options.branch) branchName = undefined; step = 4; continue; }
|
|
472
|
-
if (decision === 'back-date') { if (!options.date) dateInput = undefined; step = 3; continue; }
|
|
473
|
-
if (decision === 'back-msg') { if (!options.message) commitMsg = undefined; step = 2; continue; }
|
|
474
|
-
if (decision === 'back-url') { if (!options.repo) repoUrl = undefined; step = 1; continue; }
|
|
475
|
-
if (decision === 'back-folder') { step = options.dir ? 1 : 0; if (options.dir && cameFromMenu) { await runMainMenuLoop(); } continue; }
|
|
476
|
-
if (decision === 'back-menu') {
|
|
477
|
-
await runMainMenuLoop();
|
|
478
|
-
if (!options.repo) repoUrl = undefined;
|
|
479
|
-
if (!options.message) commitMsg = undefined;
|
|
480
|
-
if (!options.date) dateInput = undefined;
|
|
481
|
-
if (!options.branch) branchName = undefined;
|
|
482
|
-
step = options.dir ? 1 : 0;
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// 3. Thực thi
|
|
490
|
-
const spinner = ora({ color: 'cyan' });
|
|
491
|
-
printSection(`ĐANG TRIỂN KHAI (${TOTAL_STEPS} bước)`);
|
|
492
|
-
|
|
493
|
-
try {
|
|
494
|
-
// [1/6] Git init
|
|
495
|
-
if (!isGitRepo(targetDir)) {
|
|
496
|
-
spinner.start(stepLabel(1, TOTAL_STEPS, 'Khởi tạo Git repo (git init)...'));
|
|
497
|
-
initGitRepo(targetDir);
|
|
498
|
-
spinner.succeed(stepLabel(1, TOTAL_STEPS, chalk.hex('#34D399')('Đã khởi tạo Git repo')));
|
|
499
|
-
} else {
|
|
500
|
-
console.log(' ' + stepLabel(1, TOTAL_STEPS, chalk.hex('#64748B')('Đã có Git repo sẵn — bỏ qua init')));
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// [2/6] Remote
|
|
504
|
-
spinner.start(stepLabel(2, TOTAL_STEPS, 'Cấu hình remote origin...'));
|
|
505
|
-
setGitRemote(repoUrl, targetDir);
|
|
506
|
-
spinner.succeed(stepLabel(2, TOTAL_STEPS, chalk.hex('#34D399')('Cấu hình remote origin hoàn tất')));
|
|
507
|
-
|
|
508
|
-
// [3/6] Stage
|
|
509
|
-
spinner.start(stepLabel(3, TOTAL_STEPS, 'Stage files (git add .)...'));
|
|
510
|
-
stageAllFiles(targetDir);
|
|
511
|
-
spinner.succeed(stepLabel(3, TOTAL_STEPS, chalk.hex('#34D399')('Đã stage toàn bộ files')));
|
|
512
|
-
|
|
513
|
-
// [3.5] Quét secret: tên file + nội dung
|
|
514
|
-
const risky = findRiskyStagedFiles(targetDir);
|
|
515
|
-
const findings = scanStagedContentForSecrets(targetDir);
|
|
516
|
-
if (risky.length > 0 || findings.length > 0) {
|
|
517
|
-
spinner.stop();
|
|
518
|
-
printPrePushSecretWarning({ riskyFiles: risky, findings });
|
|
519
|
-
const allFiles = [...new Set([...risky, ...findings.map((f) => f.file)])];
|
|
520
|
-
|
|
521
|
-
let wantFix = options.fix === true;
|
|
522
|
-
if (!wantFix && isInteractive) {
|
|
523
|
-
const fixChoice = await selectWithBack({
|
|
524
|
-
message: 'Tự động fix giúp bạn? (git rm --cached + thêm .gitignore, giữ file ở máy)',
|
|
525
|
-
choices: [
|
|
526
|
-
{ name: '» Có, tự động fix', value: 'yes', description: 'Gỡ file secret khỏi stage' },
|
|
527
|
-
{ name: '» Không, giữ nguyên', value: 'no', description: 'Tiếp tục mà không fix' }
|
|
528
|
-
]
|
|
529
|
-
});
|
|
530
|
-
if (fixChoice === BACK) {
|
|
531
|
-
console.log(chalk.hex('#64748B')(' » Bạn chọn quay lại. Hủy push để chạy lại từ bước xác nhận.\n'));
|
|
532
|
-
printCancelled();
|
|
533
|
-
process.exit(0);
|
|
534
|
-
}
|
|
535
|
-
wantFix = fixChoice === 'yes';
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (wantFix) {
|
|
539
|
-
spinner.start('Đang tự fix: gỡ file secret khỏi stage...');
|
|
540
|
-
const res = autoFixStagedSecrets(allFiles, targetDir);
|
|
541
|
-
spinner.succeed(chalk.hex('#34D399')(`[OK] Đã gỡ ${res.fixed.length} file khỏi commit`));
|
|
542
|
-
printFixResult(res);
|
|
543
|
-
|
|
544
|
-
// Đảm bảo .gitignore được stage trước khi kiểm tra hasStagedChanges
|
|
545
|
-
try {
|
|
546
|
-
const { execSync } = await import('child_process');
|
|
547
|
-
execSync('git add .gitignore', { cwd: targetDir, stdio: 'ignore' });
|
|
548
|
-
} catch { /* bỏ qua */ }
|
|
549
|
-
|
|
550
|
-
if (!hasStagedChanges(targetDir)) {
|
|
551
|
-
printErrorBox('KHÔNG CÒN FILE ĐỂ COMMIT', [
|
|
552
|
-
'Sau khi gỡ secret thì không còn file nào để commit.',
|
|
553
|
-
'File secret vẫn nằm ở máy bạn, chỉ là không push lên nữa.',
|
|
554
|
-
]);
|
|
555
|
-
process.exit(0);
|
|
556
|
-
}
|
|
557
|
-
console.log(chalk.hex('#34D399')(' » Tiếp tục commit/push phần sạch còn lại.\n'));
|
|
558
|
-
} else if (isInteractive) {
|
|
559
|
-
const keepChoice = await selectWithBack({
|
|
560
|
-
message: findings.length > 0
|
|
561
|
-
? `Phát hiện ${findings.length} vị trí giống secret. Vẫn tiếp tục mà không fix?`
|
|
562
|
-
: 'Vẫn tiếp tục commit/push với các file trên mà không fix?',
|
|
563
|
-
choices: [
|
|
564
|
-
{ name: '» Vẫn tiếp tục (chịu rủi ro)', value: 'yes', description: 'Tiếp tục mà không fix' },
|
|
565
|
-
{ name: '» Dừng lại để fix tay', value: 'no', description: 'Dừng để xử lý' }
|
|
566
|
-
]
|
|
567
|
-
});
|
|
568
|
-
const keepGoing = keepChoice === 'yes';
|
|
569
|
-
if (keepChoice === BACK || !keepGoing) {
|
|
570
|
-
console.log(chalk.cyan('\n » Chạy lại với --fix để tool tự gỡ giúp bạn, hoặc làm tay:'));
|
|
571
|
-
allFiles.forEach((f) => console.log(chalk.white(` git rm --cached "${f}"`)));
|
|
572
|
-
console.log(chalk.gray(' Sau đó chạy lại tool.\n'));
|
|
573
|
-
process.exit(0);
|
|
574
|
-
}
|
|
575
|
-
console.log(chalk.yellow(' » Bạn chọn tiếp tục với file nhạy cảm. Nếu push bị chặn, tool sẽ hướng dẫn tiếp.\n'));
|
|
576
|
-
} else {
|
|
577
|
-
console.log(chalk.yellow(' [!] Chế độ -y: vẫn tiếp tục dù phát hiện file nhạy cảm. Dùng --fix để tự gỡ.\n'));
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// [4/6] Commit
|
|
582
|
-
spinner.start(stepLabel(4, TOTAL_STEPS, `Tạo commit [${parsedDate.display}]...`));
|
|
583
|
-
if (!hasChangesToCommit(targetDir)) {
|
|
584
|
-
spinner.info(stepLabel(4, TOTAL_STEPS, chalk.hex('#94A3B8')('Không có thay đổi mới — dùng commit hiện có')));
|
|
585
|
-
} else {
|
|
586
|
-
commitWithCustomDate(commitMsg, parsedDate.formatted, targetDir);
|
|
587
|
-
spinner.succeed(stepLabel(4, TOTAL_STEPS, chalk.hex('#34D399')(`Commit hoàn tất • ${chalk.hex('#C084FC').bold(parsedDate.display)}`)));
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// [5/6] Branch
|
|
591
|
-
spinner.start(stepLabel(5, TOTAL_STEPS, `Đổi branch -> '${branchName}'...`));
|
|
592
|
-
setBranchName(branchName, targetDir);
|
|
593
|
-
spinner.succeed(stepLabel(5, TOTAL_STEPS, chalk.hex('#34D399')(`Branch: ${chalk.hex('#5EEAD4').bold(branchName)}`)));
|
|
594
|
-
|
|
595
|
-
// [6/6] Push
|
|
596
|
-
spinner.start(stepLabel(6, TOTAL_STEPS, `Push lên origin/${branchName}...`));
|
|
597
|
-
pushToRemote(branchName, options.force, targetDir);
|
|
598
|
-
spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('Push lên remote thành công!')));
|
|
599
|
-
|
|
600
|
-
printSuccessBox({ branch: branchName, repoUrl });
|
|
601
|
-
|
|
602
|
-
printFooter();
|
|
603
|
-
} catch (error) {
|
|
604
|
-
spinner.stop();
|
|
605
|
-
if (error.code === 'SECRET_BLOCKED') {
|
|
606
|
-
printSecretBlockedBox(error.message, error.details || {});
|
|
607
|
-
|
|
608
|
-
// Kiểm tra xem có commit unpushed chứa vết secret cũ không để hỗ trợ squash tự động
|
|
609
|
-
const unpushed = getUnpushedCommits(branchName, targetDir);
|
|
610
|
-
if (unpushed.length > 0) {
|
|
611
|
-
console.log(chalk.cyanBright(`\n » Phát hiện bạn có ${unpushed.length} commit chưa push ở local có thể chứa vết secret cũ.`));
|
|
612
|
-
let doSquash = options.fix === true;
|
|
613
|
-
if (!doSquash && isInteractive) {
|
|
614
|
-
const sqChoice = await selectWithBack({
|
|
615
|
-
message: 'Tự động làm sạch lịch sử (squash các commit unpushed thành 1 commit sạch) để push lại?',
|
|
616
|
-
choices: [
|
|
617
|
-
{ name: '» Có, squash và push lại', value: 'yes', description: 'Làm sạch lịch sử' },
|
|
618
|
-
{ name: '» Không, để tự xử lý', value: 'no', description: 'Bỏ qua' }
|
|
619
|
-
]
|
|
620
|
-
});
|
|
621
|
-
doSquash = sqChoice === 'yes';
|
|
622
|
-
}
|
|
623
|
-
if (doSquash) {
|
|
624
|
-
spinner.start('Đang squash và làm sạch commit chưa push...');
|
|
625
|
-
const sqRes = squashUnpushedCommits(branchName, commitMsg, parsedDate.formatted, targetDir);
|
|
626
|
-
if (sqRes.ok) {
|
|
627
|
-
spinner.succeed(chalk.green('[OK] Đã làm sạch commit! Đang thử push lại...'));
|
|
628
|
-
try {
|
|
629
|
-
spinner.start(stepLabel(6, TOTAL_STEPS, `Push lại lên origin/${branchName}...`));
|
|
630
|
-
pushToRemote(branchName, options.force, targetDir);
|
|
631
|
-
spinner.succeed(chalk.bold.green('[OK] Push thành công'));
|
|
632
|
-
printSuccessBox({ branch: branchName, repoUrl });
|
|
633
|
-
printFooter();
|
|
634
|
-
return;
|
|
635
|
-
} catch (retryErr) {
|
|
636
|
-
spinner.fail(chalk.red(`Push lại thất bại: ${retryErr.message}`));
|
|
637
|
-
}
|
|
638
|
-
} else {
|
|
639
|
-
spinner.fail(chalk.red(`Không thể tự động squash: ${sqRes.error}`));
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
} else if (error.code === 'AUTH_FAILED') {
|
|
644
|
-
printAuthFailedBox(error.message);
|
|
645
|
-
} else if (error.code === 'NON_FAST_FORWARD') {
|
|
646
|
-
printNonFastForwardBox(error.message, branchName);
|
|
647
|
-
if (isInteractive) {
|
|
648
|
-
const ffChoice = await select({
|
|
649
|
-
message: 'Remote đã có commit mới hơn. Bạn muốn xử lý thế nào?',
|
|
650
|
-
choices: [
|
|
651
|
-
{ name: '» Pull về (rebase) rồi push lại — Tiếp tục', value: 'pull-push', description: `git pull --rebase origin ${branchName} rồi push lại` },
|
|
652
|
-
{ name: '» Chỉ Pull về (chưa push vội)', value: 'pull-only', description: 'Chỉ đồng bộ code về máy' },
|
|
653
|
-
{ name: '« Quay lại menu chính', value: 'back', description: 'Về màn hình chọn Push / Pull / Fix' },
|
|
654
|
-
{ name: 'x Thoát (tự xử lý tay)', value: 'exit', description: 'Không làm gì thêm' }
|
|
655
|
-
]
|
|
656
|
-
});
|
|
657
|
-
if (ffChoice === 'pull-push' || ffChoice === 'pull-only') {
|
|
658
|
-
try {
|
|
659
|
-
spinner.start(`Đang pull --rebase origin/${branchName}...`);
|
|
660
|
-
const pullOut = pullRebaseFromRemote('origin', branchName, targetDir);
|
|
661
|
-
spinner.succeed(chalk.hex('#34D399')(`Đã pull --rebase origin/${branchName} xong`));
|
|
662
|
-
if (pullOut) console.log(chalk.hex('#64748B')(` ${String(pullOut).split('\n')[0].slice(0, 120)}`));
|
|
663
|
-
if (ffChoice === 'pull-push') {
|
|
664
|
-
spinner.start(stepLabel(6, TOTAL_STEPS, `Push lại lên origin/${branchName}...`));
|
|
665
|
-
pushToRemote(branchName, options.force, targetDir);
|
|
666
|
-
spinner.succeed(chalk.bold.hex('#34D399')('Push lại thành công sau khi pull!'));
|
|
667
|
-
printSuccessBox({ branch: branchName, repoUrl });
|
|
668
|
-
printFooter();
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
console.log(chalk.hex('#34D399')('\n [OK] Đã pull code mới nhất về. Chạy lại tool để push khi bạn sẵn sàng.\n'));
|
|
672
|
-
const afterPull = await promptContinueOrBack('Bạn muốn quay lại menu chính hay thoát?');
|
|
673
|
-
if (afterPull !== 'exit') {
|
|
674
|
-
await runMainMenuLoop();
|
|
675
|
-
}
|
|
676
|
-
return;
|
|
677
|
-
} catch (pullErr) {
|
|
678
|
-
spinner.fail(chalk.red(`Pull --rebase thất bại: ${String(pullErr.message).split('\n')[0]}`));
|
|
679
|
-
printErrorBox('PULL --REBASE THẤT BẠI', [String(pullErr.message).split('\n')[0]], 'Có thể bị conflict. Giải quyết conflict (git status) rồi chạy lại tool.');
|
|
680
|
-
const retry = await promptContinueOrBack('Pull lỗi. Bạn muốn quay lại menu hay thoát?');
|
|
681
|
-
if (retry !== 'exit') {
|
|
682
|
-
await runMainMenuLoop();
|
|
683
|
-
}
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
} else if (ffChoice === 'back') {
|
|
687
|
-
await runMainMenuLoop();
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
} else if (error.code === 'REMOTE_OR_NETWORK') {
|
|
692
|
-
printErrorBox('LỖI KẾT NỐI REMOTE', [String(error.message).split('\n')[0]], 'Kiểm tra URL repo, mạng, và quyền truy cập.');
|
|
693
|
-
} else {
|
|
694
|
-
printErrorBox('ĐẨY LÊN GIT THẤT BẠI', [error.message]);
|
|
695
|
-
}
|
|
696
|
-
process.exit(1);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Lệnh fix độc lập: `git-push-time fix`
|
|
702
|
-
* Tự động quét và fix secret ở staged, working directory và unpushed commits.
|
|
703
|
-
*/
|
|
704
|
-
async function runFixCommand(options = {}) {
|
|
705
|
-
const fromMenu = options.fromMenu === true;
|
|
706
|
-
if (!fromMenu) printBanner(VERSION);
|
|
707
|
-
printSection('TỰ ĐỘNG FIX SECRET + BẢO VỆ REPO');
|
|
708
|
-
|
|
709
|
-
if (!checkGitInstalled()) {
|
|
710
|
-
printGitMissing();
|
|
711
|
-
if (fromMenu) return;
|
|
712
|
-
process.exit(1);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
let targetDir = process.cwd();
|
|
716
|
-
if (options.dir) {
|
|
717
|
-
const r = resolveTargetDir(options.dir, process.cwd());
|
|
718
|
-
if (!r.ok) {
|
|
719
|
-
printErrorBox('THƯ MỤC KHÔNG HỢP LỆ', [r.error]);
|
|
720
|
-
if (fromMenu) return;
|
|
721
|
-
process.exit(1);
|
|
722
|
-
}
|
|
723
|
-
targetDir = r.fullPath;
|
|
724
|
-
}
|
|
725
|
-
printTargetDir(targetDir);
|
|
726
|
-
|
|
727
|
-
if (!isGitRepo(targetDir)) {
|
|
728
|
-
printErrorBox('CHƯA PHẢI GIT REPOSITORY', [`Thư mục '${targetDir}' chưa phải là Git repository.`], 'Chạy git init hoặc cd vào đúng thư mục dự án / dùng --dir.');
|
|
729
|
-
if (fromMenu) return;
|
|
730
|
-
process.exit(1);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const spinner = ora({ color: 'cyan' });
|
|
734
|
-
spinner.start('Đang kiểm tra staged files và thư mục làm việc…');
|
|
735
|
-
|
|
736
|
-
const risky = findRiskyStagedFiles(targetDir);
|
|
737
|
-
const findings = scanStagedContentForSecrets(targetDir);
|
|
738
|
-
const riskyWorking = findRiskyWorkingTreeFiles(targetDir);
|
|
739
|
-
spinner.stop();
|
|
740
|
-
|
|
741
|
-
const allDetected = [...new Set([...risky, ...findings.map((f) => f.file), ...riskyWorking])];
|
|
742
|
-
|
|
743
|
-
if (allDetected.length === 0) {
|
|
744
|
-
printSection('KẾT QUẢ QUÉT');
|
|
745
|
-
console.log(chalk.hex('#34D399')(' [OK] Không phát hiện file secret mới ở stage hay thư mục làm việc!\n'));
|
|
746
|
-
} else {
|
|
747
|
-
printPrePushSecretWarning({ riskyFiles: allDetected, findings });
|
|
748
|
-
spinner.start('Đang tự động fix (gỡ khỏi stage + thêm .gitignore)...');
|
|
749
|
-
const res = autoFixStagedSecrets(allDetected, targetDir);
|
|
750
|
-
spinner.succeed(chalk.hex('#34D399')(`[OK] Đã xử lý xong ${res.fixed.length} file!`));
|
|
751
|
-
printFixResult(res);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// Kiểm tra xem có commit chưa push nào bị dính vết secret cũ không
|
|
755
|
-
const currentBranch = getCurrentBranch(targetDir);
|
|
756
|
-
const unpushed = getUnpushedCommits(currentBranch, targetDir);
|
|
757
|
-
if (unpushed.length > 0) {
|
|
758
|
-
printSection(`${unpushed.length} COMMIT CHƯA PUSH`);
|
|
759
|
-
unpushed.slice(0, 5).forEach((c) => console.log(chalk.hex('#94A3B8')(` - ${c}`)));
|
|
760
|
-
if (unpushed.length > 5) {
|
|
761
|
-
console.log(chalk.hex('#64748B')(` ...và ${unpushed.length - 5} commit khác`));
|
|
762
|
-
}
|
|
763
|
-
const sqChoice = await selectWithBack({
|
|
764
|
-
message: `Bạn có muốn gộp (squash) ${unpushed.length} commit chưa push thành 1 commit sạch để xóa dấu vết secret cũ không? [chọn « Quay lại để bỏ qua]`,
|
|
765
|
-
choices: [
|
|
766
|
-
{ name: '» Có, squash thành 1 commit sạch', value: 'yes', description: 'Làm sạch lịch sử' },
|
|
767
|
-
{ name: '» Không, giữ nguyên', value: 'no', description: 'Bỏ qua' }
|
|
768
|
-
]
|
|
769
|
-
});
|
|
770
|
-
const okSquash = sqChoice === 'yes';
|
|
771
|
-
if (sqChoice !== BACK && okSquash) {
|
|
772
|
-
spinner.start('Đang làm sạch lịch sử commit chưa push...');
|
|
773
|
-
const sq = squashUnpushedCommits(currentBranch, 'Clean commit without secrets', null, targetDir);
|
|
774
|
-
if (sq.ok) {
|
|
775
|
-
spinner.succeed(chalk.hex('#34D399')('[OK] Đã làm sạch commit chưa push! Sẵn sàng push lên remote.'));
|
|
776
|
-
} else {
|
|
777
|
-
spinner.fail(chalk.red(`Không thể squash: ${sq.error}`));
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
printSuccessBox({ branch: currentBranch || 'main', repoUrl: 'local repo đã sạch — sẵn sàng push' });
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
async function runPullCommand(remote = 'origin', branch, options = {}) {
|
|
786
|
-
const fromMenu = options.fromMenu === true;
|
|
787
|
-
if (!fromMenu) printBanner(VERSION);
|
|
788
|
-
if (!checkGitInstalled()) {
|
|
789
|
-
printGitMissing();
|
|
790
|
-
if (fromMenu) return;
|
|
791
|
-
process.exit(1);
|
|
792
|
-
}
|
|
793
|
-
let targetDir = process.cwd();
|
|
794
|
-
if (options.dir) {
|
|
795
|
-
const r = resolveTargetDir(options.dir, process.cwd());
|
|
796
|
-
if (!r.ok) {
|
|
797
|
-
printErrorBox('THƯ MỤC KHÔNG HỢP LỆ', [r.error]);
|
|
798
|
-
if (fromMenu) return;
|
|
799
|
-
process.exit(1);
|
|
800
|
-
}
|
|
801
|
-
targetDir = r.fullPath;
|
|
802
|
-
}
|
|
803
|
-
if (!branch) branch = getCurrentBranch(targetDir);
|
|
804
|
-
if (!isGitRepo(targetDir)) {
|
|
805
|
-
printErrorBox('CHƯA PHẢI GIT REPOSITORY', [`Thư mục '${targetDir}' chưa phải Git repository.`]);
|
|
806
|
-
if (fromMenu) return;
|
|
807
|
-
process.exit(1);
|
|
808
|
-
}
|
|
809
|
-
if (!branch) {
|
|
810
|
-
printErrorBox('KHÔNG XÁC ĐỊNH ĐƯỢC BRANCH', ['Đang ở detached HEAD nên không biết pull branch nào.'], 'Checkout về branch chính rồi chạy lại.');
|
|
811
|
-
if (fromMenu) return;
|
|
812
|
-
process.exit(1);
|
|
813
|
-
}
|
|
814
|
-
// Ở chế độ menu luôn hỏi Tiếp tục / Quay lại; ở chế độ lệnh trực tiếp cũng hỏi nếu là terminal tương tác.
|
|
815
|
-
const isInteractive = !options.yes;
|
|
816
|
-
try {
|
|
817
|
-
// Luôn fast-forward để tránh tự tạo merge commit ngoài ý muốn.
|
|
818
|
-
printSection(`PULL --ff-only ${remote}/${branch}`);
|
|
819
|
-
printTargetDir(targetDir);
|
|
820
|
-
const result = pullFromRemote(remote, branch, targetDir);
|
|
821
|
-
console.log(chalk.hex('#34D399')(` [OK] ${result || 'Repository đã cập nhật (fast-forward).'}\n`));
|
|
822
|
-
if (!fromMenu) printFooter();
|
|
823
|
-
} catch (error) {
|
|
824
|
-
printErrorBox('PULL THẤT BẠI', [String(error.message).split('\n')[0]], 'Pull --ff-only thất bại khi lịch sử bị lệch. Có thể thử pull --rebase.');
|
|
825
|
-
if (!isInteractive) {
|
|
826
|
-
if (fromMenu) return;
|
|
827
|
-
process.exit(1);
|
|
828
|
-
}
|
|
829
|
-
const choice = await select({
|
|
830
|
-
message: 'Pull fast-forward thất bại. Bạn muốn làm gì tiếp?',
|
|
831
|
-
choices: [
|
|
832
|
-
{ name: '» Tiếp tục: thử pull --rebase', value: 'rebase', description: `git pull --rebase ${remote} ${branch}` },
|
|
833
|
-
{ name: '« Quay lại menu chính', value: 'back', description: 'Về màn hình chọn hành động' },
|
|
834
|
-
{ name: 'x Thoát (tự xử lý tay)', value: 'exit', description: 'Không làm gì thêm' }
|
|
835
|
-
]
|
|
836
|
-
});
|
|
837
|
-
if (choice === 'rebase') {
|
|
838
|
-
try {
|
|
839
|
-
console.log(chalk.hex('#38BDF8')(`\n » Đang thử: git pull --rebase ${remote} ${branch}...`));
|
|
840
|
-
const out = pullRebaseFromRemote(remote, branch, targetDir);
|
|
841
|
-
console.log(chalk.hex('#34D399')(` [OK] Pull --rebase thành công! ${String(out).split('\n')[0].slice(0, 100)}\n`));
|
|
842
|
-
if (!fromMenu) printFooter();
|
|
843
|
-
} catch (rebaseErr) {
|
|
844
|
-
printErrorBox('PULL --REBASE THẤT BẠI', [String(rebaseErr.message).split('\n')[0]], 'Có thể bị conflict. Chạy git status để giải quyết rồi pull lại.');
|
|
845
|
-
if (!fromMenu) process.exit(1);
|
|
846
|
-
}
|
|
847
|
-
} else if (choice === 'back') {
|
|
848
|
-
if (!fromMenu) {
|
|
849
|
-
await runMainMenuLoop();
|
|
850
|
-
}
|
|
851
|
-
// Nếu đã ở trong menu thì return để caller hỏi Tiếp tục/Quay lại.
|
|
852
|
-
return;
|
|
853
|
-
} else {
|
|
854
|
-
if (!fromMenu) process.exit(1);
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
async function runEditCommitTimeCommand(options = {}) {
|
|
861
|
-
const fromMenu = options.fromMenu === true;
|
|
862
|
-
if (!fromMenu) printBanner(VERSION);
|
|
863
|
-
let targetDir = process.cwd();
|
|
864
|
-
if (options.dir) {
|
|
865
|
-
const r = resolveTargetDir(options.dir, process.cwd());
|
|
866
|
-
if (!r.ok) {
|
|
867
|
-
printErrorBox('THƯ MỤC KHÔNG HỢP LỆ', [r.error]);
|
|
868
|
-
if (fromMenu) return;
|
|
869
|
-
process.exit(1);
|
|
870
|
-
}
|
|
871
|
-
targetDir = r.fullPath;
|
|
872
|
-
}
|
|
873
|
-
if (!checkGitInstalled() || !isGitRepo(targetDir)) {
|
|
874
|
-
printErrorBox('CHƯA SẴN SÀNG', [`Hãy chạy lệnh trong một Git repository có Git đã cài. (Folder: ${targetDir})`]);
|
|
875
|
-
if (fromMenu) return;
|
|
876
|
-
process.exit(1);
|
|
877
|
-
}
|
|
878
|
-
printTargetDir(targetDir);
|
|
879
|
-
try {
|
|
880
|
-
const commits = getRecentCommits(100, targetDir);
|
|
881
|
-
if (commits.length === 0) {
|
|
882
|
-
printErrorBox('CHƯA CÓ COMMIT', ['Repository chưa có commit nào.']);
|
|
883
|
-
if (fromMenu) return;
|
|
884
|
-
process.exit(1);
|
|
885
|
-
}
|
|
886
|
-
// Bước 1: chọn commit — luôn có nút « Quay lại
|
|
887
|
-
let commit = commits[0];
|
|
888
|
-
if (commits.length > 1) {
|
|
889
|
-
const picked = await selectWithBack({
|
|
890
|
-
message: `Chọn commit cần đổi thời gian (${commits.length} commit gần nhất):`,
|
|
891
|
-
pageSize: 12,
|
|
892
|
-
choices: commits.map((item) => ({
|
|
893
|
-
name: `» ${item.date} ${item.shortHash} ${item.subject}`,
|
|
894
|
-
value: item,
|
|
895
|
-
description: 'Sửa commit này sẽ viết lại nó + các commit phía sau'
|
|
896
|
-
}))
|
|
897
|
-
});
|
|
898
|
-
if (picked === BACK) return; // về menu chính (caller sẽ hỏi Tiếp tục/Quay lại)
|
|
899
|
-
commit = picked;
|
|
900
|
-
} else {
|
|
901
|
-
console.log(chalk.hex('#94A3B8')(` » Chỉ có 1 commit nên tự chọn: ${commit.shortHash} — ${commit.subject}\n`));
|
|
902
|
-
}
|
|
903
|
-
// Bước 2: chọn kiểu thời gian — luôn có nút « Quay lại
|
|
904
|
-
const dateOption = await selectWithBack({
|
|
905
|
-
message: 'Chọn thời gian mới: [chọn « Quay lại để đổi commit]',
|
|
906
|
-
choices: [
|
|
907
|
-
{ name: '» Ngay bây giờ', value: 'now', description: 'Dùng thời gian hiện tại' },
|
|
908
|
-
{ name: '» Nhập thời gian tương đối', value: 'relative', description: 'VD: -2d, -5h, -30m' },
|
|
909
|
-
{ name: '» Nhập ngày giờ cụ thể', value: 'custom', description: 'VD: 2024-01-15 14:30:00' }
|
|
910
|
-
]
|
|
911
|
-
});
|
|
912
|
-
if (dateOption === BACK) {
|
|
913
|
-
// Quay lại bước chọn commit: gọi đệ quy 1 lần để chọn lại
|
|
914
|
-
return await runEditCommitTimeCommand(options);
|
|
915
|
-
}
|
|
916
|
-
let dateInput = dateOption;
|
|
917
|
-
if (dateOption === 'relative') {
|
|
918
|
-
const val = await inputWithBack({
|
|
919
|
-
message: 'Nhập thời gian (ví dụ: -2d, -5h, -30m):',
|
|
920
|
-
default: '-1d',
|
|
921
|
-
validate: (value) => {
|
|
922
|
-
const parsed = parseCustomDate(value);
|
|
923
|
-
return parsed.valid || parsed.error;
|
|
924
|
-
}
|
|
925
|
-
});
|
|
926
|
-
if (val === BACK) return await runEditCommitTimeCommand(options);
|
|
927
|
-
dateInput = val;
|
|
928
|
-
} else if (dateOption === 'custom') {
|
|
929
|
-
const val = await inputWithBack({
|
|
930
|
-
message: 'Nhập ngày giờ (YYYY-MM-DD HH:mm:ss):',
|
|
931
|
-
validate: (value) => {
|
|
932
|
-
const parsed = parseCustomDate(value);
|
|
933
|
-
return parsed.valid || parsed.error;
|
|
934
|
-
}
|
|
935
|
-
});
|
|
936
|
-
if (val === BACK) return await runEditCommitTimeCommand(options);
|
|
937
|
-
dateInput = val;
|
|
938
|
-
}
|
|
939
|
-
const parsed = parseCustomDate(dateInput);
|
|
940
|
-
if (!parsed.valid) {
|
|
941
|
-
printErrorBox('THỜI GIAN KHÔNG HỢP LỆ', [parsed.error], 'VD: now, -2d, -5h, 2024-01-15 14:30:00');
|
|
942
|
-
if (fromMenu) return;
|
|
943
|
-
process.exit(1);
|
|
944
|
-
}
|
|
945
|
-
const currentBranch = getCurrentBranch(targetDir);
|
|
946
|
-
const result = rewriteCommitDate(commit.hash, parsed.formatted, targetDir);
|
|
947
|
-
printSection('ĐÃ ĐỔI THỜI GIAN COMMIT');
|
|
948
|
-
console.log(` Branch: ${currentBranch}`);
|
|
949
|
-
console.log(` Commit: ${commit.shortHash} — ${commit.subject}`);
|
|
950
|
-
console.log(` Thời gian mới: ${parsed.display}`);
|
|
951
|
-
console.log(` Đã viết lại ${result.rewrittenCount} commit trên branch hiện tại (hash cũ đã đổi).`);
|
|
952
|
-
console.log(` Nhánh backup giữ bản gốc: ${result.backupBranch} (${result.backupRef})`);
|
|
953
|
-
console.log(chalk.hex('#FBBF24')(' [!] Nếu commit đã push, cần force push để cập nhật remote.\n'));
|
|
954
|
-
} catch (error) {
|
|
955
|
-
printErrorBox('KHÔNG THỂ ĐỔI THỜI GIAN', [error.message], 'Working tree phải sạch và commit phải nằm trên branch hiện tại.');
|
|
956
|
-
if (fromMenu) return;
|
|
957
|
-
process.exit(1);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
const program = new Command();
|
|
962
|
-
styleCliHelp(program);
|
|
963
|
-
|
|
964
|
-
program
|
|
965
|
-
.name('git-push-time')
|
|
966
|
-
.description('Đẩy dự án lên Git siêu tốc + tùy chỉnh thời gian commit (Backdate Commit)')
|
|
967
|
-
.version(VERSION)
|
|
968
|
-
.option('-r, --repo <url>', 'Đường dẫn Git repository (URL)')
|
|
969
|
-
.option('-d, --date <datetime>', 'Thời gian commit (Ví dụ: "2024-01-15 14:30:00", "-2d", "-5h", "now")')
|
|
970
|
-
.option('-m, --message <msg>', 'Thông điệp commit (Commit message)')
|
|
971
|
-
.option('-b, --branch <branch>', 'Tên branch trên remote (Mặc định: main)')
|
|
972
|
-
.option('-C, --dir <path>', 'Folder nguồn cần push (mặc định: thư mục hiện tại; interactive sẽ cho chọn)')
|
|
973
|
-
.option('-f, --force', 'Thực hiện push đè (--force)', false)
|
|
974
|
-
.option('-y, --yes', 'Bỏ qua prompt tương tác, tự động dùng tham số hoặc giá trị mặc định', false)
|
|
975
|
-
.option('--fix', 'Tự động gỡ file secret khỏi stage + thêm .gitignore (an toàn, không xóa file thật)', false)
|
|
976
|
-
.action(async (opts) => {
|
|
977
|
-
await run(opts);
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
program
|
|
981
|
-
.command('fix')
|
|
982
|
-
.description('Tự động quét và gỡ file secret khỏi stage, thêm vào .gitignore và làm sạch commit chưa push')
|
|
983
|
-
.option('-C, --dir <path>', 'Folder cần fix (mặc định: thư mục hiện tại)')
|
|
984
|
-
.action(async (opts) => {
|
|
985
|
-
await runFixCommand(opts);
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
program
|
|
989
|
-
.command('pull [remote] [branch]')
|
|
990
|
-
.description('Kéo code từ remote bằng fast-forward --ff-only (mặc định: origin và branch hiện tại)')
|
|
991
|
-
.option('-C, --dir <path>', 'Folder repo cần pull (mặc định: thư mục hiện tại)')
|
|
992
|
-
.action(async (remote, branch, opts) => {
|
|
993
|
-
await runPullCommand(remote || 'origin', branch, opts || {});
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
program
|
|
997
|
-
.command('edit-time')
|
|
998
|
-
.description('Sửa thời gian commit (viết lại hash + descendants, tự tạo nhánh backup, yêu cầu working tree sạch)')
|
|
999
|
-
.option('-C, --dir <path>', 'Folder repo cần sửa (mặc định: thư mục hiện tại)')
|
|
1000
|
-
.action(async (opts) => {
|
|
1001
|
-
await runEditCommitTimeCommand(opts);
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
program.parse(process.argv);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { select } from '@inquirer/prompts';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { checkGitInstalled, getCurrentBranch } from '../lib/git.js';
|
|
9
|
+
import { getCurrentGitHubUser } from '../lib/github.js';
|
|
10
|
+
import { runPushWizard } from '../lib/commands/push.js';
|
|
11
|
+
import { runPullCommand } from '../lib/commands/pull.js';
|
|
12
|
+
import { runEditCommitTimeCommand } from '../lib/commands/edit-time.js';
|
|
13
|
+
import { runFixCommand } from '../lib/commands/fix.js';
|
|
14
|
+
import {
|
|
15
|
+
runGitHubMenu,
|
|
16
|
+
promptCreateRepo,
|
|
17
|
+
showUserRepos,
|
|
18
|
+
} from '../lib/commands/github.js';
|
|
19
|
+
import { promptContinueOrBack } from '../lib/prompt-helpers.js';
|
|
20
|
+
import {
|
|
21
|
+
printBanner,
|
|
22
|
+
printDashboardStatus,
|
|
23
|
+
printSection,
|
|
24
|
+
printGitMissing,
|
|
25
|
+
printCancelled,
|
|
26
|
+
styleCliHelp
|
|
27
|
+
} from '../lib/ui.js';
|
|
28
|
+
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
|
|
32
|
+
function getPackageVersion() {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8');
|
|
35
|
+
return JSON.parse(raw).version || '1.1.0';
|
|
36
|
+
} catch {
|
|
37
|
+
return '1.1.0';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const VERSION = getPackageVersion();
|
|
42
|
+
|
|
43
|
+
function isExitPromptError(err) {
|
|
44
|
+
const msg = String(err?.message || '');
|
|
45
|
+
return (
|
|
46
|
+
err?.name === 'ExitPromptError' ||
|
|
47
|
+
msg.includes('User force closed the prompt') ||
|
|
48
|
+
msg.includes('SIGINT')
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Kiểm tra xem lệnh có được gọi "trần" (không flag/subcommand nào) hay không.
|
|
54
|
+
*/
|
|
55
|
+
function isBareInvocation(options = {}) {
|
|
56
|
+
return (
|
|
57
|
+
!options.repo &&
|
|
58
|
+
!options.message &&
|
|
59
|
+
!options.date &&
|
|
60
|
+
!options.branch &&
|
|
61
|
+
!options.dir &&
|
|
62
|
+
!options.force &&
|
|
63
|
+
!options.fix
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Menu chính khi chạy `git-push-time` không truyền flag
|
|
69
|
+
*/
|
|
70
|
+
async function promptMainMenu() {
|
|
71
|
+
printSection('BẢNG ĐIỀU KHIỂN CHÍNH');
|
|
72
|
+
return await select({
|
|
73
|
+
message: 'Bạn muốn thực hiện thao tác nào?',
|
|
74
|
+
pageSize: 10,
|
|
75
|
+
choices: [
|
|
76
|
+
{
|
|
77
|
+
name: 'Đẩy dự án lên Git',
|
|
78
|
+
value: 'push',
|
|
79
|
+
description: 'Chọn repository, thời gian commit và đẩy code'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Quản lý GitHub',
|
|
83
|
+
value: 'github',
|
|
84
|
+
description: 'Tạo repository, xem tài khoản và quản lý kết nối'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'Pull code mới',
|
|
88
|
+
value: 'pull',
|
|
89
|
+
description: 'Cập nhật branch hiện tại từ remote'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'Sửa thời gian commit',
|
|
93
|
+
value: 'edit-time',
|
|
94
|
+
description: 'Chọn commit trong lịch sử và cập nhật ngày giờ'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'Quét secret',
|
|
98
|
+
value: 'fix',
|
|
99
|
+
description: 'Tìm file nhạy cảm trước khi đẩy code'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'Thoát',
|
|
103
|
+
value: 'exit',
|
|
104
|
+
description: 'Thoát ứng dụng'
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Vòng lặp Menu chính (không đệ quy — callback back chỉ trả sentinel)
|
|
112
|
+
*/
|
|
113
|
+
async function runMainMenuLoop(initialOptions = {}) {
|
|
114
|
+
while (true) {
|
|
115
|
+
const cwd = process.cwd();
|
|
116
|
+
let branch = 'main';
|
|
117
|
+
try {
|
|
118
|
+
branch = getCurrentBranch(cwd);
|
|
119
|
+
} catch {}
|
|
120
|
+
let ghStatus = { authenticated: false };
|
|
121
|
+
try {
|
|
122
|
+
ghStatus = await getCurrentGitHubUser();
|
|
123
|
+
} catch {
|
|
124
|
+
ghStatus = { authenticated: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
printDashboardStatus({
|
|
128
|
+
targetDir: cwd,
|
|
129
|
+
currentBranch: branch,
|
|
130
|
+
githubUser: ghStatus.authenticated ? ghStatus.user : null
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let action;
|
|
134
|
+
try {
|
|
135
|
+
action = await promptMainMenu();
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (isExitPromptError(err)) {
|
|
138
|
+
printCancelled();
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (action === 'exit') {
|
|
145
|
+
printCancelled();
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
if (action === 'push') {
|
|
151
|
+
const res = await runPushWizard(initialOptions, async () => 'back');
|
|
152
|
+
// 'back' = user muốn về menu → tiếp tục loop; ngược lại push xong thì thoát
|
|
153
|
+
if (res === 'back') continue;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (action === 'github') {
|
|
158
|
+
const ghResult = await runGitHubMenu({ defaultDir: cwd });
|
|
159
|
+
if (ghResult && ghResult.autoPush && ghResult.repoUrl) {
|
|
160
|
+
// Sau khi tạo repo mới, chuyển tiếp thẳng vào luồng push
|
|
161
|
+
const res = await runPushWizard({ ...initialOptions, repo: ghResult.repoUrl }, async () => 'back');
|
|
162
|
+
if (res === 'back') continue;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
} else if (action === 'pull') {
|
|
166
|
+
await runPullCommand('origin', undefined, { fromMenu: true });
|
|
167
|
+
} else if (action === 'edit-time') {
|
|
168
|
+
await runEditCommitTimeCommand({ fromMenu: true });
|
|
169
|
+
} else if (action === 'fix') {
|
|
170
|
+
await runFixCommand({ fromMenu: true });
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (isExitPromptError(err)) {
|
|
174
|
+
printCancelled();
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let next;
|
|
181
|
+
try {
|
|
182
|
+
next = await promptContinueOrBack('Hoàn tất! Bạn muốn tiếp tục hay quay lại bảng điều khiển?');
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (isExitPromptError(err)) {
|
|
185
|
+
printCancelled();
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
if (next === 'exit') {
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function main(options = {}) {
|
|
197
|
+
printBanner(VERSION);
|
|
198
|
+
|
|
199
|
+
if (!checkGitInstalled()) {
|
|
200
|
+
printGitMissing();
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const isInteractive = !options.yes;
|
|
205
|
+
|
|
206
|
+
if (isInteractive && isBareInvocation(options)) {
|
|
207
|
+
try {
|
|
208
|
+
await runMainMenuLoop(options);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (isExitPromptError(err)) {
|
|
211
|
+
printCancelled();
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Chạy trực tiếp push wizard với các options được cung cấp
|
|
220
|
+
try {
|
|
221
|
+
await runPushWizard(options);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (isExitPromptError(err)) {
|
|
224
|
+
printCancelled();
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ==========================================================================
|
|
232
|
+
COMMANDER CLI CONFIGURATION
|
|
233
|
+
========================================================================== */
|
|
234
|
+
|
|
235
|
+
const program = new Command();
|
|
236
|
+
styleCliHelp(program);
|
|
237
|
+
|
|
238
|
+
program
|
|
239
|
+
.name('git-push-time')
|
|
240
|
+
.description('Bộ công cụ Git & GitHub CLI: Đẩy code siêu tốc, kết nối GitHub tạo repo, chỉnh thời gian commit và bảo vệ secret')
|
|
241
|
+
.version(VERSION)
|
|
242
|
+
.option('-r, --repo <url>', 'Đường dẫn Git repository (URL hoặc để trống để chọn/tạo mới)')
|
|
243
|
+
.option('-d, --date <datetime>', 'Thời gian commit (VD: "2024-01-15 14:30:00", "-2d", "-5h", "now")')
|
|
244
|
+
.option('-m, --message <msg>', 'Thông điệp commit (Commit message)')
|
|
245
|
+
.option('-b, --branch <branch>', 'Tên branch trên remote (Mặc định: main)')
|
|
246
|
+
.option('-C, --dir <path>', 'Thư mục nguồn cần push (Mặc định: thư mục hiện tại)')
|
|
247
|
+
.option('-f, --force', 'Thực hiện push đè (--force)', false)
|
|
248
|
+
.option('-y, --yes', 'Bỏ qua giao diện hỏi, tự động dùng tham số hoặc giá trị mặc định', false)
|
|
249
|
+
.option('--fix', 'Tự động gỡ file secret khỏi stage & thêm .gitignore (an toàn, không xóa file thật)', false)
|
|
250
|
+
.action(async (opts) => {
|
|
251
|
+
await main(opts);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// LỆNH GITHUB
|
|
255
|
+
const ghCmd = program
|
|
256
|
+
.command('github')
|
|
257
|
+
.description('Quản lý tài khoản GitHub và tạo repository mới trực tiếp')
|
|
258
|
+
.action(async () => {
|
|
259
|
+
printBanner(VERSION);
|
|
260
|
+
await runGitHubMenu();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
ghCmd
|
|
264
|
+
.command('create')
|
|
265
|
+
.description('Tạo repository mới trên GitHub (Public hoặc Private)')
|
|
266
|
+
.option('-C, --dir <path>', 'Thư mục nguồn cần liên kết')
|
|
267
|
+
.action(async (opts) => {
|
|
268
|
+
printBanner(VERSION);
|
|
269
|
+
const res = await promptCreateRepo({ targetDir: opts.dir || process.cwd() });
|
|
270
|
+
if (res.ok && res.repo) {
|
|
271
|
+
console.log(`\n » Repo URL: ${res.repo.cloneUrl}\n`);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
ghCmd
|
|
276
|
+
.command('whoami')
|
|
277
|
+
.description('Xem thông tin tài khoản GitHub đang kết nối')
|
|
278
|
+
.action(async () => {
|
|
279
|
+
printBanner(VERSION);
|
|
280
|
+
await showGitHubStatus();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
ghCmd
|
|
284
|
+
.command('repos')
|
|
285
|
+
.description('Liệt kê danh sách repository trên GitHub của bạn')
|
|
286
|
+
.action(async () => {
|
|
287
|
+
printBanner(VERSION);
|
|
288
|
+
await showUserRepos();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
ghCmd
|
|
292
|
+
.command('login')
|
|
293
|
+
.description('Đăng nhập tài khoản GitHub bằng OAuth Device Flow hoặc Token')
|
|
294
|
+
.action(async () => {
|
|
295
|
+
printBanner(VERSION);
|
|
296
|
+
await promptGitHubLogin();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
ghCmd
|
|
300
|
+
.command('logout')
|
|
301
|
+
.description('Đăng xuất tài khoản GitHub khỏi máy')
|
|
302
|
+
.action(async () => {
|
|
303
|
+
printBanner(VERSION);
|
|
304
|
+
logoutGitHub();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// LỆNH FIX
|
|
308
|
+
program
|
|
309
|
+
.command('fix')
|
|
310
|
+
.description('Quét & tự động gỡ file secret khỏi stage, cập nhật .gitignore và làm sạch commit unpushed')
|
|
311
|
+
.option('-C, --dir <path>', 'Thư mục cần quét fix (Mặc định: thư mục hiện tại)')
|
|
312
|
+
.action(async (opts) => {
|
|
313
|
+
printBanner(VERSION);
|
|
314
|
+
await runFixCommand(opts);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// LỆNH PULL
|
|
318
|
+
program
|
|
319
|
+
.command('pull [remote] [branch]')
|
|
320
|
+
.description('Cập nhật code từ remote bằng fast-forward --ff-only hoặc --rebase')
|
|
321
|
+
.option('-C, --dir <path>', 'Thư mục repo cần pull (Mặc định: thư mục hiện tại)')
|
|
322
|
+
.action(async (remote, branch, opts) => {
|
|
323
|
+
printBanner(VERSION);
|
|
324
|
+
await runPullCommand(remote || 'origin', branch, opts || {});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// LỆNH EDIT-TIME
|
|
328
|
+
program
|
|
329
|
+
.command('edit-time')
|
|
330
|
+
.description('Sửa thời gian của commit lịch sử (Backdate commit hash)')
|
|
331
|
+
.option('-C, --dir <path>', 'Thư mục repo (Mặc định: thư mục hiện tại)')
|
|
332
|
+
.action(async (opts) => {
|
|
333
|
+
printBanner(VERSION);
|
|
334
|
+
await runEditCommitTimeCommand(opts);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
program.parse(process.argv);
|