| Index: third_party/upload.py | 
| diff --git a/third_party/upload.py b/third_party/upload.py | 
| index e8fee3b0a6ca480e0e38d5109d612f67f551dee3..d5e1fb2eee5d22f4c4f00fd13f75cef8db685b5d 100755 | 
| --- a/third_party/upload.py | 
| +++ b/third_party/upload.py | 
| @@ -1,4 +1,5 @@ | 
| #!/usr/bin/env python | 
| +# coding: utf-8 | 
| # | 
| # Copyright 2007 Google Inc. | 
| # | 
| @@ -215,7 +216,7 @@ class AbstractRpcServer(object): | 
| def _CreateRequest(self, url, data=None): | 
| """Creates a new urllib request.""" | 
| logging.debug("Creating request for: '%s' with payload:\n%s", url, data) | 
| -    req = urllib2.Request(url, data=data) | 
| +    req = urllib2.Request(url, data=data, headers={"Accept": "text/plain"}) | 
| if self.host_override: | 
| req.add_header("Host", self.host_override) | 
| for key, value in self.extra_headers.iteritems(): | 
| @@ -399,14 +400,13 @@ class AbstractRpcServer(object): | 
| raise | 
| elif e.code == 401 or e.code == 302: | 
| self._Authenticate() | 
| -##           elif e.code >= 500 and e.code < 600: | 
| -##             # Server Error - try again. | 
| -##             continue | 
| elif e.code == 301: | 
| # Handle permanent redirect manually. | 
| url = e.info()["location"] | 
| url_loc = urlparse.urlparse(url) | 
| self.host = '%s://%s' % (url_loc[0], url_loc[1]) | 
| +          elif e.code >= 500: | 
| +            ErrorExit(e.read()) | 
| else: | 
| raise | 
| finally: | 
| @@ -532,14 +532,13 @@ group.add_option("--account_type", action="store", dest="account_type", | 
| "valid choices are 'GOOGLE' and 'HOSTED').")) | 
| # Issue | 
| group = parser.add_option_group("Issue options") | 
| -group.add_option("-d", "--description", action="store", dest="description", | 
| -                 metavar="DESCRIPTION", default=None, | 
| -                 help="Optional description when creating an issue.") | 
| -group.add_option("-f", "--description_file", action="store", | 
| -                 dest="description_file", metavar="DESCRIPTION_FILE", | 
| +group.add_option("-t", "--title", action="store", dest="title", | 
| +                 help="New issue subject or new patch set title") | 
| +group.add_option("-m", "--message", action="store", dest="message", | 
| default=None, | 
| -                 help="Optional path of a file that contains " | 
| -                      "the description when creating an issue.") | 
| +                 help="New issue description or new patch set message") | 
| +group.add_option("-F", "--file", action="store", dest="file", | 
| +                 default=None, help="Read the message above from file.") | 
| group.add_option("-r", "--reviewers", action="store", dest="reviewers", | 
| metavar="REVIEWERS", default=None, | 
| help="Add reviewers (comma separated email addresses).") | 
| @@ -551,10 +550,6 @@ group.add_option("--private", action="store_true", dest="private", | 
| help="Make the issue restricted to reviewers and those CCed") | 
| # Upload options | 
| group = parser.add_option_group("Patch options") | 
| -group.add_option("-m", "--message", action="store", dest="message", | 
| -                 metavar="MESSAGE", default=None, | 
| -                 help="A message to identify the patch. " | 
| -                      "Will prompt if omitted.") | 
| group.add_option("-i", "--issue", type="int", action="store", | 
| metavar="ISSUE", default=None, | 
| help="Issue number to which to add. Defaults to new issue.") | 
| @@ -785,7 +780,7 @@ class VersionControlSystem(object): | 
| options: Command line options. | 
| """ | 
| self.options = options | 
| - | 
| + | 
| def GetGUID(self): | 
| """Return string to distinguish the repository from others, for example to | 
| query all opened review issues for it""" | 
| @@ -945,7 +940,7 @@ class SubversionVCS(VersionControlSystem): | 
| # Result is cached to not guess it over and over again in GetBaseFile(). | 
| required = self.options.download_base or self.options.revision is not None | 
| self.svn_base = self._GuessBase(required) | 
| - | 
| + | 
| def GetGUID(self): | 
| return self._GetInfo("Repository UUID") | 
|  | 
| @@ -980,7 +975,7 @@ class SubversionVCS(VersionControlSystem): | 
| if required: | 
| ErrorExit("Can't find URL in output from svn info") | 
| return None | 
| - | 
| + | 
| def _GetInfo(self, key): | 
| """Parses 'svn info' for current dir. Returns value for key or None""" | 
| for line in RunShell(["svn", "info"]).splitlines(): | 
| @@ -1223,7 +1218,7 @@ class GitVCS(VersionControlSystem): | 
| self.hashes = {} | 
| # Map of new filename -> old filename for renames. | 
| self.renames = {} | 
| - | 
| + | 
| def GetGUID(self): | 
| revlist = RunShell("git rev-list --parents HEAD".split()).splitlines() | 
| # M-A: Return the 1st root hash, there could be multiple when a | 
| @@ -1310,7 +1305,7 @@ class GitVCS(VersionControlSystem): | 
| # then the diff of everything except deleted files with rename and copy | 
| # support enabled. | 
| cmd = [ | 
| -        "git", "diff", "--no-ext-diff", "--full-index", "--ignore-submodules" | 
| +        "git", "diff", "--no-color", "--no-ext-diff", "--full-index", "--ignore-submodules" | 
| ] | 
| diff = RunShell(cmd + ["--diff-filter=D"] + extra_args, env=env, | 
| silent_ok=True) | 
| @@ -1464,11 +1459,13 @@ class MercurialVCS(VersionControlSystem): | 
| def _GetRelPath(self, filename): | 
| """Get relative path of a file according to the current directory, | 
| given its logical path in the repo.""" | 
| -    assert filename.startswith(self.subdir), (filename, self.subdir) | 
| -    return filename[len(self.subdir):].lstrip(r"\/") | 
| +    absname = os.path.join(self.repo_dir, filename) | 
| +    return os.path.relpath(absname) | 
|  | 
| def GenerateDiff(self, extra_args): | 
| -    cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args | 
| +    cmd = [ | 
| +        "hg", "diff", "--color", "never", "--git", "-r", self.base_rev | 
| +        ] + extra_args | 
| data = RunShell(cmd, silent_ok=True) | 
| svndiff = [] | 
| filecount = 0 | 
| @@ -1494,7 +1491,8 @@ class MercurialVCS(VersionControlSystem): | 
| def GetUnknownFiles(self): | 
| """Return a list of files unknown to the VCS.""" | 
| args = [] | 
| -    status = RunShell(["hg", "status", "--rev", self.base_rev, "-u", "."], | 
| +    status = RunShell( | 
| +        ["hg", "status", "--color", "never", "--rev", self.base_rev, "-u", "."], | 
| silent_ok=True) | 
| unknown_files = [] | 
| for line in status.splitlines(): | 
| @@ -1504,15 +1502,16 @@ class MercurialVCS(VersionControlSystem): | 
| return unknown_files | 
|  | 
| def GetBaseFile(self, filename): | 
| -    # "hg status" and "hg cat" both take a path relative to the current subdir | 
| -    # rather than to the repo root, but "hg diff" has given us the full path | 
| -    # to the repo root. | 
| +    # "hg status" and "hg cat" both take a path relative to the current subdir, | 
| +    # but "hg diff" has given us the path relative to the repo root. | 
| base_content = "" | 
| new_content = None | 
| is_binary = False | 
| oldrelpath = relpath = self._GetRelPath(filename) | 
| # "hg status -C" returns two lines for moved/copied files, one otherwise | 
| -    out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath]) | 
| +    out = RunShell( | 
| +        [ "hg", "status", "--color", "never", "-C", "--rev", self.base_rev, | 
| +          relpath]) | 
| out = out.splitlines() | 
| # HACK: strip error message about missing file/directory if it isn't in | 
| # the working copy | 
| @@ -1575,15 +1574,15 @@ class PerforceVCS(VersionControlSystem): | 
|  | 
| ConfirmLogin() | 
|  | 
| -    if not options.message: | 
| +    if not options.title: | 
| description = self.RunPerforceCommand(["describe", self.p4_changelist], | 
| marshal_output=True) | 
| if description and "desc" in description: | 
| # Rietveld doesn't support multi-line descriptions | 
| -        raw_message = description["desc"].strip() | 
| -        lines = raw_message.splitlines() | 
| +        raw_title = description["desc"].strip() | 
| +        lines = raw_title.splitlines() | 
| if len(lines): | 
| -          options.message = lines[0] | 
| +          options.title = lines[0] | 
|  | 
| def GetGUID(self): | 
| """For now we don't know how to get repository ID for Perforce""" | 
| @@ -1974,10 +1973,12 @@ def GuessVCSName(options): | 
| if res != None: | 
| return res | 
|  | 
| -  # Subversion has a .svn in all working directories. | 
| -  if os.path.isdir('.svn'): | 
| -    logging.info("Guessed VCS = Subversion") | 
| -    return (VCS_SUBVERSION, None) | 
| +  # Subversion from 1.7 has a single centralized .svn folder | 
| +  # ( see http://subversion.apache.org/docs/release-notes/1.7.html#wc-ng ) | 
| +  # That's why we use 'svn info' instead of checking for .svn dir | 
| +  res = RunDetectCommand(VCS_SUBVERSION, ["svn", "info"]) | 
| +  if res != None: | 
| +    return res | 
|  | 
| # Git has a command to test if you're in a git tree. | 
| # Try running it, but don't die if we don't have git installed. | 
| @@ -2219,20 +2220,13 @@ def RealMain(argv, data=None): | 
| files = vcs.GetBaseFiles(data) | 
| if verbosity >= 1: | 
| print "Upload server:", options.server, "(change with -s/--server)" | 
| -  if options.issue: | 
| -    prompt = "Message describing this patch set: " | 
| -  else: | 
| -    prompt = "New issue subject: " | 
| -  message = options.message or raw_input(prompt).strip() | 
| -  if not message: | 
| -    ErrorExit("A non-empty message is required") | 
| rpc_server = GetRpcServer(options.server, | 
| options.email, | 
| options.host, | 
| options.save_cookies, | 
| options.account_type) | 
| -  form_fields = [("subject", message)] | 
| - | 
| +  form_fields = [] | 
| + | 
| repo_guid = vcs.GetGUID() | 
| if repo_guid: | 
| form_fields.append(("repo_guid", repo_guid)) | 
| @@ -2256,15 +2250,40 @@ def RealMain(argv, data=None): | 
| for cc in options.cc.split(','): | 
| CheckReviewer(cc) | 
| form_fields.append(("cc", options.cc)) | 
| -  description = options.description | 
| -  if options.description_file: | 
| -    if options.description: | 
| -      ErrorExit("Can't specify description and description_file") | 
| -    file = open(options.description_file, 'r') | 
| -    description = file.read() | 
| + | 
| +  # Process --message, --title and --file. | 
| +  message = options.message or "" | 
| +  title = options.title or "" | 
| +  if options.file: | 
| +    if options.message: | 
| +      ErrorExit("Can't specify both message and message file options") | 
| +    file = open(options.file, 'r') | 
| +    message = file.read() | 
| file.close() | 
| -  if description: | 
| -    form_fields.append(("description", description)) | 
| +  if options.issue: | 
| +    prompt = "Title describing this patch set: " | 
| +  else: | 
| +    prompt = "New issue subject: " | 
| +  title = ( | 
| +      title or message.split('\n', 1)[0].strip() or raw_input(prompt).strip()) | 
| +  if not title and not options.issue: | 
| +    ErrorExit("A non-empty title is required for a new issue") | 
| +  # For existing issues, it's fine to give a patchset an empty name. Rietveld | 
| +  # doesn't accept that so use a whitespace. | 
| +  title = title or " " | 
| +  if len(title) > 100: | 
| +    title = title[:99] + '…' | 
| +  if title and not options.issue: | 
| +    message = message or title | 
| + | 
| +  form_fields.append(("subject", title)) | 
| +  if message: | 
| +    if not options.issue: | 
| +      form_fields.append(("description", message)) | 
| +    else: | 
| +      # TODO: [ ] Use /<issue>/publish to add a comment. | 
| +      pass | 
| + | 
| # Send a hash of all the base file so the server can determine if a copy | 
| # already exists in an earlier patchset. | 
| base_hashes = "" | 
| @@ -2282,10 +2301,6 @@ def RealMain(argv, data=None): | 
| form_fields.append(("private", "1")) | 
| if options.send_patch: | 
| options.send_mail = True | 
| -  # If we're uploading base files, don't send the email before the uploads, so | 
| -  # that it contains the file status. | 
| -  if options.send_mail and options.download_base: | 
| -    form_fields.append(("send_mail", "1")) | 
| if not options.download_base: | 
| form_fields.append(("content_upload", "1")) | 
| if len(data) > MAX_UPLOAD_SIZE: | 
| @@ -2320,11 +2335,15 @@ def RealMain(argv, data=None): | 
|  | 
| if not options.download_base: | 
| vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files) | 
| -    if options.send_mail: | 
| -      payload = "" | 
| -      if options.send_patch: | 
| -        payload=urllib.urlencode({"attach_patch": "yes"}) | 
| -      rpc_server.Send("/" + issue + "/mail", payload=payload) | 
| + | 
| +  payload = {}  # payload for final request | 
| +  if options.send_mail: | 
| +    payload["send_mail"] = "yes" | 
| +    if options.send_patch: | 
| +      payload["attach_patch"] = "yes" | 
| +  payload = urllib.urlencode(payload) | 
| +  rpc_server.Send("/" + issue + "/upload_complete/" + (patchset or ""), | 
| +                  payload=payload) | 
| return issue, patchset | 
|  | 
|  | 
|  |