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/lib/git.js CHANGED
@@ -3,7 +3,7 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
 
5
5
  /**
6
- * Kiểm tra xem máy tính đã cài đặt Git hay chưa
6
+ * Check whether Git is installed on this machine.
7
7
  * @returns {boolean}
8
8
  */
9
9
  export function checkGitInstalled() {
@@ -16,36 +16,36 @@ export function checkGitInstalled() {
16
16
  }
17
17
 
18
18
  /**
19
- * Kiểm tra thư mục hiện tại đã là kho chứa Git (Git repo) chưa
19
+ * Check whether the current directory is inside a Git repository.
20
20
  * @param {string} cwd
21
21
  * @returns {boolean}
22
22
  */
23
23
  export function isGitRepo(cwd = process.cwd()) {
24
- // Dùng spawnSync dạng mảng args (không qua shell) để tránh lỗi trên Windows
25
- // khi folder chứa file tên git.* (vd: git.js) — cmd.exe sẽ nhầm `git` thành file đó do PATHEXT.
24
+ // Use spawnSync with an argument array instead of a shell to avoid Windows errors
25
+ // when the folder contains a file named git.* (e.g. git.js) that cmd.exe may resolve via PATHEXT.
26
26
  const r = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, encoding: 'utf-8' });
27
27
  return r.status === 0 && (r.stdout || '').trim() === 'true';
28
28
  }
29
29
 
30
30
  /**
31
- * Khởi tạo Git repo trong thư mục hiện tại
31
+ * Initialize a Git repository in the current directory.
32
32
  * @param {string} cwd
33
33
  */
34
34
  export function initGitRepo(cwd = process.cwd()) {
35
35
  const r = spawnSync('git', ['init'], { cwd, encoding: 'utf-8' });
36
36
  if (r.status !== 0) {
37
- throw new Error(String(r.stderr || r.stdout || 'git init thất bại').trim());
37
+ throw new Error(String(r.stderr || r.stdout || 'git init failed').trim());
38
38
  }
39
39
  }
40
40
 
41
41
  /**
42
- * Lấy top-level của Git repo chứa `cwd` (đi ngược lên cha).
43
- * Trả về null nếu không nằm trong repo nào.
42
+ * Get the top-level directory of the Git repository containing `cwd`.
43
+ * Returns null when `cwd` is not inside a repository.
44
44
  * @param {string} cwd
45
45
  * @returns {string|null}
46
46
  */
