-
Notifications
You must be signed in to change notification settings - Fork 189
/
p4-trigger-script
executable file
·500 lines (445 loc) · 15.8 KB
/
p4-trigger-script
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
#!/usr/bin/env python3
#
# Trigger script for connecting Perforce servers to Review Board.
#
# This can check changes for approval in Review Board, auto-close review
# requests, and stamp changesets with the review request URL.
#
# Usage:
#
# $ p4-trigger-script [options] %change%
#
# Connection information and options must either be provided by editing
# this script or specifying as command line options.
#
#
# To install, run:
# $ p4 triggers
# Triggers:
# reviewboard change-submit //depot/... "/path/to/python /path/to/p4-trigger-script [options] %change%"
#
# If you're using this with the RBTools for Windows installer, specify
# the path to the bundled Python.exe:
# reviewboard change-submit //depot/... "C:\Program Files\RBTools\Python\python.exe C:\Path/To/p4-trigger-script [options] %change%"
#
#
# Required Options:
#
# --server or REVIEWBOARD_URL
# --username or REVIEWBOARD_USERNAME
# --api-token, --password, REVIEWBOARD_API_TOKEN, or REVIEWBOARD_PASSWORD
# --p4-port or P4_PORT
# --p4-user or P4_USER
#
# Additional options may also be needed for your environment.
#
#
# Choosing the Perforce user:
#
# This script may either need to operate as the submitting user, or as an
# administrative user.
#
# The correct option depends on your environment. If the Perforce client
# spec specifies "Host:", the Perforce server may not be able to stamp
# changes.
#
# To stamp as the user submitting the change, specify %user%,
# %client%, and %clienthost% as arguments:
#
# p4-trigger-script --p4-user %user%
# --p4-client %client%
# --p4-host %clienthost%
#
# You can also specify --p4-host to set an explicit $P4HOST value.
# Perforce does not provide the user's Host as a value.
#
# For variables available to trigger scripts, see:
#
# https://www.perforce.com/manuals/p4sag/Content/P4SAG/scripting.triggers.variables.html
import argparse
import logging
import os
import sys
# Configure the Python Path, if needed:
#
# sys.path.insert(0, '...')
# Configure the executable search path, if necessary.
#
# os.environ['PATH'] = '...' + os.environ['PATH']
from rbtools.api.client import RBClient
from rbtools.api.errors import APIError, ServerInterfaceError
from rbtools.clients.errors import AmendError
from rbtools.clients.perforce import PerforceClient
from rbtools.hooks.common import initialize_logging
from rbtools.utils.repository import get_repository_resource
# Whether to enable enhanced debug output.
#
# Output will be shown when submitting changes. It will be output to stderr.
DEBUG = False
# The Review Board server URL.
REVIEWBOARD_URL = ''
# The username and password to be supplied to the Review Board server. This
# user must have the "can_change_status" permission granted.
REVIEWBOARD_USERNAME = ''
REVIEWBOARD_PASSWORD = ''
REVIEWBOARD_API_TOKEN = ''
# Configure the Perforce credentials and port.
#
# You will to configure a standard user, not an operator or service user.
#
# These can also be set as command line options (--p4-user, --p4-passwd,
# --p4-port).
#
# The $P4USER and $P4PORT environment variables are used by default.
P4_USER = ''
P4_PASSWORD = ''
P4_PORT = ''
# An explicit P4 client and matching host to use for any operations.
#
# If a client is not specified, the default one for the user and system will
# be used, as determined by Perforce.
#
# If a client has a "Host:" setting, then P4_HOST should be set to match.
P4_CLIENT = None
P4_HOST = None
# If using SSL, configure a path to a pre-populated p4trust file. This can
# be generated on any system.
P4_TRUST_FILE = None
# Whether to close review requests after being submitted successfully.
CLOSE_REVIEW_REQUESTS = True
# Whether to require the changeset to have a review request.
REQUIRE_REVIEW_REQUESTS = True
# Whether to decline changes that aren't approved.
REQUIRE_APPROVAL = True
# Whether to stamp the change with the review request URL.
STAMP_CHANGES = True
# The prefix for the review request field being added to the description.
REVIEW_REQUEST_URL_FIELD = 'Reviewed at:'
def main():
arg_parser = argparse.ArgumentParser()
# Debugging options.
group = arg_parser.add_argument_group('Debugging Options')
group.add_argument(
'--debug',
action='store_true',
default=DEBUG,
help='Enable debug output.')
group.add_argument(
'--no-debug',
action='store_false',
dest='debug',
default=DEBUG,
help='Disable debug output.')
# Review Board connection options.
group = arg_parser.add_argument_group('Review Board Options')
group.add_argument(
'--server',
dest='rb_server',
default=REVIEWBOARD_URL,
help='The URL to the Review Board server.')
group.add_argument(
'--username',
dest='rb_username',
default=REVIEWBOARD_USERNAME,
help=(
'The Review Board user used to verify and close review requests. '
'This user must have the can_change_status permission granted.'
))
group.add_argument(
'--api-token',
dest='rb_api_token',
default=REVIEWBOARD_API_TOKEN,
help='The API token for the Review Board user.')
group.add_argument(
'--password',
dest='rb_password',
default=REVIEWBOARD_PASSWORD,
help=(
'The password for the Review Board user, if not using API tokens.'
))
group.add_argument(
'--disable-ssl-verification',
dest='disable_ssl_verification',
action='store_true',
default=False,
help='Disable SSL certificate verification.')
# Perforce connection options.
group = arg_parser.add_argument_group('Perforce Options')
group.add_argument(
'--p4-user',
dest='p4_user',
default=P4_USER,
help=(
"The Perforce user used to stamp changes. Pass `%%user%%` to use "
"the submitting user's username."
))
group.add_argument(
'--p4-passwd',
dest='p4_passwd',
default=P4_PASSWORD,
help='The password used for the user.')
group.add_argument(
'--p4-client',
dest='p4_client',
default=P4_CLIENT,
help=(
"The Perforce client used to stamp changes. Pass `%%client%%` to "
"use the submitting user's client."
))
group.add_argument(
'--p4-host',
dest='p4_host',
default=P4_HOST,
help=(
"The Perforce host matching the client. Pass `%%clienthost%%` to "
"use the submitting user's host."
))
group.add_argument(
'--p4-port',
dest='p4_port',
default=P4_PORT or os.environ.get('P4PORT'),
help='The Perforce server to connect to.')
group.add_argument(
'--p4-trust-file',
dest='p4_trust_file',
default=P4_TRUST_FILE,
help='A Perforce trust file used for verifying server connections.')
# Trigger action options.
group = arg_parser.add_argument_group('Trigger Actions')
group.add_argument(
'--close-review-requests',
action='store_true',
dest='close_review_requests',
default=CLOSE_REVIEW_REQUESTS,
help=(
'Close review requests once submitted. Overrides the default '
'in the trigger script.'
))
group.add_argument(
'--no-close-review-requests',
action='store_false',
dest='close_review_requests',
help=(
"Leave review requests open once submitted. Overrides the "
"default in the trigger script."
))
group.add_argument(
'--require-review-requests',
action='store_true',
dest='require_review_requests',
default=REQUIRE_REVIEW_REQUESTS,
help=(
'Require a matching review request. Overrides the default in '
'the trigger script.'
))
group.add_argument(
'--no-require-review-requests',
action='store_false',
dest='require_review_requests',
help=(
"Don't requiring a matching review request. Overrides the "
"default in the trigger script."
))
group.add_argument(
'--require-approval',
action='store_true',
dest='require_approval',
default=REQUIRE_APPROVAL,
help=(
'Require approval on the review request. Overrides the default '
'in the trigger script.'
))
group.add_argument(
'--no-require-approval',
action='store_false',
dest='require_approval',
help=(
"Don't require approval on the review request. Overrides the "
"default in the trigger script."
))
group.add_argument(
'--stamp',
action='store_true',
dest='stamp',
default=STAMP_CHANGES,
help=(
'Stamp the review request URL onto a change description. '
'Overrides the default in the trigger script.'
))
group.add_argument(
'--no-stamp',
action='store_false',
dest='stamp',
help=(
"Don't stamp the review request URL onto a change description. "
"Overrides the default in the trigger script."
))
# Positional arguments.
arg_parser.add_argument(
'changenum',
type=int,
nargs=1,
help='The submitted change number.')
# Parse the options and validate them.
options = arg_parser.parse_args()
print(options)
p4_client = options.p4_client
p4_host = options.p4_host
p4_passwd = options.p4_passwd
p4_port = options.p4_port
p4_trust_file = options.p4_trust_file
p4_user = options.p4_user
rb_api_token = options.rb_api_token
rb_password = options.rb_password
rb_url = options.rb_server
rb_username = options.rb_username
if not rb_url:
sys.stderr.write('--server is required.\n')
sys.exit(1)
if not rb_username:
sys.stderr.write('--username is required.\n')
sys.exit(1)
if not rb_api_token and not rb_password:
sys.stderr.write('--api-token or --password is required.\n')
sys.exit(1)
if not p4_port:
sys.stderr.write('--p4-port or $P4PORT is required.\n')
sys.exit(1)
if not p4_user:
sys.stderr.write('--p4-user or $P4USER is required.\n')
sys.exit(1)
# Set up logging.
initialize_logging(debug=options.debug)
# Set up the Perforce environment.
os.environ.update({
'P4USER': p4_user,
'P4PORT': p4_port,
})
if p4_client:
os.environ['P4CLIENT'] = p4_client
if p4_passwd:
os.environ['P4PASSWD'] = p4_passwd
if p4_host:
os.environ['P4HOST'] = p4_host
if p4_trust_file:
os.environ['P4TRUST'] = p4_trust_file
# Get the changeset from Perforce.
changenum = options.changenum[0]
assert isinstance(changenum, int)
client = PerforceClient()
changes = client.p4.change(changenum)
# Connect to Review Board.
api_client = RBClient(url=rb_url,
username=rb_username,
password=rb_password,
api_token=rb_api_token,
in_memory_cache=True,
verify_ssl=not options.disable_ssl_verification)
try:
api_root = api_client.get_root()
except ServerInterfaceError as e:
sys.stderr.write('Could not reach the Review Board server at %s: %s\n'
% (rb_url, e))
sys.exit(1)
except APIError as e:
sys.stderr.write('Unexpected API error when talking to Review Board '
'at %s: %s'
% (rb_url, e))
sys.exit(1)
# Find the review request for this changenum.
review_request = None
if changes and len(changes) == 1:
# Look up the repository for this server.
try:
repository = get_repository_resource(
api_root=api_root,
repository_paths=[p4_port])[0]
except APIError as e:
# 100 = Does Not Exist.
if e.error_code == 100:
repository = None
else:
sys.stderr.write(
'Error looking up a repository in Review Board for '
'%s: %s\n'
% (p4_port, e))
sys.exit(1)
if repository is None:
sys.stderr.write(
'Could not find a repository in Review Board for %s.\n'
% p4_port)
sys.exit(1)
# Look up a review request for this changenum.
try:
review_requests = api_root.get_review_requests(
commit_id=changenum,
repository=repository.id,
only_fields='approved,approval_failure,id')
except APIError as e:
sys.stderr.write(
'Unexpected error looking up a matching review request: %s\n'
% e)
sys.exit(1)
if len(review_requests) == 1:
review_request = review_requests[0]
elif len(review_requests) >= 1:
sys.stderr.write(
'More than one review request was found for changenum %s and '
'P4PORT %s. This may be an internal error. Please contact '
'your Perforce administrator.\n'
% (changenum, p4_port))
sys.exit(1)
if review_request:
# A review request ID was found.
#
# We'll now stamp changes if the script is configured to enable this,
# whether or not the change is approved.
if options.stamp:
# Update the changeset if needed in order to include the review
# request URL.
assert changes is not None
change_description = changes[0]['Description']
stamp_str = '%s %s' % (REVIEW_REQUEST_URL_FIELD,
review_request.absolute_url)
if stamp_str not in change_description:
try:
client.amend_commit_description(
'%s\n\n%s' % (change_description.rstrip(), stamp_str),
revisions=client.parse_revision_spec([str(changenum)]))
except AmendError as e:
sys.stderr.write(
'Unable to amend change %s with the review request '
'URL: %s\n'
% (changenum, e))
sys.exit(1)
if not review_request.approved:
# The review request was not approved in Review Board. Display
# an error.
if options.require_approval:
sys.stderr.write('%s\n' % review_request.approval_failure)
sys.exit(1)
else:
logging.warning(
'Change #%s was not approved, but is allowed to be '
'submitted (%s)',
changenum, review_request.approval_failure)
# If we're here, the change is allowed to go into Review Board.
if options.close_review_requests:
# Close the review request, and point to this change.
review_request.update(
status='submitted',
description='Submitted as change #%s' % changenum)
else:
# A review request ID was not found.
if options.require_review_requests:
sys.stderr.write(
'A review request for this change must be posted for review '
'and approved before it can be submitted.\n')
sys.exit(1)
else:
logging.warning(
"Change #%s hasn't been posted for review, but is allowed to "
"be submitted.",
changenum)
if __name__ == '__main__':
main()