git-push-time 1.0.0 → 1.0.2
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 +18 -8
- package/bin/index.js +372 -79
- package/lib/fix.js +260 -0
- package/lib/git.js +93 -3
- package/lib/npm.js +82 -0
- package/lib/secrets.js +87 -0
- package/lib/ui.js +409 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 🚀 GitHub CLI (`
|
|
1
|
+
# 🚀 Git Push Time / GitHub CLI (`git-push-time`)
|
|
2
2
|
|
|
3
3
|
Một công cụ Dòng lệnh (CLI) mạnh mẽ và tiện lợi giúp bạn **đẩy dự án lên Git (GitHub, GitLab, Bitbucket,...) cực nhanh** chỉ bằng cách dán URL repo, đồng thời hỗ trợ **tùy chỉnh hoặc lùi thời gian commit/push** (Author Date & Committer Date).
|
|
4
4
|
|
|
@@ -16,17 +16,17 @@ Một công cụ Dòng lệnh (CLI) mạnh mẽ và tiện lợi giúp bạn **
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
## 📦 Cài đặt
|
|
19
|
+
## 📦 Cài đặt từ NPM
|
|
20
20
|
|
|
21
21
|
### Cách 1: Cài đặt toàn cục (Recommended)
|
|
22
22
|
```bash
|
|
23
|
-
npm install -g
|
|
23
|
+
npm install -g git-push-time
|
|
24
24
|
```
|
|
25
|
-
Sau khi cài đặt, bạn có thể gọi lệnh `
|
|
25
|
+
Sau khi cài đặt, bạn có thể gọi lệnh `git-push-time`, `githubcli` hoặc `git-custom-push` ở **bất kỳ thư mục dự án nào** trên máy tính!
|
|
26
26
|
|
|
27
|
-
### Cách 2: Chạy trực tiếp
|
|
27
|
+
### Cách 2: Chạy trực tiếp qua `npx`
|
|
28
28
|
```bash
|
|
29
|
-
npx
|
|
29
|
+
npx git-push-time
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
---
|
|
@@ -85,16 +85,26 @@ git-push-time -r https://github.com/username/my-repo.git -d "-5h" -m "Update doc
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
-
## 🚩 Danh sách các Flags (Options)
|
|
89
|
-
|
|
90
88
|
- `-r, --repo <url>`: Link Git Repository (VD: `https://github.com/user/repo.git`)
|
|
91
89
|
- `-d, --date <datetime>`: Thời gian commit (`now`, `-2d`, `2024-01-15 10:30:00`,...)
|
|
92
90
|
- `-m, --message <msg>`: Thông điệp commit (Mặc định: `Initial commit`)
|
|
93
91
|
- `-b, --branch <branch>`: Tên branch (Mặc định: `main`)
|
|
94
92
|
- `-f, --force`: Đẩy đè lên branch remote (`--force`)
|
|
95
93
|
- `-y, --yes`: Tự động xác nhận, không hiện prompt chọn
|
|
94
|
+
- `--fix`: Tự động gỡ file secret (.env, .npmrc...) khỏi stage và thêm vào `.gitignore`
|
|
95
|
+
- `--publish`: Publish package lên npm sau khi push Git thành công
|
|
96
96
|
- `-h, --help`: Hướng dẫn trợ giúp
|
|
97
97
|
|
|
98
|
+
### 🛠️ Lệnh Fix Secret Độc Lập
|
|
99
|
+
Nếu phát hiện repo bị dính secret hoặc GitHub Push Protection (GH013) chặn push, bạn có thể chạy ngay lệnh fix:
|
|
100
|
+
```bash
|
|
101
|
+
git-push-time fix
|
|
102
|
+
```
|
|
103
|
+
Lệnh sẽ tự động:
|
|
104
|
+
1. Quét vùng stage và thư mục làm việc để tìm file cấu hình nhạy cảm (`.env`, `.npmrc`, keys...).
|
|
105
|
+
2. Gỡ file khỏi stage và thêm rule an toàn vào `.gitignore`.
|
|
106
|
+
3. Kiểm tra các commit chưa push (unpushed) dính secret cũ và hỗ trợ tự động squash thành commit sạch để GitHub cho phép push.
|
|
107
|
+
|
|
98
108
|
---
|
|
99
109
|
|
|
100
110
|
## 📜 Giấy phép
|
package/bin/index.js
CHANGED
|
@@ -15,31 +15,53 @@ import {
|
|
|
15
15
|
commitWithCustomDate,
|
|
16
16
|
setBranchName,
|
|
17
17
|
pushToRemote,
|
|
18
|
-
getCurrentBranch
|
|
18
|
+
getCurrentBranch,
|
|
19
|
+
findRiskyStagedFiles
|
|
19
20
|
} from '../lib/git.js';
|
|
21
|
+
import { scanStagedContentForSecrets } from '../lib/secrets.js';
|
|
22
|
+
import {
|
|
23
|
+
autoFixStagedSecrets,
|
|
24
|
+
hasStagedChanges,
|
|
25
|
+
findRiskyWorkingTreeFiles,
|
|
26
|
+
getUnpushedCommits,
|
|
27
|
+
squashUnpushedCommits
|
|
28
|
+
} from '../lib/fix.js';
|
|
29
|
+
import {
|
|
30
|
+
checkNpmInstalled,
|
|
31
|
+
getPackageInfo,
|
|
32
|
+
checkNpmLogin,
|
|
33
|
+
isNpmrcTracked,
|
|
34
|
+
publishToNpm
|
|
35
|
+
} from '../lib/npm.js';
|
|
36
|
+
import {
|
|
37
|
+
printBanner,
|
|
38
|
+
printSummary,
|
|
39
|
+
printSection,
|
|
40
|
+
stepLabel,
|
|
41
|
+
printPrePushSecretWarning,
|
|
42
|
+
printSecretBlockedBox,
|
|
43
|
+
printSuccessBox,
|
|
44
|
+
printCancelled,
|
|
45
|
+
printErrorBox,
|
|
46
|
+
printGitMissing,
|
|
47
|
+
printAuthFailedBox,
|
|
48
|
+
printNonFastForwardBox,
|
|
49
|
+
printFixResult,
|
|
50
|
+
printNpmResult,
|
|
51
|
+
printFooter,
|
|
52
|
+
styleCliHelp
|
|
53
|
+
} from '../lib/ui.js';
|
|
20
54
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
program
|
|
24
|
-
.name('githubcli')
|
|
25
|
-
.description('Công cụ CLI giúp đẩy dự án lên Git và sửa thời gian commit (Backdate Commit)')
|
|
26
|
-
.version('1.0.0')
|
|
27
|
-
.option('-r, --repo <url>', 'Đường dẫn Git repository (URL)')
|
|
28
|
-
.option('-d, --date <datetime>', 'Thời gian commit (Ví dụ: "2024-01-15 14:30:00", "-2d", "-5h", "now")')
|
|
29
|
-
.option('-m, --message <msg>', 'Thông điệp commit (Commit message)')
|
|
30
|
-
.option('-b, --branch <branch>', 'Tên branch trên remote (Mặc định: main)')
|
|
31
|
-
.option('-f, --force', 'Thực hiện push đè (--force)', false)
|
|
32
|
-
.option('-y, --yes', 'Bỏ qua prompt tương tác, tự động dùng tham số hoặc giá trị mặc định', false);
|
|
55
|
+
const VERSION = '1.0.2';
|
|
56
|
+
const TOTAL_STEPS = 6;
|
|
33
57
|
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
async function run(options = {}) {
|
|
59
|
+
printBanner(VERSION);
|
|
36
60
|
|
|
37
|
-
|
|
38
|
-
console.log(chalk.bold.cyan('\n🚀 GIT PUSH & COMMIT TIME SPOOFER 🚀\n'));
|
|
39
|
-
|
|
40
|
-
// 1. Kiểm tra Git đã cài đặt chưa
|
|
61
|
+
// 1. Kiểm tra Git
|
|
41
62
|
if (!checkGitInstalled()) {
|
|
42
|
-
|
|
63
|
+
printBanner(VERSION);
|
|
64
|
+
printGitMissing();
|
|
43
65
|
process.exit(1);
|
|
44
66
|
}
|
|
45
67
|
|
|
@@ -50,36 +72,36 @@ async function run() {
|
|
|
50
72
|
|
|
51
73
|
const isInteractive = !options.yes;
|
|
52
74
|
|
|
53
|
-
// 2.
|
|
75
|
+
// 2. Thu thập thông tin
|
|
54
76
|
if (isInteractive) {
|
|
55
|
-
|
|
77
|
+
printSection('🔗 KẾT NỐI REPOSITORY');
|
|
78
|
+
|
|
56
79
|
if (!repoUrl) {
|
|
57
80
|
repoUrl = await input({
|
|
58
|
-
message: 'Dán link Git Repository
|
|
81
|
+
message: '📦 Dán link Git Repository:',
|
|
59
82
|
validate: (value) => {
|
|
60
83
|
if (!value || !value.trim()) return 'Vui lòng nhập đường dẫn repository!';
|
|
61
|
-
if (!isValidGitUrl(value)) return '
|
|
84
|
+
if (!isValidGitUrl(value)) return 'URL chưa đúng (vd: https://github.com/user/repo.git)';
|
|
62
85
|
return true;
|
|
63
86
|
}
|
|
64
87
|
});
|
|
65
88
|
}
|
|
66
89
|
|
|
67
|
-
// Nhập Commit Message nếu chưa truyền
|
|
68
90
|
if (!commitMsg) {
|
|
69
91
|
commitMsg = await input({
|
|
70
|
-
message: '
|
|
92
|
+
message: '💬 Thông điệp commit:',
|
|
71
93
|
default: 'Initial commit'
|
|
72
94
|
});
|
|
73
95
|
}
|
|
74
96
|
|
|
75
|
-
// Chọn cách nhập thời gian Commit nếu chưa truyền
|
|
76
97
|
if (!dateInput) {
|
|
98
|
+
printSection('⏰ THỜI GIAN COMMIT');
|
|
77
99
|
const dateOption = await select({
|
|
78
|
-
message: 'Chọn thời gian cho
|
|
100
|
+
message: 'Chọn thời gian cho commit:',
|
|
79
101
|
choices: [
|
|
80
|
-
{ name: '⏰
|
|
81
|
-
{ name: '⏳
|
|
82
|
-
{ name: '📅
|
|
102
|
+
{ name: '⏰ Ngay bây giờ', value: 'now', description: 'Dùng thời gian hiện tại' },
|
|
103
|
+
{ name: '⏳ Lùi thời gian', value: 'relative', description: 'VD: -2d, -5h, -30m, -1w' },
|
|
104
|
+
{ name: '📅 Ngày giờ cụ thể', value: 'custom', description: 'VD: 2024-01-15 14:30:00' }
|
|
83
105
|
]
|
|
84
106
|
});
|
|
85
107
|
|
|
@@ -87,7 +109,7 @@ async function run() {
|
|
|
87
109
|
dateInput = 'now';
|
|
88
110
|
} else if (dateOption === 'relative') {
|
|
89
111
|
dateInput = await input({
|
|
90
|
-
message: 'Nhập thời gian lùi (VD: -2d
|
|
112
|
+
message: '⏳ Nhập thời gian lùi (VD: -2d, -5h, -30m, -1w):',
|
|
91
113
|
default: '-1d',
|
|
92
114
|
validate: (val) => {
|
|
93
115
|
const parsed = parseCustomDate(val);
|
|
@@ -96,7 +118,7 @@ async function run() {
|
|
|
96
118
|
});
|
|
97
119
|
} else if (dateOption === 'custom') {
|
|
98
120
|
dateInput = await input({
|
|
99
|
-
message: 'Nhập ngày giờ
|
|
121
|
+
message: '📅 Nhập ngày giờ (YYYY-MM-DD HH:mm:ss):',
|
|
100
122
|
default: '2024-01-15 10:00:00',
|
|
101
123
|
validate: (val) => {
|
|
102
124
|
const parsed = parseCustomDate(val);
|
|
@@ -106,105 +128,376 @@ async function run() {
|
|
|
106
128
|
}
|
|
107
129
|
}
|
|
108
130
|
|
|
109
|
-
// Nhập Branch Name nếu chưa truyền và chưa đổi
|
|
110
131
|
if (!options.branch) {
|
|
111
132
|
const currentB = getCurrentBranch();
|
|
112
133
|
branchName = await input({
|
|
113
|
-
message: '
|
|
134
|
+
message: '🌿 Branch trên remote:',
|
|
114
135
|
default: currentB || 'main'
|
|
115
136
|
});
|
|
116
137
|
}
|
|
117
138
|
}
|
|
118
139
|
|
|
119
|
-
// Set default fallback values
|
|
120
140
|
if (!commitMsg) commitMsg = 'Initial commit';
|
|
121
141
|
if (!dateInput) dateInput = 'now';
|
|
122
142
|
if (!branchName) branchName = 'main';
|
|
123
143
|
|
|
124
|
-
// Validate Git URL
|
|
125
144
|
if (!repoUrl || !isValidGitUrl(repoUrl)) {
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
printErrorBox('URL REPOSITORY KHÔNG HỢP LỆ', [
|
|
146
|
+
`URL nhận được: '${repoUrl || '(trống)'}'`,
|
|
147
|
+
], 'Ví dụ đúng: https://github.com/username/repository.git');
|
|
128
148
|
process.exit(1);
|
|
129
149
|
}
|
|
130
150
|
|
|
131
|
-
// Parse Date/Time
|
|
132
151
|
const parsedDate = parseCustomDate(dateInput);
|
|
133
152
|
if (!parsedDate.valid) {
|
|
134
|
-
|
|
153
|
+
printErrorBox('THỜI GIAN KHÔNG HỢP LỆ', [parsedDate.error], 'VD: now, -2d, -5h, 2024-01-15 14:30:00');
|
|
135
154
|
process.exit(1);
|
|
136
155
|
}
|
|
137
156
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
console.log(chalk.gray('----------------------------------------\n'));
|
|
157
|
+
// Tóm tắt
|
|
158
|
+
printSummary({
|
|
159
|
+
repoUrl,
|
|
160
|
+
commitMsg,
|
|
161
|
+
dateDisplay: parsedDate.display,
|
|
162
|
+
branch: branchName,
|
|
163
|
+
force: options.force
|
|
164
|
+
});
|
|
147
165
|
|
|
148
166
|
if (isInteractive) {
|
|
149
167
|
const ok = await confirm({
|
|
150
|
-
message: 'Xác nhận
|
|
168
|
+
message: '🚀 Xác nhận đẩy lên Git ngay?',
|
|
151
169
|
default: true
|
|
152
170
|
});
|
|
153
171
|
if (!ok) {
|
|
154
|
-
|
|
172
|
+
printCancelled();
|
|
155
173
|
process.exit(0);
|
|
156
174
|
}
|
|
157
175
|
}
|
|
158
176
|
|
|
159
|
-
// 3. Thực thi
|
|
160
|
-
const spinner = ora();
|
|
177
|
+
// 3. Thực thi
|
|
178
|
+
const spinner = ora({ color: 'cyan' });
|
|
179
|
+
printSection(`⚙️ ĐANG TRIỂN KHAI (${TOTAL_STEPS} bước)`);
|
|
161
180
|
|
|
162
181
|
try {
|
|
163
|
-
//
|
|
182
|
+
// [1/6] Git init
|
|
164
183
|
if (!isGitRepo()) {
|
|
165
|
-
spinner.start('
|
|
184
|
+
spinner.start(stepLabel(1, TOTAL_STEPS, 'Khởi tạo Git repo (git init)…'));
|
|
166
185
|
initGitRepo();
|
|
167
|
-
spinner.succeed('Đã khởi tạo
|
|
186
|
+
spinner.succeed(stepLabel(1, TOTAL_STEPS, chalk.hex('#34D399')('Đã khởi tạo Git repo')));
|
|
168
187
|
} else {
|
|
169
|
-
|
|
188
|
+
console.log(' ' + stepLabel(1, TOTAL_STEPS, chalk.hex('#64748B')('Đã có Git repo sẵn — bỏ qua init')));
|
|
170
189
|
}
|
|
171
190
|
|
|
172
|
-
//
|
|
173
|
-
spinner.start(
|
|
191
|
+
// [2/6] Remote
|
|
192
|
+
spinner.start(stepLabel(2, TOTAL_STEPS, 'Cấu hình remote origin…'));
|
|
174
193
|
setGitRemote(repoUrl);
|
|
175
|
-
spinner.succeed(
|
|
194
|
+
spinner.succeed(stepLabel(2, TOTAL_STEPS, chalk.hex('#34D399')('Cấu hình remote origin hoàn tất')));
|
|
176
195
|
|
|
177
|
-
//
|
|
178
|
-
spinner.start(
|
|
196
|
+
// [3/6] Stage
|
|
197
|
+
spinner.start(stepLabel(3, TOTAL_STEPS, 'Stage files (git add .)…'));
|
|
179
198
|
stageAllFiles();
|
|
180
|
-
spinner.succeed('Đã stage
|
|
199
|
+
spinner.succeed(stepLabel(3, TOTAL_STEPS, chalk.hex('#34D399')('Đã stage toàn bộ files')));
|
|
200
|
+
|
|
201
|
+
// [3.5] Quét secret: tên file + nội dung
|
|
202
|
+
const risky = findRiskyStagedFiles();
|
|
203
|
+
const findings = scanStagedContentForSecrets();
|
|
204
|
+
if (risky.length > 0 || findings.length > 0) {
|
|
205
|
+
spinner.stop();
|
|
206
|
+
printPrePushSecretWarning({ riskyFiles: risky, findings });
|
|
207
|
+
const allFiles = [...new Set([...risky, ...findings.map((f) => f.file)])];
|
|
208
|
+
|
|
209
|
+
let wantFix = options.fix === true;
|
|
210
|
+
if (!wantFix && isInteractive) {
|
|
211
|
+
wantFix = await confirm({
|
|
212
|
+
message: `🛠️ Tự động fix giúp bạn? (git rm --cached + thêm .gitignore, giữ file ở máy)`,
|
|
213
|
+
default: true
|
|
214
|
+
});
|
|
215
|
+
}
|
|
181
216
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
217
|
+
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')(`✓ Đã gỡ ${res.fixed.length} file khỏi commit`));
|
|
221
|
+
printFixResult(res);
|
|
222
|
+
|
|
223
|
+
// Đảm bảo .gitignore được stage trước khi kiểm tra hasStagedChanges
|
|
224
|
+
try {
|
|
225
|
+
const { execSync } = await import('child_process');
|
|
226
|
+
execSync('git add .gitignore', { stdio: 'ignore' });
|
|
227
|
+
} catch { /* bỏ qua */ }
|
|
228
|
+
|
|
229
|
+
if (!hasStagedChanges()) {
|
|
230
|
+
printErrorBox('KHÔNG CÒN FILE ĐỂ COMMIT', [
|
|
231
|
+
'Sau khi gỡ secret thì không còn file nào để commit.',
|
|
232
|
+
'File secret vẫn nằm ở máy bạn, chỉ là không push lên nữa.',
|
|
233
|
+
]);
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
console.log(chalk.hex('#34D399')(' → Tiếp tục commit/push phần sạch còn lại.\n'));
|
|
237
|
+
} else if (isInteractive) {
|
|
238
|
+
const keepGoing = await confirm({
|
|
239
|
+
message: findings.length > 0
|
|
240
|
+
? `⚠️ Phát hiện ${findings.length} vị trí giống secret. Vẫn tiếp tục mà không fix?`
|
|
241
|
+
: 'Vẫn tiếp tục commit/push với các file trên mà không fix?',
|
|
242
|
+
default: false
|
|
243
|
+
});
|
|
244
|
+
if (!keepGoing) {
|
|
245
|
+
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
|
+
allFiles.forEach((f) => console.log(chalk.white(` git rm --cached "${f}"`)));
|
|
247
|
+
console.log(chalk.gray(' Sau đó chạy lại tool.\n'));
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
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
|
+
} else {
|
|
252
|
+
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
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// [4/6] Commit
|
|
257
|
+
spinner.start(stepLabel(4, TOTAL_STEPS, `Tạo commit [${parsedDate.display}]…`));
|
|
185
258
|
if (!hasChangesToCommit()) {
|
|
186
|
-
spinner.info('Không có thay đổi
|
|
259
|
+
spinner.info(stepLabel(4, TOTAL_STEPS, chalk.hex('#94A3B8')('Không có thay đổi mới — dùng commit hiện có')));
|
|
187
260
|
} else {
|
|
188
261
|
commitWithCustomDate(commitMsg, parsedDate.formatted);
|
|
189
|
-
spinner.succeed(
|
|
262
|
+
spinner.succeed(stepLabel(4, TOTAL_STEPS, chalk.hex('#34D399')(`Commit hoàn tất • ${chalk.hex('#C084FC').bold(parsedDate.display)}`)));
|
|
190
263
|
}
|
|
191
264
|
|
|
192
|
-
//
|
|
193
|
-
spinner.start(
|
|
265
|
+
// [5/6] Branch
|
|
266
|
+
spinner.start(stepLabel(5, TOTAL_STEPS, `Đổi branch → '${branchName}'…`));
|
|
194
267
|
setBranchName(branchName);
|
|
195
|
-
spinner.succeed(
|
|
268
|
+
spinner.succeed(stepLabel(5, TOTAL_STEPS, chalk.hex('#34D399')(`Branch: ${chalk.hex('#5EEAD4').bold(branchName)}`)));
|
|
196
269
|
|
|
197
|
-
//
|
|
198
|
-
spinner.start(
|
|
270
|
+
// [6/6] Push
|
|
271
|
+
spinner.start(stepLabel(6, TOTAL_STEPS, `Push lên origin/${branchName}…`));
|
|
199
272
|
pushToRemote(branchName, options.force);
|
|
200
|
-
spinner.succeed(chalk.bold.
|
|
273
|
+
spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('Push lên remote thành công!')));
|
|
201
274
|
|
|
202
|
-
|
|
275
|
+
printSuccessBox({ branch: branchName, repoUrl });
|
|
276
|
+
|
|
277
|
+
// [+1] Publish npm (optional)
|
|
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
|
+
}
|
|
203
290
|
} catch (error) {
|
|
204
|
-
spinner.
|
|
205
|
-
|
|
291
|
+
spinner.stop();
|
|
292
|
+
if (error.code === 'SECRET_BLOCKED') {
|
|
293
|
+
printSecretBlockedBox(error.message, error.details || {});
|
|
294
|
+
|
|
295
|
+
// 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);
|
|
297
|
+
if (unpushed.length > 0) {
|
|
298
|
+
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
|
+
let doSquash = options.fix === true;
|
|
300
|
+
if (!doSquash && isInteractive) {
|
|
301
|
+
doSquash = await confirm({
|
|
302
|
+
message: '🛠️ Tự động làm sạch lịch sử (squash các commit unpushed thành 1 commit sạch) để push lại?',
|
|
303
|
+
default: true
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (doSquash) {
|
|
307
|
+
spinner.start('Đang squash và làm sạch commit chưa push…');
|
|
308
|
+
const sqRes = squashUnpushedCommits(branchName, commitMsg, parsedDate.formatted);
|
|
309
|
+
if (sqRes.ok) {
|
|
310
|
+
spinner.succeed(chalk.green('✓ Đã làm sạch commit! Đang thử push lại…'));
|
|
311
|
+
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('✓ Push thành công'));
|
|
315
|
+
printSuccessBox({ branch: branchName, repoUrl });
|
|
316
|
+
printFooter();
|
|
317
|
+
return;
|
|
318
|
+
} catch (retryErr) {
|
|
319
|
+
spinner.fail(chalk.red(`Push lại thất bại: ${retryErr.message}`));
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
spinner.fail(chalk.red(`Không thể tự động squash: ${sqRes.error}`));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} else if (error.code === 'AUTH_FAILED') {
|
|
327
|
+
printAuthFailedBox(error.message);
|
|
328
|
+
} else if (error.code === 'NON_FAST_FORWARD') {
|
|
329
|
+
printNonFastForwardBox(error.message, branchName);
|
|
330
|
+
} else if (error.code === 'REMOTE_OR_NETWORK') {
|
|
331
|
+
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
|
+
} else {
|
|
333
|
+
printErrorBox('ĐẨY LÊN GIT THẤT BẠI', [error.message]);
|
|
334
|
+
}
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Lệnh fix độc lập: `git-push-time fix`
|
|
341
|
+
* Tự động quét và fix secret ở staged, working directory và unpushed commits.
|
|
342
|
+
*/
|
|
343
|
+
async function runFixCommand() {
|
|
344
|
+
printBanner(VERSION);
|
|
345
|
+
printSection('🛠️ TỰ ĐỘNG FIX SECRET & BẢO VỆ REPO');
|
|
346
|
+
|
|
347
|
+
if (!checkGitInstalled()) {
|
|
348
|
+
printGitMissing();
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!isGitRepo()) {
|
|
353
|
+
printErrorBox('CHƯA PHẢI GIT REPOSITORY', ['Thư mục này chưa phải là Git repository.'], 'Chạy git init hoặc cd vào đúng thư mục dự án.');
|
|
206
354
|
process.exit(1);
|
|
207
355
|
}
|
|
356
|
+
|
|
357
|
+
const spinner = ora({ color: 'cyan' });
|
|
358
|
+
spinner.start('Đang kiểm tra staged files và thư mục làm việc…');
|
|
359
|
+
|
|
360
|
+
const risky = findRiskyStagedFiles();
|
|
361
|
+
const findings = scanStagedContentForSecrets();
|
|
362
|
+
const riskyWorking = findRiskyWorkingTreeFiles();
|
|
363
|
+
spinner.stop();
|
|
364
|
+
|
|
365
|
+
const allDetected = [...new Set([...risky, ...findings.map((f) => f.file), ...riskyWorking])];
|
|
366
|
+
|
|
367
|
+
if (allDetected.length === 0) {
|
|
368
|
+
printSection('✨ KẾT QUẢ QUÉT');
|
|
369
|
+
console.log(chalk.hex('#34D399')(' ✨ Không phát hiện file secret mới ở stage hay thư mục làm việc!\n'));
|
|
370
|
+
} else {
|
|
371
|
+
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')(`✓ Đã xử lý xong ${res.fixed.length} file!`));
|
|
375
|
+
printFixResult(res);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 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);
|
|
381
|
+
if (unpushed.length > 0) {
|
|
382
|
+
printSection(`🔍 ${unpushed.length} COMMIT CHƯA PUSH`);
|
|
383
|
+
unpushed.slice(0, 5).forEach((c) => console.log(chalk.hex('#94A3B8')(` • ${c}`)));
|
|
384
|
+
if (unpushed.length > 5) {
|
|
385
|
+
console.log(chalk.hex('#64748B')(` …và ${unpushed.length - 5} commit khác`));
|
|
386
|
+
}
|
|
387
|
+
const okSquash = await confirm({
|
|
388
|
+
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?`,
|
|
389
|
+
default: true
|
|
390
|
+
});
|
|
391
|
+
if (okSquash) {
|
|
392
|
+
spinner.start('Đang làm sạch lịch sử commit chưa push…');
|
|
393
|
+
const sq = squashUnpushedCommits(currentBranch, 'Clean commit without secrets');
|
|
394
|
+
if (sq.ok) {
|
|
395
|
+
spinner.succeed(chalk.hex('#34D399')('✓ Đã làm sạch commit chưa push! Sẵn sàng push lên remote.'));
|
|
396
|
+
} else {
|
|
397
|
+
spinner.fail(chalk.red(`Không thể squash: ${sq.error}`));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
printSuccessBox({ branch: currentBranch || 'main', repoUrl: 'local repo đã sạch — sẵn sàng push' });
|
|
208
403
|
}
|
|
209
404
|
|
|
210
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Publish lên npm sau khi push Git xong.
|
|
407
|
+
* An toàn: không nhận token qua flag, dùng `npm login` hoặc env NPM_TOKEN.
|
|
408
|
+
* Nếu .npmrc đang bị track trong git thì cảnh báo (không in token).
|
|
409
|
+
*/
|
|
410
|
+
async function runNpmPublish({ access = 'public', dryRun = false, isInteractive = true } = {}) {
|
|
411
|
+
const spinner = ora({ color: 'magenta' });
|
|
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;
|
|
417
|
+
}
|
|
418
|
+
const pkg = getPackageInfo();
|
|
419
|
+
if (!pkg) {
|
|
420
|
+
printErrorBox('KHÔNG ĐỌC ĐƯỢC PACKAGE.JSON', ['Không tìm thấy package.json ở thư mục hiện tại.'], 'cd vào đúng thư mục dự án rồi chạy lại.');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
console.log(chalk.gray(` 📦 Package: ${chalk.white(pkg.name)}@${chalk.white(pkg.version)} • access: ${access}${dryRun ? ' • dry-run' : ''}`));
|
|
424
|
+
|
|
425
|
+
if (isNpmrcTracked()) {
|
|
426
|
+
console.log(chalk.yellowBright('\n ⚠️ Phát hiện .npmrc đang nằm trong git.'));
|
|
427
|
+
console.log(chalk.gray(' File này thường chứa token, không nên commit/publish cùng code.'));
|
|
428
|
+
console.log(chalk.gray(' Tool sẽ publish mà không cần commit .npmrc — hãy gỡ nó: git rm --cached .npmrc\n'));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const login = checkNpmLogin();
|
|
432
|
+
if (!login.loggedIn && !process.env.NPM_TOKEN && !process.env.NODE_AUTH_TOKEN) {
|
|
433
|
+
console.log(chalk.yellow(' 🔒 Chưa login npm.'));
|
|
434
|
+
console.log(chalk.cyan(' 👉 Chạy: npm login (hoặc export NPM_TOKEN=... khi chạy CI)'));
|
|
435
|
+
if (isInteractive) {
|
|
436
|
+
const go = await confirm({ message: 'Đã login xong, publish tiếp?', default: false });
|
|
437
|
+
if (!go) {
|
|
438
|
+
console.log(chalk.gray(' Đã bỏ qua publish npm.\n'));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
console.log(chalk.gray(' Chế độ -y mà chưa login nên bỏ qua publish.\n'));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
} else if (login.loggedIn) {
|
|
446
|
+
console.log(chalk.gray(` 👤 npm user: ${login.user}`));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
spinner.start(dryRun ? 'Chạy thử npm publish --dry-run…' : `Đang publish ${pkg.name}@${pkg.version}…`);
|
|
450
|
+
const res = publishToNpm({ access, dryRun, cwd: process.cwd() });
|
|
451
|
+
if (res.ok) {
|
|
452
|
+
spinner.stop();
|
|
453
|
+
printNpmResult({ ok: true, pkgName: pkg.name, version: pkg.version, dryRun });
|
|
454
|
+
printFooter();
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
spinner.fail(chalk.red('Publish thất bại'));
|
|
459
|
+
printNpmResult({ ok: false, pkgName: pkg.name, version: pkg.version, output: res.output, hint: res.parsed.hint, dryRun });
|
|
460
|
+
if (res.parsed.code === 'VERSION_EXISTS' && isInteractive) {
|
|
461
|
+
const bump = await confirm({ message: 'Tự tăng patch version (npm version patch) rồi publish lại?', default: true });
|
|
462
|
+
if (bump) {
|
|
463
|
+
const { execSync } = await import('child_process');
|
|
464
|
+
try {
|
|
465
|
+
execSync('npm version patch --no-git-tag-version', { stdio: 'ignore' });
|
|
466
|
+
console.log(chalk.hex('#34D399')(' ✓ Đã tăng version, chạy lại tool với --publish để publish bản mới.\n'));
|
|
467
|
+
} catch {
|
|
468
|
+
printErrorBox('KHÔNG TĂNG ĐƯỢC VERSION', ['Kiểm tra lại package.json.']);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
printFooter();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const program = new Command();
|
|
476
|
+
styleCliHelp(program);
|
|
477
|
+
|
|
478
|
+
program
|
|
479
|
+
.name('git-push-time')
|
|
480
|
+
.description('Đẩy dự án lên Git siêu tốc + tùy chỉnh thời gian commit (Backdate Commit)')
|
|
481
|
+
.version(VERSION)
|
|
482
|
+
.option('-r, --repo <url>', 'Đường dẫn Git repository (URL)')
|
|
483
|
+
.option('-d, --date <datetime>', 'Thời gian commit (Ví dụ: "2024-01-15 14:30:00", "-2d", "-5h", "now")')
|
|
484
|
+
.option('-m, --message <msg>', 'Thông điệp commit (Commit message)')
|
|
485
|
+
.option('-b, --branch <branch>', 'Tên branch trên remote (Mặc định: main)')
|
|
486
|
+
.option('-f, --force', 'Thực hiện push đè (--force)', false)
|
|
487
|
+
.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
|
+
.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
|
+
.action(async (opts) => {
|
|
493
|
+
await run(opts);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
program
|
|
497
|
+
.command('fix')
|
|
498
|
+
.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
|
+
.action(async () => {
|
|
500
|
+
await runFixCommand();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
program.parse(process.argv);
|