Skip to content

Commit b8e261f

Browse files
committed
Fix pysvn info command
1 parent da0b696 commit b8e261f

File tree

1 file changed

+167
-112
lines changed

1 file changed

+167
-112
lines changed

src/source_repo/svn.py

Lines changed: 167 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77
from utils import cmd, git, lockfiles
88

99
# Import Python standard modules
10+
from datetime import datetime
1011
import os
1112
import random
1213
import re
1314
import shutil
1415
import time
1516

1617
# Import third party modules
18+
19+
# Install pysvn from https://pysvn.sourceforge.io
20+
# apt-get install python3-svn
21+
# Not from pip; pip has an entirely unrelated package
22+
# PITA to install on macOS https://sourceforge.net/p/pysvn/tickets/21/
23+
# but runs fine inside of Podman as it uses an Ubuntu VM
1724
import pysvn
1825

1926
# dicts for:
@@ -31,52 +38,6 @@
3138
# Fewest local commands
3239

3340

34-
def _initialize_pysvn(ctx):
35-
"""
36-
Create and return the pysvn.client
37-
"""
38-
39-
job_config = ctx.job.get("config",{})
40-
disable_tls_verification = job_config.get("disable_tls_verification")
41-
username = job_config.get("username")
42-
password = job_config.get("password")
43-
44-
pysvn_client = pysvn.Client()
45-
46-
if disable_tls_verification:
47-
48-
def pysvn_callback_ssl_server_trust_prompt(trust_dict):
49-
"""
50-
Callback function, for pysvn module to call, when the svn server prompts if we want to trust the cert
51-
https://pysvn.sourceforge.io/Docs/pysvn_prog_ref.html#pysvn_client
52-
"""
53-
54-
print(f"pysvn_ssl_server_trust_prompt trust_dict: {trust_dict}")
55-
56-
return_code = True
57-
accepted_failures = trust_dict["failures"]
58-
save_server_cert = True
59-
60-
return return_code, accepted_failures, save_server_cert
61-
62-
63-
pysvn_client.callback_ssl_server_trust_prompt = pysvn_callback_ssl_server_trust_prompt
64-
65-
66-
def pysvn_callback_get_login(realm, callback_username, may_save):
67-
68-
return_username = username
69-
return_password = password
70-
return_code = True if return_username and return_password else False
71-
save_credentials = True
72-
73-
return return_code, return_username, return_password, save_credentials
74-
75-
pysvn_client.callback_get_login = pysvn_callback_get_login
76-
77-
return pysvn_client
78-
79-
8041
def convert(ctx: Context) -> None:
8142
"""
8243
Entrypoint / main logic / orchestration function
@@ -203,37 +164,37 @@ def _build_cli_commands(ctx: Context) -> dict:
203164

204165
# Common svn command args
205166
# Also used to convert strings to lists, to concatenate lists
206-
arg_svn_non_interactive = ["--non-interactive"]
207-
arg_svn_force_interactive = ["--force-interactive"]
167+
# arg_svn_non_interactive = ["--non-interactive"]
168+
# arg_svn_force_interactive = ["--force-interactive"]
208169
arg_repo_url = [repo_url]
209170

210-
# svn commands
211-
cmd_svn_info = ["svn", "info"] + arg_repo_url
171+
# # svn commands
172+
# cmd_svn_info = ["svn", "info"] + arg_repo_url
212173

213174

214-
# Skip TLS verification, if needed
215-
if disable_tls_verification:
175+
# # Skip TLS verification, if needed
176+
# if disable_tls_verification:
216177

217-
# This prompt only be required very rarely, ex. once per:
218-
# Deployment of the repo-converter container, ex. the ~/.subversion/auth/svn.ssl.server directory inside the container is empty, as the root volume is not retained
219-
# New Subversion server
220-
# New TLS cert on a server
221-
# However, it needs to be checked for on every job)
222-
ctx.job["config"].update(
223-
{
224-
"expect": {
225-
"prompt": "accept (p)ermanently",
226-
"response": "p",
227-
}
228-
}
229-
)
178+
# # This prompt only be required very rarely, ex. once per:
179+
# # Deployment of the repo-converter container, ex. the ~/.subversion/auth/svn.ssl.server directory inside the container is empty, as the root volume is not retained
180+
# # New Subversion server
181+
# # New TLS cert on a server
182+
# # However, it needs to be checked for on every job)
183+
# ctx.job["config"].update(
184+
# {
185+
# "expect": {
186+
# "prompt": "accept (p)ermanently",
187+
# "response": "p",
188+
# }
189+
# }
190+
# )
230191

231-
# Trusting the TLS cert requires interactive mode
232-
cmd_svn_info += arg_svn_force_interactive
233-
else:
192+
# # Trusting the TLS cert requires interactive mode
193+
# cmd_svn_info += arg_svn_force_interactive
194+
# else:
234195

235-
# If not needing to trust the TLS cert, then use non-interactive mode
236-
cmd_svn_info += arg_svn_non_interactive
196+
# # If not needing to trust the TLS cert, then use non-interactive mode
197+
# cmd_svn_info += arg_svn_non_interactive
237198

238199

239200
# Common git command args
@@ -296,7 +257,7 @@ def _build_cli_commands(ctx: Context) -> dict:
296257
'cmd_git_garbage_collection': cmd_git_garbage_collection,
297258
'cmd_git_svn_fetch': cmd_git_svn_fetch,
298259
'cmd_git_svn_init': cmd_git_svn_init,
299-
'cmd_svn_info': cmd_svn_info,
260+
# 'cmd_svn_info': cmd_svn_info,
300261
}
301262

302263

@@ -394,6 +355,68 @@ def _check_if_conversion_is_already_running_in_another_process(
394355
return False
395356

396357

358+
def _initialize_pysvn(ctx: Context) -> pysvn.Client:
359+
"""
360+
Create and return the pysvn.client
361+
Define and register callback handler functions
362+
"""
363+
364+
job_config = ctx.job.get("config",{})
365+
disable_tls_verification = job_config.get("disable_tls_verification")
366+
username = job_config.get("username")
367+
password = job_config.get("password")
368+
pysvn_client = pysvn.Client()
369+
370+
371+
# Handle the untrusted TLS certificate prompt
372+
if disable_tls_verification:
373+
374+
def pysvn_callback_ssl_server_trust_prompt(trust_dict):
375+
"""
376+
Callback function, for pysvn module to call, when the svn server prompts if we want to trust the cert
377+
https://pysvn.sourceforge.io/Docs/pysvn_prog_ref.html#pysvn_client
378+
"""
379+
380+
log(ctx, "Trusting Subversion server's untrusted TLS certificate", "warning", {"trust_dict": trust_dict})
381+
382+
return_code = True
383+
accepted_failures = trust_dict["failures"]
384+
save_server_cert = True
385+
386+
return return_code, accepted_failures, save_server_cert
387+
388+
389+
# Register the handler
390+
pysvn_client.callback_ssl_server_trust_prompt = pysvn_callback_ssl_server_trust_prompt
391+
392+
393+
# Handle login prompts
394+
def pysvn_callback_get_login(realm, callback_username, may_save):
395+
396+
return_username = username
397+
return_password = password
398+
return_code = True if return_username and return_password else False
399+
save_credentials = True
400+
401+
log_dict = {
402+
"subversion_server": {
403+
"realm": realm,
404+
"username": return_username,
405+
"return_code": return_code,
406+
"save_credentials": save_credentials,
407+
}
408+
}
409+
410+
log(ctx, "Logging into Subversion server", "debug", log_dict)
411+
412+
return return_code, return_username, return_password, save_credentials
413+
414+
# Register the handler
415+
pysvn_client.callback_get_login = pysvn_callback_get_login
416+
417+
return pysvn_client
418+
419+
397420
def _test_connection_and_credentials(
398421
ctx: Context,
399422
commands: dict,
@@ -403,67 +426,99 @@ def _test_connection_and_credentials(
403426
Run the svn info command to test:
404427
- Network connectivity to the SVN server
405428
- Authentication credentials, if provided
406-
407-
- If
429+
- If disable_tls_verification is set, then this will trust the server cert
408430
409431
Capture the output, so we can later extract the current remote rev from it
410432
411-
The svn info command should be quite lightweight, and return very quickly
433+
When called from the svn cli, the svn info command is quite lightweight, and returns very quickly
434+
When called from the pysvn module, it will return a tuple for every file in the repo, unless depth = empty
412435
"""
413436

