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.
- package/.github/workflows/npm-publish.yml +33 -0
- package/README.md +215 -215
- package/bin/index.js +337 -291
- package/lib/commands/edit-time.js +141 -127
- package/lib/commands/fix.js +113 -105
- package/lib/commands/github.js +439 -436
- package/lib/commands/pull.js +89 -88
- package/lib/commands/push.js +673 -653
- package/lib/config.js +220 -210
- package/lib/fix.js +286 -260
- package/lib/git.js +492 -452
- package/lib/github.js +322 -304
- package/lib/prompt-helpers.js +62 -62
- package/lib/secrets.js +92 -87
- package/lib/ui.js +804 -844
- package/lib/utils.js +120 -106
- package/package.json +35 -32
|
@@ -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()
|
|
34
|
-
|
|
35
|
-
if (fromMenu) return;
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
}
|
package/lib/commands/fix.js
CHANGED
|
@@ -1,105 +1,113 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import ora from 'ora';
|
|
3
|
-
import {
|
|
4
|
-
checkGitInstalled,
|
|
5
|
-
isGitRepo,
|
|
6
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
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
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
unpushed.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|