OpenCores
URL https://opencores.org/ocsvn/s80186/s80186/trunk

Subversion Repositories s80186

[/] [s80186/] [trunk/] [vendor/] [googletest/] [googletest/] [scripts/] [upload.py] - Blame information for rev 2

Details | Compare with Previous | View Log

Line No. Rev Author Line
1 2 jamieiles
#!/usr/bin/env python
2
#
3
# Copyright 2007 Google Inc.
4
#
5
# Licensed under the Apache License, Version 2.0 (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
8
#
9
#     http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
# See the License for the specific language governing permissions and
15
# limitations under the License.
16
 
17
"""Tool for uploading diffs from a version control system to the codereview app.
18
 
19
Usage summary: upload.py [options] [-- diff_options]
20
 
21
Diff options are passed to the diff command of the underlying system.
22
 
23
Supported version control systems:
24
  Git
25
  Mercurial
26
  Subversion
27
 
28
It is important for Git/Mercurial users to specify a tree/node/branch to diff
29
against by using the '--rev' option.
30
"""
31
# This code is derived from appcfg.py in the App Engine SDK (open source),
32
# and from ASPN recipe #146306.
33
 
34
import cookielib
35
import getpass
36
import logging
37
import md5
38
import mimetypes
39
import optparse
40
import os
41
import re
42
import socket
43
import subprocess
44
import sys
45
import urllib
46
import urllib2
47
import urlparse
48
 
49
try:
50
  import readline
51
except ImportError:
52
  pass
53
 
54
# The logging verbosity:
55
#  0: Errors only.
56
#  1: Status messages.
57
#  2: Info logs.
58
#  3: Debug logs.
59
verbosity = 1
60
 
61
# Max size of patch or base file.
62
MAX_UPLOAD_SIZE = 900 * 1024
63
 
64
 
65
def GetEmail(prompt):
66
  """Prompts the user for their email address and returns it.
67
 
68
  The last used email address is saved to a file and offered up as a suggestion
69
  to the user. If the user presses enter without typing in anything the last
70
  used email address is used. If the user enters a new address, it is saved
71
  for next time we prompt.
72
 
73
  """
74
  last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
75
  last_email = ""
76
  if os.path.exists(last_email_file_name):
77
    try:
78
      last_email_file = open(last_email_file_name, "r")
79
      last_email = last_email_file.readline().strip("\n")
80
      last_email_file.close()
81
      prompt += " [%s]" % last_email
82
    except IOError, e:
83
      pass
84
  email = raw_input(prompt + ": ").strip()
85
  if email:
86
    try:
87
      last_email_file = open(last_email_file_name, "w")
88
      last_email_file.write(email)
89
      last_email_file.close()
90
    except IOError, e:
91
      pass
92
  else:
93
    email = last_email
94
  return email
95
 
96
 
97
def StatusUpdate(msg):
98
  """Print a status message to stdout.
99
 
100
  If 'verbosity' is greater than 0, print the message.
101
 
102
  Args:
103
    msg: The string to print.
104
  """
105
  if verbosity > 0:
106
    print msg
107
 
108
 
109
def ErrorExit(msg):
110
  """Print an error message to stderr and exit."""
111
  print >>sys.stderr, msg
112
  sys.exit(1)
113
 
114
 
115
class ClientLoginError(urllib2.HTTPError):
116
  """Raised to indicate there was an error authenticating with ClientLogin."""
117
 
118
  def __init__(self, url, code, msg, headers, args):
119
    urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
120
    self.args = args
121
    self.reason = args["Error"]
122
 
123
 
124
class AbstractRpcServer(object):
125
  """Provides a common interface for a simple RPC server."""
126
 
127
  def __init__(self, host, auth_function, host_override=None, extra_headers={},
128
               save_cookies=False):
129
    """Creates a new HttpRpcServer.
130
 
131
    Args:
132
      host: The host to send requests to.
133
      auth_function: A function that takes no arguments and returns an
134
        (email, password) tuple when called. Will be called if authentication
135
        is required.
136
      host_override: The host header to send to the server (defaults to host).
137
      extra_headers: A dict of extra headers to append to every request.
138
      save_cookies: If True, save the authentication cookies to local disk.
139
        If False, use an in-memory cookiejar instead.  Subclasses must
140
        implement this functionality.  Defaults to False.
141
    """
142
    self.host = host
143
    self.host_override = host_override
144
    self.auth_function = auth_function
145
    self.authenticated = False
146
    self.extra_headers = extra_headers
147
    self.save_cookies = save_cookies
148
    self.opener = self._GetOpener()
149
    if self.host_override:
150
      logging.info("Server: %s; Host: %s", self.host, self.host_override)
151
    else:
152
      logging.info("Server: %s", self.host)
153
 
154
  def _GetOpener(self):
155
    """Returns an OpenerDirector for making HTTP requests.
156
 
157
    Returns:
158
      A urllib2.OpenerDirector object.
159
    """
160
    raise NotImplementedError()
161
 
162
  def _CreateRequest(self, url, data=None):
163
    """Creates a new urllib request."""
164
    logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
165
    req = urllib2.Request(url, data=data)
166
    if self.host_override:
167
      req.add_header("Host", self.host_override)
168
    for key, value in self.extra_headers.iteritems():
169
      req.add_header(key, value)
170
    return req
171
 
172
  def _GetAuthToken(self, email, password):
173
    """Uses ClientLogin to authenticate the user, returning an auth token.
174
 
175
    Args:
176
      email:    The user's email address
177
      password: The user's password
178
 
179
    Raises:
180
      ClientLoginError: If there was an error authenticating with ClientLogin.
181
      HTTPError: If there was some other form of HTTP error.
182
 
183
    Returns:
184
      The authentication token returned by ClientLogin.
185
    """
186
    account_type = "GOOGLE"
187
    if self.host.endswith(".google.com"):
188
      # Needed for use inside Google.
189
      account_type = "HOSTED"
190
    req = self._CreateRequest(
191
        url="https://www.google.com/accounts/ClientLogin",
192
        data=urllib.urlencode({
193
            "Email": email,
194
            "Passwd": password,
195
            "service": "ah",
196
            "source": "rietveld-codereview-upload",
197
            "accountType": account_type,
198
        }),
199
    )
200
    try:
201
      response = self.opener.open(req)
202
      response_body = response.read()
203
      response_dict = dict(x.split("=")
204
                           for x in response_body.split("\n") if x)
205
      return response_dict["Auth"]
206
    except urllib2.HTTPError, e:
207
      if e.code == 403:
208
        body = e.read()
209
        response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
210
        raise ClientLoginError(req.get_full_url(), e.code, e.msg,
211
                               e.headers, response_dict)
