devcommit 0.1.4.6__tar.gz → 0.1.4.7__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.
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/PKG-INFO +87 -26
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/README.md +86 -25
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/devcommit/main.py +153 -17
- devcommit-0.1.4.7/devcommit/utils/git.py +326 -0
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/devcommit/utils/parser.py +12 -0
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/pyproject.toml +1 -1
- devcommit-0.1.4.6/devcommit/utils/git.py +0 -170
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/COPYING +0 -0
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/devcommit/__init__.py +0 -0
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/devcommit/app/__init__.py +0 -0
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/devcommit/app/ai_providers.py +0 -0
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/devcommit/app/gemini_ai.py +0 -0
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/devcommit/app/prompt.py +0 -0
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/devcommit/utils/__init__.py +0 -0
- {devcommit-0.1.4.6 → devcommit-0.1.4.7}/devcommit/utils/logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devcommit
|
|
3
|
-
Version: 0.1.4.
|
|
3
|
+
Version: 0.1.4.7
|
|
4
4
|
Summary: AI-powered git commit message generator
|
|
5
5
|
License: GNU GENERAL PUBLIC LICENSE
|
|
6
6
|
Version 3, 29 June 2007
|
|
@@ -721,24 +721,27 @@ A command-line AI tool for autocommits.
|
|
|
721
721
|
|
|
722
722
|
## Installation
|
|
723
723
|
|
|
724
|
-
1. **Install DevCommit**
|
|
725
|
-
|
|
724
|
+
1. **Install DevCommit**
|
|
725
|
+
|
|
726
726
|
**Option 1: Using pip (local installation)**
|
|
727
|
+
|
|
727
728
|
```bash
|
|
728
729
|
pip install devcommit
|
|
729
730
|
```
|
|
730
|
-
|
|
731
|
+
|
|
731
732
|
**Option 2: Using pipx (global installation, recommended)**
|
|
733
|
+
|
|
732
734
|
```bash
|
|
733
735
|
# Install pipx if you don't have it
|
|
734
736
|
python3 -m pip install --user pipx
|
|
735
737
|
python3 -m pipx ensurepath
|
|
736
|
-
|
|
738
|
+
|
|
737
739
|
# Install DevCommit globally
|
|
738
740
|
pipx install devcommit
|
|
739
741
|
```
|
|
742
|
+
|
|
740
743
|
> **💡 Why pipx?** pipx installs CLI tools in isolated environments, preventing dependency conflicts while making them globally available.
|
|
741
|
-
|
|
744
|
+
|
|
742
745
|
**All AI providers are included by default!** ✅ Gemini, OpenAI, Groq, Anthropic, Ollama, and Custom API support.
|
|
743
746
|
|
|
744
747
|
2. **Set Up Configuration (Required: API Key)**
|
|
@@ -747,19 +750,21 @@ A command-line AI tool for autocommits.
|
|
|
747
750
|
**Priority Order:** `.dcommit` file → Environment Variables → Defaults
|
|
748
751
|
|
|
749
752
|
### Option 1: Environment Variables (Quickest)
|
|
753
|
+
|
|
750
754
|
```bash
|
|
751
755
|
# Using Gemini (default)
|
|
752
756
|
export GEMINI_API_KEY='your-api-key-here'
|
|
753
|
-
|
|
757
|
+
|
|
754
758
|
# Or using Groq (recommended for free tier)
|
|
755
759
|
export AI_PROVIDER='groq'
|
|
756
760
|
export GROQ_API_KEY='your-groq-key'
|
|
757
|
-
|
|
761
|
+
|
|
758
762
|
# Add to ~/.bashrc or ~/.zshrc for persistence
|
|
759
763
|
echo "export GEMINI_API_KEY='your-key'" >> ~/.bashrc
|
|
760
764
|
```
|
|
761
765
|
|
|
762
766
|
### Option 2: .dcommit File (Home Directory)
|
|
767
|
+
|
|
763
768
|
```bash
|
|
764
769
|
cat > ~/.dcommit << 'EOF'
|
|
765
770
|
GEMINI_API_KEY = your-api-key-here
|
|
@@ -772,6 +777,7 @@ A command-line AI tool for autocommits.
|
|
|
772
777
|
```
|
|
773
778
|
|
|
774
779
|
### Option 3: .dcommit File (Virtual Environment)
|
|
780
|
+
|
|
775
781
|
```bash
|
|
776
782
|
mkdir -p $VIRTUAL_ENV/config
|
|
777
783
|
cat > $VIRTUAL_ENV/config/.dcommit << 'EOF'
|
|
@@ -797,6 +803,7 @@ devcommit
|
|
|
797
803
|
### Basic Usage
|
|
798
804
|
|
|
799
805
|
- **Stage all changes and commit:**
|
|
806
|
+
|
|
800
807
|
```bash
|
|
801
808
|
devcommit --stageAll
|
|
802
809
|
```
|
|
@@ -823,6 +830,7 @@ You can set your preferred commit mode in the `.dcommit` configuration file usin
|
|
|
823
830
|
#### Command-Line Usage
|
|
824
831
|
|
|
825
832
|
- **Interactive mode (auto):** When you have changes in multiple directories, DevCommit will automatically ask if you want to:
|
|
833
|
+
|
|
826
834
|
- Create one commit for all changes (global commit)
|
|
827
835
|
- Create separate commits per directory
|
|
828
836
|
|
|
@@ -834,10 +842,40 @@ You can set your preferred commit mode in the `.dcommit` configuration file usin
|
|
|
834
842
|
```
|
|
835
843
|
|
|
836
844
|
When using directory-based commits, you can:
|
|
845
|
+
|
|
837
846
|
1. Select which directories to commit (use Space to select, Enter to confirm)
|
|
838
847
|
2. For each selected directory, review and choose a commit message
|
|
839
848
|
3. Each directory gets its own commit with AI-generated messages based on its changes
|
|
840
849
|
|
|
850
|
+
### Commit Specific Files or Folders
|
|
851
|
+
|
|
852
|
+
DevCommit allows you to commit specific files or folders. This is useful when you want to commit only certain changes without affecting other staged files.
|
|
853
|
+
|
|
854
|
+
**Usage:**
|
|
855
|
+
|
|
856
|
+
```bash
|
|
857
|
+
# Commit specific files (must be staged first)
|
|
858
|
+
git add file1.py file2.py
|
|
859
|
+
devcommit --files file1.py file2.py
|
|
860
|
+
|
|
861
|
+
# Stage and commit specific files in one command
|
|
862
|
+
devcommit --stageAll --files file1.py file2.py
|
|
863
|
+
|
|
864
|
+
# Commit specific folders (must be staged first)
|
|
865
|
+
git add src/ tests/
|
|
866
|
+
devcommit --files src/ tests/
|
|
867
|
+
|
|
868
|
+
# Short form
|
|
869
|
+
devcommit -s -f file1.py file2.py
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
When using `--files` or `-f`:
|
|
873
|
+
|
|
874
|
+
- Without `--stageAll`: Only commits files that are already staged (filters staged files to match specified paths)
|
|
875
|
+
- With `--stageAll`: Stages the specified files/folders and then commits them
|
|
876
|
+
- AI generates commit messages based on changes in those files
|
|
877
|
+
- Works with both individual files and entire directories
|
|
878
|
+
|
|
841
879
|
### Additional Options
|
|
842
880
|
|
|
843
881
|
- `--excludeFiles` or `-e`: Exclude specific files from the diff
|
|
@@ -845,6 +883,8 @@ When using directory-based commits, you can:
|
|
|
845
883
|
- `--commitType` or `-t`: Specify the type of commit (e.g., conventional)
|
|
846
884
|
- `--stageAll` or `-s`: Stage all changes before committing
|
|
847
885
|
- `--directory` or `-d`: Force directory-based commits
|
|
886
|
+
- `--files` or `-f`: Stage and commit specific files or folders (can specify multiple)
|
|
887
|
+
- `--push` or `-p`: Push commits to remote after committing
|
|
848
888
|
|
|
849
889
|
### Examples
|
|
850
890
|
|
|
@@ -857,24 +897,40 @@ devcommit --commitType conventional
|
|
|
857
897
|
|
|
858
898
|
# Exclude lock files
|
|
859
899
|
devcommit --excludeFiles package-lock.json yarn.lock
|
|
900
|
+
|
|
901
|
+
# Stage and commit specific files
|
|
902
|
+
devcommit --files file1.py file2.py
|
|
903
|
+
|
|
904
|
+
# Stage and commit specific folders
|
|
905
|
+
devcommit --files src/ tests/
|
|
906
|
+
|
|
907
|
+
# Stage and commit multiple files and folders at once
|
|
908
|
+
devcommit --files src/ file1.py tests/ config.json
|
|
909
|
+
|
|
910
|
+
# Commit and push
|
|
911
|
+
devcommit --push
|
|
912
|
+
|
|
913
|
+
# Commit specific files and push
|
|
914
|
+
devcommit --files file1.py file2.py --push
|
|
860
915
|
```
|
|
861
916
|
|
|
862
917
|
## AI Provider Support
|
|
863
918
|
|
|
864
919
|
DevCommit now supports **multiple AI providers**! Choose from:
|
|
865
920
|
|
|
866
|
-
| Provider
|
|
867
|
-
|
|
868
|
-
| 🆓 **Gemini**
|
|
869
|
-
| ⚡ **Groq**
|
|
870
|
-
| 🤖 **OpenAI**
|
|
871
|
-
| 🧠 **Anthropic** | Limited trial
|
|
872
|
-
| 🏠 **Ollama**
|
|
873
|
-
| 🔧 **Custom**
|
|
921
|
+
| Provider | Free Tier | Speed | Quality | Get API Key |
|
|
922
|
+
| ---------------- | ------------------ | ----------- | --------- | ------------------------------------------------- |
|
|
923
|
+
| 🆓 **Gemini** | 15 req/min, 1M/day | Fast | Good | [Get Key](https://aistudio.google.com/app/apikey) |
|
|
924
|
+
| ⚡ **Groq** | Very generous | **Fastest** | Good | [Get Key](https://console.groq.com/keys) |
|
|
925
|
+
| 🤖 **OpenAI** | $5 trial | Medium | **Best** | [Get Key](https://platform.openai.com/api-keys) |
|
|
926
|
+
| 🧠 **Anthropic** | Limited trial | Medium | Excellent | [Get Key](https://console.anthropic.com/) |
|
|
927
|
+
| 🏠 **Ollama** | **Unlimited** | Medium | Good | [Install](https://ollama.ai/) |
|
|
928
|
+
| 🔧 **Custom** | Varies | Varies | Varies | Your server |
|
|
874
929
|
|
|
875
930
|
### Quick Setup Examples
|
|
876
931
|
|
|
877
932
|
**Using Groq (Recommended for free tier):**
|
|
933
|
+
|
|
878
934
|
```bash
|
|
879
935
|
export AI_PROVIDER=groq
|
|
880
936
|
export GROQ_API_KEY='your-groq-api-key'
|
|
@@ -882,6 +938,7 @@ devcommit
|
|
|
882
938
|
```
|
|
883
939
|
|
|
884
940
|
**Using Ollama (Local, no API key needed):**
|
|
941
|
+
|
|
885
942
|
```bash
|
|
886
943
|
# Install Ollama: https://ollama.ai/
|
|
887
944
|
ollama pull llama3
|
|
@@ -890,6 +947,7 @@ devcommit
|
|
|
890
947
|
```
|
|
891
948
|
|
|
892
949
|
**Using Custom API:**
|
|
950
|
+
|
|
893
951
|
```bash
|
|
894
952
|
export AI_PROVIDER=custom
|
|
895
953
|
export CUSTOM_API_URL='http://localhost:8000/v1'
|
|
@@ -904,8 +962,8 @@ All configuration can be set via **environment variables** or **`.dcommit` file*
|
|
|
904
962
|
|
|
905
963
|
### AI Provider Settings
|
|
906
964
|
|
|
907
|
-
| Variable
|
|
908
|
-
|
|
965
|
+
| Variable | Description | Default | Options |
|
|
966
|
+
| ------------- | ----------------------- | -------- | ----------------------------------------------------------- |
|
|
909
967
|
| `AI_PROVIDER` | Which AI service to use | `gemini` | `gemini`, `openai`, `groq`, `anthropic`, `ollama`, `custom` |
|
|
910
968
|
|
|
911
969
|
### Provider-Specific Settings
|
|
@@ -949,21 +1007,23 @@ All configuration can be set via **environment variables** or **`.dcommit` file*
|
|
|
949
1007
|
|
|
950
1008
|
### General Settings
|
|
951
1009
|
|
|
952
|
-
| Variable
|
|
953
|
-
|
|
954
|
-
| `LOCALE`
|
|
955
|
-
| `MAX_NO`
|
|
956
|
-
| `COMMIT_TYPE`
|
|
957
|
-
| `COMMIT_MODE`
|
|
958
|
-
| `EXCLUDE_FILES` | Files to exclude from diff
|
|
959
|
-
| `MAX_TOKENS`
|
|
1010
|
+
| Variable | Description | Default | Options |
|
|
1011
|
+
| --------------- | ------------------------------------ | ------------------------------------------------------ | ---------------------------------------- |
|
|
1012
|
+
| `LOCALE` | Language for commit messages | `en-US` | Any locale code (e.g., `en`, `es`, `fr`) |
|
|
1013
|
+
| `MAX_NO` | Number of commit message suggestions | `1` | Any positive integer |
|
|
1014
|
+
| `COMMIT_TYPE` | Style of commit messages | `general` | `general`, `conventional`, etc. |
|
|
1015
|
+
| `COMMIT_MODE` | Default commit strategy | `auto` | `auto`, `directory`, `global` |
|
|
1016
|
+
| `EXCLUDE_FILES` | Files to exclude from diff | `package-lock.json, pnpm-lock.yaml, yarn.lock, *.lock` | Comma-separated file patterns |
|
|
1017
|
+
| `MAX_TOKENS` | Maximum tokens for AI response | `8192` | Any positive integer |
|
|
960
1018
|
|
|
961
1019
|
### Configuration Priority
|
|
1020
|
+
|
|
962
1021
|
1. **`.dcommit` file** (highest priority)
|
|
963
1022
|
2. **Environment variables**
|
|
964
1023
|
3. **Built-in defaults** (lowest priority)
|
|
965
1024
|
|
|
966
1025
|
### Using Environment Variables
|
|
1026
|
+
|
|
967
1027
|
```bash
|
|
968
1028
|
# Basic setup with Gemini (default)
|
|
969
1029
|
export GEMINI_API_KEY='your-api-key-here'
|
|
@@ -978,6 +1038,7 @@ export GROQ_API_KEY='your-groq-key'
|
|
|
978
1038
|
```
|
|
979
1039
|
|
|
980
1040
|
### Using .dcommit File
|
|
1041
|
+
|
|
981
1042
|
See `.dcommit.example` for a complete configuration template with all providers.
|
|
982
1043
|
|
|
983
1044
|
**Note:** The `.dcommit` file is **optional**. DevCommit will work with just environment variables!
|
|
@@ -16,24 +16,27 @@ A command-line AI tool for autocommits.
|
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
19
|
-
1. **Install DevCommit**
|
|
20
|
-
|
|
19
|
+
1. **Install DevCommit**
|
|
20
|
+
|
|
21
21
|
**Option 1: Using pip (local installation)**
|
|
22
|
+
|
|
22
23
|
```bash
|
|
23
24
|
pip install devcommit
|
|
24
25
|
```
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
**Option 2: Using pipx (global installation, recommended)**
|
|
28
|
+
|
|
27
29
|
```bash
|
|
28
30
|
# Install pipx if you don't have it
|
|
29
31
|
python3 -m pip install --user pipx
|
|
30
32
|
python3 -m pipx ensurepath
|
|
31
|
-
|
|
33
|
+
|
|
32
34
|
# Install DevCommit globally
|
|
33
35
|
pipx install devcommit
|
|
34
36
|
```
|
|
37
|
+
|
|
35
38
|
> **💡 Why pipx?** pipx installs CLI tools in isolated environments, preventing dependency conflicts while making them globally available.
|
|
36
|
-
|
|
39
|
+
|
|
37
40
|
**All AI providers are included by default!** ✅ Gemini, OpenAI, Groq, Anthropic, Ollama, and Custom API support.
|
|
38
41
|
|
|
39
42
|
2. **Set Up Configuration (Required: API Key)**
|
|
@@ -42,19 +45,21 @@ A command-line AI tool for autocommits.
|
|
|
42
45
|
**Priority Order:** `.dcommit` file → Environment Variables → Defaults
|
|
43
46
|
|
|
44
47
|
### Option 1: Environment Variables (Quickest)
|
|
48
|
+
|
|
45
49
|
```bash
|
|
46
50
|
# Using Gemini (default)
|
|
47
51
|
export GEMINI_API_KEY='your-api-key-here'
|
|
48
|
-
|
|
52
|
+
|
|
49
53
|
# Or using Groq (recommended for free tier)
|
|
50
54
|
export AI_PROVIDER='groq'
|
|
51
55
|
export GROQ_API_KEY='your-groq-key'
|
|
52
|
-
|
|
56
|
+
|
|
53
57
|
# Add to ~/.bashrc or ~/.zshrc for persistence
|
|
54
58
|
echo "export GEMINI_API_KEY='your-key'" >> ~/.bashrc
|
|
55
59
|
```
|
|
56
60
|
|
|
57
61
|
### Option 2: .dcommit File (Home Directory)
|
|
62
|
+
|
|
58
63
|
```bash
|
|
59
64
|
cat > ~/.dcommit << 'EOF'
|
|
60
65
|
GEMINI_API_KEY = your-api-key-here
|
|
@@ -67,6 +72,7 @@ A command-line AI tool for autocommits.
|
|
|
67
72
|
```
|
|
68
73
|
|
|
69
74
|
### Option 3: .dcommit File (Virtual Environment)
|
|
75
|
+
|
|
70
76
|
```bash
|
|
71
77
|
mkdir -p $VIRTUAL_ENV/config
|
|
72
78
|
cat > $VIRTUAL_ENV/config/.dcommit << 'EOF'
|
|
@@ -92,6 +98,7 @@ devcommit
|
|
|
92
98
|
### Basic Usage
|
|
93
99
|
|
|
94
100
|
- **Stage all changes and commit:**
|
|
101
|
+
|
|
95
102
|
```bash
|
|
96
103
|
devcommit --stageAll
|
|
97
104
|
```
|
|
@@ -118,6 +125,7 @@ You can set your preferred commit mode in the `.dcommit` configuration file usin
|
|
|
118
125
|
#### Command-Line Usage
|
|
119
126
|
|
|
120
127
|
- **Interactive mode (auto):** When you have changes in multiple directories, DevCommit will automatically ask if you want to:
|
|
128
|
+
|
|
121
129
|
- Create one commit for all changes (global commit)
|
|
122
130
|
- Create separate commits per directory
|
|
123
131
|
|
|
@@ -129,10 +137,40 @@ You can set your preferred commit mode in the `.dcommit` configuration file usin
|
|
|
129
137
|
```
|
|
130
138
|
|
|
131
139
|
When using directory-based commits, you can:
|
|
140
|
+
|
|
132
141
|
1. Select which directories to commit (use Space to select, Enter to confirm)
|
|
133
142
|
2. For each selected directory, review and choose a commit message
|
|
134
143
|
3. Each directory gets its own commit with AI-generated messages based on its changes
|
|
135
144
|
|
|
145
|
+
### Commit Specific Files or Folders
|
|
146
|
+
|
|
147
|
+
DevCommit allows you to commit specific files or folders. This is useful when you want to commit only certain changes without affecting other staged files.
|
|
148
|
+
|
|
149
|
+
**Usage:**
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Commit specific files (must be staged first)
|
|
153
|
+
git add file1.py file2.py
|
|
154
|
+
devcommit --files file1.py file2.py
|
|
155
|
+
|
|
156
|
+
# Stage and commit specific files in one command
|
|
157
|
+
devcommit --stageAll --files file1.py file2.py
|
|
158
|
+
|
|
159
|
+
# Commit specific folders (must be staged first)
|
|
160
|
+
git add src/ tests/
|
|
161
|
+
devcommit --files src/ tests/
|
|
162
|
+
|
|
163
|
+
# Short form
|
|
164
|
+
devcommit -s -f file1.py file2.py
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
When using `--files` or `-f`:
|
|
168
|
+
|
|
169
|
+
- Without `--stageAll`: Only commits files that are already staged (filters staged files to match specified paths)
|
|
170
|
+
- With `--stageAll`: Stages the specified files/folders and then commits them
|
|
171
|
+
- AI generates commit messages based on changes in those files
|
|
172
|
+
- Works with both individual files and entire directories
|
|
173
|
+
|
|
136
174
|
### Additional Options
|
|
137
175
|
|
|
138
176
|
- `--excludeFiles` or `-e`: Exclude specific files from the diff
|
|
@@ -140,6 +178,8 @@ When using directory-based commits, you can:
|
|
|
140
178
|
- `--commitType` or `-t`: Specify the type of commit (e.g., conventional)
|
|
141
179
|
- `--stageAll` or `-s`: Stage all changes before committing
|
|
142
180
|
- `--directory` or `-d`: Force directory-based commits
|
|
181
|
+
- `--files` or `-f`: Stage and commit specific files or folders (can specify multiple)
|
|
182
|
+
- `--push` or `-p`: Push commits to remote after committing
|
|
143
183
|
|
|
144
184
|
### Examples
|
|
145
185
|
|
|
@@ -152,24 +192,40 @@ devcommit --commitType conventional
|
|
|
152
192
|
|
|
153
193
|
# Exclude lock files
|
|
154
194
|
devcommit --excludeFiles package-lock.json yarn.lock
|
|
195
|
+
|
|
196
|
+
# Stage and commit specific files
|
|
197
|
+
devcommit --files file1.py file2.py
|
|
198
|
+
|
|
199
|
+
# Stage and commit specific folders
|
|
200
|
+
devcommit --files src/ tests/
|
|
201
|
+
|
|
202
|
+
# Stage and commit multiple files and folders at once
|
|
203
|
+
devcommit --files src/ file1.py tests/ config.json
|
|
204
|
+
|
|
205
|
+
# Commit and push
|
|
206
|
+
devcommit --push
|
|
207
|
+
|
|
208
|
+
# Commit specific files and push
|
|
209
|
+
devcommit --files file1.py file2.py --push
|
|
155
210
|
```
|
|
156
211
|
|
|
157
212
|
## AI Provider Support
|
|
158
213
|
|
|
159
214
|
DevCommit now supports **multiple AI providers**! Choose from:
|
|
160
215
|
|
|
161
|
-
| Provider
|
|
162
|
-
|
|
163
|
-
| 🆓 **Gemini**
|
|
164
|
-
| ⚡ **Groq**
|
|
165
|
-
| 🤖 **OpenAI**
|
|
166
|
-
| 🧠 **Anthropic** | Limited trial
|
|
167
|
-
| 🏠 **Ollama**
|
|
168
|
-
| 🔧 **Custom**
|
|
216
|
+
| Provider | Free Tier | Speed | Quality | Get API Key |
|
|
217
|
+
| ---------------- | ------------------ | ----------- | --------- | ------------------------------------------------- |
|
|
218
|
+
| 🆓 **Gemini** | 15 req/min, 1M/day | Fast | Good | [Get Key](https://aistudio.google.com/app/apikey) |
|
|
219
|
+
| ⚡ **Groq** | Very generous | **Fastest** | Good | [Get Key](https://console.groq.com/keys) |
|
|
220
|
+
| 🤖 **OpenAI** | $5 trial | Medium | **Best** | [Get Key](https://platform.openai.com/api-keys) |
|
|
221
|
+
| 🧠 **Anthropic** | Limited trial | Medium | Excellent | [Get Key](https://console.anthropic.com/) |
|
|
222
|
+
| 🏠 **Ollama** | **Unlimited** | Medium | Good | [Install](https://ollama.ai/) |
|
|
223
|
+
| 🔧 **Custom** | Varies | Varies | Varies | Your server |
|
|
169
224
|
|
|
170
225
|
### Quick Setup Examples
|
|
171
226
|
|
|
172
227
|
**Using Groq (Recommended for free tier):**
|
|
228
|
+
|
|
173
229
|
```bash
|
|
174
230
|
export AI_PROVIDER=groq
|
|
175
231
|
export GROQ_API_KEY='your-groq-api-key'
|
|
@@ -177,6 +233,7 @@ devcommit
|
|
|
177
233
|
```
|
|
178
234
|
|
|
179
235
|
**Using Ollama (Local, no API key needed):**
|
|
236
|
+
|
|
180
237
|
```bash
|
|
181
238
|
# Install Ollama: https://ollama.ai/
|
|
182
239
|
ollama pull llama3
|
|
@@ -185,6 +242,7 @@ devcommit
|
|
|
185
242
|
```
|
|
186
243
|
|
|
187
244
|
**Using Custom API:**
|
|
245
|
+
|
|
188
246
|
```bash
|
|
189
247
|
export AI_PROVIDER=custom
|
|
190
248
|
export CUSTOM_API_URL='http://localhost:8000/v1'
|
|
@@ -199,8 +257,8 @@ All configuration can be set via **environment variables** or **`.dcommit` file*
|
|
|
199
257
|
|
|
200
258
|
### AI Provider Settings
|
|
201
259
|
|
|
202
|
-
| Variable
|
|
203
|
-
|
|
260
|
+
| Variable | Description | Default | Options |
|
|
261
|
+
| ------------- | ----------------------- | -------- | ----------------------------------------------------------- |
|
|
204
262
|
| `AI_PROVIDER` | Which AI service to use | `gemini` | `gemini`, `openai`, `groq`, `anthropic`, `ollama`, `custom` |
|
|
205
263
|
|
|
206
264
|
### Provider-Specific Settings
|
|
@@ -244,21 +302,23 @@ All configuration can be set via **environment variables** or **`.dcommit` file*
|
|
|
244
302
|
|
|
245
303
|
### General Settings
|
|
246
304
|
|
|
247
|
-
| Variable
|
|
248
|
-
|
|
249
|
-
| `LOCALE`
|
|
250
|
-
| `MAX_NO`
|
|
251
|
-
| `COMMIT_TYPE`
|
|
252
|
-
| `COMMIT_MODE`
|
|
253
|
-
| `EXCLUDE_FILES` | Files to exclude from diff
|
|
254
|
-
| `MAX_TOKENS`
|
|
305
|
+
| Variable | Description | Default | Options |
|
|
306
|
+
| --------------- | ------------------------------------ | ------------------------------------------------------ | ---------------------------------------- |
|
|
307
|
+
| `LOCALE` | Language for commit messages | `en-US` | Any locale code (e.g., `en`, `es`, `fr`) |
|
|
308
|
+
| `MAX_NO` | Number of commit message suggestions | `1` | Any positive integer |
|
|
309
|
+
| `COMMIT_TYPE` | Style of commit messages | `general` | `general`, `conventional`, etc. |
|
|
310
|
+
| `COMMIT_MODE` | Default commit strategy | `auto` | `auto`, `directory`, `global` |
|
|
311
|
+
| `EXCLUDE_FILES` | Files to exclude from diff | `package-lock.json, pnpm-lock.yaml, yarn.lock, *.lock` | Comma-separated file patterns |
|
|
312
|
+
| `MAX_TOKENS` | Maximum tokens for AI response | `8192` | Any positive integer |
|
|
255
313
|
|
|
256
314
|
### Configuration Priority
|
|
315
|
+
|
|
257
316
|
1. **`.dcommit` file** (highest priority)
|
|
258
317
|
2. **Environment variables**
|
|
259
318
|
3. **Built-in defaults** (lowest priority)
|
|
260
319
|
|
|
261
320
|
### Using Environment Variables
|
|
321
|
+
|
|
262
322
|
```bash
|
|
263
323
|
# Basic setup with Gemini (default)
|
|
264
324
|
export GEMINI_API_KEY='your-api-key-here'
|
|
@@ -273,6 +333,7 @@ export GROQ_API_KEY='your-groq-key'
|
|
|
273
333
|
```
|
|
274
334
|
|
|
275
335
|
### Using .dcommit File
|
|
336
|
+
|
|
276
337
|
See `.dcommit.example` for a complete configuration template with all providers.
|
|
277
338
|
|
|
278
339
|
**Note:** The `.dcommit` file is **optional**. DevCommit will work with just environment variables!
|
|
@@ -12,7 +12,9 @@ from rich.console import Console
|
|
|
12
12
|
from devcommit.app.gemini_ai import generateCommitMessage
|
|
13
13
|
from devcommit.utils.git import (KnownError, assert_git_repo,
|
|
14
14
|
get_detected_message, get_diff_for_files,
|
|
15
|
-
get_staged_diff,
|
|
15
|
+
get_files_from_paths, get_staged_diff,
|
|
16
|
+
group_files_by_directory, has_commits_to_push,
|
|
17
|
+
push_to_remote, stage_files)
|
|
16
18
|
from devcommit.utils.logger import Logger, config
|
|
17
19
|
from devcommit.utils.parser import CommitFlag, parse_arguments
|
|
18
20
|
|
|
@@ -67,10 +69,90 @@ def main(flags: CommitFlag = None):
|
|
|
67
69
|
console.print(f"[dim]Provider:[/dim] [bold magenta]{provider}[/bold magenta] [dim]│[/dim] [dim]Model:[/dim] [bold magenta]{model}[/bold magenta]")
|
|
68
70
|
console.print()
|
|
69
71
|
|
|
72
|
+
# Handle staging
|
|
73
|
+
push_files_list = []
|
|
74
|
+
if flags["files"] and len(flags["files"]) > 0:
|
|
75
|
+
|
|
76
|
+
# Get the list of files from paths first
|
|
77
|
+
try:
|
|
78
|
+
push_files_list = get_files_from_paths(flags["files"])
|
|
79
|
+
if not push_files_list:
|
|
80
|
+
raise KnownError("No files found in the specified paths")
|
|
81
|
+
except KnownError as e:
|
|
82
|
+
raise e
|
|
83
|
+
except Exception as e:
|
|
84
|
+
raise KnownError(f"Failed to get files from paths: {str(e)}")
|
|
85
|
+
|
|
70
86
|
if flags["stageAll"]:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
if push_files_list:
|
|
88
|
+
# Stage specific files/folders only
|
|
89
|
+
console.print("[bold cyan]📦 Staging specific files/folders...[/bold cyan]")
|
|
90
|
+
console.print(f"[dim]Found {len(push_files_list)} file(s) to stage[/dim]")
|
|
91
|
+
for file in push_files_list:
|
|
92
|
+
console.print(f" [cyan]▸[/cyan] [white]{file}[/white]")
|
|
93
|
+
|
|
94
|
+
stage_files(push_files_list)
|
|
95
|
+
console.print("[bold green]✅ Files staged successfully[/bold green]\n")
|
|
96
|
+
else:
|
|
97
|
+
# Stage all changes
|
|
98
|
+
stage_changes(console)
|
|
99
|
+
console.print("[bold green]✅ All changes staged successfully[/bold green]\n")
|
|
100
|
+
|
|
101
|
+
# Get staged files
|
|
102
|
+
if push_files_list and len(push_files_list) > 0:
|
|
103
|
+
if flags["stageAll"]:
|
|
104
|
+
# If --files was used with --stageAll, we already staged those files
|
|
105
|
+
# Create a staged dict with only those files
|
|
106
|
+
staged = {
|
|
107
|
+
"files": push_files_list,
|
|
108
|
+
"diff": get_diff_for_files(push_files_list, flags["excludeFiles"])
|
|
109
|
+
}
|
|
110
|
+
if not staged["diff"]:
|
|
111
|
+
raise KnownError("No changes found in the specified files/folders")
|
|
112
|
+
|
|
113
|
+
console.print(f"\n[bold green]✅ {get_detected_message(staged['files'])}[/bold green]")
|
|
114
|
+
console.print("[dim]" + "─" * 60 + "[/dim]")
|
|
115
|
+
for file in staged["files"]:
|
|
116
|
+
console.print(f" [cyan]▸[/cyan] [white]{file}[/white]")
|
|
117
|
+
console.print("[dim]" + "─" * 60 + "[/dim]")
|
|
118
|
+
else:
|
|
119
|
+
# If --files was used without --stageAll, filter staged files to only those specified
|
|
120
|
+
# First, get all staged files (this will error if nothing is staged)
|
|
121
|
+
all_staged = get_staged_diff(flags["excludeFiles"])
|
|
122
|
+
if not all_staged:
|
|
123
|
+
raise KnownError(
|
|
124
|
+
"No staged changes found. Stage your changes manually, or "
|
|
125
|
+
"automatically stage specific files with the `--stageAll --files` flag."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Filter to only include files that match the specified paths
|
|
129
|
+
filtered_files = []
|
|
130
|
+
for staged_file in all_staged["files"]:
|
|
131
|
+
# Check if this staged file is in our push_files_list
|
|
132
|
+
if staged_file in push_files_list:
|
|
133
|
+
filtered_files.append(staged_file)
|
|
134
|
+
|
|
135
|
+
if not filtered_files:
|
|
136
|
+
raise KnownError(
|
|
137
|
+
f"None of the specified files/folders are staged. "
|
|
138
|
+
f"Please stage them first with 'git add' or use '--stageAll --files'"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Create a staged dict with only the filtered files
|
|
142
|
+
staged = {
|
|
143
|
+
"files": filtered_files,
|
|
144
|
+
"diff": get_diff_for_files(filtered_files, flags["excludeFiles"])
|
|
145
|
+
}
|
|
146
|
+
if not staged["diff"]:
|
|
147
|
+
raise KnownError("No changes found in the specified files/folders")
|
|
148
|
+
|
|
149
|
+
console.print(f"\n[bold green]✅ {get_detected_message(staged['files'])}[/bold green]")
|
|
150
|
+
console.print("[dim]" + "─" * 60 + "[/dim]")
|
|
151
|
+
for file in staged["files"]:
|
|
152
|
+
console.print(f" [cyan]▸[/cyan] [white]{file}[/white]")
|
|
153
|
+
console.print("[dim]" + "─" * 60 + "[/dim]")
|
|
154
|
+
else:
|
|
155
|
+
staged = detect_staged_files(console, flags["excludeFiles"])
|
|
74
156
|
|
|
75
157
|
# Determine commit strategy
|
|
76
158
|
# Priority: CLI flag > config (file or env) > interactive prompt
|
|
@@ -91,19 +173,28 @@ def main(flags: CommitFlag = None):
|
|
|
91
173
|
if len(grouped) > 1:
|
|
92
174
|
use_per_directory = prompt_commit_strategy(console, grouped)
|
|
93
175
|
|
|
176
|
+
# Track if any commits were made
|
|
177
|
+
commit_made = False
|
|
94
178
|
if use_per_directory:
|
|
95
|
-
process_per_directory_commits(console, staged, flags)
|
|
179
|
+
commit_made = process_per_directory_commits(console, staged, flags)
|
|
96
180
|
else:
|
|
97
|
-
process_global_commit(console, flags)
|
|
181
|
+
commit_made = process_global_commit(console, flags)
|
|
98
182
|
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
183
|
+
# Handle push if requested and a commit was actually made
|
|
184
|
+
if flags.get("push", False) and commit_made:
|
|
185
|
+
push_changes(console)
|
|
186
|
+
elif flags.get("push", False) and not commit_made:
|
|
187
|
+
console.print("\n[bold yellow]⚠️ No commits were made, skipping push[/bold yellow]\n")
|
|
188
|
+
|
|
189
|
+
# Print stylish completion message only if commits were made
|
|
190
|
+
if commit_made:
|
|
191
|
+
console.print()
|
|
192
|
+
console.print("╭" + "─" * 60 + "╮", style="bold green")
|
|
193
|
+
console.print("│" + " " * 60 + "│", style="bold green")
|
|
194
|
+
console.print("│" + " ✨ [bold white]All commits completed successfully![/bold white] ✨ ".ljust(68) + "│", style="bold green")
|
|
195
|
+
console.print("│" + " " * 60 + "│", style="bold green")
|
|
196
|
+
console.print("╰" + "─" * 60 + "╯", style="bold green")
|
|
197
|
+
console.print()
|
|
107
198
|
|
|
108
199
|
except KeyboardInterrupt:
|
|
109
200
|
console.print("\n\n[bold yellow]⚠️ Operation cancelled by user[/bold yellow]\n")
|
|
@@ -274,6 +365,43 @@ def commit_changes(console, commit, raw_argv):
|
|
|
274
365
|
console.print("\n[bold green]✅ Committed successfully![/bold green]")
|
|
275
366
|
|
|
276
367
|
|
|
368
|
+
def push_changes(console):
|
|
369
|
+
"""Push commits to remote repository."""
|
|
370
|
+
# Check if there are commits to push first
|
|
371
|
+
try:
|
|
372
|
+
if not has_commits_to_push():
|
|
373
|
+
console.print("\n[bold yellow]ℹ️ No commits to push (already up to date)[/bold yellow]\n")
|
|
374
|
+
return
|
|
375
|
+
except KnownError:
|
|
376
|
+
# If we can't determine, try to push anyway
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
console.print("\n[cyan]🚀 Pushing to remote...[/cyan]")
|
|
380
|
+
console.print("[dim]Note: You may be prompted for authentication[/dim]\n")
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
# Run push with stdin/stdout/stderr connected to terminal
|
|
384
|
+
# This allows interactive prompts (authentication) to work properly
|
|
385
|
+
result = subprocess.run(
|
|
386
|
+
['git', 'push'],
|
|
387
|
+
check=False, # Don't raise on error, we'll check return code
|
|
388
|
+
stdin=None, # Inherit stdin for interactive prompts
|
|
389
|
+
stdout=None, # Don't capture stdout - let it show in terminal
|
|
390
|
+
stderr=None # Don't capture stderr - let it show in terminal
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if result.returncode == 0:
|
|
394
|
+
console.print("\n[bold green]✅ Pushed to remote successfully![/bold green]")
|
|
395
|
+
else:
|
|
396
|
+
raise KnownError("Push failed. Please check the output above for details.")
|
|
397
|
+
except FileNotFoundError:
|
|
398
|
+
raise KnownError("Git command not found. Please ensure git is installed.")
|
|
399
|
+
except Exception as e:
|
|
400
|
+
if isinstance(e, KnownError):
|
|
401
|
+
raise
|
|
402
|
+
raise KnownError(f"Push failed: {str(e)}")
|
|
403
|
+
|
|
404
|
+
|
|
277
405
|
def prompt_commit_strategy(console, grouped):
|
|
278
406
|
"""Prompt user to choose between global or directory-based commits."""
|
|
279
407
|
console.print()
|
|
@@ -310,16 +438,21 @@ def prompt_commit_strategy(console, grouped):
|
|
|
310
438
|
|
|
311
439
|
|
|
312
440
|
def process_global_commit(console, flags):
|
|
313
|
-
"""Process a single global commit for all changes.
|
|
441
|
+
"""Process a single global commit for all changes.
|
|
442
|
+
Returns True if a commit was made, False otherwise."""
|
|
314
443
|
commit_message = analyze_changes(console)
|
|
315
444
|
selected_commit = prompt_commit_message(console, commit_message)
|
|
316
445
|
if selected_commit:
|
|
317
446
|
commit_changes(console, selected_commit, flags["rawArgv"])
|
|
447
|
+
return True
|
|
448
|
+
return False
|
|
318
449
|
|
|
319
450
|
|
|
320
451
|
def process_per_directory_commits(console, staged, flags):
|
|
321
|
-
"""Process separate commits for each directory.
|
|
452
|
+
"""Process separate commits for each directory.
|
|
453
|
+
Returns True if at least one commit was made, False otherwise."""
|
|
322
454
|
grouped = group_files_by_directory(staged["files"])
|
|
455
|
+
commits_made = False
|
|
323
456
|
|
|
324
457
|
console.print()
|
|
325
458
|
console.print("╭" + "─" * 60 + "╮", style="bold magenta")
|
|
@@ -368,7 +501,7 @@ def process_per_directory_commits(console, staged, flags):
|
|
|
368
501
|
|
|
369
502
|
if not selected_directories:
|
|
370
503
|
console.print("\n[bold yellow]⚠️ No directories selected[/bold yellow]\n")
|
|
371
|
-
return
|
|
504
|
+
return False
|
|
372
505
|
|
|
373
506
|
# Process each selected directory
|
|
374
507
|
for idx, directory in enumerate(selected_directories, 1):
|
|
@@ -420,8 +553,11 @@ def process_per_directory_commits(console, staged, flags):
|
|
|
420
553
|
# Commit only the files in this directory
|
|
421
554
|
subprocess.run(["git", "commit", "-m", selected_commit, *flags["rawArgv"], "--"] + files)
|
|
422
555
|
console.print(f"\n[bold green]✅ Committed {directory}[/bold green]")
|
|
556
|
+
commits_made = True
|
|
423
557
|
else:
|
|
424
558
|
console.print(f"\n[bold yellow]⊘ Skipped {directory}[/bold yellow]")
|
|
559
|
+
|
|
560
|
+
return commits_made
|
|
425
561
|
|
|
426
562
|
|
|
427
563
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Git utilities"""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KnownError(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def assert_git_repo() -> str:
|
|
15
|
+
"""
|
|
16
|
+
Asserts that the current directory is a Git repository.
|
|
17
|
+
Returns the top-level directory path of the repository.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
['git', 'rev-parse', '--show-toplevel'],
|
|
23
|
+
check=True,
|
|
24
|
+
stdout=subprocess.PIPE,
|
|
25
|
+
stderr=subprocess.PIPE,
|
|
26
|
+
text=True
|
|
27
|
+
)
|
|
28
|
+
return result.stdout.strip()
|
|
29
|
+
except subprocess.CalledProcessError:
|
|
30
|
+
raise KnownError('The current directory must be a Git repository!')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def exclude_from_diff(path: str) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Prepares a Git exclusion path string for the diff command.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
return f':(exclude){path}'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_default_excludes() -> List[str]:
|
|
42
|
+
"""
|
|
43
|
+
Get list of files to exclude from diff.
|
|
44
|
+
Priority: Config > Defaults
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
from devcommit.utils.logger import config
|
|
48
|
+
|
|
49
|
+
# Get from config (supports comma-separated list)
|
|
50
|
+
exclude_config = config("EXCLUDE_FILES", default="")
|
|
51
|
+
|
|
52
|
+
if exclude_config:
|
|
53
|
+
# Parse comma-separated values and strip whitespace
|
|
54
|
+
config_excludes = [f.strip() for f in exclude_config.split(",") if f.strip()]
|
|
55
|
+
return config_excludes
|
|
56
|
+
except:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
# Default exclusions
|
|
60
|
+
return [
|
|
61
|
+
'package-lock.json',
|
|
62
|
+
'pnpm-lock.yaml',
|
|
63
|
+
'yarn.lock',
|
|
64
|
+
'*.lock'
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Get default files to exclude (can be overridden via config)
|
|
69
|
+
files_to_exclude = get_default_excludes()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_staged_diff(
|
|
73
|
+
exclude_files: Optional[List[str]] = None) -> Optional[dict]:
|
|
74
|
+
"""
|
|
75
|
+
Gets the list of staged files and their diff, excluding specified files.
|
|
76
|
+
"""
|
|
77
|
+
exclude_files = exclude_files or []
|
|
78
|
+
diff_cached = ['git', 'diff', '--cached', '--diff-algorithm=minimal']
|
|
79
|
+
excluded_from_diff = (
|
|
80
|
+
[exclude_from_diff(f) for f in files_to_exclude + exclude_files])
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
# Get the list of staged files excluding specified files
|
|
84
|
+
files = subprocess.run(
|
|
85
|
+
diff_cached + ['--name-only'] + excluded_from_diff,
|
|
86
|
+
check=True,
|
|
87
|
+
stdout=subprocess.PIPE,
|
|
88
|
+
stderr=subprocess.PIPE,
|
|
89
|
+
text=True
|
|
90
|
+
)
|
|
91
|
+
files_result = (
|
|
92
|
+
files.stdout.strip().split('\n') if files.stdout.strip() else []
|
|
93
|
+
)
|
|
94
|
+
if not files_result:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Get the staged diff excluding specified files
|
|
98
|
+
diff = subprocess.run(
|
|
99
|
+
diff_cached + excluded_from_diff,
|
|
100
|
+
check=True,
|
|
101
|
+
stdout=subprocess.PIPE,
|
|
102
|
+
stderr=subprocess.PIPE,
|
|
103
|
+
text=True
|
|
104
|
+
)
|
|
105
|
+
diff_result = diff.stdout.strip()
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
'files': files_result,
|
|
109
|
+
'diff': diff_result
|
|
110
|
+
}
|
|
111
|
+
except subprocess.CalledProcessError:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_detected_message(files: List[str]) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Returns a message indicating the number of staged files.
|
|
118
|
+
"""
|
|
119
|
+
return (
|
|
120
|
+
f"Detected {len(files):,} staged file{'s' if len(files) > 1 else ''}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def group_files_by_directory(files: List[str]) -> Dict[str, List[str]]:
|
|
125
|
+
"""
|
|
126
|
+
Groups files by their root directory (first-level directory).
|
|
127
|
+
Files in the repository root are grouped under 'root'.
|
|
128
|
+
"""
|
|
129
|
+
grouped = defaultdict(list)
|
|
130
|
+
|
|
131
|
+
for file_path in files:
|
|
132
|
+
# Get the first directory in the path
|
|
133
|
+
parts = file_path.split(os.sep)
|
|
134
|
+
if len(parts) > 1:
|
|
135
|
+
root_dir = parts[0]
|
|
136
|
+
else:
|
|
137
|
+
root_dir = 'root'
|
|
138
|
+
grouped[root_dir].append(file_path)
|
|
139
|
+
|
|
140
|
+
return dict(grouped)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_diff_for_files(files: List[str], exclude_files: Optional[List[str]] = None) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Gets the diff for specific files.
|
|
146
|
+
"""
|
|
147
|
+
exclude_files = exclude_files or []
|
|
148
|
+
|
|
149
|
+
# Filter out excluded files from the list
|
|
150
|
+
all_excluded = files_to_exclude + exclude_files
|
|
151
|
+
filtered_files = [
|
|
152
|
+
f for f in files
|
|
153
|
+
if not any(f.endswith(excl.replace('*', '')) or excl.replace(':(exclude)', '') in f
|
|
154
|
+
for excl in all_excluded)
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
if not filtered_files:
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
diff = subprocess.run(
|
|
162
|
+
['git', 'diff', '--cached', '--diff-algorithm=minimal', '--'] + filtered_files,
|
|
163
|
+
check=True,
|
|
164
|
+
stdout=subprocess.PIPE,
|
|
165
|
+
stderr=subprocess.PIPE,
|
|
166
|
+
text=True
|
|
167
|
+
)
|
|
168
|
+
return diff.stdout.strip()
|
|
169
|
+
except subprocess.CalledProcessError:
|
|
170
|
+
return ""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_files_from_paths(paths: List[str]) -> List[str]:
|
|
174
|
+
"""
|
|
175
|
+
Gets all files from given paths (handles both files and directories).
|
|
176
|
+
Returns a list of file paths relative to the repository root.
|
|
177
|
+
"""
|
|
178
|
+
repo_root = assert_git_repo()
|
|
179
|
+
all_files = []
|
|
180
|
+
|
|
181
|
+
for path in paths:
|
|
182
|
+
# Normalize path
|
|
183
|
+
normalized_path = os.path.normpath(path)
|
|
184
|
+
full_path = os.path.join(repo_root, normalized_path) if not os.path.isabs(path) else path
|
|
185
|
+
|
|
186
|
+
if not os.path.exists(full_path):
|
|
187
|
+
raise KnownError(f"Path does not exist: {path}")
|
|
188
|
+
|
|
189
|
+
if os.path.isfile(full_path):
|
|
190
|
+
# It's a file, get relative path
|
|
191
|
+
rel_path = os.path.relpath(full_path, repo_root)
|
|
192
|
+
all_files.append(rel_path)
|
|
193
|
+
elif os.path.isdir(full_path):
|
|
194
|
+
# It's a directory, get all files in it
|
|
195
|
+
try:
|
|
196
|
+
result = subprocess.run(
|
|
197
|
+
['git', 'ls-files', '--', normalized_path],
|
|
198
|
+
check=True,
|
|
199
|
+
stdout=subprocess.PIPE,
|
|
200
|
+
stderr=subprocess.PIPE,
|
|
201
|
+
text=True,
|
|
202
|
+
cwd=repo_root
|
|
203
|
+
)
|
|
204
|
+
files_in_dir = [f.strip() for f in result.stdout.strip().split('\n') if f.strip()]
|
|
205
|
+
all_files.extend(files_in_dir)
|
|
206
|
+
except subprocess.CalledProcessError:
|
|
207
|
+
# If git ls-files fails, try to find files manually
|
|
208
|
+
for root, dirs, files in os.walk(full_path):
|
|
209
|
+
# Skip .git directories
|
|
210
|
+
if '.git' in dirs:
|
|
211
|
+
dirs.remove('.git')
|
|
212
|
+
for file in files:
|
|
213
|
+
file_path = os.path.join(root, file)
|
|
214
|
+
rel_path = os.path.relpath(file_path, repo_root)
|
|
215
|
+
all_files.append(rel_path)
|
|
216
|
+
|
|
217
|
+
# Remove duplicates and return
|
|
218
|
+
return list(set(all_files))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def stage_files(files: List[str]) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Stages specific files.
|
|
224
|
+
"""
|
|
225
|
+
if not files:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
subprocess.run(
|
|
230
|
+
['git', 'add', '--'] + files,
|
|
231
|
+
check=True,
|
|
232
|
+
stdout=subprocess.PIPE,
|
|
233
|
+
stderr=subprocess.PIPE,
|
|
234
|
+
text=True
|
|
235
|
+
)
|
|
236
|
+
except subprocess.CalledProcessError as e:
|
|
237
|
+
raise KnownError(f"Failed to stage files: {e.stderr}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_current_branch() -> str:
|
|
241
|
+
"""
|
|
242
|
+
Gets the current git branch name.
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
result = subprocess.run(
|
|
246
|
+
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
|
247
|
+
check=True,
|
|
248
|
+
stdout=subprocess.PIPE,
|
|
249
|
+
stderr=subprocess.PIPE,
|
|
250
|
+
text=True
|
|
251
|
+
)
|
|
252
|
+
return result.stdout.strip()
|
|
253
|
+
except subprocess.CalledProcessError:
|
|
254
|
+
raise KnownError("Failed to get current branch name")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def has_commits_to_push(branch: Optional[str] = None, remote: str = "origin") -> bool:
|
|
258
|
+
"""
|
|
259
|
+
Checks if there are commits ahead of the remote that need to be pushed.
|
|
260
|
+
Returns True if there are commits to push, False otherwise.
|
|
261
|
+
"""
|
|
262
|
+
if branch is None:
|
|
263
|
+
branch = get_current_branch()
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
# Check if remote tracking branch exists
|
|
267
|
+
result = subprocess.run(
|
|
268
|
+
['git', 'rev-parse', '--abbrev-ref', f'{branch}@{{upstream}}'],
|
|
269
|
+
check=True,
|
|
270
|
+
stdout=subprocess.PIPE,
|
|
271
|
+
stderr=subprocess.PIPE,
|
|
272
|
+
text=True
|
|
273
|
+
)
|
|
274
|
+
upstream = result.stdout.strip()
|
|
275
|
+
except subprocess.CalledProcessError:
|
|
276
|
+
# No upstream branch, assume we need to push
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
# Check if local branch is ahead of remote
|
|
281
|
+
result = subprocess.run(
|
|
282
|
+
['git', 'rev-list', '--count', f'{upstream}..{branch}'],
|
|
283
|
+
check=True,
|
|
284
|
+
stdout=subprocess.PIPE,
|
|
285
|
+
stderr=subprocess.PIPE,
|
|
286
|
+
text=True
|
|
287
|
+
)
|
|
288
|
+
ahead_count = int(result.stdout.strip())
|
|
289
|
+
return ahead_count > 0
|
|
290
|
+
except (subprocess.CalledProcessError, ValueError):
|
|
291
|
+
# If we can't determine, assume we need to push
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def push_to_remote(branch: Optional[str] = None, remote: str = "origin") -> None:
|
|
296
|
+
"""
|
|
297
|
+
Pushes the current branch to the remote repository.
|
|
298
|
+
"""
|
|
299
|
+
if branch is None:
|
|
300
|
+
branch = get_current_branch()
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
# Check if remote exists
|
|
304
|
+
result = subprocess.run(
|
|
305
|
+
['git', 'remote', 'get-url', remote],
|
|
306
|
+
check=True,
|
|
307
|
+
stdout=subprocess.PIPE,
|
|
308
|
+
stderr=subprocess.PIPE,
|
|
309
|
+
text=True
|
|
310
|
+
)
|
|
311
|
+
except subprocess.CalledProcessError:
|
|
312
|
+
raise KnownError(f"Remote '{remote}' does not exist. Please add a remote first.")
|
|
313
|
+
|
|
314
|
+
# Check if there are commits to push
|
|
315
|
+
if not has_commits_to_push(branch, remote):
|
|
316
|
+
return # Nothing to push
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
# Don't capture stdout/stderr to allow interactive prompts (e.g., for authentication)
|
|
320
|
+
# This allows the user to see what's happening and enter credentials if needed
|
|
321
|
+
subprocess.run(
|
|
322
|
+
['git', 'push', remote, branch],
|
|
323
|
+
check=True
|
|
324
|
+
)
|
|
325
|
+
except subprocess.CalledProcessError as e:
|
|
326
|
+
raise KnownError(f"Failed to push to remote. Please check your authentication and try again.")
|
|
@@ -9,6 +9,8 @@ class CommitFlag(TypedDict):
|
|
|
9
9
|
stageAll: bool
|
|
10
10
|
commitType: Optional[str]
|
|
11
11
|
directory: bool
|
|
12
|
+
files: List[str]
|
|
13
|
+
push: bool
|
|
12
14
|
rawArgv: List[str]
|
|
13
15
|
|
|
14
16
|
|
|
@@ -42,6 +44,14 @@ def parse_arguments() -> CommitFlag:
|
|
|
42
44
|
"--directory", "-d", action="store_true",
|
|
43
45
|
help="Generate separate commits per root directory"
|
|
44
46
|
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--files", "-f", nargs="*", default=[],
|
|
49
|
+
help="Specific files or folders to stage and commit (can specify multiple)"
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--push", "-p", action="store_true",
|
|
53
|
+
help="Push commits to remote after committing"
|
|
54
|
+
)
|
|
45
55
|
parser.add_argument(
|
|
46
56
|
"rawArgv", nargs="*", help="Additional arguments for git commit"
|
|
47
57
|
)
|
|
@@ -54,5 +64,7 @@ def parse_arguments() -> CommitFlag:
|
|
|
54
64
|
stageAll=args.stageAll,
|
|
55
65
|
commitType=args.commitType,
|
|
56
66
|
directory=args.directory,
|
|
67
|
+
files=args.files or [],
|
|
68
|
+
push=args.push,
|
|
57
69
|
rawArgv=args.rawArgv,
|
|
58
70
|
)
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Git utilities"""
|
|
3
|
-
|
|
4
|
-
import os
|
|
5
|
-
import subprocess
|
|
6
|
-
from collections import defaultdict
|
|
7
|
-
from typing import Dict, List, Optional
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class KnownError(Exception):
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def assert_git_repo() -> str:
|
|
15
|
-
"""
|
|
16
|
-
Asserts that the current directory is a Git repository.
|
|
17
|
-
Returns the top-level directory path of the repository.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
try:
|
|
21
|
-
result = subprocess.run(
|
|
22
|
-
['git', 'rev-parse', '--show-toplevel'],
|
|
23
|
-
check=True,
|
|
24
|
-
stdout=subprocess.PIPE,
|
|
25
|
-
stderr=subprocess.PIPE,
|
|
26
|
-
text=True
|
|
27
|
-
)
|
|
28
|
-
return result.stdout.strip()
|
|
29
|
-
except subprocess.CalledProcessError:
|
|
30
|
-
raise KnownError('The current directory must be a Git repository!')
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def exclude_from_diff(path: str) -> str:
|
|
34
|
-
"""
|
|
35
|
-
Prepares a Git exclusion path string for the diff command.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
return f':(exclude){path}'
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def get_default_excludes() -> List[str]:
|
|
42
|
-
"""
|
|
43
|
-
Get list of files to exclude from diff.
|
|
44
|
-
Priority: Config > Defaults
|
|
45
|
-
"""
|
|
46
|
-
try:
|
|
47
|
-
from devcommit.utils.logger import config
|
|
48
|
-
|
|
49
|
-
# Get from config (supports comma-separated list)
|
|
50
|
-
exclude_config = config("EXCLUDE_FILES", default="")
|
|
51
|
-
|
|
52
|
-
if exclude_config:
|
|
53
|
-
# Parse comma-separated values and strip whitespace
|
|
54
|
-
config_excludes = [f.strip() for f in exclude_config.split(",") if f.strip()]
|
|
55
|
-
return config_excludes
|
|
56
|
-
except:
|
|
57
|
-
pass
|
|
58
|
-
|
|
59
|
-
# Default exclusions
|
|
60
|
-
return [
|
|
61
|
-
'package-lock.json',
|
|
62
|
-
'pnpm-lock.yaml',
|
|
63
|
-
'yarn.lock',
|
|
64
|
-
'*.lock'
|
|
65
|
-
]
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# Get default files to exclude (can be overridden via config)
|
|
69
|
-
files_to_exclude = get_default_excludes()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def get_staged_diff(
|
|
73
|
-
exclude_files: Optional[List[str]] = None) -> Optional[dict]:
|
|
74
|
-
"""
|
|
75
|
-
Gets the list of staged files and their diff, excluding specified files.
|
|
76
|
-
"""
|
|
77
|
-
exclude_files = exclude_files or []
|
|
78
|
-
diff_cached = ['git', 'diff', '--cached', '--diff-algorithm=minimal']
|
|
79
|
-
excluded_from_diff = (
|
|
80
|
-
[exclude_from_diff(f) for f in files_to_exclude + exclude_files])
|
|
81
|
-
|
|
82
|
-
try:
|
|
83
|
-
# Get the list of staged files excluding specified files
|
|
84
|
-
files = subprocess.run(
|
|
85
|
-
diff_cached + ['--name-only'] + excluded_from_diff,
|
|
86
|
-
check=True,
|
|
87
|
-
stdout=subprocess.PIPE,
|
|
88
|
-
stderr=subprocess.PIPE,
|
|
89
|
-
text=True
|
|
90
|
-
)
|
|
91
|
-
files_result = (
|
|
92
|
-
files.stdout.strip().split('\n') if files.stdout.strip() else []
|
|
93
|
-
)
|
|
94
|
-
if not files_result:
|
|
95
|
-
return None
|
|
96
|
-
|
|
97
|
-
# Get the staged diff excluding specified files
|
|
98
|
-
diff = subprocess.run(
|
|
99
|
-
diff_cached + excluded_from_diff,
|
|
100
|
-
check=True,
|
|
101
|
-
stdout=subprocess.PIPE,
|
|
102
|
-
stderr=subprocess.PIPE,
|
|
103
|
-
text=True
|
|
104
|
-
)
|
|
105
|
-
diff_result = diff.stdout.strip()
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
'files': files_result,
|
|
109
|
-
'diff': diff_result
|
|
110
|
-
}
|
|
111
|
-
except subprocess.CalledProcessError:
|
|
112
|
-
return None
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def get_detected_message(files: List[str]) -> str:
|
|
116
|
-
"""
|
|
117
|
-
Returns a message indicating the number of staged files.
|
|
118
|
-
"""
|
|
119
|
-
return (
|
|
120
|
-
f"Detected {len(files):,} staged file{'s' if len(files) > 1 else ''}"
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def group_files_by_directory(files: List[str]) -> Dict[str, List[str]]:
|
|
125
|
-
"""
|
|
126
|
-
Groups files by their root directory (first-level directory).
|
|
127
|
-
Files in the repository root are grouped under 'root'.
|
|
128
|
-
"""
|
|
129
|
-
grouped = defaultdict(list)
|
|
130
|
-
|
|
131
|
-
for file_path in files:
|
|
132
|
-
# Get the first directory in the path
|
|
133
|
-
parts = file_path.split(os.sep)
|
|
134
|
-
if len(parts) > 1:
|
|
135
|
-
root_dir = parts[0]
|
|
136
|
-
else:
|
|
137
|
-
root_dir = 'root'
|
|
138
|
-
grouped[root_dir].append(file_path)
|
|
139
|
-
|
|
140
|
-
return dict(grouped)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def get_diff_for_files(files: List[str], exclude_files: Optional[List[str]] = None) -> str:
|
|
144
|
-
"""
|
|
145
|
-
Gets the diff for specific files.
|
|
146
|
-
"""
|
|
147
|
-
exclude_files = exclude_files or []
|
|
148
|
-
|
|
149
|
-
# Filter out excluded files from the list
|
|
150
|
-
all_excluded = files_to_exclude + exclude_files
|
|
151
|
-
filtered_files = [
|
|
152
|
-
f for f in files
|
|
153
|
-
if not any(f.endswith(excl.replace('*', '')) or excl.replace(':(exclude)', '') in f
|
|
154
|
-
for excl in all_excluded)
|
|
155
|
-
]
|
|
156
|
-
|
|
157
|
-
if not filtered_files:
|
|
158
|
-
return ""
|
|
159
|
-
|
|
160
|
-
try:
|
|
161
|
-
diff = subprocess.run(
|
|
162
|
-
['git', 'diff', '--cached', '--diff-algorithm=minimal', '--'] + filtered_files,
|
|
163
|
-
check=True,
|
|
164
|
-
stdout=subprocess.PIPE,
|
|
165
|
-
stderr=subprocess.PIPE,
|
|
166
|
-
text=True
|
|
167
|
-
)
|
|
168
|
-
return diff.stdout.strip()
|
|
169
|
-
except subprocess.CalledProcessError:
|
|
170
|
-
return ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|