git-push-time 1.1.3 → 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.
- package/README.md +109 -111
- package/bin/index.js +45 -46
- package/lib/commands/edit-time.js +27 -27
- package/lib/commands/fix.js +19 -19
- package/lib/commands/github.js +98 -98
- package/lib/commands/pull.js +13 -13
- package/lib/commands/push.js +147 -147
- package/lib/config.js +17 -17
- package/lib/fix.js +34 -34
- package/lib/git.js +88 -88
- package/lib/github.js +39 -39
- package/lib/prompt-helpers.js +19 -20
- package/lib/secrets.js +12 -12
- package/lib/ui.js +122 -125
- package/lib/utils.js +15 -15
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ export async function runEditCommitTimeCommand(options = {}) {
|
|
|
23
23
|
if (options.dir) {
|
|
24
24
|
const r = resolveTargetDir(options.dir, process.cwd());
|
|
25
25
|
if (!r.ok) {
|
|
26
|
-
printErrorBox('
|
|
26
|
+
printErrorBox('INVALID DIRECTORY', [r.error]);
|
|
27
27
|
if (fromMenu) return;
|
|
28
28
|
process.exit(1);
|
|
29
29
|
}
|
|
@@ -36,39 +36,39 @@ export async function runEditCommitTimeCommand(options = {}) {
|
|
|
36
36
|
process.exit(1);
|
|
37
37
|
}
|
|
38
38
|
if (!isGitRepo(targetDir)) {
|
|
39
|
-
printErrorBox('
|
|
39
|
+
printErrorBox('NOT A GIT REPOSITORY', [`'${targetDir}' is not a Git repository.`]);
|
|
40
40
|
if (fromMenu) return;
|
|
41
41
|
process.exit(1);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
printSection('
|
|
44
|
+
printSection('EDIT COMMIT TIMESTAMP');
|
|
45
45
|
printTargetDir(targetDir);
|
|
46
46
|
|
|
47
47
|
try {
|
|
48
48
|
const commits = getRecentCommits(100, targetDir);
|
|
49
49
|
if (commits.length === 0) {
|
|
50
|
-
printErrorBox('
|
|
50
|
+
printErrorBox('NO COMMITS FOUND', ['This repository does not contain any commits.']);
|
|
51
51
|
if (fromMenu) return;
|
|
52
52
|
process.exit(1);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
//
|
|
55
|
+
// Use a loop instead of recursion to avoid growing the stack after repeated Back actions.
|
|
56
56
|
let commit = null;
|
|
57
57
|
let dateInput = null;
|
|
58
58
|
while (true) {
|
|
59
|
-
//
|
|
59
|
+
// Step 1: Choose a commit.
|
|
60
60
|
if (!commit) {
|
|
61
61
|
if (commits.length === 1) {
|
|
62
62
|
commit = commits[0];
|
|
63
|
-
console.log(chalk.hex('#94A3B8')(` »
|
|
63
|
+
console.log(chalk.hex('#94A3B8')(` » Only one commit found; selected it: ${commit.shortHash} — ${commit.subject}\n`));
|
|
64
64
|
} else {
|
|
65
65
|
const picked = await selectWithBack({
|
|
66
|
-
message: `
|
|
66
|
+
message: `Choose a commit to edit (${commits.length} most recent):`,
|
|
67
67
|
pageSize: 12,
|
|
68
68
|
choices: commits.map((item) => ({
|
|
69
69
|
name: `${item.date} ${item.shortHash} ${item.subject}`,
|
|
70
70
|
value: item,
|
|
71
|
-
description: '
|
|
71
|
+
description: 'Editing this commit will rewrite it and all following commits'
|
|
72
72
|
}))
|
|
73
73
|
});
|
|
74
74
|
if (picked === BACK) return;
|
|
@@ -76,41 +76,41 @@ export async function runEditCommitTimeCommand(options = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
//
|
|
79
|
+
// Step 2: Choose a timestamp type.
|
|
80
80
|
const dateOption = await selectWithBack({
|
|
81
|
-
message: '
|
|
81
|
+
message: 'Choose a new timestamp:',
|
|
82
82
|
choices: [
|
|
83
|
-
{ name: '
|
|
84
|
-
{ name: '
|
|
85
|
-
{ name: '
|
|
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
86
|
]
|
|
87
87
|
});
|
|
88
88
|
if (dateOption === BACK) {
|
|
89
|
-
commit = null; //
|
|
89
|
+
commit = null; // Return to commit selection.
|
|
90
90
|
continue;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
dateInput = dateOption;
|
|
94
94
|
if (dateOption === 'relative') {
|
|
95
95
|
const val = await inputWithBack({
|
|
96
|
-
message: '
|
|
96
|
+
message: 'Enter a relative time (e.g. -2d, -5h, -30m, -3M):',
|
|
97
97
|
default: '-1d',
|
|
98
98
|
validate: (value) => {
|
|
99
99
|
const parsed = parseCustomDate(value);
|
|
100
100
|
return parsed.valid || parsed.error;
|
|
101
101
|
}
|
|
102
102
|
});
|
|
103
|
-
if (val === BACK) continue; //
|
|
103
|
+
if (val === BACK) continue; // Return to timestamp type selection.
|
|
104
104
|
dateInput = val;
|
|
105
105
|
} else if (dateOption === 'custom') {
|
|
106
106
|
const val = await inputWithBack({
|
|
107
|
-
message: '
|
|
107
|
+
message: 'Enter a date and time (YYYY-MM-DD HH:mm:ss):',
|
|
108
108
|
validate: (value) => {
|
|
109
109
|
const parsed = parseCustomDate(value);
|
|
110
110
|
return parsed.valid || parsed.error;
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
|
-
if (val === BACK) continue; //
|
|
113
|
+
if (val === BACK) continue; // Return to timestamp type selection.
|
|
114
114
|
dateInput = val;
|
|
115
115
|
}
|
|
116
116
|
break;
|
|
@@ -118,7 +118,7 @@ export async function runEditCommitTimeCommand(options = {}) {
|
|
|
118
118
|
|
|
119
119
|
const parsed = parseCustomDate(dateInput);
|
|
120
120
|
if (!parsed.valid) {
|
|
121
|
-
printErrorBox('
|
|
121
|
+
printErrorBox('INVALID DATE OR TIME', [parsed.error], 'Examples: now, -2d, -5h, 2024-01-15 14:30:00');
|
|
122
122
|
if (fromMenu) return;
|
|
123
123
|
process.exit(1);
|
|
124
124
|
}
|
|
@@ -126,15 +126,15 @@ export async function runEditCommitTimeCommand(options = {}) {
|
|
|
126
126
|
const currentBranch = getCurrentBranch(targetDir);
|
|
127
127
|
const result = rewriteCommitDate(commit.hash, parsed.formatted, targetDir);
|
|
128
128
|
|
|
129
|
-
printSection('
|
|
130
|
-
console.log(`
|
|
129
|
+
printSection('COMMIT TIMESTAMP UPDATED');
|
|
130
|
+
console.log(` Branch: ${chalk.hex('#38BDF8').bold(currentBranch)}`);
|
|
131
131
|
console.log(` Commit: ${chalk.hex('#FBBF24').bold(commit.shortHash)} — ${commit.subject}`);
|
|
132
|
-
console.log(`
|
|
133
|
-
console.log(`
|
|
134
|
-
console.log(`
|
|
135
|
-
console.log(chalk.hex('#FBBF24')(' [!]
|
|
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
136
|
} catch (error) {
|
|
137
|
-
printErrorBox('
|
|
137
|
+
printErrorBox('COULD NOT EDIT TIMESTAMP', [error.message], 'The working tree must be clean, and the commit must be on the current branch.');
|
|
138
138
|
if (fromMenu) return;
|
|
139
139
|
process.exit(1);
|
|
140
140
|
}
|
package/lib/commands/fix.js
CHANGED
|
@@ -37,23 +37,23 @@ export async function runFixCommand(options = {}) {
|
|
|
37
37
|
if (options.dir) {
|
|
38
38
|
const r = resolveTargetDir(options.dir, process.cwd());
|
|
39
39
|
if (!r.ok) {
|
|
40
|
-
printErrorBox('
|
|
40
|
+
printErrorBox('INVALID DIRECTORY', [r.error]);
|
|
41
41
|
if (fromMenu) return;
|
|
42
42
|
process.exit(1);
|
|
43
43
|
}
|
|
44
44
|
targetDir = r.fullPath;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
printSection('
|
|
47
|
+
printSection('SCAN FOR SECRETS');
|
|
48
48
|
printTargetDir(targetDir);
|
|
49
49
|
|
|
50
50
|
if (!isGitRepo(targetDir)) {
|
|
51
|
-
printErrorBox('
|
|
51
|
+
printErrorBox('NOT A GIT REPOSITORY', [`'${targetDir}' is not a Git repository.`]);
|
|
52
52
|
if (fromMenu) return;
|
|
53
53
|
process.exit(1);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const spinner = ora({ text: '
|
|
56
|
+
const spinner = ora({ text: 'Checking staged files and the working tree…', color: 'cyan' }).start();
|
|
57
57
|
const risky = findRiskyStagedFiles(targetDir);
|
|
58
58
|
const findings = scanStagedContentForSecrets(targetDir);
|
|
59
59
|
const riskyWorking = findRiskyWorkingTreeFiles(targetDir);
|
|
@@ -63,24 +63,24 @@ export async function runFixCommand(options = {}) {
|
|
|
63
63
|
const hadSecrets = allDetected.length > 0;
|
|
64
64
|
|
|
65
65
|
if (allDetected.length === 0) {
|
|
66
|
-
console.log(chalk.hex('#34D399')(' [OK]
|
|
66
|
+
console.log(chalk.hex('#34D399')(' [OK] No secrets found in staged files or the working tree.\n'));
|
|
67
67
|
} else {
|
|
68
68
|
printPrePushSecretWarning({ riskyFiles: allDetected, findings });
|
|
69
|
-
spinner.start('
|
|
69
|
+
spinner.start('Applying fixes (unstaging files and updating .gitignore)...');
|
|
70
70
|
const res = autoFixStagedSecrets(allDetected, targetDir);
|
|
71
|
-
spinner.succeed(chalk.hex('#34D399')(`[OK]
|
|
71
|
+
spinner.succeed(chalk.hex('#34D399')(`[OK] Processed ${res.fixed.length} file(s).`));
|
|
72
72
|
printFixResult(res);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
75
|
+
// Suggest squashing unpushed commits only when secrets were detected,
|
|
76
|
+
// avoiding unnecessary rewrites of clean history.
|
|
77
77
|
const currentBranch = getCurrentBranchStrict(targetDir) || 'main';
|
|
78
78
|
const unpushed = getUnpushedCommits(currentBranch, targetDir);
|
|
79
79
|
if (unpushed.length > 0 && hadSecrets) {
|
|
80
|
-
printSection(`${unpushed.length}
|
|
80
|
+
printSection(`${unpushed.length} UNPUSHED LOCAL COMMITS`);
|
|
81
81
|
unpushed.slice(0, 5).forEach((c) => console.log(chalk.hex('#94A3B8')(` - ${c}`)));
|
|
82
82
|
if (unpushed.length > 5) {
|
|
83
|
-
console.log(chalk.hex('#64748B')(` ...
|
|
83
|
+
console.log(chalk.hex('#64748B')(` ...and ${unpushed.length - 5} more commit(s)`));
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
let sqChoice = 'no';
|
|
@@ -88,26 +88,26 @@ export async function runFixCommand(options = {}) {
|
|
|
88
88
|
sqChoice = 'no';
|
|
89
89
|
} else {
|
|
90
90
|
sqChoice = await selectWithBack({
|
|
91
|
-
message: `
|
|
91
|
+
message: `Squash ${unpushed.length} commit(s) into one clean commit to remove old secrets from the history?`,
|
|
92
92
|
choices: [
|
|
93
|
-
{ name: '
|
|
94
|
-
{ name: '
|
|
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
95
|
]
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
if (sqChoice !== BACK && sqChoice === 'yes') {
|
|
100
|
-
spinner.start('
|
|
100
|
+
spinner.start('Cleaning unpushed commit history...');
|
|
101
101
|
const sq = squashUnpushedCommits(currentBranch, 'Clean commit without secrets', null, targetDir);
|
|
102
102
|
if (sq.ok) {
|
|
103
|
-
spinner.succeed(chalk.hex('#34D399')('[OK]
|
|
103
|
+
spinner.succeed(chalk.hex('#34D399')('[OK] Unpushed commits cleaned. Ready to push safely.'));
|
|
104
104
|
} else {
|
|
105
|
-
spinner.fail(chalk.red(`
|
|
105
|
+
spinner.fail(chalk.red(`Could not squash commits: ${sq.error}`));
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
} else if (unpushed.length > 0) {
|
|
109
|
-
console.log(chalk.hex('#64748B')(` »
|
|
109
|
+
console.log(chalk.hex('#64748B')(` » ${unpushed.length} unpushed commit(s) found; history is clean, so no squash is needed.\n`));
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
console.log(chalk.hex('#34D399')(`\n [OK]
|
|
112
|
+
console.log(chalk.hex('#34D399')(`\n [OK] Scan complete on branch ${currentBranch}. Repository is safe.\n`));
|
|
113
113
|
}
|