414437
# Get config values
415438
job_config = ctx.job.get("config", {})
416439
max_retries = job_config.get("max_retries")
417-
password = job_config.get("password")
418440
repo_url = job_config.get("repo_url")
419-
# expect = job_config.get("expect", {}).get("prompt","")
420-
# response = job_config.get("expect", {}).get("response","")
421-
cmd_svn_info = commands["cmd_svn_info"]
422441
tries_attempted = 1
423442
svn_info = {}
424443
svn_info_success = False
425444

426445
while True:
427446

428-
log(ctx, "while True:")
447+
svn_info = {}
429448

430-
svn_info_list_of_tuples = pysvn_client.info2(
431-
repo_url,
432-
# depth = depth,
433-
# fetch_actual_only = True,
434-
# fetch_excluded = False,
435-
# peg_revision = pysvn.Revision( opt_revision_kind.unspecified ),
436-
# recurse = True,
437-
# revision = pysvn.Revision( opt_revision_kind.unspecified ),
438-
)
449+
try:
439450

440-
log(ctx, "svn_info_list_of_tuples:", "debug", {"svn_info_list_of_tuples": svn_info_list_of_tuples})
451+
# log(ctx, "while True:")
441452

442-
svn_info_path = ""
443-
svn_info_dict = {}
453+
svn_info_list_of_tuples = pysvn_client.info2(
454+
repo_url,
455+
revision = pysvn.Revision(pysvn.opt_revision_kind.head),
456+
depth = pysvn.depth.empty,
457+
)
444458

