github2gerrit 0.1.10__py3-none-any.whl → 0.1.11__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.
@@ -25,19 +25,29 @@ _BASE_PATH_CACHE: dict[str, str] = {}
25
25
 
26
26
 
27
27
  class _NoRedirect(urllib.request.HTTPRedirectHandler):
28
- def http_error_301(self, req: Any, fp: Any, code: int, msg: str, headers: Any) -> Any:
28
+ def http_error_301(
29
+ self, req: Any, fp: Any, code: int, msg: str, headers: Any
30
+ ) -> Any:
29
31
  return fp
30
32
 
31
- def http_error_302(self, req: Any, fp: Any, code: int, msg: str, headers: Any) -> Any:
33
+ def http_error_302(
34
+ self, req: Any, fp: Any, code: int, msg: str, headers: Any
35
+ ) -> Any:
32
36
  return fp
33
37
 
34
- def http_error_303(self, req: Any, fp: Any, code: int, msg: str, headers: Any) -> Any:
38
+ def http_error_303(
39
+ self, req: Any, fp: Any, code: int, msg: str, headers: Any
40
+ ) -> Any:
35
41
  return fp
36
42
 
37
- def http_error_307(self, req: Any, fp: Any, code: int, msg: str, headers: Any) -> Any:
43
+ def http_error_307(
44
+ self, req: Any, fp: Any, code: int, msg: str, headers: Any
45
+ ) -> Any:
38
46
  return fp
39
47
 
40
- def http_error_308(self, req: Any, fp: Any, code: int, msg: str, headers: Any) -> Any:
48
+ def http_error_308(
49
+ self, req: Any, fp: Any, code: int, msg: str, headers: Any
50
+ ) -> Any:
41
51
  return fp
42
52
 
43
53
 
@@ -83,23 +93,31 @@ def _discover_base_path_for_host(host: str, timeout: float = 5.0) -> str:
83
93
  continue
84
94
  try:
85
95
  resp = opener.open(url, timeout=timeout)
86
- code = getattr(resp, "getcode", lambda: None)() or getattr(resp, "status", 0)
96
+ code = getattr(resp, "getcode", lambda: None)() or getattr(
97
+ resp, "status", 0
98
+ )
87
99
  # If we reached the page without redirects
88
100
  if code == 200:
89
- _BASE_PATH_CACHE[host] = ""
90
- log.info("Gerrit base path: ''")
101
+ log.debug("Gerrit base path: ''")
91
102
  return ""
92
- # Handle 3xx responses when redirects are disabled (no-redirect opener)
103
+ # Handle 3xx responses when redirects are disabled
104
+ # (no-redirect opener)
93
105
  if code in (301, 302, 303, 307, 308):
94
106
  headers = getattr(resp, "headers", {}) or {}
95
- loc = headers.get("Location") or headers.get("location") or ""
107
+ loc = (
108
+ headers.get("Location")
109
+ or headers.get("location")
110
+ or ""
111
+ )
96
112
  if loc:
97
113
  # Normalize to absolute path
98
114
  parsed = urllib.parse.urlparse(loc)
99
115
  path = (
100
116
  parsed.path
101
117
  if parsed.scheme or parsed.netloc
102
- else urllib.parse.urlparse(f"https://{host}{loc}").path
118
+ else urllib.parse.urlparse(
119
+ f"https://{host}{loc}"
120
+ ).path
103
121
  )
104
122
  # Determine candidate base path
105
123
  segs = [s for s in path.split("/") if s]
@@ -109,21 +127,28 @@ def _discover_base_path_for_host(host: str, timeout: float = 5.0) -> str:
109
127
  if first not in known_endpoints:
110
128
  base = first
111
129
  _BASE_PATH_CACHE[host] = base
112
- log.info("Gerrit base path: '%s'", base)
130
+ log.debug("Gerrit base path: '%s'", base)
113
131
  return base
114
132
  # If we get any other non-redirect response, try next probe
115
133
  continue
116
134
  except urllib.error.HTTPError as e:
117
- # HTTPError doubles as the response; capture Location for redirects
135
+ # HTTPError doubles as the response; capture Location for
136
+ # redirects
118
137
  code = e.code