212
      else:
213
        raise
214
 
215
  def _GetAuthCookie(self, auth_token):
216
    """Fetches authentication cookies for an authentication token.
217
 
218
    Args:
219
      auth_token: The authentication token returned by ClientLogin.
220
 
221
    Raises:
222
      HTTPError: If there was an error fetching the authentication cookies.
223
    """
224
    # This is a dummy value to allow us to identify when we're successful.
225
    continue_location = "http://localhost/"
226
    args = {"continue": continue_location, "auth": auth_token}
227
    req = self._CreateRequest("http://%s/_ah/login?%s" %
228
                              (self.host, urllib.urlencode(args)))
229
    try:
230
      response = self.opener.open(req)
231
    except urllib2.HTTPError, e:
232
      response = e
233
    if (response.code != 302 or
234
        response.info()["location"] != continue_location):
235
      raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
236
                              response.headers, response.fp)
237
    self.authenticated = True
238
 
239
  def _Authenticate(self):
240
    """Authenticates the user.
241
 
242
    The authentication process works as follows:
243
     1) We get a username and password from the user
244
     2) We use ClientLogin to obtain an AUTH token for the user
245
        (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
246
     3) We pass the auth token to /_ah/login on the server to obtain an
247
        authentication cookie. If login was successful, it tries to redirect
248
        us to the URL we provided.
249
 
250
    If we attempt to access the upload API without first obtaining an
251
    authentication cookie, it returns a 401 response and directs us to
252
    authenticate ourselves with ClientLogin.
253
    """
254
    for i in range(3):
255
      credentials = self.auth_function()
256
      try:
257
        auth_token = self._GetAuthToken(credentials[0], credentials[1])
258
      except ClientLoginError, e:
259
        if e.reason == "BadAuthentication":
260
          print >>sys.stderr, "Invalid username or password."
261
          continue
262
        if e.reason == "CaptchaRequired":
263
          print >>sys.stderr, (
264
              "Please go to\n"
265
              "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
266
              "and verify you are a human.  Then try again.")
267
          break
268
        if e.reason == "NotVerified":
269
          print >>sys.stderr, "Account not verified."
270
          break
271
        if e.reason == "TermsNotAgreed":
272
          print >>sys.stderr, "User has not agreed to TOS."
273
          break
274
        if e.reason == "AccountDeleted":
275
          print >>sys.stderr, "The user account has been deleted."
276
          break
277
        if e.reason == "AccountDisabled":
278
          print >>sys.stderr, "The user account has been disabled."
279
          break
280
        if e.reason == "ServiceDisabled":
281
          print >>sys.stderr, ("The user's access to the service has been "
282
                               "disabled.")
283
          break
284
        if e.reason == "ServiceUnavailable":
285
          print >>sys.stderr, "The service is not available; try again later."
286
          break
287
        raise
288
      self._GetAuthCookie(auth_token)
289
      return
290
 
291
  def Send(self, request_path, payload=None,
292
           content_type="application/octet-stream",
293
           timeout=None,
294
           **kwargs):
295
    """Sends an RPC and returns the response.
296
 
297
    Args:
298
      request_path: The path to send the request to, eg /api/appversion/create.
299
      payload: The body of the request, or None to send an empty request.
300
      content_type: The Content-Type header to use.
301
      timeout: timeout in seconds; default None i.e. no timeout.
302
        (Note: for large requests on OS X, the timeout doesn't work right.)
303
      kwargs: Any keyword arguments are converted into query string parameters.
304
 
305
    Returns:
306
      The response body, as a string.
307
    """
308
    # TODO: Don't require authentication.  Let the server say
309
    # whether it is necessary.
310
    if not self.authenticated:
311
      self._Authenticate()
312
 
313
    old_timeout = socket.getdefaulttimeout()
314
    socket.setdefaulttimeout(timeout)
315
    try:
316
      tries = 0
317
      while True:
318
        tries += 1
319
        args = dict(kwargs)
320
        url = "http://%s%s" % (self.host, request_path)
321
        if args:
322
          url += "?" + urllib.urlencode(args)
323
        req = self._CreateRequest(url=url, data=payload)
324
        req.add_header("Content-Type", content_type)
325
        try:
326
          f = self.opener.open(req)
327
          response = f.read()
328
          f.close()
329
          return response
330
        except urllib2.HTTPError, e:
331
          if tries > 3:
332
            raise
333
          elif e.code == 401:
334
            self._Authenticate()
335
##           elif e.code >= 500 and e.code < 600:
336
##             # Server Error - try again.
337
##             continue
338
          else:
339
            raise
340
    finally:
341
      socket.setdefaulttimeout(old_timeout)
342
 
343
 
344
class HttpRpcServer(AbstractRpcServer):
345
  """Provides a simplified RPC-style interface for HTTP requests."""
346
 
347
  def _Authenticate(self):
348
    """Save the cookie jar after authentication."""
349
    super(HttpRpcServer, self)._Authenticate()
350
    if self.save_cookies:
351
      StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
352
      self.cookie_jar.save()
353
 
354
  def _GetOpener(self):
355
    """Returns an OpenerDirector that supports cookies and ignores redirects.
356
 
357
    Returns:
358
      A urllib2.OpenerDirector object.
359
    """
360
    opener = urllib2.OpenerDirector()
361
    opener.add_handler(urllib2.ProxyHandler())
362
    opener.add_handler(urllib2.UnknownHandler())
363
    opener.add_handler(urllib2.HTTPHandler())
364
    opener.add_handler(urllib2.HTTPDefaultErrorHandler())
365
    opener.add_handler(urllib2.HTTPSHandler())
366
    opener.add_handler(urllib2.HTTPErrorProcessor())
367
    if self.save_cookies:
368
      self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies")
369
      self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
370
      if os.path.exists(self.cookie_file):
371
        try:
372
          self.cookie_jar.load()
373
          self.authenticated = True
374
          StatusUpdate("Loaded authentication cookies from %s" %
375
                       self.cookie_file)
376
        except (cookielib.LoadError, IOError):
377
          # Failed to load cookies - just ignore them.
378
          pass
379
      else:
380
        # Create an empty cookie file with mode 600
381
        fd = os.open(self.cookie_file, os.O_CREAT, 0600)
382
        os.close(fd)
383
      # Always chmod the cookie file
384
      os.chmod(self.cookie_file, 0600)
385
    else:
386
      # Don't save cookies across runs of update.py.
387
      self.cookie_jar = cookielib.CookieJar()
388
    opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
389
    return opener
390
 
391
 
