Skip to content

Commit e60dd00

Browse files
committed
Enhance pre-commit hook handling and UX
Integrate Laravel Prompts with new UI enhancements, including color-coded status messages and multi-selection for untracked files. Refactor staged file management and add robust handling for empty commits, user prompts, and commit messages.
1 parent 4837f7c commit e60dd00

File tree

1 file changed

+112
-44
lines changed

1 file changed

+112
-44
lines changed

src/Commands/CommitvelCommand.php

+112-44
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,57 @@
33
namespace CodingWisely\Commitvel\Commands;
44

55
use Illuminate\Console\Command;
6+
use Laravel\Prompts\Concerns\Colors;
67
use Symfony\Component\Process\Process;
7-
88
use function Laravel\Prompts\confirm;
9+
use function Laravel\Prompts\multiSelect;
910
use function Laravel\Prompts\spin;
1011
use function Laravel\Prompts\text;
1112

1213
class CommitvelCommand extends Command
1314
{
14-
protected $signature = 'cw:commitvel';
15+
use Colors;
1516

16-
protected $description = 'Kind of pre-commit hook for running Pint, PHPPest, and managing Git operations.';
17+
protected $signature = 'cw:commitvel';
18+
protected $description = 'Like a pre-commit hook for running Pint, PHPPest, and managing Git operations written with laravel prompt';
1719

1820
public function handle(): void
1921
{
20-
if (! $this->hasChanges()) {
22+
if (!$this->hasChanges()) {
2123
$this->tellJokeAndExit();
2224
}
2325

2426
$this->ensurePintInstalled();
2527
$this->ensurePestIsInstalled();
2628
$this->runPint();
27-
2829
$this->runTests();
29-
3030
$this->stageFixedFiles();
3131
$this->commitChanges();
3232
}
3333

3434
protected function hasChanges(): bool
3535
{
3636
$changes = shell_exec('git status --porcelain');
37-
$this->info("Initial git status changes: \n".$changes);
37+
$statusMessage = "Initial git status changes: \n" . $this->formatGitStatus($changes);
3838

39-
return ! empty(trim($changes));
39+
if (preg_match('/^\?\?/', $changes)) {
40+
$this->info($this->red($statusMessage));
41+
} else {
42+
$this->info($statusMessage);
43+
}
44+
45+
return !empty(trim($changes));
46+
}
47+
48+
protected function formatGitStatus(string $status): string
49+
{
50+
$lines = array_filter(explode("\n", $status));
51+
foreach ($lines as &$line) {
52+
if (str_starts_with(trim($line), '??')) {
53+
$line = $this->bgRed($this->white('NEW')) . ' ' . ltrim($line, '?? ');
54+
}
55+
}
56+
return implode("\n", $lines);
4057
}
4158

4259
protected function tellJokeAndExit(): void
@@ -48,14 +65,14 @@ protected function tellJokeAndExit(): void
4865
'Attempting to commit air? 🌬️ Sadly, our repository isn’t well-ventilated for that! 🚪',
4966
'Commits need changes, not empty promises! 📜 Maybe the dog really did eat your code this time? 🐕',
5067
"Trying to commit nothing? That's like sending an empty gift box! 🎁📦",
51-
'No changes? Did you just try to send an imaginary friend to the repo? 🧙‍♂️🦄',
68+
'No code changes? Did you just try to send an imaginary friend to the repo? 🧙‍♂️🦄',
5269
'Looks like you’re committing to commitment issues! 💍❌',
5370
'Did you know? Even black holes have more substance than your commit! 🌌🕳️',
5471
"Trying to commit empty-handed? That's like bringing a fork to a soup-eating contest! 🍴🍲",
55-
'No code changes? Are you sure you’re not just practicing your keystrokes? 🎹⌨️',
72+
'No changes? Did you just try to send a love letter to an empty mailbox? 💌📪',
5673
"Committing nothing? ❌ That's like sending a blank postcard! ✉️📬",
5774
'You’re so good, you’re committing pure potential! 🚀✨',
58-
'Trying to commit zero? That’s like trying to toast invisible bread! 🍞🔍',
75+
'Trying to commit zero? That’s like writing a book with invisible ink! 📖✒️',
5976
'No changes to commit? You just invented the stealth commit! 🕵️‍♀️✨',
6077
'Trying to commit with zero changes? That’s like writing a book with invisible ink! 📖✒️',
6178
'No changes to commit? It’s like trying to paint with an empty brush! 🎨🖌️',
@@ -64,11 +81,11 @@ protected function tellJokeAndExit(): void
6481
'Attempting an empty commit? It’s as productive as a screen door on a submarine! 🛳️🚪',
6582
'Committing no changes? That’s like trying to tune a guitar with no strings! 🎸❌',
6683
'No changes? That’s like sending a love letter to an empty mailbox! 💌📪',
67-
"Trying to commit nothing? That's as useful as a waterproof teabag! ☕🚫",
84+
"Trying to commit nothing? Thats as useful as a waterproof teabag! ☕🚫",
6885
'No changes to commit? Even ghosts leave more trace! 👻🕵️‍♂️',
6986
'Attempting an empty commit? That’s like cooking with imaginary ingredients! 🍳🥄',
7087
'No changes in your commit? That’s like racing in a stationary car! 🚗🛑',
71-
"Committing air? That's like showing up to a concert with earplugs in! 🎤👂",
88+
"Committing air? Thats like showing up to a concert with earplugs in! 🎤👂",
7289
'Trying to commit with zero changes? That’s like playing soccer with an invisible ball! ⚽🕵️‍♀️',
7390
];
7491

@@ -87,9 +104,16 @@ protected function ensurePestIsInstalled(): void
87104
$this->ensureToolInstalled('vendor/bin/pest', 'PHP Pest is not installed. Would you like to install it?', 'composer require pestphp/pest --dev --with-all-dependencies', 'PHP Pest installed successfully.', 'composer remove phpunit/phpunit', 'Removing PHP Unit...');
88105
}
89106

