/
commands.py
1617 lines (1377 loc) · 59 KB
/
commands.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Base classes for commands.
Version Added:
5.0
"""
from __future__ import annotations
import argparse
import inspect
import io
import logging
import os
import platform
import subprocess
import sys
from typing import Dict, List, Optional, TYPE_CHECKING, TextIO, Type, Union
import colorama
from six.moves.urllib.parse import urlparse
from rbtools import get_version_string
from rbtools.api.capabilities import Capabilities
from rbtools.api.client import RBClient
from rbtools.api.errors import APIError, ServerInterfaceError
from rbtools.api.resource import RootResource
from rbtools.api.transport.sync import SyncTransport
from rbtools.clients import scan_usable_client
from rbtools.clients.errors import OptionsCheckError
from rbtools.commands.base.errors import (CommandError,
CommandExit,
ParseError)
from rbtools.commands.base.options import Option, OptionGroup
from rbtools.commands.base.output import JSONOutput, OutputWrapper
from rbtools.config import RBToolsConfig, load_config
from rbtools.deprecation import RemovedInRBTools40Warning
from rbtools.diffs.tools.errors import MissingDiffToolError
from rbtools.utils.console import get_input, get_pass
from rbtools.utils.filesystem import cleanup_tempfiles, get_home_path
from rbtools.utils.repository import get_repository_resource
if TYPE_CHECKING:
from rbtools.api.resource import Resource
from rbtools.api.transport import Transport
from rbtools.clients.base.repository import RepositoryInfo
from rbtools.clients.base.scmclient import BaseSCMClient
RB_MAIN = 'rbt'
class LogLevelFilter(logging.Filter):
"""Filters log messages of a given level.
Only log messages that have the specified level will be allowed by
this filter. This prevents propagation of higher level types to lower
log handlers.
"""
def __init__(self, level):
self.level = level
def filter(self, record):
return record.levelno == self.level
class SmartHelpFormatter(argparse.HelpFormatter):
"""Smartly formats help text, preserving paragraphs.
Version Changed:
5.0:
This moved from :py:mod:`rbtools.commands` to
:py:mod:`rbtools.commands.base.commands`.
"""
def _split_lines(self, text, width):
# NOTE: This function depends on overriding _split_lines's behavior.
# It is clearly documented that this function should not be
# considered public API. However, given that the width we need
# is calculated by HelpFormatter, and HelpFormatter has no
# blessed public API, we have no other choice but to override
# it here.
lines = []
for line in text.splitlines():
lines += super(SmartHelpFormatter, self)._split_lines(line, width)
lines.append('')
return lines[:-1]
class BaseCommand:
"""Base class for RBTools commands.
This class will handle retrieving the configuration, and parsing
command line options.
``usage`` is a list of usage strings each showing a use case. These
should not include the main rbt command or the command name; they
will be added automatically.
Version Changed:
5.0:
This moved from :py:mod:`rbtools.commands` to
:py:mod:`rbtools.commands.base.commands`.
"""
#: The name of the command.
#:
#: Type:
#: str
name: str = ''
#: The author of the command.
#:
#: Type:
#: str
author: str = ''
#: A short description of the command, suitable for display in usage text.
#:
#: Type:
#: str
description: str = ''
#: Whether the command needs the API client.
#:
#: If this is set, the initialization of the command will set
#: :py:attr:`api_client` and :py:attr:`api_root`.
#:
#: Version Added:
#: 3.0
#:
#: Type:
#: bool
needs_api: bool = False
#: Whether the command needs to generate diffs.
#:
#: If this is set, the initialization of the command will check for the
#: presence of a diff tool compatible with the chosen type of repository.
#:
#: This depends on :py:attr:`needs_repository` and
#: :py:attr:`needs_scm_client` both being set to ``True``.
#:
#: Version Added:
#: 4.0
#:
#: Type:
#: bool
needs_diffs: bool = False
#: Whether the command needs the SCM client.
#:
#: If this is set, the initialization of the command will set
#: :py:attr:`repository_info` and :py:attr:`tool`.
#:
#: Version Added:
#: 3.0
#:
#: Type:
#: bool
needs_scm_client: bool = False
#: Whether the command needs the remote repository object.
#:
#: If this is set, the initialization of the command will set
#: :py:attr:`repository`.
#:
#: Setting this will imply setting both :py:attr:`needs_api` and
#: :py:attr:`needs_scm_client` to ``True``.
#:
#: Version Added:
#: 3.0
#:
#: Type:
#: bool
needs_repository: bool = False
#: Usage text for what arguments the command takes.
#:
#: Arguments for the command are anything passed in other than defined
#: options (for example, revisions passed to :command:`rbt post`).
#:
#: Type:
#: str
args: str = ''
#: Command-line options for this command.
#:
#: Type:
#: list of Option or OptionGroup
option_list: List[Union[Option, OptionGroup]] = []
######################
# Instance variables #
######################
#: The client used to connect to the API.
#:
#: This will be set when the command is run if :py:attr:`needs_api` is
#: ``True``. Otherwise it will be ``None``.
api_client: Optional[RBClient]
#: The root of the API tree.
#:
#: This will be set when the command is run if :py:attr:`needs_api` is
#: ``True``. Otherwise it will be ``None``.
api_root: Optional[RootResource]
#: Capabilities set by the API.
#:
#: This will be set when the command is run if :py:attr:`needs_api` is
#: ``True``. Otherwise it will be ``None``.
capabilities: Optional[Capabilities]
#: The loaded configuration for RBTools.
#:
#: Version Changed:
#: 5.0:
#: This is now a :py:class:`~rbtools.config.config.RBToolsConfig`
#: instance, instead of a plain dictionary.
config: RBToolsConfig
#: An output buffer for JSON results.
#:
#: Commands can set this to return data used when a command is passed
#: :option:`--json`.
json: JSONOutput
#: A logger for the command.
log: logging.Logger
#: Options parsed for the command.
options: argparse.Namespace
#: The resource for the matching repository.
#:
#: This will be set when the command is run if both :py:attr:`needs_api`
#: and :py:attr:`needs_repository` are ``True``.
repository: Optional[Resource]
#: Information on the local repository.
#:
#: This will be set when the command is run if :py:attr:`needs_scm_client`
#: is run. Otherwise it will be ``None``.
repository_info: Optional[RepositoryInfo]
#: The URL to the Review Board server.
#:
#: This will be set when the command is run if :py:attr:`needs_api` is
#: ``True``.
server_url: Optional[str]
#: The stream for writing error output as Unicode strings.
#:
#: Commands should write error text using this instead of :py:func:`print`
#: or :py:func:`sys.stderr`.
stderr: OutputWrapper
#: The stream for writing error output as byte strings.
#:
#: Commands should write error text using this instead of :py:func:`print`
#: or :py:func:`sys.stderr`.
stderr_bytes: OutputWrapper
#: Whether the stderr stream is from an interactive session.
#:
#: This applies to :py:attr:`stderr`.
#:
#: Version Added:
#: 3.1
stderr_is_atty: bool
#: The stream for reading standard input.
#:
#: Commands should read input from here instead of using
#: :py:func:`sys.stdin`.
#:
#: Version Added:
#: 3.1
stdin: TextIO
#: Whether the stdin stream is from an interactive session.
#:
#: This applies to :py:attr:`stdin`.
#:
#: Version Added:
#: 3.1
stdin_is_atty: bool
#: The stream for writing standard output as Unicode strings.
#:
#: Commands should write text using this instead of :py:func:`print` or
#: :py:func:`sys.stdout`.
stdout: OutputWrapper
#: The stream for writing standard output as byte strings.
#:
#: Commands should write text using this instead of :py:func:`print` or
#: :py:func:`sys.stdout`.
stdout_bytes: OutputWrapper
#: Whether the stdout stream is from an interactive session.
#:
#: This applies to :py:attr:`stdout`.
#:
#: Version Added:
#: 3.1
stdout_is_atty: bool
#: The client/tool used to communicate with the repository.
#:
#: This will be set when the command is run if :py:attr:`needs_scm_client`
#: is run. Otherwise it will be ``None``.
tool: Optional[BaseSCMClient]
#: The transport class used for talking to the API.
transport_cls: Type[Transport]
_global_options: List[Option] = [
Option('-d', '--debug',
action='store_true',
dest='debug',
config_key='DEBUG',
default=False,
help='Displays debug output.',
extended_help='This information can be valuable when debugging '
'problems running the command.'),
Option('--json',
action='store_true',
dest='json_output',
config_key='JSON_OUTPUT',
default=False,
added_in='3.0',
help='Output results as JSON data instead of text.')
]
server_options = OptionGroup(
name='Review Board Server Options',
description='Options necessary to communicate and authenticate '
'with a Review Board server.',
option_list=[
Option('--server',
dest='server',
metavar='URL',
config_key='REVIEWBOARD_URL',
default=None,
help='Specifies the Review Board server to use.'),
Option('--username',
dest='username',
metavar='USERNAME',
config_key='USERNAME',
default=None,
help='The user name to be supplied to the Review Board '
'server.'),
Option('--password',
dest='password',
metavar='PASSWORD',
config_key='PASSWORD',
default=None,
help='The password to be supplied to the Review Board '
'server.'),
Option('--ext-auth-cookies',
dest='ext_auth_cookies',
metavar='EXT_AUTH_COOKIES',
config_key='EXT_AUTH_COOKIES',
default=None,
help='Use an external cookie store with pre-fetched '
'authentication data. This is useful with servers '
'that require extra web authentication to access '
'Review Board, e.g. on single sign-on enabled sites.',
added_in='0.7.5'),
Option('--api-token',
dest='api_token',
metavar='TOKEN',
config_key='API_TOKEN',
default=None,
help='The API token to use for authentication, instead of '
'using a username and password.',
added_in='0.7'),
Option('--disable-proxy',
action='store_false',
dest='enable_proxy',
config_key='ENABLE_PROXY',
default=True,
help='Prevents requests from going through a proxy '
'server.'),
Option('--disable-ssl-verification',
action='store_true',
dest='disable_ssl_verification',
config_key='DISABLE_SSL_VERIFICATION',
default=False,
help='Disable SSL certificate verification. This is useful '
'with servers that have self-signed certificates.',
added_in='0.7.3'),
Option('--disable-cookie-storage',
config_key='SAVE_COOKIES',
dest='save_cookies',
action='store_false',
default=True,
help='Use an in-memory cookie store instead of writing '
'them to a file. No credentials will be saved or '
'loaded.',
added_in='0.7.3'),
Option('--disable-cache',
dest='disable_cache',
config_key='DISABLE_CACHE',
action='store_true',
default=False,
help='Disable the HTTP cache completely. This will '
'result in slower requests.',
added_in='0.7.3'),
Option('--disable-cache-storage',
dest='in_memory_cache',
config_key='IN_MEMORY_CACHE',
action='store_true',
default=False,
help='Disable storing the API cache on the filesystem, '
'instead keeping it in memory temporarily.',
added_in='0.7.3'),
Option('--cache-location',
dest='cache_location',
metavar='FILE',
config_key='CACHE_LOCATION',
default=None,
help='The file to use for the API cache database.',
added_in='0.7.3'),
Option('--ca-certs',
dest='ca_certs',
metavar='FILE',
config_key='CA_CERTS',
default=None,
help='Additional TLS CA bundle.'),
Option('--client-key',
dest='client_key',
metavar='FILE',
config_key='CLIENT_KEY',
default=None,
help='Key for TLS client authentication.'),
Option('--client-cert',
dest='client_cert',
metavar='FILE',
config_key='CLIENT_CERT',
default=None,
help='Certificate for TLS client authentication.'),
Option('--proxy-authorization',
dest='proxy_authorization',
metavar='PROXY_AUTHORIZATION',
config_key='PROXY_AUTHORIZATION',
default=None,
help='Value of the Proxy-Authorization header to send with '
'HTTP requests.'),
]
)
repository_options = OptionGroup(
name='Repository Options',
option_list=[
Option('--repository',
dest='repository_name',
metavar='NAME',
config_key='REPOSITORY',
default=None,
help='The name of the repository configured on '
'Review Board that matches the local repository.'),
Option('--repository-url',
dest='repository_url',
metavar='URL',
config_key='REPOSITORY_URL',
default=None,
help='The URL for a repository.'
'\n'
'When generating diffs, this can be used for '
'creating a diff outside of a working copy '
'(currently only supported by Subversion with '
'specific revisions or --diff-filename, and by '
'ClearCase with relative paths outside the view).'
'\n'
'For Git, this specifies the origin URL of the '
'current repository, overriding the origin URL '
'supplied by the client.',
versions_changed={
'0.6': 'Prior versions used the `REPOSITORY` setting '
'in .reviewboardrc, and allowed a '
'repository name to be passed to '
'--repository-url. This is no '
'longer supported in 0.6 and higher. You '
'may need to update your configuration and '
'scripts appropriately.',
}),
Option('--repository-type',
dest='repository_type',
metavar='TYPE',
config_key='REPOSITORY_TYPE',
default=None,
help='The type of repository in the current directory. '
'In most cases this should be detected '
'automatically, but some directory structures '
'containing multiple repositories require this '
'option to select the proper type. The '
'`rbt list-repo-types` command can be used to '
'list the supported values.'),
]
)
diff_options = OptionGroup(
name='Diff Generation Options',
description='Options for choosing what gets included in a diff, '
'and how the diff is generated.',
option_list=[
Option('--no-renames',
dest='no_renames',
action='store_true',
help='Add the --no-renames option to the git when '
'generating diff.'
'\n'
'Supported by: Git',
added_in='0.7.11'),
Option('--revision-range',
dest='revision_range',
metavar='REV1:REV2',
default=None,
help='Generates a diff for the given revision range.',
deprecated_in='0.6'),
Option('-I', '--include',
metavar='FILENAME',
dest='include_files',
action='append',
help='Includes only the specified file in the diff. '
'This can be used multiple times to specify '
'multiple files.'
'\n'
'Supported by: Bazaar, CVS, Git, Mercurial, '
'Perforce, SOS, and Subversion.',
added_in='0.6'),
Option('-X', '--exclude',
metavar='PATTERN',
dest='exclude_patterns',
action='append',
config_key='EXCLUDE_PATTERNS',
help='Excludes all files that match the given pattern '
'from the diff. This can be used multiple times to '
'specify multiple patterns. UNIX glob syntax is used '
'for pattern matching.'
'\n'
'Supported by: Bazaar, CVS, Git, Mercurial, '
'Perforce, SOS, and Subversion.',
extended_help=(
'Patterns that begin with a path separator (/ on Mac '
'OS and Linux, \\ on Windows) will be treated as being '
'relative to the root of the repository. All other '
'patterns are treated as being relative to the current '
'working directory.'
'\n'
'For example, to exclude all ".txt" files from the '
'resulting diff, you would use "-X /\'*.txt\'".'
'\n'
'When working with Mercurial, the patterns are '
'provided directly to "hg" and are not limited to '
'globs. For more information on advanced pattern '
'syntax in Mercurial, run "hg help patterns"'
'\n'
'When working with CVS all diffs are generated '
'relative to the current working directory so '
'patterns beginning with a path separator are treated '
'as relative to the current working directory.'
'\n'
'When working with Perforce, an exclude pattern '
'beginning with `//` will be matched against depot '
'paths; all other patterns will be matched against '
'local paths.'),
added_in='0.7'),
Option('--parent',
dest='parent_branch',
metavar='BRANCH',
config_key='PARENT_BRANCH',
default=None,
help='The parent branch this diff should be generated '
'against (Bazaar/Git/Mercurial only).'),
Option('--diff-filename',
dest='diff_filename',
default=None,
metavar='FILENAME',
help='Uploads an existing diff file, instead of '
'generating a new diff.'),
]
)
branch_options = OptionGroup(
name='Branch Options',
description='Options for selecting branches.',
option_list=[
Option('--tracking-branch',
dest='tracking',
metavar='BRANCH',
config_key='TRACKING_BRANCH',
default=None,
help='The remote tracking branch from which your local '
'branch is derived (Git/Mercurial only).'
'\n'
'For Git, the default is to use the remote branch '
'that the local branch is tracking, if any, falling '
'back on `origin/master`.'
'\n'
'For Mercurial, the default is one of: '
'`reviewboard`, `origin`, `parent`, or `default`.'),
]
)
git_options = OptionGroup(
name='Git Options',
description='Git-specific options for diff generation.',
option_list=[
Option('--git-find-renames-threshold',
dest='git_find_renames_threshold',
metavar='THRESHOLD',
default=None,
help='The threshold to pass to `--find-renames` when '
'generating a git diff.'
'\n'
'For more information, see `git help diff`.'),
])
perforce_options = OptionGroup(
name='Perforce Options',
description='Perforce-specific options for selecting the '
'Perforce client and communicating with the '
'repository.',
option_list=[
Option('--p4-client',
dest='p4_client',
config_key='P4_CLIENT',
default=None,
metavar='CLIENT_NAME',
help='The Perforce client name for the repository.'),
Option('--p4-port',
dest='p4_port',
config_key='P4_PORT',
default=None,
metavar='PORT',
help='The IP address for the Perforce server.'),
Option('--p4-passwd',
dest='p4_passwd',
config_key='P4_PASSWD',
default=None,
metavar='PASSWORD',
help='The Perforce password or ticket of the user '
'in the P4USER environment variable.'),
]
)
subversion_options = OptionGroup(
name='Subversion Options',
description='Subversion-specific options for controlling diff '
'generation.',
option_list=[
Option('--basedir',
dest='basedir',
config_key='BASEDIR',
default=None,
metavar='PATH',
help='The path within the repository where the diff '
'was generated. This overrides the detected path. '
'Often used when passing --diff-filename.'),
Option('--svn-username',
dest='svn_username',
default=None,
metavar='USERNAME',
help='The username for the SVN repository.'),
Option('--svn-password',
dest='svn_password',
default=None,
metavar='PASSWORD',
help='The password for the SVN repository.'),
Option('--svn-prompt-password',
dest='svn_prompt_password',
config_key='SVN_PROMPT_PASSWORD',
default=False,
action='store_true',
help="Prompt for the user's svn password. This option "
"overrides the password provided by the "
"--svn-password option.",
added_in='0.7.3'),
Option('--svn-show-copies-as-adds',
dest='svn_show_copies_as_adds',
metavar='y|n',
default=None,
help='Treat copied or moved files as new files.'
'\n'
'This is only supported in Subversion 1.7+.',
added_in='0.5.2'),
Option('--svn-changelist',
dest='svn_changelist',
default=None,
metavar='ID',
help='Generates the diff for review based on a '
'local changelist.',
deprecated_in='0.6'),
]
)
tfs_options = OptionGroup(
name='TFS Options',
description='Team Foundation Server specific options for '
'communicating with the TFS server.',
option_list=[
Option('--tfs-login',
dest='tfs_login',
default=None,
metavar='TFS_LOGIN',
help='Logs in to TFS as a specific user (ie.'
'user@domain,password). Visit https://msdn.microsoft.'
'com/en-us/library/hh190725.aspx to learn about '
'saving credentials for reuse.'),
Option('--tf-cmd',
dest='tf_cmd',
default=None,
metavar='TF_CMD',
config_key='TF_CMD',
help='The full path of where to find the tf command. This '
'overrides any detected path.'),
Option('--tfs-shelveset-owner',
dest='tfs_shelveset_owner',
default=None,
metavar='TFS_SHELVESET_OWNER',
help='When posting a shelveset name created by another '
'user (other than the one who owns the current '
'workdir), look for that shelveset using this '
'username.'),
]
)
default_transport_cls = SyncTransport
def __init__(self,
transport_cls=SyncTransport,
stdout=sys.stdout,
stderr=sys.stderr,
stdin=sys.stdin):
"""Initialize the base functionality for the command.
Args:
transport_cls (rbtools.api.transport.Transport, optional):
The transport class used for all API communication. By default,
this uses the transport defined in
:py:attr:`default_transport_cls`.
stdout (io.TextIOWrapper, optional):
The standard output stream. This can be used to capture output
programmatically.
Version Added:
3.1
stderr (io.TextIOWrapper, optional):
The standard error stream. This can be used to capture errors
programmatically.
Version Added:
3.1
stdin (io.TextIOWrapper, optional):
The standard input stream. This can be used to provide input
programmatically.
Version Added:
3.1
"""
self.log = logging.getLogger('rb.%s' % self.name)
self.transport_cls = transport_cls or self.default_transport_cls
self.api_client = None
self.api_root = None
self.capabilities = None
self.repository = None
self.repository_info = None
self.server_url = None
self.tool = None
self.config = RBToolsConfig()
self.stdout = OutputWrapper(stdout)
self.stderr = OutputWrapper(stderr)
self.stdin = stdin
self.stdout_is_atty = hasattr(stdout, 'isatty') and stdout.isatty()
self.stderr_is_atty = hasattr(stderr, 'isatty') and stderr.isatty()
self.stdin_is_atty = hasattr(stdin, 'isatty') and stdin.isatty()
if isinstance(stdout, io.TextIOWrapper):
self.stderr_bytes = OutputWrapper(stderr.buffer)
self.stdout_bytes = OutputWrapper(stdout.buffer)
else:
# Python 2.x, or while we're running unit tests (where stdout and
# stderr are actually io.StringIO)
self.stderr_bytes = OutputWrapper(stderr)
self.stdout_bytes = OutputWrapper(stdout)
self.json = JSONOutput(stdout)
def create_parser(self, config, argv=[]):
"""Create and return the argument parser for this command."""
parser = argparse.ArgumentParser(
prog=RB_MAIN,
usage=self.usage(),
formatter_class=SmartHelpFormatter)
for option in self.option_list:
option.add_to(parser, config, argv)
for option in self._global_options:
option.add_to(parser, config, argv)
return parser
def post_process_options(self):
if self.options.disable_ssl_verification:
try:
import ssl
ssl._create_unverified_context()
except Exception:
raise CommandError('The --disable-ssl-verification flag is '
'only available with Python 2.7.9+')
def usage(self):
"""Return a usage string for the command."""
usage = '%%(prog)s %s [options] %s' % (self.name, self.args)
if self.description:
return '%s\n\n%s' % (usage, self.description)
else:
return usage
def _create_formatter(self, level, fmt):
"""Create a logging formatter for the appropriate logging level.
When writing to a TTY, the format will be colorized by the colors
specified in the ``COLORS`` configuration in :file:`.reviewboardrc`.
Otherwise, the format will not be altered.
Args:
level (unicode):
The logging level name.
fmt (unicode):
The logging format.
Returns:
logging.Formatter:
The created formatter.
"""
color = ''
reset = ''
if self.stdout_is_atty:
color_name = self.config['COLOR'].get(level.upper())
if color_name:
color = getattr(colorama.Fore, color_name.upper(), '')
if color:
reset = colorama.Fore.RESET
return logging.Formatter(fmt.format(color=color, reset=reset))
def initialize(self):
"""Initialize the command.
This will set up various prerequisites for commands. Individual command
subclasses can control what gets done by setting the various
``needs_*`` attributes (as documented in this class).
"""
if self.needs_repository:
# If we need the repository, we implicitly need the API and SCM
# client as well.
self.needs_api = True
self.needs_scm_client = True
if self.needs_api:
self.server_url = self._init_server_url()
self.api_client, self.api_root = self.get_api(self.server_url)
self.capabilities = self.get_capabilities(self.api_root)
if self.needs_scm_client:
# _init_server_url might have already done this, in the case that
# it needed to use the SCM client to detect the server name. Only
# repeat if necessary.
if self.repository_info is None and self.tool is None:
self.repository_info, self.tool = self.initialize_scm_tool(
client_name=self.options.repository_type)
# Some SCMs allow configuring the repository name in the SCM
# metadata. This is a legacy configuration, and is only used as a
# fallback for when the repository name is not specified through
# the config or command line.
if self.options.repository_name is None:
self.options.repository_name = \
self.tool.get_repository_name()
self.tool.capabilities = self.capabilities
if self.needs_repository:
self.repository, info = get_repository_resource(
api_root=self.api_root,
tool=self.tool,
repository_name=self.options.repository_name,
repository_paths=self.repository_info.path)
if self.repository:
self.repository_info.update_from_remote(self.repository, info)
if self.options.json_output:
self.stdout.output_stream = None
self.stderr.output_stream = None
self.stderr_bytes.output_stream = None
self.stdout_bytes.output_stream = None
def create_arg_parser(self, argv):
"""Create and return the argument parser.
Args:
argv (list of unicode):
A list of command line arguments
Returns:
argparse.ArgumentParser:
Argument parser for commandline arguments
"""
self.config = load_config()
parser = self.create_parser(self.config, argv)
parser.add_argument('args', nargs=argparse.REMAINDER)
return parser
def run_from_argv(self, argv):
"""Execute the command using the provided arguments.
The options and commandline arguments will be parsed
from ``argv`` and the commands ``main`` method will
be called.
"""
parser = self.create_arg_parser(argv)
self.options = parser.parse_args(argv[2:])
args = self.options.args
# Check that the proper number of arguments have been provided.
if hasattr(inspect, 'getfullargspec'):
# Python 3
argspec = inspect.getfullargspec(self.main)
else:
# Python 2
argspec = inspect.getargspec(self.main)
minargs = len(argspec.args) - 1
maxargs = minargs
# Arguments that have a default value are considered optional.
if argspec.defaults is not None:
minargs -= len(argspec.defaults)
if argspec.varargs is not None:
maxargs = None
if len(args) < minargs or (maxargs is not None and
len(args) > maxargs):
parser.error('Invalid number of arguments provided')
sys.exit(1)
try:
self._init_logging()
logging.debug('Command line: %s', subprocess.list2cmdline(argv))
self.initialize()
exit_code = self.main(*args) or 0
except CommandError as e:
if isinstance(e, ParseError):
parser.error(e)
elif self.options.debug:
raise
logging.error(e)
self.json.add_error(str(e))
exit_code = 1
except CommandExit as e:
exit_code = e.exit_code
except Exception as e:
# If debugging is on, we'll let python spit out the
# stack trace and report the exception, otherwise
# we'll suppress the trace and print the exception
# manually.
if self.options.debug:
raise
self.json.add_error('Internal error: %s: %s'
% (type(e).__name__, e))
logging.critical(e)
exit_code = 1
cleanup_tempfiles()