77from utils import cmd , git , lockfiles
88
99# Import Python standard modules
10+ from datetime import datetime
1011import os
1112import random
1213import re
1314import shutil
1415import 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
1724import pysvn
1825
1926# dicts for:
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-
8041def 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+
397420def _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
525580def _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