32"""Tool for uploading diffs from a version control system to the codereview app.
34Usage summary: upload.py [options] [-- diff_options]
36Diff options are passed to the diff command of the underlying system.
38Supported version control systems:
43It is important
for Git/Mercurial users to specify a tree/node/branch to diff
44against by using the
'--rev' option.
46# This code is derived from appcfg.py in the App Engine SDK (open source),
47# and from ASPN recipe #146306.
77MAX_UPLOAD_SIZE = 900 * 1024
81 """Prompts the user for their email address and returns it.
83 The last used email address is saved to a file
and offered up
as a suggestion
84 to the user. If the user presses enter without typing
in anything the last
85 used email address
is used. If the user enters a new address, it
is saved
86 for next time we prompt.
89 last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
91 if os.path.exists(last_email_file_name):
93 last_email_file = open(last_email_file_name,
"r")
94 last_email = last_email_file.readline().strip(
"\n")
95 last_email_file.close()
96 prompt +=
" [%s]" % last_email
99 email = raw_input(prompt +
": ").strip()
102 last_email_file = open(last_email_file_name,
"w")
103 last_email_file.write(email)
104 last_email_file.close()
113 """Print a status message to stdout.
115 If 'verbosity' is greater than 0,
print the message.
118 msg: The string to
print.
125 """Print an error message to stderr and exit."""
126 print >>sys.stderr, msg
131 """Raised to indicate there was an error authenticating with ClientLogin."""
134 urllib2.HTTPError.__init__(self, url, code, msg, headers,
None)
140 """Provides a common interface for a simple RPC server."""
142 def __init__(self, host, auth_function, host_override=None, extra_headers={},
144 """Creates a new HttpRpcServer.
147 host: The host to send requests to.
148 auth_function: A function that takes no arguments and returns an
149 (email, password) tuple when called. Will be called
if authentication
151 host_override: The host header to send to the server (defaults to host).
152 extra_headers: A dict of extra headers to append to every request.
153 save_cookies: If
True, save the authentication cookies to local disk.
154 If
False, use an
in-memory cookiejar instead. Subclasses must
155 implement this functionality. Defaults to
False.
167 logging.info(
"Server: %s", self.
host)
169 def _GetOpener(self):
170 """Returns an OpenerDirector for making HTTP requests.
173 A urllib2.OpenerDirector object.
175 raise NotImplementedError()
177 def _CreateRequest(self, url, data=None):
178 """Creates a new urllib request."""
179 logging.debug(
"Creating request for: '%s' with payload:\n%s", url, data)
180 req = urllib2.Request(url, data=data)
184 req.add_header(key, value)
187 def _GetAuthToken(self, email, password):
188 """Uses ClientLogin to authenticate the user, returning an auth token.
191 email: The user's email address
192 password: The user's password
195 ClientLoginError: If there was an error authenticating with ClientLogin.
196 HTTPError: If there was some other form of HTTP error.
199 The authentication token returned by ClientLogin.
201 account_type = "GOOGLE"
202 if self.
host.endswith(
".google.com"):
204 account_type =
"HOSTED"
206 url=
"https://www.google.com/accounts/ClientLogin",
207 data=urllib.urlencode({
211 "source":
"rietveld-codereview-upload",
212 "accountType": account_type,
216 response = self.
opener.open(req)
217 response_body = response.read()
218 response_dict = dict(x.split(
"=")
219 for x
in response_body.split(
"\n")
if x)
220 return response_dict[
"Auth"]
221 except urllib2.HTTPError, e:
224 response_dict = dict(x.split(
"=", 1)
for x
in body.split(
"\n")
if x)
226 e.headers, response_dict)
230 def _GetAuthCookie(self, auth_token):
231 """Fetches authentication cookies for an authentication token.
234 auth_token: The authentication token returned by ClientLogin.
237 HTTPError: If there was an error fetching the authentication cookies.
240 continue_location =
"http://localhost/"
241 args = {
"continue": continue_location,
"auth": auth_token}
243 (self.
host, urllib.urlencode(args)))
245 response = self.
opener.open(req)
246 except urllib2.HTTPError, e:
248 if (response.code != 302
or
249 response.info()[
"location"] != continue_location):
250 raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
251 response.headers, response.fp)
254 def _Authenticate(self):
255 """Authenticates the user.
257 The authentication process works as follows:
258 1) We get a username
and password
from the user
259 2) We use ClientLogin to obtain an AUTH token
for the user
260 (see https://developers.google.com/identity/protocols/AuthForInstalledApps).
261 3) We
pass the auth token to /_ah/login on the server to obtain an
262 authentication cookie. If login was successful, it tries to redirect
263 us to the URL we provided.
265 If we attempt to access the upload API without first obtaining an
266 authentication cookie, it returns a 401 response
and directs us to
267 authenticate ourselves
with ClientLogin.
272 auth_token = self.
_GetAuthToken(credentials[0], credentials[1])
273 except ClientLoginError, e:
274 if e.reason ==
"BadAuthentication":
275 print >>sys.stderr,
"Invalid username or password."
277 if e.reason ==
"CaptchaRequired":
278 print >>sys.stderr, (
280 "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
281 "and verify you are a human. Then try again.")
283 if e.reason ==
"NotVerified":
284 print >>sys.stderr,
"Account not verified."
286 if e.reason ==
"TermsNotAgreed":
287 print >>sys.stderr,
"User has not agreed to TOS."
289 if e.reason ==
"AccountDeleted":
290 print >>sys.stderr,
"The user account has been deleted."
292 if e.reason ==
"AccountDisabled":
293 print >>sys.stderr,
"The user account has been disabled."
295 if e.reason ==
"ServiceDisabled":
296 print >>sys.stderr, (
"The user's access to the service has been "
299 if e.reason ==
"ServiceUnavailable":
300 print >>sys.stderr,
"The service is not available; try again later."
306 def Send(self, request_path, payload=None,
307 content_type="application/octet-stream",
310 """Sends an RPC and returns the response.
313 request_path: The path to send the request to, eg /api/appversion/create.
314 payload: The body of the request, or None to send an empty request.
315 content_type: The Content-Type header to use.
316 timeout: timeout
in seconds; default
None i.e. no timeout.
317 (Note:
for large requests on OS X, the timeout doesn
't work right.)
318 kwargs: Any keyword arguments are converted into query string parameters.
321 The response body, as a string.
328 old_timeout = socket.getdefaulttimeout()
329 socket.setdefaulttimeout(timeout)
335 url =
"http://%s%s" % (self.
host, request_path)
337 url +=
"?" + urllib.urlencode(args)
339 req.add_header(
"Content-Type", content_type)
345 except urllib2.HTTPError, e:
356 socket.setdefaulttimeout(old_timeout)
360 """Provides a simplified RPC-style interface for HTTP requests."""
362 def _Authenticate(self):
363 """Save the cookie jar after authentication."""
364 super(HttpRpcServer, self)._Authenticate()
369 def _GetOpener(self):
370 """Returns an OpenerDirector that supports cookies and ignores redirects.
373 A urllib2.OpenerDirector object.
375 opener = urllib2.OpenerDirector()
376 opener.add_handler(urllib2.ProxyHandler())
377 opener.add_handler(urllib2.UnknownHandler())
378 opener.add_handler(urllib2.HTTPHandler())
379 opener.add_handler(urllib2.HTTPDefaultErrorHandler())
380 opener.add_handler(urllib2.HTTPSHandler())
381 opener.add_handler(urllib2.HTTPErrorProcessor())
383 self.
cookie_file = os.path.expanduser(
"~/.codereview_upload_cookies")
391 except (cookielib.LoadError, IOError):
403 opener.add_handler(urllib2.HTTPCookieProcessor(self.
cookie_jar))
407parser = optparse.OptionParser(usage=
"%prog [options] [-- diff_options]")
408parser.add_option(
"-y",
"--assume_yes", action=
"store_true",
409 dest=
"assume_yes", default=
False,
410 help=
"Assume that the answer to yes/no questions is 'yes'.")
412group = parser.add_option_group(
"Logging options")
413group.add_option(
"-q",
"--quiet", action=
"store_const", const=0,
414 dest=
"verbose", help=
"Print errors only.")
415group.add_option(
"-v",
"--verbose", action=
"store_const", const=2,
416 dest=
"verbose", default=1,
417 help=
"Print info level logs (default).")
418group.add_option(
"--noisy", action=
"store_const", const=3,
419 dest=
"verbose", help=
"Print all logs.")
421group = parser.add_option_group(
"Review server options")
422group.add_option(
"-s",
"--server", action=
"store", dest=
"server",
423 default=
"codereview.appspot.com",
425 help=(
"The server to upload to. The format is host[:port]. "
426 "Defaults to 'codereview.appspot.com'."))
427group.add_option(
"-e",
"--email", action=
"store", dest=
"email",
428 metavar=
"EMAIL", default=
None,
429 help=
"The username to use. Will prompt if omitted.")
430group.add_option(
"-H",
"--host", action=
"store", dest=
"host",
431 metavar=
"HOST", default=
None,
432 help=
"Overrides the Host header sent with all RPCs.")
433group.add_option(
"--no_cookies", action=
"store_false",
434 dest=
"save_cookies", default=
True,
435 help=
"Do not save authentication cookies to local disk.")
437group = parser.add_option_group(
"Issue options")
438group.add_option(
"-d",
"--description", action=
"store", dest=
"description",
439 metavar=
"DESCRIPTION", default=
None,
440 help=
"Optional description when creating an issue.")
441group.add_option(
"-f",
"--description_file", action=
"store",
442 dest=
"description_file", metavar=
"DESCRIPTION_FILE",
444 help=
"Optional path of a file that contains "
445 "the description when creating an issue.")
446group.add_option(
"-r",
"--reviewers", action=
"store", dest=
"reviewers",
447 metavar=
"REVIEWERS", default=
None,
448 help=
"Add reviewers (comma separated email addresses).")
449group.add_option(
"--cc", action=
"store", dest=
"cc",
450 metavar=
"CC", default=
None,
451 help=
"Add CC (comma separated email addresses).")
453group = parser.add_option_group(
"Patch options")
454group.add_option(
"-m",
"--message", action=
"store", dest=
"message",
455 metavar=
"MESSAGE", default=
None,
456 help=
"A message to identify the patch. "
457 "Will prompt if omitted.")
458group.add_option(
"-i",
"--issue", type=
"int", action=
"store",
459 metavar=
"ISSUE", default=
None,
460 help=
"Issue number to which to add. Defaults to new issue.")
461group.add_option(
"--download_base", action=
"store_true",
462 dest=
"download_base", default=
False,
463 help=
"Base files will be downloaded by the server "
464 "(side-by-side diffs may not work on files with CRs).")
465group.add_option(
"--rev", action=
"store", dest=
"revision",
466 metavar=
"REV", default=
None,
467 help=
"Branch/tree/revision to diff against (used by DVCS).")
468group.add_option(
"--send_mail", action=
"store_true",
469 dest=
"send_mail", default=
False,
470 help=
"Send notification email to reviewers.")
474 """Returns an instance of an AbstractRpcServer.
477 A new AbstractRpcServer, on which RPC calls can be made.
480 rpc_server_class = HttpRpcServer
482 def GetUserCredentials():
483 """Prompts the user for a username and password."""
484 email = options.email
486 email =
GetEmail(
"Email (login for uploading to %s)" % options.server)
487 password = getpass.getpass(
"Password for %s: " % email)
488 return (email, password)
491 host = (options.host
or options.server).lower()
492 if host ==
"localhost" or host.startswith(
"localhost:"):
493 email = options.email
495 email =
"test@example.com"
496 logging.info(
"Using debug user %s. Override with --email" % email)
497 server = rpc_server_class(
499 lambda: (email,
"password"),
500 host_override=options.host,
501 extra_headers={
"Cookie":
502 'dev_appserver_login="%s:False"' % email},
503 save_cookies=options.save_cookies)
505 server.authenticated =
True
508 return rpc_server_class(options.server, GetUserCredentials,
509 host_override=options.host,
510 save_cookies=options.save_cookies)
514 """Encode form fields for multipart/form-data.
517 fields: A sequence of (name, value) elements for regular form fields.
518 files: A sequence of (name, filename, value) elements
for data to be
521 (content_type, body) ready
for httplib.HTTP instance.
524 https://web.archive.org/web/20160116052001/code.activestate.com/recipes/146306
526 BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
529 for (key, value)
in fields:
530 lines.append(
'--' + BOUNDARY)
531 lines.append(
'Content-Disposition: form-data; name="%s"' % key)
534 for (key, filename, value)
in files:
535 lines.append(
'--' + BOUNDARY)
536 lines.append(
'Content-Disposition: form-data; name="%s"; filename="%s"' %
541 lines.append(
'--' + BOUNDARY +
'--')
543 body = CRLF.join(lines)
544 content_type =
'multipart/form-data; boundary=%s' % BOUNDARY
545 return content_type, body
549 """Helper to guess the content-type from the filename."""
550 return mimetypes.guess_type(filename)[0]
or 'application/octet-stream'
554use_shell = sys.platform.startswith(
"win")
557 universal_newlines=True):
558 """Executes a command and returns the output from stdout and the return code.
561 command: Command to execute.
562 print_output: If True, the output
is printed to stdout.
563 If
False, both stdout
and stderr are ignored.
564 universal_newlines: Use universal_newlines flag (default:
True).
567 Tuple (output,
return code)
569 logging.info("Running %s", command)
570 p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
571 shell=use_shell, universal_newlines=universal_newlines)
575 line = p.stdout.readline()
578 print line.strip(
"\n")
579 output_array.append(line)
580 output =
"".join(output_array)
582 output = p.stdout.read()
584 errout = p.stderr.read()
585 if print_output
and errout:
586 print >>sys.stderr, errout
589 return output, p.returncode
592def RunShell(command, silent_ok=False, universal_newlines=True,
597 ErrorExit(
"Got error status from %s:\n%s" % (command, data))
598 if not silent_ok
and not data:
604 """Abstract base class providing an interface to the VCS."""
610 options: Command line options.
615 """Return the current diff as a string.
618 args: Extra arguments to pass to the diff command.
620 raise NotImplementedError(
621 "abstract method -- subclass %s must override" % self.__class__)
624 """Return a list of files unknown to the VCS."""
625 raise NotImplementedError(
626 "abstract method -- subclass %s must override" % self.__class__)
629 """Show an "are you sure?" prompt if there are unknown files."""
632 print "The following files are not added to version control:"
633 for line
in unknown_files:
635 prompt =
"Are you sure to continue?(y/N) "
636 answer = raw_input(prompt).strip()
641 """Get the content of the upstream version of a file.
644 A tuple (base_content, new_content, is_binary, status)
645 base_content: The contents of the base file.
646 new_content: For text files, this is empty. For binary files, this
is
647 the contents of the new file, since the diff output won
't contain
648 information to reconstruct the current file.
649 is_binary: True iff the file
is binary.
650 status: The status of the file.
653 raise NotImplementedError(
654 "abstract method -- subclass %s must override" % self.__class__)
658 """Helper that calls GetBase file for each file in the patch.
661 A dictionary that maps from filename to GetBaseFile
's tuple. Filenames
662 are retrieved based on lines that start with "Index:" or
663 "Property changes on:".
666 for line
in diff.splitlines(
True):
667 if line.startswith(
'Index:')
or line.startswith(
'Property changes on:'):
668 unused, filename = line.split(
':', 1)
671 filename = filename.strip().replace(
'\\',
'/')
678 """Uploads the base files (and if necessary, the current ones as well)."""
680 def UploadFile(filename, file_id, content, is_binary, status, is_base):
681 """Uploads a file to the server."""
682 file_too_large =
False
687 if len(content) > MAX_UPLOAD_SIZE:
688 print (
"Not uploading the %s file for %s because it's too large." %
690 file_too_large =
True
692 checksum = md5.new(content).hexdigest()
693 if options.verbose > 0
and not file_too_large:
694 print "Uploading %s file for %s" % (type, filename)
695 url =
"/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
696 form_fields = [(
"filename", filename),
698 (
"checksum", checksum),
699 (
"is_binary", str(is_binary)),
700 (
"is_current", str(
not is_base)),
703 form_fields.append((
"file_too_large",
"1"))
705 form_fields.append((
"user", options.email))
707 [(
"data", filename, content)])
708 response_body = rpc_server.Send(url, body,
710 if not response_body.startswith(
"OK"):
715 [patches.setdefault(v, k)
for k, v
in patch_list]
716 for filename
in patches.keys():
717 base_content, new_content, is_binary, status = files[filename]
718 file_id_str = patches.get(filename)
719 if file_id_str.find(
"nobase") != -1:
721 file_id_str = file_id_str[file_id_str.rfind(
"_") + 1:]
722 file_id = int(file_id_str)
723 if base_content !=
None:
724 UploadFile(filename, file_id, base_content, is_binary, status,
True)
725 if new_content !=
None:
726 UploadFile(filename, file_id, new_content, is_binary, status,
False)
729 """Returns true if the filename has an image extension."""
730 mimetype = mimetypes.guess_type(filename)[0]
733 return mimetype.startswith(
"image/")
737 """Implementation of the VersionControlSystem interface for Subversion."""
740 super(SubversionVCS, self).
__init__(options)
742 match = re.match(
r"(\d+)(:(\d+))?", self.
options.revision)
754 required = self.
options.download_base
or self.
options.revision
is not None
758 """Wrapper for _GuessBase."""
761 def _GuessBase(self, required):
762 """Returns the SVN base URL.
765 required: If true, exits if the url can
't be guessed, otherwise None is
769 for line
in info.splitlines():
771 if len(words) == 2
and words[0] ==
"URL:":
773 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
774 username, netloc = urllib.splituser(netloc)
776 logging.info(
"Removed username from base URL")
777 if netloc.endswith(
"svn.python.org"):
778 if netloc ==
"svn.python.org":
779 if path.startswith(
"/projects/"):
781 elif netloc !=
"pythondev@svn.python.org":
782 ErrorExit(
"Unrecognized Python URL: %s" % url)
783 base =
"http://svn.python.org/view/*checkout*%s/" % path
784 logging.info(
"Guessed Python base = %s", base)
785 elif netloc.endswith(
"svn.collab.net"):
786 if path.startswith(
"/repos/"):
788 base =
"http://svn.collab.net/viewvc/*checkout*%s/" % path
789 logging.info(
"Guessed CollabNet base = %s", base)
790 elif netloc.endswith(
".googlecode.com"):
792 base = urlparse.urlunparse((
"http", netloc, path, params,
794 logging.info(
"Guessed Google Code base = %s", base)
797 base = urlparse.urlunparse((scheme, netloc, path, params,
799 logging.info(
"Guessed base = %s", base)
802 ErrorExit(
"Can't find URL in output from svn info")
806 cmd = [
"svn",
"diff"]
808 cmd += [
"-r", self.
options.revision]
812 for line
in data.splitlines():
813 if line.startswith(
"Index:")
or line.startswith(
"Property changes on:"):
817 ErrorExit(
"No valid patches found in output from svn diff")
820 def _CollapseKeywords(self, content, keyword_str):
821 """Collapses SVN keywords."""
829 'Date': [
'Date',
'LastChangedDate'],
830 'Revision': [
'Revision',
'LastChangedRevision',
'Rev'],
831 'Author': [
'Author',
'LastChangedBy'],
832 'HeadURL': [
'HeadURL',
'URL'],
836 'LastChangedDate': [
'LastChangedDate',
'Date'],
837 'LastChangedRevision': [
'LastChangedRevision',
'Rev',
'Revision'],
838 'LastChangedBy': [
'LastChangedBy',
'Author'],
839 'URL': [
'URL',
'HeadURL'],
844 return "$%s::%s$" % (m.group(1),
" " * len(m.group(3)))
845 return "$%s$" % m.group(1)
847 for name
in keyword_str.split(
" ")
848 for keyword
in svn_keywords.get(name, [])]
849 return re.sub(
r"\$(%s):(:?)([^\$]+)\$" %
'|'.join(keywords), repl, content)
852 status =
RunShell([
"svn",
"status",
"--ignore-externals"], silent_ok=
True)
854 for line
in status.split(
"\n"):
855 if line
and line[0] ==
"?":
856 unknown_files.append(line)
860 """Returns the contents of a file."""
861 file = open(filename,
'rb')
870 """Returns the status of a file."""
872 status =
RunShell([
"svn",
"status",
"--ignore-externals", filename])
874 ErrorExit(
"svn status returned no output for %s" % filename)
875 status_lines = status.splitlines()
879 if (len(status_lines) == 3
and
880 not status_lines[0]
and
881 status_lines[1].startswith(
"--- Changelist")):
882 status = status_lines[2]
884 status = status_lines[0]
889 dirname, relfilename = os.path.split(filename)
891 cmd = [
"svn",
"list",
"-r", self.
rev_start, dirname
or "."]
894 ErrorExit(
"Failed to get status for %s." % filename)
895 old_files = out.splitlines()
896 args = [
"svn",
"list"]
899 cmd = args + [dirname
or "."]
902 ErrorExit(
"Failed to run command %s" % cmd)
903 self.
svnls_cache[dirname] = (old_files, out.splitlines())
905 if relfilename
in old_files
and relfilename
not in new_files:
907 elif relfilename
in old_files
and relfilename
in new_files:
922 if status[0] ==
"A" and status[3] !=
"+":
925 mimetype =
RunShell([
"svn",
"propget",
"svn:mime-type", filename],
928 is_binary = mimetype
and not mimetype.startswith(
"text/")
929 if is_binary
and self.
IsImage(filename):
930 new_content = self.
ReadFile(filename)
931 elif (status[0]
in (
"M",
"D",
"R")
or
932 (status[0] ==
"A" and status[3] ==
"+")
or
933 (status[0] ==
" " and status[1] ==
"M")):
940 args += [
"-r",
"BASE"]
941 cmd = [
"svn"] + args + [
"propget",
"svn:mime-type", url]
948 is_binary = mimetype
and not mimetype.startswith(
"text/")
957 new_content = self.
ReadFile(filename)
960 new_content =
RunShell([
"svn",
"cat", url],
961 universal_newlines=
True, silent_ok=
True)
969 universal_newlines =
False
971 universal_newlines =
True
976 base_content =
RunShell([
"svn",
"cat", url],
977 universal_newlines=universal_newlines,
980 base_content =
RunShell([
"svn",
"cat", filename],
981 universal_newlines=universal_newlines,
989 args += [
"-r",
"BASE"]
990 cmd = [
"svn"] + args + [
"propget",
"svn:keywords", url]
992 if keywords
and not returncode:
995 StatusUpdate(
"svn status returned unexpected output: %s" % status)
997 return base_content, new_content, is_binary, status[0:5]
1001 """Implementation of the VersionControlSystem interface for Git."""
1004 super(GitVCS, self).
__init__(options)
1013 extra_args = [self.
options.revision] + extra_args
1014 gitdiff =
RunShell([
"git",
"diff",
"--full-index"] + extra_args)
1018 for line
in gitdiff.splitlines():
1019 match = re.match(
r"diff --git a/(.*) b/.*$", line)
1022 filename = match.group(1)
1023 svndiff.append(
"Index: %s\n" % filename)
1028 match = re.match(
r"index (\w+)\.\.", line)
1031 svndiff.append(line +
"\n")
1033 ErrorExit(
"No valid patches found in output from git diff")
1034 return "".join(svndiff)
1037 status =
RunShell([
"git",
"ls-files",
"--exclude-standard",
"--others"],
1039 return status.splitlines()
1046 if hash ==
"0" * 40:
1053 ErrorExit(
"Got error status from 'git show %s'" % hash)
1054 return (base_content, new_content, is_binary, status)
1058 """Implementation of the VersionControlSystem interface for Mercurial."""
1061 super(MercurialVCS, self).
__init__(options)
1065 cwd = os.path.normpath(os.getcwd())
1066 assert cwd.startswith(self.
repo_dir)
1073 def _GetRelPath(self, filename):
1074 """Get relative path of a file according to the current directory,
1075 given its logical path in the repo.
"""
1076 assert filename.startswith(self.
subdir), filename
1077 return filename[len(self.
subdir):].lstrip(
r"\/")
1081 extra_args = extra_args
or [
"."]
1082 cmd = [
"hg",
"diff",
"--git",
"-r", self.
base_rev] + extra_args
1083 data =
RunShell(cmd, silent_ok=
True)
1086 for line
in data.splitlines():
1087 m = re.match(
"diff --git a/(\S+) b/(\S+)", line)
1094 filename = m.group(2)
1095 svndiff.append(
"Index: %s" % filename)
1096 svndiff.append(
"=" * 67)
1100 svndiff.append(line)
1102 ErrorExit(
"No valid patches found in output from hg diff")
1103 return "\n".join(svndiff) +
"\n"
1106 """Return a list of files unknown to the VCS."""
1111 for line
in status.splitlines():
1112 st, fn = line.split(
" ", 1)
1114 unknown_files.append(fn)
1115 return unknown_files
1127 out = out.splitlines()
1130 if out[0].startswith(
'%s: ' % relpath):
1135 oldrelpath = out[1].strip()
1138 status, _ = out[0].
split(
' ', 1)
1142 is_binary =
"\0" in base_content
1144 new_content = open(relpath,
"rb").read()
1145 is_binary = is_binary
or "\0" in new_content
1146 if is_binary
and base_content:
1149 silent_ok=
True, universal_newlines=
False)
1150 if not is_binary
or not self.
IsImage(relpath):
1152 return base_content, new_content, is_binary, status
1157 """Splits a patch into separate pieces for each file.
1160 data: A string containing the output of svn diff.
1163 A list of 2-tuple (filename, text) where text is the svn diff output
1164 pertaining to filename.
1169 for line
in data.splitlines(
True):
1171 if line.startswith(
'Index:'):
1172 unused, new_filename = line.split(
':', 1)
1173 new_filename = new_filename.strip()
1174 elif line.startswith(
'Property changes on:'):
1175 unused, temp_filename = line.split(
':', 1)
1179 temp_filename = temp_filename.strip().replace(
'\\',
'/')
1180 if temp_filename != filename:
1182 new_filename = temp_filename
1184 if filename
and diff:
1185 patches.append((filename,
''.join(diff)))
1186 filename = new_filename
1189 if diff
is not None:
1191 if filename
and diff:
1192 patches.append((filename,
''.join(diff)))
1197 """Uploads a separate patch for each file in the diff output.
1199 Returns a list of [patch_key, filename] for each file.
1203 for patch
in patches:
1204 if len(patch[1]) > MAX_UPLOAD_SIZE:
1205 print (
"Not uploading the patch for " + patch[0] +
1206 " because the file is too large.")
1208 form_fields = [(
"filename", patch[0])]
1209 if not options.download_base:
1210 form_fields.append((
"content_upload",
"1"))
1211 files = [(
"data",
"data.diff", patch[1])]
1213 url =
"/%d/upload_patch/%d" % (int(issue), int(patchset))
1214 print "Uploading patch for " + patch[0]
1215 response_body = rpc_server.Send(url, body, content_type=ctype)
1216 lines = response_body.splitlines()
1217 if not lines
or lines[0] !=
"OK":
1220 rv.append([lines[1], patch[0]])
1225 """Helper to guess the version control system.
1227 This examines the current directory, guesses which VersionControlSystem
1228 we're using, and returns an instance of the appropriate class. Exit with an
1229 error if we can
't figure it out.
1232 A VersionControlSystem instance. Exits if the VCS can
't be guessed.
1241 except OSError, (errno, message):
1246 if os.path.isdir(
'.svn'):
1247 logging.info(
"Guessed VCS = Subversion")
1254 "--is-inside-work-tree"])
1257 except OSError, (errno, message):
1261 ErrorExit((
"Could not guess version control system. "
1262 "Are you in a working copy directory?"))
1266 """The real main function.
1269 argv: Command line arguments.
1270 data: Diff contents. If None (default) the diff
is generated by
1271 the VersionControlSystem implementation returned by
GuessVCS().
1274 A 2-tuple (issue id, patchset id).
1275 The patchset id
is None if the base files are
not uploaded by this
1276 script (applies only to SVN checkouts).
1278 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
1279 "%(lineno)s %(message)s "))
1280 os.environ[
'LC_ALL'] =
'C'
1281 options, args = parser.parse_args(argv[1:])
1283 verbosity = options.verbose
1285 logging.getLogger().setLevel(logging.DEBUG)
1286 elif verbosity >= 2:
1287 logging.getLogger().setLevel(logging.INFO)
1289 if isinstance(vcs, SubversionVCS):
1292 base = vcs.GuessBase(options.download_base)
1295 if not base
and options.download_base:
1296 options.download_base =
True
1297 logging.info(
"Enabled upload of base file")
1298 if not options.assume_yes:
1299 vcs.CheckForUnknownFiles()
1301 data = vcs.GenerateDiff(args)
1302 files = vcs.GetBaseFiles(data)
1304 print "Upload server:", options.server,
"(change with -s/--server)"
1306 prompt =
"Message describing this patch set: "
1308 prompt =
"New issue subject: "
1309 message = options.message
or raw_input(prompt).strip()
1311 ErrorExit(
"A non-empty message is required")
1313 form_fields = [(
"subject", message)]
1315 form_fields.append((
"base", base))
1317 form_fields.append((
"issue", str(options.issue)))
1319 form_fields.append((
"user", options.email))
1320 if options.reviewers:
1321 for reviewer
in options.reviewers.split(
','):
1322 if "@" in reviewer
and not reviewer.split(
"@")[1].
count(
".") == 1:
1323 ErrorExit(
"Invalid email address: %s" % reviewer)
1324 form_fields.append((
"reviewers", options.reviewers))
1326 for cc
in options.cc.split(
','):
1327 if "@" in cc
and not cc.split(
"@")[1].
count(
".") == 1:
1328 ErrorExit(
"Invalid email address: %s" % cc)
1329 form_fields.append((
"cc", options.cc))
1330 description = options.description
1331 if options.description_file:
1332 if options.description:
1333 ErrorExit(
"Can't specify description and description_file")
1334 file = open(options.description_file,
'r')
1335 description = file.read()
1338 form_fields.append((
"description", description))
1342 for file, info
in files.iteritems():
1343 if not info[0]
is None:
1344 checksum = md5.new(info[0]).hexdigest()
1347 base_hashes += checksum +
":" + file
1348 form_fields.append((
"base_hashes", base_hashes))
1351 if options.send_mail
and options.download_base:
1352 form_fields.append((
"send_mail",
"1"))
1353 if not options.download_base:
1354 form_fields.append((
"content_upload",
"1"))
1355 if len(data) > MAX_UPLOAD_SIZE:
1356 print "Patch is large, so uploading file patches separately."
1357 uploaded_diff_file = []
1358 form_fields.append((
"separate_patches",
"1"))
1360 uploaded_diff_file = [(
"data",
"data.diff", data)]
1362 response_body = rpc_server.Send(
"/upload", body, content_type=ctype)
1364 if not options.download_base
or not uploaded_diff_file:
1365 lines = response_body.splitlines()
1368 patchset = lines[1].strip()
1369 patches = [x.split(
" ", 1)
for x
in lines[2:]]
1375 if not response_body.startswith(
"Issue created.")
and \
1376 not response_body.startswith(
"Issue updated."):
1378 issue = msg[msg.rfind(
"/")+1:]
1380 if not uploaded_diff_file:
1382 if not options.download_base:
1385 if not options.download_base:
1386 vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files)
1387 if options.send_mail:
1388 rpc_server.Send(
"/" + issue +
"/mail", payload=
"")
1389 return issue, patchset
1395 except KeyboardInterrupt:
1401if __name__ ==
"__main__":
const std::vector< std::string > split(const std::string &s, char c)
def GetContentType(filename)
def RunShellWithReturnCode(command, print_output=False, universal_newlines=True)
def EncodeMultipartFormData(fields, files)
def RealMain(argv, data=None)
def UploadSeparatePatches(issue, rpc_server, patchset, data, options)
def GetRpcServer(options)
def RunShell(command, silent_ok=False, universal_newlines=True, print_output=False)
def __init__(self, url, code, msg, headers, args)
def _CreateRequest(self, url, data=None)
def __init__(self, host, auth_function, host_override=None, extra_headers={}, save_cookies=False)
def _GetAuthCookie(self, auth_token)
def _GetAuthToken(self, email, password)
def Send(self, request_path, payload=None, content_type="application/octet-stream", timeout=None, **kwargs)
elif e.code >= 500 and e.code < 600:
def GetUnknownFiles(self)
def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options, files)
def GetBaseFiles(self, diff)
def IsImage(self, filename)
def GenerateDiff(self, args)
def __init__(self, options)
def CheckForUnknownFiles(self)
def GetBaseFile(self, filename)
def GenerateDiff(self, args)
def GetBaseFile(self, filename)
def ReadFile(self, filename)
def GetUnknownFiles(self)
def GuessBase(self, required)
def __init__(self, options)
def _CollapseKeywords(self, content, keyword_str)
def GetStatus(self, filename)
def _GuessBase(self, required)
def GenerateDiff(self, extra_args)
def GetBaseFile(self, filename)
def __init__(self, options)
def GetUnknownFiles(self)
def GetBaseFile(self, filename)
def _GetRelPath(self, filename)
def __init__(self, options, repo_dir)
def GetUnknownFiles(self)
def GenerateDiff(self, extra_args)