gitflow-analytics 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitflow_analytics/__init__.py +11 -11
- gitflow_analytics/_version.py +2 -2
- gitflow_analytics/cli.py +612 -258
- gitflow_analytics/cli_rich.py +353 -0
- gitflow_analytics/config.py +251 -141
- gitflow_analytics/core/analyzer.py +140 -103
- gitflow_analytics/core/branch_mapper.py +132 -132
- gitflow_analytics/core/cache.py +240 -169
- gitflow_analytics/core/identity.py +210 -173
- gitflow_analytics/extractors/base.py +13 -11
- gitflow_analytics/extractors/story_points.py +70 -59
- gitflow_analytics/extractors/tickets.py +101 -87
- gitflow_analytics/integrations/github_integration.py +84 -77
- gitflow_analytics/integrations/jira_integration.py +116 -104
- gitflow_analytics/integrations/orchestrator.py +86 -85
- gitflow_analytics/metrics/dora.py +181 -177
- gitflow_analytics/models/database.py +190 -53
- gitflow_analytics/qualitative/__init__.py +30 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
- gitflow_analytics/qualitative/classifiers/change_type.py +468 -0
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +399 -0
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +436 -0
- gitflow_analytics/qualitative/classifiers/risk_analyzer.py +412 -0
- gitflow_analytics/qualitative/core/__init__.py +13 -0
- gitflow_analytics/qualitative/core/llm_fallback.py +653 -0
- gitflow_analytics/qualitative/core/nlp_engine.py +373 -0
- gitflow_analytics/qualitative/core/pattern_cache.py +457 -0
- gitflow_analytics/qualitative/core/processor.py +540 -0
- gitflow_analytics/qualitative/models/__init__.py +25 -0
- gitflow_analytics/qualitative/models/schemas.py +272 -0
- gitflow_analytics/qualitative/utils/__init__.py +13 -0
- gitflow_analytics/qualitative/utils/batch_processor.py +326 -0
- gitflow_analytics/qualitative/utils/cost_tracker.py +343 -0
- gitflow_analytics/qualitative/utils/metrics.py +347 -0
- gitflow_analytics/qualitative/utils/text_processing.py +243 -0
- gitflow_analytics/reports/analytics_writer.py +11 -4
- gitflow_analytics/reports/csv_writer.py +51 -31
- gitflow_analytics/reports/narrative_writer.py +16 -14
- gitflow_analytics/tui/__init__.py +5 -0
- gitflow_analytics/tui/app.py +721 -0
- gitflow_analytics/tui/screens/__init__.py +8 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +487 -0
- gitflow_analytics/tui/screens/configuration_screen.py +547 -0
- gitflow_analytics/tui/screens/loading_screen.py +358 -0
- gitflow_analytics/tui/screens/main_screen.py +304 -0
- gitflow_analytics/tui/screens/results_screen.py +698 -0
- gitflow_analytics/tui/widgets/__init__.py +7 -0
- gitflow_analytics/tui/widgets/data_table.py +257 -0
- gitflow_analytics/tui/widgets/export_modal.py +301 -0
- gitflow_analytics/tui/widgets/progress_widget.py +192 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/METADATA +31 -4
- gitflow_analytics-1.0.3.dist-info/RECORD +62 -0
- gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/top_level.txt +0 -0
|
@@ -1,221 +1,221 @@
|
|
|
1
1
|
"""Map git branches to projects based on naming conventions."""
|
|
2
|
+
|
|
2
3
|
import re
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import Optional
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class BranchToProjectMapper:
|
|
8
9
|
"""Maps git branches to project keys based on conventions."""
|
|
9
|
-
|
|
10
|
-
def __init__(self, mapping_rules: Optional[
|
|
10
|
+
|
|
11
|
+
def __init__(self, mapping_rules: Optional[dict[str, list[str]]] = None):
|
|
11
12
|
"""
|
|
12
13
|
Initialize with custom mapping rules.
|
|
13
|
-
|
|
14
|
+
|
|
14
15
|
Args:
|
|
15
16
|
mapping_rules: Dict mapping project keys to list of branch patterns
|
|
16
17
|
e.g., {'FRONTEND': ['feature/fe-*', 'frontend/*']}
|
|
17
18
|
"""
|
|
18
19
|
self.mapping_rules = mapping_rules or self._get_default_rules()
|
|
19
20
|
self.compiled_rules = self._compile_patterns()
|
|
20
|
-
|
|
21
|
-
def _get_default_rules(self) ->
|
|
21
|
+
|
|
22
|
+
def _get_default_rules(self) -> dict[str, list[str]]:
|
|
22
23
|
"""Get default branch mapping rules."""
|
|
23
24
|
return {
|
|
24
|
-
|
|
25
|
-
r
|
|
26
|
-
r
|
|
27
|
-
r
|
|
28
|
-
r
|
|
29
|
-
r
|
|
30
|
-
r
|
|
31
|
-
r
|
|
32
|
-
r
|
|
25
|
+
"FRONTEND": [
|
|
26
|
+
r"^feature/fe[-/_]",
|
|
27
|
+
r"^feature/frontend[-/_]",
|
|
28
|
+
r"^frontend/",
|
|
29
|
+
r"^fe/",
|
|
30
|
+
r"[-/_]frontend[-/_]",
|
|
31
|
+
r"[-/_]fe[-/_]",
|
|
32
|
+
r"[-/_]ui[-/_]",
|
|
33
|
+
r"[-/_]web[-/_]",
|
|
33
34
|
],
|
|
34
|
-
|
|
35
|
-
r
|
|
36
|
-
r
|
|
37
|
-
r
|
|
38
|
-
r
|
|
39
|
-
r
|
|
40
|
-
r
|
|
41
|
-
r
|
|
42
|
-
r
|
|
43
|
-
r
|
|
35
|
+
"BACKEND": [
|
|
36
|
+
r"^feature/be[-/_]",
|
|
37
|
+
r"^feature/backend[-/_]",
|
|
38
|
+
r"^backend/",
|
|
39
|
+
r"^be/",
|
|
40
|
+
r"^api/",
|
|
41
|
+
r"[-/_]backend[-/_]",
|
|
42
|
+
r"[-/_]be[-/_]",
|
|
43
|
+
r"[-/_]api[-/_]",
|
|
44
|
+
r"[-/_]server[-/_]",
|
|
44
45
|
],
|
|
45
|
-
|
|
46
|
-
r
|
|
47
|
-
r
|
|
48
|
-
r
|
|
49
|
-
r
|
|
50
|
-
r
|
|
51
|
-
r
|
|
52
|
-
r
|
|
46
|
+
"SERVICE": [
|
|
47
|
+
r"^feature/service[-/_]",
|
|
48
|
+
r"^feature/svc[-/_]",
|
|
49
|
+
r"^service/",
|
|
50
|
+
r"^svc/",
|
|
51
|
+
r"[-/_]service[-/_]",
|
|
52
|
+
r"[-/_]svc[-/_]",
|
|
53
|
+
r"[-/_]microservice[-/_]",
|
|
53
54
|
],
|
|
54
|
-
|
|
55
|
-
r
|
|
56
|
-
r
|
|
57
|
-
r
|
|
58
|
-
r
|
|
59
|
-
r
|
|
60
|
-
r
|
|
61
|
-
r
|
|
62
|
-
r
|
|
63
|
-
r
|
|
64
|
-
r
|
|
55
|
+
"MOBILE": [
|
|
56
|
+
r"^feature/mobile[-/_]",
|
|
57
|
+
r"^feature/app[-/_]",
|
|
58
|
+
r"^mobile/",
|
|
59
|
+
r"^app/",
|
|
60
|
+
r"^ios/",
|
|
61
|
+
r"^android/",
|
|
62
|
+
r"[-/_]mobile[-/_]",
|
|
63
|
+
r"[-/_]app[-/_]",
|
|
64
|
+
r"[-/_]ios[-/_]",
|
|
65
|
+
r"[-/_]android[-/_]",
|
|
65
66
|
],
|
|
66
|
-
|
|
67
|
-
r
|
|
68
|
-
r
|
|
69
|
-
r
|
|
70
|
-
r
|
|
71
|
-
r
|
|
72
|
-
r
|
|
73
|
-
r
|
|
74
|
-
r
|
|
75
|
-
r
|
|
67
|
+
"DATA": [
|
|
68
|
+
r"^feature/data[-/_]",
|
|
69
|
+
r"^feature/etl[-/_]",
|
|
70
|
+
r"^data/",
|
|
71
|
+
r"^etl/",
|
|
72
|
+
r"^pipeline/",
|
|
73
|
+
r"[-/_]data[-/_]",
|
|
74
|
+
r"[-/_]etl[-/_]",
|
|
75
|
+
r"[-/_]pipeline[-/_]",
|
|
76
|
+
r"[-/_]analytics[-/_]",
|
|
76
77
|
],
|
|
77
|
-
|
|
78
|
-
r
|
|
79
|
-
r
|
|
80
|
-
r
|
|
81
|
-
r
|
|
82
|
-
r
|
|
83
|
-
r
|
|
84
|
-
r
|
|
85
|
-
r
|
|
86
|
-
r
|
|
78
|
+
"INFRA": [
|
|
79
|
+
r"^feature/infra[-/_]",
|
|
80
|
+
r"^feature/devops[-/_]",
|
|
81
|
+
r"^infra/",
|
|
82
|
+
r"^devops/",
|
|
83
|
+
r"^ops/",
|
|
84
|
+
r"[-/_]infra[-/_]",
|
|
85
|
+
r"[-/_]devops[-/_]",
|
|
86
|
+
r"[-/_]ops[-/_]",
|
|
87
|
+
r"[-/_]deployment[-/_]",
|
|
88
|
+
],
|
|
89
|
+
"SCRAPER": [
|
|
90
|
+
r"^feature/scraper[-/_]",
|
|
91
|
+
r"^feature/crawler[-/_]",
|
|
92
|
+
r"^scraper/",
|
|
93
|
+
r"^crawler/",
|
|
94
|
+
r"[-/_]scraper[-/_]",
|
|
95
|
+
r"[-/_]crawler[-/_]",
|
|
96
|
+
r"[-/_]scraping[-/_]",
|
|
87
97
|
],
|
|
88
|
-
'SCRAPER': [
|
|
89
|
-
r'^feature/scraper[-/_]',
|
|
90
|
-
r'^feature/crawler[-/_]',
|
|
91
|
-
r'^scraper/',
|
|
92
|
-
r'^crawler/',
|
|
93
|
-
r'[-/_]scraper[-/_]',
|
|
94
|
-
r'[-/_]crawler[-/_]',
|
|
95
|
-
r'[-/_]scraping[-/_]'
|
|
96
|
-
]
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
def _compile_patterns(self) ->
|
|
99
|
+
|
|
100
|
+
def _compile_patterns(self) -> dict[str, list[re.Pattern]]:
|
|
100
101
|
"""Compile regex patterns for efficiency."""
|
|
101
102
|
compiled = {}
|
|
102
103
|
for project, patterns in self.mapping_rules.items():
|
|
103
104
|
compiled[project] = [re.compile(pattern, re.IGNORECASE) for pattern in patterns]
|
|
104
105
|
return compiled
|
|
105
|
-
|
|
106
|
+
|
|
106
107
|
def map_branch_to_project(self, branch_name: str, repo_path: Optional[Path] = None) -> str:
|
|
107
108
|
"""
|
|
108
109
|
Map a branch name to a project key.
|
|
109
|
-
|
|
110
|
+
|
|
110
111
|
Args:
|
|
111
112
|
branch_name: Git branch name
|
|
112
113
|
repo_path: Optional repository path for context
|
|
113
|
-
|
|
114
|
+
|
|
114
115
|
Returns:
|
|
115
116
|
Project key or 'UNKNOWN'
|
|
116
117
|
"""
|
|
117
|
-
if not branch_name or branch_name in [
|
|
118
|
+
if not branch_name or branch_name in ["main", "master", "develop", "development"]:
|
|
118
119
|
# Try to infer from repo path if available
|
|
119
120
|
if repo_path:
|
|
120
121
|
return self._infer_from_repo_path(repo_path)
|
|
121
|
-
return
|
|
122
|
-
|
|
122
|
+
return "UNKNOWN"
|
|
123
|
+
|
|
123
124
|
# Check against compiled patterns
|
|
124
125
|
for project, patterns in self.compiled_rules.items():
|
|
125
126
|
for pattern in patterns:
|
|
126
127
|
if pattern.search(branch_name):
|
|
127
128
|
return project
|
|
128
|
-
|
|
129
|
+
|
|
129
130
|
# Try to extract from ticket references in branch name
|
|
130
131
|
ticket_project = self._extract_from_ticket(branch_name)
|
|
131
132
|
if ticket_project:
|
|
132
133
|
return ticket_project
|
|
133
|
-
|
|
134
|
+
|
|
134
135
|
# Try to infer from repo path if available
|
|
135
136
|
if repo_path:
|
|
136
137
|
return self._infer_from_repo_path(repo_path)
|
|
137
|
-
|
|
138
|
-
return
|
|
139
|
-
|
|
138
|
+
|
|
139
|
+
return "UNKNOWN"
|
|
140
|
+
|
|
140
141
|
def _extract_from_ticket(self, branch_name: str) -> Optional[str]:
|
|
141
142
|
"""Extract project from ticket reference in branch name."""
|
|
142
143
|
# Common ticket patterns
|
|
143
144
|
ticket_patterns = [
|
|
144
|
-
r
|
|
145
|
-
r
|
|
146
|
-
r
|
|
145
|
+
r"([A-Z]{2,})-\d+", # JIRA style: PROJ-123
|
|
146
|
+
r"#([A-Z]{2,})\d+", # Hash prefix: #PROJ123
|
|
147
|
+
r"([A-Z]{2,})_\d+", # Underscore: PROJ_123
|
|
147
148
|
]
|
|
148
|
-
|
|
149
|
+
|
|
149
150
|
for pattern in ticket_patterns:
|
|
150
151
|
match = re.search(pattern, branch_name, re.IGNORECASE)
|
|
151
152
|
if match:
|
|
152
153
|
prefix = match.group(1).upper()
|
|
153
154
|
# Map common prefixes to projects
|
|
154
155
|
prefix_map = {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
156
|
+
"FE": "FRONTEND",
|
|
157
|
+
"BE": "BACKEND",
|
|
158
|
+
"SVC": "SERVICE",
|
|
159
|
+
"MOB": "MOBILE",
|
|
160
|
+
"DATA": "DATA",
|
|
161
|
+
"ETL": "DATA",
|
|
162
|
+
"INFRA": "INFRA",
|
|
163
|
+
"OPS": "INFRA",
|
|
164
|
+
"SCRAPE": "SCRAPER",
|
|
165
|
+
"CRAWL": "SCRAPER",
|
|
165
166
|
}
|
|
166
|
-
|
|
167
|
+
|
|
167
168
|
if prefix in prefix_map:
|
|
168
169
|
return prefix_map[prefix]
|
|
169
|
-
|
|
170
|
+
|
|
170
171
|
# Check if prefix matches any project key
|
|
171
|
-
for project in self.mapping_rules
|
|
172
|
+
for project in self.mapping_rules:
|
|
172
173
|
if prefix == project or prefix in project:
|
|
173
174
|
return project
|
|
174
|
-
|
|
175
|
+
|
|
175
176
|
return None
|
|
176
|
-
|
|
177
|
+
|
|
177
178
|
def _infer_from_repo_path(self, repo_path: Path) -> str:
|
|
178
179
|
"""Infer project from repository path."""
|
|
179
180
|
repo_name = repo_path.name.lower()
|
|
180
|
-
|
|
181
|
+
|
|
181
182
|
# Direct mapping
|
|
182
183
|
path_map = {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
184
|
+
"frontend": "FRONTEND",
|
|
185
|
+
"backend": "BACKEND",
|
|
186
|
+
"service": "SERVICE",
|
|
187
|
+
"service-ts": "SERVICE_TS",
|
|
188
|
+
"services": "SERVICES",
|
|
189
|
+
"mobile": "MOBILE",
|
|
190
|
+
"ios": "MOBILE",
|
|
191
|
+
"android": "MOBILE",
|
|
192
|
+
"data": "DATA",
|
|
193
|
+
"etl": "DATA",
|
|
194
|
+
"infra": "INFRA",
|
|
195
|
+
"infrastructure": "INFRA",
|
|
196
|
+
"scraper": "SCRAPER",
|
|
197
|
+
"crawler": "SCRAPER",
|
|
198
|
+
"scrapers": "SCRAPER",
|
|
198
199
|
}
|
|
199
|
-
|
|
200
|
+
|
|
200
201
|
for key, project in path_map.items():
|
|
201
202
|
if key in repo_name:
|
|
202
203
|
return project
|
|
203
|
-
|
|
204
|
+
|
|
204
205
|
# Check parent directory
|
|
205
206
|
if repo_path.parent.name.lower() in path_map:
|
|
206
207
|
return path_map[repo_path.parent.name.lower()]
|
|
207
|
-
|
|
208
|
-
return
|
|
209
|
-
|
|
210
|
-
def add_mapping_rule(self, project: str, patterns:
|
|
208
|
+
|
|
209
|
+
return "UNKNOWN"
|
|
210
|
+
|
|
211
|
+
def add_mapping_rule(self, project: str, patterns: list[str]) -> None:
|
|
211
212
|
"""Add custom mapping rules for a project."""
|
|
212
213
|
if project not in self.mapping_rules:
|
|
213
214
|
self.mapping_rules[project] = []
|
|
214
|
-
|
|
215
|
+
|
|
215
216
|
self.mapping_rules[project].extend(patterns)
|
|
216
|
-
|
|
217
|
+
|
|
217
218
|
# Recompile patterns
|
|
218
219
|
self.compiled_rules[project] = [
|
|
219
|
-
re.compile(pattern, re.IGNORECASE)
|
|
220
|
-
|
|
221
|
-
]
|
|
220
|
+
re.compile(pattern, re.IGNORECASE) for pattern in self.mapping_rules[project]
|
|
221
|
+
]
|