claude-commit 0.6.0__tar.gz → 0.8.0__tar.gz
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.
- {claude_commit-0.6.0/src/claude_commit.egg-info → claude_commit-0.8.0}/PKG-INFO +6 -10
- {claude_commit-0.6.0 → claude_commit-0.8.0}/README.md +5 -9
- {claude_commit-0.6.0 → claude_commit-0.8.0}/pyproject.toml +1 -1
- {claude_commit-0.6.0 → claude_commit-0.8.0}/src/claude_commit/main.py +140 -57
- {claude_commit-0.6.0 → claude_commit-0.8.0/src/claude_commit.egg-info}/PKG-INFO +6 -10
- {claude_commit-0.6.0 → claude_commit-0.8.0}/src/claude_commit.egg-info/SOURCES.txt +3 -1
- claude_commit-0.8.0/tests/test_config.py +182 -0
- claude_commit-0.8.0/tests/test_main.py +315 -0
- {claude_commit-0.6.0 → claude_commit-0.8.0}/LICENSE +0 -0
- {claude_commit-0.6.0 → claude_commit-0.8.0}/setup.cfg +0 -0
- {claude_commit-0.6.0 → claude_commit-0.8.0}/src/claude_commit/__init__.py +0 -0
- {claude_commit-0.6.0 → claude_commit-0.8.0}/src/claude_commit/config.py +0 -0
- {claude_commit-0.6.0 → claude_commit-0.8.0}/src/claude_commit.egg-info/dependency_links.txt +0 -0
- {claude_commit-0.6.0 → claude_commit-0.8.0}/src/claude_commit.egg-info/entry_points.txt +0 -0
- {claude_commit-0.6.0 → claude_commit-0.8.0}/src/claude_commit.egg-info/requires.txt +0 -0
- {claude_commit-0.6.0 → claude_commit-0.8.0}/src/claude_commit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-commit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: AI-powered git commit message generator using Claude Agent SDK
|
|
5
5
|
Author-email: Johannlai <johannli666@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -35,10 +35,6 @@ Dynamic: license-file
|
|
|
35
35
|
|
|
36
36
|
`claude-commit` uses Claude AI to analyze your code changes and write meaningful commit messages. Claude reads your files, understands the context, and generates commit messages following best practices.
|
|
37
37
|
|
|
38
|
-
## Demo
|
|
39
|
-
|
|
40
|
-
[](https://asciinema.org/a/ZubvhPFyP7hPFLsqZiZUc930L?autoplay=1&speed=3)
|
|
41
|
-
|
|
42
38
|
## Installation
|
|
43
39
|
|
|
44
40
|
### Prerequisites
|
|
@@ -181,12 +177,12 @@ claude-commit alias unset quick
|
|
|
181
177
|
|
|
182
178
|
## How It Works
|
|
183
179
|
|
|
184
|
-
Claude autonomously analyzes your changes:
|
|
180
|
+
Claude autonomously analyzes your changes using [subagents](https://platform.claude.com/docs/agent-sdk/subagents) for parallel analysis:
|
|
185
181
|
|
|
186
|
-
1. **
|
|
187
|
-
2. **
|
|
188
|
-
3. **
|
|
189
|
-
4. **Generates** a clear commit message
|
|
182
|
+
1. **Detects style** — a lightweight subagent checks git history for commit conventions (conventional commits, gitmoji, language, etc.)
|
|
183
|
+
2. **Analyzes changes** — a dedicated subagent reads modified files, searches for context, and understands the intent
|
|
184
|
+
3. **Runs in parallel** — both subagents execute concurrently for faster results
|
|
185
|
+
4. **Generates** a clear commit message combining style and change analysis
|
|
190
186
|
|
|
191
187
|
**Example:**
|
|
192
188
|
```
|
|
@@ -6,10 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
`claude-commit` uses Claude AI to analyze your code changes and write meaningful commit messages. Claude reads your files, understands the context, and generates commit messages following best practices.
|
|
8
8
|
|
|
9
|
-
## Demo
|
|
10
|
-
|
|
11
|
-
[](https://asciinema.org/a/ZubvhPFyP7hPFLsqZiZUc930L?autoplay=1&speed=3)
|
|
12
|
-
|
|
13
9
|
## Installation
|
|
14
10
|
|
|
15
11
|
### Prerequisites
|
|
@@ -152,12 +148,12 @@ claude-commit alias unset quick
|
|
|
152
148
|
|
|
153
149
|
## How It Works
|
|
154
150
|
|
|
155
|
-
Claude autonomously analyzes your changes:
|
|
151
|
+
Claude autonomously analyzes your changes using [subagents](https://platform.claude.com/docs/agent-sdk/subagents) for parallel analysis:
|
|
156
152
|
|
|
157
|
-
1. **
|
|
158
|
-
2. **
|
|
159
|
-
3. **
|
|
160
|
-
4. **Generates** a clear commit message
|
|
153
|
+
1. **Detects style** — a lightweight subagent checks git history for commit conventions (conventional commits, gitmoji, language, etc.)
|
|
154
|
+
2. **Analyzes changes** — a dedicated subagent reads modified files, searches for context, and understands the intent
|
|
155
|
+
3. **Runs in parallel** — both subagents execute concurrently for faster results
|
|
156
|
+
4. **Generates** a clear commit message combining style and change analysis
|
|
161
157
|
|
|
162
158
|
**Example:**
|
|
163
159
|
```
|
|
@@ -15,6 +15,7 @@ from typing import Optional
|
|
|
15
15
|
|
|
16
16
|
import pyperclip
|
|
17
17
|
from claude_agent_sdk import (
|
|
18
|
+
AgentDefinition,
|
|
18
19
|
AssistantMessage,
|
|
19
20
|
ClaudeAgentOptions,
|
|
20
21
|
CLINotFoundError,
|
|
@@ -32,6 +33,75 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
|
32
33
|
from .config import Config, resolve_alias
|
|
33
34
|
|
|
34
35
|
console = Console()
|
|
36
|
+
error_console = Console(stderr=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def extract_commit_message(all_text: list) -> Optional[str]:
|
|
40
|
+
"""Extract commit message from collected AI response text blocks.
|
|
41
|
+
|
|
42
|
+
Looks for the COMMIT_MESSAGE: marker first. Falls back to using the last
|
|
43
|
+
text block that doesn't start with an explanatory prefix.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
all_text: List of text blocks from the AI response.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Extracted commit message or None.
|
|
50
|
+
"""
|
|
51
|
+
full_response = "\n".join(all_text)
|
|
52
|
+
commit_message = None
|
|
53
|
+
|
|
54
|
+
if "COMMIT_MESSAGE:" in full_response:
|
|
55
|
+
parts = full_response.split("COMMIT_MESSAGE:", 1)
|
|
56
|
+
if len(parts) > 1:
|
|
57
|
+
commit_message = parts[1].strip()
|
|
58
|
+
else:
|
|
59
|
+
for text in reversed(all_text):
|
|
60
|
+
text = text.strip()
|
|
61
|
+
if text and not any(
|
|
62
|
+
text.lower().startswith(prefix)
|
|
63
|
+
for prefix in [
|
|
64
|
+
"let me",
|
|
65
|
+
"i'll",
|
|
66
|
+
"i will",
|
|
67
|
+
"now i",
|
|
68
|
+
"first",
|
|
69
|
+
"i can see",
|
|
70
|
+
]
|
|
71
|
+
):
|
|
72
|
+
commit_message = text
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if commit_message:
|
|
76
|
+
commit_message = clean_markdown_fences(commit_message)
|
|
77
|
+
|
|
78
|
+
return commit_message
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def clean_markdown_fences(text: str) -> str:
|
|
82
|
+
"""Remove markdown code block fences from text, keeping content outside fences.
|
|
83
|
+
|
|
84
|
+
Lines starting with ``` toggle a "code block" state. Lines inside code blocks
|
|
85
|
+
are dropped; lines outside are kept.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
text: Text potentially containing markdown code fences.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Cleaned text with fences and their content removed.
|
|
92
|
+
"""
|
|
93
|
+
lines = text.split("\n")
|
|
94
|
+
cleaned_lines = []
|
|
95
|
+
in_code_block = False
|
|
96
|
+
|
|
97
|
+
for line in lines:
|
|
98
|
+
if line.strip().startswith("```"):
|
|
99
|
+
in_code_block = not in_code_block
|
|
100
|
+
continue
|
|
101
|
+
if not in_code_block:
|
|
102
|
+
cleaned_lines.append(line.rstrip())
|
|
103
|
+
|
|
104
|
+
return "\n".join(cleaned_lines).strip()
|
|
35
105
|
|
|
36
106
|
|
|
37
107
|
SYSTEM_PROMPT = """You are an expert software engineer tasked with analyzing code changes and writing excellent git commit messages.
|
|
@@ -82,6 +152,20 @@ You have access to these tools for analyzing the codebase:
|
|
|
82
152
|
**Pro tip**: Grep is faster than reading entire files. Use it to quickly assess impact before deciding which files to read in detail.
|
|
83
153
|
</available_tools>
|
|
84
154
|
|
|
155
|
+
<subagents>
|
|
156
|
+
You have access to specialized subagents via the **Task** tool. Use them to parallelize your analysis for faster results.
|
|
157
|
+
|
|
158
|
+
Available subagents:
|
|
159
|
+
- **style-detector**: Detects commit message style/conventions from git history. Use this to check commit style while you analyze changes in parallel.
|
|
160
|
+
- **diff-analyzer**: Deep-dives into code changes, reads files, greps for context. Use this for understanding complex or large diffs.
|
|
161
|
+
|
|
162
|
+
**Parallelization strategy**: When analyzing a repository, launch subagents in parallel to speed things up. For example:
|
|
163
|
+
- Launch `style-detector` AND `diff-analyzer` simultaneously in a single response
|
|
164
|
+
- Combine their results to generate the final commit message
|
|
165
|
+
|
|
166
|
+
To invoke a subagent, use the Task tool with `subagent_type` set to the agent name.
|
|
167
|
+
</subagents>
|
|
168
|
+
|
|
85
169
|
<analysis_approach>
|
|
86
170
|
Follow this approach (you decide what's necessary based on the changes):
|
|
87
171
|
|
|
@@ -299,7 +383,43 @@ Begin your analysis now.
|
|
|
299
383
|
"Grep", # Search patterns in files (POWERFUL!)
|
|
300
384
|
"Glob", # Find files by pattern
|
|
301
385
|
"Edit", # Make precise edits to files (useful for analyzing multi-line changes)
|
|
386
|
+
"Task", # Invoke subagents for parallel analysis
|
|
302
387
|
],
|
|
388
|
+
agents={
|
|
389
|
+
"style-detector": AgentDefinition(
|
|
390
|
+
description="Detect commit message style and conventions from git history. Use this to determine the project's commit format (conventional commits, gitmoji, language, etc.) by examining recent commits.",
|
|
391
|
+
prompt="""You are a commit style detector. Your ONLY job is to analyze the git commit history and report the style conventions used.
|
|
392
|
+
|
|
393
|
+
Steps:
|
|
394
|
+
1. Run: git log -10 --oneline
|
|
395
|
+
2. Analyze the output for:
|
|
396
|
+
- Format: conventional commits (feat:, fix:), gitmoji, plain text, etc.
|
|
397
|
+
- Language: English, Chinese, or other
|
|
398
|
+
- Emoji usage: gitmoji style or none
|
|
399
|
+
- Any other patterns (scope, capitalization, etc.)
|
|
400
|
+
|
|
401
|
+
Output a concise summary of the detected style, for example:
|
|
402
|
+
"Conventional commits format, English, no emoji. Example: feat: add user auth"
|
|
403
|
+
""",
|
|
404
|
+
tools=["Bash"],
|
|
405
|
+
model="haiku",
|
|
406
|
+
),
|
|
407
|
+
"diff-analyzer": AgentDefinition(
|
|
408
|
+
description="Deep-dive into code changes to understand what changed and why. Use this for analyzing complex or large diffs, reading modified files, and understanding code relationships.",
|
|
409
|
+
prompt="""You are a code change analyzer. Your job is to deeply understand what changed in the repository and WHY.
|
|
410
|
+
|
|
411
|
+
Steps:
|
|
412
|
+
1. Run git status and git diff (or git diff --cached for staged changes) to see what changed
|
|
413
|
+
2. For significant changes, READ the modified files to understand context
|
|
414
|
+
3. Use GREP to understand code relationships (where functions are called, imports, etc.)
|
|
415
|
+
4. Summarize: what changed, why it changed, and the impact
|
|
416
|
+
|
|
417
|
+
Output a clear, structured summary of the changes suitable for writing a commit message.
|
|
418
|
+
""",
|
|
419
|
+
tools=["Bash", "Read", "Grep", "Glob"],
|
|
420
|
+
model="sonnet",
|
|
421
|
+
),
|
|
422
|
+
},
|
|
303
423
|
permission_mode="acceptEdits",
|
|
304
424
|
cwd=str(repo_path.absolute()),
|
|
305
425
|
max_turns=30,
|
|
@@ -431,48 +551,7 @@ Begin your analysis now.
|
|
|
431
551
|
console.print(f"[cyan]🔄 Turns: {message.num_turns}[/cyan]")
|
|
432
552
|
|
|
433
553
|
if not message.is_error:
|
|
434
|
-
|
|
435
|
-
full_response = "\n".join(all_text)
|
|
436
|
-
|
|
437
|
-
# Look for COMMIT_MESSAGE: marker
|
|
438
|
-
if "COMMIT_MESSAGE:" in full_response:
|
|
439
|
-
# Extract everything after COMMIT_MESSAGE:
|
|
440
|
-
parts = full_response.split("COMMIT_MESSAGE:", 1)
|
|
441
|
-
if len(parts) > 1:
|
|
442
|
-
commit_message = parts[1].strip()
|
|
443
|
-
else:
|
|
444
|
-
# Fallback: try to extract the last meaningful text block
|
|
445
|
-
# Skip explanatory text and get the actual commit message
|
|
446
|
-
for text in reversed(all_text):
|
|
447
|
-
text = text.strip()
|
|
448
|
-
if text and not any(
|
|
449
|
-
text.lower().startswith(prefix)
|
|
450
|
-
for prefix in [
|
|
451
|
-
"let me",
|
|
452
|
-
"i'll",
|
|
453
|
-
"i will",
|
|
454
|
-
"now i",
|
|
455
|
-
"first",
|
|
456
|
-
"i can see",
|
|
457
|
-
]
|
|
458
|
-
):
|
|
459
|
-
commit_message = text
|
|
460
|
-
break
|
|
461
|
-
|
|
462
|
-
# Clean up markdown code blocks if present
|
|
463
|
-
if commit_message:
|
|
464
|
-
lines = commit_message.split("\n")
|
|
465
|
-
cleaned_lines = []
|
|
466
|
-
in_code_block = False
|
|
467
|
-
|
|
468
|
-
for line in lines:
|
|
469
|
-
if line.strip().startswith("```"):
|
|
470
|
-
in_code_block = not in_code_block
|
|
471
|
-
continue
|
|
472
|
-
if not in_code_block:
|
|
473
|
-
cleaned_lines.append(line.rstrip())
|
|
474
|
-
|
|
475
|
-
commit_message = "\n".join(cleaned_lines).strip()
|
|
554
|
+
commit_message = extract_commit_message(all_text)
|
|
476
555
|
|
|
477
556
|
# Make sure progress is stopped before returning
|
|
478
557
|
if progress is not None and task_id is not None:
|
|
@@ -490,23 +569,29 @@ Begin your analysis now.
|
|
|
490
569
|
# Stop progress on error
|
|
491
570
|
if "progress" in locals() and progress is not None:
|
|
492
571
|
progress.stop()
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
"[yellow]📦 Please install it: npm install -g @anthropic-ai/claude-code[/yellow]"
|
|
496
|
-
file=sys.stderr,
|
|
572
|
+
error_console.print("[red]❌ Error: Claude Code CLI not found.[/red]")
|
|
573
|
+
error_console.print(
|
|
574
|
+
"[yellow]📦 Please install it: npm install -g @anthropic-ai/claude-code[/yellow]"
|
|
497
575
|
)
|
|
498
576
|
return None
|
|
499
577
|
except ProcessError as e:
|
|
500
578
|
if "progress" in locals() and progress is not None:
|
|
501
579
|
progress.stop()
|
|
502
|
-
|
|
580
|
+
# If we already extracted a commit message, return it despite the process error
|
|
581
|
+
if commit_message:
|
|
582
|
+
return commit_message
|
|
583
|
+
error_console.print(f"[red]❌ Process error: {e}[/red]")
|
|
503
584
|
if e.stderr:
|
|
504
|
-
|
|
585
|
+
error_console.print(f" stderr: {e.stderr}")
|
|
505
586
|
return None
|
|
506
587
|
except Exception as e:
|
|
507
588
|
if "progress" in locals() and progress is not None:
|
|
508
589
|
progress.stop()
|
|
509
|
-
|
|
590
|
+
# If we already extracted a commit message, return it despite the error
|
|
591
|
+
# (the SDK sometimes raises after the CLI has already completed successfully)
|
|
592
|
+
if commit_message:
|
|
593
|
+
return commit_message
|
|
594
|
+
error_console.print(f"[red]❌ Unexpected error: {e}[/red]")
|
|
510
595
|
if verbose:
|
|
511
596
|
import traceback
|
|
512
597
|
|
|
@@ -947,11 +1032,11 @@ Alias Management:
|
|
|
947
1032
|
)
|
|
948
1033
|
)
|
|
949
1034
|
except KeyboardInterrupt:
|
|
950
|
-
|
|
1035
|
+
error_console.print("\n[yellow]⚠️ Interrupted by user[/yellow]")
|
|
951
1036
|
sys.exit(130)
|
|
952
1037
|
|
|
953
1038
|
if not commit_message:
|
|
954
|
-
|
|
1039
|
+
error_console.print("[red]❌ Failed to generate commit message[/red]")
|
|
955
1040
|
sys.exit(1)
|
|
956
1041
|
|
|
957
1042
|
# Display the generated message with rich formatting
|
|
@@ -975,9 +1060,7 @@ Alias Management:
|
|
|
975
1060
|
pyperclip.copy(commit_message)
|
|
976
1061
|
console.print("\n[green]✅ Commit message copied to clipboard![/green]")
|
|
977
1062
|
except Exception as e:
|
|
978
|
-
|
|
979
|
-
f"\n[yellow]⚠️ Failed to copy to clipboard: {e}[/yellow]", file=sys.stderr
|
|
980
|
-
)
|
|
1063
|
+
error_console.print(f"\n[yellow]⚠️ Failed to copy to clipboard: {e}[/yellow]")
|
|
981
1064
|
|
|
982
1065
|
if args.commit:
|
|
983
1066
|
try:
|
|
@@ -1004,12 +1087,12 @@ Alias Management:
|
|
|
1004
1087
|
if result.stdout:
|
|
1005
1088
|
console.print(result.stdout)
|
|
1006
1089
|
except subprocess.CalledProcessError as e:
|
|
1007
|
-
|
|
1090
|
+
error_console.print(f"\n[red]❌ Failed to commit: {e}[/red]")
|
|
1008
1091
|
if e.stderr:
|
|
1009
|
-
|
|
1092
|
+
error_console.print(e.stderr)
|
|
1010
1093
|
sys.exit(1)
|
|
1011
1094
|
except Exception as e:
|
|
1012
|
-
|
|
1095
|
+
error_console.print(f"\n[red]❌ Unexpected error during commit: {e}[/red]")
|
|
1013
1096
|
sys.exit(1)
|
|
1014
1097
|
else:
|
|
1015
1098
|
# Default: just show the command
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-commit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: AI-powered git commit message generator using Claude Agent SDK
|
|
5
5
|
Author-email: Johannlai <johannli666@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -35,10 +35,6 @@ Dynamic: license-file
|
|
|
35
35
|
|
|
36
36
|
`claude-commit` uses Claude AI to analyze your code changes and write meaningful commit messages. Claude reads your files, understands the context, and generates commit messages following best practices.
|
|
37
37
|
|
|
38
|
-
## Demo
|
|
39
|
-
|
|
40
|
-
[](https://asciinema.org/a/ZubvhPFyP7hPFLsqZiZUc930L?autoplay=1&speed=3)
|
|
41
|
-
|
|
42
38
|
## Installation
|
|
43
39
|
|
|
44
40
|
### Prerequisites
|
|
@@ -181,12 +177,12 @@ claude-commit alias unset quick
|
|
|
181
177
|
|
|
182
178
|
## How It Works
|
|
183
179
|
|
|
184
|
-
Claude autonomously analyzes your changes:
|
|
180
|
+
Claude autonomously analyzes your changes using [subagents](https://platform.claude.com/docs/agent-sdk/subagents) for parallel analysis:
|
|
185
181
|
|
|
186
|
-
1. **
|
|
187
|
-
2. **
|
|
188
|
-
3. **
|
|
189
|
-
4. **Generates** a clear commit message
|
|
182
|
+
1. **Detects style** — a lightweight subagent checks git history for commit conventions (conventional commits, gitmoji, language, etc.)
|
|
183
|
+
2. **Analyzes changes** — a dedicated subagent reads modified files, searches for context, and understands the intent
|
|
184
|
+
3. **Runs in parallel** — both subagents execute concurrently for faster results
|
|
185
|
+
4. **Generates** a clear commit message combining style and change analysis
|
|
190
186
|
|
|
191
187
|
**Example:**
|
|
192
188
|
```
|
|
@@ -9,4 +9,6 @@ src/claude_commit.egg-info/SOURCES.txt
|
|
|
9
9
|
src/claude_commit.egg-info/dependency_links.txt
|
|
10
10
|
src/claude_commit.egg-info/entry_points.txt
|
|
11
11
|
src/claude_commit.egg-info/requires.txt
|
|
12
|
-
src/claude_commit.egg-info/top_level.txt
|
|
12
|
+
src/claude_commit.egg-info/top_level.txt
|
|
13
|
+
tests/test_config.py
|
|
14
|
+
tests/test_main.py
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Tests for claude_commit.config — Config class and resolve_alias."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from claude_commit.config import Config, resolve_alias
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Config – defaults
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestConfigDefaults:
|
|
17
|
+
def test_new_config_has_default_aliases(self, tmp_path):
|
|
18
|
+
cfg = Config(config_path=tmp_path / "config.json")
|
|
19
|
+
aliases = cfg.list_aliases()
|
|
20
|
+
assert "cc" in aliases
|
|
21
|
+
assert "ccc" in aliases
|
|
22
|
+
assert "cca" in aliases
|
|
23
|
+
|
|
24
|
+
def test_default_alias_values(self, tmp_path):
|
|
25
|
+
cfg = Config(config_path=tmp_path / "config.json")
|
|
26
|
+
assert cfg.get_alias("cc") == ""
|
|
27
|
+
assert cfg.get_alias("ccc") == "--commit"
|
|
28
|
+
assert cfg.get_alias("cca") == "--all"
|
|
29
|
+
assert cfg.get_alias("ccac") == "--all --commit"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Config – set / get / delete / list
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestConfigAliasOps:
|
|
38
|
+
def test_set_and_get_alias(self, tmp_path):
|
|
39
|
+
cfg = Config(config_path=tmp_path / "config.json")
|
|
40
|
+
cfg.set_alias("myalias", "--all --verbose")
|
|
41
|
+
assert cfg.get_alias("myalias") == "--all --verbose"
|
|
42
|
+
|
|
43
|
+
def test_get_alias_returns_none_for_unknown(self, tmp_path):
|
|
44
|
+
cfg = Config(config_path=tmp_path / "config.json")
|
|
45
|
+
assert cfg.get_alias("nonexistent") is None
|
|
46
|
+
|
|
47
|
+
def test_delete_alias_existing(self, tmp_path):
|
|
48
|
+
cfg = Config(config_path=tmp_path / "config.json")
|
|
49
|
+
cfg.set_alias("tmp", "--verbose")
|
|
50
|
+
assert cfg.delete_alias("tmp") is True
|
|
51
|
+
assert cfg.get_alias("tmp") is None
|
|
52
|
+
|
|
53
|
+
def test_delete_alias_missing(self, tmp_path):
|
|
54
|
+
cfg = Config(config_path=tmp_path / "config.json")
|
|
55
|
+
assert cfg.delete_alias("does_not_exist") is False
|
|
56
|
+
|
|
57
|
+
def test_list_aliases_returns_copy(self, tmp_path):
|
|
58
|
+
cfg = Config(config_path=tmp_path / "config.json")
|
|
59
|
+
aliases = cfg.list_aliases()
|
|
60
|
+
aliases["injected"] = "evil"
|
|
61
|
+
assert cfg.get_alias("injected") is None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Config – first run
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestConfigFirstRun:
|
|
70
|
+
def test_is_first_run_true_when_no_file(self, tmp_path):
|
|
71
|
+
cfg = Config(config_path=tmp_path / "config.json")
|
|
72
|
+
assert cfg.is_first_run() is True
|
|
73
|
+
|
|
74
|
+
def test_is_first_run_false_after_save(self, tmp_path):
|
|
75
|
+
path = tmp_path / "config.json"
|
|
76
|
+
cfg = Config(config_path=path)
|
|
77
|
+
cfg.mark_first_run_complete()
|
|
78
|
+
assert cfg.is_first_run() is False
|
|
79
|
+
|
|
80
|
+
def test_mark_first_run_creates_file(self, tmp_path):
|
|
81
|
+
path = tmp_path / "subdir" / "config.json"
|
|
82
|
+
cfg = Config(config_path=path)
|
|
83
|
+
cfg.mark_first_run_complete()
|
|
84
|
+
assert path.exists()
|
|
85
|
+
|
|
86
|
+
def test_mark_first_run_idempotent(self, tmp_path):
|
|
87
|
+
path = tmp_path / "config.json"
|
|
88
|
+
cfg = Config(config_path=path)
|
|
89
|
+
cfg.mark_first_run_complete()
|
|
90
|
+
content1 = path.read_text()
|
|
91
|
+
cfg.mark_first_run_complete() # second call should be a no-op
|
|
92
|
+
content2 = path.read_text()
|
|
93
|
+
assert content1 == content2
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Config – persistence across instances
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestConfigPersistence:
|
|
102
|
+
def test_alias_persists_across_instances(self, tmp_path):
|
|
103
|
+
path = tmp_path / "config.json"
|
|
104
|
+
cfg1 = Config(config_path=path)
|
|
105
|
+
cfg1.set_alias("persist", "--all --commit")
|
|
106
|
+
|
|
107
|
+
cfg2 = Config(config_path=path)
|
|
108
|
+
assert cfg2.get_alias("persist") == "--all --commit"
|
|
109
|
+
|
|
110
|
+
def test_config_file_is_valid_json(self, tmp_path):
|
|
111
|
+
path = tmp_path / "config.json"
|
|
112
|
+
cfg = Config(config_path=path)
|
|
113
|
+
cfg.set_alias("test", "--verbose")
|
|
114
|
+
data = json.loads(path.read_text())
|
|
115
|
+
assert "aliases" in data
|
|
116
|
+
assert data["aliases"]["test"] == "--verbose"
|
|
117
|
+
|
|
118
|
+
def test_corrupt_config_file_returns_defaults(self, tmp_path):
|
|
119
|
+
path = tmp_path / "config.json"
|
|
120
|
+
path.write_text("not valid json!!!")
|
|
121
|
+
cfg = Config(config_path=path)
|
|
122
|
+
# Should fall back to defaults without raising
|
|
123
|
+
assert "cc" in cfg.list_aliases()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# resolve_alias
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestResolveAlias:
|
|
132
|
+
def test_empty_args_returns_empty(self, tmp_path, monkeypatch):
|
|
133
|
+
monkeypatch.setattr(
|
|
134
|
+
"claude_commit.config.Config.__init__",
|
|
135
|
+
lambda self, config_path=None: Config.__init__(
|
|
136
|
+
self, config_path=tmp_path / "config.json"
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
assert resolve_alias([]) == []
|
|
140
|
+
|
|
141
|
+
def test_known_alias_expands(self, tmp_path, monkeypatch):
|
|
142
|
+
path = tmp_path / "config.json"
|
|
143
|
+
cfg = Config(config_path=path)
|
|
144
|
+
cfg.set_alias("myalias", "--all --verbose")
|
|
145
|
+
|
|
146
|
+
# Patch Config() inside resolve_alias to use our tmp config
|
|
147
|
+
original_init = Config.__init__
|
|
148
|
+
|
|
149
|
+
def patched_init(self, config_path=None):
|
|
150
|
+
original_init(self, config_path=path)
|
|
151
|
+
|
|
152
|
+
monkeypatch.setattr("claude_commit.config.Config.__init__", patched_init)
|
|
153
|
+
|
|
154
|
+
result = resolve_alias(["myalias", "--extra"])
|
|
155
|
+
assert result == ["--all", "--verbose", "--extra"]
|
|
156
|
+
|
|
157
|
+
def test_empty_alias_drops_alias_arg(self, tmp_path, monkeypatch):
|
|
158
|
+
path = tmp_path / "config.json"
|
|
159
|
+
cfg = Config(config_path=path)
|
|
160
|
+
cfg.set_alias("bare", "")
|
|
161
|
+
|
|
162
|
+
original_init = Config.__init__
|
|
163
|
+
|
|
164
|
+
def patched_init(self, config_path=None):
|
|
165
|
+
original_init(self, config_path=path)
|
|
166
|
+
|
|
167
|
+
monkeypatch.setattr("claude_commit.config.Config.__init__", patched_init)
|
|
168
|
+
|
|
169
|
+
result = resolve_alias(["bare", "--verbose"])
|
|
170
|
+
assert result == ["--verbose"]
|
|
171
|
+
|
|
172
|
+
def test_unknown_arg_passes_through(self, tmp_path, monkeypatch):
|
|
173
|
+
path = tmp_path / "config.json"
|
|
174
|
+
original_init = Config.__init__
|
|
175
|
+
|
|
176
|
+
def patched_init(self, config_path=None):
|
|
177
|
+
original_init(self, config_path=path)
|
|
178
|
+
|
|
179
|
+
monkeypatch.setattr("claude_commit.config.Config.__init__", patched_init)
|
|
180
|
+
|
|
181
|
+
result = resolve_alias(["--all", "--verbose"])
|
|
182
|
+
assert result == ["--all", "--verbose"]
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Tests for claude_commit.main — pure logic (no Claude / git calls)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from claude_commit.config import Config
|
|
9
|
+
from claude_commit.main import (
|
|
10
|
+
clean_markdown_fences,
|
|
11
|
+
extract_commit_message,
|
|
12
|
+
handle_alias_command,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# extract_commit_message — COMMIT_MESSAGE: marker
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestExtractCommitMessage:
|
|
22
|
+
def test_extracts_after_marker(self):
|
|
23
|
+
all_text = [
|
|
24
|
+
"Let me analyze the changes.",
|
|
25
|
+
"COMMIT_MESSAGE:\nfeat: add login\n\n- Add JWT auth",
|
|
26
|
+
]
|
|
27
|
+
result = extract_commit_message(all_text)
|
|
28
|
+
assert result is not None
|
|
29
|
+
assert result.startswith("feat: add login")
|
|
30
|
+
assert "- Add JWT auth" in result
|
|
31
|
+
|
|
32
|
+
def test_marker_in_middle_of_text(self):
|
|
33
|
+
all_text = [
|
|
34
|
+
"Analysis done. COMMIT_MESSAGE:\nfix: typo in README",
|
|
35
|
+
]
|
|
36
|
+
result = extract_commit_message(all_text)
|
|
37
|
+
assert result == "fix: typo in README"
|
|
38
|
+
|
|
39
|
+
def test_marker_across_text_blocks(self):
|
|
40
|
+
all_text = [
|
|
41
|
+
"Some analysis",
|
|
42
|
+
"More analysis\nCOMMIT_MESSAGE:\nchore: bump version",
|
|
43
|
+
]
|
|
44
|
+
result = extract_commit_message(all_text)
|
|
45
|
+
assert result == "chore: bump version"
|
|
46
|
+
|
|
47
|
+
def test_only_first_marker_used(self):
|
|
48
|
+
all_text = [
|
|
49
|
+
"COMMIT_MESSAGE:\nfirst message",
|
|
50
|
+
"COMMIT_MESSAGE:\nsecond message",
|
|
51
|
+
]
|
|
52
|
+
result = extract_commit_message(all_text)
|
|
53
|
+
assert "first message" in result
|
|
54
|
+
|
|
55
|
+
def test_returns_none_for_empty_input(self):
|
|
56
|
+
assert extract_commit_message([]) is None
|
|
57
|
+
|
|
58
|
+
def test_returns_none_for_only_explanatory_text(self):
|
|
59
|
+
all_text = [
|
|
60
|
+
"Let me analyze the changes.",
|
|
61
|
+
"I'll look at the diff now.",
|
|
62
|
+
"First, checking git status.",
|
|
63
|
+
]
|
|
64
|
+
result = extract_commit_message(all_text)
|
|
65
|
+
assert result is None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# extract_commit_message — fallback (no marker)
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestExtractFallback:
|
|
74
|
+
def test_uses_last_non_explanatory_block(self):
|
|
75
|
+
all_text = [
|
|
76
|
+
"Let me analyze.",
|
|
77
|
+
"feat: add new feature",
|
|
78
|
+
"I'll commit this now.",
|
|
79
|
+
]
|
|
80
|
+
# "I'll commit this now." starts with "i'll" → skipped
|
|
81
|
+
# "feat: add new feature" → used
|
|
82
|
+
result = extract_commit_message(all_text)
|
|
83
|
+
assert result == "feat: add new feature"
|
|
84
|
+
|
|
85
|
+
def test_skips_all_known_prefixes(self):
|
|
86
|
+
prefixes = ["let me", "i'll", "i will", "now i", "first", "i can see"]
|
|
87
|
+
for prefix in prefixes:
|
|
88
|
+
all_text = [f"{prefix} do something"]
|
|
89
|
+
result = extract_commit_message(all_text)
|
|
90
|
+
assert result is None, f"Should skip prefix: {prefix}"
|
|
91
|
+
|
|
92
|
+
def test_prefix_check_is_case_insensitive(self):
|
|
93
|
+
all_text = ["Let Me analyze the diff"]
|
|
94
|
+
result = extract_commit_message(all_text)
|
|
95
|
+
assert result is None
|
|
96
|
+
|
|
97
|
+
def test_non_prefix_text_is_accepted(self):
|
|
98
|
+
all_text = ["refactor: clean up imports"]
|
|
99
|
+
result = extract_commit_message(all_text)
|
|
100
|
+
assert result == "refactor: clean up imports"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# clean_markdown_fences
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestCleanMarkdownFences:
|
|
109
|
+
def test_no_fences_unchanged(self):
|
|
110
|
+
text = "feat: add login\n\n- Add JWT auth"
|
|
111
|
+
assert clean_markdown_fences(text) == text
|
|
112
|
+
|
|
113
|
+
def test_strips_surrounding_fences(self):
|
|
114
|
+
# Content outside fences is kept; content inside is dropped.
|
|
115
|
+
# So fences around extra examples are removed.
|
|
116
|
+
text = "feat: add login\n\n```\ngit commit -m 'feat: add login'\n```"
|
|
117
|
+
result = clean_markdown_fences(text)
|
|
118
|
+
assert "```" not in result
|
|
119
|
+
assert "feat: add login" in result
|
|
120
|
+
assert "git commit" not in result
|
|
121
|
+
|
|
122
|
+
def test_strips_fences_with_language_tag(self):
|
|
123
|
+
text = "fix: typo\n\n```bash\nsome command\n```"
|
|
124
|
+
result = clean_markdown_fences(text)
|
|
125
|
+
assert "```" not in result
|
|
126
|
+
assert "some command" not in result
|
|
127
|
+
assert "fix: typo" in result
|
|
128
|
+
|
|
129
|
+
def test_multiple_fenced_blocks(self):
|
|
130
|
+
text = "header\n```\nblock1\n```\nmiddle\n```\nblock2\n```\nfooter"
|
|
131
|
+
result = clean_markdown_fences(text)
|
|
132
|
+
assert result == "header\nmiddle\nfooter"
|
|
133
|
+
|
|
134
|
+
def test_empty_string(self):
|
|
135
|
+
assert clean_markdown_fences("") == ""
|
|
136
|
+
|
|
137
|
+
def test_only_fences(self):
|
|
138
|
+
assert clean_markdown_fences("```\nhello\n```") == ""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# handle_alias_command
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class TestHandleAliasCommand:
|
|
147
|
+
def _make_config(self, tmp_path):
|
|
148
|
+
"""Create a Config with a temp path and patch Config() calls."""
|
|
149
|
+
return Config(config_path=tmp_path / "config.json")
|
|
150
|
+
|
|
151
|
+
def test_list_aliases(self, tmp_path, capsys, monkeypatch):
|
|
152
|
+
path = tmp_path / "config.json"
|
|
153
|
+
original_init = Config.__init__
|
|
154
|
+
|
|
155
|
+
def patched_init(self, config_path=None):
|
|
156
|
+
original_init(self, config_path=path)
|
|
157
|
+
|
|
158
|
+
monkeypatch.setattr(Config, "__init__", patched_init)
|
|
159
|
+
|
|
160
|
+
handle_alias_command(["list"])
|
|
161
|
+
captured = capsys.readouterr()
|
|
162
|
+
assert "Configured aliases" in captured.out
|
|
163
|
+
assert "ccc" in captured.out
|
|
164
|
+
|
|
165
|
+
def test_list_aliases_default(self, tmp_path, capsys, monkeypatch):
|
|
166
|
+
"""Calling with no args defaults to list."""
|
|
167
|
+
path = tmp_path / "config.json"
|
|
168
|
+
original_init = Config.__init__
|
|
169
|
+
|
|
170
|
+
def patched_init(self, config_path=None):
|
|
171
|
+
original_init(self, config_path=path)
|
|
172
|
+
|
|
173
|
+
monkeypatch.setattr(Config, "__init__", patched_init)
|
|
174
|
+
|
|
175
|
+
handle_alias_command([])
|
|
176
|
+
captured = capsys.readouterr()
|
|
177
|
+
assert "Configured aliases" in captured.out
|
|
178
|
+
|
|
179
|
+
def test_set_alias(self, tmp_path, capsys, monkeypatch):
|
|
180
|
+
path = tmp_path / "config.json"
|
|
181
|
+
original_init = Config.__init__
|
|
182
|
+
|
|
183
|
+
def patched_init(self, config_path=None):
|
|
184
|
+
original_init(self, config_path=path)
|
|
185
|
+
|
|
186
|
+
monkeypatch.setattr(Config, "__init__", patched_init)
|
|
187
|
+
|
|
188
|
+
handle_alias_command(["set", "myalias", "--all", "--verbose"])
|
|
189
|
+
captured = capsys.readouterr()
|
|
190
|
+
assert "myalias" in captured.out
|
|
191
|
+
|
|
192
|
+
# Verify it was persisted
|
|
193
|
+
cfg = Config(config_path=path)
|
|
194
|
+
assert cfg.get_alias("myalias") == "--all --verbose"
|
|
195
|
+
|
|
196
|
+
def test_set_alias_missing_name(self, tmp_path, monkeypatch):
|
|
197
|
+
path = tmp_path / "config.json"
|
|
198
|
+
original_init = Config.__init__
|
|
199
|
+
|
|
200
|
+
def patched_init(self, config_path=None):
|
|
201
|
+
original_init(self, config_path=path)
|
|
202
|
+
|
|
203
|
+
monkeypatch.setattr(Config, "__init__", patched_init)
|
|
204
|
+
|
|
205
|
+
with pytest.raises(SystemExit):
|
|
206
|
+
handle_alias_command(["set"])
|
|
207
|
+
|
|
208
|
+
def test_unset_alias(self, tmp_path, capsys, monkeypatch):
|
|
209
|
+
path = tmp_path / "config.json"
|
|
210
|
+
cfg = Config(config_path=path)
|
|
211
|
+
cfg.set_alias("removeme", "--verbose")
|
|
212
|
+
|
|
213
|
+
original_init = Config.__init__
|
|
214
|
+
|
|
215
|
+
def patched_init(self, config_path=None):
|
|
216
|
+
original_init(self, config_path=path)
|
|
217
|
+
|
|
218
|
+
monkeypatch.setattr(Config, "__init__", patched_init)
|
|
219
|
+
|
|
220
|
+
handle_alias_command(["unset", "removeme"])
|
|
221
|
+
captured = capsys.readouterr()
|
|
222
|
+
assert "removeme" in captured.out
|
|
223
|
+
assert "removed" in captured.out
|
|
224
|
+
|
|
225
|
+
def test_unset_alias_not_found(self, tmp_path, monkeypatch):
|
|
226
|
+
path = tmp_path / "config.json"
|
|
227
|
+
original_init = Config.__init__
|
|
228
|
+
|
|
229
|
+
def patched_init(self, config_path=None):
|
|
230
|
+
original_init(self, config_path=path)
|
|
231
|
+
|
|
232
|
+
monkeypatch.setattr(Config, "__init__", patched_init)
|
|
233
|
+
|
|
234
|
+
with pytest.raises(SystemExit):
|
|
235
|
+
handle_alias_command(["unset", "ghost"])
|
|
236
|
+
|
|
237
|
+
def test_unknown_subcommand(self, tmp_path, monkeypatch):
|
|
238
|
+
path = tmp_path / "config.json"
|
|
239
|
+
original_init = Config.__init__
|
|
240
|
+
|
|
241
|
+
def patched_init(self, config_path=None):
|
|
242
|
+
original_init(self, config_path=path)
|
|
243
|
+
|
|
244
|
+
monkeypatch.setattr(Config, "__init__", patched_init)
|
|
245
|
+
|
|
246
|
+
with pytest.raises(SystemExit):
|
|
247
|
+
handle_alias_command(["bogus"])
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Argument parsing
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TestArgumentParsing:
|
|
256
|
+
"""Test argparse defaults and flag behaviour by importing the parser setup."""
|
|
257
|
+
|
|
258
|
+
def _parse(self, args):
|
|
259
|
+
"""Build the parser identical to main() and parse args."""
|
|
260
|
+
import argparse
|
|
261
|
+
from pathlib import Path
|
|
262
|
+
|
|
263
|
+
parser = argparse.ArgumentParser()
|
|
264
|
+
parser.add_argument("-a", "--all", action="store_true")
|
|
265
|
+
parser.add_argument("-v", "--verbose", action="store_true")
|
|
266
|
+
parser.add_argument("-p", "--path", type=Path, default=None)
|
|
267
|
+
parser.add_argument("--max-diff-lines", type=int, default=500)
|
|
268
|
+
parser.add_argument("-c", "--commit", action="store_true")
|
|
269
|
+
parser.add_argument("--copy", action="store_true")
|
|
270
|
+
parser.add_argument("--preview", action="store_true")
|
|
271
|
+
return parser.parse_args(args)
|
|
272
|
+
|
|
273
|
+
def test_defaults(self):
|
|
274
|
+
ns = self._parse([])
|
|
275
|
+
assert ns.all is False
|
|
276
|
+
assert ns.verbose is False
|
|
277
|
+
assert ns.path is None
|
|
278
|
+
assert ns.max_diff_lines == 500
|
|
279
|
+
assert ns.commit is False
|
|
280
|
+
assert ns.copy is False
|
|
281
|
+
assert ns.preview is False
|
|
282
|
+
|
|
283
|
+
def test_all_flag(self):
|
|
284
|
+
for flag in ["-a", "--all"]:
|
|
285
|
+
ns = self._parse([flag])
|
|
286
|
+
assert ns.all is True
|
|
287
|
+
|
|
288
|
+
def test_verbose_flag(self):
|
|
289
|
+
for flag in ["-v", "--verbose"]:
|
|
290
|
+
ns = self._parse([flag])
|
|
291
|
+
assert ns.verbose is True
|
|
292
|
+
|
|
293
|
+
def test_commit_flag(self):
|
|
294
|
+
for flag in ["-c", "--commit"]:
|
|
295
|
+
ns = self._parse([flag])
|
|
296
|
+
assert ns.commit is True
|
|
297
|
+
|
|
298
|
+
def test_copy_flag(self):
|
|
299
|
+
ns = self._parse(["--copy"])
|
|
300
|
+
assert ns.copy is True
|
|
301
|
+
|
|
302
|
+
def test_preview_flag(self):
|
|
303
|
+
ns = self._parse(["--preview"])
|
|
304
|
+
assert ns.preview is True
|
|
305
|
+
|
|
306
|
+
def test_max_diff_lines(self):
|
|
307
|
+
ns = self._parse(["--max-diff-lines", "1000"])
|
|
308
|
+
assert ns.max_diff_lines == 1000
|
|
309
|
+
|
|
310
|
+
def test_combined_flags(self):
|
|
311
|
+
ns = self._parse(["-a", "-v", "-c", "--max-diff-lines", "200"])
|
|
312
|
+
assert ns.all is True
|
|
313
|
+
assert ns.verbose is True
|
|
314
|
+
assert ns.commit is True
|
|
315
|
+
assert ns.max_diff_lines == 200
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|