Skip to content

Commit 0298396

Browse files
committed
Wip
1 parent ac853c5 commit 0298396

26 files changed

+2526
-795
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
.phplint-cache
99
.php_cs.cache
1010
.php-cs-fixer.cache
11-
.phpunit.result.cache
1211
build/
1312
coverage/
1413
clover.xml
@@ -22,3 +21,5 @@ node_modules/
2221
package-lock.json
2322
mutation-report.html
2423
storage/logs/laravel.log
24+
.env
25+
.ai-commit.php

.php-cs-fixer.php

+3-15
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
* This source file is subject to the MIT license that is bundled.
1111
*/
1212

13-
use Ergebnis\License;
14-
1513
$header = <<<EOF
1614
This file is part of the guanguans/ai-commit.
1715
@@ -20,24 +18,13 @@
2018
This source file is subject to the MIT license that is bundled.
2119
EOF;
2220

23-
// $license = License\Type\MIT::text(
24-
// __DIR__.'/LICENSE',
25-
// License\Range::since(
26-
// License\Year::fromString('2018'),
27-
// new DateTimeZone('Asia/Shanghai'),
28-
// ),
29-
// License\Holder::fromString('guanguans'),
30-
// License\Url::fromString('https://github.com/guanguans/ai-commit'),
31-
// );
32-
33-
// $license->header();
34-
// $license->save();
35-
3621
/** @noinspection PhpParamsInspection */
3722
$finder = PhpCsFixer\Finder::create()
3823
->in([
3924
__DIR__.'/app',
25+
__DIR__.'/bootstrap',
4026
__DIR__.'/config',
27+
__DIR__.'/resources',
4128
__DIR__.'/tests',
4229
])
4330
->exclude([
@@ -47,6 +34,7 @@
4734
'vendor/',
4835
])
4936
->append([
37+
__DIR__.'/.ai-commit.php',
5038
__DIR__.'/.php-cs-fixer.php',
5139
__DIR__.'/.phpstorm.meta.php',
5240
__DIR__.'/composer-unused.php',

.phpstorm.meta.php

+12-30
Original file line numberDiff line numberDiff line change
@@ -20,74 +20,56 @@
2020
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
2121
'db' => \Illuminate\Database\DatabaseManager::class,
2222
'view.finder' => \Illuminate\View\FileViewFinder::class,
23+
'config' => \Illuminate\Config\Repository::class,
2324
]));
2425

2526
override(\Illuminate\Container\Container::makeWith(0), map([
2627
'' => '@',
27-
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
28-
'db' => \Illuminate\Database\DatabaseManager::class,
29-
'view.finder' => \Illuminate\View\FileViewFinder::class,
28+
'config' => \Illuminate\Config\Repository::class,
3029
]));
3130

3231
override(\Illuminate\Contracts\Container\Container::get(0), map([
3332
'' => '@',
34-
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
35-
'db' => \Illuminate\Database\DatabaseManager::class,
36-
'view.finder' => \Illuminate\View\FileViewFinder::class,
33+
'config' => \Illuminate\Config\Repository::class,
3734
]));
3835

3936
override(\Illuminate\Contracts\Container\Container::make(0), map([
4037
'' => '@',
41-
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
42-
'db' => \Illuminate\Database\DatabaseManager::class,
43-
'view.finder' => \Illuminate\View\FileViewFinder::class,
38+
'config' => \Illuminate\Config\Repository::class,
4439
]));
4540

4641
override(\Illuminate\Contracts\Container\Container::makeWith(0), map([
4742
'' => '@',
48-
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
49-
'db' => \Illuminate\Database\DatabaseManager::class,
50-
'view.finder' => \Illuminate\View\FileViewFinder::class,
43+
'config' => \Illuminate\Config\Repository::class,
5144
]));
45+
5246
override(\App::get(0), map([
5347
'' => '@',
54-
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
55-
'db' => \Illuminate\Database\DatabaseManager::class,
56-
'view.finder' => \Illuminate\View\FileViewFinder::class,
48+
'config' => \Illuminate\Config\Repository::class,
5749
]));
5850

5951
override(\App::make(0), map([
6052
'' => '@',
61-
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
62-
'db' => \Illuminate\Database\DatabaseManager::class,
63-
'view.finder' => \Illuminate\View\FileViewFinder::class,
53+
'config' => \Illuminate\Config\Repository::class,
6454
]));
6555

