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

Delta Between Two Patch Sets: static/upload.py

Issue 2602: git support (Closed) SVN Base: http://rietveld.googlecode.com/svn/trunk/
Left Patch Set: rebased on trunk; address review comments Created 1 year, 4 months ago
Right Patch Set: try two Created 1 year, 3 months 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
« no previous file | no next file » | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 diffs to the codereview app. 17 """Tool for uploading diffs from a version control system to the codereview app.
18 18
19 Usage summary: upload.py [options] [-- diff_options] 19 Usage summary: upload.py [options] [-- diff_options]
20 20
21 With Subversion, diff_options are passed to "svn diff". 21 Diff options are passed to the diff command of the underlying system.
22 With Git, diff_options is the tree-ish to diff against, defaulting to master. 22
23 Supported version control systems:
24 Git
25 Subversion
26
27 (It is important for Git users to specify a tree-ish to diff against.)
23 """ 28 """
24 # This code is derived from appcfg.py in the App Engine SDK (open source), 29 # This code is derived from appcfg.py in the App Engine SDK (open source),
25 # and from ASPN recipe #146306. 30 # and from ASPN recipe #146306.
26 31
27 import cookielib 32 import cookielib
28 import getpass 33 import getpass
29 import logging 34 import logging
30 import md5 35 import md5
31 import mimetypes 36 import mimetypes
32 import optparse 37 import optparse
33 import os 38 import os
34 import re 39 import re
35 import socket 40 import socket
41 import subprocess
36 import sys 42 import sys
37 import urllib 43 import urllib
38 import urllib2 44 import urllib2
39 import urlparse 45 import urlparse
40 46
41 try: 47 try:
42 import readline 48 import readline
43 except ImportError: 49 except ImportError:
44 pass 50 pass
45 51
46 # The logging verbosity: 52 # The logging verbosity:
47 # 0: Errors only. 53 # 0: Errors only.
48 # 1: Status messages. 54 # 1: Status messages.
49 # 2: Info logs. 55 # 2: Info logs.
50 # 3: Debug logs. 56 # 3: Debug logs.
51 verbosity = 1 57 verbosity = 1
58
59 # Max size of patch or base file.
60 MAX_UPLOAD_SIZE = 900 * 1024
52 61
53 62
54 def StatusUpdate(msg): 63 def StatusUpdate(msg):
55 """Print a status message to stdout. 64 """Print a status message to stdout.
56 65
57 If 'verbosity' is greater than 0, print the message. 66 If 'verbosity' is greater than 0, print the message.
58 67
59 Args: 68 Args:
60 msg: The string to print. 69 msg: The string to print.
61 """ 70 """
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
107 logging.info("Server: %s; Host: %s", self.host, self.host_override) 116 logging.info("Server: %s; Host: %s", self.host, self.host_override)
108 else: 117 else:
109 logging.info("Server: %s", self.host) 118 logging.info("Server: %s", self.host)
110 119
111 def _GetOpener(self): 120 def _GetOpener(self):
112 """Returns an OpenerDirector for making HTTP requests. 121 """Returns an OpenerDirector for making HTTP requests.
113 122
114 Returns: 123 Returns:
115 A urllib2.OpenerDirector object. 124 A urllib2.OpenerDirector object.
116 """ 125 """
117 raise NotImplemented() 126 raise NotImplementedError()
118 127
119 def _CreateRequest(self, url, data=None): 128 def _CreateRequest(self, url, data=None):
120 """Creates a new urllib request.""" 129 """Creates a new urllib request."""
121 logging.debug("Creating request for: '%s' with payload:\n%s", url, data) 130 logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
122 req = urllib2.Request(url, data=data) 131 req = urllib2.Request(url, data=data)
123 if self.host_override: 132 if self.host_override:
124 req.add_header("Host", self.host_override) 133 req.add_header("Host", self.host_override)
125 for key, value in self.extra_headers.iteritems(): 134 for key, value in self.extra_headers.iteritems():
126 req.add_header(key, value) 135 req.add_header(key, value)
127 return req 136 return req
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after
210 auth_token = self._GetAuthToken(credentials[0], credentials[1]) 219 auth_token = self._GetAuthToken(credentials[0], credentials[1])
211 except ClientLoginError, e: 220 except ClientLoginError, e:
212 if e.reason == "BadAuthentication": 221 if e.reason == "BadAuthentication":
213 print >>sys.stderr, "Invalid username or password." 222 print >>sys.stderr, "Invalid username or password."
214 continue 223 continue
215 if e.reason == "CaptchaRequired": 224 if e.reason == "CaptchaRequired":
216 print >>sys.stderr, ( 225 print >>sys.stderr, (
217 "Please go to\n" 226 "Please go to\n"
218 "https://www.google.com/accounts/DisplayUnlockCaptcha\n" 227 "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
219 "and verify you are a human. Then try again.") 228 "and verify you are a human. Then try again.")
220 break; 229 break
221 if e.reason == "NotVerified": 230 if e.reason == "NotVerified":
222 print >>sys.stderr, "Account not verified." 231 print >>sys.stderr, "Account not verified."
223 break 232 break
224 if e.reason == "TermsNotAgreed": 233 if e.reason == "TermsNotAgreed":
225 print >>sys.stderr, "User has not agreed to TOS." 234 print >>sys.stderr, "User has not agreed to TOS."
226 break 235 break
227 if e.reason == "AccountDeleted": 236 if e.reason == "AccountDeleted":
228 print >>sys.stderr, "The user account has been deleted." 237 print >>sys.stderr, "The user account has been deleted."
229 break 238 break
230 if e.reason == "AccountDisabled": 239 if e.reason == "AccountDisabled":
231 print >>sys.stderr, "The user account has been disabled." 240 print >>sys.stderr, "The user account has been disabled."
232 break 241 break
233 if e.reason == "ServiceDisabled": 242 if e.reason == "ServiceDisabled":
234 print >>sys.stderr, ("The user's access to the service has been " 243 print >>sys.stderr, ("The user's access to the service has been "
235 "disabled.") 244 "disabled.")
236 break 245 break
237 if e.reason == "ServiceUnavailable": 246 if e.reason == "ServiceUnavailable":
238 print >>sys.stderr, "The service is not available; try again later." 247 print >>sys.stderr, "The service is not available; try again later."
239 break 248 break
240 raise 249 raise
241 self._GetAuthCookie(auth_token) 250 self._GetAuthCookie(auth_token)
242 return 251 return
243 252
244 def Send(self, request_path, payload="", 253 def Send(self, request_path, payload=None,
245 content_type="application/octet-stream", 254 content_type="application/octet-stream",
246 timeout=None, 255 timeout=None,
247 **kwargs): 256 **kwargs):
248 """Sends an RPC and returns the response. 257 """Sends an RPC and returns the response.
249 258
250 Args: 259 Args:
251 request_path: The path to send the request to, eg /api/appversion/create. 260 request_path: The path to send the request to, eg /api/appversion/create.
252 payload: The body of the request, or None to send an empty request. 261 payload: The body of the request, or None to send an empty request.
253 content_type: The Content-Type header to use. 262 content_type: The Content-Type header to use.
254 timeout: timeout in seconds; default None i.e. no timeout. 263 timeout: timeout in seconds; default None i.e. no timeout.
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after
336 # Always chmod the cookie file 345 # Always chmod the cookie file
337 os.chmod(self.cookie_file, 0600) 346 os.chmod(self.cookie_file, 0600)
338 else: 347 else:
339 # Don't save cookies across runs of update.py. 348 # Don't save cookies across runs of update.py.
340 self.cookie_jar = cookielib.CookieJar() 349 self.cookie_jar = cookielib.CookieJar()
341 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar)) 350 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
342 return opener 351 return opener
343 352
344 353
345 parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]") 354 parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]")
346 parser.add_option("-q", "--quiet", action="store_const", const=0,
347 dest="verbose", help="Print errors only.")
348 parser.add_option("-v", "--verbose", action="store_const", const=2,
349 dest="verbose", default=1,
350 help="Print info level logs.")
351 parser.add_option("--noisy", action="store_const", const=3,
352 dest="verbose", help="Print all logs.")
353 parser.add_option("-s", "--server", action="store", dest="server",
354 default="codereview.appspot.com",
355 metavar="SERVER",
356 help="The server to upload to. The format is host[:port].")
357 parser.add_option("-e", "--email", action="store", dest="email",
358 metavar="EMAIL", default=None,
359 help="The username to use. Will prompt if omitted.")
360 parser.add_option("-H", "--host", action="store", dest="host",
361 metavar="HOST", default=None,
362 help="Overrides the Host header sent with all RPCs.")
363 parser.add_option("--no_cookies", action="store_false",
364 dest="save_cookies", default=True,
365 help="Do not save authentication cookies to local disk.")
366 parser.add_option("-m", "--message", action="store", dest="message",
367 metavar="MESSAGE", default=None,
368 help="A message to identify the patch. "
369 "Will prompt if omitted.")
370 parser.add_option("-i", "--issue", type="int", action="store",
371 metavar="ISSUE", default=None,
372 help="Issue number to which to add. Defaults to new issue.")
373 parser.add_option("-y", "--assume_yes", action="store_true", 355 parser.add_option("-y", "--assume_yes", action="store_true",
374 dest="assume_yes", default=False, 356 dest="assume_yes", default=False,
375 help="Assume that the answer to yes/no questions is 'yes'.") 357 help="Assume that the answer to yes/no questions is 'yes'.")
376 parser.add_option("-l", "--local_base", action="store_true", 358 # Logging
377 dest="local_base", default=False, 359 group = parser.add_option_group("Logging options")
378 help="Base files will be uploaded.") 360 group.add_option("-q", "--quiet", action="store_const", const=0,
379 parser.add_option("-r", "--reviewers", action="store", dest="reviewers", 361 dest="verbose", help="Print errors only.")
380 metavar="REVIEWERS", default=None, 362 group.add_option("-v", "--verbose", action="store_const", const=2,
381 help="Add reviewers (comma separated email addresses).") 363 dest="verbose", default=1,
382 parser.add_option("--cc", action="store", dest="cc", 364 help="Print info level logs (default).")
383 metavar="CC", default=None, 365 group.add_option("--noisy", action="store_const", const=3,
384 help="Add CC (comma separated email addresses).") 366 dest="verbose", help="Print all logs.")
367 # Review server
368 group = parser.add_option_group("Review server options")
369 group.add_option("-s", "--server", action="store", dest="server",
370 default="codereview.appspot.com",
371 metavar="SERVER",
372 help=("The server to upload to. The format is host[:port]. "
373 "Defaults to 'codereview.appspot.com'."))
374 group.add_option("-e", "--email", action="store", dest="email",
375 metavar="EMAIL", default=None,
376 help="The username to use. Will prompt if omitted.")
377 group.add_option("-H", "--host", action="store", dest="host",
378 metavar="HOST", default=None,
379 help="Overrides the Host header sent with all RPCs.")
380 group.add_option("--no_cookies", action="store_false",
381 dest="save_cookies", default=True,
382 help="Do not save authentication cookies to local disk.")
383 # Issue
384 group = parser.add_option_group("Issue options")
385 group.add_option("-d", "--description", action="store", dest="description",
386 metavar="DESCRIPTION", default=None,
387 help="Optional description when creating an issue.")
388 group.add_option("-f", "--description_file", action="store",
389 dest="description_file", metavar="DESCRIPTION_FILE",
390 default=None,
391 help="Optional path of a file that contains "
392 "the description when creating an issue.")
393 group.add_option("-r", "--reviewers", action="store", dest="reviewers",
394 metavar="REVIEWERS", default=None,
395 help="Add reviewers (comma separated email addresses).")
396 group.add_option("--cc", action="store", dest="cc",
397 metavar="CC", default=None,
398 help="Add CC (comma separated email addresses).")
399 # Upload options
400 group = parser.add_option_group("Patch options")
401 group.add_option("-m", "--message", action="store", dest="message",
402 metavar="MESSAGE", default=None,
403 help="A message to identify the patch. "
404 "Will prompt if omitted.")
405 group.add_option("-i", "--issue", type="int", action="store",
406 metavar="ISSUE", default=None,
407 help="Issue number to which to add. Defaults to new issue.")
408 group.add_option("-l", "--local_base", action="store_true",
409 dest="local_base", default=False,
410 help="Base files will be uploaded.")
411 group.add_option("--send_mail", action="store_true",
412 dest="send_mail", default=False,
413 help="Send notification email to reviewers.")
385 414
386 415
387 def GetRpcServer(options): 416 def GetRpcServer(options):
388 """Returns an instance of an AbstractRpcServer. 417 """Returns an instance of an AbstractRpcServer.
389 418
390 Returns: 419 Returns:
391 A new AbstractRpcServer, on which RPC calls can be made. 420 A new AbstractRpcServer, on which RPC calls can be made.
392 """ 421 """
393 422
394 rpc_server_class = HttpRpcServer 423 rpc_server_class = HttpRpcServer
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after
469 logging.info("Running %s", command) 498 logging.info("Running %s", command)
470 stream = os.popen(command, "r") 499 stream = os.popen(command, "r")
471 data = stream.read() 500 data = stream.read()
472 if stream.close(): 501 if stream.close():
473 ErrorExit("Got error status from %s" % command) 502 ErrorExit("Got error status from %s" % command)
474 if not silent_ok and not data: 503 if not silent_ok and not data:
475 ErrorExit("No output from %s" % command) 504 ErrorExit("No output from %s" % command)
476 return data 505 return data
477 506
478 507
479 class VersionControlSystem: 508 class VersionControlSystem(object):
480 """Abstract base class providing an interface to the VCS.""" 509 """Abstract base class providing an interface to the VCS."""
481 510
482 def __init__(self, args): 511 def GenerateDiff(self, args):
512 """Return the current diff as a string.
513
514 Args:
515 args: Extra arguments to pass to the diff command.
483 """ 516 """
484 Args: 517 raise NotImplementedError(
485 args: extra command-line arguments 518 "abstract method -- subclass %s must override" % self.__class__)
486 """
487
488 def GuessBase(self, required):
489 """Returns the SVN base URL.
490
491 Args:
492 required: If true, exits if the url can't be guessed, otherwise None is
493 returned.
494 """
495 if required:
496 ErrorExit("Cannot guess base URL; use the --local_base flag instead.")
497
498 def GenerateDiff(self):
499 """Return the current diff as a string."""
500 raise RuntimeError, \
501 "abstract method -- subclass %s must override" % self.__class__
502 519
503 def GetUnknownFiles(self): 520 def GetUnknownFiles(self):
504 """Return a list of files unknown to the VCS.""" 521 """Return a list of files unknown to the VCS."""
505 raise RuntimeError, \ 522 raise NotImplementedError(
506 "abstract method -- subclass %s must override" % self.__class__ 523 "abstract method -- subclass %s must override" % self.__class__)
507 524
508 def CheckForUnknownFiles(self): 525 def CheckForUnknownFiles(self):
509 """Show an "are you sure?" prompt if there are unknown files.""" 526 """Show an "are you sure?" prompt if there are unknown files."""
510 unknown_files = self.GetUnknownFiles() 527 unknown_files = self.GetUnknownFiles()
511 if unknown_files: 528 if unknown_files:
512 print "The following files are not added to version control:" 529 print "The following files are not added to version control:"
513 for line in unknown_files: 530 for line in unknown_files:
514 print line 531 print line
515 prompt = "Are you sure to continue?(y/N) " 532 prompt = "Are you sure to continue?(y/N) "
516 answer = raw_input(prompt).strip() 533 answer = raw_input(prompt).strip()
517 if answer != "y": 534 if answer != "y":
518 ErrorExit("User aborted") 535 ErrorExit("User aborted")
519 536
520 def GetBaseFile(self, filename): 537 def GetBaseFile(self, filename):
521 """Get the content of the upstream version of a file.""" 538 """Get the content of the upstream version of a file.
522 raise RuntimeError, \ 539
523 "abstract method -- subclass %s must override" % self.__class__ 540 Returns:
541 A tuple (content, status) representing the file content and the status of
542 the file.
543 """
544
545 raise NotImplementedError(
546 "abstract method -- subclass %s must override" % self.__class__)
524 547
525 def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options): 548 def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options):
526 """Uploads the base files.""" 549 """Uploads the base files."""
527 patches = dict() 550 patches = dict()
528 [patches.setdefault(v, k) for k, v in patch_list] 551 [patches.setdefault(v, k) for k, v in patch_list]
529 for filename in patches.keys(): 552 for filename in patches.keys():
530 content = self.GetBaseFile(filename) 553 content, status = self.GetBaseFile(filename)
554 if len(content) > MAX_UPLOAD_SIZE:
555 print ("Not uploading the base file for " + filename +
556 " because the file is too large.")
557 continue
531 checksum = md5.new(content).hexdigest() 558 checksum = md5.new(content).hexdigest()
532 parts = [] 559 parts = []
533 while content: 560 while content:
534 parts.append(content[:800000]) 561 parts.append(content[:800000])
535 content = content[800000:] 562 content = content[800000:]
536 if not parts: 563 if not parts:
537 parts = [""] # empty file 564 parts = [""] # empty file
538 for part, data in enumerate(parts): 565 for part, data in enumerate(parts):
539 if options.verbose > 0: 566 if options.verbose > 0:
540 print "Uploading %s (%d/%d)" % (filename, part+1, len(parts)) 567 print "Uploading %s (%d/%d)" % (filename, part+1, len(parts))
541 url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), 568 url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset),
542 int(patches.get(filename))) 569 int(patches.get(filename)))
543 current_checksum = md5.new(data).hexdigest() 570 current_checksum = md5.new(data).hexdigest()
544 form_fields = [("filename", filename), 571 form_fields = [("filename", filename),
572 ("status", status),
545 ("num_parts", str(len(parts))), 573 ("num_parts", str(len(parts))),
546 ("checksum", checksum), 574 ("checksum", checksum),
547 ("current_part", str(part)), 575 ("current_part", str(part)),
548 ("current_checksum", current_checksum),] 576 ("current_checksum", current_checksum),]
549 if options.email: 577 if options.email:
550 form_fields.append(("user", options.email)) 578 form_fields.append(("user", options.email))
551 ctype, body = EncodeMultipartFormData(form_fields, 579 ctype, body = EncodeMultipartFormData(form_fields,
552 [("data", filename, data)]) 580 [("data", filename, data)])
553 response_body = rpc_server.Send(url, body, 581 response_body = rpc_server.Send(url, body,
554 content_type=ctype) 582 content_type=ctype)
555 if not response_body.startswith("OK"): 583 if not response_body.startswith("OK"):
556 StatusUpdate(" --> %s" % response_body) 584 StatusUpdate(" --> %s" % response_body)
557 sys.exit(False) 585 sys.exit(False)
558 586
559 587
560 class SubversionVCS(VersionControlSystem): 588 class SubversionVCS(VersionControlSystem):
561 """Implementation of the VersionControlSystem interface for Subversion.""" 589 """Implementation of the VersionControlSystem interface for Subversion."""
562 590
563 def __init__(self, extra_args):
564 self.diff_args = extra_args
565
566
567 def GuessBase(self, required): 591 def GuessBase(self, required):
592 """Returns the SVN base URL.
593
594 Args:
595 required: If true, exits if the url can't be guessed, otherwise None is
596 returned.
597 """
568 info = RunShell("svn info") 598 info = RunShell("svn info")
569 for line in info.splitlines(): 599 for line in info.splitlines():
570 words = line.split() 600 words = line.split()
571 if len(words) == 2 and words[0] == "URL:": 601 if len(words) == 2 and words[0] == "URL:":
572 url = words[1] 602 url = words[1]
573 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) 603 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
604 username, netloc = urllib.splituser(netloc)
605 if username:
606 logging.info("Removed username from base URL")
574 if netloc.endswith("svn.python.org"): 607 if netloc.endswith("svn.python.org"):
575 if netloc == "svn.python.org": 608 if netloc == "svn.python.org":
576 if path.startswith("/projects/"): 609 if path.startswith("/projects/"):
577 path = path[9:] 610 path = path[9:]
578 elif netloc != "pythondev@svn.python.org": 611 elif netloc != "pythondev@svn.python.org":
579 ErrorExit("Unrecognized Python URL: %s" % url) 612 ErrorExit("Unrecognized Python URL: %s" % url)
580 base = "http://svn.python.org/view/*checkout*%s/" % path 613 base = "http://svn.python.org/view/*checkout*%s/" % path
581 logging.info("Guessed Python base = %s", base) 614 logging.info("Guessed Python base = %s", base)
582 elif netloc.endswith("svn.collab.net"): 615 elif netloc.endswith("svn.collab.net"):
583 if path.startswith("/repos/"): 616 if path.startswith("/repos/"):
584 path = path[6:] 617 path = path[6:]
585 base = "http://svn.collab.net/viewvc/*checkout*%s/" % path 618 base = "http://svn.collab.net/viewvc/*checkout*%s/" % path
586 logging.info("Guessed CollabNet base = %s", base) 619 logging.info("Guessed CollabNet base = %s", base)
587 elif netloc.endswith(".googlecode.com"): 620 elif netloc.endswith(".googlecode.com"):
588 base = url + "/" 621 path = path + "/"
589 if base.startswith("https"): 622 base = urlparse.urlunparse(("http", netloc, path, params,
590 base = "http" + base[5:] 623 query, fragment))
591 logging.info("Guessed Google Code base = %s", base) 624 logging.info("Guessed Google Code base = %s", base)
592 else: 625 else:
593 base = url + "/" 626 path = path + "/"
627 base = urlparse.urlunparse((scheme, netloc, path, params,
628 query, fragment))
594 logging.info("Guessed base = %s", base) 629 logging.info("Guessed base = %s", base)
595 return base 630 return base
596 if required: 631 if required:
597 ErrorExit("Can't find URL in output from svn info") 632 ErrorExit("Can't find URL in output from svn info")
598 return None 633 return None
599 634
600 def GenerateDiff(self): 635 def GenerateDiff(self, args):
601 cmd = "svn diff" 636 cmd = "svn diff"
602 if not sys.platform.startswith("win"): 637 if not sys.platform.startswith("win"):
603 cmd += " --diff-cmd=diff" 638 cmd += " --diff-cmd=diff"
604 data = RunShell(cmd, self.diff_args) 639 data = RunShell(cmd, args)
605 count = 0 640 count = 0
606 for line in data.splitlines(): 641 for line in data.splitlines():
607 if line.startswith("Index:"): 642 if line.startswith("Index:") or line.startswith("Property changes on:"):
608 count += 1 643 count += 1
609 logging.info(line) 644 logging.info(line)
610 if not count: 645 if not count:
611 ErrorExit("No valid patches found in output from svn diff") 646 ErrorExit("No valid patches found in output from svn diff")
612 return data 647 return data
613 648
614 def _CollapseKeywords(self, content, keyword_str): 649 def _CollapseKeywords(self, content, keyword_str):
615 """Collapses SVN keywords.""" 650 """Collapses SVN keywords."""
616 # svn cat translates keywords but svn diff doesn't. As a result of this 651 # svn cat translates keywords but svn diff doesn't. As a result of this
617 # behavior patching.PatchChunks() fails with a chunk mismatch error. 652 # behavior patching.PatchChunks() fails with a chunk mismatch error.
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
657 status_lines = status.splitlines() 692 status_lines = status.splitlines()
658 # If file is in a cl, the output will begin with 693 # If file is in a cl, the output will begin with
659 # "\n--- Changelist 'cl_name':\n". See 694 # "\n--- Changelist 'cl_name':\n". See
660 # http://svn.collab.net/repos/svn/trunk/notes/changelist-design.txt 695 # http://svn.collab.net/repos/svn/trunk/notes/changelist-design.txt
661 if (len(status_lines) == 3 and 696 if (len(status_lines) == 3 and
662 not status_lines[0] and 697 not status_lines[0] and
663 status_lines[1].startswith("--- Changelist")): 698 status_lines[1].startswith("--- Changelist")):
664 status = status_lines[2] 699 status = status_lines[2]
665 else: 700 else:
666 status = status_lines[0] 701 status = status_lines[0]
667 if status[0] == "A": 702 # If a file is copied its status will be "A +", which signifies
703 # "addition-with-history". See "svn st" for more information. We need to
704 # upload the original file or else diff parsing will fail if the file was
705 # edited.
706 if ((status[0] == "A" and status[3] != "+") or
707 (status[0] == " " and status[1] == "M")): # property changed
668 content = "" 708 content = ""
669 elif status[0] in ["M", "D"]: 709 elif (status[0] in ("M", "D", "R") or
710 (status[0] == "A" and status[3] == "+")):
670 mimetype = RunShell("svn -rBASE propget svn:mime-type", [filename], 711 mimetype = RunShell("svn -rBASE propget svn:mime-type", [filename],
671 silent_ok=True) 712 silent_ok=True)
672 if mimetype.startswith("application/octet-stream"): 713 if mimetype.startswith("application/octet-stream"):
673 content = "" 714 content = ""
674 else: 715 else:
675 content = RunShell("svn cat", [filename]) 716 content = RunShell("svn cat", [filename])
676 keywords = RunShell("svn -rBASE propget svn:keywords", [filename], 717 keywords = RunShell("svn -rBASE propget svn:keywords", [filename],
677 silent_ok=True) 718 silent_ok=True)
678 if keywords: 719 if keywords:
679 content = self._CollapseKeywords(content, keywords) 720 content = self._CollapseKeywords(content, keywords)
680 else: 721 else:
681 StatusUpdate("svn status returned unexpected output: %s" % status) 722 StatusUpdate("svn status returned unexpected output: %s" % status)
682 sys.exit(False) 723 sys.exit(False)
683 return content 724 return content, status[0:5]
684 725
685 726
686 class GitVCS(VersionControlSystem): 727 class GitVCS(VersionControlSystem):
687 """Implementation of the VersionControlSystem interface for Git.""" 728 """Implementation of the VersionControlSystem interface for Git."""
688 729
689 def __init__(self, extra_args): 730 def __init__(self):
690 if len(extra_args) == 0: 731 # Map of filename -> hash of base file.
691 self.diff_from = "master" 732 self.base_hashes = {}
692 elif len(extra_args) == 1: 733
693 self.diff_from = extra_args[0] 734 def GenerateDiff(self, extra_args):
694 else: 735 # This is more complicated than svn's GenerateDiff because we must convert
695 ErrorExit("git-based upload takes at most one argument") 736 # the diff output to include an svn-style "Index:" line as well as record
696 737 # the hashes of the base files, so we can upload them along with our diff.
697 738 gitdiff = RunShell("git diff", ["--full-index"] + extra_args)
698 def GenerateDiff(self):
699 """Return a Subversion-style diff representing this change as a string."""
700 gitdiff = RunShell("git diff", [self.diff_from])
701 svndiff = [] 739 svndiff = []
702 filecount = 0 740 filecount = 0
741 filename = None
703 for line in gitdiff.splitlines(): 742 for line in gitdiff.splitlines():
704 match = re.match(r"diff --git a/(.*) b/.*", line) 743 match = re.match(r"diff --git a/(.*) b/.*$", line)
705 if match: 744 if match:
706 filecount += 1 745 filecount += 1
707 svndiff.append("Index: " + match.group(1) + "\n") 746 filename = match.group(1)
747 svndiff.append("Index: %s\n" % filename)
748 else:
749 # The "index" line in a git diff looks like this (long hashes elided):
750 # index 82c0d44..b2cee3f 100755
751 # We want to save the left hash, as that identifies the base file.
752 match = re.match(r"index (\w+)\.\.", line)
753 if match:
754 self.base_hashes[filename] = match.group(1)
708 svndiff.append(line + "\n") 755 svndiff.append(line + "\n")
709 if not filecount: 756 if not filecount:
710 ErrorExit("No valid patches found in output from svn diff") 757 ErrorExit("No valid patches found in output from git diff")
711 return "".join(svndiff) 758 return "".join(svndiff)
712 759
713
714 def GetUnknownFiles(self): 760 def GetUnknownFiles(self):
715 """Get all "unknown" (not added with 'git add') files."""
716 status = RunShell("git ls-files --others", silent_ok=True) 761 status = RunShell("git ls-files --others", silent_ok=True)
717 return status.splitlines() 762 return status.splitlines()
718 763
719
720 def GetBaseFile(self, filename): 764 def GetBaseFile(self, filename):
721 """Get the base (upstream) version of a given filename.""" 765 hash = self.base_hashes[filename]
722 # TODO(Evan Martin): the git diff contains the hash necessary to get the 766 if hash == "0" * 40: # All-zero hash indicates no base file.
723 # base file's contents directly in one simple call, which would obviate all 767 return ("", "A")
724 # these rev-parse and diff calls. 768 else:
725 769 return (RunShell("git show", [hash]), "M")
726 # We must first change directories to the top of the tree. This is only 770
727 # necssary so the tree-root-relative path passed to "git diff" works. 771
728 cdup = RunShell("git rev-parse --show-cdup", silent_ok=True).strip() 772 # NOTE: this function is duplicated in engine.py, keep them in sync.
729 if cdup: 773 def SplitPatch(data):
730 os.chdir(cdup) 774 """Splits a patch into separate pieces for each file.
731 status = RunShell("git diff --name-status", [self.diff_from, filename]) 775
732 if status[0] == "A": 776 Args:
733 return "" 777 data: A string containing the output of svn diff.
734 elif status[0] in ["M", "D"]: 778
735 return RunShell("git show %s:%s" % (self.diff_from, filename)) 779 Returns:
736 ErrorExit("Unexpected git diff output: %s" % status) 780 A list of 2-tuple (filename, text) where text is the svn diff output
737 781 pertaining to filename.
738 782 """
739 def GuessVCSClass(): 783 patches = []
740 """Guess and return the proper VersionControlSystem class for this tree. 784 filename = None
785 diff = []
786 for line in data.splitlines(True):
787 new_filename = None
788 if line.startswith('Index:'):
789 unused, new_filename = line.split(':', 1)
790 new_filename = new_filename.strip()
791 elif line.startswith('Property changes on:'):
792 unused, temp_filename = line.split(':', 1)
793 # When a file is modified, paths use '/' between directories, however
794 # when a property is modified '\' is used on Windows. Make them the same
795 # otherwise the file shows up twice.
796 temp_filename = temp_filename.strip().replace('\\', '/')
797 if temp_filename != filename:
798 # File has property changes but no modifications, create a new diff.
799 new_filename = temp_filename
800 if new_filename:
801 if filename and diff:
802 patches.append((filename, ''.join(diff)))
803 filename = new_filename
804 diff = [line]
805 continue
806 if diff is not None:
807 diff.append(line)
808 if filename and diff:
809 patches.append((filename, ''.join(diff)))
810 return patches
811
812
813 def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
814 """Uploads a separate patch for each file in the diff output.
815
816 Returns a list of [patch_key, filename] for each file.
817 """
818 patches = SplitPatch(data)
819 rv = []
820 for patch in patches:
821 if len(patch[1]) > MAX_UPLOAD_SIZE:
822 print ("Not uploading the patch for " + patch[0] +
823 " because the file is too large.")
824 continue
825 form_fields = [("filename", patch[0])]
826 if options.local_base:
827 form_fields.append(("content_upload", "1"))
828 files = [("data", "data.diff", patch[1])]
829 ctype, body = EncodeMultipartFormData(form_fields, files)
830 url = "/%d/upload_patch/%d" % (int(issue), int(patchset))
831 print "Uploading patch for " + patch[0]
832 response_body = rpc_server.Send(url, body, content_type=ctype)
833 lines = response_body.splitlines()
834 if not lines or lines[0] != "OK":
835 StatusUpdate(" --> %s" % response_body)
836 sys.exit(False)
837 rv.append([lines[1], patch[0]])
838 return rv
839
840
841 def GuessVCS():
842 """Helper to guess the version control system.
741 843
742 This examines the current directory, guesses which VersionControlSystem 844 This examines the current directory, guesses which VersionControlSystem
743 we're using, and returns the appropriate class. Exit with an error if we 845 we're using, and returns an instance of the appropriate class. Exit with an
744 can't figure it out. 846 error if we can't figure it out.
Andi Albrecht 2008/08/19 19:48:34 Thanks for adding some more docstrings and comment
847
848 Returns:
849 A VersionControlSystem instance. Exits if the VCS can't be guessed.
745 """ 850 """
746
747 # Subversion has a .svn in all working directories. 851 # Subversion has a .svn in all working directories.
748 if os.path.exists('.svn'): 852 if os.path.isdir('.svn'):
749 return SubversionVCS 853 logging.info("Guessed VCS = Subversion")
854 return SubversionVCS()
750 855
751 # Git has a command to test if you're in a git tree. 856 # Git has a command to test if you're in a git tree.
752 # This will silently fail if you don't have git installed. 857 # Try running it, but don't die if we don't have git installed.
753 if os.system("git rev-parse --is-inside-work-tree >/dev/null 2>&1") == 0: 858 try:
754 return GitVCS 859 subproc = subprocess.Popen(["git", "rev-parse", "--is-inside-work-tree"],
755 860 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
756 ErrorExit("can't identify whether you're using svn or git") 861 if subproc.wait() == 0:
757 862 return GitVCS()
758 863 except OSError, (errno, message):
759 def RealMain(argv): 864 if errno != 2: # ENOENT -- they don't have git installed.
865 raise
866
867 ErrorExit(("Could not guess version control system. "
868 "Are you in a working copy directory?"))
869
870
871 def RealMain(argv, data=None):
760 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" 872 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
761 "%(lineno)s %(message)s ")) 873 "%(lineno)s %(message)s "))
762 os.environ['LC_ALL'] = 'C' 874 os.environ['LC_ALL'] = 'C'
763 options, args = parser.parse_args(argv[1:]) 875 options, args = parser.parse_args(argv[1:])
764 global verbosity 876 global verbosity
765 verbosity = options.verbose 877 verbosity = options.verbose
766 if verbosity >= 3: 878 if verbosity >= 3:
767 logging.getLogger().setLevel(logging.DEBUG) 879 logging.getLogger().setLevel(logging.DEBUG)
768 elif verbosity >= 2: 880 elif verbosity >= 2:
769 logging.getLogger().setLevel(logging.INFO) 881 logging.getLogger().setLevel(logging.INFO)
770 vcsclass = GuessVCSClass() 882 vcs = GuessVCS()
771 vcs = vcsclass(args) 883 if isinstance(vcs, SubversionVCS):
772 base = vcs.GuessBase(not options.local_base) 884 # base field is only allowed for Subversion.
885 # Note: Fetching base files may become deprecated in future releases.
886 base = vcs.GuessBase(not options.local_base)
887 else:
888 base = None
889 if not base and not options.local_base:
890 # TODO(andi): Enable local_base for other VCS by default.
891 # For future use. SubversionVCS.GuessBase() already checks this
Andi Albrecht 2008/08/19 19:48:34 I think we can enable local_base here now for all
892 # condition.
893 ErrorExit("Use '--local_base' to upload base files.")
773 if not options.assume_yes: 894 if not options.assume_yes:
774 vcs.CheckForUnknownFiles() 895 vcs.CheckForUnknownFiles()
775 data = vcs.GenerateDiff() 896 if not data:
897 data = vcs.GenerateDiff(args)
776 if verbosity >= 1: 898 if verbosity >= 1:
777 print "Upload server:", options.server, "(change with -s/--server)" 899 print "Upload server:", options.server, "(change with -s/--server)"
778 if options.issue: 900 if options.issue:
779 prompt = "Message describing this patch set: " 901 prompt = "Message describing this patch set: "
780 else: 902 else:
781 prompt = "New issue subject: " 903 prompt = "New issue subject: "
782 message = options.message or raw_input(prompt).strip() 904 message = options.message or raw_input(prompt).strip()
783 if not message: 905 if not message:
784 ErrorExit("A non-empty message is required") 906 ErrorExit("A non-empty message is required")
785 rpc_server = GetRpcServer(options) 907 rpc_server = GetRpcServer(options)
786 form_fields = [("subject", message)] 908 form_fields = [("subject", message)]
787 if base: 909 if base:
788 form_fields.append(("base", base)) 910 form_fields.append(("base", base))
789 if options.issue: 911 if options.issue:
790 form_fields.append(("issue", str(options.issue))) 912 form_fields.append(("issue", str(options.issue)))
791 if options.email: 913 if options.email:
792 form_fields.append(("user", options.email)) 914 form_fields.append(("user", options.email))
793 if options.reviewers: 915 if options.reviewers:
794 for reviewer in options.reviewers.split(','): 916 for reviewer in options.reviewers.split(','):
795 if reviewer.count("@") != 1 or "." not in reviewer.split("@")[1]: 917 if reviewer.count("@") != 1 or "." not in reviewer.split("@")[1]:
796 ErrorExit("Invalid email address: %s" % reviewer) 918 ErrorExit("Invalid email address: %s" % reviewer)
797 form_fields.append(("reviewers", options.reviewers)) 919 form_fields.append(("reviewers", options.reviewers))
798 if options.cc: 920 if options.cc:
799 for cc in options.cc.split(','): 921 for cc in options.cc.split(','):
800 if cc.count("@") != 1 or "." not in cc.split("@")[1]: 922 if cc.count("@") != 1 or "." not in cc.split("@")[1]:
801 ErrorExit("Invalid email address: %s" % cc) 923 ErrorExit("Invalid email address: %s" % cc)
802 form_fields.append(("cc", options.cc)) 924 form_fields.append(("cc", options.cc))
925 description = options.description
926 if options.description_file:
927 if options.description:
928 ErrorExit("Can't specify description and description_file")
929 file = open(options.description_file, 'r')
930 description = file.read()
931 file.close()
932 if description:
933 form_fields.append(("description", description))
934 # If we're uploading base files, don't send the email before the uploads, so
935 # that it contains the file status.
936 if options.send_mail and not options.local_base:
937 form_fields.append(("send_mail", "1"))
803 if options.local_base: 938 if options.local_base:
804 form_fields.append(("content_upload", "1")) 939 form_fields.append(("content_upload", "1"))
805 ctype, body = EncodeMultipartFormData(form_fields, 940 if len(data) > MAX_UPLOAD_SIZE:
806 [("data", "data.diff", data)]) 941 print "Patch is large, so uploading file patches separately."
942 files = []
943 form_fields.append(("separate_patches", "1"))
944 else:
945 files = [("data", "data.diff", data)]
946 ctype, body = EncodeMultipartFormData(form_fields, files)
807 response_body = rpc_server.Send("/upload", body, content_type=ctype) 947 response_body = rpc_server.Send("/upload", body, content_type=ctype)
808 if options.local_base: 948 if options.local_base or not files:
809 lines = response_body.splitlines() 949 lines = response_body.splitlines()
810 if len(lines) > 2: 950 if len(lines) >= 2:
811 msg = lines[0] 951 msg = lines[0]
812 patchset = lines[1].strip() 952 patchset = lines[1].strip()
813 patches = [x.split(" ", 1) for x in lines[2:]] 953 patches = [x.split(" ", 1) for x in lines[2:]]
814 else: 954 else:
815 msg = response_body 955 msg = response_body
816 else: 956 else:
817 msg = response_body 957 msg = response_body
818 StatusUpdate(msg) 958 StatusUpdate(msg)
819 if not response_body.startswith("Issue created.") and \ 959 if not response_body.startswith("Issue created.") and \
820 not response_body.startswith("Issue updated."): 960 not response_body.startswith("Issue updated."):
821 sys.exit(0) 961 sys.exit(0)
822 issue = msg[msg.rfind("/")+1:] 962 issue = msg[msg.rfind("/")+1:]
963
964 if not files:
965 result = UploadSeparatePatches(issue, rpc_server, patchset, data, options)
966 if options.local_base:
967 patches = result
968
823 if options.local_base: 969 if options.local_base:
824 vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options) 970 vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options)
971 if options.send_mail:
972 rpc_server.Send("/" + issue + "/mail")
825 return issue 973 return issue
826 974
827 975
828 def main(): 976 def main():
829 try: 977 try:
830 RealMain(sys.argv) 978 RealMain(sys.argv)
831 except KeyboardInterrupt: 979 except KeyboardInterrupt:
832 print 980 print
833 StatusUpdate("Interrupted.") 981 StatusUpdate("Interrupted.")
834 sys.exit(1) 982 sys.exit(1)
835 983
836 984
837 if __name__ == "__main__": 985 if __name__ == "__main__":
838 main() 986 main()
LEFTRIGHT
« no previous file | no next file » | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Toggle Comments ('s')

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld r497