commit-maker 0.2.1__py3-none-any.whl → 0.3.0__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,485 @@
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 utility that generates commit messages using AI.
2
+ import argparse
3
+ import importlib
4
+ import os
5
+ import subprocess
6
+
7
+ import requests
8
+ import rich.console
9
+
10
+ from .colored import colored
11
+ from .custom_int_prompt import CustomIntPrompt
12
+ from .cut_think_part import cut_think
13
+ from .mistral import MistralAI
14
+ from .ollama import Ollama
15
+ from .rich_custom_formatter import CustomFormatter
16
+
17
+ # Constants
18
+ mistral_api_key = os.environ.get("MISTRAL_API_KEY")
19
+ console = rich.console.Console()
20
+ prompt = CustomIntPrompt()
21
+ available_langs = ["en", "ru"]
22
+
23
+ # Argument parser
24
+ parser = argparse.ArgumentParser(
25
+ prog="commit_maker",
26
+ description="CLI utility that generates commit messages using AI. "
27
+ "Supports local models/Mistral AI API. Local models use ollama.",
28
+ formatter_class=CustomFormatter,
29
+ )
30
+
31
+ # General parameters
32
+ general_params = parser.add_argument_group("General parameters")
33
+ general_params.add_argument(
34
+ "-l",
35
+ "--local-models",
36
+ action="store_true",
37
+ default=False,
38
+ help="Use local models",
39
+ )
40
+ general_params.add_argument(
41
+ "-d",
42
+ "--dry-run",
43
+ action="store_true",
44
+ default=False,
45
+ help="Dry run: show commit message without creating commit",
46
+ )
47
+ general_params.add_argument(
48
+ "-V",
49
+ "--version",
50
+ action="version",
51
+ version=f"%(prog)s {importlib.metadata.version('commit-maker')}",
52
+ )
53
+ general_params.add_argument(
54
+ "-o",
55
+ "--timeout",
56
+ type=int,
57
+ default=None,
58
+ help="Change timeout for models. Default is None.",
59
+ )
60
+
61
+ # Generation parameters
62
+ generation_params = parser.add_argument_group("Generation parameters")
63
+ generation_params.add_argument(
64
+ "-t",
65
+ "--temperature",
66
+ default=1.0,
67
+ type=float,
68
+ help="Model temperature for message generation. "
69
+ "Range: [0.0, 1.5]. Default: 1.0",
70
+ )
71
+ generation_params.add_argument(
72
+ "-m",
73
+ "--max-symbols",
74
+ type=int,
75
+ default=200,
76
+ help="Maximum commit message length. Default: 200",
77
+ )
78
+ generation_params.add_argument(
79
+ "-M",
80
+ "--model",
81
+ type=str,
82
+ help="Model to be used by ollama",
83
+ )
84
+ generation_params.add_argument(
85
+ "-e",
86
+ "--exclude",
87
+ nargs="+",
88
+ default=[],
89
+ help="Files to exclude when generating commit message",
90
+ )
91
+ generation_params.add_argument(
92
+ "-w",
93
+ "--wish",
94
+ default=None,
95
+ type=str,
96
+ help="Custom wishes/edits for the commit message",
97
+ )
98
+ generation_params.add_argument(
99
+ "-L",
100
+ "--language",
101
+ choices=available_langs,
102
+ default="ru",
103
+ help="Language of generated commit message (en/ru)",
104
+ )
105
+
106
+
107
+ # Main function
108
+
109
+
110
+ def main() -> None:
111
+ # Parsing arguments
112
+ parsed_args = parser.parse_args()
113
+ use_local_models = parsed_args.local_models
114
+ max_symbols = parsed_args.max_symbols
115
+ model = parsed_args.model
116
+ dry_run = parsed_args.dry_run
117
+ temperature = parsed_args.temperature
118
+ excluded_files = parsed_args.exclude
119
+ wish = parsed_args.wish
120
+ timeout = parsed_args.timeout
121
+ lang = parsed_args.language
122
+
123
+ # AI prompt
124
+ prompt_for_ai = f"""You are a git commit message generator.
125
+ Generate a single commit message in
126
+ {"Russian" if lang == "ru" else "English"} that:
127
+ Clearly summarizes the purpose of the changes.
128
+ Does not exceed {max_symbols} characters.
129
+ Uses information from git status and git diff.
130
+ Takes into account user preferences: {wish}.
131
+ Output only the commit message — plain text, no markdown, no
132
+ explanations, no formatting."""
133
+
134
+ try:
135
+ if not use_local_models and not mistral_api_key:
136
+ console.print(
137
+ "MISTRAL_API_KEY not found for API usage!",
138
+ style="red",
139
+ highlight=False,
140
+ )
141
+ return
142
+
143
+ # Get git version if available
144
+ git_version = subprocess.run( # noqa
145
+ ["git", "--version"],
146
+ capture_output=True,
147
+ text=True,
148
+ encoding="utf-8",
149
+ ).stdout
150
+
151
+ # Check if .git exists
152
+ dot_git = ".git" in os.listdir("./")
153
+
154
+ # If .git exists
155
+ if dot_git:
156
+ # Get commit differences
157
+ git_status = subprocess.run(
158
+ ["git", "status"],
159
+ capture_output=True,
160
+ text=True,
161
+ encoding="utf-8",
162
+ )
163
+
164
+ new_files = subprocess.run(
165
+ ["git", "ls-files", "--others", "--exclude-standard"],
166
+ capture_output=True,
167
+ text=True,
168
+ encoding="utf-8",
169
+ )
170
+
171
+ if excluded_files:
172
+ git_diff_command = ["git", "diff", "--staged", "--", "."]
173
+ git_diff_command.extend([f":!{file}" for file in excluded_files]) # noqa
174
+ else:
175
+ git_diff_command = ["git", "diff", "--staged"]
176
+
177
+ git_diff = subprocess.run(
178
+ git_diff_command,
179
+ capture_output=True,
180
+ text=True,
181
+ encoding="utf-8",
182
+ )
183
+
184
+ if (
185
+ (not new_files.stdout)
186
+ and (not git_diff.stdout)
187
+ and (
188
+ not subprocess.run(
189
+ ["git", "diff"],
190
+ capture_output=True,
191
+ encoding="utf-8",
192
+ ).stdout
193
+ )
194
+ ): # Check for no changes
195
+ console.print(
196
+ "[red]No changes added![/red]",
197
+ highlight=False,
198
+ )
199
+ return None
200
+ if not git_diff.stdout:
201
+ if not dry_run:
202
+ if (
203
+ input(
204
+ colored("No staged changes!", "red")
205
+ + " Add all automatically using "
206
+ + colored("git add -A", "yellow")
207
+ + "? [y/N]: "
208
+ )
209
+ == "y"
210
+ ):
211
+ subprocess.run(
212
+ ["git", "add", "-A"],
213
+ )
214
+ else:
215
+ console.print(
216
+ "Add required files manually.",
217
+ style="yellow",
218
+ highlight=False,
219
+ )
220
+ return None
221
+ else:
222
+ console.print(
223
+ "[red]Nothing to commit![/red]"
224
+ " Add required files using "
225
+ "[yellow]git add <filename>[/yellow]",
226
+ highlight=False,
227
+ )
228
+ return None
229
+ git_diff = subprocess.run(
230
+ ["git", "diff", "--staged"],
231
+ capture_output=True,
232
+ text=True,
233
+ encoding="utf-8",
234
+ )
235
+ if subprocess.run(
236
+ ["git", "diff"],
237
+ capture_output=True,
238
+ encoding="utf-8",
239
+ ).stdout:
240
+ console.print(
241
+ "[red]Note: You have unstaged changes![/red]"
242
+ " To add more files, press "
243
+ "[yellow]Ctrl + C[/yellow] and run "
244
+ "[yellow]git add <filename>[/yellow]",
245
+ highlight=False,
246
+ )
247
+
248
+ if use_local_models:
249
+ # Check Ollama installation
250
+ try:
251
+ subprocess.run(
252
+ ["ollama", "--version"],
253
+ text=True,
254
+ capture_output=True,
255
+ )
256
+ except FileNotFoundError:
257
+ console.print(
258
+ "Ollama is not installed!",
259
+ style="red bold",
260
+ )
261
+ return None
262
+
263
+ # Check if Ollama is running
264
+ ollama_served = (
265
+ requests.get("http://localhost:11434").status_code == 200
266
+ )
267
+
268
+ if ollama_served:
269
+ # Get list of models from Ollama
270
+ ollama_models_json = requests.get(
271
+ "http://localhost:11434/api/tags"
272
+ ).json()
273
+ if ollama_models_json["models"]:
274
+ ollama_list_of_models = [
275
+ i["model"] for i in ollama_models_json["models"]
276
+ ]
277
+ else:
278
+ console.print(
279
+ "[yellow]Ollama model list is empty!"
280
+ "[/yellow] To install models, visit "
281
+ "https://ollama.com/models",
282
+ highlight=False,
283
+ )
284
+ return None
285
+ else:
286
+ console.print(
287
+ "[yellow]Ollama server not running\n"
288
+ "or not installed![/yellow]"
289
+ )
290
+ return None
291
+ else:
292
+ ollama_list_of_models = 0
293
+
294
+ # Handle missing Ollama
295
+ if not ollama_list_of_models and use_local_models:
296
+ console.print(
297
+ "[yellow]Ollama is not installed or model list is empty!"
298
+ "[/yellow] To install, visit "
299
+ "https://ollama.com/download",
300
+ highlight=False,
301
+ )
302
+ return None
303
+ elif not use_local_models and model:
304
+ console.print(
305
+ f"To use {model} locally, use the flag "
306
+ "[yellow]--local-models[/yellow]. For help: "
307
+ "[yellow]--help[/yellow]",
308
+ highlight=False,
309
+ )
310
+ return None
311
+ elif ollama_list_of_models and use_local_models:
312
+ if not model:
313
+ if len(ollama_list_of_models) > 1:
314
+ console.print(
315
+ "[yellow]Select a local model:[/yellow]\n"
316
+ + "\n".join(
317
+ [
318
+ f"[magenta]{i + 1}. {model}[/magenta]"
319
+ for i, model in enumerate(ollama_list_of_models) # noqa
320
+ ]
321
+ ),
322
+ highlight=False,
323
+ )
324
+ model_is_selected = False
325
+ while not model_is_selected:
326
+ model = prompt.ask(
327
+ "[yellow]Enter a number from 1 to "
328
+ f"{len(ollama_list_of_models)}[/yellow]",
329
+ )
330
+ if not (1 <= model <= len(ollama_list_of_models)):
331
+ console.print(
332
+ "[red]Enter a valid number![/red]",
333
+ highlight=False,
334
+ )
335
+ continue
336
+ model = ollama_list_of_models[model - 1]
337
+ model_is_selected = True
338
+ break
339
+ else:
340
+ model = ollama_list_of_models[0]
341
+ else:
342
+ if model not in ollama_list_of_models:
343
+ console.print(
344
+ f"[red]{model} is not an available model!"
345
+ "[/red] "
346
+ "Available models: [yellow]"
347
+ f"{', '.join(ollama_list_of_models)}[/yellow]",
348
+ highlight=False,
349
+ )
350
+ return None
351
+ if model:
352
+ console.print(
353
+ f"Selected model: [yellow]{model}[/yellow]",
354
+ highlight=False,
355
+ )
356
+ # Create AI client
357
+ if use_local_models:
358
+ client = Ollama(model=model)
359
+ else:
360
+ client = MistralAI(
361
+ api_key=mistral_api_key,
362
+ model="mistral-large-latest",
363
+ )
364
+ if not dry_run:
365
+ retry = True
366
+ while retry:
367
+ with console.status(
368
+ "[magenta bold]Generating commit message...",
369
+ spinner_style="magenta",
370
+ ):
371
+ commit_message = cut_think(
372
+ client.message(
373
+ messages=[
374
+ {
375
+ "role": "system",
376
+ "content": prompt_for_ai,
377
+ },
378
+ {
379
+ "role": "user",
380
+ "content": "Git status: "
381
+ + git_status.stdout
382
+ + "Git diff: "
383
+ + git_diff.stdout,
384
+ },
385
+ ],
386
+ temperature=temperature,
387
+ timeout=timeout,
388
+ )
389
+ )
390
+ commit_with_message_from_ai = input(
391
+ "Commit with message "
392
+ + colored(f"'{commit_message}'", "yellow")
393
+ + "? [y/N/r]: "
394
+ )
395
+ if commit_with_message_from_ai != "r":
396
+ retry = False
397
+ break
398
+ if commit_with_message_from_ai == "y":
399
+ subprocess.run(
400
+ ["git", "commit", "-m", f"{commit_message}"],
401
+ encoding="utf-8",
402
+ )
403
+ console.print(
404
+ "Commit created successfully!",
405
+ style="green bold",
406
+ highlight=False,
407
+ )
408
+ else:
409
+ with console.status(
410
+ "[magenta bold]Generating commit message...",
411
+ spinner_style="magenta",
412
+ ):
413
+ commit_message = cut_think(
414
+ client.message(
415
+ messages=[
416
+ {
417
+ "role": "system",
418
+ "content": prompt_for_ai,
419
+ },
420
+ {
421
+ "role": "user",
422
+ "content": "Git status: "
423
+ + git_status.stdout
424
+ + "Git diff: "
425
+ + git_diff.stdout,
426
+ },
427
+ ],
428
+ temperature=temperature,
429
+ timeout=timeout,
430
+ )
431
+ )
432
+ console.print(commit_message, style="yellow", highlight=False)
433
+ return None
434
+
435
+ # If .git does not exist
436
+ else:
437
+ init_git_repo = (
438
+ True
439
+ if input(
440
+ colored("Git repository not initialized!", "red")
441
+ + " Run "
442
+ + colored("git init", "yellow")
443
+ + "? [y/N]: "
444
+ )
445
+ == "y"
446
+ else False
447
+ )
448
+ if init_git_repo:
449
+ subprocess.run(
450
+ ["git", "init"],
451
+ capture_output=True,
452
+ encoding="utf-8",
453
+ )
454
+
455
+ (
456
+ (
457
+ subprocess.run(
458
+ ["git", "add", "-A"],
459
+ ),
460
+ subprocess.run(
461
+ [
462
+ "git",
463
+ "commit",
464
+ "-m",
465
+ "'Initial commit'",
466
+ ],
467
+ encoding="utf-8",
468
+ ),
469
+ )
470
+ if input(
471
+ "Make first commit with message "
472
+ + colored("'Initial commit?'", "yellow")
473
+ + " [y/N]: "
474
+ )
475
+ == "y"
476
+ else None
477
+ )
478
+ except KeyboardInterrupt:
479
+ return None
480
+ except Exception:
481
+ console.print_exception()
482
+
483
+
484
+ if __name__ == "__main__":
485
+ main()