Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(150)

Delta Between Two Patch Sets: static/upload.py

Issue 955: Allow upload with no base (Closed) SVN Base: http://rietveld.googlecode.com/svn/trunk/
Left Patch Set: I incorporated the changes as per your comments in previous patch. Created 3 months, 3 weeks ago
Right Patch Set: Created 3 months, 3 weeks ago
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
LEFTRIGHT
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # 2 #
3 # Copyright 2007 Google Inc. 3 # Copyright 2007 Google Inc.
4 # 4 #
5 # Licensed under the Apache License, Version 2.0 (the "License"); 5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License. 6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at 7 # You may obtain a copy of the License at
8 # 8 #
9 # http://www.apache.org/licenses/LICENSE-2.0 9 # http://www.apache.org/licenses/LICENSE-2.0
10 # 10 #
11 # Unless required by applicable law or agreed to in writing, software 11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS, 12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and 14 # See the License for the specific language governing permissions and
15 # limitations under the License. 15 # limitations under the License.
16 16
17 """Tool for uploading subversion diffs to the codereview app. 17 """Tool for uploading subversion diffs to the codereview app.
18 18
19 Usage summary: upload.py [options] [-- svn_diff_options] 19 Usage summary: upload.py [options] [-- svn_diff_options]
20 """ 20 """
21 # This code is derived from appcfg.py in the App Engine SDK (open source), 21 # This code is derived from appcfg.py in the App Engine SDK (open source),
22 # and from ASPN recipe #146306. 22 # and from ASPN recipe #146306.
23 23
24 import cookielib 24 import cookielib
25 import getpass 25 import getpass
26 import logging 26 import logging
27 import mimetypes 27 import mimetypes
28 import optparse 28 import optparse
29 import os 29 import os
30 import socket 30 import socket
31 import sys 31 import sys
32 import urllib 32 import urllib
33 import urllib2 33 import urllib2
34 import urlparse 34 import urlparse
35 35
36 36
37 # The logging verbosity: 37 # The logging verbosity:
38 # 0: Errors only. 38 # 0: Errors only.
39 # 1: Status messages. 39 # 1: Status messages.
40 # 2: Info logs. 40 # 2: Info logs.
41 # 3: Debug logs. 41 # 3: Debug logs.
42 verbosity = 1 42 verbosity = 1
43 43
44 44
45 def StatusUpdate(msg): 45 def StatusUpdate(msg):
46 """Print a status message to stdout. 46 """Print a status message to stdout.
47 47
48 If 'verbosity' is greater than 0, print the message. 48 If 'verbosity' is greater than 0, print the message.
49 49
50 Args: 50 Args:
(...skipping 263 matching lines...) Show 10 above Show 10 below
314 if os.path.exists(self.cookie_file): 314 if os.path.exists(self.cookie_file):
315 try: 315 try:
316 self.cookie_jar.load() 316 self.cookie_jar.load()
317 self.authenticated = True 317 self.authenticated = True
318 StatusUpdate("Loaded authentication cookies from %s" % 318 StatusUpdate("Loaded authentication cookies from %s" %
319 self.cookie_file) 319 self.cookie_file)
320 except cookielib.LoadError: 320 except cookielib.LoadError:
321 # Failed to load cookies - just ignore them. 321 # Failed to load cookies - just ignore them.
322 pass 322 pass
323 else: 323 else:
324 # Create an empty cookie file with mode 600 324 # Create an empty cookie file with mode 600
325 fd = os.open(self.cookie_file, os.O_CREAT, 0600) 325 fd = os.open(self.cookie_file, os.O_CREAT, 0600)
326 os.close(fd) 326 os.close(fd)
327 # Always chmod the cookie file 327 # Always chmod the cookie file
328 os.chmod(self.cookie_file, 0600) 328 os.chmod(self.cookie_file, 0600)
329 else: 329 else:
330 # Don't save cookies across runs of update.py. 330 # Don't save cookies across runs of update.py.
331 self.cookie_jar = cookielib.CookieJar() 331 self.cookie_jar = cookielib.CookieJar()
332 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar)) 332 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
333 return opener 333 return opener
334 334
335 335
336 parser = optparse.OptionParser(usage="%prog [options] [-- svn_diff_options]") 336 parser = optparse.OptionParser(usage="%prog [options] [-- svn_diff_options]")
337 parser.add_option("-q", "--quiet", action="store_const", const=0, 337 parser.add_option("-q", "--quiet", action="store_const", const=0,
338 dest="verbose", help="Print errors only.") 338 dest="verbose", help="Print errors only.")
339 parser.add_option("-v", "--verbose", action="store_const", const=2, 339 parser.add_option("-v", "--verbose", action="store_const", const=2,
340 dest="verbose", default=1, 340 dest="verbose", default=1,
341 help="Print info level logs.") 341 help="Print info level logs.")
342 parser.add_option("--noisy", action="store_const", const=3, 342 parser.add_option("--noisy", action="store_const", const=3,
343 dest="verbose", help="Print all logs.") 343 dest="verbose", help="Print all logs.")
344 parser.add_option("-s", "--server", action="store", dest="server", 344 parser.add_option("-s", "--server", action="store", dest="server",
345 default="codereview.appspot.com", 345 default="codereview.appspot.com",
346 metavar="SERVER", 346 metavar="SERVER",
347 help="The server to upload to. The format is host[:port].") 347 help="The server to upload to. The format is host[:port].")
348 parser.add_option("-e", "--email", action="store", dest="email", 348 parser.add_option("-e", "--email", action="store", dest="email",
349 metavar="EMAIL", default=None, 349 metavar="EMAIL", default=None,
350 help="The username to use. Will prompt if omitted.") 350 help="The username to use. Will prompt if omitted.")
351 parser.add_option("-H", "--host", action="store", dest="host", 351 parser.add_option("-H", "--host", action="store", dest="host",
352 metavar="HOST", default=None, 352 metavar="HOST", default=None,
353 help="Overrides the Host header sent with all RPCs.") 353 help="Overrides the Host header sent with all RPCs.")
354 parser.add_option("--no_cookies", action="store_false", 354 parser.add_option("--no_cookies", action="store_false",
355 dest="save_cookies", default=True, 355 dest="save_cookies", default=True,
356 help="Do not save authentication cookies to local disk.") 356 help="Do not save authentication cookies to local disk.")
357 parser.add_option("-m", "--message", action="store", dest="message", 357 parser.add_option("-m", "--message", action="store", dest="message",
358 metavar="MESSAGE", default=None, 358 metavar="MESSAGE", default=None,
359 help="A message to identify the patch. " 359 help="A message to identify the patch. "
360 "Will prompt if omitted.") 360 "Will prompt if omitted.")
361 parser.add_option("-i", "--issue", type="int", action="store", 361 parser.add_option("-i", "--issue", type="int", action="store",
362 metavar="ISSUE", default=None, 362 metavar="ISSUE", default=None,
363 help="Issue number to which to add. Defaults to new issue.") 363 help="Issue number to which to add. Defaults to new issue.")
364 parser.add_option("-l", "--local_base", action="store_true", 364 parser.add_option("--lb", action="store_true",
GvR 2008/05/17 03:29:02 I recommend "-l", "--local_base"
365 dest="local_base", default=False, 365 dest="local_base", default=False,
366 help="base file will be uploaded") 366 help="base file will be uploaded")
367 367
368 368
369 def GetRpcServer(options): 369 def GetRpcServer(options):
370 """Returns an instance of an AbstractRpcServer. 370 """Returns an instance of an AbstractRpcServer.
371 371
372 Returns: 372 Returns:
373 A new AbstractRpcServer, on which RPC calls can be made. 373 A new AbstractRpcServer, on which RPC calls can be made.
374 """ 374 """
375 375
376 rpc_server_class = HttpRpcServer 376 rpc_server_class = HttpRpcServer
377 377
378 def GetUserCredentials(): 378 def GetUserCredentials():
379 """Prompts the user for a username and password.""" 379 """Prompts the user for a username and password."""
380 email = options.email 380 email = options.email
381 if email is None: 381 if email is None:
382 email = raw_input("Email: ").strip() 382 email = raw_input("Email: ").strip()
383 password = getpass.getpass("Password for %s: " % email) 383 password = getpass.getpass("Password for %s: " % email)
384 return (email, password) 384 return (email, password)
385 385
386 # If this is the dev_appserver, use fake authentication. 386 # If this is the dev_appserver, use fake authentication.
387 host = (options.host or options.server).lower() 387 host = (options.host or options.server).lower()
388 if host == "localhost" or host.startswith("localhost:"): 388 if host == "localhost" or host.startswith("localhost:"):
389 email = options.email 389 email = options.email
390 if email is None: 390 if email is None:
391 email = "test@example.com" 391 email = "test@example.com"
392 logging.info("Using debug user %s. Override with --email" % email) 392 logging.info("Using debug user %s. Override with --email" % email)
393 server = rpc_server_class( 393 server = rpc_server_class(
394 options.server, 394 options.server,
395 lambda: (email, "password"), 395 lambda: (email, "password"),
396 host_override=options.host, 396 host_override=options.host,
397 extra_headers={"Cookie": 397 extra_headers={"Cookie":
398 'dev_appserver_login="%s:False"' % email}, 398 'dev_appserver_login="%s:False"' % email},
399 save_cookies=options.save_cookies) 399 save_cookies=options.save_cookies)
400 # Don't try to talk to ClientLogin. 400 # Don't try to talk to ClientLogin.
401 server.authenticated = True 401 server.authenticated = True
402 return server 402 return server
403 403
404 return rpc_server_class(options.server, GetUserCredentials, 404 return rpc_server_class(options.server, GetUserCredentials,
405 host_override=options.host, 405 host_override=options.host,
406 save_cookies=options.save_cookies) 406 save_cookies=options.save_cookies)
407 407
408 408
409 def EncodeMultipartFormData(fields, files): 409 def EncodeMultipartFormData(fields, files):
410 """Encode form fields for multipart/form-data. 410 """Encode form fields for multipart/form-data.
411 411
412 Args: 412 Args:
413 fields: A sequence of (name, value) elements for regular form fields. 413 fields: A sequence of (name, value) elements for regular form fields.
414 files: A sequence of (name, filename, value) elements for data to be 414 files: A sequence of (name, filename, value) elements for data to be
415 uploaded as files. 415 uploaded as files.
416 Returns: 416 Returns:
417 (content_type, body) ready for httplib.HTTP instance. 417 (content_type, body) ready for httplib.HTTP instance.
418 418
419 Source: 419 Source:
420 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306 420 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
421 """ 421 """
422 BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-' 422 BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
423 CRLF = '\r\n' 423 CRLF = '\r\n'
424 lines = [] 424 lines = []
425 for (key, value) in fields: 425 for (key, value) in fields:
426 lines.append('--' + BOUNDARY) 426 lines.append('--' + BOUNDARY)
427 lines.append('Content-Disposition: form-data; name="%s"' % key) 427 lines.append('Content-Disposition: form-data; name="%s"' % key)
428 lines.append('') 428 lines.append('')
429 lines.append(value) 429 lines.append(value)
430 for (key, filename, value) in files: 430 for (key, filename, value) in files:
431 lines.append('--' + BOUNDARY) 431 lines.append('--' + BOUNDARY)
432 lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' % 432 lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
433 (key, filename)) 433 (key, filename))
434 lines.append('Content-Type: %s' % GetContentType(filename)) 434 lines.append('Content-Type: %s' % GetContentType(filename))
435 lines.append('') 435 lines.append('')
436 lines.append(value) 436 lines.append(value)
437 lines.append('--' + BOUNDARY + '--') 437 lines.append('--' + BOUNDARY + '--')
438 lines.append('') 438 lines.append('')
439 body = CRLF.join(lines) 439 body = CRLF.join(lines)
440 content_type = 'multipart/form-data; boundary=%s' % BOUNDARY 440 content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
441 return content_type, body 441 return content_type, body
442 442
443 443
444 def GetContentType(filename): 444 def GetContentType(filename):
445 """Helper to guess the content-type from the filename.""" 445 """Helper to guess the content-type from the filename."""
446 return mimetypes.guess_type(filename)[0] or 'application/octet-stream' 446 return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
447 447
448 448
449 def RunShell(command, args=(), silent_ok=False): 449 def RunShell(command, args=(), silent_ok=False):
450 logging.info("Running %s", command) 450 logging.info("Running %s", command)
451 stream = os.popen("%s %s" % (command, " ".join(args)), "r") 451 stream = os.popen("%s %s" % (command, " ".join(args)), "r")
452 data = stream.read() 452 data = stream.read()
453 if stream.close(): 453 if stream.close():
454 ErrorExit("Got error status from %s" % command) 454 ErrorExit("Got error status from %s" % command)
455 if not silent_ok and not data: 455 if not silent_ok and not data:
456 ErrorExit("No output from %s" % command) 456 ErrorExit("No output from %s" % command)
457 return data 457 return data
458 458
459 459
460 def GuessBase(): 460 def GuessBase():
461 info = RunShell("svn info") 461 info = RunShell("svn info")
462 for line in info.splitlines(): 462 for line in info.splitlines():
463 words = line.split() 463 words = line.split()
464 if len(words) == 2 and words[0] == "URL:": 464 if len(words) == 2 and words[0] == "URL:":
465 url = words[1] 465 url = words[1]
466 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) 466 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
467 if netloc.endswith("svn.python.org"): 467 if netloc.endswith("svn.python.org"):
468 if netloc == "svn.python.org": 468 if netloc == "svn.python.org":
469 if path.startswith("/projects/"): 469 if path.startswith("/projects/"):
470 path = path[9:] 470 path = path[9:]
471 elif netloc != "pythondev@svn.python.org": 471 elif netloc != "pythondev@svn.python.org":
472 ErrorExit("Unrecognized Python URL: %s" % url) 472 ErrorExit("Unrecognized Python URL: %s" % url)
473 base = "http://svn.python.org/view/*checkout*%s/" % path 473 base = "http://svn.python.org/view/*checkout*%s/" % path
474 logging.info("Guessed Python base = %s", base) 474 logging.info("Guessed Python base = %s", base)
475 elif netloc.endswith("svn.collab.net"): 475 elif netloc.endswith("svn.collab.net"):
476 if path.startswith("/repos/"): 476 if path.startswith("/repos/"):
477 path = path[6:] 477 path = path[6:]
478 base = "http://svn.collab.net/viewvc/*checkout*%s/" % path 478 base = "http://svn.collab.net/viewvc/*checkout*%s/" % path
479 logging.info("Guessed CollabNet base = %s", base) 479 logging.info("Guessed CollabNet base = %s", base)
480 elif netloc.endswith(".googlecode.com"): 480 elif netloc.endswith(".googlecode.com"):
481 base = url + "/" 481 base = url + "/"
482 if base.startswith("https"): 482 if base.startswith("https"):
483 base = "http" + base[5:] 483 base = "http" + base[5:]
484 logging.info("Guessed Google Code base = %s", base) 484 logging.info("Guessed Google Code base = %s", base)
485 else: 485 else:
486 ErrorExit("Unrecognized svn project root: %s" % url) 486 ErrorExit("Unrecognized svn project root: %s" % url)
487 return base 487 return base
488 ErrorExit("Can't find URL in output from svn info") 488 ErrorExit("Can't find URL in output from svn info")
489 489
490 490
491 def RealMain(argv): 491 def RealMain(argv):
492 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" 492 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
493 "%(lineno)s %(message)s ")) 493 "%(lineno)s %(message)s "))
494 options, args = parser.parse_args(sys.argv[1:]) 494 options, args = parser.parse_args(sys.argv[1:])
495 global verbosity 495 global verbosity
496 verbosity = options.verbose 496 verbosity = options.verbose
497 if verbosity >= 3: 497 if verbosity >= 3:
498 logging.getLogger().setLevel(logging.DEBUG) 498 logging.getLogger().setLevel(logging.DEBUG)
499 elif verbosity >= 2: 499 elif verbosity >= 2:
500 logging.getLogger().setLevel(logging.INFO) 500 logging.getLogger().setLevel(logging.INFO)
501 if options.local_base: 501 if options.local_base:
502 base = None 502 base = "None"
GvR 2008/05/17 03:29:02 I'd set it to the empty string. That should be tur
503 else: 503 else:
504 base = GuessBase() 504 base = GuessBase()
505 CheckForUnknownFiles() 505 CheckForUnknownFiles()
506 data = RunShell("svn diff", args) 506 data = RunShell("svn diff", args)
507 count = 0 507 count = 0
508 for line in data.splitlines(): 508 for line in data.splitlines():
509 if line.startswith("Index:"): 509 if line.startswith("Index:"):
510 count += 1 510 count += 1
511 logging.info(line) 511 logging.info(line)
512 if not count: 512 if not count:
513 ErrorExit("No valid patches found in output from svn diff") 513 ErrorExit("No valid patches found in output from svn diff")
514 if options.issue: 514 if options.issue:
515 prompt = "Message describing this patch set: " 515 prompt = "Message describing this patch set: "
516 else: 516 else:
517 prompt = "New issue subject: " 517 prompt = "New issue subject: "
518 message = options.message or raw_input(prompt).strip() 518 message = options.message or raw_input(prompt).strip()
519 if not message: 519 if not message:
520 ErrorExit("A non-empty message is required") 520 ErrorExit("A non-empty message is required")
521 rpc_server = GetRpcServer(options) 521 rpc_server = GetRpcServer(options)
522 form_fields = [("subject", message)] 522 form_fields = [("base", base), ("subject", message)]
523 if base is not None:
524 form_fields.append(("base", base))
525 if options.issue: 523 if options.issue:
526 form_fields.append(("issue", str(options.issue))) 524 form_fields.append(("issue", str(options.issue)))
527 if options.email: 525 if options.email:
528 form_fields.append(("user", options.email)) 526 form_fields.append(("user", options.email))
529 ctype, body = EncodeMultipartFormData(form_fields, 527 ctype, body = EncodeMultipartFormData(form_fields,
530 [("data", "data.diff", data)]) 528 [("data", "data.diff", data)])
531 response_body = rpc_server.Send("/upload", body, content_type=ctype) 529 response_body = rpc_server.Send("/upload", body, content_type=ctype)
532 StatusUpdate(response_body) 530 StatusUpdate(response_body)
533 sys.exit(not response_body.startswith("Issue created.")) 531 sys.exit(not response_body.startswith("Issue created."))
534 532
535 def CheckForUnknownFiles(): 533 def CheckForUnknownFiles():
536 status = RunShell("svn status --ignore-externals", silent_ok=True) 534 status = RunShell("svn status --ignore-externals", silent_ok=True)
537 unknown_files = [] 535 unknown_files = []
538 for line in status.split("\n"): 536 for line in status.split("\n"):
539 if line and line[0] == "?": 537 if line and line[0] == "?":
540 unknown_files.append(line) 538 unknown_files.append(line)
541 if unknown_files: 539 if unknown_files:
542 print "The following files are not added to version control:" 540 print "The following files are not added to version control:"
543 for line in unknown_files: 541 for line in unknown_files:
544 print line 542 print line
545 prompt = "Are you sure to continue?(y/N) " 543 prompt = "Are you sure to continue?(y/N) "
546 answer = raw_input(prompt).strip() 544 answer = raw_input(prompt).strip()
547 if answer != "y": 545 if answer != "y":
548 ErrorExit("User aborted") 546 ErrorExit("User aborted")
549 547
550 def main(): 548 def main():
551 try: 549 try:
552 RealMain(sys.argv) 550 RealMain(sys.argv)
553 except KeyboardInterrupt: 551 except KeyboardInterrupt:
554 print 552 print
555 StatusUpdate("Interrupted.") 553 StatusUpdate("Interrupted.")
556 sys.exit(1) 554 sys.exit(1)
557 555
558 556
559 if __name__ == "__main__": 557 if __name__ == "__main__":
560 main() 558 main()
LEFTRIGHT

Powered by Google App Engine
This is Rietveld r305