gittable 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/LICENSE +22 -0
- package/README.md +459 -0
- package/cli.js +342 -0
- package/commands/add.js +159 -0
- package/commands/blame.js +33 -0
- package/commands/branch.js +234 -0
- package/commands/checkout.js +43 -0
- package/commands/cherry-pick.js +104 -0
- package/commands/clean.js +71 -0
- package/commands/clone.js +76 -0
- package/commands/commit.js +82 -0
- package/commands/config.js +171 -0
- package/commands/diff.js +30 -0
- package/commands/fetch.js +76 -0
- package/commands/grep.js +42 -0
- package/commands/init.js +45 -0
- package/commands/log.js +38 -0
- package/commands/merge.js +69 -0
- package/commands/mv.js +40 -0
- package/commands/pull.js +74 -0
- package/commands/push.js +97 -0
- package/commands/rebase.js +134 -0
- package/commands/remote.js +236 -0
- package/commands/restore.js +76 -0
- package/commands/revert.js +63 -0
- package/commands/rm.js +57 -0
- package/commands/show.js +47 -0
- package/commands/stash.js +201 -0
- package/commands/status.js +21 -0
- package/commands/sync.js +98 -0
- package/commands/tag.js +153 -0
- package/commands/undo.js +200 -0
- package/commands/uninit.js +57 -0
- package/index.d.ts +56 -0
- package/index.js +55 -0
- package/lib/commit/build-commit.js +64 -0
- package/lib/commit/get-previous-commit.js +15 -0
- package/lib/commit/questions.js +226 -0
- package/lib/config/read-config-file.js +54 -0
- package/lib/git/exec.js +222 -0
- package/lib/ui/ascii.js +154 -0
- package/lib/ui/banner.js +80 -0
- package/lib/ui/status-display.js +90 -0
- package/lib/ui/table.js +76 -0
- package/lib/utils/email-prompt.js +62 -0
- package/lib/utils/logger.js +47 -0
- package/lib/utils/spinner.js +57 -0
- package/lib/utils/terminal-link.js +55 -0
- package/lib/versions.js +17 -0
- package/package.json +73 -0
- package/standalone.js +24 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
const clack = require('@clack/prompts');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { execGit, getStashList } = require('../lib/git/exec');
|
|
4
|
+
const { createTable } = require('../lib/ui/table');
|
|
5
|
+
const { showBanner } = require('../lib/ui/banner');
|
|
6
|
+
|
|
7
|
+
const listStashes = () => {
|
|
8
|
+
const stashes = getStashList();
|
|
9
|
+
|
|
10
|
+
if (stashes.length === 0) {
|
|
11
|
+
console.log(chalk.dim('No stashes found'));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const rows = stashes.map((stash, _index) => [stash.ref, stash.date, stash.message]);
|
|
16
|
+
|
|
17
|
+
console.log(`\n${createTable(['Ref', 'Date', 'Message'], rows)}`);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const createStash = async (message, includeUntracked = false) => {
|
|
21
|
+
const spinner = clack.spinner();
|
|
22
|
+
spinner.start('Creating stash');
|
|
23
|
+
|
|
24
|
+
const command = includeUntracked
|
|
25
|
+
? `stash push -u -m "${message || 'Stash'}"`
|
|
26
|
+
: `stash push -m "${message || 'Stash'}"`;
|
|
27
|
+
|
|
28
|
+
const result = execGit(command, { silent: true });
|
|
29
|
+
spinner.stop();
|
|
30
|
+
|
|
31
|
+
if (result.success) {
|
|
32
|
+
clack.outro(chalk.green.bold('Stash created'));
|
|
33
|
+
} else {
|
|
34
|
+
clack.cancel(chalk.red('Failed to create stash'));
|
|
35
|
+
console.error(result.error);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const applyStash = async (stashRef) => {
|
|
41
|
+
const stashes = getStashList();
|
|
42
|
+
|
|
43
|
+
if (!stashRef) {
|
|
44
|
+
if (stashes.length === 0) {
|
|
45
|
+
clack.cancel(chalk.yellow('No stashes available'));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const options = stashes.map((stash) => ({
|
|
50
|
+
value: stash.ref,
|
|
51
|
+
label: `${stash.ref} - ${stash.message}`,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
stashRef = await clack.select({
|
|
55
|
+
message: chalk.cyan('Select stash to apply:'),
|
|
56
|
+
options,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (clack.isCancel(stashRef)) {
|
|
60
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const spinner = clack.spinner();
|
|
66
|
+
spinner.start(`Applying stash ${stashRef}`);
|
|
67
|
+
|
|
68
|
+
const result = execGit(`stash apply ${stashRef}`, { silent: false });
|
|
69
|
+
spinner.stop();
|
|
70
|
+
|
|
71
|
+
if (result.success) {
|
|
72
|
+
clack.outro(chalk.green.bold('Stash applied'));
|
|
73
|
+
} else {
|
|
74
|
+
clack.cancel(chalk.red('Failed to apply stash'));
|
|
75
|
+
console.error(result.error);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const popStash = async (stashRef) => {
|
|
81
|
+
const stashes = getStashList();
|
|
82
|
+
|
|
83
|
+
if (!stashRef) {
|
|
84
|
+
if (stashes.length === 0) {
|
|
85
|
+
clack.cancel(chalk.yellow('No stashes available'));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
stashRef = stashes[0].ref; // Default to most recent
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const spinner = clack.spinner();
|
|
93
|
+
spinner.start(`Popping stash ${stashRef}`);
|
|
94
|
+
|
|
95
|
+
const result = execGit(`stash pop ${stashRef}`, { silent: false });
|
|
96
|
+
spinner.stop();
|
|
97
|
+
|
|
98
|
+
if (result.success) {
|
|
99
|
+
clack.outro(chalk.green.bold('Stash popped'));
|
|
100
|
+
} else {
|
|
101
|
+
clack.cancel(chalk.red('Failed to pop stash'));
|
|
102
|
+
console.error(result.error);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const dropStash = async (stashRef) => {
|
|
108
|
+
const stashes = getStashList();
|
|
109
|
+
|
|
110
|
+
if (!stashRef) {
|
|
111
|
+
if (stashes.length === 0) {
|
|
112
|
+
clack.cancel(chalk.yellow('No stashes available'));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const options = stashes.map((stash) => ({
|
|
117
|
+
value: stash.ref,
|
|
118
|
+
label: `${stash.ref} - ${stash.message}`,
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
stashRef = await clack.select({
|
|
122
|
+
message: chalk.cyan('Select stash to drop:'),
|
|
123
|
+
options,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (clack.isCancel(stashRef)) {
|
|
127
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const confirm = await clack.confirm({
|
|
133
|
+
message: chalk.yellow(`Delete stash ${stashRef}?`),
|
|
134
|
+
initialValue: false,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (clack.isCancel(confirm) || !confirm) {
|
|
138
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const spinner = clack.spinner();
|
|
143
|
+
spinner.start(`Dropping stash ${stashRef}`);
|
|
144
|
+
|
|
145
|
+
const result = execGit(`stash drop ${stashRef}`, { silent: true });
|
|
146
|
+
spinner.stop();
|
|
147
|
+
|
|
148
|
+
if (result.success) {
|
|
149
|
+
clack.outro(chalk.green.bold('Stash dropped'));
|
|
150
|
+
} else {
|
|
151
|
+
clack.cancel(chalk.red('Failed to drop stash'));
|
|
152
|
+
console.error(result.error);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
module.exports = async (args) => {
|
|
158
|
+
const action = args[0] || 'list';
|
|
159
|
+
|
|
160
|
+
showBanner('STASH');
|
|
161
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Stash Management')}`);
|
|
162
|
+
|
|
163
|
+
if (action === 'list' || action === 'ls') {
|
|
164
|
+
listStashes();
|
|
165
|
+
clack.outro(chalk.green.bold('Done'));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (action === 'create' || action === 'save' || action === 'push') {
|
|
170
|
+
const message =
|
|
171
|
+
args[1] ||
|
|
172
|
+
(await clack.text({
|
|
173
|
+
message: chalk.cyan('Stash message (optional):'),
|
|
174
|
+
placeholder: 'WIP: working on feature',
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
if (!clack.isCancel(message)) {
|
|
178
|
+
const includeUntracked = args.includes('--include-untracked') || args.includes('-u');
|
|
179
|
+
await createStash(message, includeUntracked);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (action === 'apply') {
|
|
185
|
+
await applyStash(args[1]);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (action === 'pop') {
|
|
190
|
+
await popStash(args[1]);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (action === 'drop' || action === 'delete') {
|
|
195
|
+
await dropStash(args[1]);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Default: create stash
|
|
200
|
+
await createStash(action);
|
|
201
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const clack = require('@clack/prompts');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { getStatus, getCurrentBranch } = require('../lib/git/exec');
|
|
4
|
+
const { displayStatus } = require('../lib/ui/status-display');
|
|
5
|
+
const { showBanner } = require('../lib/ui/banner');
|
|
6
|
+
|
|
7
|
+
module.exports = async (_args) => {
|
|
8
|
+
showBanner('STATUS');
|
|
9
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Repository Status')}`);
|
|
10
|
+
|
|
11
|
+
const branch = getCurrentBranch();
|
|
12
|
+
const status = getStatus();
|
|
13
|
+
|
|
14
|
+
if (!status) {
|
|
15
|
+
clack.cancel(chalk.red('Failed to get repository status'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(displayStatus(status, branch));
|
|
20
|
+
clack.outro(chalk.green.bold('Status complete'));
|
|
21
|
+
};
|
package/commands/sync.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const clack = require('@clack/prompts');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { execGit, getCurrentBranch, remoteExists, getRemotes } = require('../lib/git/exec');
|
|
4
|
+
const { showBanner } = require('../lib/ui/banner');
|
|
5
|
+
const { addRemote } = require('./remote');
|
|
6
|
+
|
|
7
|
+
module.exports = async (args) => {
|
|
8
|
+
showBanner('SYNC');
|
|
9
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Synchronize Repository')}`);
|
|
10
|
+
|
|
11
|
+
const branch = getCurrentBranch();
|
|
12
|
+
let remote = args[0] || 'origin';
|
|
13
|
+
|
|
14
|
+
// Handle empty repository (no commits = no branch)
|
|
15
|
+
if (!branch || branch === 'null' || branch === 'HEAD') {
|
|
16
|
+
clack.cancel(chalk.red('No branch found'));
|
|
17
|
+
console.log(chalk.yellow('Repository has no commits yet.'));
|
|
18
|
+
console.log(chalk.gray('Make at least one commit before synchronizing.'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!remoteExists(remote)) {
|
|
23
|
+
const remotes = getRemotes();
|
|
24
|
+
if (remotes.length === 0) {
|
|
25
|
+
console.log(chalk.yellow(`Remote '${remote}' does not exist.`));
|
|
26
|
+
const shouldAdd = await clack.confirm({
|
|
27
|
+
message: chalk.cyan(`Would you like to add remote '${remote}'?`),
|
|
28
|
+
initialValue: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (clack.isCancel(shouldAdd) || !shouldAdd) {
|
|
32
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const added = await addRemote(remote, null);
|
|
37
|
+
if (!added) {
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
console.log(chalk.yellow(`Remote '${remote}' not found`));
|
|
42
|
+
console.log(chalk.dim(`Available remotes: ${remotes.join(', ')}`));
|
|
43
|
+
const shouldAdd = await clack.confirm({
|
|
44
|
+
message: chalk.cyan(`Would you like to add remote '${remote}'?`),
|
|
45
|
+
initialValue: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (clack.isCancel(shouldAdd) || !shouldAdd) {
|
|
49
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const added = await addRemote(remote, null);
|
|
54
|
+
if (!added) {
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 1: Fetch
|
|
61
|
+
let spinner = clack.spinner();
|
|
62
|
+
spinner.start('Fetching from remote');
|
|
63
|
+
let result = execGit(`fetch ${remote}`, { silent: true });
|
|
64
|
+
spinner.stop();
|
|
65
|
+
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
clack.cancel(chalk.red('Fetch failed'));
|
|
68
|
+
console.error(result.error);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 2: Rebase
|
|
73
|
+
spinner = clack.spinner();
|
|
74
|
+
spinner.start(`Rebasing onto ${remote}/${branch}`);
|
|
75
|
+
result = execGit(`rebase ${remote}/${branch}`, { silent: false });
|
|
76
|
+
spinner.stop();
|
|
77
|
+
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
clack.cancel(chalk.red('Rebase failed'));
|
|
80
|
+
console.error(result.error);
|
|
81
|
+
console.log(chalk.yellow('\nYou may need to resolve conflicts manually'));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Step 3: Push
|
|
86
|
+
spinner = clack.spinner();
|
|
87
|
+
spinner.start(`Pushing to ${remote}/${branch}`);
|
|
88
|
+
result = execGit(`push ${remote} ${branch}`, { silent: false });
|
|
89
|
+
spinner.stop();
|
|
90
|
+
|
|
91
|
+
if (result.success) {
|
|
92
|
+
clack.outro(chalk.green.bold('Synchronization completed'));
|
|
93
|
+
} else {
|
|
94
|
+
clack.cancel(chalk.red('Push failed'));
|
|
95
|
+
console.error(result.error);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
};
|
package/commands/tag.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const clack = require('@clack/prompts');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { execGit } = require('../lib/git/exec');
|
|
4
|
+
const { createTable } = require('../lib/ui/table');
|
|
5
|
+
const { showBanner } = require('../lib/ui/banner');
|
|
6
|
+
|
|
7
|
+
const listTags = () => {
|
|
8
|
+
const result = execGit('tag -l --format="%(refname:short)|%(creatordate:relative)|%(subject)"', {
|
|
9
|
+
silent: true,
|
|
10
|
+
});
|
|
11
|
+
if (!result.success || !result.output.trim()) {
|
|
12
|
+
console.log(chalk.dim('No tags found'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const lines = result.output.trim().split('\n').filter(Boolean);
|
|
17
|
+
const tags = lines.map((line) => {
|
|
18
|
+
const [name, date, ...messageParts] = line.split('|');
|
|
19
|
+
return {
|
|
20
|
+
name,
|
|
21
|
+
date,
|
|
22
|
+
message: messageParts.join('|') || '(no message)',
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const rows = tags.map((tag) => [chalk.cyan(tag.name), chalk.gray(tag.date), tag.message]);
|
|
27
|
+
|
|
28
|
+
console.log(`\n${createTable(['Tag', 'Date', 'Message'], rows)}`);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const createTag = async (name, message, commit, lightweight = false) => {
|
|
32
|
+
if (!name) {
|
|
33
|
+
name = await clack.text({
|
|
34
|
+
message: chalk.cyan('Tag name:'),
|
|
35
|
+
placeholder: 'v1.0.0',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (clack.isCancel(name)) {
|
|
39
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const annotated = !lightweight;
|
|
45
|
+
if (annotated && !message) {
|
|
46
|
+
message = await clack.text({
|
|
47
|
+
message: chalk.cyan('Tag message (optional):'),
|
|
48
|
+
placeholder: 'Release version 1.0.0',
|
|
49
|
+
required: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (clack.isCancel(message)) {
|
|
53
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const spinner = clack.spinner();
|
|
59
|
+
spinner.start(`Creating tag ${name}`);
|
|
60
|
+
|
|
61
|
+
let command = 'tag';
|
|
62
|
+
if (annotated && message) {
|
|
63
|
+
command += ` -a ${name} -m "${message}"`;
|
|
64
|
+
} else if (annotated) {
|
|
65
|
+
command += ` -a ${name}`;
|
|
66
|
+
} else {
|
|
67
|
+
command += ` ${name}`;
|
|
68
|
+
}
|
|
69
|
+
if (commit) {
|
|
70
|
+
command += ` ${commit}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = execGit(command, { silent: true });
|
|
74
|
+
spinner.stop();
|
|
75
|
+
|
|
76
|
+
if (result.success) {
|
|
77
|
+
clack.outro(chalk.green.bold(`Tag ${name} created`));
|
|
78
|
+
} else {
|
|
79
|
+
clack.cancel(chalk.red('Failed to create tag'));
|
|
80
|
+
console.error(result.error);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const deleteTag = async (name) => {
|
|
86
|
+
if (!name) {
|
|
87
|
+
name = await clack.text({
|
|
88
|
+
message: chalk.cyan('Tag name to delete:'),
|
|
89
|
+
placeholder: 'v1.0.0',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (clack.isCancel(name)) {
|
|
93
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const confirm = await clack.confirm({
|
|
99
|
+
message: chalk.yellow(`Delete tag ${name}?`),
|
|
100
|
+
initialValue: false,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (clack.isCancel(confirm) || !confirm) {
|
|
104
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const spinner = clack.spinner();
|
|
109
|
+
spinner.start(`Deleting tag ${name}`);
|
|
110
|
+
|
|
111
|
+
const result = execGit(`tag -d ${name}`, { silent: true });
|
|
112
|
+
spinner.stop();
|
|
113
|
+
|
|
114
|
+
if (result.success) {
|
|
115
|
+
clack.outro(chalk.green.bold(`Tag ${name} deleted`));
|
|
116
|
+
} else {
|
|
117
|
+
clack.cancel(chalk.red('Failed to delete tag'));
|
|
118
|
+
console.error(result.error);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
module.exports = async (args) => {
|
|
124
|
+
const action = args[0];
|
|
125
|
+
|
|
126
|
+
if (!action || action === 'list' || action === 'ls') {
|
|
127
|
+
showBanner('TAG');
|
|
128
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Tag List')}`);
|
|
129
|
+
listTags();
|
|
130
|
+
clack.outro(chalk.green.bold('Done'));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (action === 'create' || action === 'add') {
|
|
135
|
+
showBanner('TAG');
|
|
136
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Create Tag')}`);
|
|
137
|
+
const lightweight = args.includes('--lightweight');
|
|
138
|
+
await createTag(args[1], args[2], args[3], lightweight);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (action === 'delete' || action === 'del' || action === 'rm') {
|
|
143
|
+
showBanner('TAG');
|
|
144
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Delete Tag')}`);
|
|
145
|
+
await deleteTag(args[1]);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Default: create tag
|
|
150
|
+
showBanner('TAG');
|
|
151
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Create Tag')}`);
|
|
152
|
+
await createTag(action);
|
|
153
|
+
};
|
package/commands/undo.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const clack = require('@clack/prompts');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { execGit } = require('../lib/git/exec');
|
|
4
|
+
const { createTable } = require('../lib/ui/table');
|
|
5
|
+
const { showBanner } = require('../lib/ui/banner');
|
|
6
|
+
|
|
7
|
+
const getReflog = (limit = 30) => {
|
|
8
|
+
const result = execGit(`reflog --format="%gd|%ar|%gs" -n ${limit}`, { silent: true });
|
|
9
|
+
if (!result.success) return [];
|
|
10
|
+
|
|
11
|
+
return result.output
|
|
12
|
+
.trim()
|
|
13
|
+
.split('\n')
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.map((line) => {
|
|
16
|
+
const [ref, date, ...messageParts] = line.split('|');
|
|
17
|
+
return {
|
|
18
|
+
ref,
|
|
19
|
+
date,
|
|
20
|
+
message: messageParts.join('|'),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const showReflog = () => {
|
|
26
|
+
const entries = getReflog();
|
|
27
|
+
|
|
28
|
+
if (entries.length === 0) {
|
|
29
|
+
console.log(chalk.dim('No reflog entries found'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rows = entries.map((entry) => [
|
|
34
|
+
chalk.cyan(entry.ref),
|
|
35
|
+
chalk.gray(entry.date),
|
|
36
|
+
entry.message,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
console.log(`\n${createTable(['Ref', 'Date', 'Action'], rows)}`);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const resetToRef = async (ref) => {
|
|
43
|
+
const entries = getReflog();
|
|
44
|
+
|
|
45
|
+
if (!ref) {
|
|
46
|
+
if (!process.stdin.isTTY) {
|
|
47
|
+
clack.cancel(chalk.red('Interactive mode required'));
|
|
48
|
+
console.log(chalk.yellow('This command requires interactive input.'));
|
|
49
|
+
console.log(chalk.gray('Please provide a ref: gittable undo reset <ref>'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (entries.length === 0) {
|
|
54
|
+
clack.cancel(chalk.yellow('No reflog entries available'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const options = entries.slice(0, 20).map((entry) => ({
|
|
59
|
+
value: entry.ref,
|
|
60
|
+
label: `${entry.ref} - ${entry.message}`,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
ref = await clack.select({
|
|
64
|
+
message: chalk.cyan('Select commit to reset to:'),
|
|
65
|
+
options,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (clack.isCancel(ref)) {
|
|
69
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!process.stdin.isTTY) {
|
|
75
|
+
clack.cancel(chalk.red('Interactive mode required'));
|
|
76
|
+
console.log(chalk.yellow('This command requires interactive input.'));
|
|
77
|
+
console.log(chalk.gray('Please use: gittable undo reset <ref> --soft|--mixed|--hard'));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const mode = await clack.select({
|
|
82
|
+
message: chalk.cyan('Reset mode:'),
|
|
83
|
+
options: [
|
|
84
|
+
{ value: '--soft', label: 'Soft (keep changes staged)' },
|
|
85
|
+
{ value: '--mixed', label: 'Mixed (keep changes unstaged)', hint: 'default' },
|
|
86
|
+
{ value: '--hard', label: 'Hard (discard all changes)', hint: 'dangerous' },
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (clack.isCancel(mode)) {
|
|
91
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (mode === '--hard') {
|
|
96
|
+
const confirm = await clack.confirm({
|
|
97
|
+
message: chalk.red('Hard reset will discard all changes. Continue?'),
|
|
98
|
+
initialValue: false,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (clack.isCancel(confirm) || !confirm) {
|
|
102
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const spinner = clack.spinner();
|
|
108
|
+
spinner.start(`Resetting to ${ref}`);
|
|
109
|
+
|
|
110
|
+
const result = execGit(`reset ${mode} ${ref}`, { silent: false });
|
|
111
|
+
spinner.stop();
|
|
112
|
+
|
|
113
|
+
if (result.success) {
|
|
114
|
+
clack.outro(chalk.green.bold(`Reset to ${ref}`));
|
|
115
|
+
} else {
|
|
116
|
+
clack.cancel(chalk.red('Reset failed'));
|
|
117
|
+
console.error(result.error);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const undoLastCommit = async () => {
|
|
123
|
+
if (!process.stdin.isTTY) {
|
|
124
|
+
clack.cancel(chalk.red('Interactive mode required'));
|
|
125
|
+
console.log(chalk.yellow('This command requires interactive input.'));
|
|
126
|
+
console.log(chalk.gray('Please use: git reset --soft|--mixed|--hard HEAD~1'));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const mode = await clack.select({
|
|
131
|
+
message: chalk.cyan('Undo last commit:'),
|
|
132
|
+
options: [
|
|
133
|
+
{ value: '--soft', label: 'Keep changes staged' },
|
|
134
|
+
{ value: '--mixed', label: 'Keep changes unstaged', hint: 'default' },
|
|
135
|
+
{ value: '--hard', label: 'Discard all changes', hint: 'dangerous' },
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (clack.isCancel(mode)) {
|
|
140
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (mode === '--hard') {
|
|
145
|
+
const confirm = await clack.confirm({
|
|
146
|
+
message: chalk.red('This will discard all changes. Continue?'),
|
|
147
|
+
initialValue: false,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (clack.isCancel(confirm) || !confirm) {
|
|
151
|
+
clack.cancel(chalk.yellow('Cancelled'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const spinner = clack.spinner();
|
|
157
|
+
spinner.start('Undoing last commit');
|
|
158
|
+
|
|
159
|
+
const result = execGit(`reset ${mode} HEAD~1`, { silent: false });
|
|
160
|
+
spinner.stop();
|
|
161
|
+
|
|
162
|
+
if (result.success) {
|
|
163
|
+
clack.outro(chalk.green.bold('Last commit undone'));
|
|
164
|
+
} else {
|
|
165
|
+
clack.cancel(chalk.red('Failed to undo commit'));
|
|
166
|
+
console.error(result.error);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
module.exports = async (args) => {
|
|
172
|
+
const action = args[0];
|
|
173
|
+
|
|
174
|
+
if (action === 'reflog' || action === 'log') {
|
|
175
|
+
showBanner('UNDO');
|
|
176
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Reflog Browser')}`);
|
|
177
|
+
showReflog();
|
|
178
|
+
clack.outro(chalk.green.bold('Done'));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (action === 'reset') {
|
|
183
|
+
showBanner('UNDO');
|
|
184
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Reset to Commit')}`);
|
|
185
|
+
await resetToRef(args[1]);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (action === 'last' || !action) {
|
|
190
|
+
showBanner('UNDO');
|
|
191
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Undo Last Commit')}`);
|
|
192
|
+
await undoLastCommit();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Try to reset to the provided ref
|
|
197
|
+
showBanner('UNDO');
|
|
198
|
+
console.log(`${chalk.gray('├')} ${chalk.cyan.bold('Reset to Commit')}`);
|
|
199
|
+
await resetToRef(action);
|
|
200
|
+
};
|