47
47
  export function getGitTopLevel(cwd = process.cwd()) {
48
- // Dùng spawnSync (không qua shell) — xem ghi chú ở isGitRepo.
48
+ // Use spawnSync without a shell; see the note in isGitRepo.
49
49
  const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf-8' });
50
50
  if (r.status !== 0) return null;
51
51
  const out = (r.stdout || '').trim();
@@ -53,8 +53,8 @@ export function getGitTopLevel(cwd = process.cwd()) {
53
53
  }
54
54
 
55
55
  /**
56
- * Liệt kê các thư mục con trực tiếp của `cwd` để user chọn folder nguồn push.
57
- * Bỏ qua .git để tránh nhầm. Sắp xếp: thư mục thường trước, ẩn sau.
56
+ * List direct child directories of `cwd` for the user to choose as a push source.
57
+ * Skip .git. Sort regular folders before hidden folders.
58
58
  * @param {string} cwd
59
59
  * @returns {{ name: string, fullPath: string }[]}
60
60
  */
@@ -65,7 +65,7 @@ export function listSubdirectories(cwd = process.cwd()) {
65
65
  .filter((e) => {
66
66
  if (!e.isDirectory()) return false;
67
67
  if (e.name === '.git') return false;
68
- // Bỏ qua symlink gãy
68
+ // Skip broken symlinks.
69
69
  try {
70
70
  fs.statSync(path.join(cwd, e.name));
71
71
  return true;
@@ -86,29 +86,29 @@ export function listSubdirectories(cwd = process.cwd()) {
86
86
  }
87
87
 
88
88
  /**
89
- * Chuẩn hoá đường dẫn folder nguồn do user nhập/truyền vào.
90
- * @param {string} inputPath đường dẫn tương đối hoặc tuyệt đối
91
- * @param {string} base thư mục gốc để resolve đường dẫn tương đối
89
+ * Normalize a user-provided source folder path.
90
+ * @param {string} inputPath Absolute or relative path
91
+ * @param {string} base Base directory for resolving relative paths
92
92
  * @returns {{ ok: boolean, fullPath?: string, error?: string }}
93
93
  */
94
94
  export function resolveTargetDir(inputPath, base = process.cwd()) {
95
95
  if (!inputPath || !String(inputPath).trim()) {
96
- return { ok: false, error: 'Đường dẫn trống.' };
96
+ return { ok: false, error: 'Path is empty.' };
97
97
  }
98
98
  const fullPath = path.resolve(base, String(inputPath).trim());
99
99
  try {
100
100
  const stat = fs.statSync(fullPath);
101
- if (!stat.isDirectory()) return { ok: false, error: `'${inputPath}' không phải là thư mục.` };
101
+ if (!stat.isDirectory()) return { ok: false, error: `'${inputPath}' is not a directory.` };
102
102
  } catch {
103
- return { ok: false, error: `Không tìm thấy thư mục '${inputPath}'.` };
103
+ return { ok: false, error: `Directory not found: '${inputPath}'.` };
104
104
  }
105
105
  return { ok: true, fullPath };
106
106
  }
107
107
 
108
108
  /**
109
- * Lấy tên branch hiện tại (hoặc mặc định là main cho hiển thị).
110
- * Lưu ý: trả về 'main' khi detached HEAD để tương thích UI cũ.
111
- * Dùng getCurrentBranchStrict() khi cần phát hiện detached chính xác.
109
+ * Get the current branch name, defaulting to main for display.
110
+ * Returns 'main' for detached HEAD to preserve legacy UI behavior.
111
+ * Use getCurrentBranchStrict() when detached HEAD must be detected accurately.
112
112
  * @param {string} cwd
113
113
  * @returns {string}
114
114
  */
@@ -141,19 +141,19 @@ export function getDashboardGitStatus(cwd = process.cwd()) {
141
141
  }
142
142
 
143
143
  /**
144
- * Lấy tên branch hiện tại, trả về null khi detached HEAD hoặc lỗi.
144
+ * Get the current branch name, returning null for detached HEAD or errors.
145
145
  * @param {string} cwd
146
146
  * @returns {string|null}
147
147
  */
148
148
  export function getCurrentBranchStrict(cwd = process.cwd()) {
149
- // Dùng spawnSync (không qua shell) — xem ghi chú ở isGitRepo.
149
+ // Use spawnSync without a shell; see the note in isGitRepo.
150
150
  const r = spawnSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf-8' });
151
151
  if (r.status !== 0) return null;
152
152
  return (r.stdout || '').trim() || null;
153
153
  }
154
154
 
155
155
  /**
156
- * Validate tên branch để chống injection.
156
+ * Validate a branch name to prevent injection.
157
157
  * @param {string} name
158
158
  * @returns {boolean}
159
159
  */
@@ -161,8 +161,8 @@ export function isValidBranchName(name) {
161
161
  if (!name || typeof name !== 'string') return false;
162
162
  const t = name.trim();
163
163
  if (!t || t.length > 255) return false;
164
- // Cấm ký tự điều khiển, space đầu/cuối, và các ký tự git cấm: ~ ^ : ? * [ \ ..
165
- // Cho phép chữ, số, / - _ . đơn giản nhất; từ chối còn lại để an toàn.
164
+ // Reject control characters, surrounding spaces, and Git's reserved characters: ~ ^ : ? * [ \ ..
165
+ // For simplicity, allow letters, numbers, / - _ and . only.
166
166
  if (/[\s~^:?*\[\\]@{]/.test(t)) return false;
167
167
  if (t.includes('..') || t.startsWith('/') || t.endsWith('/') || t.endsWith('.lock')) return false;
168
168
  if (t === 'HEAD') return false;
@@ -170,14 +170,14 @@ export function isValidBranchName(name) {
170
170
  }
171
171
 
172
172
  /**
173
- * Thiết lập tên branch chính (ví dụ: git branch -M main)
173
+ * Set the primary branch name (for example, git branch -M main).
174
174
  * @param {string} branchName
175
175
  * @param {string} cwd
176
176
  */
177
177
  export function setBranchName(branchName = 'main', cwd = process.cwd()) {
178
178
  const clean = String(branchName || 'main').trim() || 'main';
179
179
  if (!isValidBranchName(clean)) {
180
- throw new Error(`Tên branch không hợp lệ: '${branchName}'`);
180
+ throw new Error(`Invalid branch name: '${branchName}'`);
181
181
  }
182
182
  const current = getCurrentBranchStrict(cwd);
183
183
  if (current === clean) return;
@@ -188,24 +188,24 @@ export function setBranchName(branchName = 'main', cwd = process.cwd()) {
188
188
  cwd, stdio: 'ignore'
189
189
  });
190
190
  if (existing.status === 0) {
191
- throw new Error(`Branch local '${clean}' đã tồn tại. Hãy chọn tên khác hoặc checkout branch đó trước.`);
191
+ throw new Error(`Local branch '${clean}' already exists. Choose another name or check out that branch first.`);
192
192
  }
193
193
 
194
194
  const r = spawnSync('git', ['branch', '-M', clean], { cwd, encoding: 'utf-8' });
195
195
  if (r.status !== 0) {
196
- const msg = String(r.stderr || r.stdout || 'Không thể đổi tên branch.').trim();
196
+ const msg = String(r.stderr || r.stdout || 'Could not rename branch.').trim();
197
197
  throw new Error(msg);
198
198
  }
199
199
  }
200
200
 
201
201
  /**
202
- * Cấu hình remote `origin` cho Git repo
202
+ * Configure the `origin` remote for a Git repository.
203
203
  * @param {string} repoUrl
204
204
  * @param {string} cwd
205
205
  */
206
206
  export function setGitRemote(repoUrl, cwd = process.cwd()) {
207
207
  if (!repoUrl || typeof repoUrl !== 'string' || !repoUrl.trim()) {
208
- throw new Error('URL repository trống.');
208
+ throw new Error('Repository URL is empty.');
209
209
  }
210
210
  const url = repoUrl.trim();
211
211
  try {
@@ -218,26 +218,26 @@ export function setGitRemote(repoUrl, cwd = process.cwd()) {
218
218
  r = spawnSync('git', ['remote', 'add', 'origin', url], { cwd, encoding: 'utf-8' });
219
219
  }
220
220
  if (r.status !== 0) {
221
- throw new Error(String(r.stderr || r.stdout || 'git remote thất bại').trim());
221
+ throw new Error(String(r.stderr || r.stdout || 'git remote failed').trim());
222
222
  }
223
223
  } catch (error) {
224
- throw new Error(`Không thể thiết lập Git Remote URL: ${error.message}`);
224
+ throw new Error(`Could not configure Git remote URL: ${error.message}`);
225
225
  }
226
226
  }
227
227
 
228
228
  /**
229
- * Stage toàn bộ file trong thư mục
229
+ * Stage all files in the directory.
230
230
  * @param {string} cwd
231
231
  */
232
232
  export function stageAllFiles(cwd = process.cwd()) {
233
233
  const r = spawnSync('git', ['add', '.'], { cwd, encoding: 'utf-8' });
234
234
  if (r.status !== 0) {
235
- throw new Error(String(r.stderr || r.stdout || 'git add thất bại').trim());
235
+ throw new Error(String(r.stderr || r.stdout || 'git add failed').trim());
236
236
  }
237
237
  }
238
238
 
239
239
  /**
240
- * Kiểm tra xem có thay đổi (staged hoặc unstaged) chưa commit hay không
240
+ * Check for staged or unstaged changes that have not been committed.
241
241
  * @param {string} cwd
242
242
  * @returns {boolean}
243
243
  */
@@ -252,9 +252,9 @@ export function hasChangesToCommit(cwd = process.cwd()) {
252
252
  }
253
253
 
254
254
  /**
255
- * Thực hiện Git commit với thời gian tùy chỉnh (sử dụng GIT_AUTHOR_DATE & GIT_COMMITTER_DATE)
255
+ * Create a Git commit with a custom timestamp using GIT_AUTHOR_DATE and GIT_COMMITTER_DATE.
256
256
  * @param {string} message
257
- * @param {string} dateFormatted ISO hoặc Git date format
257
+ * @param {string} dateFormatted ISO or Git date format
258
258
  * @param {string} cwd
259
259
  */
260
260
  export function commitWithCustomDate(message, dateFormatted, cwd = process.cwd()) {
@@ -271,15 +271,15 @@ export function commitWithCustomDate(message, dateFormatted, cwd = process.cwd()
271
271
  });
272
272
 
273
273
  if (result.status !== 0) {
274
- const stderr = result.stderr || result.stdout || 'Lỗi không xác định khi commit';
275
- throw new Error(`Git commit thất bại: ${stderr.trim()}`);
274
+ const stderr = result.stderr || result.stdout || 'Unknown error while committing';
275
+ throw new Error(`Git commit failed: ${stderr.trim()}`);
276
276
  }
277
277
  }
278
278
 
279
279
  /**
280
- * Đẩy code lên Git remote repo
281
- * @param {string} branch Tên branch (VD: main)
282
- * @param {boolean} force Có sử dụng --force hay không
280
+ * Push code to a Git remote repository.
281
+ * @param {string} branch Branch name (e.g. main)
282
+ * @param {boolean} force Whether to use --force
283
283
  * @param {string} cwd
284
284
  */
285
285
  export function pushToRemote(branch = 'main', force = false, cwd = process.cwd()) {
@@ -295,7 +295,7 @@ export function pushToRemote(branch = 'main', force = false, cwd = process.cwd()
295
295
  });
296
296
 
297
297
  if (result.status !== 0) {
298
- const raw = (result.stderr || result.stdout || 'Lỗi không xác định khi git push').trim();
298
+ const raw = (result.stderr || result.stdout || 'Unknown error while running git push').trim();
299
299
  const parsed = parsePushError(raw);
300
300
  const err = new Error(raw);
301
301
  err.code = parsed.code;
@@ -307,25 +307,25 @@ export function pushToRemote(branch = 'main', force = false, cwd = process.cwd()
307
307
  export function pullFromRemote(remote = 'origin', branch = null, cwd = process.cwd(), { rebase = false } = {}) {
308
308
  const targetBranch = branch || getCurrentBranchStrict(cwd);
309
309
  if (!targetBranch) {
310
- throw new Error('Đang ở detached HEAD nên không xác định được branch để pull.');
310
+ throw new Error('HEAD is detached, so the branch to pull cannot be determined.');
311
311
  }
312
312
  const strategy = rebase ? '--rebase' : '--ff-only';
313
313
  const result = spawnSync('git', ['pull', strategy, remote, targetBranch], { cwd, encoding: 'utf-8', stdio: 'pipe' });
314
314
  if (result.status !== 0) {
315
- throw new Error((result.stderr || result.stdout || `Git pull ${strategy} thất bại`).trim());
315
+ throw new Error((result.stderr || result.stdout || `Git pull ${strategy} failed`).trim());
316
316
  }
317
317
  return (result.stdout || '').trim();
318
318
  }
319
319
 
320
320
  export function getRecentCommits(limit = 30, cwd = process.cwd()) {
321
321
  const safeLimit = Math.max(1, Math.min(100, Number(limit) || 30));
322
- // Dùng mảng args (không qua shell) để an toàn trên Windows (tránh lỗi với %, ^, {}).
323
- // --date=iso để hiển thị cả giờ phút khi chọn commit.
322
+ // Pass arguments as an array to avoid shell escaping issues on Windows (%, ^, {}).
323
+ // --date=iso includes the time when displaying commits.
324
324
  const result = spawnSync('git', ['log', `--max-count=${safeLimit}`, '--format=%H%x09%h%x09%ad%x09%s', '--date=iso'], {
325
325
  cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe']
326
326
  });
327
327
  if (result.status !== 0) {
328
- // Repo chưa có commit nào -> trả về rỗng để caller báo lỗi thân thiện.
328
+ // Return an empty list for repositories without commits so the caller can show a friendly error.
329
329
  return [];
330
330
  }
331
331
  const output = (result.stdout || '').trim();
@@ -337,50 +337,50 @@ export function getRecentCommits(limit = 30, cwd = process.cwd()) {
337
337
  }
338
338
 
339
339
  /**
340
- * Sửa Author Date + Committer Date của 1 commit.
341
- * - Yêu cầu working tree sạch.
342
- * - Chỉ cho chạy trên branch thường (không detached HEAD).
343
- * - Commit phải nằm trên lịch sử branch hiện tại.
344
- * - Viết lại hash của commit đó và mọi commit phía sau (descendants).
345
- * - Giữ bản gốc bằng một nhánh backup tự động: backup/before-date-edit-*
340
+ * Edit the author and committer dates of a commit.
341
+ * - Requires a clean working tree.
342
+ * - Requires a branch checkout (not detached HEAD).
343
+ * - The commit must be in the current branch history.
344
+ * - Rewrites the selected commit and all of its descendants.
345
+ * - Preserves the original history in a backup branch: backup/before-date-edit-*
346
346
  */
347
347
  export function rewriteCommitDate(commitHash, dateFormatted, cwd = process.cwd()) {
348
- // 1. Working tree phải sạch.
348
+ // 1. Require a clean working tree.
349
349
  const statusRes = spawnSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf-8' });
350
- if (statusRes.status !== 0) throw new Error('Không đọc được trạng thái working tree.');
351
- if ((statusRes.stdout || '').trim()) throw new Error('Hãy commit hoặc stash thay đổi trước khi sửa thời gian commit.');
350
+ if (statusRes.status !== 0) throw new Error('Could not read working tree status.');
351
+ if ((statusRes.stdout || '').trim()) throw new Error('Commit or stash your changes before editing a commit timestamp.');
352
352
 
353
- // 2. Phải ở trên branch thường.
353
+ // 2. Require a normal branch checkout.
354
354
  const branchRes = spawnSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf-8' });
355
355
  const branch = (branchRes.stdout || '').trim();
356
- if (!branch) throw new Error('Không hỗ trợ sửa commit khi đang ở detached HEAD.');
356
+ if (!branch) throw new Error('Cannot edit commits while HEAD is detached.');
357
357
 
358
358
  const headRes = spawnSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf-8' });
359
- if (headRes.status !== 0) throw new Error('Không xác định được HEAD của branch hiện tại.');
359
+ if (headRes.status !== 0) throw new Error('Could not determine the current branch HEAD.');
360
360
  const head = (headRes.stdout || '').trim();
361
361
 
362
- // Dùng mảng args để tránh shell escape ^{commit} trên Windows (cmd/PowerShell).
362
+ // Pass an argument array to avoid escaping ^{commit} in Windows shells.
363
363
  const selRes = spawnSync('git', ['rev-parse', `${commitHash}^{commit}`], { cwd, encoding: 'utf-8' });
364
- if (selRes.status !== 0) throw new Error('Không tìm thấy commit cần sửa.');
364
+ if (selRes.status !== 0) throw new Error('Commit to edit not found.');
365
365
  const selected = (selRes.stdout || '').trim();
366
366
 
367
367
  const isAncestor = spawnSync('git', ['merge-base', '--is-ancestor', selected, head], { cwd, stdio: 'ignore' });
368
- if (isAncestor.status !== 0) throw new Error('Commit này không nằm trên lịch sử của branch hiện tại.');
368
+ if (isAncestor.status !== 0) throw new Error('This commit is not in the current branch history.');
369
369
 
370
- // 3. Chỉ liệt kê commit từ selected tới HEAD (đỡ nặng, đúng semantics viết lại descendants).
370
+ // 3. List only commits from selected to HEAD to limit work to rewritten descendants.
371
371
  let revRange = head;
372
372
  const hasParent = spawnSync('git', ['rev-parse', '--verify', `${selected}^`], { cwd, stdio: 'ignore' });
373
373
  if (hasParent.status === 0) {
374
374
  revRange = `${selected}^..${head}`;
375
375
  }
376
376
  const listRes = spawnSync('git', ['rev-list', '--reverse', '--topo-order', revRange], { cwd, encoding: 'utf-8' });
377
- if (listRes.status !== 0) throw new Error('Không đọc được lịch sử commit.');
377
+ if (listRes.status !== 0) throw new Error('Could not read commit history.');
378
378
  const commits = (listRes.stdout || '').trim().split(/\r?\n/).filter(Boolean);
379
- if (!commits.includes(selected)) throw new Error('Commit này không nằm trên lịch sử của branch hiện tại.');
379
+ if (!commits.includes(selected)) throw new Error('This commit is not in the current branch history.');
380
380
 
381
381
  const epoch = Math.floor(new Date(dateFormatted).getTime() / 1000);
382
- if (!Number.isFinite(epoch)) throw new Error('Thời gian commit không hợp lệ.');
383
- // Format trong object commit là "<epoch> <tz>", KHÔNG có ký tự @.
382
+ if (!Number.isFinite(epoch)) throw new Error('Invalid commit timestamp.');
383
+ // Commit object dates use the format "<epoch> <tz>" without an @ character.
384
384
  const gitDate = `${epoch} +0000`;
385
385
 
386
386
  const remapped = new Map();
@@ -388,10 +388,10 @@ export function rewriteCommitDate(commitHash, dateFormatted, cwd = process.cwd()
388
388
 
389
389
  for (const hash of commits) {
390
390
  const catRes = spawnSync('git', ['cat-file', 'commit', hash], { cwd, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
391
- if (catRes.status !== 0) throw new Error(`Không đọc được commit ${hash.slice(0, 7)}.`);
391
+ if (catRes.status !== 0) throw new Error(`Could not read commit ${hash.slice(0, 7)}.`);
392
392
  const raw = catRes.stdout || '';
393
393
  const splitAt = raw.indexOf('\n\n');
394
- if (splitAt === -1) throw new Error(`Commit ${hash.slice(0, 7)} có định dạng không hợp lệ.`);
394
+ if (splitAt === -1) throw new Error(`Commit ${hash.slice(0, 7)} has an invalid format.`);
395
395
  const headers = raw.slice(0, splitAt).split('\n');
396
396
  const message = raw.slice(splitAt);
397
397
  const parents = headers.filter((line) => line.startsWith('parent ')).map((line) => line.slice(7));
@@ -400,7 +400,7 @@ export function rewriteCommitDate(commitHash, dateFormatted, cwd = process.cwd()
400
400
  if (hash !== selected && !parentChanged) continue;
401
401
 
402
402
  if (headers.some((line) => line === 'gpgsig' || line.startsWith('gpgsig '))) {
403
- throw new Error('Lịch sử có commit đã ký số; sửa ngày sẽ làm chữ ký mất hiệu lực.');
403
+ throw new Error('The history contains a signed commit; editing its date would invalidate the signature.');
404
404
  }
405
405
  let parentIndex = 0;
406
406
  const newHeaders = headers.map((line) => {
@@ -411,37 +411,37 @@ export function rewriteCommitDate(commitHash, dateFormatted, cwd = process.cwd()
411
411
  return line;
412
412
  });
413
413
  if (hash === selected && !newHeaders.some((line) => line.startsWith('author ') && line.endsWith(gitDate))) {
414
- throw new Error('Không thể phân tích metadata của commit.');
414
+ throw new Error('Could not parse commit metadata.');
415
415
  }
416
416
  const result = spawnSync('git', ['hash-object', '-t', 'commit', '-w', '--stdin'], {
417
417
  cwd, input: `${newHeaders.join('\n')}${message}`, encoding: 'utf-8'
418
418
  });
419
- if (result.status !== 0) throw new Error((result.stderr || 'Không thể tạo commit mới').trim());
419
+ if (result.status !== 0) throw new Error((result.stderr || 'Could not create a new commit').trim());
420
420
  remapped.set(hash, (result.stdout || '').trim());
421
421
  changed = true;
422
422
  }
423
423
 
424
424
  const newHead = remapped.get(head) || head;
425
- if (!changed || newHead === head) throw new Error('Không tạo được commit đã sửa.');
425
+ if (!changed || newHead === head) throw new Error('Could not create the edited commit.');
426
426
 
427
- // 4. Giữ bản gốc bằng nhánh backup thật để user thấy trong `git branch`.
427
+ // 4. Preserve the original history in a backup branch visible in `git branch`.
428
428
  const backupBranch = `backup/before-date-edit-${Date.now()}-${head.slice(0, 7)}`;
429
429
  const backupRef = `refs/heads/${backupBranch}`;
430
430
  const bkRes = spawnSync('git', ['update-ref', backupRef, head], { cwd, encoding: 'utf-8' });
431
- if (bkRes.status !== 0) throw new Error('Không tạo được nhánh backup.');
431
+ if (bkRes.status !== 0) throw new Error('Could not create a backup branch.');
432
432
  const mvRes = spawnSync('git', ['update-ref', `refs/heads/${branch}`, newHead, head], { cwd, encoding: 'utf-8' });
433
433
  if (mvRes.status !== 0) {
434
434
  spawnSync('git', ['update-ref', '-d', backupRef], { cwd, stdio: 'ignore' });
435
- throw new Error((mvRes.stderr || 'Không thể cập nhật branch sau khi viết lại.').trim());
435
+ throw new Error((mvRes.stderr || 'Could not update the branch after rewriting history.').trim());
436
436
  }
437
437
  return { oldHead: head, newHead, backupRef, backupBranch, rewrittenCount: remapped.size };
438
438
  }
439
439
 
440
440
  /**
441
- * Phân loại lỗi khi `git push` thất bại để CLI hiển thị hướng dẫn thân thiện.
442
- * Đặc biệt xử lý GitHub Push Protection (GH013) khi push dính secret.
441
+ * Classify `git push` failures so the CLI can show helpful guidance.
442
+ * Specifically handle GitHub Push Protection (GH013) secret blocks.
443
443
  *
444
- * @param {string} rawOutput stderr/stdout từ git push
444
+ * @param {string} rawOutput stderr/stdout from git push
445
445
  * @returns {{ code: string, secretTypes: string[], files: string[], unblockUrl: string | null, resolveUrl: string | null }}
446
446
  */
447
447
  export function parsePushError(rawOutput = '') {
@@ -458,7 +458,7 @@ export function parsePushError(rawOutput = '') {
458
458
  (lower.includes('detected') && lower.includes('secret'));
459
459
 
460
460
  if (isSecretBlocked) {
461
- // VD: "—— npm Access Token ——" hoặc "— Generic API Key —"
461
+ // Examples: "—— npm Access Token ——" or "— Generic API Key —".
462
462
  const secretTypes = [];
463
463
  const typeRegex = /[—\-]{2,}\s*([A-Za-z0-9 _.'()/+-]+?(?:token|key|secret|password|private key)[A-Za-z0-9 _.'()/+-]*?)\s*[—\-]{2,}/gi;
464
464
  let m;
@@ -467,13 +467,13 @@ export function parsePushError(rawOutput = '') {
467
467
  if (name && !secretTypes.includes(name)) secretTypes.push(name);
468
468
  }
469
469
 
470
- // VD: "- commit abc123: .npmrc:1" hoặc "file: .env"
470
+ // Examples: "- commit abc123: .npmrc:1" or "file: .env".
471
471
  const files = [];
472
472
  const fileRegex = /(?:commit\s+[0-9a-f]{4,40}\s*:\s*)([^\s:]+\.[A-Za-z0-9_.-]+)/gi;
473
473
  while ((m = fileRegex.exec(text)) !== null) {
474
474
  if (!files.includes(m[1])) files.push(m[1]);
475
475
  }
476
- // fallback: tìm tên file khả nghi trong output (tránh greedy .* nuốt spaces)
476
+ // Fallback: find suspicious filenames in the output without a greedy .* match.
477
477
  const fallbackRegex = /(^|\s)(`?\.?(env|npmrc|id_rsa|id_ed25519|[^\s`'"]*\.pem|[^\s`'"]*\.key))\b/gi;
478
478
  while ((m = fallbackRegex.exec(text)) !== null) {
479
479
  const f = m[2].replace(/`/g, '');
@@ -505,10 +505,10 @@ export function parsePushError(rawOutput = '') {
505
505
  }
506
506
 
507
507
  /**
508
- * Quét nhanh staged files để cảnh báo TRƯỚC khi commit/push.
509
- * Chỉ cảnh báo, không chặn — tránh false-positive.
508
+ * Quickly scan staged files before commit/push.
509
+ * Warn without blocking to avoid false positives.
510
510
  * @param {string} cwd
511
- * @returns {string[]} danh sách file khả nghi
511
+ * @returns {string[]} List of suspicious files
512
512
  */
513
513
  export function findRiskyStagedFiles(cwd = process.cwd()) {
514
514
  const denyList = [/^\.env(\..+)?$/i, /^\.npmrc$/i, /_rsa$/, /_ed25519$/, /\.pem$/i, /\.key$/i, /secrets?\.json$/i, /credentials?\.json$/i, /\.p12$/i, /\.pfx$/i];