6656
override(\App::makeWith(0), map([
6757
'' => '@',
68-
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
69-
'db' => \Illuminate\Database\DatabaseManager::class,
70-
'view.finder' => \Illuminate\View\FileViewFinder::class,
58+
'config' => \Illuminate\Config\Repository::class,
7159
]));
7260

7361
override(\app(0), map([
7462
'' => '@',
75-
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
76-
'db' => \Illuminate\Database\DatabaseManager::class,
77-
'view.finder' => \Illuminate\View\FileViewFinder::class,
63+
'config' => \Illuminate\Config\Repository::class,
7864
]));
7965
override(\resolve(0), map([
8066
'' => '@',
81-
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
82-
'db' => \Illuminate\Database\DatabaseManager::class,
83-
'view.finder' => \Illuminate\View\FileViewFinder::class,
67+
'config' => \Illuminate\Config\Repository::class,
8468
]));
8569

8670
override(\Psr\Container\ContainerInterface::get(0), map([
8771
'' => '@',
88-
'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class,
89-
'db' => \Illuminate\Database\DatabaseManager::class,
90-
'view.finder' => \Illuminate\View\FileViewFinder::class,
72+
'config' => \Illuminate\Config\Repository::class,
9173
]));
9274

9375
override(\Illuminate\Support\Arr::add(0), type(0));

app/Commands/.gitkeep

Whitespace-only changes.

