github2gerrit 0.1.8__py3-none-any.whl → 0.1.9__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.
github2gerrit/cli.py CHANGED
@@ -3,13 +3,10 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- import io
7
6
  import json
8
7
  import logging
9
8
  import os
10
- import shutil
11
9
  import tempfile
12
- import zipfile
13
10
  from collections.abc import Callable
14
11
  from concurrent.futures import ThreadPoolExecutor
15
12
  from concurrent.futures import as_completed
@@ -20,8 +17,6 @@ from typing import Protocol
20
17
  from typing import TypeVar
21
18
  from typing import cast
22
19
  from urllib.parse import urlparse
23
- from urllib.request import Request
24
- from urllib.request import urlopen
25
20
 
26
21
  import click
27
22
  import typer
@@ -295,11 +290,14 @@ def main(
295
290
  """
296
291
  Tool to convert GitHub pull requests into Gerrit changes
297
292
 
298
- - Providing a URL to a pull request: converts that pull request into a Gerrit change
293
+ - Providing a URL to a pull request: converts that pull request
294
+ into a Gerrit change
299
295
 
300
- - Providing a URL to a GitHub repository converts all open pull requests into Gerrit changes
296
+ - Providing a URL to a GitHub repository converts all open pull
297
+ requests into Gerrit changes
301
298
 
302
- - No arguments for CI/CD environment; reads parameters from environment variables
299
+ - No arguments for CI/CD environment; reads parameters from
300
+ environment variables
303
301
  """
304
302
  # Override boolean parameters with properly parsed environment variables
305
303
  # This ensures that string "false" from GitHub Actions is handled correctly
@@ -361,15 +359,21 @@ def main(
361
359
  # URL mode handling
362
360
  if target_url:
363
361
  org, repo, pr = _parse_github_target(target_url)
362
+ log.debug("Parsed GitHub URL: org=%s, repo=%s, pr=%s", org, repo, pr)
364
363
  if org:
365
364
  os.environ["ORGANIZATION"] = org
365
+ log.debug("Set ORGANIZATION=%s", org)
366
366
  if org and repo:
367
- os.environ["GITHUB_REPOSITORY"] = f"{org}/{repo}"
367
+ github_repo = f"{org}/{repo}"
368
+ os.environ["GITHUB_REPOSITORY"] = github_repo
369
+ log.debug("Set GITHUB_REPOSITORY=%s", github_repo)
368
370
  if pr:
369
371
  os.environ["PR_NUMBER"] = str(pr)
370
372
  os.environ["SYNC_ALL_OPEN_PRS"] = "false"
373
+ log.debug("Set PR_NUMBER=%s", pr)
371
374
  else:
372
375
  os.environ["SYNC_ALL_OPEN_PRS"] = "true"
376
+ log.debug("Set SYNC_ALL_OPEN_PRS=true")
373
377
  os.environ["G2G_TARGET_URL"] = "1"
374
378
  # Debug: Show environment at CLI startup
375
379
  log.debug("CLI startup environment check:")
@@ -748,6 +752,13 @@ def _prepare_local_checkout(workspace: Path, gh: GitHubContext, data: Inputs) ->
748
752
 
749
753
  def _fallback_to_api_archive(workspace: Path, gh: GitHubContext, data: Inputs, pr_num_str: str) -> None:
750
754
  """Fallback to GitHub API archive download for private repos."""
755
+ import io
756
+ import json
757
+ import shutil
758
+ import zipfile
759
+ from urllib.request import Request
760
+ from urllib.request import urlopen
761
+
751
762
  log.info("Attempting API archive fallback for PR #%s", pr_num_str)
752
763
 
753
764
  # Get GitHub token for authenticated requests
github2gerrit/config.py CHANGED
@@ -131,6 +131,22 @@ def _coerce_value(raw: str) -> str:
131
131
  # like SSH keys or known_hosts entries can be specified inline using
132
132
  # '\n' or '\r\n' in configuration files.
133
133
  normalized_newlines = unquoted.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\r\n", "\n")
134
+
135
+ # Additional sanitization for SSH private keys
136
+ if ("-----BEGIN" in normalized_newlines and "PRIVATE KEY-----" in normalized_newlines) or (
137
+ "ssh-" in normalized_newlines.lower() and "key" in normalized_newlines.lower()
138
+ ):
139
+ # Clean up SSH key formatting: remove extra whitespace, normalize line endings
140
+ lines = normalized_newlines.split("\n")
141
+ sanitized_lines = []
142
+ for line in lines:
143
+ cleaned = line.strip()
144
+ if cleaned:
145
+ # Remove any stray quotes that might have been embedded in the key content
146
+ cleaned = cleaned.replace('"', "").replace("'", "")
147
+ sanitized_lines.append(cleaned)
148
+ normalized_newlines = "\n".join(sanitized_lines)
149
+
134
150
  b = _normalize_bool_like(normalized_newlines)
135
151
  return b if b is not None else normalized_newlines
136
152
 
@@ -149,7 +165,7 @@ def _select_section(
149
165
 
150
166
  def _load_ini(path: Path) -> configparser.RawConfigParser:
151
167
  cp = configparser.RawConfigParser()
152
- # Preserve option case; mypy requires a cast for attribute assignment
168
+ # Preserve option case; mypy requires a cast for attribute requirement
153
169
  cast(Any, cp).optionxform = str
154
170
  try:
155
171
  with path.open("r", encoding="utf-8") as fh:
@@ -162,6 +178,9 @@ def _load_ini(path: Path) -> configparser.RawConfigParser:
162
178
  # We collapse these into a single line with '\n' escapes so that
163
179
  # configparser can ingest them reliably; later, _coerce_value()
164
180
  # converts the escapes back to real newlines.
181
+ #
182
+ # We also handle SSH private keys and other multi-line values that
183
+ # might have formatting inconsistencies by sanitizing them.
165
184
  lines = raw_text.splitlines()
166
185
  out_lines: list[str] = []
167
186
  i = 0
@@ -171,6 +190,8 @@ def _load_ini(path: Path) -> configparser.RawConfigParser:
171
190
  if eq_idx != -1:
172
191
  left = line[: eq_idx + 1]
173
192
  rhs = line[eq_idx + 1 :].strip()
193
+
194
+ # Handle standard multi-line quoted values: key = "
174
195
  if rhs == '"':
175
196
  i += 1
176
197
  block: list[str] = []
@@ -187,10 +208,64 @@ def _load_ini(path: Path) -> configparser.RawConfigParser:
187
208
  else:
188
209
  # No closing quote found; fall through
189
210
  # and keep original line
211
+ log.debug("Multi-line quote not properly closed for line: %s", line[:50])
190
212
  out_lines.append(line)
191
213
  continue
214
+
215
+ # Handle SSH private keys and other values that start with a quote
216
+ # but contain embedded content that might confuse configparser
217
+ elif rhs.startswith('"') and not rhs.endswith('"'):
218
+ # This looks like a multi-line value that starts on the same line
219
+ # Collect all content until we find a line ending with a quote
220
+ content_lines = [rhs[1:]] # Remove opening quote
221
+ i += 1
222
+
223
+ while i < len(lines):
224
+ current_line = lines[i]
225
+ if current_line.strip().endswith('"') and not current_line.strip().endswith('\\"'):
226
+ # Found closing quote - remove it and add final line
227
+ final_content = current_line.rstrip()
228
+ if final_content.endswith('"'):
229
+ final_content = final_content[:-1]
230
+ if final_content: # Only add if there's content after removing quote
231
+ content_lines.append(final_content)
232
+ break
233
+ else:
234
+ content_lines.append(current_line)
235
+ i += 1
236
+
237
+ # Join all content and sanitize for SSH keys
238
+ full_content = "\\n".join(content_lines)
239
+
240
+ # Special handling for SSH private keys - remove extra whitespace and line breaks
241
+ key_name = left.split("=")[0].strip().upper()
242
+ if "SSH" in key_name and "KEY" in key_name:
243
+ # For SSH keys, clean up base64 content by removing whitespace within lines
244
+ sanitized_lines = []
245
+ for content_line in content_lines:
246
+ cleaned = content_line.strip()
247
+ # Preserve SSH key headers/footers but clean base64 content
248
+ if cleaned.startswith("-----") or not cleaned:
249
+ sanitized_lines.append(cleaned)
250
+ else:
251
+ # Remove any embedded quotes and whitespace from base64 content
252
+ cleaned = cleaned.replace('"', "").replace("'", "").strip()
253
+ if cleaned:
254
+ sanitized_lines.append(cleaned)
255
+ full_content = "\\n".join(sanitized_lines)
256
+
257
+ log.debug(
258
+ "Processed multi-line value for key %s (length: %d)",
259
+ left.split("=")[0].strip(),
260
+ len(full_content),
261
+ )
262
+ out_lines.append(f'{left} "{full_content}"')
263
+ i += 1
264
+ continue
265
+
192
266
  out_lines.append(line)
193
267
  i += 1
268
+
194
269
  preprocessed = "\n".join(out_lines) + ("\n" if out_lines else "")
195
270
  cp.read_string(preprocessed)
196
271
  except FileNotFoundError:
@@ -272,6 +347,8 @@ def load_org_config(
272
347
 
273
348
  # Report unknown configuration keys to help users catch typos
274
349
  unknown_keys = set(normalized.keys()) - KNOWN_KEYS
350
+ log.debug("All parsed keys from config: %s", sorted(normalized.keys()))
351
+ log.debug("Known keys: %s", sorted(KNOWN_KEYS))
275
352
  if unknown_keys:
276
353
  log.warning(
277
354
  "Unknown configuration keys found in [%s]: %s. "
github2gerrit/core.py CHANGED
@@ -30,10 +30,7 @@ import logging
30
30
  import os
31
31
  import re
32
32
  import shlex
33
- import shutil
34
- import socket
35
33
  import stat
36
- import tempfile
37
34
  import urllib.parse
38
35
  import urllib.request
39
36
  from collections.abc import Iterable
@@ -789,6 +786,8 @@ class Orchestrator:
789
786
 
790
787
  def _strategy_open_fchmod(self, key_path: Path, key_content: str) -> None:
791
788
  """Strategy: open with os.open and specific flags, then fchmod."""
789
+ import os
790
+ import stat
792
791
 
793
792
  flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
794
793
  mode = stat.S_IRUSR | stat.S_IWUSR # 0o600
@@ -802,6 +801,7 @@ class Orchestrator:
802
801
 
803
802
  def _strategy_umask_open(self, key_path: Path, key_content: str) -> None:
804
803
  """Strategy: set umask, create file, restore umask."""
804
+ import os
805
805
 
806
806
  original_umask = os.umask(0o077) # Only owner can read/write
807
807
  try:
@@ -813,6 +813,8 @@ class Orchestrator:
813
813
 
814
814
  def _strategy_stat_constants(self, key_path: Path, key_content: str) -> None:
815
815
  """Strategy: use stat constants for permission setting."""
816
+ import os
817
+ import stat
816
818
 
817
819
  with open(key_path, "w", encoding="utf-8") as f:
818
820
  f.write(key_content)
@@ -824,9 +826,13 @@ class Orchestrator:
824
826
 
825
827
  def _create_key_in_memory_fs(self, key_path: Path, key_content: str) -> bool:
826
828
  """Fallback: try to create key in memory filesystem."""
829
+ import shutil
830
+ import tempfile
831
+
827
832
  try:
828
833
  # Try to create in memory filesystem if available
829
834
  # Use secure temporary directories
835
+ import tempfile
830
836
 
831
837
  temp_dir = tempfile.gettempdir()
832
838
  memory_dirs = [temp_dir]
@@ -2337,6 +2343,7 @@ class Orchestrator:
2337
2343
  - Verify GitHub token by fetching repository and PR metadata
2338
2344
  - Do NOT perform any write operations
2339
2345
  """
2346
+ import socket
2340
2347
 
2341
2348
  log.info("Dry-run: starting preflight checks")
2342
2349
  if os.getenv("G2G_DRYRUN_DISABLE_NETWORK", "").strip().lower() in (
@@ -289,6 +289,8 @@ class DuplicateDetector:
289
289
  Returns:
290
290
  Hex-encoded SHA256 hash string (first 16 characters for readability)
291
291
  """
292
+ import hashlib
293
+
292
294
  # Build hash input from stable, unique PR identifiers
293
295
  # Use server_url + repository + pr_number for global uniqueness
294
296
  hash_input = f"{gh.server_url}/{gh.repository}/pull/{gh.pr_number}"
@@ -29,14 +29,12 @@ import functools
29
29
  import logging
30
30
  import random
31
31
  import socket
32
- import subprocess
33
32
  import time
34
33
  import urllib.error
35
34
  from collections.abc import Callable
36
35
  from dataclasses import dataclass
37
36
  from dataclasses import field
38
37
  from enum import Enum
39
- from pathlib import Path
40
38
  from typing import Any
41
39
  from typing import NoReturn
42
40
  from typing import TypeVar
@@ -443,6 +441,9 @@ def curl_download(
443
441
  Raises:
444
442
  RuntimeError: If curl command fails after retries
445
443
  """
444
+ import subprocess
445
+ from pathlib import Path
446
+
446
447
  if policy is None:
447
448
  policy = RetryPolicy(max_attempts=3, timeout=timeout)
448
449
 
@@ -171,7 +171,9 @@ def build_client(token: str | None = None) -> GhClient:
171
171
  def get_repo_from_env(client: GhClient) -> GhRepository:
172
172
  """Return the repository object based on GITHUB_REPOSITORY."""
173
173
  full = _getenv_str("GITHUB_REPOSITORY")
174
+ log.debug("GITHUB_REPOSITORY environment variable: '%s'", full)
174
175
  if not full or "/" not in full:
176
+ log.error("Invalid GITHUB_REPOSITORY: '%s' (expected format: 'owner/repo')", full)
175
177
  raise ValueError(_MSG_BAD_GITHUB_REPOSITORY)
176
178
  repo = client.get_repo(full)
177
179
  return repo
github2gerrit/gitutils.py CHANGED
@@ -11,10 +11,8 @@ from __future__ import annotations
11
11
 
12
12
  import logging
13
13
  import os
14
- import re
15
14
  import shlex
16
15
  import subprocess
17
- import tempfile
18
16
  import time
19
17
  from collections.abc import Callable
20
18
  from collections.abc import Iterable
@@ -483,7 +481,9 @@ def git_commit_amend(
483
481
  # Write message to a temp file to avoid shell-escaping issues
484
482
  tmp_path: Path | None = None
485
483
  if message is not None:
486
- with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as _tf:
484
+ import tempfile as _tempfile
485
+
486
+ with _tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as _tf:
487
487
  _tf.write(message)
488
488
  _tf.flush()
489
489
  tmp_path = Path(_tf.name)
@@ -493,6 +493,9 @@ def git_commit_amend(
493
493
  # Determine whether to add -s; only suppress if message already has a sign-off for current committer
494
494
  effective_signoff = bool(signoff)
495
495
  try:
496
+ import os
497
+ import re
498
+
496
499
  # Resolve committer email (prefer repo-local; fallback to global/env)
497
500
  committer_email = os.getenv("GIT_COMMITTER_EMAIL", "").strip()
498
501
  if not committer_email:
@@ -583,7 +586,9 @@ def git_commit_new(
583
586
  # Write message to a temp file to avoid shell-escaping issues
584
587
  tmp_path: Path | None = None
585
588
  if message is not None:
586
- with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as _tf:
589
+ import tempfile as _tempfile
590
+
591
+ with _tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as _tf:
587
592
  _tf.write(message)
588
593
  _tf.flush()
589
594
  tmp_path = Path(_tf.name)
@@ -593,6 +598,9 @@ def git_commit_new(
593
598
  # Determine whether to add -s; only suppress if message already has a sign-off for current committer
594
599
  effective_signoff = bool(signoff)
595
600
  try:
601
+ import os
602
+ import re
603
+
596
604
  # Resolve committer email (prefer repo-local; fallback to global/env)
597
605
  committer_email = os.getenv("GIT_COMMITTER_EMAIL", "").strip()
598
606
  if not committer_email:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Submit a GitHub pull request to a Gerrit repository.
5
5
  Author-email: Matthew Watkins <mwatkins@linuxfoundation.org>
6
6
  License-Expression: Apache-2.0
@@ -297,7 +297,7 @@ jobs:
297
297
  steps:
298
298
  - name: Submit PR to Gerrit
299
299
  id: g2g
300
- uses: lfit/github2gerrit@main
300
+ uses: lfreleng-actions/github2gerrit-action@main
301
301
  with:
302
302
  SUBMIT_SINGLE_COMMITS: "false"
303
303
  USE_PR_AS_COMMIT: "false"
@@ -343,7 +343,7 @@ uv run github2gerrit
343
343
  uvx github2gerrit https://github.com/owner/repo/pull/123
344
344
 
345
345
  # Install from specific version/source
346
- uvx --from git+https://github.com/lfit/github2gerrit@main github2gerrit https://github.com/owner/repo/pull/123
346
+ uvx --from git+https://github.com/lfreleng-actions/github2gerrit-action@main github2gerrit https://github.com/owner/repo/pull/123
347
347
  ```
348
348
 
349
349
  ### Available Options
@@ -524,7 +524,7 @@ jobs:
524
524
 
525
525
  - name: Submit PR to Gerrit (with explicit overrides)
526
526
  id: g2g
527
- uses: lfit/github2gerrit@main
527
+ uses: lfreleng-actions/github2gerrit-action@main
528
528
  with:
529
529
  # Behavior
530
530
  SUBMIT_SINGLE_COMMITS: "false"
@@ -777,7 +777,7 @@ For CI/CD pipelines (like GitHub Actions), use `uvx` to install and run without
777
777
  uvx github2gerrit <PR_URL> --dry-run
778
778
 
779
779
  # Install from a specific version or source
780
- uvx --from git+https://github.com/lfit/github2gerrit@main github2gerrit <PR_URL>
780
+ uvx --from git+https://github.com/lfreleng-actions/github2gerrit-action@main github2gerrit <PR_URL>
781
781
 
782
782
  # Run with specific Python version
783
783
  uvx --python 3.11 github2gerrit <PR_URL>
@@ -1,14 +1,14 @@
1
1
  github2gerrit/__init__.py,sha256=N1Vj1HJ28LKCJLAynQdm5jFGQQAz9YSMzZhEfvbBgow,886
2
- github2gerrit/cli.py,sha256=j2jdsfO3WZqHF0UIINVWJrPzMUHtIwzGKmbAhvP1Q3c,45483
2
+ github2gerrit/cli.py,sha256=ClwCOtMl9eecobeN6Bb1GFQ_E-kbILdxKFE89Vtvai4,45865
3
3
  github2gerrit/commit_normalization.py,sha256=u4AZigz3qOpz5XYpUOq3WUqsY-o08YrkgaT160eyIIs,16594
4
- github2gerrit/config.py,sha256=sTiujlOjCsJbaA-Ftr5XR--9nxs7XH8uEWD-IbGqLYk,17370
5
- github2gerrit/core.py,sha256=xSUBhB_xOpCrVbnNu2m1QqKrZJTa8zJrmEK28-ehqPg,105033
6
- github2gerrit/duplicate_detection.py,sha256=enzLMsMQOeB5yU_iEOfTN5oGuZ0LUZNENsQ4fFnKKRg,26520
7
- github2gerrit/external_api.py,sha256=ypMKGIsCAYTvP2u55l8X5eUC88T6EKvyZvlsAWRFVcU,17629
4
+ github2gerrit/config.py,sha256=uRVVcofzojTayA8AyM_3rgSFsV0fWGjixWmK48aZsRA,21471
5
+ github2gerrit/core.py,sha256=Mjo_TNocFE5HDmRTxh54LEHAEw5y1u6lc1qItEZ76GA,105180
6
+ github2gerrit/duplicate_detection.py,sha256=5j6EDz2P3GnnT2JkR1tGbzWCjA6TV0l5VjsEMs-tfCM,26544
7
+ github2gerrit/external_api.py,sha256=EVHh__v6lRq_ojpBI_nkGZ31AQSZ9NG8tDqVUid23XY,17638
8
8
  github2gerrit/gerrit_rest.py,sha256=NmC95hb2hLpnko8Uu3OwPEKktNv-k3qrmuA7A2q-H0Y,10562
9
9
  github2gerrit/gerrit_urls.py,sha256=pw4rjIQeQ-i-vbPqsOZjZpdz7FO03pnMUMWc8L9Mcic,12893
10
- github2gerrit/github_api.py,sha256=1cTZMunDx6_oLNR3Z__ya6vsw0_PFS5ww3jQla5SVFA,8229
11
- github2gerrit/gitutils.py,sha256=7E-bYux4E-5IJMSxYx8U0sKzD-wkn3QyEfZ7SGHkGrY,25803
10
+ github2gerrit/github_api.py,sha256=6S9DRt-Dza78cgxQwZ3_ujI0yPPozDGBIKnYgHan760,8388
11
+ github2gerrit/gitutils.py,sha256=3ob1K7dlKeWYYYOPXIIJ56p2N49lm1yj8XHHg2zSlhw,25929
12
12
  github2gerrit/models.py,sha256=sRS4rPMF_wjV19HMFTzhHXFMfsLFzm9TFb1JydJsSyQ,1875
13
13
  github2gerrit/pr_content_filter.py,sha256=w4MqSZ4uLX-3Da1DL_A56zVQa7xJ4h5jHMNteOOG2AE,16896
14
14
  github2gerrit/similarity.py,sha256=xPqdypI-Fmpeb74K4lcqZBxj17i8avS_x73dXckeicA,15268
@@ -16,9 +16,9 @@ github2gerrit/ssh_agent_setup.py,sha256=fvKuMdF9Hv5ioaSu_alzLcjeoQLGGFRLS2wb46Kd
16
16
  github2gerrit/ssh_common.py,sha256=f0QQmnj6yIW74N3ZjGkYmJryEstjEOmq41EIXOZdGTM,8102
17
17
  github2gerrit/ssh_discovery.py,sha256=zH0tX9VN1pn8DLM1J9QQquoKBhA9gk5drXHvMSHNNLg,13021
18
18
  github2gerrit/utils.py,sha256=ADE5YZe1h3PB3cgfxbsCuYU-Xs-1CMHrDHxfIdariFE,3369
19
- github2gerrit-0.1.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
20
- github2gerrit-0.1.8.dist-info/METADATA,sha256=XMojs1cbwj9CPacFK6YDE3TczE5s4tE4YFPKZXGUUIY,30930
21
- github2gerrit-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- github2gerrit-0.1.8.dist-info/entry_points.txt,sha256=MxN2_liIKo3-xJwtAulAeS5GcOS6JS96nvwOQIkP3W8,56
23
- github2gerrit-0.1.8.dist-info/top_level.txt,sha256=bWTYXjvuu4sSU90KLT1JlnjD7xV_iXZ-vKoulpjLTy8,14
24
- github2gerrit-0.1.8.dist-info/RECORD,,
19
+ github2gerrit-0.1.9.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
20
+ github2gerrit-0.1.9.dist-info/METADATA,sha256=QlTEhsWuTU9Yxim1Z__-aMr6GcGWmBzpcczbqzEyBNU,31006
21
+ github2gerrit-0.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ github2gerrit-0.1.9.dist-info/entry_points.txt,sha256=MxN2_liIKo3-xJwtAulAeS5GcOS6JS96nvwOQIkP3W8,56
23
+ github2gerrit-0.1.9.dist-info/top_level.txt,sha256=bWTYXjvuu4sSU90KLT1JlnjD7xV_iXZ-vKoulpjLTy8,14
24
+ github2gerrit-0.1.9.dist-info/RECORD,,