392
parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]")
393
parser.add_option("-y", "--assume_yes", action="store_true",
394
                  dest="assume_yes", default=False,
395
                  help="Assume that the answer to yes/no questions is 'yes'.")
396
# Logging
397
group = parser.add_option_group("Logging options")
398
group.add_option("-q", "--quiet", action="store_const", const=0,
399
                 dest="verbose", help="Print errors only.")
400
group.add_option("-v", "--verbose", action="store_const", const=2,
401
                 dest="verbose", default=1,
402
                 help="Print info level logs (default).")
403
group.add_option("--noisy", action="store_const", const=3,
404
                 dest="verbose", help="Print all logs.")
405
# Review server
406
group = parser.add_option_group("Review server options")
407
group.add_option("-s", "--server", action="store", dest="server",
408
                 default="codereview.appspot.com",
409
                 metavar="SERVER",
410
                 help=("The server to upload to. The format is host[:port]. "
411
                       "Defaults to 'codereview.appspot.com'."))
412
group.add_option("-e", "--email", action="store", dest="email",
413
                 metavar="EMAIL", default=None,
414
                 help="The username to use. Will prompt if omitted.")
415
group.add_option("-H", "--host", action="store", dest="host",
416
                 metavar="HOST", default=None,
417
                 help="Overrides the Host header sent with all RPCs.")
418
group.add_option("--no_cookies", action="store_false",
419
                 dest="save_cookies", default=True,
420
                 help="Do not save authentication cookies to local disk.")
421
# Issue
422
group = parser.add_option_group("Issue options")
423
group.add_option("-d", "--description", action="store", dest="description",
424
                 metavar="DESCRIPTION", default=None,
425
                 help="Optional description when creating an issue.")
426
group.add_option("-f", "--description_file", action="store",
427
                 dest="description_file", metavar="DESCRIPTION_FILE",
428
                 default=None,
429
                 help="Optional path of a file that contains "
430
                      "the description when creating an issue.")
431
group.add_option("-r", "--reviewers", action="store", dest="reviewers",
432
                 metavar="REVIEWERS", default=None,
433
                 help="Add reviewers (comma separated email addresses).")
434
group.add_option("--cc", action="store", dest="cc",
435
                 metavar="CC", default=None,
436
                 help="Add CC (comma separated email addresses).")
437
# Upload options
438
group = parser.add_option_group("Patch options")
439
group.add_option("-m", "--message", action="store", dest="message",
440
                 metavar="MESSAGE", default=None,
441
                 help="A message to identify the patch. "
442
                      "Will prompt if omitted.")
443
group.add_option("-i", "--issue", type="int", action="store",
444
                 metavar="ISSUE", default=None,
445
                 help="Issue number to which to add. Defaults to new issue.")
446
group.add_option("--download_base", action="store_true",
447
                 dest="download_base", default=False,
448
                 help="Base files will be downloaded by the server "
449
                 "(side-by-side diffs may not work on files with CRs).")
450
group.add_option("--rev", action="store", dest="revision",
451
                 metavar="REV", default=None,
452
                 help="Branch/tree/revision to diff against (used by DVCS).")
453
group.add_option("--send_mail", action="store_true",
454
                 dest="send_mail", default=False,
455
                 help="Send notification email to reviewers.")
456
 
457
 
458
def GetRpcServer(options):
459
  """Returns an instance of an AbstractRpcServer.
460
 
461
  Returns:
462
    A new AbstractRpcServer, on which RPC calls can be made.
463
  """
464
 
465
  rpc_server_class = HttpRpcServer
466
 
467
  def GetUserCredentials():
468
    """Prompts the user for a username and password."""
469
    email = options.email
470
    if email is None:
471
      email = GetEmail("Email (login for uploading to %s)" % options.server)
472
    password = getpass.getpass("Password for %s: " % email)
473
    return (email, password)
474
 
475
  # If this is the dev_appserver, use fake authentication.
476
  host = (options.host or options.server).lower()
477
  if host == "localhost" or host.startswith("localhost:"):
478
    email = options.email
479
    if email is None:
480
      email = "test@example.com"
481
      logging.info("Using debug user %s.  Override with --email" % email)
482
    server = rpc_server_class(
483
        options.server,
484
        lambda: (email, "password"),
485
        host_override=options.host,
486
        extra_headers={"Cookie":
487
                       'dev_appserver_login="%s:False"' % email},
488
        save_cookies=options.save_cookies)
489
    # Don't try to talk to ClientLogin.
490
    server.authenticated = True
491
    return server
492
 
493
  return rpc_server_class(options.server, GetUserCredentials,
494
                          host_override=options.host,
495
                          save_cookies=options.save_cookies)
496
 
497
 
498
def EncodeMultipartFormData(fields, files):
499
  """Encode form fields for multipart/form-data.
500
 
501
  Args:
502
    fields: A sequence of (name, value) elements for regular form fields.
503
    files: A sequence of (name, filename, value) elements for data to be
504
           uploaded as files.
505
  Returns:
506
    (content_type, body) ready for httplib.HTTP instance.
507
 
508
  Source:
509
    http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
510
  """
511
  BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
512
  CRLF = '\r\n'
513
  lines = []
514
  for (key, value) in fields:
515
    lines.append('--' + BOUNDARY)
516
    lines.append('Content-Disposition: form-data; name="%s"' % key)
517
    lines.append('')
518
    lines.append(value)
519
  for (key, filename, value) in files:
520
    lines.append('--' + BOUNDARY)
521
    lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
522
             (key, filename))
523
    lines.append('Content-Type: %s' % GetContentType(filename))
524
    lines.append('')
525
    lines.append(value)
526
  lines.append('--' + BOUNDARY + '--')
527
  lines.append('')
528
  body = CRLF.join(lines)
529
  content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
530
  return content_type, body
531
 
532
 
533
def GetContentType(filename):
534
  """Helper to guess the content-type from the filename."""
535
  return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
536
 
537
 
538
# Use a shell for subcommands on Windows to get a PATH search.
539
use_shell = sys.platform.startswith("win")
540
 
541
def RunShellWithReturnCode(command, print_output=False,
542
                           universal_newlines=True):
543
  """Executes a command and returns the output from stdout and the return code.
544
 
545
  Args:
546
    command: Command to execute.
547
    print_output: If True, the output is printed to stdout.
548
                  If False, both stdout and stderr are ignored.
549
    universal_newlines: Use universal_newlines flag (default: True).
550
 
551
  Returns:
552
    Tuple (output, return code)
553
  """
554
  logging.info("Running %s", command)
555
  p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
556
                       shell=use_shell, universal_newlines=universal_newlines)
557
  if print_output:
558
    output_array = []
559
    while True:
560
      line = p.stdout.readline()