119
- loc = e.headers.get("Location") or e.headers.get("location") or ""
138
+ loc = (
139
+ e.headers.get("Location")
140
+ or e.headers.get("location")
141
+ or ""
142
+ )
120
143
  if code in (301, 302, 303, 307, 308) and loc:
121
144
  # Normalize to absolute path
122
145
  parsed = urllib.parse.urlparse(loc)
123
146
  path = (
124
147
  parsed.path
125
148
  if parsed.scheme or parsed.netloc
126
- else urllib.parse.urlparse(f"https://{host}{loc}").path
149
+ else urllib.parse.urlparse(
150
+ f"https://{host}{loc}"
151
+ ).path
127
152
  )
128
153
  # Determine candidate base path
129
154
  segs = [s for s in path.split("/") if s]
@@ -133,12 +158,17 @@ def _discover_base_path_for_host(host: str, timeout: float = 5.0) -> str:
133
158
  if first not in known_endpoints:
134
159
  base = first
135
160
  _BASE_PATH_CACHE[host] = base
136
- log.info("Gerrit base path: '%s'", base)
161
+ log.debug("Gerrit base path: '%s'", base)
137
162
  return base
138
163
  # Non-redirect error; try next probe
139
164
  continue
140
165
  except Exception as exc:
141
- log.debug("Gerrit base path probe failed for %s%s: %s", host, probe, exc)
166
+ log.debug(
167
+ "Gerrit base path probe failed for %s%s: %s",
168
+ host,
169
+ probe,
170
+ exc,
171
+ )
142
172
  continue
143
173
 
144
174
  except Exception as exc:
@@ -146,7 +176,7 @@ def _discover_base_path_for_host(host: str, timeout: float = 5.0) -> str:
146
176
  return ""
147
177
  # Default if nothing conclusive after exhausting all probes
148
178
  _BASE_PATH_CACHE[host] = ""
149
- log.info("Gerrit base path: ''")
179
+ log.debug("Gerrit base path: ''")
150
180
  return ""
151
181
 
152
182
 
@@ -167,7 +197,8 @@ class GerritUrlBuilder:
167
197
  Args:
168
198
  host: Gerrit hostname (without protocol)
169
199
  base_path: Optional base path override. If None, reads from
170
- GERRIT_HTTP_BASE_PATH environment variable or discovers dynamically.
200
+ GERRIT_HTTP_BASE_PATH environment variable or discovers
201
+ dynamically.
171
202
  """
172
203
  self.host = host.strip()
173
204
 
@@ -203,24 +234,32 @@ class GerritUrlBuilder:
203
234
  Build the base URL with optional base path override.
204
235
 
205
236
  Args:
206
- base_path_override: Optional base path to use instead of the instance default
237
+ base_path_override: Optional base path to use instead of the
238
+ instance default
207
239
 
208
240
  Returns:
209
241
  Base URL with trailing slash
210
242
  """
211
- path = base_path_override if base_path_override is not None else self._base_path
243
+ path = (
244
+ base_path_override
245
+ if base_path_override is not None
246
+ else self._base_path
247
+ )
212
248
  if path:
213
249
  return f"https://{self.host}/{path}/"
214
250
  else:
215
251
  return f"https://{self.host}/"
216
252
 
217
- def api_url(self, endpoint: str = "", base_path_override: str | None = None) -> str:
253
+ def api_url(
254
+ self, endpoint: str = "", base_path_override: str | None = None
255
+ ) -> str:
218
256
  """
219
257
  Build a Gerrit REST API URL.
220
258
 
221
259
  Args:
222
260
  endpoint: API endpoint path (e.g., "/changes/", "/accounts/self")
223
- base_path_override: Optional base path override for fallback scenarios
261
+ base_path_override: Optional base path override for fallback
262
+ scenarios
224
263
 
225
264
  Returns:
226
265
  Complete API URL
@@ -231,13 +270,16 @@ class GerritUrlBuilder:
231
270
  endpoint = "/" + endpoint
232
271
  return urljoin(base_url, endpoint.lstrip("/"))
233
272
 
234
- def web_url(self, path: str = "", base_path_override: str | None = None) -> str:
273
+ def web_url(
274
+ self, path: str = "", base_path_override: str | None = None
275
+ ) -> str:
235
276
  """