445-
# Grab the first tuple from the list
446-
if (
447-
isinstance(svn_info_list_of_tuples, list) and
448-
len(svn_info_list_of_tuples) > 0 and
449-
isinstance(svn_info_list_of_tuples[0], tuple)
450-
):
451-
log(ctx, f"if: True")
452-
svn_info_path, svn_info_dict = svn_info_list_of_tuples[0]
459+
# log(ctx, "svn_info_list_of_tuples:", "debug", {"svn_info_list_of_tuples": svn_info_list_of_tuples})
460+
461+
# Grab the first tuple from the list
462+
if (
463+
isinstance(svn_info_list_of_tuples, list) and
464+
len(svn_info_list_of_tuples) > 0 and
465+
isinstance(svn_info_list_of_tuples[0], tuple)
466+
):
467+
# log(ctx, f"if: True")
468+
svn_info_path, svn_info_PysvnInfo = svn_info_list_of_tuples[0]
469+
470+
# log(ctx, f"svn_info_PysvnInfo: {svn_info_PysvnInfo}", "debug")
471+
472+
# Expose the .data attribute from the PysvnInfo type
473+
# This is probably overkill, but is the only thing I've found to work so far
474+
svn_info_tmp = {
475+
attribute: getattr(svn_info_PysvnInfo, attribute)
476+
for attribute in dir(svn_info_PysvnInfo)
477+
if not attribute.startswith('_')
478+
and not callable(getattr(svn_info_PysvnInfo, attribute))
479+
}
453480