561
      if not line:
562
        break
563
      print line.strip("\n")
564
      output_array.append(line)
565
    output = "".join(output_array)
566
  else:
567
    output = p.stdout.read()
568
  p.wait()
569
  errout = p.stderr.read()
570
  if print_output and errout:
571
    print >>sys.stderr, errout
572
  p.stdout.close()
573
  p.stderr.close()
574
  return output, p.returncode
575
 
576
 
577
def RunShell(command, silent_ok=False, universal_newlines=True,
578
             print_output=False):
579
  data, retcode = RunShellWithReturnCode(command, print_output,
580
                                         universal_newlines)
581
  if retcode:
582
    ErrorExit("Got error status from %s:\n%s" % (command, data))
583
  if not silent_ok and not data:
584
    ErrorExit("No output from %s" % command)
585
  return data
586
 
587
 
588
class VersionControlSystem(object):
589
  """Abstract base class providing an interface to the VCS."""
590
 
591
  def __init__(self, options):
592
    """Constructor.
593
 
594
    Args:
595
      options: Command line options.
596
    """
597
    self.options = options
598
 
599
  def GenerateDiff(self, args):
600
    """Return the current diff as a string.
601
 
602
    Args:
603
      args: Extra arguments to pass to the diff command.
604
    """
605
    raise NotImplementedError(
606
        "abstract method -- subclass %s must override" % self.__class__)
607
 
608
  def GetUnknownFiles(self):
609
    """Return a list of files unknown to the VCS."""
610
    raise NotImplementedError(
611
        "abstract method -- subclass %s must override" % self.__class__)
612
 
613
  def CheckForUnknownFiles(self):
614
    """Show an "are you sure?" prompt if there are unknown files."""
615
    unknown_files = self.GetUnknownFiles()
616
    if unknown_files:
617
      print "The following files are not added to version control:"
618
      for line in unknown_files:
619
        print line
620
      prompt = "Are you sure to continue?(y/N) "
621
      answer = raw_input(prompt).strip()
622
      if answer != "y":
623
        ErrorExit("User aborted")
624
 
625
  def GetBaseFile(self, filename):
626
    """Get the content of the upstream version of a file.
627
 
628
    Returns:
629
      A tuple (base_content, new_content, is_binary, status)
630
        base_content: The contents of the base file.
631
        new_content: For text files, this is empty.  For binary files, this is
632
          the contents of the new file, since the diff output won't contain
633
          information to reconstruct the current file.
634
        is_binary: True iff the file is binary.
635
        status: The status of the file.
636
    """
637
 
638
    raise NotImplementedError(
639
        "abstract method -- subclass %s must override" % self.__class__)
640
 
641
 
642
  def GetBaseFiles(self, diff):
643
    """Helper that calls GetBase file for each file in the patch.
644
 
645
    Returns:
646
      A dictionary that maps from filename to GetBaseFile's tuple.  Filenames
647
      are retrieved based on lines that start with "Index:" or
648
      "Property changes on:".
649
    """
650
    files = {}
651
    for line in diff.splitlines(True):
652
      if line.startswith('Index:') or line.startswith('Property changes on:'):
653
        unused, filename = line.split(':', 1)
654
        # On Windows if a file has property changes its filename uses '\'
655
        # instead of '/'.
656
        filename = filename.strip().replace('\\', '/')
657
        files[filename] = self.GetBaseFile(filename)
658
    return files
659
 
660
 
661
  def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options,
662
                      files):
663
    """Uploads the base files (and if necessary, the current ones as well)."""
664
 
665
    def UploadFile(filename, file_id, content, is_binary, status, is_base):
666
      """Uploads a file to the server."""
667
      file_too_large = False
668
      if is_base:
669
        type = "base"
670
      else:
671
        type = "current"
672
      if len(content) > MAX_UPLOAD_SIZE:
673
        print ("Not uploading the %s file for %s because it's too large." %
674
               (type, filename))
675
        file_too_large = True
676
        content = ""
677
      checksum = md5.new(content).hexdigest()
678
      if options.verbose > 0 and not file_too_large:
679
        print "Uploading %s file for %s" % (type, filename)
680
      url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
681
      form_fields = [("filename", filename),
682
                     ("status", status),
683
                     ("checksum", checksum),
684
                     ("is_binary", str(is_binary)),
685
                     ("is_current", str(not is_base)),
686
                    ]
687
      if file_too_large:
688
        form_fields.append(("file_too_large", "1"))
689
      if options.email:
690
        form_fields.append(("user", options.email))
691
      ctype, body = EncodeMultipartFormData(form_fields,
692
                                            [("data", filename, content)])
693
      response_body = rpc_server.Send(url, body,
694
                                      content_type=ctype)
695
      if not response_body.startswith("OK"):
696
        StatusUpdate("  --> %s" % response_body)
697
        sys.exit(1)
698
 
699
    patches = dict()
700
    [patches.setdefault(v, k) for k, v in patch_list]
701
    for filename in patches.keys():
702
      base_content, new_content, is_binary, status = files[filename]
703
      file_id_str = patches.get(filename)
704
      if file_id_str.find("nobase") != -1:
705
        base_content = None
706
        file_id_str = file_id_str[file_id_str.rfind("_") + 1:]
707
      file_id = int(file_id_str)
708
      if base_content != None:
709
        UploadFile(filename, file_id, base_content, is_binary, status, True)
710
      if new_content != None:
711
        UploadFile(filename, file_id, new_content, is_binary, status, False)
712
 
713
  def IsImage(self, filename):
714
    """Returns true if the filename has an image extension."""
715
    mimetype =  mimetypes.guess_type(filename)[0]
716
    if not mimetype:
717
      return False
718
    return mimetype.startswith("image/")
719
 
720
 
721
class SubversionVCS(VersionControlSystem):
722
  """Implementation of the VersionControlSystem interface for Subversion."""
723
 
724
  def __init__(self, options):
725
    super(SubversionVCS, self).__init__(options)
726
    if self.options.revision:
727
      match = re.match(r"(\d+)(:(\d+))?", self.options.revision)
728
      if not match:
729
        ErrorExit("Invalid Subversion revision %s." % self.options.revision)
730
      self.rev_start = match.group(1)
731
      self.rev_end = match.group(3)
732
    else:
733
      self.rev_start = self.rev_end = None
734
    # Cache output from "svn list -r REVNO dirname".
735
    # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev).
736
    self.svnls_cache = {}
737
    # SVN base URL is required to fetch files deleted in an older revision.
738
    # Result is cached to not guess it over and over again in GetBaseFile().
739
    required = self.options.download_base or self.options.revision is not None
740
    self.svn_base = self._GuessBase(required)
