gitsquash 0.0.1 โ†’ 0.1.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/bin/gitsquash.js CHANGED
@@ -1,3 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- require('../src');
3
+ import { program } from 'commander';
4
+ import { main } from '../src/index.js';
5
+ import chalk from 'chalk';
6
+
7
+ program
8
+ .name('gitsquash')
9
+ .description('Interactive CLI tool to squash git commits')
10
+ .version('0.0.1')
11
+ .option('-n, --number <count>', 'number of recent commits to show', '10')
12
+ .option('-m, --message <message>', 'preset commit message (skips the prompt)')
13
+ .option('--dry-run', 'show what commits would be squashed without actually squashing')
14
+ .addHelpText('after', `
15
+ Examples:
16
+ $ gitsquash # Interactive squash of last 10 commits
17
+ $ gitsquash -n 5 # Show only last 5 commits
18
+ $ gitsquash -m "feat: xyz" # Squash with preset commit message
19
+ $ gitsquash --dry-run # Preview squash operation
20
+ `);
21
+
22
+ program.parse();
23
+
24
+ const options = program.opts();
25
+
26
+ main(options).catch(error => {
27
+ console.error(chalk.red('An unexpected error occurred:'), error.message);
28
+ process.exit(1);
29
+ });
package/package.json CHANGED
@@ -1,17 +1,27 @@
1
1
  {
2
2
  "name": "gitsquash",
3
- "version": "0.0.1",
4
- "description": "Squash your git history",
3
+ "version": "0.1.0",
4
+ "description": "Interactive CLI tool to squash git commits",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "gitsquash": "bin/gitsquash.js"
8
8
  },
9
+ "type": "module",
9
10
  "files": [
10
11
  "bin",
11
12
  "src",
12
13
  "license.md",
13
14
  "package.json"
14
15
  ],
16
+ "scripts": {
17
+ "start": "node bin/gitsquash.js"
18
+ },
19
+ "dependencies": {
20
+ "chalk": "^5.3.0",
21
+ "inquirer": "^9.2.12",
22
+ "simple-git": "^3.22.0",
23
+ "commander": "^11.1.0"
24
+ },
15
25
  "author": "Anoop M D",
16
26
  "license": "MIT"
17
27
  }
