git-push-time 1.0.0
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 +101 -0
- package/bin/index.js +210 -0
- package/lib/git.js +153 -0
- package/lib/utils.js +106 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# 🚀 GitHub CLI (`githubcli`)
|
|
2
|
+
|
|
3
|
+
Một công cụ Dòng lệnh (CLI) mạnh mẽ và tiện lợi giúp bạn **đẩy dự án lên Git (GitHub, GitLab, Bitbucket,...) cực nhanh** chỉ bằng cách dán URL repo, đồng thời hỗ trợ **tùy chỉnh hoặc lùi thời gian commit/push** (Author Date & Committer Date).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ✨ Tính năng nổi bật
|
|
8
|
+
|
|
9
|
+
- 🔗 **Đẩy dự án siêu tốc**: Chỉ cần dán URL Git repository, công cụ tự động làm mọi thao tác `git init`, `git remote add/set-url`, `git add .`, `git commit`, `git push`.
|
|
10
|
+
- ⏰ **Tùy chỉnh thời gian commit (Git Backdate)**:
|
|
11
|
+
- Lùi ngày/giờ bằng chuỗi ngắn gọn: `-2d` (2 ngày trước), `-5h` (5 giờ trước), `-1w` (1 tuần trước), `-30m` (30 phút trước).
|
|
12
|
+
- Hoặc nhập ngày giờ chuẩn chính xác: `2024-01-15 14:30:00`.
|
|
13
|
+
- Cập nhật cả `GIT_AUTHOR_DATE` và `GIT_COMMITTER_DATE` chuẩn mực.
|
|
14
|
+
- 💬 **Chế độ Tương tác (Interactive Prompt)**: Giao diện trực quan đẹp mắt với câu hỏi từng bước.
|
|
15
|
+
- ⚡ **Chế độ Lệnh nhanh (Command-line flags)**: Truyền tham số để đẩy code không cần bấm enter chọn lựa.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 📦 Cài đặt
|
|
20
|
+
|
|
21
|
+
### Cách 1: Cài đặt toàn cục (Recommended)
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g githubcli
|
|
24
|
+
```
|
|
25
|
+
Sau khi cài đặt, bạn có thể gọi lệnh `githubcli`, `git-push-time` hoặc `git-custom-push` ở **bất kỳ thư mục dự án nào** trên máy tính!
|
|
26
|
+
|
|
27
|
+
### Cách 2: Chạy trực tiếp không cần cài đặt
|
|
28
|
+
```bash
|
|
29
|
+
npx githubcli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🛠️ Hướng dẫn sử dụng
|
|
35
|
+
|
|
36
|
+
### 1. Chế độ tương tác (Interactive Mode)
|
|
37
|
+
Chỉ cần mở Terminal tại thư mục dự án của bạn và gõ:
|
|
38
|
+
```bash
|
|
39
|
+
git-push-time
|
|
40
|
+
```
|
|
41
|
+
Công cụ sẽ hiển thị từng câu hỏi giao diện để bạn dán link repo, nhập commit message và chọn thời gian:
|
|
42
|
+
1. **Dán Link Repo**: `https://github.com/username/repository.git`
|
|
43
|
+
2. **Commit Message**: `Initial commit`
|
|
44
|
+
3. **Chọn thời gian**:
|
|
45
|
+
- ⏰ *Ngay bây giờ*
|
|
46
|
+
- ⏳ *Lùi thời gian* (VD: `-2d` là 2 ngày trước)
|
|
47
|
+
- 📅 *Nhập ngày giờ cụ thể* (VD: `2024-05-20 09:15:00`)
|
|
48
|
+
4. **Tên Branch**: `main`
|
|
49
|
+
5. **Xác nhận**: Có / Không.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### 2. Chế độ dòng lệnh nhanh (Command Line Flags)
|
|
54
|
+
Đẩy dự án với thông số truyền trực tiếp:
|
|
55
|
+
|
|
56
|
+
#### Ví dụ 1: Lùi thời gian commit 2 ngày trước
|
|
57
|
+
```bash
|
|
58
|
+
git-push-time -r https://github.com/username/my-repo.git -d "-2d" -m "Initial commit"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Ví dụ 2: Đặt ngày giờ commit cụ thể trong quá khứ
|
|
62
|
+
```bash
|
|
63
|
+
git-push-time -r https://github.com/username/my-repo.git -d "2024-01-15 14:30:00" -m "Add core features"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Ví dụ 3: Đẩy tự động không hỏi lại (-y) và force push (-f)
|
|
67
|
+
```bash
|
|
68
|
+
git-push-time -r https://github.com/username/my-repo.git -d "-5h" -m "Update docs" -b main -f -y
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 📊 Bảng định dạng thời gian hỗ trợ
|
|
74
|
+
|
|
75
|
+
| Định dạng ví dụ | Ý nghĩa |
|
|
76
|
+
| :--- | :--- |
|
|
77
|
+
| `now` hoặc để trống | Thời gian hiện tại |
|
|
78
|
+
| `-2d` hoặc `2d` | Lùi lại 2 ngày |
|
|
79
|
+
| `-5h` hoặc `5h` | Lùi lại 5 giờ |
|
|
80
|
+
| `-30m` hoặc `30m` | Lùi lại 30 phút |
|
|
81
|
+
| `-1w` hoặc `1w` | Lùi lại 1 tuần |
|
|
82
|
+
| `-3m` | Lùi lại 3 tháng |
|
|
83
|
+
| `2024-01-15 14:30:00` | Ngày 15/01/2024 lúc 14 giờ 30 phút |
|
|
84
|
+
| `2024-01-15` | Ngày 15/01/2024 |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 🚩 Danh sách các Flags (Options)
|
|
89
|
+
|
|
90
|
+
- `-r, --repo <url>`: Link Git Repository (VD: `https://github.com/user/repo.git`)
|
|
91
|
+
- `-d, --date <datetime>`: Thời gian commit (`now`, `-2d`, `2024-01-15 10:30:00`,...)
|
|
92
|
+
- `-m, --message <msg>`: Thông điệp commit (Mặc định: `Initial commit`)
|
|
93
|
+
- `-b, --branch <branch>`: Tên branch (Mặc định: `main`)
|
|
94
|
+
- `-f, --force`: Đẩy đè lên branch remote (`--force`)
|
|
95
|
+
- `-y, --yes`: Tự động xác nhận, không hiện prompt chọn
|
|
96
|
+
- `-h, --help`: Hướng dẫn trợ giúp
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 📜 Giấy phép
|
|
101
|
+
MIT License.
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { input, select, confirm } from '@inquirer/prompts';
|
|
7
|
+
import { isValidGitUrl, parseCustomDate } from '../lib/utils.js';
|
|
8
|
+
import {
|
|
9
|
+
checkGitInstalled,
|
|
10
|
+
isGitRepo,
|
|
11
|
+
initGitRepo,
|
|
12
|
+
setGitRemote,
|
|
13
|
+
stageAllFiles,
|
|
14
|
+
hasChangesToCommit,
|
|
15
|
+
commitWithCustomDate,
|
|
16
|
+
setBranchName,
|
|
17
|
+
pushToRemote,
|
|
18
|
+
getCurrentBranch
|
|
19
|
+
} from '../lib/git.js';
|
|
20
|
+
|
|
21
|
+
const program = new Command();
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.name('githubcli')
|
|
25
|
+
.description('Công cụ CLI giúp đẩy dự án lên Git và sửa thời gian commit (Backdate Commit)')
|
|
26
|
+
.version('1.0.0')
|
|
27
|
+
.option('-r, --repo <url>', 'Đường dẫn Git repository (URL)')
|
|
28
|
+
.option('-d, --date <datetime>', 'Thời gian commit (Ví dụ: "2024-01-15 14:30:00", "-2d", "-5h", "now")')
|
|
29
|
+
.option('-m, --message <msg>', 'Thông điệp commit (Commit message)')
|
|
30
|
+
.option('-b, --branch <branch>', 'Tên branch trên remote (Mặc định: main)')
|
|
31
|
+
.option('-f, --force', 'Thực hiện push đè (--force)', false)
|
|
32
|
+
.option('-y, --yes', 'Bỏ qua prompt tương tác, tự động dùng tham số hoặc giá trị mặc định', false);
|
|
33
|
+
|
|
34
|
+
program.parse(process.argv);
|
|
35
|
+
const options = program.opts();
|
|
36
|
+
|
|
37
|
+
async function run() {
|
|
38
|
+
console.log(chalk.bold.cyan('\n🚀 GIT PUSH & COMMIT TIME SPOOFER 🚀\n'));
|
|
39
|
+
|
|
40
|
+
// 1. Kiểm tra Git đã cài đặt chưa
|
|
41
|
+
if (!checkGitInstalled()) {
|
|
42
|
+
console.error(chalk.red('❌ Hệ thống chưa cài đặt Git. Vui lòng cài đặt Git trước khi dùng công cụ này.'));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let repoUrl = options.repo;
|
|
47
|
+
let commitMsg = options.message;
|
|
48
|
+
let dateInput = options.date;
|
|
49
|
+
let branchName = options.branch || 'main';
|
|
50
|
+
|
|
51
|
+
const isInteractive = !options.yes;
|
|
52
|
+
|
|
53
|
+
// 2. Chế độ Tương tác (Interactive Prompts) nếu thiếu tham số
|
|
54
|
+
if (isInteractive) {
|
|
55
|
+
// Nhập URL Repo nếu chưa truyền qua CLI
|
|
56
|
+
if (!repoUrl) {
|
|
57
|
+
repoUrl = await input({
|
|
58
|
+
message: 'Dán link Git Repository (Ví dụ: https://github.com/user/repo.git):',
|
|
59
|
+
validate: (value) => {
|
|
60
|
+
if (!value || !value.trim()) return 'Vui lòng nhập đường dẫn repository!';
|
|
61
|
+
if (!isValidGitUrl(value)) return 'Đường dẫn Git URL không đúng định dạng (https/ssh/.git)';
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Nhập Commit Message nếu chưa truyền
|
|
68
|
+
if (!commitMsg) {
|
|
69
|
+
commitMsg = await input({
|
|
70
|
+
message: 'Nhập thông điệp Commit (Commit message):',
|
|
71
|
+
default: 'Initial commit'
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Chọn cách nhập thời gian Commit nếu chưa truyền
|
|
76
|
+
if (!dateInput) {
|
|
77
|
+
const dateOption = await select({
|
|
78
|
+
message: 'Chọn thời gian cho Commit:',
|
|
79
|
+
choices: [
|
|
80
|
+
{ name: '⏰ Ngay bây giờ (Current Time)', value: 'now' },
|
|
81
|
+
{ name: '⏳ Lùi thời gian (Ví dụ: -2d là 2 ngày trước, -5h là 5 giờ trước, -1w là 1 tuần trước)', value: 'relative' },
|
|
82
|
+
{ name: '📅 Nhập ngày giờ cụ thể (Ví dụ: 2024-01-15 14:30:00)', value: 'custom' }
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (dateOption === 'now') {
|
|
87
|
+
dateInput = 'now';
|
|
88
|
+
} else if (dateOption === 'relative') {
|
|
89
|
+
dateInput = await input({
|
|
90
|
+
message: 'Nhập thời gian lùi (VD: -2d [2 ngày trước], -5h [5 giờ trước], -1w [1 tuần trước]):',
|
|
91
|
+
default: '-1d',
|
|
92
|
+
validate: (val) => {
|
|
93
|
+
const parsed = parseCustomDate(val);
|
|
94
|
+
return parsed.valid || parsed.error || 'Thời gian không hợp lệ';
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
} else if (dateOption === 'custom') {
|
|
98
|
+
dateInput = await input({
|
|
99
|
+
message: 'Nhập ngày giờ cụ thể (Định dạng: YYYY-MM-DD HH:mm:ss):',
|
|
100
|
+
default: '2024-01-15 10:00:00',
|
|
101
|
+
validate: (val) => {
|
|
102
|
+
const parsed = parseCustomDate(val);
|
|
103
|
+
return parsed.valid || parsed.error || 'Ngày giờ không hợp lệ';
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Nhập Branch Name nếu chưa truyền và chưa đổi
|
|
110
|
+
if (!options.branch) {
|
|
111
|
+
const currentB = getCurrentBranch();
|
|
112
|
+
branchName = await input({
|
|
113
|
+
message: 'Tên branch trên remote:',
|
|
114
|
+
default: currentB || 'main'
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Set default fallback values
|
|
120
|
+
if (!commitMsg) commitMsg = 'Initial commit';
|
|
121
|
+
if (!dateInput) dateInput = 'now';
|
|
122
|
+
if (!branchName) branchName = 'main';
|
|
123
|
+
|
|
124
|
+
// Validate Git URL
|
|
125
|
+
if (!repoUrl || !isValidGitUrl(repoUrl)) {
|
|
126
|
+
console.error(chalk.red(`❌ Lỗi: URL repository '${repoUrl}' không hợp lệ!`));
|
|
127
|
+
console.log(chalk.yellow('Ví dụ URL đúng: https://github.com/username/repository.git'));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Parse Date/Time
|
|
132
|
+
const parsedDate = parseCustomDate(dateInput);
|
|
133
|
+
if (!parsedDate.valid) {
|
|
134
|
+
console.error(chalk.red(`❌ Lỗi: ${parsedDate.error}`));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Hiển thị tóm tắt thông tin trước khi thực thi
|
|
139
|
+
console.log('\n' + chalk.bgBlue.white.bold(' 📋 THÔNG TIN ĐẨY DỰ ÁN '));
|
|
140
|
+
console.log(chalk.gray('----------------------------------------'));
|
|
141
|
+
console.log(`🔗 Repo Remote : ${chalk.green(repoUrl)}`);
|
|
142
|
+
console.log(`💬 Commit Msg : ${chalk.yellow(commitMsg)}`);
|
|
143
|
+
console.log(`📅 Commit Time : ${chalk.magenta(parsedDate.display)}`);
|
|
144
|
+
console.log(`🌿 Branch : ${chalk.cyan(branchName)}`);
|
|
145
|
+
console.log(`⚡ Force Push : ${options.force ? chalk.red('CÓ (--force)') : chalk.gray('Không')}`);
|
|
146
|
+
console.log(chalk.gray('----------------------------------------\n'));
|
|
147
|
+
|
|
148
|
+
if (isInteractive) {
|
|
149
|
+
const ok = await confirm({
|
|
150
|
+
message: 'Xác nhận thực hiện tạo commit và đẩy lên Git?',
|
|
151
|
+
default: true
|
|
152
|
+
});
|
|
153
|
+
if (!ok) {
|
|
154
|
+
console.log(chalk.yellow('Đã hủy thao tác.'));
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 3. Thực thi các bước đẩy dự án
|
|
160
|
+
const spinner = ora();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
// Bước 1: Git Init
|
|
164
|
+
if (!isGitRepo()) {
|
|
165
|
+
spinner.start('Đang khởi tạo kho chứa Git (git init)...');
|
|
166
|
+
initGitRepo();
|
|
167
|
+
spinner.succeed('Đã khởi tạo kho chứa Git cục bộ.');
|
|
168
|
+
} else {
|
|
169
|
+
spinner.info('Đã phát hiện kho chứa Git có sẵn.');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Bước 2: Thiết lập Remote URL
|
|
173
|
+
spinner.start(`Đang cấu hình Remote Origin: ${repoUrl}`);
|
|
174
|
+
setGitRemote(repoUrl);
|
|
175
|
+
spinner.succeed(`Đã cấu hình Remote Origin thành công.`);
|
|
176
|
+
|
|
177
|
+
// Bước 3: Stage files (git add .)
|
|
178
|
+
spinner.start('Đang thêm tất cả các file (git add .)...');
|
|
179
|
+
stageAllFiles();
|
|
180
|
+
spinner.succeed('Đã stage tất cả các file.');
|
|
181
|
+
|
|
182
|
+
// Bước 4: Commit với thời gian giả lập (Backdate Commit)
|
|
183
|
+
spinner.start(`Đang tạo Commit với thời gian [${parsedDate.display}]...`);
|
|
184
|
+
|
|
185
|
+
if (!hasChangesToCommit()) {
|
|
186
|
+
spinner.info('Không có thay đổi nào mới để commit. Sẽ tiến hành đẩy commit hiện có.');
|
|
187
|
+
} else {
|
|
188
|
+
commitWithCustomDate(commitMsg, parsedDate.formatted);
|
|
189
|
+
spinner.succeed(`Đã tạo commit thành công với thời gian: ${chalk.bold(parsedDate.display)}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Bước 5: Đặt tên branch
|
|
193
|
+
spinner.start(`Đang thiết lập branch chính '${branchName}'...`);
|
|
194
|
+
setBranchName(branchName);
|
|
195
|
+
spinner.succeed(`Đã đặt tên branch: ${branchName}`);
|
|
196
|
+
|
|
197
|
+
// Bước 6: Push lên Remote
|
|
198
|
+
spinner.start(`Đang đẩy code lên remote '${repoUrl}' (branch: ${branchName})...`);
|
|
199
|
+
pushToRemote(branchName, options.force);
|
|
200
|
+
spinner.succeed(chalk.bold.green(`🎉 ĐÃ ĐẨY DỰ ÁN LÊN GIT THÀNH CÔNG!`));
|
|
201
|
+
|
|
202
|
+
console.log('\n' + chalk.bold.green('✨ Hoàn tất! Bạn có thể kiểm tra commit log trên GitHub/GitLab của mình.'));
|
|
203
|
+
} catch (error) {
|
|
204
|
+
spinner.fail(chalk.red('Đã xảy ra lỗi trong quá trình thực thi:'));
|
|
205
|
+
console.error(chalk.red(`\nLỗi: ${error.message}\n`));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
run();
|
package/lib/git.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kiểm tra xem máy tính đã cài đặt Git hay chưa
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
export function checkGitInstalled() {
|
|
9
|
+
try {
|
|
10
|
+
execSync('git --version', { stdio: 'ignore' });
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Kiểm tra thư mục hiện tại đã là kho chứa Git (Git repo) chưa
|
|
19
|
+
* @param {string} cwd
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
export function isGitRepo(cwd = process.cwd()) {
|
|
23
|
+
try {
|
|
24
|
+
const output = execSync('git rev-parse --is-inside-work-tree', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
25
|
+
return output.trim() === 'true';
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Khởi tạo Git repo trong thư mục hiện tại
|
|
33
|
+
* @param {string} cwd
|
|
34
|
+
*/
|
|
35
|
+
export function initGitRepo(cwd = process.cwd()) {
|
|
36
|
+
execSync('git init', { cwd, stdio: 'pipe' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Lấy tên branch hiện tại (hoặc mặc định là main)
|
|
41
|
+
* @param {string} cwd
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function getCurrentBranch(cwd = process.cwd()) {
|
|
45
|
+
try {
|
|
46
|
+
const branch = execSync('git branch --show-current', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
47
|
+
return branch || 'main';
|
|
48
|
+
} catch {
|
|
49
|
+
return 'main';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Thiết lập tên branch chính (ví dụ: git branch -M main)
|
|
55
|
+
* @param {string} branchName
|
|
56
|
+
* @param {string} cwd
|
|
57
|
+
*/
|
|
58
|
+
export function setBranchName(branchName = 'main', cwd = process.cwd()) {
|
|
59
|
+
try {
|
|
60
|
+
execSync(`git branch -M "${branchName}"`, { cwd, stdio: 'pipe' });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// Nếu chưa có commit nào, command này có thể báo lỗi nhưng không sao
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cấu hình remote `origin` cho Git repo
|
|
68
|
+
* @param {string} repoUrl
|
|
69
|
+
* @param {string} cwd
|
|
70
|
+
*/
|
|
71
|
+
export function setGitRemote(repoUrl, cwd = process.cwd()) {
|
|
72
|
+
try {
|
|
73
|
+
const remotes = execSync('git remote', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
74
|
+
if (remotes.includes('origin')) {
|
|
75
|
+
execSync(`git remote set-url origin "${repoUrl}"`, { cwd, stdio: 'pipe' });
|
|
76
|
+
} else {
|
|
77
|
+
execSync(`git remote add origin "${repoUrl}"`, { cwd, stdio: 'pipe' });
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(`Không thể thiết lập Git Remote URL: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Stage toàn bộ file trong thư mục
|
|
86
|
+
* @param {string} cwd
|
|
87
|
+
*/
|
|
88
|
+
export function stageAllFiles(cwd = process.cwd()) {
|
|
89
|
+
execSync('git add .', { cwd, stdio: 'pipe' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Kiểm tra xem có thay đổi (staged hoặc unstaged) chưa commit hay không
|
|
94
|
+
* @param {string} cwd
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
export function hasChangesToCommit(cwd = process.cwd()) {
|
|
98
|
+
try {
|
|
99
|
+
const status = execSync('git status --porcelain', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
100
|
+
return status.trim().length > 0;
|
|
101
|
+
} catch {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Thực hiện Git commit với thời gian tùy chỉnh (sử dụng GIT_AUTHOR_DATE & GIT_COMMITTER_DATE)
|
|
108
|
+
* @param {string} message
|
|
109
|
+
* @param {string} dateFormatted ISO hoặc Git date format
|
|
110
|
+
* @param {string} cwd
|
|
111
|
+
*/
|
|
112
|
+
export function commitWithCustomDate(message, dateFormatted, cwd = process.cwd()) {
|
|
113
|
+
const env = {
|
|
114
|
+
...process.env,
|
|
115
|
+
GIT_AUTHOR_DATE: dateFormatted,
|
|
116
|
+
GIT_COMMITTER_DATE: dateFormatted
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const result = spawnSync('git', ['commit', '-m', message], {
|
|
120
|
+
cwd,
|
|
121
|
+
env,
|
|
122
|
+
encoding: 'utf-8'
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (result.status !== 0) {
|
|
126
|
+
const stderr = result.stderr || result.stdout || 'Lỗi không xác định khi commit';
|
|
127
|
+
throw new Error(`Git commit thất bại: ${stderr.trim()}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Đẩy code lên Git remote repo
|
|
133
|
+
* @param {string} branch Tên branch (VD: main)
|
|
134
|
+
* @param {boolean} force Có sử dụng --force hay không
|
|
135
|
+
* @param {string} cwd
|
|
136
|
+
*/
|
|
137
|
+
export function pushToRemote(branch = 'main', force = false, cwd = process.cwd()) {
|
|
138
|
+
const args = ['push', '-u', 'origin', branch];
|
|
139
|
+
if (force) {
|
|
140
|
+
args.push('--force');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const result = spawnSync('git', args, {
|
|
144
|
+
cwd,
|
|
145
|
+
encoding: 'utf-8',
|
|
146
|
+
stdio: 'pipe'
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (result.status !== 0) {
|
|
150
|
+
const stderr = result.stderr || result.stdout || 'Lỗi không xác định khi git push';
|
|
151
|
+
throw new Error(stderr.trim());
|
|
152
|
+
}
|
|
153
|
+
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
|
|
3
|
+
|
|
4
|
+
dayjs.extend(customParseFormat);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Kiểm tra xem URL git có hợp lệ hay không (HTTP/HTTPS, SSH, hoặc `.git`)
|
|
8
|
+
* @param {string} url
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function isValidGitUrl(url) {
|
|
12
|
+
if (!url || typeof url !== 'string') return false;
|
|
13
|
+
const trimmed = url.trim();
|
|
14
|
+
// Standard git remote URL patterns: https://, http://, git@, ssh://, or file path
|
|
15
|
+
const gitRegex = /^(https?:\/\/|git@|ssh:\/\/|ftps?:\/\/).+$/i;
|
|
16
|
+
return gitRegex.test(trimmed) || trimmed.endsWith('.git');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse chuỗi thời gian thành định dạng ISO/Git hợp lệ.
|
|
21
|
+
* Hỗ trợ các định dạng:
|
|
22
|
+
* - "now" hoặc để trống -> Thời gian hiện tại
|
|
23
|
+
* - Relative: "-2d", "-5h", "-30m", "-1w", "-3m" (month), "-1y"
|
|
24
|
+
* - Absolute: "YYYY-MM-DD HH:mm:ss", "YYYY-MM-DD HH:mm", "YYYY-MM-DD"
|
|
25
|
+
*
|
|
26
|
+
* @param {string} dateStr
|
|
27
|
+
* @returns {{ valid: boolean, formatted: string, display: string, error?: string }}
|
|
28
|
+
*/
|
|
29
|
+
export function parseCustomDate(dateStr) {
|
|
30
|
+
if (!dateStr || dateStr.trim().toLowerCase() === 'now') {
|
|
31
|
+
const now = dayjs();
|
|
32
|
+
return {
|
|
33
|
+
valid: true,
|
|
34
|
+
formatted: now.toISOString(),
|
|
35
|
+
display: now.format('YYYY-MM-DD HH:mm:ss Z')
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const str = dateStr.trim();
|
|
40
|
+
|
|
41
|
+
// Kiểm tra relative time: ví dụ -2d, -5h, -30m, -1w, -3M, -1y hoặc 2d, 5h
|
|
42
|
+
const relativeMatch = str.match(/^([+-]?\d+)\s*([smhdwMy]|sec|min|hour|day|week|month|year)s?$/i);
|
|
43
|
+
if (relativeMatch) {
|
|
44
|
+
let amount = parseInt(relativeMatch[1], 10);
|
|
45
|
+
const unitRaw = relativeMatch[2].toLowerCase();
|
|
46
|
+
|
|
47
|
+
// Map unit
|
|
48
|
+
let unit = 'day';
|
|
49
|
+
if (unitRaw === 's' || unitRaw === 'sec') unit = 'second';
|
|
50
|
+
else if (unitRaw === 'm' || unitRaw === 'min') unit = 'minute';
|
|
51
|
+
else if (unitRaw === 'h' || unitRaw === 'hour') unit = 'hour';
|
|
52
|
+
else if (unitRaw === 'd' || unitRaw === 'day') unit = 'day';
|
|
53
|
+
else if (unitRaw === 'w' || unitRaw === 'week') unit = 'week';
|
|
54
|
+
else if (unitRaw === 'm' || unitRaw === 'month') unit = 'month';
|
|
55
|
+
else if (unitRaw === 'y' || unitRaw === 'year') unit = 'year';
|
|
56
|
+
|
|
57
|
+
// Nếu người dùng nhập "2d" mà không có dấu "-", mặc định hiểu là "lùi 2d" (-2d)
|
|
58
|
+
if (amount > 0 && !str.startsWith('+')) {
|
|
59
|
+
amount = -amount;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const calculated = dayjs().add(amount, unit);
|
|
63
|
+
if (!calculated.isValid()) {
|
|
64
|
+
return { valid: false, formatted: '', display: '', error: `Không thể tính toán thời gian từ '${dateStr}'` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
valid: true,
|
|
69
|
+
formatted: calculated.toISOString(),
|
|
70
|
+
display: calculated.format('YYYY-MM-DD HH:mm:ss Z')
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Thủ định dạng ngày giờ tuyệt đối
|
|
75
|
+
const formats = [
|
|
76
|
+
'YYYY-MM-DD HH:mm:ss',
|
|
77
|
+
'YYYY-MM-DD HH:mm',
|
|
78
|
+
'YYYY-MM-DD',
|
|
79
|
+
'YYYY/MM/DD HH:mm:ss',
|
|
80
|
+
'YYYY/MM/DD HH:mm',
|
|
81
|
+
'YYYY/MM/DD',
|
|
82
|
+
'DD/MM/YYYY HH:mm:ss',
|
|
83
|
+
'DD/MM/YYYY HH:mm',
|
|
84
|
+
'DD/MM/YYYY'
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
let parsed = dayjs(str, formats, true);
|
|
88
|
+
if (!parsed.isValid()) {
|
|
89
|
+
parsed = dayjs(str); // Thử parse chuẩn ISO
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (parsed.isValid()) {
|
|
93
|
+
return {
|
|
94
|
+
valid: true,
|
|
95
|
+
formatted: parsed.toISOString(),
|
|
96
|
+
display: parsed.format('YYYY-MM-DD HH:mm:ss Z')
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
valid: false,
|
|
102
|
+
formatted: '',
|
|
103
|
+
display: '',
|
|
104
|
+
error: `Định dạng ngày giờ không hợp lệ: '${dateStr}'. Ví dụ đúng: "2024-01-15 14:30:00", "-2d", "-5h", "now"`
|
|
105
|
+
};
|
|
106
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-push-time",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool to push project to Git with custom repo URL and custom commit timestamp",
|
|
5
|
+
"main": "bin/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"githubcli": "./bin/index.js",
|
|
9
|
+
"git-push-time": "./bin/index.js",
|
|
10
|
+
"git-custom-push": "./bin/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node bin/index.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"git",
|
|
17
|
+
"push",
|
|
18
|
+
"commit-date",
|
|
19
|
+
"backdate",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@inquirer/prompts": "^7.3.2",
|
|
26
|
+
"chalk": "^5.4.1",
|
|
27
|
+
"commander": "^13.1.0",
|
|
28
|
+
"dayjs": "^1.11.13",
|
|
29
|
+
"ora": "^8.2.0"
|
|
30
|
+
}
|
|
31
|
+
}
|