236
277
  Build a Gerrit web UI URL.
237
278
 
238
279
  Args:
239
280
  path: Web path (e.g., "c/project/+/123", "dashboard")
240
- base_path_override: Optional base path override for fallback scenarios
281
+ base_path_override: Optional base path override for fallback
282
+ scenarios
241
283
 
242
284
  Returns:
243
285
  Complete web URL
@@ -261,22 +303,27 @@ class GerritUrlBuilder:
261
303
  Args:
262
304
  project: Gerrit project name
263
305
  change_number: Gerrit change number
264
- base_path_override: Optional base path override for fallback scenarios
306
+ base_path_override: Optional base path override for fallback
307
+ scenarios
265
308
 
266
309
  Returns:
267
310
  Complete change URL
268
311
  """
269
- # Don't URL-encode project names - Gerrit expects them as-is (backward compatibility)
312
+ # Don't URL-encode project names - Gerrit expects them as-is
313
+ # (backward compatibility)
270
314
  path = f"c/{project}/+/{change_number}"
271
315
  return self.web_url(path, base_path_override)
272
316
 
273
- def hook_url(self, hook_name: str, base_path_override: str | None = None) -> str:
317
+ def hook_url(
318
+ self, hook_name: str, base_path_override: str | None = None
319
+ ) -> str:
274
320
  """
275
321
  Build a URL for downloading Gerrit hooks.
276
322
 
277
323
  Args:
278
324
  hook_name: Name of the hook (e.g., "commit-msg")
279
- base_path_override: Optional base path override for fallback scenarios
325
+ base_path_override: Optional base path override for fallback
326
+ scenarios
280
327
 
281
328
  Returns:
282
329
  Complete hook download URL
@@ -318,7 +365,8 @@ class GerritUrlBuilder:
318
365
  """
319
366
  Get the web base path for URL construction.
320
367
 
321
- This is useful when you need just the path component for manual URL building.
368
+ This is useful when you need just the path component for manual URL
369
+ building.
322
370
 
323
371
  Args:
324
372
  base_path_override: Optional base path override
@@ -326,7 +374,11 @@ class GerritUrlBuilder:
326
374
  Returns:
327
375
  Web base path with leading and trailing slashes (e.g., "/r/", "/")
328
376
  """
329
- path = base_path_override if base_path_override is not None else self._base_path
377
+ path = (
378
+ base_path_override
379
+ if base_path_override is not None
380
+ else self._base_path
381
+ )
330
382
  if path:
331
383
  return f"/{path}/"
332
384
  else:
@@ -334,10 +386,15 @@ class GerritUrlBuilder:
334
386
 
335
387
  def __repr__(self) -> str:
336
388
  """String representation for debugging."""
337
- return f"GerritUrlBuilder(host='{self.host}', base_path='{self._base_path}')"
389
+ return (
390
+ f"GerritUrlBuilder(host='{self.host}', "
391
+ f"base_path='{self._base_path}')"
392
+ )
338
393
 
339
394
 
