ngpt 2.12.0__py3-none-any.whl → 2.14.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.
@@ -0,0 +1,730 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import tempfile
5
+ import time
6
+ import subprocess
7
+ from datetime import datetime
8
+ import logging
9
+ from ..formatters import COLORS
10
+ from ...utils.log import create_gitcommsg_logger
11
+
12
+ def get_diff_content(diff_file=None):
13
+ """Get git diff content from file or git staged changes.
14
+
15
+ Args:
16
+ diff_file: Path to a diff file to use instead of git staged changes
17
+
18
+ Returns:
19
+ str: Content of the diff, or None if no diff is available
20
+ """
21
+ if diff_file:
22
+ try:
23
+ with open(diff_file, 'r') as f:
24
+ content = f.read()
25
+ return content
26
+ except Exception as e:
27
+ print(f"{COLORS['yellow']}Error reading diff file: {str(e)}{COLORS['reset']}")
28
+ return None
29
+
30
+ # No diff file specified, get staged changes from git
31
+ try:
32
+ result = subprocess.run(
33
+ ["git", "diff", "--staged"],
34
+ capture_output=True,
35
+ text=True
36
+ )
37
+
38
+ if result.returncode != 0:
39
+ raise Exception(f"Git command failed: {result.stderr}")
40
+
41
+ # Check if there are staged changes
42
+ if not result.stdout.strip():
43
+ print(f"{COLORS['yellow']}No staged changes found. Stage changes with 'git add' first.{COLORS['reset']}")
44
+ return None
45
+
46
+ return result.stdout
47
+ except Exception as e:
48
+ print(f"{COLORS['yellow']}Error getting git diff: {str(e)}{COLORS['reset']}")
49
+ return None
50
+
51
+ def split_into_chunks(content, chunk_size=200):
52
+ """Split content into chunks of specified size.
53
+
54
+ Args:
55
+ content: The content to split into chunks
56
+ chunk_size: Maximum number of lines per chunk
57
+
58
+ Returns:
59
+ list: List of content chunks
60
+ """
61
+ lines = content.splitlines()
62
+ chunks = []
63
+
64
+ for i in range(0, len(lines), chunk_size):
65
+ chunk = lines[i:i+chunk_size]
66
+ chunks.append("\n".join(chunk))
67
+
68
+ return chunks
69
+
70
+ def process_context(context):
71
+ """Process context string to extract directives and filters.
72
+
73
+ Args:
74
+ context: The context string provided with -m/--message-context
75
+
76
+ Returns:
77
+ dict: Extracted context data
78
+ """
79
+ context_data = {
80
+ "file_type_filter": None,
81
+ "commit_type": None,
82
+ "focus": None,
83
+ "exclusions": [],
84
+ "raw_context": context
85
+ }
86
+
87
+ if not context:
88
+ return context_data
89
+
90
+ # Extract commit type directive (e.g., "type:feat")
91
+ if "type:" in context:
92
+ match = re.search(r"type:(\w+)", context)
93
+ if match:
94
+ context_data["commit_type"] = match.group(1)
95
+
96
+ # Extract file type filters
97
+ file_type_keywords = ["html", "css", "javascript", "python", "js", "py", "ui", "api", "config"]
98
+ for keyword in file_type_keywords:
99
+ if keyword in context.lower():
100
+ context_data["file_type_filter"] = keyword
101
+ break
102
+
103
+ # Process focus/exclusion directives
104
+ if "focus on" in context.lower() or "only mention" in context.lower():
105
+ focus_match = re.search(r"focus(?:\s+on)?\s+(\w+)", context.lower())
106
+ if focus_match:
107
+ context_data["focus"] = focus_match.group(1)
108
+
109
+ if any(x in context.lower() for x in ["ignore", "don't include", "exclude"]):
110
+ exclusion_matches = re.findall(r"(?:ignore|don't include|exclude)\s+(\w+)", context.lower())
111
+ context_data["exclusions"] = exclusion_matches
112
+
113
+ return context_data
114
+
115
+ def create_system_prompt(context_data=None):
116
+ """Create system prompt based on context data.
117
+
118
+ Args:
119
+ context_data: The processed context data
120
+
121
+ Returns:
122
+ str: System prompt for the AI
123
+ """
124
+ base_prompt = """You are an expert Git commit message writer. Your task is to analyze the git diff and create a precise, factual commit message following the conventional commit format.
125
+
126
+ FORMAT:
127
+ type[(scope)]: <concise summary> (max 50 chars)
128
+
129
+ - [type] <specific change 1> (filename:function/method/line)
130
+ - [type] <specific change 2> (filename:function/method/line)
131
+ - [type] <additional changes...>
132
+
133
+ COMMIT TYPES:
134
+ - feat: New user-facing features
135
+ - fix: Bug fixes or error corrections
136
+ - refactor: Code restructuring (no behavior change)
137
+ - style: Formatting/whitespace changes only
138
+ - docs: Documentation only
139
+ - test: Test-related changes
140
+ - perf: Performance improvements
141
+ - build: Build system changes
142
+ - ci: CI/CD pipeline changes
143
+ - chore: Routine maintenance tasks
144
+ - revert: Reverting previous changes
145
+ - add: New files without user-facing features
146
+ - remove: Removing files/code
147
+ - update: Changes to existing functionality
148
+ - security: Security-related changes
149
+ - config: Configuration changes
150
+ - ui: User interface changes
151
+ - api: API-related changes
152
+
153
+ RULES:
154
+ 1. BE 100% FACTUAL - Mention ONLY code explicitly shown in the diff
155
+ 2. NEVER invent or assume changes not directly visible in the code
156
+ 3. EVERY bullet point MUST reference specific files/functions/lines
157
+ 4. Include ALL significant changes (do not skip any important modifications)
158
+ 5. If unsure about a change's purpose, describe WHAT changed, not WHY
159
+ 6. Keep summary line under 50 characters (mandatory)
160
+ 7. Use appropriate type tags for each change (main summary and each bullet)
161
+ 8. ONLY describe code that was actually changed
162
+ 9. Focus on technical specifics, avoid general statements
163
+ 10. Include proper technical details (method names, component identifiers, etc.)
164
+ 11. When all changes are to the same file, mention it once in the summary"""
165
+
166
+ if not context_data:
167
+ return base_prompt
168
+
169
+ # Add file type filtering instructions
170
+ if context_data.get("file_type_filter"):
171
+ file_type = context_data["file_type_filter"]
172
+ file_type_prompt = f"""
173
+
174
+ CRITICAL FILE TYPE FILTERING:
175
+ You MUST INCLUDE ONLY changes to {file_type} files or files related to {file_type}.
176
+ You MUST EXCLUDE ALL other files completely from your output.
177
+ This is a strict filter - no exceptions allowed."""
178
+ base_prompt += file_type_prompt
179
+
180
+ # Add commit type directive
181
+ if context_data.get("commit_type"):
182
+ commit_type = context_data["commit_type"]
183
+ commit_type_prompt = f"""
184
+
185
+ CRITICAL COMMIT TYPE DIRECTIVE:
186
+ You MUST use exactly "{commit_type}:" as the commit type prefix.
187
+ This takes highest priority over any other commit type you might determine.
188
+ Do not override this commit type based on your own analysis."""
189
+ base_prompt += commit_type_prompt
190
+
191
+ # Add focus/exclusion directives
192
+ if context_data.get("focus"):
193
+ focus = context_data["focus"]
194
+ focus_prompt = f"""
195
+
196
+ FOCUS DIRECTIVE:
197
+ Focus exclusively on changes related to {focus}.
198
+ Exclude everything else from your analysis."""
199
+ base_prompt += focus_prompt
200
+
201
+ if context_data.get("exclusions"):
202
+ exclusions = ", ".join(context_data["exclusions"])
203
+ exclusion_prompt = f"""
204
+
205
+ EXCLUSION DIRECTIVE:
206
+ Completely ignore and exclude any mentions of: {exclusions}."""
207
+ base_prompt += exclusion_prompt
208
+
209
+ return base_prompt
210
+
211
+ def create_chunk_prompt(chunk):
212
+ """Create prompt for processing a single diff chunk.
213
+
214
+ Args:
215
+ chunk: The diff chunk to process
216
+
217
+ Returns:
218
+ str: Prompt for the AI
219
+ """
220
+ return f"""Analyze this PARTIAL git diff and create a detailed technical summary with this EXACT format:
221
+
222
+ [FILES]: Comma-separated list of affected files with full paths
223
+
224
+ [CHANGES]:
225
+ - Technical detail 1 (include specific function/method names and line numbers)
226
+ - Technical detail 2 (be precise about exactly what code was added/modified/removed)
227
+ - Additional technical details (include ALL significant changes in this chunk)
228
+
229
+ [IMPACT]: Brief technical description of what the changes accomplish
230
+
231
+ CRITICALLY IMPORTANT: Be extremely specific with technical details.
232
+ ALWAYS identify exact function names, method names, class names, and line numbers where possible.
233
+ Use format 'filename:function_name()' or 'filename:line_number' when referencing code locations.
234
+ Be precise and factual - only describe code that actually changed.
235
+
236
+ Diff chunk:
237
+
238
+ {chunk}"""
239
+
240
+ def create_rechunk_prompt(combined_analysis, depth):
241
+ """Create prompt for re-chunking process.
242
+
243
+ Args:
244
+ combined_analysis: The combined analysis to re-chunk
245
+ depth: Current recursion depth
246
+
247
+ Returns:
248
+ str: Prompt for the AI
249
+ """
250
+ return f"""IMPORTANT: You are analyzing SUMMARIES of git changes, not raw git diff.
251
+
252
+ You are in a re-chunking process (depth: {depth}) where the input is already summarized changes.
253
+ Create a TERSE summary of these summaries focusing ONLY ON TECHNICAL CHANGES:
254
+
255
+ [CHANGES]:
256
+ - Technical change 1 (specific file and function)
257
+ - Technical change 2 (specific file and function)
258
+ - Additional relevant changes
259
+
260
+ DO NOT ask for raw git diff. These summaries are all you need to work with.
261
+ Keep your response FACTUAL and SPECIFIC to what's in the summaries.
262
+
263
+ Section to summarize:
264
+
265
+ {combined_analysis}"""
266
+
267
+ def create_combine_prompt(partial_analyses):
268
+ """Create prompt for combining partial analyses.
269
+
270
+ Args:
271
+ partial_analyses: List of partial analyses to combine
272
+
273
+ Returns:
274
+ str: Prompt for the AI
275
+ """
276
+ all_analyses = "\n\n".join(partial_analyses)
277
+
278
+ return f"""===CRITICAL INSTRUCTION===
279
+ You are working with ANALYZED SUMMARIES of git changes, NOT raw git diff.
280
+ The raw git diff has ALREADY been processed into these summaries.
281
+ DO NOT ask for or expect to see the original git diff.
282
+
283
+ TASK: Synthesize these partial analyses into a complete conventional commit message:
284
+
285
+ {all_analyses}
286
+
287
+ Create a CONVENTIONAL COMMIT MESSAGE with:
288
+ 1. First line: "type[(scope)]: brief summary" (50 chars max)
289
+ - Include scope ONLY if you are 100% confident about the affected area
290
+ - Omit scope if changes affect multiple areas or scope is unclear
291
+ 2. ⚠️ ONE BLANK LINE IS MANDATORY - NEVER SKIP THIS STEP ⚠️
292
+ - This blank line MUST be present in EVERY commit message
293
+ - The blank line separates the summary from the detailed changes
294
+ - Without this blank line, the commit message format is invalid
295
+ 3. Bullet points with specific changes, each with appropriate [type] tag
296
+ 4. Reference files in EACH bullet point with function names or line numbers
297
+
298
+ FILENAME & FUNCTION HANDLING RULES:
299
+ - Include SPECIFIC function names, method names, or line numbers when available
300
+ - Format as filename:function() or filename:line_number
301
+ - Use short relative paths for files
302
+ - Group related changes to the same file when appropriate
303
+ - Avoid breaking long filenames across lines
304
+
305
+ STRICTLY follow this format with NO EXPLANATION or additional commentary.
306
+ DO NOT mention insufficient information or ask for the original diff."""
307
+
308
+ def create_final_prompt(diff_content):
309
+ """Create prompt for direct processing without chunking.
310
+
311
+ Args:
312
+ diff_content: The full diff content
313
+
314
+ Returns:
315
+ str: Prompt for the AI
316
+ """
317
+ return f"""Analyze ONLY the exact changes in this git diff and create a precise, factual commit message.
318
+
319
+ FORMAT:
320
+ type[(scope)]: <concise summary> (max 50 chars)
321
+
322
+ - [type] <specific change 1> (filename:function/method/line)
323
+ - [type] <specific change 2> (filename:function/method/line)
324
+ - [type] <additional changes...>
325
+
326
+ RULES FOR FILENAMES:
327
+ 1. Use short relative paths when possible
328
+ 2. For multiple changes to the same file, consider grouping them
329
+ 3. Abbreviate long paths when they're repeated (e.g., 'commit.zsh' instead of full path)
330
+ 4. Avoid breaking filenames across lines
331
+ 5. Only include function names when they add clarity
332
+
333
+ COMMIT TYPES:
334
+ - feat: New user-facing features
335
+ - fix: Bug fixes or error corrections
336
+ - refactor: Code restructuring (no behavior change)
337
+ - style: Formatting/whitespace changes only
338
+ - docs: Documentation only
339
+ - test: Test-related changes
340
+ - perf: Performance improvements
341
+ - build: Build system changes
342
+ - ci: CI/CD pipeline changes
343
+ - chore: Routine maintenance tasks
344
+ - revert: Reverting previous changes
345
+ - add: New files without user-facing features
346
+ - remove: Removing files/code
347
+ - update: Changes to existing functionality
348
+ - security: Security-related changes
349
+ - config: Configuration changes
350
+ - ui: User interface changes
351
+ - api: API-related changes
352
+
353
+ RULES:
354
+ 1. BE 100% FACTUAL - Mention ONLY code explicitly shown in the diff
355
+ 2. NEVER invent or assume changes not directly visible in the code
356
+ 3. EVERY bullet point MUST reference specific files/functions/lines
357
+ 4. Include ALL significant changes (do not skip any important modifications)
358
+ 5. If unsure about a change's purpose, describe WHAT changed, not WHY
359
+ 6. Keep summary line under 50 characters (mandatory)
360
+ 7. Use appropriate type tags for each change (main summary and each bullet)
361
+ 8. ONLY describe code that was actually changed
362
+ 9. Focus on technical specifics, avoid general statements
363
+ 10. Include proper technical details (method names, component identifiers, etc.)
364
+ 11. When all changes are to the same file, mention it once in the summary
365
+
366
+ Git diff to process:
367
+
368
+ {diff_content}"""
369
+
370
+ def handle_api_call(client, prompt, system_prompt=None, logger=None, max_retries=3):
371
+ """Handle API call with retries and error handling.
372
+
373
+ Args:
374
+ client: The NGPTClient instance
375
+ prompt: The prompt to send to the API
376
+ system_prompt: Optional system prompt
377
+ logger: Optional logger instance
378
+ max_retries: Maximum number of retries on error
379
+
380
+ Returns:
381
+ str: Response from the API
382
+ """
383
+ if logger:
384
+ # Enhanced logging of full prompt and system prompt
385
+ logger.log_prompt("DEBUG", system_prompt, prompt)
386
+
387
+ retry_count = 0
388
+ wait_seconds = 5
389
+
390
+ while True:
391
+ try:
392
+ # Create messages array with system prompt if available
393
+ messages = None
394
+ if system_prompt:
395
+ messages = [
396
+ {"role": "system", "content": system_prompt},
397
+ {"role": "user", "content": prompt}
398
+ ]
399
+ else:
400
+ messages = [
401
+ {"role": "user", "content": prompt}
402
+ ]
403
+
404
+ response = client.chat(
405
+ prompt=prompt,
406
+ stream=False,
407
+ markdown_format=False,
408
+ messages=messages
409
+ )
410
+
411
+ if logger:
412
+ # Log full response
413
+ logger.log_response("DEBUG", response)
414
+
415
+ return response
416
+
417
+ except Exception as e:
418
+ retry_count += 1
419
+ error_msg = f"Error (attempt {retry_count}/{max_retries}): {str(e)}"
420
+
421
+ if logger:
422
+ logger.error(error_msg)
423
+
424
+ if retry_count >= max_retries:
425
+ raise Exception(f"Failed after {max_retries} retries: {str(e)}")
426
+
427
+ print(f"{COLORS['yellow']}{error_msg}{COLORS['reset']}")
428
+ print(f"{COLORS['yellow']}Retrying in {wait_seconds} seconds...{COLORS['reset']}")
429
+
430
+ # Create a spinner effect for waiting
431
+ spinner = "⣾⣽⣻⢿⡿⣟⣯⣷"
432
+ for _ in range(wait_seconds * 5):
433
+ for char in spinner:
434
+ sys.stdout.write(f"\r{COLORS['yellow']}Waiting... {char}{COLORS['reset']}")
435
+ sys.stdout.flush()
436
+ time.sleep(0.2)
437
+
438
+ print("\r" + " " * 20 + "\r", end="")
439
+
440
+ # Exponential backoff
441
+ wait_seconds *= 2
442
+
443
+ def process_with_chunking(client, diff_content, context_data, chunk_size=200, recursive=False, max_depth=3, logger=None):
444
+ """Process diff with chunking to handle large diffs.
445
+
446
+ Args:
447
+ client: The NGPTClient instance
448
+ diff_content: The diff content to process
449
+ context_data: The processed context data
450
+ chunk_size: Maximum number of lines per chunk
451
+ recursive: Whether to use recursive chunking
452
+ max_depth: Maximum recursion depth
453
+ logger: Optional logger instance
454
+
455
+ Returns:
456
+ str: Generated commit message
457
+ """
458
+ # Create system prompt
459
+ system_prompt = create_system_prompt(context_data)
460
+
461
+ # Log initial diff content
462
+ if logger:
463
+ logger.log_diff("DEBUG", diff_content)
464
+
465
+ # Split diff into chunks
466
+ chunks = split_into_chunks(diff_content, chunk_size)
467
+ chunk_count = len(chunks)
468
+
469
+ if logger:
470
+ logger.info(f"Processing {chunk_count} chunks of {chunk_size} lines each")
471
+
472
+ print(f"{COLORS['green']}Processing diff in {chunk_count} chunks...{COLORS['reset']}")
473
+
474
+ # Process each chunk
475
+ partial_analyses = []
476
+ for i, chunk in enumerate(chunks):
477
+ print(f"\n{COLORS['cyan']}[Chunk {i+1}/{chunk_count}]{COLORS['reset']}")
478
+
479
+ # Log chunk content
480
+ if logger:
481
+ logger.log_chunks("DEBUG", i+1, chunk_count, chunk)
482
+
483
+ # Create chunk prompt
484
+ chunk_prompt = create_chunk_prompt(chunk)
485
+
486
+ # Log chunk template
487
+ if logger:
488
+ logger.log_template("DEBUG", "CHUNK", chunk_prompt)
489
+
490
+ # Process chunk
491
+ print(f"{COLORS['yellow']}Analyzing changes...{COLORS['reset']}")
492
+ try:
493
+ result = handle_api_call(client, chunk_prompt, system_prompt, logger)
494
+ partial_analyses.append(result)
495
+ print(f"{COLORS['green']}✓ Chunk {i+1} processed{COLORS['reset']}")
496
+ except Exception as e:
497
+ print(f"{COLORS['red']}Error processing chunk {i+1}: {str(e)}{COLORS['reset']}")
498
+ if logger:
499
+ logger.error(f"Error processing chunk {i+1}: {str(e)}")
500
+ return None
501
+
502
+ # Rate limit protection between chunks
503
+ if i < chunk_count - 1:
504
+ print(f"{COLORS['yellow']}Waiting to avoid rate limits...{COLORS['reset']}")
505
+ time.sleep(5)
506
+
507
+ # Combine partial analyses
508
+ print(f"\n{COLORS['cyan']}Combining analyses from {len(partial_analyses)} chunks...{COLORS['reset']}")
509
+
510
+ # Log partial analyses
511
+ if logger:
512
+ combined_analyses = "\n\n".join(partial_analyses)
513
+ logger.log_content("DEBUG", "PARTIAL_ANALYSES", combined_analyses)
514
+
515
+ # Check if we need to use recursive chunking
516
+ combined_analyses = "\n\n".join(partial_analyses)
517
+ combined_line_count = len(combined_analyses.splitlines())
518
+
519
+ if recursive and combined_line_count > 50 and max_depth > 0:
520
+ # Use recursive chunking
521
+ return recursive_process(client, combined_analyses, context_data, max_depth, logger)
522
+ else:
523
+ # Use direct combination
524
+ combine_prompt = create_combine_prompt(partial_analyses)
525
+
526
+ # Log combine template
527
+ if logger:
528
+ logger.log_template("DEBUG", "COMBINE", combine_prompt)
529
+
530
+ try:
531
+ result = handle_api_call(client, combine_prompt, system_prompt, logger)
532
+ return result
533
+ except Exception as e:
534
+ print(f"{COLORS['red']}Error combining analyses: {str(e)}{COLORS['reset']}")
535
+ if logger:
536
+ logger.error(f"Error combining analyses: {str(e)}")
537
+ return None
538
+
539
+ def recursive_process(client, combined_analysis, context_data, max_depth, logger=None, current_depth=1):
540
+ """Process large analysis results recursively.
541
+
542
+ Args:
543
+ client: The NGPTClient instance
544
+ combined_analysis: The combined analysis to process
545
+ context_data: The processed context data
546
+ max_depth: Maximum recursion depth
547
+ logger: Optional logger instance
548
+ current_depth: Current recursion depth
549
+
550
+ Returns:
551
+ str: Generated commit message
552
+ """
553
+ system_prompt = create_system_prompt(context_data)
554
+
555
+ print(f"\n{COLORS['cyan']}Recursive chunking level {current_depth}/{max_depth}...{COLORS['reset']}")
556
+
557
+ if logger:
558
+ logger.info(f"Starting recursive chunking at depth {current_depth}/{max_depth}")
559
+ logger.debug(f"Combined analysis size: {len(combined_analysis.splitlines())} lines")
560
+ logger.log_content("DEBUG", f"COMBINED_ANALYSIS_DEPTH_{current_depth}", combined_analysis)
561
+
562
+ # Create rechunk prompt
563
+ rechunk_prompt = create_rechunk_prompt(combined_analysis, current_depth)
564
+
565
+ # Log rechunk template
566
+ if logger:
567
+ logger.log_template("DEBUG", f"RECHUNK_DEPTH_{current_depth}", rechunk_prompt)
568
+
569
+ # Process rechunk
570
+ try:
571
+ result = handle_api_call(client, rechunk_prompt, system_prompt, logger)
572
+
573
+ # Check if further recursive chunking is needed
574
+ result_line_count = len(result.splitlines())
575
+
576
+ if result_line_count > 50 and current_depth < max_depth:
577
+ # Need another level of chunking
578
+ print(f"{COLORS['yellow']}Result still too large ({result_line_count} lines), continuing recursion...{COLORS['reset']}")
579
+ if logger:
580
+ logger.info(f"Result still too large ({result_line_count} lines), depth {current_depth}/{max_depth}")
581
+
582
+ return recursive_process(client, result, context_data, max_depth, logger, current_depth + 1)
583
+ else:
584
+ # Final processing
585
+ print(f"{COLORS['green']}Recursion complete, generating final commit message...{COLORS['reset']}")
586
+
587
+ # Create final combine prompt
588
+ final_prompt = f"""Create a CONVENTIONAL COMMIT MESSAGE based on these analyzed git changes:
589
+
590
+ {result}
591
+
592
+ FORMAT:
593
+ type[(scope)]: <concise summary> (max 50 chars)
594
+
595
+ - [type] <specific change 1> (filename:function/method/line)
596
+ - [type] <specific change 2> (filename:function/method/line)
597
+ - [type] <additional changes...>
598
+
599
+ RULES:
600
+ 1. First line must be under 50 characters
601
+ 2. Include a blank line after the first line
602
+ 3. Each bullet must include specific file references
603
+ 4. BE SPECIFIC - mention technical details and function names
604
+
605
+ DO NOT include any explanation or commentary outside the commit message format."""
606
+
607
+ # Log final template
608
+ if logger:
609
+ logger.log_template("DEBUG", "FINAL", final_prompt)
610
+
611
+ return handle_api_call(client, final_prompt, system_prompt, logger)
612
+ except Exception as e:
613
+ print(f"{COLORS['red']}Error in recursive processing: {str(e)}{COLORS['reset']}")
614
+ if logger:
615
+ logger.error(f"Error in recursive processing at depth {current_depth}: {str(e)}")
616
+ return None
617
+
618
+ def gitcommsg_mode(client, args, logger=None):
619
+ """Handle the Git commit message generation mode.
620
+
621
+ Args:
622
+ client: The NGPTClient instance
623
+ args: The parsed command line arguments
624
+ logger: Optional logger instance
625
+ """
626
+ # Set up logging if requested
627
+ custom_logger = None
628
+ log_path = None
629
+
630
+ if args.log:
631
+ custom_logger = create_gitcommsg_logger(args.log)
632
+
633
+ # Use both loggers if they exist
634
+ active_logger = logger if logger else custom_logger
635
+
636
+ if active_logger:
637
+ active_logger.info("Starting gitcommsg mode")
638
+ active_logger.debug(f"Args: {args}")
639
+
640
+ try:
641
+ # Get diff content
642
+ diff_content = get_diff_content(args.diff)
643
+
644
+ if not diff_content:
645
+ print(f"{COLORS['red']}No diff content available. Exiting.{COLORS['reset']}")
646
+ return
647
+
648
+ # Log the diff content
649
+ if active_logger:
650
+ active_logger.log_diff("DEBUG", diff_content)
651
+
652
+ # Process context if provided
653
+ context_data = None
654
+ if args.message_context:
655
+ context_data = process_context(args.message_context)
656
+ if active_logger:
657
+ active_logger.debug(f"Processed context: {context_data}")
658
+ active_logger.log_content("DEBUG", "CONTEXT_DATA", str(context_data))
659
+
660
+ # Create system prompt
661
+ system_prompt = create_system_prompt(context_data)
662
+
663
+ # Log system prompt
664
+ if active_logger:
665
+ active_logger.log_template("DEBUG", "SYSTEM", system_prompt)
666
+
667
+ print(f"\n{COLORS['green']}Generating commit message...{COLORS['reset']}")
668
+
669
+ # Process based on chunking options
670
+ result = None
671
+ if args.chunk_size:
672
+ chunk_size = args.chunk_size
673
+ if active_logger:
674
+ active_logger.info(f"Using chunk size: {chunk_size}")
675
+
676
+ if args.recursive_chunk:
677
+ # Use chunking with recursive processing
678
+ if active_logger:
679
+ active_logger.info(f"Using recursive chunking with max_depth: {args.max_depth}")
680
+
681
+ result = process_with_chunking(
682
+ client,
683
+ diff_content,
684
+ context_data,
685
+ chunk_size=args.chunk_size,
686
+ recursive=True,
687
+ max_depth=args.max_depth,
688
+ logger=active_logger
689
+ )
690
+ else:
691
+ # Direct processing without chunking
692
+ if active_logger:
693
+ active_logger.info("Processing without chunking")
694
+
695
+ prompt = create_final_prompt(diff_content)
696
+
697
+ # Log final template
698
+ if active_logger:
699
+ active_logger.log_template("DEBUG", "DIRECT_PROCESSING", prompt)
700
+
701
+ result = handle_api_call(client, prompt, system_prompt, active_logger)
702
+
703
+ if not result:
704
+ print(f"{COLORS['red']}Failed to generate commit message.{COLORS['reset']}")
705
+ return
706
+
707
+ # Display the result
708
+ print(f"\n{COLORS['green']}✨ Generated Commit Message:{COLORS['reset']}\n")
709
+ print(result)
710
+
711
+ # Log the result
712
+ if active_logger:
713
+ active_logger.info("Generated commit message successfully")
714
+ active_logger.log_content("INFO", "FINAL_COMMIT_MESSAGE", result)
715
+
716
+ # Try to copy to clipboard
717
+ try:
718
+ import pyperclip
719
+ pyperclip.copy(result)
720
+ print(f"\n{COLORS['green']}(Copied to clipboard){COLORS['reset']}")
721
+ if active_logger:
722
+ active_logger.info("Commit message copied to clipboard")
723
+ except ImportError:
724
+ if active_logger:
725
+ active_logger.debug("pyperclip not available, couldn't copy to clipboard")
726
+
727
+ except Exception as e:
728
+ print(f"{COLORS['red']}Error: {str(e)}{COLORS['reset']}")
729
+ if active_logger:
730
+ active_logger.error(f"Error in gitcommsg mode: {str(e)}", exc_info=True)