741
 
742
  def GuessBase(self, required):
743
    """Wrapper for _GuessBase."""
744
    return self.svn_base
745
 
746
  def _GuessBase(self, required):
747
    """Returns the SVN base URL.
748
 
749
    Args:
750
      required: If true, exits if the url can't be guessed, otherwise None is
751
        returned.
752
    """
753
    info = RunShell(["svn", "info"])
754
    for line in info.splitlines():
755
      words = line.split()
756
      if len(words) == 2 and words[0] == "URL:":
757
        url = words[1]
758
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
759
        username, netloc = urllib.splituser(netloc)
760
        if username:
761
          logging.info("Removed username from base URL")
762
        if netloc.endswith("svn.python.org"):
763
          if netloc == "svn.python.org":
764
            if path.startswith("/projects/"):
765
              path = path[9:]
766
          elif netloc != "pythondev@svn.python.org":
767
            ErrorExit("Unrecognized Python URL: %s" % url)
768
          base = "http://svn.python.org/view/*checkout*%s/" % path
769
          logging.info("Guessed Python base = %s", base)
770
        elif netloc.endswith("svn.collab.net"):
771
          if path.startswith("/repos/"):
772
            path = path[6:]
773
          base = "http://svn.collab.net/viewvc/*checkout*%s/" % path
774
          logging.info("Guessed CollabNet base = %s", base)
775
        elif netloc.endswith(".googlecode.com"):
776
          path = path + "/"
777
          base = urlparse.urlunparse(("http", netloc, path, params,
778
                                      query, fragment))
779
          logging.info("Guessed Google Code base = %s", base)
780
        else:
781
          path = path + "/"
782
          base = urlparse.urlunparse((scheme, netloc, path, params,
783
                                      query, fragment))
784
          logging.info("Guessed base = %s", base)
785
        return base
786
    if required:
787
      ErrorExit("Can't find URL in output from svn info")
788
    return None
789
 
790
  def GenerateDiff(self, args):
791
    cmd = ["svn", "diff"]
792
    if self.options.revision:
793
      cmd += ["-r", self.options.revision]
794
    cmd.extend(args)
795
    data = RunShell(cmd)
796
    count = 0
797
    for line in data.splitlines():
798
      if line.startswith("Index:") or line.startswith("Property changes on:"):
799
        count += 1
800
        logging.info(line)
801
    if not count:
802
      ErrorExit("No valid patches found in output from svn diff")
803
    return data
804
 
805
  def _CollapseKeywords(self, content, keyword_str):
806
    """Collapses SVN keywords."""
807
    # svn cat translates keywords but svn diff doesn't. As a result of this
808
    # behavior patching.PatchChunks() fails with a chunk mismatch error.
809
    # This part was originally written by the Review Board development team
810
    # who had the same problem (http://reviews.review-board.org/r/276/).
811
    # Mapping of keywords to known aliases
812
    svn_keywords = {
813
      # Standard keywords
814
      'Date':                ['Date', 'LastChangedDate'],
815
      'Revision':            ['Revision', 'LastChangedRevision', 'Rev'],
816
      'Author':              ['Author', 'LastChangedBy'],
817
      'HeadURL':             ['HeadURL', 'URL'],
818
      'Id':                  ['Id'],
819
 
820
      # Aliases
821
      'LastChangedDate':     ['LastChangedDate', 'Date'],
822
      'LastChangedRevision': ['LastChangedRevision', 'Rev', 'Revision'],
823
      'LastChangedBy':       ['LastChangedBy', 'Author'],
824
      'URL':                 ['URL', 'HeadURL'],
825
    }
826
 
827
    def repl(m):
828
       if m.group(2):
829
         return "$%s::%s$" % (m.group(1), " " * len(m.group(3)))
830
       return "$%s$" % m.group(1)
831
    keywords = [keyword
832
                for name in keyword_str.split(" ")
833
                for keyword in svn_keywords.get(name, [])]
834
    return re.sub(r"\$(%s):(:?)([^\$]+)\$" % '|'.join(keywords), repl, content)
835
 
836
  def GetUnknownFiles(self):
837
    status = RunShell(["svn", "status", "--ignore-externals"], silent_ok=True)
838
    unknown_files = []
839
    for line in status.split("\n"):
840
      if line and line[0] == "?":
841
        unknown_files.append(line)
842
    return unknown_files
843
 
844
  def ReadFile(self, filename):
845
    """Returns the contents of a file."""
846
    file = open(filename, 'rb')
847
    result = ""
848
    try:
849
      result = file.read()
850
    finally:
851
      file.close()
852
    return result
853
 
854
  def GetStatus(self, filename):
855
    """Returns the status of a file."""
856
    if not self.options.revision:
857
      status = RunShell(["svn", "status", "--ignore-externals", filename])
858
      if not status:
859
        ErrorExit("svn status returned no output for %s" % filename)
860
      status_lines = status.splitlines()
861
      # If file is in a cl, the output will begin with
862
      # "\n--- Changelist 'cl_name':\n".  See
863
      # http://svn.collab.net/repos/svn/trunk/notes/changelist-design.txt
864
      if (len(status_lines) == 3 and
865
          not status_lines[0] and
866
          status_lines[1].startswith("--- Changelist")):
867
        status = status_lines[2]
868
      else:
869
        status = status_lines[0]
870
    # If we have a revision to diff against we need to run "svn list"
871
    # for the old and the new revision and compare the results to get
872
    # the correct status for a file.
873
    else:
874
      dirname, relfilename = os.path.split(filename)
875
      if dirname not in self.svnls_cache:
876
        cmd = ["svn", "list", "-r", self.rev_start, dirname or "."]
877
        out, returncode = RunShellWithReturnCode(cmd)
878
        if returncode:
879
          ErrorExit("Failed to get status for %s." % filename)
880
        old_files = out.splitlines()
881
        args = ["svn", "list"]
882
        if self.rev_end:
883
          args += ["-r", self.rev_end]
884
        cmd = args + [dirname or "."]
885
        out, returncode = RunShellWithReturnCode(cmd)
886
        if returncode:
887
          ErrorExit("Failed to run command %s" % cmd)
888
        self.svnls_cache[dirname] = (old_files, out.splitlines())
889
      old_files, new_files = self.svnls_cache[dirname]
890
      if relfilename in old_files and relfilename not in new_files:
891
        status = "D   "
892
      elif relfilename in old_files and relfilename in new_files:
893
        status = "M   "
894
      else:
895
        status = "A   "
896
    return status
897
 
898
  def GetBaseFile(self, filename):
899
    status = self.GetStatus(filename)
900
    base_content = None
901
    new_content = None
