git-push-time 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,127 +1,141 @@
1
- import chalk from 'chalk';
2
- import {
3
- checkGitInstalled,
4
- isGitRepo,
5
- getCurrentBranch,
6
- getRecentCommits,
7
- rewriteCommitDate,
8
- resolveTargetDir
9
- } from '../git.js';
10
- import { parseCustomDate } from '../utils.js';
11
- import { selectWithBack, inputWithBack, BACK } from '../prompt-helpers.js';
12
- import {
13
- printSection,
14
- printTargetDir,
15
- printGitMissing,
16
- printErrorBox
17
- } from '../ui.js';
18
-
19
- export async function runEditCommitTimeCommand(options = {}) {
20
- const fromMenu = options.fromMenu === true;
21
-
22
- let targetDir = process.cwd();
23
- if (options.dir) {
24
- const r = resolveTargetDir(options.dir, process.cwd());
25
- if (!r.ok) {
26
- printErrorBox('THƯ MỤC KHÔNG HỢP LỆ', [r.error]);
27
- if (fromMenu) return;
28
- process.exit(1);
29
- }
30
- targetDir = r.fullPath;
31
- }
32
-
33
- if (!checkGitInstalled() || !isGitRepo(targetDir)) {
34
- printErrorBox('CHƯA SẴN SÀNG', [`Hãy chạy lệnh trong một Git repository hợp lệ. (Thư mục: ${targetDir})`]);
35
- if (fromMenu) return;
36
- process.exit(1);
37
- }
38
-
39
- printSection('SỬA THỜI GIAN COMMIT (BACKDATE)');
40
- printTargetDir(targetDir);
41
-
42
- try {
43
- const commits = getRecentCommits(100, targetDir);
44
- if (commits.length === 0) {
45
- printErrorBox('CHƯA CÓ COMMIT', ['Repository chưa có commit nào.']);
46
- if (fromMenu) return;
47
- process.exit(1);
48
- }
49
-
50
- // Bước 1: Chọn commit
51
- let commit = commits[0];
52
- if (commits.length > 1) {
53
- const picked = await selectWithBack({
54
- message: `Chọn commit cần đổi thời gian (${commits.length} commit gần nhất):`,
55
- pageSize: 12,
56
- choices: commits.map((item) => ({
57
- name: `» ${item.date} ${item.shortHash} ${item.subject}`,
58
- value: item,
59
- description: 'Sửa commit này sẽ viết lại nó và các commit phía sau'
60
- }))
61
- });
62
- if (picked === BACK) return;
63
- commit = picked;
64
- } else {
65
- console.log(chalk.hex('#94A3B8')(` » Chỉ có 1 commit nên tự chọn: ${commit.shortHash} — ${commit.subject}\n`));
66
- }
67
-
68
- // Bước 2: Chọn kiểu thời gian
69
- const dateOption = await selectWithBack({
70
- message: 'Chọn thời gian mới:',
71
- choices: [
72
- { name: '» Ngay bây giờ', value: 'now', description: 'Dùng thời gian hiện tại' },
73
- { name: '» Nhập thời gian tương đối', value: 'relative', description: 'VD: -2d, -5h, -30m' },
74
- { name: '» Nhập ngày giờ cụ thể', value: 'custom', description: 'VD: 2024-01-15 14:30:00' }
75
- ]
76
- });
77
- if (dateOption === BACK) {
78
- return await runEditCommitTimeCommand(options);
79
- }
80
-
81
- let dateInput = dateOption;
82
- if (dateOption === 'relative') {
83
- const val = await inputWithBack({
84
- message: 'Nhập thời gian (ví dụ: -2d, -5h, -30m):',
85
- default: '-1d',
86
- validate: (value) => {
87
- const parsed = parseCustomDate(value);
88
- return parsed.valid || parsed.error;
89
- }
90
- });
91
- if (val === BACK) return await runEditCommitTimeCommand(options);
92
- dateInput = val;
93
- } else if (dateOption === 'custom') {
94
- const val = await inputWithBack({
95
- message: 'Nhập ngày giờ (YYYY-MM-DD HH:mm:ss):',
96
- validate: (value) => {
97
- const parsed = parseCustomDate(value);
98
- return parsed.valid || parsed.error;
99
- }
100
- });
101
- if (val === BACK) return await runEditCommitTimeCommand(options);
102
- dateInput = val;
103
- }
104
-
105
- const parsed = parseCustomDate(dateInput);
106
- if (!parsed.valid) {
107
- printErrorBox('THỜI GIAN KHÔNG HỢP LỆ', [parsed.error], 'VD: now, -2d, -5h, 2024-01-15 14:30:00');
108
- if (fromMenu) return;
109
- process.exit(1);
110
- }
111
-
112
- const currentBranch = getCurrentBranch(targetDir);
113
- const result = rewriteCommitDate(commit.hash, parsed.formatted, targetDir);
114
-
115
- printSection('ĐÃ ĐỔI THỜI GIAN COMMIT THÀNH CÔNG');
116
- console.log(` Nhánh: ${chalk.hex('#38BDF8').bold(currentBranch)}`);
117
- console.log(` Commit: ${chalk.hex('#FBBF24').bold(commit.shortHash)} — ${commit.subject}`);
118
- console.log(` Thời gian mới: ${chalk.hex('#C084FC').bold(parsed.display)}`);
119
- console.log(` Đã viết lại ${result.rewrittenCount} commit trên branch hiện tại.`);
120
- console.log(` Nhánh backup an toàn: ${chalk.hex('#6EE7B7')(result.backupBranch)}`);
121
- console.log(chalk.hex('#FBBF24')(' [!] Nếu commit đã từng push, bạn cần dùng force push (--force) để cập nhật remote.\n'));
122
- } catch (error) {
123
- 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.');
124
- if (fromMenu) return;
125
- process.exit(1);
126
- }
127
- }
1
+ import chalk from 'chalk';
2
+ import {
3
+ checkGitInstalled,
4
+ isGitRepo,
5
+ getCurrentBranch,
6
+ getRecentCommits,
7
+ rewriteCommitDate,
8
+ resolveTargetDir
9
+ } from '../git.js';
10
+ import { parseCustomDate } from '../utils.js';
11
+ import { selectWithBack, inputWithBack, BACK } from '../prompt-helpers.js';
12
+ import {
13
+ printSection,
14
+ printTargetDir,
15
+ printGitMissing,
16
+ printErrorBox
17
+ } from '../ui.js';
18
+
19
+ export async function runEditCommitTimeCommand(options = {}) {
20
+ const fromMenu = options.fromMenu === true;
21
+
22
+ let targetDir = process.cwd();
23
+ if (options.dir) {
24
+ const r = resolveTargetDir(options.dir, process.cwd());
25
+ if (!r.ok) {
26
+ printErrorBox('THƯ MỤC KHÔNG HỢP LỆ', [r.error]);
27
+ if (fromMenu) return;
28
+ process.exit(1);
29
+ }
30
+ targetDir = r.fullPath;
31
+ }
32
+
33
+ if (!checkGitInstalled()) {
34
+ printGitMissing();
35
+ if (fromMenu) return;
36
+ process.exit(1);
37
+ }
38
+ if (!isGitRepo(targetDir)) {
39
+ printErrorBox('CHƯA PHẢI GIT REPOSITORY', [`Thư mục '${targetDir}' chưa phải là Git repository.`]);
40
+ if (fromMenu) return;
41
+ process.exit(1);
42
+ }
43
+
44
+ printSection('SỬA THỜI GIAN COMMIT (BACKDATE)');
45
+ printTargetDir(targetDir);
46
+
47
+ try {
48
+ const commits = getRecentCommits(100, targetDir);
49
+ if (commits.length === 0) {
50
+ printErrorBox('CHƯA CÓ COMMIT', ['Repository chưa có commit nào.']);
51
+ if (fromMenu) return;
52
+ process.exit(1);
53
+ }
54
+
55
+ // Dùng loop thay vì đệ quy để tránh stack tăng khi Back nhiều lần
56
+ let commit = null;
57
+ let dateInput = null;
58
+ while (true) {
59
+ // Bước 1: Chọn commit
60
+ if (!commit) {
61
+ if (commits.length === 1) {
62
+ commit = commits[0];
63
+ console.log(chalk.hex('#94A3B8')(` » Chỉ có 1 commit nên tự chọn: ${commit.shortHash} — ${commit.subject}\n`));
64
+ } else {
65
+ const picked = await selectWithBack({
66
+ message: `Chọn commit cần đổi thời gian (${commits.length} commit gần nhất):`,
67
+ pageSize: 12,
68
+ choices: commits.map((item) => ({
69
+ name: `${item.date} ${item.shortHash} ${item.subject}`,
70
+ value: item,
71
+ description: 'Sửa commit này sẽ viết lại nó và các commit phía sau'
72
+ }))
73
+ });
74
+ if (picked === BACK) return;
75
+ commit = picked;
76
+ }
77
+ }
78
+
79
+ // Bước 2: Chọn kiểu thời gian
80
+ const dateOption = await selectWithBack({
81
+ message: 'Chọn thời gian mới:',
82
+ choices: [
83
+ { name: 'Ngay bây giờ', value: 'now', description: 'Dùng thời gian hiện tại' },
84
+ { name: 'Nhập thời gian tương đối', value: 'relative', description: 'VD: -2d, -5h, -30m, -3M' },
85
+ { name: 'Nhập ngày giờ cụ thể', value: 'custom', description: 'VD: 2024-01-15 14:30:00' }
86
+ ]
87
+ });
88
+ if (dateOption === BACK) {
89
+ commit = null; // quay về chọn commit lại
90
+ continue;
91
+ }
92
+
93
+ dateInput = dateOption;
94
+ if (dateOption === 'relative') {
95
+ const val = await inputWithBack({
96
+ message: 'Nhập thời gian (ví dụ: -2d, -5h, -30m, -3M):',
97
+ default: '-1d',
98
+ validate: (value) => {
99
+ const parsed = parseCustomDate(value);
100
+ return parsed.valid || parsed.error;
101
+ }
102
+ });
103
+ if (val === BACK) continue; // về chọn kiểu thời gian
104
+ dateInput = val;
105
+ } else if (dateOption === 'custom') {
106
+ const val = await inputWithBack({
107
+ message: 'Nhập ngày giờ (YYYY-MM-DD HH:mm:ss):',
108
+ validate: (value) => {
109
+ const parsed = parseCustomDate(value);
110
+ return parsed.valid || parsed.error;
111
+ }
112
+ });
113
+ if (val === BACK) continue; // về chọn kiểu thời gian
114
+ dateInput = val;
115
+ }
116
+ break;
117
+ }
118
+
119
+ const parsed = parseCustomDate(dateInput);
120
+ if (!parsed.valid) {
121
+ printErrorBox('THỜI GIAN KHÔNG HỢP LỆ', [parsed.error], 'VD: now, -2d, -5h, 2024-01-15 14:30:00');
122
+ if (fromMenu) return;
123
+ process.exit(1);
124
+ }
125
+
126
+ const currentBranch = getCurrentBranch(targetDir);
127
+ const result = rewriteCommitDate(commit.hash, parsed.formatted, targetDir);
128
+
129
+ printSection('ĐÃ ĐỔI THỜI GIAN COMMIT THÀNH CÔNG');
130
+ console.log(` Nhánh: ${chalk.hex('#38BDF8').bold(currentBranch)}`);
131
+ console.log(` Commit: ${chalk.hex('#FBBF24').bold(commit.shortHash)} — ${commit.subject}`);
132
+ console.log(` Thời gian mới: ${chalk.hex('#C084FC').bold(parsed.display)}`);
133
+ console.log(` Đã viết lại ${result.rewrittenCount} commit trên branch hiện tại.`);
134
+ console.log(` Nhánh backup an toàn: ${chalk.hex('#6EE7B7')(result.backupBranch)}`);
135
+ console.log(chalk.hex('#FBBF24')(' [!] Nếu commit đã từng push, bạn cần dùng force push (--force) để cập nhật remote.\n'));
136
+ } catch (error) {
137
+ 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.');
138
+ if (fromMenu) return;
139
+ process.exit(1);
140
+ }
141
+ }
@@ -1,105 +1,113 @@
1
- import chalk from 'chalk';
2
- import ora from 'ora';
3
- import {
4
- checkGitInstalled,
5
- isGitRepo,
6
- getCurrentBranch,
7
- findRiskyStagedFiles,
8
- resolveTargetDir
9
- } from '../git.js';
10
- import { scanStagedContentForSecrets } from '../secrets.js';
11
- import {
12
- autoFixStagedSecrets,
13
- findRiskyWorkingTreeFiles,
14
- getUnpushedCommits,
15
- squashUnpushedCommits
16
- } from '../fix.js';
17
- import { selectWithBack, BACK } from '../prompt-helpers.js';
18
- import {
19
- printSection,
20
- printTargetDir,
21
- printGitMissing,
22
- printErrorBox,
23
- printPrePushSecretWarning,
24
- printFixResult,
25
- printSuccessBox
26
- } from '../ui.js';
27
-
28
- export async function runFixCommand(options = {}) {
29
- const fromMenu = options.fromMenu === true;
30
-
31
- if (!checkGitInstalled()) {
32
- printGitMissing();
33
- if (fromMenu) return;
34
- process.exit(1);
35
- }
36
-
37
- let targetDir = process.cwd();
38
- if (options.dir) {
39
- const r = resolveTargetDir(options.dir, process.cwd());
40
- if (!r.ok) {
41
- printErrorBox('THƯ MỤC KHÔNG HỢP LỆ', [r.error]);
42
- if (fromMenu) return;
43
- process.exit(1);
44
- }
45
- targetDir = r.fullPath;
46
- }
47
-
48
- printSection('QUÉT & GỠ SECRET BẢO VỆ REPO');
49
- printTargetDir(targetDir);
50
-
51
- if (!isGitRepo(targetDir)) {
52
- printErrorBox('CHƯA PHẢI GIT REPOSITORY', [`Thư mục '${targetDir}' chưa phải là Git repository.`]);
53
- if (fromMenu) return;
54
- process.exit(1);
55
- }
56
-
57
- const spinner = ora({ text: 'Đang kiểm tra staged files và thư mục làm việc…', color: 'cyan' }).start();
58
- const risky = findRiskyStagedFiles(targetDir);
59
- const findings = scanStagedContentForSecrets(targetDir);
60
- const riskyWorking = findRiskyWorkingTreeFiles(targetDir);
61
- spinner.stop();
62
-
63
- const allDetected = [...new Set([...risky, ...findings.map((f) => f.file), ...riskyWorking])];
64
-
65
- if (allDetected.length === 0) {
66
- 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'));
67
- } else {
68
- printPrePushSecretWarning({ riskyFiles: allDetected, findings });
69
- spinner.start('Đang tự động fix (gỡ khỏi stage + thêm .gitignore)...');
70
- const res = autoFixStagedSecrets(allDetected, targetDir);
71
- spinner.succeed(chalk.hex('#34D399')(`[OK] Đã xử lý xong ${res.fixed.length} file!`));
72
- printFixResult(res);
73
- }
74
-
75
- // Kiểm tra unpushed commits
76
- const currentBranch = getCurrentBranch(targetDir);
77
- const unpushed = getUnpushedCommits(currentBranch, targetDir);
78
- if (unpushed.length > 0) {
79
- printSection(`${unpushed.length} COMMIT CHƯA PUSH TRÊN LOCAL`);
80
- unpushed.slice(0, 5).forEach((c) => console.log(chalk.hex('#94A3B8')(` - ${c}`)));
81
- if (unpushed.length > 5) {
82
- console.log(chalk.hex('#64748B')(` ...và ${unpushed.length - 5} commit khác`));
83
- }
84
-
85
- const sqChoice = await selectWithBack({
86
- message: `Gộp (squash) ${unpushed.length} commit thành 1 commit sạch để xóa vết secret cũ trong lịch sử?`,
87
- choices: [
88
- { name: '» Có, gộp thành 1 commit sạch', value: 'yes', description: 'Làm sạch hoàn toàn lịch sử commit' },
89
- { name: '» Không, giữ nguyên', value: 'no', description: 'Bỏ qua' }
90
- ]
91
- });
92
-
93
- if (sqChoice !== BACK && sqChoice === 'yes') {
94
- spinner.start('Đang làm sạch lịch sử commit chưa push...');
95
- const sq = squashUnpushedCommits(currentBranch, 'Clean commit without secrets', null, targetDir);
96
- if (sq.ok) {
97
- spinner.succeed(chalk.hex('#34D399')('[OK] Đã làm sạch commit chưa push! Sẵn sàng push lên remote an toàn.'));
98
- } else {
99
- spinner.fail(chalk.red(`Không thể squash: ${sq.error}`));
100
- }
101
- }
102
- }
103
-
104
- printSuccessBox({ branch: currentBranch || 'main', repoUrl: 'Local repo sạch sẽ — an toàn tuyệt đối' });
105
- }
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import {
4
+ checkGitInstalled,
5
+ isGitRepo,
6
+ getCurrentBranchStrict,
7
+ findRiskyStagedFiles,
8
+ resolveTargetDir
9
+ } from '../git.js';
10
+ import { scanStagedContentForSecrets } from '../secrets.js';
11
+ import {
12
+ autoFixStagedSecrets,
13
+ findRiskyWorkingTreeFiles,
14
+ getUnpushedCommits,
15
+ squashUnpushedCommits
16
+ } from '../fix.js';
17
+ import { selectWithBack, BACK } from '../prompt-helpers.js';
18
+ import {
19
+ printSection,
20
+ printTargetDir,
21
+ printGitMissing,
22
+ printErrorBox,
23
+ printPrePushSecretWarning,
24
+ printFixResult
25
+ } from '../ui.js';
26
+
27
+ export async function runFixCommand(options = {}) {
28
+ const fromMenu = options.fromMenu === true;
29
+
30
+ if (!checkGitInstalled()) {
31
+ printGitMissing();
32
+ if (fromMenu) return;
33
+ process.exit(1);
34
+ }
35
+
36
+ let targetDir = process.cwd();
37
+ if (options.dir) {
38
+ const r = resolveTargetDir(options.dir, process.cwd());
39
+ if (!r.ok) {
40
+ printErrorBox('THƯ MỤC KHÔNG HỢP LỆ', [r.error]);
41
+ if (fromMenu) return;
42
+ process.exit(1);
43
+ }
44
+ targetDir = r.fullPath;
45
+ }
46
+
47
+ printSection('QUÉT & GỠ SECRET BẢO VỆ REPO');
48
+ printTargetDir(targetDir);
49
+
50
+ if (!isGitRepo(targetDir)) {
51
+ printErrorBox('CHƯA PHẢI GIT REPOSITORY', [`Thư mục '${targetDir}' chưa phải là Git repository.`]);
52
+ if (fromMenu) return;
53
+ process.exit(1);
54
+ }
55
+
56
+ const spinner = ora({ text: 'Đang kiểm tra staged files và thư mục làm việc…', color: 'cyan' }).start();
57
+ const risky = findRiskyStagedFiles(targetDir);
58
+ const findings = scanStagedContentForSecrets(targetDir);
59
+ const riskyWorking = findRiskyWorkingTreeFiles(targetDir);
60
+ spinner.stop();
61
+
62
+ const allDetected = [...new Set([...risky, ...findings.map((f) => f.file), ...riskyWorking])];
63
+ const hadSecrets = allDetected.length > 0;
64
+
65
+ if (allDetected.length === 0) {
66
+ 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'));
67
+ } else {
68
+ printPrePushSecretWarning({ riskyFiles: allDetected, findings });
69
+ spinner.start('Đang tự động fix (gỡ khỏi stage + thêm .gitignore)...');
70
+ const res = autoFixStagedSecrets(allDetected, targetDir);
71
+ spinner.succeed(chalk.hex('#34D399')(`[OK] Đã xử lý xong ${res.fixed.length} file!`));
72
+ printFixResult(res);
73
+ }
74
+
75
+ // Kiểm tra unpushed commits — chỉ gợi ý squash khi có dấu hiệu secret,
76
+ // tránh viết lại lịch sử sạch không cần thiết.
77
+ const currentBranch = getCurrentBranchStrict(targetDir) || 'main';
78
+ const unpushed = getUnpushedCommits(currentBranch, targetDir);
79
+ if (unpushed.length > 0 && hadSecrets) {
80
+ printSection(`${unpushed.length} COMMIT CHƯA PUSH TRÊN LOCAL`);
81
+ unpushed.slice(0, 5).forEach((c) => console.log(chalk.hex('#94A3B8')(` - ${c}`)));
82
+ if (unpushed.length > 5) {
83
+ console.log(chalk.hex('#64748B')(` ...và ${unpushed.length - 5} commit khác`));
84
+ }
85
+
86
+ let sqChoice = 'no';
87
+ if (options.yes) {
88
+ sqChoice = 'no';
89
+ } else {
90
+ sqChoice = await selectWithBack({
91
+ message: `Gộp (squash) ${unpushed.length} commit thành 1 commit sạch để xóa vết secret cũ trong lịch sử?`,
92
+ choices: [
93
+ { name: 'Có, gộp thành 1 commit sạch', value: 'yes', description: 'Làm sạch hoàn toàn lịch sử commit' },
94
+ { name: 'Không, giữ nguyên', value: 'no', description: 'Bỏ qua' }
95
+ ]
96
+ });
97
+ }
98
+
99
+ if (sqChoice !== BACK && sqChoice === 'yes') {
100
+ spinner.start('Đang làm sạch lịch sử commit chưa push...');
101
+ const sq = squashUnpushedCommits(currentBranch, 'Clean commit without secrets', null, targetDir);
102
+ if (sq.ok) {
103
+ spinner.succeed(chalk.hex('#34D399')('[OK] Đã làm sạch commit chưa push! Sẵn sàng push lên remote an toàn.'));
104
+ } else {
105
+ spinner.fail(chalk.red(`Không thể squash: ${sq.error}`));
106
+ }
107
+ }
108
+ } else if (unpushed.length > 0) {
109
+ console.log(chalk.hex('#64748B')(` » Có ${unpushed.length} commit chưa push (lịch sử sạch, không cần squash).\n`));
110
+ }
111
+
112
+ console.log(chalk.hex('#34D399')(`\n [OK] Quét hoàn tất trên branch ${currentBranch} — repo an toàn.\n`));
113
+ }