90-
private function ensureToolInstalled(string $path, string $confirmMessage, string $installCommand, string $successMessage, string $preCommand = '', string $preMessage = ''): void
107+
private function ensureToolInstalled(
108+
string $path,
109+
string $confirmMessage,
110+
string $installCommand,
111+
string $successMessage,
112+
string $preCommand = '',
113+
string $preMessage = ''
114+
): void
91115
{
92-
if (! file_exists(base_path($path))) {
116+
if (!file_exists(base_path($path))) {
93117
if (confirm($confirmMessage, true)) {
94118
if ($preCommand) {
95119
$this->info($preMessage);
@@ -106,20 +130,15 @@ private function ensureToolInstalled(string $path, string $confirmMessage, strin
106130

107131
protected function runPint(): void
108132
{
109-
$outputFiles = $this->runTool('Laravel Pint', './vendor/bin/pint --dirty', '/^\s+ (\S+)/m');
133+
$outputFiles = $this->runTool('Laravel Pint', './vendor/bin/pint --dirty', '/^\s+ (\S+)/m');
110134

111135
foreach ($outputFiles as $file) {
112-
// Check file permissions
113-
if (! is_writable($file)) {
136+
if (!is_writable($file)) {
114137
$this->warn("File $file is not writable.");
115-
116138
continue;
117139
}
118140

119-
// Stage the file
120-
shell_exec('git add '.escapeshellarg($file));
121-
$stagedStatus = shell_exec('git status --porcelain '.escapeshellarg($file));
122-
$this->info("Staging status for $file: \n".$stagedStatus);
141+
$this->stageFile($file);
123142
}
124143
}
125144

@@ -128,7 +147,13 @@ protected function runTests(): void
128147
$this->runTool('Pest Tests', './vendor/bin/pest', '', 'Fail', 'Error');
129148
}
130149

131-
private function runTool(string $toolName, string $command, string $regexPattern = '', string $errorKeyword1 = 'FAIL', string $errorKeyword2 = 'ERROR'): array
150+
private function runTool(
151+
string $toolName,
152+
string $command,
153+
string $regexPattern = '',
154+
string $errorKeyword1 = 'FAIL',
155+
string $errorKeyword2 = 'ERROR'
156+
): array
132157
{
133158
$outputFiles = [];
134159
if (confirm("Would you like to run $toolName?", true)) {
@@ -158,53 +183,96 @@ function () use ($command, &$outputFiles, $regexPattern, $errorKeyword1, $errorK
158183

159184
protected function stageFixedFiles(): void
160185
{
161-
$this->info('Running git add -u to stage any fixed files.');
186+
$untrackedFiles = $this->getUntrackedFiles();
187+
188+
if (!empty($untrackedFiles)) {
189+
$this->info($this->red('Untracked files found:'));
190+
foreach ($untrackedFiles as $file) {
191+
$this->info($this->red(' ' . $file));
192+
}
193+
194+
array_unshift($untrackedFiles, 'Select All', 'Select None');
195+
$selectedFiles = multiSelect('Select files to include in the commit:', $untrackedFiles);
196+
197+
if (in_array('Select None', $selectedFiles) || empty($selectedFiles)) {
198+
$selectedFiles = [];
199+
}
200+
201+
if (in_array('Select All', $selectedFiles)) {
202+
$selectedFiles = array_diff($untrackedFiles, ['Select All', 'Select None']);
203+
} else {
204+
$selectedFiles = array_diff($selectedFiles, ['Select All', 'Select None']);
205+
}
206+
207+
foreach ($selectedFiles as $file) {
208+
$this->stageFile($file);
209+
}
210+
211+
$filesToDelete = array_diff($untrackedFiles, $selectedFiles, ['Select All', 'Select None']);
212+
if (!empty($filesToDelete) && confirm('Would you like to delete the unselected new files? You can delete it manually if you choose no.', false)) {
213+
foreach ($filesToDelete as $file) {
214+
unlink($file);
215+
$this->info("Deleted file: $file");
216+
}
217+
}
218+
}
219+
162220
shell_exec('git add -u');
221+
163222
$stagedFiles = shell_exec('git status --porcelain --untracked-files=no');
164-
$this->info("Files after git add -u: \n".$stagedFiles);
165223

166224
if (empty(trim($stagedFiles))) {
167-
$this->info('No changes staged for commit.');
225+
$this->warn('No changes staged for commit.');
226+
}
227+
}
228+
229+
protected function stageFile(string $file): void
230+
{
231+
shell_exec('git add ' . escapeshellarg($file));
232+
$stagedStatus = shell_exec('git status --porcelain ' . escapeshellarg($file));
233+
if (!empty($stagedStatus)) {
234+
$this->info("Staging status for $file: \n" . $stagedStatus);
168235
}
169236
}
170237

238+
protected function getUntrackedFiles(): array
239+
{
240+
$output = shell_exec('git ls-files --others --exclude-standard');
241+
return array_filter(explode("\n", $output));
242+
}
243+
171244
protected function commitChanges(): void
172245
{
173246
$stagedFiles = shell_exec('git status --porcelain --untracked-files=no');
174-
$this->info("Staged files for commit: \n".$stagedFiles);
175247

176248
if (empty(trim($stagedFiles))) {
177-
$this->info('No changes staged for commit.');
178-
179249
return;
180250
}
181251

182252
$commitMessage = text('Enter the commit message');
183-
if (! $commitMessage) {
253+
if (!$commitMessage) {
184254
$this->error('Commit message cannot be empty.');
185255
exit(1);
186256
}
187257

188-
shell_exec('git commit -m '.escapeshellarg($commitMessage));
258+
shell_exec('git commit -m ' . escapeshellarg($commitMessage));
189259
$this->info('Changes committed.');
190260

191261
$currentBranch = $this->getCurrentBranch();
192-
$branch = $this->ask('Pushing code to', $currentBranch);
193262

194-
while (! $branch) {
195-
$this->error('Branch name cannot be empty.');
196-
$branch = $this->ask('Pushing code to', $currentBranch);
197-
}
198263

199-
$this->info("Pushing code to branch $branch...");
264+
if (confirm("We will push this to branch [$currentBranch].", true)) {
265+
$this->info("Pushing code to branch $currentBranch...");
200266

201-
// Spin while pushing the code
202-
spin(fn () => $this->pushToBranch($branch), "Pushing code to $branch...");
267+
spin(fn() => $this->pushToBranch($currentBranch), "Pushing code to $currentBranch...");
203268

204-
$commitHash = trim(shell_exec('git log -1 --format="%H"'));
205-
$gitUserName = trim(shell_exec('git config user.name'));
206-
$this->info("Commit hash: $commitHash");
207-
$this->info("Pushed by: $gitUserName");
269+
$commitHash = trim(shell_exec('git log -1 --format="%H"'));
270+
$gitUserName = trim(shell_exec('git config user.name'));
271+
$this->info("Commit hash: $commitHash");
272+
$this->info("Pushed by: $gitUserName");
273+
} else {
274+
$this->info("Exiting. No changes will be pushed to the server, but changes have been committed locally.");
275+
}
208276
}
209277

210278
private function getCurrentBranch(): string

0 commit comments

Comments
 (0)