|
13 | 13 | import re |
14 | 14 | import subprocess |
15 | 15 | from dataclasses import dataclass |
| 16 | +from urllib.parse import urlparse |
16 | 17 |
|
17 | 18 |
|
18 | 19 | try: |
@@ -1247,11 +1248,17 @@ def _read_openspec_change_proposals(self, include_archived: bool = True) -> list |
1247 | 1248 | if url_repo_match: |
1248 | 1249 | entry["source_repo"] = url_repo_match.group(1) |
1249 | 1250 | # Try ADO URL pattern - extract org, but we need project name from elsewhere |
1250 | | - elif "dev.azure.com" in source_url: |
1251 | | - # For ADO, we can't reliably extract project name from URL (GUID) |
1252 | | - # The source_repo should have been saved in the hidden comment |
1253 | | - # If not, we'll need to match by org only later |
1254 | | - pass |
| 1251 | + else: |
| 1252 | + # Use proper URL parsing to validate ADO URLs |
| 1253 | + try: |
| 1254 | + parsed = urlparse(source_url) |
| 1255 | + if parsed.hostname and parsed.hostname.lower() == "dev.azure.com": |
| 1256 | + # For ADO, we can't reliably extract project name from URL (GUID) |
| 1257 | + # The source_repo should have been saved in the hidden comment |
| 1258 | + # If not, we'll need to match by org only later |
| 1259 | + pass |
| 1260 | + except Exception: |
| 1261 | + pass |
1255 | 1262 | source_tracking_list.append(entry) |
1256 | 1263 |
|
1257 | 1264 | # Check for status indicators in proposal content or directory name |
@@ -1539,16 +1546,21 @@ def _find_source_tracking_entry( |
1539 | 1546 | return source_tracking |
1540 | 1547 | # Try ADO URL pattern (ADO URLs contain GUIDs, not project names) |
1541 | 1548 | # For ADO, match by org if target_repo contains the org |
1542 | | - elif "dev.azure.com" in source_url and "/" in target_repo: |
1543 | | - target_org = target_repo.split("/")[0] |
1544 | | - ado_org_match = re.search(r"dev\.azure\.com/([^/]+)/", source_url) |
1545 | | - # Org matches and source_type is "ado" - return entry (project name may differ due to GUID in URL) |
1546 | | - if ( |
1547 | | - ado_org_match |
1548 | | - and ado_org_match.group(1) == target_org |
1549 | | - and (entry_type == "ado" or entry_type == "") |
1550 | | - ): |
1551 | | - return source_tracking |
| 1549 | + elif "/" in target_repo: |
| 1550 | + try: |
| 1551 | + parsed = urlparse(source_url) |
| 1552 | + if parsed.hostname and parsed.hostname.lower() == "dev.azure.com": |
| 1553 | + target_org = target_repo.split("/")[0] |
| 1554 | + ado_org_match = re.search(r"dev\.azure\.com/([^/]+)/", source_url) |
| 1555 | + # Org matches and source_type is "ado" - return entry (project name may differ due to GUID in URL) |
| 1556 | + if ( |
| 1557 | + ado_org_match |
| 1558 | + and ado_org_match.group(1) == target_org |
| 1559 | + and (entry_type == "ado" or entry_type == "") |
| 1560 | + ): |
| 1561 | + return source_tracking |
| 1562 | + except Exception: |
| 1563 | + pass |
1552 | 1564 |
|
1553 | 1565 | # Tertiary match: for ADO, only match by org when project is truly unknown (GUID-only URLs) |
1554 | 1566 | # This prevents cross-project matches when both entry_repo and target_repo have project names |
@@ -1617,16 +1629,21 @@ def _find_source_tracking_entry( |
1617 | 1629 | return entry |
1618 | 1630 | # Try ADO URL pattern (but note: ADO URLs contain GUIDs, not project names) |
1619 | 1631 | # For ADO, match by org if target_repo contains the org |
1620 | | - elif "dev.azure.com" in source_url and "/" in target_repo: |
1621 | | - target_org = target_repo.split("/")[0] |
1622 | | - ado_org_match = re.search(r"dev\.azure\.com/([^/]+)/", source_url) |
1623 | | - # Org matches and source_type is "ado" - return entry (project name may differ due to GUID in URL) |
1624 | | - if ( |
1625 | | - ado_org_match |
1626 | | - and ado_org_match.group(1) == target_org |
1627 | | - and (entry_type == "ado" or entry_type == "") |
1628 | | - ): |
1629 | | - return entry |
| 1632 | + elif "/" in target_repo: |
| 1633 | + try: |
| 1634 | + parsed = urlparse(source_url) |
| 1635 | + if parsed.hostname and parsed.hostname.lower() == "dev.azure.com": |
| 1636 | + target_org = target_repo.split("/")[0] |
| 1637 | + ado_org_match = re.search(r"dev\.azure\.com/([^/]+)/", source_url) |
| 1638 | + # Org matches and source_type is "ado" - return entry (project name may differ due to GUID in URL) |
| 1639 | + if ( |
| 1640 | + ado_org_match |
| 1641 | + and ado_org_match.group(1) == target_org |
| 1642 | + and (entry_type == "ado" or entry_type == "") |
| 1643 | + ): |
| 1644 | + return entry |
| 1645 | + except Exception: |
| 1646 | + pass |
1630 | 1647 |
|
1631 | 1648 | # Tertiary match: for ADO, only match by org when project is truly unknown (GUID-only URLs) |
1632 | 1649 | # This prevents cross-project matches when both entry_repo and target_repo have project names |
|
0 commit comments