gitflow-analytics 3.13.5__py3-none-any.whl → 3.13.6__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.
@@ -1,4 +1,4 @@
1
1
  """Version information for gitflow-analytics."""
2
2
 
3
- __version__ = "3.13.5"
3
+ __version__ = "3.13.6"
4
4
  __version_info__ = tuple(int(x) for x in __version__.split("."))
gitflow_analytics/cli.py CHANGED
@@ -1590,6 +1590,68 @@ def analyze(
1590
1590
  total_commits += result["stats"]["total_commits"]
1591
1591
  total_tickets += result["stats"]["unique_tickets"]
1592
1592
 
1593
+ # Fetch and enrich with GitHub PRs after data collection
1594
+ if repo_config.github_repo:
1595
+ try:
1596
+ if display:
1597
+ display.print_status(
1598
+ " 📥 Fetching pull requests from GitHub...",
1599
+ "info",
1600
+ )
1601
+
1602
+ # Load commits that were just fetched from cache
1603
+ with cache.get_session() as session:
1604
+ from gitflow_analytics.models.database import CachedCommit
1605
+
1606
+ cached_commits = (
1607
+ session.query(CachedCommit)
1608
+ .filter(
1609
+ CachedCommit.repo_path == str(repo_path),
1610
+ CachedCommit.timestamp >= start_date,
1611
+ CachedCommit.timestamp <= end_date,
1612
+ )
1613
+ .all()
1614
+ )
1615
+
1616
+ # Convert to dict format for enrichment
1617
+ commits_for_enrichment = []
1618
+ for cached_commit in cached_commits:
1619
+ commit_dict = {
1620
+ "hash": cached_commit.commit_hash,
1621
+ "author_name": cached_commit.author_name,
1622
+ "author_email": cached_commit.author_email,
1623
+ "date": cached_commit.timestamp,
1624
+ "message": cached_commit.message,
1625
+ }
1626
+ commits_for_enrichment.append(commit_dict)
1627
+
1628
+ # Enrich with GitHub PR data
1629
+ enrichment = orchestrator.enrich_repository_data(
1630
+ repo_config, commits_for_enrichment, start_date
1631
+ )
1632
+
1633
+ if enrichment["prs"]:
1634
+ pr_count = len(enrichment["prs"])
1635
+ if display:
1636
+ display.print_status(
1637
+ f" ✅ Found {pr_count} pull requests",
1638
+ "success",
1639
+ )
1640
+ else:
1641
+ click.echo(f" ✅ Found {pr_count} pull requests")
1642
+
1643
+ except Exception as e:
1644
+ logger.warning(
1645
+ f"Failed to fetch PRs for {repo_config.github_repo}: {e}"
1646
+ )
1647
+ if display:
1648
+ display.print_status(
1649
+ f" ⚠️ Could not fetch PRs: {e}",
1650
+ "warning",
1651
+ )
1652
+ else:
1653
+ click.echo(f" ⚠️ Could not fetch PRs: {e}")
1654
+
1593
1655
  # Collect unique developers if available
1594
1656
  if "developers" in result["stats"]:
1595
1657
  total_developers.update(result["stats"]["developers"])
@@ -2088,6 +2150,68 @@ def analyze(
2088
2150
  total_commits += result["stats"]["total_commits"]
2089
2151
  total_tickets += result["stats"]["unique_tickets"]
2090
2152
 
2153
+ # Fetch and enrich with GitHub PRs after data collection
2154
+ if repo_config.github_repo:
2155
+ try:
2156
+ if display:
2157
+ display.print_status(
2158
+ " 📥 Fetching pull requests from GitHub...",
2159
+ "info",
2160
+ )
2161
+
2162
+ # Load commits that were just fetched from cache
2163
+ with cache.get_session() as session:
2164
+ from gitflow_analytics.models.database import CachedCommit
2165
+
2166
+ cached_commits = (
2167
+ session.query(CachedCommit)
2168
+ .filter(
2169
+ CachedCommit.repo_path == str(repo_path),
2170
+ CachedCommit.timestamp >= start_date,
2171
+ CachedCommit.timestamp <= end_date,
2172
+ )
2173
+ .all()
2174
+ )
2175
+
2176
+ # Convert to dict format for enrichment
2177
+ commits_for_enrichment = []
2178
+ for cached_commit in cached_commits:
2179
+ commit_dict = {
2180
+ "hash": cached_commit.commit_hash,
2181
+ "author_name": cached_commit.author_name,
2182
+ "author_email": cached_commit.author_email,
2183
+ "date": cached_commit.timestamp,
2184
+ "message": cached_commit.message,
2185
+ }
2186
+ commits_for_enrichment.append(commit_dict)
2187
+
2188
+ # Enrich with GitHub PR data
2189
+ enrichment = orchestrator.enrich_repository_data(
2190
+ repo_config, commits_for_enrichment, start_date
2191
+ )
2192
+
2193
+ if enrichment["prs"]:
2194
+ pr_count = len(enrichment["prs"])
2195
+ if display:
2196
+ display.print_status(
2197
+ f" ✅ Found {pr_count} pull requests",
2198
+ "success",
2199
+ )
2200
+ else:
2201
+ click.echo(f" ✅ Found {pr_count} pull requests")
2202
+
2203
+ except Exception as e:
2204
+ logger.warning(
2205
+ f"Failed to fetch PRs for {repo_config.github_repo}: {e}"
2206
+ )
2207
+ if display:
2208
+ display.print_status(
2209
+ f" ⚠️ Could not fetch PRs: {e}",
2210
+ "warning",
2211
+ )
2212
+ else:
2213
+ click.echo(f" ⚠️ Could not fetch PRs: {e}")
2214
+
2091
2215
  # Collect unique developers if available
2092
2216
  if "developers" in result["stats"]:
2093
2217
  total_developers.update(result["stats"]["developers"])
@@ -112,6 +112,80 @@ def setup_git_credentials(token: str, username: str = "git") -> bool:
112
112
  return False
113
113
 
114
114
 
115
+ def ensure_remote_url_has_token(repo_path: Path, token: str) -> bool:
116
+ """Embed GitHub token in remote URL for HTTPS authentication.
117
+
118
+ This is needed because subprocess git operations may not have access
119
+ to the credential helper store due to environment variable restrictions
120
+ (GIT_CREDENTIAL_HELPER="" and GIT_ASKPASS="/bin/echo" in git_timeout_wrapper).
121
+
122
+ Args:
123
+ repo_path: Path to the git repository
124
+ token: GitHub personal access token
125
+
126
+ Returns:
127
+ True if URL was updated with token, False if already has token,
128
+ not applicable (SSH URL), or operation failed
129
+ """
130
+ if not token:
131
+ logger.debug("No token provided, skipping remote URL update")
132
+ return False
133
+
134
+ try:
135
+ # Get current origin remote URL
136
+ result = subprocess.run(
137
+ ["git", "remote", "get-url", "origin"],
138
+ cwd=repo_path,
139
+ capture_output=True,
140
+ text=True,
141
+ check=True,
142
+ )
143
+ current_url = result.stdout.strip()
144
+
145
+ if not current_url:
146
+ logger.debug(f"No origin remote found for {repo_path}")
147
+ return False
148
+
149
+ # Check if it's an HTTPS GitHub URL without embedded token
150
+ if current_url.startswith("https://github.com/"):
151
+ # URL format: https://github.com/org/repo.git
152
+ # New format: https://git:TOKEN@github.com/org/repo.git
153
+ new_url = current_url.replace("https://github.com/", f"https://git:{token}@github.com/")
154
+
155
+ # Update the remote URL
156
+ subprocess.run(
157
+ ["git", "remote", "set-url", "origin", new_url],
158
+ cwd=repo_path,
159
+ capture_output=True,
160
+ text=True,
161
+ check=True,
162
+ )
163
+ logger.debug(f"Updated remote URL with embedded token for {repo_path.name}")
164
+ return True
165
+
166
+ elif "@github.com" in current_url:
167
+ # Already has authentication embedded (either token or SSH)
168
+ logger.debug(f"Remote URL already has authentication for {repo_path.name}")
169
+ return False
170
+
171
+ elif current_url.startswith("git@github.com:"):
172
+ # SSH URL, no need to modify
173
+ logger.debug(f"Using SSH authentication for {repo_path.name}")
174
+ return False
175
+
176
+ else:
177
+ # Unknown URL format
178
+ logger.debug(f"Unknown URL format for {repo_path.name}: {current_url}")
179
+ return False
180
+
181
+ except subprocess.CalledProcessError as e:
182
+ logger.warning(f"Could not update remote URL for {repo_path.name}: {e.stderr}")
183
+ return False
184
+ except Exception as e:
185
+ logger.warning(f"Unexpected error updating remote URL for {repo_path.name}: {e}")
186
+ return False
187
+
188
+
115
189
  def preflight_git_authentication(config: dict) -> bool:
116
190
  """Run pre-flight checks for git authentication and setup credentials.
117
191
 
@@ -14,6 +14,7 @@ from pathlib import Path
14
14
  from typing import Callable, Optional, TypeVar
15
15
 
16
16
  from ..constants import Timeouts
17
+ from .git_auth import ensure_remote_url_has_token
17
18
 
18
19
  logger = logging.getLogger(__name__)
19
20
 
@@ -208,6 +209,13 @@ class GitTimeoutWrapper:
208
209
  True if fetch succeeded, False otherwise
209
210
  """
210
211
  try:
212
+ # Embed GitHub token in remote URL if available
213
+ # This is necessary because git operations run with GIT_CREDENTIAL_HELPER=""
214
+ # and GIT_ASKPASS="/bin/echo", which disable credential helpers
215
+ token = os.environ.get("GITHUB_TOKEN")
216
+ if token:
217
+ ensure_remote_url_has_token(repo_path, token)
218
+
211
219
  self.run_git_command(
212
220
  ["git", "fetch", "--all"], cwd=repo_path, timeout=timeout, check=True
213
221
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitflow-analytics
3
- Version: 3.13.5
3
+ Version: 3.13.6
4
4
  Summary: Analyze Git repositories for developer productivity insights
5
5
  Author-email: Bob Matyas <bobmatnyc@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  gitflow_analytics/__init__.py,sha256=W3Jaey5wuT1nBPehVLTIRkVIyBa5jgYOlBKc_UFfh-4,773
2
- gitflow_analytics/_version.py,sha256=-dJJFHKImkdT2L9Kw0gRXFj8dh26CFKPTsdrAlg1aTg,138
3
- gitflow_analytics/cli.py,sha256=KdnuqiUodiqmImsDfMv15MuOlLrI_2zTv3x4s1BV0U8,302532
2
+ gitflow_analytics/_version.py,sha256=LYLmUGZ3H7nuZiBCfCqGEKil8Qex-qsdWM7XiJjo6d8,138
3
+ gitflow_analytics/cli.py,sha256=EEmj-1Q8ifFgV3U7ImIoRzUezZFDaO80M_VIegRbgec,309932
4
4
  gitflow_analytics/config.py,sha256=XRuxvzLWyn_ML7mDCcuZ9-YFNAEsnt33vIuWxQQ_jxg,1033
5
5
  gitflow_analytics/constants.py,sha256=GXEncUJS9ijOI5KWtQCTANwdqxPfXpw-4lNjhaWTKC4,2488
6
6
  gitflow_analytics/verify_activity.py,sha256=q82VnU8FhHEPlnupYMvh1XtyaDJfIPPg-AI8cSM0PIk,27054
@@ -27,8 +27,8 @@ gitflow_analytics/core/analyzer.py,sha256=apLbRFAOGDPCNnBTNOG_eXaVXh_QglO07t6p5s
27
27
  gitflow_analytics/core/branch_mapper.py,sha256=1L1ctrhTEqMZ61eS1nZRkcyaarLipeQgotw4HdXcSmM,7407
28
28
  gitflow_analytics/core/cache.py,sha256=2SBzry3FoLCJyhu-I-AgNTSzN_MkA-DunzOAxq_lyTw,69152
29
29
  gitflow_analytics/core/data_fetcher.py,sha256=KI0lGxrKvjOHf2UjnGytmcy9GSnSsA28c5mysOH7q1o,106944
30
- gitflow_analytics/core/git_auth.py,sha256=QP7U5_Mi9J-hEtoEhdjoMBl61nCukOGlL8PYXYSyN3g,6369
31
- gitflow_analytics/core/git_timeout_wrapper.py,sha256=14K8PHKSOonW4hJpLigB5XQNSWxmFbMFbrpu8cT1h-M,12534
30
+ gitflow_analytics/core/git_auth.py,sha256=KPUT6qmUUjKfXRDPEIXBzYu6g4jO2RH94xcA_H28vXY,9128
31
+ gitflow_analytics/core/git_timeout_wrapper.py,sha256=EPMOFnGYe_vrpmODraO8MHA8tUChS1pDPlQfYsyCUEQ,12945
32
32
  gitflow_analytics/core/identity.py,sha256=CTjxpM5BeeMyGQ8QbtSCsUmuzMmU7vhBwrdQctjI7Z0,31397
33
33
  gitflow_analytics/core/metrics_storage.py,sha256=2u4dxVHsCTEaVIO5udWCaHzuefRL7JVS8aN7wIwyMlc,21769
34
34
  gitflow_analytics/core/progress.py,sha256=KMXwZpJGlmUU8OehNRA7_PONpXUgSIxWl5ZN7INc108,20732
@@ -127,9 +127,9 @@ gitflow_analytics/ui/__init__.py,sha256=UBhYhZMvwlSrCuGWjkIdoP2zNbiQxOHOli-I8mqI
127
127
  gitflow_analytics/ui/progress_display.py,sha256=omCS86mCQR0QeMoM0YnsV3Gf2oALsDLu8u7XseQU6lk,59306
128
128
  gitflow_analytics/utils/__init__.py,sha256=YE3E5Mx-LmVRqLIgUUwDmbstm6gkpeavYHrQmVjwR3o,197
129
129
  gitflow_analytics/utils/commit_utils.py,sha256=TBgrWW73EODGOegGCF79ch0L0e5R6gpydNWutiQOa14,1356
130
- gitflow_analytics-3.13.5.dist-info/licenses/LICENSE,sha256=xwvSwY1GYXpRpmbnFvvnbmMwpobnrdN9T821sGvjOY0,1066
131
- gitflow_analytics-3.13.5.dist-info/METADATA,sha256=iB3vdWqPASbljtKC1rORHcIbiP1X21egIORMtIIGFws,40374
132
- gitflow_analytics-3.13.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
133
- gitflow_analytics-3.13.5.dist-info/entry_points.txt,sha256=ZOsX0GLsnMysp5FWPOfP_qyoS7WJ8IgcaDFDxWBYl1g,98
134
- gitflow_analytics-3.13.5.dist-info/top_level.txt,sha256=CQyxZXjKvpSB1kgqqtuE0PCRqfRsXZJL8JrYpJKtkrk,18
135
- gitflow_analytics-3.13.5.dist-info/RECORD,,
130
+ gitflow_analytics-3.13.6.dist-info/licenses/LICENSE,sha256=xwvSwY1GYXpRpmbnFvvnbmMwpobnrdN9T821sGvjOY0,1066
131
+ gitflow_analytics-3.13.6.dist-info/METADATA,sha256=iDW0016J8hTQPenAka3HPuGtsZrIJhFJZLw-XV2NlqU,40374
132
+ gitflow_analytics-3.13.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
133
+ gitflow_analytics-3.13.6.dist-info/entry_points.txt,sha256=ZOsX0GLsnMysp5FWPOfP_qyoS7WJ8IgcaDFDxWBYl1g,98
134
+ gitflow_analytics-3.13.6.dist-info/top_level.txt,sha256=CQyxZXjKvpSB1kgqqtuE0PCRqfRsXZJL8JrYpJKtkrk,18
135
+ gitflow_analytics-3.13.6.dist-info/RECORD,,