git-push-time 1.0.1 → 1.0.3
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 +30 -7
- package/bin/index.js +751 -250
- package/lib/git.js +222 -13
- package/lib/ui.js +564 -245
- package/package.json +4 -4
- package/lib/npm.js +0 -82
package/bin/index.js
CHANGED
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
-
import
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { input, select } from '@inquirer/prompts';
|
|
7
9
|
import { isValidGitUrl, parseCustomDate } from '../lib/utils.js';
|
|
8
10
|
import {
|
|
9
11
|
checkGitInstalled,
|
|
10
12
|
isGitRepo,
|
|
11
13
|
initGitRepo,
|
|
14
|
+
getGitTopLevel,
|
|
15
|
+
listSubdirectories,
|
|
16
|
+
resolveTargetDir,
|
|
12
17
|
setGitRemote,
|
|
13
18
|
stageAllFiles,
|
|
14
19
|
hasChangesToCommit,
|
|
@@ -16,7 +21,11 @@ import {
|
|
|
16
21
|
setBranchName,
|
|
17
22
|
pushToRemote,
|
|
18
23
|
getCurrentBranch,
|
|
19
|
-
findRiskyStagedFiles
|
|
24
|
+
findRiskyStagedFiles,
|
|
25
|
+
pullFromRemote,
|
|
26
|
+
pullRebaseFromRemote,
|
|
27
|
+
getRecentCommits,
|
|
28
|
+
rewriteCommitDate
|
|
20
29
|
} from '../lib/git.js';
|
|
21
30
|
import { scanStagedContentForSecrets } from '../lib/secrets.js';
|
|
22
31
|
import {
|
|
@@ -26,18 +35,13 @@ import {
|
|
|
26
35
|
getUnpushedCommits,
|
|
27
36
|
squashUnpushedCommits
|
|
28
37
|
} from '../lib/fix.js';
|
|
29
|
-
import {
|
|
30
|
-
checkNpmInstalled,
|
|
31
|
-
getPackageInfo,
|
|
32
|
-
checkNpmLogin,
|
|
33
|
-
isNpmrcTracked,
|
|
34
|
-
publishToNpm
|
|
35
|
-
} from '../lib/npm.js';
|
|
36
38
|
import {
|
|
37
39
|
printBanner,
|
|
38
40
|
printSummary,
|
|
39
41
|
printSection,
|
|
40
42
|
stepLabel,
|
|
43
|
+
printTargetDir,
|
|
44
|
+
printNestedRepoWarning,
|
|
41
45
|
printPrePushSecretWarning,
|
|
42
46
|
printSecretBlockedBox,
|
|
43
47
|
printSuccessBox,
|
|
@@ -47,160 +51,468 @@ import {
|
|
|
47
51
|
printAuthFailedBox,
|
|
48
52
|
printNonFastForwardBox,
|
|
49
53
|
printFixResult,
|
|
50
|
-
printNpmResult,
|
|
51
54
|
printFooter,
|
|
52
55
|
styleCliHelp
|
|
53
56
|
} from '../lib/ui.js';
|
|
54
57
|
|
|
55
|
-
const VERSION = '1.0.
|
|
58
|
+
const VERSION = '1.0.3';
|
|
56
59
|
const TOTAL_STEPS = 6;
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
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', '<<']);
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
printGitMissing();
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
69
|
+
function isBackInput(v) {
|
|
70
|
+
return BACK_TOKENS.has(String(v || '').trim());
|
|
71
|
+
}
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
}
|
|
72
88
|
|
|
73
|
-
|
|
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
|
+
}
|
|
74
100
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Cho user chọn đúng folder nguồn cần push (tránh up nhầm cả folder gốc ngoài cùng).
|
|
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
|
|
106
|
+
*/
|
|
107
|
+
async function promptForTargetDir(startDir) {
|
|
108
|
+
const subs = listSubdirectories(startDir);
|
|
109
|
+
if (subs.length === 0) return startDir;
|
|
110
|
+
|
|
111
|
+
printSection('CHỌN FOLDER NGUỒN ĐỂ PUSH');
|
|
112
|
+
printTargetDir(startDir);
|
|
113
|
+
|
|
114
|
+
const picked = await selectWithBack({
|
|
115
|
+
message: 'Folder nào sẽ được đẩy lên Git?',
|
|
116
|
+
pageSize: Math.min(12, subs.length + 3),
|
|
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
|
+
});
|
|
78
127
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
message: '📦 Dán link Git Repository:',
|
|
82
|
-
validate: (value) => {
|
|
83
|
-
if (!value || !value.trim()) return 'Vui lòng nhập đường dẫn repository!';
|
|
84
|
-
if (!isValidGitUrl(value)) return 'URL chưa đúng (vd: https://github.com/user/repo.git)';
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
128
|
+
if (picked === BACK) return BACK;
|
|
129
|
+
if (picked !== '__custom__') return picked;
|
|
89
130
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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;
|
|
95
136
|
}
|
|
137
|
+
});
|
|
138
|
+
if (customPath === BACK) return BACK;
|
|
139
|
+
return resolveTargetDir(customPath, startDir).fullPath;
|
|
140
|
+
}
|
|
96
141
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Nếu folder được chọn nằm trong Git repo của folder CHA mà chưa có .git riêng,
|
|
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
|
|
147
|
+
*/
|
|
148
|
+
async function handleNestedRepo(targetDir, isInteractive, options) {
|
|
149
|
+
const topLevel = getGitTopLevel(targetDir);
|
|
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
|
+
}
|
|
107
175
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
} else if (dateOption === 'custom') {
|
|
120
|
-
dateInput = await input({
|
|
121
|
-
message: '📅 Nhập ngày giờ (YYYY-MM-DD HH:mm:ss):',
|
|
122
|
-
default: '2024-01-15 10:00:00',
|
|
123
|
-
validate: (val) => {
|
|
124
|
-
const parsed = parseCustomDate(val);
|
|
125
|
-
return parsed.valid || parsed.error || 'Ngày giờ không hợp lệ';
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
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
|
+
}
|
|
130
187
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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');
|
|
194
|
+
return await select({
|
|
195
|
+
message: 'Bạn muốn làm gì?',
|
|
196
|
+
choices: [
|
|
197
|
+
{ name: '» PUSH | Đẩy code lên remote', value: 'push', description: 'git add + commit + push lên remote' },
|
|
198
|
+
{ name: '» PULL | Kéo code về từ remote', value: 'pull', description: 'git pull --ff-only từ remote' },
|
|
199
|
+
{ name: '» EDIT | Sửa thời gian commit', value: 'edit-time', description: 'Đổi Author/Committer Date của commit' },
|
|
200
|
+
{ name: '» FIX | Quét + gỡ secret', value: 'fix', description: 'Quét + gỡ secret, làm sạch commit chưa push' },
|
|
201
|
+
{ name: 'x THOÁT | Không làm gì', value: 'exit', description: 'Thoát, không làm gì cả' }
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Hỏi Tiếp tục / Quay lại menu / Thoát sau mỗi tác vụ hoặc khi gặp lỗi.
|
|
208
|
+
* @param {string} message
|
|
209
|
+
* @returns {Promise<string>} 'continue' | 'back' | 'exit'
|
|
210
|
+
*/
|
|
211
|
+
async function promptContinueOrBack(message = 'Bạn muốn tiếp tục hay quay lại?') {
|
|
212
|
+
return await select({
|
|
213
|
+
message,
|
|
214
|
+
choices: [
|
|
215
|
+
{ name: '» Tiếp tục', value: 'continue', description: 'Đi tiếp bước kế tiếp' },
|
|
216
|
+
{ name: '« Quay lại menu chính', value: 'back', description: 'Về màn hình chọn hành động' },
|
|
217
|
+
{ name: 'x Thoát', value: 'exit', description: 'Dừng, không làm gì' }
|
|
218
|
+
]
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
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
|
+
}
|
|
229
|
+
|
|
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
|
+
if (action === 'exit') {
|
|
238
|
+
printCancelled();
|
|
239
|
+
process.exit(0);
|
|
240
|
+
}
|
|
241
|
+
if (action === 'push') {
|
|
242
|
+
return; // thoát loop để chảy tiếp xuống flow push bên dưới
|
|
243
|
+
}
|
|
244
|
+
if (action === 'pull') {
|
|
245
|
+
await runPullCommand('origin', undefined, { fromMenu: true });
|
|
246
|
+
} else if (action === 'edit-time') {
|
|
247
|
+
await runEditCommitTimeCommand({ fromMenu: true });
|
|
248
|
+
} else if (action === 'fix') {
|
|
249
|
+
await runFixCommand({ fromMenu: true });
|
|
250
|
+
}
|
|
251
|
+
// Sau khi xong 1 tác vụ (pull/edit/fix), hỏi ở lại menu hay thoát.
|
|
252
|
+
const next = await promptContinueOrBack('Hoàn tất! Bạn muốn tiếp tục hay quay lại?');
|
|
253
|
+
if (next === 'exit') {
|
|
254
|
+
process.exit(0);
|
|
255
|
+
}
|
|
256
|
+
if (next === 'continue' || next === 'back') {
|
|
257
|
+
continue; // quay lại menu chính
|
|
137
258
|
}
|
|
138
259
|
}
|
|
260
|
+
}
|
|
139
261
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!branchName) branchName = 'main';
|
|
262
|
+
async function run(options = {}) {
|
|
263
|
+
printBanner(VERSION);
|
|
143
264
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
265
|
+
// 1. Kiểm tra Git
|
|
266
|
+
if (!checkGitInstalled()) {
|
|
267
|
+
printBanner(VERSION);
|
|
268
|
+
printGitMissing();
|
|
148
269
|
process.exit(1);
|
|
149
270
|
}
|
|
150
271
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
272
|
+
const isInteractive = !options.yes;
|
|
273
|
+
|
|
274
|
+
// 1a. Nếu chạy trần `git-push-time` (không flag, không subcommand) thì hiện menu chính:
|
|
275
|
+
// Push / Pull / Sửa thời gian / Fix secret — bổ sung lựa chọn Pull còn thiếu.
|
|
276
|
+
if (isInteractive && isBarePushInvocation(options)) {
|
|
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.
|
|
155
279
|
}
|
|
156
280
|
|
|
157
|
-
|
|
158
|
-
printSummary({
|
|
159
|
-
repoUrl,
|
|
160
|
-
commitMsg,
|
|
161
|
-
dateDisplay: parsedDate.display,
|
|
162
|
-
branch: branchName,
|
|
163
|
-
force: options.force
|
|
164
|
-
});
|
|
281
|
+
const startDir = process.cwd();
|
|
165
282
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
}
|
|
174
486
|
}
|
|
175
487
|
}
|
|
176
488
|
|
|
177
489
|
// 3. Thực thi
|
|
178
490
|
const spinner = ora({ color: 'cyan' });
|
|
179
|
-
printSection(
|
|
491
|
+
printSection(`ĐANG TRIỂN KHAI (${TOTAL_STEPS} bước)`);
|
|
180
492
|
|
|
181
493
|
try {
|
|
182
494
|
// [1/6] Git init
|
|
183
|
-
if (!isGitRepo()) {
|
|
184
|
-
spinner.start(stepLabel(1, TOTAL_STEPS, 'Khởi tạo Git repo (git init)
|
|
185
|
-
initGitRepo();
|
|
495
|
+
if (!isGitRepo(targetDir)) {
|
|
496
|
+
spinner.start(stepLabel(1, TOTAL_STEPS, 'Khởi tạo Git repo (git init)...'));
|
|
497
|
+
initGitRepo(targetDir);
|
|
186
498
|
spinner.succeed(stepLabel(1, TOTAL_STEPS, chalk.hex('#34D399')('Đã khởi tạo Git repo')));
|
|
187
499
|
} else {
|
|
188
500
|
console.log(' ' + stepLabel(1, TOTAL_STEPS, chalk.hex('#64748B')('Đã có Git repo sẵn — bỏ qua init')));
|
|
189
501
|
}
|
|
190
502
|
|
|
191
503
|
// [2/6] Remote
|
|
192
|
-
spinner.start(stepLabel(2, TOTAL_STEPS, 'Cấu hình remote origin
|
|
193
|
-
setGitRemote(repoUrl);
|
|
504
|
+
spinner.start(stepLabel(2, TOTAL_STEPS, 'Cấu hình remote origin...'));
|
|
505
|
+
setGitRemote(repoUrl, targetDir);
|
|
194
506
|
spinner.succeed(stepLabel(2, TOTAL_STEPS, chalk.hex('#34D399')('Cấu hình remote origin hoàn tất')));
|
|
195
507
|
|
|
196
508
|
// [3/6] Stage
|
|
197
|
-
spinner.start(stepLabel(3, TOTAL_STEPS, 'Stage files (git add .)
|
|
198
|
-
stageAllFiles();
|
|
509
|
+
spinner.start(stepLabel(3, TOTAL_STEPS, 'Stage files (git add .)...'));
|
|
510
|
+
stageAllFiles(targetDir);
|
|
199
511
|
spinner.succeed(stepLabel(3, TOTAL_STEPS, chalk.hex('#34D399')('Đã stage toàn bộ files')));
|
|
200
512
|
|
|
201
513
|
// [3.5] Quét secret: tên file + nội dung
|
|
202
|
-
const risky = findRiskyStagedFiles();
|
|
203
|
-
const findings = scanStagedContentForSecrets();
|
|
514
|
+
const risky = findRiskyStagedFiles(targetDir);
|
|
515
|
+
const findings = scanStagedContentForSecrets(targetDir);
|
|
204
516
|
if (risky.length > 0 || findings.length > 0) {
|
|
205
517
|
spinner.stop();
|
|
206
518
|
printPrePushSecretWarning({ riskyFiles: risky, findings });
|
|
@@ -208,110 +520,115 @@ async function run(options = {}) {
|
|
|
208
520
|
|
|
209
521
|
let wantFix = options.fix === true;
|
|
210
522
|
if (!wantFix && isInteractive) {
|
|
211
|
-
|
|
212
|
-
message:
|
|
213
|
-
|
|
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
|
+
]
|
|
214
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';
|
|
215
536
|
}
|
|
216
537
|
|
|
217
538
|
if (wantFix) {
|
|
218
|
-
spinner.start('Đang tự fix: gỡ file secret khỏi stage
|
|
219
|
-
const res = autoFixStagedSecrets(allFiles);
|
|
220
|
-
spinner.succeed(chalk.hex('#34D399')(
|
|
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`));
|
|
221
542
|
printFixResult(res);
|
|
222
543
|
|
|
223
544
|
// Đảm bảo .gitignore được stage trước khi kiểm tra hasStagedChanges
|
|
224
545
|
try {
|
|
225
546
|
const { execSync } = await import('child_process');
|
|
226
|
-
execSync('git add .gitignore', { stdio: 'ignore' });
|
|
547
|
+
execSync('git add .gitignore', { cwd: targetDir, stdio: 'ignore' });
|
|
227
548
|
} catch { /* bỏ qua */ }
|
|
228
549
|
|
|
229
|
-
if (!hasStagedChanges()) {
|
|
550
|
+
if (!hasStagedChanges(targetDir)) {
|
|
230
551
|
printErrorBox('KHÔNG CÒN FILE ĐỂ COMMIT', [
|
|
231
552
|
'Sau khi gỡ secret thì không còn file nào để commit.',
|
|
232
553
|
'File secret vẫn nằm ở máy bạn, chỉ là không push lên nữa.',
|
|
233
554
|
]);
|
|
234
555
|
process.exit(0);
|
|
235
556
|
}
|
|
236
|
-
console.log(chalk.hex('#34D399')('
|
|
557
|
+
console.log(chalk.hex('#34D399')(' » Tiếp tục commit/push phần sạch còn lại.\n'));
|
|
237
558
|
} else if (isInteractive) {
|
|
238
|
-
const
|
|
559
|
+
const keepChoice = await selectWithBack({
|
|
239
560
|
message: findings.length > 0
|
|
240
|
-
?
|
|
561
|
+
? `Phát hiện ${findings.length} vị trí giống secret. Vẫn tiếp tục mà không fix?`
|
|
241
562
|
: 'Vẫn tiếp tục commit/push với các file trên mà không fix?',
|
|
242
|
-
|
|
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
|
+
]
|
|
243
567
|
});
|
|
244
|
-
|
|
245
|
-
|
|
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:'));
|
|
246
571
|
allFiles.forEach((f) => console.log(chalk.white(` git rm --cached "${f}"`)));
|
|
247
572
|
console.log(chalk.gray(' Sau đó chạy lại tool.\n'));
|
|
248
573
|
process.exit(0);
|
|
249
574
|
}
|
|
250
|
-
console.log(chalk.yellow('
|
|
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'));
|
|
251
576
|
} else {
|
|
252
|
-
console.log(chalk.yellow('
|
|
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'));
|
|
253
578
|
}
|
|
254
579
|
}
|
|
255
580
|
|
|
256
581
|
// [4/6] Commit
|
|
257
|
-
spinner.start(stepLabel(4, TOTAL_STEPS, `Tạo commit [${parsedDate.display}]
|
|
258
|
-
if (!hasChangesToCommit()) {
|
|
582
|
+
spinner.start(stepLabel(4, TOTAL_STEPS, `Tạo commit [${parsedDate.display}]...`));
|
|
583
|
+
if (!hasChangesToCommit(targetDir)) {
|
|
259
584
|
spinner.info(stepLabel(4, TOTAL_STEPS, chalk.hex('#94A3B8')('Không có thay đổi mới — dùng commit hiện có')));
|
|
260
585
|
} else {
|
|
261
|
-
commitWithCustomDate(commitMsg, parsedDate.formatted);
|
|
586
|
+
commitWithCustomDate(commitMsg, parsedDate.formatted, targetDir);
|
|
262
587
|
spinner.succeed(stepLabel(4, TOTAL_STEPS, chalk.hex('#34D399')(`Commit hoàn tất • ${chalk.hex('#C084FC').bold(parsedDate.display)}`)));
|
|
263
588
|
}
|
|
264
589
|
|
|
265
590
|
// [5/6] Branch
|
|
266
|
-
spinner.start(stepLabel(5, TOTAL_STEPS, `Đổi branch
|
|
267
|
-
setBranchName(branchName);
|
|
591
|
+
spinner.start(stepLabel(5, TOTAL_STEPS, `Đổi branch -> '${branchName}'...`));
|
|
592
|
+
setBranchName(branchName, targetDir);
|
|
268
593
|
spinner.succeed(stepLabel(5, TOTAL_STEPS, chalk.hex('#34D399')(`Branch: ${chalk.hex('#5EEAD4').bold(branchName)}`)));
|
|
269
594
|
|
|
270
595
|
// [6/6] Push
|
|
271
|
-
spinner.start(stepLabel(6, TOTAL_STEPS, `Push lên origin/${branchName}
|
|
272
|
-
pushToRemote(branchName, options.force);
|
|
596
|
+
spinner.start(stepLabel(6, TOTAL_STEPS, `Push lên origin/${branchName}...`));
|
|
597
|
+
pushToRemote(branchName, options.force, targetDir);
|
|
273
598
|
spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('Push lên remote thành công!')));
|
|
274
599
|
|
|
275
600
|
printSuccessBox({ branch: branchName, repoUrl });
|
|
276
601
|
|
|
277
|
-
|
|
278
|
-
let wantPublish = options.publish === true;
|
|
279
|
-
if (!wantPublish && isInteractive) {
|
|
280
|
-
wantPublish = await confirm({
|
|
281
|
-
message: '📦 Publish package này lên npm luôn không?',
|
|
282
|
-
default: false
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
if (wantPublish) {
|
|
286
|
-
await runNpmPublish({ access: options.access, dryRun: options.dryRun, isInteractive });
|
|
287
|
-
} else {
|
|
288
|
-
printFooter();
|
|
289
|
-
}
|
|
602
|
+
printFooter();
|
|
290
603
|
} catch (error) {
|
|
291
604
|
spinner.stop();
|
|
292
605
|
if (error.code === 'SECRET_BLOCKED') {
|
|
293
606
|
printSecretBlockedBox(error.message, error.details || {});
|
|
294
607
|
|
|
295
608
|
// Kiểm tra xem có commit unpushed chứa vết secret cũ không để hỗ trợ squash tự động
|
|
296
|
-
const unpushed = getUnpushedCommits(branchName);
|
|
609
|
+
const unpushed = getUnpushedCommits(branchName, targetDir);
|
|
297
610
|
if (unpushed.length > 0) {
|
|
298
|
-
console.log(chalk.cyanBright(`\n
|
|
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ũ.`));
|
|
299
612
|
let doSquash = options.fix === true;
|
|
300
613
|
if (!doSquash && isInteractive) {
|
|
301
|
-
|
|
302
|
-
message: '
|
|
303
|
-
|
|
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
|
+
]
|
|
304
620
|
});
|
|
621
|
+
doSquash = sqChoice === 'yes';
|
|
305
622
|
}
|
|
306
623
|
if (doSquash) {
|
|
307
|
-
spinner.start('Đang squash và làm sạch commit chưa push
|
|
308
|
-
const sqRes = squashUnpushedCommits(branchName, commitMsg, parsedDate.formatted);
|
|
624
|
+
spinner.start('Đang squash và làm sạch commit chưa push...');
|
|
625
|
+
const sqRes = squashUnpushedCommits(branchName, commitMsg, parsedDate.formatted, targetDir);
|
|
309
626
|
if (sqRes.ok) {
|
|
310
|
-
spinner.succeed(chalk.green('
|
|
627
|
+
spinner.succeed(chalk.green('[OK] Đã làm sạch commit! Đang thử push lại...'));
|
|
311
628
|
try {
|
|
312
|
-
spinner.start(stepLabel(6, TOTAL_STEPS, `Push lại lên origin/${branchName}
|
|
313
|
-
pushToRemote(branchName, options.force);
|
|
314
|
-
spinner.succeed(chalk.bold.green('
|
|
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'));
|
|
315
632
|
printSuccessBox({ branch: branchName, repoUrl });
|
|
316
633
|
printFooter();
|
|
317
634
|
return;
|
|
@@ -327,6 +644,50 @@ async function run(options = {}) {
|
|
|
327
644
|
printAuthFailedBox(error.message);
|
|
328
645
|
} else if (error.code === 'NON_FAST_FORWARD') {
|
|
329
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
|
+
}
|
|
330
691
|
} else if (error.code === 'REMOTE_OR_NETWORK') {
|
|
331
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.');
|
|
332
693
|
} else {
|
|
@@ -340,59 +701,78 @@ async function run(options = {}) {
|
|
|
340
701
|
* Lệnh fix độc lập: `git-push-time fix`
|
|
341
702
|
* Tự động quét và fix secret ở staged, working directory và unpushed commits.
|
|
342
703
|
*/
|
|
343
|
-
async function runFixCommand() {
|
|
344
|
-
|
|
345
|
-
|
|
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');
|
|
346
708
|
|
|
347
709
|
if (!checkGitInstalled()) {
|
|
348
710
|
printGitMissing();
|
|
711
|
+
if (fromMenu) return;
|
|
349
712
|
process.exit(1);
|
|
350
713
|
}
|
|
351
714
|
|
|
352
|
-
|
|
353
|
-
|
|
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;
|
|
354
730
|
process.exit(1);
|
|
355
731
|
}
|
|
356
732
|
|
|
357
733
|
const spinner = ora({ color: 'cyan' });
|
|
358
734
|
spinner.start('Đang kiểm tra staged files và thư mục làm việc…');
|
|
359
735
|
|
|
360
|
-
const risky = findRiskyStagedFiles();
|
|
361
|
-
const findings = scanStagedContentForSecrets();
|
|
362
|
-
const riskyWorking = findRiskyWorkingTreeFiles();
|
|
736
|
+
const risky = findRiskyStagedFiles(targetDir);
|
|
737
|
+
const findings = scanStagedContentForSecrets(targetDir);
|
|
738
|
+
const riskyWorking = findRiskyWorkingTreeFiles(targetDir);
|
|
363
739
|
spinner.stop();
|
|
364
740
|
|
|
365
741
|
const allDetected = [...new Set([...risky, ...findings.map((f) => f.file), ...riskyWorking])];
|
|
366
742
|
|
|
367
743
|
if (allDetected.length === 0) {
|
|
368
|
-
printSection('
|
|
369
|
-
console.log(chalk.hex('#34D399')('
|
|
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'));
|
|
370
746
|
} else {
|
|
371
747
|
printPrePushSecretWarning({ riskyFiles: allDetected, findings });
|
|
372
|
-
spinner.start('Đang tự động fix (gỡ khỏi stage + thêm .gitignore)
|
|
373
|
-
const res = autoFixStagedSecrets(allDetected);
|
|
374
|
-
spinner.succeed(chalk.hex('#34D399')(
|
|
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!`));
|
|
375
751
|
printFixResult(res);
|
|
376
752
|
}
|
|
377
753
|
|
|
378
754
|
// Kiểm tra xem có commit chưa push nào bị dính vết secret cũ không
|
|
379
|
-
const currentBranch = getCurrentBranch();
|
|
380
|
-
const unpushed = getUnpushedCommits(currentBranch);
|
|
755
|
+
const currentBranch = getCurrentBranch(targetDir);
|
|
756
|
+
const unpushed = getUnpushedCommits(currentBranch, targetDir);
|
|
381
757
|
if (unpushed.length > 0) {
|
|
382
|
-
printSection(
|
|
383
|
-
unpushed.slice(0, 5).forEach((c) => console.log(chalk.hex('#94A3B8')(`
|
|
758
|
+
printSection(`${unpushed.length} COMMIT CHƯA PUSH`);
|
|
759
|
+
unpushed.slice(0, 5).forEach((c) => console.log(chalk.hex('#94A3B8')(` - ${c}`)));
|
|
384
760
|
if (unpushed.length > 5) {
|
|
385
|
-
console.log(chalk.hex('#64748B')(`
|
|
761
|
+
console.log(chalk.hex('#64748B')(` ...và ${unpushed.length - 5} commit khác`));
|
|
386
762
|
}
|
|
387
|
-
const
|
|
388
|
-
message:
|
|
389
|
-
|
|
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
|
+
]
|
|
390
769
|
});
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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);
|
|
394
774
|
if (sq.ok) {
|
|
395
|
-
spinner.succeed(chalk.hex('#34D399')('
|
|
775
|
+
spinner.succeed(chalk.hex('#34D399')('[OK] Đã làm sạch commit chưa push! Sẵn sàng push lên remote.'));
|
|
396
776
|
} else {
|
|
397
777
|
spinner.fail(chalk.red(`Không thể squash: ${sq.error}`));
|
|
398
778
|
}
|
|
@@ -402,74 +782,180 @@ async function runFixCommand() {
|
|
|
402
782
|
printSuccessBox({ branch: currentBranch || 'main', repoUrl: 'local repo đã sạch — sẵn sàng push' });
|
|
403
783
|
}
|
|
404
784
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
printSection('📦 PUBLISH LÊN NPM');
|
|
413
|
-
|
|
414
|
-
if (!checkNpmInstalled()) {
|
|
415
|
-
printErrorBox('CHƯA CÀI NPM', ['Chưa phát hiện npm trên máy.'], 'Cài Node.js từ https://nodejs.org rồi thử lại.');
|
|
416
|
-
return;
|
|
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);
|
|
417
792
|
}
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
|
|
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;
|
|
422
802
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
console.log(chalk.gray(' Tool sẽ publish mà không cần commit .npmrc — hãy gỡ nó: git rm --cached .npmrc\n'));
|
|
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);
|
|
429
808
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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();
|
|
440
850
|
}
|
|
851
|
+
// Nếu đã ở trong menu thì return để caller hỏi Tiếp tục/Quay lại.
|
|
852
|
+
return;
|
|
441
853
|
} else {
|
|
442
|
-
|
|
854
|
+
if (!fromMenu) process.exit(1);
|
|
443
855
|
return;
|
|
444
856
|
}
|
|
445
|
-
} else if (login.loggedIn) {
|
|
446
|
-
console.log(chalk.gray(` 👤 npm user: ${login.user}`));
|
|
447
857
|
}
|
|
858
|
+
}
|
|
448
859
|
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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;
|
|
456
872
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
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);
|
|
470
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);
|
|
471
958
|
}
|
|
472
|
-
printFooter();
|
|
473
959
|
}
|
|
474
960
|
|
|
475
961
|
const program = new Command();
|
|
@@ -483,12 +969,10 @@ program
|
|
|
483
969
|
.option('-d, --date <datetime>', 'Thời gian commit (Ví dụ: "2024-01-15 14:30:00", "-2d", "-5h", "now")')
|
|
484
970
|
.option('-m, --message <msg>', 'Thông điệp commit (Commit message)')
|
|
485
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)')
|
|
486
973
|
.option('-f, --force', 'Thực hiện push đè (--force)', false)
|
|
487
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)
|
|
488
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)
|
|
489
|
-
.option('--publish', 'Publish lên npm sau khi push Git thành công', false)
|
|
490
|
-
.option('--access <mode>', 'Quyền publish npm (public|restricted)', 'public')
|
|
491
|
-
.option('--dry-run', 'Chạy thử npm publish mà không publish thật', false)
|
|
492
976
|
.action(async (opts) => {
|
|
493
977
|
await run(opts);
|
|
494
978
|
});
|
|
@@ -496,8 +980,25 @@ program
|
|
|
496
980
|
program
|
|
497
981
|
.command('fix')
|
|
498
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')
|
|
499
|
-
.
|
|
500
|
-
|
|
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);
|
|
501
1002
|
});
|
|
502
1003
|
|
|
503
1004
|
program.parse(process.argv);
|