git-push-time 1.1.2 → 1.1.4

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,141 +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()) {
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
+ 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('INVALID DIRECTORY', [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('NOT A GIT REPOSITORY', [`'${targetDir}' is not a Git repository.`]);
40
+ if (fromMenu) return;
41
+ process.exit(1);
42
+ }
43
+
44
+ printSection('EDIT COMMIT TIMESTAMP');
45
+ printTargetDir(targetDir);
46
+
47
+ try {
48
+ const commits = getRecentCommits(100, targetDir);
49
+ if (commits.length === 0) {
50
+ printErrorBox('NO COMMITS FOUND', ['This repository does not contain any commits.']);
51
+ if (fromMenu) return;
52
+ process.exit(1);
53
+ }
54
+
55
+ // Use a loop instead of recursion to avoid growing the stack after repeated Back actions.
56
+ let commit = null;
57
+ let dateInput = null;
58
+ while (true) {
59
+ // Step 1: Choose a commit.
60
+ if (!commit) {
61
+ if (commits.length === 1) {
62
+ commit = commits[0];
63
+ console.log(chalk.hex('#94A3B8')(` » Only one commit found; selected it: ${commit.shortHash} — ${commit.subject}\n`));
64
+ } else {
65
+ const picked = await selectWithBack({
66
+ message: `Choose a commit to edit (${commits.length} most recent):`,
67
+ pageSize: 12,
68
+ choices: commits.map((item) => ({
69
+ name: `${item.date} ${item.shortHash} ${item.subject}`,
70
+ value: item,
71
+ description: 'Editing this commit will rewrite it and all following commits'
72
+ }))
73
+ });
74
+ if (picked === BACK) return;
75
+ commit = picked;
76
+ }
77
+ }
78
+
79
+ // Step 2: Choose a timestamp type.
80
+ const dateOption = await selectWithBack({
81
+ message: 'Choose a new timestamp:',
82
+ choices: [
83
+ { name: 'Now', value: 'now', description: 'Use the current time' },
84
+ { name: 'Enter a relative time', value: 'relative', description: 'e.g. -2d, -5h, -30m, -3M' },
85
+ { name: 'Enter a specific date and time', value: 'custom', description: 'e.g. 2024-01-15 14:30:00' }
86
+ ]
87
+ });
88
+ if (dateOption === BACK) {
89
+ commit = null; // Return to commit selection.
90
+ continue;
91
+ }
92
+
93
+ dateInput = dateOption;
94
+ if (dateOption === 'relative') {
95
+ const val = await inputWithBack({
96
+ message: 'Enter a relative time (e.g. -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; // Return to timestamp type selection.
104
+ dateInput = val;
105
+ } else if (dateOption === 'custom') {
106
+ const val = await inputWithBack({
107
+ message: 'Enter a date and time (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; // Return to timestamp type selection.
114
+ dateInput = val;
115
+ }
116
+ break;
117
+ }
118
+
119
+ const parsed = parseCustomDate(dateInput);
120
+ if (!parsed.valid) {
121
+ printErrorBox('INVALID DATE OR TIME', [parsed.error], 'Examples: 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('COMMIT TIMESTAMP UPDATED');
130
+ console.log(` Branch: ${chalk.hex('#38BDF8').bold(currentBranch)}`);
131
+ console.log(` Commit: ${chalk.hex('#FBBF24').bold(commit.shortHash)} — ${commit.subject}`);
132
+ console.log(` New timestamp: ${chalk.hex('#C084FC').bold(parsed.display)}`);
133
+ console.log(` Rewrote ${result.rewrittenCount} commit(s) on the current branch.`);
134
+ console.log(` Backup branch: ${chalk.hex('#6EE7B7')(result.backupBranch)}`);
135
+ console.log(chalk.hex('#FBBF24')(' [!] If these commits were already pushed, force-push (--force) to update the remote.\n'));
136
+ } catch (error) {
137
+ printErrorBox('COULD NOT EDIT TIMESTAMP', [error.message], 'The working tree must be clean, and the commit must be on the current branch.');
138
+ if (fromMenu) return;
139
+ process.exit(1);
140
+ }
141
+ }
@@ -1,113 +1,113 @@
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
- }
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('INVALID DIRECTORY', [r.error]);
41
+ if (fromMenu) return;
42
+ process.exit(1);
43
+ }
44
+ targetDir = r.fullPath;
45
+ }
46
+
47
+ printSection('SCAN FOR SECRETS');
48
+ printTargetDir(targetDir);
49
+
50
+ if (!isGitRepo(targetDir)) {
51
+ printErrorBox('NOT A GIT REPOSITORY', [`'${targetDir}' is not a Git repository.`]);
52
+ if (fromMenu) return;
53
+ process.exit(1);
54
+ }
55
+
56
+ const spinner = ora({ text: 'Checking staged files and the working tree…', 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] No secrets found in staged files or the working tree.\n'));
67
+ } else {
68
+ printPrePushSecretWarning({ riskyFiles: allDetected, findings });
69
+ spinner.start('Applying fixes (unstaging files and updating .gitignore)...');
70
+ const res = autoFixStagedSecrets(allDetected, targetDir);
71
+ spinner.succeed(chalk.hex('#34D399')(`[OK] Processed ${res.fixed.length} file(s).`));
72
+ printFixResult(res);
73
+ }
74
+
75
+ // Suggest squashing unpushed commits only when secrets were detected,
76
+ // avoiding unnecessary rewrites of clean history.
77
+ const currentBranch = getCurrentBranchStrict(targetDir) || 'main';
78
+ const unpushed = getUnpushedCommits(currentBranch, targetDir);
79
+ if (unpushed.length > 0 && hadSecrets) {
80
+ printSection(`${unpushed.length} UNPUSHED LOCAL COMMITS`);
81
+ unpushed.slice(0, 5).forEach((c) => console.log(chalk.hex('#94A3B8')(` - ${c}`)));
82
+ if (unpushed.length > 5) {
83
+ console.log(chalk.hex('#64748B')(` ...and ${unpushed.length - 5} more commit(s)`));
84
+ }
85
+
86
+ let sqChoice = 'no';
87
+ if (options.yes) {
88
+ sqChoice = 'no';
89
+ } else {
90
+ sqChoice = await selectWithBack({
91
+ message: `Squash ${unpushed.length} commit(s) into one clean commit to remove old secrets from the history?`,
92
+ choices: [
93
+ { name: 'Yes, create one clean commit', value: 'yes', description: 'Clean the commit history' },
94
+ { name: 'No, keep as is', value: 'no', description: 'Skip' }
95
+ ]
96
+ });
97
+ }
98
+
99
+ if (sqChoice !== BACK && sqChoice === 'yes') {
100
+ spinner.start('Cleaning unpushed commit history...');
101
+ const sq = squashUnpushedCommits(currentBranch, 'Clean commit without secrets', null, targetDir);
102
+ if (sq.ok) {
103
+ spinner.succeed(chalk.hex('#34D399')('[OK] Unpushed commits cleaned. Ready to push safely.'));
104
+ } else {
105
+ spinner.fail(chalk.red(`Could not squash commits: ${sq.error}`));
106
+ }
107
+ }
108
+ } else if (unpushed.length > 0) {
109
+ console.log(chalk.hex('#64748B')(` » ${unpushed.length} unpushed commit(s) found; history is clean, so no squash is needed.\n`));
110
+ }
111
+
112
+ console.log(chalk.hex('#34D399')(`\n [OK] Scan complete on branch ${currentBranch}. Repository is safe.\n`));
113
+ }