commit-maker 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
- commit_maker/main.py +614 -573
- {commit_maker-0.2.1.dist-info → commit_maker-0.2.2.dist-info}/METADATA +154 -119
- commit_maker-0.2.2.dist-info/RECORD +8 -0
- {commit_maker-0.2.1.dist-info → commit_maker-0.2.2.dist-info}/WHEEL +1 -1
- {commit_maker-0.2.1.dist-info → commit_maker-0.2.2.dist-info/licenses}/LICENSE +21 -21
- commit_maker-0.2.1.dist-info/RECORD +0 -8
- {commit_maker-0.2.1.dist-info → commit_maker-0.2.2.dist-info}/entry_points.txt +0 -0
- {commit_maker-0.2.1.dist-info → commit_maker-0.2.2.dist-info}/top_level.txt +0 -0
commit_maker/main.py
CHANGED
|
@@ -1,573 +1,614 @@
|
|
|
1
|
-
# CLI-утилита, которая будет создавать сообщение для коммита на основе ИИ.
|
|
2
|
-
# noqa: F841
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
5
|
-
import
|
|
6
|
-
import os
|
|
7
|
-
import subprocess
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
""
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
"
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
print(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
)
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
+ "
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
"
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1
|
+
# CLI-утилита, которая будет создавать сообщение для коммита на основе ИИ.
|
|
2
|
+
# noqa: F841
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
import rich.console
|
|
11
|
+
import rich_argparse
|
|
12
|
+
|
|
13
|
+
# Константы
|
|
14
|
+
mistral_api_key = os.environ.get("MISTRAL_API_KEY")
|
|
15
|
+
console = rich.console.Console()
|
|
16
|
+
|
|
17
|
+
# Настройка вывода --help (это не хардкод, такой способ указан в оф.
|
|
18
|
+
# документации rich_argparse)
|
|
19
|
+
rich_argparse.RichHelpFormatter.styles = {
|
|
20
|
+
"argparse.args": "cyan bold",
|
|
21
|
+
"argparse.groups": "green bold",
|
|
22
|
+
"argparse.metavar": "dark_cyan",
|
|
23
|
+
"argparse.prog": "dark_green bold",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Функции для цветного вывода
|
|
28
|
+
def bold(text: str) -> str:
|
|
29
|
+
"""Возвращает жирный текст
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
text (str): Текст
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
str: Жирный текст
|
|
36
|
+
"""
|
|
37
|
+
bold_start = "\033[1m"
|
|
38
|
+
bold_end = "\033[0m"
|
|
39
|
+
return f"{bold_start}{text}{bold_end}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def colored(
|
|
43
|
+
string: str,
|
|
44
|
+
color: str,
|
|
45
|
+
text_bold: bool = True,
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Функция для 'окраски' строк для красивого вывода
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
string (str): Строка, которую нужно покрасить
|
|
51
|
+
color (str): Цвет покраски ['red', 'yellow', 'green', 'magenta',\
|
|
52
|
+
'blue', 'cyan', 'reset']
|
|
53
|
+
text_bold (bool, optional): Жирный текст или нет. Defaults to True.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
str: Покрашенная строка
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
`print(colored(string='Success!', color='green'))` # Выводит 'Success!'
|
|
60
|
+
зеленого цвета
|
|
61
|
+
"""
|
|
62
|
+
COLOR_RED = "\033[31m"
|
|
63
|
+
COLOR_GREEN = "\033[32m"
|
|
64
|
+
COLOR_YELLOW = "\033[33m"
|
|
65
|
+
COLOR_BLUE = "\033[94m"
|
|
66
|
+
COLOR_MAGENTA = "\033[95m"
|
|
67
|
+
COLOR_CYAN = "\033[96m"
|
|
68
|
+
COLOR_RESET = "\033[0m"
|
|
69
|
+
COLORS_DICT = {
|
|
70
|
+
"red": COLOR_RED,
|
|
71
|
+
"green": COLOR_GREEN,
|
|
72
|
+
"yellow": COLOR_YELLOW,
|
|
73
|
+
"blue": COLOR_BLUE,
|
|
74
|
+
"magenta": COLOR_MAGENTA,
|
|
75
|
+
"cyan": COLOR_CYAN,
|
|
76
|
+
"reset": COLOR_RESET,
|
|
77
|
+
}
|
|
78
|
+
return (
|
|
79
|
+
bold(f"{COLORS_DICT[color]}{string}{COLORS_DICT['reset']}")
|
|
80
|
+
if text_bold
|
|
81
|
+
else f"{COLORS_DICT[color]}{string}{COLORS_DICT['reset']}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Парсер параметров
|
|
86
|
+
parser = argparse.ArgumentParser(
|
|
87
|
+
prog="commit_maker",
|
|
88
|
+
description="CLI-утилита, которая будет создавать сообщение "
|
|
89
|
+
"для коммита на основе ИИ. Можно использовать локальные модели/Mistral AI "
|
|
90
|
+
"API. Локальные модели используют ollama.",
|
|
91
|
+
formatter_class=rich_argparse.RichHelpFormatter,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Общие параметры
|
|
95
|
+
general_params = parser.add_argument_group("Общие параметры")
|
|
96
|
+
general_params.add_argument(
|
|
97
|
+
"-l",
|
|
98
|
+
"--local-models",
|
|
99
|
+
action="store_true",
|
|
100
|
+
default=False,
|
|
101
|
+
help="Запуск с использованием локальных моделей",
|
|
102
|
+
)
|
|
103
|
+
general_params.add_argument(
|
|
104
|
+
"-d",
|
|
105
|
+
"--dry-run",
|
|
106
|
+
action="store_true",
|
|
107
|
+
default=False,
|
|
108
|
+
help="Запуск с выводом сообщения на основе зайстейдженных "
|
|
109
|
+
"изменений, без создания коммита",
|
|
110
|
+
)
|
|
111
|
+
general_params.add_argument(
|
|
112
|
+
"-V",
|
|
113
|
+
"--version",
|
|
114
|
+
action="version",
|
|
115
|
+
version=f"%(prog)s {importlib.metadata.version('commit-maker')}",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Параметры генерации
|
|
119
|
+
generation_params = parser.add_argument_group("Параметры генерации")
|
|
120
|
+
generation_params.add_argument(
|
|
121
|
+
"-t",
|
|
122
|
+
"--temperature",
|
|
123
|
+
default=1.0,
|
|
124
|
+
type=float,
|
|
125
|
+
help="Температура модели при создании месседжа.\
|
|
126
|
+
Находится на отрезке [0.0, 1.5]. Defaults to 1.0",
|
|
127
|
+
)
|
|
128
|
+
generation_params.add_argument(
|
|
129
|
+
"-m",
|
|
130
|
+
"--max-symbols",
|
|
131
|
+
type=int,
|
|
132
|
+
default=200,
|
|
133
|
+
help="Длина сообщения коммита. Defaults to 200",
|
|
134
|
+
)
|
|
135
|
+
generation_params.add_argument(
|
|
136
|
+
"-M",
|
|
137
|
+
"--model",
|
|
138
|
+
type=str,
|
|
139
|
+
help="Модель, которую ollama будет использовать.",
|
|
140
|
+
)
|
|
141
|
+
generation_params.add_argument(
|
|
142
|
+
"-e",
|
|
143
|
+
"--exclude",
|
|
144
|
+
nargs="+",
|
|
145
|
+
default=[],
|
|
146
|
+
help="Файлы, которые нужно игнорировать при создании сообщения коммита",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Класс для использования API Mistral AI
|
|
151
|
+
class MistralAI:
|
|
152
|
+
"""Класс для общения с MistralAI.
|
|
153
|
+
Написан с помощью requests."""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
api_key: str,
|
|
158
|
+
model: str = "mistral-small-latest",
|
|
159
|
+
):
|
|
160
|
+
"""Инициализация класса
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
api_key (str): Апи ключ MistralAI
|
|
164
|
+
"""
|
|
165
|
+
self.url = "https://api.mistral.ai/v1/chat/completions"
|
|
166
|
+
self.api_key = api_key
|
|
167
|
+
self.headers = {
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
"Accept": "application/json",
|
|
170
|
+
"Authorization": f"Bearer {api_key}",
|
|
171
|
+
}
|
|
172
|
+
self.model = model
|
|
173
|
+
|
|
174
|
+
def message(
|
|
175
|
+
self,
|
|
176
|
+
message: str,
|
|
177
|
+
role: str = "user",
|
|
178
|
+
temperature: float = 0.7,
|
|
179
|
+
) -> str:
|
|
180
|
+
"""Функция сообщения
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
message (str): Сообщение
|
|
184
|
+
role (str, optional): Роль. Defaults to "user".
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
str: Json-ответ/Err
|
|
188
|
+
"""
|
|
189
|
+
data = {
|
|
190
|
+
"model": self.model,
|
|
191
|
+
"messages": [
|
|
192
|
+
{
|
|
193
|
+
"role": role,
|
|
194
|
+
"content": message,
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"temperature": 0.7,
|
|
198
|
+
}
|
|
199
|
+
try:
|
|
200
|
+
response = requests.post(
|
|
201
|
+
url=self.url,
|
|
202
|
+
json=data,
|
|
203
|
+
headers=self.headers,
|
|
204
|
+
timeout=60,
|
|
205
|
+
)
|
|
206
|
+
response.raise_for_status()
|
|
207
|
+
return response.json()["choices"][0]["message"]["content"]
|
|
208
|
+
|
|
209
|
+
except requests.exceptions.RequestException as e:
|
|
210
|
+
print(colored(f"Ошибка при обращении к Mistral AI: {e}", "red"))
|
|
211
|
+
except KeyError:
|
|
212
|
+
print(colored("Ошибка парсинга ответа от Mistral AI", "red"))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Класс для использования API Ollama
|
|
216
|
+
class Ollama:
|
|
217
|
+
"""Класс для общения с локальными моделями Ollama.
|
|
218
|
+
Написан с помощью requests."""
|
|
219
|
+
|
|
220
|
+
def __init__(
|
|
221
|
+
self,
|
|
222
|
+
model: str,
|
|
223
|
+
):
|
|
224
|
+
"""Инициализация класса"""
|
|
225
|
+
self.model = model
|
|
226
|
+
self.url = "http://localhost:11434/api/chat"
|
|
227
|
+
self.headers = {
|
|
228
|
+
"Content-Type": "application/json",
|
|
229
|
+
"Accept": "application/json",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
def message(
|
|
233
|
+
self,
|
|
234
|
+
message: str,
|
|
235
|
+
temperature: float = 0.7,
|
|
236
|
+
role: str = "user",
|
|
237
|
+
) -> str:
|
|
238
|
+
"""Функция сообщения
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
message (str): Сообщение
|
|
242
|
+
model (str): Модель, с которой будем общаться
|
|
243
|
+
temperature (float, optional): Температура общения. Defaults to 0.7
|
|
244
|
+
role (str, optional): Роль в сообщении.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
str: Json-ответ/Err
|
|
248
|
+
"""
|
|
249
|
+
data = {
|
|
250
|
+
"model": self.model,
|
|
251
|
+
"messages": [
|
|
252
|
+
{
|
|
253
|
+
"role": role,
|
|
254
|
+
"content": message,
|
|
255
|
+
}
|
|
256
|
+
],
|
|
257
|
+
"options": {
|
|
258
|
+
"temperature": temperature,
|
|
259
|
+
},
|
|
260
|
+
"stream": False,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
response = requests.post(
|
|
265
|
+
url=self.url,
|
|
266
|
+
json=data,
|
|
267
|
+
headers=self.headers,
|
|
268
|
+
timeout=60,
|
|
269
|
+
)
|
|
270
|
+
response.raise_for_status() # выбросит ошибку при плохом статусе
|
|
271
|
+
return response.json()["choices"][0]["message"]["content"]
|
|
272
|
+
|
|
273
|
+
except requests.exceptions.RequestException as e:
|
|
274
|
+
print(colored(f"Ошибка при обращении к Ollama: {e}", "red"))
|
|
275
|
+
except KeyError:
|
|
276
|
+
print(colored("Ошибка парсинга ответа от Ollama", "red"))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# main функция
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def main() -> None:
|
|
283
|
+
# Парсинг аргументов
|
|
284
|
+
parsed_args = parser.parse_args()
|
|
285
|
+
use_local_models = parsed_args.local_models
|
|
286
|
+
max_symbols = parsed_args.max_symbols
|
|
287
|
+
model = parsed_args.model
|
|
288
|
+
dry_run = parsed_args.dry_run
|
|
289
|
+
temperature = parsed_args.temperature
|
|
290
|
+
excluded_files = parsed_args.exclude
|
|
291
|
+
|
|
292
|
+
# Промпт для ИИ
|
|
293
|
+
prompt_for_ai = f"""Привет! Ты составитель коммитов для git.
|
|
294
|
+
Сгенерируй коммит-месседж на РУССКОМ языке, который:
|
|
295
|
+
1. Точно отражает суть изменений
|
|
296
|
+
2. Не превышает {max_symbols} символов
|
|
297
|
+
Опирайся на данные от 'git status' и 'git diff'.
|
|
298
|
+
В ответ на это сообщение тебе нужно предоставить
|
|
299
|
+
ТОЛЬКО коммит. Пиши просто обычный текст, без markdown!"""
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
if not use_local_models and not mistral_api_key:
|
|
303
|
+
print(
|
|
304
|
+
colored("Не найден MISTRAL_API_KEY для работы с API!", "red")
|
|
305
|
+
)
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
# Получаем версию git, если он есть
|
|
309
|
+
git_version = subprocess.run( # noqa
|
|
310
|
+
["git", "--version"],
|
|
311
|
+
capture_output=True,
|
|
312
|
+
text=True,
|
|
313
|
+
encoding="utf-8",
|
|
314
|
+
).stdout
|
|
315
|
+
|
|
316
|
+
if use_local_models:
|
|
317
|
+
# Получаем список моделей из Ollama, если Ollama есть
|
|
318
|
+
ollama_list_of_models = (
|
|
319
|
+
subprocess.run(
|
|
320
|
+
["ollama", "ls"],
|
|
321
|
+
capture_output=True,
|
|
322
|
+
text=True,
|
|
323
|
+
encoding="utf-8",
|
|
324
|
+
)
|
|
325
|
+
.stdout.strip()
|
|
326
|
+
.split("\n")
|
|
327
|
+
)
|
|
328
|
+
ollama_list_of_models = [
|
|
329
|
+
i.split()[0] for i in ollama_list_of_models[1:]
|
|
330
|
+
]
|
|
331
|
+
else:
|
|
332
|
+
ollama_list_of_models = 0
|
|
333
|
+
|
|
334
|
+
# Обработка отсутствия ollama
|
|
335
|
+
if not ollama_list_of_models and use_local_models:
|
|
336
|
+
print(
|
|
337
|
+
colored(
|
|
338
|
+
"Ollama не установлена или список моделей пуст!", "yellow"
|
|
339
|
+
)
|
|
340
|
+
+ " Для установки перейдите по https://ollama.com/download"
|
|
341
|
+
)
|
|
342
|
+
return None
|
|
343
|
+
elif not use_local_models and model:
|
|
344
|
+
print(
|
|
345
|
+
f"Для использования {model} локально используйте флаг "
|
|
346
|
+
+ colored("--local-models", "yellow")
|
|
347
|
+
+ ". Если нужна помощь: "
|
|
348
|
+
+ colored("--help", "yellow")
|
|
349
|
+
)
|
|
350
|
+
return None
|
|
351
|
+
elif ollama_list_of_models and use_local_models:
|
|
352
|
+
if not model:
|
|
353
|
+
if len(ollama_list_of_models) > 1:
|
|
354
|
+
print(
|
|
355
|
+
colored(
|
|
356
|
+
"Для использования локальных моделей необходимо "
|
|
357
|
+
"выбрать модель:",
|
|
358
|
+
"yellow",
|
|
359
|
+
)
|
|
360
|
+
+ "\n"
|
|
361
|
+
+ "\n".join(
|
|
362
|
+
[
|
|
363
|
+
f"{i + 1}. {colored(model, 'magenta', False,)}"
|
|
364
|
+
for i, model in enumerate(
|
|
365
|
+
ollama_list_of_models
|
|
366
|
+
)
|
|
367
|
+
]
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
model_is_selected = False
|
|
371
|
+
while not model_is_selected:
|
|
372
|
+
model = input(
|
|
373
|
+
colored(
|
|
374
|
+
"Введите число от 1 до "
|
|
375
|
+
f"{len(ollama_list_of_models)}: ",
|
|
376
|
+
"yellow",
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
model = int(model) if model.isdigit() else -1
|
|
380
|
+
if model > len(ollama_list_of_models) or model == -1:
|
|
381
|
+
continue
|
|
382
|
+
model = ollama_list_of_models[model - 1]
|
|
383
|
+
model_is_selected = True
|
|
384
|
+
break
|
|
385
|
+
else:
|
|
386
|
+
model = ollama_list_of_models[0]
|
|
387
|
+
else:
|
|
388
|
+
if model not in ollama_list_of_models:
|
|
389
|
+
print(
|
|
390
|
+
colored(
|
|
391
|
+
f"{model} не является доступной моделью! ", "red"
|
|
392
|
+
)
|
|
393
|
+
+ "Доступные модели: "
|
|
394
|
+
+ colored(
|
|
395
|
+
f"{', '.join(ollama_list_of_models)}", "yellow"
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
return None
|
|
399
|
+
if model:
|
|
400
|
+
print("Выбрана модель: " + colored(model, "yellow"))
|
|
401
|
+
|
|
402
|
+
# Проверяем, есть ли .git
|
|
403
|
+
dot_git = ".git" in os.listdir("./")
|
|
404
|
+
|
|
405
|
+
# Если есть
|
|
406
|
+
if dot_git:
|
|
407
|
+
# Получаем разницу в коммитах
|
|
408
|
+
git_status = subprocess.run(
|
|
409
|
+
["git", "status"],
|
|
410
|
+
capture_output=True,
|
|
411
|
+
text=True,
|
|
412
|
+
encoding="utf-8",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
new_files = subprocess.run(
|
|
416
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
417
|
+
capture_output=True,
|
|
418
|
+
text=True,
|
|
419
|
+
encoding="utf-8",
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if excluded_files:
|
|
423
|
+
git_diff_command = ["git", "diff", "--staged", "--", "."]
|
|
424
|
+
git_diff_command.extend(
|
|
425
|
+
[f":!{file}" for file in excluded_files]
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
git_diff_command = ["git", "diff", "--staged"]
|
|
429
|
+
|
|
430
|
+
git_diff = subprocess.run(
|
|
431
|
+
git_diff_command,
|
|
432
|
+
capture_output=True,
|
|
433
|
+
text=True,
|
|
434
|
+
encoding="utf-8",
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if (
|
|
438
|
+
(not new_files.stdout)
|
|
439
|
+
and (not git_diff.stdout)
|
|
440
|
+
and (
|
|
441
|
+
not subprocess.run(
|
|
442
|
+
["git", "diff"],
|
|
443
|
+
capture_output=True,
|
|
444
|
+
encoding="utf-8",
|
|
445
|
+
).stdout
|
|
446
|
+
)
|
|
447
|
+
): # Проверка на отсутствие каких-либо изменений
|
|
448
|
+
print(colored("Нет добавленных изменений!", "red"))
|
|
449
|
+
return None
|
|
450
|
+
if not git_diff.stdout:
|
|
451
|
+
if not dry_run:
|
|
452
|
+
if (
|
|
453
|
+
input(
|
|
454
|
+
colored("Нет застейдженных изменений!", "red")
|
|
455
|
+
+ " Добавить всё автоматически с помощью "
|
|
456
|
+
+ colored("git add -A", "yellow")
|
|
457
|
+
+ "? [y/N]: "
|
|
458
|
+
)
|
|
459
|
+
== "y"
|
|
460
|
+
):
|
|
461
|
+
subprocess.run(
|
|
462
|
+
["git", "add", "-A"],
|
|
463
|
+
)
|
|
464
|
+
else:
|
|
465
|
+
print(
|
|
466
|
+
colored(
|
|
467
|
+
"Добавьте необходимые файлы вручную.", "yellow"
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
return None
|
|
471
|
+
else:
|
|
472
|
+
print(
|
|
473
|
+
colored("Нечего коммитить!", "red")
|
|
474
|
+
+ " Добавьте необходимые файлы с помощью "
|
|
475
|
+
+ colored("git add <filename>", "yellow")
|
|
476
|
+
)
|
|
477
|
+
return None
|
|
478
|
+
git_diff = subprocess.run(
|
|
479
|
+
["git", "diff", "--staged"],
|
|
480
|
+
capture_output=True,
|
|
481
|
+
text=True,
|
|
482
|
+
encoding="utf-8",
|
|
483
|
+
)
|
|
484
|
+
if subprocess.run(
|
|
485
|
+
["git", "diff"],
|
|
486
|
+
capture_output=True,
|
|
487
|
+
encoding="utf-8",
|
|
488
|
+
).stdout:
|
|
489
|
+
print(
|
|
490
|
+
colored(
|
|
491
|
+
"Обратите внимание на то, что у Вас "
|
|
492
|
+
"есть незастейдженные изменения!",
|
|
493
|
+
"red",
|
|
494
|
+
)
|
|
495
|
+
+ " Для добавления дополнительных файлов "
|
|
496
|
+
+ colored("Ctrl + C", "yellow")
|
|
497
|
+
+ " и выполните "
|
|
498
|
+
+ colored("git add <filename>", "yellow")
|
|
499
|
+
+ "."
|
|
500
|
+
)
|
|
501
|
+
if use_local_models:
|
|
502
|
+
client = Ollama(model=model)
|
|
503
|
+
else:
|
|
504
|
+
client = MistralAI(
|
|
505
|
+
api_key=mistral_api_key,
|
|
506
|
+
model="mistral-large-latest",
|
|
507
|
+
)
|
|
508
|
+
if not dry_run:
|
|
509
|
+
retry = True
|
|
510
|
+
while retry:
|
|
511
|
+
with console.status(
|
|
512
|
+
"[magenta bold]Создание сообщения коммита...",
|
|
513
|
+
spinner_style="magenta",
|
|
514
|
+
):
|
|
515
|
+
commit_message = client.message(
|
|
516
|
+
message=prompt_for_ai
|
|
517
|
+
+ "Git status: "
|
|
518
|
+
+ git_status.stdout
|
|
519
|
+
+ "Git diff: "
|
|
520
|
+
+ git_diff.stdout,
|
|
521
|
+
temperature=temperature,
|
|
522
|
+
)
|
|
523
|
+
commit_with_message_from_ai = input(
|
|
524
|
+
"Закоммитить с сообщением "
|
|
525
|
+
+ colored(f"'{commit_message}'", "yellow")
|
|
526
|
+
+ "? [y/N/r]: "
|
|
527
|
+
)
|
|
528
|
+
if commit_with_message_from_ai != "r":
|
|
529
|
+
retry = False
|
|
530
|
+
break
|
|
531
|
+
if commit_with_message_from_ai == "y":
|
|
532
|
+
subprocess.run(
|
|
533
|
+
["git", "commit", "-m", f"{commit_message}"],
|
|
534
|
+
encoding="utf-8",
|
|
535
|
+
)
|
|
536
|
+
print(colored("Коммит успешно создан!", "green"))
|
|
537
|
+
else:
|
|
538
|
+
commit_message = client.message(
|
|
539
|
+
message=prompt_for_ai
|
|
540
|
+
+ "Git status: "
|
|
541
|
+
+ git_status.stdout
|
|
542
|
+
+ "Git diff: "
|
|
543
|
+
+ git_diff.stdout,
|
|
544
|
+
temperature=temperature,
|
|
545
|
+
)
|
|
546
|
+
print(
|
|
547
|
+
colored(
|
|
548
|
+
"Коммит-месседж успешно сгенерирован:", "green", False
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
print(
|
|
552
|
+
colored(
|
|
553
|
+
commit_message,
|
|
554
|
+
"yellow",
|
|
555
|
+
False,
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
return None
|
|
559
|
+
|
|
560
|
+
# Если нет
|
|
561
|
+
else:
|
|
562
|
+
init_git_repo = (
|
|
563
|
+
True
|
|
564
|
+
if input(
|
|
565
|
+
colored("Не инициализирован git репозиторий!", "red")
|
|
566
|
+
+ " Выполнить "
|
|
567
|
+
+ colored("git init", "yellow")
|
|
568
|
+
+ "? [y/N]: "
|
|
569
|
+
)
|
|
570
|
+
== "y"
|
|
571
|
+
else False
|
|
572
|
+
)
|
|
573
|
+
if init_git_repo:
|
|
574
|
+
subprocess.run(
|
|
575
|
+
["git", "init"],
|
|
576
|
+
capture_output=True,
|
|
577
|
+
encoding="utf-8",
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
(
|
|
581
|
+
(
|
|
582
|
+
subprocess.run(
|
|
583
|
+
["git", "add", "-A"],
|
|
584
|
+
),
|
|
585
|
+
subprocess.run(
|
|
586
|
+
[
|
|
587
|
+
"git",
|
|
588
|
+
"commit",
|
|
589
|
+
"-m",
|
|
590
|
+
"'Initial commit'",
|
|
591
|
+
],
|
|
592
|
+
encoding="utf-8",
|
|
593
|
+
),
|
|
594
|
+
)
|
|
595
|
+
if input(
|
|
596
|
+
"Сделать первый коммит с сообщением "
|
|
597
|
+
+ colored("'Initial commit?'", "yellow")
|
|
598
|
+
+ " [y/N]: "
|
|
599
|
+
)
|
|
600
|
+
== "y"
|
|
601
|
+
else None
|
|
602
|
+
)
|
|
603
|
+
except FileNotFoundError as e:
|
|
604
|
+
print(
|
|
605
|
+
colored("Ollama не установлена!", "red")
|
|
606
|
+
if "ollama" in str(e)
|
|
607
|
+
else colored(str(e), "red")
|
|
608
|
+
)
|
|
609
|
+
except Exception as e:
|
|
610
|
+
print(colored("Ошибка:", "red") + " " + str(e))
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
if __name__ == "__main__":
|
|
614
|
+
main()
|