1'''
2 ====================================================================
3 Copyright (c) 2003-2009 Barry A Scott.  All rights reserved.
4
5 This software is licensed as described in the file LICENSE.txt,
6 which you should have received as part of this distribution.
7
8 ====================================================================
9'''
10import pysvn
11import time
12import sys
13import os
14import parse_datetime
15import glob
16import locale
17import types
18
19if sys.version_info.major == 2:
20    # suport cp65001 aka utf-8
21    import codecs
22    codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
23
24try:
25    sorted( [] )
26except NameError:
27    def sorted( list_in ):
28        list_out = list( list_in )
29        list_out.sort()
30        return list_out
31
32if hasattr( types, 'StringTypes' ):
33    StringTypes = types.StringTypes
34else:
35    StringTypes = [type( '' )]
36
37if hasattr( types, 'DictType' ):
38    DictType = types.DictType
39else:
40    DictType = type( {} )
41
42class CommandError( Exception ):
43    def __init__( self, reason ):
44        Exception.__init__( self )
45        self._reason = reason
46
47    def reason( self ):
48        return self._reason
49
50    def __str__( self ):
51        return self._reason
52
53def main( args ):
54    progname = os.path.basename( args[0] )
55    pause = False
56    if args[1:2] == ['--pause']:
57        del args[1]
58        pause = True
59
60    # if the locale is not setup SVN can report errors handling non ascii file names
61    initLocale()
62
63    svn_cmd = SvnCommand( progname )
64    rc = svn_cmd.dispatch( args[1:] )
65    if pause:
66        sys.stdin.readline()
67    return rc
68
69def initLocale():
70    # init the locale
71    if sys.platform in ['win32','cygwin']:
72        locale.setlocale( locale.LC_ALL, '' )
73
74    else:
75        language_code, encoding = locale.getdefaultlocale()
76        if language_code is None:
77            language_code = 'en_GB'
78
79        if encoding is None:
80            encoding = 'UTF-8'
81        if encoding.lower() == 'utf':
82            encoding = 'UTF-8'
83
84        try:
85            # setlocale fails when params it does not understand are passed
86            locale.setlocale( locale.LC_ALL, '%s.%s' % (language_code, encoding) )
87        except locale.Error:
88            # force a locale that will work
89            locale.setlocale( locale.LC_ALL, 'en_GB.UTF-8' )
90
91def fmtDateTime( t ):
92    return time.strftime( '%d-%b-%Y %H:%M:%S', time.localtime( t ) )
93
94wc_status_kind_map = {
95pysvn.wc_status_kind.added: 'A',
96pysvn.wc_status_kind.conflicted: 'C',
97pysvn.wc_status_kind.deleted: 'D',
98pysvn.wc_status_kind.external: 'X',
99pysvn.wc_status_kind.ignored: 'I',
100pysvn.wc_status_kind.incomplete: '!',
101pysvn.wc_status_kind.missing: '!',
102pysvn.wc_status_kind.merged: 'G',
103pysvn.wc_status_kind.modified: 'M',
104pysvn.wc_status_kind.none: ' ',
105pysvn.wc_status_kind.normal: ' ',
106pysvn.wc_status_kind.obstructed: '~',
107pysvn.wc_status_kind.replaced: 'R',
108pysvn.wc_status_kind.unversioned: '?',
109}
110
111wc_notify_action_map = {
112pysvn.wc_notify_action.add: 'A',
113pysvn.wc_notify_action.commit_added: 'A',
114pysvn.wc_notify_action.commit_deleted: 'D',
115pysvn.wc_notify_action.commit_modified: 'M',
116pysvn.wc_notify_action.commit_postfix_txdelta: None,
117pysvn.wc_notify_action.commit_replaced: 'R',
118pysvn.wc_notify_action.copy: 'c',
119pysvn.wc_notify_action.delete: 'D',
120pysvn.wc_notify_action.failed_revert: 'F',
121pysvn.wc_notify_action.resolved: 'R',
122pysvn.wc_notify_action.restore: 'R',
123pysvn.wc_notify_action.revert: 'R',
124pysvn.wc_notify_action.skip: 'skip',
125pysvn.wc_notify_action.status_completed: None,
126pysvn.wc_notify_action.status_external: 'X',
127pysvn.wc_notify_action.update_add: 'A',
128pysvn.wc_notify_action.update_completed: None,
129pysvn.wc_notify_action.update_delete: 'D',
130pysvn.wc_notify_action.update_external: 'X',
131pysvn.wc_notify_action.update_update: 'U',
132pysvn.wc_notify_action.annotate_revision: 'A',
133}
134
135# new in svn 1.4?
136if hasattr( pysvn.wc_notify_action, 'locked' ):
137    wc_notify_action_map[ pysvn.wc_notify_action.locked ] = 'locked'
138    wc_notify_action_map[ pysvn.wc_notify_action.unlocked ] = 'unlocked'
139    wc_notify_action_map[ pysvn.wc_notify_action.failed_lock ] = 'failed_lock'
140    wc_notify_action_map[ pysvn.wc_notify_action.failed_unlock ] = 'failed_unlock'
141
142# new in svn 1.5
143if hasattr( pysvn.wc_notify_action, 'exists' ):
144    wc_notify_action_map[ pysvn.wc_notify_action.exists ] = 'exists'
145    wc_notify_action_map[ pysvn.wc_notify_action.changelist_set ] = 'changelist_set'
146    wc_notify_action_map[ pysvn.wc_notify_action.changelist_clear ] = 'changelist_clear'
147    wc_notify_action_map[ pysvn.wc_notify_action.changelist_moved ] = 'changelist_moved'
148    wc_notify_action_map[ pysvn.wc_notify_action.foreign_merge_begin ] = 'foreign_merge_begin'
149    wc_notify_action_map[ pysvn.wc_notify_action.merge_begin ] = 'merge_begin'
150    wc_notify_action_map[ pysvn.wc_notify_action.update_replace ] = 'update_replace'
151
152# new in svn 1.6
153if hasattr( pysvn.wc_notify_action, 'property_added' ):
154    wc_notify_action_map[ pysvn.wc_notify_action.property_added ] = 'property_added'
155    wc_notify_action_map[ pysvn.wc_notify_action.property_modified ] = 'property_modified'
156    wc_notify_action_map[ pysvn.wc_notify_action.property_deleted ] = 'property_deleted'
157    wc_notify_action_map[ pysvn.wc_notify_action.property_deleted_nonexistent ] = 'property_deleted_nonexistent'
158    wc_notify_action_map[ pysvn.wc_notify_action.revprop_set ] = 'revprop_set'
159    wc_notify_action_map[ pysvn.wc_notify_action.revprop_deleted ] = 'revprop_deleted'
160    wc_notify_action_map[ pysvn.wc_notify_action.merge_completed ] = 'merge_completed'
161    wc_notify_action_map[ pysvn.wc_notify_action.tree_conflict ] = 'tree_conflict'
162    wc_notify_action_map[ pysvn.wc_notify_action.failed_external ] = 'failed_external'
163
164# new in svn 1.7
165if hasattr( pysvn.wc_notify_action, 'update_started' ):
166    wc_notify_action_map[ pysvn.wc_notify_action.update_started ] = 'update_started'
167    wc_notify_action_map[ pysvn.wc_notify_action.update_skip_obstruction ] = 'update_skip_obstruction'
168    wc_notify_action_map[ pysvn.wc_notify_action.update_skip_working_only ] = 'update_skip_working_only'
169    wc_notify_action_map[ pysvn.wc_notify_action.update_external_removed ] = 'update_external_removed'
170    wc_notify_action_map[ pysvn.wc_notify_action.update_shadowed_add ] = 'update_shadowed_add'
171    wc_notify_action_map[ pysvn.wc_notify_action.update_shadowed_update ] = 'update_shadowed_update'
172    wc_notify_action_map[ pysvn.wc_notify_action.update_shadowed_delete ] = 'update_shadowed_delete'
173    wc_notify_action_map[ pysvn.wc_notify_action.merge_record_info ] = 'merge_record_info'
174    wc_notify_action_map[ pysvn.wc_notify_action.upgraded_path ] = 'upgraded_path'
175    wc_notify_action_map[ pysvn.wc_notify_action.merge_record_info_begin ] = 'merge_record_info_begin'
176    wc_notify_action_map[ pysvn.wc_notify_action.merge_elide_info ] = 'merge_elide_info'
177    wc_notify_action_map[ pysvn.wc_notify_action.patch ] = 'patch'
178    wc_notify_action_map[ pysvn.wc_notify_action.patch_applied_hunk ] = 'patch_applied_hunk'
179    wc_notify_action_map[ pysvn.wc_notify_action.patch_rejected_hunk ] = 'patch_rejected_hunk'
180    wc_notify_action_map[ pysvn.wc_notify_action.patch_hunk_already_applied ] = 'patch_hunk_already_applied'
181    wc_notify_action_map[ pysvn.wc_notify_action.commit_copied ] = 'commit_copied'
182    wc_notify_action_map[ pysvn.wc_notify_action.commit_copied_replaced ] = 'commit_copied_replaced'
183    wc_notify_action_map[ pysvn.wc_notify_action.url_redirect ] = 'url_redirect'
184    wc_notify_action_map[ pysvn.wc_notify_action.path_nonexistent ] = 'path_nonexistent'
185    wc_notify_action_map[ pysvn.wc_notify_action.exclude ] = 'exclude'
186    wc_notify_action_map[ pysvn.wc_notify_action.failed_conflict ] = 'failed_conflict'
187    wc_notify_action_map[ pysvn.wc_notify_action.failed_missing ] = 'failed_missing'
188    wc_notify_action_map[ pysvn.wc_notify_action.failed_out_of_date ] = 'failed_out_of_date'
189    wc_notify_action_map[ pysvn.wc_notify_action.failed_no_parent ] = 'failed_no_parent'
190
191# new in svn 1.7.1+?
192if hasattr( pysvn.wc_notify_action, 'failed_locked' ):
193    wc_notify_action_map[ pysvn.wc_notify_action.failed_locked ] = 'failed_locked'
194    wc_notify_action_map[ pysvn.wc_notify_action.failed_forbidden_by_server ] = 'failed_forbidden_by_server'
195    wc_notify_action_map[ pysvn.wc_notify_action.skip_conflicted ] = 'skip_conflicted'
196
197# new in svn 1.8
198if hasattr( pysvn.wc_notify_action, 'update_broken_lock' ):
199    wc_notify_action_map[ pysvn.wc_notify_action.update_broken_lock ] = 'update_broken_lock'
200    wc_notify_action_map[ pysvn.wc_notify_action.failed_obstruction ] = 'failed_obstruction'
201    wc_notify_action_map[ pysvn.wc_notify_action.conflict_resolver_starting ] = 'conflict_resolver_starting'
202    wc_notify_action_map[ pysvn.wc_notify_action.conflict_resolver_done ] = 'conflict_resolver_done'
203    wc_notify_action_map[ pysvn.wc_notify_action.left_local_modifications ] = 'left_local_modifications'
204    wc_notify_action_map[ pysvn.wc_notify_action.foreign_copy_begin ] = 'foreign_copy_begin'
205    wc_notify_action_map[ pysvn.wc_notify_action.move_broken ] = 'move_broken'
206
207# new in svn 1.9
208if hasattr( pysvn.wc_notify_action, 'cleanup_external' ):
209    wc_notify_action_map[ pysvn.wc_notify_action.cleanup_external ] = 'cleanup_external'
210    wc_notify_action_map[ pysvn.wc_notify_action.failed_requires_target ] = 'failed_requires_target'
211    wc_notify_action_map[ pysvn.wc_notify_action.info_external ] = 'info_external'
212    wc_notify_action_map[ pysvn.wc_notify_action.commit_finalizing ] = 'commit_finalizing'
213
214class SvnCommand:
215    def __init__( self, progname ):
216        self.progname = progname
217        self.client = None
218        self.revision_update_complete = None
219        self.notify_message_list = []
220        self.pysvn_testing = False
221        self.debug_enabled = False
222        self.next_log_message = None
223
224    def debug( self, msg, args=() ):
225        if self.debug_enabled:
226            print( 'Debug: %s' % (msg % args) )
227
228    def initClient( self, config_dir ):
229        self.client = pysvn.Client( config_dir )
230        self.client.exception_style = 1
231        self.client.commit_info_style = 1
232        self.client.callback_get_login = self.callback_getLogin
233        self.client.callback_get_log_message = self.callback_getLogMessage
234        self.client.callback_notify = self.callback_notify
235        self.client.callback_cancel = self.callback_cancel
236        if hasattr( self.client, 'callback_conflict_resolver' ):
237            self.client.callback_conflict_resolver = self.callback_conflict_resolver
238        self.client.callback_cancel = self.callback_cancel
239        self.client.callback_ssl_client_cert_password_prompt = self.callback_ssl_client_cert_password_prompt
240        self.client.callback_ssl_client_cert_prompt = self.callback_ssl_client_cert_prompt
241        self.client.callback_ssl_server_prompt = self.callback_ssl_server_prompt
242        self.client.callback_ssl_server_trust_prompt = self.callback_ssl_server_trust_prompt
243
244    def callback_ssl_client_cert_password_prompt( self ):
245        print( 'callback_ssl_client_cert_password_prompt' )
246
247    def callback_ssl_client_cert_prompt( self ):
248        print( 'callback_ssl_client_cert_prompt' )
249
250    def callback_ssl_server_prompt( self ):
251        print( 'callback_ssl_server_prompt' )
252
253    def callback_ssl_server_trust_prompt( self, trust_data ):
254        for key,value in trust_data.items():
255            print( '%s: %s' % (key, value) )
256        print('')
257        answer = ''
258        while answer.lower() not in ['p','t','r']:
259            sys.stdout.write( '(P)ermanent accept, (T)emporary accept or (R)eject: ' )
260            answer = sys.stdin.readline().strip()
261        if answer.lower() == 'p':
262            return True, trust_data['failures'], True
263        if answer.lower() == 't':
264            return True, trust_data['failures'], False
265        return False, 0, False
266
267    def callback_cancel( self ):
268        return False
269
270    def callback_notify( self, arg_dict ):
271        if arg_dict['action'] == pysvn.wc_notify_action.update_completed:
272            self.revision_update_complete = arg_dict['revision']
273        elif arg_dict['path'] != '' and wc_notify_action_map[ arg_dict['action'] ] is not None:
274            msg = '%s %s' % (wc_notify_action_map[ arg_dict['action'] ], arg_dict['path'])
275            if self.pysvn_testing != '99.99.99':
276                self.notify_message_list.append( msg )
277            else:
278                print( msg )
279
280    def callback_conflict_resolver( self, arg_dict ):
281        print( 'callback_conflict_resolver' )
282        for key in sorted( arg_dict.keys() ):
283            value = arg_dict[ key ]
284
285            if type(value) == DictType:
286                value = '{%s}' % (', '.join( ['%r: %r' % (key, value) for key, value in sorted( value.items() )] ),)
287
288            elif type(value) not in StringTypes:
289                value = repr(value)
290
291            print( '  %s: %s' % (key, value) )
292
293        return pysvn.wc_conflict_choice.postpone, None, False
294
295    def callback_getLogin( self, realm, username, may_save ):
296        print( 'May save: %s ' % may_save )
297        print( 'Realm: %s ' % realm )
298        if username:
299            print( 'Username: %s' % username )
300        else:
301            sys.stdout.write( 'Username: ' )
302            username = sys.stdin.readline().strip()
303            if len(username) == 0:
304                return 0, '', '', False
305
306        sys.stdout.write( 'Password: ' )
307        password = sys.stdin.readline().strip()
308
309        save_password = 'x'
310        while save_password.lower() not in ['y','ye','yes','n', 'no','']:
311            sys.stdout.write( 'Save password? [y/n] ' )
312            save_password = sys.stdin.readline().strip()
313
314        return 1, username, password, save_password in ['y','ye','yes']
315
316    def getLogMessage( self ):
317        if self.next_log_message is not None:
318            message = self.next_log_message
319            self.next_log_message = None
320            return message
321
322        sys.stdout.write( 'Log message\n' )
323        sys.stdout.write( '--- -------\n' )
324        message = sys.stdin.read()
325        return message
326
327    def callback_getLogMessage( self ):
328        return True, self.getLogMessage()
329
330    def dispatch( self, argv ):
331        try:
332            args = SvnArguments( argv )
333            cmd_name = 'cmd_%s' % args.getCommandName( 'help' )
334
335            self.initClient( args.getOptionalValue( '--config-dir', '' ) )
336            self.client.set_auth_cache( args.getBooleanOption( '--no-auth-cache', False ) )
337            self.pysvn_testing = args.getOptionalValue( '--pysvn-testing', '99.99.99' )
338            self.debug_enabled = args.getBooleanOption( '--debug', True )
339
340            getattr( self, cmd_name, self.cmd_help )( args )
341
342            self.printNotifyMessages()
343
344        except pysvn.ClientError as e:
345            self.printNotifyMessages()
346            print( e.args[0] )
347            return 1
348
349        except CommandError as e:
350            self.printNotifyMessages()
351            print( e.reason() )
352            return 1
353
354        return 0
355
356    def printNotifyMessages( self ):
357            # different versions of SVN notify messages in different orders
358            # by sorting before printing we hope to have one set of regression
359            # test data for multiple versions of SVN
360            self.notify_message_list.sort()
361            for msg in self.notify_message_list:
362                print( msg )
363            self.notify_message_list = []
364
365    def cmd_version( self, args ):
366        print( 'PYSVN Version: %r' % (pysvn.version,) )
367        print( 'SVN Version: %r' % (pysvn.svn_version,) )
368        if hasattr( pysvn, 'svn_api_version' ):
369            print( 'SVN API Version: %r' % (pysvn.svn_api_version,) )
370        print( 'pysvn._pysvn %r' % (pysvn._pysvn,) )
371
372
373    def cmd_is_url( self, args ):
374        path = args.getPositionalArgs( 1 )[0]
375        is_url = self.client.is_url( path )
376        if is_url:
377            print( 'url %s' % path )
378        else:
379            print( 'path %s' % path )
380
381    def cmd_add( self, args ):
382        recurse = args.getBooleanOption( '--non-recursive', False )
383        force = args.getBooleanOption( '--force', False )
384
385        self.client.add( args.getPositionalArgs( 1 ), recurse=recurse, force=force )
386
387    def cmd_add_to_changelist( self, args ):
388        if not hasattr( self.client, add_to_changelist ):
389            print( 'Error: add_to_changelist is not supported by this version of Subversion' )
390            return
391
392        path, changelist = args.getPositionalArgs( 2, 2 )
393        self.client.add_to_changelist( path, changelist )
394
395    def cmd_annotate( self, args ):
396        start_revision, end_revision = args.getOptionalRevisionPair( '--revision', '0', 'head' )
397        positional_args = args.getPositionalArgs( 1, 1 )
398        all_lines = self.client.annotate(
399                                    positional_args[0],
400                                    revision_start=start_revision,
401                                    revision_end=end_revision )
402        self.printNotifyMessages()
403
404        for line in all_lines:
405            print( '%d| r%d | %s | %s | %s' %
406                (line['number']+1
407                ,line['revision'].number
408                ,line['author']
409                ,line['date']
410                ,line['line']) )
411    cmd_ann = cmd_annotate
412
413    def cmd_annotate2( self, args ):
414        if not hasattr( self.client, 'annotate2' ):
415            print( 'annotate2 is not available in this version of subversion' )
416            return
417
418        start_revision, end_revision = args.getOptionalRevisionPair( '--revision', '0', 'head' )
419        positional_args = args.getPositionalArgs( 1, 1 )
420
421        all_lines = self.client.annotate2(
422                                positional_args[0],
423                                revision_start=start_revision,
424                                revision_end=end_revision )
425
426        self.printNotifyMessages()
427
428        for line in all_lines:
429            print( '%d| r%d | %s' %
430                (line['number']+1
431                ,line['revision'].number
432                ,line['line']) )
433            if line['merged_revision'] is not None:
434                print( '   Merged from r%d %s' %
435                    (line['merged_revision']
436                    ,line['merged_path']) )
437
438    cmd_ann2 = cmd_annotate2
439
440    def cmd_cat( self, args ):
441        revision = args.getOptionalRevision( '--revision', 'head' )
442        text = self.client.cat( args.getPositionalArgs( 1, 1 )[0], revision=revision )
443        print( text.decode( 'utf-8' ).replace( '\r\n', '\n' ) )
444
445    def cmd_checkout( self, args ):
446        recurse = args.getBooleanOption( '--non-recursive', False )
447        positional_args = args.getPositionalArgs( 1, 2 )
448        if len(positional_args) == 1:
449            positional_args.append( os.path.basename( positional_args[0] ) )
450
451        self.revision_update_complete = None
452        self.client.checkout( positional_args[0], positional_args[1], recurse=recurse )
453        self.printNotifyMessages()
454
455        if self.revision_update_complete is not None:
456            print( 'Checked out revision %s' % self.revision_update_complete.number )
457        else:
458            print( 'Checked out unknown revision - checkout failed?' )
459
460    cmd_co = cmd_checkout
461
462    def cmd_cleanup( self, args ):
463        positional_args = args.getPositionalArgs( 0, 1 )
464        if len(positional_args) == 0:
465            positional_args.append( '.' )
466
467        self.client.cleanup( positional_args[0] )
468
469    def cmd_checkin( self, args ):
470        msg = args.getOptionalValue( '--message', '' )
471
472        recurse = args.getBooleanOption( '--non-recursive', False )
473        positional_args = args.getPositionalArgs( 0 )
474        if len(positional_args) == 0:
475            positional_args.append( '.' )
476        if msg == '':
477            msg = self.getLogMessage()
478
479        commit_info = self.client.checkin( positional_args, msg, recurse=recurse )
480        rev = commit_info["revision"]
481        self.printNotifyMessages()
482
483        if commit_info['post_commit_err'] is not None:
484            print( commit_info['post_commit_err'] )
485
486        if rev is None:
487            print( 'Nothing to commit' )
488        elif rev.number > 0:
489            print( 'Revision %s' % rev.number )
490        else:
491            print( 'Commit failed' )
492
493
494    cmd_commit = cmd_checkin
495    cmd_ci = cmd_checkin
496
497    def cmd_copy( self, args ):
498        positional_args = args.getPositionalArgs( 2, 2 )
499        self.client.copy( positional_args[0], positional_args[1] )
500    cmd_cp = cmd_copy
501
502    def cmd_diff( self, args ):
503        recurse = args.getBooleanOption( '--non-recursive', False )
504        revision1, revision2 = args.getOptionalRevisionPair( '--revision', 'base', 'working' )
505        positional_args = args.getPositionalArgs( 0, 1 )
506        if len(positional_args) == 0:
507            positional_args.append( '.' )
508
509        if 'TEMP' in os.environ:
510            tmpdir = os.environ['TEMP']
511        elif 'TMPDIR' in os.environ:
512            tmpdir = os.environ['TMPDIR']
513        elif 'TMP' in os.environ:
514            tmpdir = os.environ['TMP']
515        elif os.path.exists( '/usr/tmp' ):
516            tmpdir = '/usr/tmp'
517        elif os.path.exists( '/tmp' ):
518            tmpdir = '/tmp'
519        else:
520            print( 'No tmp dir!' )
521            return
522
523        self.debug( 'cmd_diff %r, %r, %r, %r, %r' % (tmpdir, positional_args[0], recurse, revision1, revision2) )
524        diff_text = self.client.diff( tmpdir, positional_args[0], recurse=recurse,
525                                            revision1=revision1, revision2=revision2,
526                                            diff_options=['-u'] )
527        print( diff_text.replace( '\r\n', '\n' ) )
528
529    def cmd_export( self, args ):
530        force = args.getBooleanOption( '--force', False )
531        revision_url = args.getOptionalRevision( '--revision', 'head' )
532        revision_wc = args.getOptionalRevision( '--revision', 'working' )
533        native_eol = args.getOptionalValue( '--native-eol', None )
534        positional_args = args.getPositionalArgs( 2, 2 )
535        if self.client.is_url( positional_args[0] ):
536            revision = revision_url
537        else:
538            revision = revision_wc
539
540        self.client.export( positional_args[0], positional_args[1], revision=revision, force=force, native_eol=native_eol )
541
542    def cmd_info( self, args ):
543        positional_args = args.getPositionalArgs( 0, 1 )
544        if len(positional_args) == 0:
545            positional_args.append( '.' )
546
547        path = positional_args[0]
548
549        entry = self.client.info( path )
550
551        print( 'Path: %s' % path )
552        if entry.name and entry.name != 'svn:this_dir':
553            print( 'Name: %s' % entry.name )
554        if entry.url:
555            print( 'Url: %s' % entry.url )
556        if entry.repos and self.pysvn_testing >= '01.03.00':
557            print( 'Repository: %s' % entry.repos )
558        if entry.uuid:
559            print( 'Repository UUID: %s' % entry.uuid )
560        if entry.revision.kind == pysvn.opt_revision_kind.number:
561            print( 'Revision: %s' % entry.revision.number )
562        if entry.kind == pysvn.node_kind.file:
563            print( 'Node kind: file' )
564        elif entry.kind == pysvn.node_kind.dir:
565            print( 'Node kind: directory' )
566        elif entry.kind == pysvn.node_kind.none:
567            print( 'Node kind: none' )
568        else:
569            print( 'Node kind: unknown' )
570
571        if entry.schedule == pysvn.wc_schedule.normal:
572            print( "Schedule: normal" )
573        elif entry.schedule == pysvn.wc_schedule.add:
574            print( "Schedule: add" )
575        elif entry.schedule == pysvn.wc_schedule.delete:
576            print( "Schedule: delete" )
577        elif entry.schedule == pysvn.wc_schedule.replace:
578            print( "Schedule: replace" )
579        if entry.is_copied:
580            if entry.copyfrom_url:
581                print( 'Copied From URL: %s' %  entry.copyfrom_url )
582            if entry.copyfrom_rev.number:
583                print( 'Copied From Rev: %s' %  entry.copyfrom_rev.number )
584        if entry.commit_author:
585            print( 'Last Changed Author: %s' %  entry.commit_author )
586        if entry.commit_revision.number:
587            print( 'Last Changed Rev: %s' %  entry.commit_revision.number )
588        if entry.commit_time:
589            print( 'Last Changed Date: %s' %  fmtDateTime( entry.commit_time ) )
590        if entry.text_time:
591            print( 'Text Last Updated: %s' %  fmtDateTime( entry.text_time ) )
592        if entry.properties_time and self.pysvn_testing == '99.99.99':
593            print( 'Properties Last Updated: %s' %  fmtDateTime( entry.properties_time ) )
594        if entry.checksum:
595            print( 'Checksum: %s' %  entry.checksum )
596
597    def cmd_info2( self, args ):
598        recurse = args.getBooleanOption( '--recursive', True )
599        revision_url = args.getOptionalRevision( '--revision', 'head' )
600        revision_path = args.getOptionalRevision( '--revision', 'unspecified' )
601
602        positional_args = args.getPositionalArgs( 0, 1 )
603        if len(positional_args) == 0:
604            positional_args.append( '.' )
605
606        path = positional_args[0]
607
608        if self.client.is_url( path ):
609            revision = revision_url
610        else:
611            revision = revision_path
612
613        all_entries = self.client.info2( path, revision=revision,  recurse=recurse )
614
615        for path, info in all_entries:
616            print('')
617            print( 'Path: %s' % path )
618
619            if info.URL:
620                print( 'Url: %s' % info.URL )
621            if info.rev:
622                print( 'Revision: %s' % info.rev.number )
623            if info.repos_root_URL and self.pysvn_testing >= '01.03.00':
624                print( 'Repository root_URL: %s' % info.repos_root_URL )
625            if info.repos_UUID:
626                print( 'Repository UUID: %s' % info.repos_UUID )
627            if info.last_changed_author:
628                print( 'Last changed author: %s' % info.last_changed_author )
629            if info.last_changed_date:
630                print( 'Last Changed Date: %s' %  fmtDateTime( info.last_changed_date ) )
631            if info.last_changed_rev.kind == pysvn.opt_revision_kind.number:
632                print( 'Last changed revision: %s' % info.last_changed_rev.number )
633            if info.kind == pysvn.node_kind.file:
634                print( 'Node kind: file' )
635            elif info.kind == pysvn.node_kind.dir:
636                print( 'Node kind: directory' )
637            elif info.kind == pysvn.node_kind.none:
638                print( 'Node kind: none' )
639            else:
640                print( 'Node kind: unknown' )
641            if info.lock:
642                print( 'Lock Owner: %s' % info.lock.owner )
643                print( 'Lock Creation Date: %s' % fmtDateTime( info.lock.creation_date ) )
644                if info.lock.expiration_date is not None:
645                    print( 'Lock Expiration Date: %s' % fmtDateTime( info.lock.expiration_date ) )
646                print( 'Lock Token: %s' % info.lock.token )
647                print( 'Lock Comment:' )
648                if info.lock.comment not in ['', None]:
649                    print( info.lock.comment )
650            if info.wc_info:
651                wc_info = info.wc_info
652                if wc_info.schedule == pysvn.wc_schedule.normal:
653                    print( "Schedule: normal" )
654                elif wc_info.schedule == pysvn.wc_schedule.add:
655                    print( "Schedule: add" )
656                elif wc_info.schedule == pysvn.wc_schedule.delete:
657                    print( "Schedule: delete" )
658                elif wc_info.schedule == pysvn.wc_schedule.replace:
659                    print( "Schedule: replace" )
660                if wc_info.copyfrom_url:
661                    print( 'Copied From URL: %s' %  wc_info.copyfrom_url )
662                    print( 'Copied From Rev: %s' %  wc_info.copyfrom_rev.number )
663                if wc_info.text_time:
664                    print( 'Text Last Updated: %s' %  fmtDateTime( wc_info.text_time ) )
665                if wc_info.prop_time and self.pysvn_testing == '99.99.99':
666                    print( 'Properties Last Updated: %s' %  fmtDateTime( wc_info.prop_time ) )
667                if wc_info.checksum:
668                    print( 'Checksum: %s' %  wc_info.checksum )
669
670    def cmd_import( self, args ):
671        msg = args.getOptionalValue( '--message', '' )
672        recurse = args.getBooleanOption( '--non-recursive', False )
673        positional_args = args.getPositionalArgs( 2, 2 )
674        self.client.import_( positional_args[0], positional_args[1], msg, recurse=recurse )
675
676    def cmd_lock( self, args ):
677        msg = args.getOptionalValue( '--message', '' )
678        force = args.getBooleanOption( '--force', True )
679        positional_args = args.getPositionalArgs( 1, 1 )
680        self.client.lock( positional_args[0], msg, force );
681
682    def cmd_log( self, args ):
683        start_revision, end_revision = args.getOptionalRevisionPair( '--revision', 'head', '0' )
684        limit = args.getOptionalValue( '--limit', 0 )
685        verbose = args.getBooleanOption( '--verbose', True )
686        positional_args = args.getPositionalArgs( 1, 1 )
687        all_logs = self.client.log( positional_args[0],
688                                    revision_start=start_revision,
689                                    revision_end=end_revision,
690                                    discover_changed_paths=verbose,
691                                    limit=limit )
692
693        for log in all_logs:
694            print( '-'*60 )
695            print( 'rev %d: %s | %s | %d lines' %
696                (log.revision.number
697                ,log.author
698                ,fmtDateTime( log.date )
699                ,len( log.message.split('\n') )) )
700
701            if len( log.changed_paths ) > 0:
702                print( 'Changed paths:' )
703                for change_info in log.changed_paths:
704                    if change_info.copyfrom_path is None:
705                        print( '  %s %s' % (change_info.action, change_info.path) )
706                    else:
707                        print( '  %s %s (from %s:%d)' %
708                            (change_info.action
709                            ,change_info.path
710                            ,change_info.copyfrom_path
711                            ,change_info.copyfrom_revision.number) )
712
713            print( log.message )
714
715        print( '-'*60 )
716
717    def cmd_ls( self, args ):
718        recurse = args.getBooleanOption( '--recursive', True )
719        revision = args.getOptionalRevision( '--revision', 'head' )
720        verbose = args.getBooleanOption( '--verbose', True )
721        positional_args = args.getPositionalArgs( 0 )
722        if len(positional_args) == 0:
723            positional_args.append( '.' )
724
725        for arg in positional_args:
726            all_entries = self.client.ls( arg, revision=revision, recurse=recurse )
727            all_entries.sort( key=self.__sortKeyLsList )
728            if verbose:
729                for entry in all_entries:
730                    args = {}
731                    args.update( entry )
732                    args['time_str'] = fmtDateTime( entry.time )
733                    args['created_rev_num'] = entry.created_rev.number
734                    if args['size'] is None:
735                        args['size'] = '-'
736                    else:
737                        args['size'] = '%d' % (args['size'],)
738                    print( '%(created_rev_num)7d %(last_author)-10s %(size)6s %(time_str)s %(name)s' % args )
739
740            else:
741                for entry in all_entries:
742                    print( '%(name)s' % entry )
743
744    def __sortKeyLsList( self, entry ):
745        return entry['name']
746
747    def cmd_list( self, args ):
748        recurse = args.getBooleanOption( '--recursive', True )
749        revision = args.getOptionalRevision( '--revision', 'head' )
750        verbose = args.getBooleanOption( '--verbose', True )
751        fetch_locks = args.getBooleanOption( '--fetch-locks', True )
752        include_externals = args.getBooleanOption( '--include-externals', True )
753        search_pattern = args.getOptionalValue( '--search', None )
754        positional_args = args.getPositionalArgs( 0 )
755        if len(positional_args) == 0:
756            positional_args.append( '.' )
757
758        for arg in positional_args:
759            if self.pysvn_testing >= '01.10.00':
760                all_entries = self.client.list( arg, revision=revision, recurse=recurse, fetch_locks=fetch_locks, include_externals=include_externals, patterns=search_pattern )
761
762            elif self.pysvn_testing >= '01.08.00':
763                all_entries = self.client.list( arg, revision=revision, recurse=recurse, fetch_locks=fetch_locks, include_externals=include_externals )
764
765            else:
766                all_entries = self.client.list( arg, revision=revision, recurse=recurse, fetch_locks=fetch_locks )
767
768            if verbose:
769                for entry_tuple in all_entries:
770                    entry = entry_tuple[0]
771                    lock_info = entry_tuple[1]
772                    args = {}
773                    args.update( entry )
774                    args['time_str'] = fmtDateTime( entry.time )
775                    args['created_rev_num'] = entry.created_rev.number
776                    if args['size'] is None:
777                        args['size'] = '-'
778                    else:
779                        args['size'] = '%d' % (args['size'],)
780                    print( '%(created_rev_num)7d %(last_author)-10s %(size)6s %(time_str)s %(path)s' % args )
781                    if lock_info is not None:
782                        print( '        Lock   owner: %s' % (lock_info.owner,) )
783                        print( '        Lock comment: %s' % (lock_info.comment,) )
784                        if lock_info.creation_date is not None:
785                            print( '        Lock created: %s' % (time.strftime( '%Y-%m-%d %H:%M:%S', time.localtime( lock_info.creation_date ) ),) )
786                        if lock_info.expiration_date is not None:
787                            print( '        Lock expires: %s' % (time.strftime( '%Y-%m-%d %H:%M:%S', time.localtime( lock_info.expiration_date ) ),) )
788                    if self.pysvn_testing >= '01.08.00' and include_externals and entry_tuple[2] is not None:
789                        print( '        External target %s' % (entry_tuple[3],) )
790                        print( '        External    URL %s' % (entry_tuple[2],) )
791
792            else:
793                for entry_tuple in all_entries:
794                    print( '%(path)s' % entry_tuple[0] )
795
796    def cmd_merge( self, args ):
797        recurse = args.getBooleanOption( '--recursive', True )
798        dry_run = args.getBooleanOption( '--dry-run', False )
799        notice_ancestry = args.getBooleanOption( '--notice-ancestry', False )
800
801        # need to figure out which variaty of the merge command this is
802        if args.haveOption( '--revision' ):
803            # its merge -r N:M SOURCE [WCPATH]
804            revision1, revision2 = args.getMandatoryRevisionPair( '--revision' )
805            positional_args = args.getPositionalArgs( 1, 2 )
806            if len(positional_args) == 1:
807                positional_args.append( '.' )
808            path1 = positional_args[0]
809            path2 = positional_args[0]
810            wcpath = positional_args[1]
811        else:
812            # its merge sourceURL1[@N] sourceURL2[@M] [WCPATH]
813            positional_args = args.getPositionalArgs( 2, 3 )
814            if len(positional_args) == 2:
815                positional_args.append( '.' )
816
817            path1, rev1 = self.parsePathWithRevision( positional_args[0] )
818            path2, rev2 = self.parsePathWithRevision( positional_args[1] )
819            wcpath = positional_args[2]
820
821        self.client.merge( path1, revision1, path2, revision2, wcpath,
822                recurse=recurse, dry_run=dry_run, notice_ancestry=notice_ancestry )
823
824    def cmd_mkdir( self, args ):
825        if args.haveOption( '--message' ):
826            msg = args.getOptionalValue( '--message', '' )
827            if msg == '':
828                msg = self.getLogMessage()
829
830        else:
831            msg = ''
832
833        self.client.mkdir( args.getPositionalArgs( 1, 1 )[0], msg )
834
835    def cmd_move( self, args ):
836        positional_args = args.getPositionalArgs( 2, 2 )
837        self.client.move( positional_args[0], positional_args[1] )
838    cmd_mv = cmd_move
839
840    def cmd_patch( self, args ):
841        dry_run = args.getBooleanOption( '--dry-run', True )
842        reverse = args.getBooleanOption( '--reverse', True )
843        ignore_whitespace = args.getBooleanOption( '--ignore-whitespace', True )
844        remove_tempfiles = not args.getBooleanOption( '--no-remove-tempfiles', True )
845
846        patch_path, wc_dir_path = args.getPositionalArgs( 2, 2 )
847        abs_patch_path = os.path.abspath( patch_path )
848        abs_wc_dir_path = os.path.abspath( wc_dir_path )
849
850        self.client.patch( abs_patch_path, abs_wc_dir_path,
851                            dry_run=dry_run,
852                            reverse=reverse,
853                            ignore_whitespace=ignore_whitespace,
854                            remove_tempfiles=remove_tempfiles )
855
856    def key_props_by_path( self, a ):
857        return a[0]
858
859    def cmd_proplist( self, args ):
860        recurse = args.getBooleanOption( '--recursive', True )
861        revision = args.getOptionalRevision( '--revision', 'working' )
862        verbose = args.getBooleanOption( '--verbose', True )
863        positional_args = args.getPositionalArgs( 0, 0 )
864        if len(positional_args) == 0:
865            positional_args.append( '.' )
866
867        for arg in positional_args:
868
869            if self.client.is_url( arg ):
870                revision = args.getOptionalRevision( '--revision', 'head' )
871
872            all_props = self.client.proplist( arg, revision=revision, recurse=recurse )
873            all_props.sort( key=self.key_props_by_path )
874
875            for path, props in all_props:
876                print( "Properties on '%s':" % path )
877                prop_names = sorted( props.keys() )
878                for name in prop_names:
879                    if verbose:
880                        print( '  %s: %s' % (name, props[name]) )
881                    else:
882                        print( '  %s' % name )
883
884    cmd_pl = cmd_proplist
885
886    def cmd_propget( self, args ):
887        recurse = args.getBooleanOption( '--recursive', True )
888        revision = args.getOptionalRevision( '--revision', 'working' )
889        if self.pysvn_testing >= '01.08.00' and args.getBooleanOption( '--show-inherited-props' ):
890            get_inherited_props = True
891        else:
892            get_inherited_props = False
893        positional_args = args.getPositionalArgs( 1, 2 )
894        if len(positional_args) == 1:
895            positional_args.append( '.' )
896        if self.client.is_url( positional_args[0] ):
897            revision = args.getOptionalRevision( '--revision', 'head' )
898
899        if get_inherited_props:
900            props, inherited_props = self.client.propget(
901                    positional_args[0], positional_args[1],
902                    revision=revision,
903                    recurse=recurse,
904                    get_inherited_props=True )
905        else:
906            props = self.client.propget(
907                    positional_args[0], positional_args[1],
908                    revision=revision,
909                    recurse=recurse )
910            inherited_props = {}
911
912        prop_names = sorted( props.keys() )
913        for name in prop_names:
914            print( '%s: %s' % (name, props[name]) )
915
916        if len(inherited_props) > 0:
917            print( 'Inherited props' )
918            prop_names = sorted( inherited_props.keys() )
919            for name in prop_names:
920                print( '%s: %s' % (name, inherited_props[name]) )
921
922    cmd_pg = cmd_propget
923
924    def cmd_propset( self, args ):
925        recurse = args.getBooleanOption( '--recursive', True )
926        revision = args.getOptionalRevision( '--revision', 'working' )
927        positional_args = args.getPositionalArgs( 2, 3 )
928        if len(positional_args) == 2:
929            positional_args.append( '.' )
930        if self.client.is_url( positional_args[0] ):
931            revision = args.getOptionalRevision( '--revision', 'head' )
932
933        self.client.propset( positional_args[0], positional_args[1], positional_args[2], revision=revision, recurse=recurse )
934    cmd_ps = cmd_propset
935
936    def cmd_propdel( self, args ):
937        recurse = args.getBooleanOption( '--recursive', True )
938        revision = args.getOptionalRevision( '--revision', 'working' )
939        positional_args = args.getPositionalArgs( 1, 2 )
940        if len(positional_args) == 1:
941            positional_args.append( '.' )
942        if self.client.is_url( positional_args[0] ):
943            revision = args.getOptionalRevision( '--revision', 'head' )
944
945        self.client.propdel( positional_args[0], positional_args[1], revision=revision, recurse=recurse )
946    cmd_pd = cmd_propdel
947
948    def cmd_propset_local( self, args ):
949        skip_checks = args.getBooleanOption( '--skip-checks', True )
950        changelist = args.getOptionalValue( '--change-list', None )
951        positional_args = args.getPositionalArgs( 3, 3 )
952
953        if changelist is not None:
954            changelist = [s.strip() for s in changelist.split(',')]
955            self.client.propset_local( positional_args[0], positional_args[1], positional_args[2], skip_checks=skip_checks, changelist=changelist )
956
957        else:
958            self.client.propset_local( positional_args[0], positional_args[1], positional_args[2], skip_checks=skip_checks )
959
960    def cmd_propdel_local( self, args ):
961        changelist = args.getOptionalValue( '--change-list', None )
962        positional_args = args.getPositionalArgs( 2, 2 )
963
964        if changelist is not None:
965            changelist = [s.strip() for s in changelist.split(',')]
966            self.client.propdel_local( positional_args[0], positional_args[1], changelist=changelist )
967
968        else:
969            self.client.propdel_local( positional_args[0], positional_args[1] )
970
971    def cmd_propset_remote( self, args ):
972        self.next_log_message = args.getOptionalValue( '--message', None )
973
974        skip_checks = args.getBooleanOption( '--skip-checks', True )
975        revision = args.getOptionalRevision( '--revision', '0' )
976        positional_args = args.getPositionalArgs( 3, 3 )
977
978        if args.haveOption( '--revision' ):
979            self.client.propset_remote( positional_args[0], positional_args[1], positional_args[2], skip_checks=skip_checks, base_revision_for_url=revision )
980
981        else:
982            self.client.propset_remote( positional_args[0], positional_args[1], positional_args[2], skip_checks=skip_checks )
983
984    def cmd_propdel_remote( self, args ):
985        self.next_log_message = args.getOptionalValue( '--message', None )
986
987        revision = args.getOptionalRevision( '--revision', '0' )
988        positional_args = args.getPositionalArgs( 2, 2 )
989
990        self.client.propdel_remote( positional_args[0], positional_args[1], base_revision_for_url=revision )
991
992    def cmd_revproplist( self, args ):
993        revision = args.getOptionalRevision( '--revision', 'head' )
994        verbose = args.getBooleanOption( '--verbose', False )
995        positional_args = args.getPositionalArgs( 0, 1 )
996        if len(positional_args) == 0:
997            positional_args.append( '.' )
998
999        rev, prop_dict = self.client.revproplist( positional_args[0], revision=revision )
1000        print( 'Revision: %s' % rev.number )
1001        prop_keys = prop_dict.keys()
1002        for key in sorted( prop_keys ):
1003            print( '%s: %s' % (key, prop_dict[ key ]) )
1004
1005    cmd_rpl = cmd_revproplist
1006
1007    def cmd_revpropget( self, args ):
1008        revision = args.getOptionalRevision( '--revision', 'head' )
1009        positional_args = args.getPositionalArgs( 1, 2 )
1010        if len(positional_args) == 1:
1011            positional_args.append( '.' )
1012
1013        rev, value = self.client.revpropget( positional_args[0], positional_args[1], revision=revision )
1014        print( 'Revision: %s' % rev.number )
1015        print( '%s: %s' % (positional_args[0], value) )
1016
1017    cmd_rpg = cmd_revpropget
1018
1019    def cmd_revpropset( self, args ):
1020        force = args.getBooleanOption( '--force', False )
1021        revision = args.getOptionalRevision( '--revision', 'head' )
1022        positional_args = args.getPositionalArgs( 2, 3 )
1023        if len(positional_args) == 2:
1024            positional_args.append( '.' )
1025
1026        rev = self.client.revpropset( positional_args[0], positional_args[1], positional_args[2], revision=revision, force=force )
1027    cmd_rps = cmd_revpropset
1028
1029    def cmd_revpropdel( self, args ):
1030        force = args.getBooleanOption( '--force', False )
1031        revision = args.getOptionalRevision( '--revision', 'head' )
1032        positional_args = args.getPositionalArgs( 1, 2 )
1033        if len(positional_args) == 1:
1034            positional_args.append( '.' )
1035
1036        self.client.revpropdel( positional_args[0], positional_args[1], revision=revision, force=force )
1037    cmd_rpd = cmd_revpropdel
1038
1039    def cmd_remove( self, args ):
1040        force = args.getBooleanOption( '--force', True )
1041        positional_args = args.getPositionalArgs( 1, 0 )
1042        self.client.remove( positional_args, force=force )
1043    cmd_rm = cmd_remove
1044
1045    def cmd_remove_from_changelists( self, args ):
1046        if not hasattr( self.client, remove_from_changelists ):
1047            print( 'Error: remove_from_changelists is not supported by this version of Subversion' )
1048            return
1049
1050        path = args.getPositionalArgs( 1, 1 )[0]
1051        self.client.remove_from_changelists( path )
1052
1053    def cmd_resolved( self, args ):
1054        recurse = args.getBooleanOption( '--recursive', True )
1055        positional_args = args.getPositionalArgs( 1, 1 )
1056        self.client.resolved( positional_args[0], recurse=recurse )
1057
1058    def cmd_revert( self, args ):
1059        recurse = args.getBooleanOption( '--recursive', True )
1060        positional_args = args.getPositionalArgs( 1, 1 )
1061        self.client.revert( positional_args[0], recurse=recurse )
1062
1063    def key_by_path( self, a ):
1064        return a.path
1065
1066    def cmd_status( self, args ):
1067        recurse = args.getBooleanOption( '--non-recursive', False )
1068        verbose = args.getBooleanOption( '--verbose', True )
1069        quiet = args.getBooleanOption( '--quiet', True )
1070        ignore = args.getBooleanOption( '--no-ignore', False )
1071        update = args.getBooleanOption( '--show-updates', True )
1072
1073        positional_args = args.getPositionalArgs( 0 )
1074        if len(positional_args) == 0:
1075            all_entries = self.client.status( '', recurse=recurse, get_all=verbose, ignore=ignore, update=update )
1076            self._cmd_status_print( all_entries, verbose, update, ignore, quiet )
1077        else:
1078            for arg in positional_args:
1079                all_entries = self.client.status( arg, recurse=recurse, get_all=verbose, ignore=ignore, update=update )
1080                self._cmd_status_print( all_entries, verbose, update, ignore, quiet )
1081
1082    def _cmd_status_print( self, all_entries, detailed, update, ignore, quiet ):
1083        all_entries.sort( key=self.key_by_path )
1084        for entry in all_entries:
1085            if entry.text_status == pysvn.wc_status_kind.ignored and ignore:
1086                continue
1087
1088            if entry.text_status == pysvn.wc_status_kind.unversioned and quiet:
1089                continue
1090
1091            state = '%s%s%s%s%s' % (wc_status_kind_map[ entry.text_status ],
1092                    wc_status_kind_map[ entry.prop_status ],
1093                    ' L'[ entry.is_locked ],
1094                    ' +'[ entry.is_copied ],
1095                    ' S'[ entry.is_switched ])
1096
1097            if( entry.repos_text_status != pysvn.wc_status_kind.none
1098            or  entry.repos_prop_status != pysvn.wc_status_kind.none ):
1099                odd_status = '%s%s' % (wc_status_kind_map[ entry.repos_text_status ],
1100                    wc_status_kind_map[ entry.repos_prop_status ])
1101            else:
1102                odd_status = '  '
1103
1104            lock_state = ' '
1105            if entry.entry is not None and hasattr( entry.entry, 'lock_token' ):
1106                if entry.entry.lock_token is not None:
1107                    lock_state = 'K'
1108
1109            if hasattr( entry, 'repos_lock' ) and entry.repos_lock is not None:
1110                lock_state = 'O'
1111
1112            if entry.entry is not None and detailed:
1113                print( '%s%s %s %6d %6d %-14s %s' %
1114                    (state,
1115                    lock_state,
1116                    odd_status,
1117                    entry.entry.revision.number,
1118                    entry.entry.commit_revision.number,
1119                    entry.entry.commit_author,
1120                    entry.path) )
1121
1122            elif detailed:
1123                print( '%s%s %s %6s %6s %-14s %s' %
1124                    (state,
1125                    lock_state,
1126                    odd_status,
1127                    '',
1128                    '',
1129                    '',
1130                    entry.path) )
1131
1132            elif update:
1133                print( '%s%s %s %s' %
1134                    (state,
1135                    lock_state,
1136                    odd_status,
1137                    entry.path) )
1138
1139            else:
1140                if( entry.text_status != pysvn.wc_status_kind.normal
1141                or entry.prop_status != pysvn.wc_status_kind.normal
1142                or lock_state.strip() != ''):
1143                    print( '%s%s %s' % (state, lock_state, entry.path) )
1144
1145    cmd_st = cmd_status
1146    cmd_stat = cmd_status
1147
1148    def cmd_status2( self, args ):
1149        recurse = args.getBooleanOption( '--non-recursive', False )
1150        verbose = args.getBooleanOption( '--verbose', True )
1151        quiet = args.getBooleanOption( '--quiet', True )
1152        ignore = args.getBooleanOption( '--no-ignore', False )
1153        update = args.getBooleanOption( '--show-updates', True )
1154
1155        positional_args = args.getPositionalArgs( 0 )
1156        if len(positional_args) == 0:
1157            all_entries = self.client.status2( '', recurse=recurse, get_all=verbose, ignore=ignore, update=update )
1158            self._cmd_status2_print( all_entries, verbose, update, ignore, quiet )
1159        else:
1160            for arg in positional_args:
1161                all_entries = self.client.status2( arg, recurse=recurse, get_all=verbose, ignore=ignore, update=update )
1162                self._cmd_status2_print( all_entries, verbose, update, ignore, quiet )
1163
1164    def _cmd_status2_print( self, all_entries, detailed, update, ignore, quiet ):
1165        all_entries.sort( key=self.key_by_path )
1166        for entry in all_entries:
1167            if entry.text_status == pysvn.wc_status_kind.ignored and ignore:
1168                continue
1169
1170            if entry.text_status == pysvn.wc_status_kind.unversioned and quiet:
1171                continue
1172
1173            if entry.text_status == pysvn.wc_status_kind.none and quiet:
1174                continue
1175
1176            if entry.text_status == pysvn.wc_status_kind.none:
1177                text_status = '?'
1178            else:
1179                text_status = wc_status_kind_map[ entry.text_status ]
1180
1181            state = '%s%s%s%s%s' % (text_status,
1182                    wc_status_kind_map[ entry.prop_status ],
1183                    ' L'[ entry.wc_is_locked ],
1184                    ' +'[ entry.is_copied ],
1185                    ' S'[ entry.is_switched ])
1186
1187            if( entry.repos_text_status != pysvn.wc_status_kind.none
1188            or  entry.repos_prop_status != pysvn.wc_status_kind.none ):
1189                odd_status = '%s%s' % (wc_status_kind_map[ entry.repos_text_status ],
1190                    wc_status_kind_map[ entry.repos_prop_status ])
1191            else:
1192                odd_status = '  '
1193
1194            lock_state = ' '
1195            if entry.lock is not None and entry.lock.token is not None:
1196                lock_state = 'K'
1197
1198            if entry.repos_lock is not None:
1199                lock_state = 'O'
1200
1201            # convert from abs to rel path
1202            cwd = os.getcwd()
1203            if entry.path == cwd:
1204                path = '.'
1205
1206            elif entry.path.startswith( cwd + '/' ):
1207                path = entry.path[len(cwd)+1:]
1208
1209            else:
1210                path = entry.path
1211
1212            if detailed and entry.is_versioned:
1213                print( '%s%s %s %6d %6d %-14s %s' %
1214                    (state,
1215                    lock_state,
1216                    odd_status,
1217                    entry.revision.number,
1218                    entry.changed_revision.number,
1219                    entry.changed_author,
1220                    path) )
1221
1222            elif detailed:
1223                print( '%s%s %s %6s %6s %-14s %s' %
1224                    (state,
1225                    lock_state,
1226                    odd_status,
1227                    '',
1228                    '',
1229                    '',
1230                    path) )
1231
1232            elif update:
1233                print( '%s%s %s %s' %
1234                    (state,
1235                    lock_state,
1236                    odd_status,
1237                    path) )
1238
1239            else:
1240                if( entry.text_status != pysvn.wc_status_kind.normal
1241                or entry.prop_status != pysvn.wc_status_kind.normal
1242                or lock_state.strip() != ''):
1243                    print( '%s%s %s' % (state, lock_state, path) )
1244
1245    def cmd_switch( self, args ):
1246        recurse = args.getBooleanOption( '--non-recursive', False )
1247        revision = args.getOptionalRevision( '--revision', 'head' )
1248        positional_args = args.getPositionalArgs( 1, 2 )
1249        if len(positional_args) == 1:
1250            positional_args.append( '.' )
1251        self.client.switch( positional_args[0], positional_args[1],
1252                recurse=recurse, revision=revision )
1253
1254    def cmd_relocate( self, args ):
1255        recurse = args.getBooleanOption( '--non-recursive', False )
1256        positional_args = args.getPositionalArgs( 2, 3 )
1257        if len(positional_args) == 2:
1258            positional_args.append( '.' )
1259        self.client.relocate( positional_args[0], positional_args[1],
1260                positional_args[2], recurse=recurse )
1261
1262    def cmd_unlock( self, args ):
1263        force = args.getBooleanOption( '--force', False )
1264        positional_args = args.getPositionalArgs( 1, 1 )
1265        self.client.unlock( positional_args[0], force );
1266
1267    def cmd_update( self, args ):
1268        recurse = args.getBooleanOption( '--non-recursive', False )
1269        positional_args = args.getPositionalArgs( 0 )
1270        if len(positional_args) == 0:
1271            positional_args.append( '.' )
1272
1273        rev_list = self.client.update( positional_args[0], recurse=recurse )
1274        self.printNotifyMessages()
1275        if type(rev_list) == type([]) and len(rev_list) != 1:
1276            print( 'rev_list = %r' % [rev.number for rev in rev_list] )
1277
1278        if self.revision_update_complete is not None:
1279            print( 'Updated to revision %s' % self.revision_update_complete.number )
1280        else:
1281            print( 'Updated to unknown revision - update failed?' )
1282
1283    cmd_up = cmd_update
1284
1285    def cmd_vacuum( self, args ):
1286        remove_unversioned_items = args.getBooleanOption( '--remove-unversioned-items', True )
1287        remove_ignored_items = args.getBooleanOption( '--remove-ignored-items', True )
1288        fix_recorded_timestamps = args.getBooleanOption( '--fix-recorded-timestamps', True )
1289        vacuum_pristines = args.getBooleanOption( '--vacuum-pristines', True )
1290        include_externals = args.getBooleanOption( '--include-externals', True )
1291
1292        positional_args = args.getPositionalArgs( 0, 1 )
1293        if len(positional_args) == 0:
1294            positional_args.append( '.' )
1295
1296        self.client.vacuum(
1297                positional_args[0],
1298                remove_unversioned_items=remove_unversioned_items,
1299                remove_ignored_items=remove_ignored_items,
1300                fix_recorded_timestamps=fix_recorded_timestamps,
1301                vacuum_pristines=vacuum_pristines,
1302                include_externals=include_externals
1303                )
1304
1305    def cmd_help( self, args ):
1306        print( 'Version: pysvn %d.%d.%d-%d' % pysvn.version,'svn %d.%d.%d-%s' % pysvn.svn_version )
1307        valid_cmd_names = [name for name in SvnCommand.__dict__.keys() if name.find('cmd_') == 0]
1308        valid_cmd_names.sort()
1309        print( 'Available subcommands:' )
1310        index = 0
1311        line = ''
1312        for name in valid_cmd_names:
1313            line = line + ('   %-12s' % name[4:])
1314            if index % 4 == 3:
1315                print( line )
1316                line = ''
1317            index += 1
1318
1319# key is long option name, value is 1 if need next arg as value
1320long_opt_info = {
1321    # svn_cmd.py control
1322    '--pause': 0,
1323    '--pysvn-testing': 1,       # modify behaviour to assist testing pysvn
1324    '--debug': 0,               # do debug stuff
1325
1326    # command options
1327    '--auto-props': 0,          # enable automatic properties
1328    '--change-list': 1,         # changelist
1329    '--config-dir': 1,          # read user configuration files from directory ARG
1330    '--diff-cmd': 1,            # use ARG as diff command
1331    '--diff3-cmd': 1,           # use ARG as merge command
1332    '--dry-run': 0,             # try operation but make no changes
1333    '--editor-cmd': 1,          # use ARG as external editor
1334    '--encoding': 1,            # treat value as being in charset encoding ARG
1335    '--extensions': 1,          # pass ARG as bundled options to GNU diff
1336    '--fetch-locks': 0,         # as list to fetch lock info
1337    '--file': 1,                # read data from file ARG
1338    '--fix-recorded-timestamps': 0,
1339    '--force': 0,               # force operation to run
1340    '--force-log': 0,           # force validity of log message source
1341    '--include-externals': 0,
1342    '--include-externals': 0,   # include external information in list() output
1343    '--incremental': 0,         # give output suitable for concatenation
1344    '--limit': 1,               # number of logs to fetch
1345    '--message': 1,             # specify commit message ARG
1346    '--native-eol': 1,          # native eol ARG
1347    '--new': 1,                 # use ARG as the newer target
1348    '--no-auth-cache': 0,       # do not cache authentication tokens
1349    '--no-auto-props': 0,       # disable automatic properties
1350    '--no-diff-deleted': 0,     # do not print differences for deleted files
1351    '--no-ignore': 0,           # disregard default and svn:ignore property ignores
1352    '--no-remove-tempfiles': 0, # do not remove temp files
1353    '--non-interactive': 0,     # do no interactive prompting
1354    '--non-recursive': 0,       # operate on single directory only
1355    '--notice-ancestry': 0,     # notice ancestry when calculating differences
1356    '--old': 1,                 # use ARG as the older target
1357    '--password': 1,            # specify a password ARG
1358    '--quiet': 0,               # print as little as possible
1359    '--recursive': 0,           # descend recursively
1360    '--relocate': 0,            # relocate via URL-rewriting
1361    '--remove-ignored-items': 0,
1362    '--remove-unversioned-items': 0,
1363    '--revision': 1,            # revision X or X:Y range.  X or Y can be one of:
1364    '--revprop': 0,             # operate on a revision property (use with -r)
1365    '--search': 2,              # search for pattern
1366    '--show-inherited-props': 0, # show inherited props
1367    '--show-updates': 0,        # display update information
1368    '--skip-checks': 0,         # skip-checks
1369    '--strict': 0,              # use strict semantics
1370    '--targets': 1,             # pass contents of file ARG as additional args
1371    '--username': 1,            # specify a username ARG
1372    '--vacuum-pristines': 0,
1373    '--verbose': 0,             # print extra information
1374    '--version': 0,             # print client version info
1375    '--xml': 0,                 # output in xml
1376}
1377
1378# map short name to long
1379short_opt_info = {
1380    '-F': '--file',
1381    '-N': '--non-recursive',
1382    '-R': '--recursive',
1383    '-m': '--message',
1384    '-q': '--quiet',
1385    '-r': '--revision',
1386    '-u': '--show-updates',
1387    '-v': '--verbose',
1388    '-x': '--extensions',
1389}
1390
1391
1392#
1393# Usage:
1394#    Construct with a commnad list
1395#    call getCommandName()
1396#    call getBooleanOption() and getOptionalValue() as needed
1397#    finally call getPositionalArgs()
1398#
1399#
1400class SvnArguments:
1401    def __init__( self, all_args ):
1402        self.positional_args = []
1403        self.named_options = {}
1404        self.used_named_options = {}
1405
1406        need_next_arg = 0
1407        name = ''
1408
1409        for arg in all_args:
1410            if need_next_arg > 1:
1411                self.named_options.setdefault( name, [] ).append( arg )
1412                need_next_arg = 0
1413
1414            elif need_next_arg:
1415                self.named_options[ name ] = arg
1416                need_next_arg = 0
1417
1418            elif self._isOption( arg ):
1419                name, need_next_arg = self._optionInfo( arg )
1420                if not need_next_arg:
1421                    self.named_options[ name ] = None
1422
1423            else:
1424                expanded_arg = glob.glob( arg )
1425                if len(expanded_arg) > 0:
1426                    self.positional_args.extend( expanded_arg )
1427                else:
1428                    self.positional_args.append( arg )
1429        if need_next_arg:
1430            raise CommandError( 'Missing arg to option %s' % name )
1431
1432    def _isOption( self, arg ):
1433        return arg[0] == '-'
1434
1435    def _optionInfo( self, opt ):
1436        # return long_name, arg_needed
1437        long_opt = short_opt_info.get( opt, opt )
1438        if long_opt in long_opt_info:
1439            return long_opt, long_opt_info[ long_opt ]
1440        raise CommandError( 'unknown option %s' % opt )
1441
1442    def _checkOptionsUsed( self ):
1443        # check all options have been used
1444        for opt_name in self.named_options.keys():
1445            if opt_name not in self.used_named_options:
1446                raise CommandError( 'unused option %s' % opt_name )
1447
1448    def parsePathWithRevision( self, path_rev, default_rev ):
1449        if '@' in path_rev:
1450            path = path_rev[:path_rev.find('@')]
1451            rev = self._parseRevisionArg( path_rev[path_rev.find('@')+1:] )
1452        else:
1453            path = path_rev
1454            rev = self._parseRevisionArg( default_rev )
1455        return path, rev
1456
1457    def _parseRevisionArg( self, rev_string ):
1458        if rev_string.lower() == 'base':
1459            return pysvn.Revision( pysvn.opt_revision_kind.base )
1460        if rev_string.lower() == 'head':
1461            return pysvn.Revision( pysvn.opt_revision_kind.head )
1462        if rev_string.lower() == 'working':
1463            return pysvn.Revision( pysvn.opt_revision_kind.working )
1464        if rev_string.lower() == 'committed':
1465            return pysvn.Revision( pysvn.opt_revision_kind.committed )
1466        if rev_string.lower() == 'prev':
1467            return pysvn.Revision( pysvn.opt_revision_kind.prev )
1468        if rev_string.lower() == 'unspecified':
1469            return pysvn.Revision( pysvn.opt_revision_kind.unspecified )
1470        if rev_string[0] == '{' and rev_string[-1] == '}':
1471            try:
1472                date = parse_datetime.parse_time( rev_string[1:-2] )
1473                return pysvn.Revision( pysvn.opt_revision_kind.date, date )
1474            except parse_datetime.DateTimeSyntaxError as e:
1475                raise CommandError( e.reason() )
1476        # either a rev number or a date
1477        try:
1478            return pysvn.Revision( pysvn.opt_revision_kind.number, int(rev_string) )
1479        except ValueError:
1480            pass
1481        raise CommandError( 'Cannot parse %s as a revision value' % rev_string )
1482
1483
1484    def _splitRevisionString( self, rev_string ):
1485        # split the string at the first : that is not inside a {} pair
1486        if rev_string[0] == '{':
1487            # the : may be after the closing }
1488            close_paren_index = rev_string.find( '}' )
1489            if close_paren_index == -1:
1490                # error leave to others to report
1491                return [rev_string]
1492
1493            if close_paren_index == len(rev_string ):
1494                # its just one revision
1495                return [rev_string]
1496
1497            if rev_string[close_paren_index+1] == ':':
1498                return [rev_string[:close_paren_index+1], rev_string[close_paren_index+2:]]
1499
1500            # another error case
1501            return [rev_string]
1502        else:
1503            return rev_string.split(':',1)
1504
1505    def getCommandName( self, default_command ):
1506        if len(self.positional_args) > 0:
1507            return self.positional_args.pop( 0 )
1508        else:
1509            return default_command
1510
1511    def haveOption( self, opt_name ):
1512        return opt_name in self.named_options
1513
1514    def getBooleanOption( self, opt_name, present_value=True ):
1515        if opt_name in self.named_options:
1516            self.used_named_options[ opt_name ] = None
1517            return present_value
1518        else:
1519            return not present_value
1520
1521    def getOptionalValue( self, opt_name, default ):
1522        if opt_name in self.named_options:
1523            self.used_named_options[ opt_name ] = None
1524            return self.named_options[ opt_name ]
1525        else:
1526            return default
1527
1528    def getOptionalRevision( self, opt_name, start_default ):
1529        if opt_name in self.named_options:
1530            self.used_named_options[ opt_name ] = None
1531            rev_string = self.named_options[ opt_name ]
1532
1533            return self._parseRevisionArg( rev_string )
1534        else:
1535            return self._parseRevisionArg( start_default )
1536
1537    def getMandatoryRevisionPair( self, opt_name ):
1538        # parse a M:N or M as revision pair
1539        if opt_name not in self.named_options:
1540            raise CommandError( 'mandatory %s required' % opt_name )
1541
1542        self.used_named_options[ opt_name ] = None
1543
1544        rev_strings = self._splitRevisionString( self.named_options[ opt_name ] )
1545        if len(rev_strings) == 1:
1546            raise CommandError( 'mandatory %s requires a pair of revisions' % opt_name )
1547
1548        return [self._parseRevisionArg( rev_strings[0] ),
1549            self._parseRevisionArg( rev_strings[1] )]
1550
1551    def getOptionalRevisionPair( self, opt_name, start_default, end_default=None ):
1552        # parse a M:N or M as revision pair
1553        if opt_name in self.named_options:
1554            self.used_named_options[ opt_name ] = None
1555            rev_strings = self._splitRevisionString( self.named_options[ opt_name ] )
1556            if len(rev_strings) == 1:
1557                if end_default is None:
1558                    # M means M:M
1559                    rev_strings.append( rev_strings[0] )
1560                else:
1561                    # M means M:end_default
1562                    rev_strings.append( end_default )
1563
1564            return [self._parseRevisionArg( rev_strings[0] ),
1565                self._parseRevisionArg( rev_strings[1] )]
1566        else:
1567            return (self._parseRevisionArg( start_default ),
1568                self._parseRevisionArg( end_default ))
1569
1570    def getPositionalArgs( self, min_args, max_args=0 ):
1571        # check min and max then return the list
1572        if len(self.positional_args) < min_args:
1573            raise CommandError( 'too few arguments - need at least %d' % min_args )
1574        if max_args != 0 and len(self.positional_args) > max_args:
1575            raise CommandError( 'too many arguments - need no more then %d' % max_args )
1576
1577        # as this is the last call on the args object we check the option where all used
1578        self._checkOptionsUsed()
1579
1580        return self.positional_args
1581
1582if __name__ == '__main__':
1583    sys.exit( main( sys.argv ) )
1584