340
- def create_gerrit_url_builder(host: str, base_path: str | None = None) -> GerritUrlBuilder:
395
+ def create_gerrit_url_builder(
396
+ host: str, base_path: str | None = None
397
+ ) -> GerritUrlBuilder:
341
398
  """
342
399
  Factory function to create a GerritUrlBuilder instance.
343
400
 
@@ -44,7 +44,9 @@ class RateLimitExceededExceptionType(GithubExceptionType):
44
44
  pass
45
45
 
46
46
 
47
- def _load_github_classes() -> tuple[Any | None, type[BaseException], type[BaseException]]:
47
+ def _load_github_classes() -> tuple[
48
+ Any | None, type[BaseException], type[BaseException]
49
+ ]:
48
50
  try:
49
51
  exc_mod = import_module("github.GithubException")
50
52
  ge = exc_mod.GithubException
@@ -151,17 +153,23 @@ def build_client(token: str | None = None) -> GhClient:
151
153
  if auth_factory is not None and hasattr(auth_factory, "Token"):
152
154
  auth_obj = auth_factory.Token(tok)
153
155
  if base_url:
154
- client_any = Github(auth=auth_obj, per_page=100, base_url=base_url)
156
+ client_any = Github(
157
+ auth=auth_obj, per_page=100, base_url=base_url
158
+ )
155
159
  else:
156
160
  client_any = Github(auth=auth_obj, per_page=100)
157
161
  else:
158
162
  if base_url:
159
- client_any = Github(login_or_token=tok, per_page=100, base_url=base_url)
163
+ client_any = Github(
164
+ login_or_token=tok, per_page=100, base_url=base_url
165
+ )
160
166
  else:
161
167
  client_any = Github(login_or_token=tok, per_page=100)
162
168
  except Exception:
163
169
  if base_url:
164
- client_any = Github(login_or_token=tok, per_page=100, base_url=base_url)
170
+ client_any = Github(
171
+ login_or_token=tok, per_page=100, base_url=base_url
172
+ )
165
173
  else:
166
174
  client_any = Github(login_or_token=tok, per_page=100)
167
175
  return cast(GhClient, client_any)
@@ -173,7 +181,10 @@ def get_repo_from_env(client: GhClient) -> GhRepository:
173
181
  full = _getenv_str("GITHUB_REPOSITORY")
174
182
  log.debug("GITHUB_REPOSITORY environment variable: '%s'", full)
175
183
  if not full or "/" not in full:
176
- log.error("Invalid GITHUB_REPOSITORY: '%s' (expected format: 'owner/repo')", full)
184
+ log.error(
185
+ "Invalid GITHUB_REPOSITORY: '%s' (expected format: 'owner/repo')",
186
+ full,
187
+ )
177
188
  raise ValueError(_MSG_BAD_GITHUB_REPOSITORY)
178
189
  repo = client.get_repo(full)
179
190
  return repo
@@ -259,5 +270,7 @@ def close_pr(pr: GhPullRequest, *, comment: str | None = None) -> None:
259
270
  try:
260
271
  create_pr_comment(pr, comment)
261
272
  except Exception as exc:
262
- log.warning("Failed to add close comment to PR #%s: %s", pr.number, exc)
273
+ log.warning(
274
+ "Failed to add close comment to PR #%s: %s", pr.number, exc
275
+ )
263
276
  pr.edit(state="closed")
github2gerrit/gitutils.py CHANGED
@@ -53,7 +53,10 @@ if not log.handlers:
53
53
  # Provide a minimal default if the app has not configured logging.
54
54
  level_name = os.getenv("G2G_LOG_LEVEL", "INFO").upper()
55
55
  level = getattr(logging, level_name, logging.INFO)
56
- fmt = "%(asctime)s %(levelname)-8s %(name)s %(filename)s:%(lineno)d | %(message)s"
56
+ fmt = (
57
+ "%(asctime)s %(levelname)-8s %(name)s %(filename)s:%(lineno)d | "
58
+ "%(message)s"
59
+ )
57
60
  logging.basicConfig(level=level, format=fmt)
58
61
 
59
62
 
@@ -238,9 +241,14 @@ def run_cmd(
238
241
  filtered_lines = []
239
242
  skip_hint = False
240
243
  for line in stderr_lines:
241
- if "hint: Using '" in line and "as the name for the initial branch" in line:
244
+ if (
245
+ "hint: Using '" in line
246
+ and "as the name for the initial branch" in line
247
+ ):
242
248
  skip_hint = True
243
- elif (skip_hint and line.startswith("hint:")) or (skip_hint and not line.strip()):
249
+ elif (skip_hint and line.startswith("hint:")) or (
250
+ skip_hint and not line.strip()
251
+ ):
244
252
  continue
245
253
  else:
246
254
  skip_hint = False
@@ -259,7 +267,8 @@ def non_interactive_env(include_git_ssh_command: bool = True) -> dict[str, str]:
259
267
 
260
268
  Args:
261
269
  include_git_ssh_command: Whether to include a default GIT_SSH_COMMAND.
262
- Set to False when the caller will provide their own.
270
+ Set to False when the caller will provide their
271
+ own.
263
272
 
264
273
  Returns:
265
274
  Dictionary of environment variables for non-interactive operations.
@@ -332,10 +341,11 @@ def run_cmd_with_retries(
332
341
  if predicate(res):
333
342
  delay = _backoff_delay(attempt)
334
343
  log.warning(
335
- "Retrying (attempt %d) after transient error; delay %.1fs. cmd=%s",
344
+ "Retrying (attempt %d) after transient error; delay %.1fs. "
345
+ "cmd=%s",
336
346
  attempt,
337
347
  delay,
338
- _format_cmd_for_log(cmd, masks),
348
+ " ".join(cmd) if isinstance(cmd, list) else str(cmd),
339
349
  )
340
350
  time.sleep(delay)
341
351
  continue
@@ -483,14 +493,17 @@ def git_commit_amend(
483
493
  if message is not None:
484
494
  import tempfile as _tempfile
485
495
 
486
- with _tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as _tf:
496
+ with _tempfile.NamedTemporaryFile(
497
+ "w", delete=False, encoding="utf-8"
498
+ ) as _tf:
487
499
  _tf.write(message)
488
500
  _tf.flush()
489
501
  tmp_path = Path(_tf.name)
490
502
  message_file = tmp_path
491
503
  message = None
492
504
 
493
- # Determine whether to add -s; only suppress if message already has a sign-off for current committer
505
+ # Determine whether to add -s; only suppress if message already has a
506
+ # sign-off for current committer
494
507
  effective_signoff = bool(signoff)
495
508
  try:
496
509
  import os
@@ -516,7 +529,12 @@ def git_commit_amend(
516
529
  for ln in text.splitlines():
517
530
  if ln.lower().startswith("signed-off-by:"):
518
531
  m = re.search(r"<([^>]+)>", ln)
519
- if m and committer_email and m.group(1).strip().lower() == committer_email.lower():
532
+ if (
533
+ m
534
+ and committer_email
535
+ and m.group(1).strip().lower()
536
+ == committer_email.lower()
537
+ ):
520
538
  return True
521
539
  return False
522
540
 
@@ -588,14 +606,17 @@ def git_commit_new(
588
606
  if message is not None:
589
607
  import tempfile as _tempfile
590
608
 
591
- with _tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as _tf:
609
+ with _tempfile.NamedTemporaryFile(
610
+ "w", delete=False, encoding="utf-8"
611
+ ) as _tf:
592
612
  _tf.write(message)
593
613
  _tf.flush()
594
614
  tmp_path = Path(_tf.name)
595
615
  message_file = tmp_path
596
616
  message = None
597
617
 
598
- # Determine whether to add -s; only suppress if message already has a sign-off for current committer
618
+ # Determine whether to add -s; only suppress if message already has a
619
+ # sign-off for current committer
599
620
  effective_signoff = bool(signoff)
600
621
  try:
601
622
  import os
@@ -621,7 +642,12 @@ def git_commit_new(
621
642
  for ln in text.splitlines():
622
643
  if ln.lower().startswith("signed-off-by:"):
623
644
  m = re.search(r"<([^>]+)>", ln)
624
- if m and committer_email and m.group(1).strip().lower() == committer_email.lower():
645
+ if (
646
+ m
647
+ and committer_email
648
+ and m.group(1).strip().lower()
649
+ == committer_email.lower()
650
+ ):
625
651
  return True
626
652
  return False
627
653
 
@@ -693,7 +719,8 @@ def _parse_trailers(text: str) -> dict[str, list[str]]:
693
719
 
694
720
  Git trailers are key-value pairs that appear at the end of commit messages,
695
721
  separated from the body by a blank line. This function only parses trailers
696
- from the actual footer section to avoid false positives from the message body.
722
+ from the actual footer section to avoid false positives from the message
723
+ body.
697
724
  """
698
725
  trailers: dict[str, list[str]] = {}
699
726
  lines = text.splitlines()
@@ -804,7 +831,9 @@ def git_config_get_all(
804
831
  try:
805
832
  res = git_quiet(args, cwd=None)
806
833
  if res.returncode == 0:
807
- values = [ln.strip() for ln in res.stdout.splitlines() if ln.strip()]
834
+ values = [
835
+ ln.strip() for ln in res.stdout.splitlines() if ln.strip()
836
+ ]
808
837
  return values
809
838
  else:
810
839
  return []