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
package/lib/config.js
CHANGED
|
@@ -17,7 +17,7 @@ function ensureConfigDir() {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Read the user's complete configuration file.
|
|
21
21
|
* @returns {Record<string, any>}
|
|
22
22
|
*/
|
|
23
23
|
export function getConfig() {
|
|
@@ -33,7 +33,7 @@ export function getConfig() {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Save the user's configuration.
|
|
37
37
|
* @param {Record<string, any>} data
|
|
38
38
|
*/
|
|
39
39
|
export function saveConfig(data) {
|
|
@@ -43,10 +43,10 @@ export function saveConfig(data) {
|
|
|
43
43
|
const merged = { ...current, ...data };
|
|
44
44
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf-8');
|
|
45
45
|
try {
|
|
46
|
-
//
|
|
46
|
+
// Restrict access to sensitive tokens to the owner (chmod 600).
|
|
47
47
|
fs.chmodSync(CONFIG_FILE, 0o600);
|
|
48
48
|
} catch {
|
|
49
|
-
//
|
|
49
|
+
// Ignore on Windows or filesystems that do not support chmod.
|
|
50
50
|
}
|
|
51
51
|
return true;
|
|
52
52
|
} catch {
|
|
@@ -55,7 +55,7 @@ export function saveConfig(data) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
58
|
+
* Get all saved GitHub accounts.
|
|
59
59
|
* @returns {Array<{ login: string, name: string, token: string, active: boolean, avatar_url?: string }>}
|
|
60
60
|
*/
|
|
61
61
|
export function getAllGitHubAccounts() {
|
|
@@ -76,7 +76,7 @@ export function getAllGitHubAccounts() {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
//
|
|
79
|
+
// Support the legacy `github` field if it has not been migrated.
|
|
80
80
|
if (result.length === 0 && cfg.github && cfg.github.token) {
|
|
81
81
|
const login = cfg.github.user?.login || 'default';
|
|
82
82
|
result.push({
|
|
@@ -92,7 +92,7 @@ export function getAllGitHubAccounts() {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
*
|
|
95
|
+
* Set the active GitHub account.
|
|
96
96
|
* @param {string} login
|
|
97
97
|
*/
|
|
98
98
|
export function setActiveGitHubAccount(login) {
|
|
@@ -109,14 +109,14 @@ export function setActiveGitHubAccount(login) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
113
|
-
* 1.
|
|
114
|
-
* 2.
|
|
115
|
-
* 3.
|
|
112
|
+
* Get a GitHub token from the available sources, in priority order:
|
|
113
|
+
* 1. GITHUB_TOKEN or GH_TOKEN environment variable
|
|
114
|
+
* 2. Active GitHub account in the config file
|
|
115
|
+
* 3. GitHub CLI token (`gh auth token`), if available
|
|
116
116
|
* @returns {{ token: string | null, source: 'env' | 'config' | 'gh-cli' | null, user?: any }}
|
|
117
117
|
*/
|
|
118
118
|
export function getGitHubToken() {
|
|
119
|
-
// 1.
|
|
119
|
+
// 1. Check environment variables.
|
|
120
120
|
if (process.env.GITHUB_TOKEN && process.env.GITHUB_TOKEN.trim()) {
|
|
121
121
|
return { token: process.env.GITHUB_TOKEN.trim(), source: 'env' };
|
|
122
122
|
}
|
|
@@ -124,7 +124,7 @@ export function getGitHubToken() {
|
|
|
124
124
|
return { token: process.env.GH_TOKEN.trim(), source: 'env' };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
// 2.
|
|
127
|
+
// 2. Check the saved config file.
|
|
128
128
|
const cfg = getConfig();
|
|
129
129
|
const activeLogin = cfg.activeGitHubLogin;
|
|
130
130
|
if (activeLogin && cfg.githubAccounts && cfg.githubAccounts[activeLogin]) {
|
|
@@ -138,7 +138,7 @@ export function getGitHubToken() {
|
|
|
138
138
|
return { token: cfg.github.token.trim(), source: 'config', user: cfg.github.user };
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
// 3.
|
|
141
|
+
// 3. Check the GitHub CLI (`gh`).
|
|
142
142
|
try {
|
|
143
143
|
const r = spawnSync('gh', ['auth', 'token'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
144
144
|
if (r.status === 0 && r.stdout && r.stdout.trim()) {
|
|
@@ -152,7 +152,7 @@ export function getGitHubToken() {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
*
|
|
155
|
+
* Save a GitHub account to the config file (supports multiple accounts).
|
|
156
156
|
* @param {string} token
|
|
157
157
|
* @param {object} userInfo
|
|
158
158
|
*/
|
|
@@ -190,8 +190,8 @@ export function saveGitHubToken(token, userInfo = null) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
*
|
|
194
|
-
* @param {string} [login]
|
|
193
|
+
* Remove a GitHub account from the config file.
|
|
194
|
+
* @param {string} [login] If omitted, removes the active account.
|
|
195
195
|
*/
|
|
196
196
|
export function removeGitHubToken(login = null) {
|
|
197
197
|
const current = getConfig();
|
package/lib/fix.js
CHANGED
|
@@ -5,8 +5,8 @@ import { findRiskyStagedFiles } from './git.js';
|
|
|
5
5
|
import { scanStagedContentForSecrets } from './secrets.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Source-code and documentation file extensions.
|
|
9
|
+
* Never add these files to .gitignore automatically; that could hide project code.
|
|
10
10
|
*/
|
|
11
11
|
const CODE_EXTENSIONS = new Set([
|
|
12
12
|
'.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx',
|
|
@@ -24,12 +24,12 @@ function isCodeOrDocFile(filePath) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
* 1. git rm --cached -r -f <file> (
|
|
29
|
-
* 2.
|
|
30
|
-
* 3.
|
|
27
|
+
* Safely remediate detected secrets:
|
|
28
|
+
* 1. git rm --cached -r -f <file> (remove from Git while keeping the local file)
|
|
29
|
+
* 2. Add a matching .gitignore rule (only for secret config files, never source code)
|
|
30
|
+
* 3. Stage .gitignore so the rule is ready to commit
|
|
31
31
|
*
|
|
32
|
-
* @param {string[]} files
|
|
32
|
+
* @param {string[]} files Files to unstage (e.g. ['.env', '.npmrc'])
|
|
33
33
|
* @param {string} cwd
|
|
34
34
|
* @returns {{ fixed: string[], alreadyClean: string[], failed: {file:string, reason:string}[], gitignoreAdded: string[], codeFilesWithSecrets: string[] }}
|
|
35
35
|
*/
|
|
@@ -49,13 +49,13 @@ export function autoFixStagedSecrets(files = [], cwd = process.cwd()) {
|
|
|
49
49
|
let ok = false;
|
|
50
50
|
let errReason = '';
|
|
51
51
|
|
|
52
|
-
// 1.
|
|
52
|
+
// 1. Try to unstage with git rm --cached -r -f.
|
|
53
53
|
const r = spawnSync('git', ['rm', '--cached', '-r', '-f', '--', normalized], { cwd, encoding: 'utf-8' });
|
|
54
54
|
if (r.status === 0) {
|
|
55
55
|
ok = true;
|
|
56
56
|
} else {
|
|
57
57
|
const msg = String(r.stderr || r.stdout || '');
|
|
58
|
-
//
|
|
58
|
+
// Fall back to git restore --staged if git rm cannot match the path.
|
|
59
59
|
const rRestore = spawnSync('git', ['restore', '--staged', '--', normalized], { cwd, encoding: 'utf-8' });
|
|
60
60
|
if (rRestore.status === 0) {
|
|
61
61
|
ok = true;
|
|
@@ -70,18 +70,18 @@ export function autoFixStagedSecrets(files = [], cwd = process.cwd()) {
|
|
|
70
70
|
if (ok) {
|
|
71
71
|
result.fixed.push(file);
|
|
72
72
|
} else {
|
|
73
|
-
result.failed.push({ file, reason: errReason || '
|
|
73
|
+
result.failed.push({ file, reason: errReason || 'Could not unstage file' });
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// 2.
|
|
77
|
+
// 2. Add .gitignore rules for secret config files (skip source files).
|
|
78
78
|
const rules = [];
|
|
79
79
|
for (const f of unique) {
|
|
80
80
|
const normalized = f.replace(/\\/g, '/');
|
|
81
81
|
const base = path.basename(normalized);
|
|
82
82
|
|
|
83
83
|
if (isCodeOrDocFile(normalized)) {
|
|
84
|
-
//
|
|
84
|
+
// Unstage source files containing secrets, but do not add them to .gitignore.
|
|
85
85
|
result.codeFilesWithSecrets.push(f);
|
|
86
86
|
continue;
|
|
87
87
|
}
|
|
@@ -127,17 +127,17 @@ export function autoFixStagedSecrets(files = [], cwd = process.cwd()) {
|
|
|
127
127
|
fs.appendFileSync(gitignorePath, prefix + toAdd.join('\n') + '\n', 'utf-8');
|
|
128
128
|
result.gitignoreAdded = toAdd;
|
|
129
129
|
|
|
130
|
-
//
|
|
130
|
+
// Stage .gitignore so the new rules are not missed when committing.
|
|
131
131
|
spawnSync('git', ['add', '.gitignore'], { cwd });
|
|
132
132
|
} catch {
|
|
133
|
-
//
|
|
133
|
+
// Ignore file-write errors.
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
return result;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
/**
|
|
140
|
+
/** Check whether anything remains to commit after auto-fix. */
|
|
141
141
|
export function hasStagedChanges(cwd = process.cwd()) {
|
|
142
142
|
try {
|
|
143
143
|
const r = spawnSync('git', ['diff', '--cached', '--name-only'], { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
@@ -149,7 +149,7 @@ export function hasStagedChanges(cwd = process.cwd()) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
152
|
+
* Find common secret files in the working directory that are not ignored by .gitignore.
|
|
153
153
|
*/
|
|
154
154
|
export function findRiskyWorkingTreeFiles(cwd = process.cwd()) {
|
|
155
155
|
const denyNames = [
|
|
@@ -171,11 +171,11 @@ export function findRiskyWorkingTreeFiles(cwd = process.cwd()) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
|
-
*
|
|
174
|
+
* Find the upstream ref for the current branch (e.g. origin/main or origin/master).
|
|
175
175
|
*/
|
|
176
176
|
export function getUpstreamRef(branch = 'main', cwd = process.cwd()) {
|
|
177
177
|
const cleanBranch = String(branch || 'main').trim() || 'main';
|
|
178
|
-
//
|
|
178
|
+
// Reject unsafe Git ref characters before passing the value to Git.
|
|
179
179
|
if (/[\s~^:?*\[\\@{]/.test(cleanBranch) || cleanBranch.includes('..')) return null;
|
|
180
180
|
try {
|
|
181
181
|
const r = spawnSync('git', ['rev-parse', '--abbrev-ref', `${cleanBranch}@{u}`], { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
@@ -196,7 +196,7 @@ export function getUpstreamRef(branch = 'main', cwd = process.cwd()) {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
/**
|
|
199
|
-
*
|
|
199
|
+
* Get the list of unpushed commits.
|
|
200
200
|
*/
|
|
201
201
|
export function getUnpushedCommits(branch = 'main', cwd = process.cwd()) {
|
|
202
202
|
const upstream = getUpstreamRef(branch, cwd);
|
|
@@ -219,8 +219,8 @@ export function getUnpushedCommits(branch = 'main', cwd = process.cwd()) {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
* Squash
|
|
223
|
-
*
|
|
222
|
+
* Squash unpushed commits into one clean commit, removing secrets from
|
|
223
|
+
* intermediate commits (for example, an earlier commit containing .npmrc or .env).
|
|
224
224
|
*/
|
|
225
225
|
export function squashUnpushedCommits(branch = 'main', commitMsg = 'Clean commit without secrets', dateFormatted = null, cwd = process.cwd()) {
|
|
226
226
|
const upstream = getUpstreamRef(branch, cwd);
|
|
@@ -229,21 +229,21 @@ export function squashUnpushedCommits(branch = 'main', commitMsg = 'Clean commit
|
|
|
229
229
|
if (upstream) {
|
|
230
230
|
const r1 = spawnSync('git', ['reset', '--soft', upstream], { cwd, encoding: 'utf-8' });
|
|
231
231
|
if (r1.status !== 0) {
|
|
232
|
-
throw new Error(String(r1.stderr || r1.stdout || '
|
|
232
|
+
throw new Error(String(r1.stderr || r1.stdout || 'Could not reset to upstream').trim());
|
|
233
233
|
}
|
|
234
234
|
} else {
|
|
235
|
-
//
|
|
236
|
-
//
|
|
235
|
+
// Without an upstream (a new repository), squash everything into one commit
|
|
236
|
+
// by soft-resetting to the root and amending it instead of creating a second commit.
|
|
237
237
|
const rRoot = spawnSync('git', ['rev-list', '--max-parents=0', 'HEAD'], { cwd, encoding: 'utf-8' });
|
|
238
|
-
if (rRoot.status !== 0) throw new Error('
|
|
238
|
+
if (rRoot.status !== 0) throw new Error('Could not read the root commit.');
|
|
239
239
|
const rootCommit = (rRoot.stdout || '').trim().split(/\s+/)[0];
|
|
240
|
-
if (!rootCommit) throw new Error('
|
|
240
|
+
if (!rootCommit) throw new Error('Root commit not found.');
|
|
241
241
|
const rReset = spawnSync('git', ['reset', '--soft', rootCommit], { cwd, encoding: 'utf-8' });
|
|
242
|
-
if (rReset.status !== 0) throw new Error('
|
|
242
|
+
if (rReset.status !== 0) throw new Error('Could not reset to the root commit.');
|
|
243
243
|
usedAmendForRoot.value = true;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
//
|
|
246
|
+
// Scan again and remove any secret files still staged.
|
|
247
247
|
const risky = findRiskyStagedFiles(cwd);
|
|
248
248
|
const findings = scanStagedContentForSecrets(cwd);
|
|
249
249
|
const allSecretFiles = [...new Set([...risky, ...findings.map((f) => f.file)])];
|
|
@@ -251,14 +251,14 @@ export function squashUnpushedCommits(branch = 'main', commitMsg = 'Clean commit
|
|
|
251
251
|
autoFixStagedSecrets(allSecretFiles, cwd);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
//
|
|
254
|
+
// Ensure .gitignore is staged, if it exists.
|
|
255
255
|
spawnSync('git', ['add', '.gitignore'], { cwd });
|
|
256
256
|
|
|
257
257
|
if (!hasStagedChanges(cwd) && !usedAmendForRoot.value) {
|
|
258
|
-
return { ok: false, error: '
|
|
258
|
+
return { ok: false, error: 'No changes remain to squash into a new commit.' };
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
//
|
|
261
|
+
// Create a new clean commit.
|
|
262
262
|
const env = { ...process.env };
|
|
263
263
|
if (dateFormatted) {
|
|
264
264
|
env.GIT_AUTHOR_DATE = dateFormatted;
|
|
@@ -267,7 +267,7 @@ export function squashUnpushedCommits(branch = 'main', commitMsg = 'Clean commit
|
|
|
267
267
|
const safeMsg = String(commitMsg || 'Clean commit without secrets');
|
|
268
268
|
let rCommit;
|
|
269
269
|
if (usedAmendForRoot.value) {
|
|
270
|
-
// Amend root
|
|
270
|
+
// Amend the root to keep a single commit when there is no upstream.
|
|
271
271
|
rCommit = spawnSync('git', ['commit', '--amend', '-m', safeMsg], { cwd, env, encoding: 'utf-8' });
|
|
272
272
|
} else {
|
|
273
273
|
rCommit = spawnSync('git', ['commit', '-m', safeMsg], { cwd, env, encoding: 'utf-8' });
|
|
@@ -275,9 +275,9 @@ export function squashUnpushedCommits(branch = 'main', commitMsg = 'Clean commit
|
|
|
275
275
|
if (rCommit.status !== 0) {
|
|
276
276
|
const msg = String(rCommit.stderr || rCommit.stdout || '').trim();
|
|
277
277
|
if (msg.toLowerCase().includes('nothing to commit')) {
|
|
278
|
-
return { ok: false, error: '
|
|
278
|
+
return { ok: false, error: 'Nothing to commit after cleanup (working tree is clean).' };
|
|
279
279
|
}
|
|
280
|
-
throw new Error(msg || '
|
|
280
|
+
throw new Error(msg || 'Could not create the squashed commit');
|
|
281
281
|
}
|
|
282
282
|
return { ok: true };
|
|
283
283
|
} catch (err) {
|