git-copilot-commit 0.5.0__tar.gz → 0.5.2__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.
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/.justfile +1 -1
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/PKG-INFO +34 -25
- git_copilot_commit-0.5.2/README.md +191 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/src/git_copilot_commit/cli.py +30 -62
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/src/git_copilot_commit/git.py +43 -13
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/src/git_copilot_commit/github_copilot.py +205 -134
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/src/git_copilot_commit/prompts/split-commit-planner-prompt.md +0 -1
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/src/git_copilot_commit/settings.py +9 -3
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/src/git_copilot_commit/split_commits.py +26 -55
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/tests/test_cli.py +171 -21
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/tests/test_git.py +63 -1
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/tests/test_github_copilot_utils.py +141 -1
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/tests/test_settings.py +21 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/tests/test_split_commits.py +7 -81
- git_copilot_commit-0.5.0/README.md +0 -182
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/.github/dependabot.yml +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/.github/workflows/ci.yml +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/.gitignore +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/.python-version +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/LICENSE +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/pyproject.toml +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/src/git_copilot_commit/__init__.py +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/src/git_copilot_commit/prompts/commit-message-generator-prompt.md +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/src/git_copilot_commit/py.typed +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/src/git_copilot_commit/version.py +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/tests/conftest.py +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/uv.lock +0 -0
- {git_copilot_commit-0.5.0 → git_copilot_commit-0.5.2}/vhs/demo.vhs +0 -0
|
@@ -38,7 +38,7 @@ bump type="patch":
|
|
|
38
38
|
new_version=$(just next-version {{type}})
|
|
39
39
|
echo "New version: $new_version"
|
|
40
40
|
|
|
41
|
-
git commit --allow-empty -m "Bump version to $new_version"
|
|
41
|
+
git commit --allow-empty -m "chore(build): Bump version to $new_version"
|
|
42
42
|
git tag "$new_version"
|
|
43
43
|
|
|
44
44
|
echo "✓ Created commit and tag for $new_version"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-copilot-commit
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Automatically generate and commit changes using GitHub Copilot
|
|
5
5
|
Author-email: Dheepak Krishnamurthy <1813121+kdheepak@users.noreply.github.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -14,6 +14,10 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
|
|
15
15
|
# `git-copilot-commit`
|
|
16
16
|
|
|
17
|
+
[](https://github.com/kdheepak/git-copilot-commit/actions/workflows/ci.yml)
|
|
18
|
+
[](https://pypi.org/project/git-copilot-commit/)
|
|
19
|
+
[](https://github.com/kdheepak/git-copilot-commit/blob/main/LICENSE)
|
|
20
|
+
|
|
17
21
|
AI-powered Git commit assistant that generates conventional commit messages using GitHub Copilot.
|
|
18
22
|
|
|
19
23
|

|
|
@@ -21,13 +25,13 @@ AI-powered Git commit assistant that generates conventional commit messages usin
|
|
|
21
25
|
## Features
|
|
22
26
|
|
|
23
27
|
- Generates commit messages based on your staged changes
|
|
24
|
-
- Supports multiple
|
|
28
|
+
- Supports multiple LLM models: GPT-4, Claude, Gemini, and more
|
|
25
29
|
- Allows editing of generated messages before committing
|
|
26
30
|
- Follows the [Conventional Commits](https://www.conventionalcommits.org/) standard
|
|
27
31
|
|
|
28
32
|
## Installation
|
|
29
33
|
|
|
30
|
-
### Install the tool using [`uv`]
|
|
34
|
+
### Install the tool using [`uv`]
|
|
31
35
|
|
|
32
36
|
Install [`uv`]:
|
|
33
37
|
|
|
@@ -39,14 +43,15 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
|
39
43
|
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
You can
|
|
46
|
+
You can run the latest version of tool directly every time by invoking this one command:
|
|
43
47
|
|
|
44
48
|
```bash
|
|
45
|
-
#
|
|
49
|
+
# Every invocation installs latest version into temporary environment and runs --help
|
|
46
50
|
uvx git-copilot-commit --help
|
|
47
51
|
```
|
|
48
52
|
|
|
49
|
-
Alternatively, you can install into a global isolated environment
|
|
53
|
+
Alternatively, you can install the tool once into a global isolated environment
|
|
54
|
+
and run `git-copilot-commit` to invoke it:
|
|
50
55
|
|
|
51
56
|
```bash
|
|
52
57
|
# Install into global isolated environment
|
|
@@ -58,14 +63,6 @@ git-copilot-commit --help
|
|
|
58
63
|
|
|
59
64
|
[`uv`]: https://github.com/astral-sh/uv
|
|
60
65
|
|
|
61
|
-
### Install with `pipx`
|
|
62
|
-
|
|
63
|
-
If you prefer to use `pipx`:
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
pipx install git-copilot-commit
|
|
67
|
-
```
|
|
68
|
-
|
|
69
66
|
## Prerequisites
|
|
70
67
|
|
|
71
68
|
- Active GitHub Copilot subscription
|
|
@@ -100,17 +97,26 @@ pipx install git-copilot-commit
|
|
|
100
97
|
|
|
101
98
|
```bash
|
|
102
99
|
$ uvx git-copilot-commit commit --help
|
|
103
|
-
Usage: git-copilot-commit commit [OPTIONS]
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
Usage: git-copilot-commit commit [OPTIONS]
|
|
102
|
+
|
|
103
|
+
Generate commit message based on changes in the current git repository and commit them.
|
|
104
|
+
|
|
105
|
+
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
106
|
+
│ --all -a Stage all files before committing │
|
|
107
|
+
│ --split Split staged hunks into multiple commits automatically. │
|
|
108
|
+
│ Pass `--split=N` to express a preference for N commits. │
|
|
109
|
+
│ --model -m MODEL_ID Model to use for generating commit message │
|
|
110
|
+
│ --yes -y Automatically accept the generated commit message │
|
|
111
|
+
│ --context -c TEXT Optional user-provided context to guide commit message │
|
|
112
|
+
│ --ca-bundle PATH Path to a custom CA bundle (PEM) │
|
|
113
|
+
│ --insecure Disable SSL certificate verification. │
|
|
114
|
+
│ --native-tls --no-native-tls Use the OS's native certificate store via 'truststore' │
|
|
115
|
+
│ for httpx instead of the Python bundle. Ignored if │
|
|
116
|
+
│ --ca-bundle or --insecure is used. │
|
|
117
|
+
│ [default: no-native-tls] │
|
|
118
|
+
│ --help Show this message and exit. │
|
|
119
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
114
120
|
```
|
|
115
121
|
|
|
116
122
|
## Examples
|
|
@@ -139,7 +145,7 @@ Split staged hunks into separate commits:
|
|
|
139
145
|
uvx git-copilot-commit commit --split
|
|
140
146
|
```
|
|
141
147
|
|
|
142
|
-
Prefer
|
|
148
|
+
Prefer two commits:
|
|
143
149
|
|
|
144
150
|
```bash
|
|
145
151
|
uvx git-copilot-commit commit --split 2
|
|
@@ -194,3 +200,6 @@ git ai-commit --all --yes --model claude-3.5-sonnet
|
|
|
194
200
|
> ```bash
|
|
195
201
|
> git config --global diff.context 3
|
|
196
202
|
> ```
|
|
203
|
+
>
|
|
204
|
+
> This may be useful because this tool sends the diffs with surrounding context
|
|
205
|
+
> to the LLM for generating a commit message
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# `git-copilot-commit`
|
|
2
|
+
|
|
3
|
+
[](https://github.com/kdheepak/git-copilot-commit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/git-copilot-commit/)
|
|
5
|
+
[](https://github.com/kdheepak/git-copilot-commit/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
AI-powered Git commit assistant that generates conventional commit messages using GitHub Copilot.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Generates commit messages based on your staged changes
|
|
14
|
+
- Supports multiple LLM models: GPT-4, Claude, Gemini, and more
|
|
15
|
+
- Allows editing of generated messages before committing
|
|
16
|
+
- Follows the [Conventional Commits](https://www.conventionalcommits.org/) standard
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### Install the tool using [`uv`]
|
|
21
|
+
|
|
22
|
+
Install [`uv`]:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# On macOS and Linux.
|
|
26
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
27
|
+
|
|
28
|
+
# On Windows.
|
|
29
|
+
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You can run the latest version of tool directly every time by invoking this one command:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Every invocation installs latest version into temporary environment and runs --help
|
|
36
|
+
uvx git-copilot-commit --help
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Alternatively, you can install the tool once into a global isolated environment
|
|
40
|
+
and run `git-copilot-commit` to invoke it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Install into global isolated environment
|
|
44
|
+
uv tool install git-copilot-commit
|
|
45
|
+
|
|
46
|
+
# Run --help to see available commands
|
|
47
|
+
git-copilot-commit --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
[`uv`]: https://github.com/astral-sh/uv
|
|
51
|
+
|
|
52
|
+
## Prerequisites
|
|
53
|
+
|
|
54
|
+
- Active GitHub Copilot subscription
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
1. Authenticate with GitHub Copilot:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uvx git-copilot-commit authenticate
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If your cached GitHub token is revoked or expires, refresh it with:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uvx git-copilot-commit authenticate --force
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
2. Make changes in your repository.
|
|
71
|
+
|
|
72
|
+
3. Generate and commit:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uvx git-copilot-commit commit
|
|
76
|
+
# Or, if you want to stage all files and accept the generated commit message, use:
|
|
77
|
+
uvx git-copilot-commit commit --all --yes
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
### Commit changes
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
$ uvx git-copilot-commit commit --help
|
|
86
|
+
|
|
87
|
+
Usage: git-copilot-commit commit [OPTIONS]
|
|
88
|
+
|
|
89
|
+
Generate commit message based on changes in the current git repository and commit them.
|
|
90
|
+
|
|
91
|
+
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
92
|
+
│ --all -a Stage all files before committing │
|
|
93
|
+
│ --split Split staged hunks into multiple commits automatically. │
|
|
94
|
+
│ Pass `--split=N` to express a preference for N commits. │
|
|
95
|
+
│ --model -m MODEL_ID Model to use for generating commit message │
|
|
96
|
+
│ --yes -y Automatically accept the generated commit message │
|
|
97
|
+
│ --context -c TEXT Optional user-provided context to guide commit message │
|
|
98
|
+
│ --ca-bundle PATH Path to a custom CA bundle (PEM) │
|
|
99
|
+
│ --insecure Disable SSL certificate verification. │
|
|
100
|
+
│ --native-tls --no-native-tls Use the OS's native certificate store via 'truststore' │
|
|
101
|
+
│ for httpx instead of the Python bundle. Ignored if │
|
|
102
|
+
│ --ca-bundle or --insecure is used. │
|
|
103
|
+
│ [default: no-native-tls] │
|
|
104
|
+
│ --help Show this message and exit. │
|
|
105
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Examples
|
|
109
|
+
|
|
110
|
+
Commit all changes:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uvx git-copilot-commit commit --all
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Accept the generated commit message without editing:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uvx git-copilot-commit commit --yes
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use a specific model:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
uvx git-copilot-commit commit --model claude-3.5-sonnet
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Split staged hunks into separate commits:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
uvx git-copilot-commit commit --split
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Prefer two commits:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
uvx git-copilot-commit commit --split 2
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Commit Message Format
|
|
141
|
+
|
|
142
|
+
Follows [Conventional Commits](https://www.conventionalcommits.org/):
|
|
143
|
+
|
|
144
|
+
```plaintext
|
|
145
|
+
<type>[optional scope]: <description>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Types:**
|
|
149
|
+
|
|
150
|
+
- `feat`: New feature
|
|
151
|
+
- `fix`: Bug fix
|
|
152
|
+
- `docs`: Documentation
|
|
153
|
+
- `style`: Formatting only
|
|
154
|
+
- `refactor`: Code restructure
|
|
155
|
+
- `perf`: Performance
|
|
156
|
+
- `test`: Tests
|
|
157
|
+
- `chore`: Maintenance
|
|
158
|
+
- `revert`: Revert changes
|
|
159
|
+
|
|
160
|
+
## Git Configuration
|
|
161
|
+
|
|
162
|
+
Add a git alias by adding the following to your `~/.gitconfig`:
|
|
163
|
+
|
|
164
|
+
```ini
|
|
165
|
+
[alias]
|
|
166
|
+
ai-commit = "!f() { uvx git-copilot-commit commit $@; }; f"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Now you can run to review the message before committing:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
git ai-commit
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Alternatively, you can stage all files and auto accept the commit message and
|
|
176
|
+
specify which model should be used to generate the commit in one CLI invocation.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
git ai-commit --all --yes --model claude-3.5-sonnet
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
> [!TIP]
|
|
183
|
+
>
|
|
184
|
+
> Show more context in diffs by running the following command:
|
|
185
|
+
>
|
|
186
|
+
> ```bash
|
|
187
|
+
> git config --global diff.context 3
|
|
188
|
+
> ```
|
|
189
|
+
>
|
|
190
|
+
> This may be useful because this tool sends the diffs with surrounding context
|
|
191
|
+
> to the LLM for generating a commit message
|
|
@@ -19,11 +19,9 @@ from .git import GitRepository, GitError, GitStatus, NotAGitRepositoryError
|
|
|
19
19
|
from .split_commits import (
|
|
20
20
|
PatchUnit,
|
|
21
21
|
SplitCommitPlan,
|
|
22
|
-
SplitCommitLimitExceededError,
|
|
23
22
|
SplitPlanningError,
|
|
24
23
|
build_split_plan_prompt,
|
|
25
24
|
build_status_for_patch_units,
|
|
26
|
-
evaluate_auto_split,
|
|
27
25
|
extract_patch_units,
|
|
28
26
|
group_patch_units,
|
|
29
27
|
parse_split_plan_response,
|
|
@@ -37,7 +35,6 @@ app = typer.Typer(help=__doc__, add_completion=False)
|
|
|
37
35
|
|
|
38
36
|
COMMIT_MESSAGE_PROMPT_FILENAME = "commit-message-generator-prompt.md"
|
|
39
37
|
SPLIT_COMMIT_PLANNER_PROMPT_FILENAME = "split-commit-planner-prompt.md"
|
|
40
|
-
DEFAULT_AUTO_MAX_COMMITS = 10
|
|
41
38
|
SPLIT_DIFF_ARGS = [
|
|
42
39
|
"--binary",
|
|
43
40
|
"--full-index",
|
|
@@ -75,7 +72,7 @@ SplitOption = Annotated[
|
|
|
75
72
|
"--split",
|
|
76
73
|
help=(
|
|
77
74
|
"Split staged hunks into multiple commits automatically. Pass "
|
|
78
|
-
"`--split=N` to
|
|
75
|
+
"`--split=N` to express a preference for N commits."
|
|
79
76
|
),
|
|
80
77
|
),
|
|
81
78
|
]
|
|
@@ -128,11 +125,7 @@ def preprocess_cli_args(args: Sequence[str]) -> list[str]:
|
|
|
128
125
|
index += 1
|
|
129
126
|
continue
|
|
130
127
|
|
|
131
|
-
if (
|
|
132
|
-
in_commit_command
|
|
133
|
-
and arg == "--split"
|
|
134
|
-
and index + 1 < len(args)
|
|
135
|
-
):
|
|
128
|
+
if in_commit_command and arg == "--split" and index + 1 < len(args):
|
|
136
129
|
split_value = args[index + 1].strip().lower()
|
|
137
130
|
if split_value == "auto":
|
|
138
131
|
processed_args.append("--split")
|
|
@@ -352,21 +345,6 @@ def generate_commit_message_for_status(
|
|
|
352
345
|
)
|
|
353
346
|
|
|
354
347
|
|
|
355
|
-
def generate_commit_message(
|
|
356
|
-
repo: GitRepository,
|
|
357
|
-
model: str | None = None,
|
|
358
|
-
context: str = "",
|
|
359
|
-
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
360
|
-
) -> str:
|
|
361
|
-
"""Generate a conventional commit message using the repository's staged diff."""
|
|
362
|
-
return generate_commit_message_for_status(
|
|
363
|
-
repo.get_status(),
|
|
364
|
-
model=model,
|
|
365
|
-
context=context,
|
|
366
|
-
http_client_config=http_client_config,
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
|
|
370
348
|
def commit_with_retry_no_verify(
|
|
371
349
|
repo: GitRepository,
|
|
372
350
|
message: str,
|
|
@@ -471,7 +449,6 @@ def request_split_commit_plan(
|
|
|
471
449
|
status: GitStatus,
|
|
472
450
|
patch_units: tuple[PatchUnit, ...],
|
|
473
451
|
*,
|
|
474
|
-
max_commits: int,
|
|
475
452
|
preferred_commits: int | None = None,
|
|
476
453
|
model: str | None = None,
|
|
477
454
|
context: str = "",
|
|
@@ -482,7 +459,6 @@ def request_split_commit_plan(
|
|
|
482
459
|
planner_prompt = build_split_plan_prompt(
|
|
483
460
|
status,
|
|
484
461
|
patch_units,
|
|
485
|
-
max_commits=max_commits,
|
|
486
462
|
preferred_commits=preferred_commits,
|
|
487
463
|
context=context,
|
|
488
464
|
)
|
|
@@ -499,7 +475,6 @@ def request_split_commit_plan(
|
|
|
499
475
|
return parse_split_plan_response(
|
|
500
476
|
response,
|
|
501
477
|
patch_units,
|
|
502
|
-
max_commits=max_commits,
|
|
503
478
|
)
|
|
504
479
|
except github_copilot.CopilotError as exc:
|
|
505
480
|
print_copilot_error("Could not generate a split commit plan", exc)
|
|
@@ -541,25 +516,31 @@ def request_split_commit_messages(
|
|
|
541
516
|
raise typer.Exit(1)
|
|
542
517
|
|
|
543
518
|
|
|
544
|
-
def
|
|
545
|
-
|
|
519
|
+
def confirm_split_commit_count(
|
|
520
|
+
plan: SplitCommitPlan,
|
|
521
|
+
*,
|
|
522
|
+
preferred_commits: int,
|
|
523
|
+
yes: bool = False,
|
|
546
524
|
) -> SplitCommitPlan:
|
|
547
|
-
"""Ask whether to proceed when the planner exceeds the
|
|
525
|
+
"""Ask whether to proceed when the planner exceeds the preferred count."""
|
|
526
|
+
actual_commits = len(plan.commits)
|
|
527
|
+
if actual_commits <= preferred_commits:
|
|
528
|
+
return plan
|
|
529
|
+
|
|
548
530
|
console.print(
|
|
549
|
-
|
|
531
|
+
"[yellow]Split planning produced "
|
|
532
|
+
f"{actual_commits} commits, exceeding the preferred count of "
|
|
533
|
+
f"{preferred_commits}.[/yellow]"
|
|
550
534
|
)
|
|
551
535
|
|
|
552
536
|
if yes:
|
|
553
|
-
|
|
554
|
-
"[red]Cannot ask whether to proceed because --yes was used. Re-run without --yes to review the larger plan.[/red]"
|
|
555
|
-
)
|
|
556
|
-
raise typer.Exit(1)
|
|
537
|
+
return plan
|
|
557
538
|
|
|
558
539
|
if Confirm.ask(
|
|
559
|
-
f"Proceed with [bold]{
|
|
540
|
+
f"Proceed with [bold]{actual_commits} commits[/] anyway?",
|
|
560
541
|
default=False,
|
|
561
542
|
):
|
|
562
|
-
return
|
|
543
|
+
return plan
|
|
563
544
|
|
|
564
545
|
console.print("Split commit plan cancelled.")
|
|
565
546
|
raise typer.Exit()
|
|
@@ -747,44 +728,24 @@ def handle_split_commit_flow(
|
|
|
747
728
|
return
|
|
748
729
|
|
|
749
730
|
if preferred_commits is None:
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
"[yellow]Auto split not triggered: "
|
|
754
|
-
f"{reason}. Creating a single commit. Use [bold]--split N[/] to suggest an upper bound.[/yellow]"
|
|
755
|
-
)
|
|
756
|
-
handle_single_commit_flow(
|
|
757
|
-
repo,
|
|
758
|
-
status,
|
|
759
|
-
model=model,
|
|
760
|
-
yes=yes,
|
|
761
|
-
context=context,
|
|
762
|
-
http_client_config=http_client_config,
|
|
763
|
-
)
|
|
764
|
-
return
|
|
765
|
-
|
|
766
|
-
console.print(f"[yellow]Auto split triggered: {reason}.[/yellow]")
|
|
731
|
+
console.print(
|
|
732
|
+
"[yellow]Planning split commits from the staged patch units.[/yellow]"
|
|
733
|
+
)
|
|
767
734
|
else:
|
|
768
735
|
console.print(
|
|
769
|
-
|
|
736
|
+
"[yellow]Planning split commits with a preference for "
|
|
737
|
+
f"{preferred_commits} commits.[/yellow]"
|
|
770
738
|
)
|
|
771
739
|
|
|
772
740
|
try:
|
|
773
741
|
split_plan = request_split_commit_plan(
|
|
774
742
|
status,
|
|
775
743
|
patch_units,
|
|
776
|
-
max_commits=(
|
|
777
|
-
DEFAULT_AUTO_MAX_COMMITS
|
|
778
|
-
if preferred_commits is None
|
|
779
|
-
else preferred_commits
|
|
780
|
-
),
|
|
781
744
|
preferred_commits=preferred_commits,
|
|
782
745
|
model=model,
|
|
783
746
|
context=context,
|
|
784
747
|
http_client_config=http_client_config,
|
|
785
748
|
)
|
|
786
|
-
except SplitCommitLimitExceededError as exc:
|
|
787
|
-
split_plan = resolve_split_commit_limit(exc, yes=yes)
|
|
788
749
|
except SplitPlanningError as exc:
|
|
789
750
|
console.print(
|
|
790
751
|
"[yellow]Split planning returned an invalid plan; falling back to a single commit.[/yellow]"
|
|
@@ -800,6 +761,13 @@ def handle_split_commit_flow(
|
|
|
800
761
|
)
|
|
801
762
|
return
|
|
802
763
|
|
|
764
|
+
if preferred_commits is not None:
|
|
765
|
+
split_plan = confirm_split_commit_count(
|
|
766
|
+
split_plan,
|
|
767
|
+
preferred_commits=preferred_commits,
|
|
768
|
+
yes=yes,
|
|
769
|
+
)
|
|
770
|
+
|
|
803
771
|
prepared_commits = request_split_commit_messages(
|
|
804
772
|
split_plan,
|
|
805
773
|
patch_units,
|
|
@@ -121,16 +121,31 @@ class GitRepository:
|
|
|
121
121
|
Raises:
|
|
122
122
|
NotAGitRepositoryError: If the path is not a git repository.
|
|
123
123
|
"""
|
|
124
|
-
self.
|
|
124
|
+
self.cwd = (repo_path or Path.cwd()).resolve()
|
|
125
125
|
self.timeout = timeout
|
|
126
|
-
self.
|
|
126
|
+
self.repo_path = self._resolve_repo_root()
|
|
127
127
|
|
|
128
|
-
def
|
|
129
|
-
"""
|
|
128
|
+
def _resolve_repo_root(self) -> Path:
|
|
129
|
+
"""Resolve and cache the repository top-level path."""
|
|
130
130
|
try:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
133
|
+
cwd=self.cwd,
|
|
134
|
+
capture_output=True,
|
|
135
|
+
text=True,
|
|
136
|
+
timeout=self.timeout,
|
|
137
|
+
check=True,
|
|
138
|
+
)
|
|
139
|
+
except subprocess.CalledProcessError:
|
|
140
|
+
raise NotAGitRepositoryError(f"{self.cwd} is not a git repository")
|
|
141
|
+
except subprocess.TimeoutExpired:
|
|
142
|
+
raise GitCommandError("Git command timed out: git rev-parse --show-toplevel")
|
|
143
|
+
|
|
144
|
+
repo_root = result.stdout.strip()
|
|
145
|
+
if not repo_root:
|
|
146
|
+
raise NotAGitRepositoryError(f"{self.cwd} is not a git repository")
|
|
147
|
+
|
|
148
|
+
return Path(repo_root)
|
|
134
149
|
|
|
135
150
|
def _run_git_command(
|
|
136
151
|
self,
|
|
@@ -188,6 +203,21 @@ class GitRepository:
|
|
|
188
203
|
merged_env.update(env)
|
|
189
204
|
return merged_env
|
|
190
205
|
|
|
206
|
+
def _normalize_paths(self, paths: list[str]) -> list[str]:
|
|
207
|
+
"""Normalize user paths relative to the repository root."""
|
|
208
|
+
normalized_paths: list[str] = []
|
|
209
|
+
for path in paths:
|
|
210
|
+
path_obj = Path(path)
|
|
211
|
+
if path_obj.is_absolute():
|
|
212
|
+
normalized_paths.append(str(path_obj))
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
normalized_paths.append(
|
|
216
|
+
os.path.relpath(self.cwd / path_obj, start=self.repo_path)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return normalized_paths
|
|
220
|
+
|
|
191
221
|
def get_status(self, env: Mapping[str, str] | None = None) -> GitStatus:
|
|
192
222
|
"""
|
|
193
223
|
Get comprehensive git status information.
|
|
@@ -263,16 +293,16 @@ class GitRepository:
|
|
|
263
293
|
Stage files for commit.
|
|
264
294
|
|
|
265
295
|
Args:
|
|
266
|
-
paths: List of file paths to stage. If None, stages all files
|
|
296
|
+
paths: List of file paths to stage. If None, stages all files.
|
|
267
297
|
"""
|
|
268
298
|
if paths is None:
|
|
269
|
-
self._run_git_command(["add", "
|
|
299
|
+
self._run_git_command(["add", "--all"])
|
|
270
300
|
else:
|
|
271
|
-
self._run_git_command(["add"] + paths)
|
|
301
|
+
self._run_git_command(["add"] + self._normalize_paths(paths))
|
|
272
302
|
|
|
273
303
|
def stage_modified(self) -> None:
|
|
274
|
-
"""Stage all modified files
|
|
275
|
-
self._run_git_command(["add", "
|
|
304
|
+
"""Stage all modified tracked files."""
|
|
305
|
+
self._run_git_command(["add", "--update"])
|
|
276
306
|
|
|
277
307
|
def unstage_files(self, paths: list[str] | None = None) -> None:
|
|
278
308
|
"""
|
|
@@ -284,7 +314,7 @@ class GitRepository:
|
|
|
284
314
|
if paths is None:
|
|
285
315
|
self._run_git_command(["reset", "HEAD"])
|
|
286
316
|
else:
|
|
287
|
-
self._run_git_command(["reset", "HEAD"] + paths)
|
|
317
|
+
self._run_git_command(["reset", "HEAD"] + self._normalize_paths(paths))
|
|
288
318
|
|
|
289
319
|
def create_alternate_index(self, from_ref: str = "HEAD") -> AlternateGitIndex:
|
|
290
320
|
"""Create a temporary git index initialized from the provided ref."""
|