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