git-push-time 1.1.1 → 1.1.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/.github/workflows/npm-publish.yml +1 -2
- package/README.md +219 -215
- package/bin/index.js +25 -23
- package/lib/commands/github.js +16 -10
- package/lib/commands/pull.js +3 -2
- package/lib/commands/push.js +84 -37
- package/lib/git.js +24 -0
- package/lib/prompt-helpers.js +194 -62
- package/lib/ui.js +359 -100
- package/package.json +2 -1
package/lib/commands/github.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { select } from '@inquirer/prompts';
|
|
5
4
|
import {
|
|
6
5
|
getCurrentGitHubUser,
|
|
7
6
|
verifyAndFetchUser,
|
|
@@ -17,7 +16,7 @@ import {
|
|
|
17
16
|
getAllGitHubAccounts,
|
|
18
17
|
setActiveGitHubAccount
|
|
19
18
|
} from '../config.js';
|
|
20
|
-
import { selectWithBack, inputWithBack, BACK } from '../prompt-helpers.js';
|
|
19
|
+
import { selectWithBack, inputWithBack, selectBox, BACK } from '../prompt-helpers.js';
|
|
21
20
|
import {
|
|
22
21
|
printSection,
|
|
23
22
|
printGitHubProfile,
|
|
@@ -110,7 +109,8 @@ async function loginViaToken() {
|
|
|
110
109
|
console.log(chalk.hex('#94A3B8')(' 4. Copy mã token (bắt đầu bằng ghp_ hoặc github_pat_) và dán vào bên dưới.\n'));
|
|
111
110
|
|
|
112
111
|
const tokenInput = await inputWithBack({
|
|
113
|
-
message: '
|
|
112
|
+
message: 'GitHub Token (PAT)',
|
|
113
|
+
hint: ['Bắt đầu bằng ghp_ hoặc github_pat_ · cần quyền repo'],
|
|
114
114
|
validate: (val) => {
|
|
115
115
|
if (!val || !val.trim()) return 'Vui lòng không để trống token!';
|
|
116
116
|
if (val.trim().length < 15) return 'Mã token dường như quá ngắn. Vui lòng kiểm tra lại.';
|
|
@@ -166,8 +166,8 @@ export async function promptGitHubLogin() {
|
|
|
166
166
|
* Bước chọn GitHub Account trong luồng Wizard Push
|
|
167
167
|
* @returns {Promise<{ useGitHub: boolean, user?: any, back?: boolean }>}
|
|
168
168
|
*/
|
|
169
|
-
export async function promptSelectGitHubAccount() {
|
|
170
|
-
printSection('CHỌN TÀI KHOẢN GITHUB');
|
|
169
|
+
export async function promptSelectGitHubAccount({ clearPromptOnDone = false } = {}) {
|
|
170
|
+
if (!clearPromptOnDone) printSection('CHỌN TÀI KHOẢN GITHUB');
|
|
171
171
|
|
|
172
172
|
const accounts = getAllGitHubAccounts();
|
|
173
173
|
const currentStatus = await getCurrentGitHubUser();
|
|
@@ -176,6 +176,7 @@ export async function promptSelectGitHubAccount() {
|
|
|
176
176
|
// Chưa có tài khoản nào
|
|
177
177
|
const loginChoice = await selectWithBack({
|
|
178
178
|
message: 'Bạn chưa kết nối tài khoản GitHub. Bạn muốn làm gì?',
|
|
179
|
+
clearPromptOnDone,
|
|
179
180
|
choices: [
|
|
180
181
|
{
|
|
181
182
|
name: 'Đăng nhập GitHub ngay (Mở trình duyệt xác thực OAuth)',
|
|
@@ -238,6 +239,7 @@ export async function promptSelectGitHubAccount() {
|
|
|
238
239
|
|
|
239
240
|
const picked = await selectWithBack({
|
|
240
241
|
message: 'Chọn tài khoản GitHub sẽ sử dụng:',
|
|
242
|
+
clearPromptOnDone,
|
|
241
243
|
choices
|
|
242
244
|
});
|
|
243
245
|
|
|
@@ -286,7 +288,8 @@ export async function promptCreateRepo({ targetDir = process.cwd() } = {}) {
|
|
|
286
288
|
.replace(/[^a-z0-9_.-]/g, '-');
|
|
287
289
|
|
|
288
290
|
const repoName = await inputWithBack({
|
|
289
|
-
message: 'Tên repository mới
|
|
291
|
+
message: 'Tên repository mới',
|
|
292
|
+
hint: ['Chỉ chữ, số, -, _ , . (VD: my-awesome-repo)'],
|
|
290
293
|
default: folderName || 'my-awesome-repo',
|
|
291
294
|
validate: (val) => {
|
|
292
295
|
const trimmed = (val || '').trim();
|
|
@@ -301,7 +304,8 @@ export async function promptCreateRepo({ targetDir = process.cwd() } = {}) {
|
|
|
301
304
|
if (repoName === BACK) return { ok: false, back: true };
|
|
302
305
|
|
|
303
306
|
const description = await inputWithBack({
|
|
304
|
-
message: 'Mô tả ngắn cho dự án
|
|
307
|
+
message: 'Mô tả ngắn cho dự án',
|
|
308
|
+
hint: ['Tùy chọn, Enter để bỏ qua'],
|
|
305
309
|
default: ''
|
|
306
310
|
});
|
|
307
311
|
if (description === BACK) return { ok: false, back: true };
|
|
@@ -404,8 +408,9 @@ export async function runGitHubMenu({ defaultDir = process.cwd() } = {}) {
|
|
|
404
408
|
{ name: 'Quay lại', value: BACK, description: 'Về bảng điều khiển' }
|
|
405
409
|
];
|
|
406
410
|
|
|
407
|
-
const action = await
|
|
411
|
+
const action = await selectBox({
|
|
408
412
|
message: 'Chọn tác vụ GitHub:',
|
|
413
|
+
hint: 'Tạo repo · xem profile · đổi tài khoản',
|
|
409
414
|
choices
|
|
410
415
|
});
|
|
411
416
|
|
|
@@ -418,8 +423,9 @@ export async function runGitHubMenu({ defaultDir = process.cwd() } = {}) {
|
|
|
418
423
|
} else if (action === 'create') {
|
|
419
424
|
const res = await promptCreateRepo({ targetDir: defaultDir });
|
|
420
425
|
if (res.ok && res.repo) {
|
|
421
|
-
const askPush = await
|
|
422
|
-
message: '
|
|
426
|
+
const askPush = await selectBox({
|
|
427
|
+
message: 'Đẩy dự án hiện tại lên repo mới ngay?',
|
|
428
|
+
hint: 'Tự động gán origin và chuyển sang màn hình push',
|
|
423
429
|
choices: [
|
|
424
430
|
{ name: 'Có, đẩy dự án lên repo này ngay lập tức', value: 'push', description: 'Tự động gán origin và chuyển sang màn hình push' },
|
|
425
431
|
{ name: 'Không, chỉ tạo repo thôi', value: 'skip', description: 'Hoàn tất' }
|
package/lib/commands/pull.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
2
|
+
import { selectBox } from '../prompt-helpers.js';
|
|
3
3
|
import {
|
|
4
4
|
checkGitInstalled,
|
|
5
5
|
isGitRepo,
|
|
@@ -65,8 +65,9 @@ export async function runPullCommand(remote = 'origin', branch, options = {}) {
|
|
|
65
65
|
process.exit(1);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const choice = await
|
|
68
|
+
const choice = await selectBox({
|
|
69
69
|
message: 'Pull fast-forward thất bại. Bạn muốn làm gì tiếp?',
|
|
70
|
+
hint: `git pull --rebase ${remote} ${branch}`,
|
|
70
71
|
choices: [
|
|
71
72
|
{ name: 'Thử kéo về bằng pull --rebase', value: 'rebase', description: `git pull --rebase ${remote} ${branch}` },
|
|
72
73
|
{ name: 'Quay lại', value: 'back', description: 'Về màn hình trước' },
|
package/lib/commands/push.js
CHANGED
|
@@ -3,7 +3,6 @@ import ora from 'ora';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { spawnSync } from 'child_process';
|
|
6
|
-
import { select } from '@inquirer/prompts';
|
|
7
6
|
import { isValidGitUrl, parseCustomDate } from '../utils.js';
|
|
8
7
|
import {
|
|
9
8
|
isGitRepo,
|
|
@@ -34,12 +33,12 @@ import {
|
|
|
34
33
|
listUserRepos
|
|
35
34
|
} from '../github.js';
|
|
36
35
|
import { promptCreateRepo, promptSelectGitHubAccount } from './github.js';
|
|
37
|
-
import { selectWithBack, inputWithBack, promptContinueOrBack, BACK } from '../prompt-helpers.js';
|
|
36
|
+
import { selectWithBack, inputWithBack, promptContinueOrBack, selectBox, BACK } from '../prompt-helpers.js';
|
|
38
37
|
import {
|
|
39
38
|
printSummary,
|
|
40
39
|
printSection,
|
|
40
|
+
printPushProgress,
|
|
41
41
|
stepLabel,
|
|
42
|
-
printTargetDir,
|
|
43
42
|
printNestedRepoWarning,
|
|
44
43
|
printPrePushSecretWarning,
|
|
45
44
|
printSecretBlockedBox,
|
|
@@ -53,19 +52,18 @@ import {
|
|
|
53
52
|
} from '../ui.js';
|
|
54
53
|
|
|
55
54
|
const TOTAL_STEPS = 6;
|
|
55
|
+
const WIZARD_STEPS = 7;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Cho user chọn folder nguồn cần push
|
|
59
59
|
*/
|
|
60
|
-
export async function promptForTargetDir(startDir) {
|
|
60
|
+
export async function promptForTargetDir(startDir, clearPromptOnDone = false) {
|
|
61
61
|
const subs = listSubdirectories(startDir);
|
|
62
62
|
if (subs.length === 0) return startDir;
|
|
63
63
|
|
|
64
|
-
printSection('📁 1. CHỌN FOLDER DỰ ÁN (PROJECT)');
|
|
65
|
-
printTargetDir(startDir);
|
|
66
|
-
|
|
67
64
|
const picked = await selectWithBack({
|
|
68
65
|
message: 'Thư mục nào sẽ được đẩy lên Git?',
|
|
66
|
+
clearPromptOnDone,
|
|
69
67
|
pageSize: Math.min(12, subs.length + 3),
|
|
70
68
|
choices: [
|
|
71
69
|
{ name: 'Thư mục hiện tại (.)', value: startDir, description: 'Push toàn bộ thư mục hiện tại' },
|
|
@@ -82,7 +80,9 @@ export async function promptForTargetDir(startDir) {
|
|
|
82
80
|
if (picked !== '__custom__') return picked;
|
|
83
81
|
|
|
84
82
|
const customPath = await inputWithBack({
|
|
85
|
-
message: '
|
|
83
|
+
message: 'Đường dẫn folder nguồn',
|
|
84
|
+
hint: ['Nhập tuyệt đối hoặc tương đối (VD: ./my-app, E:/projects/demo)'],
|
|
85
|
+
clearPromptOnDone,
|
|
86
86
|
validate: (value) => {
|
|
87
87
|
const r = resolveTargetDir(value, startDir);
|
|
88
88
|
return r.ok || r.error;
|
|
@@ -135,9 +135,7 @@ export async function handleNestedRepo(targetDir, isInteractive, options) {
|
|
|
135
135
|
/**
|
|
136
136
|
* Prompt chọn hoặc tạo repository URL
|
|
137
137
|
*/
|
|
138
|
-
async function promptForRepoSelection(targetDir, gitHubUser = null) {
|
|
139
|
-
printSection('📦 3. CHỌN REPOSITORY');
|
|
140
|
-
|
|
138
|
+
async function promptForRepoSelection(targetDir, gitHubUser = null, clearPromptOnDone = false) {
|
|
141
139
|
const choices = [];
|
|
142
140
|
|
|
143
141
|
if (gitHubUser) {
|
|
@@ -171,6 +169,7 @@ async function promptForRepoSelection(targetDir, gitHubUser = null) {
|
|
|
171
169
|
|
|
172
170
|
const method = await selectWithBack({
|
|
173
171
|
message: 'Bạn muốn chọn repository bằng cách nào?',
|
|
172
|
+
clearPromptOnDone,
|
|
174
173
|
choices
|
|
175
174
|
});
|
|
176
175
|
|
|
@@ -198,6 +197,7 @@ async function promptForRepoSelection(targetDir, gitHubUser = null) {
|
|
|
198
197
|
|
|
199
198
|
const picked = await selectWithBack({
|
|
200
199
|
message: 'Chọn repository bạn muốn push lên:',
|
|
200
|
+
clearPromptOnDone,
|
|
201
201
|
pageSize: Math.min(12, repoChoices.length + 2),
|
|
202
202
|
choices: repoChoices
|
|
203
203
|
});
|
|
@@ -208,7 +208,9 @@ async function promptForRepoSelection(targetDir, gitHubUser = null) {
|
|
|
208
208
|
|
|
209
209
|
// Dán URL thủ công
|
|
210
210
|
const val = await inputWithBack({
|
|
211
|
-
message: '
|
|
211
|
+
message: 'Link Git Repository',
|
|
212
|
+
hint: ['HTTPS hoặc SSH (VD: https://github.com/user/repo.git)'],
|
|
213
|
+
clearPromptOnDone,
|
|
212
214
|
validate: (value) => {
|
|
213
215
|
if (!value || !value.trim()) return 'Vui lòng nhập đường dẫn repository!';
|
|
214
216
|
if (!isValidGitUrl(value)) return 'URL chưa đúng (vd: https://github.com/user/repo.git)';
|
|
@@ -233,8 +235,8 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
233
235
|
let targetDir = startDir;
|
|
234
236
|
let selectedGitHubUser = null;
|
|
235
237
|
let repoUrl = eff.repo;
|
|
236
|
-
let branchName = eff.branch;
|
|
237
|
-
let commitMsg = eff.message;
|
|
238
|
+
let branchName = eff.branch ? String(eff.branch).trim() : undefined;
|
|
239
|
+
let commitMsg = eff.message ? String(eff.message).trim() : undefined;
|
|
238
240
|
let dateInput = eff.date;
|
|
239
241
|
let parsedDate = null;
|
|
240
242
|
|
|
@@ -280,9 +282,22 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
280
282
|
}
|
|
281
283
|
|
|
282
284
|
while (true) {
|
|
285
|
+
const progressTitles = ['Folder', 'Account', 'Repository', 'Branch', 'Message', 'Time', 'Xác nhận'];
|
|
286
|
+
const completed = [];
|
|
287
|
+
if (step > 0) completed.push({ label: 'Folder', value: targetDir });
|
|
288
|
+
if (step > 1) completed.push({ label: 'Account', value: selectedGitHubUser?.login || 'URL thủ công' });
|
|
289
|
+
if (step > 2) {
|
|
290
|
+
const shortRepo = String(repoUrl || '').replace(/\.git$/i, '').match(/(?:github\.com[:/])([^/]+\/[^/]+)$/i)?.[1] || repoUrl;
|
|
291
|
+
completed.push({ label: 'Repository', value: shortRepo || 'Đã chọn' });
|
|
292
|
+
}
|
|
293
|
+
if (step > 3) completed.push({ label: 'Branch', value: branchName || 'main' });
|
|
294
|
+
if (step > 4) completed.push({ label: 'Message', value: commitMsg || 'Initial commit' });
|
|
295
|
+
if (step > 5) completed.push({ label: 'Time', value: parsedDate?.display || dateInput || 'now' });
|
|
296
|
+
printPushProgress({ current: step + 1, total: WIZARD_STEPS, title: progressTitles[step], completed });
|
|
297
|
+
|
|
283
298
|
// BƯỚC 0: 📁 Chọn folder project
|
|
284
299
|
if (step === 0) {
|
|
285
|
-
const picked = await promptForTargetDir(startDir);
|
|
300
|
+
const picked = await promptForTargetDir(startDir, true);
|
|
286
301
|
if (picked === BACK) {
|
|
287
302
|
if (onBackToMenu) return await onBackToMenu();
|
|
288
303
|
printCancelled();
|
|
@@ -298,7 +313,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
298
313
|
|
|
299
314
|
// BƯỚC 1: 👤 Chọn GitHub account
|
|
300
315
|
if (step === 1) {
|
|
301
|
-
const accRes = await promptSelectGitHubAccount();
|
|
316
|
+
const accRes = await promptSelectGitHubAccount({ clearPromptOnDone: true });
|
|
302
317
|
if (accRes.back) {
|
|
303
318
|
// Cho phép quay về chọn folder dù ban đầu có --dir
|
|
304
319
|
delete eff.dir;
|
|
@@ -318,7 +333,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
318
333
|
step = 3;
|
|
319
334
|
continue;
|
|
320
335
|
}
|
|
321
|
-
const pickedUrl = await promptForRepoSelection(targetDir, selectedGitHubUser);
|
|
336
|
+
const pickedUrl = await promptForRepoSelection(targetDir, selectedGitHubUser, true);
|
|
322
337
|
if (pickedUrl === BACK) {
|
|
323
338
|
step = 1;
|
|
324
339
|
continue;
|
|
@@ -338,28 +353,33 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
338
353
|
// BƯỚC 3: 🌿 Chọn branch
|
|
339
354
|
if (step === 3) {
|
|
340
355
|
if (eff.branch) {
|
|
341
|
-
branchName = eff.branch;
|
|
356
|
+
branchName = String(eff.branch).trim();
|
|
342
357
|
delete eff.branch;
|
|
343
358
|
step = 4;
|
|
344
359
|
continue;
|
|
345
360
|
}
|
|
346
|
-
printSection('🌿 4. CHỌN BRANCH REMOTE');
|
|
347
361
|
const currentB = getCurrentBranch(targetDir);
|
|
348
362
|
const val = await inputWithBack({
|
|
349
|
-
message: '
|
|
363
|
+
message: 'Đặt tên nhánh Git',
|
|
364
|
+
promptLabel: 'Tên nhánh',
|
|
365
|
+
hint: [
|
|
366
|
+
'Nhánh local sẽ được đổi tên này trước khi push lên origin.',
|
|
367
|
+
'Ví dụ: main, dev, feature/login.',
|
|
368
|
+
],
|
|
350
369
|
default: currentB || 'main',
|
|
370
|
+
clearPromptOnDone: true,
|
|
351
371
|
validate: (v) => {
|
|
352
372
|
if (!v || !v.trim()) return true;
|
|
353
|
-
return isValidBranchName(v.trim()) || 'Tên
|
|
373
|
+
return isValidBranchName(v.trim()) || 'Tên nhánh không hợp lệ: không dùng dấu cách, .., ~ ^ : ? * [ \\ hoặc @{.';
|
|
354
374
|
}
|
|
355
375
|
});
|
|
356
376
|
if (val === BACK) {
|
|
357
377
|
step = 2;
|
|
358
378
|
continue;
|
|
359
379
|
}
|
|
360
|
-
branchName = val || currentB || 'main';
|
|
380
|
+
branchName = String(val || currentB || 'main').trim();
|
|
361
381
|
if (!isValidBranchName(branchName)) {
|
|
362
|
-
printErrorBox('
|
|
382
|
+
printErrorBox('TÊN NHÁNH KHÔNG HỢP LỆ', [`Tên nhánh: '${branchName}'`]);
|
|
363
383
|
continue;
|
|
364
384
|
}
|
|
365
385
|
step = 4;
|
|
@@ -369,21 +389,26 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
369
389
|
// BƯỚC 4: 💬 Nhập commit message
|
|
370
390
|
if (step === 4) {
|
|
371
391
|
if (eff.message) {
|
|
372
|
-
commitMsg = eff.message;
|
|
392
|
+
commitMsg = String(eff.message).trim();
|
|
373
393
|
delete eff.message;
|
|
374
394
|
step = 5;
|
|
375
395
|
continue;
|
|
376
396
|
}
|
|
377
|
-
printSection('💬 5. NHẬP COMMIT MESSAGE');
|
|
378
397
|
const val = await inputWithBack({
|
|
379
|
-
message: '
|
|
380
|
-
|
|
398
|
+
message: 'Viết commit message',
|
|
399
|
+
promptLabel: 'Commit',
|
|
400
|
+
hint: [
|
|
401
|
+
'Mô tả ngắn thay đổi bạn sắp đẩy.',
|
|
402
|
+
'Ví dụ: feat: thêm đăng nhập · fix: sửa lỗi push.',
|
|
403
|
+
],
|
|
404
|
+
default: 'Initial commit',
|
|
405
|
+
clearPromptOnDone: true
|
|
381
406
|
});
|
|
382
407
|
if (val === BACK) {
|
|
383
408
|
step = 3;
|
|
384
409
|
continue;
|
|
385
410
|
}
|
|
386
|
-
commitMsg = val || 'Initial commit';
|
|
411
|
+
commitMsg = String(val || '').trim() || 'Initial commit';
|
|
387
412
|
step = 5;
|
|
388
413
|
continue;
|
|
389
414
|
}
|
|
@@ -396,9 +421,10 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
396
421
|
step = 6;
|
|
397
422
|
continue;
|
|
398
423
|
}
|
|
399
|
-
printSection('⏰ 6. CHỌN THỜI GIAN COMMIT');
|
|
400
424
|
const dateOption = await selectWithBack({
|
|
401
|
-
message: 'Thời gian
|
|
425
|
+
message: 'Bước 6/7 · Thời gian commit',
|
|
426
|
+
hint: 'Chọn mốc thời gian ghi vào commit',
|
|
427
|
+
clearPromptOnDone: true,
|
|
402
428
|
choices: [
|
|
403
429
|
{ name: 'Ngay bây giờ', value: 'now', description: 'Sử dụng thời gian hiện tại' },
|
|
404
430
|
{ name: 'Lùi thời gian (Backdate)', value: 'relative', description: 'VD: -2d (2 ngày trước), -5h, -30m, -1w' },
|
|
@@ -414,16 +440,20 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
414
440
|
dateInput = 'now';
|
|
415
441
|
} else if (dateOption === 'relative') {
|
|
416
442
|
const val = await inputWithBack({
|
|
417
|
-
message: '
|
|
443
|
+
message: 'Lùi thời gian commit (Backdate)',
|
|
444
|
+
hint: ['-2d = 2 ngày trước · -5h = 5 giờ trước · -30m = 30 phút trước · -1w = 1 tuần trước'],
|
|
418
445
|
default: '-1d',
|
|
446
|
+
clearPromptOnDone: true,
|
|
419
447
|
validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Thời gian không hợp lệ'; }
|
|
420
448
|
});
|
|
421
449
|
if (val === BACK) continue;
|
|
422
450
|
dateInput = val;
|
|
423
451
|
} else if (dateOption === 'custom') {
|
|
424
452
|
const val = await inputWithBack({
|
|
425
|
-
message: '
|
|
453
|
+
message: 'Ngày giờ cụ thể cho commit',
|
|
454
|
+
hint: ['Định dạng: YYYY-MM-DD HH:mm:ss (VD: 2024-01-15 10:00:00)'],
|
|
426
455
|
default: '2024-01-15 10:00:00',
|
|
456
|
+
clearPromptOnDone: true,
|
|
427
457
|
validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Ngày giờ không hợp lệ'; }
|
|
428
458
|
});
|
|
429
459
|
if (val === BACK) continue;
|
|
@@ -460,8 +490,9 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
460
490
|
sourceDir: targetDir
|
|
461
491
|
});
|
|
462
492
|
|
|
463
|
-
const decision = await
|
|
493
|
+
const decision = await selectBox({
|
|
464
494
|
message: 'Xác nhận đẩy dự án lên Git ngay?',
|
|
495
|
+
hint: 'Kiểm tra tóm tắt bên trên trước khi push',
|
|
465
496
|
choices: [
|
|
466
497
|
{ name: 'Bắt đầu đẩy code (Push)', value: 'continue', description: 'Thực thi git init/add/commit/push' },
|
|
467
498
|
{ name: 'Sửa thời gian commit', value: 'back-date', description: 'Về bước chọn thời gian' },
|
|
@@ -494,6 +525,8 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
494
525
|
|
|
495
526
|
// 🚀 BƯỚC 7: THỰC THI PUSH
|
|
496
527
|
const spinner = ora({ color: 'cyan' });
|
|
528
|
+
let secretStatus = '✓ Không phát hiện';
|
|
529
|
+
let pushDurationSeconds = null;
|
|
497
530
|
printSection(`🚀 ĐANG ĐẨY CODE LÊN GIT (${TOTAL_STEPS} BƯỚC)`);
|
|
498
531
|
|
|
499
532
|
try {
|
|
@@ -519,6 +552,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
519
552
|
// 3.5. Quét secret nhạy cảm
|
|
520
553
|
const risky = findRiskyStagedFiles(targetDir);
|
|
521
554
|
const findings = scanStagedContentForSecrets(targetDir);
|
|
555
|
+
if (risky.length > 0 || findings.length > 0) secretStatus = `! Có ${risky.length + findings.length} cảnh báo`;
|
|
522
556
|
if (risky.length > 0 || findings.length > 0) {
|
|
523
557
|
spinner.stop();
|
|
524
558
|
printPrePushSecretWarning({ riskyFiles: risky, findings });
|
|
@@ -546,6 +580,12 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
546
580
|
spinner.succeed(chalk.hex('#34D399')(`[OK] Đã gỡ ${res.fixed.length} file khỏi commit`));
|
|
547
581
|
printFixResult(res);
|
|
548
582
|
|
|
583
|
+
const remainingRisky = findRiskyStagedFiles(targetDir);
|
|
584
|
+
const remainingFindings = scanStagedContentForSecrets(targetDir);
|
|
585
|
+
secretStatus = remainingRisky.length || remainingFindings.length
|
|
586
|
+
? `! Còn ${remainingRisky.length + remainingFindings.length} cảnh báo`
|
|
587
|
+
: '✓ Đã gỡ cảnh báo';
|
|
588
|
+
|
|
549
589
|
spawnSync('git', ['add', '.gitignore'], { cwd: targetDir, stdio: 'ignore' });
|
|
550
590
|
|
|
551
591
|
if (!hasStagedChanges(targetDir)) {
|
|
@@ -576,10 +616,12 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
576
616
|
|
|
577
617
|
// 6. Push
|
|
578
618
|
spinner.start(stepLabel(6, TOTAL_STEPS, `Push lên origin/${branchName}...`));
|
|
619
|
+
const pushStartedAt = Date.now();
|
|
579
620
|
pushToRemote(branchName, options.force, targetDir);
|
|
621
|
+
pushDurationSeconds = (Date.now() - pushStartedAt) / 1000;
|
|
580
622
|
spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('Push lên remote thành công!')));
|
|
581
623
|
|
|
582
|
-
printSuccessBox({ branch: branchName, repoUrl });
|
|
624
|
+
printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
|
|
583
625
|
printFooter();
|
|
584
626
|
} catch (error) {
|
|
585
627
|
spinner.stop();
|
|
@@ -607,9 +649,11 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
607
649
|
spinner.succeed(chalk.green('[OK] Đã làm sạch commit! Đang thử push lại...'));
|
|
608
650
|
try {
|
|
609
651
|
spinner.start(stepLabel(6, TOTAL_STEPS, `Push lại lên origin/${branchName}...`));
|
|
652
|
+
const retryStartedAt = Date.now();
|
|
610
653
|
pushToRemote(branchName, options.force, targetDir);
|
|
654
|
+
pushDurationSeconds = (Date.now() - retryStartedAt) / 1000;
|
|
611
655
|
spinner.succeed(chalk.bold.green('[OK] Push thành công'));
|
|
612
|
-
printSuccessBox({ branch: branchName, repoUrl });
|
|
656
|
+
printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
|
|
613
657
|
printFooter();
|
|
614
658
|
return;
|
|
615
659
|
} catch (retryErr) {
|
|
@@ -625,8 +669,9 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
625
669
|
} else if (error.code === 'NON_FAST_FORWARD') {
|
|
626
670
|
printNonFastForwardBox(error.message, branchName);
|
|
627
671
|
if (isInteractive) {
|
|
628
|
-
const ffChoice = await
|
|
672
|
+
const ffChoice = await selectBox({
|
|
629
673
|
message: 'Remote đã có commit mới hơn. Bạn muốn xử lý thế nào?',
|
|
674
|
+
hint: `git pull --rebase origin ${branchName} rồi push lại`,
|
|
630
675
|
choices: [
|
|
631
676
|
{ name: 'Pull (rebase) rồi push lại — Khuyên dùng', value: 'pull-push', description: `git pull --rebase origin ${branchName} rồi push lại` },
|
|
632
677
|
{ name: 'Chỉ Pull về (chưa push vội)', value: 'pull-only', description: 'Chỉ đồng bộ code về máy' },
|
|
@@ -642,9 +687,11 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
|
642
687
|
if (pullOut) console.log(chalk.hex('#64748B')(` ${String(pullOut).split('\n')[0].slice(0, 120)}`));
|
|
643
688
|
if (ffChoice === 'pull-push') {
|
|
644
689
|
spinner.start(stepLabel(6, TOTAL_STEPS, `Push lại lên origin/${branchName}...`));
|
|
690
|
+
const retryStartedAt = Date.now();
|
|
645
691
|
pushToRemote(branchName, options.force, targetDir);
|
|
692
|
+
pushDurationSeconds = (Date.now() - retryStartedAt) / 1000;
|
|
646
693
|
spinner.succeed(chalk.bold.hex('#34D399')('Push lại thành công sau khi pull!'));
|
|
647
|
-
printSuccessBox({ branch: branchName, repoUrl });
|
|
694
|
+
printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
|
|
648
695
|
printFooter();
|
|
649
696
|
return;
|
|
650
697
|
}
|
package/lib/git.js
CHANGED
|
@@ -116,6 +116,30 @@ export function getCurrentBranch(cwd = process.cwd()) {
|
|
|
116
116
|
return getCurrentBranchStrict(cwd) || 'main';
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** Return the current local change count, origin URL, and last commit for the dashboard. */
|
|
120
|
+
export function getDashboardGitStatus(cwd = process.cwd()) {
|
|
121
|
+
const status = spawnSync('git', ['status', '--short'], { cwd, encoding: 'utf-8' });
|
|
122
|
+
const remote = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd, encoding: 'utf-8' });
|
|
123
|
+
const log = spawnSync('git', ['log', '-1', '--format=%h|%s|%cr'], { cwd, encoding: 'utf-8' });
|
|
124
|
+
const changes = status.status === 0
|
|
125
|
+
? (status.stdout || '').split(/\r?\n/).filter(Boolean).length
|
|
126
|
+
: 0;
|
|
127
|
+
|
|
128
|
+
let lastCommit = null;
|
|
129
|
+
if (log.status === 0 && (log.stdout || '').trim()) {
|
|
130
|
+
const [hash, msg, rel] = (log.stdout || '').trim().split('|');
|
|
131
|
+
if (hash) {
|
|
132
|
+
lastCommit = { hash, message: msg || '', relativeTime: rel || '' };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
changes,
|
|
138
|
+
remoteUrl: remote.status === 0 ? (remote.stdout || '').trim() : null,
|
|
139
|
+
lastCommit,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
119
143
|
/**
|
|
120
144
|
* Lấy tên branch hiện tại, trả về null khi detached HEAD hoặc lỗi.
|
|
121
145
|
* @param {string} cwd
|