902
 
903
    # If a file is copied its status will be "A  +", which signifies
904
    # "addition-with-history".  See "svn st" for more information.  We need to
905
    # upload the original file or else diff parsing will fail if the file was
906
    # edited.
907
    if status[0] == "A" and status[3] != "+":
908
      # We'll need to upload the new content if we're adding a binary file
909
      # since diff's output won't contain it.
910
      mimetype = RunShell(["svn", "propget", "svn:mime-type", filename],
911
                          silent_ok=True)
912
      base_content = ""
913
      is_binary = mimetype and not mimetype.startswith("text/")
914
      if is_binary and self.IsImage(filename):
915
        new_content = self.ReadFile(filename)
916
    elif (status[0] in ("M", "D", "R") or
917
          (status[0] == "A" and status[3] == "+") or  # Copied file.
918
          (status[0] == " " and status[1] == "M")):  # Property change.
919
      args = []
920
      if self.options.revision:
921
        url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
922
      else:
923
        # Don't change filename, it's needed later.
924
        url = filename
925
        args += ["-r", "BASE"]
926
      cmd = ["svn"] + args + ["propget", "svn:mime-type", url]
927
      mimetype, returncode = RunShellWithReturnCode(cmd)
928
      if returncode:
929
        # File does not exist in the requested revision.
930
        # Reset mimetype, it contains an error message.
931
        mimetype = ""
932
      get_base = False
933
      is_binary = mimetype and not mimetype.startswith("text/")
934
      if status[0] == " ":
935
        # Empty base content just to force an upload.
936
        base_content = ""
937
      elif is_binary:
938
        if self.IsImage(filename):
939
          get_base = True
940
          if status[0] == "M":
941
            if not self.rev_end:
942
              new_content = self.ReadFile(filename)
943
            else:
944
              url = "%s/%s@%s" % (self.svn_base, filename, self.rev_end)
945
              new_content = RunShell(["svn", "cat", url],
946
                                     universal_newlines=True, silent_ok=True)
947
        else:
948
          base_content = ""
949
      else:
950
        get_base = True
951
 
952
      if get_base:
953
        if is_binary:
954
          universal_newlines = False
955
        else:
956
          universal_newlines = True
957
        if self.rev_start:
958
          # "svn cat -r REV delete_file.txt" doesn't work. cat requires
959
          # the full URL with "@REV" appended instead of using "-r" option.
960
          url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
961
          base_content = RunShell(["svn", "cat", url],
962
                                  universal_newlines=universal_newlines,
963
                                  silent_ok=True)
964
        else:
965
          base_content = RunShell(["svn", "cat", filename],
966
                                  universal_newlines=universal_newlines,
967
                                  silent_ok=True)
968
        if not is_binary:
969
          args = []
970
          if self.rev_start:
971
            url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
972
          else:
973
            url = filename
974
            args += ["-r", "BASE"]
975
          cmd = ["svn"] + args + ["propget", "svn:keywords", url]
976
          keywords, returncode = RunShellWithReturnCode(cmd)
977
          if keywords and not returncode:
978
            base_content = self._CollapseKeywords(base_content, keywords)
979
    else:
980
      StatusUpdate("svn status returned unexpected output: %s" % status)
981
      sys.exit(1)
982
    return base_content, new_content, is_binary, status[0:5]
983
 
984
 
985
class GitVCS(VersionControlSystem):
986
  """Implementation of the VersionControlSystem interface for Git."""
987
 
988
  def __init__(self, options):
989
    super(GitVCS, self).__init__(options)
990
    # Map of filename -> hash of base file.
991
    self.base_hashes = {}
992
 
993
  def GenerateDiff(self, extra_args):
994
    # This is more complicated than svn's GenerateDiff because we must convert
995
    # the diff output to include an svn-style "Index:" line as well as record
996
    # the hashes of the base files, so we can upload them along with our diff.
997
    if self.options.revision:
998
      extra_args = [self.options.revision] + extra_args
999
    gitdiff = RunShell(["git", "diff", "--full-index"] + extra_args)
1000
    svndiff = []
1001
    filecount = 0
1002
    filename = None
1003
    for line in gitdiff.splitlines():
1004
      match = re.match(r"diff --git a/(.*) b/.*$", line)
1005
      if match:
1006
        filecount += 1
1007
        filename = match.group(1)
1008
        svndiff.append("Index: %s\n" % filename)
1009
      else:
1010
        # The "index" line in a git diff looks like this (long hashes elided):
1011
        #   index 82c0d44..b2cee3f 100755
1012
        # We want to save the left hash, as that identifies the base file.
1013
        match = re.match(r"index (\w+)\.\.", line)
1014
        if match:
1015
          self.base_hashes[filename] = match.group(1)
1016
      svndiff.append(line + "\n")
1017
    if not filecount:
1018
      ErrorExit("No valid patches found in output from git diff")
1019
    return "".join(svndiff)
1020
 
1021
  def GetUnknownFiles(self):
1022
    status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
1023
                      silent_ok=True)
1024
    return status.splitlines()
1025
 
1026
  def GetBaseFile(self, filename):
1027
    hash = self.base_hashes[filename]
1028
    base_content = None
1029
    new_content = None
1030
    is_binary = False
1031
    if hash == "0" * 40:  # All-zero hash indicates no base file.
1032
      status = "A"
1033
      base_content = ""
1034
    else:
1035
      status = "M"
1036
      base_content, returncode = RunShellWithReturnCode(["git", "show", hash])
1037
      if returncode:
1038
        ErrorExit("Got error status from 'git show %s'" % hash)
1039
    return (base_content, new_content, is_binary, status)
1040
 
1041
 
1042
class MercurialVCS(VersionControlSystem):
1043
  """Implementation of the VersionControlSystem interface for Mercurial."""
1044
 
1045
  def __init__(self, options, repo_dir):
1046
    super(MercurialVCS, self).__init__(options)
1047
    # Absolute path to repository (we can be in a subdir)
1048
    self.repo_dir = os.path.normpath(repo_dir)
1049
    # Compute the subdir
1050
    cwd = os.path.normpath(os.getcwd())
1051
    assert cwd.startswith(self.repo_dir)
1052
    self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/")
1053
    if self.options.revision:
1054
      self.base_rev = self.options.revision
1055
    else:
1056
      self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip()
1057
 
1058
  def _GetRelPath(self, filename):
1059
    """Get relative path of a file according to the current directory,
1060
    given its logical path in the repo."""
1061
    assert filename.startswith(self.subdir), filename
1062
    return filename[len(self.subdir):].lstrip(r"\/")
1063
 
1064
  def GenerateDiff(self, extra_args):
