github2gerrit 0.1.10__py3-none-any.whl → 0.1.12__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.
@@ -0,0 +1,345 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
3
+ """
4
+ Mapping comment utilities for serializing and deserializing Change-ID mapping
5
+ blocks.
6
+
7
+ This module handles the structured PR comments that track the mapping between
8
+ local commits and Gerrit Change-IDs for reconciliation purposes.
9
+ """
10
+
11
+ import logging
12
+ from dataclasses import dataclass
13
+
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+ # Error message constants to comply with TRY003
18
+ _MSG_INVALID_MODE = "Invalid mode"
19
+ _MSG_NO_CHANGE_IDS = "At least one Change-ID is required"
20
+ _MSG_INVALID_CHANGE_ID_FORMAT = "Invalid Change-ID format"
21
+
22
+
23
+ @dataclass
24
+ class ChangeIdMapping:
25
+ """Represents a Change-ID mapping from a PR comment (with optional
26
+ digest)."""
27
+
28
+ pr_url: str
29
+ mode: str # "multi-commit" or "squash"
30
+ topic: str
31
+ change_ids: list[str]
32
+ github_hash: str = ""
33
+ digest: str = ""
34
+
35
+ def __post_init__(self) -> None:
36
+ """Validate mapping data after initialization."""
37
+ if self.mode not in ("multi-commit", "squash"):
38
+ raise ValueError(_MSG_INVALID_MODE)
39
+
40
+ if not self.change_ids:
41
+ raise ValueError(_MSG_NO_CHANGE_IDS)
42
+
43
+ # Validate Change-Id format
44
+ for cid in self.change_ids:
45
+ if not cid.startswith("I") or len(cid) < 8:
46
+ raise ValueError(_MSG_INVALID_CHANGE_ID_FORMAT)
47
+
48
+
49
+ def serialize_mapping_comment(
50
+ pr_url: str,
51
+ mode: str,
52
+ topic: str,
53
+ change_ids: list[str],
54
+ github_hash: str,
55
+ digest: str | None = None,
56
+ ) -> str:
57
+ """
58
+ Serialize a Change-ID mapping into a structured PR comment.
59
+
60
+ Args:
61
+ pr_url: Full GitHub PR URL
62
+ mode: Submission mode ("multi-commit" or "squash")
63
+ topic: Gerrit topic name
64
+ change_ids: Ordered list of Change-IDs
65
+ github_hash: GitHub-Hash trailer value for verification
66
+
67
+ Returns:
68
+ Formatted comment body with mapping block
69
+ """
70
+ if not change_ids:
71
+ raise ValueError(_MSG_NO_CHANGE_IDS)
72
+
73
+ lines = [
74
+ "<!-- github2gerrit:change-id-map v1 -->",
75
+ f"PR: {pr_url}",
76
+ f"Mode: {mode}",
77
+ f"Topic: {topic}",
78
+ "Change-Ids:",
79
+ ]
80
+
81
+ # Add indented Change-IDs
82
+ for cid in change_ids:
83
+ lines.append(f" {cid}")
84
+
85
+ if digest:
86
+ lines.append(f"Digest: {digest}")
87
+
88
+ lines.extend(
89
+ [
90
+ f"GitHub-Hash: {github_hash}",
91
+ "<!-- end github2gerrit:change-id-map -->",
92
+ ]
93
+ )
94
+
95
+ return "\n".join(lines)
96
+
97
+
98
+ def parse_mapping_comments(comment_bodies: list[str]) -> ChangeIdMapping | None:
99
+ """
100
+ Parse Change-ID mapping from PR comment bodies.
101
+
102
+ Scans comments from oldest to newest, returning the latest valid mapping.
103
+
104
+ Args:
105
+ comment_bodies: List of comment body texts to scan
106
+
107
+ Returns:
108
+ Latest valid ChangeIdMapping or None if no mapping found
109
+ """
110
+ latest_mapping: ChangeIdMapping | None = None
111
+
112
+ start_marker = "<!-- github2gerrit:change-id-map v1 -->"
113
+ end_marker = "<!-- end github2gerrit:change-id-map -->"
114
+
115
+ for body in comment_bodies:
116
+ if start_marker not in body or end_marker not in body:
117
+ continue
118
+
119
+ try:
120
+ # Extract the mapping block
121
+ start_idx = body.find(start_marker)
122
+ end_idx = body.find(end_marker)
123
+
124
+ if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
125
+ continue
126
+
127
+ block = body[start_idx + len(start_marker) : end_idx].strip()
128
+ mapping = _parse_mapping_block(block)
129
+
130
+ if mapping:
131
+ latest_mapping = mapping
132
+ log.debug(
133
+ "Found mapping with %d Change-IDs in %s mode",
134
+ len(mapping.change_ids),
135
+ mapping.mode,
136
+ )
137
+
138
+ except Exception as exc:
139
+ log.debug("Failed to parse mapping block: %s", exc)
140
+ continue
141
+
142
+ if latest_mapping:
143
+ log.debug(
144
+ "Recovered mapping with %d Change-ID(s) for topic '%s'",
145
+ len(latest_mapping.change_ids),
146
+ latest_mapping.topic,
147
+ )
148
+
149
+ return latest_mapping
150
+
151
+
152
+ def _parse_mapping_block(block: str) -> ChangeIdMapping | None:
153
+ """
154
+ Parse a single mapping block into a ChangeIdMapping.
155
+
156
+ Args:
157
+ block: Mapping block content (without HTML markers)
158
+
159
+ Returns:
160
+ Parsed ChangeIdMapping or None if invalid
161
+ """
162
+ lines = [line.strip() for line in block.split("\n")]
163
+
164
+ pr_url = ""
165
+ mode = ""
166
+ topic = ""
167
+ change_ids: list[str] = []
168
+ github_hash = ""
169
+ digest = ""
170
+
171
+ in_change_ids = False
172
+
173
+ for line in lines:
174
+ if not line:
175
+ continue
176
+
177
+ if line.startswith("PR:"):
178
+ pr_url = line[3:].strip()
179
+ elif line.startswith("Mode:"):
180
+ mode = line[5:].strip()
181
+ elif line.startswith("Topic:"):
182
+ topic = line[6:].strip()
183
+ elif line.startswith("Change-Ids:"):
184
+ in_change_ids = True
185
+ elif line.startswith("GitHub-Hash:"):
186
+ github_hash = line[12:].strip()
187
+ in_change_ids = False
188
+ elif line.startswith("Digest:"):
189
+ digest = line[7:].strip()
190
+ elif in_change_ids and line.startswith("I"):
191
+ # Extract Change-ID (handle potential whitespace/formatting)
192
+ cid = line.split()[0]
193
+ if cid not in change_ids: # Avoid duplicates
194
+ change_ids.append(cid)
195
+
196
+ # Validate required fields (github_hash is optional for backward
197
+ # compatibility)
198
+ if not all([pr_url, mode, topic, change_ids]):
199
+ log.debug(
200
+ "Incomplete mapping block: pr_url=%s, mode=%s, topic=%s, "
201
+ "change_ids=%d, github_hash=%s",
202
+ bool(pr_url),
203
+ mode,
204
+ bool(topic),
205
+ len(change_ids),
206
+ bool(github_hash),
207
+ )
208
+ return None
209
+
210
+ try:
211
+ return ChangeIdMapping(
212
+ pr_url=pr_url,
213
+ mode=mode,
214
+ topic=topic,
215
+ change_ids=change_ids,
216
+ github_hash=github_hash or "",
217
+ digest=digest or "",
218
+ )
219
+ except ValueError as exc:
220
+ log.debug("Invalid mapping data: %s", exc)
221
+ return None
222
+
223
+
224
+ def find_mapping_comments(comment_bodies: list[str]) -> list[int]:
225
+ """
226
+ Find indices of comments containing Change-ID mapping blocks.
227
+
228
+ Args:
229
+ comment_bodies: List of comment body texts
230
+
231
+ Returns:
232
+ List of comment indices that contain mapping blocks
233
+ """
234
+ indices = []
235
+ start_marker = "<!-- github2gerrit:change-id-map v1 -->"
236
+
237
+ for i, body in enumerate(comment_bodies):
238
+ if start_marker in body:
239
+ indices.append(i)
240
+
241
+ return indices
242
+
243
+
244
+ def update_mapping_comment_body(
245
+ original_body: str,
246
+ new_mapping: ChangeIdMapping,
247
+ ) -> str:
248
+ """
249
+ Update a comment body with a new mapping, replacing the existing one.
250
+
251
+ Args:
252
+ original_body: Original comment body text
253
+ new_mapping: New mapping to insert
254
+
255
+ Returns:
256
+ Updated comment body with new mapping
257
+ """
258
+ start_marker = "<!-- github2gerrit:change-id-map v1 -->"
259
+ end_marker = "<!-- end github2gerrit:change-id-map -->"
260
+
261
+ # Generate new mapping block
262
+ new_block = serialize_mapping_comment(
263
+ pr_url=new_mapping.pr_url,
264
+ mode=new_mapping.mode,
265
+ topic=new_mapping.topic,
266
+ change_ids=new_mapping.change_ids,
267
+ github_hash=new_mapping.github_hash,
268
+ )
269
+
270
+ # If no existing mapping, append the new one
271
+ if start_marker not in original_body:
272
+ if original_body and not original_body.endswith("\n"):
273
+ return original_body + "\n\n" + new_block
274
+ else:
275
+ return original_body + new_block
276
+
277
+ # Replace existing mapping
278
+ start_idx = original_body.find(start_marker)
279
+ end_idx = original_body.find(end_marker)
280
+
281
+ if start_idx == -1 or end_idx == -1:
282
+ # Malformed existing mapping, append new one
283
+ return original_body + "\n\n" + new_block
284
+
285
+ # Include the end marker in the replacement
286
+ end_idx += len(end_marker)
287
+
288
+ # Replace the old mapping with the new one
289
+ updated_body = (
290
+ original_body[:start_idx] + new_block + original_body[end_idx:]
291
+ )
292
+
293
+ return updated_body.strip()
294
+
295
+
296
+ def compute_mapping_digest(change_ids: list[str]) -> str:
297
+ """
298
+ Compute a digest of the Change-ID list for quick comparison.
299
+
300
+ Args:
301
+ change_ids: Ordered list of Change-IDs
302
+
303
+ Returns:
304
+ SHA-256 digest (first 12 hex chars) of the ordered Change-IDs
305
+ """
306
+ import hashlib
307
+
308
+ content = "\n".join(change_ids)
309
+ hash_obj = hashlib.sha256(content.encode("utf-8"))
310
+ return hash_obj.hexdigest()[:12]
311
+
312
+
313
+ def validate_mapping_consistency(
314
+ mapping: ChangeIdMapping,
315
+ expected_pr_url: str,
316
+ expected_github_hash: str,
317
+ ) -> bool:
318
+ """
319
+ Validate that a mapping is consistent with expected PR metadata.
320
+
321
+ Args:
322
+ mapping: Parsed mapping to validate
323
+ expected_pr_url: Expected GitHub PR URL
324
+ expected_github_hash: Expected GitHub-Hash value
325
+
326
+ Returns:
327
+ True if mapping is consistent with expectations
328
+ """
329
+ if mapping.pr_url != expected_pr_url:
330
+ log.warning(
331
+ "Mapping PR URL mismatch: expected=%s, found=%s",
332
+ expected_pr_url,
333
+ mapping.pr_url,
334
+ )
335
+ return False
336
+
337
+ if mapping.github_hash != expected_github_hash:
338
+ log.warning(
339
+ "Mapping GitHub-Hash mismatch: expected=%s, found=%s",
340
+ expected_github_hash,
341
+ mapping.github_hash,
342
+ )
343
+ return False
344
+
345
+ return True
github2gerrit/models.py CHANGED
@@ -49,13 +49,27 @@ class Inputs:
49
49
 
