e-git-zain 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 +105 -0
- package/index.js +635 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# e-git š
|
|
2
|
+
|
|
3
|
+
**The ultimate CLI tool to automate your GitHub workflow. Stop typing, start pushing.**
|
|
4
|
+
|
|
5
|
+
`e-git` is a powerful, interactive command-line interface designed to take the friction out of Git. Whether you're a beginner or a pro, `e-git` makes staging, committing, pushing, and even "time traveling" (undo/redo) easier than ever.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## š Key Features
|
|
10
|
+
|
|
11
|
+
- **šØ One-Command Push**: Stage all changes, write a commit message, and push to GitHub in one go.
|
|
12
|
+
- **š”ļø Smart GitIgnore Assistant**: Automatically detects missing `.gitignore` files and helps you hide sensitive data (like `.env`) or heavy folders (`node_modules`).
|
|
13
|
+
- **š°ļø Time Travel (Undo & Redo)**:
|
|
14
|
+
- `undo`: Instantly revert to your last clean push.
|
|
15
|
+
- `redo`: Jump forward if you changed your mind after an undo (with a 30-minute safety warning).
|
|
16
|
+
- **š Professional History Management**:
|
|
17
|
+
- `history`: Interactive menu to browse past pushes and view detailed colorized diffs.
|
|
18
|
+
- `list`: A quick table view of all your recorded pushes.
|
|
19
|
+
- **š Auth Shield**: Verifies your GitHub login status and helps you authenticate via GitHub CLI if needed.
|
|
20
|
+
- **š” Auto-Remote Setup**: Detects missing remotes and helps you link to GitHub interactively.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## š Installation
|
|
25
|
+
|
|
26
|
+
Install `e-git` globally via NPM to use it in any project:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g e-git
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## š Usage Guide
|
|
35
|
+
|
|
36
|
+
### 1. Pushing Code
|
|
37
|
+
Simply type `e-git` in your terminal.
|
|
38
|
+
```bash
|
|
39
|
+
# Interactive mode (will ask for message)
|
|
40
|
+
e-git
|
|
41
|
+
|
|
42
|
+
# Quick push (one-shot)
|
|
43
|
+
e-git "Initial commit for my awesome app"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Undoing Mistakes (The "Reset" Button)
|
|
47
|
+
Made a mess of your code? Revert back to safety:
|
|
48
|
+
```bash
|
|
49
|
+
e-git undo
|
|
50
|
+
```
|
|
51
|
+
*Note: If you just pushed, it will step back to the push BEFORE the current one.*
|
|
52
|
+
|
|
53
|
+
### 3. Redoing (The "Jump Forward")
|
|
54
|
+
Accidentally undid something you actually liked? Jump back forward:
|
|
55
|
+
```bash
|
|
56
|
+
e-git redo
|
|
57
|
+
```
|
|
58
|
+
*Note: If the code is older than 30 minutes, it will give you a special warning.*
|
|
59
|
+
|
|
60
|
+
### 4. Viewing History
|
|
61
|
+
```bash
|
|
62
|
+
# Interactive menu (Browse changes, view diffs, or restore versions)
|
|
63
|
+
e-git history
|
|
64
|
+
|
|
65
|
+
# Simple table list for a quick glance
|
|
66
|
+
e-git list
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 5. Maintenance & Credits
|
|
70
|
+
```bash
|
|
71
|
+
# Clear all local push records
|
|
72
|
+
e-git clear
|
|
73
|
+
|
|
74
|
+
# See the creators
|
|
75
|
+
e-git credits
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## š
Monthly Updates
|
|
81
|
+
This project is actively maintained. I promise to release **new features and performance updates every month** to ensure `e-git` remains the best tool in your developer kit.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## ⨠Credits & Author
|
|
86
|
+
|
|
87
|
+
**e-git** was created with ā¤ļø by **Zain Ali** to make developers' lives easier.
|
|
88
|
+
|
|
89
|
+
- **Portfolio**: [zain-mughal.vercel.app](https://zain-mughal.vercel.app)
|
|
90
|
+
- **GitHub**: [@usernamezain](https://github.com/usernamezain)
|
|
91
|
+
- **Learning Course**: [m-learn.eu.cc](http://m-learn.eu.cc)
|
|
92
|
+
- **Email**: [devmughal8@gmail.com](mailto:devmughal8@gmail.com)
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## š„ Support & Donations
|
|
97
|
+
|
|
98
|
+
If `e-git` saved you time and made your workflow easier, feel free to support the project!
|
|
99
|
+
|
|
100
|
+
- **Help & Donations**: Contact me directly if you'd like to contribute or help with the project.
|
|
101
|
+
- **WhatsApp / Contact**: `03124030056`
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
Built by **Zain Ali** & Antigravity AI.
|
package/index.js
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import simpleGit from 'simple-git';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { execSync, spawnSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
const git = simpleGit();
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns the absolute path to the .git-easy-history.json file in the git root.
|
|
17
|
+
*/
|
|
18
|
+
async function getHistoryPath() {
|
|
19
|
+
try {
|
|
20
|
+
const root = await git.revparse(['--show-toplevel']);
|
|
21
|
+
return path.join(root.trim(), '.git', 'git-easy-history.json');
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Checks if a command exists in the current environment.
|
|
29
|
+
*/
|
|
30
|
+
function commandExists(cmd) {
|
|
31
|
+
try {
|
|
32
|
+
execSync(`command -v ${cmd} || where ${cmd}`, { stdio: 'ignore' });
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Verifies if the user is authenticated and has access to the remote repo.
|
|
41
|
+
*/
|
|
42
|
+
async function ensureAuthenticated() {
|
|
43
|
+
const remotes = await git.getRemotes(true);
|
|
44
|
+
if (remotes.length === 0) return;
|
|
45
|
+
|
|
46
|
+
const origin = remotes.find(r => r.name === 'origin');
|
|
47
|
+
if (!origin) return;
|
|
48
|
+
|
|
49
|
+
const spinner = ora(chalk.blue('Verifying GitHub authentication...')).start();
|
|
50
|
+
try {
|
|
51
|
+
await git.listRemote(['--heads', 'origin']);
|
|
52
|
+
spinner.succeed(chalk.green('Authentication verified!'));
|
|
53
|
+
} catch (err) {
|
|
54
|
+
spinner.stop();
|
|
55
|
+
console.log(chalk.yellow('\nā ļø Authentication failed or permission denied.'));
|
|
56
|
+
|
|
57
|
+
const { action } = await inquirer.prompt([
|
|
58
|
+
{
|
|
59
|
+
type: 'list',
|
|
60
|
+
name: 'action',
|
|
61
|
+
message: 'How would you like to proceed?',
|
|
62
|
+
choices: [
|
|
63
|
+
{ name: 'Login using GitHub CLI (Recommended)', value: 'gh' },
|
|
64
|
+
{ name: 'I will handle it manually (Paste a PAT/SSH)', value: 'manual' },
|
|
65
|
+
{ name: 'Abort', value: 'abort' }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
if (action === 'abort') process.exit(1);
|
|
71
|
+
|
|
72
|
+
if (action === 'gh') {
|
|
73
|
+
if (commandExists('gh')) {
|
|
74
|
+
console.log(chalk.blue('\nLaunching GitHub CLI login...'));
|
|
75
|
+
spawnSync('gh', ['auth', 'login'], { stdio: 'inherit' });
|
|
76
|
+
return ensureAuthenticated();
|
|
77
|
+
} else {
|
|
78
|
+
console.log(chalk.red('\nā GitHub CLI ("gh") is not installed.'));
|
|
79
|
+
console.log(chalk.cyan('Download it here: https://cli.github.com/\n'));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (action === 'manual') {
|
|
85
|
+
console.log(chalk.cyan('\nTip: You can use a Personal Access Token (PAT) as your password.'));
|
|
86
|
+
console.log(chalk.cyan('Generate one here: https://github.com/settings/tokens\n'));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Ensures the repo has an origin remote. If not, asks the user to add one.
|
|
93
|
+
*/
|
|
94
|
+
async function checkAndSetupRemote() {
|
|
95
|
+
const remotes = await git.getRemotes(true);
|
|
96
|
+
if (remotes.length === 0) {
|
|
97
|
+
console.log(chalk.yellow('\nā¹ No remote found for this repository.'));
|
|
98
|
+
const { setupRemote } = await inquirer.prompt([
|
|
99
|
+
{
|
|
100
|
+
type: 'confirm',
|
|
101
|
+
name: 'setupRemote',
|
|
102
|
+
message: 'Would you like to add a GitHub remote now?',
|
|
103
|
+
default: true
|
|
104
|
+
}
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
if (setupRemote) {
|
|
108
|
+
const { remoteUrl } = await inquirer.prompt([
|
|
109
|
+
{
|
|
110
|
+
type: 'input',
|
|
111
|
+
name: 'remoteUrl',
|
|
112
|
+
message: 'Enter the GitHub repository URL (e.g., https://github.com/user/repo.git):',
|
|
113
|
+
validate: (input) => input.trim().length > 0 ? true : 'Please enter a valid Git URL.'
|
|
114
|
+
}
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const spinner = ora(chalk.blue('Adding remote "origin"...')).start();
|
|
118
|
+
try {
|
|
119
|
+
await git.addRemote('origin', remoteUrl);
|
|
120
|
+
spinner.succeed(chalk.green('Remote "origin" added successfully!'));
|
|
121
|
+
} catch (err) {
|
|
122
|
+
spinner.fail(chalk.red('Failed to add remote.'));
|
|
123
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
console.log(chalk.red('\nā Error: A remote is required to push code.\n'));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Logs a successful push event to a local JSON file in .git directory.
|
|
135
|
+
*/
|
|
136
|
+
async function logPushEvent(message, branch, hash) {
|
|
137
|
+
try {
|
|
138
|
+
const historyPath = await getHistoryPath();
|
|
139
|
+
if (!historyPath) return;
|
|
140
|
+
|
|
141
|
+
const timestamp = new Date().toISOString();
|
|
142
|
+
const displayTimestamp = new Date().toLocaleString();
|
|
143
|
+
const newEntry = { timestamp, displayTimestamp, message, branch, hash };
|
|
144
|
+
|
|
145
|
+
let history = [];
|
|
146
|
+
try {
|
|
147
|
+
const data = await fs.readFile(historyPath, 'utf8');
|
|
148
|
+
history = JSON.parse(data);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
// File doesn't exist
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (history.length > 0 && history[0].hash === hash) return;
|
|
154
|
+
|
|
155
|
+
history.unshift(newEntry);
|
|
156
|
+
await fs.writeFile(historyPath, JSON.stringify(history.slice(0, 50), null, 2));
|
|
157
|
+
} catch (err) {
|
|
158
|
+
// Silently fail
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Checks for a .gitignore file and helps the user create one if missing.
|
|
164
|
+
*/
|
|
165
|
+
async function ensureGitIgnore() {
|
|
166
|
+
try {
|
|
167
|
+
const root = await git.revparse(['--show-toplevel']);
|
|
168
|
+
const gitIgnorePath = path.join(root.trim(), '.gitignore');
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await fs.access(gitIgnorePath);
|
|
172
|
+
return; // .gitignore already exists
|
|
173
|
+
} catch {
|
|
174
|
+
// .gitignore does not exist
|
|
175
|
+
console.log(chalk.yellow('\nš”ļø No .gitignore found in your project.'));
|
|
176
|
+
const { createIgnore } = await inquirer.prompt([
|
|
177
|
+
{
|
|
178
|
+
type: 'confirm',
|
|
179
|
+
name: 'createIgnore',
|
|
180
|
+
message: 'Would you like to create a .gitignore to keep your repo clean?',
|
|
181
|
+
default: true
|
|
182
|
+
}
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
if (createIgnore) {
|
|
186
|
+
const { patterns } = await inquirer.prompt([
|
|
187
|
+
{
|
|
188
|
+
type: 'checkbox',
|
|
189
|
+
name: 'patterns',
|
|
190
|
+
message: 'Select folders/files to ignore:',
|
|
191
|
+
choices: [
|
|
192
|
+
{ name: 'node_modules (NPM dependencies)', value: 'node_modules/', checked: true },
|
|
193
|
+
{ name: '.env (Sensitive secrets & keys)', value: '.env', checked: true },
|
|
194
|
+
{ name: 'dist (Build output)', value: 'dist/' },
|
|
195
|
+
{ name: 'build (Build output)', value: 'build/' },
|
|
196
|
+
{ name: '.DS_Store (Mac junk files)', value: '.DS_Store' },
|
|
197
|
+
{ name: 'npm-debug.log', value: 'npm-debug.log' },
|
|
198
|
+
{ name: '.git-easy-history.json (Git-Easy local log)', value: '.git/git-easy-history.json' }
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
if (patterns.length > 0) {
|
|
204
|
+
const content = patterns.join('\n') + '\n';
|
|
205
|
+
await fs.writeFile(gitIgnorePath, content);
|
|
206
|
+
console.log(chalk.green('ā
.gitignore created successfully! Your secrets are safe. š”ļø\n'));
|
|
207
|
+
} else {
|
|
208
|
+
console.log(chalk.cyan('No patterns selected. Skipping .gitignore creation.\n'));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
// Silently fail if not in a git repo or other issues
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Destructively restores the repository to a specific commit hash.
|
|
219
|
+
*/
|
|
220
|
+
async function restoreToState(hash, autoConfirm = false, extraWarning = false) {
|
|
221
|
+
try {
|
|
222
|
+
if (!autoConfirm) {
|
|
223
|
+
console.log(chalk.red.bold('\nā ļø WARNING: DESTRUCTIVE ACTION'));
|
|
224
|
+
console.log(chalk.red('This will delete all unpushed changes and reset your files to this specific version.'));
|
|
225
|
+
|
|
226
|
+
if (extraWarning) {
|
|
227
|
+
console.log(chalk.red.bold('CRITICAL: This version is more than 30 minutes old. Proceed with caution!'));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { confirm } = await inquirer.prompt([
|
|
231
|
+
{
|
|
232
|
+
type: 'confirm',
|
|
233
|
+
name: 'confirm',
|
|
234
|
+
message: chalk.yellow('Are you absolutely sure you want to redo/undo to this state?'),
|
|
235
|
+
default: false
|
|
236
|
+
}
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
if (!confirm) {
|
|
240
|
+
console.log(chalk.cyan('\nRestore aborted. Your files are safe.\n'));
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const spinner = ora(chalk.blue(`Restoring files to ${hash.substring(0, 7)}...`)).start();
|
|
246
|
+
await git.reset(['--hard', hash]);
|
|
247
|
+
spinner.succeed(chalk.green('Files restored successfully! Your workspace is back in time. š°ļø'));
|
|
248
|
+
return true;
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(chalk.red(`\nā Restore failed: ${err.message}\n`));
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Displays the diff for a specific commit hash.
|
|
257
|
+
*/
|
|
258
|
+
async function viewCommitDiff(hash) {
|
|
259
|
+
try {
|
|
260
|
+
console.log(chalk.cyan.bold(`\nš Showing changes for push: ${chalk.white(hash.substring(0, 7))}\n`));
|
|
261
|
+
const diff = await git.show([hash, '--color=always', '--pretty=format:%B', '--compact-summary']);
|
|
262
|
+
console.log(diff);
|
|
263
|
+
console.log(chalk.gray('\n' + ''.padEnd(80, '-') + '\n'));
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.error(chalk.red(`\nā Could not fetch diff: ${err.message}\n`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Displays a simple list of pushes without interactivity.
|
|
271
|
+
*/
|
|
272
|
+
async function listPushes() {
|
|
273
|
+
try {
|
|
274
|
+
const isRepo = await git.checkIsRepo();
|
|
275
|
+
if (!isRepo) {
|
|
276
|
+
console.error(chalk.red('\nā Error: Not a git repository.\n'));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const historyPath = await getHistoryPath();
|
|
281
|
+
if (!historyPath) {
|
|
282
|
+
console.log(chalk.yellow('\nā¹ No push history found for this repository.\n'));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let history = [];
|
|
287
|
+
try {
|
|
288
|
+
const data = await fs.readFile(historyPath, 'utf8');
|
|
289
|
+
history = JSON.parse(data);
|
|
290
|
+
} catch (e) {
|
|
291
|
+
console.log(chalk.yellow('\nā¹ No push history found for this repository.\n'));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (history.length === 0) {
|
|
296
|
+
console.log(chalk.yellow('\nā¹ No push history found for this repository.\n'));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log(chalk.cyan.bold('\nš Git-Easy Push History (List View):\n'));
|
|
301
|
+
console.log(chalk.gray(''.padEnd(80, '-')));
|
|
302
|
+
console.log(
|
|
303
|
+
`${chalk.bold('Date & Time').padEnd(30)} | ${chalk.bold('Branch').padEnd(15)} | ${chalk.bold('Message')}`
|
|
304
|
+
);
|
|
305
|
+
console.log(chalk.gray(''.padEnd(80, '-')));
|
|
306
|
+
|
|
307
|
+
history.forEach(entry => {
|
|
308
|
+
const time = entry.displayTimestamp || entry.timestamp || 'Unknown';
|
|
309
|
+
console.log(
|
|
310
|
+
`${time.padEnd(28)} | ${chalk.green((entry.branch || 'main').padEnd(15))} | ${entry.message}`
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
console.log(chalk.gray(''.padEnd(80, '-')) + '\n');
|
|
314
|
+
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.log(chalk.yellow('\nā¹ No push history found for this repository.\n'));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Displays the push history in an interactive format.
|
|
322
|
+
*/
|
|
323
|
+
async function displayHistory() {
|
|
324
|
+
try {
|
|
325
|
+
const isRepo = await git.checkIsRepo();
|
|
326
|
+
if (!isRepo) {
|
|
327
|
+
console.error(chalk.red('\nā Error: Not a git repository.\n'));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const historyPath = await getHistoryPath();
|
|
332
|
+
if (!historyPath) {
|
|
333
|
+
console.log(chalk.yellow('\nā¹ No push history found for this repository.\n'));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let history = [];
|
|
338
|
+
try {
|
|
339
|
+
const data = await fs.readFile(historyPath, 'utf8');
|
|
340
|
+
history = JSON.parse(data);
|
|
341
|
+
} catch (e) {
|
|
342
|
+
console.log(chalk.yellow('\nā¹ No push history found for this repository.\n'));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (history.length === 0) {
|
|
347
|
+
console.log(chalk.yellow('\nā¹ No push history found for this repository.\n'));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
console.log(chalk.cyan.bold('\nš Git-Easy Push History:'));
|
|
352
|
+
|
|
353
|
+
const choices = history.map((entry) => ({
|
|
354
|
+
name: `${chalk.gray((entry.displayTimestamp || entry.timestamp).padEnd(25))} | ${chalk.green((entry.branch || 'main').padEnd(10))} | ${entry.message}`,
|
|
355
|
+
value: entry
|
|
356
|
+
}));
|
|
357
|
+
|
|
358
|
+
choices.push(new inquirer.Separator());
|
|
359
|
+
choices.push({ name: 'ā Exit', value: 'exit' });
|
|
360
|
+
|
|
361
|
+
const { selectedPush } = await inquirer.prompt([
|
|
362
|
+
{
|
|
363
|
+
type: 'list',
|
|
364
|
+
name: 'selectedPush',
|
|
365
|
+
message: 'Select a push to view details/restore:',
|
|
366
|
+
choices,
|
|
367
|
+
pageSize: 15
|
|
368
|
+
}
|
|
369
|
+
]);
|
|
370
|
+
|
|
371
|
+
if (selectedPush === 'exit' || !selectedPush) return;
|
|
372
|
+
|
|
373
|
+
if (selectedPush.hash) {
|
|
374
|
+
await viewCommitDiff(selectedPush.hash);
|
|
375
|
+
|
|
376
|
+
const { action } = await inquirer.prompt([
|
|
377
|
+
{
|
|
378
|
+
type: 'list',
|
|
379
|
+
name: 'action',
|
|
380
|
+
message: 'What would like to do with this version?',
|
|
381
|
+
choices: [
|
|
382
|
+
{ name: 'š Back to list', value: 'back' },
|
|
383
|
+
{ name: 'š°ļø Restore My Files to this Version (Undo Mistakes)', value: 'restore' },
|
|
384
|
+
{ name: 'ā Exit', value: 'exit' }
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
]);
|
|
388
|
+
|
|
389
|
+
if (action === 'back') return displayHistory();
|
|
390
|
+
if (action === 'restore') {
|
|
391
|
+
await restoreToState(selectedPush.hash);
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
console.log(chalk.yellow('\nā¹ Detailed view only available for newer pushes.\n'));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
} catch (err) {
|
|
398
|
+
console.error(chalk.red('\nā An error occurred while browsing history:'));
|
|
399
|
+
console.error(chalk.gray(err.message));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Clears the push history from the repo.
|
|
405
|
+
*/
|
|
406
|
+
async function clearHistory() {
|
|
407
|
+
try {
|
|
408
|
+
const historyPath = await getHistoryPath();
|
|
409
|
+
if (!historyPath) return;
|
|
410
|
+
|
|
411
|
+
const { confirm } = await inquirer.prompt([
|
|
412
|
+
{
|
|
413
|
+
type: 'confirm',
|
|
414
|
+
name: 'confirm',
|
|
415
|
+
message: chalk.red('Are you sure you want to clear your local push history? (This cannot be undone)'),
|
|
416
|
+
default: false
|
|
417
|
+
}
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
if (confirm) {
|
|
421
|
+
await fs.unlink(historyPath);
|
|
422
|
+
console.log(chalk.green('\nā
Push history cleared successfully!\n'));
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.log(chalk.yellow('\nā¹ No history file found to clear.\n'));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
program
|
|
430
|
+
.name('git-easy')
|
|
431
|
+
.description('š Git-Easy: The ultimate CLI to automate your GitHub workflow.')
|
|
432
|
+
.version('2.0.0');
|
|
433
|
+
|
|
434
|
+
// Main Push Command (Default)
|
|
435
|
+
program
|
|
436
|
+
.argument('[message]', 'ā” Commits all changes and pushes them to GitHub. If no message is given, it will ask for one.')
|
|
437
|
+
.action(async (message) => {
|
|
438
|
+
try {
|
|
439
|
+
const isRepo = await git.checkIsRepo();
|
|
440
|
+
if (!isRepo) {
|
|
441
|
+
console.error(chalk.red('\nā Error: Not a git repository.\n'));
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await checkAndSetupRemote();
|
|
446
|
+
await ensureAuthenticated();
|
|
447
|
+
await ensureGitIgnore();
|
|
448
|
+
|
|
449
|
+
const status = await git.status();
|
|
450
|
+
if (status.files.length === 0) {
|
|
451
|
+
console.log(chalk.yellow('\nā¹ No changes detected. Nothing to push.\n'));
|
|
452
|
+
process.exit(0);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let commitMessage = message;
|
|
456
|
+
if (!commitMessage) {
|
|
457
|
+
const answers = await inquirer.prompt([
|
|
458
|
+
{
|
|
459
|
+
type: 'input',
|
|
460
|
+
name: 'message',
|
|
461
|
+
message: 'Enter commit message:',
|
|
462
|
+
validate: (input) => input.trim() !== '' ? true : 'Commit message cannot be empty.'
|
|
463
|
+
}
|
|
464
|
+
]);
|
|
465
|
+
commitMessage = answers.message;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const spinner = ora(chalk.blue('Processing your push...')).start();
|
|
469
|
+
|
|
470
|
+
spinner.text = chalk.blue('Staging files...');
|
|
471
|
+
await git.add('.');
|
|
472
|
+
|
|
473
|
+
spinner.text = chalk.blue('Committing changes...');
|
|
474
|
+
const commitResult = await git.commit(commitMessage);
|
|
475
|
+
const commitHash = commitResult.commit;
|
|
476
|
+
|
|
477
|
+
const currentBranch = status.current || 'main';
|
|
478
|
+
spinner.text = chalk.blue(`Pushing to remote (${currentBranch})...`);
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
await git.push('origin', currentBranch);
|
|
482
|
+
spinner.succeed(chalk.green('Code pushed successfully! š'));
|
|
483
|
+
|
|
484
|
+
await logPushEvent(commitMessage, currentBranch, commitHash);
|
|
485
|
+
|
|
486
|
+
} catch (pushErr) {
|
|
487
|
+
spinner.fail(chalk.red('Failed to push to remote.'));
|
|
488
|
+
console.error(chalk.red(`\nError details: ${pushErr.message}`));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
} catch (err) {
|
|
492
|
+
console.error(chalk.red(`\nā An unexpected error occurred: ${err.message}\n`));
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// History Command (Interactive)
|
|
498
|
+
program
|
|
499
|
+
.command('history')
|
|
500
|
+
.description('š Opens an interactive menu to browse past pushes, view detailed code changes, or RESTORE your project to any previous version.')
|
|
501
|
+
.action(displayHistory);
|
|
502
|
+
|
|
503
|
+
// List Command (Non-interactive)
|
|
504
|
+
program
|
|
505
|
+
.command('list')
|
|
506
|
+
.description('š Provides a simple, non-interactive table view of all recorded pushes for a quick overview.')
|
|
507
|
+
.action(listPushes);
|
|
508
|
+
|
|
509
|
+
// Clear Command
|
|
510
|
+
program
|
|
511
|
+
.command('clear')
|
|
512
|
+
.description('š§¹ Wipes your local Git-Easy history log. Use this if you want to start your push history tracking from scratch.')
|
|
513
|
+
.action(clearHistory);
|
|
514
|
+
|
|
515
|
+
// Undo Command
|
|
516
|
+
program
|
|
517
|
+
.command('undo')
|
|
518
|
+
.description('š The "Mistake Eraser". Instantly reverts all files in your project to the state of the last successful push. If you just pushed, it steps back to the push before that.')
|
|
519
|
+
.action(async () => {
|
|
520
|
+
try {
|
|
521
|
+
const isRepo = await git.checkIsRepo();
|
|
522
|
+
if (!isRepo) {
|
|
523
|
+
console.error(chalk.red('\nā Error: Not a git repository.\n'));
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const historyPath = await getHistoryPath();
|
|
528
|
+
if (!historyPath) {
|
|
529
|
+
console.log(chalk.yellow('\nā¹ No push history found. Nothing to undo.\n'));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
let history = [];
|
|
534
|
+
try {
|
|
535
|
+
const data = await fs.readFile(historyPath, 'utf8');
|
|
536
|
+
history = JSON.parse(data);
|
|
537
|
+
} catch (e) {
|
|
538
|
+
console.log(chalk.yellow('\nā¹ No push history found. Nothing to undo.\n'));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const latestPush = history.find(entry => entry.hash);
|
|
543
|
+
if (!latestPush) {
|
|
544
|
+
console.log(chalk.yellow('\nā¹ No tracked pushes found to undo.\n'));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const currentHash = (await git.revparse(['HEAD'])).trim();
|
|
549
|
+
let targetPush = latestPush;
|
|
550
|
+
|
|
551
|
+
if (currentHash === latestPush.hash) {
|
|
552
|
+
const previousPush = history.find((entry) => entry.hash && entry.hash !== latestPush.hash);
|
|
553
|
+
if (previousPush) {
|
|
554
|
+
targetPush = previousPush;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
console.log(chalk.cyan(`\nTargeting undo to push: "${chalk.white(targetPush.message)}" (${targetPush.displayTimestamp || targetPush.timestamp})`));
|
|
559
|
+
await restoreToState(targetPush.hash);
|
|
560
|
+
|
|
561
|
+
} catch (err) {
|
|
562
|
+
console.error(chalk.red(`\nā Undo failed: ${err.message}\n`));
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Redo Command
|
|
567
|
+
program
|
|
568
|
+
.command('redo')
|
|
569
|
+
.description('āļø The "Forward Jumper". If you ran an "undo" by accident, this command jumps your project forward to the newer state you were previously on.')
|
|
570
|
+
.action(async () => {
|
|
571
|
+
try {
|
|
572
|
+
const isRepo = await git.checkIsRepo();
|
|
573
|
+
if (!isRepo) {
|
|
574
|
+
console.error(chalk.red('\nā Error: Not a git repository.\n'));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const historyPath = await getHistoryPath();
|
|
579
|
+
if (!historyPath) {
|
|
580
|
+
console.log(chalk.yellow('\nā¹ No push history found. Nothing to redo.\n'));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
let history = [];
|
|
585
|
+
try {
|
|
586
|
+
const data = await fs.readFile(historyPath, 'utf8');
|
|
587
|
+
history = JSON.parse(data);
|
|
588
|
+
} catch (e) {
|
|
589
|
+
console.log(chalk.yellow('\nā¹ No push history found. Nothing to redo.\n'));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const currentHash = (await git.revparse(['HEAD'])).trim();
|
|
594
|
+
const currentIndex = history.findIndex(entry => entry.hash === currentHash);
|
|
595
|
+
|
|
596
|
+
if (currentIndex <= 0) {
|
|
597
|
+
console.log(chalk.yellow('\nā¹ You are already at the latest redoable state.\n'));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const targetPush = history[currentIndex - 1];
|
|
602
|
+
const pushTime = new Date(targetPush.timestamp).getTime();
|
|
603
|
+
const now = Date.now();
|
|
604
|
+
const minutesDiff = (now - pushTime) / (1000 * 60);
|
|
605
|
+
|
|
606
|
+
const extraWarning = minutesDiff > 30;
|
|
607
|
+
|
|
608
|
+
console.log(chalk.cyan(`\nTargeting redo to push: "${chalk.white(targetPush.message)}" (${targetPush.displayTimestamp || targetPush.timestamp})`));
|
|
609
|
+
await restoreToState(targetPush.hash, false, extraWarning);
|
|
610
|
+
|
|
611
|
+
} catch (err) {
|
|
612
|
+
console.error(chalk.red(`\nā Redo failed: ${err.message}\n`));
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Credits Command
|
|
617
|
+
program
|
|
618
|
+
.command('credits')
|
|
619
|
+
.description('⨠View the creators of Git-Easy.')
|
|
620
|
+
.action(() => {
|
|
621
|
+
console.log(chalk.cyan.bold('\n⨠Git-Easy Credits āØ'));
|
|
622
|
+
console.log(chalk.white('-----------------------------------'));
|
|
623
|
+
console.log(`${chalk.yellow('Principal Creator:')} ${chalk.green.bold('Zain Ali')} (for the easy work!)`);
|
|
624
|
+
console.log(`${chalk.yellow('Developed by:')} ${chalk.blue('Antigravity AI')}`);
|
|
625
|
+
console.log(chalk.white('-----------------------------------\n'));
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Custom Help Text
|
|
629
|
+
program.addHelpText('after', `
|
|
630
|
+
${chalk.yellow('Credits:')}
|
|
631
|
+
Created with ā¤ļø by ${chalk.green.bold('Zain Ali')} & Antigravity.
|
|
632
|
+
Use ${chalk.cyan('git-easy credits')} to see more!
|
|
633
|
+
`);
|
|
634
|
+
|
|
635
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "e-git-zain",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "š The ultimate CLI to automate your GitHub workflow. Simplified push, undo, redo, and history management.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"e-git": "index.js",
|
|
9
|
+
"git-easy": "index.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"git",
|
|
13
|
+
"github",
|
|
14
|
+
"cli",
|
|
15
|
+
"automation",
|
|
16
|
+
"push",
|
|
17
|
+
"undo",
|
|
18
|
+
"redo",
|
|
19
|
+
"e-git",
|
|
20
|
+
"zain-ali"
|
|
21
|
+
],
|
|
22
|
+
"author": "Zain Ali",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"chalk": "^5.3.0",
|
|
26
|
+
"commander": "^12.0.0",
|
|
27
|
+
"inquirer": "^9.2.16",
|
|
28
|
+
"ora": "^8.0.1",
|
|
29
|
+
"path": "^0.12.7",
|
|
30
|
+
"simple-git": "^3.22.0"
|
|
31
|
+
}
|
|
32
|
+
}
|