1065
    # If no file specified, restrict to the current subdir
1066
    extra_args = extra_args or ["."]
1067
    cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
1068
    data = RunShell(cmd, silent_ok=True)
1069
    svndiff = []
1070
    filecount = 0
1071
    for line in data.splitlines():
1072
      m = re.match("diff --git a/(\S+) b/(\S+)", line)
1073
      if m:
1074
        # Modify line to make it look like as it comes from svn diff.
1075
        # With this modification no changes on the server side are required
1076
        # to make upload.py work with Mercurial repos.
1077
        # NOTE: for proper handling of moved/copied files, we have to use
1078
        # the second filename.
1079
        filename = m.group(2)
1080
        svndiff.append("Index: %s" % filename)
1081
        svndiff.append("=" * 67)
1082
        filecount += 1
1083
        logging.info(line)
1084
      else:
1085
        svndiff.append(line)
1086
    if not filecount:
1087
      ErrorExit("No valid patches found in output from hg diff")
1088
    return "\n".join(svndiff) + "\n"
1089
 
1090
  def GetUnknownFiles(self):
1091
    """Return a list of files unknown to the VCS."""
1092
    args = []
1093
    status = RunShell(["hg", "status", "--rev", self.base_rev, "-u", "."],
1094
        silent_ok=True)
1095
    unknown_files = []
1096
    for line in status.splitlines():
1097
      st, fn = line.split(" ", 1)
1098
      if st == "?":
1099
        unknown_files.append(fn)
1100
    return unknown_files
1101
 
1102
  def GetBaseFile(self, filename):
1103
    # "hg status" and "hg cat" both take a path relative to the current subdir
1104
    # rather than to the repo root, but "hg diff" has given us the full path
1105
    # to the repo root.
1106
    base_content = ""
1107
    new_content = None
1108
    is_binary = False
1109
    oldrelpath = relpath = self._GetRelPath(filename)
1110
    # "hg status -C" returns two lines for moved/copied files, one otherwise
1111
    out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath])
1112
    out = out.splitlines()
1113
    # HACK: strip error message about missing file/directory if it isn't in
1114
    # the working copy
1115
    if out[0].startswith('%s: ' % relpath):
1116
      out = out[1:]
1117
    if len(out) > 1:
1118
      # Moved/copied => considered as modified, use old filename to
1119
      # retrieve base contents
1120
      oldrelpath = out[1].strip()
1121
      status = "M"
1122
    else:
1123
      status, _ = out[0].split(' ', 1)
1124
    if status != "A":
1125
      base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
1126
        silent_ok=True)
1127
      is_binary = "\0" in base_content  # Mercurial's heuristic
1128
    if status != "R":
1129
      new_content = open(relpath, "rb").read()
1130
      is_binary = is_binary or "\0" in new_content
1131
    if is_binary and base_content:
1132
      # Fetch again without converting newlines
1133
      base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
1134
        silent_ok=True, universal_newlines=False)
1135
    if not is_binary or not self.IsImage(relpath):
1136
      new_content = None
1137
    return base_content, new_content, is_binary, status
1138
 
1139
 
1140
# NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync.
1141
def SplitPatch(data):
1142
  """Splits a patch into separate pieces for each file.
1143
 
1144
  Args:
1145
    data: A string containing the output of svn diff.
1146
 
1147
  Returns:
1148
    A list of 2-tuple (filename, text) where text is the svn diff output
1149
      pertaining to filename.
1150
  """
1151
  patches = []
1152
  filename = None
1153
  diff = []
1154
  for line in data.splitlines(True):
1155
    new_filename = None
1156
    if line.startswith('Index:'):
1157
      unused, new_filename = line.split(':', 1)
1158
      new_filename = new_filename.strip()
1159
    elif line.startswith('Property changes on:'):
1160
      unused, temp_filename = line.split(':', 1)
1161
      # When a file is modified, paths use '/' between directories, however
1162
      # when a property is modified '\' is used on Windows.  Make them the same
1163
      # otherwise the file shows up twice.
1164
      temp_filename = temp_filename.strip().replace('\\', '/')
1165
      if temp_filename != filename:
1166
        # File has property changes but no modifications, create a new diff.
1167
        new_filename = temp_filename
1168
    if new_filename:
1169
      if filename and diff:
1170
        patches.append((filename, ''.join(diff)))
1171
      filename = new_filename
1172
      diff = [line]
1173
      continue
1174
    if diff is not None:
1175
      diff.append(line)
1176
  if filename and diff:
1177
    patches.append((filename, ''.join(diff)))
1178
  return patches
1179
 
1180
 
1181
def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
1182
  """Uploads a separate patch for each file in the diff output.
1183
 
1184
  Returns a list of [patch_key, filename] for each file.
1185
  """
1186
  patches = SplitPatch(data)
1187
  rv = []
1188
  for patch in patches:
1189
    if len(patch[1]) > MAX_UPLOAD_SIZE:
1190
      print ("Not uploading the patch for " + patch[0] +
1191
             " because the file is too large.")
1192
      continue
1193
    form_fields = [("filename", patch[0])]
1194
    if not options.download_base:
1195
      form_fields.append(("content_upload", "1"))
1196
    files = [("data", "data.diff", patch[1])]
1197
    ctype, body = EncodeMultipartFormData(form_fields, files)
1198
    url = "/%d/upload_patch/%d" % (int(issue), int(patchset))
1199
    print "Uploading patch for " + patch[0]
1200
    response_body = rpc_server.Send(url, body, content_type=ctype)
1201
    lines = response_body.splitlines()
1202
    if not lines or lines[0] != "OK":
1203
      StatusUpdate("  --> %s" % response_body)
1204
      sys.exit(1)
1205
    rv.append([lines[1], patch[0]])
1206
  return rv
1207
 
1208
 
1209
def GuessVCS(options):
1210
  """Helper to guess the version control system.
1211
 
1212
  This examines the current directory, guesses which VersionControlSystem
1213
  we're using, and returns an instance of the appropriate class.  Exit with an
1214
  error if we can't figure it out.
1215
 
1216
  Returns:
1217
    A VersionControlSystem instance. Exits if the VCS can't be guessed.
1218
  """
1219
  # Mercurial has a command to get the base directory of a repository
1220
  # Try running it, but don't die if we don't have hg installed.
1221
  # NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
1222
  try:
1223
    out, returncode = RunShellWithReturnCode(["hg", "root"])
1224
    if returncode == 0:
1225
      return MercurialVCS(options, out.strip())
1226
  except OSError, (errno, message):
1227
    if errno != 2:  # ENOENT -- they don't have hg installed.
1228
      raise
1229
 