50
50
  # Optional (reusable workflow compatibility / overrides)
51
51
  gerrit_server: str
52
- gerrit_server_port: str
52
+ gerrit_server_port: int
53
53
  gerrit_project: str
54
54
  issue_id: str
55
55
  allow_duplicates: bool
56
56
  ci_testing: bool
57
57
  duplicates_filter: str = "open"
58
58
 
59
+ # Reconciliation configuration options
60
+ reuse_strategy: str = "topic+comment" # topic, comment, topic+comment, none
61
+ similarity_subject: float = 0.7 # Subject token Jaccard threshold
62
+ similarity_files: bool = True # File signature match requirement
63
+ allow_orphan_changes: bool = (
64
+ False # Keep unmatched Gerrit changes without warning
65
+ )
66
+ persist_single_mapping_comment: bool = (
67
+ True # Replace vs append mapping comments
68
+ )
69
+ log_reconcile_json: bool = (
70
+ True # Emit structured JSON reconciliation summary
71
+ )
72
+
59
73
 
60
74
  @dataclass(frozen=True)
61
75
  class GitHubContext:
@@ -0,0 +1,25 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
3
+ """
4
+ Orchestrator subpackage.
5
+
6
+ Phase 1 extraction scaffold:
7
+ - Provides a namespace for incremental migration of logic from the legacy
8
+ monolithic `core.Orchestrator` implementation toward a phased pipeline.
9
+ - Initial deliverable only extracts reconciliation invocation (see
10
+ `reconciliation.py`) while preserving public behavior.
11
+
12
+ Subsequent phases will populate:
13
+ - `pipeline.py` for ordered phase coordination
14
+ - `context.py` for immutable run/repository context objects
15
+ - Additional modules (ssh, git_ops, mapping, backref, verification, etc.)
16
+
17
+ Import surface kept intentionally minimal to avoid premature coupling.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from .reconciliation import perform_reconciliation # re-export convenience
23
+
24
+
25
+ __all__ = ["perform_reconciliation"]