git-push-time 1.0.3 → 1.1.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 +144 -63
- package/bin/index.js +199 -912
- package/lib/commands/edit-time.js +127 -0
- package/lib/commands/fix.js +105 -0
- package/lib/commands/github.js +436 -0
- package/lib/commands/pull.js +88 -0
- package/lib/commands/push.js +653 -0
- package/lib/config.js +210 -0
- package/lib/github.js +304 -0
- package/lib/prompt-helpers.js +62 -0
- package/lib/ui.js +186 -70
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -1,1003 +1,290 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
4
|
+
import { select } from '@inquirer/prompts';
|
|
5
|
+
import { checkGitInstalled, getCurrentBranch } from '../lib/git.js';
|
|
6
|
+
import { getCurrentGitHubUser } from '../lib/github.js';
|
|
7
|
+
import { runPushWizard } from '../lib/commands/push.js';
|
|
8
|
+
import { runPullCommand } from '../lib/commands/pull.js';
|
|
9
|
+
import { runEditCommitTimeCommand } from '../lib/commands/edit-time.js';
|
|
10
|
+
import { runFixCommand } from '../lib/commands/fix.js';
|
|
10
11
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
hasChangesToCommit,
|
|
20
|
-
commitWithCustomDate,
|
|
21
|
-
setBranchName,
|
|
22
|
-
pushToRemote,
|
|
23
|
-
getCurrentBranch,
|
|
24
|
-
findRiskyStagedFiles,
|
|
25
|
-
pullFromRemote,
|
|
26
|
-
pullRebaseFromRemote,
|
|
27
|
-
getRecentCommits,
|
|
28
|
-
rewriteCommitDate
|
|
29
|
-
} from '../lib/git.js';
|
|
30
|
-
import { scanStagedContentForSecrets } from '../lib/secrets.js';
|
|
31
|
-
import {
|
|
32
|
-
autoFixStagedSecrets,
|
|
33
|
-
hasStagedChanges,
|
|
34
|
-
findRiskyWorkingTreeFiles,
|
|
35
|
-
getUnpushedCommits,
|
|
36
|
-
squashUnpushedCommits
|
|
37
|
-
} from '../lib/fix.js';
|
|
12
|
+
runGitHubMenu,
|
|
13
|
+
showGitHubStatus,
|
|
14
|
+
promptCreateRepo,
|
|
15
|
+
showUserRepos,
|
|
16
|
+
promptGitHubLogin,
|
|
17
|
+
logoutGitHub
|
|
18
|
+
} from '../lib/commands/github.js';
|
|
19
|
+
import { promptContinueOrBack } from '../lib/prompt-helpers.js';
|
|
38
20
|
import {
|
|
39
21
|
printBanner,
|
|
40
|
-
|
|
22
|
+
printDashboardStatus,
|
|
41
23
|
printSection,
|
|
42
|
-
stepLabel,
|
|
43
|
-
printTargetDir,
|
|
44
|
-
printNestedRepoWarning,
|
|
45
|
-
printPrePushSecretWarning,
|
|
46
|
-
printSecretBlockedBox,
|
|
47
|
-
printSuccessBox,
|
|
48
|
-
printCancelled,
|
|
49
|
-
printErrorBox,
|
|
50
24
|
printGitMissing,
|
|
51
|
-
|
|
52
|
-
printNonFastForwardBox,
|
|
53
|
-
printFixResult,
|
|
54
|
-
printFooter,
|
|
25
|
+
printCancelled,
|
|
55
26
|
styleCliHelp
|
|
56
27
|
} from '../lib/ui.js';
|
|
57
28
|
|
|
58
|
-
const VERSION = '1.0
|
|
59
|
-
const TOTAL_STEPS = 6;
|
|
60
|
-
|
|
61
|
-
/* ==========================================================================
|
|
62
|
-
BACK-NAVIGATION HELPERS — moi buoc chon/nhap deu co nut Quay lai.
|
|
63
|
-
Quy uoc: go ".." trong o nhap = Quay lai (tuong duong nut « Quay lai).
|
|
64
|
-
Khong dung emoji — an tuong bang chu in hoa + vien + mau.
|
|
65
|
-
========================================================================== */
|
|
66
|
-
const BACK = '__back__';
|
|
67
|
-
const BACK_TOKENS = new Set(['..', '/back', '<<']);
|
|
68
|
-
|
|
69
|
-
function isBackInput(v) {
|
|
70
|
-
return BACK_TOKENS.has(String(v || '').trim());
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function selectWithBack({ message, choices, pageSize, ...rest }) {
|
|
74
|
-
const hasBack = (choices || []).some((c) => c && c.value === BACK);
|
|
75
|
-
const finalChoices = hasBack
|
|
76
|
-
? choices
|
|
77
|
-
: [
|
|
78
|
-
...choices,
|
|
79
|
-
{ name: '« Quay lại (bước trước)', value: BACK, description: 'Về bước trước / menu chính' }
|
|
80
|
-
];
|
|
81
|
-
return await select({
|
|
82
|
-
message,
|
|
83
|
-
choices: finalChoices,
|
|
84
|
-
pageSize: pageSize || Math.min(12, finalChoices.length + 2),
|
|
85
|
-
...rest
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function inputWithBack({ message, validate, ...rest }) {
|
|
90
|
-
const hinted = `${message} [gõ ".." để quay lại]`;
|
|
91
|
-
const wrappedValidate = (val) => {
|
|
92
|
-
if (isBackInput(val)) return true;
|
|
93
|
-
if (validate) return validate(val);
|
|
94
|
-
return true;
|
|
95
|
-
};
|
|
96
|
-
const val = await input({ message: hinted, validate: wrappedValidate, ...rest });
|
|
97
|
-
if (isBackInput(val)) return BACK;
|
|
98
|
-
return val;
|
|
99
|
-
}
|
|
29
|
+
const VERSION = '1.1.0';
|
|
100
30
|
|
|
101
31
|
/**
|
|
102
|
-
*
|
|
103
|
-
* Luon co nut « Quay lai de ve menu chinh.
|
|
104
|
-
* @param {string} startDir thư mục chạy lệnh
|
|
105
|
-
* @returns {Promise<string>} đường dẫn tuyệt đối của folder được chọn, hoac BACK
|
|
32
|
+
* Kiểm tra xem lệnh có được gọi "trần" (không flag/subcommand nào) hay không.
|
|
106
33
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
choices: [
|
|
118
|
-
{ name: '» Thư mục hiện tại (.)', value: startDir, description: 'Push toàn bộ thư mục hiện tại' },
|
|
119
|
-
...subs.map((s) => ({
|
|
120
|
-
name: `» ${s.name}`,
|
|
121
|
-
value: s.fullPath,
|
|
122
|
-
description: s.fullPath
|
|
123
|
-
})),
|
|
124
|
-
{ name: '» Nhập đường dẫn khác...', value: '__custom__', description: 'Gõ tay đường dẫn tuyệt đối/tương đối' }
|
|
125
|
-
]
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
if (picked === BACK) return BACK;
|
|
129
|
-
if (picked !== '__custom__') return picked;
|
|
130
|
-
|
|
131
|
-
const customPath = await inputWithBack({
|
|
132
|
-
message: 'Nhập đường dẫn folder nguồn:',
|
|
133
|
-
validate: (value) => {
|
|
134
|
-
const r = resolveTargetDir(value, startDir);
|
|
135
|
-
return r.ok || r.error;
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
if (customPath === BACK) return BACK;
|
|
139
|
-
return resolveTargetDir(customPath, startDir).fullPath;
|
|
34
|
+
function isBareInvocation(options = {}) {
|
|
35
|
+
return (
|
|
36
|
+
!options.repo &&
|
|
37
|
+
!options.message &&
|
|
38
|
+
!options.date &&
|
|
39
|
+
!options.branch &&
|
|
40
|
+
!options.dir &&
|
|
41
|
+
!options.force &&
|
|
42
|
+
!options.fix
|
|
43
|
+
);
|
|
140
44
|
}
|
|
141
45
|
|
|
142
46
|
/**
|
|
143
|
-
*
|
|
144
|
-
* hỏi user muốn tách repo riêng (chỉ push folder này) hay dùng chung repo cha.
|
|
145
|
-
* Luon co nut « Quay lai de chon lai folder.
|
|
146
|
-
* @returns {Promise<string>} 'separate' | 'shared' | BACK
|
|
47
|
+
* Menu chính khi chạy `git-push-time` không truyền flag
|
|
147
48
|
*/
|
|
148
|
-
async function
|
|
149
|
-
|
|
150
|
-
if (!topLevel) return; // chưa có repo nào -> bước init sẽ tạo mới, an toàn
|
|
151
|
-
if (path.resolve(topLevel) === path.resolve(targetDir)) return; // đã là root repo, ok
|
|
152
|
-
if (fs.existsSync(path.join(targetDir, '.git'))) return; // đã có repo riêng, ok
|
|
153
|
-
|
|
154
|
-
// Nằm trong repo cha -> cảnh báo bằng card theo đúng style UI chung
|
|
155
|
-
printNestedRepoWarning({ targetDir, topLevel });
|
|
156
|
-
|
|
157
|
-
let choice = 'separate';
|
|
158
|
-
if (isInteractive && !options.yes) {
|
|
159
|
-
choice = await selectWithBack({
|
|
160
|
-
message: 'Bạn muốn push thế nào?',
|
|
161
|
-
choices: [
|
|
162
|
-
{ name: '» Tạo repo RIÊNG chỉ cho folder này (khuyên dùng)', value: 'separate', description: 'git init trong folder này, chỉ up nội dung của nó' },
|
|
163
|
-
{ name: '» Dùng chung repo của folder cha', value: 'shared', description: 'Giữ nguyên, push theo repo cha' },
|
|
164
|
-
{ name: 'x Hủy để kiểm tra lại', value: 'abort', description: 'Thoát, không làm gì' }
|
|
165
|
-
]
|
|
166
|
-
});
|
|
167
|
-
if (choice === BACK) return BACK;
|
|
168
|
-
} else if (options.dir) {
|
|
169
|
-
// Chế độ -y + có --dir: mặc định tách repo riêng cho đúng ý "chọn đúng folder"
|
|
170
|
-
choice = 'separate';
|
|
171
|
-
} else {
|
|
172
|
-
// Chế độ -y ở thư mục hiện tại: giữ hành vi cũ, không tự tách
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (choice === 'abort') {
|
|
177
|
-
printCancelled();
|
|
178
|
-
process.exit(0);
|
|
179
|
-
}
|
|
180
|
-
if (choice === 'separate') {
|
|
181
|
-
initGitRepo(targetDir);
|
|
182
|
-
console.log(chalk.hex('#34D399')(' [OK] Đã tạo Git repo riêng cho folder này.\n'));
|
|
183
|
-
} else {
|
|
184
|
-
console.log(chalk.hex('#64748B')(' » Dùng chung repo cha (chỉ stage file trong folder này).\n'));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Menu chính khi chạy `git-push-time` mà không truyền flag/subcommand.
|
|
190
|
-
* @returns {Promise<string>} 'push' | 'pull' | 'edit-time' | 'fix' | 'exit'
|
|
191
|
-
*/
|
|
192
|
-
async function promptMainAction() {
|
|
193
|
-
printSection('CHỌN HÀNH ĐỘNG');
|
|
49
|
+
async function promptMainMenu() {
|
|
50
|
+
printSection('BẢNG ĐIỀU KHIỂN CHÍNH');
|
|
194
51
|
return await select({
|
|
195
|
-
message: 'Bạn muốn
|
|
52
|
+
message: 'Bạn muốn thực hiện thao tác nào?',
|
|
53
|
+
pageSize: 10,
|
|
196
54
|
choices: [
|
|
197
|
-
{
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
55
|
+
{
|
|
56
|
+
name: '» [PUSH] | Đẩy code lên Git (kèm chọn folder, backdate & chống lộ secret)',
|
|
57
|
+
value: 'push',
|
|
58
|
+
description: 'Đẩy dự án lên remote repository hiện tại hoặc mới'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: '» [GITHUB] | Quản lý GitHub & Tạo repository mới',
|
|
62
|
+
value: 'github',
|
|
63
|
+
description: 'Tạo repo mới trên GitHub, xem profile, chọn repo nhanh'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: '» [PULL] | Cập nhật code mới về máy',
|
|
67
|
+
value: 'pull',
|
|
68
|
+
description: 'git pull --ff-only hoặc --rebase từ remote'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: '» [TIME] | Sửa thời gian commit lịch sử',
|
|
72
|
+
value: 'edit-time',
|
|
73
|
+
description: 'Đổi Author/Committer Date của commit trong quá khứ'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: '» [FIX] | Quét & gỡ sạch secret khỏi repo',
|
|
77
|
+
value: 'fix',
|
|
78
|
+
description: 'Quét file nhạy cảm, gỡ khỏi stage và squash commit'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'x [THOÁT] | Kết thúc chương trình',
|
|
82
|
+
value: 'exit',
|
|
83
|
+
description: 'Thoát ứng dụng'
|
|
84
|
+
}
|
|
202
85
|
]
|
|
203
86
|
});
|
|
204
87
|
}
|
|
205
88
|
|
|
206
89
|
/**
|
|
207
|
-
*
|
|
208
|
-
* @param {string} message
|
|
209
|
-
* @returns {Promise<string>} 'continue' | 'back' | 'exit'
|
|
90
|
+
* Vòng lặp Menu chính
|
|
210
91
|
*/
|
|
211
|
-
async function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
92
|
+
async function runMainMenuLoop(initialOptions = {}) {
|
|
93
|
+
while (true) {
|
|
94
|
+
const cwd = process.cwd();
|
|
95
|
+
const branch = getCurrentBranch(cwd);
|
|
96
|
+
const ghStatus = await getCurrentGitHubUser();
|
|
97
|
+
|
|
98
|
+
printDashboardStatus({
|
|
99
|
+
targetDir: cwd,
|
|
100
|
+
currentBranch: branch,
|
|
101
|
+
githubUser: ghStatus.authenticated ? ghStatus.user : null
|
|
102
|
+
});
|
|
221
103
|
|
|
222
|
-
|
|
223
|
-
* Kiểm tra lệnh có được gọi "trần" (không flag push nào) hay không.
|
|
224
|
-
* Chỉ khi gọi trần + interactive mới hiện menu chính để không phá hành vi cũ (-y / -r / -m...).
|
|
225
|
-
*/
|
|
226
|
-
function isBarePushInvocation(options = {}) {
|
|
227
|
-
return !options.repo && !options.message && !options.date && !options.branch && !options.dir && !options.force && !options.fix;
|
|
228
|
-
}
|
|
104
|
+
const action = await promptMainMenu();
|
|
229
105
|
|
|
230
|
-
/**
|
|
231
|
-
* Vòng lặp menu chính: Push / Pull / Edit-time / Fix.
|
|
232
|
-
* Sau mỗi tác vụ xong sẽ hỏi Tiếp tục (ở lại menu) hay Thoát.
|
|
233
|
-
*/
|
|
234
|
-
async function runMainMenuLoop() {
|
|
235
|
-
while (true) {
|
|
236
|
-
const action = await promptMainAction();
|
|
237
106
|
if (action === 'exit') {
|
|
238
107
|
printCancelled();
|
|
239
108
|
process.exit(0);
|
|
240
109
|
}
|
|
110
|
+
|
|
241
111
|
if (action === 'push') {
|
|
242
|
-
|
|
112
|
+
await runPushWizard(initialOptions, async () => {
|
|
113
|
+
// Callback quay lại menu chính
|
|
114
|
+
return await runMainMenuLoop(initialOptions);
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
243
117
|
}
|
|
244
|
-
|
|
118
|
+
|
|
119
|
+
if (action === 'github') {
|
|
120
|
+
const ghResult = await runGitHubMenu({ defaultDir: cwd });
|
|
121
|
+
if (ghResult && ghResult.autoPush && ghResult.repoUrl) {
|
|
122
|
+
// Sau khi tạo repo mới, chuyển tiếp thẳng vào luồng push
|
|
123
|
+
await runPushWizard({ ...initialOptions, repo: ghResult.repoUrl });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
} else if (action === 'pull') {
|
|
245
127
|
await runPullCommand('origin', undefined, { fromMenu: true });
|
|
246
128
|
} else if (action === 'edit-time') {
|
|
247
129
|
await runEditCommitTimeCommand({ fromMenu: true });
|
|
248
130
|
} else if (action === 'fix') {
|
|
249
131
|
await runFixCommand({ fromMenu: true });
|
|
250
132
|
}
|
|
251
|
-
|
|
252
|
-
const next = await promptContinueOrBack('Hoàn tất! Bạn muốn tiếp tục hay quay lại?');
|
|
133
|
+
|
|
134
|
+
const 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?');
|
|
253
135
|
if (next === 'exit') {
|
|
254
136
|
process.exit(0);
|
|
255
137
|
}
|
|
256
|
-
if (next === 'continue' || next === 'back') {
|
|
257
|
-
continue; // quay lại menu chính
|
|
258
|
-
}
|
|
259
138
|
}
|
|
260
139
|
}
|
|
261
140
|
|
|
262
|
-
async function
|
|
141
|
+
async function main(options = {}) {
|
|
263
142
|
printBanner(VERSION);
|
|
264
143
|
|
|
265
|
-
// 1. Kiểm tra Git
|
|
266
144
|
if (!checkGitInstalled()) {
|
|
267
|
-
printBanner(VERSION);
|
|
268
145
|
printGitMissing();
|
|
269
146
|
process.exit(1);
|
|
270
147
|
}
|
|
271
148
|
|
|
272
149
|
const isInteractive = !options.yes;
|
|
273
150
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
await runMainMenuLoop();
|
|
278
|
-
// Người dùng đã chọn "Đẩy code lên (Push)" -> chảy tiếp xuống flow push bên dưới.
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const startDir = process.cwd();
|
|
282
|
-
|
|
283
|
-
// Wizard Push theo tung buoc — moi buoc deu co nut « Quay lai.
|
|
284
|
-
// Buoc 0: folder | 1: repoUrl | 2: commitMsg | 3: date | 4: branch | 5: xac nhan
|
|
285
|
-
let targetDir = startDir;
|
|
286
|
-
let repoUrl = options.repo;
|
|
287
|
-
let commitMsg = options.message;
|
|
288
|
-
let dateInput = options.date;
|
|
289
|
-
let branchName = options.branch;
|
|
290
|
-
let parsedDate = null;
|
|
291
|
-
|
|
292
|
-
if (options.dir) {
|
|
293
|
-
const r = resolveTargetDir(options.dir, startDir);
|
|
294
|
-
if (!r.ok) {
|
|
295
|
-
printErrorBox('THƯ MỤC NGUỒN KHÔNG HỢP LỆ', [r.error], 'VD: --dir ./my-app hoặc --dir "E:/projects/my-app"');
|
|
296
|
-
process.exit(1);
|
|
297
|
-
}
|
|
298
|
-
targetDir = r.fullPath;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (!isInteractive) {
|
|
302
|
-
if (!commitMsg) commitMsg = 'Initial commit';
|
|
303
|
-
if (!dateInput) dateInput = 'now';
|
|
304
|
-
if (!branchName) branchName = 'main';
|
|
305
|
-
if (!repoUrl || !isValidGitUrl(repoUrl)) {
|
|
306
|
-
printErrorBox('URL REPOSITORY KHÔNG HỢP LỆ', [`URL nhận được: '${repoUrl || '(trống)'}'`], 'Ví dụ đúng: https://github.com/username/repository.git');
|
|
307
|
-
process.exit(1);
|
|
308
|
-
}
|
|
309
|
-
parsedDate = parseCustomDate(dateInput);
|
|
310
|
-
if (!parsedDate.valid) {
|
|
311
|
-
printErrorBox('THỜI GIAN KHÔNG HỢP LỆ', [parsedDate.error], 'VD: now, -2d, -5h, 2024-01-15 14:30:00');
|
|
312
|
-
process.exit(1);
|
|
313
|
-
}
|
|
314
|
-
printSummary({ repoUrl, commitMsg, dateDisplay: parsedDate.display, branch: branchName, force: options.force, sourceDir: targetDir });
|
|
315
|
-
} else {
|
|
316
|
-
let step = options.dir ? 1 : 0;
|
|
317
|
-
const cameFromMenu = isBarePushInvocation(options);
|
|
318
|
-
while (true) {
|
|
319
|
-
// Buoc 0: chon folder
|
|
320
|
-
if (step === 0) {
|
|
321
|
-
const picked = await promptForTargetDir(startDir);
|
|
322
|
-
if (picked === BACK) {
|
|
323
|
-
if (cameFromMenu) {
|
|
324
|
-
await runMainMenuLoop();
|
|
325
|
-
// Menu tra ve nghia la user lai chon Push -> o lai buoc 0 de chon lai folder
|
|
326
|
-
if (!options.repo) repoUrl = undefined;
|
|
327
|
-
if (!options.message) commitMsg = undefined;
|
|
328
|
-
if (!options.date) dateInput = undefined;
|
|
329
|
-
if (!options.branch) branchName = undefined;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
printCancelled();
|
|
333
|
-
process.exit(0);
|
|
334
|
-
}
|
|
335
|
-
targetDir = picked;
|
|
336
|
-
const nested = await handleNestedRepo(targetDir, isInteractive, options);
|
|
337
|
-
if (nested === BACK) continue; // quay lai chon folder
|
|
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
|
-
}
|
|
151
|
+
if (isInteractive && isBareInvocation(options)) {
|
|
152
|
+
await runMainMenuLoop(options);
|
|
153
|
+
return;
|
|
487
154
|
}
|
|
488
155
|
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
}
|
|
156
|
+
// Chạy trực tiếp push wizard với các options được cung cấp
|
|
157
|
+
await runPushWizard(options);
|
|
158
|
+
}
|
|
580
159
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
}
|
|
160
|
+
/* ==========================================================================
|
|
161
|
+
COMMANDER CLI CONFIGURATION
|
|
162
|
+
========================================================================== */
|
|
589
163
|
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
}
|
|
164
|
+
const program = new Command();
|
|
165
|
+
styleCliHelp(program);
|
|
699
166
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
167
|
+
program
|
|
168
|
+
.name('git-push-time')
|
|
169
|
+
.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')
|
|
170
|
+
.version(VERSION)
|
|
171
|
+
.option('-r, --repo <url>', 'Đường dẫn Git repository (URL hoặc để trống để chọn/tạo mới)')
|
|
172
|
+
.option('-d, --date <datetime>', 'Thời gian commit (VD: "2024-01-15 14:30:00", "-2d", "-5h", "now")')
|
|
173
|
+
.option('-m, --message <msg>', 'Thông điệp commit (Commit message)')
|
|
174
|
+
.option('-b, --branch <branch>', 'Tên branch trên remote (Mặc định: main)')
|
|
175
|
+
.option('-C, --dir <path>', 'Thư mục nguồn cần push (Mặc định: thư mục hiện tại)')
|
|
176
|
+
.option('-f, --force', 'Thực hiện push đè (--force)', false)
|
|
177
|
+
.option('-y, --yes', 'Bỏ qua giao diện hỏi, tự động dùng tham số hoặc giá trị mặc định', false)
|
|
178
|
+
.option('--fix', 'Tự động gỡ file secret khỏi stage & thêm .gitignore (an toàn, không xóa file thật)', false)
|
|
179
|
+
.action(async (opts) => {
|
|
180
|
+
await main(opts);
|
|
181
|
+
});
|
|
708
182
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
183
|
+
// LỆNH GITHUB
|
|
184
|
+
const ghCmd = program
|
|
185
|
+
.command('github')
|
|
186
|
+
.description('Quản lý tài khoản GitHub và tạo repository mới trực tiếp')
|
|
187
|
+
.action(async () => {
|
|
188
|
+
printBanner(VERSION);
|
|
189
|
+
await runGitHubMenu();
|
|
190
|
+
});
|
|
714
191
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
192
|
+
ghCmd
|
|
193
|
+
.command('create')
|
|
194
|
+
.description('Tạo repository mới trên GitHub (Public hoặc Private)')
|
|
195
|
+
.option('-C, --dir <path>', 'Thư mục nguồn cần liên kết')
|
|
196
|
+
.action(async (opts) => {
|
|
197
|
+
printBanner(VERSION);
|
|
198
|
+
const res = await promptCreateRepo({ targetDir: opts.dir || process.cwd() });
|
|
199
|
+
if (res.ok && res.repo) {
|
|
200
|
+
console.log(`\n » Repo URL: ${res.repo.cloneUrl}\n`);
|
|
722
201
|
}
|
|
723
|
-
|
|
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
|
-
}
|
|
202
|
+
});
|
|
732
203
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
}
|
|
204
|
+
ghCmd
|
|
205
|
+
.command('whoami')
|
|
206
|
+
.description('Xem thông tin tài khoản GitHub đang kết nối')
|
|
207
|
+
.action(async () => {
|
|
208
|
+
printBanner(VERSION);
|
|
209
|
+
await showGitHubStatus();
|
|
210
|
+
});
|
|
753
211
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
}
|
|
212
|
+
ghCmd
|
|
213
|
+
.command('repos')
|
|
214
|
+
.description('Liệt kê danh sách repository trên GitHub của bạn')
|
|
215
|
+
.action(async () => {
|
|
216
|
+
printBanner(VERSION);
|
|
217
|
+
await showUserRepos();
|
|
218
|
+
});
|
|
781
219
|
|
|
782
|
-
|
|
783
|
-
|
|
220
|
+
ghCmd
|
|
221
|
+
.command('login')
|
|
222
|
+
.description('Đăng nhập tài khoản GitHub bằng OAuth Device Flow hoặc Token')
|
|
223
|
+
.action(async () => {
|
|
224
|
+
printBanner(VERSION);
|
|
225
|
+
await promptGitHubLogin();
|
|
226
|
+
});
|
|
784
227
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
}
|
|
228
|
+
ghCmd
|
|
229
|
+
.command('logout')
|
|
230
|
+
.description('Đăng xuất tài khoản GitHub khỏi máy')
|
|
231
|
+
.action(async () => {
|
|
232
|
+
printBanner(VERSION);
|
|
233
|
+
logoutGitHub();
|
|
234
|
+
});
|
|
859
235
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
}
|
|
236
|
+
// LỆNH LOGIN / LOGOUT / WHOAMI TRỰC TIẾP
|
|
237
|
+
program
|
|
238
|
+
.command('login')
|
|
239
|
+
.description('Đăng nhập GitHub qua OAuth Device Flow (mở trình duyệt) hoặc Token')
|
|
240
|
+
.action(async () => {
|
|
241
|
+
printBanner(VERSION);
|
|
242
|
+
await promptGitHubLogin();
|
|
243
|
+
});
|
|
960
244
|
|
|
961
|
-
|
|
962
|
-
|
|
245
|
+
program
|
|
246
|
+
.command('whoami')
|
|
247
|
+
.description('Xem thông tin tài khoản GitHub đang kết nối')
|
|
248
|
+
.action(async () => {
|
|
249
|
+
printBanner(VERSION);
|
|
250
|
+
await showGitHubStatus();
|
|
251
|
+
});
|
|
963
252
|
|
|
964
253
|
program
|
|
965
|
-
.
|
|
966
|
-
.description('
|
|
967
|
-
.
|
|
968
|
-
|
|
969
|
-
|
|
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);
|
|
254
|
+
.command('logout')
|
|
255
|
+
.description('Đăng xuất tài khoản GitHub khỏi máy')
|
|
256
|
+
.action(async () => {
|
|
257
|
+
printBanner(VERSION);
|
|
258
|
+
logoutGitHub();
|
|
978
259
|
});
|
|
979
260
|
|
|
261
|
+
// LỆNH FIX
|
|
980
262
|
program
|
|
981
263
|
.command('fix')
|
|
982
|
-
.description('
|
|
983
|
-
.option('-C, --dir <path>', '
|
|
264
|
+
.description('Quét & tự động gỡ file secret khỏi stage, cập nhật .gitignore và làm sạch commit unpushed')
|
|
265
|
+
.option('-C, --dir <path>', 'Thư mục cần quét fix (Mặc định: thư mục hiện tại)')
|
|
984
266
|
.action(async (opts) => {
|
|
267
|
+
printBanner(VERSION);
|
|
985
268
|
await runFixCommand(opts);
|
|
986
269
|
});
|
|
987
270
|
|
|
271
|
+
// LỆNH PULL
|
|
988
272
|
program
|
|
989
273
|
.command('pull [remote] [branch]')
|
|
990
|
-
.description('
|
|
991
|
-
.option('-C, --dir <path>', '
|
|
274
|
+
.description('Cập nhật code từ remote bằng fast-forward --ff-only hoặc --rebase')
|
|
275
|
+
.option('-C, --dir <path>', 'Thư mục repo cần pull (Mặc định: thư mục hiện tại)')
|
|
992
276
|
.action(async (remote, branch, opts) => {
|
|
277
|
+
printBanner(VERSION);
|
|
993
278
|
await runPullCommand(remote || 'origin', branch, opts || {});
|
|
994
279
|
});
|
|
995
280
|
|
|
281
|
+
// LỆNH EDIT-TIME
|
|
996
282
|
program
|
|
997
283
|
.command('edit-time')
|
|
998
|
-
.description('Sửa thời gian commit (
|
|
999
|
-
.option('-C, --dir <path>', '
|
|
284
|
+
.description('Sửa thời gian của commit lịch sử (Backdate commit hash)')
|
|
285
|
+
.option('-C, --dir <path>', 'Thư mục repo (Mặc định: thư mục hiện tại)')
|
|
1000
286
|
.action(async (opts) => {
|
|
287
|
+
printBanner(VERSION);
|
|
1001
288
|
await runEditCommitTimeCommand(opts);
|
|
1002
289
|
});
|
|
1003
290
|
|