app/Commands/CommitCommand.php

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the guanguans/ai-commit.
7+
*
8+
* (c) guanguans <[email protected]>
9+
*
10+
* This source file is subject to the MIT license that is bundled.
11+
*/
12+
13+
namespace App\Commands;
14+
15+
use App\Contracts\GeneratorContract;
16+
use App\Contracts\OutputAwareContract;
17+
use App\Exceptions\TaskException;
18+
use App\GeneratorManager;
19+
use Illuminate\Console\Scheduling\Schedule;
20+
use Illuminate\Support\Collection;
21+
use Illuminate\Support\Stringable;
22+
use LaravelZero\Framework\Commands\Command;
23+
use Symfony\Component\Console\Input\InputInterface;
24+
use Symfony\Component\Console\Output\OutputInterface;
25+
use Symfony\Component\Process\Process;
26+
27+
class CommitCommand extends Command
28+
{
29+
/**
30+
* @var string
31+
*/
32+
protected $signature = /** @lang text */
33+
'
34+
commit
35+
{--commit-options=* : Append options for the `git commit` command <comment>[default: "--edit"]</comment>}
36+
{--diff-options=* : Append options for the `git diff` command <comment>[default: ":!*.lock"]</comment>}
37+
{--generator=openai : Specify generator}
38+
{--num=3 : Specify number of generated messages}
39+
{--template= : Specify template of messages generated}
40+
';
41+
42+
protected $description = 'Automagically generate commit messages with AI.';
43+
44+
protected function initialize(InputInterface $input, OutputInterface $output)
45+
{
46+
}
47+
48+
public function handle()
49+
{
50+
$this->task('1. Checking environment', function () use (&$stagedDiff) {
51+
$isInsideWorkTree = Process::fromShellCommandline('git rev-parse --is-inside-work-tree')
52+
->mustRun()
53+
->getOutput();
54+
if (! \str($isInsideWorkTree)->rtrim()->is('true')) {
55+
$message = <<<'message'
56+
It looks like you are not in a git repository.
57+
Please run this command from the root of a git repository, or initialize one using `git init`.
58+
message;
59+
60+
throw new TaskException($message);
61+
}
62+
63+
$stagedDiff = (new Process($this->getDiffCommand()))->mustRun()->getOutput();
64+
if (empty($stagedDiff)) {
65+
throw new TaskException('There are no staged files to commit. Try running `git add` to stage some files.');
66+
}
67+
}, 'checking...');
68+
69+
$this->task('2. Generating commit messages', function () use (&$commitMessages, $stagedDiff) {
70+
$commitMessages = $this->getGenerator()->generate($this->getPromptOfAI($stagedDiff));
71+
if (\str($commitMessages)->isEmpty()) {
72+
throw new TaskException('No commit messages generated.');
73+
}
74+
75+
if (! is_json($commitMessages)) {
76+
throw new TaskException('The generated commit messages is an invalid JSON.');
77+
}
78+
79+
$this->line('');
80+
$this->line('');
81+
}, 'generating...');
82+
83+
$this->task('3. Choosing commit message', function () use ($commitMessages, &$commitMessage) {
84+
$commitMessages = collect(json_decode($commitMessages, true));
85+
86+
$chosenSubject = $this->choice('Please choice a commit message', $commitMessages->pluck('subject', 'id')->all());
87+
88+
$commitMessage = $commitMessages->first(function ($commitMessage) use ($chosenSubject) {
89+
return $commitMessage['subject'] === $chosenSubject;
90+
});
91+
}, 'choosing...');
92+
93+
$this->task('4. Committing message', function () use ($commitMessage) {
94+
(new Process($this->getCommitCommand($commitMessage)))
95+
->setTty(true)
96+
->setTimeout(null)
97+
->mustRun();
98+
}, 'committing...');
99+
100+
return self::SUCCESS;
101+
}
102+
103+
protected function getDiffCommand(): array
104+
{
105+
return array_merge(
106+
['git', 'diff', '--staged'],
107+
$this->option('diff-options') ?: $this->laravel->get('config')->get('ai-commit.diff-options')
108+
);
109+
}
110+
111+
protected function getGenerator(): GeneratorContract
112+
{
113+
$generator = $this->laravel->get(GeneratorManager::class)->driver(
114+
$this->option('generator') ?: $this->laravel->get('config')->get('ai-commit.generator')
115+
);
116+
117+
return tap($generator, function (GeneratorContract $generator) {
118+
$generator instanceof OutputAwareContract and $generator->setOutput($this->output);
119+
});
120+
}
121+
122+
protected function getCommitCommand(array $commitMessage): array
123+
{
124+
return collect($commitMessage)
125+
->filter(function ($val) {
126+
return $val && is_string($val);
127+
})
128+
->map(function (string $val) {
129+
return trim($val, " \t\n\r\x0B");
130+
})
131+
->pipe(function (Collection $collection): array {
132+
return array_merge(
133+
['git', 'commit', '--message', $collection->implode(str_repeat(PHP_EOL, 2))],
134+
$this->option('commit-options') ?: $this->laravel->get('config')->get('ai-commit.commit-options')
135+
);
136+
});
137+
}
138+
139+
protected function getPromptOfAI(string $stagedDiff): string
140+
{
141+
return \str($this->option('template') ?: $this->laravel->get('config')->get('ai-commit.template'))
142+
->replace(
143+
['{{diff}}', '{{num}}'],
144+
[$stagedDiff, $this->option('num') ?: $this->laravel->get('config')->get('ai-commit.num')]
145+
)
146+
->when($this->option('verbose'), function (Stringable $diff) {
147+
$this->line('');
148+
$this->line('===================================================');
149+
$this->line($diff);
150+
$this->output->write('===================================================');
151+
})
152+
->__toString();
153+
}
154+
155+
/**
156+
* Define the command's schedule.
157+
*
158+
* @return void
159+
*/
160+
public function schedule(Schedule $schedule)
161+
{
162+
// $schedule->command(static::class)->everyMinute();
163+
}
164+
}

app/Contracts/GeneratorContract.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the guanguans/ai-commit.
7+
*
8+
* (c) guanguans <[email protected]>
9+
*
10+
* This source file is subject to the MIT license that is bundled.
11+
*/
12+
13+
namespace App\Contracts;
14+
15+
interface GeneratorContract
16+
{
17+
public function generate(string $prompt): string;
18+
}

app/Contracts/OutputAwareContract.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the guanguans/ai-commit.
7+
*
8+
* (c) guanguans <[email protected]>
9+
*
10+
* This source file is subject to the MIT license that is bundled.
11+
*/
12+
13+
namespace App\Contracts;
14+
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
17+
interface OutputAwareContract
18+
{
19+
public function setOutput(OutputInterface $output): void;
20+
}

app/Contracts/ThrowableContract.php

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the guanguans/ai-commit.
7+
*
8+
* (c) guanguans <[email protected]>
9+
*
10+
* This source file is subject to the MIT license that is bundled.
11+
*/
12+
13+
namespace App\Contracts;
14+
15+
interface ThrowableContract extends \Throwable
16+
{
17+
}

0 commit comments

Comments
 (0)