454-
svn_info_url = svn_info_dict.get("URL","")
455-
if repo_url in svn_info_url:
456-
log(ctx, f"repo_url in svn_info_url: {repo_url} in {svn_info_url}")
457-
else:
458-
log(ctx, f"repo_url NOT in svn_info_url: {repo_url} NOT in {svn_info_url}")
481+
# Use the .data attribute, now that it's available
482+
data = svn_info_tmp.get("data", {})
483+
remote_url = data.get("URL", "")
459484

460-
svn_info = svn_info_dict
485+
# Get attributes and convert types
486+
remote_head_rev = data.get("rev", -1) # <Revision kind=number 2141>
487+
if remote_head_rev != -1:
488+
remote_head_rev = int(remote_head_rev.number)
461489

490+
remote_last_changed_rev = data.get("last_changed_rev", -1) # <Revision kind=number 2141>
491+
if remote_last_changed_rev != -1:
492+
remote_last_changed_rev = int(remote_last_changed_rev.number)
462493

463-
svn_info_success = True
494+
remote_last_changed_date = data.get("last_changed_date", -1) # 1753437840.73516
495+
if remote_last_changed_date != -1 and isinstance(remote_last_changed_date, float):
496+
remote_last_changed_date = datetime.fromtimestamp(remote_last_changed_date).isoformat(sep=' ', timespec='seconds')
464497

465-
else:
466-
log(ctx, f"if: False")
498+
svn_info.update(
499+
{
500+
"remote_url": remote_url,
501+
"remote_head_rev": remote_head_rev,
502+
"remote_last_changed_rev": remote_last_changed_rev,
503+
"remote_last_changed_date": remote_last_changed_date,
504+
}
505+
)
506+
507+
log(ctx, f"svn_info: {svn_info}", "debug")
508+
509+
if remote_url and repo_url in remote_url:
510+
log(ctx, f"repo_url in remote_url: {repo_url} in {remote_url}", "debug", {"svn_info": svn_info})
511+
svn_info_success = True
512+
else:
513+
log(ctx, f"repo_url NOT in remote_url: {repo_url} NOT in {remote_url}", "debug", {"svn_info": svn_info})
514+
svn_info_success = False
515+
516+
else:
517+
log(ctx, f"if: False")
518+
519+
except:
520+
log(ctx, f"pysvn exception", "error", {"svn_info": svn_info})
521+
raise
467522

468523
# # Attempt to use pexpect
469524
# test = cmd.run_pexpect(
@@ -497,7 +552,7 @@ def _test_connection_and_credentials(
497552
if tries_attempted > 1:
498553
log(ctx, f"Successfully connected to repo remote after {tries_attempted} tries", "warning")
499554

500-
ctx.job["svn_info"] = svn_info.get("output",[])
555+
ctx.job["svn_info"] = svn_info
501556
return True
502557

503558
# If we've hit the max_retries limit, return here
@@ -524,7 +579,7 @@ def _test_connection_and_credentials(
524579

525580
def _extract_svn_remote_state_from_svn_info_output(ctx: Context) -> bool:
526581
"""
527-
Extract revision information from svn info command output
582+
Extract revision information from output of svn info cli command
528583
"""
529584

530585
repo_key = ctx.job.get("config",{}).get("repo_key")
@@ -692,8 +747,8 @@ def _configure_git_repo(ctx: Context, commands: dict) -> None:
692747
# TODO: Move to git module
693748
cmd.run_subprocess(ctx, commands["cmd_git_default_branch"], quiet=True, name="cmd_git_default_branch")
694749

695-
if disable_tls_verification:
696-
git.set_config(ctx, "http.sslVerify", "false")
750+
# if disable_tls_verification:
751+
# git.set_config(ctx, "http.sslVerify", "false")
697752

698753

699754
# Set repo configs, as a list of tuples [(git config key, git config value),]

0 commit comments

Comments
 (0)