1230
  # Subversion has a .svn in all working directories.
1231
  if os.path.isdir('.svn'):
1232
    logging.info("Guessed VCS = Subversion")
1233
    return SubversionVCS(options)
1234
 
1235
  # Git has a command to test if you're in a git tree.
1236
  # Try running it, but don't die if we don't have git installed.
1237
  try:
1238
    out, returncode = RunShellWithReturnCode(["git", "rev-parse",
1239
                                              "--is-inside-work-tree"])
1240
    if returncode == 0:
1241
      return GitVCS(options)
1242
  except OSError, (errno, message):
1243
    if errno != 2:  # ENOENT -- they don't have git installed.
1244
      raise
1245
 
1246
  ErrorExit(("Could not guess version control system. "
1247
             "Are you in a working copy directory?"))
1248
 
1249
 
1250
def RealMain(argv, data=None):
1251
  """The real main function.
1252
 
1253
  Args:
1254
    argv: Command line arguments.
1255
    data: Diff contents. If None (default) the diff is generated by
1256
      the VersionControlSystem implementation returned by GuessVCS().
1257
 
1258
  Returns:
1259
    A 2-tuple (issue id, patchset id).
1260
    The patchset id is None if the base files are not uploaded by this
1261
    script (applies only to SVN checkouts).
1262
  """
1263
  logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
1264
                              "%(lineno)s %(message)s "))
1265
  os.environ['LC_ALL'] = 'C'
1266
  options, args = parser.parse_args(argv[1:])
1267
  global verbosity
1268
  verbosity = options.verbose
1269
  if verbosity >= 3:
1270
    logging.getLogger().setLevel(logging.DEBUG)
1271
  elif verbosity >= 2:
1272
    logging.getLogger().setLevel(logging.INFO)
1273
  vcs = GuessVCS(options)
1274
  if isinstance(vcs, SubversionVCS):
1275
    # base field is only allowed for Subversion.
1276
    # Note: Fetching base files may become deprecated in future releases.
1277
    base = vcs.GuessBase(options.download_base)
1278
  else:
1279
    base = None
1280
  if not base and options.download_base:
1281
    options.download_base = True
1282
    logging.info("Enabled upload of base file")
1283
  if not options.assume_yes:
1284
    vcs.CheckForUnknownFiles()
1285
  if data is None:
1286
    data = vcs.GenerateDiff(args)
1287
  files = vcs.GetBaseFiles(data)
1288
  if verbosity >= 1:
1289
    print "Upload server:", options.server, "(change with -s/--server)"
1290
  if options.issue:
1291
    prompt = "Message describing this patch set: "
1292
  else:
1293
    prompt = "New issue subject: "
1294
  message = options.message or raw_input(prompt).strip()
1295
  if not message:
1296
    ErrorExit("A non-empty message is required")
1297
  rpc_server = GetRpcServer(options)
1298
  form_fields = [("subject", message)]
1299
  if base:
1300
    form_fields.append(("base", base))
1301
  if options.issue:
1302
    form_fields.append(("issue", str(options.issue)))
1303
  if options.email:
1304
    form_fields.append(("user", options.email))
1305
  if options.reviewers:
1306
    for reviewer in options.reviewers.split(','):
1307
      if "@" in reviewer and not reviewer.split("@")[1].count(".") == 1:
1308
        ErrorExit("Invalid email address: %s" % reviewer)
1309
    form_fields.append(("reviewers", options.reviewers))
1310
  if options.cc:
1311
    for cc in options.cc.split(','):
1312
      if "@" in cc and not cc.split("@")[1].count(".") == 1:
1313
        ErrorExit("Invalid email address: %s" % cc)
1314
    form_fields.append(("cc", options.cc))
1315
  description = options.description
1316
  if options.description_file:
1317
    if options.description:
1318
      ErrorExit("Can't specify description and description_file")
1319
    file = open(options.description_file, 'r')
1320
    description = file.read()
1321
    file.close()
1322
  if description:
1323
    form_fields.append(("description", description))
1324
  # Send a hash of all the base file so the server can determine if a copy
1325
  # already exists in an earlier patchset.
1326
  base_hashes = ""
1327
  for file, info in files.iteritems():
1328
    if not info[0] is None:
1329
      checksum = md5.new(info[0]).hexdigest()
1330
      if base_hashes:
1331
        base_hashes += "|"
1332
      base_hashes += checksum + ":" + file
1333
  form_fields.append(("base_hashes", base_hashes))
1334
  # If we're uploading base files, don't send the email before the uploads, so
1335
  # that it contains the file status.
1336
  if options.send_mail and options.download_base:
1337
    form_fields.append(("send_mail", "1"))
1338
  if not options.download_base:
1339
    form_fields.append(("content_upload", "1"))
1340
  if len(data) > MAX_UPLOAD_SIZE:
1341
    print "Patch is large, so uploading file patches separately."
1342
    uploaded_diff_file = []
1343
    form_fields.append(("separate_patches", "1"))
1344
  else:
1345
    uploaded_diff_file = [("data", "data.diff", data)]
1346
  ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file)
1347
  response_body = rpc_server.Send("/upload", body, content_type=ctype)
1348
  patchset = None
1349
  if not options.download_base or not uploaded_diff_file:
1350
    lines = response_body.splitlines()
1351
    if len(lines) >= 2:
1352
      msg = lines[0]
1353
      patchset = lines[1].strip()
1354
      patches = [x.split(" ", 1) for x in lines[2:]]
1355
    else:
1356
      msg = response_body
1357
  else:
1358
    msg = response_body
1359
  StatusUpdate(msg)
1360
  if not response_body.startswith("Issue created.") and \
1361
  not response_body.startswith("Issue updated."):
1362
    sys.exit(0)
1363
  issue = msg[msg.rfind("/")+1:]
1364
 
1365
  if not uploaded_diff_file:
1366
    result = UploadSeparatePatches(issue, rpc_server, patchset, data, options)
1367
    if not options.download_base:
1368
      patches = result
1369
 
1370
  if not options.download_base:
1371
    vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files)
1372
    if options.send_mail:
1373
      rpc_server.Send("/" + issue + "/mail", payload="")
1374
  return issue, patchset
1375
 
1376
 
1377
def main():
1378
  try:
1379
    RealMain(sys.argv)
1380
  except KeyboardInterrupt:
1381
    print
1382
    StatusUpdate("Interrupted.")
1383
    sys.exit(1)
1384
 
1385
 
1386
if __name__ == "__main__":
1387
  main()

powered by: WebSVN 2.1.0

© copyright 1999-2024 OpenCores.org, equivalent to Oliscience, all rights reserved. OpenCores®, registered trademark.