package/readme.md ADDED
@@ -0,0 +1,67 @@
1
+ # gitsquash
2
+
3
+ An interactive CLI tool that makes git commit squashing simple and intuitive. Select multiple commits using an interactive interface, provide a new commit message, and squash them into a single commit.
4
+
5
+ ## Features
6
+
7
+ - ๐Ÿ” Interactive commit selection with checkboxes
8
+ - ๐Ÿ“ Preview commit details (hash, date, message)
9
+ - โšก๏ธ Simple keyboard-based navigation
10
+ - ๐Ÿ”„ Automatic stashing of uncommitted changes
11
+ - ๐Ÿš€ Dry-run mode to preview changes
12
+ - ๐Ÿ’ฌ Optional preset commit messages
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ # Install globally
18
+ npm install -g gitsquash
19
+
20
+ # Or run directly with npx
21
+ npx gitsquash
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ # Basic interactive mode - shows last 10 commits
28
+ gitsquash
29
+
30
+ # Show only last 5 commits
31
+ gitsquash -n 5
32
+
33
+ # Squash with a preset commit message (skips the message prompt)
34
+ gitsquash -m "feat: combine recent changes"
35
+
36
+ # Preview what would happen without making changes
37
+ gitsquash --dry-run
38
+ ```
39
+
40
+ ## Options
41
+
42
+ | Option | Description |
43
+ |--------|-------------|
44
+ | `-n, --number <count>` | Number of recent commits to show (default: 10) |
45
+ | `-m, --message <message>` | Preset commit message (skips the message prompt) |
46
+ | `--dry-run` | Preview squash operation without making changes |
47
+ | `--help` | Display help information |
48
+ | `--version` | Display version number |
49
+
50
+ ## How It Works
51
+
52
+ 1. Shows you a list of recent commits
53
+ 2. Use space bar to select commits you want to squash
54
+ 3. Press enter to confirm selection
55
+ 4. Enter a new commit message (or use preset with `-m`)
56
+ 5. The selected commits will be squashed into a single commit
57
+
58
+ ## Notes
59
+
60
+ - Requires git to be installed and available in PATH
61
+ - Works on any git repository
62
+ - Automatically handles uncommitted changes by stashing them
63
+ - Minimum of 2 commits required for squashing
64
+
65
+ ## License
66
+
67
+ MIT
package/src/index.js CHANGED
@@ -1 +1,270 @@
1
- console.log('Hello World');
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import simpleGit from 'simple-git';
4
+
5
+ const git = simpleGit();
6
+
7
+ async function getRecentCommits(options) {
8
+ try {
9
+ const maxCount = parseInt(options.number) || 10;
10
+ const log = await git.log({ maxCount });
11
+ return log.all;
12
+ } catch (error) {
13
+ console.error(chalk.red('Error fetching git commits:'), error.message);
14
+ process.exit(1);
15
+ }
16
+ }
17
+
18
+ async function selectCommits(commits) {
19
+ const choices = commits.map(commit => ({
20
+ name: `${chalk.yellow(commit.hash.slice(0, 7))} - ${chalk.blue(commit.date)} - ${commit.message}`,
21
+ value: commit.hash,
22
+ short: commit.hash.slice(0, 7)
23
+ }));
24
+
25
+ const { selectedCommits } = await inquirer.prompt([
26
+ {
27
+ type: 'checkbox',
28
+ name: 'selectedCommits',
29
+ message: 'Select commits to squash (space to select, enter to confirm):',
30
+ choices,
31
+ pageSize: 10,
32
+ validate: input => {
33
+ if (input.length < 2) return 'Please select at least 2 commits to squash';
34
+ return true;
35
+ }
36
+ }
37
+ ]);
38
+
39
+ return selectedCommits;
40
+ }
41
+
42
+ async function getNewCommitMessage(presetMessage) {
43
+ if (presetMessage) {
44
+ return presetMessage;
45
+ }
46
+
47
+ const { message } = await inquirer.prompt([
48
+ {
49
+ type: 'input',
50
+ name: 'message',
51
+ message: 'Enter the new commit message:',
52
+ validate: input => {
53
+ if (!input.trim()) return 'Commit message cannot be empty';
54
+ return true;
55
+ }
56
+ }
57
+ ]);
58
+
59
+ return message;
60
+ }
61
+
62
+ async function squashLatestCommits(commits, message) {
63
+ const oldestCommit = commits[commits.length - 1];
64
+
65
+ // Reset to the commit before the oldest commit we want to squash
66
+ await git.reset(['--soft', `${oldestCommit}~1`]);
67
+ // Create a new commit with all the changes
68
+ await git.commit(message);
69
+ }
70
+
71
+ async function squashNonConsecutiveCommits(commits, message) {
72
+ // Get the current branch name
73
+ const branchData = await git.branch();
74
+ const currentBranch = branchData.current;
75
+
76
+ // Create a temporary branch
77
+ const tempBranch = `temp-squash-${Date.now()}`;
78
+
79
+ // Get all commits after our newest selected commit
80
+ console.log(chalk.yellow('\n๐Ÿ” Phase 1: Analyzing commits...'));
81
+ const newestSelectedCommit = commits[0];
82
+ const log = await git.log();
83
+ const laterCommits = [];
84
+ for (const commit of log.all) {
85
+ if (commit.hash === newestSelectedCommit) {
86
+ break;
87
+ }
88
+ laterCommits.push({ hash: commit.hash, message: commit.message.split('\n')[0] });
89
+ }
90
+ console.log(chalk.dim(` Found ${laterCommits.length} newer commits to preserve`));
91
+
92
+ console.log(chalk.yellow('\n๐Ÿ“ฆ Phase 2: Preparing workspace...'));
93
+ console.log(chalk.dim(` Creating temporary branch: ${tempBranch}`));
94
+ await git.checkout(['-b', tempBranch]);
95
+
96
+ try {
97
+ console.log(chalk.yellow('\n๐Ÿ”„ Phase 3: Reconstructing history...'));
98
+ const oldestCommit = commits[commits.length - 1];
99
+ console.log(chalk.dim(` Resetting to parent of ${oldestCommit.slice(0, 7)}`));
100
+ await git.reset(['--hard', `${oldestCommit}~1`]);
101
+
102
+ // Cherry pick all commits in order
103
+ console.log(chalk.magenta('\n ๐Ÿ’ Cherry picking selected commits:'));
104
+ for (const commit of commits.reverse()) {
105
+ const shortHash = commit.slice(0, 7);
106
+ process.stdout.write(chalk.dim(` Processing ${shortHash}... `));
107
+ await git.raw(['cherry-pick', commit]);
108
+ console.log(chalk.green('โœ“'));
109
+ }
110
+
111
+ // Now squash all the commits
112
+ console.log(chalk.magenta('\n ๐Ÿ’ผ Creating squashed commit:'));
113
+ console.log(chalk.dim(` Resetting to parent of ${oldestCommit.slice(0, 7)}`));
114
+ await git.reset(['--soft', `${oldestCommit}~1`]);
115
+ process.stdout.write(chalk.dim(' Creating new commit... '));
116
+ await git.commit(message);
117
+ console.log(chalk.green('โœ“'));
118
+
119
+ // Get the new commit hash
120
+ const newCommit = await git.revparse(['HEAD']);
121
+ const shortNewCommit = newCommit.slice(0, 7);
122
+ console.log(chalk.dim(` New commit hash: ${shortNewCommit}`));
123
+
124
+ console.log(chalk.yellow('\n๐Ÿ”„ Phase 4: Applying changes to main branch...'));
125
+ console.log(chalk.dim(` Switching back to ${currentBranch}`));
126
+ await git.checkout([currentBranch]);
127
+
128
+ console.log(chalk.dim(` Resetting to parent of ${oldestCommit.slice(0, 7)}`));
129
+ await git.reset(['--hard', `${oldestCommit}~1`]);
130
+
131
+ process.stdout.write(chalk.dim(` Applying squashed commit ${shortNewCommit}... `));
132
+ await git.raw(['cherry-pick', newCommit]);
133
+ console.log(chalk.green('โœ“'));
134
+
135
+ // Now cherry pick all the later commits back on top
136
+ if (laterCommits.length > 0) {
137
+ console.log(chalk.magenta('\n ๐Ÿ”„ Restoring newer commits:'));
138
+ for (const commit of laterCommits.reverse()) {
139
+ const shortHash = commit.hash.slice(0, 7);
140
+ process.stdout.write(chalk.dim(` ${shortHash} ${commit.message}... `));
141
+ await git.raw(['cherry-pick', commit.hash]);
142
+ console.log(chalk.green('โœ“'));
143
+ }
144
+ }
145
+
146
+ // Clean up: delete temporary branch
147
+ console.log(chalk.yellow('\n๐Ÿงน Phase 5: Cleanup'));
148
+ process.stdout.write(chalk.dim(` Removing temporary branch ${tempBranch}... `));
149
+ await git.branch(['-D', tempBranch]);
150
+ console.log(chalk.green('โœ“'));
151
+
152
+ } catch (error) {
153
+ // If something goes wrong, try to cleanup
154
+ console.log(chalk.red('\nโŒ Error: Squash failed!'));
155
+ console.log(chalk.yellow('๐Ÿงน Cleaning up...'));
156
+ console.log(chalk.dim(` Switching back to ${currentBranch}`));
157
+ await git.checkout([currentBranch]);
158
+ process.stdout.write(chalk.dim(` Removing temporary branch ${tempBranch}... `));
159
+ await git.branch(['-D', tempBranch]).catch(() => {});
160
+ console.log(chalk.green('โœ“'));
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ async function areCommitsLatestAndConsecutive(commits) {
166
+ // Get recent commits - fetch at least the number of commits we're checking
167
+ const log = await git.log({ maxCount: commits.length });
168
+ const recentCommits = log.all.map(c => c.hash);
169
+
170
+ // Sort our selected commits by comparing their positions in the recent commits
171
+ const sortedSelectedCommits = [...commits].sort((a, b) => {
172
+ return recentCommits.indexOf(a) - recentCommits.indexOf(b);
173
+ });
174
+
175
+ // Check if they are the latest commits
176
+ for (let i = 0; i < commits.length; i++) {
177
+ if (sortedSelectedCommits[i] !== recentCommits[i]) {
178
+ return false;
179
+ }
180
+ }
181
+
182
+ return true;
183
+ }
184
+
185
+ async function squashCommits(commits, message, isDryRun) {
186
+ try {
187
+ const oldestCommit = commits[commits.length - 1];
188
+ const status = await git.status();
189
+
190
+ if (isDryRun) {
191
+ // Get all commits to show context - fetch more than selected for better context
192
+ const log = await git.log({ maxCount: Math.max(10, commits.length + 3) });
193
+
194
+ console.log(chalk.blue('\n๐Ÿ“‹ Dry Run - Squash Preview\n'));
195
+
196
+ // Show current state
197
+ console.log(chalk.yellow('Current Commits:'));
198
+ log.all.forEach(commit => {
199
+ const isSelected = commits.includes(commit.hash);
200
+ const prefix = isSelected ? '๐Ÿ”ท' : 'โšช๏ธ';
201
+ const hash = commit.hash.slice(0, 7);
202
+ const message = commit.message.split('\n')[0];
203
+ console.log(`${prefix} ${chalk.dim(hash)} ${isSelected ? chalk.yellow(message) : message}`);
204
+ });
205
+
206
+ // Show future state
207
+ console.log(chalk.yellow('\nAfter Squash:'));
208
+ log.all.forEach(commit => {
209
+ const hash = commit.hash.slice(0, 7);
210
+ if (commits.includes(commit.hash)) {
211
+ if (commit.hash === oldestCommit) {
212
+ // Show the new squashed commit
213
+ console.log(`๐Ÿ”ถ ${chalk.dim('NEW')} ${chalk.green(message)}`);
214
+ }
215
+ // Skip other commits that will be squashed
216
+ return;
217
+ }
218
+ // Show unaffected commits
219
+ console.log(`โšช๏ธ ${chalk.dim(hash)} ${commit.message.split('\n')[0]}`);
220
+ });
221
+
222
+ console.log(chalk.blue('\nDetails:'));
223
+ console.log(`โ€ข ${commits.length} commits will be squashed into one`);
224
+ console.log(`โ€ข New commit message: "${message}"`);
225
+ if (status.files.length > 0) {
226
+ console.log(`โ€ข ${status.files.length} uncommitted changes will be preserved`);
227
+ }
228
+ return;
229
+ }
230
+
231
+ if (status.files.length > 0) {
232
+ console.log(chalk.yellow('Warning: You have uncommitted changes. Stashing them...'));
233
+ await git.stash(['save', 'temporary stash before squash']);
234
+ }
235
+
236
+ const useSimpleApproach = await areCommitsLatestAndConsecutive(commits);
237
+
238
+ if (useSimpleApproach) {
239
+ await squashLatestCommits(commits, message);
240
+ } else {
241
+ console.log(chalk.yellow('\n๐Ÿ”„ Using advanced squash approach (non-consecutive or non-latest commits)...'));
242
+ await squashNonConsecutiveCommits(commits, message);
243
+ }
244
+
245
+ if (status.files.length > 0) {
246
+ await git.stash(['pop']);
247
+ }
248
+
249
+ console.log(chalk.green('\nโœจ Successfully squashed commits!'));
250
+ } catch (error) {
251
+ console.error(chalk.red('Error during squash:'), error.message);
252
+ process.exit(1);
253
+ }
254
+ }
255
+
256
+ export async function main(options = {}) {
257
+ console.log(chalk.blue('๐Ÿ” Fetching recent commits...'));
258
+
259
+ const commits = await getRecentCommits(options);
260
+ if (commits.length < 2) {
261
+ console.log(chalk.yellow('Not enough commits to squash. Need at least 2 commits.'));
262
+ process.exit(0);
263
+ }
264
+
265
+ const selectedCommits = await selectCommits(commits);
266
+ const newMessage = await getNewCommitMessage(options.message);
267
+
268
+ console.log(chalk.blue('\n๐Ÿ”„ Squashing commits...'));
269
+ await squashCommits(selectedCommits, newMessage, options.dryRun);
270
+ }